feat(admin): 提示词控制面页 + 概览/状态页重做 + 接入 vitest

- 新增「提示词」页(/prompts):接 /api/v1/prompts,建版本/激活热下发/
  对比激活版行级 diff/撤销,挂到配置组
- 概览页重做为系统控制塔:吃 admin/overview(全平台口径)——平台任务/
  评测/用户规模/服务在线 + 模型路由态 + 提示词覆盖态 + 健康拓扑 + 30s自刷
- 服务状态页重做:中性克制风 + 请求链路做成深色实时数据管道(流动光点/
  节点辉光) + MCP 工具改能力域配色小卡片
- 接入 vitest(此前零测试):41 单测(api 控制面/lib diff/路由派生)
- api.ts 增 statsOverview/adminOverview/prompt 控制面四接口 + groupPrompts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-02 08:59:40 +08:00
parent e2104efc64
commit 30e80c0eed
13 changed files with 3825 additions and 470 deletions
+132
View File
@@ -181,3 +181,135 @@ export async function getStatus(): Promise<SystemStatus> {
if (!res.ok) throw new Error(`status failed: ${res.status}`);
return (await res.json()) as SystemStatus;
}
// —— 工作台概览(仪表盘聚合)——
// /api/v1/stats/overview 在 RequireAuth 组下;Task/Eval 表无 owner,故任务/评测口径为全局。
export interface DayCount {
key: string;
count: number;
}
export interface RecentRun {
task_id: string;
status: string;
detail: string;
at: string;
}
export interface Overview {
tasks_today: number;
tasks_total: number;
status_count: DayCount[]; // 近 7 天终态分布
task_trend: DayCount[]; // 近 7 天每日任务数(仅含有任务的天)
eval_avg: number;
faithful_avg: number; // 仅有来源的评测;无来源时为 0
eval_count: number;
kb_docs: number;
kb_count: number;
tokens_today: number;
daily_budget: number; // 0 = 不限额
token_trend: DayCount[];
recent_runs: RecentRun[];
services: Record<string, boolean>;
}
export async function statsOverview(): Promise<Overview> {
const res = guard(await fetch(`${GATEWAY}/api/v1/stats/overview`, { headers: authHeaders() }));
if (!res.ok) throw new Error(`overview failed: ${res.status}`);
return (await res.json()) as Overview;
}
// —— 管理端系统级聚合(控制塔口径,RequireAdmin)——
// 区别于 statsOverview(桌面端个人工作台):这里一律全平台口径——全部用户/任务/评测/模型态/提示词态/健康。
export interface AdminOverview {
users: number;
kb_count: number; // 全平台知识库数
kb_docs: number; // 全平台文档数
tasks_today: number;
tasks_total: number;
status_count: DayCount[];
task_trend: DayCount[];
eval_avg: number;
faithful_avg: number;
eval_count: number;
models: { chat_count: number; embedding_count: number; active_chat: string; active_embedding: string; fallbacks: number };
prompts: { managed: number; overrides: number };
services: Record<string, boolean>;
checked_at: string;
}
export async function adminOverview(): Promise<AdminOverview> {
const res = guard(await fetch(`${ADMIN}/overview`, { headers: authHeaders() }));
if (!res.ok) throw new Error(`admin overview failed: ${res.status}`);
return (await res.json()) as AdminOverview;
}
// —— Prompt 控制面(建版本 → 激活 → 控制面热下发各服务,不重启即生效)——
// 注意:prompt 路由在 RequireAuth 组下(/api/v1/prompts),不在 /admin 前缀内。
const PROMPTS = `${GATEWAY}/api/v1/prompts`;
export interface PromptVersion {
key: string;
version: number;
active: boolean;
note: string;
content: string;
}
export interface PromptListResp {
keys: string[]; // 平台受管的全部可配 key(即便还没建过版本)
versions: PromptVersion[];
}
// 某 key 的聚合视图:版本按号倒序、当前激活版(无则 null = 回退代码内置默认)。
export interface PromptGroup {
key: string;
versions: PromptVersion[];
active: PromptVersion | null;
}
// groupPrompts 把扁平的 {keys, versions} 归并成按 key 的分组(控制面页面的事实源)。
// 纯函数:保持 keys 的注册顺序,每组版本号倒序,挑出激活版。
export function groupPrompts(resp: PromptListResp): PromptGroup[] {
const byKey = new Map<string, PromptVersion[]>();
for (const k of resp.keys) byKey.set(k, []);
for (const v of resp.versions) {
if (!byKey.has(v.key)) byKey.set(v.key, []); // 容错:DB 有但 Known 未列的 key 也展示
byKey.get(v.key)!.push(v);
}
return Array.from(byKey.entries()).map(([key, vs]) => {
const versions = [...vs].sort((a, b) => b.version - a.version);
return { key, versions, active: versions.find((v) => v.active) ?? null };
});
}
export async function listPrompts(): Promise<PromptListResp> {
const res = guard(await fetch(PROMPTS, { headers: authHeaders() }));
if (!res.ok) throw new Error(`list prompts failed: ${res.status}`);
const data = (await res.json()) as Partial<PromptListResp>;
return { keys: data.keys ?? [], versions: data.versions ?? [] };
}
export async function createPromptVersion(key: string, content: string, note: string): Promise<number> {
const res = guard(
await fetch(`${PROMPTS}/version`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ key, content, note }) }),
);
const data = (await res.json()) as { version?: number; error?: string };
if (!res.ok) throw new Error(data.error ?? `create version failed: ${res.status}`);
return data.version ?? 0;
}
export async function activatePrompt(key: string, version: number): Promise<void> {
const res = guard(
await fetch(`${PROMPTS}/activate`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ key, version }) }),
);
const data = (await res.json()) as { error?: string; warn?: string };
if (!res.ok) throw new Error(data.error ?? `activate failed: ${res.status}`);
if (data.warn) throw new Error(data.warn); // 已激活但广播失败 → 当作错误提示运维
}
export async function deactivatePrompt(key: string): Promise<void> {
const res = guard(await fetch(`${PROMPTS}/deactivate`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ key }) }));
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(d.error ?? `deactivate failed: ${res.status}`);
}
}