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:
@@ -219,6 +219,15 @@ export async function statsOverview(): Promise<Overview> {
|
||||
|
||||
// —— 管理端系统级聚合(控制塔口径,RequireAdmin)——
|
||||
// 区别于 statsOverview(桌面端个人工作台):这里一律全平台口径——全部用户/任务/评测/模型态/提示词态/健康。
|
||||
// 单模型运行时健康态(failover 链上主/备各一条)。
|
||||
export interface ModelHealthItem {
|
||||
provider: string;
|
||||
model: string;
|
||||
role: string; // primary / fallback
|
||||
state: string; // closed(在线) / open(熔断中) / half-open(半开探测) / single(无备用链)
|
||||
fails: number;
|
||||
}
|
||||
|
||||
export interface AdminOverview {
|
||||
users: number;
|
||||
kb_count: number; // 全平台知识库数
|
||||
@@ -230,7 +239,14 @@ export interface AdminOverview {
|
||||
eval_avg: number;
|
||||
faithful_avg: number;
|
||||
eval_count: number;
|
||||
models: { chat_count: number; embedding_count: number; active_chat: string; active_embedding: string; fallbacks: number };
|
||||
models: {
|
||||
chat_count: number;
|
||||
embedding_count: number;
|
||||
active_chat: string;
|
||||
active_embedding: string;
|
||||
fallbacks: number;
|
||||
health: ModelHealthItem[]; // 运行时每模型 failover/熔断态(来自 dispatcher)
|
||||
};
|
||||
prompts: { managed: number; overrides: number };
|
||||
services: Record<string, boolean>;
|
||||
checked_at: string;
|
||||
|
||||
@@ -14,6 +14,15 @@ const STATUS_STYLE: Record<string, { label: string; color: string }> = {
|
||||
};
|
||||
const statusStyle = (s: string) => STATUS_STYLE[s] ?? { label: s, color: "#94a3b8" };
|
||||
|
||||
// 模型熔断态 → 展示样式。
|
||||
const BREAKER_STYLE: Record<string, { label: string; dot: string; text: string }> = {
|
||||
closed: { label: "在线", dot: "bg-emerald-500", text: "text-emerald-600" },
|
||||
open: { label: "熔断中", dot: "bg-rose-500", text: "text-rose-500" },
|
||||
"half-open": { label: "半开探测", dot: "bg-amber-500", text: "text-amber-600" },
|
||||
single: { label: "单点", dot: "bg-gray-300", text: "text-gray-400" },
|
||||
};
|
||||
const breakerStyle = (s: string) => BREAKER_STYLE[s] ?? { label: s, dot: "bg-gray-300", text: "text-gray-400" };
|
||||
|
||||
function last7DaysTrend(trend: { key: string; count: number }[]): { key: string; count: number }[] {
|
||||
const byKey = new Map(trend.map((d) => [d.key, d.count]));
|
||||
const out: { key: string; count: number }[] = [];
|
||||
@@ -169,6 +178,27 @@ export function DashboardPage() {
|
||||
<span className="text-gray-500">chat {m.chat_count} · embedding {m.embedding_count}</span>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 运行时 failover/熔断态(来自 dispatcher) */}
|
||||
{m.health && m.health.length > 0 && (
|
||||
<div className="mt-3 border-t border-gray-50 pt-3">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-300">运行时链路态(实时)</div>
|
||||
<div className="space-y-1.5">
|
||||
{m.health.map((h, i) => {
|
||||
const st = breakerStyle(h.state);
|
||||
return (
|
||||
<div key={`${h.model}-${i}`} className="flex items-center gap-2 text-xs">
|
||||
<span className={`h-2 w-2 shrink-0 rounded-full ${st.dot}`} title={st.label} />
|
||||
<span className="rounded bg-gray-50 px-1.5 text-[10px] text-gray-400">{h.role === "primary" ? "主" : "备"}</span>
|
||||
<code className="min-w-0 flex-1 truncate text-[11px] text-gray-600">{h.model}</code>
|
||||
<span className={`shrink-0 text-[10px] ${st.text}`}>{st.label}</span>
|
||||
{h.fails > 0 && h.state !== "closed" && <span className="shrink-0 text-[10px] text-gray-300">失败 {h.fails}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
|
||||
Reference in New Issue
Block a user