1344bf98a0
桌面端首屏原是静态宣传页(stat 全硬编码、唯一活数据是网关在线),"太单调"。 重做为活的驾驶舱: 后端 GET /api/v1/stats/overview(聚合,几条轻量查询): - 任务今日/累计、7 日趋势、近 7 天终态分布(实例级,Task 无 owner) - 评测均分 + 忠实度均值 + 计数;知识库文档/库数(owner 级) - token 今日 + 7 日趋势(Redis 日计数)、近期运行 feed、服务健康(复用 health 口径) - store.StatsOverview + RecentTasks。 前端 Home 重写为仪表盘:4 指标卡(今日任务/Token/评测均分/知识库)带 SVG 火花线、 近期运行 feed(状态点+相对时间,点进运行观测)、7 日任务量柱图、服务健康灯带、能力入口、 快捷动作。5s 轮询刷新。配色沿用现有 ink 暗底 + brand/accent。 live(vite dev + 预览截图):登录后仪表盘渲染真实活数据(今日任务/Token/评测 1.00/近期运行/ 服务全绿),无控制台报错。tsc+vite 构建通过,gateway 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
486 lines
19 KiB
TypeScript
486 lines
19 KiB
TypeScript
// Gateway HTTP/SSE 客户端:提交 DSL 任务、订阅 Token 流、登记偏好记忆。
|
||
import type { TaskDsl } from "./dsl";
|
||
|
||
// 开发期直连 Gateway;Wails 打包后可改为本地后端地址或经 Go 绑定。
|
||
export const GATEWAY: string =
|
||
(import.meta.env.VITE_GATEWAY as string | undefined) ?? "http://localhost:8080";
|
||
|
||
export interface Identity {
|
||
userId: string;
|
||
sessionId: string;
|
||
}
|
||
|
||
export interface AuthUser {
|
||
id: string;
|
||
email: string;
|
||
name?: string;
|
||
}
|
||
|
||
// ---- JWT 令牌存储(localStorage)----
|
||
const TOKEN_KEY = "sdx_token";
|
||
let authToken: string = typeof localStorage !== "undefined" ? localStorage.getItem(TOKEN_KEY) ?? "" : "";
|
||
|
||
export function setToken(t: string): void {
|
||
authToken = t;
|
||
try {
|
||
localStorage.setItem(TOKEN_KEY, t);
|
||
} catch {
|
||
/* 隐私模式忽略 */
|
||
}
|
||
}
|
||
export function clearToken(): void {
|
||
authToken = "";
|
||
try {
|
||
localStorage.removeItem(TOKEN_KEY);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
export function getToken(): string {
|
||
return authToken;
|
||
}
|
||
|
||
// bearer 把 JWT 放进请求头(无令牌则不带)。
|
||
function bearer(): Record<string, string> {
|
||
return authToken ? { Authorization: `Bearer ${authToken}` } : {};
|
||
}
|
||
|
||
// guard401 在收到 401 时清理令牌并广播登出事件(App 监听后回到登录页)。
|
||
function guard401(res: Response): Response {
|
||
if (res.status === 401) {
|
||
clearToken();
|
||
if (typeof window !== "undefined") window.dispatchEvent(new Event("sdx:logout"));
|
||
}
|
||
return res;
|
||
}
|
||
|
||
// ---- 鉴权 API ----
|
||
export async function authRegister(email: string, password: string, name: string): Promise<AuthUser> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/auth/register`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ email, password, name }),
|
||
});
|
||
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 authLogin(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;
|
||
}
|
||
|
||
// authMe 用当前令牌取登录用户;无效/过期返回 null(用于应用启动校验)。
|
||
export async function authMe(): Promise<AuthUser | null> {
|
||
if (!authToken) return null;
|
||
const res = await fetch(`${GATEWAY}/api/v1/auth/me`, { headers: bearer() });
|
||
if (!res.ok) {
|
||
clearToken();
|
||
return null;
|
||
}
|
||
const data = (await res.json()) as { user?: AuthUser };
|
||
return data.user ?? null;
|
||
}
|
||
|
||
export function logout(): void {
|
||
clearToken();
|
||
}
|
||
|
||
// submitTask: POST /api/v1/tasks,返回 task_id。
|
||
export async function submitTask(dsl: TaskDsl, id: Identity): Promise<string> {
|
||
const res = guard401(
|
||
await fetch(`${GATEWAY}/api/v1/tasks`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify(dsl),
|
||
}),
|
||
);
|
||
if (!res.ok) throw new Error(`submit failed: ${res.status} ${await res.text()}`);
|
||
const data = (await res.json()) as { task_id: string };
|
||
return data.task_id;
|
||
}
|
||
|
||
// taskStatus: GET /api/v1/tasks/:id —— 任务生命周期状态(submitted/running/waiting/done/failed/timeout/rejected)。
|
||
// 轮询它来可靠捕获 waiting(审批中断)—— 不依赖易抢跑的实时 exec 事件。
|
||
export async function taskStatus(taskId: string): Promise<{ status: string; detail: string }> {
|
||
const res = guard401(await fetch(`${GATEWAY}/api/v1/tasks/${taskId}`, { headers: bearer() }));
|
||
if (!res.ok) throw new Error(`status failed: ${res.status}`);
|
||
const d = (await res.json()) as { status?: string; detail?: string };
|
||
return { status: d.status ?? "", detail: d.detail ?? "" };
|
||
}
|
||
|
||
// approveTask: POST /api/v1/tasks/:id/approve —— HITL 人工审批决定(批准放行 / 拒绝中止)。
|
||
export async function approveTask(taskId: string, approved: boolean, opts?: { node?: string; note?: string }): Promise<void> {
|
||
const res = guard401(
|
||
await fetch(`${GATEWAY}/api/v1/tasks/${taskId}/approve`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...bearer() },
|
||
body: JSON.stringify({ approved, node: opts?.node ?? "", note: opts?.note ?? "" }),
|
||
}),
|
||
);
|
||
if (!res.ok) throw new Error(`approve failed: ${res.status} ${await res.text()}`);
|
||
}
|
||
|
||
// streamTokens: 订阅 SSE /api/v1/tasks/:id/stream,逐 token 回调,done 收尾。
|
||
// 返回关闭函数。注意 EventSource 无法带请求头,但流按 task_id 寻址,无需身份头。
|
||
export function streamTokens(
|
||
taskId: string,
|
||
onToken: (t: string) => void,
|
||
onDone: () => void,
|
||
onError?: (e: unknown) => void,
|
||
): () => void {
|
||
const es = new EventSource(`${GATEWAY}/api/v1/tasks/${taskId}/stream`);
|
||
es.addEventListener("token", (e) => onToken((e as MessageEvent).data));
|
||
es.addEventListener("done", () => {
|
||
es.close();
|
||
onDone();
|
||
});
|
||
es.onerror = (e) => {
|
||
es.close();
|
||
onError?.(e);
|
||
};
|
||
return () => es.close();
|
||
}
|
||
|
||
// 执行轨迹事件(与后端 contract.ExecEvent 对应):运行·观测的节点级实时事件。
|
||
export interface ExecEvent {
|
||
seq: number;
|
||
ts: number;
|
||
node: string; // init / tool:wiki_search / prompt / model / plan / section:0 / render / task / compile
|
||
kind: string; // memory|tool|prompt|model|plan|section|render|system
|
||
phase: string; // start|end|error|info
|
||
label: string;
|
||
detail?: string;
|
||
ms?: number;
|
||
}
|
||
|
||
// streamExec: 订阅 SSE /api/v1/tasks/:id/exec —— 与 token 流并行,逐节点点亮执行轨迹。
|
||
export function streamExec(
|
||
taskId: string,
|
||
onEvent: (ev: ExecEvent) => void,
|
||
onDone: () => void,
|
||
onError?: (e: unknown) => void,
|
||
): () => void {
|
||
const es = new EventSource(`${GATEWAY}/api/v1/tasks/${taskId}/exec`);
|
||
es.addEventListener("exec", (e) => onEvent(JSON.parse((e as MessageEvent).data) as ExecEvent));
|
||
es.addEventListener("done", () => {
|
||
es.close();
|
||
onDone();
|
||
});
|
||
es.onerror = (e) => {
|
||
es.close();
|
||
onError?.(e);
|
||
};
|
||
return () => es.close();
|
||
}
|
||
|
||
// 入库进度事件(与后端 contract.IngestEvent 对应)。
|
||
export interface IngestEvent {
|
||
stage: string;
|
||
msg?: string;
|
||
done?: number;
|
||
total?: number;
|
||
chunks?: string[];
|
||
preview?: string; // 解析阶段:解析出的文本片段
|
||
triples?: Triple[]; // 抽实体阶段:LLM 抽出的知识三元组
|
||
error?: string;
|
||
}
|
||
|
||
// idHeaders 把身份带进请求头:JWT(Bearer) 作鉴权与 owner 作用域;X-Session-ID 作多轮会话标识。
|
||
// 不再发 X-User-ID —— owner 由网关从已验证 JWT 取(伪造头无效)。
|
||
function idHeaders(id: Identity): Record<string, string> {
|
||
return { ...bearer(), "X-Session-ID": id.sessionId };
|
||
}
|
||
|
||
export interface KbInfo {
|
||
name: string;
|
||
kind: string; // folder / project / case / general
|
||
}
|
||
|
||
// listKb: GET /api/v1/kb/list —— 当前用户的知识库列表(owner 隔离)。
|
||
export async function listKb(id: Identity): Promise<KbInfo[]> {
|
||
const res = guard401(await fetch(`${GATEWAY}/api/v1/kb/list`, { headers: idHeaders(id) }));
|
||
const data = (await res.json()) as { kbs?: KbInfo[]; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `list failed: ${res.status}`);
|
||
return data.kbs ?? [];
|
||
}
|
||
|
||
// createKb: POST /api/v1/kb/create —— 新建知识库(项目/案件/文件夹/通用)。
|
||
export async function createKb(id: Identity, name: string, kind: string): Promise<KbInfo> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/create`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify({ name, kind }),
|
||
});
|
||
const data = (await res.json()) as { name?: string; kind?: string; error?: string };
|
||
if (!res.ok || !data.name) throw new Error(data.error ?? `create failed: ${res.status}`);
|
||
return { name: data.name, kind: data.kind ?? kind };
|
||
}
|
||
|
||
export interface VaultDoc {
|
||
id: string; // 文件主表雪花 ID —— 选中/取正文/关联一律用它
|
||
name: string;
|
||
ext?: string; // 文件后缀(.md/.pdf…;笔记为空)
|
||
size?: number;
|
||
preview?: string;
|
||
}
|
||
export interface DocLink {
|
||
from: string; // 源文件 ID
|
||
to: string; // 目标文件 ID
|
||
}
|
||
|
||
// ---- 我的 Agent 编排(服务端保存,owner 隔离)----
|
||
export interface AgentInfo {
|
||
name: string;
|
||
graph: string; // {nodes,edges} JSON
|
||
updated_at?: string;
|
||
}
|
||
|
||
export async function listAgents(id: Identity): Promise<AgentInfo[]> {
|
||
const res = guard401(await fetch(`${GATEWAY}/api/v1/agents`, { headers: idHeaders(id) }));
|
||
const data = (await res.json()) as { agents?: AgentInfo[]; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `list agents failed: ${res.status}`);
|
||
return data.agents ?? [];
|
||
}
|
||
|
||
export async function saveAgent(id: Identity, name: string, graph: string): Promise<void> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/agents`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify({ name, graph }),
|
||
});
|
||
const data = (await res.json()) as { name?: string; error?: string };
|
||
if (!res.ok || !data.name) throw new Error(data.error ?? `save agent failed: ${res.status}`);
|
||
}
|
||
|
||
export async function deleteAgent(id: Identity, name: string): Promise<void> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/agents?name=${encodeURIComponent(name)}`, { method: "DELETE", headers: idHeaders(id) });
|
||
if (!res.ok) {
|
||
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||
throw new Error(data.error ?? `delete agent failed: ${res.status}`);
|
||
}
|
||
}
|
||
|
||
// listChatModels: GET /api/v1/admin/models?kind=chat —— 已登记的对话模型名(供编排 Agent 节点选择)。
|
||
export async function listChatModels(): Promise<string[]> {
|
||
try {
|
||
const res = await fetch(`${GATEWAY}/api/v1/admin/models?kind=chat`);
|
||
const data = (await res.json()) as { models?: Array<{ model: string }> };
|
||
return (data.models ?? []).map((m) => m.model);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// listVault: GET /api/v1/kb/vault —— 文库列表(仅元数据 + 预览,不拉全文)。
|
||
export async function listVault(id: Identity, kb: string): Promise<VaultDoc[]> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/vault?kb=${encodeURIComponent(kb)}`, { headers: idHeaders(id) });
|
||
const data = (await res.json()) as { docs?: VaultDoc[]; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `vault failed: ${res.status}`);
|
||
return data.docs ?? [];
|
||
}
|
||
|
||
// getDoc: GET /api/v1/kb/doc?id= —— 按文件 ID 取单篇全文(按需加载,避免列表拉全量)。
|
||
export async function getDoc(id: Identity, docId: string): Promise<string> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/doc?id=${encodeURIComponent(docId)}`, { headers: idHeaders(id) });
|
||
const data = (await res.json()) as { content?: string; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `doc failed: ${res.status}`);
|
||
return data.content ?? "";
|
||
}
|
||
|
||
// listLinks: GET /api/v1/kb/links —— 某库全部 [[双链]](反链/笔记关系图,数据小)。
|
||
export async function listLinks(id: Identity, kb: string): Promise<DocLink[]> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/links?kb=${encodeURIComponent(kb)}`, { headers: idHeaders(id) });
|
||
const data = (await res.json()) as { links?: DocLink[]; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `links failed: ${res.status}`);
|
||
return data.links ?? [];
|
||
}
|
||
|
||
// saveNote: POST /api/v1/kb/note —— 新建/编辑笔记(落库 + 按 doc 重入库替换旧块)。
|
||
export async function saveNote(id: Identity, kb: string, name: string, content: string): Promise<void> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/note`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify({ kb, name, content }),
|
||
});
|
||
const data = (await res.json()) as { name?: string; error?: string };
|
||
if (!res.ok || !data.name) throw new Error(data.error ?? `save failed: ${res.status}`);
|
||
}
|
||
|
||
// ingestKb: POST /api/v1/kb/ingest —— 文本入库(异步,返回 job_id)。
|
||
export async function ingestKb(id: Identity, kb: string, text: string): Promise<string> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/ingest`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify({ kb, text }),
|
||
});
|
||
const data = (await res.json()) as { job_id?: string; error?: string };
|
||
if (!res.ok || !data.job_id) throw new Error(data.error ?? `ingest failed: ${res.status}`);
|
||
return data.job_id;
|
||
}
|
||
|
||
// ingestFile: POST /api/v1/kb/ingest_file(multipart)—— 文件入库(异步,返回 job_id)。
|
||
export async function ingestFile(id: Identity, kb: string, file: File): Promise<string> {
|
||
const fd = new FormData();
|
||
fd.append("kb", kb);
|
||
fd.append("file", file);
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/ingest_file`, { method: "POST", headers: idHeaders(id), body: fd });
|
||
const data = (await res.json()) as { job_id?: string; error?: string };
|
||
if (!res.ok || !data.job_id) throw new Error(data.error ?? `ingest file failed: ${res.status}`);
|
||
return data.job_id;
|
||
}
|
||
|
||
// streamIngest: SSE 订阅入库进度(/kb/ingest/:id/stream)。返回关闭函数。
|
||
export function streamIngest(
|
||
jobId: string,
|
||
onEvent: (ev: IngestEvent) => void,
|
||
onDone: () => void,
|
||
onError?: () => void,
|
||
): () => void {
|
||
const es = new EventSource(`${GATEWAY}/api/v1/kb/ingest/${jobId}/stream`);
|
||
es.addEventListener("progress", (e) => onEvent(JSON.parse((e as MessageEvent).data) as IngestEvent));
|
||
es.addEventListener("done", () => {
|
||
es.close();
|
||
onDone();
|
||
});
|
||
es.onerror = () => {
|
||
es.close();
|
||
onError?.();
|
||
};
|
||
return () => es.close();
|
||
}
|
||
|
||
export interface KbHit {
|
||
text: string;
|
||
score: number;
|
||
}
|
||
|
||
export interface Triple {
|
||
s: string;
|
||
p: string;
|
||
o: string;
|
||
}
|
||
|
||
// graphKb: GET /api/v1/kb/graph —— 取某知识库的图谱三元组(→ mcp-go kb_graph,Neo4j)。
|
||
export async function graphKb(id: Identity, kb: string): Promise<Triple[]> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/graph?kb=${encodeURIComponent(kb)}`, { headers: idHeaders(id) });
|
||
const data = (await res.json()) as { triples?: Triple[]; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `graph failed: ${res.status}`);
|
||
return data.triples ?? [];
|
||
}
|
||
|
||
// searchKb: POST /api/v1/kb/search,检索台查询(→ mcp-go kb_search,带分数)。
|
||
export async function searchKb(id: Identity, kb: string, q: string, topK = 5): Promise<KbHit[]> {
|
||
const res = await fetch(`${GATEWAY}/api/v1/kb/search`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify({ kb, q, topK }),
|
||
});
|
||
const data = (await res.json()) as { hits?: KbHit[]; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `search failed: ${res.status}`);
|
||
return data.hits ?? [];
|
||
}
|
||
|
||
// generateReport: POST /api/v1/reports —— 触发报告生成,返回 task_id。
|
||
// 用 streamTokens(task_id) 看实时进度,完成后用 reportDownloadUrl(task_id) 下载 Word。
|
||
export async function generateReport(id: Identity, topic: string, kb?: string): Promise<string> {
|
||
const res = guard401(
|
||
await fetch(`${GATEWAY}/api/v1/reports`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify({ topic, kb: kb ?? "" }),
|
||
}),
|
||
);
|
||
const data = (await res.json()) as { task_id?: string; error?: string };
|
||
if (!res.ok || !data.task_id) throw new Error(data.error ?? `report failed: ${res.status}`);
|
||
return data.task_id;
|
||
}
|
||
|
||
// reportDownloadUrl: 渲染好的 Word(.docx) 下载地址(兼容旧入口)。
|
||
export function reportDownloadUrl(taskId: string): string {
|
||
return `${GATEWAY}/api/v1/reports/${taskId}/download`;
|
||
}
|
||
|
||
// reportExportUrl: 按需导出报告地址(format=docx|md;后端现渲染)。PDF 由前端打印预览生成。
|
||
export function reportExportUrl(taskId: string, format: "docx" | "md"): string {
|
||
return `${GATEWAY}/api/v1/reports/${taskId}/export?format=${format}`;
|
||
}
|
||
|
||
// setMemory: PUT /api/v1/memory,登记一条用户偏好(→ mcp-go memory_upsert)。
|
||
export async function setMemory(
|
||
id: Identity,
|
||
key: string,
|
||
value: string,
|
||
): Promise<string> {
|
||
const res = guard401(
|
||
await fetch(`${GATEWAY}/api/v1/memory`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json", ...idHeaders(id) },
|
||
body: JSON.stringify({ key, value }),
|
||
}),
|
||
);
|
||
const data = (await res.json()) as { message?: string; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `memory failed: ${res.status}`);
|
||
return data.message ?? "ok";
|
||
}
|
||
|
||
// 一条长期偏好(含读路径打分维度)。
|
||
export interface MemoryItem {
|
||
key: string;
|
||
value: string;
|
||
importance: number;
|
||
last_seen: string;
|
||
}
|
||
|
||
// listMemory: GET /api/v1/memory —— 列出当前用户偏好(已按 Score 降序)。
|
||
export async function listMemory(id: Identity): Promise<MemoryItem[]> {
|
||
const res = guard401(await fetch(`${GATEWAY}/api/v1/memory`, { headers: idHeaders(id) }));
|
||
const data = (await res.json()) as { memories?: MemoryItem[]; error?: string };
|
||
if (!res.ok) throw new Error(data.error ?? `list memory failed: ${res.status}`);
|
||
return data.memories ?? [];
|
||
}
|
||
|
||
// deleteMemory: DELETE /api/v1/memory?key= —— 软删一条偏好。
|
||
export async function deleteMemory(id: Identity, key: string): Promise<void> {
|
||
const res = guard401(await fetch(`${GATEWAY}/api/v1/memory?key=${encodeURIComponent(key)}`, { method: "DELETE", headers: idHeaders(id) }));
|
||
if (!res.ok) {
|
||
const d = (await res.json().catch(() => ({}))) as { error?: string };
|
||
throw new Error(d.error ?? `delete memory failed: ${res.status}`);
|
||
}
|
||
}
|
||
|
||
// ── 工作台仪表盘聚合(GET /stats/overview)──
|
||
export interface KeyCount { 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: KeyCount[];
|
||
task_trend: KeyCount[];
|
||
token_trend: KeyCount[];
|
||
eval_avg: number;
|
||
faithful_avg: number;
|
||
eval_count: number;
|
||
kb_docs: number;
|
||
kb_count: number;
|
||
tokens_today: number;
|
||
daily_budget: number;
|
||
recent_runs: RecentRun[];
|
||
services: Record<string, boolean>;
|
||
}
|
||
|
||
export async function statsOverview(): Promise<Overview> {
|
||
const res = guard401(await fetch(`${GATEWAY}/api/v1/stats/overview`, { headers: bearer() }));
|
||
if (!res.ok) throw new Error(`stats failed: ${res.status}`);
|
||
return res.json() as Promise<Overview>;
|
||
}
|