feat: 模型健康/熔断态 surface 到管理端(T4.F 可观测)

failover/熔断的运行时态原来只在 dispatcher 日志、admin 看不到 —— 本次接到概览可见:
- harness: CircuitBreaker.Snapshot() 只读观测访问器(state + fails,不动状态机)
- llm: Pool.ModelHealth() 上报主备链每模型 {provider,model,role,state,fails};
  buildWithFallbacks 把模型名↔breaker 配对(同包直接读 failoverModel.breakers);
  newFailoverModel 改返回具体类型以便读 breakers
- dispatcher 心跳 payload 加 models[]
- gateway /admin/overview 独立超时 Ping dispatcher,合并进 models.health
- admin 概览「模型路由」新增「运行时链路态(实时)」:逐模型状态点
  (🟢在线/🔴熔断中+失败数/🟡半开探测/单点)
- 单测:Snapshot、Pool.ModelHealth(名字↔态配对/单点/空)
- live:配坏主→提交任务打熔断→概览显示 broken-demo「熔断中·失败3」,备用在线

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-06 12:00:13 +08:00
parent 6a258fc884
commit 55d50417a9
11 changed files with 1719 additions and 17 deletions
@@ -119,6 +119,30 @@ func (h *Handler) AdminOverview(c *gin.Context) {
}
}
// 模型运行时健康态:Ping dispatcher 心跳取每模型 failover/熔断态(在线/熔断中/半开)。
// 权威配置来自 DB(上面 chat/emb),运行时态只有 dispatcher 知道 → 二者互补。
modelHealth := []gin.H{}
dctx, dcancel := context.WithTimeout(ctx, 2*time.Second) // 独立超时,不与 mcp-go 探活共用 cctx(避免被挤掉)
defer dcancel()
if data, err := h.bus.Ping(dctx, contract.SubjectHealthDispatcher); err == nil && len(data) > 0 {
var dh struct {
Models []struct {
Provider string `json:"provider"`
Model string `json:"model"`
Role string `json:"role"`
State string `json:"state"`
Fails int `json:"fails"`
} `json:"models"`
}
if json.Unmarshal(data, &dh) == nil {
for _, m := range dh.Models {
modelHealth = append(modelHealth, gin.H{
"provider": m.Provider, "model": m.Model, "role": m.Role, "state": m.State, "fails": m.Fails,
})
}
}
}
c.JSON(http.StatusOK, gin.H{
"users": users, "kb_count": kbs, "kb_docs": docs,
"tasks_today": ov.TasksToday, "tasks_total": ov.TasksTotal,
@@ -127,6 +151,7 @@ func (h *Handler) AdminOverview(c *gin.Context) {
"models": gin.H{
"chat_count": len(chat), "embedding_count": len(emb),
"active_chat": activeChat, "active_embedding": activeEmb, "fallbacks": fallbacks,
"health": modelHealth, // 运行时每模型 failover/熔断态
},
"prompts": gin.H{"managed": len(prompts.Known), "overrides": len(overrides)},
"services": services,