From a3871e75b7c23c6e1cb4be94c8fcd83508387cac Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Fri, 8 May 2026 20:18:00 +0800 Subject: [PATCH] feat: add pre and post command --- agent/agent.go | 15 +++- cmd/reposync/reposync.go | 94 +++++++++++++-------- internal/controllers/task_controller.go | 10 ++- internal/executor/cron.go | 4 +- internal/executor/executor.go | 21 ++++- internal/executor/scheduler.go | 10 ++- internal/models/agent.go | 20 +++-- internal/models/task.go | 10 +++ internal/models/vo/task_vo.go | 4 + internal/services/agent_service.go | 27 ++++-- internal/services/tasks/executor_service.go | 34 ++++++-- internal/services/tasks/task_service.go | 9 +- internal/utils/repo.go | 23 ++++- web/src/api/index.ts | 2 + web/src/utils/repo-parser.ts | 6 ++ web/src/views/tasks/RepoDialog.vue | 21 ++++- web/src/views/tasks/TaskDialog.vue | 12 ++- 17 files changed, 255 insertions(+), 67 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 4858cdc..e91ea03 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -46,6 +46,8 @@ type AgentTask struct { ID string `json:"id"` Name string `json:"name"` Command string `json:"command"` + PreCommand string `json:"pre_command"` + PostCommand string `json:"post_command"` Schedule string `json:"schedule"` Cron string `json:"cron"` Timeout int `json:"timeout"` @@ -69,6 +71,14 @@ func (t *AgentTask) GetCommand() string { return t.Command } +func (t *AgentTask) GetPreCommand() string { + return t.PreCommand +} + +func (t *AgentTask) GetPostCommand() string { + return t.PostCommand +} + func (t *AgentTask) GetTimeout() int { return t.Timeout } @@ -527,7 +537,9 @@ func (a *Agent) handleExecute(data json.RawMessage) { TaskID: task.ID, LogID: req.LogID, Name: task.Name, - Command: task.Command, + Command: task.Command, + PreCommand: task.PreCommand, + PostCommand: task.PostCommand, WorkDir: task.WorkDir, Envs: executor.ParseEnvVars(envs), Secrets: req.Secrets, @@ -690,6 +702,7 @@ func (a *Agent) updateTasks(tasks []AgentTask) { for id, task := range newTasks { oldTask, exists := a.tasks[id] if !exists || oldTask.Schedule != task.Schedule || oldTask.Command != task.Command || + oldTask.PreCommand != task.PreCommand || oldTask.PostCommand != task.PostCommand || oldTask.Enabled != task.Enabled || oldTask.Timeout != task.Timeout || oldTask.WorkDir != task.WorkDir || oldTask.Envs != task.Envs || oldTask.RandomRange != task.RandomRange { diff --git a/cmd/reposync/reposync.go b/cmd/reposync/reposync.go index 688c5d5..830854d 100644 --- a/cmd/reposync/reposync.go +++ b/cmd/reposync/reposync.go @@ -20,14 +20,14 @@ import ( ) type Config struct { - SourceType string - SourceURL string - TargetPath string - Branch string - Path string - SingleFile bool - Proxy string - ProxyURL string + SourceType string + SourceURL string + TargetPath string + Branch string + Path string + SingleFile bool + Proxy string + ProxyURL string AuthToken string HttpProxy string WhitelistPaths string // Comma or vertical line separated paths to preserve or filter (whitelist) @@ -38,6 +38,8 @@ type Config struct { TaskLanguages string TaskTimeout int CommentToTask string + PreCommand string + PostCommand string } func Run(args []string) { @@ -62,9 +64,11 @@ func Run(args []string) { fs.StringVar(&cfg.TaskID, "repo-task-id", "", "Original Task ID") fs.IntVar(&cfg.TaskTimeout, "task-timeout", 30, "Task timeout (minutes)") fs.StringVar(&cfg.CommentToTask, "commenttotask", "false", "Compatible with QL format script comment parsing (true/false)") + fs.StringVar(&cfg.PreCommand, "pre-command", "", "Default pre-command for discovered tasks") + fs.StringVar(&cfg.PostCommand, "post-command", "", "Default post-command for discovered tasks") fs.Parse(args) - + // 处理 $SCRIPTS_DIR$ 代号替换 if strings.Contains(cfg.TargetPath, "$SCRIPTS_DIR$") { scriptsDir := os.Getenv("BH_SCRIPTS_DIR") @@ -86,6 +90,19 @@ func Run(args []string) { syncURL(cfg) } + // 执行前置指令 + if cfg.PreCommand != "" { + fmt.Printf("[准备] 执行同步前指令: %s\n", cfg.PreCommand) + // 计算当前仓库真实的物理路径 + repoDir := getActualRepoDir(cfg) + fmt.Printf("[准备] 工作目录: %s\n", repoDir) + fmt.Printf("[准备] 注入环境变量: CURR_REPO_DIR=%s\n", repoDir) + + shell, shellArgs := utils.GetShellCommand(cfg.PreCommand) + envs := append(os.Environ(), "CURR_REPO_DIR="+repoDir) + runCmd(append([]string{shell}, shellArgs...), repoDir, envs) + } + // 执行脚本过滤(仅限 git 模式,url 加载通常为单文件,暂不处理过滤) if cfg.SourceType == "git" { fmt.Printf("[3/3] 正在执行脚本过滤与文件清理...\n") @@ -98,11 +115,34 @@ func Run(args []string) { } } } + + // 执行后置指令 + if cfg.PostCommand != "" { + fmt.Printf("[收尾] 执行同步后指令: %s\n", cfg.PostCommand) + + // 计算当前仓库真实的物理路径 + repoDir := getActualRepoDir(cfg) + fmt.Printf("[收尾] 工作目录: %s\n", repoDir) + fmt.Printf("[收尾] 注入环境变量: CURR_REPO_DIR=%s\n", repoDir) + + shell, shellArgs := utils.GetShellCommand(cfg.PostCommand) + envs := append(os.Environ(), "CURR_REPO_DIR="+repoDir) + runCmd(append([]string{shell}, shellArgs...), repoDir, envs) + } + fmt.Println("\n========================================") fmt.Println(" 仓库同步任务完成 ") fmt.Println("========================================") } +func getActualRepoDir(cfg Config) string { + if cfg.SourceType == "git" { + repoName := utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch) + return filepath.Join(cfg.TargetPath, repoName) + } + return cfg.TargetPath +} + func notifyMainServerToSyncRepoTasks(repoID string, upsertedIDs []string, deletedIDs []string) { appCfg := services.GetConfig() if appCfg != nil { @@ -174,7 +214,7 @@ func syncGit(cfg Config) { if pathExists(gitDir) { fmt.Println("检测到已存在仓库,正在更新...") runCmd([]string{"git", "fetch", "--all"}, dest, env) - + targetBranch := cfg.Branch if targetBranch != "" { // 如果切换了分支,或者当前分支偏离,强制切换并对齐远程 @@ -205,7 +245,7 @@ func syncGit(cfg Config) { fmt.Println("提示: 请清空目标目录或指定一个新目录") os.Exit(1) } - // If dest exists but is empty now, git clone might still complain if the directory itself exists? + // If dest exists but is empty now, git clone might still complain if the directory itself exists? // No, git clone works if dir is empty. cloneCmd := []string{"git", "clone", "--depth", "1"} @@ -306,7 +346,7 @@ func buildProxyURL(url string, proxyType string, proxyURL string) string { if proxyType == "" || proxyType == "none" { return url } - + // 如果 URL 已经包含明显的代理前缀 (如用户手动填写的 http://ghproxy.com/...) // 则跳过内置代理逻辑 if strings.Contains(url, "googo.win") || (proxyType == "custom" && strings.HasPrefix(url, proxyURL)) { @@ -392,8 +432,6 @@ func isRawFileURL(url string) bool { return false } - - var ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]") type cleanWriter struct { @@ -454,11 +492,11 @@ func runCmd(args []string, dir string, env []string) { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = dir cmd.Env = env - + cw := &cleanWriter{out: os.Stdout} cmd.Stdout = cw cmd.Stderr = cw - + if err := cmd.Run(); err != nil { cw.Flush() fmt.Printf("命令执行失败: %v\n", err) @@ -601,18 +639,7 @@ func filterFiles(cfg Config) { return } - dest := cfg.TargetPath - // If the dest appended a repo name in syncGit, we need to find it. - // However, BuildRepoCommand already passes the abs path which might already be the specific repo dir. - // We'll walk from cfg.TargetPath. - - gitDir := filepath.Join(dest, ".git") - if isDir(dest) && !pathExists(gitDir) { - repoName := utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch) - if pathExists(filepath.Join(dest, repoName)) { - dest = filepath.Join(dest, repoName) - } - } + dest := getActualRepoDir(cfg) fmt.Printf("开始执行脚本过滤: %s\n", dest) @@ -623,7 +650,7 @@ func filterFiles(cfg Config) { // We'll collect files to delete to avoid modifying while walking if possible. // But os.RemoveAll is fine. - + count := 0 filepath.Walk(dest, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -704,7 +731,7 @@ func splitKeywords(s string) []string { } else { parts = []string{s} } - + var res []string for _, p := range parts { p = strings.TrimSpace(p) @@ -761,13 +788,15 @@ func cleanEmptyDirs(root string) { } return nil }) - + // Actually we need to do this recursively or multiple times. // A simpler way: entries, _ := os.ReadDir(root) for _, entry := range entries { if entry.IsDir() { - if entry.Name() == ".git" { continue } + if entry.Name() == ".git" { + continue + } dirPath := filepath.Join(root, entry.Name()) cleanEmptyDirs(dirPath) // Check if now empty @@ -778,4 +807,3 @@ func cleanEmptyDirs(root string) { } } - diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index c4ec7f6..b9f7e8b 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -61,6 +61,8 @@ func (tc *TaskController) CreateTask(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` Command string `json:"command"` + PreCommand string `json:"pre_command"` + PostCommand string `json:"post_command"` Tags string `json:"tags"` Type string `json:"type"` Config string `json:"config"` @@ -119,12 +121,12 @@ func (tc *TaskController) CreateTask(c *gin.Context) { if sourceID != "" { task = tc.taskService.GetTaskBySourceID(sourceID) if task != nil { - task = tc.taskService.UpdateTask(task.ID, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, true, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType) + task = tc.taskService.UpdateTask(task.ID, req.Name, req.Command, req.PreCommand, req.PostCommand, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, true, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType) } } if task == nil { - task = tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType) + task = tc.taskService.CreateTask(req.Name, req.Command, req.PreCommand, req.PostCommand, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType) } // 如果是 Agent 任务,通知 Agent;否则添加到本地 cron @@ -225,6 +227,8 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { var req struct { Name string `json:"name"` Command string `json:"command"` + PreCommand string `json:"pre_command"` + PostCommand string `json:"post_command"` Tags string `json:"tags"` Type string `json:"type"` Config string `json:"config"` @@ -274,7 +278,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { sourceID = oldTask.SourceID } - task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType) + task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.PreCommand, req.PostCommand, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType) if task == nil { utils.NotFound(c, "任务不存在") return diff --git a/internal/executor/cron.go b/internal/executor/cron.go index 23ff36f..ba0c6d7 100644 --- a/internal/executor/cron.go +++ b/internal/executor/cron.go @@ -107,7 +107,9 @@ func (m *CronManager) AddTask(task CronTask) error { return &ExecutionRequest{ TaskID: taskID, Name: name, - Command: cmd, + Command: cmd, + PreCommand: task.GetPreCommand(), + PostCommand: task.GetPostCommand(), Type: TaskTypeCron, Timeout: timeout, WorkDir: workDir, diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 2bd56b6..55d9b1b 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -21,6 +21,8 @@ type Task interface { GetID() string GetName() string GetCommand() string + GetPreCommand() string + GetPostCommand() string GetTimeout() int GetWorkDir() string GetEnvs() string @@ -40,8 +42,10 @@ type CronTask interface { // Request 任务执行请求 type Request struct { - Command string - WorkDir string + Command string + PreCommand string + PostCommand string + WorkDir string Envs []string Timeout int // 任务超时时间(分钟) Languages []map[string]string @@ -121,6 +125,19 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer req.UseMise = false } + // 组合指令(如果存在前置或后置指令) + if req.PreCommand != "" || req.PostCommand != "" { + finalCmd := "" + if req.PreCommand != "" { + finalCmd += req.PreCommand + "\n" + } + finalCmd += req.Command + if req.PostCommand != "" { + finalCmd += "\n" + req.PostCommand + } + req.Command = finalCmd + } + // 1. 执行前钩子 var logID string if hooks != nil { diff --git a/internal/executor/scheduler.go b/internal/executor/scheduler.go index 505f841..1b4ab19 100644 --- a/internal/executor/scheduler.go +++ b/internal/executor/scheduler.go @@ -66,8 +66,10 @@ type ExecutionRequest struct { LogID string // 日志 ID Name string // 任务名称 Type TaskType // 任务类型 - Command string // 命令 - WorkDir string // 工作目录 + Command string // 命令 + PreCommand string // 前置命令 + PostCommand string // 后置命令 + WorkDir string // 工作目录 Envs []string // 环境变量 Secrets []string // 需要脱敏的密码 Timeout int // 超时时间(分钟) @@ -204,7 +206,9 @@ func NewScheduler(config SchedulerConfig, handler SchedulerEventHandler) *Schedu executor: func(ctx context.Context, req *ExecutionRequest, stdout, stderr io.Writer) (*Result, error) { hooks := &schedulerHooksAdapter{handler: handler, req: req} return ExecuteWithHooks(ctx, Request{ - Command: req.Command, + Command: req.Command, + PreCommand: req.PreCommand, + PostCommand: req.PostCommand, WorkDir: req.WorkDir, Envs: req.Envs, Timeout: req.Timeout, diff --git a/internal/models/agent.go b/internal/models/agent.go index e9741d8..b20150c 100644 --- a/internal/models/agent.go +++ b/internal/models/agent.go @@ -50,11 +50,13 @@ func (AgentToken) TableName() string { type AgentTask struct { ID string `json:"id"` Name string `json:"name"` - Command string `json:"command"` - Schedule string `json:"schedule"` - Timeout int `json:"timeout"` - WorkDir string `json:"work_dir"` - Envs string `json:"envs"` + Command string `json:"command"` + PreCommand string `json:"pre_command"` + PostCommand string `json:"post_command"` + Schedule string `json:"schedule"` + Timeout int `json:"timeout"` + WorkDir string `json:"work_dir"` + Envs string `json:"envs"` Languages []map[string]string `json:"languages"` RandomRange int `json:"random_range"` Secrets []string `json:"secrets"` @@ -73,6 +75,14 @@ func (t AgentTask) GetCommand() string { return t.Command } +func (t AgentTask) GetPreCommand() string { + return t.PreCommand +} + +func (t AgentTask) GetPostCommand() string { + return t.PostCommand +} + func (t AgentTask) GetSchedule() string { return t.Schedule } diff --git a/internal/models/task.go b/internal/models/task.go index 26a18ec..193b603 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -74,6 +74,8 @@ type Task struct { Remark string `json:"remark" gorm:"size:255;default:''"` PinType string `json:"pin_type" gorm:"size:20;default:none;index"` // 置顶类型: constant.PinTypeNone, constant.PinTypeTop Command BigText `json:"command"` // 普通任务的命令 + PreCommand BigText `json:"pre_command"` // 执行前的命令 + PostCommand BigText `json:"post_command"` // 执行后的命令 Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔 Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: constant.TaskTypeNormal, constant.TaskTypeRepo TriggerType string `json:"trigger_type" gorm:"size:25;default:'cron'"` // 触发类型: constant.TriggerTypeCron, constant.TriggerTypeBaihuStartup @@ -123,6 +125,14 @@ func (t *Task) GetCommand() string { return string(t.Command) } +func (t *Task) GetPreCommand() string { + return string(t.PreCommand) +} + +func (t *Task) GetPostCommand() string { + return string(t.PostCommand) +} + func (t *Task) GetTimeout() int { return t.Timeout } diff --git a/internal/models/vo/task_vo.go b/internal/models/vo/task_vo.go index 4370eb3..40282ab 100644 --- a/internal/models/vo/task_vo.go +++ b/internal/models/vo/task_vo.go @@ -11,6 +11,8 @@ type TaskVO struct { ID string `json:"id"` Name string `json:"name"` Command string `json:"command"` + PreCommand string `json:"pre_command"` + PostCommand string `json:"post_command"` Tags string `json:"tags"` Type string `json:"type"` TriggerType string `json:"trigger_type"` @@ -44,6 +46,8 @@ func ToTaskVO(task *models.Task) *TaskVO { ID: task.ID, Name: task.Name, Command: string(task.Command), + PreCommand: string(task.PreCommand), + PostCommand: string(task.PostCommand), Tags: task.Tags, Type: task.Type, TriggerType: task.TriggerType, diff --git a/internal/services/agent_service.go b/internal/services/agent_service.go index fee2e4e..f1b000c 100644 --- a/internal/services/agent_service.go +++ b/internal/services/agent_service.go @@ -314,13 +314,13 @@ func (s *AgentService) Heartbeat(token, ip, version, buildTime, hostname, osType // GetTasks 获取 Agent 的任务列表 func (s *AgentService) GetTasks(agentID string) []models.AgentTask { - var tasks []models.Task - database.DB.Where("agent_id = ? AND enabled = ?", agentID, true).Find(&tasks) + var tasksList []models.Task + database.DB.Where("agent_id = ? AND enabled = ?", agentID, true).Find(&tasksList) - result := make([]models.AgentTask, len(tasks)) + result := make([]models.AgentTask, len(tasksList)) envService := NewEnvService() - for i, task := range tasks { + for i, task := range tasksList { // 加载环境配置 var envVars []string @@ -343,14 +343,29 @@ func (s *AgentService) GetTasks(agentID string) []models.AgentTask { } envVarsStr := executor.FormatEnvVars(envVars) + + command := string(task.Command) + preCommand := string(task.PreCommand) + postCommand := string(task.PostCommand) + workDir := task.WorkDir + + // 仓库同步任务特殊处理:将配置转换为 reposync 命令行 + if task.Type == constant.TaskTypeRepo { + command, workDir = tasks.BuildRepoCommand(&task) + // 仓库任务的前置/后置命令已作为参数传给 reposync 内部处理,此处清空防止重复执行 + preCommand = "" + postCommand = "" + } result[i] = models.AgentTask{ ID: task.ID, Name: task.Name, - Command: string(task.Command), + Command: command, + PreCommand: preCommand, + PostCommand: postCommand, Schedule: task.Schedule, Timeout: task.Timeout, - WorkDir: task.WorkDir, + WorkDir: workDir, Envs: envVarsStr, Languages: []map[string]string(task.Languages), RandomRange: task.RandomRange, diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index 3333232..db35c32 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -404,7 +404,9 @@ func (es *ExecutorService) HandleTaskRetry(task *models.Task, req *executor.Exec return &executor.ExecutionRequest{ TaskID: req.TaskID, Name: latestTask.Name, - Command: string(latestTask.Command), + Command: string(latestTask.Command), + PreCommand: string(latestTask.PreCommand), + PostCommand: string(latestTask.PostCommand), WorkDir: latestTask.WorkDir, Envs: newEnvs, Secrets: newSecrets, @@ -454,6 +456,8 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor. // 解析路径变量 (如 $SCRIPTS_DIR$) req.Command = es.ResolvePath(req.Command) + req.PreCommand = es.ResolvePath(req.PreCommand) + req.PostCommand = es.ResolvePath(req.PostCommand) req.WorkDir = es.ResolvePath(req.WorkDir) task := es.taskService.GetTaskByID(taskID) @@ -478,10 +482,14 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor. req.Command = cmd req.WorkDir = workDir req.UseMise = false // 仓库同步任务不使用 mise,由系统原生执行 + // 仓库任务的前置/后置命令已作为参数传给 reposync 内部处理,此处清空防止重复执行 + req.PreCommand = "" + req.PostCommand = "" + // 强制脱敏并更新数据库日志 masks := append([]string{}, req.Secrets...) masks = append(masks, utils.GetSystemSecrets()...) - + // 补充仓库特有的 AuthToken var repoCfg models.RepoConfig if err := json.Unmarshal([]byte(task.Config), &repoCfg); err == nil && repoCfg.AuthToken != "" { @@ -489,17 +497,20 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor. } maskedCmd := utils.MaskSecrets(req.Command, masks) - + // 更新数据库中的任务日志命令内容 if req.LogID != "" { es.taskLogService.UpdateLogCommand(req.LogID, maskedCmd) } - + // 在控制台打印最终执行的脱敏命令 logger.Infof("[Executor] 仓库同步最终执行命令: %s", maskedCmd) } } + // 组合指令逻辑已移至 executor.ExecuteWithHooks 中,此处不再处理 + // 以避免指令被重复组合。 + // 远程任务 if task.AgentID != nil && *task.AgentID != "" { // 将请求中已包含的环境变量(已合并)传递给 Agent @@ -651,7 +662,9 @@ func (es *ExecutorService) ExecuteTask(taskID string, extraEnvs []string) *execu req := &executor.ExecutionRequest{ TaskID: task.ID, Name: task.Name, - Command: string(task.Command), + Command: string(task.Command), + PreCommand: string(task.PreCommand), + PostCommand: string(task.PostCommand), WorkDir: task.WorkDir, Envs: envs, Secrets: secrets, @@ -1039,6 +1052,11 @@ func (es *ExecutorService) HandleAgentResult(result *models.AgentTaskResult) err // BuildRepoCommand 构建仓库同步任务的命令 func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) { + return BuildRepoCommand(task) +} + +// BuildRepoCommand 构建仓库同步任务的命令(独立函数,方便 AgentService 调用) +func BuildRepoCommand(task *models.Task) (string, string) { var config models.RepoConfig if err := json.Unmarshal([]byte(task.Config), &config); err != nil { return "", "" @@ -1107,6 +1125,12 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) if config.Extensions != "" { args = append(args, "--extensions", config.Extensions) } + if string(task.PreCommand) != "" { + args = append(args, "--pre-command", string(task.PreCommand)) + } + if string(task.PostCommand) != "" { + args = append(args, "--post-command", string(task.PostCommand)) + } // 传递任务 ID,以便 reposync 内部直接处理脚本注册并输出日志 args = append(args, "--task-id", task.ID) diff --git a/internal/services/tasks/task_service.go b/internal/services/tasks/task_service.go index d1161fd..4d8f715 100644 --- a/internal/services/tasks/task_service.go +++ b/internal/services/tasks/task_service.go @@ -24,7 +24,7 @@ func (ts *TaskService) GetTaskBySourceID(sourceID string) *models.Task { return &task } -func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task { +func (ts *TaskService) CreateTask(name, command, preCommand, postCommand, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task { if taskType == "" { taskType = "task" } @@ -38,6 +38,8 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w ID: utils.GenerateID(), Name: name, Command: models.BigText(command), + PreCommand: models.BigText(preCommand), + PostCommand: models.BigText(postCommand), PinType: pinType, Tags: tags, Type: taskType, @@ -122,7 +124,7 @@ func (ts *TaskService) GetTaskByID(id string) *models.Task { return &task } -func (ts *TaskService) UpdateTask(id string, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task { +func (ts *TaskService) UpdateTask(id string, name, command, preCommand, postCommand, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task { var task models.Task res := database.DB.Where("id = ?", id).Limit(1).Find(&task) if res.Error != nil || res.RowsAffected == 0 { @@ -130,6 +132,8 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim } task.Name = name task.Command = models.BigText(command) + task.PreCommand = models.BigText(preCommand) + task.PostCommand = models.BigText(postCommand) task.PinType = pinType task.Tags = tags task.Schedule = schedule @@ -159,6 +163,7 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim "CleanConfig", "Envs", "Enabled", "AgentID", "Languages", "RetryCount", "RetryInterval", "RandomRange", "Type", "TriggerType", "Config", "SourceID", "PinType", + "PreCommand", "PostCommand", ).Updates(&task) return &task diff --git a/internal/utils/repo.go b/internal/utils/repo.go index 96bc0d3..7f0acff 100644 --- a/internal/utils/repo.go +++ b/internal/utils/repo.go @@ -1,6 +1,10 @@ package utils -import "strings" +import ( + "os" + "path/filepath" + "strings" +) // GetRepoIdentifier 返回根据仓库URL和分支生成的作者_仓库名标识符 func GetRepoIdentifier(url string, branch string) string { @@ -45,3 +49,20 @@ func GetRepoIdentifier(url string, branch string) string { identifier = strings.ReplaceAll(identifier, ".", "_") return identifier } + +// GetActualRepoDir 返回仓库真实的物理目录 +func GetActualRepoDir(targetPath, sourceURL, branch, sourceType string) string { + repoDir := targetPath + if sourceType == "git" && sourceURL != "" { + repoName := GetRepoIdentifier(sourceURL, branch) + // 检查 targetPath 是否已存在且是 Git 仓库 + gitDir := filepath.Join(repoDir, ".git") + if info, err := os.Stat(repoDir); err == nil && info.IsDir() { + if _, err := os.Stat(gitDir); os.IsNotExist(err) { + // 只有当目标目录存在但不是 Git 仓库时,才追加仓库名 + repoDir = filepath.Join(repoDir, repoName) + } + } + } + return repoDir +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index d8ec8fa..e5601de 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -349,6 +349,8 @@ export interface Task { name: string remark: string command: string + pre_command: string + post_command: string tags: string type: string trigger_type: string diff --git a/web/src/utils/repo-parser.ts b/web/src/utils/repo-parser.ts index 86ffb29..d039188 100644 --- a/web/src/utils/repo-parser.ts +++ b/web/src/utils/repo-parser.ts @@ -117,6 +117,12 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null { console.error('Parse task-langs failed', e) } break + case '--pre-command': + task.pre_command = value + break + case '--post-command': + task.post_command = value + break } } diff --git a/web/src/views/tasks/RepoDialog.vue b/web/src/views/tasks/RepoDialog.vue index 06ba982..c9a2d77 100644 --- a/web/src/views/tasks/RepoDialog.vue +++ b/web/src/views/tasks/RepoDialog.vue @@ -217,6 +217,8 @@ function submitBaihuImport() { 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.pre_command) form.value.pre_command = result.task.pre_command + if (result.task.post_command) form.value.post_command = result.task.post_command if (result.task.languages) { selectedLangs.value = result.task.languages.map(l => ({ @@ -308,6 +310,8 @@ watch(() => props.open, async (val: boolean) => { random_range: props.task?.random_range ?? 0, timeout: props.task?.timeout ?? 30, pin_type: props.task?.pin_type ?? 'none', + pre_command: props.task?.pre_command ?? '', + post_command: props.task?.post_command ?? '', ...props.task } // 解析清理配置 @@ -440,12 +444,12 @@ async function save() {
- +
{{ isEdit ? '编辑仓库同步' : '新建仓库同步' }} -
+
+ +
+ +
+
+
+ +
+
@@ -923,10 +936,10 @@ async function save() {
- +
- baihu reposync --source-url 'https://...' --branch 'main' --blacklist '...' + baihu reposync --source-url 'https://...' --branch 'main' --blacklist '...' --pre-command '...' --post-command '...'
diff --git a/web/src/views/tasks/TaskDialog.vue b/web/src/views/tasks/TaskDialog.vue index ec1118e..fded2ff 100644 --- a/web/src/views/tasks/TaskDialog.vue +++ b/web/src/views/tasks/TaskDialog.vue @@ -241,6 +241,8 @@ watch(() => props.open, async (val: boolean) => { random_range: props.task?.random_range ?? 0, timeout: props.task?.timeout ?? 30, pin_type: props.task?.pin_type ?? 'none', + pre_command: props.task?.pre_command ?? '', + post_command: props.task?.post_command ?? '', ...props.task } // 解析清理配置 @@ -576,9 +578,17 @@ async function save() {
- + +
+
+
+
+
+ +
+