diff --git a/agent/agent.go b/agent/agent.go index 3cc4354..0122b6a 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/executor" "github.com/engigu/baihu-panel/internal/logger" "github.com/engigu/baihu-panel/internal/utils" @@ -21,19 +22,19 @@ import ( // WebSocket 消息类型 const ( - WSTypeHeartbeat = "heartbeat" - WSTypeHeartbeatAck = "heartbeat_ack" - WSTypeTasks = "tasks" - WSTypeTaskResult = "task_result" - WSTypeUpdate = "update" - WSTypeConnected = "connected" - WSTypeDisabled = "disabled" - WSTypeEnabled = "enabled" - WSTypeFetchTasks = "fetch_tasks" - WSTypeTaskLog = "task_log" - WSTypeExecute = "execute" - WSTypeTaskHeartbeat = "task_heartbeat" - WSTypeStop = "stop" + WSTypeHeartbeat = constant.WSTypeHeartbeat + WSTypeHeartbeatAck = constant.WSTypeHeartbeatAck + WSTypeTasks = constant.WSTypeTasks + WSTypeTaskResult = constant.WSTypeTaskResult + WSTypeUpdate = constant.WSTypeUpdate + WSTypeConnected = constant.WSTypeConnected + WSTypeDisabled = constant.WSTypeDisabled + WSTypeEnabled = constant.WSTypeEnabled + WSTypeFetchTasks = constant.WSTypeFetchTasks + WSTypeTaskLog = constant.WSTypeTaskLog + WSTypeExecute = constant.WSTypeExecute + WSTypeTaskHeartbeat = constant.WSTypeTaskHeartbeat + WSTypeStop = constant.WSTypeStop ) type WSMessage struct { @@ -193,7 +194,7 @@ func (h *AgentHandler) OnTaskCompleted(req *executor.ExecutionRequest, result *e EndTime: result.EndTime.Unix(), }) - if result.Status == "failed" { + if result.Status == constant.TaskStatusFailed { h.agent.printLastLogs(result.LogID) } h.agent.clearTaskLog(result.LogID) @@ -216,7 +217,7 @@ func (h *AgentHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) { Command: req.Command, Output: "", Error: err.Error(), - Status: "failed", + Status: constant.TaskStatusFailed, Duration: 0, ExitCode: 1, StartTime: time.Now().Unix(), diff --git a/internal/constant/constant.go b/internal/constant/constant.go index b262303..c8ead6a 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -64,6 +64,23 @@ const ( WSTypeFetchTasks = "fetch_tasks" WSTypeTaskHeartbeat = "task_heartbeat" WSTypeStop = "stop" + + // 任务状态 + TaskStatusSuccess = "success" + TaskStatusFailed = "failed" + TaskStatusRunning = "running" + TaskStatusPending = "pending" + TaskStatusTimeout = "timeout" + TaskStatusCancelled = "cancelled" + TaskStatusQueued = "queued" + + // 任务类型 + TaskTypeNormal = "task" + TaskTypeRepo = "repo" + + // Agent 状态 + AgentStatusOnline = "online" + AgentStatusOffline = "offline" ) // TablePrefix 表前缀,从配置文件读取 diff --git a/internal/controllers/dashboard_controller.go b/internal/controllers/dashboard_controller.go index 48a5e4f..cca7bf9 100644 --- a/internal/controllers/dashboard_controller.go +++ b/internal/controllers/dashboard_controller.go @@ -111,7 +111,7 @@ func (dc *DashboardController) GetSendStats(c *gin.Context) { } ds := dayMap[s.Day] ds.Total += s.Num - if s.Status == "success" { + if s.Status == constant.TaskStatusSuccess { ds.Success += s.Num } else { ds.Failed += s.Num diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index 0d6d3e9..7c48d19 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -69,7 +69,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) { } // 普通任务需要命令 - if req.Type != "repo" && req.Command == "" { + if req.Type != constant.TaskTypeRepo && req.Command == "" { utils.BadRequest(c, "命令不能为空") return } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index a3378c5..c9c1b5e 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -10,6 +10,7 @@ import ( "time" "github.com/creack/pty" + "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/logger" "github.com/engigu/baihu-panel/internal/utils" ) @@ -76,7 +77,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer id, err := hooks.PreExecute(ctx, req) if err != nil { return &Result{ - Status: "failed", + Status: constant.TaskStatusFailed, Duration: 0, ExitCode: 1, StartTime: start, @@ -185,7 +186,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer // Start 失败的处理 end := time.Now() result := &Result{ - Status: "failed", + Status: constant.TaskStatusFailed, Duration: end.Sub(start).Milliseconds(), ExitCode: 1, StartTime: start, // 修正为 start @@ -248,7 +249,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer } if err != nil { - result.Status = "failed" + result.Status = constant.TaskStatusFailed result.Error = err.Error() if exitErr, ok := err.(*exec.ExitError); ok { result.ExitCode = exitErr.ExitCode() @@ -256,7 +257,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer result.ExitCode = 1 } } else { - result.Status = "success" + result.Status = constant.TaskStatusSuccess result.ExitCode = 0 } diff --git a/internal/executor/scheduler.go b/internal/executor/scheduler.go index ddd6fea..a3b3f20 100644 --- a/internal/executor/scheduler.go +++ b/internal/executor/scheduler.go @@ -8,6 +8,8 @@ import ( "os" "sync" "time" + + "github.com/engigu/baihu-panel/internal/constant" ) // safeBuffer 一个线程安全的字节缓冲区,用于合并 stdout 和 stderr @@ -49,12 +51,12 @@ const ( type TaskStatus string const ( - TaskStatusPending TaskStatus = "pending" // 等待中 - TaskStatusRunning TaskStatus = "running" // 运行中 - TaskStatusSuccess TaskStatus = "success" // 成功 - TaskStatusFailed TaskStatus = "failed" // 失败 - TaskStatusTimeout TaskStatus = "timeout" // 超时 - TaskStatusCancelled TaskStatus = "cancelled" // 已取消 + TaskStatusPending TaskStatus = TaskStatus(constant.TaskStatusPending) // 等待中 + TaskStatusRunning TaskStatus = TaskStatus(constant.TaskStatusRunning) // 运行中 + TaskStatusSuccess TaskStatus = TaskStatus(constant.TaskStatusSuccess) // 成功 + TaskStatusFailed TaskStatus = TaskStatus(constant.TaskStatusFailed) // 失败 + TaskStatusTimeout TaskStatus = TaskStatus(constant.TaskStatusTimeout) // 超时 + TaskStatusCancelled TaskStatus = TaskStatus(constant.TaskStatusCancelled) // 已取消 ) // ExecutionRequest 执行请求(标准接口) @@ -329,7 +331,7 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error) return &ExecutionResult{ TaskID: req.TaskID, Success: false, - Status: "failed", + Status: constant.TaskStatusFailed, Error: err.Error(), Duration: 0, ExitCode: 1, @@ -402,7 +404,7 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error) } if execResult != nil { - result.Success = execResult.Status == "success" + result.Success = execResult.Status == constant.TaskStatusSuccess result.Output = combinedBuf.String() result.Status = execResult.Status result.Duration = execResult.Duration @@ -411,7 +413,7 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error) result.EndTime = execResult.EndTime } else { result.Success = false - result.Status = "failed" + result.Status = constant.TaskStatusFailed result.StartTime = start result.EndTime = time.Now() result.Duration = result.EndTime.Sub(result.StartTime).Milliseconds() @@ -421,9 +423,9 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error) if execErr != nil { result.Error = execErr.Error() if ctx.Err() == context.Canceled { - result.Status = "cancelled" + result.Status = constant.TaskStatusCancelled } else if ctx.Err() == context.DeadlineExceeded { - result.Status = "timeout" + result.Status = constant.TaskStatusTimeout } } diff --git a/internal/models/agent.go b/internal/models/agent.go index 5049537..9da0809 100644 --- a/internal/models/agent.go +++ b/internal/models/agent.go @@ -9,20 +9,20 @@ import ( // Agent 远程执行代理 type Agent struct { ID uint `json:"id" gorm:"primaryKey"` - Name string `json:"name" gorm:"size:100;not null"` // Agent 名称 - Token string `json:"token" gorm:"size:64;index"` // 认证 Token(可重复使用) - MachineID string `json:"machine_id" gorm:"size:64;uniqueIndex"` // 机器识别码(唯一) - Description string `json:"description" gorm:"size:255"` // 描述 - Status string `json:"status" gorm:"size:20;default:'pending'"` // 状态: pending(待审核), online, offline, blocked(拉黑) - LastSeen *LocalTime `json:"last_seen"` // 最后心跳时间 - IP string `json:"ip" gorm:"size:45"` // Agent IP 地址 - Version string `json:"version" gorm:"size:50"` // Agent 版本 - BuildTime string `json:"build_time" gorm:"size:30"` // Agent 构建时间 - Hostname string `json:"hostname" gorm:"size:100"` // Agent 主机名 - OS string `json:"os" gorm:"size:20"` // 操作系统 - Arch string `json:"arch" gorm:"size:20"` // 架构 - ForceUpdate bool `json:"force_update" gorm:"default:false"` // 强制更新标志 - Enabled bool `json:"enabled" gorm:"default:true"` // 是否启用 + Name string `json:"name" gorm:"size:100;not null"` // Agent 名称 + Token string `json:"token" gorm:"size:64;index"` // 认证 Token(可重复使用) + MachineID string `json:"machine_id" gorm:"size:64;uniqueIndex"` // 机器识别码(唯一) + Description string `json:"description" gorm:"size:255"` // 描述 + Status string `json:"status" gorm:"size:20;default:'pending';index"` // 状态: constant.AgentStatusOnline, constant.AgentStatusOffline + LastSeen *LocalTime `json:"last_seen"` // 最后心跳时间 + IP string `json:"ip" gorm:"size:45"` // Agent IP 地址 + Version string `json:"version" gorm:"size:50"` // Agent 版本 + BuildTime string `json:"build_time" gorm:"size:30"` // Agent 构建时间 + Hostname string `json:"hostname" gorm:"size:100"` // Agent 主机名 + OS string `json:"os" gorm:"size:20"` // 操作系统 + Arch string `json:"arch" gorm:"size:20"` // 架构 + ForceUpdate bool `json:"force_update" gorm:"default:false"` // 强制更新标志 + Enabled bool `json:"enabled" gorm:"default:true"` // 是否启用 CreatedAt LocalTime `json:"created_at"` UpdatedAt LocalTime `json:"updated_at"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` diff --git a/internal/models/login_log.go b/internal/models/login_log.go index d893136..bdb73a8 100644 --- a/internal/models/login_log.go +++ b/internal/models/login_log.go @@ -10,7 +10,7 @@ type LoginLog struct { Username string `json:"username" gorm:"size:100;index;not null"` IP string `json:"ip" gorm:"size:50"` UserAgent string `json:"user_agent" gorm:"size:500"` - Status string `json:"status" gorm:"size:20"` // success, failed + Status string `json:"status" gorm:"size:20;index"` // success, failed Message string `json:"message" gorm:"size:255"` CreatedAt LocalTime `json:"created_at" gorm:"index"` } diff --git a/internal/models/task.go b/internal/models/task.go index b97fb42..b9e8797 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -37,7 +37,7 @@ type Task struct { ID uint `json:"id" gorm:"primaryKey"` Name string `json:"name" gorm:"size:255;not null"` Command string `json:"command" gorm:"type:text"` // 普通任务的命令 - Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: task(普通任务), repo(仓库同步) + Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: constant.TaskTypeNormal, constant.TaskTypeRepo Config string `json:"config" gorm:"type:text"` // 配置 JSON(仓库同步配置等) Schedule string `json:"schedule" gorm:"size:100"` // cron expression Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟 @@ -92,10 +92,10 @@ type TaskLog struct { TaskID uint `json:"task_id" gorm:"index"` AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行 Command string `json:"command" gorm:"type:text"` - Output string `json:"-" gorm:"type:longtext"` // gzip+base64 compressed - Error string `json:"error" gorm:"type:text"` // 额外的系统错误信息 - Status string `json:"status" gorm:"size:20"` // success, failed - Duration int64 `json:"duration"` // milliseconds + Output string `json:"-" gorm:"type:longtext"` // gzip+base64 compressed + Error string `json:"error" gorm:"type:text"` // 额外的系统错误信息 + Status string `json:"status" gorm:"size:20;index"` // success, failed + Duration int64 `json:"duration"` // milliseconds ExitCode int `json:"exit_code"` StartTime *LocalTime `json:"start_time"` EndTime *LocalTime `json:"end_time"` diff --git a/internal/services/agent_service.go b/internal/services/agent_service.go index 61dd825..87bc797 100644 --- a/internal/services/agent_service.go +++ b/internal/services/agent_service.go @@ -122,7 +122,7 @@ func (s *AgentService) RegisterByToken(token string, machineID string, ip string database.DB.Model(&existing).Updates(map[string]interface{}{ "token": token, "ip": ip, - "status": "online", + "status": constant.AgentStatusOnline, "last_seen": now, }) s.UseToken(agentToken.ID) @@ -138,7 +138,7 @@ func (s *AgentService) RegisterByToken(token string, machineID string, ip string Token: token, MachineID: machineID, IP: ip, - Status: "online", + Status: constant.AgentStatusOnline, LastSeen: &now, Enabled: true, } @@ -179,7 +179,7 @@ func (s *AgentService) Register(req *models.AgentRegisterRequest, ip string) (*m Version: req.Version, BuildTime: req.BuildTime, IP: ip, - Status: "online", + Status: constant.AgentStatusOnline, LastSeen: &now, Enabled: true, } @@ -288,7 +288,7 @@ func (s *AgentService) Heartbeat(token, ip, version, buildTime, hostname, osType database.DB.Model(&models.Agent{}).Where("id = ?", agent.ID).Updates(updates) - agent.Status = "online" + agent.Status = constant.AgentStatusOnline agent.LastSeen = &now agent.IP = ip agent.Version = version @@ -386,15 +386,15 @@ func (s *AgentService) UpdateTaskDuration(logID uint, duration int64) error { func (s *AgentService) UpdateOfflineAgents() { cutoff := time.Now().Add(-2 * time.Minute) database.DB.Model(&models.Agent{}). - Where("status = ? AND last_seen < ?", "online", cutoff). - Update("status", "offline") + Where("status = ? AND last_seen < ?", constant.AgentStatusOnline, cutoff). + Update("status", constant.AgentStatusOffline) } // ResetAllAgentsToOffline 将所有 Agents 状态重置为离线(用于服务启动时) func (s *AgentService) ResetAllAgentsToOffline() { database.DB.Model(&models.Agent{}). - Where("status = ?", "online"). - Update("status", "offline") + Where("status = ?", constant.AgentStatusOnline). + Update("status", constant.AgentStatusOffline) } // GetLatestVersion 获取最新 Agent 版本 diff --git a/internal/services/agent_ws_service.go b/internal/services/agent_ws_service.go index f302b07..cc72166 100644 --- a/internal/services/agent_ws_service.go +++ b/internal/services/agent_ws_service.go @@ -292,7 +292,7 @@ func (m *AgentWSManager) cleanupLoop() { conn.Close() delete(m.connections, agentID) // 更新数据库状态 - database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", "offline") + database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", constant.AgentStatusOffline) logger.Infof("[AgentWS] Agent #%d 心跳超时,已断开", agentID) } } @@ -301,8 +301,8 @@ func (m *AgentWSManager) cleanupLoop() { // 有些 Agent 虽然没有连接,但数据库状态可能是 "online" cutoff := now.Add(-2 * time.Minute) database.DB.Model(&models.Agent{}). - Where("status = ? AND last_seen < ?", "online", cutoff). - Update("status", "offline") + Where("status = ? AND last_seen < ?", constant.AgentStatusOnline, cutoff). + Update("status", constant.AgentStatusOffline) // 清理过期的限流记录(超过 10 分钟未活动) diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index 28be30d..690917f 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -134,7 +134,7 @@ func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest) goid, err := h.es.AddRunningGo(task.ID) if err != nil { // 并发限制,更新日志状态为失败 - taskLog.Status = "failed" + taskLog.Status = constant.TaskStatusFailed taskLog.Output, _ = utils.CompressToBase64("任务并发数限制,拒绝执行") h.es.taskLogService.SaveTaskLog(taskLog) return nil, nil, fmt.Errorf("任务并发限制: %v", err) @@ -268,7 +268,7 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er Command: req.Command, Output: output, Error: err.Error(), - Status: "failed", + Status: constant.TaskStatusFailed, Duration: 0, ExitCode: 1, StartTime: &now, @@ -330,7 +330,7 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor. } // 特殊处理仓库同步任务 - if task.Type == "repo" { + if task.Type == constant.TaskTypeRepo { cmd, workDir := es.BuildRepoCommand(task) if cmd != "" { req.Command = cmd @@ -475,7 +475,7 @@ func (es *ExecutorService) ExecuteTask(taskID int) *executor.ExecutionResult { return &executor.ExecutionResult{ TaskID: fmt.Sprintf("%d", task.ID), Success: true, - Status: "queued", + Status: constant.TaskStatusQueued, StartTime: time.Now(), } } @@ -487,7 +487,7 @@ func (es *ExecutorService) StopTaskExecution(logID uint) error { return fmt.Errorf("日志不存在") } - if taskLog.Status != "running" { + if taskLog.Status != constant.TaskStatusRunning { return fmt.Errorf("任务已结束") } @@ -692,7 +692,7 @@ func (es *ExecutorService) ExecuteRemoteForScheduler(task *models.Task, logID ui case <-time.After(time.Duration(timeout) * time.Minute): end := time.Now() return &executor.Result{ - Status: "failed", + Status: constant.TaskStatusFailed, Error: "等待 Agent 结果超时", Duration: end.Sub(start).Milliseconds(), ExitCode: -1, diff --git a/web/src/constants/index.ts b/web/src/constants/index.ts index ab189b5..3909254 100644 --- a/web/src/constants/index.ts +++ b/web/src/constants/index.ts @@ -17,3 +17,25 @@ export const FILE_RUNNERS: Record = { sh: 'bash', bash: 'bash', } as const + +// 任务状态 +export const TASK_STATUS = { + SUCCESS: 'success', + FAILED: 'failed', + RUNNING: 'running', + PENDING: 'pending', + TIMEOUT: 'timeout', + CANCELLED: 'cancelled', +} as const + +// 任务类型 +export const TASK_TYPE = { + NORMAL: 'task', + REPO: 'repo', +} as const + +// Agent 状态 +export const AGENT_STATUS = { + ONLINE: 'online', + OFFLINE: 'offline', +} as const diff --git a/web/src/views/agents/Agents.vue b/web/src/views/agents/Agents.vue index ecd97ba..34cbab1 100644 --- a/web/src/views/agents/Agents.vue +++ b/web/src/views/agents/Agents.vue @@ -10,6 +10,7 @@ import { RefreshCw, Trash2, Edit, Copy, Server, Search, Download, RotateCw, Plus import { api, type Agent, type AgentToken } from '@/api' import { toast } from 'vue-sonner' import { useRouter } from 'vue-router' +import { AGENT_STATUS } from '@/constants' const router = useRouter() @@ -43,11 +44,7 @@ const filteredAgents = computed(() => { }) function isOnline(agent: Agent): boolean { - if (!agent.last_seen) return false - const lastSeen = new Date(agent.last_seen) - const now = new Date() - const diffMs = now.getTime() - lastSeen.getTime() - return diffMs < 2 * 60 * 1000 + return agent.status === AGENT_STATUS.ONLINE } async function loadAgents() { @@ -326,7 +323,7 @@ onUnmounted(() => { class="w-24 sm:w-32 shrink-0 font-medium text-xs sm:text-sm truncate cursor-pointer hover:text-primary" @click="viewDetail(agent)" :title="agent.name">{{ agent.name }} {{ agent.ip || '-' - }} + }}