From b4129419835738357f7680c5f0162a90854dd447 Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Thu, 7 May 2026 20:18:19 +0800 Subject: [PATCH] chore: refact repo parser.ts --- web/src/utils/repo-parser.ts | 167 +++++++++++++++++++++++++++++ web/src/views/tasks/RepoDialog.vue | 163 ++++++---------------------- 2 files changed, 200 insertions(+), 130 deletions(-) create mode 100644 web/src/utils/repo-parser.ts diff --git a/web/src/utils/repo-parser.ts b/web/src/utils/repo-parser.ts new file mode 100644 index 0000000..86ffb29 --- /dev/null +++ b/web/src/utils/repo-parser.ts @@ -0,0 +1,167 @@ +import type { RepoConfig, Task } from '@/api' + +/** + * 仓库命令解析结果 + */ +export interface ParsedRepoResult { + repoConfig: Partial + task: Partial +} + +/** + * 解析带引号的命令行字符串为参数数组 + */ +export function parseArgs(command: string): string[] { + const args: string[] = [] + const regex = /[^\s"']+|"([^"]*)"|'([^']*)'/g + let match + while ((match = regex.exec(command)) !== null) { + args.push(match[1] || match[2] || match[0]) + } + return args +} + +/** + * 解析 Baihu 格式的导入命令 + */ +export function parseBaihuCommand(command: string): ParsedRepoResult | null { + const s = command.trim() + if (!s) return null + + const args = parseArgs(s) + let i = 0 + // 跳过开头的 'baihu' 或 'reposync' + if (args[i] === 'baihu') i++ + if (args[i] === 'reposync') i++ + + const repoConfig: Partial = {} + const task: Partial = {} + 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++ // 跳过已处理的 value + hasValidField = true + + switch (arg) { + case '--source-type': + repoConfig.source_type = value + break + case '--source-url': + repoConfig.source_url = value + // 如果 URL 存在,尝试生成默认名称 + if (value) { + try { + const urlPaths = value.split('/') + const name = urlPaths[urlPaths.length - 1]?.replace('.git', '') || '未命名仓库' + task.name = '同步 ' + name + } catch { /* ignore */ } + } + break + case '--target-path': + // 处理 $SCRIPTS_DIR$ 占位符 + if (value.startsWith('$SCRIPTS_DIR$/')) { + repoConfig.target_path = value.replace('$SCRIPTS_DIR$/', '') + } else if (value === '$SCRIPTS_DIR$') { + repoConfig.target_path = '' + } else { + repoConfig.target_path = value + } + break + case '--branch': + repoConfig.branch = value + break + 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' + break + case '--auth-token': + repoConfig.auth_token = value + break + case '--whitelist-paths': + repoConfig.whitelist_paths = value + break + case '--blacklist': + repoConfig.blacklist = value + break + case '--dependence': + repoConfig.dependence = value + break + case '--extensions': + repoConfig.extensions = value + break + case '--task-timeout': + task.timeout = parseInt(value) || 30 + break + case '--task-langs': + try { + const langs = JSON.parse(value) + if (Array.isArray(langs)) { + task.languages = langs.map(l => ({ + name: l.name || '', + version: l.version || '' + })) + } + } catch (e) { + console.error('Parse task-langs failed', e) + } + break + } + } + + if (!hasValidField) return null + + // 设置默认规则 + repoConfig.auto_add_cron = true + repoConfig.commenttotask = 'true' + + return { repoConfig, task } +} + +/** + * 解析 青龙 (Qinglong) 格式的导入命令 + */ +export function parseQlCommand(command: string): ParsedRepoResult | null { + const s = command.trim() + if (!s || !s.startsWith('ql repo')) return null + + const args = parseArgs(s) + const repoConfig: Partial = {} + const task: Partial = {} + + if (args[2]) { + repoConfig.source_url = args[2] + repoConfig.source_type = 'git' + // 生成名称 + try { + const urlPaths = args[2].split('/') + const name = urlPaths.length > 0 ? urlPaths[urlPaths.length - 1]?.replace('.git', '') : '未命名仓库' + task.name = '同步 ' + (name || '未命名仓库') + } catch { + task.name = '同步 未命名仓库' + } + } + + if (args[3]) repoConfig.whitelist_paths = args[3] + if (args[4]) repoConfig.blacklist = args[4] + if (args[5]) repoConfig.dependence = args[5] + if (args[6]) repoConfig.branch = args[6] + if (args[7]) repoConfig.extensions = args[7] + + repoConfig.auto_add_cron = true + repoConfig.commenttotask = 'true' + repoConfig.repo_source = 'ql' + + return { repoConfig, task } +} diff --git a/web/src/views/tasks/RepoDialog.vue b/web/src/views/tasks/RepoDialog.vue index 91129fd..06ba982 100644 --- a/web/src/views/tasks/RepoDialog.vue +++ b/web/src/views/tasks/RepoDialog.vue @@ -15,6 +15,7 @@ import { api, type Task, type RepoConfig, type Agent, type MiseLanguage } from ' import { toast } from 'vue-sonner' import { cn } from '@/lib/utils' import { getCronDescription } from '@/utils/cron' +import { parseBaihuCommand, parseQlCommand } from '@/utils/repo-parser' import TaskNotificationConfig from './components/TaskNotificationConfig.vue' const notificationConfigRef = ref | null>(null) @@ -202,99 +203,32 @@ function importFromBaihu() { } function submitBaihuImport() { - const s = baihuCommandInput.value.trim() - if (!s) { - showBaihuImportDialog.value = false + const result = parseBaihuCommand(baihuCommandInput.value) + if (!result) { + if (!baihuCommandInput.value.trim()) { + showBaihuImportDialog.value = false + } else { + toast.error('未识别到有效的 reposync 参数') + } 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]) + // 应用解析结果 + repoConfig.value = { ...repoConfig.value, ...result.repoConfig } + if (result.task.name) form.value.name = result.task.name + if (result.task.timeout) form.value.timeout = result.task.timeout + + if (result.task.languages) { + selectedLangs.value = result.task.languages.map(l => ({ + name: l.name || '', + version: l.version || '', + availableVersions: [] + })) + selectedLangs.value.forEach(l => updateAvailableVersions(l)) } - 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 参数') - } + // toast.success('命令解析成功,已自动填充表单') + showBaihuImportDialog.value = false } function importFromQl() { @@ -303,51 +237,20 @@ function importFromQl() { } function submitQlImport() { - const s = qlCommandInput.value.trim() - if (!s) { - showQlImportDialog.value = false - return - } - if (!s.startsWith('ql repo')) { - toast.error('无效的指令:必须以 ql repo 开头') + const result = parseQlCommand(qlCommandInput.value) + if (!result) { + if (!qlCommandInput.value.trim()) { + showQlImportDialog.value = false + } else { + toast.error('无效的指令:必须以 ql repo 开头') + } 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]) - } - - if (args[2]) { - repoConfig.value.source_url = args[2] - repoConfig.value.source_type = 'git' - // form task name - let name = '同步 ' - try { - const urlPaths = args[2]?.split('/') - if (urlPaths && urlPaths.length > 0) { - name += urlPaths[urlPaths.length - 1]?.replace('.git', '') || '' - } else { - name += '未命名仓库' - } - } catch { - name += '未命名仓库' - } - form.value.name = name - } - - if (args[3]) repoConfig.value.whitelist_paths = args[3] - if (args[4]) repoConfig.value.blacklist = args[4] - if (args[5]) repoConfig.value.dependence = args[5] - if (args[6]) repoConfig.value.branch = args[6] - if (args[7]) repoConfig.value.extensions = args[7] - - repoConfig.value.auto_add_cron = true - repoConfig.value.commenttotask = 'true' - repoConfig.value.repo_source = 'ql' + // 应用解析结果 + repoConfig.value = { ...repoConfig.value, ...result.repoConfig } + if (result.task.name) form.value.name = result.task.name + toast.success('指令解析成功,已开启自动添加任务,请继续完善其他设置') showQlImportDialog.value = false }