feat(task): support custom repo sync dir name and fix UI copy (#132)

This commit is contained in:
duorameng
2026-06-18 10:43:49 +08:00
parent 2141831075
commit e2c934558c
9 changed files with 68 additions and 50 deletions
+46 -33
View File
@@ -59,7 +59,7 @@ func resolveWorkDir(workDir string) string {
// isValidDirName 校验目录名是否合法 // isValidDirName 校验目录名是否合法
func isValidDirName(dirName string) bool { func isValidDirName(dirName string) bool {
if strings.Contains(dirName, "/") || strings.Contains(dirName, "\\") || strings.Contains(dirName, "..") { if dirName == "." || strings.Contains(dirName, "/") || strings.Contains(dirName, "\\") || strings.Contains(dirName, "..") {
return false return false
} }
for _, ch := range dirName { for _, ch := range dirName {
@@ -141,35 +141,39 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
// 如果是仓库同步任务,根据 URL 生成 SourceID 用于去重 // 如果是仓库同步任务,根据 URL 生成 SourceID 用于去重
if req.Type == constant.TaskTypeRepo && req.Config != "" { if req.Type == constant.TaskTypeRepo && req.Config != "" {
var repoCfg struct { var repoCfg struct {
SourceURL string `json:"source_url"` SourceURL string `json:"source_url"`
Branch string `json:"branch"` Branch string `json:"branch"`
DirName string `json:"dir_name"` RepoDirName string `json:"repo_dir_name"`
TargetPath string `json:"target_path"` TargetPath string `json:"target_path"`
} }
if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" {
if repoCfg.DirName != "" && repoCfg.DirName != "." { if repoCfg.RepoDirName != "" {
if !isValidDirName(repoCfg.DirName) { if !isValidDirName(repoCfg.RepoDirName) {
utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑") utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,不能只有点,且不能包含路径逻辑")
return return
} }
} }
// 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame
if repoCfg.DirName != "" && repoCfg.DirName != "." { if repoCfg.RepoDirName != "" {
sourceID = "repo_" + repoCfg.DirName sourceID = "repo_" + repoCfg.RepoDirName
} else { } else {
sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch)
} }
// 如果是全新任务,校验物理目录是否存在 // 校验 SourceID 是否存在(任务唯一性)
existingTask := tc.taskService.GetTaskBySourceID(sourceID) existingTask := tc.taskService.GetTaskBySourceID(sourceID)
if existingTask == nil { if existingTask != nil {
newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.DirName, repoCfg.SourceURL, repoCfg.Branch) utils.BadRequest(c, "当前任务已存在,请检查或更换仓库目录名称")
if newAbsPath != "" { return
if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() { }
utils.BadRequest(c, "本地已存在同名仓库文件夹,请更换自定义目录名或清理残留文件")
return // 校验物理目录是否存在
} newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.RepoDirName, repoCfg.SourceURL, repoCfg.Branch)
if newAbsPath != "" {
if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() {
utils.BadRequest(c, "本地已存在同名仓库文件夹,请更换自定义目录名或清理残留文件")
return
} }
} }
} }
@@ -331,39 +335,48 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
var sourceID string var sourceID string
if req.Type == constant.TaskTypeRepo && req.Config != "" { if req.Type == constant.TaskTypeRepo && req.Config != "" {
var repoCfg struct { var repoCfg struct {
SourceURL string `json:"source_url"` SourceURL string `json:"source_url"`
Branch string `json:"branch"` Branch string `json:"branch"`
DirName string `json:"dir_name"` RepoDirName string `json:"repo_dir_name"`
TargetPath string `json:"target_path"` TargetPath string `json:"target_path"`
} }
if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" {
if repoCfg.DirName != "" && repoCfg.DirName != "." { if repoCfg.RepoDirName != "" {
if !isValidDirName(repoCfg.DirName) { if !isValidDirName(repoCfg.RepoDirName) {
utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑") utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,不能只有点,且不能包含路径逻辑")
return return
} }
} }
// 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame
if repoCfg.DirName != "" && repoCfg.DirName != "." { if repoCfg.RepoDirName != "" {
sourceID = "repo_" + repoCfg.DirName sourceID = "repo_" + repoCfg.RepoDirName
} else { } else {
sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch)
} }
// 验证更新后的 SourceID 是否和别的任务冲突
if sourceID != oldTask.SourceID {
existingTask := tc.taskService.GetTaskBySourceID(sourceID)
if existingTask != nil && existingTask.ID != oldTask.ID {
utils.BadRequest(c, "当前任务已存在,请检查或更换仓库目录名称")
return
}
}
// 计算新的物理路径 // 计算新的物理路径
newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.DirName, repoCfg.SourceURL, repoCfg.Branch) newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.RepoDirName, repoCfg.SourceURL, repoCfg.Branch)
var oldAbsPath string var oldAbsPath string
if oldTask != nil && oldTask.Type == constant.TaskTypeRepo && oldTask.Config != "" { if oldTask != nil && oldTask.Type == constant.TaskTypeRepo && oldTask.Config != "" {
var oldCfg struct { var oldCfg struct {
SourceURL string `json:"source_url"` SourceURL string `json:"source_url"`
Branch string `json:"branch"` Branch string `json:"branch"`
DirName string `json:"dir_name"` RepoDirName string `json:"repo_dir_name"`
TargetPath string `json:"target_path"` TargetPath string `json:"target_path"`
} }
if json.Unmarshal([]byte(oldTask.Config), &oldCfg) == nil { if json.Unmarshal([]byte(oldTask.Config), &oldCfg) == nil {
oldAbsPath = getRepoPhysicalPath(oldCfg.TargetPath, oldCfg.DirName, oldCfg.SourceURL, oldCfg.Branch) oldAbsPath = getRepoPhysicalPath(oldCfg.TargetPath, oldCfg.RepoDirName, oldCfg.SourceURL, oldCfg.Branch)
} }
} }
+1 -1
View File
@@ -59,7 +59,7 @@ type RepoConfig struct {
AutoAddCron bool `json:"auto_add_cron"` // 自动解析脚本注释添加定时任务 AutoAddCron bool `json:"auto_add_cron"` // 自动解析脚本注释添加定时任务
CommentToTask string `json:"commenttotask"` // 兼容 QL 格式任务脚本注释解析 CommentToTask string `json:"commenttotask"` // 兼容 QL 格式任务脚本注释解析
RepoSource string `json:"repo_source"` // 仓库来源,如果是选择了这个 ql 导入的仓库,= ql RepoSource string `json:"repo_source"` // 仓库来源,如果是选择了这个 ql 导入的仓库,= ql
DirName string `json:"dir_name"` // 自定义仓库目录名 (可填 "." 表示不追加子目录) RepoDirName string `json:"repo_dir_name"` // 自定义仓库目录名
} }
// TaskConfig 任务配置 RepoConfig+TaskConfig=task.config // TaskConfig 任务配置 RepoConfig+TaskConfig=task.config
+2 -2
View File
@@ -1267,8 +1267,8 @@ func BuildRepoCommand(task *models.Task) (string, string) {
if config.Extensions != "" { if config.Extensions != "" {
args = append(args, "--extensions", config.Extensions) args = append(args, "--extensions", config.Extensions)
} }
if config.DirName != "" { if config.RepoDirName != "" {
args = append(args, "--repo-name", config.DirName) args = append(args, "--repo-name", config.RepoDirName)
} }
if string(task.PreCommand) != "" { if string(task.PreCommand) != "" {
args = append(args, "--pre-command", string(task.PreCommand)) args = append(args, "--pre-command", string(task.PreCommand))
+2 -2
View File
@@ -47,7 +47,7 @@
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.3.1", "vite": "^7.3.5",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.2.0",
"vite-plugin-static-copy": "^2.3.0", "vite-plugin-static-copy": "^2.3.0",
"vue-tsc": "^3.1.4" "vue-tsc": "^3.1.4"
@@ -7411,7 +7411,7 @@
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.5", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.5.tgz",
"integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -48,7 +48,7 @@
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.3.1", "vite": "^7.3.5",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.2.0",
"vite-plugin-static-copy": "^2.3.0", "vite-plugin-static-copy": "^2.3.0",
"vue-tsc": "^3.1.4" "vue-tsc": "^3.1.4"
+1 -1
View File
@@ -487,7 +487,7 @@ export interface RepoConfig {
commenttotask?: string commenttotask?: string
concurrency?: number concurrency?: number
repo_source?: string repo_source?: string
dir_name?: string repo_dir_name?: string
} }
export interface ExecutionResult { export interface ExecutionResult {
+3 -2
View File
@@ -28,14 +28,15 @@ export async function copyToClipboard(text: string): Promise<boolean> {
textarea.style.opacity = '0' textarea.style.opacity = '0'
textarea.style.left = '-9999px' textarea.style.left = '-9999px'
document.body.appendChild(textarea) const parent = document.activeElement?.parentNode || document.body
parent.appendChild(textarea)
// 兼容 iOS 的选中方式 // 兼容 iOS 的选中方式
textarea.select() textarea.select()
textarea.setSelectionRange(0, 99999) textarea.setSelectionRange(0, 99999)
const success = document.execCommand('copy') const success = document.execCommand('copy')
document.body.removeChild(textarea) parent.removeChild(textarea)
if (success) { if (success) {
return true return true
+3 -3
View File
@@ -127,7 +127,7 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null {
} }
break break
case '--repo-name': case '--repo-name':
repoConfig.dir_name = value repoConfig.repo_dir_name = value
break break
case '--pre-command': case '--pre-command':
task.pre_command = value task.pre_command = value
@@ -216,8 +216,8 @@ export function generateBaihuCommand(task: Task): string {
if (config.branch) { if (config.branch) {
args.push('--branch', config.branch) args.push('--branch', config.branch)
} }
if (config.dir_name) { if (config.repo_dir_name) {
args.push('--repo-name', config.dir_name) args.push('--repo-name', config.repo_dir_name)
} }
if (config.sparse_path) { if (config.sparse_path) {
args.push('--path', config.sparse_path) args.push('--path', config.sparse_path)
+9 -5
View File
@@ -63,7 +63,7 @@ const repoConfig = ref<RepoConfig>({
concurrency: 1, concurrency: 1,
repo_source: '', repo_source: '',
proxy: '', proxy: '',
dir_name: '' repo_dir_name: ''
}) })
const allAgents = ref<Agent[]>([]) const allAgents = ref<Agent[]>([])
@@ -99,7 +99,7 @@ function exportBaihuCommand() {
if (repoConfig.value.source_url) parts.push(`--source-url "${repoConfig.value.source_url}"`) if (repoConfig.value.source_url) parts.push(`--source-url "${repoConfig.value.source_url}"`)
if (repoConfig.value.target_path) parts.push(`--target-path "${repoConfig.value.target_path}"`) if (repoConfig.value.target_path) parts.push(`--target-path "${repoConfig.value.target_path}"`)
if (repoConfig.value.branch) parts.push(`--branch "${repoConfig.value.branch}"`) if (repoConfig.value.branch) parts.push(`--branch "${repoConfig.value.branch}"`)
if (repoConfig.value.dir_name) parts.push(`--repo-name "${repoConfig.value.dir_name}"`) if (repoConfig.value.repo_dir_name) parts.push(`--repo-name "${repoConfig.value.repo_dir_name}"`)
if (repoConfig.value.sparse_path) parts.push(`--path "${repoConfig.value.sparse_path}"`) if (repoConfig.value.sparse_path) parts.push(`--path "${repoConfig.value.sparse_path}"`)
if (repoConfig.value.single_file) parts.push(`--single-file`) if (repoConfig.value.single_file) parts.push(`--single-file`)
if (repoConfig.value.proxy && repoConfig.value.proxy !== 'none') parts.push(`--proxy ${repoConfig.value.proxy}`) if (repoConfig.value.proxy && repoConfig.value.proxy !== 'none') parts.push(`--proxy ${repoConfig.value.proxy}`)
@@ -231,7 +231,7 @@ watch(() => props.open, async (val: boolean) => {
commenttotask: 'false', commenttotask: 'false',
concurrency: 1, concurrency: 1,
repo_source: '', repo_source: '',
dir_name: '' repo_dir_name: ''
} }
const configStr = props.task?.config const configStr = props.task?.config
if (configStr) { if (configStr) {
@@ -308,7 +308,11 @@ async function save() {
} }
emit('update:open', false) emit('update:open', false)
emit('saved') emit('saved')
} catch { toast.error('保存失败') } } catch (error: any) {
toast.error('保存失败', {
description: error.response?.data?.error || error.response?.data?.message || error.message || '未知错误'
})
}
} }
</script> </script>
@@ -419,7 +423,7 @@ async function save() {
<div v-if="repoConfig.source_type === 'git'" class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3"> <div v-if="repoConfig.source_type === 'git'" 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-medium">目录名定制</Label> <Label class="sm:text-right text-xs text-foreground/70 uppercase tracking-wider font-medium">目录名定制</Label>
<div class="sm:col-span-3 relative"> <div class="sm:col-span-3 relative">
<Input v-model="repoConfig.dir_name" placeholder="自定义生成目录名 (输入 . 表示不追加子目录)" class="h-9 bg-muted/30 border-muted-foreground/20 focus:bg-background transition-all" autocomplete="off" /> <Input v-model="repoConfig.repo_dir_name" placeholder="留空则默认为 username_reponame 拼接" class="h-9 bg-muted/30 border-muted-foreground/20 focus:bg-background transition-all" autocomplete="off" />
</div> </div>
</div> </div>
<div v-if="repoConfig.source_type === 'git'" class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3"> <div v-if="repoConfig.source_type === 'git'" class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3">