chore: refact reposync

This commit is contained in:
duorameng
2026-05-06 18:02:35 +08:00
parent 31d904ec4f
commit 6bce5685c7
21 changed files with 1098 additions and 528 deletions
+180 -9
View File
@@ -10,7 +10,7 @@ import { Checkbox } from '@/components/ui/checkbox'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import DirTreeSelect from '@/components/DirTreeSelect.vue'
import { X, Globe, GitBranch, Shield, Zap, Clock, Download, Plus, Search, Check, ChevronsUpDown, Loader2, AlertCircle } from 'lucide-vue-next'
import { X, Globe, GitBranch, Shield, Zap, Clock, Download, Plus, Search, Check, ChevronsUpDown, Loader2, AlertCircle, Terminal } from 'lucide-vue-next'
import { api, type Task, type RepoConfig, type Agent, type MiseLanguage } from '@/api'
import { toast } from 'vue-sonner'
import { cn } from '@/lib/utils'
@@ -64,6 +64,7 @@ const repoConfig = ref<RepoConfig>({
dependence: '',
extensions: '',
auto_add_cron: false,
commenttotask: 'false',
concurrency: 1,
repo_source: '',
proxy: ''
@@ -81,6 +82,13 @@ const autoAddCron = computed({
}
})
const pullQlConfig = computed({
get: () => repoConfig.value.commenttotask === 'true',
set: (val: boolean) => {
repoConfig.value.commenttotask = val ? 'true' : 'false'
}
})
// === 语言环境相关 ===
const installedLangs = ref<MiseLanguage[]>([])
const loadingLangs = ref(false)
@@ -185,6 +193,110 @@ function updateLangName(index: number, name: string) {
const showQlImportDialog = ref(false)
const qlCommandInput = ref('')
const showBaihuImportDialog = ref(false)
const baihuCommandInput = ref('')
function importFromBaihu() {
baihuCommandInput.value = ''
showBaihuImportDialog.value = true
}
function submitBaihuImport() {
const s = baihuCommandInput.value.trim()
if (!s) {
showBaihuImportDialog.value = false
return
}
// Parse arguments handling quotes
const args: string[] = []
const regex = /[^\s"']+|"([^"]*)"|'([^']*)'/g
let match
while ((match = regex.exec(s)) !== null) {
args.push(match[1] || match[2] || match[0])
}
let i = 0
// Skip leading 'baihu' or 'reposync'
if (args[i] === 'baihu') i++
if (args[i] === 'reposync') i++
let hasValidField = false
for (; i < args.length; i++) {
const arg = args[i]
if (!arg || !arg.startsWith('--')) continue
const value = args[i + 1]
if (value === undefined || value.startsWith('--')) continue
i++ // Skip value in next iteration
hasValidField = true
switch (arg) {
case '--source-type': repoConfig.value.source_type = value; break
case '--source-url':
repoConfig.value.source_url = value
// Auto-generate name from URL if name is empty
if (!form.value.name) {
try {
const urlPaths = value.split('/')
const name = urlPaths[urlPaths.length - 1]?.replace('.git', '') || '未命名仓库'
form.value.name = '同步 ' + name
} catch { /* ignore */ }
}
break
case '--target-path':
// Strip $SCRIPTS_DIR$/ prefix for UI
if (value.startsWith('$SCRIPTS_DIR$/')) {
repoConfig.value.target_path = value.replace('$SCRIPTS_DIR$/', '')
} else if (value === '$SCRIPTS_DIR$') {
repoConfig.value.target_path = ''
} else {
repoConfig.value.target_path = value
}
break
case '--branch': repoConfig.value.branch = value; break
case '--path': repoConfig.value.sparse_path = value; break
case '--single-file': isSingleFile.value = value === 'true'; break
case '--proxy-url':
repoConfig.value.proxy_url = value
repoConfig.value.proxy = 'custom'
break
case '--auth-token': repoConfig.value.auth_token = value; break
case '--whitelist-paths': repoConfig.value.whitelist_paths = value; break
case '--blacklist': repoConfig.value.blacklist = value; break
case '--dependence': repoConfig.value.dependence = value; break
case '--extensions': repoConfig.value.extensions = value; break
case '--task-timeout': form.value.timeout = parseInt(value) || 30; break
case '--task-langs':
try {
const langs = JSON.parse(value)
if (Array.isArray(langs)) {
selectedLangs.value = langs.map(l => ({
name: l.name || '',
version: l.version || '',
availableVersions: []
}))
// Trigger available versions update
selectedLangs.value.forEach(l => updateAvailableVersions(l))
}
} catch (e) {
console.error('Parse task-langs failed', e)
}
break
}
}
if (hasValidField) {
repoConfig.value.auto_add_cron = true
repoConfig.value.commenttotask = 'true'
toast.success('命令解析成功,已自动填充表单')
showBaihuImportDialog.value = false
} else {
toast.error('未识别到有效的 reposync 参数')
}
}
function importFromQl() {
qlCommandInput.value = ''
showQlImportDialog.value = true
@@ -234,6 +346,7 @@ function submitQlImport() {
if (args[7]) repoConfig.value.extensions = args[7]
repoConfig.value.auto_add_cron = true
repoConfig.value.commenttotask = 'true'
repoConfig.value.repo_source = 'ql'
toast.success('指令解析成功,已开启自动添加任务,请继续完善其他设置')
showQlImportDialog.value = false
@@ -325,6 +438,7 @@ watch(() => props.open, async (val: boolean) => {
dependence: '',
extensions: '',
auto_add_cron: false,
commenttotask: 'false',
concurrency: 1,
repo_source: ''
}
@@ -428,10 +542,16 @@ async function save() {
<DialogTitle class="text-xl font-bold">
{{ isEdit ? '编辑仓库同步' : '新建仓库同步' }}
</DialogTitle>
<Button v-if="!isEdit" variant="outline" size="sm" @click="importFromQl" class="h-8 gap-1.5 bg-primary/5 hover:bg-primary/10 border-primary/20 hover:border-primary/40 text-primary">
<Download class="w-3.5 h-3.5" />
青龙格式导入
</Button>
<div class="flex items-center gap-2">
<Button v-if="!isEdit" variant="outline" size="sm" @click="importFromBaihu" class="h-8 gap-1.5 bg-primary/5 hover:bg-primary/10 border-primary/20 hover:border-primary/40 text-primary">
<Terminal class="w-3.5 h-3.5" />
Baihu 命令导入
</Button>
<Button v-if="!isEdit" variant="outline" size="sm" @click="importFromQl" class="h-8 gap-1.5 bg-muted/50 hover:bg-muted border-muted-foreground/20 text-muted-foreground">
<Download class="w-3.5 h-3.5" />
Qinlong格式导入
</Button>
</div>
</div>
</DialogHeader>
@@ -786,12 +906,12 @@ async function save() {
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs font-semibold">
<Zap :class="cn('h-3.5 w-3.5', autoAddCron ? 'text-primary' : 'text-muted-foreground')" />
自动添加任务
自动添加任务并解析元数据
</div>
<Switch :model-value="autoAddCron" @update:model-value="(v: boolean) => autoAddCron = v" />
<Switch :model-value="autoAddCron" @update:model-value="(v: boolean) => { autoAddCron = v; pullQlConfig = v }" />
</div>
<p class="text-[11px] text-muted-foreground leading-relaxed">
{{ autoAddCron ? '同步完成后将尝试自动分析脚本并注册定时任务。' : '仅拉取脚本,不自动注册成面板任务。' }}
<p class="text-[11px] text-muted-foreground leading-relaxed italic">
{{ autoAddCron ? '同步后将自动识别脚本中的 new Env("xxx") 和 cron 信息并注册任务。' : '仅拉取脚本,不自动注册任务。' }}
</p>
</div>
@@ -879,6 +999,57 @@ async function save() {
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Baihu 导入提示对话框 -->
<Dialog :open="showBaihuImportDialog" @update:open="v => showBaihuImportDialog = v">
<DialogContent class="sm:max-w-[550px] p-0 border-none bg-background shadow-xl overflow-hidden">
<DialogHeader class="px-6 pt-6 pb-2">
<DialogTitle class="text-lg font-bold flex items-center gap-2">
<Terminal class="w-4 h-4 text-primary" />
命令行快速导入
</DialogTitle>
</DialogHeader>
<div class="px-6 py-4 space-y-5">
<div class="p-3 rounded-lg bg-primary/5 border border-primary/10">
<p class="text-xs text-primary/80 leading-relaxed">
粘贴包含 <code class="px-1 py-0.5 rounded bg-primary/10 font-mono">reposync</code> 及其参数的命令系统将自动填充表单
</p>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<Label class="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">示例命令</Label>
<button class="text-[10px] text-primary hover:underline font-medium" @click="baihuCommandInput = 'baihu reposync --source-url \'https://github.com/example/repo.git\' --branch \'main\' --blacklist \'test|dev\''">填入示例</button>
</div>
<div class="p-3 rounded-lg bg-muted/40 font-mono text-[11px] text-muted-foreground/70 border border-muted/20 leading-relaxed break-all">
baihu reposync --source-url 'https://...' --branch 'main' --blacklist '...'
</div>
</div>
<div class="relative group">
<textarea
v-model="baihuCommandInput"
placeholder="在此处粘贴完整指令,如 baihu reposync --source-url ..."
class="w-full min-h-[140px] p-4 rounded-lg bg-muted/30 border border-muted/30 focus:border-primary/40 focus:ring-1 focus:ring-primary/20 transition-all text-sm resize-none outline-none"
@keydown.enter.ctrl.prevent="submitBaihuImport"
/>
<div class="absolute bottom-3 right-3 text-[10px] text-muted-foreground/40 font-medium">
CTRL + ENTER 快速确认
</div>
</div>
</div>
<DialogFooter class="px-6 pb-6 pt-2 flex gap-2">
<Button variant="ghost" size="sm" @click="showBaihuImportDialog = false" class="flex-1 h-9 rounded-md font-medium text-xs">
取消
</Button>
<Button size="sm" @click="submitBaihuImport" class="flex-1 h-9 rounded-md font-bold text-xs shadow-sm bg-primary hover:bg-primary/90">
确认解析并填充
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<style scoped>
+37 -3
View File
@@ -56,6 +56,7 @@ const envSearchQuery = ref('')
const workDirCache = ref<Record<string, string>>({})
const concurrency = ref(0)
const concurrencyEnabled = ref(false)
const commentToTaskEnabled = ref(false)
const allEnvsEnabled = ref(false)
const SCRIPTS_DIR_PLACEHPLDER = '$SCRIPTS_DIR$'
const scriptsDir = ref<string>(PATHS.SCRIPTS_DIR)
@@ -281,10 +282,13 @@ watch(() => props.open, async (val: boolean) => {
// 解析全部环境变量配置
allEnvsEnabled.value = !!parsed['$task_all_envs']
// 解析注释解析配置
commentToTaskEnabled.value = !!parsed['$task_comment_to_task']
} else {
concurrency.value = 1
concurrencyEnabled.value = true
allEnvsEnabled.value = false
commentToTaskEnabled.value = false
}
} catch {
concurrency.value = 1
@@ -419,6 +423,8 @@ async function save() {
config['$task_concurrency'] = concurrencyEnabled.value ? 1 : 0
// 更新注入全部环境变量字段
config['$task_all_envs'] = !!allEnvsEnabled.value
// 更新注释解析字段
config['$task_comment_to_task'] = !!commentToTaskEnabled.value
// 重新序列化配置
form.value.config = JSON.stringify(config)
@@ -681,9 +687,37 @@ async function save() {
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3">
<Label class="sm:text-right text-xs text-foreground/70 uppercase tracking-wider font-semibold">运行策略</Label>
<div class="sm:col-span-3 flex items-center gap-3">
<Input :model-value="form.timeout" @update:model-value="(v: string | number) => form.timeout = Number(v)" type="number" :min="0" class="w-20 h-9 bg-muted/30 text-center font-semibold text-xs" />
<span class="text-[11px] font-semibold text-muted-foreground">分钟超时</span>
<div class="sm:col-span-3 space-y-4">
<div class="flex items-center gap-3">
<Input :model-value="form.timeout" @update:model-value="(v: string | number) => form.timeout = Number(v)" type="number" :min="0" class="w-20 h-9 bg-muted/30 text-center font-semibold text-xs" />
<span class="text-[11px] font-semibold text-muted-foreground">分钟超时</span>
</div>
<div class="p-3 rounded-xl bg-muted/20 border border-muted-foreground/10 space-y-2.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs font-semibold">
<Zap :class="cn('h-3.5 w-3.5', commentToTaskEnabled ? 'text-primary' : 'text-muted-foreground')" />
兼容 QL 格式任务脚本注释解析
</div>
<Switch :model-value="commentToTaskEnabled" @update:model-value="v => commentToTaskEnabled = v" />
</div>
<p class="text-[11px] text-muted-foreground leading-relaxed italic">
{{ commentToTaskEnabled ? '尝试从脚本注释中提取任务名称和定时规则。' : '仅使用当前手动配置。' }}
</p>
</div>
<div class="p-3 rounded-xl bg-muted/20 border border-muted-foreground/10 space-y-2.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs font-semibold">
<Zap :class="cn('h-3.5 w-3.5', concurrencyEnabled ? 'text-primary' : 'text-muted-foreground')" />
并发控制
</div>
<Switch :model-value="concurrencyEnabled" @update:model-value="v => concurrencyEnabled = v" />
</div>
<p class="text-[11px] text-muted-foreground leading-relaxed">
{{ concurrencyEnabled ? '允许同时开启多个副本。' : '当前任务未结束时,新触发将被静默忽略。' }}
</p>
</div>
</div>
</div>
</div>
+19 -4
View File
@@ -19,6 +19,8 @@ import {
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { api, type Agent, type Task, type TaskLog } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
@@ -41,6 +43,7 @@ const isEdit = ref(false)
const showDeleteDialog = ref(false)
const deleteTaskId = ref<string | null>(null)
const deleteFiles = ref(false)
const filterName = ref('')
const filterTags = ref('')
@@ -169,6 +172,7 @@ const showBatchDeleteDialog = ref(false)
function confirmDelete(id: string) {
deleteTaskId.value = id
deleteFiles.value = false
showDeleteDialog.value = true
}
@@ -196,7 +200,7 @@ async function batchDeleteTasks() {
async function deleteTask() {
if (!deleteTaskId.value) return
try {
await api.tasks.delete(deleteTaskId.value)
await api.tasks.delete(deleteTaskId.value, { delete_files: deleteFiles.value })
toast.success('任务已删除')
loadTasks()
} catch { toast.error('删除失败') }
@@ -898,13 +902,24 @@ watch(() => route.query.agent_id, (newVal: any) => {
<!-- 删除确认 (单个) -->
<BaihuDialog v-model:open="showDeleteDialog" title="确认删除任务">
<div class="text-sm text-muted-foreground leading-relaxed">
<div class="text-sm text-muted-foreground leading-relaxed py-2">
确定要删除任务 <b class="text-foreground">{{ tasks.find(t => t.id === deleteTaskId)?.name }}</b> 吗?
<p class="mt-2 text-destructive font-medium">⚠️ 此操作无法撤销。</p>
</div>
<template #footer>
<Button variant="ghost" @click="showDeleteDialog = false">取消</Button>
<Button variant="destructive" class="shadow-lg shadow-destructive/20" @click="deleteTask">确认删除</Button>
<div class="flex items-center justify-between w-full gap-4">
<div v-if="tasks.find(t => t.id === deleteTaskId)?.type === TASK_TYPE.REPO"
class="flex items-center gap-2 mr-auto">
<Checkbox id="delete-files" v-model="deleteFiles" />
<Label for="delete-files" class="text-sm font-medium text-destructive cursor-pointer select-none">
同时物理删除仓库文件夹
</Label>
</div>
<div class="flex justify-end gap-2 ml-auto">
<Button variant="ghost" size="sm" @click="showDeleteDialog = false">取消</Button>
<Button variant="destructive" size="sm" @click="deleteTask">确定删除</Button>
</div>
</div>
</template>
</BaihuDialog>
</div>