Files
sundynix-agentix/sundynix-admin/src/api.ts
T
Blizzard 8bcb90cdb2 feat(admin): 服务状态面板(基建/服务探活 + MCP 工具注册)+ mcp-go 工具注册表
管理端新增「运维 › 服务状态」:总览横幅 + 摘要数字 + 请求链路拓扑
(客户端→网关→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>
2026-06-22 16:38:10 +08:00

184 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 运维控制台 → 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;
}