Files
memind/src/api/client.ts
T
Your Name b0f5d6a51c Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:39:39 -07:00

1839 lines
52 KiB
TypeScript

import type {
AdminDashboardSummary,
AdminUserRow,
AuthStatus,
CapabilityDefinition,
CapabilityMap,
ChatSaveAnalysis,
ChatSaveResult,
PolicyDefinition,
PolicyMap,
SkillDefinition,
SkillMap,
BillingConfig,
HarnessBootstrapResponse,
InsufficientBalanceDetails,
LedgerEntry,
LlmProviderDefinition,
LlmProviderKeyRow,
LlmGlobalSettings,
LlmConnectionTestResult,
RechargeOrder,
Message,
MindSpace,
MindSpaceAsset,
MindSpaceAgentJob,
MindSpaceCleanupItem,
MindSpacePage,
MindSpacePageDeletePreview,
MindSpacePageDeleteResult,
MindSpaceQuota,
MindSpaceSaveCategory,
MindSpacePublication,
MindSpacePublicationStats,
MindSpacePublishCheck,
PlazaCategory,
PlazaPostBrief,
MindSpaceRedactedCopyResult,
MindSpaceUpload,
PortalUser,
Session,
SessionEvent,
SessionListResponse,
UsageRecord,
BalanceUpdate,
} from '../types';
const API = '/api';
export class ApiError extends Error {
readonly status: number;
readonly code?: string;
readonly details?: InsufficientBalanceDetails | Record<string, unknown>;
constructor(
status: number,
message: string,
code?: string,
details?: InsufficientBalanceDetails | Record<string, unknown>,
) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
this.details = details;
}
}
async function parseErrorResponse(res: Response) {
const text = await res.text().catch(() => '');
try {
const body = JSON.parse(text) as Record<string, unknown>;
const nested =
body.error && typeof body.error === 'object'
? (body.error as Record<string, unknown>)
: body;
const message =
typeof nested.message === 'string'
? nested.message
: typeof body.message === 'string'
? body.message
: text;
const code =
typeof nested.code === 'string'
? nested.code
: typeof body.code === 'string'
? body.code
: undefined;
const details = nested.details ?? body.details;
if (code === 'INSUFFICIENT_BALANCE') {
return {
message,
code,
details: {
code: 'INSUFFICIENT_BALANCE' as const,
balanceCents: Number((details as Record<string, unknown>)?.balanceCents ?? body.balanceCents ?? 0),
minRechargeCents: Number(
(details as Record<string, unknown>)?.minRechargeCents ?? body.minRechargeCents ?? 500,
),
suggestedTiers: Array.isArray((details as Record<string, unknown>)?.suggestedTiers)
? ((details as Record<string, unknown>).suggestedTiers as unknown[]).map((value) => Number(value))
: Array.isArray(body.suggestedTiers)
? body.suggestedTiers.map((value) => Number(value))
: [],
},
};
}
return { message, code, details };
} catch {
return { message: text || res.statusText };
}
}
let unauthorizedHandler: (() => void) | null = null;
let unauthorizedHandling = false;
export function setUnauthorizedHandler(handler: (() => void) | null) {
unauthorizedHandler = handler;
if (handler) unauthorizedHandling = false;
}
export function resetUnauthorizedGuard() {
unauthorizedHandling = false;
}
function notifyUnauthorized() {
if (!unauthorizedHandler || unauthorizedHandling) return;
unauthorizedHandling = true;
unauthorizedHandler();
}
async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
let res: Response;
try {
res = await fetch(path, {
...init,
headers: {
'Content-Type': 'application/json',
...init?.headers,
},
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
if (res.status === 401) {
notifyUnauthorized();
const text = await res.text().catch(() => '');
throw new ApiError(401, text || '未授权,请重新登录');
}
if (!res.ok) {
const parsed = await parseErrorResponse(res);
throw new ApiError(
res.status,
parsed.message || `${res.status} ${res.statusText}`,
parsed.code,
parsed.details,
);
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
}
function formatNetworkError(err: unknown) {
const message = err instanceof Error ? err.message : '网络请求失败';
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
return '无法连接后端服务,请先运行: pnpm dev 或 node server.mjs';
}
return message;
}
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
let res: Response;
try {
res = await fetch(`${API}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
...init?.headers,
},
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
if (res.status === 401) {
notifyUnauthorized();
const text = await res.text().catch(() => '');
throw new ApiError(401, text || '未授权,请重新登录');
}
if (!res.ok) {
const parsed = await parseErrorResponse(res);
throw new ApiError(
res.status,
parsed.message || `${res.status} ${res.statusText}`,
parsed.code,
parsed.details,
);
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
}
export async function startSession(): Promise<Session> {
return apiFetch<Session>('/agent/start', {
method: 'POST',
body: JSON.stringify({}),
});
}
export async function bootstrapProjectMemory(
sessionId: string,
query: string,
force = false,
): Promise<HarnessBootstrapResponse> {
return apiFetch<HarnessBootstrapResponse>('/agent/harness_bootstrap', {
method: 'POST',
body: JSON.stringify({ sessionId, query, force }),
});
}
export async function rememberProjectContext(
sessionId: string,
content: string,
title: string,
): Promise<void> {
await apiFetch('/agent/harness_remember', {
method: 'POST',
body: JSON.stringify({ sessionId, content, title }),
});
}
export type WechatAuthConfig = {
enabled: boolean;
inWechat?: boolean;
scanEnabled?: boolean;
openScanEnabled?: boolean;
openAppId?: string;
oauthCallbackUrl?: string;
};
export type WechatPendingProfile = {
nickname: string | null;
avatarUrl: string | null;
returnTo: string;
};
export type WechatBindingStatus = {
enabled: boolean;
bound: boolean;
nickname?: string | null;
avatarUrl?: string | null;
};
export async function getWechatAuthConfig(): Promise<WechatAuthConfig> {
try {
const response = await fetch('/auth/wechat/config');
if (!response.ok) return { enabled: false };
return (await response.json()) as WechatAuthConfig;
} catch {
return { enabled: false };
}
}
export async function checkAuth(): Promise<AuthStatus> {
try {
const response = await fetch('/auth/status');
if (!response.ok) return { authenticated: false };
const status = (await response.json()) as AuthStatus;
if (status.authenticated) resetUnauthorizedGuard();
if (status.authenticated && status.mode === 'user' && !status.capabilities) {
try {
const me = await getMe();
return {
...status,
capabilities: me.capabilities,
grantedSkills: me.grantedSkills,
user: me.user,
};
} catch {
return status;
}
}
return status;
} catch {
return { authenticated: false };
}
}
export async function getMe(): Promise<{
user: PortalUser;
capabilities?: CapabilityMap;
grantedSkills?: string[];
unrestricted?: boolean;
}> {
return portalFetch('/auth/me');
}
export async function getMyUsage(): Promise<UsageRecord[]> {
const result = await portalFetch<{ records: UsageRecord[] }>('/auth/usage');
return result.records ?? [];
}
export async function getMyBillingLedger(limit = 30): Promise<LedgerEntry[]> {
const query = limit ? `?limit=${encodeURIComponent(String(limit))}` : '';
const result = await portalFetch<{ entries: LedgerEntry[] }>(`/auth/billing/ledger${query}`);
return result.entries ?? [];
}
export async function getBillingConfig(): Promise<BillingConfig> {
return portalFetch('/auth/billing/config');
}
export async function createRechargeOrder(input: {
amountCents: number;
payScene: 'native' | 'h5' | 'jsapi';
}): Promise<RechargeOrder> {
const result = await portalFetch<{ order: RechargeOrder }>('/auth/billing/recharge-orders', {
method: 'POST',
body: JSON.stringify(input),
});
return result.order;
}
export async function getRechargeOrder(orderId: string): Promise<{
order: RechargeOrder;
balanceCents: number | null;
}> {
return portalFetch(`/auth/billing/recharge-orders/${orderId}`);
}
export async function getMindSpace(): Promise<MindSpace> {
const result = await apiFetch<{ data: MindSpace }>('/mindspace/v1/space');
return result.data;
}
export async function listMindSpaceCleanupItems(): Promise<{
items: MindSpaceCleanupItem[];
totalBytes: number;
}> {
const result = await apiFetch<{
data: { items: MindSpaceCleanupItem[]; totalBytes: number };
}>('/mindspace/v1/space/cleanup');
return result.data;
}
export async function runMindSpaceCleanup(itemIds: string[]): Promise<{
removedCount: number;
freedBytes: number;
quota?: MindSpaceQuota;
}> {
const result = await apiFetch<{
data: { removedCount: number; freedBytes: number; quota?: MindSpaceQuota };
}>('/mindspace/v1/space/cleanup', {
method: 'POST',
body: JSON.stringify({ item_ids: itemIds }),
});
return result.data;
}
export async function listMindSpaceAssets(
categoryCode?: string,
): Promise<MindSpaceAsset[]> {
const query = categoryCode ? `?category_code=${encodeURIComponent(categoryCode)}` : '';
const result = await apiFetch<{ data: MindSpaceAsset[] }>(`/mindspace/v1/assets${query}`);
return result.data;
}
export async function uploadMindSpaceAsset(
categoryId: string,
file: File,
): Promise<MindSpaceAsset> {
const created = await apiFetch<{ data: MindSpaceUpload }>('/mindspace/v1/uploads', {
method: 'POST',
body: JSON.stringify({
category_id: categoryId,
filename: file.name,
size_bytes: file.size,
declared_mime_type: file.type || null,
}),
});
try {
const contentResponse = await fetch(created.data.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/octet-stream' },
body: file,
});
if (!contentResponse.ok) {
const body = (await contentResponse.json().catch(() => null)) as {
error?: { message?: string };
} | null;
throw new ApiError(
contentResponse.status,
body?.error?.message ?? '文件内容上传失败',
);
}
const completed = await apiFetch<{ data: MindSpaceAsset }>(
`/mindspace/v1/uploads/${created.data.id}/complete`,
{ method: 'POST', body: JSON.stringify({}) },
);
return completed.data;
} catch (error) {
await apiFetch(`/mindspace/v1/uploads/${created.data.id}`, {
method: 'DELETE',
}).catch(() => {});
throw error;
}
}
export async function deleteMindSpaceAsset(assetId: string): Promise<void> {
await apiFetch(`/mindspace/v1/assets/${assetId}`, { method: 'DELETE' });
}
export async function getMindSpacePageDeletePreview(
pageId: string,
): Promise<MindSpacePageDeletePreview> {
const result = await apiFetch<{ data: MindSpacePageDeletePreview }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/delete-preview`,
);
return result.data;
}
export async function deleteMindSpacePage(pageId: string): Promise<MindSpacePageDeleteResult> {
const result = await apiFetch<{ data: MindSpacePageDeleteResult }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
{ method: 'DELETE' },
);
return result.data;
}
export async function listMindSpacePages(status?: string): Promise<MindSpacePage[]> {
const query = status ? `?status=${encodeURIComponent(status)}` : '';
const result = await apiFetch<{ data: MindSpacePage[] }>(`/mindspace/v1/pages${query}`);
return result.data;
}
export async function getMindSpacePage(pageId: string): Promise<MindSpacePage> {
const result = await apiFetch<{ data: MindSpacePage }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
);
return result.data;
}
export async function bindMindSpacePageLiveEdit(
pageId: string,
sessionId: string,
options?: { parentSessionId?: string },
): Promise<{ sessionId: string; pageId: string; parentSessionId?: string }> {
const result = await apiFetch<{ data: { sessionId: string; pageId: string; parentSessionId?: string } }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/bind`,
{
method: 'POST',
body: JSON.stringify({
session_id: sessionId,
...(options?.parentSessionId ? { parent_session_id: options.parentSessionId } : {}),
}),
},
);
return result.data;
}
export async function forkMindSpacePageEditSession(
pageId: string,
parentSessionId: string,
h5ApiBase?: string | null,
): Promise<{ sessionId: string; pageId: string; parentSessionId: string }> {
const result = await apiFetch<{
data: { sessionId: string; pageId: string; parentSessionId: string };
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/fork-session`, {
method: 'POST',
body: JSON.stringify({
parent_session_id: parentSessionId,
...(h5ApiBase ? { h5_api_base: h5ApiBase } : {}),
}),
});
return result.data;
}
export async function closeMindSpacePageEditSession(
pageId: string,
input: {
sessionId: string;
parentSessionId?: string | null;
summary?: string;
},
): Promise<{ sessionId: string; pageId: string; merged: boolean }> {
const result = await apiFetch<{
data: { sessionId: string; pageId: string; merged: boolean };
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/close-session`, {
method: 'POST',
body: JSON.stringify({
session_id: input.sessionId,
parent_session_id: input.parentSessionId ?? undefined,
summary: input.summary ?? '',
}),
});
return result.data;
}
export async function getMindSpacePageLiveRevision(pageId: string): Promise<{
pageId: string;
versionNo: number;
updatedAt: number;
liveRevision: number;
}> {
const result = await apiFetch<{
data: { pageId: string; versionNo: number; updatedAt: number; liveRevision: number };
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/revision`);
return result.data;
}
export async function analyzeChatSave(input: {
sessionId: string;
messageId: string;
selectedLinkIndex?: number;
previewTitle?: string;
previewSummary?: string;
}): Promise<ChatSaveAnalysis> {
const result = await apiFetch<{ data: ChatSaveAnalysis }>(
'/mindspace/v1/pages/analyze-chat-save',
{
method: 'POST',
body: JSON.stringify({
session_id: input.sessionId,
message_id: input.messageId,
selected_link_index: input.selectedLinkIndex ?? 0,
preview_title: input.previewTitle,
preview_summary: input.previewSummary,
}),
},
);
return result.data;
}
export function buildChatSavePreviewFrameUrl(input: {
sessionId: string;
messageId: string;
selectedLinkIndex?: number;
}) {
const params = new URLSearchParams({
session_id: input.sessionId,
message_id: input.messageId,
selected_link_index: String(input.selectedLinkIndex ?? 0),
});
return `/api/mindspace/v1/pages/chat-save-preview?${params.toString()}`;
}
export function buildChatSaveThumbnailUrl(input: {
sessionId: string;
messageId: string;
selectedLinkIndex?: number;
previewTitle?: string;
previewSummary?: string;
cacheKey?: number | string;
}) {
const params = new URLSearchParams({
session_id: input.sessionId,
message_id: input.messageId,
selected_link_index: String(input.selectedLinkIndex ?? 0),
});
const title = input.previewTitle?.trim();
const summary = input.previewSummary?.trim();
if (title) params.set('preview_title', title);
if (summary) params.set('preview_summary', summary);
if (input.cacheKey != null) params.set('v', String(input.cacheKey));
return `/api/mindspace/v1/pages/chat-save-thumbnail?${params.toString()}`;
}
export async function fetchPreviewAsset(path: string): Promise<string> {
let res: Response;
try {
res = await fetch(path, { credentials: 'same-origin' });
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
if (res.status === 401) {
notifyUnauthorized();
throw new ApiError(401, '未授权,请重新登录');
}
if (!res.ok) {
const parsed = await parseErrorResponse(res);
throw new ApiError(res.status, parsed.message || `${res.status} ${res.statusText}`, parsed.code);
}
const blob = await res.blob();
return URL.createObjectURL(blob);
}
export async function saveChatMessageAsPage(input: {
sessionId: string;
messageId: string;
title: string;
summary?: string;
templateId?: string;
categoryCode?: MindSpaceSaveCategory;
selectedLinkIndex?: number;
acknowledgedFindingIds?: string[];
}): Promise<ChatSaveResult> {
const result = await apiFetch<{ data: ChatSaveResult }>(
'/mindspace/v1/pages/save-from-chat',
{
method: 'POST',
body: JSON.stringify({
session_id: input.sessionId,
message_id: input.messageId,
title: input.title,
summary: input.summary,
template_id: input.templateId ?? 'editorial',
category_code: input.categoryCode ?? 'draft',
selected_link_index: input.selectedLinkIndex ?? 0,
acknowledged_finding_ids: input.acknowledgedFindingIds,
page_type: input.templateId === 'report' ? 'report' : 'article',
}),
},
);
return result.data;
}
export async function createMindSpacePageFromAsset(input: {
assetId: string;
title?: string;
summary?: string;
}): Promise<MindSpacePage> {
const result = await apiFetch<{ data: { page: MindSpacePage } }>(
'/mindspace/v1/pages/from-asset',
{
method: 'POST',
body: JSON.stringify({
asset_id: input.assetId,
title: input.title,
summary: input.summary,
}),
},
);
return result.data.page;
}
export async function createMindSpacePage(input: {
title: string;
summary?: string;
content: string;
templateId: string;
}): Promise<MindSpacePage> {
const result = await apiFetch<{ data: MindSpacePage }>('/mindspace/v1/pages', {
method: 'POST',
body: JSON.stringify({
title: input.title,
summary: input.summary,
content: input.content,
template_id: input.templateId,
page_type: input.templateId === 'report' ? 'report' : 'article',
}),
});
return result.data;
}
export async function updateMindSpacePage(
pageId: string,
input: {
expectedVersion: number;
title: string;
summary: string;
content: string;
templateId: string;
changeNote?: string;
},
): Promise<MindSpacePage> {
const result = await apiFetch<{ data: MindSpacePage }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
{
method: 'PUT',
body: JSON.stringify({
expected_version: input.expectedVersion,
title: input.title,
summary: input.summary,
content: input.content,
template_id: input.templateId,
page_type: input.templateId === 'report' ? 'report' : 'article',
change_note: input.changeNote,
}),
},
);
return result.data;
}
export async function fetchMindSpacePageDraftPreview(
pageId: string,
input: {
title: string;
summary: string;
content: string;
templateId: string;
},
): Promise<string> {
let res: Response;
try {
res = await fetch(`/api/mindspace/v1/pages/${encodeURIComponent(pageId)}/preview-draft`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: input.title,
summary: input.summary,
content: input.content,
template_id: input.templateId,
}),
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
if (res.status === 401) {
notifyUnauthorized();
throw new ApiError(401, '未授权,请重新登录');
}
if (!res.ok) {
const parsed = await parseErrorResponse(res);
throw new ApiError(res.status, parsed.message || `${res.status} ${res.statusText}`, parsed.code);
}
return res.text();
}
export function openMindSpaceDraftPreviewWindow(html: string) {
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const opened = window.open(url, '_blank', 'noopener,noreferrer');
if (!opened) {
URL.revokeObjectURL(url);
throw new ApiError(0, '无法打开新窗口,请检查浏览器是否拦截弹窗');
}
window.setTimeout(() => URL.revokeObjectURL(url), 120_000);
}
export async function uploadMindSpacePageThumbnail(
pageId: string,
input: {
imageBase64: string;
mimeType?: string;
title?: string;
summary?: string;
content?: string;
},
): Promise<{ updatedAt: number }> {
const result = await apiFetch<{ data: { updatedAt: number } }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/upload`,
{
method: 'POST',
body: JSON.stringify({
image_base64: input.imageBase64,
mime_type: input.mimeType,
title: input.title,
summary: input.summary,
html: input.content,
}),
},
);
return result.data;
}
export async function regenerateMindSpacePageThumbnail(
pageId: string,
input: {
title?: string;
summary?: string;
content?: string;
useAi?: boolean;
instruction?: string;
} = {},
): Promise<{ updatedAt: number; content?: string | null }> {
const result = await apiFetch<{ data: { updatedAt: number; content?: string | null } }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/regenerate`,
{
method: 'POST',
body: JSON.stringify({
title: input.title,
summary: input.summary,
html: input.content,
use_ai: input.useAi ?? false,
instruction: input.instruction,
}),
},
);
return result.data;
}
export async function checkMindSpacePagePublication(
pageId: string,
input: {
pageVersionId: string;
accessMode: MindSpacePublishCheck['accessMode'];
urlSlug: string;
password?: string;
expiresAt?: number | null;
},
): Promise<MindSpacePublishCheck> {
const result = await apiFetch<{ data: MindSpacePublishCheck }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/publish-check`,
{
method: 'POST',
body: JSON.stringify({
page_version_id: input.pageVersionId,
access_mode: input.accessMode,
url_slug: input.urlSlug,
password: input.password,
expires_at: input.expiresAt,
}),
},
);
return result.data;
}
export async function publishMindSpacePage(
pageId: string,
input: {
pageVersionId: string;
accessMode: MindSpacePublishCheck['accessMode'];
urlSlug: string;
acknowledgedFindingIds: string[];
password?: string;
expiresAt?: number | null;
},
): Promise<MindSpacePublication> {
const result = await apiFetch<{ data: MindSpacePublication }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/publish`,
{
method: 'POST',
body: JSON.stringify({
page_version_id: input.pageVersionId,
access_mode: input.accessMode,
url_slug: input.urlSlug,
password: input.password,
expires_at: input.expiresAt,
acknowledged_finding_ids: input.acknowledgedFindingIds,
}),
},
);
return result.data;
}
export async function redactMindSpacePage(
pageId: string,
input: {
pageVersionId: string;
expectedVersion: number;
title: string;
summary: string;
content: string;
},
): Promise<MindSpaceRedactedCopyResult> {
const result = await apiFetch<{ data: MindSpaceRedactedCopyResult }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/redact`,
{
method: 'POST',
body: JSON.stringify({
page_version_id: input.pageVersionId,
expected_version: input.expectedVersion,
title: input.title,
summary: input.summary,
content: input.content,
}),
},
);
return result.data;
}
/** @deprecated use redactMindSpacePage */
export async function createMindSpaceRedactedCopy(
pageId: string,
pageVersionId: string,
): Promise<MindSpaceRedactedCopyResult> {
const page = await getMindSpacePage(pageId);
return redactMindSpacePage(pageId, {
pageVersionId,
expectedVersion: page.versionNo,
title: page.title,
summary: page.summary,
content: page.content ?? '',
});
}
export async function offlineMindSpacePublication(publicationId: string): Promise<void> {
await apiFetch(
`/mindspace/v1/publications/${encodeURIComponent(publicationId)}/offline`,
{ method: 'POST', body: JSON.stringify({}) },
);
}
export async function getMindSpacePublicationStats(
publicationId: string,
): Promise<MindSpacePublicationStats> {
const result = await apiFetch<{ data: MindSpacePublicationStats }>(
`/mindspace/v1/publications/${encodeURIComponent(publicationId)}/stats`,
);
return result.data;
}
export async function listPlazaCategories(): Promise<PlazaCategory[]> {
const result = await apiFetch<{ data: { categories: PlazaCategory[] } }>('/plaza/v1/categories');
return result.data.categories;
}
export async function publishPageToPlaza(input: {
publicationId: string;
categoryId: string;
tags?: string[];
coverUrl?: string;
allowComment?: boolean;
}): Promise<PlazaPostBrief> {
const result = await apiFetch<{ data: { post: PlazaPostBrief } }>('/plaza/v1/posts', {
method: 'POST',
body: JSON.stringify({
publication_id: input.publicationId,
category_id: input.categoryId,
tags: input.tags ?? [],
cover_url: input.coverUrl ?? '',
allow_comment: input.allowComment ?? true,
}),
});
return result.data.post;
}
export async function createMindSpaceAgentJob(input: {
jobType: string;
instruction: string;
allowedAssetIds: string[];
outputType?: 'page_draft' | 'html_page' | 'markdown';
outputCategoryId?: string;
idempotencyKey?: string;
locale?: string;
timezone?: string;
capabilities?: {
network?: boolean;
shell?: boolean;
createPage?: boolean;
};
}): Promise<MindSpaceAgentJob> {
const result = await apiFetch<{ data: MindSpaceAgentJob }>('/mindspace/v1/agent/jobs', {
method: 'POST',
body: JSON.stringify({
job_type: input.jobType,
instruction: input.instruction,
allowed_asset_ids: input.allowedAssetIds,
output_type: input.outputType ?? 'page_draft',
output_category_id: input.outputCategoryId,
idempotency_key: input.idempotencyKey,
locale: input.locale,
timezone: input.timezone,
capabilities: input.capabilities,
}),
});
return result.data;
}
export async function getMindSpaceAgentJob(jobId: string): Promise<MindSpaceAgentJob> {
const result = await apiFetch<{ data: MindSpaceAgentJob }>(
`/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}`,
);
return result.data;
}
export type MindSpaceListPage = {
total?: number;
offset?: number;
limit?: number;
has_more?: boolean;
};
export async function listMindSpaceAgentJobs(options?: {
limit?: number;
offset?: number;
}): Promise<{ items: MindSpaceAgentJob[]; page: MindSpaceListPage }> {
const limit = options?.limit ?? 10;
const offset = options?.offset ?? 0;
const result = await apiFetch<{ data: MindSpaceAgentJob[]; page?: MindSpaceListPage }>(
`/mindspace/v1/agent/jobs?limit=${encodeURIComponent(String(limit))}&offset=${encodeURIComponent(String(offset))}`,
);
return { items: result.data, page: result.page ?? {} };
}
export async function runMindSpaceAgentJob(
jobId: string,
): Promise<{ started: boolean; jobId: string }> {
const result = await apiFetch<{ data: { started: boolean; jobId: string } }>(
`/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}/run`,
{ method: 'POST', body: JSON.stringify({}) },
);
return result.data;
}
export async function cancelMindSpaceAgentJob(jobId: string): Promise<MindSpaceAgentJob> {
const result = await apiFetch<{ data: MindSpaceAgentJob }>(
`/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}/cancel`,
{ method: 'POST', body: JSON.stringify({}) },
);
return result.data;
}
export async function retryMindSpaceAgentJob(jobId: string): Promise<MindSpaceAgentJob> {
const result = await apiFetch<{ data: MindSpaceAgentJob }>(
`/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}/retry`,
{ method: 'POST', body: JSON.stringify({}) },
);
return result.data;
}
export async function getAdminDashboardSummary(): Promise<AdminDashboardSummary> {
const result = await portalFetch<{ summary: AdminDashboardSummary }>('/admin-api/summary');
return result.summary;
}
export async function listAdminUsage(userId?: string): Promise<UsageRecord[]> {
const query = userId ? `?userId=${encodeURIComponent(userId)}` : '';
const result = await portalFetch<{ records: UsageRecord[] }>(`/admin-api/usage${query}`);
return result.records ?? [];
}
export async function listAdminLedger(userId?: string): Promise<LedgerEntry[]> {
const query = userId ? `?userId=${encodeURIComponent(userId)}` : '';
const result = await portalFetch<{ entries: LedgerEntry[] }>(`/admin-api/ledger${query}`);
return result.entries ?? [];
}
export async function listCapabilityCatalog(): Promise<CapabilityDefinition[]> {
const result = await portalFetch<{ catalog: CapabilityDefinition[] }>(
'/admin-api/capabilities/catalog',
);
return result.catalog ?? [];
}
export async function getRoleCapabilities(role: 'user' = 'user'): Promise<{
role: string;
capabilities: CapabilityMap;
}> {
return portalFetch(`/admin-api/capabilities/role/${role}`);
}
export async function updateRoleCapabilities(
role: 'user',
capabilities: CapabilityMap,
): Promise<{ role: string; capabilities: CapabilityMap }> {
return portalFetch(`/admin-api/capabilities/role/${role}`, {
method: 'PUT',
body: JSON.stringify({ capabilities }),
});
}
export async function getUserCapabilities(userId: string): Promise<{
userId: string;
role: string;
unrestricted: boolean;
capabilities: CapabilityMap;
overrides: CapabilityMap;
}> {
return portalFetch(`/admin-api/users/${userId}/capabilities`);
}
export async function updateUserCapabilities(
userId: string,
capabilities: CapabilityMap,
): Promise<{
userId: string;
capabilities: CapabilityMap;
}> {
return portalFetch(`/admin-api/users/${userId}/capabilities`, {
method: 'PUT',
body: JSON.stringify({ capabilities }),
});
}
export async function clearUserCapabilityOverrides(userId: string): Promise<void> {
await portalFetch(`/admin-api/users/${userId}/capabilities`, { method: 'DELETE' });
}
export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
const result = await portalFetch<{ catalog: PolicyDefinition[] }>('/admin-api/policies/catalog');
return result.catalog ?? [];
}
export async function getRolePolicies(role: 'user' = 'user'): Promise<{
role: string;
policies: PolicyMap;
}> {
return portalFetch(`/admin-api/policies/role/${role}`);
}
export async function updateRolePolicies(
role: 'user',
policies: PolicyMap,
): Promise<{ role: string; policies: PolicyMap }> {
return portalFetch(`/admin-api/policies/role/${role}`, {
method: 'PUT',
body: JSON.stringify({ policies }),
});
}
export async function getUserPolicies(userId: string): Promise<{
userId: string;
role: string;
unrestricted: boolean;
policies: PolicyMap;
overrides: PolicyMap;
}> {
return portalFetch(`/admin-api/users/${userId}/policies`);
}
export async function updateUserPolicies(
userId: string,
policies: PolicyMap,
): Promise<{
userId: string;
policies: PolicyMap;
}> {
return portalFetch(`/admin-api/users/${userId}/policies`, {
method: 'PUT',
body: JSON.stringify({ policies }),
});
}
export async function clearUserPolicyOverrides(userId: string): Promise<void> {
await portalFetch(`/admin-api/users/${userId}/policies`, { method: 'DELETE' });
}
export async function listSkillCatalog(): Promise<SkillDefinition[]> {
const result = await portalFetch<{ catalog: SkillDefinition[] }>('/admin-api/skills/catalog');
return result.catalog ?? [];
}
export async function getRoleSkills(role: 'user' = 'user'): Promise<{
role: string;
skills: SkillMap;
}> {
return portalFetch(`/admin-api/skills/role/${role}`);
}
export async function updateRoleSkills(
role: 'user',
skills: SkillMap,
): Promise<{ role: string; skills: SkillMap }> {
return portalFetch(`/admin-api/skills/role/${role}`, {
method: 'PUT',
body: JSON.stringify({ skills }),
});
}
export async function getUserSkills(userId: string): Promise<{
userId: string;
role: string;
skills: SkillMap;
grantedSkills: string[];
overrides: SkillMap;
}> {
return portalFetch(`/admin-api/users/${userId}/skills`);
}
export async function updateUserSkills(
userId: string,
skills: SkillMap,
): Promise<{
userId: string;
skills: SkillMap;
grantedSkills: string[];
}> {
return portalFetch(`/admin-api/users/${userId}/skills`, {
method: 'PUT',
body: JSON.stringify({ skills }),
});
}
export async function clearUserSkillOverrides(userId: string): Promise<void> {
await portalFetch(`/admin-api/users/${userId}/skills`, { method: 'DELETE' });
}
export async function login(username: string, password: string): Promise<PortalUser | null> {
let response: Response;
try {
response = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as {
message?: string;
user?: PortalUser;
mode?: string;
} | null;
if (!response.ok) {
throw new ApiError(response.status, body?.message ?? '登录失败');
}
resetUnauthorizedGuard();
if (body?.user) {
try {
const me = await getMe();
return me.user;
} catch {
return body.user;
}
}
return null;
}
export async function loginLegacy(password: string): Promise<void> {
let response: Response;
try {
response = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
if (!response.ok) {
const body = (await response.json().catch(() => null)) as { message?: string } | null;
throw new ApiError(response.status, body?.message ?? '登录失败');
}
}
export async function resetPassword(
username: string,
email: string,
password: string,
): Promise<void> {
let response: Response;
try {
response = await fetch('/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password }),
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as { message?: string } | null;
if (!response.ok) {
throw new ApiError(response.status, body?.message ?? '重置失败');
}
}
export async function getWechatPendingProfile(token: string): Promise<WechatPendingProfile> {
let response: Response;
try {
response = await fetch(`/auth/wechat/pending/${encodeURIComponent(token)}`);
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as WechatPendingProfile & {
message?: string;
};
if (!response.ok) {
throw new ApiError(response.status, body?.message ?? '绑定会话无效');
}
return body;
}
export async function completeWechatRegister(pendingToken: string): Promise<{
user: PortalUser;
returnTo: string;
}> {
let response: Response;
try {
response = await fetch('/auth/wechat/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pendingToken }),
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as {
message?: string;
user?: PortalUser;
returnTo?: string;
};
if (!response.ok || !body?.user) {
throw new ApiError(response.status, body?.message ?? '微信注册失败');
}
return { user: body.user, returnTo: body.returnTo ?? '/' };
}
export async function completeWechatBind(
pendingToken: string,
username: string,
password: string,
): Promise<{ user: PortalUser; returnTo: string }> {
let response: Response;
try {
response = await fetch('/auth/wechat/bind', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pendingToken, username, password }),
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as {
message?: string;
user?: PortalUser;
returnTo?: string;
};
if (!response.ok || !body?.user) {
throw new ApiError(response.status, body?.message ?? '绑定失败');
}
return { user: body.user, returnTo: body.returnTo ?? '/' };
}
export async function getWechatBindingStatus(): Promise<WechatBindingStatus> {
try {
const response = await fetch('/auth/wechat/status');
if (!response.ok) return { enabled: false, bound: false };
return (await response.json()) as WechatBindingStatus;
} catch {
return { enabled: false, bound: false };
}
}
export type WechatScanSession =
| {
mode: 'open';
state: string;
openAppId: string;
redirectUri: string;
expiresInMs: number;
}
| {
mode: 'mp';
state: string;
qrUrl: string;
expiresInMs: number;
};
export async function startWechatScanLogin(returnTo?: string): Promise<WechatScanSession> {
const params = new URLSearchParams();
if (returnTo) params.set('return_to', returnTo);
const query = params.toString();
let response: Response;
try {
response = await fetch(`/auth/wechat/scan/start${query ? `?${query}` : ''}`, {
method: 'POST',
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as {
message?: string;
mode?: 'open' | 'mp';
state?: string;
openAppId?: string;
redirectUri?: string;
qrUrl?: string;
expiresInMs?: number;
};
if (!response.ok || !body?.state || !body?.mode) {
throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
}
if (body.mode === 'open') {
if (!body.openAppId || !body.redirectUri) {
throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
}
return {
mode: 'open',
state: body.state,
openAppId: body.openAppId,
redirectUri: body.redirectUri,
expiresInMs: body.expiresInMs ?? 600000,
};
}
if (!body.qrUrl) {
throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
}
return {
mode: 'mp',
state: body.state,
qrUrl: body.qrUrl,
expiresInMs: body.expiresInMs ?? 600000,
};
}
export async function pollWechatScanLogin(state: string): Promise<{
status: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
pendingToken?: string;
message?: string;
}> {
let response: Response;
try {
response = await fetch(`/auth/wechat/scan/poll?state=${encodeURIComponent(state)}`);
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as {
status?: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
pendingToken?: string;
message?: string;
};
if (!response.ok) {
throw new ApiError(response.status, body?.message ?? '扫码状态查询失败');
}
return {
status: body?.status ?? 'error',
pendingToken: body?.pendingToken,
message: body?.message,
};
}
export async function register(
username: string,
password: string,
displayName?: string,
email?: string,
attribution?: {
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
ref?: string;
},
): Promise<PortalUser> {
let response: Response;
try {
response = await fetch('/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
password,
displayName,
email,
...(attribution ?? {}),
}),
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as {
message?: string;
user?: PortalUser;
} | null;
if (!response.ok) {
throw new ApiError(response.status, body?.message ?? '注册失败');
}
if (!body?.user) throw new ApiError(response.status, '注册失败');
return body.user;
}
export async function listAdminUsers(): Promise<AdminUserRow[]> {
const result = await portalFetch<{ users: AdminUserRow[] }>('/admin-api/users');
return result.users ?? [];
}
export async function createAdminUser(payload: {
username: string;
password: string;
displayName?: string;
workspaceRoot?: string;
balanceCents?: number;
role?: 'user' | 'admin';
email?: string;
}): Promise<PortalUser> {
const result = await portalFetch<{ user: PortalUser }>('/admin-api/users', {
method: 'POST',
body: JSON.stringify(payload),
});
return result.user;
}
export async function updateAdminUser(
userId: string,
payload: Partial<{
displayName: string;
status: 'active' | 'suspended' | 'disabled';
workspaceRoot: string;
balanceCents: number;
role: 'user' | 'admin';
}>,
): Promise<PortalUser> {
const result = await portalFetch<{ user: PortalUser }>(`/admin-api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
return result.user;
}
export async function rechargeUser(
userId: string,
amountCents: number,
note?: string,
): Promise<PortalUser> {
const result = await portalFetch<{ user: PortalUser }>(`/admin-api/users/${userId}/recharge`, {
method: 'POST',
body: JSON.stringify({ amountCents, note }),
});
return result.user;
}
export async function listLlmProviderCatalog(): Promise<LlmProviderDefinition[]> {
const result = await portalFetch<{ catalog: LlmProviderDefinition[] }>(
'/admin-api/llm-providers/catalog',
);
return result.catalog ?? [];
}
export async function listLlmProviderKeys(): Promise<LlmProviderKeyRow[]> {
const result = await portalFetch<{ keys: LlmProviderKeyRow[] }>('/admin-api/llm-providers/keys');
return result.keys ?? [];
}
export async function createLlmProviderKey(payload: {
providerId: string;
name: string;
apiKey: string;
defaultModel?: string;
apiUrl?: string;
basePath?: string;
models?: string | string[];
engine?: string;
relayProvider?: string;
}): Promise<LlmProviderKeyRow> {
const result = await portalFetch<{ key: LlmProviderKeyRow }>('/admin-api/llm-providers/keys', {
method: 'POST',
body: JSON.stringify(payload),
});
return result.key;
}
export async function updateLlmProviderKey(
keyId: string,
payload: {
name?: string;
apiKey?: string;
defaultModel?: string;
status?: 'active' | 'disabled';
apiUrl?: string;
basePath?: string;
models?: string | string[];
engine?: string;
relayProvider?: string;
},
): Promise<LlmProviderKeyRow> {
const result = await portalFetch<{ key: LlmProviderKeyRow }>(
`/admin-api/llm-providers/keys/${keyId}`,
{
method: 'PATCH',
body: JSON.stringify(payload),
},
);
return result.key;
}
export async function selectLlmProviderKey(keyId: string): Promise<LlmProviderKeyRow> {
const result = await portalFetch<{ key: LlmProviderKeyRow }>(
`/admin-api/llm-providers/keys/${keyId}/select`,
{ method: 'POST' },
);
return result.key;
}
export async function deleteLlmProviderKey(keyId: string): Promise<void> {
await portalFetch(`/admin-api/llm-providers/keys/${keyId}`, { method: 'DELETE' });
}
export async function syncLlmProviderToGoosed(): Promise<{ synced: boolean }> {
return portalFetch('/admin-api/llm-providers/sync', { method: 'POST' });
}
export async function getLlmGlobalSettings(): Promise<LlmGlobalSettings> {
const result = await portalFetch<{ global: LlmGlobalSettings }>('/admin-api/llm-providers/global');
return result.global;
}
export async function setLlmGlobalModel(model: string): Promise<{ global: LlmGlobalSettings }> {
return portalFetch('/admin-api/llm-providers/global', {
method: 'PUT',
body: JSON.stringify({ model }),
});
}
export async function testLlmProviderDraft(payload: {
providerId: string;
name: string;
apiKey: string;
apiUrl?: string;
basePath?: string;
models?: string | string[];
defaultModel?: string;
testModel?: string;
engine?: string;
relayProvider?: string;
}): Promise<LlmConnectionTestResult> {
return portalFetch('/admin-api/llm-providers/test', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function testLlmProviderKey(
keyId: string,
model?: string,
): Promise<LlmConnectionTestResult> {
return portalFetch(`/admin-api/llm-providers/keys/${keyId}/test`, {
method: 'POST',
body: JSON.stringify(model ? { model } : {}),
});
}
export async function logout(): Promise<void> {
if (import.meta.env.DEV) return;
await fetch('/auth/logout', { method: 'POST' });
}
export async function resumeSession(
sessionId: string,
options?: { skipReconcile?: boolean },
): Promise<Session> {
const result = await apiFetch<{ session: Session }>('/agent/resume', {
method: 'POST',
body: JSON.stringify({
session_id: sessionId,
load_model_and_extensions: true,
...(options?.skipReconcile ? { skip_reconcile: true } : {}),
}),
});
return result.session;
}
export async function readConfig(key: string): Promise<string | null> {
return apiFetch<string | null>('/config/read', {
method: 'POST',
body: JSON.stringify({ key, is_secret: false }),
});
}
export async function updateProvider(
sessionId: string,
provider: string,
model: string,
): Promise<void> {
await apiFetch('/agent/update_provider', {
method: 'POST',
body: JSON.stringify({
session_id: sessionId,
provider,
model,
}),
});
}
export async function applyLocalLlmFallback(sessionId: string): Promise<{
ok: boolean;
providerId?: string;
model?: string;
source?: string;
message?: string;
}> {
return apiFetch('/llm/apply-local-fallback', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId }),
});
}
export async function listSessions(): Promise<Session[]> {
const result = await apiFetch<SessionListResponse>('/sessions');
return result.sessions ?? [];
}
export async function getSession(sessionId: string): Promise<Session> {
return apiFetch<Session>(`/sessions/${sessionId}`);
}
export async function deleteChatSession(sessionId: string): Promise<void> {
await apiFetch(`/sessions/${sessionId}`, { method: 'DELETE' });
}
export async function loadSessionDetail(sessionId: string): Promise<{
session: Session;
messages: Message[];
}> {
const detail = await getSession(sessionId);
const messages = (detail.conversation ?? []).filter((m) => m.metadata?.userVisible);
return { session: detail, messages };
}
export async function sendReply(
sessionId: string,
requestId: string,
userMessage: Message,
): Promise<void> {
await apiFetch(`/sessions/${sessionId}/reply`, {
method: 'POST',
body: JSON.stringify({
request_id: requestId,
user_message: userMessage,
}),
});
}
export async function cancelRequest(sessionId: string, requestId: string): Promise<void> {
await apiFetch(`/sessions/${sessionId}/cancel`, {
method: 'POST',
body: JSON.stringify({ request_id: requestId }),
});
}
export async function confirmTool(
sessionId: string,
id: string,
action: 'allow_once' | 'deny_once' | 'always_allow',
): Promise<void> {
await apiFetch('/action-required/tool-confirmation', {
method: 'POST',
body: JSON.stringify({
sessionId,
id,
action,
principalType: 'Tool',
}),
});
}
type SubscribeOptions = {
pauseWhenHidden?: boolean;
onBalance?: (update: BalanceUpdate) => void;
};
export function subscribeSessionEvents(
sessionId: string,
onEvent: (event: SessionEvent) => void,
onError: (error: Error) => void,
options: SubscribeOptions = {},
): () => void {
const { pauseWhenHidden = true, onBalance } = options;
let lastEventId: string | undefined;
let closed = false;
let paused = false;
let activeController: AbortController | null = null;
const run = async (controller: AbortController) => {
while (!closed && !controller.signal.aborted && !paused) {
try {
const headers: Record<string, string> = { Accept: 'text/event-stream' };
if (lastEventId) headers['Last-Event-ID'] = lastEventId;
const res = await fetch(`${API}/sessions/${sessionId}/events`, {
headers,
signal: controller.signal,
});
if (res.status === 401) {
notifyUnauthorized();
throw new ApiError(401, 'SSE 未授权');
}
if (!res.ok || !res.body) {
throw new ApiError(res.status, `SSE failed: ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (!closed && !controller.signal.aborted && !paused) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
for (const chunk of chunks) {
let eventId: string | undefined;
let eventName: string | undefined;
let data: string | undefined;
for (const line of chunk.split('\n')) {
if (line.startsWith('id:')) eventId = line.slice(3).trim();
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) data = line.slice(5).trim();
}
if (eventId) lastEventId = eventId;
if (!data) continue;
try {
if (eventName === 'balance') {
const payload = JSON.parse(data) as BalanceUpdate & { balanceCents?: number };
if (typeof payload.balanceCents === 'number') {
onBalance?.({
balanceCents: payload.balanceCents,
tokensUsed:
typeof payload.tokensUsed === 'number' ? payload.tokensUsed : undefined,
lastUsage: payload.lastUsage,
});
}
continue;
}
onEvent(JSON.parse(data) as SessionEvent);
} catch {
// ignore malformed frames
}
}
}
} catch (err) {
if (controller.signal.aborted || closed || paused) return;
onError(err instanceof Error ? err : new Error(String(err)));
await new Promise((r) => setTimeout(r, 1500));
}
}
};
const startConnection = () => {
activeController?.abort();
activeController = new AbortController();
void run(activeController);
};
const handleVisibility = () => {
if (!pauseWhenHidden) return;
if (document.hidden) {
paused = true;
activeController?.abort();
} else if (paused) {
paused = false;
startConnection();
}
};
if (pauseWhenHidden) {
document.addEventListener('visibilitychange', handleVisibility);
}
startConnection();
return () => {
closed = true;
activeController?.abort();
if (pauseWhenHidden) {
document.removeEventListener('visibilitychange', handleVisibility);
}
};
}