chore: refact reposync
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user