package controllers import ( "encoding/json" "path/filepath" "strings" "github.com/engigu/taskpool/internal/constant" "github.com/engigu/taskpool/internal/database" "github.com/engigu/taskpool/internal/logger" "github.com/engigu/taskpool/internal/models" "github.com/engigu/taskpool/internal/models/vo" "github.com/engigu/taskpool/internal/services" "github.com/engigu/taskpool/internal/services/tasks" "github.com/engigu/taskpool/internal/utils" "github.com/gin-gonic/gin" "os" ) type TaskController struct { taskService *tasks.TaskService executorService *tasks.ExecutorService agentWSManager *services.AgentWSManager } func NewTaskController(taskService *tasks.TaskService, executorService *tasks.ExecutorService) *TaskController { return &TaskController{ taskService: taskService, executorService: executorService, agentWSManager: services.GetAgentWSManager(), } } // resolveWorkDir 将相对路径转换为绝对路径 func resolveWorkDir(workDir string) string { if workDir == "" { // 空则使用默认 scripts 目录 absPath, err := filepath.Abs(constant.ScriptsWorkDir) if err != nil { return constant.ScriptsWorkDir } return absPath } // 如果已经是绝对路径,直接返回 if strings.HasPrefix(workDir, constant.ScriptsDirPlaceholder) { return workDir } if filepath.IsAbs(workDir) { return workDir } // 相对路径,基于 scripts 目录 fullPath := filepath.Join(constant.ScriptsWorkDir, workDir) absPath, err := filepath.Abs(fullPath) if err != nil { return fullPath } return absPath } // isValidDirName 校验目录名是否合法 func isValidDirName(dirName string) bool { if dirName == "." || strings.Contains(dirName, "/") || strings.Contains(dirName, "\\") || strings.Contains(dirName, "..") { return false } for _, ch := range dirName { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '.') { return false } } return true } // getRepoPhysicalPath 计算仓库任务的最终物理绝对路径 func getRepoPhysicalPath(targetPath, dirName, sourceURL, branch string) string { if dirName == "." { return "" // 如果不追加目录,此逻辑不负责判断其根目录(共享的 scripts 目录) } finalDirName := dirName if finalDirName == "" { finalDirName = utils.GetRepoIdentifier(sourceURL, branch) } if finalDirName == "" { return "" } basePath := targetPath if basePath == "" || basePath == constant.ScriptsDirPlaceholder { basePath = constant.ScriptsWorkDir } else if strings.HasPrefix(basePath, constant.ScriptsDirPlaceholder) { basePath = filepath.Join(constant.ScriptsWorkDir, strings.TrimPrefix(basePath, constant.ScriptsDirPlaceholder)) } else if !filepath.IsAbs(basePath) { basePath = filepath.Join(constant.ScriptsWorkDir, basePath) } fullPath := filepath.Join(basePath, finalDirName) absPath, err := filepath.Abs(fullPath) if err != nil { return "" } return absPath } // CreateTask 创建任务 // @Summary 创建任务 // @Description 创建一个新的任务 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Param body body vo.TaskCreateReq true "任务创建信息" // @Success 200 {object} utils.Response{data=vo.TaskVO} // @Failure 400 {object} utils.Response // @Router /tasks [post] func (tc *TaskController) CreateTask(c *gin.Context) { var req vo.TaskCreateReq if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, err.Error()) return } // 普通任务需要命令 if req.Type != constant.TaskTypeRepo && req.Command == "" { utils.BadRequest(c, "命令不能为空") return } if req.Schedule != "" { if err := tc.executorService.ValidateCron(req.Schedule); err != nil { utils.BadRequest(c, "无效的cron表达式: "+err.Error()) return } } // 转换为绝对路径(Agent 任务保持原样) workDir := req.WorkDir if req.AgentID == nil || *req.AgentID == "" { workDir = resolveWorkDir(req.WorkDir) } var sourceID string // 如果是仓库同步任务,根据 URL 生成 SourceID 用于去重 if req.Type == constant.TaskTypeRepo && req.Config != "" { var repoCfg struct { SourceURL string `json:"source_url"` Branch string `json:"branch"` RepoDirName string `json:"repo_dir_name"` TargetPath string `json:"target_path"` } if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { if repoCfg.RepoDirName != "" { if !isValidDirName(repoCfg.RepoDirName) { utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,不能只有点,且不能包含路径逻辑") return } } // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame if repoCfg.RepoDirName != "" { sourceID = "repo_" + repoCfg.RepoDirName } else { sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) } // 校验 SourceID 是否已存在(任务唯一性) existingTask := tc.taskService.GetTaskBySourceID(sourceID) if existingTask != nil { utils.BadRequest(c, "当前任务已存在,请检查或更换仓库目录名称") return } // 校验物理目录是否存在 newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.RepoDirName, repoCfg.SourceURL, repoCfg.Branch) if newAbsPath != "" { if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() { utils.BadRequest(c, "本地已存在同名仓库文件夹,请更换自定义目录名或清理残留文件") return } } } } param := tasks.TaskParam{ Name: req.Name, Remark: req.Remark, Command: req.Command, PreCommand: req.PreCommand, PostCommand: req.PostCommand, Tags: req.Tags, Type: req.Type, Config: req.Config, Schedule: req.Schedule, Timeout: req.Timeout, WorkDir: workDir, CleanConfig: req.CleanConfig, Envs: req.Envs, Languages: req.Languages, AgentID: req.AgentID, TriggerType: req.TriggerType, RetryCount: req.RetryCount, RetryInterval: req.RetryInterval, RandomRange: req.RandomRange, SourceID: sourceID, PinType: req.PinType, Enabled: true, } var task *models.Task // 去重逻辑:如果已存在相同 SourceID 的仓库任务,则改为更新 if sourceID != "" { task = tc.taskService.GetTaskBySourceID(sourceID) if task != nil { task = tc.taskService.UpdateTask(task.ID, ¶m) } } if task == nil { task = tc.taskService.CreateTask(¶m) } // 如果是 Agent 任务,通知 Agent;否则添加到本地 cron if task.AgentID != nil && *task.AgentID != "" { tc.agentWSManager.BroadcastTasks(*task.AgentID) } else { tc.executorService.AddCronTask(task) } utils.Success(c, vo.ToTaskVO(task)) } // BulkSaveTask 批量保存/导入任务配置(用于主节点下发同步) // @Summary 批量保存任务 // @Description 批量导入任务配置,如果ID或同名存在则更新,不存在则创建 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Router /tasks/bulk_save [post] func (tc *TaskController) BulkSaveTask(c *gin.Context) { var reqs []vo.TaskVO if err := c.ShouldBindJSON(&reqs); err != nil { utils.BadRequest(c, err.Error()) return } for _, req := range reqs { param := tasks.TaskParam{ Name: req.Name, Remark: req.Remark, Command: req.Command, PreCommand: req.PreCommand, PostCommand: req.PostCommand, Tags: req.Tags, Type: req.Type, Config: req.Config, Schedule: req.Schedule, Timeout: req.Timeout, WorkDir: req.WorkDir, CleanConfig: req.CleanConfig, Envs: req.Envs, Languages: req.Languages, AgentID: req.AgentID, TriggerType: req.TriggerType, RetryCount: req.RetryCount, RetryInterval: req.RetryInterval, RandomRange: req.RandomRange, PinType: req.PinType, Enabled: req.Enabled, SourceID: "", // 不直接覆盖 } var existingTask *models.Task // 优先按 ID 匹配 if req.ID != "" { existingTask = tc.taskService.GetTaskByID(req.ID) } // 如果 ID 没找到,尝试按 Name 匹配 if existingTask == nil { var t models.Task res := database.DB.Where("name = ?", req.Name).First(&t) if res.Error == nil { existingTask = &t } } var savedTask *models.Task if existingTask != nil { savedTask = tc.taskService.UpdateTask(existingTask.ID, ¶m) } else { savedTask = tc.taskService.CreateTask(¶m) // 如果原始有 ID,强制覆盖更新 ID 保持强同步一致性 if req.ID != "" && savedTask != nil { database.DB.Model(savedTask).Update("id", req.ID) savedTask.ID = req.ID } } // 如果是 Agent 任务,通知 Agent;否则添加到本地 cron if savedTask != nil { if savedTask.AgentID != nil && *savedTask.AgentID != "" { tc.agentWSManager.BroadcastTasks(*savedTask.AgentID) } else { tc.executorService.AddCronTask(savedTask) } } } utils.Success(c, nil) } // GetTasks 获取任务列表 // @Summary 获取任务列表 // @Description 分页获取任务列表,支持按名称、Agent ID、标签、类型筛选 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Param name query string false "任务名称" // @Param agent_id query string false "Agent ID" // @Param tags query string false "标签" // @Param type query string false "任务类型" // @Param page query int false "页码" // @Param page_size query int false "每页数量" // @Success 200 {object} utils.Response{data=utils.PaginationData{data=[]vo.TaskVO}} // @Router /tasks [get] func (tc *TaskController) GetTasks(c *gin.Context) { p := utils.ParsePagination(c) name := c.DefaultQuery("name", "") agentIDStr := c.DefaultQuery("agent_id", "") tags := c.DefaultQuery("tags", "") taskType := c.DefaultQuery("type", "") var agentID *string if agentIDStr != "" { agentID = &agentIDStr } sortBy := c.DefaultQuery("sort_by", "") order := c.DefaultQuery("order", "") tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name, agentID, tags, taskType, sortBy, order) utils.PaginatedResponse(c, vo.ToTaskVOListFromModels(tasks), total, p) } // GetTask 获取任务详情 // @Summary 获取任务详情 // @Description 根据 ID 获取任务详情 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "任务ID" // @Success 200 {object} utils.Response{data=vo.TaskVO} // @Failure 404 {object} utils.Response // @Router /tasks/{id} [get] func (tc *TaskController) GetTask(c *gin.Context) { id := c.Param("id") if id == "" { utils.BadRequest(c, "无效的任务ID") return } task := tc.taskService.GetTaskByID(id) if task == nil { utils.NotFound(c, "任务不存在") return } utils.Success(c, vo.ToTaskVO(task)) } // UpdateTask 更新任务 // @Summary 更新任务 // @Description 根据 ID 更新任务信息 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "任务ID" // @Param body body vo.TaskUpdateReq true "任务更新信息" // @Success 200 {object} utils.Response{data=vo.TaskVO} // @Failure 404 {object} utils.Response // @Router /tasks/{id} [put] func (tc *TaskController) UpdateTask(c *gin.Context) { id := c.Param("id") if id == "" { utils.BadRequest(c, "无效的任务ID") return } // 获取旧任务信息(用于判断 agent 变更) oldTask := tc.taskService.GetTaskByID(id) var oldAgentID *string if oldTask != nil { oldAgentID = oldTask.AgentID } var req vo.TaskUpdateReq if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, err.Error()) return } if req.Schedule != "" { if err := tc.executorService.ValidateCron(req.Schedule); err != nil { utils.BadRequest(c, "无效的cron表达式: "+err.Error()) return } } // 转换为绝对路径(Agent 任务保持原样) workDir := req.WorkDir if req.AgentID == nil || *req.AgentID == "" { workDir = resolveWorkDir(req.WorkDir) } var sourceID string if req.Type == constant.TaskTypeRepo && req.Config != "" { var repoCfg struct { SourceURL string `json:"source_url"` Branch string `json:"branch"` RepoDirName string `json:"repo_dir_name"` TargetPath string `json:"target_path"` } if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { if repoCfg.RepoDirName != "" { if !isValidDirName(repoCfg.RepoDirName) { utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,不能只有点,且不能包含路径逻辑") return } } // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame if repoCfg.RepoDirName != "" { sourceID = "repo_" + repoCfg.RepoDirName } else { sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) } // 验证更新后的 SourceID 是否和别的任务冲突 if sourceID != oldTask.SourceID { existingTask := tc.taskService.GetTaskBySourceID(sourceID) if existingTask != nil && existingTask.ID != oldTask.ID { utils.BadRequest(c, "当前任务已存在,请检查或更换仓库目录名称") return } } // 计算新的物理路径 newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.RepoDirName, repoCfg.SourceURL, repoCfg.Branch) var oldAbsPath string if oldTask != nil && oldTask.Type == constant.TaskTypeRepo && oldTask.Config != "" { var oldCfg struct { SourceURL string `json:"source_url"` Branch string `json:"branch"` RepoDirName string `json:"repo_dir_name"` TargetPath string `json:"target_path"` } if json.Unmarshal([]byte(oldTask.Config), &oldCfg) == nil { oldAbsPath = getRepoPhysicalPath(oldCfg.TargetPath, oldCfg.RepoDirName, oldCfg.SourceURL, oldCfg.Branch) } } // 如果路径发生了改变(或者是个全新计算的路径),并且新路径已存在,则报错拦截 if newAbsPath != "" && newAbsPath != oldAbsPath { if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() { utils.BadRequest(c, "目标目录在本地已存在同名文件夹,请更换目录名或清理残留文件") return } } } } else if oldTask != nil { sourceID = oldTask.SourceID } param := tasks.TaskParam{ Name: req.Name, Remark: req.Remark, Command: req.Command, PreCommand: req.PreCommand, PostCommand: req.PostCommand, Tags: req.Tags, Type: req.Type, Config: req.Config, Schedule: req.Schedule, Timeout: req.Timeout, WorkDir: workDir, CleanConfig: req.CleanConfig, Envs: req.Envs, Languages: req.Languages, AgentID: req.AgentID, TriggerType: req.TriggerType, RetryCount: req.RetryCount, RetryInterval: req.RetryInterval, RandomRange: req.RandomRange, SourceID: sourceID, PinType: req.PinType, Enabled: req.Enabled, } task := tc.taskService.UpdateTask(id, ¶m) if task == nil { utils.NotFound(c, "任务不存在") return } // 处理任务调度 if task.AgentID != nil && *task.AgentID != "" { // Agent 任务:从本地 cron 移除,通知 Agent tc.executorService.RemoveCronTask(task.ID) tc.agentWSManager.BroadcastTasks(*task.AgentID) // 如果 agent 变更了,也通知旧 agent if oldAgentID != nil && *oldAgentID != "" && *oldAgentID != *task.AgentID { tc.agentWSManager.BroadcastTasks(*oldAgentID) } } else { // 本地任务 if utils.DerefBool(task.Enabled, true) { tc.executorService.AddCronTask(task) } else { tc.executorService.RemoveCronTask(task.ID) } // 如果之前是 agent 任务,通知旧 agent 移除 if oldAgentID != nil && *oldAgentID != "" { tc.agentWSManager.BroadcastTasks(*oldAgentID) } } utils.Success(c, vo.ToTaskVO(task)) } // DeleteTask 删除任务 // @Summary 删除任务 // @Description 根据 ID 删除任务 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "任务ID" // @Success 200 {object} utils.Response // @Failure 404 {object} utils.Response // @Router /tasks/{id} [delete] func (tc *TaskController) DeleteTask(c *gin.Context) { id := c.Param("id") if id == "" { utils.BadRequest(c, "无效的任务ID") return } // 获取任务信息(用于通知 agent 和物理删除校验) task := tc.taskService.GetTaskByID(id) if task == nil { utils.NotFound(c, "任务不存在") return } agentID := task.AgentID deleteFiles := c.Query("delete_files") == "true" // 如果需要删除物理文件且是仓库任务 if deleteFiles && task.Type == constant.TaskTypeRepo { tc.deleteRepoPhysicalFiles(task) } tc.executorService.RemoveCronTask(id) tc.executorService.GetScheduler().StopTask(id) success := tc.taskService.DeleteTask(id) if !success { utils.NotFound(c, "任务不存在") return } // 如果是 agent 任务,通知 agent if agentID != nil && *agentID != "" { tc.agentWSManager.BroadcastTasks(*agentID) } utils.SuccessMsg(c, "删除成功") } // deleteRepoPhysicalFiles 删除仓库关联的物理文件 func (tc *TaskController) deleteRepoPhysicalFiles(task *models.Task) { if task.Type != constant.TaskTypeRepo { return } logger.Infof("[Controller] 开始尝试物理删除任务关联文件: %s", task.Name) var repoCfg models.RepoConfig if err := json.Unmarshal([]byte(task.Config), &repoCfg); err != nil { logger.Errorf("[Controller] 解析任务配置失败: %v", err) return } targetPath := repoCfg.TargetPath if targetPath == "" { // 如果 TargetPath 为空,调用系统的计算函数获取默认目录名 repoId := utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) if repoId != "" { targetPath = repoId logger.Infof("[Controller] TargetPath 为空,使用计算出的标识符: %s", targetPath) } } if targetPath == "" || targetPath == constant.ScriptsDirPlaceholder { logger.Warnf("[Controller] 任务 %s 无法确定有效的物理删除路径,跳过", task.Name) return } // 确定绝对路径 scriptsDir, _ := filepath.Abs(constant.ScriptsWorkDir) fullPath := targetPath if strings.HasPrefix(targetPath, constant.ScriptsDirPlaceholder) { fullPath = filepath.Join(scriptsDir, strings.TrimPrefix(targetPath, constant.ScriptsDirPlaceholder)) } else if !filepath.IsAbs(targetPath) { fullPath = filepath.Join(scriptsDir, targetPath) } absTargetPath, _ := filepath.Abs(fullPath) logger.Infof("[Controller] 最终计算的绝对路径: %s, Scripts目录: %s", absTargetPath, scriptsDir) scriptsDir, _ = filepath.Abs(constant.ScriptsWorkDir) // 安全检查:使用 Rel 判断路径关系 rel, err := filepath.Rel(scriptsDir, absTargetPath) if err != nil { logger.Errorf("[Controller] 计算相对路径失败: %v", err) return } // 必须是在 scripts 目录下(不以 .. 开头)且不能是 scripts 目录本身 (.) if rel != "." && !strings.HasPrefix(rel, "..") { err := os.RemoveAll(absTargetPath) if err != nil { logger.Errorf("[Controller] 物理删除文件夹失败: %s, 路径: %s, 错误: %v", task.Name, absTargetPath, err) } else { logger.Infof("[Controller] 已成功物理删除文件夹: %s, 路径: %s", task.Name, absTargetPath) } } else { logger.Warnf("[Controller] 拒绝物理删除安全目录之外的路径: %s", absTargetPath) } } func (tc *TaskController) BatchDeleteTasks(c *gin.Context) { var req struct { IDs []string `json:"ids" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, err.Error()) return } // 收集涉及到的 AgentID agentIDs := make(map[string]struct{}) for _, id := range req.IDs { // 获取任务信息 task := tc.taskService.GetTaskByID(id) if task != nil { if task.AgentID != nil && *task.AgentID != "" { agentIDs[*task.AgentID] = struct{}{} } } // 移除 cron 调度 tc.executorService.RemoveCronTask(id) tc.executorService.GetScheduler().StopTask(id) } // 执行批量删除 count := tc.taskService.BatchDeleteTasks(req.IDs) // 通知受影响的 Agent for agentID := range agentIDs { tc.agentWSManager.BroadcastTasks(agentID) } utils.Success(c, gin.H{"count": count}) } // BatchDeleteByQuery 根据查询条件批量删除任务 // @Summary 根据查询条件批量删除任务 // @Description 根据查询条件批量删除匹配的所有任务 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Param name query string false "任务名称关键词" // @Param tags query string false "标签关键词" // @Param type query string false "任务类型" // @Param agent_id query string false "执行位置(节点ID)" // @Success 200 {object} utils.Response{data=map[string]int} // @Failure 401 {object} utils.Response "未授权" // @Router /tasks/batch-by-query [delete] func (tc *TaskController) BatchDeleteByQuery(c *gin.Context) { name := c.Query("name") agentIDStr := c.Query("agent_id") tags := c.Query("tags") taskType := c.Query("type") var agentID *string if agentIDStr != "" { agentID = &agentIDStr } tasks, _ := tc.taskService.GetTasksWithPagination(1, 999999, name, agentID, tags, taskType, "", "") if len(tasks) == 0 { utils.Success(c, gin.H{"count": 0}) return } var ids []string agentIDs := make(map[string]struct{}) for _, task := range tasks { ids = append(ids, task.ID) if task.AgentID != nil && *task.AgentID != "" { agentIDs[*task.AgentID] = struct{}{} } // 移除 cron 调度 tc.executorService.RemoveCronTask(task.ID) tc.executorService.GetScheduler().StopTask(task.ID) } // 执行批量删除 count := tc.taskService.BatchDeleteTasks(ids) // 通知受影响的 Agent for aID := range agentIDs { tc.agentWSManager.BroadcastTasks(aID) } utils.Success(c, gin.H{"count": count}) } // StopTask 停止任务 // @Summary 停止任务 // @Description 根据运行日志 ID 停止正在执行的任务 // @Tags 任务管理 // @Accept json // @Produce json // @Security BearerAuth // @Param logID path string true "运行日志ID" // @Success 200 {object} utils.Response // @Failure 400 {object} utils.Response // @Router /tasks/stop/{logID} [post] func (tc *TaskController) StopTask(c *gin.Context) { logID := c.Param("logID") if logID == "" { utils.BadRequest(c, "无效的日志ID") return } err := tc.executorService.StopTaskExecution(logID) if err != nil { utils.BadRequest(c, err.Error()) return } utils.SuccessMsg(c, "停止请求已发送") } // GetTags 获取所有任务标签 // @Summary 获取所有任务标签 // @Description 获取系统中所有任务已使用的唯一标签列表 // @Tags 任务管理 // @Produce json // @Security BearerAuth // @Success 200 {object} utils.Response{data=[]string} // @Router /tasks/tags [get] func (tc *TaskController) GetTags(c *gin.Context) { tags, err := tc.taskService.GetAllTags() if err != nil { utils.ServerError(c, err.Error()) return } utils.Success(c, tags) } // SyncRepoTasks 增量同步仓库任务状态(供本地 reposync 进程调用) func (tc *TaskController) SyncRepoTasks(c *gin.Context) { var req struct { RepoID string `json:"repo_id"` UpsertedIDs []string `json:"upserted_ids"` DeletedIDs []string `json:"deleted_ids"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, err.Error()) return } tc.executorService.SyncRepoTasks(req.UpsertedIDs, req.DeletedIDs) utils.SuccessMsg(c, "增量同步成功") } // ToggleTask 切换任务启用/禁用状态 func (tc *TaskController) ToggleTask(c *gin.Context) { id := c.Param("id") if id == "" { utils.BadRequest(c, "无效的任务ID") return } var req struct { Enabled bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, err.Error()) return } task := tc.taskService.GetTaskByID(id) if task == nil { utils.NotFound(c, "任务不存在") return } // 获取旧 AgentID var oldAgentID *string oldAgentID = task.AgentID // 构造更新参数,仅修改 Enabled param := tasks.TaskParam{ Name: task.Name, Remark: task.Remark, Command: string(task.Command), PreCommand: string(task.PreCommand), PostCommand: string(task.PostCommand), Tags: task.Tags, Type: task.Type, Config: string(task.Config), Schedule: task.Schedule, Timeout: task.Timeout, WorkDir: task.WorkDir, CleanConfig: task.CleanConfig, Envs: string(task.Envs), Languages: task.Languages, AgentID: task.AgentID, TriggerType: task.TriggerType, RetryCount: task.RetryCount, RetryInterval: task.RetryInterval, RandomRange: task.RandomRange, SourceID: task.SourceID, PinType: task.PinType, Enabled: req.Enabled, } updatedTask := tc.taskService.UpdateTask(id, ¶m) if updatedTask == nil { utils.NotFound(c, "任务不存在") return } // 处理调度器更新 if updatedTask.AgentID != nil && *updatedTask.AgentID != "" { tc.executorService.RemoveCronTask(updatedTask.ID) tc.agentWSManager.BroadcastTasks(*updatedTask.AgentID) } else { if req.Enabled { tc.executorService.AddCronTask(updatedTask) } else { tc.executorService.RemoveCronTask(updatedTask.ID) } if oldAgentID != nil && *oldAgentID != "" { tc.agentWSManager.BroadcastTasks(*oldAgentID) } } utils.Success(c, vo.ToTaskVO(updatedTask)) }