import type { AdminLogLine, AdminLogQuery, AdminNotification, AdminNotificationQuery, AdminObservabilityMetrics, AdminStorageStatus, AdminTraceSpan, AuditRecord, AdminConfigFacts, AdminTidyDryRunResult, BackthinkItem, FlowGraph, SystemBackupResp, SystemRestoreResp, SystemRevokeKeysResp, SystemRotateJwtResp, } from "../types"; import { limitQuery, request } from "./core"; /** GET /api/v1/admin/observability/metrics — gateway request metrics. */ export function adminObservabilityMetrics(): Promise { return request("/admin/observability/metrics"); } /** GET /api/v1/admin/observability/trace?limit=N — most recent traced spans. */ export function adminObservabilityTrace(limit?: number): Promise { return request(`/admin/observability/trace${limitQuery(limit)}`); } /** GET /api/v1/admin/observability/audit?limit=N — most recent audit entries. */ export function adminObservabilityAudit(limit?: number): Promise { return request(`/admin/observability/audit${limitQuery(limit)}`); } /** GET /api/v1/admin/observability/logs?level=&limit= — the gateway's structured * log tail, newest first (owner/admin only). An omitted `level` means “all”; the * backend defaults `limit` to 200 and caps it at 1000. */ export function adminObservabilityLogs( params: AdminLogQuery = {} ): Promise { const qs = new URLSearchParams(); if (params.level) qs.set("level", params.level); if (params.limit !== undefined) qs.set("limit", String(params.limit)); const q = qs.toString(); return request(`/admin/observability/logs${q ? `?${q}` : ""}`); } /** GET /api/v1/admin/backthink — persisted background-think records (newest first). */ export function adminBackthink(limit?: number): Promise { return request(`/admin/backthink${limitQuery(limit)}`); } /** 一条**已落盘轮次**的摘要(契约 v0.73 §4.11 的 `turns[]`)。面板用它渲染历史轮 * 选择器:把 `turnId` 回带给 `adminFlowGraph` 即可精确取回那一轮。 */ export interface AdminFlowTurn { turnId: string; createdAt: string; } /** 链路图端点的后端响应(契约 v0.71 起;**v0.73 新增 `turnId`/`source`/`turns`**)。 * * `graph` **恒在**(`null` 而不是缺席):`null` = 该轮**确实**没有图,客户端必须据此 * 渲染空态,绝不允许补画任何节点或假图。 * * `source` 告诉客户端这张图是哪来的:`live` = 内存里最近一轮(实时图);`persisted` = * 从本地库读回的历史轮;`none` = 没有图。`turns` 是该会话已落盘的轮次摘要(最新在前), * 没有时为 `[]`(后端保证非 null 数组)。 */ export interface AdminFlowGraphResponse { sessionId: string; turnId: string; source: "live" | "persisted" | "none"; graph: FlowGraph | null; /** 最近一轮**每一路**的图(契约 v0.77,纯增量;真并行 `chat.parallel` 时不止一条)。 * `graph` 只是其中最新的一条(向后兼容),`lanes` 才是全貌 —— 面板据此把两路并列 * 画出来,而不是让后起的那一路把前一路顶掉。没有图时是 `[]`(后端保证非 null)。 */ lanes: AdminFlowLane[]; turns: AdminFlowTurn[]; } /** `lanes[]` 里的一条路:`turnId` 是该路的权威回合标识(design/15 §2.5.9), * `graph` 与响应的 `graph` / 帧的 data 逐字同形。 */ export interface AdminFlowLane { turnId: string; graph: FlowGraph | null; } /** GET /api/v1/admin/flow/graph?sessionId=&turnId= — **链路图快照 + 多轮历史回放**(契约 * v0.71 起,v0.73 增量;只读、不审计,owner/admin 档)。 * * 为什么需要它:`graph_update` 是**纯实时**帧、从不重放。v0.71 补了「打开页面拉最近一轮 * 快照」;v0.73 把图**每轮落盘**,于是可以: * - **省略 `turnId`** ⇒ 最近一轮(后端**先查内存、miss 才落盘** ⇒ 最新一轮永远最新); * - **给了 `turnId`** ⇒ 精确取该会话已落盘的某一轮(历史回放)。 * * `turns`(同一响应里)就是给选择器用的历史轮列表。⚠️ 白名单之外的会话(群聊/陌生人) * 从不建图、也从不落盘 ⇒ 它们**永远**是空态,这不是缺陷。 */ export function adminFlowGraph(sessionId: string, turnId?: string): Promise { const qs = new URLSearchParams(); if (sessionId) qs.set("sessionId", sessionId); if (turnId) qs.set("turnId", turnId); const q = qs.toString(); return request(`/admin/flow/graph${q ? `?${q}` : ""}`); } /** 时间轴里的**一个回合**(`design/19 §7`;形状取自设计规格 —— W1 的出口)。 */ export interface AdminFlowTimelineRound { /** 该回合的权威标识。🔴 **一律用后端给的值**:面板既不自己拼 `{会话}-{序号}`, * 也不从 `graph.id` 反解(契约 v0.79 起 `graph.id` 含 `#` 且单路也有后缀)。 */ turnId: string; /** 会话轮次号。**可能缺席 / `null`**(旧行、或后端尚未落这一列)⇒ 面板显示 * 「轮次未知」,**绝不用 0 冒充**。 */ round?: number | null; /** 该回合的开始时间(RFC3339)。它是**时间轴排序与 `before` 游标的唯一依据**。 */ startedAt: string; /** 收尾时间。缺席 / `null` = 该回合**未收尾**(不许读成「已完成」)。 */ endedAt?: string | null; /** 该回合**每一路**的图(真并行时不止一条;`graph` 为 `null` = 那一路没有留下图)。 */ lanes: AdminFlowLane[]; } /** 链路**时间轴**端点的响应(`design/19 §7.1`;W1 新增的只读出口)。 */ export interface AdminFlowTimelineResponse { /** 解析后的会话(回显)。取数只认调用时给的 `sessionId`,不依赖这一键。 */ sessionId?: string; /** 保留期内的回合。后端按**时间正序**(最旧在前)返回;面板渲染前自己归一排序。 */ rounds: AdminFlowTimelineRound[]; /** `flow_graphs` 的保留期(小时)。`≤0` = 后端配置为不回收(见 `FLOW_GRAPH_RETENTION`)。 */ retentionHours: number; /** **更早的回合已过保留期**(`design/19 §7.1`)⇒ 面板必须**如实标注**,不许静默少块。 */ truncated: boolean; } /** GET /api/v1/admin/flow/timeline?sessionId=&limit=&before= — **多轮时间轴**(`design/19 §7`; * 只读、不审计,owner/admin 档)。它是老出口 `adminFlowGraph`(最近一轮 + 历史轮列表)的 * **增量兄弟**:一次给**多轮**,每轮连同它**每一路**的图,供面板画「所有消息按时间顺序串 * 起来」的时间轴。 * * 分页:`limit` 缺省 50(后端硬上限 200);`before` 是**开始时间游标** —— 传「当前手上 * 最旧那一回合的 `startedAt`(原样回传后端给的值)」即继续取更早的。⚠️ 本函数**不解释** * `before`(不做字符串/毫秒换算),只原样转交:面板的游标就是后端给的时间字符串。 * * 老后端(该出口未接线)⇒ `404`;调用方按「当前只支持最近一轮」诚实降级,别把 404 读成 * 「这段时间没有回合」。 */ export function adminFlowTimeline( sessionId: string, opts: { limit?: number; before?: string } = {} ): Promise { const qs = new URLSearchParams(); if (sessionId) qs.set("sessionId", sessionId); if (opts.limit && opts.limit > 0) qs.set("limit", String(opts.limit)); if (opts.before) qs.set("before", opts.before); const q = qs.toString(); return request(`/admin/flow/timeline${q ? `?${q}` : ""}`); } /** GET /api/v1/admin/config-facts — **只读**配置事实面(契约 v0.68 §4.10(a)): * 记忆整理 7 键 + 安静时段两键的**进程生效值** + 声明默认值,外加安静时段窗口的 * 解释(跨午夜 / 已关闭)。owner/admin 档;**不含任何凭据值**(后端有哨兵测试钉死), * 网关也**不读 `.env`** —— 值就是运行实例真正在用的数。 */ export function adminConfigFacts(): Promise { return request("/admin/config-facts"); } /** POST /api/v1/admin/memories/tidy/dry-run — 记忆整理「试算」(契约 v0.75 §4.12)。 * * 它跑**同一套**整理规则(与低峰定时轮共用代码路径)并把候选直接回在响应里, * 但**一行都不写**(不写候选表/运行表、不改任何记忆)—— 因此可以在任何时刻、 * 在整理器第一次开跑之前,先看清「它会建议合并哪些」。 * * ⚠️ 它不是看板数据源:试算是一次真实的规则计算(扫描 + 两两召回 + 包含判定), * **只在用户点击时调用,不要轮询**。 * ⚠️ 失败走 `error.code`(tidy_unavailable / tidy_disabled / tidy_recall_unavailable / * tidy_timeout),**不会**回 `candidates: []` —— 空数组只表示「算过了,没有建议」。 */ export function adminMemoryTidyDryRun(): Promise { return request("/admin/memories/tidy/dry-run", { method: "POST" }); } /** GET /api/v1/admin/notifications?type=&before=&limit= — the unified notification * feed (design/10 §3 #16: audit + channel + backthink + system aggregated). * `type` omitted = all types, `before` = createdAt(ms) cursor for older pages, * `limit` defaults to 50 (backend caps at 200). Returned newest first. */ export function adminListNotifications( params: AdminNotificationQuery = {} ): Promise { const qs = new URLSearchParams(); if (params.type) qs.set("type", params.type); if (params.before !== undefined) qs.set("before", String(params.before)); if (params.limit !== undefined) qs.set("limit", String(params.limit)); const q = qs.toString(); return request(`/admin/notifications${q ? `?${q}` : ""}`); } /* ---------------------------------------------------------------------------- * System management admin writes (owner-only, audited). Reference: * internal/services/gateway/admin_system.go + Docs/api-client.md §系统设置契约. * All four are implemented (not 501) — they return real data. * -------------------------------------------------------------------------- */ /** GET /api/v1/admin/system/storage — the **read-only** storage snapshot behind the * dashboard's 存储 card (v0.60): SQLite file/WAL/page stats, the documents table's * count+bytes, and whether the optional pgvector enhancement is configured and * actually live. The `vector` block never carries credentials (no DSN, no password). * owner-only; the backend answers `500 storage_unavailable` rather than a fake * all-zero snapshot when the live DB handle is not wired. */ export function adminSystemStorage(): Promise { return request("/admin/system/storage"); } /** POST /api/v1/admin/system/revoke-keys — revokes every registered API key. */ export function adminSystemRevokeKeys(): Promise { return request("/admin/system/revoke-keys", { method: "POST" }); } /** POST /api/v1/admin/system/rotate-jwt — swaps the JWT signing secret (≥32 chars). * The secret is provided by the owner and never echoed back; all existing tokens * become invalid, so the client must re-login afterward. */ export function adminSystemRotateJWT(newSecret: string): Promise { return request("/admin/system/rotate-jwt", { method: "POST", body: JSON.stringify({ newSecret }), }); } /** POST /api/v1/admin/system/backup — snapshots the SQLite DB (VACUUM INTO, incl. WAL). */ export function adminSystemBackup(): Promise { return request("/admin/system/backup", { method: "POST" }); } /** POST /api/v1/admin/system/restore — restores a data-dir backup; path must live * inside the data directory (path-traversal guard). The backend pre-backups the * current DB and reports that the swap needs a restart. */ export function adminSystemRestore(path: string): Promise { return request("/admin/system/restore", { method: "POST", body: JSON.stringify({ path }), }); }