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"`
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 {
+61 -33
View File
@@ -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) {
}
}
+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
}
+2
View File
@@ -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
+6
View File
@@ -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
}
}
+17 -4
View File
@@ -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() {
<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>
<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">
<DialogTitle class="text-xl font-bold whitespace-nowrap">
{{ isEdit ? '编辑仓库同步' : '新建仓库同步' }}
</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">
<Terminal class="w-3.5 h-3.5" />
<span class="text-xs">Baihu 命令导入</span>
@@ -573,6 +577,15 @@ async function save() {
</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>
</section>
@@ -923,10 +936,10 @@ async function save() {
<div class="space-y-2">
<div class="flex items-center justify-between">
<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 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>
+11 -1
View File
@@ -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() {
</div>
</template>
<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>
<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">
<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>