chore: refact repo parser.ts

This commit is contained in:
duorameng
2026-05-07 20:18:19 +08:00
parent 4bf9ea8802
commit b412941983
2 changed files with 200 additions and 130 deletions
+167
View File
@@ -0,0 +1,167 @@
import type { RepoConfig, Task } from '@/api'
/**
* 仓库命令解析结果
*/
export interface ParsedRepoResult {
repoConfig: Partial<RepoConfig>
task: Partial<Task>
}
/**
* 解析带引号的命令行字符串为参数数组
*/
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<RepoConfig> = {}
const task: Partial<Task> = {}
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<RepoConfig> = {}
const task: Partial<Task> = {}
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 }
}
+33 -130
View File
@@ -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<InstanceType<typeof TaskNotificationConfig> | 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
}