diff --git a/web/src/utils/repo-parser.ts b/web/src/utils/repo-parser.ts index ac8f464..1567082 100644 --- a/web/src/utils/repo-parser.ts +++ b/web/src/utils/repo-parser.ts @@ -43,11 +43,22 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null { const arg = args[i] if (!arg || !arg.startsWith('--')) continue + hasValidField = true + + if (arg === '--single-file') { + repoConfig.single_file = true + const value = args[i + 1] + if (value === 'true' || value === 'false') { + repoConfig.single_file = value === 'true' + i++ + } + continue + } + const value = args[i + 1] if (value === undefined || value.startsWith('--')) continue - i++ // 跳过已处理的 value - hasValidField = true + i++ // 推进索引并跳过已处理的 value switch (arg) { case '--source-type': @@ -80,9 +91,6 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null { case '--path': repoConfig.sparse_path = value break - case '--single-file': - repoConfig.single_file = value === 'true' - break case '--proxy-url': repoConfig.proxy_url = value repoConfig.proxy = 'custom' @@ -172,3 +180,95 @@ export function parseQlCommand(command: string): ParsedRepoResult | null { return { repoConfig, task } } + +/** + * 根据 Task 生成用于导出的 baihu reposync 命令 + */ +export function generateBaihuCommand(task: Task): string { + try { + const config = JSON.parse(task.config) as RepoConfig + const args: string[] = ['baihu', 'reposync'] + + args.push('--source-type', config.source_type || 'git') + args.push('--source-url', config.source_url || '') + + // 处理 target_path + let targetPath = config.target_path || '' + if (!targetPath) { + targetPath = PATHS.SCRIPTS_DIR_PLACEHOLDER + } else if (targetPath.startsWith(`${PATHS.SCRIPTS_DIR_PLACEHOLDER}/`) || targetPath === PATHS.SCRIPTS_DIR_PLACEHOLDER) { + // 保持原样 + } else if (targetPath.startsWith('/') || /^[a-zA-Z]:/.test(targetPath)) { + // 绝对路径,保持原样 + } else { + // 相对路径,转换为带占位符的格式 + targetPath = `${PATHS.SCRIPTS_DIR_PLACEHOLDER}/${targetPath.replace(/^\/+/, '')}` + } + + // 如果 targetPath 不是默认值,才需输出 --target-path + if (targetPath !== PATHS.SCRIPTS_DIR_PLACEHOLDER) { + args.push('--target-path', targetPath) + } + + if (config.branch) { + args.push('--branch', config.branch) + } + if (config.sparse_path) { + args.push('--path', config.sparse_path) + } + if (config.single_file) { + args.push('--single-file') + } + if (config.proxy && config.proxy !== 'none') { + args.push('--proxy', config.proxy) + if (config.proxy === 'custom' && config.proxy_url) { + args.push('--proxy-url', config.proxy_url) + } + } + if (config.auth_token) { + args.push('--auth-token', config.auth_token) + } + if (config.whitelist_paths) { + args.push('--whitelist-paths', config.whitelist_paths) + } + if (config.blacklist) { + args.push('--blacklist', config.blacklist) + } + if (config.dependence) { + args.push('--dependence', config.dependence) + } + if (config.commenttotask === 'true') { + args.push('--commenttotask', 'true') + } + if (config.extensions) { + args.push('--extensions', config.extensions) + } + if (task.pre_command) { + args.push('--pre-command', task.pre_command) + } + if (task.post_command) { + args.push('--post-command', task.post_command) + } + if (task.timeout !== undefined && task.timeout !== 30) { + args.push('--task-timeout', String(task.timeout)) + } + if (task.languages && task.languages.length > 0) { + const langs = task.languages.map(l => ({ name: l.name, version: l.version })) + args.push('--task-langs', JSON.stringify(langs)) + } + + // 转义并加引号(仅在包含特殊字符或为空时加引号,提升指令可读性) + return args.map(arg => { + if (arg === 'baihu' || arg === 'reposync') return arg + if (arg === '') return "''" + // 如果只包含安全字符(字母、数字、短横线、下划线、点、斜杠、冒号、@),则无需加引号 + if (/^[a-zA-Z0-9_\-\.\/:@]+$/.test(arg)) { + return arg + } + return "'" + arg.replace(/'/g, "'\\''") + "'" + }).join(' ') + } catch (e) { + console.error('generateBaihuCommand failed', e) + return '' + } +} diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index f3a4e4b..0369198 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -29,6 +29,7 @@ import { useRouter, useRoute } from 'vue-router' import { TASK_TYPE, AGENT_STATUS, TRIGGER_TYPE, TASK_STATUS } from '@/constants' import TextOverflow from '@/components/TextOverflow.vue' import { getCronDescription } from '@/utils/cron' +import { generateBaihuCommand } from '@/utils/repo-parser' const router = useRouter() @@ -169,6 +170,29 @@ function duplicateTask(task: Task) { } } +const showExportDialog = ref(false) +const exportCommandText = ref('') + +function openExportDialog(task: Task) { + const cmd = generateBaihuCommand(task) + if (!cmd) { + toast.error('生成导出指令失败') + return + } + exportCommandText.value = cmd + showExportDialog.value = true +} + +function copyCommandText() { + if (!exportCommandText.value) return + navigator.clipboard.writeText(exportCommandText.value).then(() => { + toast.success('同步指令已复制到剪贴板') + showExportDialog.value = false + }).catch(() => { + toast.error('复制失败,请手动选择复制') + }) +} + const showBatchDeleteDialog = ref(false) function confirmDelete(id: string) { @@ -709,6 +733,10 @@ watch(() => route.query.agent_id, (newVal: any) => { 复制任务 + + + 导出指令 + @@ -789,6 +817,10 @@ watch(() => route.query.agent_id, (newVal: any) => { 复制任务 + + + 导出指令 + @@ -876,6 +908,10 @@ watch(() => route.query.agent_id, (newVal: any) => { 复制任务 + + + 导出指令 + @@ -942,5 +978,29 @@ watch(() => route.query.agent_id, (newVal: any) => { + + + +
+
+ 您可以复制并分享此指令,在其他部署了白虎面板的系统上导入该仓库同步任务: +
+
+