Files
Cyrene_v2/frontend/web/src/lib/api.ts
T
Aska 0a645b7212
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 后台思考面板并入已存记录——不再只读WS实时事件
后台思考页此前只读 /ws/chat 的 backthink/thinking WS 实时事件(后台任务完成才有),DB里
270 条 backthink_records 看不到。新增 adminBackthink() 拉 GET /api/v1/admin/backthink +
useAutoReload(10s),与 WS 事件按 id 去重、按 at 倒序展示;空态/数据源说明更新。
验收: npm run build 99模块全绿
2026-09-07 18:44:58 +08:00

692 lines
26 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.
import type {
AdminAutomationRecord,
AdminChannelMessage,
AdminChannelRecord,
AdminIoTDevice,
AdminLlmConfig,
AdminLlmProvider,
AdminObservabilityMetrics,
AdminPluginRecord,
AdminRagHit,
AdminRagResponse,
AdminTraceSpan,
AdminUserRecord,
ApiErrorShape,
ApiKeyIssueResult,
AuditRecord,
BackthinkItem,
AutomationCreateResult,
AutomationRunResult,
ChannelTestResult,
ChatMessage,
ChatSyncResult,
LlmProviderRequest,
LlmTestRequest,
LlmTestResult,
LoginResult,
MemoryAddRequest,
MemoryAddResult,
MemoryRecord,
MemorySearchRequest,
MemorySearchResult,
Mode,
PersonaRecord,
PersonaUpsertRequest,
KookFriendList,
KookMessageTestResult,
KookViewResponse,
RagIndexRequest,
Role,
SessionMeta,
SessionRecord,
} from "./types";
import { getClientId } from "./client";
/**
* REST client for the Cyrene v2 gateway. The gateway wraps every success in
* `{data}` and every failure in `{error:{code,message}}` (Docs/api-client.md).
* We use same-origin relative paths so the same code works behind the Vite dev
* proxy and inside the single-binary gateway on :8080.
*/
export class ApiError extends Error {
readonly status: number;
readonly code?: string;
constructor(status: number, code: string | undefined, message: string) {
super(message);
this.name = "ApiError";
this.status = status;
this.code = code;
}
}
const API_BASE = "/api/v1";
const TOKEN_KEY = "cyrene.jwt";
const ROLE_KEY = "cyrene.role";
const API_KEY_KEY = "cyrene.apikey";
export function getAuthToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
export function setAuthToken(token: string | null): void {
if (token) {
localStorage.setItem(TOKEN_KEY, token);
setStoredRole(decodeJwtRole(token));
} else {
localStorage.removeItem(TOKEN_KEY);
setStoredRole(null);
}
}
/** Decode the RBAC role from a JWT payload (client-side only; never verified).
* base64url is converted to base64 before decoding to survive `-`/`_`. */
export function decodeJwtRole(token: string): Role | null {
const parts = token.split(".");
if (parts.length < 2) return null;
try {
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
const payload = JSON.parse(atob(padded)) as Record<string, unknown>;
const r = payload.role;
return r === "owner" || r === "admin" || r === "viewer" ? r : null;
} catch {
return null;
}
}
/** Persisted RBAC role, refreshed when the token changes. Falls back to
* decoding the token so a session created before role persistence still works. */
export function getStoredRole(): Role | null {
const r = localStorage.getItem(ROLE_KEY);
if (r === "owner" || r === "admin" || r === "viewer") return r;
const token = getAuthToken();
return token ? decodeJwtRole(token) : null;
}
export function setStoredRole(role: Role | null): void {
if (role) localStorage.setItem(ROLE_KEY, role);
else localStorage.removeItem(ROLE_KEY);
}
/** True when the JWT's `exp` (unix seconds) is already in the past. Used client-side
* to recognise a handshake rejection as "token expired" before the browser ever
* exposes the 401 (WS handshake failures surface only as a generic close). */
export function isTokenExpired(token: string): boolean {
const parts = token.split(".");
if (parts.length < 2) return false;
try {
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
const payload = JSON.parse(atob(padded)) as { exp?: number };
const exp = payload.exp;
return typeof exp === "number" && exp > 0 && Math.floor(Date.now() / 1000) >= exp;
} catch {
return false;
}
}
/** Subscribe to an "auth expired" event. The REST layer fires this exactly once
* when a non-login request comes back 401/unauthorized, after already clearing
* the stored token; the WS layer subscribes to stop reconnecting + flag the UI. */
type AuthExpiredHandler = () => void;
const authExpiredHandlers = new Set<AuthExpiredHandler>();
export function onAuthExpired(handler: AuthExpiredHandler): () => void {
authExpiredHandlers.add(handler);
return () => {
authExpiredHandlers.delete(handler);
};
}
function fireAuthExpired(): void {
authExpiredHandlers.forEach((h) => h());
}
/** Optional API key for admin endpoints (X-API-Key). */
export function getApiKey(): string | null {
return localStorage.getItem(API_KEY_KEY);
}
export function setApiKey(key: string | null): void {
if (key) localStorage.setItem(API_KEY_KEY, key);
else localStorage.removeItem(API_KEY_KEY);
}
interface Envelope {
data?: unknown;
error?: ApiErrorShape;
}
/** Result of saving/deleting a channel account config (persisted to channels.json). */
export interface ChannelConfigSaveResult {
persisted: boolean;
path: string;
platform: string;
accountId: string;
}
async function request<T>(
path: string,
init: RequestInit = {},
opts: { skipAuthExpired?: boolean } = {}
): Promise<T> {
const headers = new Headers(init.headers);
headers.set("Content-Type", "application/json");
headers.set("X-API-Version", "1");
const token = getAuthToken();
if (token) headers.set("Authorization", `Bearer ${token}`);
const apiKey = getApiKey();
if (apiKey) headers.set("X-API-Key", apiKey);
let res: Response;
try {
res = await fetch(`${API_BASE}${path}`, { ...init, headers });
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "网络请求失败";
throw new ApiError(0, "network_error", msg);
}
let body: Envelope;
try {
body = (await res.json()) as Envelope;
} catch {
throw new ApiError(res.status, "bad_response", "服务端返回了无法解析的响应");
}
if (!res.ok || body.error) {
const code = body.error?.code ?? (res.status === 401 ? "unauthorized" : "unknown");
// A 401/unauthorized on a logged-in (non-login) call means the stored token is
// gone/expired: clear it and notify the WS layer so it stops reconnecting, then
// throw a dedicated code so the UI can prompt re-login. The login call itself is
// exempt — a bad password is a per-request failure, not a stale session.
if (!opts.skipAuthExpired && (res.status === 401 || code === "unauthorized")) {
setAuthToken(null);
fireAuthExpired();
throw new ApiError(res.status, "auth_expired", "登录已过期,请重新登录");
}
throw new ApiError(
res.status,
code,
body.error?.message ?? `请求失败(${res.status})`
);
}
return body.data as T;
}
/** POST /api/v1/auth/login — exchanges user/password for a JWT. */
export function login(username: string, password: string): Promise<LoginResult> {
return request<LoginResult>(
"/auth/login",
{
method: "POST",
body: JSON.stringify({ username, password }),
},
{ skipAuthExpired: true }
);
}
/** GET /api/v1/health — liveness probe. */
export function health(): Promise<{ status: string }> {
return request<{ status: string }>("/health");
}
/** POST /api/v1/chat — one synchronous agent turn. The body carries the sender's
* persistent per-browser deviceId (design/16 §4.5) so the persisted user and
* assistant messages are tagged with the concrete client. */
export function chatSync(
sessionId: string,
text: string
): Promise<ChatSyncResult> {
return request<ChatSyncResult>("/chat", {
method: "POST",
body: JSON.stringify({ sessionId, text, deviceId: getClientId() }),
});
}
/** POST /api/v1/chat with stream:true — triggers streaming over the WS channel.
* `image` (a base64 data: URI or URL) is attached for the vision path; the
* gateway's chat body currently has no image field, so an unconfigured backend
* simply ignores it. `deviceId` mirrors the client's per-browser id onto the
* persisted message (design/16 §4.5). */
export function chatStream(
sessionId: string,
text: string,
image?: string
): Promise<{ stream: true }> {
return request<{ stream: true }>("/chat", {
method: "POST",
body: JSON.stringify({ sessionId, text, image, stream: true, deviceId: getClientId() }),
});
}
/** One chat message as returned by GET /api/v1/sessions/{id}/messages. Mirrors
* the `chat_messages` row; `mode`/`mood`/`image`/`thinking` are optional. */
export interface MessageDto {
id: string;
sessionId: string;
role: "user" | "assistant" | "system";
content: string;
mode?: Mode;
mood?: string;
image?: string;
thinking?: string;
/** Source-platform tag (design/16 §4.4): "web" or an IM channel platform. */
platform?: string;
/** Concrete client/device that produced the message (design/16 §4.5). */
deviceId?: string;
/** Sender display name of a third-party message (design/16 §4.6). Blank for the owner. */
senderName?: string;
createdAt: number;
}
/** Map a wire message into a display ChatMessage (carrying its sessionId). */
function toChatMessage(m: MessageDto): ChatMessage {
return {
id: m.id,
sessionId: m.sessionId,
role: m.role,
content: m.content,
mode: m.mode,
mood: m.mood,
image: m.image,
thinking: m.thinking,
platform: m.platform,
deviceId: m.deviceId,
senderName: m.senderName,
createdAt: m.createdAt,
};
}
/** GET /api/v1/sessions — the main + secondary sessions for this web account. */
export function fetchSessions(): Promise<SessionMeta[]> {
return request<SessionMeta[]>("/sessions");
}
/** POST /api/v1/sessions — creates a new secondary session. */
export function createSubSession(title?: string): Promise<SessionMeta> {
return request<SessionMeta>("/sessions", {
method: "POST",
body: JSON.stringify(title ? { title } : {}),
});
}
/** GET /api/v1/sessions/{id}/messages?before=&limit=&platform= — hot-paginated
* history, optionally filtered to one source platform (design/16 §4.4). The wire
* returns newest-first, but callers render chronologically (oldest first), so we
* reverse here: page 1 is the most recent `limit` messages in display order, and
* `before` (the oldest message's createdAt) walks upward. The Web client omits
* `platform` for the full shared main session; an IM adapter passes its own
* platform to see only its lane. */
export async function fetchMessages(
sessionId: string,
before?: number,
limit = 50,
platform?: string
): Promise<ChatMessage[]> {
const qs = new URLSearchParams();
qs.set("limit", String(limit));
if (before !== undefined) qs.set("before", String(before));
if (platform) qs.set("platform", platform);
const list = await request<MessageDto[]>(
`/sessions/${encodeURIComponent(sessionId)}/messages?${qs.toString()}`
);
return list.map(toChatMessage).reverse();
}
/** DELETE /api/v1/sessions/{id} — removes a secondary session (and its messages). */
export function deleteSession(
sessionId: string
): Promise<{ id: string; deleted: true }> {
return request<{ id: string; deleted: true }>(
`/sessions/${encodeURIComponent(sessionId)}`,
{ method: "DELETE" }
);
}
/** GET /api/v1/personas — persona summaries. */
export function fetchPersonas(): Promise<PersonaRecord[]> {
return request<PersonaRecord[]>("/personas");
}
/** Is this error a "backend has not implemented it yet" case (404/501/403 on a
* not-yet-wired route) rather than a real connectivity problem? */
export function isNotImplemented(err: unknown): boolean {
if (err instanceof ApiError) {
return err.status === 404 || err.status === 501 || err.code === "not_found";
}
return false;
}
/* ----------------------------------------------------------------------------
* Admin API (/api/v1/admin/*). Reference: Docs/api-client.md §2 + design/10.
* Implemented endpoints return real data; the rest are contract-shaped and are
* expected to 404 until the backend wires them — callers show a graceful shell.
* -------------------------------------------------------------------------- */
/** GET /api/v1/admin/personas */
export function adminListPersonas(): Promise<PersonaRecord[]> {
return request<PersonaRecord[]>("/admin/personas");
}
/** GET /api/v1/admin/sessions — the persistent main + secondary sessions (design/16).
* The main session (fixed id) is always present, so the panel is never empty in a
* history-wired deployment. */
export function adminListSessions(): Promise<SessionMeta[]> {
return request<SessionMeta[]>("/admin/sessions");
}
/** GET /api/v1/admin/memories */
export function adminListMemories(): Promise<MemoryRecord[]> {
return request<MemoryRecord[]>("/admin/memories");
}
/** GET /api/v1/admin/plugins */
export function adminListPlugins(): Promise<AdminPluginRecord[]> {
return request<AdminPluginRecord[]>("/admin/plugins");
}
/** POST /api/v1/admin/plugins/policy — owner-only, audited. */
export function adminSetPluginPolicy(
name: string,
allowInGroup: boolean,
allowInStranger: boolean
): Promise<AdminPluginRecord> {
return request<AdminPluginRecord>("/admin/plugins/policy", {
method: "POST",
body: JSON.stringify({ name, allowInGroup, allowInStranger }),
});
}
/** GET /api/v1/admin/channels */
export function adminListChannels(): Promise<AdminChannelRecord[]> {
return request<AdminChannelRecord[]>("/admin/channels");
}
/** GET /api/v1/admin/iot */
export function adminListIoT(): Promise<AdminIoTDevice[]> {
return request<AdminIoTDevice[]>("/admin/iot");
}
// --- Planned M10 endpoints (contract-shaped, backend may 404). ---
/** GET /api/v1/admin/users */
export function adminListUsers(): Promise<AdminUserRecord[]> {
return request<AdminUserRecord[]>("/admin/users");
}
/** GET /api/v1/admin/llm */
export function adminGetLlm(): Promise<AdminLlmConfig> {
return request<AdminLlmConfig>("/admin/llm");
}
/** GET /api/v1/admin/automations */
export function adminListAutomations(): Promise<AdminAutomationRecord[]> {
return request<AdminAutomationRecord[]>("/admin/automations");
}
/** GET /api/v1/admin/rag — lists knowledge documents; when query is present the
* backend also returns matching hits. */
export function adminListRag(query?: string): Promise<AdminRagResponse> {
const qs = query ? `?query=${encodeURIComponent(query)}` : "";
return request<AdminRagResponse>(`/admin/rag${qs}`);
}
/** POST /api/v1/admin/rag/query — runs a real knowledge-base query. */
export function adminSearchRag(
query: string
): Promise<{ query: string; hits: AdminRagHit[] }> {
return request<{ query: string; hits: AdminRagHit[] }>("/admin/rag/query", {
method: "POST",
body: JSON.stringify({ query }),
});
}
/** POST /api/v1/admin/rag/delete — deletes a document. The backend returns a
* clear 501 until M10 lands the write path (upload/chunk/embed/delete). */
export function adminDeleteRag(id: string): Promise<{ id: string }> {
return request<{ id: string }>("/admin/rag/delete", {
method: "POST",
body: JSON.stringify({ id }),
});
}
/** POST /api/v1/admin/rag — imports one knowledge document (chunk + index), so it
* becomes keyword/searchable in the project index. */
export function adminIndexRag(req: RagIndexRequest): Promise<{ name: string }> {
return request<{ name: string }>("/admin/rag", {
method: "POST",
body: JSON.stringify(req),
});
}
/* --- Admin write endpoints (owner-only, audited; real store or honest 501). --- */
/** POST /api/v1/admin/personas — registers a persona (persisted to YAML + registry). */
export function adminCreatePersona(req: PersonaUpsertRequest): Promise<PersonaRecord> {
return request<PersonaRecord>("/admin/personas", {
method: "POST",
body: JSON.stringify(req),
});
}
/** PUT /api/v1/admin/personas/{id} — edits an existing persona. */
export function adminUpdatePersona(id: string, req: PersonaUpsertRequest): Promise<PersonaRecord> {
return request<PersonaRecord>(`/admin/personas/${encodeURIComponent(id)}`, {
method: "PUT",
body: JSON.stringify(req),
});
}
/** POST /api/v1/admin/users — add user/role. No persistent user store yet, so the
* backend answers honestly with a 501 ("M10"). */
export function adminCreateUser(req: { username: string; role: string }): Promise<AdminUserRecord> {
return request<AdminUserRecord>("/admin/users", {
method: "POST",
body: JSON.stringify(req),
});
}
/** POST /api/v1/admin/users/apikey — mints a key; the plaintext comes back once. */
export function adminIssueApiKey(role: string): Promise<ApiKeyIssueResult> {
return request<ApiKeyIssueResult>("/admin/users/apikey", {
method: "POST",
body: JSON.stringify({ role }),
});
}
/** DELETE /api/v1/admin/users/apikey/{id} — revokes a key by masked id. */
export function adminRevokeApiKey(id: string): Promise<{ id: string; revoked: boolean }> {
return request<{ id: string; revoked: boolean }>(`/admin/users/apikey/${encodeURIComponent(id)}`, {
method: "DELETE",
});
}
/** POST /api/v1/admin/iot/{id}/control — sends a control instruction to a device. */
export function adminIoTControl(id: string, action: string, params?: Record<string, unknown>): Promise<AdminIoTDevice> {
return request<AdminIoTDevice>(`/admin/iot/${encodeURIComponent(id)}/control`, {
method: "POST",
body: JSON.stringify({ action, params }),
});
}
/** POST /api/v1/admin/iot/scenarios — honest 501 (no scenario store yet). */
export function adminIoTScenario(req: { name: string }): Promise<{ name: string }> {
return request<{ name: string }>("/admin/iot/scenarios", { method: "POST", body: JSON.stringify(req) });
}
/** POST /api/v1/admin/iot/rules — honest 501 (no IoT rule store yet). */
export function adminIoTRule(req: { name: string; expression: string }): Promise<{ name: string }> {
return request<{ name: string }>("/admin/iot/rules", { method: "POST", body: JSON.stringify(req) });
}
/** POST /api/v1/admin/automations — registers a real cron/event automation. */
export function adminCreateAutomation(req: {
name: string;
kind: string;
expression: string;
}): Promise<AutomationCreateResult> {
return request<AutomationCreateResult>("/admin/automations", {
method: "POST",
body: JSON.stringify(req),
});
}
/** POST /api/v1/admin/automations/{id}/run — manually triggers a rule. */
export function adminRunAutomation(id: string): Promise<AutomationRunResult> {
return request<AutomationRunResult>(`/admin/automations/${encodeURIComponent(id)}/run`, { method: "POST" });
}
/** DELETE /api/v1/admin/automations/{id} — removes a rule (cron or event). */
export function adminDeleteAutomation(id: string): Promise<{ id: string; deleted: boolean }> {
return request<{ id: string; deleted: boolean }>(`/admin/automations/${encodeURIComponent(id)}`, { method: "DELETE" });
}
/** POST /api/v1/admin/llm — saves a provider group to models.json and hot-reloads
* the running LLM stack (design/04 §3.1). The provider is the wire protocol. */
export function adminAddLlmProvider(req: LlmProviderRequest): Promise<{
provider: AdminLlmProvider;
persisted: boolean;
path?: string;
}> {
return request<{ provider: AdminLlmProvider; persisted: boolean; path?: string }>("/admin/llm", {
method: "POST",
body: JSON.stringify(req),
});
}
/** POST /api/v1/admin/llm/test — performs a real model call and reports the result. */
export function adminTestLlm(req: LlmTestRequest): Promise<LlmTestResult> {
return request<LlmTestResult>("/admin/llm/test", { method: "POST", body: JSON.stringify(req) });
}
/** POST /api/v1/admin/channels/open — connects a registered channel adapter. */
export function adminChannelOpen(name: string): Promise<{ name: string; connected: boolean }> {
return request<{ name: string; connected: boolean }>("/admin/channels/open", {
method: "POST",
body: JSON.stringify({ name }),
});
}
/** POST /api/v1/admin/channels/close — disconnects a registered channel adapter. */
export function adminChannelClose(name: string): Promise<{ name: string; connected: boolean }> {
return request<{ name: string; connected: boolean }>("/admin/channels/close", {
method: "POST",
body: JSON.stringify({ name }),
});
}
/** POST /api/v1/admin/channels/test — structural identity test (actor × whitelist). */
export function adminChannelTest(req: {
platform: string;
accountId: string;
isGroup: boolean;
}): Promise<ChannelTestResult> {
return request<ChannelTestResult>("/admin/channels/test", { method: "POST", body: JSON.stringify(req) });
}
/** GET /api/v1/admin/channels/messages?adapter=<composite> — one adapter config's
* (platform:account) send/receive message log, newest first. */
export function adminChannelMessages(adapter: string, limit?: number): Promise<AdminChannelMessage[]> {
const q = new URLSearchParams({ adapter });
if (limit && limit > 0) q.set("limit", String(limit));
return request<AdminChannelMessage[]>(`/admin/channels/messages?${q.toString()}`);
}
/** GET /api/v1/admin/channels/messages — adapter overview log across all adapters. */
export function adminChannelLogOverview(limit?: number): Promise<AdminChannelMessage[]> {
const q = limit && limit > 0 ? `?limit=${limit}` : "";
return request<AdminChannelMessage[]>(`/admin/channels/messages${q}`);
}
/** PUT /api/v1/admin/channels/{platform}/config — save an account config + hot-reload. */
export function adminSaveChannelConfig(
platform: string,
req: {
id?: string;
token?: string;
baseURL?: string;
remoteURL?: string;
listenAddr?: string;
mode?: string;
accessToken?: string;
selfID?: string;
config?: Record<string, unknown>;
allowGroups?: string[];
denyGroups?: string[];
allowUsers?: string[];
denyUsers?: string[];
groupMsgPerMin?: number;
msgPerMin?: number;
maxMessages?: number;
},
): Promise<ChannelConfigSaveResult> {
return request<ChannelConfigSaveResult>(`/admin/channels/${platform}/config`, {
method: "PUT",
body: JSON.stringify(req),
});
}
/** DELETE /api/v1/admin/channels/{platform}/config?id=… — delete an account + hot-reload. */
export function adminDeleteChannelAccount(platform: string, id: string): Promise<ChannelConfigSaveResult> {
return request<ChannelConfigSaveResult>(`/admin/channels/${platform}/config?id=${encodeURIComponent(id)}`, {
method: "DELETE",
});
}
/** GET /api/v1/admin/channels/{platform}/view — one platform account's aggregated
* panel data (friends / servers / channels / members / private chats), owner only. */
export function adminChannelPlatformView(platform: string, accountId?: string): Promise<KookViewResponse> {
const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : "";
return request<KookViewResponse>(`/admin/channels/${encodeURIComponent(platform)}/view${q}`);
}
/** GET /api/v1/admin/channels/{platform}/friends?type= — friend/request/blocked list. */
export function adminChannelPlatformFriends(
platform: string,
type: string,
accountId?: string,
): Promise<KookFriendList> {
const q = new URLSearchParams();
if (type) q.set("type", type);
if (accountId) q.set("accountId", accountId);
const qs = q.toString();
return request<KookFriendList>(`/admin/channels/${encodeURIComponent(platform)}/friends${qs ? `?${qs}` : ""}`);
}
/** POST /api/v1/admin/channels/{platform}/friend/handle — accept/reject a friend request. */
export function adminChannelFriendHandle(
platform: string,
id: number,
accept: boolean,
accountId?: string,
): Promise<{ handled: boolean; id: number; accept: boolean }> {
const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : "";
return request<{ handled: boolean; id: number; accept: boolean }>(
`/admin/channels/${encodeURIComponent(platform)}/friend/handle${q}`,
{ method: "POST", body: JSON.stringify({ id, accept }) },
);
}
/** POST /api/v1/admin/channels/{platform}/message/test — send one test message. */
export function adminChannelMessageTest(
platform: string,
targetId: string,
content: string,
accountId?: string,
): Promise<KookMessageTestResult> {
const q = accountId ? `?accountId=${encodeURIComponent(accountId)}` : "";
return request<KookMessageTestResult>(`/admin/channels/${encodeURIComponent(platform)}/message/test${q}`, {
method: "POST",
body: JSON.stringify({ targetId, content }),
});
}
/** POST /api/v1/admin/plugins/install — honest 501 (needs a plugin source). */
export function adminPluginInstall(req: { name: string; kind: string; description: string }): Promise<AdminPluginRecord> {
return request<AdminPluginRecord>("/admin/plugins/install", { method: "POST", body: JSON.stringify(req) });
}
/** POST /api/v1/admin/plugins/uninstall — removes a capability from the registry. */
export function adminPluginUninstall(req: { name: string }): Promise<{ name: string; uninstalled: boolean }> {
return request<{ name: string; uninstalled: boolean }>("/admin/plugins/uninstall", {
method: "POST",
body: JSON.stringify(req),
});
}
/** POST /api/v1/admin/memories/search — real retrieval over the memory store. */
export function adminMemorySearch(req: MemorySearchRequest): Promise<MemorySearchResult> {
return request<MemorySearchResult>("/admin/memories/search", { method: "POST", body: JSON.stringify(req) });
}
/** POST /api/v1/admin/memories — writes one memory item (manual "添加记忆"). */
export function adminAddMemory(req: MemoryAddRequest): Promise<MemoryAddResult> {
return request<MemoryAddResult>("/admin/memories", {
method: "POST",
body: JSON.stringify(req),
});
}
/** GET /api/v1/admin/sessions/{id}/export — exports a session as JSON. */
export function adminExportSession(id: string): Promise<{ session: SessionRecord }> {
return request<{ session: SessionRecord }>(`/admin/sessions/${encodeURIComponent(id)}/export`);
}
/** GET /api/v1/admin/observability/metrics — gateway request metrics. */
export function adminObservabilityMetrics(): Promise<AdminObservabilityMetrics> {
return request<AdminObservabilityMetrics>("/admin/observability/metrics");
}
/** GET /api/v1/admin/observability/trace?limit=N — most recent traced spans. */
export function adminObservabilityTrace(limit?: number): Promise<AdminTraceSpan[]> {
return request<AdminTraceSpan[]>(`/admin/observability/trace${limitQuery(limit)}`);
}
/** GET /api/v1/admin/observability/audit?limit=N — most recent audit entries. */
export function adminObservabilityAudit(limit?: number): Promise<AuditRecord[]> {
return request<AuditRecord[]>(`/admin/observability/audit${limitQuery(limit)}`);
}
/** Builds an optional ?limit=N query suffix (empty when no positive limit). */
function limitQuery(limit?: number): string {
return limit && limit > 0 ? `?limit=${limit}` : "";
}
/** GET /api/v1/admin/backthink — persisted background-think records (newest first). */
export function adminBackthink(limit?: number): Promise<BackthinkItem[]> {
return request<BackthinkItem[]>(`/admin/backthink${limitQuery(limit)}`);
}