From 40e37b797c3669e31c68066f9ad4c7bc596aa0f0 Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Thu, 30 Apr 2026 16:08:22 +0800 Subject: [PATCH] chore: try to add stop task more info #75 --- internal/services/agent_ws_service.go | 8 ++++ internal/services/notification_service.go | 7 +-- internal/services/tasks/executor_service.go | 48 ++++++++++++++++++--- web/src/api/index.ts | 16 ++++--- web/src/views/history/LogViewer.vue | 2 + web/src/views/tasks/Tasks.vue | 32 +++++++++++--- 6 files changed, 91 insertions(+), 22 deletions(-) diff --git a/internal/services/agent_ws_service.go b/internal/services/agent_ws_service.go index 5196ea9..ba5327c 100644 --- a/internal/services/agent_ws_service.go +++ b/internal/services/agent_ws_service.go @@ -194,6 +194,14 @@ func (m *AgentWSManager) GetConnection(agentID string) *AgentConnection { return m.connections[agentID] } +// IsAgentOnline 检查指定 Agent 是否在线 +func (m *AgentWSManager) IsAgentOnline(agentID string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + _, exists := m.connections[agentID] + return exists +} + // SendToAgent 发送消息给指定 Agent func (m *AgentWSManager) SendToAgent(agentID string, msgType string, data interface{}) error { conn := m.GetConnection(agentID) diff --git a/internal/services/notification_service.go b/internal/services/notification_service.go index 9cf898e..e75817a 100644 --- a/internal/services/notification_service.go +++ b/internal/services/notification_service.go @@ -414,13 +414,14 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler { tmplTextKey = constant.KeyNotifyTemplatePasswordChangedText case constant.EventTaskSuccess, constant.EventTaskFailed, constant.EventTaskTimeout: - if e.Type == constant.EventTaskSuccess { + switch e.Type { + case constant.EventTaskSuccess: tmplTitleKey = constant.KeyNotifyTemplateTaskSuccessTitle tmplTextKey = constant.KeyNotifyTemplateTaskSuccessText - } else if e.Type == constant.EventTaskFailed { + case constant.EventTaskFailed: tmplTitleKey = constant.KeyNotifyTemplateTaskFailedTitle tmplTextKey = constant.KeyNotifyTemplateTaskFailedText - } else { + case constant.EventTaskTimeout: tmplTitleKey = constant.KeyNotifyTemplateTaskTimeoutTitle tmplTextKey = constant.KeyNotifyTemplateTaskTimeoutText } diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index b09e9ca..c599299 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -27,6 +27,7 @@ type AgentWSManager interface { RegisterRemoteWaiter(logID string) chan *models.AgentTaskResult UnregisterRemoteWaiter(logID string) SendToAgent(agentID string, msgType string, data interface{}) error + IsAgentOnline(agentID string) bool } // SettingsService 接口定义(避免循环依赖) @@ -633,33 +634,66 @@ func (es *ExecutorService) StopTaskExecution(logID string) error { var taskLog models.TaskLog res := database.DB.Where("id = ?", logID).Limit(1).Find(&taskLog) if res.Error != nil || res.RowsAffected == 0 { - return fmt.Errorf("日志不存在") + return fmt.Errorf("停止失败:找不到指定的执行记录 (LogID: %s)", logID) } + // 1. 状态预校验 if taskLog.Status != constant.TaskStatusRunning { - return fmt.Errorf("任务已结束") + statusText := "已结束" + switch taskLog.Status { + case constant.TaskStatusSuccess: + statusText = "执行成功" + case constant.TaskStatusFailed: + statusText = "执行失败" + case constant.TaskStatusTimeout: + statusText = "执行超时" + case constant.TaskStatusCancelled: + statusText = "已取消" + } + return fmt.Errorf("操作无效:任务当前状态为 [%s],无需停止", statusText) } task := es.taskService.GetTaskByID(taskLog.TaskID) if task == nil { - return fmt.Errorf("任务不存在") + return fmt.Errorf("停止失败:关联的任务信息已丢失") } - // 远程任务:发送停止指令到 Agent + // 2. 远程任务逻辑 if task.AgentID != nil && *task.AgentID != "" { + // 校验 Agent 是否在线 + if !es.agentWSManager.IsAgentOnline(*task.AgentID) { + return fmt.Errorf("停止失败:目标 Agent (%s) 当前离线,无法下发指令", *task.AgentID) + } + logger.Infof("[Executor] 请求停止远程任务 #%s (Agent #%s, LogID: %s)", task.ID, *task.AgentID, logID) - return es.agentWSManager.SendToAgent(*task.AgentID, constant.WSTypeStop, map[string]interface{}{ + err := es.agentWSManager.SendToAgent(*task.AgentID, constant.WSTypeStop, map[string]interface{}{ "log_id": logID, }) + if err != nil { + return fmt.Errorf("下发停止指令失败: %v", err) + } + return nil } - // 本地任务:直接停止调度器中的执行实例 + // 3. 本地任务逻辑 logger.Infof("[Executor] 请求停止本地任务 #%s (LogID: %s)", task.ID, logID) if es.scheduler.StopLog(logID) { return nil } - return fmt.Errorf("任务当前不在运行队列中或已完成") + // 4. 容错处理:如果调度器中没有句柄,但数据库状态还是 running + // 这通常发生在程序异常重启后,需要手动清理掉这个“僵尸状态” + taskLog.Status = constant.TaskStatusFailed + errorMessage := "任务执行实例已丢失(可能由于系统重启导致),已自动同步状态为失败" + taskLog.Error = models.BigText(errorMessage) + + // 更新数据库状态 + database.DB.Model(&taskLog).Updates(map[string]interface{}{ + "status": taskLog.Status, + "error": taskLog.Error, + }) + + return fmt.Errorf("停止失败:%s", errorMessage) } // GetRunningCount 获取正在运行任务数量 diff --git a/web/src/api/index.ts b/web/src/api/index.ts index c54ea57..3faa2a7 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -387,12 +387,16 @@ export interface RepoConfig { } export interface ExecutionResult { - TaskID: string - Success: boolean - Output: string - Error: string - Start: string - End: string + task_id: string + log_id?: string + success: boolean + status?: string + output?: string + error?: string + duration?: number + exit_code?: number + start_time?: string + end_time?: string } export interface TaskListResponse { diff --git a/web/src/views/history/LogViewer.vue b/web/src/views/history/LogViewer.vue index e0d59cd..8b27920 100644 --- a/web/src/views/history/LogViewer.vue +++ b/web/src/views/history/LogViewer.vue @@ -18,6 +18,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ 'update:open': [value: boolean] + 'stop': [] }>() const isFullscreen = ref(false) @@ -74,6 +75,7 @@ onUnmounted(() => { :empty-description="emptyDescription" @close="close" @maximize="isFullscreen = !isFullscreen" + @stop="$emit('stop')" /> diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index 334ff64..bb3f91a 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -205,23 +205,41 @@ async function deleteTask() { } const executingTaskId = ref(null) +const isStopping = ref(false) async function runTask(id: string) { + if (executingTaskId.value) return executingTaskId.value = id - toast.message('正在执行...', { id: 'executing' }) try { const res = await api.tasks.execute(id) - if (res.Success === false) { - throw new Error(res.Error || '执行失败') + toast.success('执行指令已发送') + if (res.log_id) { + // 开启日志查看器 + viewLogs(id) } - toast.success('触发成功', { id: 'executing' }) } catch (error: any) { - toast.error(error?.message || '执行失败', { id: 'executing' }) + toast.error(error.message || '执行失败') } finally { executingTaskId.value = null } } +async function handleStopTask() { + if (!selectedLog.value || isStopping.value) return + + isStopping.value = true + try { + await api.tasks.stop(selectedLog.value.id) + toast.success('停止指令已发送') + } catch (error: any) { + toast.error(error.message || '停止失败') + // 出错时也尝试刷新,因为后端可能已经自动修正了“僵尸”状态 + loadTasks() + } finally { + isStopping.value = false + } +} + async function toggleTask(task: Task, enabled: boolean) { try { await api.tasks.update(task.id, { ...task, enabled }) @@ -830,8 +848,10 @@ watch(() => route.query.agent_id, (newVal: any) => { variant="full" :log="selectedLog" :content="displayLogContent" + :is-stopping="isStopping" :empty-title="logEmptyTitle" - :empty-description="logEmptyDesc" /> + :empty-description="logEmptyDesc" + @stop="handleStopTask" />