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
@@ -139,3 +139,16 @@ func (c *CircuitBreaker) State() State {
defer c.mu.Unlock()
return c.state
}
// Snapshot 是熔断器的只读观测快照(供 surface 到管理端)。
type Snapshot struct {
State State // 当前状态:closed / open / half-open
Fails int // 闭合态连续失败计数
}
// Snapshot 返回当前观测快照(不改状态机;管理端展示 breaker 态用)。
func (c *CircuitBreaker) Snapshot() Snapshot {
c.mu.Lock()
defer c.mu.Unlock()
return Snapshot{State: c.state, Fails: c.fails}
}
@@ -15,6 +15,24 @@ func newTestCB(threshold int, cooldown time.Duration, clock *time.Time) *Circuit
return c
}
// Snapshot 应如实反映当前状态与连续失败计数(管理端展示用,不改状态机)。
func TestCircuitBreaker_Snapshot(t *testing.T) {
now := time.Unix(0, 0)
c := newTestCB(3, 10*time.Second, &now)
if s := c.Snapshot(); s.State != Closed || s.Fails != 0 {
t.Fatalf("初始应 closed/0, got %+v", s)
}
c.Report(false)
c.Report(false)
if s := c.Snapshot(); s.State != Closed || s.Fails != 2 {
t.Fatalf("2 次失败未到阈值应 closed/2, got %+v", s)
}
c.Report(false) // 第 3 次 → 熔断
if s := c.Snapshot(); s.State != Open || s.Fails != 3 {
t.Fatalf("阈值后应 open/3, got %+v", s)
}
}
func TestCircuitBreaker_OpensAfterThreshold(t *testing.T) {
now := time.Unix(0, 0)
c := newTestCB(3, 10*time.Second, &now)