feat(task): support exporting repository sync command with optimized format
This commit is contained in:
@@ -43,11 +43,22 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null {
|
|||||||
const arg = args[i]
|
const arg = args[i]
|
||||||
if (!arg || !arg.startsWith('--')) continue
|
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]
|
const value = args[i + 1]
|
||||||
if (value === undefined || value.startsWith('--')) continue
|
if (value === undefined || value.startsWith('--')) continue
|
||||||
|
|
||||||
i++ // 跳过已处理的 value
|
i++ // 推进索引并跳过已处理的 value
|
||||||
hasValidField = true
|
|
||||||
|
|
||||||
switch (arg) {
|
switch (arg) {
|
||||||
case '--source-type':
|
case '--source-type':
|
||||||
@@ -80,9 +91,6 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null {
|
|||||||
case '--path':
|
case '--path':
|
||||||
repoConfig.sparse_path = value
|
repoConfig.sparse_path = value
|
||||||
break
|
break
|
||||||
case '--single-file':
|
|
||||||
repoConfig.single_file = value === 'true'
|
|
||||||
break
|
|
||||||
case '--proxy-url':
|
case '--proxy-url':
|
||||||
repoConfig.proxy_url = value
|
repoConfig.proxy_url = value
|
||||||
repoConfig.proxy = 'custom'
|
repoConfig.proxy = 'custom'
|
||||||
@@ -172,3 +180,95 @@ export function parseQlCommand(command: string): ParsedRepoResult | null {
|
|||||||
|
|
||||||
return { repoConfig, task }
|
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 ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { TASK_TYPE, AGENT_STATUS, TRIGGER_TYPE, TASK_STATUS } from '@/constants'
|
import { TASK_TYPE, AGENT_STATUS, TRIGGER_TYPE, TASK_STATUS } from '@/constants'
|
||||||
import TextOverflow from '@/components/TextOverflow.vue'
|
import TextOverflow from '@/components/TextOverflow.vue'
|
||||||
import { getCronDescription } from '@/utils/cron'
|
import { getCronDescription } from '@/utils/cron'
|
||||||
|
import { generateBaihuCommand } from '@/utils/repo-parser'
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter()
|
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)
|
const showBatchDeleteDialog = ref(false)
|
||||||
|
|
||||||
function confirmDelete(id: string) {
|
function confirmDelete(id: string) {
|
||||||
@@ -709,6 +733,10 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<Copy class="h-3.5 w-3.5 mr-2" />
|
<Copy class="h-3.5 w-3.5 mr-2" />
|
||||||
<span>复制任务</span>
|
<span>复制任务</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-if="task.type === TASK_TYPE.REPO" @click="openExportDialog(task)">
|
||||||
|
<Terminal class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<span>导出指令</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
||||||
<Trash2 class="h-3.5 w-3.5 mr-2" />
|
<Trash2 class="h-3.5 w-3.5 mr-2" />
|
||||||
@@ -789,6 +817,10 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<Copy class="h-3.5 w-3.5 mr-2" />
|
<Copy class="h-3.5 w-3.5 mr-2" />
|
||||||
<span>复制任务</span>
|
<span>复制任务</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-if="task.type === TASK_TYPE.REPO" @click="openExportDialog(task)">
|
||||||
|
<Terminal class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<span>导出指令</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
||||||
<Trash2 class="h-3.5 w-3.5 mr-2" />
|
<Trash2 class="h-3.5 w-3.5 mr-2" />
|
||||||
@@ -876,6 +908,10 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<Copy class="h-4 w-4 mr-2" />
|
<Copy class="h-4 w-4 mr-2" />
|
||||||
<span>复制任务</span>
|
<span>复制任务</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-if="task.type === TASK_TYPE.REPO" @click="openExportDialog(task)">
|
||||||
|
<Terminal class="h-4 w-4 mr-2" />
|
||||||
|
<span>导出指令</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
||||||
<Trash2 class="h-4 w-4 mr-2" />
|
<Trash2 class="h-4 w-4 mr-2" />
|
||||||
@@ -942,5 +978,29 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BaihuDialog>
|
</BaihuDialog>
|
||||||
|
|
||||||
|
<!-- 导出指令弹窗 -->
|
||||||
|
<BaihuDialog v-model:open="showExportDialog" title="导出同步指令">
|
||||||
|
<div class="space-y-4 py-2">
|
||||||
|
<div class="text-xs text-muted-foreground leading-relaxed">
|
||||||
|
您可以复制并分享此指令,在其他部署了白虎面板的系统上导入该仓库同步任务:
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<textarea
|
||||||
|
readonly
|
||||||
|
:value="exportCommandText"
|
||||||
|
rows="6"
|
||||||
|
class="w-full p-4 font-mono text-[11px] leading-normal bg-muted/50 border border-muted-foreground/10 rounded-lg resize-none select-all focus:outline-none focus:ring-1 focus:ring-primary shadow-inner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button variant="ghost" size="sm" @click="showExportDialog = false">关闭</Button>
|
||||||
|
<Button size="sm" class="gap-1.5 shadow-md shadow-primary/20 font-medium" @click="copyCommandText">
|
||||||
|
<Copy class="h-3.5 w-3.5" />
|
||||||
|
<span>一键复制</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</BaihuDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user