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; constructor( status: number, message: string, code?: string, details?: InsufficientBalanceDetails | Record, ) { 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; const nested = body.error && typeof body.error === 'object' ? (body.error as Record) : 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)?.balanceCents ?? body.balanceCents ?? 0), minRechargeCents: Number( (details as Record)?.minRechargeCents ?? body.minRechargeCents ?? 500, ), suggestedTiers: Array.isArray((details as Record)?.suggestedTiers) ? ((details as Record).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(path: string, init?: RequestInit): Promise { 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; } 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(path: string, init?: RequestInit): Promise { 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; } export async function startSession(): Promise { return apiFetch('/agent/start', { method: 'POST', body: JSON.stringify({}), }); } export async function bootstrapProjectMemory( sessionId: string, query: string, force = false, ): Promise { return apiFetch('/agent/harness_bootstrap', { method: 'POST', body: JSON.stringify({ sessionId, query, force }), }); } export async function rememberProjectContext( sessionId: string, content: string, title: string, ): Promise { 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 { 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 { 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 { const result = await portalFetch<{ records: UsageRecord[] }>('/auth/usage'); return result.records ?? []; } export async function getMyBillingLedger(limit = 30): Promise { 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 { return portalFetch('/auth/billing/config'); } export async function createRechargeOrder(input: { amountCents: number; payScene: 'native' | 'h5' | 'jsapi'; }): Promise { 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 { 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 { 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 { 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 { await apiFetch(`/mindspace/v1/assets/${assetId}`, { method: 'DELETE' }); } export async function getMindSpacePageDeletePreview( pageId: string, ): Promise { const result = await apiFetch<{ data: MindSpacePageDeletePreview }>( `/mindspace/v1/pages/${encodeURIComponent(pageId)}/delete-preview`, ); return result.data; } export async function deleteMindSpacePage(pageId: string): Promise { const result = await apiFetch<{ data: MindSpacePageDeleteResult }>( `/mindspace/v1/pages/${encodeURIComponent(pageId)}`, { method: 'DELETE' }, ); return result.data; } export async function listMindSpacePages(status?: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { await apiFetch( `/mindspace/v1/publications/${encodeURIComponent(publicationId)}/offline`, { method: 'POST', body: JSON.stringify({}) }, ); } export async function getMindSpacePublicationStats( publicationId: string, ): Promise { const result = await apiFetch<{ data: MindSpacePublicationStats }>( `/mindspace/v1/publications/${encodeURIComponent(publicationId)}/stats`, ); return result.data; } export async function listPlazaCategories(): Promise { 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 { 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 { 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 { 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 { 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 { 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 { const result = await portalFetch<{ summary: AdminDashboardSummary }>('/admin-api/summary'); return result.summary; } export async function listAdminUsage(userId?: string): Promise { 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 { const query = userId ? `?userId=${encodeURIComponent(userId)}` : ''; const result = await portalFetch<{ entries: LedgerEntry[] }>(`/admin-api/ledger${query}`); return result.entries ?? []; } export async function listCapabilityCatalog(): Promise { 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 { await portalFetch(`/admin-api/users/${userId}/capabilities`, { method: 'DELETE' }); } export async function listPolicyCatalog(): Promise { 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 { await portalFetch(`/admin-api/users/${userId}/policies`, { method: 'DELETE' }); } export async function listSkillCatalog(): Promise { 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 { await portalFetch(`/admin-api/users/${userId}/skills`, { method: 'DELETE' }); } export async function login(username: string, password: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { const result = await portalFetch<{ catalog: LlmProviderDefinition[] }>( '/admin-api/llm-providers/catalog', ); return result.catalog ?? []; } export async function listLlmProviderKeys(): Promise { 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 { 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 { 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 { 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 { 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 { 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 { return portalFetch('/admin-api/llm-providers/test', { method: 'POST', body: JSON.stringify(payload), }); } export async function testLlmProviderKey( keyId: string, model?: string, ): Promise { return portalFetch(`/admin-api/llm-providers/keys/${keyId}/test`, { method: 'POST', body: JSON.stringify(model ? { model } : {}), }); } export async function logout(): Promise { if (import.meta.env.DEV) return; await fetch('/auth/logout', { method: 'POST' }); } export async function resumeSession( sessionId: string, options?: { skipReconcile?: boolean }, ): Promise { 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 { return apiFetch('/config/read', { method: 'POST', body: JSON.stringify({ key, is_secret: false }), }); } export async function updateProvider( sessionId: string, provider: string, model: string, ): Promise { 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 { const result = await apiFetch('/sessions'); return result.sessions ?? []; } export async function getSession(sessionId: string): Promise { return apiFetch(`/sessions/${sessionId}`); } export async function deleteChatSession(sessionId: string): Promise { 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 { 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 { 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 { 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 = { 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); } }; }