feat: add pre and post command

This commit is contained in:
duorameng
2026-05-08 20:18:00 +08:00
parent 2f88fa0718
commit a3871e75b7
17 changed files with 255 additions and 67 deletions
+14 -1
View File
@@ -46,6 +46,8 @@ type AgentTask struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Command string `json:"command"` Command string `json:"command"`
PreCommand string `json:"pre_command"`
PostCommand string `json:"post_command"`
Schedule string `json:"schedule"` Schedule string `json:"schedule"`
Cron string `json:"cron"` Cron string `json:"cron"`
Timeout int `json:"timeout"` Timeout int `json:"timeout"`
@@ -69,6 +71,14 @@ func (t *AgentTask) GetCommand() string {
return t.Command return t.Command
} }
func (t *AgentTask) GetPreCommand() string {
return t.PreCommand
}
func (t *AgentTask) GetPostCommand() string {
return t.PostCommand
}
func (t *AgentTask) GetTimeout() int { func (t *AgentTask) GetTimeout() int {
return t.Timeout return t.Timeout
} }
@@ -527,7 +537,9 @@ func (a *Agent) handleExecute(data json.RawMessage) {
TaskID: task.ID, TaskID: task.ID,
LogID: req.LogID, LogID: req.LogID,
Name: task.Name, Name: task.Name,
Command: task.Command, Command: task.Command,
PreCommand: task.PreCommand,
PostCommand: task.PostCommand,
WorkDir: task.WorkDir, WorkDir: task.WorkDir,
Envs: executor.ParseEnvVars(envs), Envs: executor.ParseEnvVars(envs),
Secrets: req.Secrets, Secrets: req.Secrets,
@@ -690,6 +702,7 @@ func (a *Agent) updateTasks(tasks []AgentTask) {
for id, task := range newTasks { for id, task := range newTasks {
oldTask, exists := a.tasks[id] oldTask, exists := a.tasks[id]
if !exists || oldTask.Schedule != task.Schedule || oldTask.Command != task.Command || 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.Enabled != task.Enabled || oldTask.Timeout != task.Timeout ||
oldTask.WorkDir != task.WorkDir || oldTask.Envs != task.Envs || oldTask.WorkDir != task.WorkDir || oldTask.Envs != task.Envs ||
oldTask.RandomRange != task.RandomRange { oldTask.RandomRange != task.RandomRange {
+52 -24
View File
@@ -20,14 +20,14 @@ import (
) )
type Config struct { type Config struct {
SourceType string SourceType string
SourceURL string SourceURL string
TargetPath string TargetPath string
Branch string Branch string
Path string Path string
SingleFile bool SingleFile bool
Proxy string Proxy string
ProxyURL string ProxyURL string
AuthToken string AuthToken string
HttpProxy string HttpProxy string
WhitelistPaths string // Comma or vertical line separated paths to preserve or filter (whitelist) WhitelistPaths string // Comma or vertical line separated paths to preserve or filter (whitelist)
@@ -38,6 +38,8 @@ type Config struct {
TaskLanguages string TaskLanguages string
TaskTimeout int TaskTimeout int
CommentToTask string CommentToTask string
PreCommand string
PostCommand string
} }
func Run(args []string) { func Run(args []string) {
@@ -62,6 +64,8 @@ func Run(args []string) {
fs.StringVar(&cfg.TaskID, "repo-task-id", "", "Original Task ID") fs.StringVar(&cfg.TaskID, "repo-task-id", "", "Original Task ID")
fs.IntVar(&cfg.TaskTimeout, "task-timeout", 30, "Task timeout (minutes)") 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.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) fs.Parse(args)
@@ -86,6 +90,19 @@ func Run(args []string) {
syncURL(cfg) 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 加载通常为单文件,暂不处理过滤) // 执行脚本过滤(仅限 git 模式,url 加载通常为单文件,暂不处理过滤)
if cfg.SourceType == "git" { if cfg.SourceType == "git" {
fmt.Printf("[3/3] 正在执行脚本过滤与文件清理...\n") 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("\n========================================")
fmt.Println(" 仓库同步任务完成 ") fmt.Println(" 仓库同步任务完成 ")
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) { func notifyMainServerToSyncRepoTasks(repoID string, upsertedIDs []string, deletedIDs []string) {
appCfg := services.GetConfig() appCfg := services.GetConfig()
if appCfg != nil { if appCfg != nil {
@@ -392,8 +432,6 @@ func isRawFileURL(url string) bool {
return false return false
} }
var ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]") var ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]")
type cleanWriter struct { type cleanWriter struct {
@@ -601,18 +639,7 @@ func filterFiles(cfg Config) {
return return
} }
dest := cfg.TargetPath dest := getActualRepoDir(cfg)
// 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)
}
}
fmt.Printf("开始执行脚本过滤: %s\n", dest) fmt.Printf("开始执行脚本过滤: %s\n", dest)
@@ -767,7 +794,9 @@ func cleanEmptyDirs(root string) {
entries, _ := os.ReadDir(root) entries, _ := os.ReadDir(root)
for _, entry := range entries { for _, entry := range entries {
if entry.IsDir() { if entry.IsDir() {
if entry.Name() == ".git" { continue } if entry.Name() == ".git" {
continue
}
dirPath := filepath.Join(root, entry.Name()) dirPath := filepath.Join(root, entry.Name())
cleanEmptyDirs(dirPath) cleanEmptyDirs(dirPath)
// Check if now empty // Check if now empty
@@ -778,4 +807,3 @@ func cleanEmptyDirs(root string) {
} }
} }
+7 -3
View File
@@ -61,6 +61,8 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
var req struct { var req struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Command string `json:"command"` Command string `json:"command"`
PreCommand string `json:"pre_command"`
PostCommand string `json:"post_command"`
Tags string `json:"tags"` Tags string `json:"tags"`
Type string `json:"type"` Type string `json:"type"`
Config string `json:"config"` Config string `json:"config"`
@@ -119,12 +121,12 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
if sourceID != "" { if sourceID != "" {
task = tc.taskService.GetTaskBySourceID(sourceID) task = tc.taskService.GetTaskBySourceID(sourceID)
if task != nil { 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 { 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 // 如果是 Agent 任务,通知 Agent;否则添加到本地 cron
@@ -225,6 +227,8 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
var req struct { var req struct {
Name string `json:"name"` Name string `json:"name"`
Command string `json:"command"` Command string `json:"command"`
PreCommand string `json:"pre_command"`
PostCommand string `json:"post_command"`
Tags string `json:"tags"` Tags string `json:"tags"`
Type string `json:"type"` Type string `json:"type"`
Config string `json:"config"` Config string `json:"config"`
@@ -274,7 +278,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
sourceID = oldTask.SourceID 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 { if task == nil {
utils.NotFound(c, "任务不存在") utils.NotFound(c, "任务不存在")
return return
+3 -1
View File
@@ -107,7 +107,9 @@ func (m *CronManager) AddTask(task CronTask) error {
return &ExecutionRequest{ return &ExecutionRequest{
TaskID: taskID, TaskID: taskID,
Name: name, Name: name,
Command: cmd, Command: cmd,
PreCommand: task.GetPreCommand(),
PostCommand: task.GetPostCommand(),
Type: TaskTypeCron, Type: TaskTypeCron,
Timeout: timeout, Timeout: timeout,
WorkDir: workDir, WorkDir: workDir,
+19 -2
View File
@@ -21,6 +21,8 @@ type Task interface {
GetID() string GetID() string
GetName() string GetName() string
GetCommand() string GetCommand() string
GetPreCommand() string
GetPostCommand() string
GetTimeout() int GetTimeout() int
GetWorkDir() string GetWorkDir() string
GetEnvs() string GetEnvs() string
@@ -40,8 +42,10 @@ type CronTask interface {
// Request 任务执行请求 // Request 任务执行请求
type Request struct { type Request struct {
Command string Command string
WorkDir string PreCommand string
PostCommand string
WorkDir string
Envs []string Envs []string
Timeout int // 任务超时时间(分钟) Timeout int // 任务超时时间(分钟)
Languages []map[string]string Languages []map[string]string
@@ -121,6 +125,19 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
req.UseMise = false 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. 执行前钩子 // 1. 执行前钩子
var logID string var logID string
if hooks != nil { if hooks != nil {
+7 -3
View File
@@ -66,8 +66,10 @@ type ExecutionRequest struct {
LogID string // 日志 ID LogID string // 日志 ID
Name string // 任务名称 Name string // 任务名称
Type TaskType // 任务类型 Type TaskType // 任务类型
Command string // 命令 Command string // 命令
WorkDir string // 工作目录 PreCommand string // 前置命令
PostCommand string // 后置命令
WorkDir string // 工作目录
Envs []string // 环境变量 Envs []string // 环境变量
Secrets []string // 需要脱敏的密码 Secrets []string // 需要脱敏的密码
Timeout int // 超时时间(分钟) 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) { executor: func(ctx context.Context, req *ExecutionRequest, stdout, stderr io.Writer) (*Result, error) {
hooks := &schedulerHooksAdapter{handler: handler, req: req} hooks := &schedulerHooksAdapter{handler: handler, req: req}
return ExecuteWithHooks(ctx, Request{ return ExecuteWithHooks(ctx, Request{
Command: req.Command, Command: req.Command,
PreCommand: req.PreCommand,
PostCommand: req.PostCommand,
WorkDir: req.WorkDir, WorkDir: req.WorkDir,
Envs: req.Envs, Envs: req.Envs,
Timeout: req.Timeout, Timeout: req.Timeout,
+15 -5
View File
@@ -50,11 +50,13 @@ func (AgentToken) TableName() string {
type AgentTask struct { type AgentTask struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Command string `json:"command"` Command string `json:"command"`
Schedule string `json:"schedule"` PreCommand string `json:"pre_command"`
Timeout int `json:"timeout"` PostCommand string `json:"post_command"`
WorkDir string `json:"work_dir"` Schedule string `json:"schedule"`
Envs string `json:"envs"` Timeout int `json:"timeout"`
WorkDir string `json:"work_dir"`
Envs string `json:"envs"`
Languages []map[string]string `json:"languages"` Languages []map[string]string `json:"languages"`
RandomRange int `json:"random_range"` RandomRange int `json:"random_range"`
Secrets []string `json:"secrets"` Secrets []string `json:"secrets"`
@@ -73,6 +75,14 @@ func (t AgentTask) GetCommand() string {
return t.Command return t.Command
} }
func (t AgentTask) GetPreCommand() string {
return t.PreCommand
}
func (t AgentTask) GetPostCommand() string {
return t.PostCommand
}
func (t AgentTask) GetSchedule() string { func (t AgentTask) GetSchedule() string {
return t.Schedule return t.Schedule
} }
+10
View File
@@ -74,6 +74,8 @@ type Task struct {
Remark string `json:"remark" gorm:"size:255;default:''"` Remark string `json:"remark" gorm:"size:255;default:''"`
PinType string `json:"pin_type" gorm:"size:20;default:none;index"` // 置顶类型: constant.PinTypeNone, constant.PinTypeTop PinType string `json:"pin_type" gorm:"size:20;default:none;index"` // 置顶类型: constant.PinTypeNone, constant.PinTypeTop
Command BigText `json:"command"` // 普通任务的命令 Command BigText `json:"command"` // 普通任务的命令
PreCommand BigText `json:"pre_command"` // 执行前的命令
PostCommand BigText `json:"post_command"` // 执行后的命令
Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔 Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔
Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: constant.TaskTypeNormal, constant.TaskTypeRepo 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 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) 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 { func (t *Task) GetTimeout() int {
return t.Timeout return t.Timeout
} }
+4
View File
@@ -11,6 +11,8 @@ type TaskVO struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Command string `json:"command"` Command string `json:"command"`
PreCommand string `json:"pre_command"`
PostCommand string `json:"post_command"`
Tags string `json:"tags"` Tags string `json:"tags"`
Type string `json:"type"` Type string `json:"type"`
TriggerType string `json:"trigger_type"` TriggerType string `json:"trigger_type"`
@@ -44,6 +46,8 @@ func ToTaskVO(task *models.Task) *TaskVO {
ID: task.ID, ID: task.ID,
Name: task.Name, Name: task.Name,
Command: string(task.Command), Command: string(task.Command),
PreCommand: string(task.PreCommand),
PostCommand: string(task.PostCommand),
Tags: task.Tags, Tags: task.Tags,
Type: task.Type, Type: task.Type,
TriggerType: task.TriggerType, TriggerType: task.TriggerType,
+21 -6
View File
@@ -314,13 +314,13 @@ func (s *AgentService) Heartbeat(token, ip, version, buildTime, hostname, osType
// GetTasks 获取 Agent 的任务列表 // GetTasks 获取 Agent 的任务列表
func (s *AgentService) GetTasks(agentID string) []models.AgentTask { func (s *AgentService) GetTasks(agentID string) []models.AgentTask {
var tasks []models.Task var tasksList []models.Task
database.DB.Where("agent_id = ? AND enabled = ?", agentID, true).Find(&tasks) 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() envService := NewEnvService()
for i, task := range tasks { for i, task := range tasksList {
// 加载环境配置 // 加载环境配置
var envVars []string var envVars []string
@@ -344,13 +344,28 @@ func (s *AgentService) GetTasks(agentID string) []models.AgentTask {
envVarsStr := executor.FormatEnvVars(envVars) 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{ result[i] = models.AgentTask{
ID: task.ID, ID: task.ID,
Name: task.Name, Name: task.Name,
Command: string(task.Command), Command: command,
PreCommand: preCommand,
PostCommand: postCommand,
Schedule: task.Schedule, Schedule: task.Schedule,
Timeout: task.Timeout, Timeout: task.Timeout,
WorkDir: task.WorkDir, WorkDir: workDir,
Envs: envVarsStr, Envs: envVarsStr,
Languages: []map[string]string(task.Languages), Languages: []map[string]string(task.Languages),
RandomRange: task.RandomRange, RandomRange: task.RandomRange,
+26 -2
View File
@@ -404,7 +404,9 @@ func (es *ExecutorService) HandleTaskRetry(task *models.Task, req *executor.Exec
return &executor.ExecutionRequest{ return &executor.ExecutionRequest{
TaskID: req.TaskID, TaskID: req.TaskID,
Name: latestTask.Name, Name: latestTask.Name,
Command: string(latestTask.Command), Command: string(latestTask.Command),
PreCommand: string(latestTask.PreCommand),
PostCommand: string(latestTask.PostCommand),
WorkDir: latestTask.WorkDir, WorkDir: latestTask.WorkDir,
Envs: newEnvs, Envs: newEnvs,
Secrets: newSecrets, Secrets: newSecrets,
@@ -454,6 +456,8 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.
// 解析路径变量 (如 $SCRIPTS_DIR$) // 解析路径变量 (如 $SCRIPTS_DIR$)
req.Command = es.ResolvePath(req.Command) req.Command = es.ResolvePath(req.Command)
req.PreCommand = es.ResolvePath(req.PreCommand)
req.PostCommand = es.ResolvePath(req.PostCommand)
req.WorkDir = es.ResolvePath(req.WorkDir) req.WorkDir = es.ResolvePath(req.WorkDir)
task := es.taskService.GetTaskByID(taskID) task := es.taskService.GetTaskByID(taskID)
@@ -478,6 +482,10 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.
req.Command = cmd req.Command = cmd
req.WorkDir = workDir req.WorkDir = workDir
req.UseMise = false // 仓库同步任务不使用 mise,由系统原生执行 req.UseMise = false // 仓库同步任务不使用 mise,由系统原生执行
// 仓库任务的前置/后置命令已作为参数传给 reposync 内部处理,此处清空防止重复执行
req.PreCommand = ""
req.PostCommand = ""
// 强制脱敏并更新数据库日志 // 强制脱敏并更新数据库日志
masks := append([]string{}, req.Secrets...) masks := append([]string{}, req.Secrets...)
masks = append(masks, utils.GetSystemSecrets()...) masks = append(masks, utils.GetSystemSecrets()...)
@@ -500,6 +508,9 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.
} }
} }
// 组合指令逻辑已移至 executor.ExecuteWithHooks 中,此处不再处理
// 以避免指令被重复组合。
// 远程任务 // 远程任务
if task.AgentID != nil && *task.AgentID != "" { if task.AgentID != nil && *task.AgentID != "" {
// 将请求中已包含的环境变量(已合并)传递给 Agent // 将请求中已包含的环境变量(已合并)传递给 Agent
@@ -651,7 +662,9 @@ func (es *ExecutorService) ExecuteTask(taskID string, extraEnvs []string) *execu
req := &executor.ExecutionRequest{ req := &executor.ExecutionRequest{
TaskID: task.ID, TaskID: task.ID,
Name: task.Name, Name: task.Name,
Command: string(task.Command), Command: string(task.Command),
PreCommand: string(task.PreCommand),
PostCommand: string(task.PostCommand),
WorkDir: task.WorkDir, WorkDir: task.WorkDir,
Envs: envs, Envs: envs,
Secrets: secrets, Secrets: secrets,
@@ -1039,6 +1052,11 @@ func (es *ExecutorService) HandleAgentResult(result *models.AgentTaskResult) err
// BuildRepoCommand 构建仓库同步任务的命令 // BuildRepoCommand 构建仓库同步任务的命令
func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) { 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 var config models.RepoConfig
if err := json.Unmarshal([]byte(task.Config), &config); err != nil { if err := json.Unmarshal([]byte(task.Config), &config); err != nil {
return "", "" return "", ""
@@ -1107,6 +1125,12 @@ func (es *ExecutorService) 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 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 内部直接处理脚本注册并输出日志 // 传递任务 ID,以便 reposync 内部直接处理脚本注册并输出日志
args = append(args, "--task-id", task.ID) args = append(args, "--task-id", task.ID)
+7 -2
View File
@@ -24,7 +24,7 @@ func (ts *TaskService) GetTaskBySourceID(sourceID string) *models.Task {
return &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 == "" { if taskType == "" {
taskType = "task" taskType = "task"
} }
@@ -38,6 +38,8 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w
ID: utils.GenerateID(), ID: utils.GenerateID(),
Name: name, Name: name,
Command: models.BigText(command), Command: models.BigText(command),
PreCommand: models.BigText(preCommand),
PostCommand: models.BigText(postCommand),
PinType: pinType, PinType: pinType,
Tags: tags, Tags: tags,
Type: taskType, Type: taskType,
@@ -122,7 +124,7 @@ func (ts *TaskService) GetTaskByID(id string) *models.Task {
return &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 var task models.Task
res := database.DB.Where("id = ?", id).Limit(1).Find(&task) res := database.DB.Where("id = ?", id).Limit(1).Find(&task)
if res.Error != nil || res.RowsAffected == 0 { 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.Name = name
task.Command = models.BigText(command) task.Command = models.BigText(command)
task.PreCommand = models.BigText(preCommand)
task.PostCommand = models.BigText(postCommand)
task.PinType = pinType task.PinType = pinType
task.Tags = tags task.Tags = tags
task.Schedule = schedule task.Schedule = schedule
@@ -159,6 +163,7 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim
"CleanConfig", "Envs", "Enabled", "AgentID", "Languages", "CleanConfig", "Envs", "Enabled", "AgentID", "Languages",
"RetryCount", "RetryInterval", "RandomRange", "Type", "RetryCount", "RetryInterval", "RandomRange", "Type",
"TriggerType", "Config", "SourceID", "PinType", "TriggerType", "Config", "SourceID", "PinType",
"PreCommand", "PostCommand",
).Updates(&task) ).Updates(&task)
return &task return &task
+22 -1
View File
@@ -1,6 +1,10 @@
package utils package utils
import "strings" import (
"os"
"path/filepath"
"strings"
)
// GetRepoIdentifier 返回根据仓库URL和分支生成的作者_仓库名标识符 // GetRepoIdentifier 返回根据仓库URL和分支生成的作者_仓库名标识符
func GetRepoIdentifier(url string, branch string) string { func GetRepoIdentifier(url string, branch string) string {
@@ -45,3 +49,20 @@ func GetRepoIdentifier(url string, branch string) string {
identifier = strings.ReplaceAll(identifier, ".", "_") identifier = strings.ReplaceAll(identifier, ".", "_")
return 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
}
+2
View File
@@ -349,6 +349,8 @@ export interface Task {
name: string name: string
remark: string remark: string
command: string command: string
pre_command: string
post_command: string
tags: string tags: string
type: string type: string
trigger_type: string trigger_type: string
+6
View File
@@ -117,6 +117,12 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null {
console.error('Parse task-langs failed', e) console.error('Parse task-langs failed', e)
} }
break break
case '--pre-command':
task.pre_command = value
break
case '--post-command':
task.post_command = value
break
} }
} }
+17 -4
View File
@@ -217,6 +217,8 @@ function submitBaihuImport() {
repoConfig.value = { ...repoConfig.value, ...result.repoConfig } repoConfig.value = { ...repoConfig.value, ...result.repoConfig }
if (result.task.name) form.value.name = result.task.name if (result.task.name) form.value.name = result.task.name
if (result.task.timeout) form.value.timeout = result.task.timeout 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) { if (result.task.languages) {
selectedLangs.value = result.task.languages.map(l => ({ selectedLangs.value = result.task.languages.map(l => ({
@@ -308,6 +310,8 @@ watch(() => props.open, async (val: boolean) => {
random_range: props.task?.random_range ?? 0, random_range: props.task?.random_range ?? 0,
timeout: props.task?.timeout ?? 30, timeout: props.task?.timeout ?? 30,
pin_type: props.task?.pin_type ?? 'none', pin_type: props.task?.pin_type ?? 'none',
pre_command: props.task?.pre_command ?? '',
post_command: props.task?.post_command ?? '',
...props.task ...props.task
} }
// 解析清理配置 // 解析清理配置
@@ -440,12 +444,12 @@ async function save() {
<Dialog :open="open" @update:open="emit('update:open', $event)"> <Dialog :open="open" @update:open="emit('update:open', $event)">
<DialogContent class="max-w-[95vw] sm:max-w-[700px] xl:max-w-[950px] p-0 overflow-hidden border-none bg-background shadow-2xl transition-all duration-300" style="text-rendering: optimizeLegibility;" @openAutoFocus.prevent> <DialogContent class="max-w-[95vw] sm:max-w-[700px] xl:max-w-[950px] p-0 overflow-hidden border-none bg-background shadow-2xl transition-all duration-300" style="text-rendering: optimizeLegibility;" @openAutoFocus.prevent>
<div class="flex flex-col max-h-[85vh]"> <div class="flex flex-col max-h-[85vh]">
<DialogHeader class="px-5 sm:px-6 pr-10 pt-6 pb-2 shrink-0"> <DialogHeader class="px-5 sm:px-6 pr-20 pt-6 pb-2 shrink-0">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-2"> <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-2">
<DialogTitle class="text-xl font-bold whitespace-nowrap"> <DialogTitle class="text-xl font-bold whitespace-nowrap">
{{ isEdit ? '编辑仓库同步' : '新建仓库同步' }} {{ isEdit ? '编辑仓库同步' : '新建仓库同步' }}
</DialogTitle> </DialogTitle>
<div v-if="!isEdit" class="flex flex-wrap items-center gap-2"> <div v-if="!isEdit" class="flex flex-wrap items-center gap-2 sm:mr-4">
<Button variant="outline" size="sm" @click="importFromBaihu" class="flex-1 sm:flex-initial h-8 gap-1.5 bg-primary/5 hover:bg-primary/10 border-primary/20 hover:border-primary/40 text-primary px-3"> <Button variant="outline" size="sm" @click="importFromBaihu" class="flex-1 sm:flex-initial h-8 gap-1.5 bg-primary/5 hover:bg-primary/10 border-primary/20 hover:border-primary/40 text-primary px-3">
<Terminal class="w-3.5 h-3.5" /> <Terminal class="w-3.5 h-3.5" />
<span class="text-xs">Baihu 命令导入</span> <span class="text-xs">Baihu 命令导入</span>
@@ -573,6 +577,15 @@ async function save() {
</div> </div>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3 mt-4">
<Label class="sm:text-right text-xs text-foreground/70 uppercase tracking-wider font-bold">前置脚本</Label>
<div class="sm:col-span-3 relative"><Input v-model="form.pre_command" placeholder="同步前运行的指令 (可选)" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50 pr-10', form.pre_command ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /><Zap class="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground opacity-40 pointer-events-none" /></div>
</div>
<div 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-bold">后置脚本</Label>
<div class="sm:col-span-3 relative"><Input v-model="form.post_command" placeholder="同步后运行的指令 (可选)" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50 pr-10', form.post_command ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /><Zap class="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground opacity-40 pointer-events-none" /></div>
</div>
</div> </div>
</section> </section>
@@ -923,10 +936,10 @@ async function save() {
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Label class="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">示例命令</Label> <Label class="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">示例命令</Label>
<button class="text-[10px] text-primary hover:underline font-medium" @click="baihuCommandInput = 'baihu reposync --source-url \'https://github.com/example/repo.git\' --branch \'main\' --blacklist \'test|dev\''">填入示例</button> <button class="text-[10px] text-primary hover:underline font-medium" @click="baihuCommandInput = 'baihu reposync --source-url \'https://github.com/example/repo.git\' --branch \'main\' --blacklist \'test|dev\' --pre-command \'npm install\' --post-command \'echo done\''">填入示例</button>
</div> </div>
<div class="p-3 rounded-lg bg-muted/40 font-mono text-[11px] text-muted-foreground/70 border border-muted/20 leading-relaxed break-all"> <div class="p-3 rounded-lg bg-muted/40 font-mono text-[11px] text-muted-foreground/70 border border-muted/20 leading-relaxed break-all">
baihu reposync --source-url 'https://...' --branch 'main' --blacklist '...' baihu reposync --source-url 'https://...' --branch 'main' --blacklist '...' --pre-command '...' --post-command '...'
</div> </div>
</div> </div>
+11 -1
View File
@@ -241,6 +241,8 @@ watch(() => props.open, async (val: boolean) => {
random_range: props.task?.random_range ?? 0, random_range: props.task?.random_range ?? 0,
timeout: props.task?.timeout ?? 30, timeout: props.task?.timeout ?? 30,
pin_type: props.task?.pin_type ?? 'none', pin_type: props.task?.pin_type ?? 'none',
pre_command: props.task?.pre_command ?? '',
post_command: props.task?.post_command ?? '',
...props.task ...props.task
} }
// 解析清理配置 // 解析清理配置
@@ -576,9 +578,17 @@ async function save() {
</div> </div>
</template> </template>
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3"> <div 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-bold">执行命</Label> <Label class="sm:text-right text-xs text-foreground/70 uppercase tracking-wider font-bold">前置指</Label>
<div class="sm:col-span-3 relative"><Input v-model="form.pre_command" placeholder="执行主命令前运行的指令 (可选)" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50 pr-10', form.pre_command ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /><Zap class="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground opacity-40 pointer-events-none" /></div>
</div>
<div 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-bold">核心命令</Label>
<div class="sm:col-span-3 relative"><Input v-model="form.command" placeholder="例如: python main.py --args" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50 pr-10', form.command ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /><Terminal class="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground opacity-40 pointer-events-none" /></div> <div class="sm:col-span-3 relative"><Input v-model="form.command" placeholder="例如: python main.py --args" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50 pr-10', form.command ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /><Terminal class="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground opacity-40 pointer-events-none" /></div>
</div> </div>
<div 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-bold">后置指令</Label>
<div class="sm:col-span-3 relative"><Input v-model="form.post_command" placeholder="主命令执行后运行的指令 (可选)" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50 pr-10', form.post_command ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /><Zap class="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground opacity-40 pointer-events-none" /></div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3"> <div 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-bold">工作目录</Label> <Label class="sm:text-right text-xs text-foreground/70 uppercase tracking-wider font-bold">工作目录</Label>
<div class="sm:col-span-3"><DirTreeSelect v-if="selectedAgentId === 'local'" v-model="currentWorkDir" class="h-9" /><Input v-else v-model="currentWorkDir" placeholder="任务运行路径(留空取 Agent 默认值)" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50', currentWorkDir ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /></div> <div class="sm:col-span-3"><DirTreeSelect v-if="selectedAgentId === 'local'" v-model="currentWorkDir" class="h-9" /><Input v-else v-model="currentWorkDir" placeholder="任务运行路径(留空取 Agent 默认值)" :class="cn('h-9 bg-muted/20 border-muted-foreground/15 transition-all focus:bg-background/50', currentWorkDir ? 'font-mono text-sm tracking-tight font-medium' : 'text-[11px] font-normal')" /></div>