Files
sundynix-agentix/sundynix-admin/src/api.ts
T

819 lines
31 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;
credit_weight: number; // 每模型积分权重(0/缺省=1.0,不加权)
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}`);
}
}
// —— 全局计费规则(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<void> {
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<number> {
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<CreditPack[]> {
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 ?? [];
}
// savePackid 空 = 新建。credits 单位为「积分」(面向人,服务端转 micro)。
export async function savePack(p: { id?: string; name: string; credits: number; price_fen: number; active: boolean; sort: number }): Promise<void> {
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<string[]> {
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<RedeemCodeRow[]> {
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 ?? [];
}
// ---- 微信支付配置(DB 存储、热生效;APIv3 密钥密文入库、不回显)----
export interface WechatPayConfig {
mchid: string;
cert_serial: string;
private_key_path: string;
public_key_path: string; // 微信支付公钥(2024 起新商户体系;本商户 2025-09 开户)
public_key_id: string; // PUB_KEY_ID_ 开头
appid: string;
notify_url: string;
has_apiv3_key: boolean;
}
export async function getWechatPay(): Promise<{ config: WechatPayConfig; enabled: boolean; reason: string }> {
const res = guard(await fetch(`${ADMIN}/payment/wechat`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { config?: WechatPayConfig; enabled?: boolean; reason?: string; error?: string };
if (!res.ok) throw new Error(d.error ?? `load failed: ${res.status}`);
return { config: d.config!, enabled: !!d.enabled, reason: d.reason ?? "" };
}
// saveWechatPayapiv3_key 传空串 = 沿用已保存的密钥。返回热重载后的渠道状态。
export async function saveWechatPay(body: {
mchid: string;
cert_serial: string;
private_key_path: string;
public_key_path: string;
public_key_id: string;
apiv3_key: string;
appid: string;
notify_url: string;
}): Promise<{ enabled: boolean; reason: string }> {
const res = guard(await fetch(`${ADMIN}/payment/wechat`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(body) }));
const d = (await res.json().catch(() => ({}))) as { enabled?: boolean; reason?: string; error?: string };
if (!res.ok) throw new Error(d.error ?? `save failed: ${res.status}`);
return { enabled: !!d.enabled, reason: d.reason ?? "" };
}
// ---- 充值订单流 + 对账(P5.3----
export interface PayOrder {
id: string;
tenant_id: string;
tenant_name: string;
amount_fen: number;
credits_micro: number;
channel: string;
status: string;
created_at: string;
}
export interface OrderStats {
pending: number;
paid: number;
expired: number;
paid_fen_total: number;
}
export interface ReconcileDiff {
order_id: string;
tenant_id: string;
credits_micro: number;
issue: string;
}
export async function adminOrders(status = ""): Promise<{ orders: PayOrder[]; stats: OrderStats }> {
const q = status ? `?status=${status}` : "";
const res = guard(await fetch(`${ADMIN}/orders${q}`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { orders?: PayOrder[]; stats?: OrderStats; error?: string };
if (!res.ok) throw new Error(d.error ?? `orders failed: ${res.status}`);
return { orders: d.orders ?? [], stats: d.stats ?? { pending: 0, paid: 0, expired: 0, paid_fen_total: 0 } };
}
export async function adminReconcile(): Promise<{ diffs: ReconcileDiff[]; ok: boolean }> {
const res = guard(await fetch(`${ADMIN}/orders/reconcile`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { diffs?: ReconcileDiff[]; ok?: boolean; error?: string };
if (!res.ok) throw new Error(d.error ?? `reconcile failed: ${res.status}`);
return { diffs: d.diffs ?? [], ok: !!d.ok };
}
// 人工退款:仅对已入账(paid)单——置 refunded + 记 adjust 负分录 + 回退余额(幂等)。
// status="noop" 表示该单本就无需退(已退/未支付)。真渠道钱的原路退回需 admin 另在商户后台操作。
export async function adminRefundOrder(id: string, memo: string): Promise<{ status: string; detail?: string }> {
const res = guard(await fetch(`${ADMIN}/orders/${id}/refund`, {
method: "POST",
headers: { ...authHeaders(), "Content-Type": "application/json" },
body: JSON.stringify({ memo }),
}));
const d = (await res.json().catch(() => ({}))) as { status?: string; detail?: string; error?: string };
if (!res.ok) throw new Error(d.error ?? `refund failed: ${res.status}`);
return { status: d.status ?? "", detail: d.detail };
}
// ---- 全平台任务/运行观测(管理端,来自 sundynix_task 跨租户)----
export interface AdminTask {
task_id: string;
tenant_id: string;
tenant_name: string;
owner: string;
owner_email: string;
status: string;
detail: string;
topic: string;
at: string; // RFC3339
eval_level: string;
eval_overall: number;
}
// adminTasks 全平台任务流 + 状态计数。status 空=全部;tenant 空=全租户;含 HITL 待审批(status=waiting)。
export async function adminTasks(
status = "",
tenant = "",
limit = 50,
): Promise<{ tasks: AdminTask[]; counts: Record<string, number> }> {
const q = new URLSearchParams();
if (status) q.set("status", status);
if (tenant) q.set("tenant", tenant);
q.set("limit", String(limit));
const res = guard(await fetch(`${ADMIN}/tasks?${q.toString()}`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { tasks?: AdminTask[]; counts?: Record<string, number>; error?: string };
if (!res.ok) throw new Error(d.error ?? `tasks failed: ${res.status}`);
return { tasks: d.tasks ?? [], counts: d.counts ?? {} };
}
// ---- 全平台空间(Space)观测(管理端,跨租户 sundynix_space----
export interface AdminSpace {
id: string;
tenant_id: string;
tenant_name: string;
name: string;
kind: string; // personal / project / tenant
creator: string;
creator_email: string;
archived: boolean;
members: number;
created_at: string;
}
export async function adminSpaces(limit = 200): Promise<AdminSpace[]> {
const res = guard(await fetch(`${ADMIN}/spaces?limit=${limit}`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { spaces?: AdminSpace[]; error?: string };
if (!res.ok) throw new Error(d.error ?? `spaces failed: ${res.status}`);
return d.spaces ?? [];
}
// ---- 自动评测观测(真数据,来自 sundynix_eval----
export interface EvalDay {
day: string; // YYYYMMDD
avg_overall: number;
avg_faithful: number;
count: number;
poor_count: number;
}
export interface EvalSummary {
total: number;
ok: number;
warn: number;
poor: number;
corrected: number;
avg_overall: number;
}
export interface PoorEval {
task_id: string;
tenant_name: string;
owner: string;
overall: number;
rule: number;
llm: number;
faithful: number;
level: string;
reason: string;
sources: number;
corrected: boolean;
created_at: string;
}
export async function adminEvals(days = 14): Promise<{ from: string; to: string; trend: EvalDay[]; summary: EvalSummary; poor: PoorEval[] }> {
const res = guard(await fetch(`${ADMIN}/evals?days=${days}`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { from?: string; to?: string; trend?: EvalDay[]; summary?: EvalSummary; poor?: PoorEval[]; error?: string };
if (!res.ok) throw new Error(d.error ?? `evals failed: ${res.status}`);
return {
from: d.from ?? "",
to: d.to ?? "",
trend: d.trend ?? [],
summary: d.summary ?? { total: 0, ok: 0, warn: 0, poor: 0, corrected: 0, avg_overall: 0 },
poor: d.poor ?? [],
};
}
// ---- 输入护栏安全事件(真数据,来自 guardrail_event----
export interface GuardrailEvent {
id: string;
actor: string;
kind: string; // blocked(硬拦)/ suspect(灰区放行)
reason: string;
signals: string; // 命中软信号 JSON 数组字符串
method: string;
path: string;
ip: string;
at: string;
}
export async function guardrailEvents(limit = 100): Promise<GuardrailEvent[]> {
const res = guard(await fetch(`${ADMIN}/guardrail-events?limit=${limit}`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { events?: GuardrailEvent[]; error?: string };
if (!res.ok) throw new Error(d.error ?? `guardrail failed: ${res.status}`);
return d.events ?? [];
}
// ---- 数据源清单(真数据,全平台知识库)----
export interface DatasourceKB {
id: string;
name: string;
kind: string;
tenant_id: string;
tenant_name: string;
owner: string;
doc_count: number;
total_words: number;
}
export async function adminDatasources(): Promise<{ counts: { users: number; kbs: number; docs: number }; datasources: DatasourceKB[] }> {
const res = guard(await fetch(`${ADMIN}/datasources`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { counts?: { users: number; kbs: number; docs: number }; datasources?: DatasourceKB[]; error?: string };
if (!res.ok) throw new Error(d.error ?? `datasources failed: ${res.status}`);
return { counts: d.counts ?? { users: 0, kbs: 0, docs: 0 }, datasources: d.datasources ?? [] };
}
// 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;
}
// —— 工作台概览(仪表盘聚合)——
// /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(桌面端个人工作台):这里一律全平台口径——全部用户/任务/评测/模型态/提示词态/健康。
// 单模型运行时健康态(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<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;
}
// —— 审计 / 安全事件(敏感操作留痕 + 护栏命中)——
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<AuditEntry[]> {
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<GuardrailEventItem[]> {
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<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}`);
}
}
// —— 用量 / 计费(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<UsageReport> {
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<TenantRow[]> {
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, plan?: string): Promise<void> {
const res = guard(await fetch(`${ADMIN}/tenants`, {
method: "POST", headers: authHeaders(true),
body: JSON.stringify({ name, slug, owner_email: ownerEmail ?? "", plan: plan ?? "free" }),
}));
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<Member[]> {
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<void> {
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<void> {
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<void> {
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<void> {
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}`);
}
}
// —— KB 存储迁移(管理员一次性运维操作:owner/kb 作用域 → space_id/kb 作用域)——
export interface MigrateKbResult {
total: number; // 扫描文档总数
enqueued: number; // 成功入队重灌的文档数
skipped: number; // 已是新作用域跳过数
}
export async function migrateKbStorage(): Promise<MigrateKbResult> {
const res = guard(await fetch(`${ADMIN}/migrate-kb-storage`, { method: "POST", headers: authHeaders(true) }));
const d = (await res.json().catch(() => ({}))) as Partial<MigrateKbResult> & { error?: string };
if (!res.ok) throw new Error(d.error ?? `migrate kb failed: ${res.status}`);
return { total: d.total ?? 0, enqueued: d.enqueued ?? 0, skipped: d.skipped ?? 0 };
}
export async function setTenantPlan(tenantId: string, plan: string): Promise<void> {
const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/plan`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ plan }) }));
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(d.error ?? `set tenant plan failed: ${res.status}`);
}
}
export async function setTenantStatus(tenantId: string, status: string): Promise<void> {
const res = guard(await fetch(`${ADMIN}/tenants/${tenantId}/status`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ status }) }));
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(d.error ?? `set tenant status failed: ${res.status}`);
}
}