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
+7 -3
View File
@@ -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
+3 -1
View File
@@ -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,
+19 -2
View File
@@ -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 {
+7 -3
View File
@@ -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,
+15 -5
View File
@@ -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
}
+10
View File
@@ -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
}
+4
View File
@@ -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,
+21 -6
View File
@@ -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,
+29 -5
View File
@@ -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)
+7 -2
View File
@@ -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
+22 -1
View File
@@ -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
}