// 运维控制台 → 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 { const h: Record = 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 { 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 { 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 { 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 { 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 { 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 { 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; credit_weight: number; // 每模型积分权重(0/缺省=1.0,不加权) currency: string; } export async function listPricing(): Promise { 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 { 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}`); } } // —— 全局计费规则(token→积分汇率 + 硬拦截开关)—— export async function getBillingConfig(): Promise<{ tokens_per_credit: number; credit_enforce: boolean }> { const res = guard(await fetch(`${ADMIN}/billing-config`, { headers: authHeaders() })); if (!res.ok) throw new Error(`billing config failed: ${res.status}`); const d = (await res.json()) as { tokens_per_credit?: string; credit_enforce?: boolean }; return { tokens_per_credit: Number(d.tokens_per_credit) || 1000, credit_enforce: !!d.credit_enforce }; // 空/未设 → 回退默认 } export async function saveBillingConfig(tokensPerCredit: number, creditEnforce: boolean): Promise { const res = guard( await fetch(`${ADMIN}/billing-config`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ tokens_per_credit: tokensPerCredit, credit_enforce: creditEnforce }), }), ); if (!res.ok) { const d = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(d.error ?? `save billing config failed: ${res.status}`); } } // —— 充值/发放积分(正=充值,负=校正)—— export async function grantCredits(tenantId: string, credits: number, memo: string): Promise { const res = guard( await fetch(`${ADMIN}/credits/grant`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ tenant_id: tenantId, credits, memo }) }), ); const d = (await res.json().catch(() => ({}))) as { balance_micro?: number; error?: string }; if (!res.ok) throw new Error(d.error ?? `grant failed: ${res.status}`); return d.balance_micro ?? 0; } // ---- 充值渠道(P5.1):积分包配置 + 兑换码 ---- export interface CreditPack { id: string; name: string; credits_micro: number; price_fen: number; active: boolean; sort: number; } export async function adminPacks(): Promise { const res = guard(await fetch(`${ADMIN}/packs`, { headers: authHeaders() })); const d = (await res.json().catch(() => ({}))) as { packs?: CreditPack[]; error?: string }; if (!res.ok) throw new Error(d.error ?? `packs failed: ${res.status}`); return d.packs ?? []; } // savePack:id 空 = 新建。credits 单位为「积分」(面向人,服务端转 micro)。 export async function savePack(p: { id?: string; name: string; credits: number; price_fen: number; active: boolean; sort: number }): Promise { const res = guard(await fetch(`${ADMIN}/packs`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(p) })); const d = (await res.json().catch(() => ({}))) as { error?: string }; if (!res.ok) throw new Error(d.error ?? `save failed: ${res.status}`); } export interface RedeemCodeRow { id: string; code: string; credits_micro: number; status: string; // unused / used used_tenant: string; used_at: string | null; memo: string; created_at: string; } export async function genRedeemCodes(credits: number, count: number, memo: string): Promise { const res = guard( await fetch(`${ADMIN}/redeem-codes`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ credits, count, memo }) }), ); const d = (await res.json().catch(() => ({}))) as { codes?: string[]; error?: string }; if (!res.ok) throw new Error(d.error ?? `生成失败: ${res.status}`); return d.codes ?? []; } export async function listRedeemCodes(): Promise { const res = guard(await fetch(`${ADMIN}/redeem-codes`, { headers: authHeaders() })); const d = (await res.json().catch(() => ({}))) as { codes?: RedeemCodeRow[]; error?: string }; if (!res.ok) throw new Error(d.error ?? `codes failed: ${res.status}`); return d.codes ?? []; } // gatewayOnline 用公开的 /healthz 探活(不受鉴权影响)。 export async function gatewayOnline(): Promise { 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 { 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; } // —— 工作台概览(仪表盘聚合)—— // /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; } export async function statsOverview(): Promise { 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(桌面端个人工作台):这里一律全平台口径——全部用户/任务/评测/模型态/提示词态/健康。 // 单模型运行时健康态(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; // 全平台知识库数 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; health: ModelHealthItem[]; // 运行时每模型 failover/熔断态(来自 dispatcher) }; prompts: { managed: number; overrides: number }; services: Record; checked_at: string; } export async function adminOverview(): Promise { 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; } // —— 审计 / 安全事件(敏感操作留痕 + 护栏命中)—— export interface AuditEntry { id: string; actor: string; // 操作者 uid action: string; // POST / PUT / DELETE / PATCH route: string; path: string; status: number; ip: string; detail: string; at: string; } export interface GuardrailEventItem { id: string; actor: string; kind: string; // blocked / suspect reason: string; signals: string; // JSON 数组字符串 method: string; path: string; ip: string; at: string; } export async function listAudit(limit = 50, offset = 0): Promise { const res = guard(await fetch(`${ADMIN}/audit?limit=${limit}&offset=${offset}`, { headers: authHeaders() })); if (!res.ok) throw new Error(`audit failed: ${res.status}`); return ((await res.json()) as { logs?: AuditEntry[] }).logs ?? []; } export async function listGuardrailEvents(limit = 50, offset = 0): Promise { const res = guard(await fetch(`${ADMIN}/guardrail-events?limit=${limit}&offset=${offset}`, { headers: authHeaders() })); if (!res.ok) throw new Error(`guardrail events failed: ${res.status}`); return ((await res.json()) as { events?: GuardrailEventItem[] }).events ?? []; } // —— 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(); 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 { 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; return { keys: data.keys ?? [], versions: data.versions ?? [] }; } export async function createPromptVersion(key: string, content: string, note: string): Promise { 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 { 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 { 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}`); } } // —— 用量 / 计费(SaaS P2 计量:token / 积分 / 成本,按租户)—— // 金额与积分均为「微单位」(×10⁻⁶):展示时 ÷1e6。走 /admin/usage(系统级,跨租户)。 export interface UsageDay { day: string; // YYYYMMDD total_tok: number; credits_micro: number; cost_micros: number; task_count: number; } export interface UsageTenantSum { tenant_id: string; name: string; total_tok: number; credits_micro: number; cost_micros: number; task_count: number; balance_micro: number; } export interface UsageReport { from: string; to: string; tenant: string; // 空 = 全平台口径 trend: UsageDay[]; totals: { total_tok: number; credits_micro: number; cost_micros: number; task_count: number }; balance_micro?: number; // 仅单租户口径 tenants?: UsageTenantSum[]; // 仅全平台口径:各租户排行 } export async function adminUsage(params?: { tenant?: string; from?: string; to?: string }): Promise { const q = new URLSearchParams(); if (params?.tenant) q.set("tenant", params.tenant); if (params?.from) q.set("from", params.from); if (params?.to) q.set("to", params.to); const qs = q.toString(); const res = guard(await fetch(`${ADMIN}/usage${qs ? "?" + qs : ""}`, { headers: authHeaders() })); if (!res.ok) throw new Error(`usage failed: ${res.status}`); const d = (await res.json()) as UsageReport; return { ...d, trend: d.trend ?? [], tenants: d.tenants ?? [] }; // Go 空切片可能序列化为 null } // —— 多成员租户:租户目录 + 成员管理 —— export interface TenantRow { id: string; name: string; slug: string; plan: string; status: string; credit_balance_micro: number; shared_billing: boolean; members: number; } export interface Member { user_id: string; email: string; name: string; role: string; // owner / admin / member / viewer / billing_admin status: string; joined_at: string; } export async function listTenants(): Promise { const res = guard(await fetch(`${ADMIN}/tenants`, { headers: authHeaders() })); if (!res.ok) throw new Error(`list tenants failed: ${res.status}`); return ((await res.json()) as { tenants?: TenantRow[] }).tenants ?? []; } export async function createTenant(name: string, slug: string, ownerEmail?: string): Promise { const res = guard(await fetch(`${ADMIN}/tenants`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ name, slug, owner_email: ownerEmail ?? "" }) })); const d = (await res.json().catch(() => ({}))) as { error?: string; warn?: string }; if (!res.ok) throw new Error(d.error ?? `create tenant failed: ${res.status}`); if (d.warn) throw new Error(d.warn); // 租户已建但指定 owner 失败 → 当提示 } export async function listMembers(tenantId: string): Promise { const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members`, { headers: authHeaders() })); if (!res.ok) throw new Error(`list members failed: ${res.status}`); return ((await res.json()) as { members?: Member[] }).members ?? []; } export async function addMember(tenantId: string, email: string, role: string): Promise { const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members`, { method: "POST", headers: authHeaders(true), body: JSON.stringify({ email, role }) })); if (!res.ok) { const d = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(d.error ?? `add member failed: ${res.status}`); } } export async function setMemberRole(tenantId: string, uid: string, role: string): Promise { const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members/${uid}`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ role }) })); if (!res.ok) { const d = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(d.error ?? `set role failed: ${res.status}`); } } export async function removeMember(tenantId: string, uid: string): Promise { const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/members/${uid}`, { method: "DELETE", headers: authHeaders() })); if (!res.ok) { const d = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(d.error ?? `remove member failed: ${res.status}`); } } export async function setSharedBilling(tenantId: string, on: boolean): Promise { const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/shared-billing`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ shared_billing: on }) })); if (!res.ok) { const d = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(d.error ?? `set shared billing failed: ${res.status}`); } }