feat(admin): 服务状态面板(基建/服务探活 + MCP 工具注册)+ mcp-go 工具注册表
管理端新增「运维 › 服务状态」:总览横幅 + 摘要数字 + 请求链路拓扑 (客户端→网关→NATS→调度→MCP,按健康三态着色)+ 应用服务卡(含探针 延迟)+ 基建磁贴 + MCP 工具按能力域分组(中文名/作用)。 探活机制(全走 NATS,无 HTTP): - mcp-go/mcp-py 新增 list_tools 自省工具,能应答=在线 + 上报工具清单 - dispatcher 无端点 → 新增 NATS 心跳主题 sundynix.health.dispatcher (ServeHealth 应答 model/ready/uptime),网关用 bus.Ping 探 - 网关 GET /api/v1/admin/status 并发聚合四探针 + 各项延迟 mcp-go 重构:switch → map 注册表(buildRegistry),dispatch 与 list_tools 共用单一事实源,杜绝漂移;每个工具带中文名 + 作用描述。mcp-py 同样补元信息。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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 探针耗时
|
||||
)
|
||||
|
||||
wg.Add(4)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
c.JSON(http.StatusOK, systemStatus{
|
||||
CheckedAt: time.Now().Format(time.RFC3339),
|
||||
Infra: []statusItem{
|
||||
{Name: "postgres", Up: h.db.Enabled()},
|
||||
{Name: "redis", Up: h.cache.Enabled()},
|
||||
{Name: "nats", Up: true}, // 网关连不上 NATS 即 fatal,能应答即在线
|
||||
{Name: "milvus", Up: milvus},
|
||||
{Name: "neo4j", Up: neo4j},
|
||||
},
|
||||
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)
|
||||
}
|
||||
@@ -54,6 +54,11 @@ func (b *Bus) CallTool(ctx context.Context, subject string, call *contract.ToolC
|
||||
return b.inner.CallTool(ctx, subject, call)
|
||||
}
|
||||
|
||||
// Ping 同步探测某节点健康(如 dispatcher 心跳主题)。无人应答 / 超时即返回错误(视为下线)。
|
||||
func (b *Bus) Ping(ctx context.Context, subject string) ([]byte, error) {
|
||||
return b.inner.Ping(ctx, subject)
|
||||
}
|
||||
|
||||
// ServeConfig 让网关作为配置控制面,响应某 kind 的配置请求。
|
||||
func (b *Bus) ServeConfig(kind string, provide func() *contract.ModelConfig) (func() error, error) {
|
||||
return b.inner.ServeConfig(kind, provide)
|
||||
|
||||
@@ -79,6 +79,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
admin.POST("/models/test", h.TestModel)
|
||||
admin.GET("/pricing", h.ListPricing) // 各模型计价(token↔真钱)
|
||||
admin.PUT("/pricing", h.SavePricing) // 设置某模型输入/输出单价
|
||||
admin.GET("/status", h.AdminStatus) // 服务状态:基建/服务探活 + MCP 工具注册
|
||||
}
|
||||
}
|
||||
return r
|
||||
|
||||
Reference in New Issue
Block a user