8bcb90cdb2
管理端新增「运维 › 服务状态」:总览横幅 + 摘要数字 + 请求链路拓扑 (客户端→网关→NATS→调度→MCP,按健康三态着色)+ 应用服务卡(含探针 延迟)+ 基建磁贴 + MCP 工具按能力域分组(中文名/作用)。 探活机制(全走 NATS,无 HTTP): - mcp-go/mcp-py 新增 list_tools 自省工具,能应答=在线 + 上报工具清单 - dispatcher 无端点 → 新增 NATS 心跳主题 sundynix.health.dispatcher (ServeHealth 应答 model/ready/uptime),网关用 bus.Ping 探 - 网关 GET /api/v1/admin/status 并发聚合四探针 + 各项延迟 mcp-go 重构:switch → map 注册表(buildRegistry),dispatch 与 list_tools 共用单一事实源,杜绝漂移;每个工具带中文名 + 作用描述。mcp-py 同样补元信息。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
184 lines
5.7 KiB
TypeScript
184 lines
5.7 KiB
TypeScript
// 运维控制台 → Gateway 控制面 API(带 JWT 鉴权;/admin 受 RequireAdmin 保护)。
|
||
export const GATEWAY: string =
|
||
(import.meta.env.VITE_GATEWAY as string | undefined) ?? "http://localhost:8080";
|
||
const ADMIN = `${GATEWAY}/api/v1/admin`;
|
||
|
||
// ---- 鉴权(JWT,存 localStorage)----
|
||
const TOKEN_KEY = "sdx_admin_token";
|
||
let token = typeof localStorage !== "undefined" ? localStorage.getItem(TOKEN_KEY) ?? "" : "";
|
||
|
||
export function setToken(t: string): void {
|
||
token = t;
|
||
try {
|
||
localStorage.setItem(TOKEN_KEY, t);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
export function clearToken(): void {
|
||
token = "";
|
||
try {
|
||
localStorage.removeItem(TOKEN_KEY);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
export function getToken(): string {
|
||
return token;
|
||
}
|
||
|
||
function authHeaders(json = false): Record<string, string> {
|
||
const h: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
|
||
if (json) h["Content-Type"] = "application/json";
|
||
return h;
|
||
}
|
||
|
||
// guard 在 401(未登录) 时清令牌并广播登出(403=已登录但非管理员,照常抛错)。
|
||
function guard(res: Response): Response {
|
||
if (res.status === 401) {
|
||
clearToken();
|
||
if (typeof window !== "undefined") window.dispatchEvent(new Event("sdx:logout"));
|
||
}
|
||
return res;
|
||
}
|
||
|
||
export interface AuthUser {
|
||
id: string;
|
||
email: string;
|
||
name?: string;
|
||
}
|
||
|
||
export async function login(email: string, password: string): Promise<AuthUser> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/auth/login`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ email, password }),
|
||
});
|
||
const data = (await res.json()) as { token?: string; user?: AuthUser; error?: string };
|
||
if (!res.ok || !data.token || !data.user) throw new Error(data.error ?? `登录失败: ${res.status}`);
|
||
setToken(data.token);
|
||
return data.user;
|
||
}
|
||
|
||
export async function me(): Promise<AuthUser | null> {
|
||
if (!token) return null;
|
||
const res = await fetch(`${GATEWAY}/api/v1/auth/me`, { headers: authHeaders() });
|
||
if (!res.ok) {
|
||
clearToken();
|
||
return null;
|
||
}
|
||
return ((await res.json()) as { user?: AuthUser }).user ?? null;
|
||
}
|
||
|
||
// ---- 模型配置(id 为雪花字符串)----
|
||
export type Kind = "chat" | "embedding";
|
||
|
||
export interface Model {
|
||
id: string;
|
||
kind: Kind;
|
||
provider: string;
|
||
base_url: string;
|
||
api_key: string; // 列表里是脱敏值
|
||
model: string;
|
||
active: boolean;
|
||
}
|
||
|
||
export interface ModelInput {
|
||
id?: string;
|
||
kind: Kind;
|
||
provider: string;
|
||
base_url: string;
|
||
api_key: string;
|
||
model: string;
|
||
}
|
||
|
||
export async function listModels(kind: Kind): Promise<Model[]> {
|
||
const res = guard(await fetch(`${ADMIN}/models?kind=${kind}`, { headers: authHeaders() }));
|
||
if (!res.ok) throw new Error(`list failed: ${res.status}`);
|
||
return ((await res.json()) as { models: Model[] }).models;
|
||
}
|
||
|
||
export async function saveModel(m: ModelInput): Promise<string> {
|
||
const res = guard(await fetch(`${ADMIN}/models`, { method: "POST", headers: authHeaders(true), body: JSON.stringify(m) }));
|
||
const data = (await res.json()) as { id?: string; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `save failed: ${res.status}`);
|
||
return data.id ?? "";
|
||
}
|
||
|
||
export async function setActive(id: string): Promise<void> {
|
||
const res = guard(await fetch(`${ADMIN}/models/${id}/active`, { method: "POST", headers: authHeaders() }));
|
||
if (!res.ok) throw new Error(`activate failed: ${res.status}`);
|
||
}
|
||
|
||
export async function deleteModel(id: string): Promise<void> {
|
||
const res = guard(await fetch(`${ADMIN}/models/${id}`, { method: "DELETE", headers: authHeaders() }));
|
||
if (!res.ok) throw new Error(`delete failed: ${res.status}`);
|
||
}
|
||
|
||
export async function testModel(m: ModelInput): Promise<{ ok: boolean; message: string }> {
|
||
const res = guard(await fetch(`${ADMIN}/models/test`, { method: "POST", headers: authHeaders(true), body: JSON.stringify(m) }));
|
||
return (await res.json()) as { ok: boolean; message: string };
|
||
}
|
||
|
||
// ---- 计价(token↔真钱,按模型分输入/输出)----
|
||
export interface Pricing {
|
||
model_id: string;
|
||
input_per_1k: number;
|
||
output_per_1k: number;
|
||
currency: string;
|
||
}
|
||
|
||
export async function listPricing(): Promise<Pricing[]> {
|
||
const res = guard(await fetch(`${ADMIN}/pricing`, { headers: authHeaders() }));
|
||
if (!res.ok) throw new Error(`list pricing failed: ${res.status}`);
|
||
return ((await res.json()) as { pricing: Pricing[] }).pricing ?? [];
|
||
}
|
||
|
||
export async function savePricing(p: Pricing): Promise<void> {
|
||
const res = guard(await fetch(`${ADMIN}/pricing`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(p) }));
|
||
if (!res.ok) {
|
||
const d = (await res.json().catch(() => ({}))) as { error?: string };
|
||
throw new Error(d.error ?? `save pricing failed: ${res.status}`);
|
||
}
|
||
}
|
||
|
||
// gatewayOnline 用公开的 /healthz 探活(不受鉴权影响)。
|
||
export async function gatewayOnline(): Promise<boolean> {
|
||
try {
|
||
const res = await fetch(`${GATEWAY}/healthz`);
|
||
return res.ok;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// —— 服务状态:基建 / 应用服务探活 + MCP 工具注册 ——
|
||
export interface StatusItem {
|
||
name: string;
|
||
up: boolean;
|
||
detail?: string;
|
||
latency_ms?: number;
|
||
}
|
||
export interface ToolInfo {
|
||
name: string;
|
||
cn: string;
|
||
desc: string;
|
||
}
|
||
export interface ToolGroup {
|
||
server: string;
|
||
up: boolean;
|
||
tools: ToolInfo[] | null;
|
||
}
|
||
export interface SystemStatus {
|
||
checked_at: string;
|
||
infra: StatusItem[];
|
||
services: StatusItem[];
|
||
tools: ToolGroup[];
|
||
}
|
||
|
||
export async function getStatus(): Promise<SystemStatus> {
|
||
const res = guard(await fetch(`${ADMIN}/status`, { headers: authHeaders() }));
|
||
if (!res.ok) throw new Error(`status failed: ${res.status}`);
|
||
return (await res.json()) as SystemStatus;
|
||
}
|