diff --git a/backend/controller/agent.go b/backend/controller/agent.go index 2e23bdf..a740b48 100644 --- a/backend/controller/agent.go +++ b/backend/controller/agent.go @@ -61,29 +61,29 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { // 存储到数据库 report := model.AgentReport{ - Hostname: req.Hostname, - CPUUsage: req.CPU.UsagePercent, - CPUIOwait: req.CPU.IOWait, - CPUCores: req.CPU.CoreCount, - Load1: req.CPU.Load1, - Load5: req.CPU.Load5, - Load15: req.CPU.Load15, - MemTotal: req.Memory.TotalGB, - MemUsed: req.Memory.UsedGB, - MemUsage: req.Memory.UsagePercent, + Hostname: req.Hostname, + CPUUsage: req.CPU.UsagePercent, + CPUIOwait: req.CPU.IOWait, + CPUCores: req.CPU.CoreCount, + Load1: req.CPU.Load1, + Load5: req.CPU.Load5, + Load15: req.CPU.Load15, + MemTotal: req.Memory.TotalGB, + MemUsed: req.Memory.UsedGB, + MemUsage: req.Memory.UsagePercent, MemAvailable: req.Memory.AvailableGB, MemSwapUsed: req.Memory.SwapUsedGB, - DiskInfo: string(diskJSON), - PortInfo: string(portJSON), - NetInfo: string(netJSON), - DiskIOInfo: string(dioJSON), + DiskInfo: string(diskJSON), + PortInfo: string(portJSON), + NetInfo: string(netJSON), + DiskIOInfo: string(dioJSON), // v2.0 新增 SwapInfo: string(swapJSON), ProcessTopInfo: string(processTopJSON), FDInfo: string(fdJSON), CSInfo: string(csJSON), HeartbeatInfo: string(heartbeatJSON), - ReportTime: reportTime, + ReportTime: reportTime, } if err := h.db.Create(&report).Error; err != nil { @@ -99,9 +99,9 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { for _, d := range req.Disks { totalDiskGB += d.TotalGB } - memGB := math.Ceil(req.Memory.TotalGB) // 31.34 → 32, 62.79 → 63(物理内存向上取整) - memMB := int64(memGB * 1024) // GB → MB - diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40 + memGB := math.Ceil(req.Memory.TotalGB) // 31.34 → 32, 62.79 → 63(物理内存向上取整) + memMB := int64(memGB * 1024) // GB → MB + diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40 clientIP := c.ClientIP() diff --git a/backend/controller/agent_cli.go b/backend/controller/agent_cli.go index c24a11a..1b65e49 100644 --- a/backend/controller/agent_cli.go +++ b/backend/controller/agent_cli.go @@ -19,6 +19,7 @@ import ( "ezops/model" "ezops/pkg/agentcli" "ezops/pkg/agentcli/core" + "ezops/pkg/agentcli/guardrail" "ezops/pkg/agentcli/llm" "ezops/pkg/agentcli/safety" "ezops/pkg/agentcli/tools" @@ -86,7 +87,7 @@ type agentConn struct { server model.Server sshCfg remoteexec.SSHConfig kubernetesCtx *kubernetesAgentContext - session *model.AgentCLISession + session *model.AIRun taskCtx context.Context cancel context.CancelFunc totalTokens int @@ -97,6 +98,9 @@ type agentConn struct { convHistory []llm.Message running bool runningMu sync.Mutex + pendingSpans map[string]string + seq int + seqMu sync.Mutex } // Run WebSocket 入口 // Logan.Li @@ -144,7 +148,7 @@ func (h *AgentCLIHandler) runHostWS(c *gin.Context) { ac := &agentConn{ conn: conn, db: h.db, mode: agentcli.RunModeHost, userID: middleware.GetUserID(c), server: server, sshCfg: sshCfg, - taskCtx: context.Background(), confirms: make(map[string]chan bool), + taskCtx: context.Background(), confirms: make(map[string]chan bool), pendingSpans: make(map[string]string), } ac.write(wsEvent{Type: "session", SessionID: 0, Message: fmt.Sprintf("已连接主机 %s(%s),LLM: %s。请描述要执行的任务。", server.Hostname, server.IP, llmCfg.Model)}) @@ -181,7 +185,7 @@ func (h *AgentCLIHandler) runKubernetesWS(c *gin.Context) { ac := &agentConn{ conn: conn, db: h.db, mode: agentcli.RunModeKubernetes, userID: middleware.GetUserID(c), kubernetesCtx: kCtx, - taskCtx: context.Background(), confirms: make(map[string]chan bool), + taskCtx: context.Background(), confirms: make(map[string]chan bool), pendingSpans: make(map[string]string), } target := buildKubernetesTargetInfo(kCtx) ac.write(wsEvent{Type: "session", SessionID: 0, @@ -194,7 +198,9 @@ func (ac *agentConn) wsReadLoop() { for { _, data, err := ac.conn.ReadMessage() if err != nil { - ac.abort() + if ac.isRunning() { + ac.abort() + } return } var ctrl wsControl @@ -256,20 +262,27 @@ func (ac *agentConn) startTask(query, safetyMode string) { if r := []rune(title); len(r) > 60 { title = string(r[:60]) + "..." } - sess := &model.AgentCLISession{ - UserID: ac.userID, Mode: ac.mode, Title: title, Status: "running", + sess := &model.AIRun{ + RunID: guardrail.NewTraceID(), UserID: ac.userID, Feature: "agent_cli", + Mode: ac.mode, Title: title, Status: "running", Provider: llmCfg.Provider, Model: llmCfg.Model, + SafetyMode: safetyMode, StartedAt: time.Now(), } if ac.mode == agentcli.RunModeKubernetes && ac.kubernetesCtx != nil { - sess.ClusterID = ac.kubernetesCtx.cluster.ID - sess.ClusterName = ac.kubernetesCtx.cluster.Name - sess.Namespace = ac.kubernetesCtx.namespace - sess.PodName = ac.kubernetesCtx.podName - sess.Container = ac.kubernetesCtx.container + sess.SubjectType = "k8s_cluster" + if ac.kubernetesCtx.podName != "" { + sess.SubjectType = "pod" + } else if ac.kubernetesCtx.namespace != "" { + sess.SubjectType = "namespace" + } + sess.SubjectID = fmt.Sprintf("%d", ac.kubernetesCtx.cluster.ID) + sess.SubjectName = ac.kubernetesCtx.cluster.Name + sess.SubjectScope = ac.kubernetesCtx.namespace } else { - sess.ServerID = ac.server.ID - sess.ServerName = ac.server.Hostname - sess.ServerHost = ac.server.IP + sess.SubjectType = "host" + sess.SubjectID = fmt.Sprintf("%d", ac.server.ID) + sess.SubjectName = ac.server.Hostname + sess.SubjectScope = ac.server.IP } if err := ac.db.Create(sess).Error; err != nil { ac.write(wsEvent{Type: "error", Message: "创建会话失败: " + err.Error()}) @@ -287,15 +300,15 @@ func (ac *agentConn) resumeSession(sessionID uint) { ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"}) return } - var sess model.AgentCLISession + var sess model.AIRun q := ac.db.Where("user_id = ?", ac.userID) if ac.mode == agentcli.RunModeKubernetes { q = q.Where("mode IN ?", []string{agentcli.RunModeKubernetes, "k8s"}) if ac.kubernetesCtx != nil { - q = q.Where("cluster_id = ? AND namespace = ?", ac.kubernetesCtx.cluster.ID, ac.kubernetesCtx.namespace) + q = q.Where("subject_id = ? AND subject_scope = ?", fmt.Sprintf("%d", ac.kubernetesCtx.cluster.ID), ac.kubernetesCtx.namespace) } } else { - q = q.Where("mode = ? AND server_id = ?", ac.mode, ac.server.ID) + q = q.Where("mode = ? AND subject_type = ? AND subject_id = ?", ac.mode, "host", fmt.Sprintf("%d", ac.server.ID)) } if err := q.First(&sess, sessionID).Error; err != nil { ac.write(wsEvent{Type: "error", Message: "会话不存在或无权访问"}) @@ -305,8 +318,8 @@ func (ac *agentConn) resumeSession(sessionID uint) { ac.write(wsEvent{Type: "error", Message: "该会话仍在执行中,无法恢复"}) return } - var msgs []model.AgentCLIMessage - ac.db.Where("session_id = ?", sessionID).Order("id ASC").Find(&msgs) + var msgs []model.AIRunMessage + ac.db.Where("run_id = ?", sess.RunID).Order("seq ASC, id ASC").Find(&msgs) ac.session = &sess ac.convHistory = buildConvHistoryFromMessages(msgs) ac.setTotalTokens(sess.TotalTokens) @@ -340,7 +353,7 @@ func (ac *agentConn) continueTask(query, safetyMode string) { } // runTask 执行一轮 ReAct(新建或追问共用) // Logan.Li -func (ac *agentConn) runTask(sess *model.AgentCLISession, query, safetyMode string, llmCfg model.LLMConfig, isContinue bool) { +func (ac *agentConn) runTask(sess *model.AIRun, query, safetyMode string, llmCfg model.LLMConfig, isContinue bool) { if ac.isRunning() { ac.write(wsEvent{Type: "error", Message: "任务进行中,请等待完成或停止"}) return @@ -393,9 +406,9 @@ func (ac *agentConn) runTask(sess *model.AgentCLISession, query, safetyMode stri ac.taskCtx = ctx ac.cancel = cancel ac.setRunning(true) - ac.db.Model(sess).Update("status", "running") + ac.db.Model(sess).Updates(map[string]interface{}{"status": "running", "safety_mode": safetyMode}) - ac.persistMsg(sess.ID, 0, "user", "user", query, "", "", "", "", false, "", 0) + ac.persistMsg(sess.RunID, 0, "user", "user", query, "", "", "", "", false, "", 0) targetInfo := ac.buildTargetInfo() @@ -457,6 +470,9 @@ func (ac *agentConn) onEvent(e core.Event) { if e.Type == core.EventProgress && e.Tokens > 0 { ac.setTotalTokens(e.Tokens) } + if e.Type == core.EventDone || e.Type == core.EventError { + ac.setRunning(false) + } ev := wsEvent{ Type: string(e.Type), Phase: e.Phase, Summary: e.Summary, @@ -474,23 +490,25 @@ func (ac *agentConn) onEvent(e core.Event) { if ac.session == nil { return } - sid := ac.session.ID + runID := ac.session.RunID switch e.Type { case core.EventStatus: // status 事件不改 DB schema:duration 字段在 type=status 时复用为 phase。 - ac.persistMsg(sid, e.Round, "status", "system", e.Summary, e.ToolName, "", "", e.Risk, e.Phase != core.PhaseError && e.Phase != core.PhaseBlocked, e.Phase, e.Tokens) + ac.persistMsg(runID, e.Round, "status", "system", e.Summary, e.ToolName, "", "", e.Risk, e.Phase != core.PhaseError && e.Phase != core.PhaseBlocked, e.Phase, e.Tokens) case core.EventThought: - ac.persistMsg(sid, e.Round, "thought", "assistant", e.Thought, "", "", "", "", false, "", 0) + ac.persistMsg(runID, e.Round, "thought", "assistant", e.Thought, "", "", "", "", false, "", 0) case core.EventToolCall: argsJSON, _ := json.Marshal(e.ToolArgs) - ac.persistMsg(sid, e.Round, "tool_call", "tool", "", e.ToolName, string(argsJSON), "", e.Risk, false, "", 0) + ac.persistMsg(runID, e.Round, "tool_call", "tool", string(argsJSON), e.ToolName, string(argsJSON), "", e.Risk, false, "", 0) + ac.startToolSpan(e, string(argsJSON)) case core.EventToolResult: - ac.persistMsg(sid, e.Round, "tool_result", "tool", e.Output, e.ToolName, "", "", "", e.Success, e.Duration, 0) + ac.persistMsg(runID, e.Round, "tool_result", "tool", e.Output, e.ToolName, "", "", "", e.Success, e.Duration, 0) + ac.finishToolSpan(e) case core.EventDone: ac.setTotalTokens(e.Tokens) - ac.persistMsg(sid, e.Round, "done", "assistant", e.Answer, "", "", "", "", true, "", e.Tokens) + ac.persistMsg(runID, e.Round, "done", "assistant", e.Answer, "", "", "", "", true, "", e.Tokens) case core.EventError: - ac.persistMsg(sid, e.Round, "error", "assistant", e.Message, "", "", "", "", false, "", 0) + ac.persistMsg(runID, e.Round, "error", "assistant", e.Message, "", "", "", "", false, "", 0) } } @@ -519,6 +537,7 @@ func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, ris select { case approved := <-ch: + ac.recordConfirmSpan(toolName, args, risk, approved) return approved, nil case <-ac.taskCtx.Done(): ac.delConfirm(confirmID) @@ -529,6 +548,25 @@ func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, ris } } +func (ac *agentConn) recordConfirmSpan(toolName string, args map[string]interface{}, risk tools.RiskLevel, approved bool) { + if ac.session == nil { + return + } + decision := "rejected" + status := "skipped" + if approved { + decision = "approved" + status = "success" + } + argsJSON, _ := json.Marshal(args) + span := &model.AIRunSpan{ + RunID: ac.session.RunID, SpanID: guardrail.NewSpanID(), Kind: "confirm", Name: toolName, + ToolSource: ac.toolSource(toolName), Input: guardrail.SanitizeJSON(string(argsJSON)), + Risk: risk.String(), Decision: decision, Status: status, StartedAt: time.Now(), CreatedAt: time.Now(), + } + _ = ac.db.Create(span).Error +} + // resolveConfirm 前端回传确认结果 // Logan.Li func (ac *agentConn) resolveConfirm(confirmID string, approved bool) { ac.cfMu.Lock() @@ -571,13 +609,13 @@ func (ac *agentConn) lastRound() int { return 0 } var maxRound int - ac.db.Model(&model.AgentCLIMessage{}).Where("session_id = ?", ac.session.ID). + ac.db.Model(&model.AIRunMessage{}).Where("run_id = ?", ac.session.RunID). Select("COALESCE(MAX(round),0)").Scan(&maxRound) return maxRound } // finish 结束本轮执行并更新会话 DB // Logan.Li -func (ac *agentConn) finish(sess *model.AgentCLISession, status, errMsg string, rounds int, dur time.Duration) { +func (ac *agentConn) finish(sess *model.AIRun, status, errMsg string, rounds int, dur time.Duration) { ac.setRunning(false) tokens := ac.getTotalTokens() // done/error 已由引擎 onEvent 推送;此处仅 aborted 需补推 WS(引擎初始化失败在 runTask 内单独推送) // Logan.Li @@ -588,6 +626,10 @@ func (ac *agentConn) finish(sess *model.AgentCLISession, status, errMsg string, "status": status, "rounds": rounds, "duration": dur.Milliseconds(), "total_tokens": tokens, } + if status == "done" || status == "error" || status == "aborted" { + now := time.Now() + updates["finished_at"] = &now + } if errMsg != "" { updates["error"] = errMsg } @@ -602,15 +644,145 @@ func (ac *agentConn) write(ev wsEvent) { _ = ac.conn.WriteMessage(websocket.TextMessage, b) } -// persistMsg 持久化一条消息 // Logan.Li -func (ac *agentConn) persistMsg(sid uint, round int, typ, role, content, toolName, args, output, risk string, success bool, dur string, tokens int) { - msg := &model.AgentCLIMessage{ - SessionID: sid, Round: round, Type: typ, Role: role, Content: content, - ToolName: toolName, ToolArgs: args, ToolOutput: output, Risk: risk, - Success: success, Duration: dur, Tokens: tokens, +// persistMsg 持久化一条 AI Run 回放消息。 // Logan.Li +func (ac *agentConn) persistMsg(runID string, round int, typ, role, content, toolName, args, output, risk string, success bool, dur string, tokens int) { + content = guardrail.SanitizeText(content) + if typ == "tool_call" { + content = guardrail.SanitizeJSON(content) + } + msg := &model.AIRunMessage{ + RunID: runID, Round: round, Seq: ac.nextSeq(), + Type: typ, Role: role, Content: content, + ToolName: toolName, Tokens: tokens, } if err := ac.db.Create(msg).Error; err != nil { // 持久化失败不阻断主流程 _ = err } } + +func (ac *agentConn) nextSeq() int { + ac.seqMu.Lock() + defer ac.seqMu.Unlock() + ac.seq++ + return ac.seq +} + +func (ac *agentConn) startToolSpan(e core.Event, argsJSON string) { + if ac.session == nil { + return + } + spanID := guardrail.NewSpanID() + key := ac.pendingSpanKey(e.Round, e.ToolName) + ac.runningMu.Lock() + ac.pendingSpans[key] = spanID + ac.runningMu.Unlock() + span := &model.AIRunSpan{ + RunID: ac.session.RunID, SpanID: spanID, Kind: "tool", Name: e.ToolName, + ToolSource: ac.toolSource(e.ToolName), Input: guardrail.SanitizeJSON(argsJSON), + Risk: e.Risk, Decision: "auto", Status: "running", + StartedAt: time.Now(), + } + _ = ac.db.Create(span).Error +} + +func (ac *agentConn) finishToolSpan(e core.Event) { + if ac.session == nil { + return + } + key := ac.pendingSpanKey(e.Round, e.ToolName) + ac.runningMu.Lock() + spanID := ac.pendingSpans[key] + delete(ac.pendingSpans, key) + ac.runningMu.Unlock() + if spanID == "" { + return + } + status := "success" + if !e.Success { + status = "error" + } + now := time.Now() + ac.db.Model(&model.AIRunSpan{}).Where("span_id = ?", spanID).Updates(map[string]interface{}{ + "output": guardrail.SanitizeText(e.Output), "output_size": e.OutputSize, + "duration": parseDurationMillis(e.Duration), "status": status, "ended_at": &now, + }) + ac.recordOpsCommand(e, spanID, status) +} + +func (ac *agentConn) pendingSpanKey(round int, toolName string) string { + return fmt.Sprintf("%d:%s", round, toolName) +} + +func (ac *agentConn) toolSource(toolName string) string { + switch toolName { + case "ssh_execute": + return "shell" + case "kubernetes_diagnose", "kubernetes_get", "pod_logs", "pod_execute", "kubernetes_action": + return "kubernetes" + case "aiops_asset_search", "aiops_alert_list", "aiops_metric_summary": + return "aiops" + default: + return "builtin" + } +} + +func (ac *agentConn) recordOpsCommand(e core.Event, spanID, status string) { + if ac.session == nil { + return + } + command := e.ToolName + if e.ToolArgs != nil { + if cmd, ok := e.ToolArgs["command"].(string); ok && cmd != "" { + command = cmd + } else if action, ok := e.ToolArgs["action"]; ok { + command = fmt.Sprintf("%v %v", action, e.ToolArgs["name"]) + } + } + targetType, targetID := ac.currentTarget() + cmd := &model.OpsCommand{ + CommandID: guardrail.NewSpanID(), RunID: ac.session.RunID, SpanID: spanID, + UserID: ac.userID, ActorType: "ai", TargetType: targetType, TargetID: targetID, + CommandType: ac.commandType(e.ToolName), Command: guardrail.SanitizeText(command), + Risk: e.Risk, Decision: "auto", Status: status, OutputSize: e.OutputSize, + Duration: parseDurationMillis(e.Duration), ExecutedAt: time.Now(), + } + if !e.Success { + cmd.Error = guardrail.SanitizeText(e.Output) + } + _ = ac.db.Create(cmd).Error +} + +func (ac *agentConn) currentTarget() (string, string) { + if ac.mode == agentcli.RunModeKubernetes && ac.kubernetesCtx != nil { + if ac.kubernetesCtx.podName != "" { + return "pod", fmt.Sprintf("%d/%s/%s", ac.kubernetesCtx.cluster.ID, ac.kubernetesCtx.namespace, ac.kubernetesCtx.podName) + } + return "k8s_cluster", fmt.Sprintf("%d", ac.kubernetesCtx.cluster.ID) + } + return "host", fmt.Sprintf("%d", ac.server.ID) +} + +func (ac *agentConn) commandType(toolName string) string { + switch toolName { + case "ssh_execute", "pod_execute": + return "shell" + case "kubernetes_diagnose", "kubernetes_action", "kubernetes_get", "pod_logs": + return "kubernetes" + case "aiops_asset_search", "aiops_alert_list", "aiops_metric_summary": + return "api" + default: + return "tool" + } +} + +func parseDurationMillis(raw string) int64 { + if raw == "" { + return 0 + } + d, err := time.ParseDuration(raw) + if err != nil { + return 0 + } + return d.Milliseconds() +} diff --git a/backend/controller/agent_cli_api.go b/backend/controller/agent_cli_api.go index ee578fb..9a71340 100644 --- a/backend/controller/agent_cli_api.go +++ b/backend/controller/agent_cli_api.go @@ -26,7 +26,7 @@ func (h *AgentCLIHandler) ListSessions(c *gin.Context) { pageSize = 20 } - q := h.db.Model(&model.AgentCLISession{}).Where("user_id = ?", uid) + q := h.db.Model(&model.AIRun{}).Where("user_id = ? AND feature = ?", uid, "agent_cli") if mode := c.Query("mode"); mode != "" { mode = agentcli.NormalizeRunMode(mode) if mode == agentcli.RunModeKubernetes { @@ -36,18 +36,18 @@ func (h *AgentCLIHandler) ListSessions(c *gin.Context) { } } if sid := c.Query("server_id"); sid != "" { - q = q.Where("server_id = ?", sid) + q = q.Where("subject_type = ? AND subject_id = ?", "host", sid) } if cid := c.Query("cluster_id"); cid != "" { - q = q.Where("cluster_id = ?", cid) + q = q.Where("subject_id = ?", cid) } if ns := c.Query("namespace"); ns != "" { - q = q.Where("namespace = ?", ns) + q = q.Where("subject_scope = ?", ns) } var total int64 q.Count(&total) - var sessions []model.AgentCLISession + var sessions []model.AIRun if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&sessions).Error; err != nil { c.JSON(http.StatusOK, model.Response{Code: 500, Message: "查询失败: " + err.Error()}) return @@ -60,13 +60,13 @@ func (h *AgentCLIHandler) ListSessions(c *gin.Context) { func (h *AgentCLIHandler) GetSession(c *gin.Context) { uid := middleware.GetUserID(c) id := c.Param("id") - var sess model.AgentCLISession + var sess model.AIRun if err := h.db.Where("user_id = ?", uid).First(&sess, id).Error; err != nil { c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"}) return } - var msgs []model.AgentCLIMessage - h.db.Where("session_id = ?", id).Order("id ASC").Find(&msgs) + var msgs []model.AIRunMessage + h.db.Where("run_id = ?", sess.RunID).Order("seq ASC, id ASC").Find(&msgs) c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"session": sess, "messages": msgs}}) } @@ -75,16 +75,18 @@ func (h *AgentCLIHandler) GetSession(c *gin.Context) { func (h *AgentCLIHandler) DeleteSession(c *gin.Context) { uid := middleware.GetUserID(c) id := c.Param("id") - var sess model.AgentCLISession + var sess model.AIRun if err := h.db.Where("user_id = ?", uid).First(&sess, id).Error; err != nil { c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"}) return } - if err := h.db.Where("session_id = ?", id).Delete(&model.AgentCLIMessage{}).Error; err != nil { + if err := h.db.Where("run_id = ?", sess.RunID).Delete(&model.AIRunMessage{}).Error; err != nil { c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除消息失败: " + err.Error()}) return } - if err := h.db.Delete(&model.AgentCLISession{}, id).Error; err != nil { + _ = h.db.Where("run_id = ?", sess.RunID).Delete(&model.AIRunSpan{}).Error + _ = h.db.Where("run_id = ?", sess.RunID).Delete(&model.OpsCommand{}).Error + if err := h.db.Delete(&model.AIRun{}, id).Error; err != nil { c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除会话失败: " + err.Error()}) return } diff --git a/backend/controller/agent_cli_history.go b/backend/controller/agent_cli_history.go index 550ed3f..75e39c7 100644 --- a/backend/controller/agent_cli_history.go +++ b/backend/controller/agent_cli_history.go @@ -10,7 +10,7 @@ import ( ) // buildConvHistoryFromMessages 将 DB 消息重建为追问用的 prior 历史(不含 system) // Logan.Li -func buildConvHistoryFromMessages(msgs []model.AgentCLIMessage) []llm.Message { +func buildConvHistoryFromMessages(msgs []model.AIRunMessage) []llm.Message { var out []llm.Message var pendingThought string var pendingToolCalls []llm.ToolCall @@ -40,7 +40,7 @@ func buildConvHistoryFromMessages(msgs []model.AgentCLIMessage) []llm.Message { case "thought": pendingThought = m.Content case "tool_call": - args := validateToolArgsJSON(m.ToolArgs) + args := validateToolArgsJSON(m.Content) tcID := fmt.Sprintf("hist-%d", m.ID) pendingToolCalls = append(pendingToolCalls, llm.ToolCall{ ID: tcID, Type: "function", @@ -56,9 +56,6 @@ func buildConvHistoryFromMessages(msgs []model.AgentCLIMessage) []llm.Message { pendingToolCallIDs = pendingToolCallIDs[1:] } content := m.Content - if content == "" { - content = m.ToolOutput - } out = append(out, llm.Message{ Role: "tool", Name: m.ToolName, Content: content, ToolCallID: toolID, }) @@ -86,4 +83,4 @@ func validateToolArgsJSON(raw string) string { return "{}" } return raw -} \ No newline at end of file +} diff --git a/backend/controller/agent_cli_kubernetes.go b/backend/controller/agent_cli_kubernetes.go index d14b275..65fe626 100644 --- a/backend/controller/agent_cli_kubernetes.go +++ b/backend/controller/agent_cli_kubernetes.go @@ -108,4 +108,4 @@ func kubernetesEvolveHostKey(ctx *kubernetesAgentContext) string { return base + "/" + ctx.podName } return base -} \ No newline at end of file +} diff --git a/backend/controller/agent_install.go b/backend/controller/agent_install.go index 5d61559..9d24c37 100644 --- a/backend/controller/agent_install.go +++ b/backend/controller/agent_install.go @@ -339,7 +339,7 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) { } c.JSON(http.StatusOK, model.Response{ - Code: 200, + Code: 200, Message: func() string { if installed { return "Agent 安装成功,已自动启动并设置开机自启" diff --git a/backend/controller/agent_task.go b/backend/controller/agent_task.go index 4b0c062..0e93425 100644 --- a/backend/controller/agent_task.go +++ b/backend/controller/agent_task.go @@ -25,7 +25,7 @@ func NewAgentTaskHandler(db *gorm.DB) *AgentTaskHandler { func (h *AgentTaskHandler) CreateTask(c *gin.Context) { var req struct { Hostname string `json:"hostname"` - Type string `json:"type"` // command / script + Type string `json:"type"` // command / script Command string `json:"command"` Timeout int `json:"timeout"` } @@ -47,12 +47,12 @@ func (h *AgentTaskHandler) CreateTask(c *gin.Context) { username, _ := c.Get("username") task := model.AgentTask{ - TaskID: uuid.New().String(), - Hostname: req.Hostname, - Type: req.Type, - Command: req.Command, - Timeout: req.Timeout, - Status: "pending", + TaskID: uuid.New().String(), + Hostname: req.Hostname, + Type: req.Type, + Command: req.Command, + Timeout: req.Timeout, + Status: "pending", CreatedBy: fmt.Sprintf("%v", username), } @@ -180,11 +180,11 @@ func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) { } type ruleDef struct { - Name string - Metric string - Operator string + Name string + Metric string + Operator string Threshold float64 - Duration int + Duration int } defaultRules := []ruleDef{ diff --git a/backend/controller/alert_channels.go b/backend/controller/alert_channels.go index 6785ba9..3516910 100644 --- a/backend/controller/alert_channels.go +++ b/backend/controller/alert_channels.go @@ -473,5 +473,3 @@ func (h *AlertHandler) NotifyRecovery(ruleName, hostname, metric, message string } } } - - diff --git a/backend/controller/alert_contacts.go b/backend/controller/alert_contacts.go index a0b79fd..a65fd12 100644 --- a/backend/controller/alert_contacts.go +++ b/backend/controller/alert_contacts.go @@ -60,11 +60,21 @@ func (h *AlertHandler) UpdateContact(c *gin.Context) { return } updates := map[string]interface{}{} - if req.Name != "" { updates["name"] = req.Name } - if req.Phone != "" { updates["phone"] = req.Phone } - if req.Email != "" { updates["email"] = req.Email } - if req.ChannelType != "" { updates["channel_type"] = req.ChannelType } - if req.WebhookURL != "" { updates["webhook_url"] = req.WebhookURL } + if req.Name != "" { + updates["name"] = req.Name + } + if req.Phone != "" { + updates["phone"] = req.Phone + } + if req.Email != "" { + updates["email"] = req.Email + } + if req.ChannelType != "" { + updates["channel_type"] = req.ChannelType + } + if req.WebhookURL != "" { + updates["webhook_url"] = req.WebhookURL + } h.db.Model(&contact).Updates(updates) h.db.First(&contact, id) c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: contact}) @@ -127,8 +137,12 @@ func (h *AlertHandler) UpdateContactGroup(c *gin.Context) { return } updates := map[string]interface{}{} - if req.GroupName != "" { updates["group_name"] = req.GroupName } - if req.Notes != "" { updates["notes"] = req.Notes } + if req.GroupName != "" { + updates["group_name"] = req.GroupName + } + if req.Notes != "" { + updates["notes"] = req.Notes + } if len(req.ContactIDs) > 0 { idsJSON, _ := json.Marshal(req.ContactIDs) updates["contact_ids"] = string(idsJSON) diff --git a/backend/controller/alert_engine_monitor.go b/backend/controller/alert_engine_monitor.go index c8df9eb..de1b4d2 100644 --- a/backend/controller/alert_engine_monitor.go +++ b/backend/controller/alert_engine_monitor.go @@ -384,4 +384,4 @@ func isMiddlewareMetric(metric string) bool { } } return false -} \ No newline at end of file +} diff --git a/backend/controller/alert_engine_snmp.go b/backend/controller/alert_engine_snmp.go index 246b393..e1b0f00 100644 --- a/backend/controller/alert_engine_snmp.go +++ b/backend/controller/alert_engine_snmp.go @@ -181,4 +181,4 @@ func (e *AlertEngine) fireSNMPAlert(rule model.MonitorAlertRule, device model.SN // isSNMPMetric 判断 AgentAlert 是否属于 SNMP // Logan.Li func isSNMPMetric(metric string) bool { return len(metric) >= 5 && metric[:5] == "snmp_" -} \ No newline at end of file +} diff --git a/backend/controller/alert_metric_catalog.go b/backend/controller/alert_metric_catalog.go index 5192806..d4d500c 100644 --- a/backend/controller/alert_metric_catalog.go +++ b/backend/controller/alert_metric_catalog.go @@ -104,4 +104,4 @@ var metricCatalog = []MetricCatalogCategory{ // GET /api/v1/alerts/metric-catalog func (h *AlertHandler) GetMetricCatalog(c *gin.Context) { c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"categories": metricCatalog}}) -} \ No newline at end of file +} diff --git a/backend/controller/alert_notify_dispatch.go b/backend/controller/alert_notify_dispatch.go index 1ba16cf..c066f2a 100644 --- a/backend/controller/alert_notify_dispatch.go +++ b/backend/controller/alert_notify_dispatch.go @@ -178,4 +178,4 @@ func fallbackNotifyRecovery(db *gorm.DB, ruleName, hostname, metric, message str fmt.Printf("[AlertNotify] 飞书恢复回退失败 (渠道:%s): %v\n", ch.Name, err) } } -} \ No newline at end of file +} diff --git a/backend/controller/alert_notify_message.go b/backend/controller/alert_notify_message.go index be331dc..3b590fd 100644 --- a/backend/controller/alert_notify_message.go +++ b/backend/controller/alert_notify_message.go @@ -79,16 +79,16 @@ func notifyTemplateVars(ruleName, hostname, metric, severity, message string, va platform = defaultNotifyPlatformName } return map[string]string{ - "time": time.Now().Format("2006-01-02 15:04:05"), - "platform": platform, - "rule_name": ruleName, - "hostname": hostname, - "metric": metric, - "severity": severity, - "severity_label": sevLabel, - "message": message, - "value": fmt.Sprintf("%.2f", value), - "threshold": fmt.Sprintf("%.2f", threshold), + "time": time.Now().Format("2006-01-02 15:04:05"), + "platform": platform, + "rule_name": ruleName, + "hostname": hostname, + "metric": metric, + "severity": severity, + "severity_label": sevLabel, + "message": message, + "value": fmt.Sprintf("%.2f", value), + "threshold": fmt.Sprintf("%.2f", threshold), } } @@ -134,4 +134,4 @@ func buildAlertContent(ruleName, hostname, metric, severity, message string, val "**规则名称:** %s\\n**目标主机:** %s\\n**指标类型:** %s\\n**告警级别:** %s\\n**当前值:** %.2f\\n**阈值:** %.2f\\n**详细信息:** %s\\n**触发时间:** %s", ruleName, hostname, metric, severity, value, threshold, message, time.Now().Format("2006-01-02 15:04:05"), ) -} \ No newline at end of file +} diff --git a/backend/controller/alert_notify_policy.go b/backend/controller/alert_notify_policy.go index 3d119fd..d58970d 100644 --- a/backend/controller/alert_notify_policy.go +++ b/backend/controller/alert_notify_policy.go @@ -126,4 +126,4 @@ func isValidNotifyCategory(cat model.AlertNotifyCategory) bool { } } return false -} \ No newline at end of file +} diff --git a/backend/controller/alert_silence.go b/backend/controller/alert_silence.go index 58b8d88..b6aecf5 100644 --- a/backend/controller/alert_silence.go +++ b/backend/controller/alert_silence.go @@ -15,30 +15,30 @@ import ( // CreateSilenceRequest 创建静默规则请求 type CreateSilenceRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - RuleIDs []uint `json:"rule_ids"` - TargetIDs []uint `json:"target_ids"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + RuleIDs []uint `json:"rule_ids"` + TargetIDs []uint `json:"target_ids"` Matchers []model.AlertSilenceMatcher `json:"matchers"` - SilenceType string `json:"silence_type"` - StartTime string `json:"start_time"` - EndTime string `json:"end_time" binding:"required"` - Weekdays []int `json:"weekdays"` - Status *int `json:"status"` + SilenceType string `json:"silence_type"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time" binding:"required"` + Weekdays []int `json:"weekdays"` + Status *int `json:"status"` } // UpdateSilenceRequest 更新静默规则请求 type UpdateSilenceRequest struct { - Name string `json:"name"` - Description string `json:"description"` - RuleIDs []uint `json:"rule_ids"` - TargetIDs []uint `json:"target_ids"` + Name string `json:"name"` + Description string `json:"description"` + RuleIDs []uint `json:"rule_ids"` + TargetIDs []uint `json:"target_ids"` Matchers []model.AlertSilenceMatcher `json:"matchers"` - SilenceType string `json:"silence_type"` - StartTime string `json:"start_time"` - EndTime string `json:"end_time"` - Weekdays []int `json:"weekdays"` - Status *int `json:"status"` + SilenceType string `json:"silence_type"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + Weekdays []int `json:"weekdays"` + Status *int `json:"status"` } // ListSilences GET /alerts/silences @@ -60,11 +60,11 @@ func (h *AlertHandler) ListSilences(c *gin.Context) { // 解析 JSON 字段为前端友好的格式 type SilenceWithParsed struct { model.AlertSilence - RuleIDList []uint `json:"rule_id_list"` - TargetIDList []uint `json:"target_id_list"` - MatcherList []model.AlertSilenceMatcher `json:"matcher_list"` - WeekdayList []int `json:"weekday_list"` - IsActive bool `json:"is_active"` + RuleIDList []uint `json:"rule_id_list"` + TargetIDList []uint `json:"target_id_list"` + MatcherList []model.AlertSilenceMatcher `json:"matcher_list"` + WeekdayList []int `json:"weekday_list"` + IsActive bool `json:"is_active"` } var result []SilenceWithParsed diff --git a/backend/controller/alert_unified.go b/backend/controller/alert_unified.go index f205988..86af4bf 100644 --- a/backend/controller/alert_unified.go +++ b/backend/controller/alert_unified.go @@ -13,19 +13,19 @@ import ( // UnifiedAlertItem 统一告警事件视图 // Logan.Li type UnifiedAlertItem struct { - ID string `json:"id"` - SourceType string `json:"source_type"` - RuleName string `json:"rule_name"` - Title string `json:"title"` - Message string `json:"message"` - Severity string `json:"severity"` - Status string `json:"status"` - Target string `json:"target"` - Metric string `json:"metric"` - Value float64 `json:"value"` - Threshold float64 `json:"threshold"` - StartedAt string `json:"started_at"` - ResolvedAt *string `json:"resolved_at,omitempty"` + ID string `json:"id"` + SourceType string `json:"source_type"` + RuleName string `json:"rule_name"` + Title string `json:"title"` + Message string `json:"message"` + Severity string `json:"severity"` + Status string `json:"status"` + Target string `json:"target"` + Metric string `json:"metric"` + Value float64 `json:"value"` + Threshold float64 `json:"threshold"` + StartedAt string `json:"started_at"` + ResolvedAt *string `json:"resolved_at,omitempty"` } // ListUnifiedAlerts 统一告警列表(主机 + 中间件 + 站点 + 日志) @@ -212,4 +212,4 @@ func countSeverityStatus(items []UnifiedAlertItem, sev, status string) int { } } return n -} \ No newline at end of file +} diff --git a/backend/controller/approval_process.go b/backend/controller/approval_process.go index 0606339..04d067a 100644 --- a/backend/controller/approval_process.go +++ b/backend/controller/approval_process.go @@ -114,8 +114,8 @@ func (h *ApprovalHandler) CreateProcess(c *gin.Context) { var workflowSteps []model.ApprovalWorkflowStep stepCount := 1 var approvalSteps []struct { - name string - stepType string + name string + stepType string approverIDs []uint } if workflow.ID > 0 { @@ -126,8 +126,8 @@ func (h *ApprovalHandler) CreateProcess(c *gin.Context) { } stepCount++ approvalSteps = append(approvalSteps, struct { - name string - stepType string + name string + stepType string approverIDs []uint }{ws.Name, ws.StepType, ParseApproverIDs(ws.ApproverIDs)}) } diff --git a/backend/controller/approval_workflow.go b/backend/controller/approval_workflow.go index 6c905c4..1ca38ec 100644 --- a/backend/controller/approval_workflow.go +++ b/backend/controller/approval_workflow.go @@ -111,14 +111,14 @@ func (h *WorkflowHandler) CreateWorkflow(c *gin.Context) { for _, s := range req.Steps { step := model.ApprovalWorkflowStep{ - WorkflowID: workflow.ID, - StepNumber: s.StepNumber, - Name: s.Name, - StepType: s.StepType, - ApproverIDs: s.ApproverIDs, - IsRequired: s.IsRequired, - TimeoutHours: s.TimeoutHours, - AutoApprove: s.AutoApproveTimeout, + WorkflowID: workflow.ID, + StepNumber: s.StepNumber, + Name: s.Name, + StepType: s.StepType, + ApproverIDs: s.ApproverIDs, + IsRequired: s.IsRequired, + TimeoutHours: s.TimeoutHours, + AutoApprove: s.AutoApproveTimeout, } if step.TimeoutHours == 0 { step.TimeoutHours = 24 @@ -195,14 +195,14 @@ func (h *WorkflowHandler) UpdateWorkflow(c *gin.Context) { tx.Where("workflow_id = ?", workflow.ID).Delete(&model.ApprovalWorkflowStep{}) for _, s := range req.Steps { step := model.ApprovalWorkflowStep{ - WorkflowID: workflow.ID, - StepNumber: s.StepNumber, - Name: s.Name, - StepType: s.StepType, - ApproverIDs: s.ApproverIDs, - IsRequired: s.IsRequired, - TimeoutHours: s.TimeoutHours, - AutoApprove: s.AutoApproveTimeout, + WorkflowID: workflow.ID, + StepNumber: s.StepNumber, + Name: s.Name, + StepType: s.StepType, + ApproverIDs: s.ApproverIDs, + IsRequired: s.IsRequired, + TimeoutHours: s.TimeoutHours, + AutoApprove: s.AutoApproveTimeout, } if step.TimeoutHours == 0 { step.TimeoutHours = 24 diff --git a/backend/controller/billing_sync.go b/backend/controller/billing_sync.go index 2a8ad74..3385e17 100644 --- a/backend/controller/billing_sync.go +++ b/backend/controller/billing_sync.go @@ -82,4 +82,4 @@ func (h *BillingHandler) SyncAccountBilling(c *gin.Context) { } c.JSON(http.StatusOK, model.Response{Code: 200, Message: msg, Data: result}) -} \ No newline at end of file +} diff --git a/backend/controller/build_node.go b/backend/controller/build_node.go index 247c3e8..0e02ad6 100644 --- a/backend/controller/build_node.go +++ b/backend/controller/build_node.go @@ -34,7 +34,7 @@ func (h *PipelineHandler) ListBuildNodes(c *gin.Context) { c.JSON(http.StatusOK, model.Response{ Code: 200, Data: map[string]interface{}{ - "list": nodes, + "list": nodes, "total": len(nodes), "master": map[string]interface{}{ "name": "Master", @@ -221,4 +221,4 @@ func (h *PipelineHandler) UpdateBuildMaster(c *gin.Context) { "name": "Master", "max_executors": cfg.MaxExecutors, "running_count": running, }, }) -} \ No newline at end of file +} diff --git a/backend/controller/cloud_accounts.go b/backend/controller/cloud_accounts.go index c67c24b..9008321 100644 --- a/backend/controller/cloud_accounts.go +++ b/backend/controller/cloud_accounts.go @@ -199,4 +199,3 @@ func (h *CloudAccountHandler) DeleteCloudAccount(c *gin.Context) { Message: "删除成功", }) } - diff --git a/backend/controller/cloud_domain.go b/backend/controller/cloud_domain.go index 9084537..176b679 100644 --- a/backend/controller/cloud_domain.go +++ b/backend/controller/cloud_domain.go @@ -227,4 +227,3 @@ func (h *DomainHandler) UpdateDomain(c *gin.Context) { Data: domain, }) } - diff --git a/backend/controller/cloud_platform.go b/backend/controller/cloud_platform.go index 712a5b3..a428e5f 100644 --- a/backend/controller/cloud_platform.go +++ b/backend/controller/cloud_platform.go @@ -129,4 +129,3 @@ func (h *PlatformHandler) DeletePlatform(c *gin.Context) { } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"}) } - diff --git a/backend/controller/cloud_sync.go b/backend/controller/cloud_sync.go index 1eb8bdb..03d38c8 100644 --- a/backend/controller/cloud_sync.go +++ b/backend/controller/cloud_sync.go @@ -227,10 +227,28 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) { ip = inst.InstanceID } - // 检查是否已存在(按 IP 或 hostname) - var count int64 - h.db.Model(&model.Server{}).Where("ip = ? OR hostname = ?", ip, inst.Hostname).Count(&count) - if count > 0 { + hostname := cloudImportHostname(inst) + alias := cloudImportAlias(inst) + + // 检查是否已存在(优先实例 ID,其次 IP/hostname),重复导入只补空别名,避免覆盖人工命名。 // Logan.Li + var existing model.Server + existingQuery := h.db.Where("ip = ? OR hostname = ?", ip, hostname) + if inst.InstanceID != "" { + existingQuery = h.db.Where("cloud_instance_id = ? OR ip = ? OR hostname = ?", inst.InstanceID, ip, hostname) + } + if err := existingQuery.Take(&existing).Error; err == nil { + updates := map[string]interface{}{ + "cloud_instance_name": inst.InstanceName, + "cloud_instance_id": inst.InstanceID, + "description": "从 " + account.Provider + " 导入 (" + inst.InstanceID + ")", + } + if existing.Alias == "" && alias != "" { + updates["alias"] = alias + } + h.db.Model(&existing).Updates(updates) + skipped++ + continue + } else if err != gorm.ErrRecordNotFound { skipped++ continue } @@ -264,33 +282,28 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) { if ipAddr == "" { ipAddr = inst.InstanceID } - hostname := inst.Hostname - if hostname == "" { - hostname = inst.InstanceName - } - if hostname == "" { - hostname = inst.InstanceID - } - // 注意:servers 表的 cloud_id 外键引用的是 clouds 表(非 cloud_accounts), // region_id 外键引用 regions 表。未找到关联时设为 NULL 避免外键约束失败。 server := model.Server{ - Hostname: hostname, - IP: ipAddr, - PublicIP: inst.PublicIP, - Port: defaultPort, - Username: username, - AuthType: "password", - OSType: osType, - Protocol: protocol, - RDPPort: 3389, - OS: osName, - CPU: inst.CPU, - Memory: int64(inst.Memory), - Status: status, - RegionID: regionID, // nil = NULL - Description: "从 " + account.Provider + " 导入 (" + inst.InstanceID + ")", - AgentStatus: "offline", + Hostname: hostname, + Alias: alias, + CloudInstanceName: inst.InstanceName, + CloudInstanceID: inst.InstanceID, + IP: ipAddr, + PublicIP: inst.PublicIP, + Port: defaultPort, + Username: username, + AuthType: "password", + OSType: osType, + Protocol: protocol, + RDPPort: 3389, + OS: osName, + CPU: inst.CPU, + Memory: int64(inst.Memory), + Status: status, + RegionID: regionID, // nil = NULL + Description: "从 " + account.Provider + " 导入 (" + inst.InstanceID + ")", + AgentStatus: "offline", } if protocol == "rdp" && hasRDPCred { applyCredential(rdpCred, rdpPlain, &server) @@ -317,3 +330,31 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) { }, }) } + +func cloudImportHostname(inst cloud.CloudInstance) string { + // hostname 保留技术主机名;展示友好名称写入 alias/cloud_instance_name。 // Logan.Li + if inst.Hostname != "" && inst.Hostname != inst.InstanceID { + return inst.Hostname + } + if inst.InstanceName != "" && inst.InstanceName != inst.InstanceID { + return inst.InstanceName + } + if inst.PrivateIP != "" { + return inst.PrivateIP + } + if inst.PublicIP != "" { + return inst.PublicIP + } + if inst.InstanceName != "" { + return inst.InstanceName + } + return inst.InstanceID +} + +func cloudImportAlias(inst cloud.CloudInstance) string { + // 云导入初始别名优先使用控制台实例名称,后续人工别名不被同步覆盖。 // Logan.Li + if inst.InstanceName != "" && inst.InstanceName != inst.InstanceID { + return inst.InstanceName + } + return "" +} diff --git a/backend/controller/custom_dashboard.go b/backend/controller/custom_dashboard.go deleted file mode 100644 index 55624e1..0000000 --- a/backend/controller/custom_dashboard.go +++ /dev/null @@ -1,258 +0,0 @@ -package controller - -import ( - "encoding/json" - "net/http" - "strconv" - - ezops "ezops/model" - - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -// CustomDashboardHandler 自定义监控仪表盘 handler // Logan.Li -type CustomDashboardHandler struct { - db *gorm.DB -} - -// NewCustomDashboardHandler 创建 handler -func NewCustomDashboardHandler(db *gorm.DB) *CustomDashboardHandler { - return &CustomDashboardHandler{db: db} -} - -// ============================================ -// DTO -// ============================================ - -type CreateDashboardReq struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - Datasource string `json:"datasource"` - DatasourceURL string `json:"datasource_url"` - Panels string `json:"panels"` // JSON string - IsDefault bool `json:"is_default"` -} - -type UpdateDashboardReq struct { - Name *string `json:"name"` - Description *string `json:"description"` - Datasource *string `json:"datasource"` - DatasourceURL *string `json:"datasource_url"` - Panels *string `json:"panels"` - IsDefault *bool `json:"is_default"` - IsFavorite *bool `json:"is_favorite"` -} - -// ============================================ -// CRUD -// ============================================ - -// List 仪表盘列表 -// GET /api/v1/custom-dashboards -func (h *CustomDashboardHandler) List(c *gin.Context) { - var dashboards []ezops.CustomDashboard - if err := h.db.Order("is_default DESC, updated_at DESC").Find(&dashboards).Error; err != nil { - c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "查询失败: " + err.Error()}) - return - } - - // 转换为前端期望的格式(id 转为 string,panels JSON 解析为数组) - result := make([]gin.H, 0, len(dashboards)) - for _, d := range dashboards { - item := gin.H{ - "id": strconv.Itoa(int(d.ID)), - "name": d.Name, - "description": d.Description, - "datasource": d.Datasource, - "datasourceUrl": d.DatasourceURL, - "datasource_url": d.DatasourceURL, - "createdAt": d.CreatedAt, - "updatedAt": d.UpdatedAt, - "createdBy": d.CreatedBy, - "isDefault": d.IsDefault, - "is_default": d.IsDefault, - } - // 解析 panels JSON - var panels []interface{} - if d.Panels != "" { - if err := json.Unmarshal([]byte(d.Panels), &panels); err == nil { - item["panels"] = panels - } else { - item["panels"] = []interface{}{} - } - } else { - item["panels"] = []interface{}{} - } - result = append(result, item) - } - - c.JSON(http.StatusOK, ezops.Response{Code: 200, Data: gin.H{ - "list": result, - "total": len(result), - }}) -} - -// Get 获取单个仪表盘 -// GET /api/v1/custom-dashboards/:id -func (h *CustomDashboardHandler) Get(c *gin.Context) { - id := c.Param("id") - var d ezops.CustomDashboard - if err := h.db.First(&d, id).Error; err != nil { - c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"}) - return - } - - var panels []interface{} - if d.Panels != "" { - json.Unmarshal([]byte(d.Panels), &panels) - } - if panels == nil { - panels = []interface{}{} - } - - c.JSON(http.StatusOK, ezops.Response{Code: 200, Data: gin.H{ - "id": strconv.Itoa(int(d.ID)), - "name": d.Name, - "description": d.Description, - "datasource": d.Datasource, - "datasourceUrl": d.DatasourceURL, - "datasource_url": d.DatasourceURL, - "panels": panels, - "createdAt": d.CreatedAt, - "updatedAt": d.UpdatedAt, - "createdBy": d.CreatedBy, - "isDefault": d.IsDefault, - "is_default": d.IsDefault, - }}) -} - -// Create 创建仪表盘 -// POST /api/v1/custom-dashboards -func (h *CustomDashboardHandler) Create(c *gin.Context) { - var req CreateDashboardReq - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, ezops.Response{Code: 400, Message: "参数错误: " + err.Error()}) - return - } - - if req.Datasource == "" { - req.Datasource = "prometheus" - } - if req.Panels == "" { - req.Panels = "[]" - } - - // 如果设为默认,先清除其他默认 - if req.IsDefault { - h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ?", true).Update("is_default", false) - } - - // 从 JWT 获取用户名 - username := "admin" - if u, exists := c.Get("username"); exists { - if s, ok := u.(string); ok { - username = s - } - } - - d := ezops.CustomDashboard{ - Name: req.Name, - Description: req.Description, - Datasource: req.Datasource, - DatasourceURL: req.DatasourceURL, - Panels: req.Panels, - CreatedBy: username, - IsDefault: req.IsDefault, - } - - if err := h.db.Create(&d).Error; err != nil { - c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "创建失败: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "创建成功", Data: gin.H{ - "success": true, - "id": strconv.Itoa(int(d.ID)), - }}) -} - -// Update 更新仪表盘 -// PUT /api/v1/custom-dashboards/:id -func (h *CustomDashboardHandler) Update(c *gin.Context) { - id := c.Param("id") - var d ezops.CustomDashboard - if err := h.db.First(&d, id).Error; err != nil { - c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"}) - return - } - - var req UpdateDashboardReq - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, ezops.Response{Code: 400, Message: "参数错误: " + err.Error()}) - return - } - - updates := map[string]interface{}{} - if req.Name != nil { - updates["name"] = *req.Name - } - if req.Description != nil { - updates["description"] = *req.Description - } - if req.Datasource != nil { - updates["datasource"] = *req.Datasource - } - if req.DatasourceURL != nil { - updates["datasource_url"] = *req.DatasourceURL - } - if req.Panels != nil { - updates["panels"] = *req.Panels - } - if req.IsFavorite != nil { - updates["is_favorite"] = *req.IsFavorite - } - if req.IsDefault != nil && *req.IsDefault { - // 设为默认前先清除其他默认 - h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ? AND id != ?", true, d.ID).Update("is_default", false) - updates["is_default"] = true - } - - if len(updates) > 0 { - if err := h.db.Model(&d).Updates(updates).Error; err != nil { - c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "更新失败: " + err.Error()}) - return - } - } - - c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "更新成功", Data: gin.H{"success": true}}) -} - -// Delete 删除仪表盘 -// DELETE /api/v1/custom-dashboards/:id -func (h *CustomDashboardHandler) Delete(c *gin.Context) { - id := c.Param("id") - if err := h.db.Delete(&ezops.CustomDashboard{}, id).Error; err != nil { - c.JSON(http.StatusInternalServerError, ezops.Response{Code: 500, Message: "删除失败: " + err.Error()}) - return - } - c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "删除成功", Data: gin.H{"success": true}}) -} - -// SetDefault 设为默认仪表盘 -// POST /api/v1/custom-dashboards/:id/default -func (h *CustomDashboardHandler) SetDefault(c *gin.Context) { - id := c.Param("id") - var d ezops.CustomDashboard - if err := h.db.First(&d, id).Error; err != nil { - c.JSON(http.StatusNotFound, ezops.Response{Code: 404, Message: "仪表盘不存在"}) - return - } - - // 清除其他默认 - h.db.Model(&ezops.CustomDashboard{}).Where("is_default = ? AND id != ?", true, d.ID).Update("is_default", false) - // 设置当前为默认 - h.db.Model(&d).Update("is_default", true) - - c.JSON(http.StatusOK, ezops.Response{Code: 200, Message: "设置成功", Data: gin.H{"success": true}}) -} diff --git a/backend/controller/dashboard.go b/backend/controller/dashboard.go index be01b56..103297c 100644 --- a/backend/controller/dashboard.go +++ b/backend/controller/dashboard.go @@ -359,7 +359,7 @@ func (h *DashboardHandler) GetRecentReleases(c *gin.Context) { // @Success 200 {object} model.Response{data=[]model.MetricData} // @Router /dashboard/metrics [get] func (h *DashboardHandler) GetMetrics(c *gin.Context) { - hours := 6 // 默认6小时,减少扫描行数 + hours := 6 // 默认6小时,减少扫描行数 if hv := c.Query("hours"); hv != "" { if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 { hours = parsed diff --git a/backend/controller/deploy.go b/backend/controller/deploy.go deleted file mode 100644 index 6ccbe3b..0000000 --- a/backend/controller/deploy.go +++ /dev/null @@ -1,164 +0,0 @@ -package controller - -import ( - "net/http" - - "ezops/model" - - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -// DeployHandler 发布系统处理器 -type DeployHandler struct { - db *gorm.DB -} - -func NewDeployHandler(db *gorm.DB) *DeployHandler { - return &DeployHandler{db: db} -} - -// ==================== Git 仓库认证 ==================== - -func (h *DeployHandler) ListRepos(c *gin.Context) { - var items []model.PipelineRepo - h.db.Order("updated_at DESC").Find(&items) - c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: int64(len(items))}}) -} - -func (h *DeployHandler) GetRepo(c *gin.Context) { - var item model.PipelineRepo - if err := h.db.First(&item, c.Param("id")); err.Error != nil { - c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"}) - return - } - c.JSON(http.StatusOK, model.Response{Code: 200, Data: item}) -} - -func (h *DeployHandler) CreateRepo(c *gin.Context) { - var req struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - URL string `json:"url" binding:"required"` - AuthType model.RepoAuthType `json:"auth_type"` - CredentialID *uint `json:"credential_id"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) - return - } - if req.AuthType == "" { req.AuthType = model.RepoAuthToken } - item := model.PipelineRepo{ - Name: req.Name, Description: req.Description, URL: req.URL, - AuthType: req.AuthType, CredentialID: req.CredentialID, - CreatedBy: c.GetString("username"), - } - h.db.Create(&item) - c.JSON(http.StatusOK, model.Response{Code: 200, Data: item}) -} - -func (h *DeployHandler) UpdateRepo(c *gin.Context) { - var item model.PipelineRepo - if err := h.db.First(&item, c.Param("id")); err.Error != nil { - c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"}) - return - } - var req struct { - Name *string `json:"name"` - Description *string `json:"description"` - URL *string `json:"url"` - AuthType *model.RepoAuthType `json:"auth_type"` - CredentialID *uint `json:"credential_id"` - Enabled *bool `json:"enabled"` - } - c.ShouldBindJSON(&req) - updates := map[string]interface{}{} - if req.Name != nil { updates["name"] = *req.Name } - if req.Description != nil { updates["description"] = *req.Description } - if req.URL != nil { updates["url"] = *req.URL } - if req.AuthType != nil { updates["auth_type"] = *req.AuthType } - if req.Enabled != nil { updates["enabled"] = *req.Enabled } - if req.CredentialID != nil { updates["credential_id"] = req.CredentialID } - h.db.Model(&item).Updates(updates) - h.db.First(&item, item.ID) - c.JSON(http.StatusOK, model.Response{Code: 200, Data: item}) -} - -func (h *DeployHandler) DeleteRepo(c *gin.Context) { - h.db.Delete(&model.PipelineRepo{}, c.Param("id")) - c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"}) -} - -// ==================== 镜像仓库认证 ==================== - -func (h *DeployHandler) ListRegistries(c *gin.Context) { - var items []model.PipelineRegistry - h.db.Order("updated_at DESC").Find(&items) - c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: int64(len(items))}}) -} - -func (h *DeployHandler) GetRegistry(c *gin.Context) { - var item model.PipelineRegistry - if err := h.db.First(&item, c.Param("id")); err.Error != nil { - c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"}) - return - } - c.JSON(http.StatusOK, model.Response{Code: 200, Data: item}) -} - -func (h *DeployHandler) CreateRegistry(c *gin.Context) { - var req struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - RegistryURL string `json:"registry_url" binding:"required"` - Namespace string `json:"namespace"` - AuthType model.RegistryAuthType `json:"auth_type"` - CredentialID *uint `json:"credential_id"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) - return - } - if req.AuthType == "" { req.AuthType = model.RegistryAuthPassword } - item := model.PipelineRegistry{ - Name: req.Name, Description: req.Description, RegistryURL: req.RegistryURL, - Namespace: req.Namespace, AuthType: req.AuthType, CredentialID: req.CredentialID, - CreatedBy: c.GetString("username"), - } - h.db.Create(&item) - c.JSON(http.StatusOK, model.Response{Code: 200, Data: item}) -} - -func (h *DeployHandler) UpdateRegistry(c *gin.Context) { - var item model.PipelineRegistry - if err := h.db.First(&item, c.Param("id")); err.Error != nil { - c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"}) - return - } - var req struct { - Name *string `json:"name"` - Description *string `json:"description"` - RegistryURL *string `json:"registry_url"` - Namespace *string `json:"namespace"` - AuthType *model.RegistryAuthType `json:"auth_type"` - CredentialID *uint `json:"credential_id"` - Enabled *bool `json:"enabled"` - } - c.ShouldBindJSON(&req) - updates := map[string]interface{}{} - if req.Name != nil { updates["name"] = *req.Name } - if req.Description != nil { updates["description"] = *req.Description } - if req.RegistryURL != nil { updates["registry_url"] = *req.RegistryURL } - if req.Namespace != nil { updates["namespace"] = *req.Namespace } - if req.AuthType != nil { updates["auth_type"] = *req.AuthType } - if req.Enabled != nil { updates["enabled"] = *req.Enabled } - if req.CredentialID != nil { updates["credential_id"] = req.CredentialID } - h.db.Model(&item).Updates(updates) - h.db.First(&item, item.ID) - c.JSON(http.StatusOK, model.Response{Code: 200, Data: item}) -} - -func (h *DeployHandler) DeleteRegistry(c *gin.Context) { - h.db.Delete(&model.PipelineRegistry{}, c.Param("id")) - c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"}) -} diff --git a/backend/controller/deploy_environments.go b/backend/controller/deploy_environments.go index 207fdb1..fe1c3b5 100644 --- a/backend/controller/deploy_environments.go +++ b/backend/controller/deploy_environments.go @@ -35,7 +35,9 @@ func (h *PipelineHandler) CreateEnvironment(c *gin.Context) { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } - if req.Color == "" { req.Color = typeDefaultColor(req.Type) } + if req.Color == "" { + req.Color = typeDefaultColor(req.Type) + } item := model.DeployEnvironment{ Name: req.Name, DisplayName: req.DisplayName, Type: model.EnvironmentType(req.Type), Color: req.Color, @@ -67,13 +69,27 @@ func (h *PipelineHandler) UpdateEnvironment(c *gin.Context) { } c.ShouldBindJSON(&req) updates := map[string]interface{}{} - if req.Name != nil { updates["name"] = *req.Name } - if req.DisplayName != nil { updates["display_name"] = *req.DisplayName } - if req.Type != nil { updates["type"] = *req.Type } - if req.Color != nil { updates["color"] = *req.Color } - if req.SortOrder != nil { updates["sort_order"] = *req.SortOrder } - if req.Description != nil { updates["description"] = *req.Description } - if req.Enabled != nil { updates["enabled"] = *req.Enabled } + if req.Name != nil { + updates["name"] = *req.Name + } + if req.DisplayName != nil { + updates["display_name"] = *req.DisplayName + } + if req.Type != nil { + updates["type"] = *req.Type + } + if req.Color != nil { + updates["color"] = *req.Color + } + if req.SortOrder != nil { + updates["sort_order"] = *req.SortOrder + } + if req.Description != nil { + updates["description"] = *req.Description + } + if req.Enabled != nil { + updates["enabled"] = *req.Enabled + } h.db.Model(&item).Updates(updates) h.db.First(&item, id) c.JSON(http.StatusOK, model.Response{Code: 200, Data: item}) @@ -87,10 +103,14 @@ func (h *PipelineHandler) DeleteEnvironment(c *gin.Context) { func typeDefaultColor(t string) string { switch t { - case "production": return "#EF4444" - case "staging": return "#F59E0B" - case "testing": return "#10B981" - default: return "#3B82F6" // development + case "production": + return "#EF4444" + case "staging": + return "#F59E0B" + case "testing": + return "#10B981" + default: + return "#3B82F6" // development } } @@ -212,8 +232,8 @@ func (h *PipelineHandler) GetRunLog(c *gin.Context) { } c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{ - "run": run, - "logs": logs, + "run": run, + "logs": logs, }}) } diff --git a/backend/controller/deploy_groups.go b/backend/controller/deploy_groups.go index f4260d9..a195bec 100644 --- a/backend/controller/deploy_groups.go +++ b/backend/controller/deploy_groups.go @@ -53,8 +53,12 @@ func (h *PipelineHandler) UpdateGroup(c *gin.Context) { } c.ShouldBindJSON(&req) updates := map[string]interface{}{} - if req.Name != nil { updates["name"] = *req.Name } - if req.Description != nil { updates["description"] = *req.Description } + if req.Name != nil { + updates["name"] = *req.Name + } + if req.Description != nil { + updates["description"] = *req.Description + } h.db.Model(&group).Updates(updates) h.db.First(&group, id) c.JSON(http.StatusOK, model.Response{Code: 200, Data: group}) diff --git a/backend/controller/k8s_cluster.go b/backend/controller/k8s_cluster.go index c0d31df..8a1dd0b 100644 --- a/backend/controller/k8s_cluster.go +++ b/backend/controller/k8s_cluster.go @@ -216,28 +216,28 @@ func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) { Code: 200, Message: "success", Data: map[string]interface{}{ - "id": cluster.ID, - "name": cluster.Name, - "code": cluster.Code, - "version": cluster.Version, - "status": cluster.Status, - "config_type": cluster.ConfigType, - "api_server": cluster.APIServer, - "api_endpoint": cluster.APIEndpoint, - "description": cluster.Description, - "has_token": cluster.Token != "", - "has_kube_config": cluster.KubeConfig != "", + "id": cluster.ID, + "name": cluster.Name, + "code": cluster.Code, + "version": cluster.Version, + "status": cluster.Status, + "config_type": cluster.ConfigType, + "api_server": cluster.APIServer, + "api_endpoint": cluster.APIEndpoint, + "description": cluster.Description, + "has_token": cluster.Token != "", + "has_kube_config": cluster.KubeConfig != "", "node_count": cluster.NodeCount, "pod_count": cluster.PodCount, "service_count": cluster.ServiceCount, "deployment_count": cluster.DeploymentCount, "ingress_count": cluster.IngressCount, - "cpu_usage": cluster.CPUUsage, - "memory_usage": cluster.MemoryUsage, - "labels": cluster.Labels, - "last_sync_at": cluster.LastSyncAt, - "created_at": cluster.CreatedAt, - "updated_at": cluster.UpdatedAt, + "cpu_usage": cluster.CPUUsage, + "memory_usage": cluster.MemoryUsage, + "labels": cluster.Labels, + "last_sync_at": cluster.LastSyncAt, + "created_at": cluster.CreatedAt, + "updated_at": cluster.UpdatedAt, }, }) } @@ -320,7 +320,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) { // Sync nodes if nodes, err := client.ListNodes(); err == nil { // Use raw SQL for reliability (avoid GORM soft-delete / auto-timestamp issues) - h.db.Exec("DELETE FROM kubernetes_nodes WHERE cluster_id = ?", cluster.ID) + h.db.Exec("DELETE FROM k8s_nodes WHERE cluster_id = ?", cluster.ID) for _, n := range nodes.Items { role := "worker" if _, ok := n.Metadata.Labels["node-role.kubernetes.io/master"]; ok { @@ -358,7 +358,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) { memUsagePct = float64(memMi) / float64(memMB) * 100 } } - err2 := h.db.Exec(`INSERT INTO kubernetes_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, cpu_usage, memory_usage, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + err2 := h.db.Exec(`INSERT INTO k8s_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, cpu_usage, memory_usage, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, cluster.ID, n.Metadata.Name, ip, role, nodeStatus, n.Status.NodeInfo.OSImage, n.Status.NodeInfo.KubeletVersion, parseIntResource(n.Status.Capacity["cpu"]), parseMemResource(n.Status.Capacity["memory"]), parseIntResource(n.Status.Capacity["pods"]), cpuUsagePct, memUsagePct, string(labelsJSON), now, now).Error @@ -508,7 +508,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) { c.JSON(http.StatusOK, model.Response{ Code: 200, Message: msg, - Data: result, + Data: result, }) } diff --git a/backend/controller/k8s_compare.go b/backend/controller/k8s_compare.go index 773161a..b9f3818 100644 --- a/backend/controller/k8s_compare.go +++ b/backend/controller/k8s_compare.go @@ -14,27 +14,27 @@ func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) { h.db.Find(&clusters) type ClusterComparison struct { - ID uint `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - Status string `json:"status"` - Version string `json:"version"` - NodeCount int `json:"node_count"` - PodCount int `json:"pod_count"` - DeploymentCount int `json:"deployment_count"` - ServiceCount int `json:"service_count"` - CPUUsage float64 `json:"cpu_usage"` - MemoryUsage float64 `json:"memory_usage"` - APIServer string `json:"api_server"` - LastSyncAt *time.Time `json:"last_sync_at"` + ID uint `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Status string `json:"status"` + Version string `json:"version"` + NodeCount int `json:"node_count"` + PodCount int `json:"pod_count"` + DeploymentCount int `json:"deployment_count"` + ServiceCount int `json:"service_count"` + CPUUsage float64 `json:"cpu_usage"` + MemoryUsage float64 `json:"memory_usage"` + APIServer string `json:"api_server"` + LastSyncAt *time.Time `json:"last_sync_at"` - // Node-level aggregated data from kubernetes_nodes table - TotalCPUCores int `json:"total_cpu_cores"` - TotalMemoryMB int64 `json:"total_memory_mb"` - MasterCount int `json:"master_count"` - WorkerCount int `json:"worker_count"` - HealthyNodes int `json:"healthy_nodes"` - UnhealthyNodes int `json:"unhealthy_nodes"` + // Node-level aggregated data from k8s_nodes table + TotalCPUCores int `json:"total_cpu_cores"` + TotalMemoryMB int64 `json:"total_memory_mb"` + MasterCount int `json:"master_count"` + WorkerCount int `json:"worker_count"` + HealthyNodes int `json:"healthy_nodes"` + UnhealthyNodes int `json:"unhealthy_nodes"` } // 2. 获取所有节点的聚合数据(按 cluster_id 分组) @@ -64,8 +64,16 @@ func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) { agg.TotalCPU += n.CPU agg.TotalMemory += n.Memory if n.CPUUsage > 0 || n.MemoryUsage > 0 { - if agg.AvgCPUUsage == 0 { agg.AvgCPUUsage = n.CPUUsage } else { agg.AvgCPUUsage = (agg.AvgCPUUsage*float64(agg.NodeCount-1) + n.CPUUsage) / float64(agg.NodeCount) } - if agg.AvgMemUsage == 0 { agg.AvgMemUsage = n.MemoryUsage } else { agg.AvgMemUsage = (agg.AvgMemUsage*float64(agg.NodeCount-1) + n.MemoryUsage) / float64(agg.NodeCount) } + if agg.AvgCPUUsage == 0 { + agg.AvgCPUUsage = n.CPUUsage + } else { + agg.AvgCPUUsage = (agg.AvgCPUUsage*float64(agg.NodeCount-1) + n.CPUUsage) / float64(agg.NodeCount) + } + if agg.AvgMemUsage == 0 { + agg.AvgMemUsage = n.MemoryUsage + } else { + agg.AvgMemUsage = (agg.AvgMemUsage*float64(agg.NodeCount-1) + n.MemoryUsage) / float64(agg.NodeCount) + } } if n.Role == "master" { agg.MasterCount++ diff --git a/backend/controller/k8s_events.go b/backend/controller/k8s_events.go index 27ce6e3..00d471c 100644 --- a/backend/controller/k8s_events.go +++ b/backend/controller/k8s_events.go @@ -14,14 +14,23 @@ import ( func (h *KubernetesHandler) ListEvents(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Query("namespace") eventType := c.Query("type") // Normal or Warning events, err := client.ListEvents(ns) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type EventSummary struct { Namespace string `json:"namespace"` @@ -40,7 +49,9 @@ func (h *KubernetesHandler) ListEvents(c *gin.Context) { } result := []EventSummary{} for _, e := range events.Items { - if eventType != "" && e.Type != eventType { continue } + if eventType != "" && e.Type != eventType { + continue + } objectKind := e.InvolvedObject.Kind objectName := e.InvolvedObject.Name object := fmt.Sprintf("%s/%s", objectKind, objectName) diff --git a/backend/controller/k8s_hpa_cronjob.go b/backend/controller/k8s_hpa_cronjob.go index b0006f4..8d4e0e1 100644 --- a/backend/controller/k8s_hpa_cronjob.go +++ b/backend/controller/k8s_hpa_cronjob.go @@ -246,30 +246,45 @@ func (h *KubernetesHandler) DeleteCronJobNS(c *gin.Context) { } func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } hpas, err := client.ListHPAs() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type HPASummary struct { - Namespace string `json:"namespace"` - Name string `json:"name"` - TargetKind string `json:"target_kind"` - TargetName string `json:"target_name"` - MinReplicas int `json:"min_replicas"` - MaxReplicas int `json:"max_replicas"` - CPUPercent int `json:"cpu_percent"` - MemPercent int `json:"mem_percent"` + Namespace string `json:"namespace"` + Name string `json:"name"` + TargetKind string `json:"target_kind"` + TargetName string `json:"target_name"` + MinReplicas int `json:"min_replicas"` + MaxReplicas int `json:"max_replicas"` + CPUPercent int `json:"cpu_percent"` + MemPercent int `json:"mem_percent"` CurrentReplicas int `json:"current_replicas"` } result := []HPASummary{} for _, h := range hpas.Items { - if h.Metadata.Namespace != ns { continue } - cpu := 0; mem := 0 + if h.Metadata.Namespace != ns { + continue + } + cpu := 0 + mem := 0 for _, m := range h.Spec.Metrics { if m.Resource != nil { - if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization } - if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization } + if m.Resource.Name == "cpu" { + cpu = m.Resource.Target.AverageUtilization + } + if m.Resource.Name == "memory" { + mem = m.Resource.Target.AverageUtilization + } } } result = append(result, HPASummary{ @@ -288,80 +303,165 @@ func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) { } func (h *KubernetesHandler) createHPAWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } var req k8sclient.CreateHPARequest - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return } - if req.Namespace == "" { req.Namespace = ns } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误"}) + return + } + if req.Namespace == "" { + req.Namespace = ns + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } hpa, err := client.CreateHPA(req) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Data: hpa}) } func (h *KubernetesHandler) deleteHPAWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.DeleteHPA(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.DeleteHPA(ns, c.Param("name")); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "已删除"}) } func (h *KubernetesHandler) listCronJobsWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } cjs, err := client.ListCronJobs() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } filtered := []k8sclient.CronJobItem{} - for _, cj := range cjs.Items { if cj.Metadata.Namespace == ns { filtered = append(filtered, cj) } } + for _, cj := range cjs.Items { + if cj.Metadata.Namespace == ns { + filtered = append(filtered, cj) + } + } c.JSON(200, model.Response{Code: 200, Data: filtered}) } func (h *KubernetesHandler) createCronJobWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } var req k8sclient.CreateCronJobRequest - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return } - if req.Namespace == "" { req.Namespace = ns } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误"}) + return + } + if req.Namespace == "" { + req.Namespace = ns + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } cj, err := client.CreateCronJob(req) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Data: cj}) } func (h *KubernetesHandler) suspendCronJobWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } - var req struct { Suspend bool `json:"suspend"` }; c.ShouldBindJSON(&req) + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } + var req struct { + Suspend bool `json:"suspend"` + } + c.ShouldBindJSON(&req) client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil { c.JSON(500, model.Response{Code: 500, Message: "操作失败: " + err.Error()}); return } - action := "恢复"; if req.Suspend { action = "暂停" } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "操作失败: " + err.Error()}) + return + } + action := "恢复" + if req.Suspend { + action = "暂停" + } c.JSON(200, model.Response{Code: 200, Message: "CronJob 已" + action}) } func (h *KubernetesHandler) triggerCronJobWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.TriggerCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "触发失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.TriggerCronJob(ns, c.Param("name")); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "触发失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "手动触发成功"}) } func (h *KubernetesHandler) deleteCronJobWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.DeleteCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.DeleteCronJob(ns, c.Param("name")); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "已删除"}) } func (h *KubernetesHandler) ListHPAsAllNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } hpas, err := client.ListHPAs() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type HPASummary struct { Namespace string `json:"namespace"` Name string `json:"name"` @@ -375,11 +475,16 @@ func (h *KubernetesHandler) ListHPAsAllNS(c *gin.Context) { } result := []HPASummary{} for _, h := range hpas.Items { - cpu := 0; mem := 0 + cpu := 0 + mem := 0 for _, m := range h.Spec.Metrics { if m.Resource != nil { - if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization } - if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization } + if m.Resource.Name == "cpu" { + cpu = m.Resource.Target.AverageUtilization + } + if m.Resource.Name == "memory" { + mem = m.Resource.Target.AverageUtilization + } } } result = append(result, HPASummary{ diff --git a/backend/controller/k8s_namespace.go b/backend/controller/k8s_namespace.go index 57bb4fd..bf4fa40 100644 --- a/backend/controller/k8s_namespace.go +++ b/backend/controller/k8s_namespace.go @@ -38,16 +38,16 @@ func (h *KubernetesHandler) ListNamespaces(c *gin.Context) { // NamespaceDetail is the enriched namespace info for the management page type NamespaceDetail struct { - Name string `json:"name"` - Status string `json:"status"` - Age string `json:"age"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations,omitempty"` - ResourceCounts map[string]int `json:"resource_counts"` - HasResourceQuota bool `json:"has_resource_quota"` - HasLimitRange bool `json:"has_limit_range"` - ResourceQuotas []k8sclient.ResourceQuotaItem `json:"resource_quotas,omitempty"` - LimitRanges []k8sclient.LimitRangeItem `json:"limit_ranges,omitempty"` + Name string `json:"name"` + Status string `json:"status"` + Age string `json:"age"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations,omitempty"` + ResourceCounts map[string]int `json:"resource_counts"` + HasResourceQuota bool `json:"has_resource_quota"` + HasLimitRange bool `json:"has_limit_range"` + ResourceQuotas []k8sclient.ResourceQuotaItem `json:"resource_quotas,omitempty"` + LimitRanges []k8sclient.LimitRangeItem `json:"limit_ranges,omitempty"` } // GetNamespacesDetail returns all namespaces with metadata, resource counts and quota info @@ -110,11 +110,11 @@ func (h *KubernetesHandler) GetNamespacesDetail(c *gin.Context) { for _, ns := range nsList.Items { name := ns.Metadata.Name nd := NamespaceDetail{ - Name: name, - Status: ns.Status.Phase, - Age: formatAge(ns.Metadata.CreationTimestamp), - Labels: ns.Metadata.Labels, - Annotations: ns.Metadata.Annotations, + Name: name, + Status: ns.Status.Phase, + Age: formatAge(ns.Metadata.CreationTimestamp), + Labels: ns.Metadata.Labels, + Annotations: ns.Metadata.Annotations, ResourceCounts: map[string]int{ "deployments": depCounts[name], "pods": podCounts[name], @@ -181,21 +181,27 @@ func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) { if deps, err := client.ListDeployments(); err == nil { count := 0 for _, d := range deps.Items { - if d.Metadata.Namespace == nsName { count++ } + if d.Metadata.Namespace == nsName { + count++ + } } nd.ResourceCounts["deployments"] = count } if pods, err := client.ListPods(); err == nil { count := 0 for _, p := range pods.Items { - if p.Metadata.Namespace == nsName { count++ } + if p.Metadata.Namespace == nsName { + count++ + } } nd.ResourceCounts["pods"] = count } if svcs, err := client.ListServices(); err == nil { count := 0 for _, s := range svcs.Items { - if s.Metadata.Namespace == nsName { count++ } + if s.Metadata.Namespace == nsName { + count++ + } } nd.ResourceCounts["services"] = count } diff --git a/backend/controller/k8s_node.go b/backend/controller/k8s_node.go index 3cda9ee..0d8e000 100644 --- a/backend/controller/k8s_node.go +++ b/backend/controller/k8s_node.go @@ -305,20 +305,20 @@ func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) { } detail := map[string]interface{}{ - "name": node.Metadata.Name, - "role": role, - "status": nodeStatus, - "creation_time": node.Metadata.CreationTimestamp, - "internal_ip": internalIP, - "external_ip": externalIP, - "hostname": hostname, - "pod_cidr": node.Spec.PodCIDR, + "name": node.Metadata.Name, + "role": role, + "status": nodeStatus, + "creation_time": node.Metadata.CreationTimestamp, + "internal_ip": internalIP, + "external_ip": externalIP, + "hostname": hostname, + "pod_cidr": node.Spec.PodCIDR, "container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion, - "kubelet_version": node.Status.NodeInfo.KubeletVersion, - "os_image": node.Status.NodeInfo.OSImage, - "kernel_version": node.Status.NodeInfo.KernelVersion, - "architecture": node.Status.NodeInfo.Architecture, - "operating_system": node.Status.NodeInfo.OperatingSystem, + "kubelet_version": node.Status.NodeInfo.KubeletVersion, + "os_image": node.Status.NodeInfo.OSImage, + "kernel_version": node.Status.NodeInfo.KernelVersion, + "architecture": node.Status.NodeInfo.Architecture, + "operating_system": node.Status.NodeInfo.OperatingSystem, // Capacity & Allocatable "capacity": node.Status.Capacity, "allocatable": node.Status.Allocatable, @@ -326,18 +326,18 @@ func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) { "cpu_usage_m": cpuUsageM, "mem_usage_mi": memUsageMi, // Allocation - "pod_count": podCount, - "cpu_request_m": totalCPURequestM, - "mem_request_mi": totalMemRequestMi, - "cpu_alloc_pct": cpuAllocPct, - "mem_alloc_pct": memAllocPct, - "pod_alloc_pct": podAllocPct, - "cpu_capacity": capCPU, - "cpu_allocatable": allocCPU, - "mem_capacity_mb": capMem, + "pod_count": podCount, + "cpu_request_m": totalCPURequestM, + "mem_request_mi": totalMemRequestMi, + "cpu_alloc_pct": cpuAllocPct, + "mem_alloc_pct": memAllocPct, + "pod_alloc_pct": podAllocPct, + "cpu_capacity": capCPU, + "cpu_allocatable": allocCPU, + "mem_capacity_mb": capMem, "mem_allocatable_mb": allocMem, - "pod_capacity": capPods, - "pod_allocatable": allocPods, + "pod_capacity": capPods, + "pod_allocatable": allocPods, // Lists "conditions": conditions, "taints": taints, @@ -397,20 +397,29 @@ func parseMemToMi(val string) int64 { // @Router /kubernetes-clusters/{id}/pods [get] func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Query("namespace") deps, err := client.ListDeployments() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type ContainerResource struct { - Name string `json:"name"` - CPUReq string `json:"cpu_request"` - CPULim string `json:"cpu_limit"` - MemReq string `json:"mem_request"` - MemLim string `json:"mem_limit"` + Name string `json:"name"` + CPUReq string `json:"cpu_request"` + CPULim string `json:"cpu_limit"` + MemReq string `json:"mem_request"` + MemLim string `json:"mem_limit"` } type CapacityRow struct { Namespace string `json:"namespace"` @@ -420,7 +429,9 @@ func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) { } result := []CapacityRow{} for _, d := range deps.Items { - if ns != "" && d.Metadata.Namespace != ns { continue } + if ns != "" && d.Metadata.Namespace != ns { + continue + } containers := []ContainerResource{} for _, c := range d.Spec.Template.Spec.Containers { cpuReq := c.Resources.Requests["cpu"] diff --git a/backend/controller/k8s_pod.go b/backend/controller/k8s_pod.go index 161e803..617b3d4 100644 --- a/backend/controller/k8s_pod.go +++ b/backend/controller/k8s_pod.go @@ -198,8 +198,8 @@ func (h *KubernetesHandler) ExecTerminal(c *gin.Context) { // Dial K8s API server via WebSocket with channel protocol subprotocol k8sDialer := &websocket.Dialer{ - TLSClientConfig: client.TLSConfig, - Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "v5.channel.k8s.io"}, + TLSClientConfig: client.TLSConfig, + Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "v5.channel.k8s.io"}, HandshakeTimeout: 10 * time.Second, } k8sHeader := http.Header{} @@ -354,30 +354,46 @@ func (h *KubernetesHandler) ExecInPodNS(c *gin.Context) { // Internal helpers that resolve clusterCode and inject namespace func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } pods, err := client.ListPods() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } // M2: inject pod metrics (best-effort) // Logan.Li podMetrics := buildPodMetricsMap(client, ns) type PodSummary struct { - Namespace string `json:"namespace"` - Name string `json:"name"` - Status string `json:"status"` - NodeName string `json:"node_name"` - IP string `json:"ip"` - RestartCount int `json:"restart_count"` - Image string `json:"image"` - CPUUsageM string `json:"cpu_usage_m,omitempty"` - MemUsageMi string `json:"mem_usage_mi,omitempty"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Status string `json:"status"` + NodeName string `json:"node_name"` + IP string `json:"ip"` + RestartCount int `json:"restart_count"` + Image string `json:"image"` + CPUUsageM string `json:"cpu_usage_m,omitempty"` + MemUsageMi string `json:"mem_usage_mi,omitempty"` Conditions []k8sclient.PodCondition `json:"conditions,omitempty"` } result := []PodSummary{} for _, p := range pods.Items { - if p.Metadata.Namespace != ns { continue } - image := ""; if len(p.Spec.Containers) > 0 { image = p.Spec.Containers[0].Image } - restarts := 0; if len(p.Status.ContainerStatuses) > 0 { restarts = p.Status.ContainerStatuses[0].RestartCount } + if p.Metadata.Namespace != ns { + continue + } + image := "" + if len(p.Spec.Containers) > 0 { + image = p.Spec.Containers[0].Image + } + restarts := 0 + if len(p.Status.ContainerStatuses) > 0 { + restarts = p.Status.ContainerStatuses[0].RestartCount + } ps := PodSummary{ Namespace: p.Metadata.Namespace, Name: p.Metadata.Name, @@ -398,7 +414,9 @@ func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) { } func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } podName := c.Param("podName") stream := c.Query("stream") == "true" // 日志增强参数:container / sinceTime / previous / tailLines(兼容 tail) // Logan.Li @@ -416,34 +434,61 @@ func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) { Previous: c.Query("previous") == "true", } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } if stream { body, err := client.GetPodLogsStreamWithOptions(ns, podName, opts) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取日志流失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "获取日志流失败: " + err.Error()}) + return + } defer body.Close() c.Header("Content-Type", "text/plain; charset=utf-8") c.Header("Cache-Control", "no-cache") c.Stream(func(w io.Writer) bool { - buf := make([]byte, 4096); n, err := body.Read(buf) - if n > 0 { w.Write(buf[:n]); return true } + buf := make([]byte, 4096) + n, err := body.Read(buf) + if n > 0 { + w.Write(buf[:n]) + return true + } return err == nil }) return } logs, err := client.GetPodLogsWithOptions(ns, podName, opts) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取日志失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "获取日志失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "success", Data: gin.H{"logs": logs, "pod": podName, "namespace": ns}}) } func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } podName := c.Param("podName") - var req struct { Command []string `json:"command"`; Container string `json:"container"` } - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return } + var req struct { + Command []string `json:"command"` + Container string `json:"container"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } result, err := client.ExecInPod(ns, podName, req.Container, req.Command) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "执行失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "执行失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Data: result}) } @@ -454,11 +499,20 @@ func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) { // ListDeploymentsAllNS lists all deployments across all namespaces using clusterCode. func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } pods, err := client.ListPods() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } // M2: inject pod metrics (best-effort) // Logan.Li podMetrics := buildPodMetricsMap(client, "") type PodSummary struct { @@ -474,8 +528,14 @@ func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) { } result := []PodSummary{} for _, p := range pods.Items { - image := ""; if len(p.Spec.Containers) > 0 { image = p.Spec.Containers[0].Image } - restarts := 0; if len(p.Status.ContainerStatuses) > 0 { restarts = p.Status.ContainerStatuses[0].RestartCount } + image := "" + if len(p.Spec.Containers) > 0 { + image = p.Spec.Containers[0].Image + } + restarts := 0 + if len(p.Status.ContainerStatuses) > 0 { + restarts = p.Status.ContainerStatuses[0].RestartCount + } ps := PodSummary{ Namespace: p.Metadata.Namespace, Name: p.Metadata.Name, @@ -559,58 +619,80 @@ func parseMemResource(val string) int64 { // formatCpuFromNano converts K8s metrics CPU (nanocores "80410173n") to millicores "80m" func formatCpuFromNano(raw string) string { - if raw == "" { return "" } + if raw == "" { + return "" + } val := raw val = strings.TrimSuffix(val, "n") var nano int64 fmt.Sscanf(val, "%d", &nano) milli := nano / 1000000 - if milli < 1 { milli = 1 } + if milli < 1 { + milli = 1 + } return fmt.Sprintf("%dm", milli) } // parseCpuNanoToMilli converts K8s metrics CPU (nanocores "100354992n") to millicores (int) // Logan.Li func parseCpuNanoToMilli(raw string) int { - if raw == "" { return 0 } + if raw == "" { + return 0 + } val := strings.TrimSuffix(raw, "n") var nano int64 fmt.Sscanf(val, "%d", &nano) milli := nano / 1000000 - if milli < 1 { milli = 1 } + if milli < 1 { + milli = 1 + } return int(milli) } // formatMemFromKi converts K8s metrics memory (Ki "2154292Ki") to Mi "2105Mi" func formatMemFromKi(raw string) string { - if raw == "" { return "" } + if raw == "" { + return "" + } val := strings.TrimSuffix(raw, "Ki") var ki int64 fmt.Sscanf(val, "%d", &ki) mi := ki / 1024 - if mi < 1 { mi = 1 } + if mi < 1 { + mi = 1 + } return fmt.Sprintf("%dMi", mi) } // parseMemKiToMi converts K8s metrics memory (Ki "2154292Ki") to Mi (int) // Logan.Li func parseMemKiToMi(raw string) int { - if raw == "" { return 0 } + if raw == "" { + return 0 + } val := strings.TrimSuffix(raw, "Ki") var ki int64 fmt.Sscanf(val, "%d", &ki) return int(ki / 1024) } - // GetResourceYAMLNS returns the YAML of a K8s resource func (h *KubernetesHandler) GetPodMetrics(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Query("namespace") metrics, err := client.ListPodMetrics(ns) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "metrics API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "metrics API error: " + err.Error()}) + return + } type ContainerMetric struct { Name string `json:"name"` @@ -643,13 +725,22 @@ func (h *KubernetesHandler) DeletePodNS(c *gin.Context) { } func (h *KubernetesHandler) deletePodWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } podName := c.Param("podName") - if podName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 podName"}); return } + if podName == "" { + c.JSON(400, model.Response{Code: 400, Message: "缺少 podName"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } if err := client.DeletePod(ns, podName); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "删除 Pod 失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "删除 Pod 失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("Pod %s 已删除", podName)}) } diff --git a/backend/controller/k8s_pod_files.go b/backend/controller/k8s_pod_files.go index cc87dad..7167990 100644 --- a/backend/controller/k8s_pod_files.go +++ b/backend/controller/k8s_pod_files.go @@ -19,20 +19,33 @@ func (h *KubernetesHandler) ListPodFilesNS(c *gin.Context) { } func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } podName := c.Param("podName") var req struct { Path string `json:"path"` Container string `json:"container"` } - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return } - if req.Path == "" { req.Path = "/" } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}) + return + } + if req.Path == "" { + req.Path = "/" + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } // ls -la — BusyBox ignores --time-style, use default "Mon DD HH:MM" format cmd := []string{"ls", "-la", req.Path} result, err := client.ExecInPod(ns, podName, req.Container, cmd) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()}) + return + } type FileInfo struct { Name string `json:"name"` @@ -45,11 +58,17 @@ func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) { output := result.Stdout // WebSocket output is already clean text for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "total ") { continue } + if line == "" || strings.HasPrefix(line, "total ") { + continue + } fields := strings.Fields(line) - if len(fields) < 7 { continue } + if len(fields) < 7 { + continue + } mode := fields[0] - if len(mode) == 0 || (mode[0] != 'd' && mode[0] != '-' && mode[0] != 'l') { continue } + if len(mode) == 0 || (mode[0] != 'd' && mode[0] != '-' && mode[0] != 'l') { + continue + } size := fields[4] // BusyBox ls date format: "Jun 23 02:17" (3 fields: month day time) // GNU ls --time-style: "2026-06-23T02:17:00" (1 field) @@ -64,7 +83,9 @@ func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) { modTime = strings.Join(fields[5:8], " ") name = strings.Join(fields[8:], " ") } - if name == "" || name == "." || name == ".." { continue } + if name == "" || name == "." || name == ".." { + continue + } files = append(files, FileInfo{ Name: name, Size: size, @@ -82,16 +103,27 @@ func (h *KubernetesHandler) DownloadPodFileNS(c *gin.Context) { } func (h *KubernetesHandler) downloadPodFileWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } podName := c.Param("podName") filePath := c.Query("path") container := c.Query("container") - if filePath == "" { c.JSON(400, model.Response{Code: 400, Message: "path 参数不能为空"}); return } + if filePath == "" { + c.JSON(400, model.Response{Code: 400, Message: "path 参数不能为空"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } cmd := []string{"cat", filePath} result, err := client.ExecInPod(ns, podName, container, cmd) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()}) + return + } content := result.Stdout fileName := path.Base(filePath) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) diff --git a/backend/controller/k8s_resource.go b/backend/controller/k8s_resource.go index 0590ecd..90a98c1 100644 --- a/backend/controller/k8s_resource.go +++ b/backend/controller/k8s_resource.go @@ -223,7 +223,6 @@ func (h *KubernetesHandler) SaveTopology(c *gin.Context) { c.JSON(http.StatusOK, model.Response{Code: 200, Message: "保存成功"}) } - // SyncCluster 同步集群资源(连接真实K8s API拉取数据) func (h *KubernetesHandler) ListConfigMaps(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) @@ -313,11 +312,19 @@ func (h *KubernetesHandler) ListIngressesNS(c *gin.Context) { } func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } svcs, err := client.ListServices() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type SvcPortDetail struct { Name string `json:"name,omitempty"` Port int `json:"port"` @@ -326,17 +333,19 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) { Protocol string `json:"protocol,omitempty"` } type SvcSummary struct { - Namespace string `json:"namespace"` - Name string `json:"name"` - Type string `json:"type"` - ClusterIP string `json:"cluster_ip"` - Ports int `json:"ports"` - PortList []SvcPortDetail `json:"portList,omitempty"` - Selector map[string]string `json:"selector,omitempty"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Type string `json:"type"` + ClusterIP string `json:"cluster_ip"` + Ports int `json:"ports"` + PortList []SvcPortDetail `json:"portList,omitempty"` + Selector map[string]string `json:"selector,omitempty"` } result := []SvcSummary{} for _, s := range svcs.Items { - if s.Metadata.Namespace != ns { continue } + if s.Metadata.Namespace != ns { + continue + } ports := []SvcPortDetail{} for _, p := range s.Spec.Ports { ports = append(ports, SvcPortDetail{ @@ -345,28 +354,36 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) { }) } result = append(result, SvcSummary{ - Namespace: s.Metadata.Namespace, - Name: s.Metadata.Name, - Type: s.Spec.Type, - ClusterIP: s.Spec.ClusterIP, - Ports: len(s.Spec.Ports), - PortList: ports, - Selector: s.Spec.Selector, + Namespace: s.Metadata.Namespace, + Name: s.Metadata.Name, + Type: s.Spec.Type, + ClusterIP: s.Spec.ClusterIP, + Ports: len(s.Spec.Ports), + PortList: ports, + Selector: s.Spec.Selector, }) } c.JSON(200, model.Response{Code: 200, Data: result}) } func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ings, err := client.ListIngresses() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type PathDetail struct { - Path string `json:"path"` - PathType string `json:"pathType,omitempty"` - BackendSvc string `json:"backendService,omitempty"` + Path string `json:"path"` + PathType string `json:"pathType,omitempty"` + BackendSvc string `json:"backendService,omitempty"` BackendPort string `json:"backendPort,omitempty"` } type RuleDetail struct { @@ -374,20 +391,24 @@ func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) { Paths []PathDetail `json:"paths"` } type IngSummary struct { - Namespace string `json:"namespace"` - Name string `json:"name"` - Hosts []string `json:"hosts"` - TLS bool `json:"tls"` - IngressClass string `json:"ingressClass,omitempty"` - Rules []RuleDetail `json:"rules,omitempty"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Hosts []string `json:"hosts"` + TLS bool `json:"tls"` + IngressClass string `json:"ingressClass,omitempty"` + Rules []RuleDetail `json:"rules,omitempty"` } result := []IngSummary{} for _, ing := range ings.Items { - if ing.Metadata.Namespace != ns { continue } + if ing.Metadata.Namespace != ns { + continue + } hosts := []string{} rules := []RuleDetail{} for _, rule := range ing.Spec.Rules { - if rule.Host != "" { hosts = append(hosts, rule.Host) } + if rule.Host != "" { + hosts = append(hosts, rule.Host) + } rd := RuleDetail{Host: rule.Host, Paths: []PathDetail{}} if rule.HTTP != nil { for _, p := range rule.HTTP.Paths { @@ -453,12 +474,21 @@ func (h *KubernetesHandler) ListIngressClasses(c *gin.Context) { func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") pvcs, err := client.ListPVCs(ns) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type PVCSummary struct { Namespace string `json:"namespace"` Name string `json:"name"` @@ -470,14 +500,16 @@ func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) { for _, p := range pvcs.Items { size := "" if p.Status.Capacity != nil { - if s, ok := p.Status.Capacity["storage"]; ok { size = s } + if s, ok := p.Status.Capacity["storage"]; ok { + size = s + } } result = append(result, PVCSummary{ - Namespace: p.Metadata.Namespace, - Name: p.Metadata.Name, - Phase: p.Status.Phase, - Size: size, - StorageClass: p.Spec.StorageClassName, + Namespace: p.Metadata.Namespace, + Name: p.Metadata.Name, + Phase: p.Status.Phase, + Size: size, + StorageClass: p.Spec.StorageClassName, }) } c.JSON(200, model.Response{Code: 200, Data: result}) @@ -486,11 +518,18 @@ func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) { // DeletePVCNS deletes a PVC (code-based) func (h *KubernetesHandler) DeletePVCNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } if err := client.DeletePVC(c.Param("namespace"), c.Param("name")); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "已删除"}) } diff --git a/backend/controller/k8s_workload.go b/backend/controller/k8s_workload.go index a7a3d0b..bc5342a 100644 --- a/backend/controller/k8s_workload.go +++ b/backend/controller/k8s_workload.go @@ -208,15 +208,25 @@ func (h *KubernetesHandler) GetDeploymentNS(c *gin.Context) { ns := c.Param("namespace") name := c.Param("name") clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } deps, err := client.ListDeployments() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } for _, d := range deps.Items { if d.Metadata.Namespace == ns && d.Metadata.Name == name { image := "" - if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image } + if len(d.Spec.Template.Spec.Containers) > 0 { + image = d.Spec.Template.Spec.Containers[0].Image + } c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{ "namespace": d.Metadata.Namespace, "name": d.Metadata.Name, @@ -248,11 +258,19 @@ func (h *KubernetesHandler) ResumeDeploymentNS(c *gin.Context) { } func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } deps, err := client.ListDeployments() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type DepSummary struct { Namespace string `json:"namespace"` Name string `json:"name"` @@ -265,8 +283,13 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) { } result := []DepSummary{} for _, d := range deps.Items { - if d.Metadata.Namespace != ns { continue } - image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image } + if d.Metadata.Namespace != ns { + continue + } + image := "" + if len(d.Spec.Template.Spec.Containers) > 0 { + image = d.Spec.Template.Spec.Containers[0].Image + } result = append(result, DepSummary{ Namespace: d.Metadata.Namespace, Name: d.Metadata.Name, @@ -275,63 +298,131 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) { ReadyReplicas: d.Status.ReadyReplicas, Image: image, Paused: d.Spec.Paused, - Selector: func() map[string]string { if d.Spec.Selector != nil { return d.Spec.Selector.MatchLabels }; return nil }(), + Selector: func() map[string]string { + if d.Spec.Selector != nil { + return d.Spec.Selector.MatchLabels + } + return nil + }(), }) } c.JSON(200, model.Response{Code: 200, Data: result}) } func (h *KubernetesHandler) scaleDeploymentWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } - var req struct { Replicas int `json:"replicas"` } - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } + var req struct { + Replicas int `json:"replicas"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil { c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)}) } func (h *KubernetesHandler) restartDeploymentWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.RestartDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.RestartDeployment(ns, c.Param("name")); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "重启指令已下发"}) } func (h *KubernetesHandler) updateDeploymentImageWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } - var req struct { Image string `json:"image"`; ContainerName string `json:"container_name"` } - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return } - if strings.TrimSpace(req.Image) == "" { c.JSON(400, model.Response{Code: 400, Message: "image 不能为空"}); return } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } + var req struct { + Image string `json:"image"` + ContainerName string `json:"container_name"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误"}) + return + } + if strings.TrimSpace(req.Image) == "" { + c.JSON(400, model.Response{Code: 400, Message: "image 不能为空"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { c.JSON(500, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s", req.Image)}) } func (h *KubernetesHandler) pauseDeploymentWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.PauseDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.PauseDeployment(ns, c.Param("name")); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "已暂停滚动更新"}) } func (h *KubernetesHandler) resumeDeploymentWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } - if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } + if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { + c.JSON(500, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "已恢复滚动更新"}) } func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } deps, err := client.ListDeployments() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type DepSummary struct { Namespace string `json:"namespace"` Name string `json:"name"` @@ -342,7 +433,10 @@ func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) { } result := []DepSummary{} for _, d := range deps.Items { - image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image } + image := "" + if len(d.Spec.Template.Spec.Containers) > 0 { + image = d.Spec.Template.Spec.Containers[0].Image + } result = append(result, DepSummary{ Namespace: d.Metadata.Namespace, Name: d.Metadata.Name, @@ -358,11 +452,20 @@ func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) { // ListPodsAllNS lists all pods across all namespaces using clusterCode. func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } sts, err := client.ListStatefulSets() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } ns := c.Param("namespace") type StsSummary struct { Namespace string `json:"namespace"` @@ -373,8 +476,13 @@ func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) { } result := []StsSummary{} for _, s := range sts.Items { - if s.Metadata.Namespace != ns { continue } - image := ""; if len(s.Spec.Template.Spec.Containers) > 0 { image = s.Spec.Template.Spec.Containers[0].Image } + if s.Metadata.Namespace != ns { + continue + } + image := "" + if len(s.Spec.Template.Spec.Containers) > 0 { + image = s.Spec.Template.Spec.Containers[0].Image + } result = append(result, StsSummary{ Namespace: s.Metadata.Namespace, Name: s.Metadata.Name, @@ -389,11 +497,20 @@ func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) { // ListDaemonSetsNS lists DaemonSets in a namespace func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ds, err := client.ListDaemonSets() - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } ns := c.Param("namespace") type DsSummary struct { Namespace string `json:"namespace"` @@ -405,8 +522,13 @@ func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) { } result := []DsSummary{} for _, d := range ds.Items { - if d.Metadata.Namespace != ns { continue } - image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image } + if d.Metadata.Namespace != ns { + continue + } + image := "" + if len(d.Spec.Template.Spec.Containers) > 0 { + image = d.Spec.Template.Spec.Containers[0].Image + } result = append(result, DsSummary{ Namespace: d.Metadata.Namespace, Name: d.Metadata.Name, @@ -418,18 +540,31 @@ func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) { } c.JSON(200, model.Response{Code: 200, Data: result}) } + // ScaleStatefulSetNS scales a StatefulSet (Kuboard code route) func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") - var req struct{ Replicas int `json:"replicas"` } - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return } + var req struct { + Replicas int `json:"replicas"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误"}) + return + } if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)}) } @@ -437,16 +572,30 @@ func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) { // ScaleStatefulSet scales a StatefulSet (ID route) func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") - var req struct{ Replicas int `json:"replicas"` } - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return } + var req struct { + Replicas int `json:"replicas"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误"}) + return + } if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)}) } @@ -454,13 +603,20 @@ func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) { // RestartStatefulSetNS restarts a StatefulSet (Kuboard code route) func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") if err := client.RestartStatefulSet(ns, name); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"}) } @@ -468,14 +624,23 @@ func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) { // RestartStatefulSet restarts a StatefulSet (ID route) func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") if err := client.RestartStatefulSet(ns, name); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"}) } @@ -483,13 +648,20 @@ func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) { // RestartDaemonSetNS restarts a DaemonSet (Kuboard code route) func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") if err := client.RestartDaemonSet(ns, name); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"}) } @@ -497,14 +669,23 @@ func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) { // RestartDaemonSet restarts a DaemonSet (ID route) func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") if err := client.RestartDaemonSet(ns, name); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"}) } @@ -512,26 +693,44 @@ func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) { // GetStatefulSetDetailNS returns a single StatefulSet detail (Kuboard code route) func (h *KubernetesHandler) GetStatefulSetDetailNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") sts, err := client.GetStatefulSet(ns, name) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Data: sts}) } // GetDaemonSetDetailNS returns a single DaemonSet detail (Kuboard code route) func (h *KubernetesHandler) GetDaemonSetDetailNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") name := c.Param("name") ds, err := client.GetDaemonSet(ns, name) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Data: ds}) } @@ -545,11 +744,19 @@ func (h *KubernetesHandler) ListReplicaSetsNS(c *gin.Context) { } func (h *KubernetesHandler) listReplicaSetsWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } items, err := client.ListReplicaSetsRaw(ns) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type RSSummary struct { Namespace string `json:"namespace"` Name string `json:"name"` @@ -564,11 +771,16 @@ func (h *KubernetesHandler) listReplicaSetsWithNS(c *gin.Context, ns string) { result := []RSSummary{} for _, rs := range items { image := "" - if len(rs.Spec.Template.Spec.Containers) > 0 { image = rs.Spec.Template.Spec.Containers[0].Image } + if len(rs.Spec.Template.Spec.Containers) > 0 { + image = rs.Spec.Template.Spec.Containers[0].Image + } revision := rs.Metadata.Annotations["deployment.kubernetes.io/revision"] owner := "" for _, o := range rs.Metadata.OwnerReferences { - if o.Kind == "Deployment" { owner = o.Name; break } + if o.Kind == "Deployment" { + owner = o.Name + break + } } result = append(result, RSSummary{ Namespace: rs.Metadata.Namespace, @@ -591,13 +803,24 @@ func (h *KubernetesHandler) ListDeploymentRevisionsNS(c *gin.Context) { } func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } deploymentName := c.Param("name") - if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return } + if deploymentName == "" { + c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } items, err := client.ListReplicaSetsRaw(ns) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) + return + } type Revision struct { Revision string `json:"revision"` ReplicaSet string `json:"replica_set"` @@ -610,11 +833,18 @@ func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns str for _, rs := range items { isOwner := false for _, o := range rs.Metadata.OwnerReferences { - if o.Kind == "Deployment" && o.Name == deploymentName { isOwner = true; break } + if o.Kind == "Deployment" && o.Name == deploymentName { + isOwner = true + break + } + } + if !isOwner { + continue } - if !isOwner { continue } image := "" - if len(rs.Spec.Template.Spec.Containers) > 0 { image = rs.Spec.Template.Spec.Containers[0].Image } + if len(rs.Spec.Template.Spec.Containers) > 0 { + image = rs.Spec.Template.Spec.Containers[0].Image + } result = append(result, Revision{ Revision: rs.Metadata.Annotations["deployment.kubernetes.io/revision"], ReplicaSet: rs.Metadata.Name, @@ -639,28 +869,44 @@ func (h *KubernetesHandler) RollbackDeploymentNS(c *gin.Context) { } func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string) { clusterID, _ := strconv.Atoi(c.Param("id")) - if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) } + if clusterID == 0 { + clusterID = h.resolveClusterCode(c.Param("clusterCode")) + } deploymentName := c.Param("name") - if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return } + if deploymentName == "" { + c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}) + return + } var req struct { Revision int `json:"revision"` ReplicaSet string `json:"replicaSet"` } _ = c.ShouldBindJSON(&req) // 允许空 body(但下面会校验) client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } targetRS := req.ReplicaSet // 通过 revision 定位 ReplicaSet if targetRS == "" && req.Revision > 0 { items, err := client.ListReplicaSetsRaw(ns) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 列表失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 列表失败: " + err.Error()}) + return + } for _, rs := range items { isOwner := false for _, o := range rs.Metadata.OwnerReferences { - if o.Kind == "Deployment" && o.Name == deploymentName { isOwner = true; break } + if o.Kind == "Deployment" && o.Name == deploymentName { + isOwner = true + break + } + } + if !isOwner { + continue } - if !isOwner { continue } if rs.Metadata.Annotations["deployment.kubernetes.io/revision"] == strconv.Itoa(req.Revision) { targetRS = rs.Metadata.Name break @@ -671,18 +917,31 @@ func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string) return } } - if targetRS == "" { c.JSON(400, model.Response{Code: 400, Message: "需要 revision 或 replicaSet 参数"}); return } + if targetRS == "" { + c.JSON(400, model.Response{Code: 400, Message: "需要 revision 或 replicaSet 参数"}) + return + } // 取 ReplicaSet 的 template,patch 到 Deployment.spec.template 实现回滚 rsJSON, err := client.GetReplicaSet(ns, targetRS) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()}) + return + } specMap, ok := rsJSON["spec"].(map[string]interface{}) - if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 spec"}); return } + if !ok { + c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 spec"}) + return + } template, ok := specMap["template"].(map[string]interface{}) - if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 template"}); return } + if !ok { + c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 template"}) + return + } if err := client.PatchDeploymentSpecTemplate(ns, deploymentName, template); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "回滚失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "回滚失败: " + err.Error()}) + return } c.JSON(200, model.Response{ Code: 200, diff --git a/backend/controller/k8s_yaml.go b/backend/controller/k8s_yaml.go index d7ecfbf..cca8e33 100644 --- a/backend/controller/k8s_yaml.go +++ b/backend/controller/k8s_yaml.go @@ -12,9 +12,15 @@ import ( func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") resource := c.Param("resource") // Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param @@ -62,10 +68,14 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) { case "rolebindings", "rolebinding": apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name) default: - c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return + c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}) + return } yamlBytes, err := client.GetResourceYAML(apiPath) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取YAML失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "获取YAML失败: " + err.Error()}) + return + } c.Header("Content-Type", "application/yaml") c.String(200, string(yamlBytes)) } @@ -73,9 +83,15 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) { // UpdateResourceYAMLNS applies YAML to update a K8s resource func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") resource := c.Param("resource") // Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param @@ -90,7 +106,10 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) { } name := c.Param("name") yamlData, err := io.ReadAll(c.Request.Body) - if err != nil { c.JSON(400, model.Response{Code: 400, Message: "读取body失败"}); return } + if err != nil { + c.JSON(400, model.Response{Code: 400, Message: "读取body失败"}) + return + } var apiPath string switch resource { case "deployments", "deployment": @@ -124,10 +143,12 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) { case "rolebindings", "rolebinding": apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name) default: - c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return + c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}) + return } if err := client.UpdateResourceYAML(apiPath, yamlData); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "更新失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "更新失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "YAML已应用"}) } @@ -173,16 +194,26 @@ func resourceAPIPath(ns, resource, name string) (string, error) { // DeleteResourceYAMLNS deletes a K8s resource (Service/Ingress/ConfigMap etc.) func (h *KubernetesHandler) DeleteResourceYAMLNS(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } ns := c.Param("namespace") resource := c.Param("resource") name := c.Param("name") apiPath, err := resourceAPIPath(ns, resource, name) - if err != nil { c.JSON(400, model.Response{Code: 400, Message: err.Error()}); return } + if err != nil { + c.JSON(400, model.Response{Code: 400, Message: err.Error()}) + return + } if err := client.DeleteResource(apiPath); err != nil { - c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return + c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}) + return } c.JSON(200, model.Response{Code: 200, Message: "资源已删除"}) } @@ -190,18 +221,33 @@ func (h *KubernetesHandler) DeleteResourceYAMLNS(c *gin.Context) { // getK8sClient builds a K8s API client from cluster's stored kubeconfig func (h *KubernetesHandler) ApplyYAML(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) - if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return } + if clusterID == 0 { + c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) + return + } client, _, err := h.getK8sClient(clusterID) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: err.Error()}) + return + } var req struct { YAML string `json:"yaml"` } - if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return } - if req.YAML == "" { c.JSON(400, model.Response{Code: 400, Message: "YAML 内容不能为空"}); return } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}) + return + } + if req.YAML == "" { + c.JSON(400, model.Response{Code: 400, Message: "YAML 内容不能为空"}) + return + } result, err := client.ApplyYAML(req.YAML) - if err != nil { c.JSON(500, model.Response{Code: 500, Message: "Apply 失败: " + err.Error()}); return } + if err != nil { + c.JSON(500, model.Response{Code: 500, Message: "Apply 失败: " + err.Error()}) + return + } c.JSON(200, model.Response{Code: 200, Message: "资源已应用", Data: result}) } diff --git a/backend/controller/ldap_ad.go b/backend/controller/ldap_ad.go index 16922b2..6ac5359 100644 --- a/backend/controller/ldap_ad.go +++ b/backend/controller/ldap_ad.go @@ -151,4 +151,4 @@ func ldapAddADUser(conn *ldap.Conn, userDN, username, displayName, email, passwo modEnable := ldap.NewModifyRequest(userDN, nil) modEnable.Replace("userAccountControl", []string{"512"}) return conn.Modify(modEnable) -} \ No newline at end of file +} diff --git a/backend/controller/llm.go b/backend/controller/llm.go index b524ff2..8791ee7 100644 --- a/backend/controller/llm.go +++ b/backend/controller/llm.go @@ -60,17 +60,17 @@ func (h *LLMHandler) GetLLMConfig(c *gin.Context) { return } c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{ - "id": cfg.ID, - "provider": cfg.Provider, - "model": cfg.Model, - "api_key": cfg.APIKey, - "endpoint": cfg.Endpoint, - "temperature": cfg.Temperature, - "max_tokens": cfg.MaxTokens, - "system_prompt": cfg.SystemPrompt, - "enabled": cfg.Enabled, + "id": cfg.ID, + "provider": cfg.Provider, + "model": cfg.Model, + "api_key": cfg.APIKey, + "endpoint": cfg.Endpoint, + "temperature": cfg.Temperature, + "max_tokens": cfg.MaxTokens, + "system_prompt": cfg.SystemPrompt, + "enabled": cfg.Enabled, "reasoning_enabled": cfg.ReasoningEnabled, - "configured": true, + "configured": true, }}) } diff --git a/backend/controller/llm_client.go b/backend/controller/llm_client.go index 17df285..3d6c574 100644 --- a/backend/controller/llm_client.go +++ b/backend/controller/llm_client.go @@ -23,4 +23,4 @@ func buildLLMConfigFromModel(cfg model.LLMConfig) llm.Config { // buildLLMClientFromConfig 从 model.LLMConfig 构造 LLM 客户端(OpenAI/Anthropic 自动路由) // Logan.Li func buildLLMClientFromConfig(cfg model.LLMConfig) llm.Client { return llm.NewClient(buildLLMConfigFromModel(cfg)) -} \ No newline at end of file +} diff --git a/backend/controller/log.go b/backend/controller/log.go index d398777..d518279 100644 --- a/backend/controller/log.go +++ b/backend/controller/log.go @@ -74,10 +74,10 @@ func (h *LogHandler) CreateSource(c *gin.Context) { configJSON, _ := json.Marshal(req.Config) now := time.Now() item := model.LogSource{ - Name: req.Name, - Type: req.Type, - Status: "active", - Config: string(configJSON), + Name: req.Name, + Type: req.Type, + Status: "active", + Config: string(configJSON), LastSeen: &now, } h.db.Create(&item) diff --git a/backend/controller/log_datasource.go b/backend/controller/log_datasource.go index 74630cf..a2fdf70 100644 --- a/backend/controller/log_datasource.go +++ b/backend/controller/log_datasource.go @@ -76,7 +76,7 @@ func (h *LogDSHandler) Create(c *gin.Context) { } ds := model.LogDataSource{ Name: req.Name, Type: req.Type, URL: strings.TrimRight(req.URL, "/"), - AuthType: normalizeLogDSAuthType(req.AuthType), + AuthType: normalizeLogDSAuthType(req.AuthType), BasicAuth: req.BasicAuth, BasicAuthUser: req.BasicAuthUser, BasicAuthPass: req.BasicAuthPass, Token: req.Token, Headers: req.Headers, SkipTLSVerify: req.SkipTLSVerify, DefaultIndex: req.DefaultIndex, @@ -439,4 +439,4 @@ func normalizeLogDSAuthType(t string) string { default: return "none" } -} \ No newline at end of file +} diff --git a/backend/controller/log_proxy_explore.go b/backend/controller/log_proxy_explore.go index 288b039..7c428b8 100644 --- a/backend/controller/log_proxy_explore.go +++ b/backend/controller/log_proxy_explore.go @@ -477,4 +477,4 @@ func formatLokiMetricTimestamp(v interface{}) string { } } return formatLokiTimestamp(fmt.Sprintf("%v", v)) -} \ No newline at end of file +} diff --git a/backend/controller/log_scope_role.go b/backend/controller/log_scope_role.go index 77b711c..b5e8741 100644 --- a/backend/controller/log_scope_role.go +++ b/backend/controller/log_scope_role.go @@ -198,4 +198,4 @@ func (h *LogScopeRoleHandler) PreviewLogScope(c *gin.Context) { "matched": matched, "total": len(matched), }}) -} \ No newline at end of file +} diff --git a/backend/controller/metrics.go b/backend/controller/metrics.go index 087c21b..3667f9f 100644 --- a/backend/controller/metrics.go +++ b/backend/controller/metrics.go @@ -134,15 +134,15 @@ func (h *MetricsHandler) TopHosts(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "5")) type HostRank struct { - Hostname string `json:"hostname"` - IP string `json:"ip"` - CPUUsage float64 `json:"cpu_usage"` - MemUsage float64 `json:"memory_usage"` + Hostname string `json:"hostname"` + IP string `json:"ip"` + CPUUsage float64 `json:"cpu_usage"` + MemUsage float64 `json:"memory_usage"` DiskUsage float64 `json:"disk_usage"` - CPUCores int `json:"cpu_cores"` - MemMB int64 `json:"memory_mb"` - DiskGB int64 `json:"disk_gb"` - Status string `json:"status"` + CPUCores int `json:"cpu_cores"` + MemMB int64 `json:"memory_mb"` + DiskGB int64 `json:"disk_gb"` + Status string `json:"status"` } var servers []model.Server @@ -166,15 +166,15 @@ func (h *MetricsHandler) TopHosts(c *gin.Context) { result := make([]HostRank, len(servers)) for i, s := range servers { result[i] = HostRank{ - Hostname: s.Hostname, - IP: s.IP, - CPUUsage: s.CPUUsage, - MemUsage: s.MemoryUsage, + Hostname: s.Hostname, + IP: s.IP, + CPUUsage: s.CPUUsage, + MemUsage: s.MemoryUsage, DiskUsage: s.DiskUsage, - CPUCores: s.CPU, - MemMB: s.Memory, - DiskGB: s.Disk, - Status: s.Status, + CPUCores: s.CPU, + MemMB: s.Memory, + DiskGB: s.Disk, + Status: s.Status, } } if result == nil { diff --git a/backend/controller/metrics_ws.go b/backend/controller/metrics_ws.go index ae955f7..12c316d 100644 --- a/backend/controller/metrics_ws.go +++ b/backend/controller/metrics_ws.go @@ -14,7 +14,7 @@ import ( // MetricsUpdate 单个主机的指标更新 // Logan.Li type MetricsUpdate struct { - Type string `json:"type"` // "metrics_update" + Type string `json:"type"` // "metrics_update" Hostname string `json:"hostname"` CPU float64 `json:"cpu_usage"` Memory float64 `json:"memory_usage"` diff --git a/backend/controller/middleware_monitor.go b/backend/controller/middleware_monitor.go index d07df03..7e1f0dd 100644 --- a/backend/controller/middleware_monitor.go +++ b/backend/controller/middleware_monitor.go @@ -15,8 +15,8 @@ import ( "ezops/model" "ezops/pkg/systask" - _ "github.com/go-sql-driver/mysql" "github.com/gin-gonic/gin" + _ "github.com/go-sql-driver/mysql" "gorm.io/gorm" ) @@ -28,8 +28,8 @@ import ( // MiddlewareConfig 存储在 MonitorTarget.Config JSON 字段中 type MiddlewareConfig struct { Password string `json:"password,omitempty"` - Username string `json:"username,omitempty"` // MySQL 专用 - Database string `json:"database,omitempty"` // MySQL 专用 + Username string `json:"username,omitempty"` // MySQL 专用 + Database string `json:"database,omitempty"` // MySQL 专用 RedisMode string `json:"redis_mode,omitempty"` // Redis: standalone / cluster } diff --git a/backend/controller/middleware_monitor_redis.go b/backend/controller/middleware_monitor_redis.go index 3d79d18..234a2cb 100644 --- a/backend/controller/middleware_monitor_redis.go +++ b/backend/controller/middleware_monitor_redis.go @@ -98,4 +98,4 @@ func buildRedisMetrics(info string, dbSize int64, clusterInfo, mode string) map[ } return result -} \ No newline at end of file +} diff --git a/backend/controller/middleware_monitor_seed.go b/backend/controller/middleware_monitor_seed.go index 0a31d92..6476888 100644 --- a/backend/controller/middleware_monitor_seed.go +++ b/backend/controller/middleware_monitor_seed.go @@ -14,12 +14,12 @@ import ( // middlewareSeedResult 种子创建结果 // Logan.Li type middlewareSeedResult struct { - Created int `json:"created"` - Skipped []string `json:"skipped"` - Failed []string `json:"failed"` - Total int64 `json:"total"` - Message string `json:"message"` - Success bool `json:"success"` + Created int `json:"created"` + Skipped []string `json:"skipped"` + Failed []string `json:"failed"` + Total int64 `json:"total"` + Message string `json:"message"` + Success bool `json:"success"` } // SeedDefaultMiddlewareTargets 首次启动时创建平台默认 Redis/MySQL 监控目标 // Logan.Li @@ -117,4 +117,4 @@ func seedDefaultMiddlewareTargets(db *gorm.DB) middlewareSeedResult { result.Message = "未创建任何默认目标" } return result -} \ No newline at end of file +} diff --git a/backend/controller/monitor_alert_rule.go b/backend/controller/monitor_alert_rule.go index 4ac3d47..1f504a5 100644 --- a/backend/controller/monitor_alert_rule.go +++ b/backend/controller/monitor_alert_rule.go @@ -282,4 +282,4 @@ func (h *AlertHandler) ResolveUnifiedAlert(c *gin.Context) { return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已恢复"}) -} \ No newline at end of file +} diff --git a/backend/controller/pipeline.go b/backend/controller/pipeline.go index 1d526c0..5733fc4 100644 --- a/backend/controller/pipeline.go +++ b/backend/controller/pipeline.go @@ -6,8 +6,8 @@ import ( "strconv" "time" - "ezops/model" "ezops/middleware" + "ezops/model" ezredis "ezops/pkg/redis" "ezops/pkg/toolinstaller" @@ -69,7 +69,7 @@ func NewPipelineHandler(db *gorm.DB, cache ezredis.Cache, ti *toolinstaller.Tool // PipelineListItem 列表项(含最近一次运行摘要)// Logan.Li type PipelineListItem struct { model.Pipeline - LastRunStages string `json:"last_run_stages,omitempty"` // JSON: [{name,status}] + LastRunStages string `json:"last_run_stages,omitempty"` // JSON: [{name,status}] LastImageTag string `json:"last_image_tag,omitempty"` LastCommitID string `json:"last_commit_id,omitempty"` LastRunDuration int `json:"last_duration,omitempty"` @@ -123,7 +123,7 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) { } // 子查询:每个 pipeline_id 的最新 run id var latestRuns []model.PipelineRun - h.db.Where("id IN (SELECT MAX(id) FROM ops_pipeline_runs WHERE pipeline_id IN ? AND status IN ('success','failed','running','pending') GROUP BY pipeline_id)", pids). + h.db.Where("id IN (SELECT MAX(id) FROM deploy_pipeline_runs WHERE pipeline_id IN ? AND status IN ('success','failed','running','pending') GROUP BY pipeline_id)", pids). Find(&latestRuns) runMap := map[uint]*model.PipelineRun{} for i := range latestRuns { @@ -147,8 +147,8 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) { } c.JSON(http.StatusOK, model.Response{ - Code: 200, - Data: model.ListResponse{List: items, Total: total}, + Code: 200, + Data: model.ListResponse{List: items, Total: total}, }) } @@ -211,30 +211,28 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) { } pipeline := model.Pipeline{ - Name: req.Name, - Code: req.Code, - JobName: req.JobName, - Description: req.Description, - Type: req.Type, - Source: req.Source, - Branch: req.Branch, - Dockerfile: req.Dockerfile, - Stages: req.Stages, - Variables: req.Variables, - DeployMode: req.DeployMode, - DeployTarget: req.DeployTarget, - ServerIDs: req.ServerIDs, - DeployRemotePath: req.DeployRemotePath, - DeployScript: req.DeployScript, - K8sClusterID: req.K8sClusterID, - K8sNamespace: req.K8sNamespace, - K8sDeployment: req.K8sDeployment, - K8sContainerName: req.K8sContainerName, - RepoID: req.RepoID, - RegistryID: req.RegistryID, + Name: req.Name, + Code: req.Code, + JobName: req.JobName, + Description: req.Description, + Type: req.Type, + Source: req.Source, + Branch: req.Branch, + Dockerfile: req.Dockerfile, + Stages: req.Stages, + Variables: req.Variables, + DeployMode: req.DeployMode, + DeployTarget: req.DeployTarget, + ServerIDs: req.ServerIDs, + DeployRemotePath: req.DeployRemotePath, + DeployScript: req.DeployScript, + K8sClusterID: req.K8sClusterID, + K8sNamespace: req.K8sNamespace, + K8sDeployment: req.K8sDeployment, + K8sContainerName: req.K8sContainerName, GitCredentialID: req.GitCredentialID, RegistryCredentialID: req.RegistryCredentialID, - EnvironmentID: req.EnvironmentID, + EnvironmentID: req.EnvironmentID, // 触发配置 TriggerMode: req.TriggerMode, WatchBranches: req.WatchBranches, @@ -246,7 +244,7 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) { // 并发控制 MaxExecutors: req.MaxExecutors, BuildNodeID: req.BuildNodeID, - Enabled: true, + Enabled: true, } if err := h.db.Create(&pipeline).Error; err != nil { @@ -301,38 +299,96 @@ func (h *PipelineHandler) UpdatePipeline(c *gin.Context) { } updates := map[string]interface{}{} - if req.Name != "" { updates["name"] = req.Name } - if req.JobName != "" { updates["job_name"] = req.JobName } - if req.Description != "" { updates["description"] = req.Description } - if req.Type != "" { updates["type"] = req.Type } - if req.Source != "" { updates["source"] = req.Source } - if req.Branch != "" { updates["branch"] = req.Branch } - if req.Dockerfile != "" { updates["dockerfile"] = req.Dockerfile } - if req.Stages != "" { updates["stages"] = req.Stages } - if req.Variables != "" { updates["variables"] = req.Variables } - if req.DeployMode != "" { updates["deploy_mode"] = req.DeployMode } - if req.DeployTarget != "" { updates["deploy_target"] = req.DeployTarget } - if req.ServerIDs != "" { updates["server_ids"] = req.ServerIDs } - if req.DeployRemotePath != "" { updates["deploy_remote_path"] = req.DeployRemotePath } - if req.DeployScript != "" { updates["deploy_script"] = req.DeployScript } - if req.K8sNamespace != "" { updates["k8s_namespace"] = req.K8sNamespace } - if req.K8sDeployment != "" { updates["k8s_deployment"] = req.K8sDeployment } - if req.K8sContainerName != "" { updates["k8s_container_name"] = req.K8sContainerName } - if req.K8sClusterID != nil { updates["k8s_cluster_id"] = req.K8sClusterID } - if req.RepoID != nil { updates["repo_id"] = req.RepoID } - if req.RegistryID != nil { updates["registry_id"] = req.RegistryID } - if req.GitCredentialID != nil { updates["git_credential_id"] = req.GitCredentialID } - if req.RegistryCredentialID != nil { updates["registry_credential_id"] = req.RegistryCredentialID } - if req.EnvironmentID != nil { updates["environment_id"] = req.EnvironmentID } - if req.TriggerMode != "" { updates["trigger_mode"] = req.TriggerMode } - if req.WatchBranches != "" { updates["watch_branches"] = req.WatchBranches } - if req.ExcludeBranch != "" { updates["exclude_branch"] = req.ExcludeBranch } - if req.CronExpr != "" { updates["cron_expr"] = req.CronExpr } - if req.WebhookToken != "" { updates["webhook_token"] = req.WebhookToken } - if req.CachePaths != "" { updates["cache_paths"] = req.CachePaths } - if req.Enabled != nil { updates["enabled"] = req.Enabled } - if req.MaxExecutors > 0 { updates["max_executors"] = req.MaxExecutors } - if req.BuildNodeID != nil { updates["build_node_id"] = req.BuildNodeID } + if req.Name != "" { + updates["name"] = req.Name + } + if req.JobName != "" { + updates["job_name"] = req.JobName + } + if req.Description != "" { + updates["description"] = req.Description + } + if req.Type != "" { + updates["type"] = req.Type + } + if req.Source != "" { + updates["source"] = req.Source + } + if req.Branch != "" { + updates["branch"] = req.Branch + } + if req.Dockerfile != "" { + updates["dockerfile"] = req.Dockerfile + } + if req.Stages != "" { + updates["stages"] = req.Stages + } + if req.Variables != "" { + updates["variables"] = req.Variables + } + if req.DeployMode != "" { + updates["deploy_mode"] = req.DeployMode + } + if req.DeployTarget != "" { + updates["deploy_target"] = req.DeployTarget + } + if req.ServerIDs != "" { + updates["server_ids"] = req.ServerIDs + } + if req.DeployRemotePath != "" { + updates["deploy_remote_path"] = req.DeployRemotePath + } + if req.DeployScript != "" { + updates["deploy_script"] = req.DeployScript + } + if req.K8sNamespace != "" { + updates["k8s_namespace"] = req.K8sNamespace + } + if req.K8sDeployment != "" { + updates["k8s_deployment"] = req.K8sDeployment + } + if req.K8sContainerName != "" { + updates["k8s_container_name"] = req.K8sContainerName + } + if req.K8sClusterID != nil { + updates["k8s_cluster_id"] = req.K8sClusterID + } + if req.GitCredentialID != nil { + updates["git_credential_id"] = req.GitCredentialID + } + if req.RegistryCredentialID != nil { + updates["registry_credential_id"] = req.RegistryCredentialID + } + if req.EnvironmentID != nil { + updates["environment_id"] = req.EnvironmentID + } + if req.TriggerMode != "" { + updates["trigger_mode"] = req.TriggerMode + } + if req.WatchBranches != "" { + updates["watch_branches"] = req.WatchBranches + } + if req.ExcludeBranch != "" { + updates["exclude_branch"] = req.ExcludeBranch + } + if req.CronExpr != "" { + updates["cron_expr"] = req.CronExpr + } + if req.WebhookToken != "" { + updates["webhook_token"] = req.WebhookToken + } + if req.CachePaths != "" { + updates["cache_paths"] = req.CachePaths + } + if req.Enabled != nil { + updates["enabled"] = req.Enabled + } + if req.MaxExecutors > 0 { + updates["max_executors"] = req.MaxExecutors + } + if req.BuildNodeID != nil { + updates["build_node_id"] = req.BuildNodeID + } if err := h.db.Model(&pipeline).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{ diff --git a/backend/controller/pipeline_executor.go b/backend/controller/pipeline_executor.go index 57b2d74..df351a4 100644 --- a/backend/controller/pipeline_executor.go +++ b/backend/controller/pipeline_executor.go @@ -162,7 +162,7 @@ func (h *PipelineHandler) executePipeline(pipeline model.Pipeline, run model.Pip } runUpdates := map[string]interface{}{ - "workspace_path": workspace, + "workspace_path": workspace, "build_node_name": execCtx.NodeName, } if nodeID > 0 { @@ -545,13 +545,10 @@ func (h *PipelineHandler) runDeploy(workspace string, pipeline model.Pipeline, t // resolvePipelineImage 解析流水线构建镜像全名(与镜像打包阶段一致) // Logan.Li func (h *PipelineHandler) resolvePipelineImage(pipeline model.Pipeline, tag string) string { var registryURL, imageName string - if pipeline.RegistryID != nil { - var reg model.PipelineRegistry - if err := h.db.First(®, *pipeline.RegistryID).Error; err == nil { - registryURL = reg.RegistryURL - if reg.Namespace != "" { - imageName = reg.Namespace + "/" - } + if pipeline.RegistryCredentialID != nil { + var credential model.Credential + if err := h.db.Take(&credential, *pipeline.RegistryCredentialID).Error; err == nil { + registryURL = credential.Summary } } appName := pipeline.Code @@ -838,7 +835,7 @@ func cleanOldWorkspaces(baseDir string, maxAge time.Duration) { var ( pipelineRunningCount = make(map[uint]int) // pipelineID → 当前运行数 - pipelineQueues = make(map[uint][]uint) // pipelineID → 排队 runID 列表 + pipelineQueues = make(map[uint][]uint) // pipelineID → 排队 runID 列表 queueMu sync.Mutex ) @@ -909,9 +906,13 @@ func (h *PipelineHandler) dequeueNext(pipelineID uint) { // 加载 pipeline + run var pipeline model.Pipeline - if err := h.db.First(&pipeline, pipelineID).Error; err != nil { return } + if err := h.db.First(&pipeline, pipelineID).Error; err != nil { + return + } var run model.PipelineRun - if err := h.db.First(&run, nextRunID).Error; err != nil { return } + if err := h.db.First(&run, nextRunID).Error; err != nil { + return + } nodeID, canNode := h.tryAcquireBuildNode(pipeline) if !canNode { @@ -935,8 +936,8 @@ func (h *PipelineHandler) GetQueueStatus(pipelineID uint) map[string]interface{} running := pipelineRunningCount[pipelineID] queued := pipelineQueues[pipelineID] return map[string]interface{}{ - "running": running, - "queued": len(queued), + "running": running, + "queued": len(queued), "queue_ids": queued, } } diff --git a/backend/controller/pipeline_node.go b/backend/controller/pipeline_node.go index cab3247..86a30fb 100644 --- a/backend/controller/pipeline_node.go +++ b/backend/controller/pipeline_node.go @@ -260,4 +260,4 @@ func (h *PipelineHandler) hasBuildFileCtx(ctx *PipelineExecCtx, dir string) bool } } return false -} \ No newline at end of file +} diff --git a/backend/controller/rdp_cred.go b/backend/controller/rdp_cred.go index f7c8a73..f7c7cee 100644 --- a/backend/controller/rdp_cred.go +++ b/backend/controller/rdp_cred.go @@ -32,4 +32,4 @@ func loadRDPCredential(crypt *crypto.SecretBox, server model.Server) (model.RDPS return cred, errors.New("RDP 凭据缺少用户名或密码") } return cred, nil -} \ No newline at end of file +} diff --git a/backend/controller/remote_exec.go b/backend/controller/remote_exec.go index bf85255..3bc2587 100644 --- a/backend/controller/remote_exec.go +++ b/backend/controller/remote_exec.go @@ -84,8 +84,8 @@ func (h *RemoteExecHandler) BatchExec(c *gin.Context) { // BatchUploadRequest 批量上传请求 type BatchUploadRequest struct { - ServerIDs []uint `json:"server_ids" binding:"required"` // 目标服务器 ID 列表 - LocalPath string `json:"local_path" binding:"required"` // AIOps 主机上的文件路径 + ServerIDs []uint `json:"server_ids" binding:"required"` // 目标服务器 ID 列表 + LocalPath string `json:"local_path" binding:"required"` // AIOps 主机上的文件路径 RemotePath string `json:"remote_path" binding:"required"` // 远程目标路径 } diff --git a/backend/controller/server.go b/backend/controller/server.go index a3167e0..2808be4 100644 --- a/backend/controller/server.go +++ b/backend/controller/server.go @@ -94,8 +94,8 @@ func (h *ServerHandler) ListServers(c *gin.Context) { // 关键词搜索 if query.Keyword != "" { - db = db.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ?", - "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%") + db = db.Where("alias LIKE ? OR cloud_instance_name LIKE ? OR hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ? OR cloud_instance_id LIKE ?", + "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%") } // 状态过滤 @@ -194,23 +194,26 @@ func (h *ServerHandler) CreateServer(c *gin.Context) { } server := model.Server{ - Hostname: req.Hostname, - IP: req.IP, - PublicIP: req.PublicIP, - Port: req.Port, - Username: req.Username, - Password: req.Password, - SSHKey: req.SSHKey, - AuthType: req.AuthType, - OS: req.OS, - CPU: req.CPU, - Memory: req.Memory, - Disk: req.Disk, - RegionID: req.RegionID, - CloudID: req.CloudID, - Tags: req.Tags, - Description: req.Description, - Status: "stopped", + Hostname: req.Hostname, + Alias: req.Alias, + CloudInstanceName: req.CloudInstanceName, + CloudInstanceID: req.CloudInstanceID, + IP: req.IP, + PublicIP: req.PublicIP, + Port: req.Port, + Username: req.Username, + Password: req.Password, + SSHKey: req.SSHKey, + AuthType: req.AuthType, + OS: req.OS, + CPU: req.CPU, + Memory: req.Memory, + Disk: req.Disk, + RegionID: req.RegionID, + CloudID: req.CloudID, + Tags: req.Tags, + Description: req.Description, + Status: "stopped", } if req.Status != "" { @@ -295,6 +298,15 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) { if req.Hostname != "" { updates["hostname"] = req.Hostname } + if req.Alias != nil { + updates["alias"] = *req.Alias + } + if req.CloudInstanceName != "" { + updates["cloud_instance_name"] = req.CloudInstanceName + } + if req.CloudInstanceID != "" { + updates["cloud_instance_id"] = req.CloudInstanceID + } if req.IP != "" { updates["ip"] = req.IP } diff --git a/backend/controller/session_command.go b/backend/controller/session_command.go index 382e43b..14b1c21 100644 --- a/backend/controller/session_command.go +++ b/backend/controller/session_command.go @@ -67,7 +67,7 @@ func (h *SessionCommandHandler) ListSessionCommands(c *gin.Context) { // 联表带出会话录制大小,前端可判断能否回放 // Logan.Li var items []model.SessionCommandItem - listQuery := h.db.Table("sys_session_commands AS sc"). + listQuery := h.db.Table("audit_session_commands AS sc"). Select("sc.*, COALESCE(ts.record_size, rs.record_size, 0) AS session_record_size"). Joins("LEFT JOIN sys_terminal_sessions AS ts ON ts.id = sc.session_id AND COALESCE(sc.session_type, 'terminal') = 'terminal'"). Joins("LEFT JOIN sys_rdp_sessions AS rs ON rs.id = sc.session_id AND sc.session_type = 'rdp'") diff --git a/backend/controller/sftp_handler.go b/backend/controller/sftp_handler.go index 14824b3..2154545 100644 --- a/backend/controller/sftp_handler.go +++ b/backend/controller/sftp_handler.go @@ -35,8 +35,8 @@ func (h *SFTPHandler) sftpClient(serverID uint) (*sftp.Client, *ssh.Client, erro } sshConfig := &ssh.ClientConfig{ - User: server.Username, - Timeout: 10 * time.Second, + User: server.Username, + Timeout: 10 * time.Second, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } diff --git a/backend/controller/site.go b/backend/controller/site.go index dfcc926..635f265 100644 --- a/backend/controller/site.go +++ b/backend/controller/site.go @@ -267,4 +267,4 @@ func (h *SiteHandler) GetSiteStats(c *gin.Context) { "availability": availability, "probe_count": totalRecent, }}) -} \ No newline at end of file +} diff --git a/backend/controller/site_view.go b/backend/controller/site_view.go index 8dbfe69..62aa50c 100644 --- a/backend/controller/site_view.go +++ b/backend/controller/site_view.go @@ -12,23 +12,23 @@ import ( // SiteTaskView 前端拨测任务视图(字段与前端对齐)// Logan.Li type SiteTaskView struct { - ID uint `json:"id"` - Name string `json:"name"` - URL string `json:"url"` - Type string `json:"type"` - FrequencySec int `json:"frequency_sec"` - TimeoutSec int `json:"timeout_sec"` - ExpectedStatusCode int `json:"expected_status_code"` - Enabled bool `json:"enabled"` - LastStatus string `json:"last_status,omitempty"` - LastResponseTimeMs float64 `json:"last_response_time_ms,omitempty"` - LastCheckedAt *string `json:"last_checked_at,omitempty"` - SuccessRate float64 `json:"success_rate"` - AlertStatus string `json:"alert_status"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - SSLDaysRemaining *int `json:"ssl_days_remaining,omitempty"` - SSLCertExpiry *string `json:"ssl_cert_expiry,omitempty"` + ID uint `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Type string `json:"type"` + FrequencySec int `json:"frequency_sec"` + TimeoutSec int `json:"timeout_sec"` + ExpectedStatusCode int `json:"expected_status_code"` + Enabled bool `json:"enabled"` + LastStatus string `json:"last_status,omitempty"` + LastResponseTimeMs float64 `json:"last_response_time_ms,omitempty"` + LastCheckedAt *string `json:"last_checked_at,omitempty"` + SuccessRate float64 `json:"success_rate"` + AlertStatus string `json:"alert_status"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + SSLDaysRemaining *int `json:"ssl_days_remaining,omitempty"` + SSLCertExpiry *string `json:"ssl_cert_expiry,omitempty"` } func toSiteTaskView(task model.SiteTask) SiteTaskView { @@ -220,4 +220,4 @@ func parseCreateTaskRequest(raw map[string]interface{}) model.SiteTask { task.DNSResolver = strings.TrimSpace(v) } return task -} \ No newline at end of file +} diff --git a/backend/controller/snmp.go b/backend/controller/snmp.go index 5ea6285..f23a599 100644 --- a/backend/controller/snmp.go +++ b/backend/controller/snmp.go @@ -50,16 +50,16 @@ func (h *SNMPHandler) ListDevices(c *gin.Context) { // POST /api/v1/snmp/devices func (h *SNMPHandler) CreateDevice(c *gin.Context) { var req struct { - Name string `json:"name" binding:"required"` - IP string `json:"ip" binding:"required"` - Port int `json:"port"` - SnmpVersion string `json:"snmp_version"` - Version string `json:"version"` - Community string `json:"community"` - TemplateID uint `json:"template_id"` - DeviceType string `json:"device_type"` - Vendor string `json:"vendor"` - Description string `json:"description"` + Name string `json:"name" binding:"required"` + IP string `json:"ip" binding:"required"` + Port int `json:"port"` + SnmpVersion string `json:"snmp_version"` + Version string `json:"version"` + Community string `json:"community"` + TemplateID uint `json:"template_id"` + DeviceType string `json:"device_type"` + Vendor string `json:"vendor"` + Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) @@ -172,8 +172,8 @@ func (h *SNMPHandler) GetSNMPStats(c *gin.Context) { h.db.Model(&model.SNMPDevice{}).Where("status = ?", "online").Count(&online) c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{ - "total": total, - "online": online, + "total": total, + "online": online, "abnormal": total - online, }}) } diff --git a/backend/controller/snmp_collect.go b/backend/controller/snmp_collect.go index 000a899..60c1b4c 100644 --- a/backend/controller/snmp_collect.go +++ b/backend/controller/snmp_collect.go @@ -198,4 +198,4 @@ func (h *SNMPHandler) ListDeviceMetrics(c *gin.Context) { func (h *SNMPHandler) StopCollector() { close(h.stopCh) h.wg.Wait() -} \ No newline at end of file +} diff --git a/backend/controller/snmp_metrics_history.go b/backend/controller/snmp_metrics_history.go index 51a6596..abcc8dc 100644 --- a/backend/controller/snmp_metrics_history.go +++ b/backend/controller/snmp_metrics_history.go @@ -260,7 +260,7 @@ func snmpMetricLabel(name, index string) string { "h3c_fan_speed": "H3C 风扇", "hw_fan_speed": "华为 风扇", "ruijie_fan_speed": "锐捷 风扇", "dell_fan_speed": "Dell 风扇", "inspur_fan_speed": "浪潮 风扇", "ent_sensor_value": "实体传感器", - "if_in_octets": "入方向流量", "if_out_octets": "出方向流量", + "if_in_octets": "入方向流量", "if_out_octets": "出方向流量", } label := labels[name] if label == "" { @@ -270,4 +270,4 @@ func snmpMetricLabel(name, index string) string { label += " #" + index } return label -} \ No newline at end of file +} diff --git a/backend/controller/system_task.go b/backend/controller/system_task.go index c3d235d..a124a0e 100644 --- a/backend/controller/system_task.go +++ b/backend/controller/system_task.go @@ -32,7 +32,7 @@ func (h *SystemTaskHandler) ListSystemTasks(c *gin.Context) { c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", - Data: model.ListResponse{List: list, Total: int64(len(list))}, + Data: model.ListResponse{List: list, Total: int64(len(list))}, }) } @@ -50,4 +50,4 @@ func (h *SystemTaskHandler) RunSystemTask(c *gin.Context) { Message: "执行完成", Data: item, }) -} \ No newline at end of file +} diff --git a/backend/controller/tool_config.go b/backend/controller/tool_config.go index 9bc8c40..ecb5490 100644 --- a/backend/controller/tool_config.go +++ b/backend/controller/tool_config.go @@ -105,11 +105,11 @@ func (h *ToolConfigHandler) UpdateTool(c *gin.Context) { } var req struct { - DisplayName *string `json:"display_name"` - Description *string `json:"description"` + DisplayName *string `json:"display_name"` + Description *string `json:"description"` InstallMethod *model.ToolInstallMethod `json:"install_method"` - Enabled *bool `json:"enabled"` - SortOrder *int `json:"sort_order"` + Enabled *bool `json:"enabled"` + SortOrder *int `json:"sort_order"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) @@ -356,4 +356,4 @@ func getPresetVersions(toolName string) []map[string]string { default: return nil } -} \ No newline at end of file +} diff --git a/backend/controller/trace_proxy.go b/backend/controller/trace_proxy.go index 227dee9..36c336f 100644 --- a/backend/controller/trace_proxy.go +++ b/backend/controller/trace_proxy.go @@ -34,37 +34,37 @@ type jaegerServicesResp struct { } type jaegerTraceResp struct { - Data []jaegerTrace `json:"data"` - Total int `json:"total"` + Data []jaegerTrace `json:"data"` + Total int `json:"total"` } type jaegerTrace struct { - TraceID string `json:"traceID"` - Spans []jaegerSpan `json:"spans"` + TraceID string `json:"traceID"` + Spans []jaegerSpan `json:"spans"` Processes map[string]jaegerProc `json:"processes"` } type jaegerSpan struct { - TraceID string `json:"traceID"` - SpanID string `json:"spanID"` - OperationName string `json:"operationName"` - References []jaegerRef `json:"references"` - StartTime int64 `json:"startTime"` // microseconds - Duration int64 `json:"duration"` // microseconds - Tags []jaegerTag `json:"tags"` - ProcessID string `json:"processID"` - Logs []jaegerLog `json:"logs"` + TraceID string `json:"traceID"` + SpanID string `json:"spanID"` + OperationName string `json:"operationName"` + References []jaegerRef `json:"references"` + StartTime int64 `json:"startTime"` // microseconds + Duration int64 `json:"duration"` // microseconds + Tags []jaegerTag `json:"tags"` + ProcessID string `json:"processID"` + Logs []jaegerLog `json:"logs"` } type jaegerRef struct { - RefType string `json:"refType"` - TraceID string `json:"traceID"` - SpanID string `json:"spanID"` + RefType string `json:"refType"` + TraceID string `json:"traceID"` + SpanID string `json:"spanID"` } type jaegerProc struct { - ServiceName string `json:"serviceName"` - Tags []jaegerTag `json:"tags"` + ServiceName string `json:"serviceName"` + Tags []jaegerTag `json:"tags"` } type jaegerTag struct { @@ -74,8 +74,8 @@ type jaegerTag struct { } type jaegerLog struct { - Timestamp int64 `json:"timestamp"` - Fields []jaegerTag `json:"fields"` + Timestamp int64 `json:"timestamp"` + Fields []jaegerTag `json:"fields"` } // ---- 前端期望的格式 ---- @@ -115,7 +115,7 @@ type serviceMetric struct { ServiceName string `json:"serviceName"` RequestCount int `json:"requestCount"` ErrorRate float64 `json:"errorRate"` - AvgDuration float64 `json:"avgDuration"` // ms + AvgDuration float64 `json:"avgDuration"` // ms P95Duration float64 `json:"p95Duration"` P99Duration float64 `json:"p99Duration"` } diff --git a/backend/controller/user.go b/backend/controller/user.go index b3e6d54..5a21b9f 100644 --- a/backend/controller/user.go +++ b/backend/controller/user.go @@ -4,8 +4,8 @@ import ( "net/http" "time" - "ezops/middleware" "ezops/config" + "ezops/middleware" "ezops/model" "github.com/gin-gonic/gin" @@ -62,20 +62,31 @@ func parseUserAgent(ua string) (browser, os string) { } // Browser switch { - case containsAny(ua, "Edg/"): browser = "Edge" - case containsAny(ua, "Chrome"): browser = "Chrome" - case containsAny(ua, "Safari") && !containsAny(ua, "Chrome"): browser = "Safari" - case containsAny(ua, "Firefox"): browser = "Firefox" - case containsAny(ua, "curl"): browser = "curl" - case containsAny(ua, "Python"): browser = "Python" + case containsAny(ua, "Edg/"): + browser = "Edge" + case containsAny(ua, "Chrome"): + browser = "Chrome" + case containsAny(ua, "Safari") && !containsAny(ua, "Chrome"): + browser = "Safari" + case containsAny(ua, "Firefox"): + browser = "Firefox" + case containsAny(ua, "curl"): + browser = "curl" + case containsAny(ua, "Python"): + browser = "Python" } // OS switch { - case containsAny(ua, "Windows"): os = "Windows" - case containsAny(ua, "Mac OS"): os = "macOS" - case containsAny(ua, "Linux") || containsAny(ua, "X11"): os = "Linux" - case containsAny(ua, "Android"): os = "Android" - case containsAny(ua, "iPhone") || containsAny(ua, "iPad"): os = "iOS" + case containsAny(ua, "Windows"): + os = "Windows" + case containsAny(ua, "Mac OS"): + os = "macOS" + case containsAny(ua, "Linux") || containsAny(ua, "X11"): + os = "Linux" + case containsAny(ua, "Android"): + os = "Android" + case containsAny(ua, "iPhone") || containsAny(ua, "iPad"): + os = "iOS" } return } @@ -363,7 +374,7 @@ func collectUserPermissionCodes(db *gorm.DB, userID uint, isAdmin bool) []string if isAdmin { return []string{"*"} } - // raw SQL: user → iam_user_roles → iam_roles → role_permissions → iam_permissions + // raw SQL: user → iam_user_roles → iam_roles → iam_role_permissions → iam_permissions // 逐层 JOIN,任一层缺失自然返回空 type permRow struct { Code string @@ -373,7 +384,7 @@ func collectUserPermissionCodes(db *gorm.DB, userID uint, isAdmin bool) []string SELECT DISTINCT p.code FROM iam_user_roles ur JOIN iam_roles r ON r.id = ur.role_id AND r.status = 1 - JOIN role_permissions rp ON rp.role_id = r.id + JOIN iam_role_permissions rp ON rp.role_id = r.id JOIN iam_permissions p ON p.id = rp.permission_id AND p.status = 1 WHERE ur.user_id = ? `, userID).Scan(&rows).Error diff --git a/backend/controller/user_admin.go b/backend/controller/user_admin.go index ba2498a..3264def 100644 --- a/backend/controller/user_admin.go +++ b/backend/controller/user_admin.go @@ -396,6 +396,7 @@ func (h *UserHandler) ListRoles(c *gin.Context) { Code: 200, Data: model.ListResponse{List: roles, Total: int64(len(roles))}, }) } + // GET /api/v1/admin/users/import-template func (h *UserHandler) ImportTemplate(c *gin.Context) { // UTF-8 BOM for Excel compatibility diff --git a/backend/controller/user_api_token.go b/backend/controller/user_api_token.go index cc2d341..2474c6c 100644 --- a/backend/controller/user_api_token.go +++ b/backend/controller/user_api_token.go @@ -273,7 +273,9 @@ func (h *UserHandler) UpdateApiAccessKey(c *gin.Context) { // ExtendApiAccessKey 延长有效期(兼容旧接口)// Logan.Li func (h *UserHandler) ExtendApiAccessKey(c *gin.Context) { userID := middleware.GetUserID(c) - var req struct{ DurationMonths int `json:"duration_months"` } + var req struct { + DurationMonths int `json:"duration_months"` + } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"}) return @@ -311,4 +313,4 @@ func (h *UserHandler) RevokeApiAccessKey(c *gin.Context) { return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "访问密钥已删除"}) -} \ No newline at end of file +} diff --git a/backend/controller/webshell.go b/backend/controller/webshell.go index dff6ca4..036e7f2 100644 --- a/backend/controller/webshell.go +++ b/backend/controller/webshell.go @@ -60,10 +60,10 @@ func newAsciicastRecorder() *asciicastRecorder { func (r *asciicastRecorder) writeHeader() { h, _ := json.Marshal(map[string]interface{}{ - "version": 2, - "width": 80, - "height": 24, - "timestamp": r.startTime.Unix(), + "version": 2, + "width": 80, + "height": 24, + "timestamp": r.startTime.Unix(), "aiops_input_recorded": false, "env": map[string]string{ "SHELL": "/bin/bash", diff --git a/backend/controller/ws_pipeline_log.go b/backend/controller/ws_pipeline_log.go index cf20829..6cdcc12 100644 --- a/backend/controller/ws_pipeline_log.go +++ b/backend/controller/ws_pipeline_log.go @@ -9,8 +9,8 @@ import ( "time" "ezops/middleware" - "ezops/pkg/wshub" ezredis "ezops/pkg/redis" + "ezops/pkg/wshub" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" @@ -125,7 +125,7 @@ func (h *WSPipelineLogHandler) GetLogHistory(c *gin.Context) { // 先查 Redis(执行中) logKey := fmt.Sprintf("pipeline:run:%d:logs", uint(runID)) ctx := c.Request.Context() - + raw, _ := h.cache.Get(ctx, logKey) if raw != "" && !strings.HasPrefix(raw, "[") { raw = "[]" @@ -137,7 +137,7 @@ func (h *WSPipelineLogHandler) GetLogHistory(c *gin.Context) { // 查 DB(已完成) var logs string - h.db.Table("ops_pipeline_runs").Where("id = ?", uint(runID)).Select("logs").Scan(&logs) + h.db.Table("deploy_pipeline_runs").Where("id = ?", uint(runID)).Select("logs").Scan(&logs) if logs == "" { logs = "[]" } diff --git a/backend/controller/zabbix_datasource.go b/backend/controller/zabbix_datasource.go index b6f0854..5ea8a28 100644 --- a/backend/controller/zabbix_datasource.go +++ b/backend/controller/zabbix_datasource.go @@ -113,24 +113,48 @@ func (h *ZabbixDSHandler) Update(c *gin.Context) { } updates := map[string]interface{}{} - if req.Name != nil { updates["name"] = *req.Name } - if req.URL != nil { updates["url"] = *req.URL } - if req.Username != nil { updates["username"] = *req.Username } - if req.Password != nil && *req.Password != "" { updates["password"] = *req.Password } - if req.TrendsAfter != nil { updates["trends_after"] = *req.TrendsAfter } - if req.TrendsRange != nil { updates["trends_range"] = *req.TrendsRange } - if req.TrendsCacheTTL != nil { updates["trends_cache_ttl"] = *req.TrendsCacheTTL } - if req.TrendsTimeout != nil { updates["trends_timeout"] = *req.TrendsTimeout } - if req.DirectDB != nil { updates["direct_db"] = *req.DirectDB } - if req.DisableAcknowledges != nil { updates["disable_acknowledges"] = *req.DisableAcknowledges } - if req.DisableDataAlignment != nil { updates["disable_data_alignment"] = *req.DisableDataAlignment } + if req.Name != nil { + updates["name"] = *req.Name + } + if req.URL != nil { + updates["url"] = *req.URL + } + if req.Username != nil { + updates["username"] = *req.Username + } + if req.Password != nil && *req.Password != "" { + updates["password"] = *req.Password + } + if req.TrendsAfter != nil { + updates["trends_after"] = *req.TrendsAfter + } + if req.TrendsRange != nil { + updates["trends_range"] = *req.TrendsRange + } + if req.TrendsCacheTTL != nil { + updates["trends_cache_ttl"] = *req.TrendsCacheTTL + } + if req.TrendsTimeout != nil { + updates["trends_timeout"] = *req.TrendsTimeout + } + if req.DirectDB != nil { + updates["direct_db"] = *req.DirectDB + } + if req.DisableAcknowledges != nil { + updates["disable_acknowledges"] = *req.DisableAcknowledges + } + if req.DisableDataAlignment != nil { + updates["disable_data_alignment"] = *req.DisableDataAlignment + } if req.IsDefault != nil { updates["is_default"] = *req.IsDefault if *req.IsDefault { h.db.Model(&model.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false) } } - if req.Status != nil { updates["status"] = *req.Status } + if req.Status != nil { + updates["status"] = *req.Status + } if len(updates) > 0 { if err := h.db.Model(&ds).Updates(updates).Error; err != nil { diff --git a/backend/controller/zabbix_proxy.go b/backend/controller/zabbix_proxy.go index 5cfca39..c16c2c2 100644 --- a/backend/controller/zabbix_proxy.go +++ b/backend/controller/zabbix_proxy.go @@ -28,7 +28,7 @@ type ZabbixHostSummary struct { Hostname string `json:"hostname"` Name string `json:"name"` IP string `json:"ip"` - Available string `json:"available"` // 0=未知 1=可用 2=不可用 + Available string `json:"available"` // 0=未知 1=可用 2=不可用 CPUUsage float64 `json:"cpu_usage"` MemoryUsage float64 `json:"memory_usage"` DiskUsage float64 `json:"disk_usage"` @@ -71,7 +71,7 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) { // 获取主机列表(含接口和组信息) hosts, err := client.GetHosts(ctx, &zabbix.GetHostsParams{ - Status: 0, // 只获取被监控的主机 + Status: 0, // 只获取被监控的主机 SelectInterfaces: []string{"interfaceid", "hostid", "main", "type", "useip", "ip", "dns", "port"}, SelectGroups: []string{"groupid", "name"}, Output: []string{"hostid", "host", "name", "status", "available", "description"}, @@ -85,17 +85,17 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) { // 为每台主机查询监控项 var summaries []ZabbixHostSummary itemKeys := []string{ - "system.cpu.util[,idle]", // CPU 空闲率 - "system.cpu.util", // CPU 使用率(备用) - "vm.memory.size[total]", // 总内存 bytes - "vm.memory.size[available]", // 可用内存 bytes - "vfs.fs.size[/,total]", // 总磁盘 bytes - "vfs.fs.size[/,used]", // 已用磁盘 bytes - "system.cpu.load[all,avg5]", // 5分钟负载 - "net.if.in[ens18]", // 网络流入 - "net.if.out[ens18]", // 网络流出 - "system.cpu.num", // CPU 数量 - "system.uptime", // 运行时间 + "system.cpu.util[,idle]", // CPU 空闲率 + "system.cpu.util", // CPU 使用率(备用) + "vm.memory.size[total]", // 总内存 bytes + "vm.memory.size[available]", // 可用内存 bytes + "vfs.fs.size[/,total]", // 总磁盘 bytes + "vfs.fs.size[/,used]", // 已用磁盘 bytes + "system.cpu.load[all,avg5]", // 5分钟负载 + "net.if.in[ens18]", // 网络流入 + "net.if.out[ens18]", // 网络流出 + "system.cpu.num", // CPU 数量 + "system.uptime", // 运行时间 } for _, host := range hosts { @@ -185,9 +185,9 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) { Code: 200, Message: "success", Data: gin.H{ - "version": version, - "hosts": summaries, - "total": len(summaries), + "version": version, + "hosts": summaries, + "total": len(summaries), }, }) } diff --git a/backend/embed.go b/backend/embed.go index 83b97d1..0d86eac 100644 --- a/backend/embed.go +++ b/backend/embed.go @@ -68,6 +68,13 @@ func setupFrontend(r *gin.Engine) { return } + if strings.HasPrefix(cleanPath, "assets/") { + // 构建哈希资源缺失时返回真实 404,避免 SPA fallback 把 index.html 当 JS 模块返回。 // Logan.Li + c.Header("Cache-Control", "no-store, no-cache, must-revalidate") + c.String(http.StatusNotFound, "static asset not found") + return + } + // SPA fallback → 直接返回 index.html 字节(避免 http.FileServer 的 301 redirect) indexBytes, err := fs.ReadFile(staticFS, "index.html") if err != nil { diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index 3ac4586..e74da6d 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -171,4 +171,4 @@ func GetIsAdmin(c *gin.Context) bool { return false } return isAdmin.(bool) -} \ No newline at end of file +} diff --git a/backend/middleware/automation_access.go b/backend/middleware/automation_access.go index f3cbb74..64d9bd7 100644 --- a/backend/middleware/automation_access.go +++ b/backend/middleware/automation_access.go @@ -46,7 +46,7 @@ func collectPermissionCodes(db *gorm.DB, userID uint) []string { SELECT DISTINCT p.code AS code FROM iam_user_roles ur JOIN iam_roles r ON r.id = ur.role_id AND r.status = 1 - JOIN role_permissions rp ON rp.role_id = r.id + JOIN iam_role_permissions rp ON rp.role_id = r.id JOIN iam_permissions p ON p.id = rp.permission_id AND p.status = 1 WHERE ur.user_id = ? `, userID).Scan(&rows) @@ -57,4 +57,4 @@ func collectPermissionCodes(db *gorm.DB, userID uint) []string { } } return codes -} \ No newline at end of file +} diff --git a/backend/model/agent.go b/backend/model/agent.go index 8808160..32dab02 100644 --- a/backend/model/agent.go +++ b/backend/model/agent.go @@ -6,33 +6,33 @@ import ( // AgentReport Agent 上报的原始指标数据 // Logan.Li type AgentReport struct { - ID uint `gorm:"primarykey" json:"id"` - Hostname string `gorm:"size:255;index;not null" json:"hostname"` - CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"` - CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"` - CPUCores int `json:"cpu_cores"` - Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"` - Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"` - Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"` - MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"` - MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"` - MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"` - MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"` - MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"` - DiskInfo string `gorm:"type:text" json:"disk_info"` - PortInfo string `gorm:"type:text" json:"port_info"` - NetInfo string `gorm:"type:text" json:"net_info"` - DiskIOInfo string `gorm:"type:text" json:"disk_io_info"` + ID uint `gorm:"primarykey" json:"id"` + Hostname string `gorm:"size:255;index;not null" json:"hostname"` + CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"` + CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"` + CPUCores int `json:"cpu_cores"` + Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"` + Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"` + Load15 float64 `gorm:"type:decimal(8,2)" json:"load15"` + MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"` + MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"` + MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"` + MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"` + MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"` + DiskInfo string `gorm:"type:text" json:"disk_info"` + PortInfo string `gorm:"type:text" json:"port_info"` + NetInfo string `gorm:"type:text" json:"net_info"` + DiskIOInfo string `gorm:"type:text" json:"disk_io_info"` // ──── v2.0 新增字段 ──── - SwapInfo string `gorm:"type:text" json:"swap_info"` // Swap JSON - ProcessTopInfo string `gorm:"type:text" json:"process_top_info"` // 进程 TOP JSON - FDInfo string `gorm:"type:text" json:"fd_info"` // FD 句柄 JSON - CSInfo string `gorm:"type:text" json:"cs_info"` // 上下文切换 JSON - HeartbeatInfo string `gorm:"type:text" json:"heartbeat_info"` // Agent 心跳 JSON + SwapInfo string `gorm:"type:text" json:"swap_info"` // Swap JSON + ProcessTopInfo string `gorm:"type:text" json:"process_top_info"` // 进程 TOP JSON + FDInfo string `gorm:"type:text" json:"fd_info"` // FD 句柄 JSON + CSInfo string `gorm:"type:text" json:"cs_info"` // 上下文切换 JSON + HeartbeatInfo string `gorm:"type:text" json:"heartbeat_info"` // Agent 心跳 JSON // ────────────────────────── - ReportTime time.Time `json:"report_time"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ReportTime time.Time `json:"report_time"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (AgentReport) TableName() string { @@ -41,20 +41,20 @@ func (AgentReport) TableName() string { // AgentLatestResponse Agent 最新状态(含服务器元信息) type AgentLatestResponse struct { - ServerID uint `json:"server_id"` - Hostname string `json:"hostname"` - IP string `json:"ip"` - CPUUsage float64 `json:"cpu_usage"` - CPUCores int `json:"cpu_cores"` - MemUsage float64 `json:"mem_usage"` - MemTotal float64 `json:"mem_total"` - MemUsed float64 `json:"mem_used"` - DiskUsage float64 `json:"disk_usage"` - DiskTotal int64 `json:"disk_total"` - DiskInfo string `json:"disk_info"` - PortInfo string `json:"port_info"` - NetInfo string `json:"net_info"` - DiskIOInfo string `json:"disk_io_info"` + ServerID uint `json:"server_id"` + Hostname string `json:"hostname"` + IP string `json:"ip"` + CPUUsage float64 `json:"cpu_usage"` + CPUCores int `json:"cpu_cores"` + MemUsage float64 `json:"mem_usage"` + MemTotal float64 `json:"mem_total"` + MemUsed float64 `json:"mem_used"` + DiskUsage float64 `json:"disk_usage"` + DiskTotal int64 `json:"disk_total"` + DiskInfo string `json:"disk_info"` + PortInfo string `json:"port_info"` + NetInfo string `json:"net_info"` + DiskIOInfo string `json:"disk_io_info"` // ──── v2.0 新增 ──── SwapInfo string `json:"swap_info"` ProcessTopInfo string `json:"process_top_info"` @@ -74,23 +74,23 @@ type AgentLatestResponse struct { // AgentReportRequest Agent 上报请求体 type AgentReportRequest struct { - Hostname string `json:"hostname" binding:"required"` - IP string `json:"ip"` - Timestamp string `json:"timestamp"` - AgentVersion string `json:"agent_version"` - CPU CPUMetric `json:"cpu"` - Memory MemMetric `json:"memory"` - Swap *SwapMetric `json:"swap,omitempty"` - Disks []DiskMetric `json:"disks"` - Ports []PortMetric `json:"ports"` - Network NetworkStatModel `json:"network"` - DiskIO DiskIOStatModel `json:"disk_io"` - System *SystemStatModel `json:"system,omitempty"` + Hostname string `json:"hostname" binding:"required"` + IP string `json:"ip"` + Timestamp string `json:"timestamp"` + AgentVersion string `json:"agent_version"` + CPU CPUMetric `json:"cpu"` + Memory MemMetric `json:"memory"` + Swap *SwapMetric `json:"swap,omitempty"` + Disks []DiskMetric `json:"disks"` + Ports []PortMetric `json:"ports"` + Network NetworkStatModel `json:"network"` + DiskIO DiskIOStatModel `json:"disk_io"` + System *SystemStatModel `json:"system,omitempty"` // ──── v2.0 新增 ──── - ProcessTop []ProcessMetric `json:"process_top,omitempty"` - FD *FDMetric `json:"fd,omitempty"` - ContextSwitches *CSMetric `json:"context_switches,omitempty"` - Heartbeat HeartbeatMetric `json:"heartbeat"` + ProcessTop []ProcessMetric `json:"process_top,omitempty"` + FD *FDMetric `json:"fd,omitempty"` + ContextSwitches *CSMetric `json:"context_switches,omitempty"` + Heartbeat HeartbeatMetric `json:"heartbeat"` ProbeResults []HealthProbeResult `json:"probe_results,omitempty"` // v2.1 健康探针结果 } @@ -198,14 +198,14 @@ type DiskIOStatModel struct { // AgentPortPolicy 主机端口监控策略 // Logan.Li type AgentPortPolicy struct { - ID uint `gorm:"primarykey" json:"id"` - Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"` - PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist - PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379] + ID uint `gorm:"primarykey" json:"id"` + Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"` + PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist + PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379] } func (AgentPortPolicy) TableName() string { - return "monitor_agent_port_policy" + return "monitor_agent_port_policies" } // AgentPortPolicyDTO 前端交互用的端口策略格式(含已解析的数组) @@ -214,4 +214,3 @@ type AgentPortPolicyDTO struct { PortFilterMode string `json:"port_filter_mode"` PortWhitelist []int `json:"port_whitelist"` } - diff --git a/backend/model/agent_cli.go b/backend/model/agent_cli.go deleted file mode 100644 index 2593a08..0000000 --- a/backend/model/agent_cli.go +++ /dev/null @@ -1,55 +0,0 @@ -// agent_cli.go - Agent CLI 会话与消息持久化模型 // Logan.Li -// 记录 AI 运维 Agent 的每次任务(关联主机 + 用户)及其 ReAct 推理过程 -// (思考/工具调用/输出/结果),供前端历史回放与审计。 -package model - -import "time" - -// AgentCLISession Agent CLI 任务会话 // Logan.Li -type AgentCLISession struct { - ID uint `gorm:"primarykey" json:"id"` - UserID uint `gorm:"index" json:"user_id"` // 发起用户 - Mode string `gorm:"size:16;default:host;index" json:"mode"` // host | kubernetes - ServerID uint `gorm:"index" json:"server_id"` // 主机模式目标主机 - ServerName string `gorm:"size:128" json:"server_name"` // 主机名快照 - ServerHost string `gorm:"size:64" json:"server_host"` // 主机 IP 快照 - ClusterID uint `gorm:"index" json:"cluster_id"` // K8s 模式集群 ID - ClusterName string `gorm:"size:128" json:"cluster_name"` // 集群名快照 - Namespace string `gorm:"size:128" json:"namespace"` // K8s 命名空间 - PodName string `gorm:"size:128" json:"pod_name"` // 可选目标 Pod - Container string `gorm:"size:128" json:"container"` // 可选目标容器 - Title string `gorm:"size:255" json:"title"` // 任务标题(query 截断) - Status string `gorm:"size:32;default:running" json:"status"` // running/done/aborted/error - Provider string `gorm:"size:50" json:"provider"` // LLM 提供商 - Model string `gorm:"size:100" json:"model"` // 模型名 - Rounds int `json:"rounds"` // ReAct 轮次 - TotalTokens int `json:"total_tokens"` // token 消耗 - Duration int64 `json:"duration"` // 耗时(毫秒) - Error string `gorm:"type:text" json:"error,omitempty"` // 错误信息 - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// TableName 表名 // Logan.Li -func (AgentCLISession) TableName() string { return "agent_cli_sessions" } - -// AgentCLIMessage Agent CLI 任务消息(ReAct 推理过程的每一步) // Logan.Li -type AgentCLIMessage struct { - ID uint `gorm:"primarykey" json:"id"` - SessionID uint `gorm:"index;not null" json:"session_id"` - Round int `json:"round"` // ReAct 轮次 - Type string `gorm:"size:32" json:"type"` // thought/tool_call/tool_result/done/error/session - Role string `gorm:"size:32" json:"role"` // user/assistant/tool - Content string `gorm:"type:text" json:"content"` // 思考文本/最终答案 - ToolName string `gorm:"size:64" json:"tool_name"` // 工具名 - ToolArgs string `gorm:"type:text" json:"tool_args"` // 工具参数 JSON - ToolOutput string `gorm:"type:text" json:"tool_output"` // 工具输出 - Risk string `gorm:"size:32" json:"risk"` // 风险等级 - Success bool `json:"success"` - Duration string `gorm:"size:32" json:"duration"` // 工具耗时 - Tokens int `json:"tokens"` - CreatedAt time.Time `gorm:"index" json:"created_at"` -} - -// TableName 表名 // Logan.Li -func (AgentCLIMessage) TableName() string { return "agent_cli_messages" } diff --git a/backend/model/agent_task.go b/backend/model/agent_task.go index b5a2db5..21384eb 100644 --- a/backend/model/agent_task.go +++ b/backend/model/agent_task.go @@ -2,25 +2,24 @@ package model import ( "time" - ) // AgentTask Agent 远程任务 // Logan.Li type AgentTask struct { - ID uint `gorm:"primarykey" json:"id"` - TaskID string `gorm:"size:36;uniqueIndex;not null" json:"task_id"` // UUID - Hostname string `gorm:"size:255;index;not null" json:"hostname"` // 目标主机 - Type string `gorm:"size:32;not null" json:"type"` // command / script / file - Command string `gorm:"type:text" json:"command"` // 命令或脚本内容 - Timeout int `gorm:"default:60" json:"timeout"` // 超时秒数 - Status string `gorm:"size:20;default:pending;index" json:"status"` // pending/running/success/failed/timeout - Result string `gorm:"type:text" json:"result"` // 执行输出 - ExitCode int `gorm:"default:-1" json:"exit_code"` // 退出码 - CreatedBy string `gorm:"size:255" json:"created_by"` // 创建人 - CreatedAt time.Time `json:"created_at"` - PickedAt *time.Time `json:"picked_at"` // Agent 拉取时间 - FinishedAt *time.Time `json:"finished_at"` // 完成时间 - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + TaskID string `gorm:"size:36;uniqueIndex;not null" json:"task_id"` // UUID + Hostname string `gorm:"size:255;index;not null" json:"hostname"` // 目标主机 + Type string `gorm:"size:32;not null" json:"type"` // command / script / file + Command string `gorm:"type:text" json:"command"` // 命令或脚本内容 + Timeout int `gorm:"default:60" json:"timeout"` // 超时秒数 + Status string `gorm:"size:20;default:pending;index" json:"status"` // pending/running/success/failed/timeout + Result string `gorm:"type:text" json:"result"` // 执行输出 + ExitCode int `gorm:"default:-1" json:"exit_code"` // 退出码 + CreatedBy string `gorm:"size:255" json:"created_by"` // 创建人 + CreatedAt time.Time `json:"created_at"` + PickedAt *time.Time `json:"picked_at"` // Agent 拉取时间 + FinishedAt *time.Time `json:"finished_at"` // 完成时间 + UpdatedAt time.Time `json:"updated_at"` } func (AgentTask) TableName() string { @@ -29,17 +28,17 @@ func (AgentTask) TableName() string { // AgentRule 告警规则(基于 Agent 数据) // Logan.Li type AgentRule struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:255;not null" json:"name"` - Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机 - Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process/fd - Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= == - Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"` - Port int `json:"port"` // port_down 专用 - Enabled bool `gorm:"default:true" json:"enabled"` - LastTriggered *time.Time `json:"last_triggered"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机 + Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process/fd + Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= == + Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"` + Port int `json:"port"` // port_down 专用 + Enabled bool `gorm:"default:true" json:"enabled"` + LastTriggered *time.Time `json:"last_triggered"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (AgentRule) TableName() string { @@ -48,18 +47,18 @@ func (AgentRule) TableName() string { // AgentAlert Agent 告警事件 // Logan.Li type AgentAlert struct { - ID uint `gorm:"primarykey" json:"id"` - RuleName string `gorm:"size:255" json:"rule_name"` - Hostname string `gorm:"size:255;index" json:"hostname"` - Metric string `gorm:"size:32" json:"metric"` - Severity string `gorm:"size:20;default:warning" json:"severity"` // critical/warning/info - Message string `gorm:"type:text" json:"message"` - Value float64 `gorm:"type:decimal(10,2)" json:"value"` - Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"` - Resolved bool `gorm:"default:false;index" json:"resolved"` - ResolvedAt *time.Time `json:"resolved_at"` - CreatedAt time.Time `gorm:"index" json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + RuleName string `gorm:"size:255" json:"rule_name"` + Hostname string `gorm:"size:255;index" json:"hostname"` + Metric string `gorm:"size:32" json:"metric"` + Severity string `gorm:"size:20;default:warning" json:"severity"` // critical/warning/info + Message string `gorm:"type:text" json:"message"` + Value float64 `gorm:"type:decimal(10,2)" json:"value"` + Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"` + Resolved bool `gorm:"default:false;index" json:"resolved"` + ResolvedAt *time.Time `json:"resolved_at"` + CreatedAt time.Time `gorm:"index" json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (AgentAlert) TableName() string { diff --git a/backend/model/ai_runtime.go b/backend/model/ai_runtime.go new file mode 100644 index 0000000..c6b4d28 --- /dev/null +++ b/backend/model/ai_runtime.go @@ -0,0 +1,172 @@ +// ai_runtime.go - AI 运行、工具能力和执行审计统一模型 // Logan.Li +package model + +import "time" + +// AIRun 统一记录 Agent CLI、告警分析、智能助手、MCP/技能调用发起的 AI 任务。 // Logan.Li +type AIRun struct { + ID uint `gorm:"primarykey" json:"id"` + RunID string `gorm:"size:64;uniqueIndex;not null" json:"run_id"` // 对外 trace/run 标识 // Logan.Li + ParentRunID string `gorm:"size:64;index" json:"parent_run_id"` // 子任务关联 // Logan.Li + UserID uint `gorm:"index" json:"user_id"` // 发起用户 // Logan.Li + Feature string `gorm:"size:64;index;not null" json:"feature"` // agent_cli/alert_analysis/assistant/mcp // Logan.Li + Mode string `gorm:"size:32;index" json:"mode"` // host/kubernetes/aiops/general // Logan.Li + Title string `gorm:"size:255" json:"title"` // 任务标题 // Logan.Li + Status string `gorm:"size:32;index;default:running" json:"status"` // running/done/error/aborted // Logan.Li + SubjectType string `gorm:"size:64;index" json:"subject_type"` // host/k8s_cluster/namespace/pod/aiops_module/mcp_server/skill // Logan.Li + SubjectID string `gorm:"size:128;index" json:"subject_id"` // 资源 ID 或稳定键 // Logan.Li + SubjectName string `gorm:"size:255" json:"subject_name"` // 资源快照名称 // Logan.Li + SubjectScope string `gorm:"size:255;index" json:"subject_scope"` // prod/ns/app 等范围键 // Logan.Li + Provider string `gorm:"size:50" json:"provider"` // LLM 提供商 // Logan.Li + Model string `gorm:"size:100" json:"model"` // 模型名 // Logan.Li + SafetyMode string `gorm:"size:32" json:"safety_mode"` // strict/balanced/permissive // Logan.Li + ContextPackID string `gorm:"size:64;index" json:"context_pack_id"` // 上下文包引用 // Logan.Li + PromptTokens int `json:"prompt_tokens"` // 输入 token // Logan.Li + CompletionTokens int `json:"completion_tokens"` // 输出 token // Logan.Li + TotalTokens int `json:"total_tokens"` // 总 token // Logan.Li + Rounds int `json:"rounds"` // Agent 轮次 // Logan.Li + Duration int64 `json:"duration"` // 毫秒 // Logan.Li + Error string `gorm:"type:text" json:"error,omitempty"` // 错误摘要 // Logan.Li + StartedAt time.Time `gorm:"index" json:"started_at"` + FinishedAt *time.Time `json:"finished_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (AIRun) TableName() string { return "ai_runs" } + +// AIRunMessage 统一 AI 对话消息,负责回放,不承载工具审计明细。 // Logan.Li +type AIRunMessage struct { + ID uint `gorm:"primarykey" json:"id"` + RunID string `gorm:"size:64;index;not null" json:"run_id"` + Round int `gorm:"index" json:"round"` + Seq int `gorm:"index" json:"seq"` + Role string `gorm:"size:32;index" json:"role"` // system/user/assistant/tool // Logan.Li + Type string `gorm:"size:32;index" json:"type"` // message/thought/tool_call/tool_result/done/error/status // Logan.Li + Content string `gorm:"type:text" json:"content"` // 脱敏内容 // Logan.Li + ToolName string `gorm:"size:128;index" json:"tool_name"` // 可选工具名 // Logan.Li + ToolCallID string `gorm:"size:128;index" json:"tool_call_id"` // LLM 工具调用 ID // Logan.Li + SpanID string `gorm:"size:64;index" json:"span_id"` // 关联 span // Logan.Li + Tokens int `json:"tokens"` + CreatedAt time.Time `gorm:"index" json:"created_at"` +} + +func (AIRunMessage) TableName() string { return "ai_run_messages" } + +// AIRunSpan 统一 AI trace span,记录 LLM、工具、guardrail、确认、审批等事件。 // Logan.Li +type AIRunSpan struct { + ID uint `gorm:"primarykey" json:"id"` + RunID string `gorm:"size:64;index;not null" json:"run_id"` + SpanID string `gorm:"size:64;uniqueIndex;not null" json:"span_id"` + ParentSpanID string `gorm:"size:64;index" json:"parent_span_id"` + Kind string `gorm:"size:32;index;not null" json:"kind"` // llm/tool/guardrail/confirm/approval/context // Logan.Li + Name string `gorm:"size:128;index" json:"name"` // 工具名或步骤名 // Logan.Li + CapabilityID uint `gorm:"index" json:"capability_id"` // 能力注册表 ID // Logan.Li + ToolSource string `gorm:"size:32;index" json:"tool_source"` // builtin/mcp/skill/shell/kubernetes/aiops // Logan.Li + Input string `gorm:"type:text" json:"input"` // 脱敏输入 // Logan.Li + Output string `gorm:"type:text" json:"output"` // 脱敏输出摘要 // Logan.Li + OutputSize int `json:"output_size"` // 脱敏后输出长度 // Logan.Li + Risk string `gorm:"size:32;index" json:"risk"` // safe/low/medium/high/critical // Logan.Li + Decision string `gorm:"size:32;index" json:"decision"` // auto/approved/rejected/denied/escalated // Logan.Li + Status string `gorm:"size:32;index" json:"status"` // running/success/error/skipped // Logan.Li + Error string `gorm:"type:text" json:"error"` // 错误摘要 // Logan.Li + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + Duration int64 `json:"duration"` // 毫秒 // Logan.Li + StartedAt time.Time `gorm:"index" json:"started_at"` + EndedAt *time.Time `json:"ended_at,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func (AIRunSpan) TableName() string { return "ai_run_spans" } + +// AICapability 注册 AIOps 内置工具、MCP 工具、Skill、Shell/K8s 工具的统一能力目录。 // Logan.Li +type AICapability struct { + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;uniqueIndex;not null" json:"name"` // 稳定工具名 // Logan.Li + DisplayName string `gorm:"size:128" json:"display_name"` + Source string `gorm:"size:32;index;not null" json:"source"` // builtin/mcp/skill/shell/kubernetes/aiops // Logan.Li + Category string `gorm:"size:64;index" json:"category"` // asset/k8s/monitor/alert/change/security // Logan.Li + RiskLevel string `gorm:"size:32;index;default:safe" json:"risk_level"` + Schema string `gorm:"type:text" json:"schema"` // JSON Schema // Logan.Li + Config string `gorm:"type:text" json:"config"` // MCP server/skill 配置快照 // Logan.Li + Description string `gorm:"type:text" json:"description"` + Enabled bool `gorm:"default:true;index" json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (AICapability) TableName() string { return "ai_capabilities" } + +// AICapabilityPolicy 描述工具能力在用户、角色、资源范围上的准入和审批策略。 // Logan.Li +type AICapabilityPolicy struct { + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` + CapabilityID uint `gorm:"index" json:"capability_id"` + SubjectType string `gorm:"size:32;index" json:"subject_type"` // user/role/all // Logan.Li + SubjectID string `gorm:"size:64;index" json:"subject_id"` + ResourceType string `gorm:"size:64;index" json:"resource_type"` // host/k8s_cluster/namespace/pod/aiops_module/mcp_server/skill // Logan.Li + ResourcePattern string `gorm:"size:255" json:"resource_pattern"` // 正则或通配符 // Logan.Li + Effect string `gorm:"size:16;index;default:allow" json:"effect"` // allow/deny // Logan.Li + RequireConfirm bool `gorm:"default:false" json:"require_confirm"` + RequireApproval bool `gorm:"default:false" json:"require_approval"` + MaxRisk string `gorm:"size:32;default:high" json:"max_risk"` + Enabled bool `gorm:"default:true;index" json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (AICapabilityPolicy) TableName() string { return "ai_capability_policies" } + +// OpsSession 统一人机执行会话,覆盖 SSH/RDP/Pod/kubectl/Agent CLI。 // Logan.Li +type OpsSession struct { + ID uint `gorm:"primarykey" json:"id"` + SessionID string `gorm:"size:64;uniqueIndex;not null" json:"session_id"` + RunID string `gorm:"size:64;index" json:"run_id"` // AI 发起时关联 ai_runs // Logan.Li + UserID uint `gorm:"index" json:"user_id"` + SessionType string `gorm:"size:32;index;not null" json:"session_type"` // ssh/rdp/pod_exec/kubectl/agent_cli // Logan.Li + ActorType string `gorm:"size:32;index;default:user" json:"actor_type"` // user/ai/system // Logan.Li + TargetType string `gorm:"size:64;index" json:"target_type"` // host/k8s_cluster/pod // Logan.Li + TargetID string `gorm:"size:128;index" json:"target_id"` + TargetName string `gorm:"size:255" json:"target_name"` + ClientIP string `gorm:"size:64" json:"client_ip"` + Status string `gorm:"size:32;index" json:"status"` // active/closed/failed // Logan.Li + RecordRef string `gorm:"size:255" json:"record_ref"` // 录制对象存储或分片引用 // Logan.Li + RecordSize int `json:"record_size"` + Duration int64 `json:"duration"` // 秒 // Logan.Li + Error string `gorm:"type:text" json:"error,omitempty"` + StartedAt time.Time `gorm:"index" json:"started_at"` + EndedAt *time.Time `json:"ended_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (OpsSession) TableName() string { return "ai_ops_sessions" } + +// OpsCommand 统一命令/动作审计,覆盖人执行和 AI 工具执行。 // Logan.Li +type OpsCommand struct { + ID uint `gorm:"primarykey" json:"id"` + CommandID string `gorm:"size:64;uniqueIndex;not null" json:"command_id"` + SessionID string `gorm:"size:64;index" json:"session_id"` + RunID string `gorm:"size:64;index" json:"run_id"` + SpanID string `gorm:"size:64;index" json:"span_id"` + UserID uint `gorm:"index" json:"user_id"` + ActorType string `gorm:"size:32;index;default:user" json:"actor_type"` // user/ai/system // Logan.Li + TargetType string `gorm:"size:64;index" json:"target_type"` + TargetID string `gorm:"size:128;index" json:"target_id"` + CommandType string `gorm:"size:32;index" json:"command_type"` // shell/k8s_action/mcp/skill/api // Logan.Li + Command string `gorm:"type:text" json:"command"` // 脱敏命令或动作参数 // Logan.Li + Risk string `gorm:"size:32;index" json:"risk"` + Decision string `gorm:"size:32;index" json:"decision"` + Status string `gorm:"size:32;index" json:"status"` + ExitCode int `gorm:"default:-1" json:"exit_code"` + OutputRef string `gorm:"size:255" json:"output_ref"` // 大输出引用 // Logan.Li + OutputSize int `json:"output_size"` + Duration int64 `json:"duration"` // 毫秒 // Logan.Li + Error string `gorm:"type:text" json:"error"` + ExecutedAt time.Time `gorm:"index" json:"executed_at"` + CreatedAt time.Time `json:"created_at"` +} + +func (OpsCommand) TableName() string { return "ai_ops_commands" } diff --git a/backend/model/alert.go b/backend/model/alert.go index e6cfce4..e4300be 100644 --- a/backend/model/alert.go +++ b/backend/model/alert.go @@ -56,41 +56,41 @@ const ( // AlertRule 告警规则(增强版) type AlertRule struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:规则名称" json:"name"` - Description string `gorm:"type:varchar(500);comment:规则描述" json:"description,omitempty"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:规则名称" json:"name"` + Description string `gorm:"type:varchar(500);comment:规则描述" json:"description,omitempty"` // ====== 原 V1 兼容字段 ====== - Type string `gorm:"type:varchar(32);comment:规则类型: metric/log/event" json:"type,omitempty"` - Source string `gorm:"type:varchar(64);comment:数据源: prometheus/zabbix/custom/loki/elasticsearch" json:"source,omitempty"` - Expression string `gorm:"type:text;comment:PromQL/Zabbix/LogQL/Lucene表达式" json:"expression,omitempty"` - DatasourceID uint `gorm:"index;comment:日志/指标数据源ID" json:"datasource_id,omitempty"` - Severity string `gorm:"type:varchar(32);default:'warning';comment:告警级别" json:"severity"` - Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"` - Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"` + Type string `gorm:"type:varchar(32);comment:规则类型: metric/log/event" json:"type,omitempty"` + Source string `gorm:"type:varchar(64);comment:数据源: prometheus/zabbix/custom/loki/elasticsearch" json:"source,omitempty"` + Expression string `gorm:"type:text;comment:PromQL/Zabbix/LogQL/Lucene表达式" json:"expression,omitempty"` + DatasourceID uint `gorm:"index;comment:日志/指标数据源ID" json:"datasource_id,omitempty"` + Severity string `gorm:"type:varchar(32);default:'warning';comment:告警级别" json:"severity"` + Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"` + Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"` // ====== V2 增强字段 ====== - Status AlertRuleStatus `gorm:"type:varchar(16);default:'enabled';comment:规则状态" json:"status,omitempty"` - TargetType string `gorm:"type:varchar(32);comment:目标类型" json:"target_type,omitempty"` - TargetID uint `gorm:"index;comment:目标ID(0表示全部)" json:"target_id,omitempty"` - TargetTags string `gorm:"type:varchar(256);comment:目标标签筛选" json:"target_tags,omitempty"` - Conditions string `gorm:"type:text;comment:触发条件JSON数组" json:"conditions,omitempty"` - AggWindow int `gorm:"default:300;comment:聚合窗口(秒)" json:"agg_window,omitempty"` - AggFunction string `gorm:"type:varchar(16);default:'avg';comment:聚合函数" json:"agg_function,omitempty"` - RecoverCount int `gorm:"default:1;comment:恢复次数" json:"recover_count,omitempty"` - RecoverWindow int `gorm:"default:300;comment:恢复检查窗口(秒)" json:"recover_window,omitempty"` - Interval int `gorm:"default:60;comment:评估间隔(秒)" json:"interval,omitempty"` - CheckTimes int `gorm:"default:1;comment:连续触发次数" json:"check_times,omitempty"` - NotifyMode string `gorm:"type:varchar(16);default:'every';comment:通知模式: every/once/upgrade" json:"notify_mode,omitempty"` - ChannelIDs string `gorm:"type:varchar(256);comment:通知渠道ID列表JSON" json:"channel_ids,omitempty"` - NotifyChannels string `gorm:"type:text;comment:通知渠道JSON(兼容V1)" json:"notify_channels,omitempty"` - TemplateID uint `gorm:"comment:通知模板ID" json:"template_id,omitempty"` - UpgradeID uint `gorm:"comment:升级策略ID" json:"upgrade_id,omitempty"` - SilenceID uint `gorm:"comment:静默规则ID" json:"silence_id,omitempty"` - Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"` - Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"` - CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by,omitempty"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + Status AlertRuleStatus `gorm:"type:varchar(16);default:'enabled';comment:规则状态" json:"status,omitempty"` + TargetType string `gorm:"type:varchar(32);comment:目标类型" json:"target_type,omitempty"` + TargetID uint `gorm:"index;comment:目标ID(0表示全部)" json:"target_id,omitempty"` + TargetTags string `gorm:"type:varchar(256);comment:目标标签筛选" json:"target_tags,omitempty"` + Conditions string `gorm:"type:text;comment:触发条件JSON数组" json:"conditions,omitempty"` + AggWindow int `gorm:"default:300;comment:聚合窗口(秒)" json:"agg_window,omitempty"` + AggFunction string `gorm:"type:varchar(16);default:'avg';comment:聚合函数" json:"agg_function,omitempty"` + RecoverCount int `gorm:"default:1;comment:恢复次数" json:"recover_count,omitempty"` + RecoverWindow int `gorm:"default:300;comment:恢复检查窗口(秒)" json:"recover_window,omitempty"` + Interval int `gorm:"default:60;comment:评估间隔(秒)" json:"interval,omitempty"` + CheckTimes int `gorm:"default:1;comment:连续触发次数" json:"check_times,omitempty"` + NotifyMode string `gorm:"type:varchar(16);default:'every';comment:通知模式: every/once/upgrade" json:"notify_mode,omitempty"` + ChannelIDs string `gorm:"type:varchar(256);comment:通知渠道ID列表JSON" json:"channel_ids,omitempty"` + NotifyChannels string `gorm:"type:text;comment:通知渠道JSON(兼容V1)" json:"notify_channels,omitempty"` + TemplateID uint `gorm:"comment:通知模板ID" json:"template_id,omitempty"` + UpgradeID uint `gorm:"comment:升级策略ID" json:"upgrade_id,omitempty"` + SilenceID uint `gorm:"comment:静默规则ID" json:"silence_id,omitempty"` + Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"` + Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"` + CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by,omitempty"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` } func (AlertRule) TableName() string { @@ -112,40 +112,40 @@ const ( // AlertEvent 告警事件(增强版) type AlertEvent struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - RuleID uint `gorm:"not null;index:idx_rule_time;comment:规则ID" json:"rule_id"` - RuleName string `gorm:"type:varchar(128);comment:规则名称" json:"rule_name,omitempty"` - Severity string `gorm:"type:varchar(32);comment:告警级别" json:"severity,omitempty"` - Status string `gorm:"type:varchar(32);default:'firing';comment:事件状态" json:"status"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + RuleID uint `gorm:"not null;index:idx_rule_time;comment:规则ID" json:"rule_id"` + RuleName string `gorm:"type:varchar(128);comment:规则名称" json:"rule_name,omitempty"` + Severity string `gorm:"type:varchar(32);comment:告警级别" json:"severity,omitempty"` + Status string `gorm:"type:varchar(32);default:'firing';comment:事件状态" json:"status"` // ====== 原V1兼容字段 ====== - Target string `gorm:"type:varchar(255);comment:目标标签" json:"target,omitempty"` - Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"` - Value float64 `gorm:"comment:触发值" json:"value,omitempty"` + Target string `gorm:"type:varchar(255);comment:目标标签" json:"target,omitempty"` + Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"` + Value float64 `gorm:"comment:触发值" json:"value,omitempty"` // ====== V2增强字段 ====== - TargetID uint `gorm:"index;comment:目标ID" json:"target_id,omitempty"` - TargetName string `gorm:"type:varchar(128);comment:目标名称" json:"target_name,omitempty"` - TargetIP string `gorm:"type:varchar(64);comment:目标IP" json:"target_ip,omitempty"` - Title string `gorm:"type:varchar(256);comment:告警标题" json:"title,omitempty"` - MetricName string `gorm:"type:varchar(64);comment:指标名称" json:"metric_name,omitempty"` - Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"` - Operator string `gorm:"type:varchar(8);comment:运算符" json:"operator,omitempty"` - Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` - Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"` - Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"` - StartedAt time.Time `gorm:"not null;index:idx_rule_time;comment:触发时间" json:"started_at"` - ResolvedAt *time.Time `gorm:"comment:恢复时间" json:"resolved_at,omitempty"` - Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"` - LastEvalAt time.Time `gorm:"comment:最后评估时间" json:"last_eval_at,omitempty"` - NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count,omitempty"` - LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"` - UpgradeLevel int `gorm:"default:0;comment:当前升级级别" json:"upgrade_level,omitempty"` - Acknowledged bool `gorm:"default:false;comment:是否已确认" json:"acknowledged"` - AcknowledgedBy *uint `gorm:"comment:确认人ID" json:"acknowledged_by,omitempty"` - AcknowledgedAt *time.Time `gorm:"comment:确认时间" json:"acknowledged_at,omitempty"` - AckComment string `gorm:"type:varchar(256);comment:确认备注" json:"ack_comment,omitempty"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + TargetID uint `gorm:"index;comment:目标ID" json:"target_id,omitempty"` + TargetName string `gorm:"type:varchar(128);comment:目标名称" json:"target_name,omitempty"` + TargetIP string `gorm:"type:varchar(64);comment:目标IP" json:"target_ip,omitempty"` + Title string `gorm:"type:varchar(256);comment:告警标题" json:"title,omitempty"` + MetricName string `gorm:"type:varchar(64);comment:指标名称" json:"metric_name,omitempty"` + Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"` + Operator string `gorm:"type:varchar(8);comment:运算符" json:"operator,omitempty"` + Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` + Labels string `gorm:"type:text;comment:标签JSON" json:"labels,omitempty"` + Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"` + StartedAt time.Time `gorm:"not null;index:idx_rule_time;comment:触发时间" json:"started_at"` + ResolvedAt *time.Time `gorm:"comment:恢复时间" json:"resolved_at,omitempty"` + Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"` + LastEvalAt time.Time `gorm:"comment:最后评估时间" json:"last_eval_at,omitempty"` + NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count,omitempty"` + LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"` + UpgradeLevel int `gorm:"default:0;comment:当前升级级别" json:"upgrade_level,omitempty"` + Acknowledged bool `gorm:"default:false;comment:是否已确认" json:"acknowledged"` + AcknowledgedBy *uint `gorm:"comment:确认人ID" json:"acknowledged_by,omitempty"` + AcknowledgedAt *time.Time `gorm:"comment:确认时间" json:"acknowledged_at,omitempty"` + AckComment string `gorm:"type:varchar(256);comment:确认备注" json:"ack_comment,omitempty"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` // 关联 Rule *AlertRule `json:"rule,omitempty" gorm:"foreignKey:RuleID"` @@ -160,14 +160,14 @@ func (AlertEvent) TableName() string { // ============================================ type AlertCondition struct { - Metric string `json:"metric"` - Operator Operator `json:"operator"` - Threshold float64 `json:"threshold"` - Unit string `json:"unit,omitempty"` - Index string `json:"index,omitempty"` - LogicOp string `json:"logic_op,omitempty"` - AggFunc AggFunction `json:"agg_func,omitempty"` - Window int `json:"window,omitempty"` + Metric string `json:"metric"` + Operator Operator `json:"operator"` + Threshold float64 `json:"threshold"` + Unit string `json:"unit,omitempty"` + Index string `json:"index,omitempty"` + LogicOp string `json:"logic_op,omitempty"` + AggFunc AggFunction `json:"agg_func,omitempty"` + Window int `json:"window,omitempty"` } func (AlertCondition) TableName() string { @@ -184,21 +184,21 @@ type AlertConditionGroup struct { // ============================================ type AlertNotifyTemplate struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"` - Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` - ChannelType string `gorm:"type:varchar(32);not null;comment:适用渠道类型: email/sms/webhook/dingtalk/wecom/feishu" json:"channel_type"` - TitleTemplate string `gorm:"type:varchar(256);comment:标题模板" json:"title_template,omitempty"` - BodyTemplate string `gorm:"type:text;comment:正文模板" json:"body_template,omitempty"` - Variables string `gorm:"type:text;comment:可用变量说明JSON" json:"variables,omitempty"` - RecoverTitleTemplate string `gorm:"type:varchar(256);comment:恢复标题模板" json:"recover_title_template,omitempty"` - RecoverBodyTemplate string `gorm:"type:text;comment:恢复正文模板" json:"recover_body_template,omitempty"` - IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"` - Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` - CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + ChannelType string `gorm:"type:varchar(32);not null;comment:适用渠道类型: email/sms/webhook/dingtalk/wecom/feishu" json:"channel_type"` + TitleTemplate string `gorm:"type:varchar(256);comment:标题模板" json:"title_template,omitempty"` + BodyTemplate string `gorm:"type:text;comment:正文模板" json:"body_template,omitempty"` + Variables string `gorm:"type:text;comment:可用变量说明JSON" json:"variables,omitempty"` + RecoverTitleTemplate string `gorm:"type:varchar(256);comment:恢复标题模板" json:"recover_title_template,omitempty"` + RecoverBodyTemplate string `gorm:"type:text;comment:恢复正文模板" json:"recover_body_template,omitempty"` + IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` } func (AlertNotifyTemplate) TableName() string { @@ -249,12 +249,12 @@ func (AlertUpgrade) TableName() string { } type AlertUpgradeRule struct { - Level int `json:"level"` - Duration int `json:"duration"` - Severity string `json:"severity"` + Level int `json:"level"` + Duration int `json:"duration"` + Severity string `json:"severity"` ChannelIDs []string `json:"channel_ids"` - NotifyUsers []uint `json:"notify_users"` - RepeatInterval int `json:"repeat_interval"` + NotifyUsers []uint `json:"notify_users"` + RepeatInterval int `json:"repeat_interval"` } type AlertUpgradeRecord struct { @@ -321,19 +321,19 @@ type AlertSilenceCheckResult struct { // NotifyChannel 通知渠道模型 type NotifyChannel struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:64;not null" json:"name"` - Type string `gorm:"size:32;not null" json:"type"` // email, sms, webhook, dingtalk, wechat - Config string `gorm:"type:text" json:"config"` // JSON - Enabled bool `gorm:"default:true" json:"enabled"` - TestStatus string `gorm:"size:32" json:"test_status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:64;not null" json:"name"` + Type string `gorm:"size:32;not null" json:"type"` // email, sms, webhook, dingtalk, wechat + Config string `gorm:"type:text" json:"config"` // JSON + Enabled bool `gorm:"default:true" json:"enabled"` + TestStatus string `gorm:"size:32" json:"test_status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 func (NotifyChannel) TableName() string { - return "monitor_notify_channels" + return "monitor_alert_channels" } // MetricData 监控指标数据 @@ -383,14 +383,14 @@ type UpdateAlertRuleRequest struct { // AlertStats 告警统计 type AlertStats struct { - Total int64 `json:"total"` - Firing int64 `json:"firing"` - Resolved int64 `json:"resolved"` - Critical int64 `json:"critical"` - Warning int64 `json:"warning"` - Info int64 `json:"info"` - TodayCount int64 `json:"today_count"` - WeekCount int64 `json:"week_count"` + Total int64 `json:"total"` + Firing int64 `json:"firing"` + Resolved int64 `json:"resolved"` + Critical int64 `json:"critical"` + Warning int64 `json:"warning"` + Info int64 `json:"info"` + TodayCount int64 `json:"today_count"` + WeekCount int64 `json:"week_count"` } // AcknowledgeAlertRequest 确认告警请求 @@ -491,7 +491,7 @@ type FeishuWebhookConfig struct { // FeishuCardMessage 飞书卡片消息 type FeishuCardMessage struct { - MsgType string `json:"msg_type"` + MsgType string `json:"msg_type"` Content FeishuCardContent `json:"content"` } @@ -499,6 +499,7 @@ type FeishuCardContent struct { Title string `json:"title"` Content [][]FeishuCardElement `json:"content"` } + // FeishuCardElement 飞书卡片元素 type FeishuCardElement struct { Tag string `json:"tag"` diff --git a/backend/model/alert_notify_policy.go b/backend/model/alert_notify_policy.go index a2cf85a..9d031fa 100644 --- a/backend/model/alert_notify_policy.go +++ b/backend/model/alert_notify_policy.go @@ -31,4 +31,4 @@ type AlertNotifyPolicy struct { UpdatedAt time.Time `json:"updated_at"` } -func (AlertNotifyPolicy) TableName() string { return "monitor_alert_notify_policies" } \ No newline at end of file +func (AlertNotifyPolicy) TableName() string { return "monitor_alert_notify_policies" } diff --git a/backend/model/alert_notify_settings.go b/backend/model/alert_notify_settings.go index 94ce018..d5515da 100644 --- a/backend/model/alert_notify_settings.go +++ b/backend/model/alert_notify_settings.go @@ -12,4 +12,4 @@ type AlertNotifySettings struct { UpdatedAt time.Time `json:"updated_at"` } -func (AlertNotifySettings) TableName() string { return "monitor_alert_notify_settings" } \ No newline at end of file +func (AlertNotifySettings) TableName() string { return "monitor_alert_notify_settings" } diff --git a/backend/model/approval.go b/backend/model/approval.go index 3f3e528..825aa5a 100644 --- a/backend/model/approval.go +++ b/backend/model/approval.go @@ -2,44 +2,43 @@ package model import ( "time" - ) // ApprovalTemplate 审批模板模型 type ApprovalTemplate struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:128;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - Description string `gorm:"size:500" json:"description"` - Type string `gorm:"size:32;not null" json:"type"` // release, server_apply, app_deploy, permission_request - FormSchema string `gorm:"type:text" json:"form_schema"` // JSON form fields config - FlowConfig string `gorm:"type:text" json:"flow_config"` // JSON approval flow steps - Category string `gorm:"size:32;default:resource" json:"category"` // resource, app, permission - Enabled bool `gorm:"default:true" json:"enabled"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + Description string `gorm:"size:500" json:"description"` + Type string `gorm:"size:32;not null" json:"type"` // release, server_apply, app_deploy, permission_request + FormSchema string `gorm:"type:text" json:"form_schema"` // JSON form fields config + FlowConfig string `gorm:"type:text" json:"flow_config"` // JSON approval flow steps + Category string `gorm:"size:32;default:resource" json:"category"` // resource, app, permission + Enabled bool `gorm:"default:true" json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 func (ApprovalTemplate) TableName() string { - return "sys_approval_templates" + return "approval_templates" } // ApprovalProcess 审批流程模型 type ApprovalProcess struct { - ID uint `gorm:"primarykey" json:"id"` - TemplateID uint `json:"template_id"` - Title string `gorm:"size:255;not null" json:"title"` - Applicant uint `json:"applicant"` - Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected - FormData string `gorm:"type:text" json:"form_data"` // JSON - CurrentStep int `gorm:"default:0" json:"current_step"` - TotalSteps int `json:"total_steps"` - ApprovedAt *time.Time `json:"approved_at"` - RejectedAt *time.Time `json:"rejected_at"` - RejectReason string `gorm:"size:500" json:"reject_reason"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + TemplateID uint `json:"template_id"` + Title string `gorm:"size:255;not null" json:"title"` + Applicant uint `json:"applicant"` + Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected + FormData string `gorm:"type:text" json:"form_data"` // JSON + CurrentStep int `gorm:"default:0" json:"current_step"` + TotalSteps int `json:"total_steps"` + ApprovedAt *time.Time `json:"approved_at"` + RejectedAt *time.Time `json:"rejected_at"` + RejectReason string `gorm:"size:500" json:"reject_reason"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Template *ApprovalTemplate `gorm:"foreignKey:TemplateID" json:"template,omitempty"` @@ -54,16 +53,16 @@ func (ApprovalProcess) TableName() string { // ApprovalStep 审批步骤模型 type ApprovalStep struct { - ID uint `gorm:"primarykey" json:"id"` - ProcessID uint `json:"process_id"` - Step int `json:"step"` - StepType string `gorm:"size:32;default:audit" json:"step_type"` // audit, execute - Approver uint `json:"approver"` - Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected - Comment string `gorm:"size:500" json:"comment"` - ApprovedAt *time.Time `json:"approved_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + ProcessID uint `json:"process_id"` + Step int `json:"step"` + StepType string `gorm:"size:32;default:audit" json:"step_type"` // audit, execute + Approver uint `json:"approver"` + Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected + Comment string `gorm:"size:500" json:"comment"` + ApprovedAt *time.Time `json:"approved_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 ApproverUser *User `gorm:"foreignKey:Approver" json:"approver_user,omitempty"` @@ -94,15 +93,15 @@ type CreateApprovalProcessRequest struct { // ApproveRequest 审批请求 type ApproveRequest struct { - Status string `json:"status"` + Status string `json:"status"` Comment string `json:"comment"` } // ApprovalStats 审批统计 type ApprovalStats struct { - Pending int64 `json:"pending"` - Approved int64 `json:"approved"` - Rejected int64 `json:"rejected"` - Total int64 `json:"total"` + Pending int64 `json:"pending"` + Approved int64 `json:"approved"` + Rejected int64 `json:"rejected"` + Total int64 `json:"total"` MyPending int64 `json:"my_pending"` } diff --git a/backend/model/approval_workflow.go b/backend/model/approval_workflow.go index 0cbdc84..9495867 100644 --- a/backend/model/approval_workflow.go +++ b/backend/model/approval_workflow.go @@ -17,52 +17,52 @@ type ApprovalWorkflow struct { } func (ApprovalWorkflow) TableName() string { - return "sys_approval_workflows" + return "approval_workflows" } // ApprovalWorkflowStep 审批流程步骤 type ApprovalWorkflowStep struct { - ID uint `gorm:"primarykey" json:"id"` - WorkflowID uint `json:"workflow_id"` - StepNumber int `gorm:"not null" json:"step_number"` - Name string `gorm:"size:128;not null" json:"name"` - StepType string `gorm:"size:32;not null" json:"step_type"` // submit, audit, execute - ApproverIDs string `gorm:"type:text" json:"approver_ids"` // JSON array of user IDs - IsRequired bool `gorm:"default:true" json:"is_required"` - TimeoutHours int `gorm:"default:24" json:"timeout_hours"` - AutoApprove bool `gorm:"default:false" json:"auto_approve_on_timeout"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + WorkflowID uint `json:"workflow_id"` + StepNumber int `gorm:"not null" json:"step_number"` + Name string `gorm:"size:128;not null" json:"name"` + StepType string `gorm:"size:32;not null" json:"step_type"` // submit, audit, execute + ApproverIDs string `gorm:"type:text" json:"approver_ids"` // JSON array of user IDs + IsRequired bool `gorm:"default:true" json:"is_required"` + TimeoutHours int `gorm:"default:24" json:"timeout_hours"` + AutoApprove bool `gorm:"default:false" json:"auto_approve_on_timeout"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (ApprovalWorkflowStep) TableName() string { - return "sys_approval_workflow_steps" + return "approval_workflow_steps" } // CreateWorkflowRequest 创建审批流程请求 type CreateWorkflowRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - Category string `json:"category"` - Steps []CreateWorkflowStepReq `json:"steps"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Category string `json:"category"` + Steps []CreateWorkflowStepReq `json:"steps"` } type CreateWorkflowStepReq struct { - StepNumber int `json:"step_number"` - Name string `json:"name" binding:"required"` - StepType string `json:"step_type" binding:"required,oneof=submit audit execute"` - ApproverIDs string `json:"approver_ids"` - IsRequired bool `json:"is_required"` - TimeoutHours int `json:"timeout_hours"` - AutoApproveTimeout bool `json:"auto_approve_on_timeout"` + StepNumber int `json:"step_number"` + Name string `json:"name" binding:"required"` + StepType string `json:"step_type" binding:"required,oneof=submit audit execute"` + ApproverIDs string `json:"approver_ids"` + IsRequired bool `json:"is_required"` + TimeoutHours int `json:"timeout_hours"` + AutoApproveTimeout bool `json:"auto_approve_on_timeout"` } // UpdateWorkflowRequest 更新审批流程请求 type UpdateWorkflowRequest struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - Category *string `json:"category,omitempty"` - IsActive *bool `json:"is_active,omitempty"` - IsDefault *bool `json:"is_default,omitempty"` - Steps []CreateWorkflowStepReq `json:"steps,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Category *string `json:"category,omitempty"` + IsActive *bool `json:"is_active,omitempty"` + IsDefault *bool `json:"is_default,omitempty"` + Steps []CreateWorkflowStepReq `json:"steps,omitempty"` } diff --git a/backend/model/audit.go b/backend/model/audit.go index 55034c9..565a3fa 100644 --- a/backend/model/audit.go +++ b/backend/model/audit.go @@ -25,7 +25,7 @@ type AuditLog struct { // TableName 表名 func (AuditLog) TableName() string { - return "sys_audit_logs" + return "audit_operation_logs" } // AuditLogQuery 审计日志查询 @@ -48,11 +48,11 @@ type AuditLogListResponse struct { // AuditStats 审计统计 type AuditStats struct { - TodayCount int64 `json:"today_count"` - WeekCount int64 `json:"week_count"` - MonthCount int64 `json:"month_count"` - TopModules []ModuleStat `json:"top_modules"` - TopUsers []UserStat `json:"top_users"` + TodayCount int64 `json:"today_count"` + WeekCount int64 `json:"week_count"` + MonthCount int64 `json:"month_count"` + TopModules []ModuleStat `json:"top_modules"` + TopUsers []UserStat `json:"top_users"` } // ModuleStat 模块统计 diff --git a/backend/model/audit_setting.go b/backend/model/audit_setting.go index 7b2866c..56a9c46 100644 --- a/backend/model/audit_setting.go +++ b/backend/model/audit_setting.go @@ -20,7 +20,7 @@ type AuditSettings struct { UpdatedAt time.Time `json:"updated_at"` } -func (AuditSettings) TableName() string { return "sys_audit_settings" } +func (AuditSettings) TableName() string { return "audit_settings" } // AllowedAuditRetentionDays 允许的日志保留天数 // Logan.Li var AllowedAuditRetentionDays = []int{7, 15, 30, 90, 180, 365} diff --git a/backend/model/automation.go b/backend/model/automation.go index db85718..0e2cbea 100644 --- a/backend/model/automation.go +++ b/backend/model/automation.go @@ -2,30 +2,29 @@ package model import ( "time" - ) // CronJob 定时任务模型 type CronJob struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:128;not null" json:"name"` - Description string `gorm:"size:500" json:"description"` - CronExpr string `gorm:"size:64;not null" json:"cron_expr"` - Type string `gorm:"size:32;not null" json:"type"` // shell, python, command - Content string `gorm:"type:text" json:"content"` - ScriptID *uint `json:"script_id"` - TargetType string `gorm:"size:32;default:server" json:"target_type"` // server, group, tag - Targets string `gorm:"type:text" json:"targets"` // JSON - Timeout int `gorm:"default:300" json:"timeout"` - Enabled bool `gorm:"default:true" json:"enabled"` - LastRunAt *time.Time `json:"last_run_at"` - LastStatus string `gorm:"size:32" json:"last_status"` - RunCount int `gorm:"default:0" json:"run_count"` - SuccessCount int `gorm:"default:0" json:"success_count"` - FailCount int `gorm:"default:0" json:"fail_count"` - NotifyOn string `gorm:"size:32;default:failure" json:"notify_on"` // always, failure, success - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` + Description string `gorm:"size:500" json:"description"` + CronExpr string `gorm:"size:64;not null" json:"cron_expr"` + Type string `gorm:"size:32;not null" json:"type"` // shell, python, command + Content string `gorm:"type:text" json:"content"` + ScriptID *uint `json:"script_id"` + TargetType string `gorm:"size:32;default:server" json:"target_type"` // server, group, tag + Targets string `gorm:"type:text" json:"targets"` // JSON + Timeout int `gorm:"default:300" json:"timeout"` + Enabled bool `gorm:"default:true" json:"enabled"` + LastRunAt *time.Time `json:"last_run_at"` + LastStatus string `gorm:"size:32" json:"last_status"` + RunCount int `gorm:"default:0" json:"run_count"` + SuccessCount int `gorm:"default:0" json:"success_count"` + FailCount int `gorm:"default:0" json:"fail_count"` + NotifyOn string `gorm:"size:32;default:failure" json:"notify_on"` // always, failure, success + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Script *Script `json:"script,omitempty"` @@ -38,20 +37,20 @@ func (CronJob) TableName() string { // Script 脚本库模型 type Script struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:128;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - Description string `gorm:"size:500" json:"description"` - Type string `gorm:"size:32;default:shell" json:"type"` // shell, python - Content string `gorm:"type:text;not null" json:"content"` - Params string `gorm:"type:text" json:"params"` // JSON - Timeout int `gorm:"default:300" json:"timeout"` - Version int `gorm:"default:1" json:"version"` - RunCount int `gorm:"default:0" json:"run_count"` - CreatedBy uint `json:"created_by"` - UpdatedBy uint `json:"updated_by"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + Description string `gorm:"size:500" json:"description"` + Type string `gorm:"size:32;default:shell" json:"type"` // shell, python + Content string `gorm:"type:text;not null" json:"content"` + Params string `gorm:"type:text" json:"params"` // JSON + Timeout int `gorm:"default:300" json:"timeout"` + Version int `gorm:"default:1" json:"version"` + RunCount int `gorm:"default:0" json:"run_count"` + CreatedBy uint `json:"created_by"` + UpdatedBy uint `json:"updated_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 @@ -61,21 +60,21 @@ func (Script) TableName() string { // TaskExecution 任务执行记录(cron / script,不含 Ansible;Ansible 走 AnsibleJob) type TaskExecution struct { - ID uint `gorm:"primarykey" json:"id"` - TaskType string `gorm:"size:32;not null" json:"task_type"` // cron, script - TaskID uint `json:"task_id"` - TaskName string `gorm:"size:128" json:"task_name"` - Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled - TargetType string `gorm:"size:32" json:"target_type"` - Targets string `gorm:"type:text" json:"targets"` - Output string `gorm:"type:longtext" json:"output"` - Error string `gorm:"type:text" json:"error"` - Duration int `json:"duration"` - StartedAt *time.Time `json:"started_at"` - FinishedAt *time.Time `json:"finished_at"` - CreatedBy uint `json:"created_by"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + TaskType string `gorm:"size:32;not null" json:"task_type"` // cron, script + TaskID uint `json:"task_id"` + TaskName string `gorm:"size:128" json:"task_name"` + Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled + TargetType string `gorm:"size:32" json:"target_type"` + Targets string `gorm:"type:text" json:"targets"` + Output string `gorm:"type:longtext" json:"output"` + Error string `gorm:"type:text" json:"error"` + Duration int `json:"duration"` + StartedAt *time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` + CreatedBy uint `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 @@ -85,16 +84,16 @@ func (TaskExecution) TableName() string { // CreateCronJobRequest 创建定时任务请求 type CreateCronJobRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - CronExpr string `json:"cron_expr" binding:"required"` - Type string `json:"type" binding:"required,oneof=shell python powershell command bash"` - Content string `json:"content"` - ScriptID *uint `json:"script_id"` - TargetType string `json:"target_type" binding:"omitempty,oneof=server group tag"` - Targets []uint `json:"targets"` - Timeout int `json:"timeout"` - NotifyOn string `json:"notify_on" binding:"omitempty,oneof=always failure success"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + CronExpr string `json:"cron_expr" binding:"required"` + Type string `json:"type" binding:"required,oneof=shell python powershell command bash"` + Content string `json:"content"` + ScriptID *uint `json:"script_id"` + TargetType string `json:"target_type" binding:"omitempty,oneof=server group tag"` + Targets []uint `json:"targets"` + Timeout int `json:"timeout"` + NotifyOn string `json:"notify_on" binding:"omitempty,oneof=always failure success"` } // CreateScriptRequest 创建脚本请求 @@ -117,9 +116,9 @@ type ExecuteScriptRequest struct { // AutomationStats 自动化统计 type AutomationStats struct { - CronJobCount int64 `json:"cron_job_count"` - ScriptCount int64 `json:"script_count"` - PlaybookCount int64 `json:"playbook_count"` - TodayExecutions int64 `json:"today_executions"` - SuccessRate float64 `json:"success_rate"` + CronJobCount int64 `json:"cron_job_count"` + ScriptCount int64 `json:"script_count"` + PlaybookCount int64 `json:"playbook_count"` + TodayExecutions int64 `json:"today_executions"` + SuccessRate float64 `json:"success_rate"` } diff --git a/backend/model/billing.go b/backend/model/billing.go index db465a3..f59afbb 100644 --- a/backend/model/billing.go +++ b/backend/model/billing.go @@ -2,64 +2,63 @@ package model import ( "time" - ) // CloudBilling 云账单模型 - 每日成本记录 type CloudBilling struct { - ID uint `gorm:"primarykey" json:"id"` - CloudAccountID *uint `gorm:"index" json:"cloud_account_id"` - AccountName string `gorm:"size:128" json:"account_name"` - Provider string `gorm:"size:64;index" json:"provider"` // aliyun, tencent, aws, huawei, azure - BillingDate string `gorm:"size:10;index;not null" json:"billing_date"` // YYYY-MM-DD - Cost float64 `gorm:"type:decimal(12,2);default:0" json:"cost"` // 费用(元) - Currency string `gorm:"size:8;default:CNY" json:"currency"` // CNY, USD - Service string `gorm:"size:128" json:"service"` // ECS, RDS, OSS, CDN 等 - Region string `gorm:"size:64" json:"region"` - Tags string `gorm:"type:text" json:"tags"` // JSON - Description string `gorm:"size:500" json:"description"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + CloudAccountID *uint `gorm:"index" json:"cloud_account_id"` + AccountName string `gorm:"size:128" json:"account_name"` + Provider string `gorm:"size:64;index" json:"provider"` // aliyun, tencent, aws, huawei, azure + BillingDate string `gorm:"size:10;index;not null" json:"billing_date"` // YYYY-MM-DD + Cost float64 `gorm:"type:decimal(12,2);default:0" json:"cost"` // 费用(元) + Currency string `gorm:"size:8;default:CNY" json:"currency"` // CNY, USD + Service string `gorm:"size:128" json:"service"` // ECS, RDS, OSS, CDN 等 + Region string `gorm:"size:64" json:"region"` + Tags string `gorm:"type:text" json:"tags"` // JSON + Description string `gorm:"size:500" json:"description"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (CloudBilling) TableName() string { - return "asset_cloud_billings" + return "asset_cloud_billing_records" } // BillingAlertRule 账单预警规则 type BillingAlertRule struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:128;not null" json:"name"` - Provider string `gorm:"size:64" json:"provider"` // 空=所有云厂商 - CloudAccountID *uint `gorm:"index" json:"cloud_account_id"` // 空=所有账号 - Metric string `gorm:"size:32;default:daily" json:"metric"` // daily, monthly - Threshold float64 `gorm:"type:decimal(12,2)" json:"threshold"` // 阈值(元) - Operator string `gorm:"size:8;default:>" json:"operator"` // >, >=, <, <= - Enabled bool `gorm:"default:true" json:"enabled"` - LastTriggered *time.Time `json:"last_triggered"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` + Provider string `gorm:"size:64" json:"provider"` // 空=所有云厂商 + CloudAccountID *uint `gorm:"index" json:"cloud_account_id"` // 空=所有账号 + Metric string `gorm:"size:32;default:daily" json:"metric"` // daily, monthly + Threshold float64 `gorm:"type:decimal(12,2)" json:"threshold"` // 阈值(元) + Operator string `gorm:"size:8;default:>" json:"operator"` // >, >=, <, <= + Enabled bool `gorm:"default:true" json:"enabled"` + LastTriggered *time.Time `json:"last_triggered"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (BillingAlertRule) TableName() string { - return "monitor_billing_alert_rules" + return "asset_cloud_billing_alert_rules" } // BillingStats 账单统计 type BillingStats struct { - TodayCost float64 `json:"today_cost"` - MonthCost float64 `json:"month_cost"` - YearCost float64 `json:"year_cost"` - YesterdayCost float64 `json:"yesterday_cost"` - MonthTrend []DailyCost `json:"month_trend"` - ProviderDist []ProviderCost `json:"provider_dist"` - Alerts []BillingAlert `json:"alerts"` + TodayCost float64 `json:"today_cost"` + MonthCost float64 `json:"month_cost"` + YearCost float64 `json:"year_cost"` + YesterdayCost float64 `json:"yesterday_cost"` + MonthTrend []DailyCost `json:"month_trend"` + ProviderDist []ProviderCost `json:"provider_dist"` + Alerts []BillingAlert `json:"alerts"` } // DailyCost 每日成本 type DailyCost struct { - Date string `json:"date"` - Cost float64 `json:"cost"` + Date string `json:"date"` + Cost float64 `json:"cost"` } // ProviderCost 厂商成本分布 @@ -82,15 +81,15 @@ type BillingAlert struct { // CreateBillingRequest 创建账单记录请求 type CreateBillingRequest struct { - CloudAccountID *uint `json:"cloud_account_id"` - AccountName string `json:"account_name"` - Provider string `json:"provider" binding:"required"` - BillingDate string `json:"billing_date" binding:"required"` + CloudAccountID *uint `json:"cloud_account_id"` + AccountName string `json:"account_name"` + Provider string `json:"provider" binding:"required"` + BillingDate string `json:"billing_date" binding:"required"` Cost float64 `json:"cost" binding:"required"` - Currency string `json:"currency"` - Service string `json:"service"` - Region string `json:"region"` - Description string `json:"description"` + Currency string `json:"currency"` + Service string `json:"service"` + Region string `json:"region"` + Description string `json:"description"` } // CreateBillingAlertRequest 创建预警规则请求 diff --git a/backend/model/build_node.go b/backend/model/build_node.go index ecdb8f7..1651ad2 100644 --- a/backend/model/build_node.go +++ b/backend/model/build_node.go @@ -32,7 +32,7 @@ type BuildMasterConfig struct { UpdatedAt time.Time `json:"updated_at"` } -func (BuildMasterConfig) TableName() string { return "deploy_build_master_config" } +func (BuildMasterConfig) TableName() string { return "deploy_build_master_configs" } // UpdateBuildMasterRequest 更新 Master 并发数 // Logan.Li type UpdateBuildMasterRequest struct { @@ -58,4 +58,4 @@ type UpdateBuildNodeRequest struct { MaxExecutors int `json:"max_executors"` WorkspacePath string `json:"workspace_path"` Enabled *bool `json:"enabled"` -} \ No newline at end of file +} diff --git a/backend/model/cicd.go b/backend/model/cicd.go index 5ee6ca9..8b7ff70 100644 --- a/backend/model/cicd.go +++ b/backend/model/cicd.go @@ -2,23 +2,22 @@ package model import ( "time" - ) // Pipeline CI/CD流水线模型 type Pipeline struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:128;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - JobName string `gorm:"size:128;uniqueIndex;comment:Job名称(webhook URL标识, 如 cex-admin-server)" json:"job_name,omitempty"` - Description string `gorm:"size:500" json:"description"` - Type string `gorm:"size:32;default:deploy" json:"type"` // build, deploy, test - Source string `gorm:"size:255" json:"source"` // Git仓库地址 - Branch string `gorm:"size:64;default:main" json:"branch"` - Dockerfile string `gorm:"size:255" json:"dockerfile"` - Stages string `gorm:"type:text" json:"stages"` // JSON - Variables string `gorm:"type:text" json:"variables"` // JSON - Enabled bool `gorm:"default:true" json:"enabled"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + JobName string `gorm:"size:128;uniqueIndex;comment:Job名称(webhook URL标识, 如 cex-admin-server)" json:"job_name,omitempty"` + Description string `gorm:"size:500" json:"description"` + Type string `gorm:"size:32;default:deploy" json:"type"` // build, deploy, test + Source string `gorm:"size:255" json:"source"` // Git仓库地址 + Branch string `gorm:"size:64;default:main" json:"branch"` + Dockerfile string `gorm:"size:255" json:"dockerfile"` + Stages string `gorm:"type:text" json:"stages"` // JSON + Variables string `gorm:"type:text" json:"variables"` // JSON + Enabled bool `gorm:"default:true" json:"enabled"` // 部署模式 DeployMode string `gorm:"size:16;default:branch;comment:部署模式 branch/tag" json:"deploy_mode"` @@ -36,16 +35,12 @@ type Pipeline struct { DeployScript string `gorm:"type:text;comment:SSH部署后执行命令" json:"deploy_script,omitempty"` // K8s 部署目标 - K8sClusterID *uint `json:"k8s_cluster_id,omitempty"` // FK → kubernetes_clusters + K8sClusterID *uint `json:"k8s_cluster_id,omitempty"` // FK → k8s_clusters K8sNamespace string `gorm:"size:128;comment:K8s部署命名空间" json:"k8s_namespace,omitempty"` K8sDeployment string `gorm:"size:128;comment:K8s Deployment 名称" json:"k8s_deployment,omitempty"` K8sContainerName string `gorm:"size:128;comment:K8s更新镜像目标容器" json:"k8s_container_name,omitempty"` - // 仓库和镜像仓库关联(旧字段,待废弃) - RepoID *uint `json:"repo_id"` // FK → deploy_pipeline_repos - RegistryID *uint `json:"registry_id"` // FK → deploy_pipeline_registries - - // 凭证关联(直接关联凭证 ID) + // 凭证关联(直接关联统一凭证 ID)// Logan.Li GitCredentialID *uint `json:"git_credential_id"` // FK → iam_credentials (Git Token) RegistryCredentialID *uint `json:"registry_credential_id"` // FK → iam_credentials (Docker Password) @@ -71,39 +66,37 @@ type Pipeline struct { UpdatedAt time.Time `json:"updated_at"` // 关联 - Repo *PipelineRepo `json:"repo,omitempty" gorm:"foreignKey:RepoID"` - Registry *PipelineRegistry `json:"registry,omitempty" gorm:"foreignKey:RegistryID"` - Environment *DeployEnvironment `json:"environment,omitempty" gorm:"foreignKey:EnvironmentID"` + Environment *DeployEnvironment `json:"environment,omitempty" gorm:"foreignKey:EnvironmentID"` } // TableName 表名 func (Pipeline) TableName() string { - return "ops_pipelines" + return "deploy_pipelines" } // PipelineRun 流水线运行记录(CI 执行) type PipelineRun struct { - ID uint `gorm:"primarykey" json:"id"` - PipelineID uint `gorm:"not null" json:"pipeline_id"` - BuildNumber int `json:"build_number"` - Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled - Branch string `gorm:"size:64" json:"branch"` - CommitID string `gorm:"size:64" json:"commit_id"` - CommitMsg string `gorm:"size:500" json:"commit_msg"` - Author string `gorm:"size:64" json:"author"` - ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签 - ArtifactURL string `gorm:"size:500" json:"artifact_url"` // 构建产物地址 - WorkspacePath string `gorm:"size:500" json:"workspace_path"` // 工作区路径 - BuildNodeID *uint `json:"build_node_id,omitempty"` // 实际执行节点(0/nil=Master) - BuildNodeName string `gorm:"size:64" json:"build_node_name,omitempty"` - Duration int `json:"duration"` // 秒 - Stages string `gorm:"type:text" json:"stages"` // JSON - Logs string `gorm:"type:longtext" json:"logs"` - TriggeredBy string `gorm:"size:64" json:"triggered_by"` - StartedAt *time.Time `json:"started_at"` - FinishedAt *time.Time `json:"finished_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + PipelineID uint `gorm:"not null" json:"pipeline_id"` + BuildNumber int `json:"build_number"` + Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled + Branch string `gorm:"size:64" json:"branch"` + CommitID string `gorm:"size:64" json:"commit_id"` + CommitMsg string `gorm:"size:500" json:"commit_msg"` + Author string `gorm:"size:64" json:"author"` + ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签 + ArtifactURL string `gorm:"size:500" json:"artifact_url"` // 构建产物地址 + WorkspacePath string `gorm:"size:500" json:"workspace_path"` // 工作区路径 + BuildNodeID *uint `json:"build_node_id,omitempty"` // 实际执行节点(0/nil=Master) + BuildNodeName string `gorm:"size:64" json:"build_node_name,omitempty"` + Duration int `json:"duration"` // 秒 + Stages string `gorm:"type:text" json:"stages"` // JSON + Logs string `gorm:"type:longtext" json:"logs"` + TriggeredBy string `gorm:"size:64" json:"triggered_by"` + StartedAt *time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Pipeline *Pipeline `json:"pipeline,omitempty"` @@ -111,34 +104,34 @@ type PipelineRun struct { // TableName 表名 func (PipelineRun) TableName() string { - return "ops_pipeline_runs" + return "deploy_pipeline_runs" } // AppRelease 应用发布模型(CD:从 CI 产物部署到目标环境) type AppRelease struct { - ID uint `gorm:"primarykey" json:"id"` - PipelineID *uint `json:"pipeline_id"` // 来源流水线 - PipelineRunID *uint `json:"pipeline_run_id"` // 来源 CI 执行记录 - AppName string `gorm:"size:128;not null" json:"app_name"` - Version string `gorm:"size:64;not null" json:"version"` - Environment string `gorm:"size:32;not null" json:"environment"` // dev, test, staging, prod - DeployType string `gorm:"size:32;default:server" json:"deploy_type"` // server, kubernetes - Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected, deploying, success, failed, rolled_back - CommitID string `gorm:"size:64" json:"commit_id"` - CommitMsg string `gorm:"size:500" json:"commit_msg"` - Author string `gorm:"size:64" json:"author"` - Approver string `gorm:"size:64" json:"approver"` - ApprovedAt *time.Time `json:"approved_at"` - DeployedAt *time.Time `json:"deployed_at"` - RollbackAt *time.Time `json:"rollback_at"` - Duration int `json:"duration"` - ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签 - K8sClusterID *uint `json:"k8s_cluster_id"` // Kubernetes 部署目标集群 - K8sNamespace string `gorm:"size:128" json:"k8s_namespace"` // Kubernetes 部署目标命名空间 - Servers string `gorm:"type:text" json:"servers"` // JSON(传统服务器部署目标) - Description string `gorm:"size:500" json:"description"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + PipelineID *uint `json:"pipeline_id"` // 来源流水线 + PipelineRunID *uint `json:"pipeline_run_id"` // 来源 CI 执行记录 + AppName string `gorm:"size:128;not null" json:"app_name"` + Version string `gorm:"size:64;not null" json:"version"` + Environment string `gorm:"size:32;not null" json:"environment"` // dev, test, staging, prod + DeployType string `gorm:"size:32;default:server" json:"deploy_type"` // server, kubernetes + Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected, deploying, success, failed, rolled_back + CommitID string `gorm:"size:64" json:"commit_id"` + CommitMsg string `gorm:"size:500" json:"commit_msg"` + Author string `gorm:"size:64" json:"author"` + Approver string `gorm:"size:64" json:"approver"` + ApprovedAt *time.Time `json:"approved_at"` + DeployedAt *time.Time `json:"deployed_at"` + RollbackAt *time.Time `json:"rollback_at"` + Duration int `json:"duration"` + ImageTag string `gorm:"size:255" json:"image_tag"` // 构建产物镜像标签 + K8sClusterID *uint `json:"k8s_cluster_id"` // Kubernetes 部署目标集群 + K8sNamespace string `gorm:"size:128" json:"k8s_namespace"` // Kubernetes 部署目标命名空间 + Servers string `gorm:"type:text" json:"servers"` // JSON(传统服务器部署目标) + Description string `gorm:"size:500" json:"description"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 @@ -150,7 +143,7 @@ func (AppRelease) TableName() string { type CreatePipelineRequest struct { Name string `json:"name" binding:"required"` Code string `json:"code" binding:"required"` - JobName string `json:"job_name"` // Webhook Job名称 + JobName string `json:"job_name"` // Webhook Job名称 Description string `json:"description"` Type string `json:"type" binding:"omitempty,oneof=build deploy test"` Source string `json:"source"` @@ -160,9 +153,9 @@ type CreatePipelineRequest struct { Variables string `json:"variables"` // 部署模式 - DeployMode string `json:"deploy_mode"` // branch / tag - DeployTarget string `json:"deploy_target"` // ssh / k8s - ServerIDs string `json:"server_ids"` // SSH 服务器ID JSON [1,2,3] + DeployMode string `json:"deploy_mode"` // branch / tag + DeployTarget string `json:"deploy_target"` // ssh / k8s + ServerIDs string `json:"server_ids"` // SSH 服务器ID JSON [1,2,3] DeployRemotePath string `json:"deploy_remote_path"` DeployScript string `json:"deploy_script"` K8sClusterID *uint `json:"k8s_cluster_id"` @@ -170,12 +163,10 @@ type CreatePipelineRequest struct { K8sDeployment string `json:"k8s_deployment"` K8sContainerName string `json:"k8s_container_name"` - RepoID *uint `json:"repo_id"` - RegistryID *uint `json:"registry_id"` - // 凭证关联 + // 凭证关联 // Logan.Li GitCredentialID *uint `json:"git_credential_id"` RegistryCredentialID *uint `json:"registry_credential_id"` - EnvironmentID *uint `json:"environment_id"` + EnvironmentID *uint `json:"environment_id"` // 触发配置 TriggerMode string `json:"trigger_mode"` @@ -196,17 +187,17 @@ type CreatePipelineRequest struct { // UpdatePipelineRequest 更新流水线请求 type UpdatePipelineRequest struct { - Name string `json:"name"` - JobName string `json:"job_name"` - Description string `json:"description"` - Type string `json:"type"` - Source string `json:"source"` - Branch string `json:"branch"` - Dockerfile string `json:"dockerfile"` - Stages string `json:"stages"` - Variables string `json:"variables"` - DeployMode string `json:"deploy_mode"` - DeployTarget string `json:"deploy_target"` + Name string `json:"name"` + JobName string `json:"job_name"` + Description string `json:"description"` + Type string `json:"type"` + Source string `json:"source"` + Branch string `json:"branch"` + Dockerfile string `json:"dockerfile"` + Stages string `json:"stages"` + Variables string `json:"variables"` + DeployMode string `json:"deploy_mode"` + DeployTarget string `json:"deploy_target"` ServerIDs string `json:"server_ids"` DeployRemotePath string `json:"deploy_remote_path"` DeployScript string `json:"deploy_script"` @@ -214,12 +205,10 @@ type UpdatePipelineRequest struct { K8sNamespace string `json:"k8s_namespace"` K8sDeployment string `json:"k8s_deployment"` K8sContainerName string `json:"k8s_container_name"` - RepoID *uint `json:"repo_id"` - RegistryID *uint `json:"registry_id"` - // 凭证关联 + // 凭证关联 // Logan.Li GitCredentialID *uint `json:"git_credential_id"` RegistryCredentialID *uint `json:"registry_credential_id"` - EnvironmentID *uint `json:"environment_id"` + EnvironmentID *uint `json:"environment_id"` // 触发配置 TriggerMode string `json:"trigger_mode"` WatchBranches string `json:"watch_branches"` @@ -228,7 +217,7 @@ type UpdatePipelineRequest struct { WebhookToken string `json:"webhook_token"` // 缓存 CachePaths string `json:"cache_paths"` - Enabled *bool `json:"enabled"` + Enabled *bool `json:"enabled"` // 并发控制 MaxExecutors int `json:"max_executors"` @@ -240,7 +229,7 @@ type UpdatePipelineRequest struct { // TriggerPipelineRequest 触发流水线请求 type TriggerPipelineRequest struct { Branch string `json:"branch"` - Tag string `json:"tag"` // 发布 Tag + Tag string `json:"tag"` // 发布 Tag Variables map[string]string `json:"variables"` } @@ -263,10 +252,10 @@ type CreateAppReleaseRequest struct { // PipelineStats 流水线统计 type PipelineStats struct { - Total int64 `json:"total"` - Success int64 `json:"success"` - Failed int64 `json:"failed"` - Running int64 `json:"running"` - TodayCount int64 `json:"today_count"` - SuccessRate float64 `json:"success_rate"` + Total int64 `json:"total"` + Success int64 `json:"success"` + Failed int64 `json:"failed"` + Running int64 `json:"running"` + TodayCount int64 `json:"today_count"` + SuccessRate float64 `json:"success_rate"` } diff --git a/backend/model/common.go b/backend/model/common.go index 532b308..da02bdd 100644 --- a/backend/model/common.go +++ b/backend/model/common.go @@ -80,10 +80,10 @@ type OpsOverviewStats struct { // CostStats 成本统计 type CostStats struct { - TotalCost float64 `json:"total_cost"` - MonthCost float64 `json:"month_cost"` - CloudCosts []CloudCost `json:"cloud_costs"` - MonthTrend []MonthTrend `json:"month_trend"` + TotalCost float64 `json:"total_cost"` + MonthCost float64 `json:"month_cost"` + CloudCosts []CloudCost `json:"cloud_costs"` + MonthTrend []MonthTrend `json:"month_trend"` } // CloudCost 云平台成本 @@ -115,8 +115,8 @@ type ServerResource struct { // DomainCert 域名证书 type DomainCert struct { - ID uint `json:"id"` - Name string `json:"name"` - ExpireDate string `json:"expire_date"` - DaysUntilExpiry int `json:"days_until_expiry"` + ID uint `json:"id"` + Name string `json:"name"` + ExpireDate string `json:"expire_date"` + DaysUntilExpiry int `json:"days_until_expiry"` } diff --git a/backend/model/custom_dashboard.go b/backend/model/custom_dashboard.go deleted file mode 100644 index 91061ee..0000000 --- a/backend/model/custom_dashboard.go +++ /dev/null @@ -1,31 +0,0 @@ -package model - -import ( - "time" -) - -// CustomDashboard 自定义监控仪表盘(面板配置存 JSON) // Logan.Li -type CustomDashboard struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:仪表盘名称" json:"name"` - Description string `gorm:"type:varchar(512);comment:描述" json:"description"` - Datasource string `gorm:"type:varchar(32);default:'prometheus';comment:数据源类型" json:"datasource"` - DatasourceURL string `gorm:"type:varchar(512);comment:数据源URL(留空则用默认Prometheus)" json:"datasource_url"` - // Panels 存储面板配置 JSON 数组: [{id,title,type,query,width,height,x,y,options}] - Panels string `gorm:"type:longtext;comment:面板配置JSON" json:"panels"` - // Tags 标签(JSON 数组字符串) - Tags string `gorm:"type:varchar(512);comment:标签JSON" json:"tags,omitempty"` - // 归属 - CreatedBy string `gorm:"type:varchar(64);comment:创建人" json:"created_by"` - IsDefault bool `gorm:"default:false;index;comment:是否默认仪表盘" json:"is_default"` - IsFavorite bool `gorm:"default:false;comment:是否收藏" json:"is_favorite"` - // 时间戳 - CreatedAt time.Time `gorm:"not null;index;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` -} - -// TableName 指定表名 -func (CustomDashboard) TableName() string { - return "monitor_custom_dashboards" -} diff --git a/backend/model/deploy.go b/backend/model/deploy.go index ab81096..cc1d84f 100644 --- a/backend/model/deploy.go +++ b/backend/model/deploy.go @@ -2,91 +2,27 @@ package model import "time" -// ============================================ -// PipelineRepo Git 仓库认证配置 -// ============================================ - -// RepoAuthType 仓库认证方式 -type RepoAuthType string - -const ( - RepoAuthToken RepoAuthType = "token" // Personal Access Token - RepoAuthSSH RepoAuthType = "ssh" // SSH Key - RepoAuthPassword RepoAuthType = "password" // 用户名+密码 -) - -// PipelineRepo Git 仓库认证配置表 -type PipelineRepo struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:配置名称" json:"name"` - Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` - URL string `gorm:"type:varchar(512);not null;comment:仓库 URL" json:"url"` - AuthType RepoAuthType `gorm:"type:varchar(16);default:'token';comment:认证方式: token/ssh/password" json:"auth_type"` - CredentialID *uint `gorm:"index;comment:凭据ID(关联 iam_credentials)" json:"credential_id,omitempty"` - Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"` - CreatedBy string `gorm:"type:varchar(64);comment:创建人" json:"created_by,omitempty"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` -} - -// TableName 表名 -func (PipelineRepo) TableName() string { - return "deploy_pipeline_repos" -} - -// ============================================ -// PipelineRegistry 镜像仓库认证配置 -// ============================================ - -// RegistryAuthType 镜像仓库认证方式 -type RegistryAuthType string - -const ( - RegistryAuthToken RegistryAuthType = "token" // Access Token - RegistryAuthPassword RegistryAuthType = "password" // 用户名+密码 -) - -// PipelineRegistry 镜像仓库认证配置表 -type PipelineRegistry struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:配置名称" json:"name"` - Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` - RegistryURL string `gorm:"type:varchar(512);not null;comment:镜像仓库地址" json:"registry_url"` - Namespace string `gorm:"type:varchar(128);comment:默认命名空间" json:"namespace,omitempty"` - AuthType RegistryAuthType `gorm:"type:varchar(16);default:'password';comment:认证方式: token/password" json:"auth_type"` - CredentialID *uint `gorm:"index;comment:凭据ID(关联 iam_credentials)" json:"credential_id,omitempty"` - Enabled bool `gorm:"default:true;comment:是否启用" json:"enabled"` - CreatedBy string `gorm:"type:varchar(64);comment:创建人" json:"created_by,omitempty"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` -} - -// TableName 表名 -func (PipelineRegistry) TableName() string { - return "deploy_pipeline_registries" -} - // ============================================ // PipelineStage 流水线阶段定义(JSON) // ============================================ // PipelineStage 一个阶段包含多个任务 type PipelineStage struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` // source/build/deploy/test/custom + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` // source/build/deploy/test/custom Tasks []PipelineTask `json:"tasks"` } // PipelineTask 阶段内的任务 type PipelineTask struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` // build/deploy/test/custom - Icon string `json:"icon,omitempty"` - Config map[string]string `json:"config,omitempty"` - Script string `json:"script,omitempty"` - ScriptType string `json:"script_type,omitempty"` // shell/python + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` // build/deploy/test/custom + Icon string `json:"icon,omitempty"` + Config map[string]string `json:"config,omitempty"` + Script string `json:"script,omitempty"` + ScriptType string `json:"script_type,omitempty"` // shell/python } // ============================================ diff --git a/backend/model/deploy_tool.go b/backend/model/deploy_tool.go index 390a550..6dddf18 100644 --- a/backend/model/deploy_tool.go +++ b/backend/model/deploy_tool.go @@ -3,8 +3,8 @@ package model import "time" // ============================================ -// DeployTool 工具配置(懒加载/预装/禁用) -// 按 deploy_ 前缀,与 PipelineRepo/Registry 同属发布配置域 +// DeployTool 工具配置(懒加载/预装/禁用)// Logan.Li +// 按 deploy_ 前缀归属发布配置域。// Logan.Li // ============================================ // ToolInstallMethod 安装方式 @@ -395,29 +395,29 @@ echo "[Node.js] Installed: $(/usr/local/bin/node --version) / npm $("$NVM_DIR/ve }, // ──── Git — 版本控制(懒加载, U4 镜像瘦身)// Logan.Li ──── { - Name: "git", - DisplayName: "Git", - Description: "版本控制工具,流水线代码拉取使用(首次使用自动安装)", - Category: "build", - InstallMethod: ToolLazy, - BinaryName: "git", - InstallPath: "/usr/bin/git", - CheckCommand: "git --version 2>/dev/null", + Name: "git", + DisplayName: "Git", + Description: "版本控制工具,流水线代码拉取使用(首次使用自动安装)", + Category: "build", + InstallMethod: ToolLazy, + BinaryName: "git", + InstallPath: "/usr/bin/git", + CheckCommand: "git --version 2>/dev/null", InstallScript: `apk add --no-cache git`, - SortOrder: 6, + SortOrder: 6, }, // ──── Docker CLI — 容器构建(懒加载, U4 镜像瘦身)// Logan.Li ──── { - Name: "docker-cli", - DisplayName: "Docker CLI", - Description: "Docker 命令行工具,流水线镜像构建/推送使用(首次使用自动安装)", - Category: "deploy", - InstallMethod: ToolLazy, - BinaryName: "docker", - InstallPath: "/usr/bin/docker", - CheckCommand: "docker --version 2>/dev/null", - InstallScript: `apk add --no-cache docker-cli`, - SortOrder: 7, + Name: "docker-cli", + DisplayName: "Docker CLI", + Description: "Docker 命令行工具,流水线镜像构建/推送使用(首次使用自动安装)", + Category: "deploy", + InstallMethod: ToolLazy, + BinaryName: "docker", + InstallPath: "/usr/bin/docker", + CheckCommand: "docker --version 2>/dev/null", + InstallScript: `apk add --no-cache docker-cli`, + SortOrder: 7, }, } } diff --git a/backend/model/kubernetes.go b/backend/model/kubernetes.go index 7efbdb2..1a3b6cb 100644 --- a/backend/model/kubernetes.go +++ b/backend/model/kubernetes.go @@ -2,61 +2,60 @@ package model import ( "time" - ) // KubernetesCluster K8s集群模型 type KubernetesCluster struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:128;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - Version string `gorm:"size:32" json:"version"` - Status string `gorm:"size:32;default:healthy" json:"status"` // healthy, warning, error - ConfigType int `gorm:"default:1" json:"config_type"` // 0=token, 1=kubeconfig - APIServer string `gorm:"size:255" json:"api_server"` - APIEndpoint string `gorm:"size:255" json:"api_endpoint"` - Token string `gorm:"type:text" json:"-"` - KubeConfig string `gorm:"type:text" json:"-"` - NodeCount int `gorm:"default:0" json:"node_count"` - PodCount int `gorm:"default:0" json:"pod_count"` - ServiceCount int `gorm:"default:0" json:"service_count"` - DeploymentCount int `gorm:"default:0" json:"deployment_count"` - IngressCount int `gorm:"default:0" json:"ingress_count"` // 路由/Ingress 数量 // Logan.Li - CPUUsage float64 `json:"cpu_usage"` - MemoryUsage float64 `json:"memory_usage"` - Description string `gorm:"size:500" json:"description"` - Labels string `gorm:"type:text" json:"labels"` // JSON - LastSyncAt *time.Time `json:"last_sync_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + Version string `gorm:"size:32" json:"version"` + Status string `gorm:"size:32;default:healthy" json:"status"` // healthy, warning, error + ConfigType int `gorm:"default:1" json:"config_type"` // 0=token, 1=kubeconfig + APIServer string `gorm:"size:255" json:"api_server"` + APIEndpoint string `gorm:"size:255" json:"api_endpoint"` + Token string `gorm:"type:text" json:"-"` + KubeConfig string `gorm:"type:text" json:"-"` + NodeCount int `gorm:"default:0" json:"node_count"` + PodCount int `gorm:"default:0" json:"pod_count"` + ServiceCount int `gorm:"default:0" json:"service_count"` + DeploymentCount int `gorm:"default:0" json:"deployment_count"` + IngressCount int `gorm:"default:0" json:"ingress_count"` // 路由/Ingress 数量 // Logan.Li + CPUUsage float64 `json:"cpu_usage"` + MemoryUsage float64 `json:"memory_usage"` + Description string `gorm:"size:500" json:"description"` + Labels string `gorm:"type:text" json:"labels"` // JSON + LastSyncAt *time.Time `json:"last_sync_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 func (KubernetesCluster) TableName() string { - return "kubernetes_clusters" + return "k8s_clusters" } // KubernetesNode K8s节点模型 type KubernetesNode struct { - ID uint `gorm:"primarykey" json:"id"` - ClusterID uint `gorm:"not null" json:"cluster_id"` - Name string `gorm:"size:128;not null" json:"name"` - IP string `gorm:"size:64" json:"ip"` - Role string `gorm:"size:32" json:"role"` // master, worker - Status string `gorm:"size:32" json:"status"` - OS string `gorm:"size:64" json:"os"` - KubeletVer string `gorm:"size:32" json:"kubelet_version"` - CPU int `json:"cpu"` - Memory int64 `json:"memory"` - Disk int64 `json:"disk"` - CPUUsage float64 `json:"cpu_usage"` - MemoryUsage float64 `json:"memory_usage"` - DiskUsage float64 `json:"disk_usage"` - PodCount int `json:"pod_count"` - MaxPods int `json:"max_pods"` - Labels string `gorm:"type:text" json:"labels"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + ClusterID uint `gorm:"not null" json:"cluster_id"` + Name string `gorm:"size:128;not null" json:"name"` + IP string `gorm:"size:64" json:"ip"` + Role string `gorm:"size:32" json:"role"` // master, worker + Status string `gorm:"size:32" json:"status"` + OS string `gorm:"size:64" json:"os"` + KubeletVer string `gorm:"size:32" json:"kubelet_version"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + Disk int64 `json:"disk"` + CPUUsage float64 `json:"cpu_usage"` + MemoryUsage float64 `json:"memory_usage"` + DiskUsage float64 `json:"disk_usage"` + PodCount int `json:"pod_count"` + MaxPods int `json:"max_pods"` + Labels string `gorm:"type:text" json:"labels"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Cluster *KubernetesCluster `json:"cluster,omitempty"` @@ -64,25 +63,25 @@ type KubernetesNode struct { // TableName 表名 func (KubernetesNode) TableName() string { - return "kubernetes_nodes" + return "k8s_nodes" } // KubernetesPod K8s Pod模型 type KubernetesPod struct { - ID uint `gorm:"primarykey" json:"id"` - ClusterID uint `gorm:"not null" json:"cluster_id"` - Namespace string `gorm:"size:64;not null" json:"namespace"` - Name string `gorm:"size:128;not null" json:"name"` - Status string `gorm:"size:32" json:"status"` // Running, Pending, Succeeded, Failed - NodeName string `gorm:"size:128" json:"node_name"` - IP string `gorm:"size:64" json:"ip"` - RestartCount int `json:"restart_count"` - Image string `gorm:"size:255" json:"image"` - CPUUsage float64 `json:"cpu_usage"` - MemoryUsage float64 `json:"memory_usage"` - Labels string `gorm:"type:text" json:"labels"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + ClusterID uint `gorm:"not null" json:"cluster_id"` + Namespace string `gorm:"size:64;not null" json:"namespace"` + Name string `gorm:"size:128;not null" json:"name"` + Status string `gorm:"size:32" json:"status"` // Running, Pending, Succeeded, Failed + NodeName string `gorm:"size:128" json:"node_name"` + IP string `gorm:"size:64" json:"ip"` + RestartCount int `json:"restart_count"` + Image string `gorm:"size:255" json:"image"` + CPUUsage float64 `json:"cpu_usage"` + MemoryUsage float64 `json:"memory_usage"` + Labels string `gorm:"type:text" json:"labels"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Cluster *KubernetesCluster `json:"cluster,omitempty"` @@ -90,23 +89,23 @@ type KubernetesPod struct { // TableName 表名 func (KubernetesPod) TableName() string { - return "kubernetes_pods" + return "k8s_pods" } // KubernetesDeployment K8s Deployment模型 type KubernetesDeployment struct { - ID uint `gorm:"primarykey" json:"id"` - ClusterID uint `gorm:"not null" json:"cluster_id"` - Namespace string `gorm:"size:64;not null" json:"namespace"` - Name string `gorm:"size:128;not null" json:"name"` - Replicas int `json:"replicas"` - AvailableReplicas int `json:"available_replicas"` - ReadyReplicas int `json:"ready_replicas"` - Image string `gorm:"size:255" json:"image"` - Strategy string `gorm:"size:32" json:"strategy"` - Labels string `gorm:"type:text" json:"labels"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + ClusterID uint `gorm:"not null" json:"cluster_id"` + Namespace string `gorm:"size:64;not null" json:"namespace"` + Name string `gorm:"size:128;not null" json:"name"` + Replicas int `json:"replicas"` + AvailableReplicas int `json:"available_replicas"` + ReadyReplicas int `json:"ready_replicas"` + Image string `gorm:"size:255" json:"image"` + Strategy string `gorm:"size:32" json:"strategy"` + Labels string `gorm:"type:text" json:"labels"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Cluster *KubernetesCluster `json:"cluster,omitempty"` @@ -114,23 +113,23 @@ type KubernetesDeployment struct { // TableName 表名 func (KubernetesDeployment) TableName() string { - return "kubernetes_deployments" + return "k8s_deployments" } // KubernetesService K8s Service模型 type KubernetesService struct { - ID uint `gorm:"primarykey" json:"id"` - ClusterID uint `gorm:"not null" json:"cluster_id"` - Namespace string `gorm:"size:64;not null" json:"namespace"` - Name string `gorm:"size:128;not null" json:"name"` - Type string `gorm:"size:32" json:"type"` // ClusterIP, NodePort, LoadBalancer - ClusterIP string `gorm:"size:64" json:"cluster_ip"` - ExternalIP string `gorm:"size:255" json:"external_ip"` - Ports string `gorm:"type:text" json:"ports"` // JSON - Selector string `gorm:"type:text" json:"selector"` - Labels string `gorm:"type:text" json:"labels"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + ClusterID uint `gorm:"not null" json:"cluster_id"` + Namespace string `gorm:"size:64;not null" json:"namespace"` + Name string `gorm:"size:128;not null" json:"name"` + Type string `gorm:"size:32" json:"type"` // ClusterIP, NodePort, LoadBalancer + ClusterIP string `gorm:"size:64" json:"cluster_ip"` + ExternalIP string `gorm:"size:255" json:"external_ip"` + Ports string `gorm:"type:text" json:"ports"` // JSON + Selector string `gorm:"type:text" json:"selector"` + Labels string `gorm:"type:text" json:"labels"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Cluster *KubernetesCluster `json:"cluster,omitempty"` @@ -138,22 +137,22 @@ type KubernetesService struct { // TableName 表名 func (KubernetesService) TableName() string { - return "kubernetes_services" + return "k8s_services" } // KubernetesIngress K8s Ingress模型 type KubernetesIngress struct { - ID uint `gorm:"primarykey" json:"id"` - ClusterID uint `gorm:"not null" json:"cluster_id"` - Namespace string `gorm:"size:64;not null" json:"namespace"` - Name string `gorm:"size:128;not null" json:"name"` - Hosts string `gorm:"type:text" json:"hosts"` // JSON - Backend string `gorm:"size:255" json:"backend"` - TLS bool `json:"tls"` - TLSHosts string `gorm:"type:text" json:"tls_hosts"` - Labels string `gorm:"type:text" json:"labels"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + ClusterID uint `gorm:"not null" json:"cluster_id"` + Namespace string `gorm:"size:64;not null" json:"namespace"` + Name string `gorm:"size:128;not null" json:"name"` + Hosts string `gorm:"type:text" json:"hosts"` // JSON + Backend string `gorm:"size:255" json:"backend"` + TLS bool `json:"tls"` + TLSHosts string `gorm:"type:text" json:"tls_hosts"` + Labels string `gorm:"type:text" json:"labels"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Cluster *KubernetesCluster `json:"cluster,omitempty"` @@ -161,7 +160,7 @@ type KubernetesIngress struct { // TableName 表名 func (KubernetesIngress) TableName() string { - return "kubernetes_ingresses" + return "k8s_ingresses" } // CreateKubernetesClusterRequest 创建K8s集群请求 @@ -169,7 +168,7 @@ type CreateKubernetesClusterRequest struct { Name string `json:"name" binding:"required"` Code string `json:"code" binding:"required"` Version string `json:"version"` - ConfigType int `json:"config_type"` // 0=token, 1=kubeconfig + ConfigType int `json:"config_type"` // 0=token, 1=kubeconfig APIServer string `json:"api_server"` APIEndpoint string `json:"api_endpoint"` Token string `json:"token"` @@ -201,5 +200,5 @@ type TopologyLayout struct { // TableName 指定表名 func (TopologyLayout) TableName() string { - return "kubernetes_topology_layouts" + return "k8s_topology_layouts" } diff --git a/backend/model/llm.go b/backend/model/llm.go index 7efd4ea..fff6c3e 100644 --- a/backend/model/llm.go +++ b/backend/model/llm.go @@ -5,16 +5,16 @@ import "time" // LLMConfig 大语言模型配置(单行配置,类似 LDAPConfig) // Logan.Li // 支持 OpenAI 兼容接口(OpenAI / DeepSeek / 智谱GLM / 通义千问 / Moonshot / Kimi / 小米MiMo / Anthropic(需代理) / Ollama / 自定义) type LLMConfig struct { - ID uint `gorm:"primarykey" json:"id"` - Provider string `gorm:"size:50;not null;default:openai" json:"provider"` // deepseek/glm/glm-coding/kimi/moonshot/minimax/minimax-coding/mimo/siliconflow/openai/anthropic/ollama/vllm/custom - Model string `gorm:"size:100;not null;default:gpt-4o-mini" json:"model"` // 模型名,如 gpt-4o-mini / deepseek-v4-flash - APIKey string `gorm:"size:500" json:"api_key"` // API Key(GET 时会被脱敏) - Endpoint string `gorm:"size:500" json:"endpoint"` // 基础地址,如 https://api.openai.com/v1 - Temperature float64 `gorm:"default:0.7" json:"temperature"` // 采样温度 0-2 - MaxTokens int `gorm:"default:2000" json:"max_tokens"` // 最大输出 token - SystemPrompt string `gorm:"type:text" json:"system_prompt"` // 系统提示词(AIOps 运维助手角色) - Enabled bool `gorm:"default:false" json:"enabled"` // 是否启用 - ReasoningEnabled bool `gorm:"default:false" json:"reasoning_enabled"` // 是否启用推理/Plan 模式(OpenAI o-series / DeepSeek R1 / Claude thinking 等) // Logan.Li + ID uint `gorm:"primarykey" json:"id"` + Provider string `gorm:"size:50;not null;default:openai" json:"provider"` // deepseek/glm/glm-coding/kimi/moonshot/minimax/minimax-coding/mimo/siliconflow/openai/anthropic/ollama/vllm/custom + Model string `gorm:"size:100;not null;default:gpt-4o-mini" json:"model"` // 模型名,如 gpt-4o-mini / deepseek-v4-flash + APIKey string `gorm:"size:500" json:"api_key"` // API Key(GET 时会被脱敏) + Endpoint string `gorm:"size:500" json:"endpoint"` // 基础地址,如 https://api.openai.com/v1 + Temperature float64 `gorm:"default:0.7" json:"temperature"` // 采样温度 0-2 + MaxTokens int `gorm:"default:2000" json:"max_tokens"` // 最大输出 token + SystemPrompt string `gorm:"type:text" json:"system_prompt"` // 系统提示词(AIOps 运维助手角色) + Enabled bool `gorm:"default:false" json:"enabled"` // 是否启用 + ReasoningEnabled bool `gorm:"default:false" json:"reasoning_enabled"` // 是否启用推理/Plan 模式(OpenAI o-series / DeepSeek R1 / Claude thinking 等) // Logan.Li CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -26,7 +26,7 @@ func (LLMConfig) TableName() string { return "ai_llm_configs" } type SaveLLMConfigRequest struct { Provider string `json:"provider"` Model string `json:"model"` - APIKey string `json:"api_key"` // 若为脱敏值(含 ****)则后端保留原值 + APIKey string `json:"api_key"` // 若为脱敏值(含 ****)则后端保留原值 Endpoint string `json:"endpoint"` Temperature float64 `json:"temperature"` MaxTokens int `json:"max_tokens"` @@ -37,7 +37,7 @@ type SaveLLMConfigRequest struct { // ChatMessage 对话消息 // Logan.Li type ChatMessage struct { - Role string `json:"role"` // system / user / assistant + Role string `json:"role"` // system / user / assistant Content string `json:"content"` } @@ -58,11 +58,11 @@ type ChatResponse struct { // 仅持久化成功完成的对话(用户消息 + 助手回复成对写入),错误气泡为瞬时 UI 不入库 type ChatHistory struct { ID uint `gorm:"primarykey" json:"id"` - UserID uint `gorm:"index;not null" json:"user_id"` // 所属用户(JWT 中间件注入) + UserID uint `gorm:"index;not null" json:"user_id"` // 所属用户(JWT 中间件注入) SessionID string `gorm:"size:64;index;not null;default:''" json:"session_id"` // 对话会话 ID(前端 UUID) - Role string `gorm:"size:16;not null" json:"role"` // user / assistant - Content string `gorm:"type:text;not null" json:"content"` // 消息内容 - Model string `gorm:"size:100" json:"model"` // 生成回复所用模型(仅 assistant 行有意义) + Role string `gorm:"size:16;not null" json:"role"` // user / assistant + Content string `gorm:"type:text;not null" json:"content"` // 消息内容 + Model string `gorm:"size:100" json:"model"` // 生成回复所用模型(仅 assistant 行有意义) CreatedAt time.Time `json:"created_at"` } diff --git a/backend/model/log_datasource.go b/backend/model/log_datasource.go index 496b507..ac65b0d 100644 --- a/backend/model/log_datasource.go +++ b/backend/model/log_datasource.go @@ -78,4 +78,4 @@ type TestLogDSReq struct { Token string `json:"token"` Headers string `json:"headers"` SkipTLSVerify bool `json:"skip_tls_verify"` -} \ No newline at end of file +} diff --git a/backend/model/log_scope_role.go b/backend/model/log_scope_role.go index 567dea8..c6fdc0c 100644 --- a/backend/model/log_scope_role.go +++ b/backend/model/log_scope_role.go @@ -4,33 +4,33 @@ import "time" // LogScopeRole 日志查询范围权限(正则匹配 ES 索引 / Loki label) // Logan.Li type LogScopeRole struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:64;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - Engine string `gorm:"size:32;default:*;comment:引擎 loki/elasticsearch/*" json:"engine"` - MatchField string `gorm:"size:32;not null;comment:匹配维度 index/app/namespace/job" json:"match_field"` - Pattern string `gorm:"size:255;not null;comment:正则表达式" json:"pattern"` - DatasourceID *uint `json:"datasource_id,omitempty"` - Role string `gorm:"size:16;default:viewer;comment:viewer/analyst" json:"role"` - Description string `gorm:"size:255" json:"description"` - Status int `gorm:"default:1;comment:0禁用1启用" json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:64;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + Engine string `gorm:"size:32;default:*;comment:引擎 loki/elasticsearch/*" json:"engine"` + MatchField string `gorm:"size:32;not null;comment:匹配维度 index/app/namespace/job" json:"match_field"` + Pattern string `gorm:"size:255;not null;comment:正则表达式" json:"pattern"` + DatasourceID *uint `json:"datasource_id,omitempty"` + Role string `gorm:"size:16;default:viewer;comment:viewer/analyst" json:"role"` + Description string `gorm:"size:255" json:"description"` + Status int `gorm:"default:1;comment:0禁用1启用" json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } -func (LogScopeRole) TableName() string { return "monitor_log_scope_roles" } +func (LogScopeRole) TableName() string { return "iam_log_scope_roles" } // UserLogScopeRole 用户 ↔ 日志范围权限 // Logan.Li type UserLogScopeRole struct { ID uint `gorm:"primarykey" json:"id"` - UserID uint `gorm:"uniqueIndex:idx_user_log_scope;not null" json:"user_id"` - LogScopeRoleID uint `gorm:"uniqueIndex:idx_user_log_scope;not null" json:"log_scope_role_id"` + UserID uint `gorm:"uniqueIndex:idx_iam_user_log_scope_role;not null" json:"user_id"` + LogScopeRoleID uint `gorm:"uniqueIndex:idx_iam_user_log_scope_role;not null" json:"log_scope_role_id"` CreatedAt time.Time `json:"created_at"` - LogScopeRole *LogScopeRole `json:"log_scope_role,omitempty" gorm:"foreignKey:LogScopeRoleID"` + LogScopeRole *LogScopeRole `json:"log_scope_role,omitempty" gorm:"foreignKey:LogScopeRoleID;references:ID"` } -func (UserLogScopeRole) TableName() string { return "monitor_user_log_scope_roles" } +func (UserLogScopeRole) TableName() string { return "iam_user_log_scope_roles" } // CreateLogScopeRoleRequest 创建日志权限策略 // Logan.Li type CreateLogScopeRoleRequest struct { @@ -64,9 +64,9 @@ type AssignLogScopeRoleRequest struct { // PreviewLogScopeRequest 命中预览 // Logan.Li type PreviewLogScopeRequest struct { - Engine string `json:"engine" binding:"required,oneof=loki elasticsearch"` - DatasourceID uint `json:"datasource_id"` - Index string `json:"index"` - Query string `json:"query"` - SampleValue string `json:"sample_value"` -} \ No newline at end of file + Engine string `json:"engine" binding:"required,oneof=loki elasticsearch"` + DatasourceID uint `json:"datasource_id"` + Index string `json:"index"` + Query string `json:"query"` + SampleValue string `json:"sample_value"` +} diff --git a/backend/model/login_log.go b/backend/model/login_log.go index 3e89541..a1aad75 100644 --- a/backend/model/login_log.go +++ b/backend/model/login_log.go @@ -8,19 +8,19 @@ type LoginLog struct { UserID *uint `json:"user_id"` Username string `gorm:"size:64;index" json:"username"` IP string `gorm:"size:64" json:"ip"` - Location string `gorm:"size:128" json:"location"` // IP 归属地 - Browser string `gorm:"size:128" json:"browser"` // 浏览器 - OS string `gorm:"size:128" json:"os"` // 操作系统 + Location string `gorm:"size:128" json:"location"` // IP 归属地 + Browser string `gorm:"size:128" json:"browser"` // 浏览器 + OS string `gorm:"size:128" json:"os"` // 操作系统 LoginType string `gorm:"size:32;default:password" json:"login_type"` // password / ldap UserAgent string `gorm:"size:500" json:"user_agent"` - Status string `gorm:"size:32" json:"status"` // success, failed + Status string `gorm:"size:32" json:"status"` // success, failed FailReason string `gorm:"size:255" json:"fail_reason"` LoginAt time.Time `gorm:"index" json:"login_at"` CreatedAt time.Time `json:"created_at"` } func (LoginLog) TableName() string { - return "sys_login_logs" + return "audit_login_logs" } // LoginLogQuery 登录日志查询 diff --git a/backend/model/metrics_aggregate.go b/backend/model/metrics_aggregate.go index e017968..d92c267 100644 --- a/backend/model/metrics_aggregate.go +++ b/backend/model/metrics_aggregate.go @@ -7,8 +7,8 @@ import "time" type MetricsAggregate struct { ID uint `gorm:"primarykey" json:"id"` Hostname string `gorm:"size:255;index:idx_agg;not null" json:"hostname"` - Metric string `gorm:"size:32;index:idx_agg;not null" json:"metric"` // cpu / mem / load1 / disk / net_rx / net_tx - Interval string `gorm:"size:8;index:idx_agg;not null" json:"interval"` // 1m / 5m / 1h / 6h / 1d + Metric string `gorm:"size:32;index:idx_agg;not null" json:"metric"` // cpu / mem / load1 / disk / net_rx / net_tx + Interval string `gorm:"size:8;index:idx_agg;not null" json:"interval"` // 1m / 5m / 1h / 6h / 1d Avg float64 `gorm:"type:decimal(10,2)" json:"avg"` MaxV float64 `gorm:"column:max_v;type:decimal(10,2)" json:"max"` MinV float64 `gorm:"column:min_v;type:decimal(10,2)" json:"min"` @@ -19,7 +19,7 @@ type MetricsAggregate struct { } func (MetricsAggregate) TableName() string { - return "monitor_metrics_aggregate" + return "monitor_metric_aggregates" } // AggregateReq 聚合触发请求 diff --git a/backend/model/monitor.go b/backend/model/monitor.go index 395257e..69a7b44 100644 --- a/backend/model/monitor.go +++ b/backend/model/monitor.go @@ -42,18 +42,18 @@ type MonitorTarget struct { OS string `gorm:"type:varchar(64);comment:操作系统" json:"os,omitempty"` OSVersion string `gorm:"type:varchar(64);comment:OS版本" json:"os_version,omitempty"` Arch string `gorm:"type:varchar(16);comment:系统架构" json:"arch,omitempty"` - Kernel string `gorm:"type:varchar(64);comment:内核版本" json:"kernel,omitempty"` - HostGroupID uint `gorm:"index;comment:主机组ID" json:"host_group_id,omitempty"` - Tags string `gorm:"type:varchar(512);comment:标签JSON" json:"tags,omitempty"` - CollectMode string `gorm:"type:varchar(16);default:'agent';comment:采集模式:agent/snmp/api" json:"collect_mode"` - SNMPConfig string `gorm:"type:text;comment:SNMP配置JSON" json:"snmp_config,omitempty"` - APIConfig string `gorm:"type:text;comment:API配置JSON" json:"api_config,omitempty"` - Address string `gorm:"type:varchar(255);comment:监控地址(IP或域名)" json:"address,omitempty"` - Protocol string `gorm:"type:varchar(32);comment:协议类型(http/https/tcp/udp/icmp/snmp)" json:"protocol,omitempty"` - Timeout int `gorm:"default:30;comment:超时时间(秒)" json:"timeout,omitempty"` - Config string `gorm:"type:text;comment:通用配置JSON" json:"config,omitempty"` - Interval int `gorm:"default:60;comment:采集间隔(秒)" json:"interval"` - Labels string `gorm:"type:varchar(512);comment:标签JSON" json:"labels,omitempty"` + Kernel string `gorm:"type:varchar(64);comment:内核版本" json:"kernel,omitempty"` + HostGroupID uint `gorm:"index;comment:主机组ID" json:"host_group_id,omitempty"` + Tags string `gorm:"type:varchar(512);comment:标签JSON" json:"tags,omitempty"` + CollectMode string `gorm:"type:varchar(16);default:'agent';comment:采集模式:agent/snmp/api" json:"collect_mode"` + SNMPConfig string `gorm:"type:text;comment:SNMP配置JSON" json:"snmp_config,omitempty"` + APIConfig string `gorm:"type:text;comment:API配置JSON" json:"api_config,omitempty"` + Address string `gorm:"type:varchar(255);comment:监控地址(IP或域名)" json:"address,omitempty"` + Protocol string `gorm:"type:varchar(32);comment:协议类型(http/https/tcp/udp/icmp/snmp)" json:"protocol,omitempty"` + Timeout int `gorm:"default:30;comment:超时时间(秒)" json:"timeout,omitempty"` + Config string `gorm:"type:text;comment:通用配置JSON" json:"config,omitempty"` + Interval int `gorm:"default:60;comment:采集间隔(秒)" json:"interval"` + Labels string `gorm:"type:varchar(512);comment:标签JSON" json:"labels,omitempty"` Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` CreatedAt time.Time `gorm:"not null;index;comment:创建时间" json:"created_at"` UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` @@ -86,36 +86,36 @@ const ( type DataSourceType string const ( - DataSourceAgent DataSourceType = "agent" // Agent采集 - DataSourceSNMP DataSourceType = "snmp" // SNMP采集 - DataSourceAPI DataSourceType = "api" // API采集 + DataSourceAgent DataSourceType = "agent" // Agent采集 + DataSourceSNMP DataSourceType = "snmp" // SNMP采集 + DataSourceAPI DataSourceType = "api" // API采集 DataSourcePrometheus DataSourceType = "prometheus" // Prometheus - DataSourceZabbix DataSourceType = "zabbix" // Zabbix + DataSourceZabbix DataSourceType = "zabbix" // Zabbix ) // MonitorMetric 监控指标定义表 // 定义系统中所有可采集的监控指标元数据 type MonitorMetric struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;uniqueIndex:uk_metric_name;comment:指标名称" json:"name"` - DisplayName string `gorm:"type:varchar(128);not null;comment:显示名称" json:"display_name"` - Description string `gorm:"type:varchar(256);comment:指标描述" json:"description,omitempty"` - MetricType MetricType `gorm:"type:varchar(16);not null;comment:指标类型: gauge/counter/summary" json:"metric_type"` - Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` - DataSource DataSourceType `gorm:"type:varchar(16);not null;comment:数据来源" json:"data_source"` - CollectExpr string `gorm:"type:varchar(256);comment:采集表达式/OID/PromQL" json:"collect_expr,omitempty"` - Interval int `gorm:"default:60;comment:采集间隔(秒)" json:"interval"` - Labels string `gorm:"type:varchar(512);comment:指标标签定义JSON" json:"labels,omitempty"` - ValueType string `gorm:"type:varchar(16);default:'float64';comment:值类型" json:"value_type"` - Category string `gorm:"type:varchar(64);comment:指标分类" json:"category,omitempty"` - IsDefault bool `gorm:"default:true;comment:是否默认启用" json:"is_default"` - IsInternal bool `gorm:"default:false;comment:是否内置指标" json:"is_internal"` - Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` - Icon string `gorm:"type:varchar(64);comment:图标" json:"icon,omitempty"` - SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;uniqueIndex:uk_metric_name;comment:指标名称" json:"name"` + DisplayName string `gorm:"type:varchar(128);not null;comment:显示名称" json:"display_name"` + Description string `gorm:"type:varchar(256);comment:指标描述" json:"description,omitempty"` + MetricType MetricType `gorm:"type:varchar(16);not null;comment:指标类型: gauge/counter/summary" json:"metric_type"` + Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` + DataSource DataSourceType `gorm:"type:varchar(16);not null;comment:数据来源" json:"data_source"` + CollectExpr string `gorm:"type:varchar(256);comment:采集表达式/OID/PromQL" json:"collect_expr,omitempty"` + Interval int `gorm:"default:60;comment:采集间隔(秒)" json:"interval"` + Labels string `gorm:"type:varchar(512);comment:指标标签定义JSON" json:"labels,omitempty"` + ValueType string `gorm:"type:varchar(16);default:'float64';comment:值类型" json:"value_type"` + Category string `gorm:"type:varchar(64);comment:指标分类" json:"category,omitempty"` + IsDefault bool `gorm:"default:true;comment:是否默认启用" json:"is_default"` + IsInternal bool `gorm:"default:false;comment:是否内置指标" json:"is_internal"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + Icon string `gorm:"type:varchar(64);comment:图标" json:"icon,omitempty"` + SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` } // TableName 指定表名 @@ -147,7 +147,7 @@ type MetricValue struct { // TableName 指定表名 func (MetricValue) TableName() string { - return "metric_values" + return "monitor_metric_values" } // MetricHistory 指标历史数据(简化版,供监控查询使用) @@ -213,29 +213,29 @@ func (MetricTag) TableName() string { type DashboardType string const ( - DashboardTypeSystem DashboardType = "system" // 系统内置 - DashboardTypeCustom DashboardType = "custom" // 自定义 - DashboardTypeShared DashboardType = "shared" // 共享 + DashboardTypeSystem DashboardType = "system" // 系统内置 + DashboardTypeCustom DashboardType = "custom" // 自定义 + DashboardTypeShared DashboardType = "shared" // 共享 ) // MonitorDashboard 监控大屏配置表 // 定义监控大屏的元数据配置 type MonitorDashboard struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:大屏名称" json:"name"` - Type DashboardType `gorm:"type:varchar(16);default:'custom';comment:大屏类型" json:"type"` - Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` - Layout string `gorm:"type:text;comment:布局配置JSON" json:"layout,omitempty"` - Theme string `gorm:"type:varchar(32);default:'dark';comment:主题: dark/light" json:"theme"` - RefreshRate int `gorm:"default:30;comment:自动刷新间隔(秒)" json:"refresh_rate"` - IsDefault bool `gorm:"default:false;comment:是否默认大屏" json:"is_default"` - IsPublic bool `gorm:"default:false;comment:是否公开" json:"is_public"` - CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"` - Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` - SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:大屏名称" json:"name"` + Type DashboardType `gorm:"type:varchar(16);default:'custom';comment:大屏类型" json:"type"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + Layout string `gorm:"type:text;comment:布局配置JSON" json:"layout,omitempty"` + Theme string `gorm:"type:varchar(32);default:'dark';comment:主题: dark/light" json:"theme"` + RefreshRate int `gorm:"default:30;comment:自动刷新间隔(秒)" json:"refresh_rate"` + IsDefault bool `gorm:"default:false;comment:是否默认大屏" json:"is_default"` + IsPublic bool `gorm:"default:false;comment:是否公开" json:"is_public"` + CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` // 关联关系 Panels []DashboardPanel `gorm:"foreignKey:DashboardID" json:"panels,omitempty"` @@ -254,16 +254,16 @@ func (MonitorDashboard) TableName() string { type PanelType string const ( - PanelTypeChart PanelType = "chart" // 图表 - PanelTypeGauge PanelType = "gauge" // 仪表盘 - PanelTypeCounter PanelType = "counter" // 计数器 - PanelTypeTable PanelType = "table" // 表格 - PanelTypeStatus PanelType = "status" // 状态 - PanelTypeTopology PanelType = "topology" // 拓扑 - PanelTypeLog PanelType = "log" // 日志 - PanelTypeAlertList PanelType = "alert_list" // 告警列表 - PanelTypeHeatmap PanelType = "heatmap" // 热力图 - PanelTypeText PanelType = "text" // 文本 + PanelTypeChart PanelType = "chart" // 图表 + PanelTypeGauge PanelType = "gauge" // 仪表盘 + PanelTypeCounter PanelType = "counter" // 计数器 + PanelTypeTable PanelType = "table" // 表格 + PanelTypeStatus PanelType = "status" // 状态 + PanelTypeTopology PanelType = "topology" // 拓扑 + PanelTypeLog PanelType = "log" // 日志 + PanelTypeAlertList PanelType = "alert_list" // 告警列表 + PanelTypeHeatmap PanelType = "heatmap" // 热力图 + PanelTypeText PanelType = "text" // 文本 ) // ChartType 图表类型 @@ -298,13 +298,13 @@ type DashboardPanel struct { QueryExpr string `gorm:"type:text;comment:查询表达式" json:"query_expr,omitempty"` QueryConfig string `gorm:"type:text;comment:查询配置JSON" json:"query_config,omitempty"` // 展示配置 - TimeRange string `gorm:"type:varchar(16);default:'1h';comment:时间范围" json:"time_range"` - RefreshRate int `gorm:"default:0;comment:刷新间隔(秒),0表示继承大屏" json:"refresh_rate"` - Thresholds string `gorm:"type:text;comment:阈值配置JSON" json:"thresholds,omitempty"` - Colors string `gorm:"type:text;comment:颜色配置JSON" json:"colors,omitempty"` - Legend string `gorm:"type:varchar(64);comment:图例配置" json:"legend,omitempty"` - Unit string `gorm:"type:varchar(32);comment:显示单位" json:"unit,omitempty"` - Decimals int `gorm:"default:2;comment:小数位数" json:"decimals"` + TimeRange string `gorm:"type:varchar(16);default:'1h';comment:时间范围" json:"time_range"` + RefreshRate int `gorm:"default:0;comment:刷新间隔(秒),0表示继承大屏" json:"refresh_rate"` + Thresholds string `gorm:"type:text;comment:阈值配置JSON" json:"thresholds,omitempty"` + Colors string `gorm:"type:text;comment:颜色配置JSON" json:"colors,omitempty"` + Legend string `gorm:"type:varchar(64);comment:图例配置" json:"legend,omitempty"` + Unit string `gorm:"type:varchar(32);comment:显示单位" json:"unit,omitempty"` + Decimals int `gorm:"default:2;comment:小数位数" json:"decimals"` // 状态 Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` @@ -314,18 +314,18 @@ type DashboardPanel struct { // TableName 指定表名 func (DashboardPanel) TableName() string { - return "dashboard_panels" + return "monitor_dashboard_panels" } // PanelQueryConfig 面板查询配置 type PanelQueryConfig struct { - Metrics []string `json:"metrics"` // 指标名称列表 - Targets []uint `json:"targets"` // 目标ID列表 - Instances []string `json:"instances"` // 实例列表 - Aggregations []string `json:"aggregations"` // 聚合函数 - GroupBy []string `json:"group_by"` // 分组标签 - Filters map[string]string `json:"filters"` // 过滤条件 - Step int `json:"step"` // 步长 + Metrics []string `json:"metrics"` // 指标名称列表 + Targets []uint `json:"targets"` // 目标ID列表 + Instances []string `json:"instances"` // 实例列表 + Aggregations []string `json:"aggregations"` // 聚合函数 + GroupBy []string `json:"group_by"` // 分组标签 + Filters map[string]string `json:"filters"` // 过滤条件 + Step int `json:"step"` // 步长 LegendFormat string `json:"legend_format"` // 图例格式 } @@ -362,9 +362,9 @@ func (HostGroup) TableName() string { type ProbeType string const ( - ProbeTypeHTTP ProbeType = "http_get" // HTTP GET 探测 - ProbeTypeTCP ProbeType = "tcp_socket" // TCP 端口连接探测 - ProbeTypeExec ProbeType = "exec_command" // 命令执行探测 + ProbeTypeHTTP ProbeType = "http_get" // HTTP GET 探测 + ProbeTypeTCP ProbeType = "tcp_socket" // TCP 端口连接探测 + ProbeTypeExec ProbeType = "exec_command" // 命令执行探测 ) // ProbeStatus 探针结果状态 @@ -378,33 +378,33 @@ const ( // HealthProbe 健康探针配置表 // 存储在平台 DB,Agent 拉取配置后在目标主机本地执行,结果随心跳上报 type HealthProbe struct { - ID uint `gorm:"primaryKey;autoIncrement" json:"id"` - Name string `gorm:"type:varchar(128);not null;index" json:"name"` - Hostname string `gorm:"type:varchar(255);index;not null" json:"hostname"` - ProbeType ProbeType `gorm:"type:varchar(32);not null" json:"probe_type"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Name string `gorm:"type:varchar(128);not null;index" json:"name"` + Hostname string `gorm:"type:varchar(255);index;not null" json:"hostname"` + ProbeType ProbeType `gorm:"type:varchar(32);not null" json:"probe_type"` // HTTP 探针配置 - HTTPURL string `gorm:"type:varchar(512)" json:"http_url,omitempty"` - HTTPMethod string `gorm:"type:varchar(10);default:GET" json:"http_method,omitempty"` - HTTPHeaders string `gorm:"type:text" json:"http_headers,omitempty"` - HTTPExpectCode int `gorm:"default:200" json:"http_expect_code,omitempty"` - HTTPExpectBody string `gorm:"type:varchar(256)" json:"http_expect_body,omitempty"` + HTTPURL string `gorm:"type:varchar(512)" json:"http_url,omitempty"` + HTTPMethod string `gorm:"type:varchar(10);default:GET" json:"http_method,omitempty"` + HTTPHeaders string `gorm:"type:text" json:"http_headers,omitempty"` + HTTPExpectCode int `gorm:"default:200" json:"http_expect_code,omitempty"` + HTTPExpectBody string `gorm:"type:varchar(256)" json:"http_expect_body,omitempty"` // TCP 探针配置 - TCPHost string `gorm:"type:varchar(255)" json:"tcp_host,omitempty"` - TCPPort int `json:"tcp_port,omitempty"` + TCPHost string `gorm:"type:varchar(255)" json:"tcp_host,omitempty"` + TCPPort int `json:"tcp_port,omitempty"` // Exec 探针配置 - ExecCommand string `gorm:"type:text" json:"exec_command,omitempty"` + ExecCommand string `gorm:"type:text" json:"exec_command,omitempty"` // 通用配置 - Timeout int `gorm:"default:10" json:"timeout"` - Interval int `gorm:"default:60" json:"interval"` - Enabled bool `gorm:"default:true;index" json:"enabled"` - LastStatus ProbeStatus `gorm:"type:varchar(8);default:''" json:"last_status,omitempty"` - LastCheckedAt *time.Time `json:"last_checked_at,omitempty"` - LastLatencyMs int `json:"last_latency_ms"` - FailCount int `json:"fail_count"` - Description string `gorm:"type:varchar(256)" json:"description,omitempty"` - CreatedAt time.Time `gorm:"not null" json:"created_at"` - UpdatedAt time.Time `gorm:"not null" json:"updated_at"` - DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` + Timeout int `gorm:"default:10" json:"timeout"` + Interval int `gorm:"default:60" json:"interval"` + Enabled bool `gorm:"default:true;index" json:"enabled"` + LastStatus ProbeStatus `gorm:"type:varchar(8);default:''" json:"last_status,omitempty"` + LastCheckedAt *time.Time `json:"last_checked_at,omitempty"` + LastLatencyMs int `json:"last_latency_ms"` + FailCount int `json:"fail_count"` + Description string `gorm:"type:varchar(256)" json:"description,omitempty"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` + DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` } // TableName 指定表名 @@ -417,9 +417,8 @@ type HealthProbeResult struct { ProbeID uint `json:"probe_id"` Name string `json:"name"` ProbeType string `json:"probe_type"` - Status ProbeStatus `json:"status"` // up / down - LatencyMs int `json:"latency_ms"` // 响应延迟(ms) + Status ProbeStatus `json:"status"` // up / down + LatencyMs int `json:"latency_ms"` // 响应延迟(ms) StatusCode int `json:"status_code,omitempty"` // HTTP 状态码 Message string `json:"message,omitempty"` // 错误/附加信息 } - diff --git a/backend/model/monitor_alert_rule.go b/backend/model/monitor_alert_rule.go index 2ba039d..8f72bad 100644 --- a/backend/model/monitor_alert_rule.go +++ b/backend/model/monitor_alert_rule.go @@ -14,21 +14,21 @@ const ( // MonitorAlertRule 中间件/站点拨测告警规则(主机规则仍用 AgentRule)// Logan.Li type MonitorAlertRule struct { - ID uint `gorm:"primaryKey;autoIncrement" json:"id"` - Category MonitorAlertCategory `gorm:"type:varchar(16);not null;index" json:"category"` - Name string `gorm:"type:varchar(128);not null" json:"name"` - TargetID uint `gorm:"default:0;index;comment:目标ID,0=全部" json:"target_id"` - Metric string `gorm:"type:varchar(64);not null" json:"metric"` - Operator string `gorm:"type:varchar(8);default:>" json:"operator"` - Threshold float64 `gorm:"type:decimal(12,4)" json:"threshold"` - Severity string `gorm:"type:varchar(16);default:warning" json:"severity"` - Enabled bool `gorm:"default:true" json:"enabled"` - DurationSec int `gorm:"default:0;comment:持续秒数(预留)" json:"duration_sec"` - Port int `gorm:"default:0" json:"port,omitempty"` - Description string `gorm:"type:varchar(256)" json:"description,omitempty"` - LastTriggered *time.Time `json:"last_triggered,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Category MonitorAlertCategory `gorm:"type:varchar(16);not null;index" json:"category"` + Name string `gorm:"type:varchar(128);not null" json:"name"` + TargetID uint `gorm:"default:0;index;comment:目标ID,0=全部" json:"target_id"` + Metric string `gorm:"type:varchar(64);not null" json:"metric"` + Operator string `gorm:"type:varchar(8);default:>" json:"operator"` + Threshold float64 `gorm:"type:decimal(12,4)" json:"threshold"` + Severity string `gorm:"type:varchar(16);default:warning" json:"severity"` + Enabled bool `gorm:"default:true" json:"enabled"` + DurationSec int `gorm:"default:0;comment:持续秒数(预留)" json:"duration_sec"` + Port int `gorm:"default:0" json:"port,omitempty"` + Description string `gorm:"type:varchar(256)" json:"description,omitempty"` + LastTriggered *time.Time `json:"last_triggered,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } -func (MonitorAlertRule) TableName() string { return "monitor_alert_rules" } \ No newline at end of file +func (MonitorAlertRule) TableName() string { return "monitor_probe_alert_rules" } diff --git a/backend/model/navigation.go b/backend/model/navigation.go index 5f95399..51577da 100644 --- a/backend/model/navigation.go +++ b/backend/model/navigation.go @@ -2,21 +2,20 @@ package model import ( "time" - ) // NavigationGroup 导航分组模型 type NavigationGroup struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:64;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - Description string `gorm:"size:255" json:"description"` - Icon string `gorm:"size:64" json:"icon"` - Sort int `gorm:"default:0" json:"sort"` - Enabled bool `gorm:"default:true" json:"enabled"` - LinkCount int `gorm:"default:0" json:"link_count"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:64;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + Description string `gorm:"size:255" json:"description"` + Icon string `gorm:"size:64" json:"icon"` + Sort int `gorm:"default:0" json:"sort"` + Enabled bool `gorm:"default:true" json:"enabled"` + LinkCount int `gorm:"default:0" json:"link_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 @@ -26,18 +25,18 @@ func (NavigationGroup) TableName() string { // NavigationLink 导航链接模型 type NavigationLink struct { - ID uint `gorm:"primarykey" json:"id"` - GroupID uint `json:"group_id"` - Name string `gorm:"size:128;not null" json:"name"` - URL string `gorm:"size:500;not null" json:"url"` - Description string `gorm:"size:255" json:"description"` - Icon string `gorm:"size:64" json:"icon"` - Tags string `gorm:"type:text" json:"tags"` // JSON - IsFavorite bool `gorm:"default:false" json:"is_favorite"` - Sort int `gorm:"default:0" json:"sort"` - Enabled bool `gorm:"default:true" json:"enabled"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + GroupID uint `json:"group_id"` + Name string `gorm:"size:128;not null" json:"name"` + URL string `gorm:"size:500;not null" json:"url"` + Description string `gorm:"size:255" json:"description"` + Icon string `gorm:"size:64" json:"icon"` + Tags string `gorm:"type:text" json:"tags"` // JSON + IsFavorite bool `gorm:"default:false" json:"is_favorite"` + Sort int `gorm:"default:0" json:"sort"` + Enabled bool `gorm:"default:true" json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Group *NavigationGroup `json:"group,omitempty"` @@ -45,7 +44,7 @@ type NavigationLink struct { // TableName 表名 func (NavigationLink) TableName() string { - return "navigation_links" + return "sys_navigation_links" } // CreateNavigationGroupRequest 创建导航分组请求 diff --git a/backend/model/ops_modules.go b/backend/model/ops_modules.go index 421c1b5..6ba2390 100644 --- a/backend/model/ops_modules.go +++ b/backend/model/ops_modules.go @@ -2,83 +2,82 @@ package model import ( "time" - ) // AnsiblePlaybook Playbook 模型 // Logan.Li type AnsiblePlaybook struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:255;not null" json:"name"` - Description string `gorm:"type:text" json:"description"` - Content string `gorm:"type:text;not null" json:"content"` // YAML 内容 - Tags string `gorm:"size:500" json:"tags"` // 逗号分隔 - Category string `gorm:"size:100" json:"category"` // deployment/maintenance/etc - CreatedBy string `gorm:"size:255" json:"created_by"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + Description string `gorm:"type:text" json:"description"` + Content string `gorm:"type:text;not null" json:"content"` // YAML 内容 + Tags string `gorm:"size:500" json:"tags"` // 逗号分隔 + Category string `gorm:"size:100" json:"category"` // deployment/maintenance/etc + CreatedBy string `gorm:"size:255" json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (AnsiblePlaybook) TableName() string { return "ops_ansible_playbooks" } // AnsibleJob 执行任务记录 // Logan.Li type AnsibleJob struct { - ID uint `gorm:"primarykey" json:"id"` - PlaybookID uint `gorm:"not null" json:"playbook_id"` - PlaybookName string `gorm:"size:255" json:"playbook_name"` - Status string `gorm:"size:20;default:pending" json:"status"` // pending/running/success/failed - TargetHosts string `gorm:"type:text" json:"target_hosts"` // JSON 数组 - Variables string `gorm:"type:text" json:"variables"` // JSON - Output string `gorm:"type:text" json:"output"` - Duration int64 `gorm:"default:0" json:"duration"` // 毫秒 - TriggeredBy string `gorm:"size:255" json:"triggered_by"` - StartedAt *time.Time `json:"started_at"` - CompletedAt *time.Time `json:"completed_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + PlaybookID uint `gorm:"not null" json:"playbook_id"` + PlaybookName string `gorm:"size:255" json:"playbook_name"` + Status string `gorm:"size:20;default:pending" json:"status"` // pending/running/success/failed + TargetHosts string `gorm:"type:text" json:"target_hosts"` // JSON 数组 + Variables string `gorm:"type:text" json:"variables"` // JSON + Output string `gorm:"type:text" json:"output"` + Duration int64 `gorm:"default:0" json:"duration"` // 毫秒 + TriggeredBy string `gorm:"size:255" json:"triggered_by"` + StartedAt *time.Time `json:"started_at"` + CompletedAt *time.Time `json:"completed_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (AnsibleJob) TableName() string { return "ops_ansible_jobs" } // AnsibleInventory 主机清单 // Logan.Li type AnsibleInventory struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:255;not null" json:"name"` - Description string `gorm:"type:text" json:"description"` - Hosts string `gorm:"type:text" json:"hosts"` // JSON 数组 [{name,ip,port,user,os}] - CreatedBy string `gorm:"size:255" json:"created_by"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + Description string `gorm:"type:text" json:"description"` + Hosts string `gorm:"type:text" json:"hosts"` // JSON 数组 [{name,ip,port,user,os}] + CreatedBy string `gorm:"size:255" json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (AnsibleInventory) TableName() string { return "ops_ansible_inventories" } // LogSource 日志源 // Logan.Li type LogSource struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:255;not null" json:"name"` - Type string `gorm:"size:50;not null" json:"type"` // application/syslog/docker/kubernetes - Status string `gorm:"size:20;default:active" json:"status"` - Config string `gorm:"type:text" json:"config"` // JSON {path/pattern} - LogCount int64 `gorm:"default:0" json:"log_count"` - LastSeen *time.Time `json:"last_seen"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + Type string `gorm:"size:50;not null" json:"type"` // application/syslog/docker/kubernetes + Status string `gorm:"size:20;default:active" json:"status"` + Config string `gorm:"type:text" json:"config"` // JSON {path/pattern} + LogCount int64 `gorm:"default:0" json:"log_count"` + LastSeen *time.Time `json:"last_seen"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (LogSource) TableName() string { return "sys_log_sources" } // LogEntry 日志条目 // Logan.Li type LogEntry struct { - ID uint `gorm:"primarykey" json:"id"` - SourceID uint `gorm:"index" json:"source_id"` - Timestamp time.Time `gorm:"index" json:"timestamp"` - Level string `gorm:"size:20;index" json:"level"` // error/warn/info/debug - Service string `gorm:"size:255;index" json:"service"` - Host string `gorm:"size:255;index" json:"host"` - Message string `gorm:"type:text" json:"message"` - TraceID string `gorm:"size:100;index" json:"trace_id"` - Raw string `gorm:"type:text" json:"raw"` - CreatedAt time.Time `json:"created_at"` + ID uint `gorm:"primarykey" json:"id"` + SourceID uint `gorm:"index" json:"source_id"` + Timestamp time.Time `gorm:"index" json:"timestamp"` + Level string `gorm:"size:20;index" json:"level"` // error/warn/info/debug + Service string `gorm:"size:255;index" json:"service"` + Host string `gorm:"size:255;index" json:"host"` + Message string `gorm:"type:text" json:"message"` + TraceID string `gorm:"size:100;index" json:"trace_id"` + Raw string `gorm:"type:text" json:"raw"` + CreatedAt time.Time `json:"created_at"` } func (LogEntry) TableName() string { return "sys_log_entries" } diff --git a/backend/model/prometheus_datasource.go b/backend/model/prometheus_datasource.go index 5369d71..15d0bb8 100644 --- a/backend/model/prometheus_datasource.go +++ b/backend/model/prometheus_datasource.go @@ -12,10 +12,10 @@ type PrometheusDataSource struct { Access string `gorm:"type:varchar(16);default:'server';comment:访问模式: server/proxy" json:"access"` // Basic Auth - BasicAuth bool `gorm:"default:false;comment:启用基础认证" json:"basic_auth"` - BasicAuthUser string `gorm:"type:varchar(128);comment:认证用户名" json:"basic_auth_user,omitempty"` - BasicAuthPass string `gorm:"type:varchar(256);comment:认证密码" json:"basic_auth_pass,omitempty"` - WithCredentials bool `gorm:"default:false;comment:携带凭证" json:"with_credentials"` + BasicAuth bool `gorm:"default:false;comment:启用基础认证" json:"basic_auth"` + BasicAuthUser string `gorm:"type:varchar(128);comment:认证用户名" json:"basic_auth_user,omitempty"` + BasicAuthPass string `gorm:"type:varchar(256);comment:认证密码" json:"basic_auth_pass,omitempty"` + WithCredentials bool `gorm:"default:false;comment:携带凭证" json:"with_credentials"` // TLS Client Auth TLSClientAuth bool `gorm:"default:false;comment:启用TLS客户端认证" json:"tls_client_auth"` @@ -31,8 +31,8 @@ type PrometheusDataSource struct { AlertmanagerDS string `gorm:"type:varchar(128);comment:Alertmanager数据源" json:"alertmanager_ds,omitempty"` // 采集与查询 - ScrapeInterval int `gorm:"default:15;comment:采集间隔(秒)" json:"scrape_interval"` - QueryTimeout int `gorm:"default:60;comment:查询超时(秒)" json:"query_timeout"` + ScrapeInterval int `gorm:"default:15;comment:采集间隔(秒)" json:"scrape_interval"` + QueryTimeout int `gorm:"default:60;comment:查询超时(秒)" json:"query_timeout"` HTTPMethod string `gorm:"type:varchar(8);default:'POST';comment:HTTP方法" json:"http_method"` // 杂项 @@ -109,9 +109,9 @@ type UpdatePromDSReq struct { // TestPromDSReq 测试数据源连接请求 type TestPromDSReq struct { - URL string `json:"url" binding:"required"` - BasicAuth bool `json:"basic_auth"` - BasicAuthUser string `json:"basic_auth_user"` - BasicAuthPass string `json:"basic_auth_pass"` - SkipTLSVerify bool `json:"skip_tls_verify"` + URL string `json:"url" binding:"required"` + BasicAuth bool `json:"basic_auth"` + BasicAuthUser string `json:"basic_auth_user"` + BasicAuthPass string `json:"basic_auth_pass"` + SkipTLSVerify bool `json:"skip_tls_verify"` } diff --git a/backend/model/rdp_session.go b/backend/model/rdp_session.go index 79c931f..e38206f 100644 --- a/backend/model/rdp_session.go +++ b/backend/model/rdp_session.go @@ -25,7 +25,7 @@ type RDPSession struct { } func (RDPSession) TableName() string { - return "sys_rdp_sessions" + return "audit_rdp_sessions" } // RDPSessionQuery Windows RDP 图形会话查询 // Logan.Li diff --git a/backend/model/resource_model.go b/backend/model/resource_model.go index 10c6535..a9b45b6 100644 --- a/backend/model/resource_model.go +++ b/backend/model/resource_model.go @@ -2,20 +2,19 @@ package model import ( "time" - ) // ResourceModel 资源模型 (CMDB 模型定义) type ResourceModel struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:64;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - Description string `gorm:"size:500" json:"description"` - Type string `gorm:"size:32;default:normal" json:"type"` // normal, parent - Attributes string `gorm:"type:text" json:"attributes"` // JSON 数组 - Icon string `gorm:"size:64" json:"icon"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:64;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + Description string `gorm:"size:500" json:"description"` + Type string `gorm:"size:32;default:normal" json:"type"` // normal, parent + Attributes string `gorm:"type:text" json:"attributes"` // JSON 数组 + Icon string `gorm:"size:64" json:"icon"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (ResourceModel) TableName() string { diff --git a/backend/model/server.go b/backend/model/server.go index 8feeab3..04a8daf 100644 --- a/backend/model/server.go +++ b/backend/model/server.go @@ -6,40 +6,43 @@ import ( // Server 服务器模型 type Server struct { - ID uint `gorm:"primarykey" json:"id"` - Hostname string `gorm:"size:128;not null" json:"hostname"` - IP string `gorm:"size:64;not null" json:"ip"` - PublicIP string `gorm:"size:64" json:"public_ip"` - Port int `gorm:"default:22" json:"port"` - Username string `gorm:"size:64;default:root" json:"username"` - Password string `gorm:"size:255" json:"-"` - SSHKey string `gorm:"type:text" json:"-"` - AuthType string `gorm:"size:32;default:password" json:"auth_type"` // password, key - OSType string `gorm:"size:32;default:linux" json:"os_type"` // 操作系统类型:linux/windows // Logan.Li - Protocol string `gorm:"size:32;default:ssh" json:"protocol"` // 远程协议:ssh/rdp // Logan.Li - RDPPort int `gorm:"default:3389" json:"rdp_port"` // Windows RDP 端口 // Logan.Li - OS string `gorm:"size:64" json:"os"` - Kernel string `gorm:"size:64" json:"kernel"` - OSVersion string `gorm:"size:64" json:"os_version"` - Arch string `gorm:"size:32" json:"arch"` - CPU int `json:"cpu"` - Memory int64 `json:"memory"` // MB - Disk int64 `json:"disk"` // GB - Status string `gorm:"size:32;default:stopped" json:"status"` // running, stopped, maintenance, error - CPUUsage float64 `json:"cpu_usage"` - MemoryUsage float64 `json:"memory_usage"` - DiskUsage float64 `json:"disk_usage"` - Uptime int64 `json:"uptime"` // seconds - AgentVersion string `gorm:"size:32" json:"agent_version"` - AgentStatus string `gorm:"size:32;default:offline" json:"agent_status"` // online, offline - HasAuth bool `gorm:"-" json:"has_auth"` // 是否有认证凭据(密码或SSH密钥) — 不存DB,handler 计算后返回 // Logan.Li - RegionID *uint `json:"region_id"` - CloudID *uint `json:"cloud_id"` - Tags string `gorm:"type:text" json:"tags"` // JSON - Description string `gorm:"size:500" json:"description"` - LastCheckAt *time.Time `json:"last_check_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Hostname string `gorm:"size:128;not null" json:"hostname"` + Alias string `gorm:"size:128" json:"alias"` // 人工可读别名,展示优先级高于 hostname // Logan.Li + CloudInstanceName string `gorm:"size:128" json:"cloud_instance_name"` // 云平台实例名称,低于人工别名高于 hostname 展示 // Logan.Li + CloudInstanceID string `gorm:"size:128;index" json:"cloud_instance_id"` // 云平台实例 ID,仅做关联/去重,不作为首选展示名 // Logan.Li + IP string `gorm:"size:64;not null" json:"ip"` + PublicIP string `gorm:"size:64" json:"public_ip"` + Port int `gorm:"default:22" json:"port"` + Username string `gorm:"size:64;default:root" json:"username"` + Password string `gorm:"size:255" json:"-"` + SSHKey string `gorm:"type:text" json:"-"` + AuthType string `gorm:"size:32;default:password" json:"auth_type"` // password, key + OSType string `gorm:"size:32;default:linux" json:"os_type"` // 操作系统类型:linux/windows // Logan.Li + Protocol string `gorm:"size:32;default:ssh" json:"protocol"` // 远程协议:ssh/rdp // Logan.Li + RDPPort int `gorm:"default:3389" json:"rdp_port"` // Windows RDP 端口 // Logan.Li + OS string `gorm:"size:64" json:"os"` + Kernel string `gorm:"size:64" json:"kernel"` + OSVersion string `gorm:"size:64" json:"os_version"` + Arch string `gorm:"size:32" json:"arch"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` // MB + Disk int64 `json:"disk"` // GB + Status string `gorm:"size:32;default:stopped" json:"status"` // running, stopped, maintenance, error + CPUUsage float64 `json:"cpu_usage"` + MemoryUsage float64 `json:"memory_usage"` + DiskUsage float64 `json:"disk_usage"` + Uptime int64 `json:"uptime"` // seconds + AgentVersion string `gorm:"size:32" json:"agent_version"` + AgentStatus string `gorm:"size:32;default:offline" json:"agent_status"` // online, offline + HasAuth bool `gorm:"-" json:"has_auth"` // 是否有认证凭据(密码或SSH密钥) — 不存DB,handler 计算后返回 // Logan.Li + RegionID *uint `json:"region_id"` + CloudID *uint `json:"cloud_id"` + Tags string `gorm:"type:text" json:"tags"` // JSON + Description string `gorm:"size:500" json:"description"` + LastCheckAt *time.Time `json:"last_check_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Region *Region `json:"region,omitempty"` @@ -66,7 +69,7 @@ type Cloud struct { // TableName 表名 func (Cloud) TableName() string { - return "asset_clouds" + return "asset_cloud_platforms" } // CloudAccount 云账号模型 @@ -106,7 +109,7 @@ type Region struct { // TableName 表名 func (Region) TableName() string { - return "asset_regions" + return "asset_cloud_regions" } // Domain 域名模型 @@ -131,52 +134,58 @@ func (Domain) TableName() string { // CreateServerRequest 创建服务器请求 type CreateServerRequest struct { - Hostname string `json:"hostname" binding:"required"` - IP string `json:"ip" binding:"required,ip"` - PublicIP string `json:"public_ip" binding:"omitempty,ip"` - Port int `json:"port" binding:"omitempty,min=1,max=65535"` - Username string `json:"username"` - Password string `json:"password"` - SSHKey string `json:"ssh_key"` - AuthType string `json:"auth_type" binding:"omitempty,oneof=password key"` - CredentialID *uint `json:"credential_id,omitempty"` // 关联 SSH 凭证,非空则后端解密覆盖认证字段 - OSType string `json:"os_type" binding:"omitempty,oneof=linux windows"` // 操作系统类型 // Logan.Li - Protocol string `json:"protocol" binding:"omitempty,oneof=ssh rdp"` // 远程协议 // Logan.Li - RDPPort int `json:"rdp_port" binding:"omitempty,min=1,max=65535"` // RDP 端口 // Logan.Li - OS string `json:"os"` - CPU int `json:"cpu"` - Memory int64 `json:"memory"` - Disk int64 `json:"disk"` - Status string `json:"status" binding:"omitempty,oneof=running stopped maintenance error"` - RegionID *uint `json:"region_id"` - CloudID *uint `json:"cloud_id"` - Tags string `json:"tags"` - Description string `json:"description"` + Hostname string `json:"hostname" binding:"required"` + Alias string `json:"alias"` + CloudInstanceName string `json:"cloud_instance_name"` + CloudInstanceID string `json:"cloud_instance_id"` + IP string `json:"ip" binding:"required,ip"` + PublicIP string `json:"public_ip" binding:"omitempty,ip"` + Port int `json:"port" binding:"omitempty,min=1,max=65535"` + Username string `json:"username"` + Password string `json:"password"` + SSHKey string `json:"ssh_key"` + AuthType string `json:"auth_type" binding:"omitempty,oneof=password key"` + CredentialID *uint `json:"credential_id,omitempty"` // 关联 SSH 凭证,非空则后端解密覆盖认证字段 + OSType string `json:"os_type" binding:"omitempty,oneof=linux windows"` // 操作系统类型 // Logan.Li + Protocol string `json:"protocol" binding:"omitempty,oneof=ssh rdp"` // 远程协议 // Logan.Li + RDPPort int `json:"rdp_port" binding:"omitempty,min=1,max=65535"` // RDP 端口 // Logan.Li + OS string `json:"os"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + Disk int64 `json:"disk"` + Status string `json:"status" binding:"omitempty,oneof=running stopped maintenance error"` + RegionID *uint `json:"region_id"` + CloudID *uint `json:"cloud_id"` + Tags string `json:"tags"` + Description string `json:"description"` } // UpdateServerRequest 更新服务器请求 type UpdateServerRequest struct { - Hostname string `json:"hostname"` - IP string `json:"ip" binding:"omitempty,ip"` - PublicIP string `json:"public_ip" binding:"omitempty,ip"` - Port int `json:"port" binding:"omitempty,min=1,max=65535"` - Username string `json:"username"` - Password string `json:"password"` - SSHKey string `json:"ssh_key"` - AuthType string `json:"auth_type" binding:"omitempty,oneof=password key"` - CredentialID *uint `json:"credential_id,omitempty"` // 关联 SSH 凭证,非空则后端解密覆盖认证字段 - OSType string `json:"os_type" binding:"omitempty,oneof=linux windows"` // 操作系统类型 // Logan.Li - Protocol string `json:"protocol" binding:"omitempty,oneof=ssh rdp"` // 远程协议 // Logan.Li - RDPPort int `json:"rdp_port" binding:"omitempty,min=1,max=65535"` // RDP 端口 // Logan.Li - OS string `json:"os"` - CPU int `json:"cpu"` - Memory int64 `json:"memory"` - Disk int64 `json:"disk"` - Status string `json:"status" binding:"omitempty,oneof=running stopped maintenance error"` - RegionID *uint `json:"region_id"` - CloudID *uint `json:"cloud_id"` - Tags string `json:"tags"` - Description string `json:"description"` + Hostname string `json:"hostname"` + Alias *string `json:"alias"` + CloudInstanceName string `json:"cloud_instance_name"` + CloudInstanceID string `json:"cloud_instance_id"` + IP string `json:"ip" binding:"omitempty,ip"` + PublicIP string `json:"public_ip" binding:"omitempty,ip"` + Port int `json:"port" binding:"omitempty,min=1,max=65535"` + Username string `json:"username"` + Password string `json:"password"` + SSHKey string `json:"ssh_key"` + AuthType string `json:"auth_type" binding:"omitempty,oneof=password key"` + CredentialID *uint `json:"credential_id,omitempty"` // 关联 SSH 凭证,非空则后端解密覆盖认证字段 + OSType string `json:"os_type" binding:"omitempty,oneof=linux windows"` // 操作系统类型 // Logan.Li + Protocol string `json:"protocol" binding:"omitempty,oneof=ssh rdp"` // 远程协议 // Logan.Li + RDPPort int `json:"rdp_port" binding:"omitempty,min=1,max=65535"` // RDP 端口 // Logan.Li + OS string `json:"os"` + CPU int `json:"cpu"` + Memory int64 `json:"memory"` + Disk int64 `json:"disk"` + Status string `json:"status" binding:"omitempty,oneof=running stopped maintenance error"` + RegionID *uint `json:"region_id"` + CloudID *uint `json:"cloud_id"` + Tags string `json:"tags"` + Description string `json:"description"` } // BatchServerItem 批量导入单条服务器 // Logan.Li diff --git a/backend/model/session_command.go b/backend/model/session_command.go index 79f3363..bdc5b95 100644 --- a/backend/model/session_command.go +++ b/backend/model/session_command.go @@ -18,7 +18,7 @@ type SessionCommand struct { } func (SessionCommand) TableName() string { - return "sys_session_commands" + return "audit_session_commands" } // SessionCommandQuery 会话命令查询 // Logan.Li diff --git a/backend/model/session_command_item.go b/backend/model/session_command_item.go index af84e61..f3710de 100644 --- a/backend/model/session_command_item.go +++ b/backend/model/session_command_item.go @@ -4,4 +4,4 @@ package model type SessionCommandItem struct { SessionCommand SessionRecordSize int `gorm:"column:session_record_size" json:"session_record_size"` // 关联会话 record_size,0=无录制 // Logan.Li -} \ No newline at end of file +} diff --git a/backend/model/site.go b/backend/model/site.go index 44fd9fb..e761bb5 100644 --- a/backend/model/site.go +++ b/backend/model/site.go @@ -12,14 +12,14 @@ import ( type SiteTaskType string const ( - SiteTaskTypeHTTP SiteTaskType = "http" // HTTP/HTTPS Web 拨测 - SiteTaskTypeSSL SiteTaskType = "ssl" // SSL/TLS 证书拨测 - SiteTaskTypeTCP SiteTaskType = "tcp" // TCP拨测 - SiteTaskTypePing SiteTaskType = "ping" // ICMP Ping拨测 - SiteTaskTypeDNS SiteTaskType = "dns" // DNS拨测 - SiteTaskTypeUDP SiteTaskType = "udp" // UDP拨测 - SiteTaskTypeFTP SiteTaskType = "ftp" // FTP拨测 - SiteTaskTypeSMTP SiteTaskType = "smtp" // SMTP拨测 + SiteTaskTypeHTTP SiteTaskType = "http" // HTTP/HTTPS Web 拨测 + SiteTaskTypeSSL SiteTaskType = "ssl" // SSL/TLS 证书拨测 + SiteTaskTypeTCP SiteTaskType = "tcp" // TCP拨测 + SiteTaskTypePing SiteTaskType = "ping" // ICMP Ping拨测 + SiteTaskTypeDNS SiteTaskType = "dns" // DNS拨测 + SiteTaskTypeUDP SiteTaskType = "udp" // UDP拨测 + SiteTaskTypeFTP SiteTaskType = "ftp" // FTP拨测 + SiteTaskTypeSMTP SiteTaskType = "smtp" // SMTP拨测 SiteTaskTypeTraceroute SiteTaskType = "traceroute" // 路由追踪 ) @@ -27,9 +27,9 @@ const ( type SiteTaskMethod string const ( - SiteTaskMethodGET SiteTaskMethod = "GET" - SiteTaskMethodPOST SiteTaskMethod = "POST" - SiteTaskMethodHEAD SiteTaskMethod = "HEAD" + SiteTaskMethodGET SiteTaskMethod = "GET" + SiteTaskMethodPOST SiteTaskMethod = "POST" + SiteTaskMethodHEAD SiteTaskMethod = "HEAD" ) // SiteTaskStatus 拨测任务状态 @@ -44,37 +44,37 @@ const ( // SiteTask 站点拨测任务表 // 定义对网站、服务等进行主动拨测的任务配置 type SiteTask struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:任务名称" json:"name"` - URL string `gorm:"type:varchar(512);not null;comment:拨测URL/地址" json:"url"` - TaskType SiteTaskType `gorm:"type:varchar(16);not null;comment:拨测类型" json:"task_type"` - Method SiteTaskMethod `gorm:"type:varchar(8);default:'GET';comment:HTTP方法" json:"method"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:任务名称" json:"name"` + URL string `gorm:"type:varchar(512);not null;comment:拨测URL/地址" json:"url"` + TaskType SiteTaskType `gorm:"type:varchar(16);not null;comment:拨测类型" json:"task_type"` + Method SiteTaskMethod `gorm:"type:varchar(8);default:'GET';comment:HTTP方法" json:"method"` // HTTP 拨测配置 - Headers string `gorm:"type:text;comment:请求头JSON" json:"headers,omitempty"` - Body string `gorm:"type:text;comment:请求体" json:"body,omitempty"` - Timeout int `gorm:"default:10;comment:超时时间(秒)" json:"timeout"` - FollowRedirect bool `gorm:"default:true;comment:是否跟随重定向" json:"follow_redirect"` - VerifySSL bool `gorm:"default:true;comment:是否验证SSL证书" json:"verify_ssl"` + Headers string `gorm:"type:text;comment:请求头JSON" json:"headers,omitempty"` + Body string `gorm:"type:text;comment:请求体" json:"body,omitempty"` + Timeout int `gorm:"default:10;comment:超时时间(秒)" json:"timeout"` + FollowRedirect bool `gorm:"default:true;comment:是否跟随重定向" json:"follow_redirect"` + VerifySSL bool `gorm:"default:true;comment:是否验证SSL证书" json:"verify_ssl"` // 期望响应配置 ExpectedStatus int `gorm:"default:200;comment:期望状态码" json:"expected_status"` ExpectedContent string `gorm:"type:text;comment:期望响应内容(支持正则)" json:"expected_content,omitempty"` ExpectedLength int `gorm:"default:0;comment:期望响应长度(0表示不检查)" json:"expected_length"` // 执行配置 - Frequency int `gorm:"default:60;comment:拨测频率(秒)" json:"frequency"` - Nodes string `gorm:"type:varchar(256);comment:执行节点列表JSON" json:"nodes,omitempty"` + Frequency int `gorm:"default:60;comment:拨测频率(秒)" json:"frequency"` + Nodes string `gorm:"type:varchar(256);comment:执行节点列表JSON" json:"nodes,omitempty"` // DNS 配置 - DNSResolver string `gorm:"type:varchar(64);comment:DNS解析服务器" json:"dns_resolver,omitempty"` + DNSResolver string `gorm:"type:varchar(64);comment:DNS解析服务器" json:"dns_resolver,omitempty"` // 状态 - Status SiteTaskStatus `gorm:"type:varchar(16);default:'running';comment:任务状态" json:"status"` - LastResult string `gorm:"type:varchar(16);comment:最近结果: success/fail" json:"last_result,omitempty"` - LastCheckAt *time.Time `gorm:"comment:最近检查时间" json:"last_check_at,omitempty"` - SuccessRate float64 `gorm:"type:decimal(5,2);default:100.00;comment:成功率(%)" json:"success_rate"` - AvgResponse float64 `gorm:"comment:平均响应时间(ms)" json:"avg_response,omitempty"` - Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` - CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + Status SiteTaskStatus `gorm:"type:varchar(16);default:'running';comment:任务状态" json:"status"` + LastResult string `gorm:"type:varchar(16);comment:最近结果: success/fail" json:"last_result,omitempty"` + LastCheckAt *time.Time `gorm:"comment:最近检查时间" json:"last_check_at,omitempty"` + SuccessRate float64 `gorm:"type:decimal(5,2);default:100.00;comment:成功率(%)" json:"success_rate"` + AvgResponse float64 `gorm:"comment:平均响应时间(ms)" json:"avg_response,omitempty"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` // 关联关系 Results []SiteResult `gorm:"foreignKey:TaskID" json:"results,omitempty"` @@ -83,7 +83,7 @@ type SiteTask struct { // TableName 指定表名 func (SiteTask) TableName() string { - return "ops_site_tasks" + return "monitor_site_tasks" } // ============================================ @@ -117,16 +117,16 @@ type SiteResult struct { ResponseSize int `gorm:"comment:响应大小(bytes)" json:"response_size,omitempty"` ResponseBody string `gorm:"type:text;comment:响应内容摘要" json:"response_body,omitempty"` // 网络信息 - RemoteIP string `gorm:"type:varchar(64);comment:远端IP" json:"remote_ip,omitempty"` - LocalNode string `gorm:"type:varchar(64);comment:执行节点" json:"local_node,omitempty"` + RemoteIP string `gorm:"type:varchar(64);comment:远端IP" json:"remote_ip,omitempty"` + LocalNode string `gorm:"type:varchar(64);comment:执行节点" json:"local_node,omitempty"` // 错误信息 ErrorMessage string `gorm:"type:text;comment:错误信息" json:"error_message,omitempty"` ErrorDetail string `gorm:"type:text;comment:错误详情" json:"error_detail,omitempty"` // SSL 证书信息 - SSLCertExpiry *time.Time `gorm:"comment:SSL证书过期时间" json:"ssl_cert_expiry,omitempty"` - SSLCertIssuer string `gorm:"type:varchar(256);comment:SSL证书签发者" json:"ssl_cert_issuer,omitempty"` - SSLCertSubject string `gorm:"type:varchar(256);comment:SSL证书主题" json:"ssl_cert_subject,omitempty"` - SSLDaysRemaining int `gorm:"comment:SSL证书剩余天数" json:"ssl_days_remaining,omitempty"` + SSLCertExpiry *time.Time `gorm:"comment:SSL证书过期时间" json:"ssl_cert_expiry,omitempty"` + SSLCertIssuer string `gorm:"type:varchar(256);comment:SSL证书签发者" json:"ssl_cert_issuer,omitempty"` + SSLCertSubject string `gorm:"type:varchar(256);comment:SSL证书主题" json:"ssl_cert_subject,omitempty"` + SSLDaysRemaining int `gorm:"comment:SSL证书剩余天数" json:"ssl_days_remaining,omitempty"` // 检查内容匹配 ContentMatch bool `gorm:"comment:内容匹配结果" json:"content_match,omitempty"` // 时间戳 @@ -139,7 +139,7 @@ type SiteResult struct { // TableName 指定表名 func (SiteResult) TableName() string { - return "ops_site_results" + return "monitor_site_results" } // ============================================ @@ -167,31 +167,31 @@ const ( // SiteAlert 拨测告警表 // 记录拨测任务触发的告警事件 type SiteAlert struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - TaskID uint `gorm:"not null;index;comment:任务ID" json:"task_id"` - Severity SiteAlertSeverity `gorm:"type:varchar(16);not null;comment:告警级别" json:"severity"` - Status SiteAlertStatus `gorm:"type:varchar(16);default:'active';index;comment:告警状态" json:"status"` - Title string `gorm:"type:varchar(256);not null;comment:告警标题" json:"title"` - Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + TaskID uint `gorm:"not null;index;comment:任务ID" json:"task_id"` + Severity SiteAlertSeverity `gorm:"type:varchar(16);not null;comment:告警级别" json:"severity"` + Status SiteAlertStatus `gorm:"type:varchar(16);default:'active';index;comment:告警状态" json:"status"` + Title string `gorm:"type:varchar(256);not null;comment:告警标题" json:"title"` + Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"` // 触发条件 - TriggerType string `gorm:"type:varchar(32);comment:触发类型: timeout/status_error/response_time/ssl_expiry" json:"trigger_type"` - TriggerValue string `gorm:"type:varchar(128);comment:触发值" json:"trigger_value,omitempty"` - Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"` + TriggerType string `gorm:"type:varchar(32);comment:触发类型: timeout/status_error/response_time/ssl_expiry" json:"trigger_type"` + TriggerValue string `gorm:"type:varchar(128);comment:触发值" json:"trigger_value,omitempty"` + Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"` // 响应信息 - ResponseTime float64 `gorm:"comment:响应时间(ms)" json:"response_time,omitempty"` - HTTPStatus int `gorm:"comment:HTTP状态码" json:"http_status,omitempty"` - ErrorMessage string `gorm:"type:text;comment:错误信息" json:"error_message,omitempty"` + ResponseTime float64 `gorm:"comment:响应时间(ms)" json:"response_time,omitempty"` + HTTPStatus int `gorm:"comment:HTTP状态码" json:"http_status,omitempty"` + ErrorMessage string `gorm:"type:text;comment:错误信息" json:"error_message,omitempty"` // 状态变化 - StartedAt time.Time `gorm:"not null;comment:告警开始时间" json:"started_at"` - ResolvedAt *time.Time `gorm:"comment:告警恢复时间" json:"resolved_at,omitempty"` - Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"` - NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count"` - LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"` - Acknowledged bool `gorm:"default:false;comment:是否已确认" json:"acknowledged"` - AcknowledgedBy uint `gorm:"comment:确认人ID" json:"acknowledged_by,omitempty"` - AcknowledgedAt *time.Time `gorm:"comment:确认时间" json:"acknowledged_at,omitempty"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + StartedAt time.Time `gorm:"not null;comment:告警开始时间" json:"started_at"` + ResolvedAt *time.Time `gorm:"comment:告警恢复时间" json:"resolved_at,omitempty"` + Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"` + NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count"` + LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"` + Acknowledged bool `gorm:"default:false;comment:是否已确认" json:"acknowledged"` + AcknowledgedBy uint `gorm:"comment:确认人ID" json:"acknowledged_by,omitempty"` + AcknowledgedAt *time.Time `gorm:"comment:确认时间" json:"acknowledged_at,omitempty"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` // 关联关系 Task *SiteTask `gorm:"foreignKey:TaskID" json:"task,omitempty"` @@ -199,45 +199,45 @@ type SiteAlert struct { // TableName 指定表名 func (SiteAlert) TableName() string { - return "ops_site_alerts" + return "monitor_site_alerts" } // SiteTaskCreateRequest 拨测任务创建请求 type SiteTaskCreateRequest struct { - Name string `json:"name" binding:"required,max=128"` - URL string `json:"url" binding:"required,max=512,url"` - TaskType SiteTaskType `json:"task_type" binding:"required"` - Method SiteTaskMethod `json:"method"` - Headers string `json:"headers"` - Body string `json:"body"` - Timeout int `json:"timeout"` - FollowRedirect bool `json:"follow_redirect"` - VerifySSL bool `json:"verify_ssl"` - ExpectedStatus int `json:"expected_status"` - ExpectedContent string `json:"expected_content"` - ExpectedLength int `json:"expected_length"` - Frequency int `json:"frequency"` - Nodes []string `json:"nodes"` - DNSResolver string `json:"dns_resolver"` - Description string `json:"description"` + Name string `json:"name" binding:"required,max=128"` + URL string `json:"url" binding:"required,max=512,url"` + TaskType SiteTaskType `json:"task_type" binding:"required"` + Method SiteTaskMethod `json:"method"` + Headers string `json:"headers"` + Body string `json:"body"` + Timeout int `json:"timeout"` + FollowRedirect bool `json:"follow_redirect"` + VerifySSL bool `json:"verify_ssl"` + ExpectedStatus int `json:"expected_status"` + ExpectedContent string `json:"expected_content"` + ExpectedLength int `json:"expected_length"` + Frequency int `json:"frequency"` + Nodes []string `json:"nodes"` + DNSResolver string `json:"dns_resolver"` + Description string `json:"description"` } // SiteTaskUpdateRequest 拨测任务更新请求 type SiteTaskUpdateRequest struct { - Name string `json:"name" binding:"max=128"` - URL string `json:"url" binding:"max=512,url"` - Method SiteTaskMethod `json:"method"` - Headers string `json:"headers"` - Body string `json:"body"` - Timeout int `json:"timeout"` - FollowRedirect bool `json:"follow_redirect"` - VerifySSL bool `json:"verify_ssl"` - ExpectedStatus int `json:"expected_status"` - ExpectedContent string `json:"expected_content"` - Frequency int `json:"frequency"` - Nodes []string `json:"nodes"` - Status SiteTaskStatus `json:"status"` - Description string `json:"description"` + Name string `json:"name" binding:"max=128"` + URL string `json:"url" binding:"max=512,url"` + Method SiteTaskMethod `json:"method"` + Headers string `json:"headers"` + Body string `json:"body"` + Timeout int `json:"timeout"` + FollowRedirect bool `json:"follow_redirect"` + VerifySSL bool `json:"verify_ssl"` + ExpectedStatus int `json:"expected_status"` + ExpectedContent string `json:"expected_content"` + Frequency int `json:"frequency"` + Nodes []string `json:"nodes"` + Status SiteTaskStatus `json:"status"` + Description string `json:"description"` } // SiteTaskStatistics 拨测任务统计 diff --git a/backend/model/snmp.go b/backend/model/snmp.go index d8f2575..7a1a686 100644 --- a/backend/model/snmp.go +++ b/backend/model/snmp.go @@ -30,9 +30,9 @@ const ( type SNMPPrivProtocol string const ( - SNMPPrivDES SNMPPrivProtocol = "DES" // DES加密 - SNMPPrivAES SNMPPrivProtocol = "AES" // AES加密 - SNMPPrivAES256 SNMPPrivProtocol = "AES256" // AES256加密 + SNMPPrivDES SNMPPrivProtocol = "DES" // DES加密 + SNMPPrivAES SNMPPrivProtocol = "AES" // AES加密 + SNMPPrivAES256 SNMPPrivProtocol = "AES256" // AES256加密 ) // SNMPDeviceStatus SNMP设备状态 @@ -47,43 +47,43 @@ const ( // SNMPDevice SNMP设备表 // 存储通过SNMP协议监控的网络设备信息 type SNMPDevice struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:设备名称" json:"name"` - IP string `gorm:"type:varchar(64);not null;index;comment:设备IP地址" json:"ip"` - Port int `gorm:"default:161;comment:SNMP端口" json:"port"` - Version SNMPVersion `gorm:"type:varchar(4);default:'v2c';comment:SNMP版本: v1/v2c/v3" json:"version"` - Community string `gorm:"type:varchar(64);comment:共同体字符串(v1/v2c)" json:"community,omitempty"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:设备名称" json:"name"` + IP string `gorm:"type:varchar(64);not null;index;comment:设备IP地址" json:"ip"` + Port int `gorm:"default:161;comment:SNMP端口" json:"port"` + Version SNMPVersion `gorm:"type:varchar(4);default:'v2c';comment:SNMP版本: v1/v2c/v3" json:"version"` + Community string `gorm:"type:varchar(64);comment:共同体字符串(v1/v2c)" json:"community,omitempty"` // SNMP v3 安全参数 - Username string `gorm:"type:varchar(64);comment:v3用户名" json:"username,omitempty"` - AuthProtocol SNMPAuthProtocol `gorm:"type:varchar(16);comment:v3认证协议" json:"auth_protocol,omitempty"` - AuthPassword string `gorm:"type:varchar(128);comment:v3认证密码" json:"auth_password,omitempty"` - PrivProtocol SNMPPrivProtocol `gorm:"type:varchar(16);comment:v3加密协议" json:"priv_protocol,omitempty"` - PrivPassword string `gorm:"type:varchar(128);comment:v3加密密码" json:"priv_password,omitempty"` - SecurityLevel string `gorm:"type:varchar(16);comment:v3安全级别: noAuthNoPriv/authNoPriv/authPriv" json:"security_level,omitempty"` + Username string `gorm:"type:varchar(64);comment:v3用户名" json:"username,omitempty"` + AuthProtocol SNMPAuthProtocol `gorm:"type:varchar(16);comment:v3认证协议" json:"auth_protocol,omitempty"` + AuthPassword string `gorm:"type:varchar(128);comment:v3认证密码" json:"auth_password,omitempty"` + PrivProtocol SNMPPrivProtocol `gorm:"type:varchar(16);comment:v3加密协议" json:"priv_protocol,omitempty"` + PrivPassword string `gorm:"type:varchar(128);comment:v3加密密码" json:"priv_password,omitempty"` + SecurityLevel string `gorm:"type:varchar(16);comment:v3安全级别: noAuthNoPriv/authNoPriv/authPriv" json:"security_level,omitempty"` // 设备信息 - DeviceType string `gorm:"type:varchar(64);comment:设备类型: switch/router/firewall/printer/server" json:"device_type"` - Vendor string `gorm:"type:varchar(64);comment:厂商" json:"vendor,omitempty"` - Model string `gorm:"type:varchar(64);comment:型号" json:"model,omitempty"` - SysDescr string `gorm:"type:text;comment:系统描述" json:"sys_descr,omitempty"` - SysObjectID string `gorm:"type:varchar(128);comment:系统OID" json:"sys_object_id,omitempty"` - SysUptime uint64 `gorm:"comment:系统运行时间(百分之一秒)" json:"sys_uptime,omitempty"` - SysContact string `gorm:"type:varchar(128);comment:系统联系人" json:"sys_contact,omitempty"` - SysName string `gorm:"type:varchar(128);comment:系统名称" json:"sys_name,omitempty"` - SysLocation string `gorm:"type:varchar(128);comment:系统位置" json:"sys_location,omitempty"` + DeviceType string `gorm:"type:varchar(64);comment:设备类型: switch/router/firewall/printer/server" json:"device_type"` + Vendor string `gorm:"type:varchar(64);comment:厂商" json:"vendor,omitempty"` + Model string `gorm:"type:varchar(64);comment:型号" json:"model,omitempty"` + SysDescr string `gorm:"type:text;comment:系统描述" json:"sys_descr,omitempty"` + SysObjectID string `gorm:"type:varchar(128);comment:系统OID" json:"sys_object_id,omitempty"` + SysUptime uint64 `gorm:"comment:系统运行时间(百分之一秒)" json:"sys_uptime,omitempty"` + SysContact string `gorm:"type:varchar(128);comment:系统联系人" json:"sys_contact,omitempty"` + SysName string `gorm:"type:varchar(128);comment:系统名称" json:"sys_name,omitempty"` + SysLocation string `gorm:"type:varchar(128);comment:系统位置" json:"sys_location,omitempty"` // 采集配置 - TemplateID uint `gorm:"index;comment:OID模板ID" json:"template_id,omitempty"` - CollectInterval int `gorm:"default:300;comment:采集间隔(秒)" json:"collect_interval"` - Timeout int `gorm:"default:10;comment:超时时间(秒)" json:"timeout"` - Retries int `gorm:"default:3;comment:重试次数" json:"retries"` - MaxRepetitions uint32 `gorm:"default:25;comment:BulkWalk最大重复数" json:"max_repetitions"` + TemplateID uint `gorm:"index;comment:OID模板ID" json:"template_id,omitempty"` + CollectInterval int `gorm:"default:300;comment:采集间隔(秒)" json:"collect_interval"` + Timeout int `gorm:"default:10;comment:超时时间(秒)" json:"timeout"` + Retries int `gorm:"default:3;comment:重试次数" json:"retries"` + MaxRepetitions uint32 `gorm:"default:25;comment:BulkWalk最大重复数" json:"max_repetitions"` // 状态 - Status SNMPDeviceStatus `gorm:"type:varchar(16);default:'offline';index;comment:状态" json:"status"` - LastCollectAt *time.Time `gorm:"comment:最后采集时间" json:"last_collect_at,omitempty"` - LastError string `gorm:"type:text;comment:最后错误信息" json:"last_error,omitempty"` - Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` - DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + Status SNMPDeviceStatus `gorm:"type:varchar(16);default:'offline';index;comment:状态" json:"status"` + LastCollectAt *time.Time `gorm:"comment:最后采集时间" json:"last_collect_at,omitempty"` + LastError string `gorm:"type:text;comment:最后错误信息" json:"last_error,omitempty"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` // 关联关系 Template *SNMPTemplate `gorm:"foreignKey:TemplateID" json:"template,omitempty"` @@ -92,7 +92,7 @@ type SNMPDevice struct { // TableName 指定表名 func (SNMPDevice) TableName() string { - return "ops_snmp_devices" + return "monitor_snmp_devices" } // ============================================ @@ -102,16 +102,16 @@ func (SNMPDevice) TableName() string { // SNMPTemplate OID模板表 // 预定义的OID模板,用于快速配置同类设备的监控 type SNMPTemplate struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"` - Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` - DeviceType string `gorm:"type:varchar(64);comment:适用设备类型" json:"device_type,omitempty"` - Vendor string `gorm:"type:varchar(64);comment:适用厂商" json:"vendor,omitempty"` - IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"` - IsSystem bool `gorm:"default:false;comment:是否系统模板" json:"is_system"` - Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + DeviceType string `gorm:"type:varchar(64);comment:适用设备类型" json:"device_type,omitempty"` + Vendor string `gorm:"type:varchar(64);comment:适用厂商" json:"vendor,omitempty"` + IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"` + IsSystem bool `gorm:"default:false;comment:是否系统模板" json:"is_system"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` // 关联关系 @@ -120,7 +120,7 @@ type SNMPTemplate struct { // TableName 指定表名 func (SNMPTemplate) TableName() string { - return "ops_snmp_templates" + return "monitor_snmp_templates" } // SNMPTemplateOID OID模板条目表 @@ -142,7 +142,7 @@ type SNMPTemplateOID struct { // TableName 指定表名 func (SNMPTemplateOID) TableName() string { - return "ops_snmp_template_oids" + return "monitor_snmp_template_oids" } // ============================================ @@ -170,7 +170,7 @@ type SNMPMetric struct { // TableName 指定表名 func (SNMPMetric) TableName() string { - return "ops_snmp_metrics" + return "monitor_snmp_samples" } // ============================================ @@ -180,26 +180,26 @@ func (SNMPMetric) TableName() string { // SNMPInterface SNMP接口信息表 // 存储网络设备的接口详细信息 type SNMPInterface struct { - ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` - DeviceID uint `gorm:"not null;index;comment:设备ID" json:"device_id"` - IfIndex int `gorm:"not null;comment:接口索引" json:"if_index"` - IfDescr string `gorm:"type:varchar(128);comment:接口描述" json:"if_descr,omitempty"` - IfName string `gorm:"type:varchar(64);comment:接口名称" json:"if_name,omitempty"` - IfAlias string `gorm:"type:varchar(128);comment:接口别名" json:"if_alias,omitempty"` - IfType int `gorm:"comment:接口类型" json:"if_type,omitempty"` - IfSpeed uint64 `gorm:"comment:接口速率(bps)" json:"if_speed,omitempty"` - IfPhysAddr string `gorm:"type:varchar(32);comment:物理地址(MAC)" json:"if_phys_addr,omitempty"` - IfAdminStatus int `gorm:"comment:管理状态: 1=up 2=down 3=testing" json:"if_admin_status,omitempty"` - IfOperStatus int `gorm:"comment:操作状态: 1=up 2=down 3=testing" json:"if_oper_status,omitempty"` - IfHighSpeed uint32 `gorm:"comment:高速接口速率(Mbps)" json:"if_high_speed,omitempty"` - Status int `gorm:"default:1;comment:状态" json:"status"` - CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` - UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + DeviceID uint `gorm:"not null;index;comment:设备ID" json:"device_id"` + IfIndex int `gorm:"not null;comment:接口索引" json:"if_index"` + IfDescr string `gorm:"type:varchar(128);comment:接口描述" json:"if_descr,omitempty"` + IfName string `gorm:"type:varchar(64);comment:接口名称" json:"if_name,omitempty"` + IfAlias string `gorm:"type:varchar(128);comment:接口别名" json:"if_alias,omitempty"` + IfType int `gorm:"comment:接口类型" json:"if_type,omitempty"` + IfSpeed uint64 `gorm:"comment:接口速率(bps)" json:"if_speed,omitempty"` + IfPhysAddr string `gorm:"type:varchar(32);comment:物理地址(MAC)" json:"if_phys_addr,omitempty"` + IfAdminStatus int `gorm:"comment:管理状态: 1=up 2=down 3=testing" json:"if_admin_status,omitempty"` + IfOperStatus int `gorm:"comment:操作状态: 1=up 2=down 3=testing" json:"if_oper_status,omitempty"` + IfHighSpeed uint32 `gorm:"comment:高速接口速率(Mbps)" json:"if_high_speed,omitempty"` + Status int `gorm:"default:1;comment:状态" json:"status"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` } // TableName 指定表名 func (SNMPInterface) TableName() string { - return "ops_snmp_interfaces" + return "monitor_snmp_interfaces" } // ============================================ diff --git a/backend/model/system_task.go b/backend/model/system_task.go index 4d52b5b..2d87825 100644 --- a/backend/model/system_task.go +++ b/backend/model/system_task.go @@ -13,4 +13,4 @@ type SystemTaskStatus struct { UpdatedAt time.Time `json:"updated_at"` } -func (SystemTaskStatus) TableName() string { return "sys_system_task_status" } \ No newline at end of file +func (SystemTaskStatus) TableName() string { return "sys_system_task_statuses" } diff --git a/backend/model/terminal_session.go b/backend/model/terminal_session.go index d17fb64..0e756dc 100644 --- a/backend/model/terminal_session.go +++ b/backend/model/terminal_session.go @@ -12,10 +12,10 @@ type TerminalSession struct { ServerIP string `gorm:"size:64" json:"server_ip"` ServerPort int `json:"server_port"` ClientIP string `gorm:"size:64" json:"client_ip"` - Status string `gorm:"size:32" json:"status"` // active, closed, failed - Duration int `json:"duration"` // seconds + Status string `gorm:"size:32" json:"status"` // active, closed, failed + Duration int `json:"duration"` // seconds RecordData string `gorm:"type:mediumtext" json:"-"` // 终端录制原始输出(asciicast v2 格式,max 16MB) - RecordSize int `json:"record_size"` // 录制字节数 + RecordSize int `json:"record_size"` // 录制字节数 StartTime time.Time `json:"start_time"` EndTime *time.Time `json:"end_time"` CreatedAt time.Time `json:"created_at"` @@ -23,18 +23,18 @@ type TerminalSession struct { // TableName 表名 func (TerminalSession) TableName() string { - return "sys_terminal_sessions" + return "audit_terminal_sessions" } // TerminalSessionQuery 终端会话查询 // Logan.Li type TerminalSessionQuery struct { - ServerID uint `form:"server_id"` - Username string `form:"username"` - Status string `form:"status"` - Keyword string `form:"keyword"` // 模糊匹配用户/主机名/IP/客户端IP - StartDate string `form:"start_date"` // YYYY-MM-DD - EndDate string `form:"end_date"` // YYYY-MM-DD - HasRecord string `form:"has_record"` // 1/true 仅含录制 - Page int `form:"page,default=1"` - PageSize int `form:"page_size,default=20"` + ServerID uint `form:"server_id"` + Username string `form:"username"` + Status string `form:"status"` + Keyword string `form:"keyword"` // 模糊匹配用户/主机名/IP/客户端IP + StartDate string `form:"start_date"` // YYYY-MM-DD + EndDate string `form:"end_date"` // YYYY-MM-DD + HasRecord string `form:"has_record"` // 1/true 仅含录制 + Page int `form:"page,default=1"` + PageSize int `form:"page_size,default=20"` } diff --git a/backend/model/user.go b/backend/model/user.go index 821cd32..d4ee6c0 100644 --- a/backend/model/user.go +++ b/backend/model/user.go @@ -2,22 +2,21 @@ package model import ( "time" - ) // User 用户模型 type User struct { - ID uint `gorm:"primarykey" json:"id"` - Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` - Password string `gorm:"size:255;not null" json:"-"` - Email string `gorm:"size:128" json:"email"` - Phone string `gorm:"size:32" json:"phone"` - RealName string `gorm:"size:64" json:"real_name"` - Avatar string `gorm:"size:255" json:"avatar"` - Status int `gorm:"default:1" json:"status"` // 0:禁用 1:启用 - IsAdmin bool `gorm:"default:false" json:"is_admin"` - AuthSource string `gorm:"size:20;default:'local'" json:"auth_source"` // local / ldap - LastLogin *time.Time `json:"last_login"` + ID uint `gorm:"primarykey" json:"id"` + Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` + Password string `gorm:"size:255;not null" json:"-"` + Email string `gorm:"size:128" json:"email"` + Phone string `gorm:"size:32" json:"phone"` + RealName string `gorm:"size:64" json:"real_name"` + Avatar string `gorm:"size:255" json:"avatar"` + Status int `gorm:"default:1" json:"status"` // 0:禁用 1:启用 + IsAdmin bool `gorm:"default:false" json:"is_admin"` + AuthSource string `gorm:"size:20;default:'local'" json:"auth_source"` // local / ldap + LastLogin *time.Time `json:"last_login"` // CI/CD Webhook 长期访问密钥(每用户仅一条,明文仅生成时返回一次)// Logan.Li ApiAccessKeyHash string `gorm:"size:255" json:"-"` ApiAccessKeyPrefix string `gorm:"size:32;index" json:"api_access_key_prefix,omitempty"` @@ -27,8 +26,8 @@ type User struct { ApiAccessKeyAt *time.Time `json:"api_access_key_at,omitempty"` ApiAccessKeyExpiresAt *time.Time `json:"api_access_key_expires_at,omitempty"` ApiAccessKeyLastUsedAt *time.Time `json:"api_access_key_last_used_at,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 Roles []Role `gorm:"many2many:iam_user_roles;" json:"roles,omitempty"` @@ -41,17 +40,17 @@ func (User) TableName() string { // Role 角色模型 type Role struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:64;uniqueIndex;not null" json:"name"` - Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` - Description string `gorm:"size:255" json:"description"` - Status int `gorm:"default:1" json:"status"` - IsSystem bool `gorm:"default:false" json:"is_system"` // 系统默认角色,禁止删除 - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:64;uniqueIndex;not null" json:"name"` + Code string `gorm:"size:64;uniqueIndex;not null" json:"code"` + Description string `gorm:"size:255" json:"description"` + Status int `gorm:"default:1" json:"status"` + IsSystem bool `gorm:"default:false" json:"is_system"` // 系统默认角色,禁止删除 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // 关联 - Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions,omitempty"` + Permissions []Permission `gorm:"many2many:iam_role_permissions;" json:"permissions,omitempty"` } // TableName 表名 @@ -61,17 +60,17 @@ func (Role) TableName() string { // Permission 权限模型 type Permission struct { - ID uint `gorm:"primarykey" json:"id"` - Name string `gorm:"size:64;not null" json:"name"` - Code string `gorm:"size:128;uniqueIndex;not null" json:"code"` - Type string `gorm:"size:32" json:"type"` // menu, button, api - Path string `gorm:"size:255" json:"path"` - Method string `gorm:"size:32" json:"method"` - ParentID *uint `json:"parent_id"` - Sort int `gorm:"default:0" json:"sort"` - Status int `gorm:"default:1" json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:64;not null" json:"name"` + Code string `gorm:"size:128;uniqueIndex;not null" json:"code"` + Type string `gorm:"size:32" json:"type"` // menu, button, api + Path string `gorm:"size:255" json:"path"` + Method string `gorm:"size:32" json:"method"` + ParentID *uint `json:"parent_id"` + Sort int `gorm:"default:0" json:"sort"` + Status int `gorm:"default:1" json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TableName 表名 @@ -147,9 +146,9 @@ type UpdateUserRequest struct { // CreateRoleRequest 创建角色请求 // Logan.Li type CreateRoleRequest struct { - Name string `json:"name" binding:"required,min=1,max=64"` - Code string `json:"code" binding:"required,min=1,max=64"` - Description string `json:"description"` + Name string `json:"name" binding:"required,min=1,max=64"` + Code string `json:"code" binding:"required,min=1,max=64"` + Description string `json:"description"` PermissionIDs []uint `json:"permission_ids"` } diff --git a/backend/pkg/agentcli/agentcli.go b/backend/pkg/agentcli/agentcli.go index 3b60d74..e00d894 100644 --- a/backend/pkg/agentcli/agentcli.go +++ b/backend/pkg/agentcli/agentcli.go @@ -96,6 +96,7 @@ func NewEngine(cfg EngineConfig) (*Engine, error) { return nil, fmt.Errorf("Kubernetes 模式缺少集群客户端") } kt := cfg.Kubernetes + registry.Register(tools.NewKubernetesDiagnoseTool(kt.Client, kt.Namespace)) registry.Register(tools.NewKubernetesGetTool(kt.Client, kt.Namespace)) registry.Register(tools.NewPodLogsTool(kt.Client, kt.Namespace, kt.PodName, kt.Container)) registry.Register(tools.NewPodExecuteTool(kt.Client, kt.Namespace, kt.PodName, kt.Container, coreCfg.ToolTimeout)) diff --git a/backend/pkg/agentcli/core/agent.go b/backend/pkg/agentcli/core/agent.go index 035d9e7..531a2ed 100644 --- a/backend/pkg/agentcli/core/agent.go +++ b/backend/pkg/agentcli/core/agent.go @@ -116,6 +116,7 @@ type Agent struct { evolver *evolver.EvolverEngine callback EventCallback totalTokens int + cache map[string]string mu sync.Mutex } @@ -130,7 +131,7 @@ func NewAgent(client llm.Client, registry *tools.Registry, cfg *Config, sc *safe if cfg.MaxContextTokens <= 0 { cfg.MaxContextTokens = 8000 } - return &Agent{llm: client, registry: registry, config: cfg, safetyCtl: sc, evolver: ev} + return &Agent{llm: client, registry: registry, config: cfg, safetyCtl: sc, evolver: ev, cache: make(map[string]string)} } // SetCallback 注入事件回调 // Logan.Li @@ -293,6 +294,14 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist continue } + if msg := a.suppressBroadKubernetesGet(tc.Function.Name, args, history); msg != "" { + a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: tools.RiskSafe.Display(), Summary: "工具执行中"}) + a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name, + Output: msg, Success: true, Duration: "0s", OutputSize: len(msg)}) + obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: msg}) + continue + } + risk := a.toolRisk(tool, args) a.emit(Event{Type: EventStatus, Phase: PhaseSafety, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "安全检查"}) @@ -315,6 +324,16 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist // 执行 a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "工具执行中"}) + if cached, ok := a.cachedToolResult(tc.Function.Name, args); ok { + a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name, + Output: cached, Success: true, Duration: "0s", OutputSize: len(cached)}) + records = append(records, evolver.ToolCallRecord{ + ToolName: tc.Function.Name, Args: args, Output: cached, + Duration: 0, Success: true, RiskLevel: int(risk), + }) + obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: cached}) + continue + } toolStart := time.Now() toolCtx, cancel := context.WithTimeout(ctx, a.config.ToolTimeout) result, execErr := tool.Execute(toolCtx, args) @@ -346,6 +365,7 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name, Output: output, Success: success, Duration: dur.Truncate(time.Millisecond).String(), OutputSize: len(output)}) + a.storeToolResult(tc.Function.Name, args, output, success) records = append(records, evolver.ToolCallRecord{ ToolName: tc.Function.Name, Args: args, Output: output, @@ -369,6 +389,82 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist return obs, records, failInc } +// cachedToolResult 返回同一任务内只读工具的缓存结果,避免 LLM 重复消耗 token。 // Logan.Li +func (a *Agent) cachedToolResult(toolName string, args map[string]interface{}) (string, bool) { + if !cacheableTool(toolName) { + return "", false + } + key := toolCacheKey(toolName, args) + if key == "" { + return "", false + } + if out, ok := a.cache[key]; ok { + return "已复用本轮任务缓存结果;不要重复查询同一对象,请基于首次结果总结,或改用更具体的 name/status_filter/pod 参数补查。\n\n" + out, true + } + return "", false +} + +// storeToolResult 缓存成功的只读工具输出。 // Logan.Li +func (a *Agent) storeToolResult(toolName string, args map[string]interface{}, output string, success bool) { + if !success || !cacheableTool(toolName) { + return + } + key := toolCacheKey(toolName, args) + if key == "" { + return + } + a.cache[key] = output +} + +func cacheableTool(toolName string) bool { + switch toolName { + case "kubernetes_diagnose", "kubernetes_get", "pod_logs", "aiops_asset_search", "aiops_alert_list", "aiops_metric_summary": + return true + default: + return false + } +} + +func toolCacheKey(toolName string, args map[string]interface{}) string { + b, err := json.Marshal(args) + if err != nil { + return "" + } + return toolName + ":" + string(b) +} + +// suppressBroadKubernetesGet 在聚合诊断后拦截全量 K8s 查询,迫使模型按异常对象下钻。 // Logan.Li +func (a *Agent) suppressBroadKubernetesGet(toolName string, args map[string]interface{}, history map[string]int) string { + if a.config.Mode != "kubernetes" && a.config.Mode != "k8s" { + return "" + } + if toolName != "kubernetes_get" || !hasDiagnoseHistory(history) { + return "" + } + resource := strings.ToLower(strings.TrimSpace(fmt.Sprint(args["resource"]))) + name := strings.TrimSpace(fmt.Sprint(args["name"])) + labelSelector := strings.TrimSpace(fmt.Sprint(args["label_selector"])) + statusFilter := strings.TrimSpace(fmt.Sprint(args["status_filter"])) + if name != "" && name != "" || labelSelector != "" && labelSelector != "" || statusFilter != "" && statusFilter != "" { + return "" + } + switch resource { + case "pods", "pod", "deployments", "deployment", "deploy", "services", "service", "svc", "nodes", "node", "events", "event": + return "已拦截全量 Kubernetes 查询:本轮已经执行过 kubernetes_diagnose,里面包含异常 Workload、异常 Pod、Top Events 和节点摘要。请不要再次拉全量资源;如需补证据,请按诊断结果中的具体对象调用 kubernetes_get(name/status_filter) 或 pod_logs(pod)。" + default: + return "" + } +} + +func hasDiagnoseHistory(history map[string]int) bool { + for key, count := range history { + if count > 0 && strings.HasPrefix(key, "kubernetes_diagnose:") { + return true + } + } + return false +} + // triggerEvolve 异步触发进化(带超时 + panic 恢复,绝不阻塞主循环) // Logan.Li func (a *Agent) triggerEvolve(ctx context.Context, query, serverHost, answer string, records []evolver.ToolCallRecord, success bool, start time.Time) { if a.evolver == nil { @@ -522,34 +618,46 @@ func (a *Agent) trimMessages(msgs []llm.Message) []llm.Message { return ensureToolMessageIntegrity(result) } -// ensureToolMessageIntegrity 确保 tool 消息前面有对应 assistant(tool_calls) // Logan.Li +// ensureToolMessageIntegrity 确保 assistant(tool_calls) 后紧跟完整 tool 结果组 // Logan.Li func ensureToolMessageIntegrity(msgs []llm.Message) []llm.Message { - validIDs := make(map[string]bool) - for _, m := range msgs { - if m.Role == "assistant" { - for _, tc := range m.ToolCalls { - validIDs[tc.ID] = true - } - } - } out := make([]llm.Message, 0, len(msgs)) - for i, m := range msgs { + for i := 0; i < len(msgs); i++ { + m := msgs[i] if m.Role == "tool" { - if !validIDs[m.ToolCallID] || i == 0 || msgs[i-1].Role != "assistant" { - continue - } - matched := false - for _, tc := range msgs[i-1].ToolCalls { - if tc.ID == m.ToolCallID { - matched = true - break - } - } - if !matched { - continue + continue + } + if m.Role != "assistant" || len(m.ToolCalls) == 0 { + out = append(out, m) + continue + } + + required := make(map[string]bool, len(m.ToolCalls)) + for _, tc := range m.ToolCalls { + if tc.ID != "" { + required[tc.ID] = true } } - out = append(out, m) + if len(required) == 0 { + continue + } + + seen := make(map[string]bool, len(required)) + tools := make([]llm.Message, 0, len(required)) + j := i + 1 + for ; j < len(msgs) && msgs[j].Role == "tool"; j++ { + tm := msgs[j] + if !required[tm.ToolCallID] || seen[tm.ToolCallID] { + continue + } + seen[tm.ToolCallID] = true + tools = append(tools, tm) + } + + if len(seen) == len(required) { + out = append(out, m) + out = append(out, tools...) + } + i = j - 1 } return out } diff --git a/backend/pkg/agentcli/core/agent_test.go b/backend/pkg/agentcli/core/agent_test.go new file mode 100644 index 0000000..01df16d --- /dev/null +++ b/backend/pkg/agentcli/core/agent_test.go @@ -0,0 +1,98 @@ +package core + +import ( + "testing" + + "ezops/pkg/agentcli/llm" +) + +func TestEnsureToolMessageIntegrityKeepsCompleteMultiToolGroup(t *testing.T) { + msgs := []llm.Message{ + {Role: "system", Content: "sys"}, + {Role: "user", Content: "查看异常副本"}, + {Role: "assistant", Content: "并行检查", ToolCalls: []llm.ToolCall{ + {ID: "call-1", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}}, + {ID: "call-2", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}}, + {ID: "call-3", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}}, + }}, + {Role: "tool", ToolCallID: "call-1", Name: "kubernetes_get", Content: "deployments"}, + {Role: "tool", ToolCallID: "call-2", Name: "kubernetes_get", Content: "statefulsets"}, + {Role: "tool", ToolCallID: "call-3", Name: "kubernetes_get", Content: "pods"}, + } + + got := ensureToolMessageIntegrity(msgs) + if len(got) != len(msgs) { + t.Fatalf("完整多工具结果组应全部保留,got=%d want=%d", len(got), len(msgs)) + } + for i := 3; i < len(got); i++ { + if got[i].Role != "tool" || got[i].ToolCallID == "" { + t.Fatalf("第 %d 条工具结果异常: %+v", i, got[i]) + } + } +} + +func TestEnsureToolMessageIntegrityDropsIncompleteToolGroup(t *testing.T) { + msgs := []llm.Message{ + {Role: "system", Content: "sys"}, + {Role: "assistant", Content: "并行检查", ToolCalls: []llm.ToolCall{ + {ID: "call-1", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}}, + {ID: "call-2", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}}, + }}, + {Role: "tool", ToolCallID: "call-1", Name: "kubernetes_get", Content: "deployments"}, + {Role: "user", Content: "继续"}, + } + + got := ensureToolMessageIntegrity(msgs) + if len(got) != 2 { + t.Fatalf("不完整工具结果组应整体丢弃,got=%d want=2: %+v", len(got), got) + } + if got[0].Role != "system" || got[1].Role != "user" { + t.Fatalf("剩余消息异常: %+v", got) + } +} + +func TestSuppressBroadKubernetesGetAfterDiagnose(t *testing.T) { + agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil) + history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx"}`: 1} + msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{"resource": "pods"}, history) + if msg == "" { + t.Fatal("expected broad pods query to be suppressed after diagnose") + } +} + +func TestSuppressBroadKubernetesGetBlocksAllPodAliasesAfterDiagnose(t *testing.T) { + agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil) + history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx","focus":"all"}`: 1} + for _, resource := range []string{"pods", "pod"} { + msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{"resource": resource}, history) + if msg == "" { + t.Fatalf("expected broad %s query to be suppressed after diagnose", resource) + } + } +} + +func TestSuppressBroadKubernetesGetAllowsFilteredQuery(t *testing.T) { + agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil) + history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx"}`: 1} + msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{ + "resource": "pods", "status_filter": "NotRunning", + }, history) + if msg != "" { + t.Fatalf("expected filtered pods query to pass, got %q", msg) + } +} + +func TestSuppressBroadKubernetesGetAllowsStatusFilteredPodsAfterDiagnose(t *testing.T) { + agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil) + history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx","focus":"pods"}`: 1} + cases := []map[string]interface{}{ + {"resource": "pods", "status_filter": "NotRunning"}, + {"resource": "pod", "status_filter": "CrashLoopBackOff"}, + } + for _, args := range cases { + msg := agent.suppressBroadKubernetesGet("kubernetes_get", args, history) + if msg != "" { + t.Fatalf("expected status_filter query to pass for args=%v, got %q", args, msg) + } + } +} diff --git a/backend/pkg/agentcli/guardrail/sanitize.go b/backend/pkg/agentcli/guardrail/sanitize.go new file mode 100644 index 0000000..9fe6089 --- /dev/null +++ b/backend/pkg/agentcli/guardrail/sanitize.go @@ -0,0 +1,108 @@ +// Package guardrail 提供 Agent CLI 输入输出治理能力。 // Logan.Li +package guardrail + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "regexp" + "strings" +) + +var sensitivePatterns = []*regexp.Regexp{ + regexp.MustCompile(`(?i)(bearer\s+)[a-z0-9._~+/=-]{16,}`), + regexp.MustCompile(`(?i)((?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|secret[_-]?key)\s*[:=]\s*)[^\s'"&]+`), + regexp.MustCompile(`(?i)((?:mysql|postgres|postgresql|redis|mongodb)://[^:\s/@]+:)[^@\s]+(@)`), + regexp.MustCompile(`(?is)-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----`), + regexp.MustCompile(`(?i)((?:Cookie|Set-Cookie):\s*)[^\r\n]+`), + regexp.MustCompile(`(?i)(AKIA[0-9A-Z]{16})`), +} + +var sensitiveKeys = map[string]bool{ + "password": true, "passwd": true, "pwd": true, "secret": true, + "token": true, "api_key": true, "apikey": true, "access_key": true, + "secret_key": true, "private_key": true, "cookie": true, +} + +// SanitizeText 脱敏工具输出、LLM 文本和错误信息,避免敏感信息进入前端/DB/记忆。 // Logan.Li +func SanitizeText(input string) string { + if input == "" { + return "" + } + out := input + for _, re := range sensitivePatterns { + out = re.ReplaceAllStringFunc(out, func(match string) string { + if strings.Contains(match, "PRIVATE KEY") { + return "[REDACTED_PRIVATE_KEY]" + } + sub := re.FindStringSubmatch(match) + if len(sub) >= 3 { + return sub[1] + "[REDACTED]" + sub[2] + } + if len(sub) >= 2 { + return sub[1] + "[REDACTED]" + } + return "[REDACTED]" + }) + } + return out +} + +// SanitizeJSON 脱敏 JSON 文本;非法 JSON 时退回文本脱敏。 // Logan.Li +func SanitizeJSON(raw string) string { + if strings.TrimSpace(raw) == "" { + return raw + } + var v interface{} + if err := json.Unmarshal([]byte(raw), &v); err != nil { + return SanitizeText(raw) + } + v = sanitizeValue(v) + b, err := json.Marshal(v) + if err != nil { + return SanitizeText(raw) + } + return string(b) +} + +func sanitizeValue(v interface{}) interface{} { + switch val := v.(type) { + case map[string]interface{}: + out := make(map[string]interface{}, len(val)) + for k, item := range val { + if sensitiveKeys[strings.ToLower(strings.ReplaceAll(k, "-", "_"))] { + out[k] = "[REDACTED]" + continue + } + out[k] = sanitizeValue(item) + } + return out + case []interface{}: + for i := range val { + val[i] = sanitizeValue(val[i]) + } + return val + case string: + return SanitizeText(val) + default: + return val + } +} + +// NewTraceID 生成短 trace id,便于串联一次 Agent 任务。 // Logan.Li +func NewTraceID() string { + return "tr-" + randomHex(12) +} + +// NewSpanID 生成短 span id,便于串联工具和消息事件。 // Logan.Li +func NewSpanID() string { + return "sp-" + randomHex(8) +} + +func randomHex(n int) string { + buf := make([]byte, n) + if _, err := rand.Read(buf); err != nil { + return "0000000000000000" + } + return hex.EncodeToString(buf) +} diff --git a/backend/pkg/agentcli/guardrail/sanitize_test.go b/backend/pkg/agentcli/guardrail/sanitize_test.go new file mode 100644 index 0000000..da3aaf6 --- /dev/null +++ b/backend/pkg/agentcli/guardrail/sanitize_test.go @@ -0,0 +1,31 @@ +package guardrail + +import ( + "strings" + "testing" +) + +func TestSanitizeTextMasksCommonSecrets(t *testing.T) { + input := "Authorization: Bearer abcdefghijklmnopqrstuvwxyz password=secret123 mysql://root:pass@db/prod" + got := SanitizeText(input) + for _, leak := range []string{"abcdefghijklmnopqrstuvwxyz", "secret123", "root:pass@db"} { + if strings.Contains(got, leak) { + t.Fatalf("敏感信息未脱敏: %s in %s", leak, got) + } + } + if !strings.Contains(got, "[REDACTED]") { + t.Fatalf("脱敏标记缺失: %s", got) + } +} + +func TestSanitizeJSONMasksSensitiveKeys(t *testing.T) { + got := SanitizeJSON(`{"command":"echo ok","password":"p1","nested":{"api_key":"k1"}}`) + for _, leak := range []string{"p1", "k1"} { + if strings.Contains(got, leak) { + t.Fatalf("JSON 敏感字段未脱敏: %s in %s", leak, got) + } + } + if !strings.Contains(got, `"command":"echo ok"`) { + t.Fatalf("非敏感字段不应被改写: %s", got) + } +} diff --git a/backend/pkg/agentcli/prompt/prompt.go b/backend/pkg/agentcli/prompt/prompt.go index 5752e40..e5b5182 100644 --- a/backend/pkg/agentcli/prompt/prompt.go +++ b/backend/pkg/agentcli/prompt/prompt.go @@ -51,9 +51,10 @@ func BuildSystemPrompt(mode, targetInfo, toolDescs, evolveContext string) string sb.WriteString("1. 破坏性命令(rm -rf /、mkfs、dd 覆写磁盘等)被严格禁止,绝不执行。\n") sb.WriteString("2. 高危操作(删除 Pod、重启 Deployment、扩缩容、换镜像等)需要用户确认后才能执行。\n") if mode == runModeKubernetes { - sb.WriteString("3. 排障时优先使用只读工具:kubernetes_get 查资源状态、pod_logs 看日志;确认根因后再 pod_execute 或 kubernetes_action。\n") - sb.WriteString("4. 容器内可能没有 systemctl、宿主机路径;不要假设节点级命令可用。\n") - sb.WriteString("5. 未选定 Pod 时,用 kubernetes_get 和 pod_logs(带 pod 参数)排障;选定 Pod 后可直接 pod_execute。\n") + sb.WriteString("3. 排障时优先使用只读工具;涉及异常副本、集群健康、Pod 异常、ImagePullBackOff、CrashLoopBackOff、探针失败时,第一步必须调用 kubernetes_diagnose。\n") + sb.WriteString("4. kubernetes_diagnose 已聚合 Workload/Pod/Event/Node,不要重复拉全量 pods/deployments/events;只针对异常对象补查 pod_logs 或 kubernetes_get name。\n") + sb.WriteString("5. 容器内可能没有 systemctl、宿主机路径;不要假设节点级命令可用。\n") + sb.WriteString("6. 未选定 Pod 时,用 kubernetes_diagnose 和必要的 pod_logs(带 pod 参数)排障;选定 Pod 后可直接 pod_execute。\n") } else { sb.WriteString("3. 排障时优先使用只读命令(df/ps/cat/grep/tail/systemctl status/journalctl)收集信息,确认根因后再动手修复。\n") sb.WriteString("4. 不要执行任何你不确定后果的命令;宁可多问用户,也不盲目操作。\n") @@ -70,7 +71,7 @@ func BuildSystemPrompt(mode, targetInfo, toolDescs, evolveContext string) string sb.WriteString("你采用「思考-行动-观察」循环解决任务:\n") sb.WriteString("1. 思考:分析当前状态,决定下一步。在回复中先用简短文字说明你的判断和计划。\n") if mode == runModeKubernetes { - sb.WriteString("2. 行动:优先 kubernetes_get / pod_logs;需要容器内信息时用 pod_execute;变更用 kubernetes_action(一次一步)。\n") + sb.WriteString("2. 行动:健康巡检/异常副本先用 kubernetes_diagnose;需要细节时对异常对象调用 kubernetes_get / pod_logs;需要容器内信息时用 pod_execute;变更用 kubernetes_action(一次一步)。\n") sb.WriteString("3. 观察:仔细阅读工具输出;pod_execute 输出含 [exit_code=0] 表示成功。\n") } else { sb.WriteString("2. 行动:调用 ssh_execute 执行命令(一次只做关键的、可验证的一步)。\n") @@ -98,4 +99,4 @@ func FormatToolDescriptions(items []struct{ Name, Desc string }) string { sb.WriteString(fmt.Sprintf("- %s: %s\n", it.Name, it.Desc)) } return sb.String() -} \ No newline at end of file +} diff --git a/backend/pkg/agentcli/tools/kubernetes_diagnose.go b/backend/pkg/agentcli/tools/kubernetes_diagnose.go new file mode 100644 index 0000000..9340e06 --- /dev/null +++ b/backend/pkg/agentcli/tools/kubernetes_diagnose.go @@ -0,0 +1,601 @@ +// kubernetes_diagnose.go - Kubernetes 异常副本与健康诊断聚合工具 // Logan.Li +package tools + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + k8sclient "ezops/pkg/kubernetesclient" +) + +const defaultDiagnoseLimit = 8 + +// KubernetesDiagnoseTool 聚合查询异常 Workload、Pod、Event 和节点健康,减少 LLM 反复拉全量资源。 // Logan.Li +type KubernetesDiagnoseTool struct { + client *k8sclient.Client + namespace string +} + +// NewKubernetesDiagnoseTool 创建 Kubernetes 聚合诊断工具。 // Logan.Li +func NewKubernetesDiagnoseTool(client *k8sclient.Client, namespace string) *KubernetesDiagnoseTool { + return &KubernetesDiagnoseTool{client: client, namespace: namespace} +} + +func (t *KubernetesDiagnoseTool) Name() string { return "kubernetes_diagnose" } + +func (t *KubernetesDiagnoseTool) Description() string { + return "一键诊断 Kubernetes 命名空间健康和异常副本(只读)。优先用于“异常副本、集群健康、Pod 异常、Deployment 不可用、ImagePullBackOff、CrashLoopBackOff”等任务。" + + "工具会聚合 Deployment/StatefulSet/DaemonSet/Pod/Event/Node,只返回异常摘要、可能根因和建议补充检查,避免反复查询全量资源。" +} + +func (t *KubernetesDiagnoseTool) Parameters() map[string]interface{} { + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "namespace": map[string]interface{}{ + "type": "string", + "description": "命名空间;空则使用会话绑定命名空间", + }, + "focus": map[string]interface{}{ + "type": "string", + "description": "关注点:replicas|pods|events|image|crash|probe|all,默认 all", + }, + "limit": map[string]interface{}{ + "type": "integer", + "description": "每类最多返回数量,默认 8,最大 20", + }, + }, + } +} + +func (t *KubernetesDiagnoseTool) RiskLevel() RiskLevel { return RiskSafe } + +func (t *KubernetesDiagnoseTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) { + ns := strings.TrimSpace(StringParam(args, "namespace")) + if ns == "" { + ns = t.namespace + } + if ns == "" { + return &Result{Success: false, Error: "namespace 不能为空"}, nil + } + limit := IntParam(args, "limit") + if limit <= 0 { + limit = defaultDiagnoseLimit + } + if limit > 20 { + limit = 20 + } + focus := strings.ToLower(strings.TrimSpace(StringParam(args, "focus"))) + if focus == "" { + focus = "all" + } + + report, err := t.buildReport(ns, focus, limit) + if err != nil { + return &Result{Success: false, Error: err.Error(), Output: err.Error()}, nil + } + b, err := json.MarshalIndent(report, "", " ") + if err != nil { + return nil, fmt.Errorf("诊断结果 JSON 序列化失败: %w", err) + } + return &Result{Success: true, Output: string(b)}, nil +} + +type k8sDiagnoseReport struct { + Namespace string `json:"namespace"` + Focus string `json:"focus"` + Summary k8sDiagnoseSummary `json:"summary"` + AbnormalWorkloads []k8sWorkloadFinding `json:"abnormal_workloads"` + AbnormalPods []k8sPodFinding `json:"abnormal_pods"` + TopEvents []k8sEventFinding `json:"top_events"` + NodeHealth []k8sNodeFinding `json:"node_health,omitempty"` + LikelyRootCauses []string `json:"likely_root_causes"` + RecommendedActions []string `json:"recommended_actions"` + NextTools []map[string]interface{} `json:"next_tools"` +} + +type k8sDiagnoseSummary struct { + Deployments int `json:"deployments"` + StatefulSets int `json:"statefulsets"` + DaemonSets int `json:"daemonsets"` + Pods int `json:"pods"` + AbnormalWorkloads int `json:"abnormal_workloads"` + AbnormalPods int `json:"abnormal_pods"` + WarningEvents int `json:"warning_events"` + NotReadyNodes int `json:"not_ready_nodes"` +} + +type k8sWorkloadFinding struct { + Kind string `json:"kind"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Desired int `json:"desired"` + Ready int `json:"ready"` + Available int `json:"available,omitempty"` + Reason string `json:"reason"` + Image string `json:"image,omitempty"` +} + +type k8sPodFinding struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Status string `json:"status"` + Reason string `json:"reason"` + Restarts int `json:"restarts"` + Node string `json:"node"` + IP string `json:"ip,omitempty"` + Image string `json:"image,omitempty"` + Owner string `json:"owner,omitempty"` + Messages []string `json:"messages,omitempty"` +} + +type k8sEventFinding struct { + Type string `json:"type"` + Reason string `json:"reason"` + Object string `json:"object"` + Count int32 `json:"count"` + LastSeen string `json:"last_seen,omitempty"` + Message string `json:"message"` +} + +type k8sNodeFinding struct { + Name string `json:"name"` + Ready string `json:"ready"` + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` +} + +func (t *KubernetesDiagnoseTool) buildReport(ns, focus string, limit int) (*k8sDiagnoseReport, error) { + pods, err := t.client.ListPods() + if err != nil { + return nil, fmt.Errorf("获取 Pod 失败: %w", err) + } + deployments, err := t.client.ListDeployments() + if err != nil { + return nil, fmt.Errorf("获取 Deployment 失败: %w", err) + } + statefulsets, err := t.client.ListStatefulSets() + if err != nil { + return nil, fmt.Errorf("获取 StatefulSet 失败: %w", err) + } + daemonsets, err := t.client.ListDaemonSets() + if err != nil { + return nil, fmt.Errorf("获取 DaemonSet 失败: %w", err) + } + events, err := t.client.ListEvents(ns) + if err != nil { + return nil, fmt.Errorf("获取 Event 失败: %w", err) + } + nodes, _ := t.client.ListNodes() + + report := &k8sDiagnoseReport{Namespace: ns, Focus: focus} + report.AbnormalWorkloads = diagnoseWorkloads(ns, deployments.Items, statefulsets.Items, daemonsets.Items) + report.AbnormalPods = diagnosePods(ns, pods.Items, deployments.Items) + report.TopEvents = diagnoseEvents(events.Items, limit) + report.NodeHealth = diagnoseNodes(nodes) + report.Summary = k8sDiagnoseSummary{ + Deployments: countDeployments(ns, deployments.Items), StatefulSets: countStatefulSets(ns, statefulsets.Items), + DaemonSets: countDaemonSets(ns, daemonsets.Items), Pods: countPods(ns, pods.Items), + AbnormalWorkloads: len(report.AbnormalWorkloads), AbnormalPods: len(report.AbnormalPods), + WarningEvents: countWarningEvents(events.Items), NotReadyNodes: len(report.NodeHealth), + } + report.LikelyRootCauses = inferK8sRootCauses(report.AbnormalPods, report.TopEvents) + report.RecommendedActions = recommendK8sActions(report.AbnormalPods, report.AbnormalWorkloads) + report.NextTools = recommendK8sNextTools(report.AbnormalPods, report.TopEvents, limit) + report.AbnormalWorkloads = limitWorkloadFindings(report.AbnormalWorkloads, limit) + report.AbnormalPods = limitPodFindings(report.AbnormalPods, limit) + return report, nil +} + +func diagnoseWorkloads(ns string, deployments []k8sclient.DeploymentItem, statefulsets []k8sclient.StatefulSetItem, daemonsets []k8sclient.DaemonSetItem) []k8sWorkloadFinding { + var out []k8sWorkloadFinding + for _, d := range deployments { + if d.Metadata.Namespace != ns || d.Spec.Replicas == 0 { + continue + } + if d.Status.ReadyReplicas < d.Spec.Replicas || d.Status.AvailableReplicas < d.Spec.Replicas { + out = append(out, k8sWorkloadFinding{ + Kind: "Deployment", Namespace: d.Metadata.Namespace, Name: d.Metadata.Name, + Desired: d.Spec.Replicas, Ready: d.Status.ReadyReplicas, Available: d.Status.AvailableReplicas, + Reason: "ready/available replicas less than desired", Image: firstDeploymentImage(d), + }) + } + } + for _, s := range statefulsets { + if s.Metadata.Namespace != ns || s.Spec.Replicas == 0 { + continue + } + if s.Status.ReadyReplicas < s.Spec.Replicas { + out = append(out, k8sWorkloadFinding{ + Kind: "StatefulSet", Namespace: s.Metadata.Namespace, Name: s.Metadata.Name, + Desired: s.Spec.Replicas, Ready: s.Status.ReadyReplicas, + Reason: "ready replicas less than desired", Image: firstStatefulSetImage(s), + }) + } + } + for _, d := range daemonsets { + if d.Metadata.Namespace != ns || d.Status.DesiredNumberScheduled == 0 { + continue + } + if d.Status.NumberReady < d.Status.DesiredNumberScheduled { + out = append(out, k8sWorkloadFinding{ + Kind: "DaemonSet", Namespace: d.Metadata.Namespace, Name: d.Metadata.Name, + Desired: d.Status.DesiredNumberScheduled, Ready: d.Status.NumberReady, + Reason: "ready pods less than desired scheduled pods", Image: firstDaemonSetImage(d), + }) + } + } + sort.Slice(out, func(i, j int) bool { + if out[i].Kind == out[j].Kind { + return out[i].Name < out[j].Name + } + return out[i].Kind < out[j].Kind + }) + return out +} + +func diagnosePods(ns string, pods []k8sclient.PodItem, deployments []k8sclient.DeploymentItem) []k8sPodFinding { + var out []k8sPodFinding + for _, p := range pods { + if p.Metadata.Namespace != ns { + continue + } + status, reason, restarts, messages := podDiagnosisStatus(p) + if !isAbnormalPod(status, reason, restarts) { + continue + } + out = append(out, k8sPodFinding{ + Namespace: p.Metadata.Namespace, Name: p.Metadata.Name, Status: status, Reason: reason, + Restarts: restarts, Node: p.Spec.NodeName, IP: p.Status.PodIP, Image: firstPodImage(p), + Owner: inferDeploymentOwner(p, deployments), Messages: messages, + }) + } + sort.Slice(out, func(i, j int) bool { + if out[i].Reason == out[j].Reason { + return out[i].Name < out[j].Name + } + return podReasonRank(out[i].Reason) < podReasonRank(out[j].Reason) + }) + return out +} + +func podDiagnosisStatus(p k8sclient.PodItem) (string, string, int, []string) { + status := p.Status.Phase + reason := "" + restarts := 0 + var messages []string + for _, cs := range p.Status.ContainerStatuses { + if cs.RestartCount > restarts { + restarts = cs.RestartCount + } + if cs.State.Waiting != nil && cs.State.Waiting.Reason != "" { + status = cs.State.Waiting.Reason + reason = cs.State.Waiting.Reason + if cs.State.Waiting.Message != "" { + messages = append(messages, truncateK8sText(cs.State.Waiting.Message, 220)) + } + } + if cs.LastState.Terminated != nil && cs.LastState.Terminated.Reason != "" { + if reason == "" { + reason = cs.LastState.Terminated.Reason + } + msg := fmt.Sprintf("last terminated: %s exit=%d", cs.LastState.Terminated.Reason, cs.LastState.Terminated.ExitCode) + messages = append(messages, msg) + } + } + for _, c := range p.Status.Conditions { + if c.Type == "Ready" && c.Status != "True" { + if reason == "" && c.Reason != "" { + reason = c.Reason + } + if c.Message != "" { + messages = append(messages, truncateK8sText(c.Message, 220)) + } + } + } + if reason == "" { + reason = status + } + return status, reason, restarts, dedupeStrings(messages) +} + +func diagnoseEvents(events []k8sclient.EventItem, limit int) []k8sEventFinding { + sort.Slice(events, func(i, j int) bool { + return eventTime(events[i]).After(eventTime(events[j])) + }) + var out []k8sEventFinding + seen := make(map[string]bool) + for _, e := range events { + if e.Type != "Warning" && !isImportantEventReason(e.Reason) { + continue + } + key := fmt.Sprintf("%s/%s/%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Reason, e.Message) + if seen[key] { + continue + } + seen[key] = true + out = append(out, k8sEventFinding{ + Type: e.Type, Reason: e.Reason, + Object: fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name), + Count: e.Count, LastSeen: eventTime(e).Format(time.RFC3339), + Message: truncateK8sText(e.Message, 260), + }) + if len(out) >= limit { + break + } + } + return out +} + +func diagnoseNodes(nodes *k8sclient.NodeList) []k8sNodeFinding { + if nodes == nil { + return nil + } + var out []k8sNodeFinding + for _, n := range nodes.Items { + for _, c := range n.Status.Conditions { + if c.Type == "Ready" && c.Status != "True" { + out = append(out, k8sNodeFinding{Name: n.Metadata.Name, Ready: c.Status, Reason: c.Reason, Message: truncateK8sText(c.Message, 220)}) + break + } + } + } + return out +} + +func inferK8sRootCauses(pods []k8sPodFinding, events []k8sEventFinding) []string { + flags := make(map[string]bool) + for _, p := range pods { + r := strings.ToLower(p.Reason + " " + p.Status) + switch { + case strings.Contains(r, "imagepull") || strings.Contains(r, "errimagepull"): + flags["存在镜像拉取失败,优先检查镜像 tag、Harbor 仓库、imagePullSecrets 和节点到仓库网络。"] = true + case strings.Contains(r, "crashloop"): + flags["存在容器反复崩溃,优先查看 previous logs、启动参数、依赖连接和最近变更。"] = true + case strings.Contains(r, "oom"): + flags["存在 OOMKilled 风险,优先检查内存 limit、最近内存趋势和应用内存泄漏。"] = true + case strings.Contains(r, "containersnotready") || strings.Contains(r, "unready"): + flags["存在容器未就绪,需结合 readiness/liveness probe、事件和应用日志确认。"] = true + case strings.Contains(r, "pending") || strings.Contains(r, "failedscheduling"): + flags["存在调度失败或 Pending,优先检查节点资源、污点、亲和性和 PVC。"] = true + } + } + for _, e := range events { + msg := strings.ToLower(e.Reason + " " + e.Message) + if strings.Contains(msg, "liveness probe") || strings.Contains(msg, "readiness probe") { + flags["事件显示健康探针失败,需检查探针路径、超时时间、应用响应耗时和依赖服务。"] = true + } + } + return sortedMapKeys(flags) +} + +func recommendK8sActions(pods []k8sPodFinding, workloads []k8sWorkloadFinding) []string { + var out []string + if len(workloads) > 0 { + out = append(out, "先处理 ready/available 副本不足的 Workload,再回查副本是否恢复。") + } + if hasPodReason(pods, "ImagePullBackOff") || hasPodReason(pods, "ErrImagePull") { + out = append(out, "对镜像拉取失败的 Pod,确认镜像 tag 是否存在、仓库权限是否有效、节点能否访问镜像仓库。") + } + if hasPodReason(pods, "CrashLoopBackOff") { + out = append(out, "对 CrashLoopBackOff 的 Pod,补查 previous logs 和启动命令,避免只看当前空日志。") + } + if hasPodReason(pods, "ContainersNotReady") { + out = append(out, "对未就绪 Pod,查看事件中的 readiness/liveness probe 失败原因和应用健康端点耗时。") + } + if len(out) == 0 { + out = append(out, "当前聚合诊断未发现明显异常;如仍有业务问题,继续按服务名查看日志、Ingress、Service selector 和指标。") + } + return out +} + +func recommendK8sNextTools(pods []k8sPodFinding, events []k8sEventFinding, limit int) []map[string]interface{} { + var out []map[string]interface{} + for _, p := range pods { + if len(out) >= limit { + break + } + if strings.Contains(strings.ToLower(p.Reason), "imagepull") { + out = append(out, map[string]interface{}{"tool": "kubernetes_get", "args": map[string]interface{}{"resource": "events", "name": p.Name}}) + continue + } + if p.Status == "CrashLoopBackOff" || p.Restarts >= 5 || strings.Contains(strings.ToLower(p.Reason), "notready") { + out = append(out, map[string]interface{}{"tool": "pod_logs", "args": map[string]interface{}{"namespace": p.Namespace, "pod": p.Name, "tail": 120}}) + } + } + if len(out) == 0 && len(events) > 0 { + out = append(out, map[string]interface{}{"tool": "kubernetes_get", "args": map[string]interface{}{"resource": "events"}}) + } + return out +} + +func isAbnormalPod(status, reason string, restarts int) bool { + s := strings.ToLower(status + " " + reason) + if status != "Running" && status != "Succeeded" && status != "PodCompleted" && status != "Completed" { + return true + } + if strings.Contains(s, "notready") || strings.Contains(s, "crashloop") || strings.Contains(s, "imagepull") || strings.Contains(s, "oom") { + return true + } + return restarts >= 10 +} + +func podReasonRank(reason string) int { + r := strings.ToLower(reason) + switch { + case strings.Contains(r, "imagepull"): + return 1 + case strings.Contains(r, "crashloop"): + return 2 + case strings.Contains(r, "oom"): + return 3 + case strings.Contains(r, "notready"): + return 4 + default: + return 9 + } +} + +func inferDeploymentOwner(p k8sclient.PodItem, deployments []k8sclient.DeploymentItem) string { + for _, d := range deployments { + if d.Metadata.Namespace != p.Metadata.Namespace || d.Spec.Selector == nil { + continue + } + if labelsMatch(p.Metadata.Labels, d.Spec.Selector.MatchLabels) { + return "Deployment/" + d.Metadata.Name + } + } + return "" +} + +func firstDeploymentImage(d k8sclient.DeploymentItem) string { + if len(d.Spec.Template.Spec.Containers) == 0 { + return "" + } + return d.Spec.Template.Spec.Containers[0].Image +} + +func firstStatefulSetImage(s k8sclient.StatefulSetItem) string { + if len(s.Spec.Template.Spec.Containers) == 0 { + return "" + } + return s.Spec.Template.Spec.Containers[0].Image +} + +func firstDaemonSetImage(d k8sclient.DaemonSetItem) string { + if len(d.Spec.Template.Spec.Containers) == 0 { + return "" + } + return d.Spec.Template.Spec.Containers[0].Image +} + +func firstPodImage(p k8sclient.PodItem) string { + if len(p.Spec.Containers) == 0 { + return "" + } + return p.Spec.Containers[0].Image +} + +func eventTime(e k8sclient.EventItem) time.Time { + if !e.LastTimestamp.IsZero() { + return e.LastTimestamp + } + if !e.EventTime.IsZero() { + return e.EventTime + } + return e.FirstTimestamp +} + +func isImportantEventReason(reason string) bool { + r := strings.ToLower(reason) + return strings.Contains(r, "failed") || strings.Contains(r, "backoff") || strings.Contains(r, "unhealthy") +} + +func countDeployments(ns string, items []k8sclient.DeploymentItem) int { + n := 0 + for _, it := range items { + if it.Metadata.Namespace == ns { + n++ + } + } + return n +} + +func countStatefulSets(ns string, items []k8sclient.StatefulSetItem) int { + n := 0 + for _, it := range items { + if it.Metadata.Namespace == ns { + n++ + } + } + return n +} + +func countDaemonSets(ns string, items []k8sclient.DaemonSetItem) int { + n := 0 + for _, it := range items { + if it.Metadata.Namespace == ns { + n++ + } + } + return n +} + +func countPods(ns string, items []k8sclient.PodItem) int { + n := 0 + for _, it := range items { + if it.Metadata.Namespace == ns { + n++ + } + } + return n +} + +func countWarningEvents(items []k8sclient.EventItem) int { + n := 0 + for _, it := range items { + if it.Type == "Warning" { + n++ + } + } + return n +} + +func hasPodReason(pods []k8sPodFinding, reason string) bool { + needle := strings.ToLower(reason) + for _, p := range pods { + if strings.Contains(strings.ToLower(p.Reason+" "+p.Status), needle) { + return true + } + } + return false +} + +func limitWorkloadFindings(items []k8sWorkloadFinding, limit int) []k8sWorkloadFinding { + if len(items) <= limit { + return items + } + return items[:limit] +} + +func limitPodFindings(items []k8sPodFinding, limit int) []k8sPodFinding { + if len(items) <= limit { + return items + } + return items[:limit] +} + +func truncateK8sText(s string, max int) string { + s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " ")) + if len([]rune(s)) <= max { + return s + } + r := []rune(s) + return string(r[:max]) + "..." +} + +func dedupeStrings(items []string) []string { + seen := make(map[string]bool) + var out []string + for _, item := range items { + if item == "" || seen[item] { + continue + } + seen[item] = true + out = append(out, item) + } + return out +} + +func sortedMapKeys(m map[string]bool) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} diff --git a/backend/pkg/agentcli/tools/kubernetes_diagnose_test.go b/backend/pkg/agentcli/tools/kubernetes_diagnose_test.go new file mode 100644 index 0000000..92f0435 --- /dev/null +++ b/backend/pkg/agentcli/tools/kubernetes_diagnose_test.go @@ -0,0 +1,64 @@ +package tools + +import ( + "testing" + + k8sclient "ezops/pkg/kubernetesclient" +) + +func TestPodDiagnosisStatusDetectsWaitingReason(t *testing.T) { + pod := k8sclient.PodItem{ + Status: k8sclient.PodStatus{ + Phase: "Pending", + ContainerStatuses: []k8sclient.ContainerStatus{{ + Name: "app", + State: k8sclient.ContainerStatusState{Waiting: &k8sclient.ContainerStateWaiting{ + Reason: "ImagePullBackOff", Message: "Back-off pulling image", + }}, + }}, + }, + } + status, reason, restarts, messages := podDiagnosisStatus(pod) + if status != "ImagePullBackOff" || reason != "ImagePullBackOff" { + t.Fatalf("unexpected status=%s reason=%s", status, reason) + } + if restarts != 0 { + t.Fatalf("unexpected restarts=%d", restarts) + } + if len(messages) != 1 { + t.Fatalf("expected waiting message, got %v", messages) + } + if !isAbnormalPod(status, reason, restarts) { + t.Fatal("ImagePullBackOff should be abnormal") + } +} + +func TestPodDiagnosisStatusDetectsOOMKilled(t *testing.T) { + pod := k8sclient.PodItem{ + Status: k8sclient.PodStatus{ + Phase: "Running", + ContainerStatuses: []k8sclient.ContainerStatus{{ + Name: "app", Ready: true, RestartCount: 3, + LastState: k8sclient.ContainerStatusState{Terminated: &k8sclient.ContainerStateTerminated{ + Reason: "OOMKilled", ExitCode: 137, + }}, + }}, + }, + } + status, reason, restarts, _ := podDiagnosisStatus(pod) + if status != "Running" || reason != "OOMKilled" || restarts != 3 { + t.Fatalf("unexpected status=%s reason=%s restarts=%d", status, reason, restarts) + } + if !isAbnormalPod(status, reason, restarts) { + t.Fatal("OOMKilled should be abnormal") + } +} + +func TestIsAbnormalPodByRestartThreshold(t *testing.T) { + if !isAbnormalPod("Running", "Running", 10) { + t.Fatal("high restart pod should be abnormal") + } + if isAbnormalPod("Running", "Running", 2) { + t.Fatal("healthy running pod should not be abnormal") + } +} diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index 5550580..48ffec3 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -96,7 +96,6 @@ func AutoMigrate(db *gorm.DB) error { &model.AlertNotifyPolicy{}, // 告警集合通知策略 // Logan.Li &model.AlertNotifySettings{}, // 告警消息模版 // Logan.Li &model.NotifyChannel{}, - &model.MetricData{}, &model.Pipeline{}, &model.PipelineRun{}, &model.BuildNode{}, @@ -121,11 +120,10 @@ func AutoMigrate(db *gorm.DB) error { &model.AgentTask{}, &model.AgentRule{}, &model.AgentAlert{}, + &model.AgentPortPolicy{}, // Agent 端口策略 // Logan.Li &model.AnsiblePlaybook{}, &model.AnsibleJob{}, &model.AnsibleInventory{}, - &model.LogSource{}, - &model.LogEntry{}, &model.TopologyLayout{}, // 拓扑布局保存 // Logan.Li &model.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li &model.RDPSession{}, // RDP 图形会话审计 // Logan.Li @@ -136,7 +134,6 @@ func AutoMigrate(db *gorm.DB) error { &model.MetricsAggregate{}, // 指标时序聚合(Redis → MySQL)// Logan.Li &model.MonitorTarget{}, // 监控目标(主机/中间件/设备/站点) // Logan.Li &model.MonitorMetric{}, // 监控指标定义 // Logan.Li - &model.MetricValue{}, // 指标当前值 // Logan.Li &model.MetricHistory{}, // 指标历史时序 // Logan.Li &model.HostGroup{}, // 主机组 // Logan.Li &model.SNMPDevice{}, // SNMP设备 // Logan.Li @@ -152,8 +149,6 @@ func AutoMigrate(db *gorm.DB) error { &model.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li &model.LogDataSource{}, // Loki / ES 日志数据源 // Logan.Li &model.Credential{}, // 凭据管理(SSH密钥/API Token) // Logan.Li - &model.PipelineRepo{}, // Git 仓库认证 // Logan.Li - &model.PipelineRegistry{}, // 镜像仓库认证 // Logan.Li &model.DeployEnvironment{}, // 发布环境分组 // Logan.Li &model.DeployPipelineMember{}, // 流水线成员 // Logan.Li &model.DeployUserGroup{}, // 用户组 // Logan.Li @@ -172,9 +167,13 @@ func AutoMigrate(db *gorm.DB) error { &model.HealthProbe{}, // 健康探针配置 // Logan.Li &model.ApprovalWorkflow{}, // 审批流程定义 // Logan.Li &model.ApprovalWorkflowStep{}, // 审批流程步骤 // Logan.Li - &model.AgentCLISession{}, // Agent CLI 任务会话 // Logan.Li - &model.AgentCLIMessage{}, // Agent CLI 推理消息 // Logan.Li - &model.CustomDashboard{}, // 自定义监控仪表盘 // Logan.Li + &model.AIRun{}, // AI 任务统一运行记录 // Logan.Li + &model.AIRunMessage{}, // AI 任务回放消息 // Logan.Li + &model.AIRunSpan{}, // AI 工具/LLM/Guardrail Trace Span // Logan.Li + &model.AICapability{}, // AI 内置/MCP/技能能力注册 // Logan.Li + &model.AICapabilityPolicy{}, // AI 工具能力策略 // Logan.Li + &model.OpsSession{}, // 人机统一执行会话 // Logan.Li + &model.OpsCommand{}, // 人机统一命令审计 // Logan.Li &model.TraceDataSource{}, // 链路追踪数据源 Jaeger/Tempo // Logan.Li } @@ -231,8 +230,10 @@ func applySchemaPatches(db *gorm.DB) { } } } - ensureTable(&model.LogScopeRole{}, "monitor_log_scope_roles") - ensureTable(&model.UserLogScopeRole{}, "monitor_user_log_scope_roles") + ensureTable(&model.LogScopeRole{}, "iam_log_scope_roles") + if !m.HasTable("iam_user_log_scope_roles") { + createUserLogScopeRolesTable(db) + } // LDAP 目录类型列(openldap | ad) // Logan.Li if m.HasTable(&model.LDAPConfig{}) && !m.HasColumn(&model.LDAPConfig{}, "server_type") { @@ -252,6 +253,9 @@ func applySchemaPatches(db *gorm.DB) { patchServerColumn("OSType", "os_type") patchServerColumn("Protocol", "protocol") patchServerColumn("RDPPort", "rdp_port") + patchServerColumn("Alias", "alias") + patchServerColumn("CloudInstanceName", "cloud_instance_name") + patchServerColumn("CloudInstanceID", "cloud_instance_id") // 将历史仅填 os=windows 的记录补齐协议字段 // Logan.Li if m.HasColumn(&model.Server{}, "os_type") && m.HasColumn(&model.Server{}, "protocol") { @@ -263,3 +267,26 @@ func applySchemaPatches(db *gorm.DB) { `).Error } } + +func createUserLogScopeRolesTable(db *gorm.DB) { + sql := `CREATE TABLE IF NOT EXISTS iam_user_log_scope_roles ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + log_scope_role_id BIGINT UNSIGNED NOT NULL, + created_at DATETIME(3) NULL, + UNIQUE KEY idx_iam_user_log_scope_role (user_id, log_scope_role_id), + KEY idx_iam_user_log_scope_role_role (log_scope_role_id) + )` + if db.Dialector.Name() == "sqlite" { + sql = `CREATE TABLE IF NOT EXISTS iam_user_log_scope_roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + log_scope_role_id INTEGER NOT NULL, + created_at DATETIME, + UNIQUE(user_id, log_scope_role_id) + )` + } + if err := db.Exec(sql).Error; err != nil { + log.Printf("[DB] 创建 iam_user_log_scope_roles 失败: %v", err) + } +} diff --git a/backend/pkg/database/dialect.go b/backend/pkg/database/dialect.go index e022072..7627a88 100644 --- a/backend/pkg/database/dialect.go +++ b/backend/pkg/database/dialect.go @@ -51,13 +51,13 @@ func CleanupSNMPMetricsSQL(driver string, retentionDays int) string { switch driver { case "sqlite": return fmt.Sprintf( - "DELETE FROM ops_snmp_metrics WHERE timestamp < CAST(strftime('%%s', 'now', '-%d days') AS INTEGER)", + "DELETE FROM monitor_snmp_samples WHERE timestamp < CAST(strftime('%%s', 'now', '-%d days') AS INTEGER)", retentionDays, ) default: return fmt.Sprintf( - "DELETE FROM ops_snmp_metrics WHERE timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL %d DAY))", + "DELETE FROM monitor_snmp_samples WHERE timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL %d DAY))", retentionDays, ) } -} \ No newline at end of file +} diff --git a/backend/pkg/kubernetesclient/client.go b/backend/pkg/kubernetesclient/client.go index 02f61b4..143be4d 100644 --- a/backend/pkg/kubernetesclient/client.go +++ b/backend/pkg/kubernetesclient/client.go @@ -203,16 +203,22 @@ func (c *Client) get(path string, result interface{}) error { // getRaw performs a GET request returning raw bytes with custom Accept header func (c *Client) getRaw(path string, accept string) ([]byte, error) { req, err := http.NewRequest("GET", c.BaseURL+path, nil) - if err != nil { return nil, err } + if err != nil { + return nil, err + } req.Header.Set("Accept", accept) if c.BearerToken != "" { req.Header.Set("Authorization", "Bearer "+c.BearerToken) } resp, err := c.HTTPClient.Do(req) - if err != nil { return nil, fmt.Errorf("GET %s: %w", path, err) } + if err != nil { + return nil, fmt.Errorf("GET %s: %w", path, err) + } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) - if err != nil { return nil, fmt.Errorf("read body: %w", err) } + if err != nil { + return nil, fmt.Errorf("read body: %w", err) + } if resp.StatusCode >= 400 { return nil, fmt.Errorf("K8s API error %d: %s", resp.StatusCode, string(body[:min(len(body), 200)])) } @@ -222,13 +228,17 @@ func (c *Client) getRaw(path string, accept string) ([]byte, error) { // putRaw performs a PUT request with raw body and custom Content-Type func (c *Client) putRaw(path string, contentType string, data []byte) error { req, err := http.NewRequest("PUT", c.BaseURL+path, strings.NewReader(string(data))) - if err != nil { return err } + if err != nil { + return err + } req.Header.Set("Content-Type", contentType) if c.BearerToken != "" { req.Header.Set("Authorization", "Bearer "+c.BearerToken) } resp, err := c.HTTPClient.Do(req) - if err != nil { return fmt.Errorf("PUT %s: %w", path, err) } + if err != nil { + return fmt.Errorf("PUT %s: %w", path, err) + } defer resp.Body.Close() if resp.StatusCode >= 400 { body, _ := io.ReadAll(resp.Body) @@ -245,9 +255,9 @@ type NodeList struct { } type NodeItem struct { - Metadata ObjectMeta `json:"metadata"` - Status NodeStatus `json:"status"` - Spec NodeSpec `json:"spec"` + Metadata ObjectMeta `json:"metadata"` + Status NodeStatus `json:"status"` + Spec NodeSpec `json:"spec"` } type NodeSpec struct { @@ -263,11 +273,11 @@ type NodeTaint struct { } type NodeStatus struct { - Capacity ResourceList `json:"capacity"` - Allocatable ResourceList `json:"allocatable"` + Capacity ResourceList `json:"capacity"` + Allocatable ResourceList `json:"allocatable"` Conditions []NodeCondition `json:"conditions"` - NodeInfo NodeInfo `json:"nodeInfo"` - Addresses []NodeAddress `json:"addresses"` + NodeInfo NodeInfo `json:"nodeInfo"` + Addresses []NodeAddress `json:"addresses"` } type ResourceList map[string]string @@ -305,14 +315,14 @@ type PodItem struct { } type PodSpec struct { - NodeName string `json:"nodeName"` + NodeName string `json:"nodeName"` Containers []Container `json:"containers"` } type Container struct { - Name string `json:"name"` - Image string `json:"image"` - Resources ContainerRes `json:"resources"` + Name string `json:"name"` + Image string `json:"image"` + Resources ContainerRes `json:"resources"` } type ContainerRes struct { @@ -321,10 +331,10 @@ type ContainerRes struct { } type PodStatus struct { - Phase string `json:"phase"` - PodIP string `json:"podIP"` - ContainerStatuses []ContainerStatus `json:"containerStatuses"` - Conditions []PodCondition `json:"conditions,omitempty"` + Phase string `json:"phase"` + PodIP string `json:"podIP"` + ContainerStatuses []ContainerStatus `json:"containerStatuses"` + Conditions []PodCondition `json:"conditions,omitempty"` } // PodCondition for pod status conditions (Scheduled, Initialized, Ready etc) @@ -337,8 +347,30 @@ type PodCondition struct { } type ContainerStatus struct { - RestartCount int `json:"restartCount"` - Ready bool `json:"ready"` + Name string `json:"name"` + RestartCount int `json:"restartCount"` + Ready bool `json:"ready"` + State ContainerStatusState `json:"state,omitempty"` + LastState ContainerStatusState `json:"lastState,omitempty"` +} + +// ContainerStatusState 解析容器等待/终止状态,用于 AI 诊断 ImagePullBackOff、OOMKilled 等原因。 // Logan.Li +type ContainerStatusState struct { + Waiting *ContainerStateWaiting `json:"waiting,omitempty"` + Terminated *ContainerStateTerminated `json:"terminated,omitempty"` +} + +// ContainerStateWaiting 容器等待状态。 // Logan.Li +type ContainerStateWaiting struct { + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` +} + +// ContainerStateTerminated 容器终止状态。 // Logan.Li +type ContainerStateTerminated struct { + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` + ExitCode int `json:"exitCode,omitempty"` } // DeploymentList response @@ -380,8 +412,8 @@ type ServiceList struct { } type ServiceItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec ServiceSpec `json:"spec"` + Metadata ObjectMeta `json:"metadata"` + Spec ServiceSpec `json:"spec"` } type ServiceSpec struct { @@ -392,11 +424,11 @@ type ServiceSpec struct { } type ServicePort struct { - Name string `json:"name"` - Port int `json:"port"` + Name string `json:"name"` + Port int `json:"port"` TargetPort interface{} `json:"targetPort"` - NodePort int `json:"nodePort"` - Protocol string `json:"protocol"` + NodePort int `json:"nodePort"` + Protocol string `json:"protocol"` } // IngressList response @@ -405,13 +437,13 @@ type IngressList struct { } type IngressItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec IngressSpec `json:"spec"` + Metadata ObjectMeta `json:"metadata"` + Spec IngressSpec `json:"spec"` } type IngressSpec struct { - TLS []IngressTLS `json:"tls"` - Rules []IngressRule `json:"rules"` + TLS []IngressTLS `json:"tls"` + Rules []IngressRule `json:"rules"` } type IngressTLS struct { @@ -419,7 +451,7 @@ type IngressTLS struct { } type IngressRule struct { - Host string `json:"host"` + Host string `json:"host"` HTTP *IngressHTTP `json:"http,omitempty"` } @@ -428,8 +460,8 @@ type IngressHTTP struct { } type IngressPath struct { - Path string `json:"path"` - PathType string `json:"pathType"` + Path string `json:"path"` + PathType string `json:"pathType"` Backend IngressBackend `json:"backend"` } @@ -438,7 +470,7 @@ type IngressBackend struct { } type IngressBackendService struct { - Name string `json:"name"` + Name string `json:"name"` Port IngressServicePort `json:"port"` } @@ -461,20 +493,20 @@ type IngressClassItem struct { // ObjectMeta type ObjectMeta struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - CreationTimestamp string `json:"creationTimestamp"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + CreationTimestamp string `json:"creationTimestamp"` } // SyncResult holds sync counts type SyncResult struct { - Nodes int `json:"nodes"` - Pods int `json:"pods"` - Deployments int `json:"deployments"` - Services int `json:"services"` - Ingresses int `json:"ingresses"` + Nodes int `json:"nodes"` + Pods int `json:"pods"` + Deployments int `json:"deployments"` + Services int `json:"services"` + Ingresses int `json:"ingresses"` Errors []string `json:"errors,omitempty"` // 各资源同步错误,便于排查 // Logan.Li } @@ -812,8 +844,8 @@ func (c *Client) ExecInPod(namespace, podName, container string, command []strin url.PathEscape(namespace), url.PathEscape(podName), params.Encode()) dialer := websocket.Dialer{ - TLSClientConfig: c.TLSConfig, - Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "channel.k8s.io"}, + TLSClientConfig: c.TLSConfig, + Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "channel.k8s.io"}, HandshakeTimeout: 10 * time.Second, } header := http.Header{} @@ -863,9 +895,9 @@ type HPAList struct { // HPAItem represents a HorizontalPodAutoscaler type HPAItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec HPASpec `json:"spec"` - Status HPAStatus `json:"status"` + Metadata ObjectMeta `json:"metadata"` + Spec HPASpec `json:"spec"` + Status HPAStatus `json:"status"` } // HPASpec defines the HPA spec @@ -885,7 +917,7 @@ type ScaleTargetRef struct { // HPAMetric defines a metric for HPA type HPAMetric struct { - Type string `json:"type"` + Type string `json:"type"` Resource *struct { Name string `json:"name"` Target struct { @@ -914,12 +946,12 @@ func (c *Client) ListHPAs() (*HPAList, error) { type CreateHPARequest struct { Name string `json:"name"` Namespace string `json:"namespace"` - TargetKind string `json:"targetKind"` // Deployment, StatefulSet + TargetKind string `json:"targetKind"` // Deployment, StatefulSet TargetName string `json:"targetName"` MinReplicas int `json:"minReplicas"` MaxReplicas int `json:"maxReplicas"` - CPUPercent int `json:"cpuPercent"` // target average CPU utilization - MemPercent int `json:"memPercent"` // target average memory utilization (0 = disabled) + CPUPercent int `json:"cpuPercent"` // target average CPU utilization + MemPercent int `json:"memPercent"` // target average memory utilization (0 = disabled) } // CreateHPA creates a new HPA @@ -1004,9 +1036,9 @@ type CronJobList struct { // CronJobItem represents a CronJob type CronJobItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec CronJobSpec `json:"spec"` - Status CronJobStatus `json:"status"` + Metadata ObjectMeta `json:"metadata"` + Spec CronJobSpec `json:"spec"` + Status CronJobStatus `json:"status"` } // CronJobSpec defines the CronJob spec @@ -1042,10 +1074,10 @@ func (c *Client) ListCronJobs() (*CronJobList, error) { // CreateCronJobRequest is the payload for creating a CronJob type CreateCronJobRequest struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Schedule string `json:"schedule"` // cron expression - Image string `json:"image"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Schedule string `json:"schedule"` // cron expression + Image string `json:"image"` Command []string `json:"command"` } @@ -1145,9 +1177,9 @@ func (c *Client) ListNamespaces() ([]string, error) { // NamespaceItem holds full namespace metadata type NamespaceItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec NamespaceSpec `json:"spec"` - Status NamespaceStatus `json:"status"` + Metadata ObjectMeta `json:"metadata"` + Spec NamespaceSpec `json:"spec"` + Status NamespaceStatus `json:"status"` } // NamespaceSpec holds namespace spec @@ -1209,15 +1241,15 @@ func (c *Client) DeleteNamespace(name string) error { // ResourceQuotaItem represents a K8s ResourceQuota type ResourceQuotaItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec ResourceQuotaSpec `json:"spec"` - Status ResourceQuotaStatus `json:"status"` + Metadata ObjectMeta `json:"metadata"` + Spec ResourceQuotaSpec `json:"spec"` + Status ResourceQuotaStatus `json:"status"` } // ResourceQuotaSpec holds hard limits type ResourceQuotaSpec struct { - Hard map[string]string `json:"hard"` - Scopes []string `json:"scopes,omitempty"` + Hard map[string]string `json:"hard"` + Scopes []string `json:"scopes,omitempty"` } // ResourceQuotaStatus holds used/hard counts @@ -1245,8 +1277,8 @@ func (c *Client) ListResourceQuotas(namespace string) ([]ResourceQuotaItem, erro // LimitRangeItem represents a K8s LimitRange type LimitRangeItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec LimitRangeSpec `json:"spec"` + Metadata ObjectMeta `json:"metadata"` + Spec LimitRangeSpec `json:"spec"` } // LimitRangeSpec holds limit items @@ -1303,17 +1335,17 @@ type SecretList struct { // SecretItem represents a Secret (return keys only, not values) type SecretItem struct { - Metadata ObjectMeta `json:"metadata"` - Type string `json:"type"` - DataKeys []string `json:"-"` + Metadata ObjectMeta `json:"metadata"` + Type string `json:"type"` + DataKeys []string `json:"-"` } // ListSecrets fetches Secrets in a namespace (names+types only, no data) func (c *Client) ListSecrets(namespace string) (*SecretList, error) { var result struct { Items []struct { - Metadata ObjectMeta `json:"metadata"` - Type string `json:"type"` + Metadata ObjectMeta `json:"metadata"` + Type string `json:"type"` Data map[string]string `json:"data"` } `json:"items"` } @@ -1348,14 +1380,14 @@ type PVCItem struct { Metadata ObjectMeta `json:"metadata"` Spec PVCSpec `json:"spec"` Status struct { - Phase string `json:"phase"` - Capacity map[string]string `json:"capacity"` + Phase string `json:"phase"` + Capacity map[string]string `json:"capacity"` } `json:"status"` } type PVCSpec struct { - AccessModes []string `json:"accessModes"` - Resources struct { + AccessModes []string `json:"accessModes"` + Resources struct { Requests map[string]string `json:"requests"` } `json:"resources"` StorageClassName string `json:"storageClassName"` @@ -1386,16 +1418,16 @@ type StatefulSetList struct { } type StatefulSetItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec StatefulSetSpec `json:"spec"` + Metadata ObjectMeta `json:"metadata"` + Spec StatefulSetSpec `json:"spec"` Status struct { - Replicas int `json:"replicas"` - ReadyReplicas int `json:"readyReplicas"` + Replicas int `json:"replicas"` + ReadyReplicas int `json:"readyReplicas"` } `json:"status"` } type StatefulSetSpec struct { - Replicas int `json:"replicas"` + Replicas int `json:"replicas"` Template struct { Spec struct { Containers []struct { @@ -1424,8 +1456,8 @@ type DaemonSetList struct { } type DaemonSetItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec DaemonSetSpec `json:"spec"` + Metadata ObjectMeta `json:"metadata"` + Spec DaemonSetSpec `json:"spec"` Status struct { DesiredNumberScheduled int `json:"desiredNumberScheduled"` CurrentNumberScheduled int `json:"currentNumberScheduled"` @@ -1589,18 +1621,18 @@ type EventSource struct { } type EventItem struct { - Metadata ObjectMeta `json:"metadata"` - InvolvedObject InvolvedObject `json:"involvedObject"` - Type string `json:"type"` - Reason string `json:"reason"` - Message string `json:"message"` - Source EventSource `json:"source"` - ReportingComponent string `json:"reportingComponent"` - ReportingInstance string `json:"reportingInstance"` - Count int32 `json:"count"` - FirstTimestamp time.Time `json:"firstTimestamp"` - LastTimestamp time.Time `json:"lastTimestamp"` - EventTime time.Time `json:"eventTime"` + Metadata ObjectMeta `json:"metadata"` + InvolvedObject InvolvedObject `json:"involvedObject"` + Type string `json:"type"` + Reason string `json:"reason"` + Message string `json:"message"` + Source EventSource `json:"source"` + ReportingComponent string `json:"reportingComponent"` + ReportingInstance string `json:"reportingInstance"` + Count int32 `json:"count"` + FirstTimestamp time.Time `json:"firstTimestamp"` + LastTimestamp time.Time `json:"lastTimestamp"` + EventTime time.Time `json:"eventTime"` } type InvolvedObject struct { @@ -1673,7 +1705,7 @@ type RoleList struct { } type RoleItem struct { - Metadata ObjectMeta `json:"metadata"` + Metadata ObjectMeta `json:"metadata"` Rules []PolicyRule `json:"rules"` } @@ -1700,9 +1732,9 @@ type RoleBindingList struct { } type RoleBindingItem struct { - Metadata ObjectMeta `json:"metadata"` - RoleRef RoleRef `json:"roleRef"` - Subjects []Subject `json:"subjects"` + Metadata ObjectMeta `json:"metadata"` + RoleRef RoleRef `json:"roleRef"` + Subjects []Subject `json:"subjects"` } type RoleRef struct { @@ -1775,7 +1807,7 @@ func (c *Client) applyRaw(yamlContent string) (map[string]interface{}, error) { } // Try GET first — if 200, it exists, do PUT. If 404, do POST. - parentPath := apiPath[:strings.LastIndex(apiPath, "/")] // without /name suffix + parentPath := apiPath[:strings.LastIndex(apiPath, "/")] // without /name suffix _, getErr := c.getRaw(apiPath, "application/yaml") if getErr == nil { // Resource exists → PUT update @@ -1801,8 +1833,10 @@ func (c *Client) applyRaw(yamlContent string) (map[string]interface{}, error) { func (c *Client) resourceAPIPath(apiVersion, kind, namespace, name string) (string, bool) { resource := strings.ToLower(kind) + "s" switch kind { - case "Endpoints": resource = "endpoints" - case "PodMetrics": resource = "pods" + case "Endpoints": + resource = "endpoints" + case "PodMetrics": + resource = "pods" } var groupPrefix string @@ -1829,7 +1863,9 @@ func (c *Client) resourceAPIPath(apiVersion, kind, namespace, name string) (stri } ns := namespace - if ns == "" { ns = "default" } + if ns == "" { + ns = "default" + } if groupPrefix != "" { switch kind { @@ -1866,15 +1902,15 @@ type ReplicaSetList struct { // ReplicaSetItem 表示一个 ReplicaSet(含完整 spec/status/metadata,用于回滚取 template) type ReplicaSetItem struct { - Metadata ObjectMeta `json:"metadata"` - Spec ReplicaSetSpec `json:"spec"` - Status ReplicaSetStatus `json:"status"` + Metadata ObjectMeta `json:"metadata"` + Spec ReplicaSetSpec `json:"spec"` + Status ReplicaSetStatus `json:"status"` } // ReplicaSetSpec 与 DeploymentSpec 结构一致,关键是 template type ReplicaSetSpec struct { - Replicas int `json:"replicas"` - Template ReplicaSetTemplate `json:"template"` + Replicas int `json:"replicas"` + Template ReplicaSetTemplate `json:"template"` } // ReplicaSetTemplate pod 模板 diff --git a/backend/pkg/logaccess/access.go b/backend/pkg/logaccess/access.go index e1524eb..feffb1f 100644 --- a/backend/pkg/logaccess/access.go +++ b/backend/pkg/logaccess/access.go @@ -27,9 +27,9 @@ var ( // GetUserLogScopeRoles 获取用户已启用的日志权限策略 // Logan.Li func GetUserLogScopeRoles(db *gorm.DB, userID uint) ([]model.LogScopeRole, error) { var roles []model.LogScopeRole - err := db.Table("monitor_log_scope_roles"). - Joins("JOIN monitor_user_log_scope_roles ON monitor_user_log_scope_roles.log_scope_role_id = monitor_log_scope_roles.id"). - Where("monitor_user_log_scope_roles.user_id = ? AND monitor_log_scope_roles.status = 1", userID). + err := db.Table("iam_log_scope_roles"). + Joins("JOIN iam_user_log_scope_roles ON iam_user_log_scope_roles.log_scope_role_id = iam_log_scope_roles.id"). + Where("iam_user_log_scope_roles.user_id = ? AND iam_log_scope_roles.status = 1", userID). Find(&roles).Error return roles, err } diff --git a/backend/router/routes.go b/backend/router/routes.go index 2e0024b..e1ab6c0 100644 --- a/backend/router/routes.go +++ b/backend/router/routes.go @@ -668,24 +668,6 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c releases.POST("/:id/rollback", pipelineHandler.RollbackRelease) } - // 仓库认证 & 镜像仓库认证路由 - deployHandler := controller.NewDeployHandler(db) - { - repos := authorized.Group("/deploy/repos") - repos.GET("", deployHandler.ListRepos) - repos.POST("", deployHandler.CreateRepo) - repos.GET("/:id", deployHandler.GetRepo) - repos.PUT("/:id", deployHandler.UpdateRepo) - repos.DELETE("/:id", deployHandler.DeleteRepo) - - registries := authorized.Group("/deploy/registries") - registries.GET("", deployHandler.ListRegistries) - registries.POST("", deployHandler.CreateRegistry) - registries.GET("/:id", deployHandler.GetRegistry) - registries.PUT("/:id", deployHandler.UpdateRegistry) - registries.DELETE("/:id", deployHandler.DeleteRegistry) - } - // 工具配置路由(发布配置 Tab ④)// Logan.Li toolConfigHandler := controller.NewToolConfigHandler(db, ti) tools := authorized.Group("/deploy/tools") @@ -1053,18 +1035,6 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c zabbixDSGroup.POST("/test", zabbixDSHandler.TestConnection) } - // 自定义监控仪表盘 — CRUD + 设默认 // Logan.Li - customDashHandler := controller.NewCustomDashboardHandler(db) - customDashGroup := authorized.Group("/custom-dashboards") - { - customDashGroup.GET("", customDashHandler.List) - customDashGroup.GET("/:id", customDashHandler.Get) - customDashGroup.POST("", customDashHandler.Create) - customDashGroup.PUT("/:id", customDashHandler.Update) - customDashGroup.DELETE("/:id", customDashHandler.Delete) - customDashGroup.POST("/:id/default", customDashHandler.SetDefault) - } - // 链路追踪数据源管理 — CRUD + 测试连接 + 设默认 // Logan.Li traceDSHandler := controller.NewTraceDSHandler(db) traceProxyHandler := controller.NewTraceProxyHandler(traceDSHandler) diff --git a/backend/seed/seed.go b/backend/seed/seed.go index b69f4cd..082da98 100644 --- a/backend/seed/seed.go +++ b/backend/seed/seed.go @@ -84,8 +84,8 @@ func SeedData(db *gorm.DB) { }`, FlowConfig: `[{"name":"直接主管","approver_role":"team_lead","step_type":"audit"},{"name":"运维负责人","approver_role":"ops_manager","step_type":"audit"},{"name":"运维执行","approver_role":"ops_manager","step_type":"execute"}]`, Enabled: true, - }, - { + }, + { Name: "应用部署申请", Code: "app_deploy", Description: "申请新应用部署,包含 Git 地址、端口、Nacos 等配置信息", @@ -108,7 +108,7 @@ func SeedData(db *gorm.DB) { }`, FlowConfig: `[{"name":"技术负责人","approver_role":"tech_lead","step_type":"audit"},{"name":"运维负责人","approver_role":"ops_manager","step_type":"audit"},{"name":"部署执行","approver_role":"ops_manager","step_type":"execute"}]`, Enabled: true, - }, + }, { Name: "权限申请", Code: "permission_request", diff --git a/backend/seed/snmp_seed.go b/backend/seed/snmp_seed.go index c9b27c8..693b8ee 100644 --- a/backend/seed/snmp_seed.go +++ b/backend/seed/snmp_seed.go @@ -1,8 +1,8 @@ package seed import ( - snmppkg "ezops/pkg/snmp" "ezops/model" + snmppkg "ezops/pkg/snmp" "go.uber.org/zap" "gorm.io/gorm" @@ -20,7 +20,7 @@ func SeedSNMPTemplates(db *gorm.DB) { Name: bt.Name, Description: bt.Description, DeviceType: bt.DeviceType, Vendor: bt.Vendor, IsDefault: bt.Vendor == "H3C" && bt.DeviceType == "switch", - IsSystem: true, Status: 1, + IsSystem: true, Status: 1, } if err := db.Create(&tpl).Error; err != nil { zap.L().Warn("seed snmp template failed", zap.String("name", bt.Name), zap.Error(err)) @@ -39,4 +39,4 @@ func SeedSNMPTemplates(db *gorm.DB) { } zap.L().Info("seeded snmp template", zap.String("name", bt.Name), zap.Int("oids", len(bt.OIDs))) } -} \ No newline at end of file +} diff --git a/backend/static/.gitkeep b/backend/static/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/static/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/db-table-audit.md b/docs/db-table-audit.md new file mode 100644 index 0000000..11a47c7 --- /dev/null +++ b/docs/db-table-audit.md @@ -0,0 +1,395 @@ +# 数据表与菜单域审计记录 + +> 审计范围:`backend/model`、`backend/pkg/database/database.go`、主要 controller/service/repository 引用、`backend/seed/seed.go` 默认菜单权限。 +> 目标:按菜单域整理表清单,记录表名前缀、设计合理性、疑似旧表/无用表和后续整改建议。 + +## 重建版目标设计 + +测试环境允许直接重建时,建议采用 **Schema v2 全局统一方案**:按菜单域和平台基础域重新归类,旧表不做兼容 rename,直接调整 `TableName()`、AutoMigrate 清单、种子数据和相关查询引用,然后清空数据库重建。 + +### 全局命名原则 + +1. 表名统一使用:`_`。 +2. 领域前缀与菜单域保持一致,避免同一功能散落在 `sys_`、`ops_`、`monitor_`。 +3. 关联表统一使用:`__` 或同域 `__`。 +4. 配置类表可用单数语义但表名仍用复数,例如 `deploy_build_master_configs`。 +5. 时序/日志大表必须明确后缀:`*_samples`、`*_histories`、`*_events`、`*_logs`。 +6. 不再新增无前缀表。 + +### 目标前缀 + +| 前缀 | 菜单/领域 | 说明 | +| --- | --- | --- | +| `iam_` | 身份与权限 | 用户、角色、权限、凭据、LDAP、资源级授权 | +| `sys_` | 系统基础配置 | 系统任务、站内导航、全局配置 | +| `audit_` | 安全审计 | 操作审计、登录日志、终端/RDP 会话、命令审计 | +| `asset_` | 资产/云资源 | 服务器、云账号、云区域、域名、账单、资源型号 | +| `k8s_` | Kubernetes | 集群与同步缓存资源 | +| `deploy_` | 应用发布 | 流水线、运行记录、发布、环境、构建节点、工具 | +| `monitor_` | 监控告警 | 告警、指标、Agent、SNMP、站点拨测、中间件、数据源 | +| `ops_` | 自动化运维 | 定时任务、脚本、远程执行、Ansible 如保留 | +| `approval_` | 资源申请/审批 | 模板、流程、实例、步骤 | +| `ai_` | AIOps/AI | LLM、会话、分析轨迹、Agent CLI 运行记录 | + +### 目标表清单 + +#### IAM / 系统基础 + +| 目标表 | 来源/处理 | +| --- | --- | +| `iam_users` | 保留 | +| `iam_roles` | 保留 | +| `iam_permissions` | 保留 | +| `iam_user_roles` | 保留 | +| `iam_role_permissions` | 替换 `role_permissions` | +| `iam_credentials` | 保留,作为全局凭据唯一主表 | +| `iam_ldap_configs` | 保留 | +| `iam_item_roles` | 保留 | +| `iam_user_item_roles` | 保留 | +| `iam_log_scope_roles` | 从 `monitor_log_scope_roles` 调整,日志查询权限本质是权限控制 | +| `iam_user_log_scope_roles` | 从 `monitor_user_log_scope_roles` 调整 | +| `sys_system_task_statuses` | 从 `sys_system_task_status` 调整为复数 | +| `sys_navigation_groups` | 保留 | +| `sys_navigation_links` | 替换 `navigation_links` | + +#### 安全审计 + +| 目标表 | 来源/处理 | +| --- | --- | +| `audit_operation_logs` | 替换 `sys_audit_logs` | +| `audit_settings` | 替换 `sys_audit_settings` | +| `audit_login_logs` | 替换 `sys_login_logs` | +| `audit_terminal_sessions` | 替换 `sys_terminal_sessions` | +| `audit_rdp_sessions` | 替换 `sys_rdp_sessions` | +| `audit_session_commands` | 替换 `sys_session_commands` | + +#### 资产 / 云资源 + +| 目标表 | 来源/处理 | +| --- | --- | +| `asset_servers` | 保留 | +| `asset_host_groups` | 保留 | +| `asset_cloud_platforms` | 替换 `asset_clouds` | +| `asset_cloud_accounts` | 保留,敏感字段统一转凭据 ID | +| `asset_cloud_regions` | 替换 `asset_regions` | +| `asset_domains` | 保留 | +| `asset_resource_models` | 保留 | +| `asset_cloud_billing_records` | 替换 `asset_cloud_billings`,语义更明确 | +| `asset_cloud_billing_alert_rules` | 替换 `monitor_billing_alert_rules`,账单告警属于资产账单域配置,触发事件再进监控事件 | + +#### Kubernetes + +| 目标表 | 来源/处理 | +| --- | --- | +| `k8s_clusters` | 替换 `kubernetes_clusters` | +| `k8s_nodes` | 替换 `kubernetes_nodes` | +| `k8s_pods` | 替换 `kubernetes_pods` | +| `k8s_deployments` | 替换 `kubernetes_deployments` | +| `k8s_services` | 替换 `kubernetes_services` | +| `k8s_ingresses` | 替换 `kubernetes_ingresses` | +| `k8s_topology_layouts` | 替换 `kubernetes_topology_layouts` | + +#### 应用发布 + +| 目标表 | 来源/处理 | +| --- | --- | +| `deploy_pipelines` | 替换 `ops_pipelines` | +| `deploy_pipeline_runs` | 替换 `ops_pipeline_runs` | +| `deploy_app_releases` | 保留 | +| `deploy_environments` | 保留 | +| `deploy_build_nodes` | 保留 | +| `deploy_build_master_configs` | 替换 `deploy_build_master_config` | +| `deploy_pipeline_members` | 保留 | +| `deploy_user_groups` | 保留 | +| `deploy_user_group_members` | 保留 | +| `deploy_tools` | 保留 | +| 已删除 `deploy_pipeline_repos` | 发布配置直接使用 `iam_credentials` + Pipeline 凭据字段 | +| 已删除 `deploy_pipeline_registries` | 发布配置直接使用 `iam_credentials` + Pipeline 凭据字段 | + +#### 监控告警 + +| 目标表 | 来源/处理 | +| --- | --- | +| `monitor_targets` | 保留 | +| `monitor_metrics` | 保留,作为指标定义表 | +| `monitor_metric_samples` | 合并/替换 `metric_values` 和 `monitor_metric_histories` | +| `monitor_metric_aggregates` | 替换 `monitor_metrics_aggregate` | +| `monitor_metric_tags` | 保留 | +| `monitor_alert_rules` | 保留但只保留一个模型定义 | +| `monitor_alert_conditions` | 保留 | +| `monitor_alert_events` | 保留 | +| `monitor_alert_channels` | 替换 `monitor_notify_channels` | +| `monitor_alert_contacts` | 保留 | +| `monitor_alert_contact_groups` | 保留 | +| `monitor_alert_notify_policies` | 保留 | +| `monitor_alert_notify_templates` | 保留 | +| `monitor_alert_notify_settings` | 保留 | +| `monitor_alert_upgrades` | 保留 | +| `monitor_alert_upgrade_records` | 保留 | +| `monitor_alert_silences` | 保留 | +| `monitor_agent_reports` | 保留 | +| `monitor_agent_tasks` | 保留 | +| `monitor_agent_rules` | 保留 | +| `monitor_agent_alerts` | 保留 | +| `monitor_agent_port_policies` | 替换 `monitor_agent_port_policy`,并加入迁移 | +| `monitor_health_probes` | 保留 | +| `monitor_prometheus_datasources` | 保留 | +| `monitor_zabbix_datasources` | 保留 | +| `monitor_log_datasources` | 保留 | +| `monitor_trace_datasources` | 保留 | +| `monitor_site_tasks` | 替换 `ops_site_tasks` | +| `monitor_site_results` | 替换 `ops_site_results` | +| `monitor_site_alerts` | 替换 `ops_site_alerts` | +| `monitor_snmp_devices` | 替换 `ops_snmp_devices` | +| `monitor_snmp_templates` | 替换 `ops_snmp_templates` | +| `monitor_snmp_template_oids` | 替换 `ops_snmp_template_oids` | +| `monitor_snmp_samples` | 替换 `ops_snmp_metrics`,删除 `snmp_history` | +| `monitor_snmp_interfaces` | 替换 `ops_snmp_interfaces` | +| `monitor_dashboards` | 保留一个大屏主表 | +| `monitor_dashboard_panels` | 替换 `dashboard_panels` | +| 已删除 `monitor_custom_dashboards` | 合并到 `monitor_dashboards`,用 `type/custom` 区分 | +| 删除 `monitor_metric_data` | 用 `monitor_metrics` 替代 | + +#### 自动化运维 + +| 目标表 | 来源/处理 | +| --- | --- | +| `ops_cron_jobs` | 保留 | +| `ops_scripts` | 保留 | +| `ops_task_executions` | 保留 | +| `ops_remote_executions` | 建议新增,记录远程批量执行任务级审计 | +| `ops_ansible_playbooks` | 如保留 Ansible 页面则保留,否则删除 | +| `ops_ansible_jobs` | 同上 | +| `ops_ansible_inventories` | 同上 | + +#### 审批 + +| 目标表 | 来源/处理 | +| --- | --- | +| `approval_templates` | 替换 `sys_approval_templates` | +| `approval_processes` | 保留 | +| `approval_steps` | 保留 | +| `approval_workflows` | 替换 `sys_approval_workflows` | +| `approval_workflow_steps` | 替换 `sys_approval_workflow_steps` | + +建议:模板保留表单 schema,工作流表保留审批流定义,流程实例引用 `workflow_id`,逐步弱化 `flow_config` JSON。 + +#### AIOps / AI + +| 目标表 | 来源/处理 | +| --- | --- | +| `ai_llm_configs` | 保留 | +| `ai_chat_histories` | 保留 | +| `ai_analysis_traces` | 保留 | +| `ai_runs` | 保留 | +| `ai_run_messages` | 保留 | +| `ai_run_spans` | 保留 | +| `ai_capabilities` | 保留 | +| `ai_capability_policies` | 保留 | +| `ai_ops_sessions` | 替换 `ops_sessions` | +| `ai_ops_commands` | 替换 `ops_commands` | + +### 重建执行策略 + +1. 修改所有 `TableName()`,把目标表名一次性统一。 +2. 修改 GORM many2many tag:`role_permissions` -> `iam_role_permissions`。 +3. 修改硬编码 `db.Table(...)` 和 SQL 清理逻辑。 +4. 精简 AutoMigrate:删除已合并/废弃模型,补上漏迁移模型。 +5. 更新 seed:默认权限、审批模板、工具配置、演示数据按新表生成。 +6. 删除测试库数据卷或 SQLite 文件,重新启动服务让 AutoMigrate 建表。 +7. 跑 `go build`、`npx tsc --noEmit`、集成测试和关键页面验证。 + +## 总体结论 + +当前表名前缀没有形成统一分层,主要使用 `asset_`、`monitor_`、`ops_`、`deploy_`、`sys_`、`iam_`、`ai_`、`kubernetes_`。整体可以运行,但存在几类需要后续整改的问题: + +1. **无前缀或前缀不一致表**:`approval_processes`、`approval_steps`、`navigation_links`、`metric_values`、`dashboard_panels`、`snmp_history`、`role_permissions`。 +2. **同一职责多套表并存**:审批有 `sys_approval_templates / approval_processes / approval_steps` 与 `sys_approval_workflows / sys_approval_workflow_steps`;监控大屏有 `monitor_dashboards / dashboard_panels` 与 `monitor_custom_dashboards`;告警规则有 `AlertRule` 和 `MonitorAlertRule` 共用 `monitor_alert_rules`。 +3. **表定义但迁移/引用不完整**:`SNMPHistory` 定义了 `snmp_history`,但未进入 AutoMigrate,代码实际查询 `ops_snmp_metrics`;`AgentPortPolicy` 有控制器引用,但未进入 AutoMigrate。 +4. **疑似旧表/低价值表**:`sys_log_sources`、`sys_log_entries` 属于旧日志模块,与当前日志中心 `monitor_log_datasources` + 外部 Loki/ES 架构重复;`ops_ansible_*` 有种子数据但“远程执行”现已主要走 SSH/脚本执行,需要确认是否仍保留 Ansible 页面。 +5. **设计一致性问题**:大量 JSON 字符串字段承载结构化配置,短期可接受,但要明确边界;高频时序表缺少统一保留策略和分区方案;部分关联表缺少显式外键/唯一约束命名规范。 + +## 现状前缀评估 + +> 本节是现状评估口径;重建整改时以“重建版目标设计”为准。 + +| 领域 | 当前/建议前缀 | 说明 | +| --- | --- | --- | +| IAM/权限/凭据 | `iam_` | 用户、角色、权限、LDAP、凭据、ItemRole | +| 系统基础配置 | `sys_` | 系统任务、站内导航、全局配置 | +| 安全审计 | `audit_` | 重建后从 `sys_` 拆出,避免系统配置与审计数据混在一起 | +| 资产/云资源 | `asset_` | 服务器、云账号、域名、账单、资源模型 | +| Kubernetes | `k8s_` | 重建后统一短前缀,替换当前 `kubernetes_` | +| 发布/部署 | `deploy_` | 环境、凭据、工具、发布记录、成员权限 | +| 自动化执行 | `ops_` | 定时任务、脚本、任务执行、远程执行记录 | +| 监控告警 | `monitor_` | 指标、目标、告警、数据源、站点拨测、SNMP、Agent | +| 审批 | `approval_` | 重建后模板、流程、实例统一归入审批域 | +| AI/AIOps | `ai_` | LLM 配置、会话、分析轨迹、能力策略 | + +## 问题清单 + +| 编号 | 严重度 | 表/模型 | 问题 | 建议 | +| --- | --- | --- | --- | --- | +| DB-001 | 高 | `AlertRule` / `MonitorAlertRule` -> `monitor_alert_rules` | 两个模型返回同一张表,字段定义不同。AutoMigrate 会叠加字段,后续维护容易误删/误覆盖。 | 合并为一个模型或拆成 `monitor_alert_rules` 与 `monitor_probe_alert_rules`,迁移前确认线上字段使用。 | +| DB-002 | 高 | `AgentPortPolicy` -> `monitor_agent_port_policy` | 控制器有读写,但未加入 AutoMigrate,新库端口策略接口可能报“表不存在”。 | 加入 AutoMigrate;表名建议改为复数 `monitor_agent_port_policies`,如改名需迁移旧表。 | +| DB-003 | 高 | `SNMPHistory` -> `snmp_history` | 定义未迁移,代码未引用;清理逻辑清的是 `ops_snmp_metrics`。 | 若 `ops_snmp_metrics` 已兼任历史表,删除 `SNMPHistory`;否则新增迁移并把查询/清理改到 `monitor_snmp_histories`。 | +| DB-004 | 中 | `approval_processes`、`approval_steps` | 审批流程表无前缀,而模板表是 `sys_approval_templates`。 | 重建后统一为 `approval_templates`、`approval_processes`、`approval_steps`、`approval_workflows`。 | +| DB-005 | 中 | `sys_approval_workflows`、`sys_approval_workflow_steps` | 与 `sys_approval_templates.flow_config` 和 `approval_steps` 职责有重叠。 | 明确“模板流程配置”和“流程定义管理”的关系;如果新工作流已替代模板内 JSON,应迁移数据并下线旧字段。 | +| DB-006 | 中 | `navigation_links` | 导航组是 `sys_navigation_groups`,链接表无前缀。 | 重建后改为 `sys_navigation_links`。 | +| DB-007 | 中 | `metric_values`、`monitor_metric_histories`、`monitor_metrics_aggregate` | 监控时序有三套表:原始值、简化历史、聚合;当前控制器大量使用 `monitor_metric_histories`,`metric_values` 引用少。 | 重建后合并为 `monitor_metric_samples` + `monitor_metric_aggregates`。 | +| DB-008 | 已处理 | `monitor_dashboards`、`monitor_dashboard_panels` | 自定义大屏旧模型与路由已删除,统一使用 `monitor_dashboards` + `monitor_dashboard_panels`。 | 后续仅补齐新大屏 API/页面。 | +| DB-009 | 中 | `role_permissions` | GORM many2many 明确指定为 `role_permissions`,无 `iam_` 前缀。 | 重建后改为 `iam_role_permissions`。 | +| DB-010 | 中 | `sys_log_sources`、`sys_log_entries` | 旧日志采集/入库表,与当前日志中心外部数据源架构重复。 | 重建后删除,日志中心只保留 `monitor_log_datasources` 和外部查询结果。 | +| DB-011 | 中 | `ops_site_*`、`ops_snmp_*` | 站点拨测和 SNMP 属于监控域,但表名前缀使用 `ops_`。 | 重建后改为 `monitor_site_*`、`monitor_snmp_*`。 | +| DB-012 | 中 | `ai_runs`、`ops_sessions`、`ops_commands` | AI 运行表和人机执行会话跨 `ai_` / `ops_`,边界不清。 | 重建后改为 `ai_ops_sessions`、`ai_ops_commands`。 | +| DB-013 | 低 | `backend/model/monitor.go`、`deploy_tool.go` | 模型文件超过或接近项目红线,维护成本高。 | 后续按子域拆分:target/metric/dashboard/health_probe/tool_registry。 | + +## 菜单域表清单 + +### 仪表盘 + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 首页统计 | 多域聚合,无独立主表 | 统计来自资产、告警、K8s、流水线等表,合理。 | +| 自定义监控仪表盘 | `monitor_dashboards`、`monitor_dashboard_panels` | 已统一到监控大屏主表和面板表。 | + +### 资产管理 / 云资源 + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 服务器管理 | `asset_servers` | 前缀规范;字段较多但与 Agent/RDP/SSH 场景匹配。 | +| 主机组 | `asset_host_groups` | 放资产域合理。 | +| 云平台 | `asset_clouds` | 命名偏泛,实际是云平台类型/供应商。可考虑改为 `asset_cloud_platforms`。 | +| 云账号 | `asset_cloud_accounts` | 前缀规范;凭证字段需确认是否已统一走 `iam_credentials` 加密。 | +| 云区域 | `asset_regions` | 前缀规范;如果仅云区域,建议 `asset_cloud_regions` 更清晰。 | +| 域名管理 | `asset_domains` | 前缀规范。 | +| 资源型号 | `asset_resource_models` | 前缀规范。 | +| 云账单 | `asset_cloud_billings` | 前缀规范。 | +| 账单告警规则 | `monitor_billing_alert_rules` | 归入监控域合理,但与资产账单强关联,需文档化。 | + +### Kubernetes + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 集群管理 | `kubernetes_clusters` | 当前统一 `kubernetes_` 前缀,虽较长但一致。 | +| 节点缓存 | `kubernetes_nodes` | 从集群同步缓存,合理。 | +| Pod 缓存 | `kubernetes_pods` | 从集群同步缓存,合理。 | +| Deployment 缓存 | `kubernetes_deployments` | 从集群同步缓存,合理。 | +| Service 缓存 | `kubernetes_services` | 从集群同步缓存,合理。 | +| Ingress 缓存 | `kubernetes_ingresses` | 从集群同步缓存,合理。 | +| 拓扑布局 | `kubernetes_topology_layouts` | 用户布局数据,合理。 | + +建议:重建后统一改为 `k8s_`,后续不再新增 `kubernetes_` 表。 + +### 应用发布 / CI/CD + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 流水线 | `ops_pipelines` | 历史命名属 `ops_`,但该域现在已经有 `deploy_` 前缀,建议长期统一。 | +| 流水线运行 | `ops_pipeline_runs` | 同上;日志存在 `logs longtext`,数据增长需清理策略。 | +| 应用发布 | `deploy_app_releases` | 前缀规范。 | +| Git 仓库配置 | `deploy_pipeline_repos` | 可能与统一凭据 `iam_credentials` 存在职责重叠,当前通过 `credential_id` 关联可接受。 | +| 镜像仓库配置 | `deploy_pipeline_registries` | 同上。 | +| 发布环境 | `deploy_environments` | 前缀规范。 | +| 构建节点 | `deploy_build_nodes`、`deploy_build_master_config` | 前缀规范。 | +| 流水线成员 | `deploy_pipeline_members` | 前缀规范;直接用户 + 用户组双授权设计合理。 | +| 发布用户组 | `deploy_user_groups`、`deploy_user_group_members` | 前缀规范,但与 IAM 角色/组职责需边界清晰。 | +| ItemRole | `iam_item_roles`、`iam_user_item_roles` | 放 IAM 域合理,用于流水线正则授权。 | +| 工具配置 | `deploy_tools` | 前缀规范。 | + +建议:重建后统一改为 `deploy_pipelines`、`deploy_pipeline_runs`。 + +### 监控告警 + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 告警规则/事件 | `monitor_alert_rules`、`monitor_alert_events` | 主表合理;但规则模型重复是高风险问题。 | +| 告警条件/模板/升级/静默 | `monitor_alert_conditions`、`monitor_alert_notify_templates`、`monitor_alert_upgrades`、`monitor_alert_upgrade_records`、`monitor_alert_silences` | 前缀规范;需确认哪些已接入 UI/引擎。 | +| 通知渠道/联系人 | `monitor_notify_channels`、`monitor_alert_contacts`、`monitor_alert_contact_groups`、`monitor_alert_notify_policies`、`monitor_alert_notify_settings` | 前缀规范;通知设置/策略/模板职责较多,建议后续画数据流。 | +| Agent 指标 | `monitor_agent_reports`、`monitor_metrics_aggregate` | 前缀规范;按 hostname 关联服务器,删除服务器时有清理。 | +| Agent 任务/规则/告警 | `monitor_agent_tasks`、`monitor_agent_rules`、`monitor_agent_alerts` | 前缀规范。 | +| Agent 端口策略 | `monitor_agent_port_policy` | 未迁移,且表名非复数。 | +| 监控目标/指标 | `monitor_targets`、`monitor_metrics`、`monitor_metric_histories`、`monitor_metric_tags`、`metric_values` | 需要统一时序表职责和命名前缀。 | +| 健康探针 | `monitor_health_probes` | 前缀规范;结果不独立入库,随 Agent 心跳更新。 | +| Prometheus/Zabbix/日志/链路数据源 | `monitor_prometheus_datasources`、`monitor_zabbix_datasources`、`monitor_log_datasources`、`monitor_trace_datasources` | 前缀规范。 | +| 站点拨测 | `ops_site_tasks`、`ops_site_results`、`ops_site_alerts` | 功能属监控,前缀却是 `ops_`。 | +| SNMP | `ops_snmp_devices`、`ops_snmp_templates`、`ops_snmp_template_oids`、`ops_snmp_metrics`、`ops_snmp_interfaces`、`snmp_history` | 功能属监控;`snmp_history` 未迁移/未引用。 | +| 指标目录旧表 | `monitor_metric_data` | 需确认是否仍有 UI/引擎依赖;可能与 `monitor_metrics` 重复。 | + +### 自动化运维 + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 定时任务 | `ops_cron_jobs` | 前缀规范。 | +| 脚本库 | `ops_scripts` | 前缀规范。 | +| 执行记录 | `ops_task_executions` | 前缀规范。 | +| Ansible | `ops_ansible_playbooks`、`ops_ansible_jobs`、`ops_ansible_inventories` | 有模型和种子数据;需确认 UI 是否仍作为主功能。若已由 Go-native SSH 替代,可降级为 deprecated。 | + +### AIOps / AI + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| LLM 配置 | `ai_llm_configs` | 前缀规范。 | +| 对话历史 | `ai_chat_histories` | 前缀规范。 | +| 告警分析轨迹 | `ai_analysis_traces` | 前缀规范。 | +| Agent CLI 运行 | `ai_runs`、`ai_run_messages`、`ai_run_spans` | 前缀规范。 | +| 能力注册/策略 | `ai_capabilities`、`ai_capability_policies` | 前缀规范。 | +| 人机执行会话 | `ops_sessions`、`ops_commands` | 与 AI Agent 强关联但用 `ops_`,边界需确认。 | + +### 资源申请 / 审批 + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 审批模板 | `sys_approval_templates` | 前缀规范。 | +| 审批实例 | `approval_processes` | 无前缀。 | +| 审批步骤 | `approval_steps` | 无前缀。 | +| 审批工作流 | `sys_approval_workflows`、`sys_approval_workflow_steps` | 前缀规范,但与模板内 `flow_config` 和实例步骤存在职责重叠。 | + +建议:先确认现行前端使用旧模板流程还是新 workflow;再决定迁移方向。 + +### 安全审计 + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 操作审计 | `sys_audit_logs` | 前缀规范。 | +| 审计策略 | `sys_audit_settings` | 前缀规范。 | +| 登录日志 | `sys_login_logs` | 前缀规范。 | +| 终端会话 | `sys_terminal_sessions` | 前缀规范。 | +| RDP 会话 | `sys_rdp_sessions` | 前缀规范。 | +| 会话命令 | `sys_session_commands` | 前缀规范。 | + +### 系统管理 / IAM + +| 功能 | 表 | 评价 | +| --- | --- | --- | +| 用户 | `iam_users` | 前缀规范。 | +| 角色 | `iam_roles` | 前缀规范。 | +| 权限 | `iam_permissions` | 前缀规范。 | +| 用户角色关系 | `iam_user_roles` | GORM many2many 指定,前缀规范。 | +| 角色权限关系 | `role_permissions` | GORM many2many 指定,无 `iam_` 前缀。 | +| LDAP | `iam_ldap_configs` | 前缀规范。 | +| 凭据 | `iam_credentials` | 前缀规范;建议继续作为全局凭据统一入口。 | +| 日志范围权限 | `monitor_log_scope_roles`、`monitor_user_log_scope_roles` | 属日志中心/监控域,放 `monitor_` 合理。 | +| 系统任务状态 | `sys_system_task_status` | 前缀规范。 | +| 站内导航 | `sys_navigation_groups`、`navigation_links` | 链接表无前缀。 | + +## 疑似可清理候选 + +测试库重建时可直接删除;生产整改前仍需查数据量、最近写入时间、controller/frontend 是否仍有入口。 + +| 表/模型 | 判断依据 | 建议动作 | +| --- | --- | --- | +| `snmp_history` / `SNMPHistory` | 未 AutoMigrate,未被业务引用。 | 重建后删除模型。 | +| `sys_log_sources`、`sys_log_entries` | 旧日志入库模块,与日志中心外部数据源重复。 | 重建后删除。 | +| `monitor_custom_dashboards` | 与 `monitor_dashboards` 重复。 | 已删除模型与路由,重建后不再创建。 | +| `metric_values`、`monitor_metric_histories` | 时序职责重复。 | 重建后合并为 `monitor_metric_samples`。 | +| `ops_ansible_*` | 远程执行主路径已偏 Go-native SSH。 | 若确认无页面入口,重建后删除;否则保留在 `ops_`。 | +| `deploy_pipeline_repos`、`deploy_pipeline_registries` | Pipeline 已直接使用 `GitCredentialID` / `RegistryCredentialID`。 | 已删除模型、路由和前端旧配置入口,重建后不再创建。 | + +## 后续整改顺序 + +1. **先统一命名和模型归属**:按 Schema v2 修改 `TableName()`、many2many tag、硬编码 SQL。 +2. **处理重复模型**:合并 `AlertRule` / `MonitorAlertRule`、大屏表、指标时序表、审批 workflow/模板关系。 +3. **删除废弃模型**:`SNMPHistory`、旧日志入库、旧仓库/镜像仓库配置、确认不用的 Ansible 表。 +4. **重建测试库**:删除 SQLite 文件或 Docker 数据卷,让 AutoMigrate 从零建表。 +5. **补充数据生命周期**:流水线日志、Agent 报告、指标样本、SNMP 样本、站点拨测结果、审计日志都需要保留期和清理任务。 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c392693..c0a5b62 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { Suspense, lazy } from 'react'; +import { Suspense, lazy, useEffect, useRef } from 'react'; import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ThemeProvider } from '@/hooks/useTheme'; @@ -10,6 +10,7 @@ import { PageTransition } from '@/components/layout/PageTransition'; import { ErrorBoundary } from '@/components/layout/ErrorBoundary'; import { Toaster } from '@/components/ui/sonner'; import i18n from '@/i18n'; +import { toast } from 'sonner'; // Lazy load page components for code splitting const Dashboard = lazy(() => import('@/components/dashboard/Dashboard').then(m => ({ default: m.Dashboard }))); @@ -104,12 +105,63 @@ const queryClient = new QueryClient({ }, }); +function AssetVersionGuard() { + const currentVersionRef = useRef(''); + const notifiedRef = useRef(false); + + useEffect(() => { + const readVersion = async () => { + try { + const resp = await fetch(`/version?t=${Date.now()}`, { cache: 'no-store' }); + if (!resp.ok) return; + const data = await resp.json(); + const nextVersion = [data.version, data.git_commit, data.build_time].filter(Boolean).join(':'); + if (!nextVersion) return; + if (!currentVersionRef.current) { + currentVersionRef.current = nextVersion; + return; + } + if (currentVersionRef.current !== nextVersion && !notifiedRef.current) { + notifiedRef.current = true; + toast.info(i18n.t('common.appUpdated'), { + description: i18n.t('common.appUpdatedDesc'), + action: { + label: i18n.t('common.reloadPage'), + onClick: () => window.location.reload(), + }, + duration: Infinity, + }); + } + } catch { + // 版本检查失败不影响业务页面,后续定时器会重试。 // Logan.Li + } + }; + + void readVersion(); + const timer = window.setInterval(readVersion, 60_000); + const handleFocus = () => void readVersion(); + const handleVisibility = () => { + if (document.visibilityState === 'visible') void readVersion(); + }; + window.addEventListener('focus', handleFocus); + document.addEventListener('visibilitychange', handleVisibility); + return () => { + window.clearInterval(timer); + window.removeEventListener('focus', handleFocus); + document.removeEventListener('visibilitychange', handleVisibility); + }; + }, []); + + return null; +} + function App() { return ( + } /> diff --git a/frontend/src/api/aiops/agent-cli.ts b/frontend/src/api/aiops/agent-cli.ts index a78503c..72e928d 100644 --- a/frontend/src/api/aiops/agent-cli.ts +++ b/frontend/src/api/aiops/agent-cli.ts @@ -2,44 +2,47 @@ import { http } from '@/utils/request'; import { useUserStore } from '@/store'; -// 会话与消息类型(与后端 model.AgentCLISession/Message 对应) // Logan.Li +// 会话与消息类型(与后端 model.AIRun/AIRunMessage 对应) // Logan.Li export interface AgentCliSession { id: number; + run_id: string; + parent_run_id: string; user_id: number; + feature: string; mode: string; // host | kubernetes - server_id: number; - server_name: string; - server_host: string; - cluster_id: number; - cluster_name: string; - namespace: string; - pod_name: string; - container: string; + subject_type: string; + subject_id: string; + subject_name: string; + subject_scope: string; title: string; status: string; // running/done/aborted/error provider: string; model: string; + safety_mode: string; + context_pack_id: string; + prompt_tokens: number; + completion_tokens: number; rounds: number; total_tokens: number; duration: number; error?: string; + started_at: string; + finished_at?: string; created_at: string; updated_at: string; } export interface AgentCliMessage { id: number; - session_id: number; + run_id: string; round: number; + seq: number; type: string; // thought/tool_call/tool_result/done/error/user role: string; content: string; tool_name: string; - tool_args: string; - tool_output: string; - risk: string; - success: boolean; - duration: string; + tool_call_id: string; + span_id: string; tokens: number; created_at: string; } @@ -96,4 +99,4 @@ export function buildAgentCliWsUrl(params: AgentCliWsParams | number | string): } return `${base}?mode=host&id=${params.serverId}&token=${token}`; -} \ No newline at end of file +} diff --git a/frontend/src/api/deploy/config.ts b/frontend/src/api/deploy/config.ts index 5255752..a56b41f 100644 --- a/frontend/src/api/deploy/config.ts +++ b/frontend/src/api/deploy/config.ts @@ -3,18 +3,6 @@ import { http, type ListResponse } from '@/utils/request'; export const deployApi = { - // Git 仓库认证 - getRepos: () => http.get('/deploy/repos'), - getRepo: (id: number | string) => http.get(`/deploy/repos/${id}`), - createRepo: ( data: Record) => http.post('/deploy/repos', data), - updateRepo: (id: number | string, data: Record) => http.put(`/deploy/repos/${id}`, data), - deleteRepo: (id: number | string) => http.delete(`/deploy/repos/${id}`), - // 镜像仓库认证 - getRegistries: () => http.get('/deploy/registries'), - getRegistry: (id: number | string) => http.get(`/deploy/registries/${id}`), - createRegistry: ( data: Record) => http.post('/deploy/registries', data), - updateRegistry: (id: number | string, data: Record) => http.put(`/deploy/registries/${id}`, data), - deleteRegistry: (id: number | string) => http.delete(`/deploy/registries/${id}`), // 环境管理 getEnvironments: () => http.get('/deploy/environments'), createEnvironment: ( data: Record) => http.post('/deploy/environments', data), @@ -45,4 +33,4 @@ export const deployApi = { uninstallTool: (id: number | string) => http.post(`/deploy/tools/${id}/uninstall`), checkTool: (id: number | string) => http.post(`/deploy/tools/${id}/check`), getToolVersions: (id: number | string) => http.get(`/deploy/tools/${id}/versions`), -}; \ No newline at end of file +}; diff --git a/frontend/src/components/aiops/AgentCLIMessages.tsx b/frontend/src/components/aiops/AgentCLIMessages.tsx index e595045..f53d0d9 100644 --- a/frontend/src/components/aiops/AgentCLIMessages.tsx +++ b/frontend/src/components/aiops/AgentCLIMessages.tsx @@ -54,17 +54,17 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] { items.push({ id: nextId(), kind: 'thought', round: m.round, content: m.content }); break; case 'status': - items.push({ id: nextId(), kind: 'status', round: m.round, phase: m.duration || undefined, content: m.content, toolName: m.tool_name, risk: m.risk }); + items.push({ id: nextId(), kind: 'status', round: m.round, content: m.content, toolName: m.tool_name }); break; case 'tool_call': { let command = ''; try { - const args = JSON.parse(m.tool_args || '{}') as Record; - command = String(args.command || ''); + const args = JSON.parse(m.content || '{}') as Record; + command = String(args.command || (args.action ? `${args.action} ${args.name || ''}` : '')); } catch { /* 忽略解析失败 */ } items.push({ id: nextId(), kind: 'tool', round: m.round, - toolName: m.tool_name, command, risk: m.risk, pending: true, + toolName: m.tool_name, command, pending: true, }); break; } @@ -73,9 +73,8 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] { if (items[i].kind === 'tool' && items[i].pending && items[i].toolName === m.tool_name) { items[i] = { ...items[i], - output: m.content || m.tool_output, - success: m.success, - duration: m.duration, + output: m.content, + success: true, pending: false, }; break; diff --git a/frontend/src/components/deploy/CredentialFormDialog.tsx b/frontend/src/components/deploy/CredentialFormDialog.tsx deleted file mode 100644 index 30a03de..0000000 --- a/frontend/src/components/deploy/CredentialFormDialog.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Plus, Key, Globe, Container } from 'lucide-react'; -import { toast } from 'sonner'; -import { credentialApi, deployApi } from '@/api'; -import type { JsonData } from '@/types/common'; - -interface Repo { - id: number; name: string; url: string; credential_id: number | null; -} -interface Registry { - id: number; name: string; registry_url: string; namespace: string; credential_id: number | null; -} -interface Credential { - id: number; name: string; secret_type: string; summary: string; -} - -interface Props { - open: boolean; - onOpenChange: (v: boolean) => void; - type: 'repo' | 'registry'; - editId: number | null; - repos: Repo[]; - registries: Registry[]; - onSave: () => void; -} - -export default function CredentialFormDialog({ open, onOpenChange, type, editId, repos, registries, onSave }: Props) { - const { t } = useTranslation(); - const isRepo = type === 'repo'; - const [name, setName] = useState(''); - const [desc, setDesc] = useState(''); - const [url, setUrl] = useState(''); - const [namespace, setNamespace] = useState(''); - const [credentialId, setCredentialId] = useState(''); - const [credentials, setCredentials] = useState([]); - const [saving, setSaving] = useState(false); - - // Docker 仓库 → docker 类型;Git 仓库 → api 或 docker 类型 - const filteredCredentials = credentials.filter(c => - isRepo ? true : c.secret_type === 'docker' - ); - - const loadCredentials = async () => { - try { - const res: JsonData = await credentialApi.list({ page_size: 100 }); - setCredentials(res?.list || []); - } catch { /* silent */ } - }; - - useEffect(() => { - loadCredentials(); - if (editId) { - if (isRepo) { - const r = repos.find(x => x.id === editId); - if (r) { setName(r.name); setDesc(''); setUrl(r.url); setCredentialId(r.credential_id || ''); } - } else { - const r = registries.find(x => x.id === editId); - if (r) { setName(r.name); setDesc(''); setUrl(r.registry_url); setNamespace(r.namespace || ''); setCredentialId(r.credential_id || ''); } - } - } else { - setName(''); setDesc(''); setUrl(''); setNamespace(''); setCredentialId(''); - } - }, [editId, open, isRepo]); - - const handleSave = async () => { - if (!name.trim() || !url.trim()) { toast.error(t('deploy.form.nameAddrRequired')); return; } - setSaving(true); - try { - const cid = credentialId ? Number(credentialId) : null; - const payload = isRepo - ? { name: name.trim(), description: desc.trim(), url: url.trim(), credential_id: cid } - : { name: name.trim(), description: desc.trim(), registry_url: url.trim(), namespace: namespace.trim(), credential_id: cid }; - if (editId) { - if (isRepo) await deployApi.updateRepo(editId, payload); - else await deployApi.updateRegistry(editId, payload); - toast.success(t('deploy.toast.updated')); - } else { - if (isRepo) await deployApi.createRepo(payload); - else await deployApi.createRegistry(payload); - toast.success(t('deploy.toast.created')); - } - onOpenChange(false); - onSave(); - } catch { toast.error(t('deploy.toast.saveFailed')); } - finally { setSaving(false); } - }; - - const CredIcon = isRepo ? Globe : Container; - - return ( - - - - - - {editId ? t('common.edit') : t('common.add')} {isRepo ? t('deploy.credForm.gitRepo') : t('deploy.credForm.imageRegistry')} - - -
-
- setName(e.target.value)} placeholder={isRepo ? t('deploy.credForm.repoNamePlaceholder') : t('deploy.credForm.registryNamePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
-
- setUrl(e.target.value)} placeholder={isRepo ? 'http://10.10.10.72:9000/group/repo.git' : 'http://10.10.10.72'} className="font-mono text-sm bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
- {!isRepo &&
- setNamespace(e.target.value)} placeholder={t('deploy.credForm.namespacePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
} - - {/* 关联凭证 */} -
- - - {filteredCredentials.length === 0 && ( -

- - {t('deploy.credForm.noCredHint', { type: isRepo ? '' : t('deploy.credForm.dockerPwd') })} -

- )} -
-
- - - - -
-
- ); -} diff --git a/frontend/src/components/deploy/DeployCredentials.tsx b/frontend/src/components/deploy/DeployCredentials.tsx index 0f2d306..58d560a 100644 --- a/frontend/src/components/deploy/DeployCredentials.tsx +++ b/frontend/src/components/deploy/DeployCredentials.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Plus, Pencil, Trash2, RefreshCw, GitBranch, Container, Globe, Settings2 } from 'lucide-react'; +import { Plus, Pencil, Trash2, RefreshCw, Globe, Settings2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; @@ -12,24 +12,12 @@ import { useSearchParams } from 'react-router-dom'; import BuildNodesManager from './BuildNodesManager'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { deployApi } from '@/api'; -import CredentialFormDialog from './CredentialFormDialog'; import ToolConfigTab from './ToolConfigTab'; -import type { JsonData } from '@/types/common'; -interface Repo { - id: number; name: string; description: string; url: string; - auth_type: string; credential_id: number | null; enabled: boolean; - created_by: string; updated_at: string; -} -interface Registry { - id: number; name: string; description: string; registry_url: string; namespace: string; - auth_type: string; credential_id: number | null; enabled: boolean; - created_by: string; updated_at: string; -} interface Environment { id: number; name: string; display_name: string; type: string; color: string; sort_order: number; description: string; enabled: boolean; @@ -74,7 +62,10 @@ function EnvFormDialog({ open, onClose, onSaved, editData }: { open: boolean; on return ( - {editData ? t('deploy.env.editEnv') : t('deploy.env.addEnv')} + + {editData ? t('deploy.env.editEnv') : t('deploy.env.addEnv')} + {t('deploy.label.envManage')} +
setName(e.target.value)} placeholder="production" />
setDisplayName(e.target.value)} placeholder={t('deploy.env.prodEnv')} />
@@ -102,46 +93,42 @@ function EnvFormDialog({ open, onClose, onSaved, editData }: { open: boolean; on ); } -const CONFIG_TABS = ['repos', 'registries', 'environments', 'tools', 'build-nodes'] as const; +const CONFIG_TABS = ['environments', 'build-nodes', 'tools'] as const; export default function DeployCredentials() { const { t } = useTranslation(); - const _confirm = useConfirm(); + const confirm = useConfirm(); const [searchParams] = useSearchParams(); const tabFromUrl = searchParams.get('tab'); - const [repos, setRepos] = useState([]); - const [registries, setRegistries] = useState([]); const [environments, setEnvironments] = useState([]); const [loading, setLoading] = useState(false); - const [editorOpen, setEditorOpen] = useState(false); - const [editType, setEditType] = useState<'repo' | 'registry'>('repo'); - const [editingId, setEditingId] = useState(null); const [envDialogOpen, setEnvDialogOpen] = useState(false); const [editEnv, setEditEnv] = useState(null); const [activeTab, setActiveTab] = useState( - tabFromUrl && (CONFIG_TABS as readonly string[]).includes(tabFromUrl) ? tabFromUrl : 'repos', + tabFromUrl && (CONFIG_TABS as readonly string[]).includes(tabFromUrl) ? tabFromUrl : 'environments', ); const fetchAll = async () => { setLoading(true); try { - const [rR, rReg, rEnv] = await Promise.all([deployApi.getRepos(), deployApi.getRegistries(), deployApi.getEnvironments()]); - setRepos((rR as JsonData)?.list || []); - setRegistries((rReg as JsonData)?.list || []); - setEnvironments((rEnv as JsonData)?.list || []); + const rEnv = await deployApi.getEnvironments(); + setEnvironments((rEnv as { list?: Environment[] })?.list || []); } catch { toast.error(t('deploy.toast.getDataFailed')); } finally { setLoading(false); } }; useEffect(() => { fetchAll(); }, []); - const handleDelete = async (type: 'repo' | 'registry' | 'env', item: JsonData) => { - const ok = await _confirm({ title: t('common.delete'), description: t('deploy.toast.deleteConfirm', { name: item.name || item.display_name }), confirmText: t('common.delete'), variant: 'destructive' }); + const handleDeleteEnv = async (item: Environment) => { + const ok = await confirm({ + title: t('common.delete'), + description: t('deploy.toast.deleteConfirm', { name: item.display_name || item.name }), + confirmText: t('common.delete'), + variant: 'destructive', + }); if (!ok) return; try { - if (type === 'repo') await deployApi.deleteRepo(item.id); - else if (type === 'registry') await deployApi.deleteRegistry(item.id); - else await deployApi.deleteEnvironment(item.id); + await deployApi.deleteEnvironment(item.id); toast.success(t('deploy.toast.deleted')); fetchAll(); } catch { toast.error(t('deploy.toast.deleteFailed')); } @@ -150,8 +137,6 @@ export default function DeployCredentials() { const envTypeLabel: Record = { production: t('deploy.env.production'), staging: t('deploy.env.staging'), testing: t('deploy.env.testing'), development: t('deploy.env.development') }; const pageTabs = [ - { id: 'repos', label: t('deploy.credForm.gitRepo'), badge: repos.length || undefined }, - { id: 'registries', label: t('deploy.credForm.imageRegistry'), badge: registries.length || undefined }, { id: 'environments', label: t('deploy.label.envManage'), badge: environments.length || undefined }, { id: 'build-nodes', label: t('deploy.deployNav.buildNodes') }, { id: 'tools', label: t('deploy.tool.title') }, @@ -168,109 +153,25 @@ export default function DeployCredentials() { activeTab={activeTab} onTabChange={setActiveTab} actions={activeTab === 'build-nodes' ? undefined : ( - <> - - - - - )} - contentClassName="space-y-4" - > - {activeTab === 'repos' && ( - repos.length === 0 ? ( - } title={t('deploy.credForm.noRepo')} /> - ) : ( - -
- - - - - - - - - - - - {repos.map(r => ( - - - - - - - - ))} - -
{t('deploy.table.name')}{t('deploy.table.repoUrl')}{t('deploy.cred.authMethod')}{t('deploy.table.status')}{t('common.actions')}
{r.name}{r.url}{r.auth_type} - {r.enabled ? t('common.enable') : t('common.disable')} - -
- - -
-
-
-
- ) - )} - - {activeTab === 'registries' && ( - registries.length === 0 ? ( - } title={t('deploy.credForm.noRegistry')} /> - ) : ( - -
- - - - - - - - - - - - - {registries.map(r => ( - - - - - - - - - ))} - -
{t('deploy.table.name')}Registry URL{t('deploy.credForm.namespace')}{t('deploy.cred.authMethod')}{t('deploy.table.status')}{t('common.actions')}
{r.name}{r.registry_url}{r.namespace || '-'}{r.auth_type} - {r.enabled ? t('common.enable') : t('common.disable')} - -
- - -
-
-
-
- ) - )} - - {activeTab === 'environments' && ( - <> -
+ activeTab === 'environments' ? ( + <> + -
+ + ) : ( + + ) + )} + contentClassName="space-y-4" + > + {activeTab === 'environments' && ( + <> {environments.length === 0 ? ( } @@ -316,7 +217,7 @@ export default function DeployCredentials() {
- +
@@ -333,11 +234,6 @@ export default function DeployCredentials() { {activeTab === 'tools' && } - - setEnvDialogOpen(false)} onSaved={fetchAll} editData={editEnv} /> ); diff --git a/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx b/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx index b8c202d..a674031 100644 --- a/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx +++ b/frontend/src/components/deploy/pipeline-manager/PipelineCard.tsx @@ -14,8 +14,6 @@ import { Trash2, ChevronDown, ChevronRight, - Container, - Globe, History, Server, Network, @@ -54,8 +52,6 @@ export function PipelineCard({ const navigate = useNavigate(); const { t } = useTranslation(); const stages = parseStages(pipeline.stages); - const repo = pipeline.repo as Record | undefined; - const registry = pipeline.registry as Record | undefined; const environment = pipeline.environment as Record | undefined; const code = (pipeline.code || pipeline.id) as string; const deployTarget = String(pipeline.deploy_target || 'ssh'); @@ -151,8 +147,6 @@ export function PipelineCard({ )} - {repo && {String(repo.name)}} - {registry && {String(registry.name)}} {pipeline.last_run_at && ( {new Date(pipeline.last_run_at as string).toLocaleString('zh-CN')} )} @@ -260,4 +254,4 @@ export function PipelineCard({ ); -} \ No newline at end of file +} diff --git a/frontend/src/components/layout/ErrorBoundary.tsx b/frontend/src/components/layout/ErrorBoundary.tsx index dbe0b0f..834e805 100644 --- a/frontend/src/components/layout/ErrorBoundary.tsx +++ b/frontend/src/components/layout/ErrorBoundary.tsx @@ -15,6 +15,15 @@ interface State { errorInfo: ErrorInfo | null; } +function isChunkLoadError(error: Error | null) { + // 发版后旧入口引用已删除 chunk 时,浏览器会抛动态 import 加载失败。 // Logan.Li + const message = error?.message || ''; + return message.includes('Failed to fetch dynamically imported module') || + message.includes('Importing a module script failed') || + message.includes('ChunkLoadError') || + message.includes('Loading chunk'); +} + /** * ErrorBoundary - 全局错误边界组件 * @@ -38,6 +47,10 @@ export class ErrorBoundary extends Component { } handleRetry = () => { + if (isChunkLoadError(this.state.error)) { + window.location.reload(); + return; + } this.setState({ hasError: false, error: null, errorInfo: null }); }; @@ -60,6 +73,7 @@ export class ErrorBoundary extends Component { if (this.props.fallback) { return this.props.fallback; } + const chunkLoadError = isChunkLoadError(this.state.error); return (
@@ -75,7 +89,7 @@ export class ErrorBoundary extends Component { {i18n.t('common.ebPageError')}

- {i18n.t('common.ebDescription')} + {chunkLoadError ? i18n.t('common.ebChunkDescription') : i18n.t('common.ebDescription')}

@@ -116,7 +130,7 @@ export class ErrorBoundary extends Component { onClick={this.handleRetry} > - {i18n.t('common.reload')} + {chunkLoadError ? i18n.t('common.reloadPage') : i18n.t('common.reload')}