From e2c934558c1cf672ca31e23a3159312d63c071fc Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Thu, 18 Jun 2026 10:43:49 +0800 Subject: [PATCH] feat(task): support custom repo sync dir name and fix UI copy (#132) --- internal/controllers/task_controller.go | 79 ++++++++++++--------- internal/models/task.go | 2 +- internal/services/tasks/executor_service.go | 4 +- web/package-lock.json | 4 +- web/package.json | 2 +- web/src/api/index.ts | 2 +- web/src/utils/clipboard.ts | 5 +- web/src/utils/repo-parser.ts | 6 +- web/src/views/tasks/RepoDialog.vue | 14 ++-- 9 files changed, 68 insertions(+), 50 deletions(-) diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index 3d75552..49207cb 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -59,7 +59,7 @@ func resolveWorkDir(workDir string) string { // isValidDirName 校验目录名是否合法 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 } for _, ch := range dirName { @@ -141,35 +141,39 @@ func (tc *TaskController) CreateTask(c *gin.Context) { // 如果是仓库同步任务,根据 URL 生成 SourceID 用于去重 if req.Type == constant.TaskTypeRepo && req.Config != "" { var repoCfg struct { - SourceURL string `json:"source_url"` - Branch string `json:"branch"` - DirName string `json:"dir_name"` - TargetPath string `json:"target_path"` + SourceURL string `json:"source_url"` + Branch string `json:"branch"` + RepoDirName string `json:"repo_dir_name"` + TargetPath string `json:"target_path"` } if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { - if repoCfg.DirName != "" && repoCfg.DirName != "." { - if !isValidDirName(repoCfg.DirName) { - utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑") + if repoCfg.RepoDirName != "" { + if !isValidDirName(repoCfg.RepoDirName) { + utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,不能只有点,且不能包含路径逻辑") return } } // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame - if repoCfg.DirName != "" && repoCfg.DirName != "." { - sourceID = "repo_" + repoCfg.DirName + if repoCfg.RepoDirName != "" { + sourceID = "repo_" + repoCfg.RepoDirName } else { sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) } - // 如果是全新任务,校验物理目录是否存在 + // 校验 SourceID 是否已存在(任务唯一性) existingTask := tc.taskService.GetTaskBySourceID(sourceID) - if existingTask == nil { - newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.DirName, repoCfg.SourceURL, repoCfg.Branch) - if newAbsPath != "" { - if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() { - utils.BadRequest(c, "本地已存在同名仓库文件夹,请更换自定义目录名或清理残留文件") - return - } + if existingTask != nil { + 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 if req.Type == constant.TaskTypeRepo && req.Config != "" { var repoCfg struct { - SourceURL string `json:"source_url"` - Branch string `json:"branch"` - DirName string `json:"dir_name"` - TargetPath string `json:"target_path"` + SourceURL string `json:"source_url"` + Branch string `json:"branch"` + RepoDirName string `json:"repo_dir_name"` + TargetPath string `json:"target_path"` } if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { - if repoCfg.DirName != "" && repoCfg.DirName != "." { - if !isValidDirName(repoCfg.DirName) { - utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑") + if repoCfg.RepoDirName != "" { + if !isValidDirName(repoCfg.RepoDirName) { + utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,不能只有点,且不能包含路径逻辑") return } } // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame - if repoCfg.DirName != "" && repoCfg.DirName != "." { - sourceID = "repo_" + repoCfg.DirName + if repoCfg.RepoDirName != "" { + sourceID = "repo_" + repoCfg.RepoDirName } else { 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 if oldTask != nil && oldTask.Type == constant.TaskTypeRepo && oldTask.Config != "" { var oldCfg struct { - SourceURL string `json:"source_url"` - Branch string `json:"branch"` - DirName string `json:"dir_name"` - TargetPath string `json:"target_path"` + SourceURL string `json:"source_url"` + Branch string `json:"branch"` + RepoDirName string `json:"repo_dir_name"` + TargetPath string `json:"target_path"` } 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) } } diff --git a/internal/models/task.go b/internal/models/task.go index ce0fdfe..2c51adf 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -59,7 +59,7 @@ type RepoConfig struct { AutoAddCron bool `json:"auto_add_cron"` // 自动解析脚本注释添加定时任务 CommentToTask string `json:"commenttotask"` // 兼容 QL 格式任务脚本注释解析 RepoSource string `json:"repo_source"` // 仓库来源,如果是选择了这个 ql 导入的仓库,= ql - DirName string `json:"dir_name"` // 自定义仓库目录名 (可填 "." 表示不追加子目录) + RepoDirName string `json:"repo_dir_name"` // 自定义仓库目录名 } // TaskConfig 任务配置 RepoConfig+TaskConfig=task.config diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index b58d7b1..d039b4f 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -1267,8 +1267,8 @@ func BuildRepoCommand(task *models.Task) (string, string) { if config.Extensions != "" { args = append(args, "--extensions", config.Extensions) } - if config.DirName != "" { - args = append(args, "--repo-name", config.DirName) + if config.RepoDirName != "" { + args = append(args, "--repo-name", config.RepoDirName) } if string(task.PreCommand) != "" { args = append(args, "--pre-command", string(task.PreCommand)) diff --git a/web/package-lock.json b/web/package-lock.json index 6a4d66a..2771c25 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -47,7 +47,7 @@ "@vue/tsconfig": "^0.8.1", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "vite": "^7.3.1", + "vite": "^7.3.5", "vite-plugin-pwa": "^1.2.0", "vite-plugin-static-copy": "^2.3.0", "vue-tsc": "^3.1.4" @@ -7411,7 +7411,7 @@ }, "node_modules/vite": { "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==", "license": "MIT", "dependencies": { diff --git a/web/package.json b/web/package.json index bcc1e23..37295f8 100644 --- a/web/package.json +++ b/web/package.json @@ -48,7 +48,7 @@ "@vue/tsconfig": "^0.8.1", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "vite": "^7.3.1", + "vite": "^7.3.5", "vite-plugin-pwa": "^1.2.0", "vite-plugin-static-copy": "^2.3.0", "vue-tsc": "^3.1.4" diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 4821909..76e25da 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -487,7 +487,7 @@ export interface RepoConfig { commenttotask?: string concurrency?: number repo_source?: string - dir_name?: string + repo_dir_name?: string } export interface ExecutionResult { diff --git a/web/src/utils/clipboard.ts b/web/src/utils/clipboard.ts index 458f6aa..efb35cb 100644 --- a/web/src/utils/clipboard.ts +++ b/web/src/utils/clipboard.ts @@ -28,14 +28,15 @@ export async function copyToClipboard(text: string): Promise { textarea.style.opacity = '0' textarea.style.left = '-9999px' - document.body.appendChild(textarea) + const parent = document.activeElement?.parentNode || document.body + parent.appendChild(textarea) // 兼容 iOS 的选中方式 textarea.select() textarea.setSelectionRange(0, 99999) const success = document.execCommand('copy') - document.body.removeChild(textarea) + parent.removeChild(textarea) if (success) { return true diff --git a/web/src/utils/repo-parser.ts b/web/src/utils/repo-parser.ts index cfd820a..83bed7c 100644 --- a/web/src/utils/repo-parser.ts +++ b/web/src/utils/repo-parser.ts @@ -127,7 +127,7 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null { } break case '--repo-name': - repoConfig.dir_name = value + repoConfig.repo_dir_name = value break case '--pre-command': task.pre_command = value @@ -216,8 +216,8 @@ export function generateBaihuCommand(task: Task): string { if (config.branch) { args.push('--branch', config.branch) } - if (config.dir_name) { - args.push('--repo-name', config.dir_name) + if (config.repo_dir_name) { + args.push('--repo-name', config.repo_dir_name) } if (config.sparse_path) { args.push('--path', config.sparse_path) diff --git a/web/src/views/tasks/RepoDialog.vue b/web/src/views/tasks/RepoDialog.vue index a6763ce..10ad49b 100644 --- a/web/src/views/tasks/RepoDialog.vue +++ b/web/src/views/tasks/RepoDialog.vue @@ -63,7 +63,7 @@ const repoConfig = ref({ concurrency: 1, repo_source: '', proxy: '', - dir_name: '' + repo_dir_name: '' }) const allAgents = ref([]) @@ -99,7 +99,7 @@ function exportBaihuCommand() { 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.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.single_file) parts.push(`--single-file`) 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', concurrency: 1, repo_source: '', - dir_name: '' + repo_dir_name: '' } const configStr = props.task?.config if (configStr) { @@ -308,7 +308,11 @@ async function save() { } emit('update:open', false) emit('saved') - } catch { toast.error('保存失败') } + } catch (error: any) { + toast.error('保存失败', { + description: error.response?.data?.error || error.response?.data?.message || error.message || '未知错误' + }) + } } @@ -419,7 +423,7 @@ async function save() {
- +