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:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user