Files
sundynix-agentix/sundynix-gateway/internal/handler/status_handler.go
T
Blizzard 373167b705 feat(admin): 补全平台任务观测 + 空间管理 + 基建加 MinIO/实时探针
对照已实现系统功能补 admin 缺失模块(feat/site):

- 全平台任务/运行观测:GET /admin/tasks(跨租户查 sundynix_task,join 租户名/提交人邮箱/评测,
  按状态/租户筛 + 状态计数;含 HITL 待审批=筛 waiting)+ TasksPage(状态卡+筛+表)。
- 空间(Space)管理:GET /admin/spaces(跨租户列 Space + 成员数子查询 + kind/归档态)+ SpacesPage。
- 基建观测:infra 加 MinIO(126 对象存储);postgres/redis/minio 从启动标志升级为实时 ping
  (blob.Ping/Postgres.Ping/Redis.Ping),能反映中途掉线。StatusPage 加 minio 标签、6 个依赖。
- 模型健康/熔断:确认 DashboardPage 已有「运行时链路态」渲染 m.health,无需重做。

验证:admin tsc + vitest 41 过、gateway build/vet/test 过;两新页浏览器实测渲染+优雅错误处理;
两新端点起临时 gateway 打真 PG 实测——tasks(counts+3路join)、spaces(成员数子查询)均返真数据。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:06:25 +08:00

210 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-shared/contract"
)
// statusItem 是一项依赖/服务的存活状态(基建灯、服务灯共用)。
type statusItem struct {
Name string `json:"name"`
Up bool `json:"up"`
Detail string `json:"detail,omitempty"`
Latency int `json:"latency_ms,omitempty"` // NATS 探针往返耗时(毫秒),本地检查项为 0
}
// toolInfo 是一个注册工具的元信息(透传各 MCP 服务 list_tools 的上报)。
type toolInfo struct {
Name string `json:"name"`
CN string `json:"cn"`
Desc string `json:"desc"`
}
// toolGroup 是一台 MCP 服务的工具注册情况。
type toolGroup struct {
Server string `json:"server"`
Up bool `json:"up"`
Tools []toolInfo `json:"tools"`
}
// systemStatus 是「服务状态」面板的聚合视图:基建 / 应用服务 / MCP 工具注册。
type systemStatus struct {
CheckedAt string `json:"checked_at"`
Infra []statusItem `json:"infra"`
Services []statusItem `json:"services"`
Tools []toolGroup `json:"tools"`
}
// probeTimeout 是各探针的单次超时(无响应即判为下线)。
const probeTimeout = 2 * time.Second
// AdminStatus: GET /api/v1/admin/status —— 聚合基建、应用服务与 MCP 工具注册的实时状态,
// 供管理端「服务状态」一眼看出哪个服务没起、基建是否就绪、工具是否注册。
func (h *Handler) AdminStatus(c *gin.Context) {
parent := c.Request.Context()
// 各探针互不依赖,并发执行,整体只等最慢的一个。
var (
wg sync.WaitGroup
milvus, neo4j bool // mcp-go health
goUp bool // mcp-go 在线
goTools []toolInfo // mcp-go 注册工具
goLatency int // mcp-go 探针耗时
pyUp bool // mcp-py 在线
pyTools []toolInfo // mcp-py 注册工具
pyLatency int // mcp-py 探针耗时
dispUp bool // dispatcher 在线
dispDetail string // dispatcher 详情(模型/运行时长)
dispLatency int // dispatcher 探针耗时
pgUp, redisUp, minioUp bool // 基建活性探针(实时 ping,非仅启动标志)
)
wg.Add(5)
// 1) mcp-go health → milvus / neo4j 基建灯
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(parent, probeTimeout)
defer cancel()
if res, err := h.bus.CallTool(ctx, contract.ToolSubjectGo("health"),
&contract.ToolCall{Tool: "health"}); err == nil && res != nil && res.OK {
var sub map[string]bool
if json.Unmarshal([]byte(res.Content), &sub) == nil {
milvus, neo4j = sub["milvus"], sub["neo4j"]
}
}
}()
// 2) mcp-go list_tools → 在线判定 + 工具清单
go func() {
defer wg.Done()
goUp, goTools, goLatency = h.probeTools(parent, contract.ToolSubjectGo("list_tools"))
}()
// 3) mcp-py list_tools → 在线判定 + 工具清单
go func() {
defer wg.Done()
pyUp, pyTools, pyLatency = h.probeTools(parent, contract.ToolSubjectPy("list_tools"))
}()
// 4) dispatcher 心跳 → 在线判定 + 模型/运行时长
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(parent, probeTimeout)
defer cancel()
start := time.Now()
if data, err := h.bus.Ping(ctx, contract.SubjectHealthDispatcher); err == nil {
dispUp = true
dispLatency = int(time.Since(start).Milliseconds())
var st struct {
Model string `json:"model"`
Ready bool `json:"ready"`
UptimeS int `json:"uptime_s"`
}
if json.Unmarshal(data, &st) == nil {
dispDetail = dispatcherDetail(st.Model, st.Ready, st.UptimeS)
}
}
}()
// 5) 基建活性探针:PG / Redis / MinIO 实时 ping(非仅启动标志,能反映中途掉线)。
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(parent, probeTimeout)
defer cancel()
pgUp = h.db.Ping(ctx)
redisUp = h.cache.Ping(ctx)
minioUp = h.blob != nil && h.blob.Ping(ctx)
}()
wg.Wait()
c.JSON(http.StatusOK, systemStatus{
CheckedAt: time.Now().Format(time.RFC3339),
Infra: []statusItem{
{Name: "postgres", Up: pgUp},
{Name: "redis", Up: redisUp},
{Name: "nats", Up: true}, // 网关连不上 NATS 即 fatal,能应答即在线
{Name: "milvus", Up: milvus},
{Name: "neo4j", Up: neo4j},
{Name: "minio", Up: minioUp}, // 对象存储(报告/KB 正文/blob126
},
Services: []statusItem{
{Name: "gateway", Up: true, Detail: "在线"},
{Name: "dispatcher", Up: dispUp, Detail: serviceDetail(dispUp, dispDetail), Latency: dispLatency},
{Name: "mcp-go", Up: goUp, Detail: toolsDetail(goUp, len(goTools)), Latency: goLatency},
{Name: "mcp-py", Up: pyUp, Detail: toolsDetail(pyUp, len(pyTools)), Latency: pyLatency},
},
Tools: []toolGroup{
{Server: "mcp-go", Up: goUp, Tools: goTools},
{Server: "mcp-py", Up: pyUp, Tools: pyTools},
},
})
}
// probeTools 调一台 MCP 服务的 list_tools:能应答即在线,并解析其工具清单(含中文名/作用)+ 往返耗时。
func (h *Handler) probeTools(parent context.Context, subject string) (up bool, tools []toolInfo, latency int) {
ctx, cancel := context.WithTimeout(parent, probeTimeout)
defer cancel()
start := time.Now()
res, err := h.bus.CallTool(ctx, subject, &contract.ToolCall{Tool: "list_tools"})
if err != nil || res == nil || !res.OK {
return false, nil, 0
}
var payload struct {
Tools []toolInfo `json:"tools"`
}
_ = json.Unmarshal([]byte(res.Content), &payload)
return true, payload.Tools, int(time.Since(start).Milliseconds())
}
func dispatcherDetail(model string, ready bool, uptimeS int) string {
d := "运行 " + humanDuration(uptimeS)
if model != "" {
d = "模型 " + model + " · " + d
}
if !ready {
d += "(模型未配置,降级桩)"
}
return d
}
func serviceDetail(up bool, detail string) string {
if !up {
return "无响应(未启动?)"
}
if detail == "" {
return "在线"
}
return detail
}
func toolsDetail(up bool, n int) string {
if !up {
return "无响应(未启动?)"
}
return fmt.Sprintf("%d 个工具", n)
}
// humanDuration 把秒数转人读(< 1h 显示分钟,否则小时+分钟)。
func humanDuration(s int) string {
if s < 60 {
return fmt.Sprintf("%ds", s)
}
m := s / 60
if m < 60 {
return fmt.Sprintf("%dm", m)
}
return fmt.Sprintf("%dh%dm", m/60, m%60)
}