chore: try to add stop task more info #75

This commit is contained in:
duorameng
2026-04-30 16:08:22 +08:00
parent cfdf186b93
commit 40e37b797c
6 changed files with 91 additions and 22 deletions
+8
View File
@@ -194,6 +194,14 @@ func (m *AgentWSManager) GetConnection(agentID string) *AgentConnection {
return m.connections[agentID] 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 // SendToAgent 发送消息给指定 Agent
func (m *AgentWSManager) SendToAgent(agentID string, msgType string, data interface{}) error { func (m *AgentWSManager) SendToAgent(agentID string, msgType string, data interface{}) error {
conn := m.GetConnection(agentID) conn := m.GetConnection(agentID)
+4 -3
View File
@@ -414,13 +414,14 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler {
tmplTextKey = constant.KeyNotifyTemplatePasswordChangedText tmplTextKey = constant.KeyNotifyTemplatePasswordChangedText
case constant.EventTaskSuccess, constant.EventTaskFailed, constant.EventTaskTimeout: case constant.EventTaskSuccess, constant.EventTaskFailed, constant.EventTaskTimeout:
if e.Type == constant.EventTaskSuccess { switch e.Type {
case constant.EventTaskSuccess:
tmplTitleKey = constant.KeyNotifyTemplateTaskSuccessTitle tmplTitleKey = constant.KeyNotifyTemplateTaskSuccessTitle
tmplTextKey = constant.KeyNotifyTemplateTaskSuccessText tmplTextKey = constant.KeyNotifyTemplateTaskSuccessText
} else if e.Type == constant.EventTaskFailed { case constant.EventTaskFailed:
tmplTitleKey = constant.KeyNotifyTemplateTaskFailedTitle tmplTitleKey = constant.KeyNotifyTemplateTaskFailedTitle
tmplTextKey = constant.KeyNotifyTemplateTaskFailedText tmplTextKey = constant.KeyNotifyTemplateTaskFailedText
} else { case constant.EventTaskTimeout:
tmplTitleKey = constant.KeyNotifyTemplateTaskTimeoutTitle tmplTitleKey = constant.KeyNotifyTemplateTaskTimeoutTitle
tmplTextKey = constant.KeyNotifyTemplateTaskTimeoutText tmplTextKey = constant.KeyNotifyTemplateTaskTimeoutText
} }
+41 -7
View File
@@ -27,6 +27,7 @@ type AgentWSManager interface {
RegisterRemoteWaiter(logID string) chan *models.AgentTaskResult RegisterRemoteWaiter(logID string) chan *models.AgentTaskResult
UnregisterRemoteWaiter(logID string) UnregisterRemoteWaiter(logID string)
SendToAgent(agentID string, msgType string, data interface{}) error SendToAgent(agentID string, msgType string, data interface{}) error
IsAgentOnline(agentID string) bool
} }
// SettingsService 接口定义(避免循环依赖) // SettingsService 接口定义(避免循环依赖)
@@ -633,33 +634,66 @@ func (es *ExecutorService) StopTaskExecution(logID string) error {
var taskLog models.TaskLog var taskLog models.TaskLog
res := database.DB.Where("id = ?", logID).Limit(1).Find(&taskLog) res := database.DB.Where("id = ?", logID).Limit(1).Find(&taskLog)
if res.Error != nil || res.RowsAffected == 0 { if res.Error != nil || res.RowsAffected == 0 {
return fmt.Errorf("日志不存在") return fmt.Errorf("停止失败:找不到指定的执行记录 (LogID: %s)", logID)
} }
// 1. 状态预校验
if taskLog.Status != constant.TaskStatusRunning { 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) task := es.taskService.GetTaskByID(taskLog.TaskID)
if task == nil { if task == nil {
return fmt.Errorf("任务不存在") return fmt.Errorf("停止失败:关联的任务信息已丢失")
} }
// 远程任务:发送停止指令到 Agent // 2. 远程任务逻辑
if task.AgentID != nil && *task.AgentID != "" { 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) 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, "log_id": logID,
}) })
if err != nil {
return fmt.Errorf("下发停止指令失败: %v", err)
}
return nil
} }
// 本地任务:直接停止调度器中的执行实例 // 3. 本地任务逻辑
logger.Infof("[Executor] 请求停止本地任务 #%s (LogID: %s)", task.ID, logID) logger.Infof("[Executor] 请求停止本地任务 #%s (LogID: %s)", task.ID, logID)
if es.scheduler.StopLog(logID) { if es.scheduler.StopLog(logID) {
return nil 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 获取正在运行任务数量 // GetRunningCount 获取正在运行任务数量
+10 -6
View File
@@ -387,12 +387,16 @@ export interface RepoConfig {
} }
export interface ExecutionResult { export interface ExecutionResult {
TaskID: string task_id: string
Success: boolean log_id?: string
Output: string success: boolean
Error: string status?: string
Start: string output?: string
End: string error?: string
duration?: number
exit_code?: number
start_time?: string
end_time?: string
} }
export interface TaskListResponse { export interface TaskListResponse {
+2
View File
@@ -18,6 +18,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
'update:open': [value: boolean] 'update:open': [value: boolean]
'stop': []
}>() }>()
const isFullscreen = ref(false) const isFullscreen = ref(false)
@@ -74,6 +75,7 @@ onUnmounted(() => {
:empty-description="emptyDescription" :empty-description="emptyDescription"
@close="close" @close="close"
@maximize="isFullscreen = !isFullscreen" @maximize="isFullscreen = !isFullscreen"
@stop="$emit('stop')"
/> />
</div> </div>
</div> </div>
+26 -6
View File
@@ -205,23 +205,41 @@ async function deleteTask() {
} }
const executingTaskId = ref<string | null>(null) const executingTaskId = ref<string | null>(null)
const isStopping = ref(false)
async function runTask(id: string) { async function runTask(id: string) {
if (executingTaskId.value) return
executingTaskId.value = id executingTaskId.value = id
toast.message('正在执行...', { id: 'executing' })
try { try {
const res = await api.tasks.execute(id) const res = await api.tasks.execute(id)
if (res.Success === false) { toast.success('执行指令已发送')
throw new Error(res.Error || '执行失败') if (res.log_id) {
// 开启日志查看器
viewLogs(id)
} }
toast.success('触发成功', { id: 'executing' })
} catch (error: any) { } catch (error: any) {
toast.error(error?.message || '执行失败', { id: 'executing' }) toast.error(error.message || '执行失败')
} finally { } finally {
executingTaskId.value = null 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) { async function toggleTask(task: Task, enabled: boolean) {
try { try {
await api.tasks.update(task.id, { ...task, enabled }) await api.tasks.update(task.id, { ...task, enabled })
@@ -830,8 +848,10 @@ watch(() => route.query.agent_id, (newVal: any) => {
variant="full" variant="full"
:log="selectedLog" :log="selectedLog"
:content="displayLogContent" :content="displayLogContent"
:is-stopping="isStopping"
:empty-title="logEmptyTitle" :empty-title="logEmptyTitle"
:empty-description="logEmptyDesc" /> :empty-description="logEmptyDesc"
@stop="handleStopTask" />
<!-- 删除确认 (批量) --> <!-- 删除确认 (批量) -->