import type { AdminDashboardSummary, AdminServiceRestartAction, AdminServiceRestartResult, AdminSystemTestReport, AdminSystemTestAccount, AdminSubscription, AdminUserRow, AuthStatus, BlockedWord, CapabilityDefinition, CapabilityMap, InsufficientBalanceDetails, LedgerEntry, LlmConnectionTestResult, LlmExecutorBinding, LlmExecutorId, LlmExecutorLaunchPlan, LlmExecutorLaunchState, LlmExecutorRuntime, LlmGlobalSettings, LlmVisionSettings, LlmProviderDefinition, LlmProviderKeyRow, PlanDefinition, PlanSyncResult, PolicyDefinition, PolicyMap, PortalUser, MindSpaceAdminConfig, MemoryV2AdminConfig, MemoryV2AdminConfigResponse, MemoryV2ModelApiType, SkillDefinition, SkillMap, UsageRecord, WechatAdminSummary, WechatBinding, WechatDeliveryLog, WechatDigestSubscription, WechatScheduleLlmConfig, WechatMessage, WechatWebNotification, } from '../types'; 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; return { message, code }; } 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(); } function formatNetworkError(err: unknown) { const message = err instanceof Error ? err.message : '网络请求失败'; if (message.includes('Failed to fetch') || message.includes('NetworkError')) { return '无法连接后端服务'; } return message; } async function portalFetch(path: string, init?: RequestInit): Promise { let res: Response; try { res = await fetch(path, { ...init, credentials: 'include', 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); } if (res.status === 204) return undefined as T; return res.json() as Promise; } // ── Auth ────────────────────────────────────────────── export async function checkAuth(): Promise { try { const response = await fetch('/auth/status', { credentials: 'include' }); if (!response.ok) return { authenticated: false }; const status = (await response.json()) as AuthStatus; if (status.authenticated) resetUnauthorizedGuard(); return status; } catch { return { authenticated: false }; } } export async function login(username: string, password: string): Promise { let response: Response; try { response = await fetch('/auth/login', { method: 'POST', credentials: 'include', 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; } | null; if (!response.ok) { throw new ApiError(response.status, body?.message ?? '登录失败'); } resetUnauthorizedGuard(); return body?.user ?? null; } export async function logout(): Promise { await fetch('/auth/logout', { method: 'POST', credentials: 'include' }); } // ── Pagination helpers ──────────────────────────────── export type PagedResult = { items: T[]; total: number; page: number; pageSize: number; totalPages: number; }; // ── Admin dashboard ─────────────────────────────────── export async function getAdminDashboardSummary(): Promise { const result = await portalFetch<{ summary: AdminDashboardSummary }>('/admin-api/summary'); return result.summary; } export async function restartAdminService( action: AdminServiceRestartAction, ): Promise { return portalFetch(`/admin-api/service-restart/${action}`, { method: 'POST', }); } export async function listAdminUsage(params?: { userId?: string; page?: number; pageSize?: number; }): Promise> { const q = new URLSearchParams(); if (params?.userId) q.set('userId', params.userId); if (params?.page) q.set('page', String(params.page)); if (params?.pageSize) q.set('pageSize', String(params.pageSize)); const result = await portalFetch<{ records: UsageRecord[]; total: number; page: number; pageSize: number }>( `/admin-api/usage${q.toString() ? `?${q}` : ''}`, ); const total = result.total ?? 0; const pageSize = result.pageSize ?? 50; return { items: result.records ?? [], total, page: result.page ?? 1, pageSize, totalPages: Math.ceil(total / pageSize) }; } export async function listAdminLedger(params?: { userId?: string; page?: number; pageSize?: number; }): Promise> { const q = new URLSearchParams(); if (params?.userId) q.set('userId', params.userId); if (params?.page) q.set('page', String(params.page)); if (params?.pageSize) q.set('pageSize', String(params.pageSize)); const result = await portalFetch<{ entries: LedgerEntry[]; total: number; page: number; pageSize: number }>( `/admin-api/ledger${q.toString() ? `?${q}` : ''}`, ); const total = result.total ?? 0; const pageSize = result.pageSize ?? 50; return { items: result.entries ?? [], total, page: result.page ?? 1, pageSize, totalPages: Math.ceil(total / pageSize) }; } // ── WeChat MP ───────────────────────────────────────── export async function getWechatAdminSummary(): Promise { const summary = await portalFetch>('/admin-api/wechat/summary'); return { config: { mpEnabled: summary.config?.mpEnabled ?? false, scheduleEnabled: summary.config?.scheduleEnabled ?? false, reminderWorkerEnabled: summary.config?.reminderWorkerEnabled ?? false, appId: summary.config?.appId ?? null, publicBaseUrl: summary.config?.publicBaseUrl ?? null, bindPath: summary.config?.bindPath ?? null, tokenEndpointConfigured: summary.config?.tokenEndpointConfigured ?? false, customerServiceEndpointConfigured: summary.config?.customerServiceEndpointConfigured ?? false, scheduleLlmEnabled: summary.config?.scheduleLlmEnabled ?? false, }, counts: { boundUsers: summary.counts?.boundUsers ?? 0, routes: { total: summary.counts?.routes?.total ?? 0, active: summary.counts?.routes?.active ?? 0, }, recentMessages: summary.counts?.recentMessages ?? {}, digests: summary.counts?.digests ?? {}, recentDeliveries: summary.counts?.recentDeliveries ?? {}, }, }; } export async function updateWechatScheduleLlmConfig( payload: Pick, ): Promise { return portalFetch('/admin-api/wechat/schedule-llm-config', { method: 'PATCH', body: JSON.stringify(payload), }); } export async function getMindSpaceAdminConfig(): Promise { const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config'); return result.config; } export async function updateMindSpaceAdminConfig( payload: Partial, ): Promise { const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config', { method: 'PATCH', body: JSON.stringify(payload), }); return result.config; } export async function getMemoryV2AdminConfig(): Promise { return portalFetch('/admin-api/memory-v2/config'); } export async function updateMemoryV2AdminConfig( payload: Partial, ): Promise { return portalFetch('/admin-api/memory-v2/config', { method: 'PATCH', body: JSON.stringify(payload), }); } export async function listMemoryV2ModelOptions(): Promise<{ global: LlmGlobalSettings; keys: LlmProviderKeyRow[]; supportedApiTypes: MemoryV2ModelApiType[]; }> { const [global, keys] = await Promise.all([ getLlmGlobalSettings(), listLlmProviderKeys(), ]); return { global, keys, supportedApiTypes: ['chat', 'response'], }; } export async function runSystemSkillValidation(payload: { accountId?: string; username: string; password: string; skillName: string; }): Promise { return portalFetch('/admin-api/system-tests/skill-validation', { method: 'POST', body: JSON.stringify(payload), }); } export async function listSystemTestAccounts(): Promise { const result = await portalFetch<{ accounts: AdminSystemTestAccount[] }>('/admin-api/system-tests/accounts'); return result.accounts ?? []; } export async function createSystemTestAccount(payload: { label?: string; username: string; password: string; }): Promise { const result = await portalFetch<{ account: AdminSystemTestAccount }>('/admin-api/system-tests/accounts', { method: 'POST', body: JSON.stringify(payload), }); return result.account; } export async function deleteSystemTestAccount(accountId: string): Promise { await portalFetch(`/admin-api/system-tests/accounts/${encodeURIComponent(accountId)}`, { method: 'DELETE', }); } export async function listWechatBindings(params?: { search?: string; limit?: number; }): Promise { const q = new URLSearchParams(); if (params?.search) q.set('search', params.search); if (params?.limit) q.set('limit', String(params.limit)); const result = await portalFetch<{ bindings: WechatBinding[] }>( `/admin-api/wechat/bindings${q.toString() ? `?${q}` : ''}`, ); return result.bindings ?? []; } export async function listWechatMessages(params?: { status?: string; limit?: number; }): Promise { const q = new URLSearchParams(); if (params?.status) q.set('status', params.status); if (params?.limit) q.set('limit', String(params.limit)); const result = await portalFetch<{ messages: WechatMessage[] }>( `/admin-api/wechat/messages${q.toString() ? `?${q}` : ''}`, ); return result.messages ?? []; } export async function listWechatDigests(params?: { status?: string; limit?: number; }): Promise { const q = new URLSearchParams(); if (params?.status) q.set('status', params.status); if (params?.limit) q.set('limit', String(params.limit)); const result = await portalFetch<{ digests: WechatDigestSubscription[] }>( `/admin-api/wechat/digests${q.toString() ? `?${q}` : ''}`, ); return result.digests ?? []; } export async function listWechatDeliveries(params?: { status?: string; limit?: number; }): Promise { const q = new URLSearchParams(); if (params?.status) q.set('status', params.status); if (params?.limit) q.set('limit', String(params.limit)); const result = await portalFetch<{ deliveries: WechatDeliveryLog[] }>( `/admin-api/wechat/deliveries${q.toString() ? `?${q}` : ''}`, ); return result.deliveries ?? []; } export async function listWechatWebNotifications(params?: { status?: string; limit?: number; }): Promise { const q = new URLSearchParams(); if (params?.status) q.set('status', params.status); if (params?.limit) q.set('limit', String(params.limit)); const result = await portalFetch<{ notifications: WechatWebNotification[] }>( `/admin-api/wechat/web-notifications${q.toString() ? `?${q}` : ''}`, ); return result.notifications ?? []; } export async function createWechatWebNotification(payload: { userId?: string; userIds?: string[]; audience?: 'all'; allUsers?: boolean; title: string; body: string; notificationType?: string; channel?: 'web' | 'wechat'; channels?: Array<'web' | 'wechat'>; }): Promise<{ ok: boolean; created: number; wechatSent: number; wechatFailures: Array<{ userId: string; message: string }>; targets: number; }> { return portalFetch('/admin-api/wechat/web-notifications', { method: 'POST', body: JSON.stringify(payload), }); } export async function clearWechatRoute(userId: string): Promise<{ ok: boolean; deleted: number }> { return portalFetch(`/admin-api/wechat/users/${userId}/route/clear`, { method: 'POST' }); } export async function cancelWechatDigest(id: string): Promise<{ ok: boolean }> { return portalFetch(`/admin-api/wechat/digests/${id}/cancel`, { method: 'POST' }); } export async function resumeWechatDigest(id: string): Promise<{ ok: boolean; nextRunAt: number }> { return portalFetch(`/admin-api/wechat/digests/${id}/resume`, { method: 'POST' }); } // ── Admin users ─────────────────────────────────────── export async function listAdminUsers(params?: { page?: number; pageSize?: number; search?: string; role?: string; status?: string; }): Promise> { const q = new URLSearchParams(); if (params?.page) q.set('page', String(params.page)); if (params?.pageSize) q.set('pageSize', String(params.pageSize)); if (params?.search) q.set('search', params.search); if (params?.role) q.set('role', params.role); if (params?.status) q.set('status', params.status); const result = await portalFetch<{ users: AdminUserRow[]; total: number; page: number; pageSize: number }>( `/admin-api/users${q.toString() ? `?${q}` : ''}`, ); const total = result.total ?? 0; const pageSize = result.pageSize ?? 20; return { items: result.users ?? [], total, page: result.page ?? 1, pageSize, totalPages: Math.ceil(total / pageSize) }; } export async function getAdminUser(userId: string): Promise { const result = await portalFetch<{ user: PortalUser }>(`/admin-api/users/${userId}`); return result.user; } 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; spaceQuotaBytes: 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; } // ── Capabilities ────────────────────────────────────── 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' }); } // ── Policies ────────────────────────────────────────── 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' }); } // ── Skills ──────────────────────────────────────────── 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' }); } // ── LLM Providers ───────────────────────────────────── 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 getLlmVisionSettings(): Promise { const result = await portalFetch<{ vision: LlmVisionSettings }>('/admin-api/llm-providers/vision'); return result.vision; } export async function setLlmVisionKey(keyId: string, model: string): Promise { const result = await portalFetch<{ vision: LlmVisionSettings }>('/admin-api/llm-providers/vision', { method: 'PUT', body: JSON.stringify({ keyId, model }), }); return result.vision; } export async function clearLlmVisionKey(): Promise { await portalFetch('/admin-api/llm-providers/vision', { method: 'DELETE' }); } export async function listLlmExecutorBindings(): Promise { const result = await portalFetch<{ bindings: LlmExecutorBinding[] }>( '/admin-api/llm-providers/executor-bindings', ); return result.bindings ?? []; } export async function setLlmExecutorBinding( executor: LlmExecutorId, payload: { keyId: string | null; model: string; enabled: boolean; purpose?: string }, ): Promise { const result = await portalFetch<{ binding: LlmExecutorBinding }>( `/admin-api/llm-providers/executor-bindings/${executor}`, { method: 'PUT', body: JSON.stringify(payload) }, ); return result.binding; } export async function listLlmExecutorRuntime(): Promise { const result = await portalFetch<{ runtimes: LlmExecutorRuntime[] }>( '/admin-api/llm-providers/executor-runtime', ); return result.runtimes ?? []; } export async function listLlmExecutorLaunchPlans(params: { mode?: 'headless' | 'serve'; cwd?: string; instruction?: string; purpose?: string; } = {}): Promise { const q = new URLSearchParams(); if (params.mode) q.set('mode', params.mode); if (params.cwd) q.set('cwd', params.cwd); if (params.instruction) q.set('instruction', params.instruction); if (params.purpose) q.set('purpose', params.purpose); const result = await portalFetch<{ plans: LlmExecutorLaunchPlan[] }>( `/admin-api/llm-providers/executor-launch-plan${q.toString() ? `?${q}` : ''}`, ); return result.plans ?? []; } export async function listLlmExecutorLaunchStatus(params: { purpose?: string; } = {}): Promise> { const q = new URLSearchParams(); if (params.purpose) q.set('purpose', params.purpose); const result = await portalFetch<{ launches: Array }>( `/admin-api/llm-providers/executor-launch-status${q.toString() ? `?${q}` : ''}`, ); return result.launches ?? []; } export async function launchLlmExecutor( executor: LlmExecutorId, payload: { mode?: 'headless' | 'serve'; cwd?: string; instruction?: string; purpose?: string } = {}, ): Promise { const result = await portalFetch<{ launch: LlmExecutorLaunchState }>( `/admin-api/llm-providers/executor-launch/${executor}`, { method: 'POST', body: JSON.stringify(payload) }, ); return result.launch; } export async function stopLlmExecutor( executor: LlmExecutorId, payload: { purpose?: string; force?: boolean } = {}, ): Promise { const result = await portalFetch<{ launch: LlmExecutorLaunchState | null }>( `/admin-api/llm-providers/executor-stop/${executor}`, { method: 'POST', body: JSON.stringify(payload) }, ); return result.launch; } export async function restartLlmExecutor( executor: LlmExecutorId, payload: { mode?: 'headless' | 'serve'; cwd?: string; instruction?: string; purpose?: string } = {}, ): Promise { const result = await portalFetch<{ launch: LlmExecutorLaunchState }>( `/admin-api/llm-providers/executor-restart/${executor}`, { method: 'POST', body: JSON.stringify(payload) }, ); return result.launch; } 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 } : {}), }); } // ── Blocked words ───────────────────────────────────── export async function listBlockedWords(): Promise { const result = await portalFetch<{ words: BlockedWord[] }>('/admin-api/blocked-words'); return result.words ?? []; } export async function createBlockedWord(payload: { word: string; replacement?: string; note?: string; }): Promise { const result = await portalFetch<{ blockedWord: BlockedWord }>('/admin-api/blocked-words', { method: 'POST', body: JSON.stringify(payload), }); return result.blockedWord; } export async function updateBlockedWord( id: string, payload: Partial<{ word: string; replacement: string; note: string; status: 'active' | 'disabled' }>, ): Promise { const result = await portalFetch<{ blockedWord: BlockedWord }>(`/admin-api/blocked-words/${id}`, { method: 'PATCH', body: JSON.stringify(payload), }); return result.blockedWord; } export async function deleteBlockedWord(id: string): Promise { await portalFetch(`/admin-api/blocked-words/${id}`, { method: 'DELETE' }); } // ── Subscriptions ────────────────────────────────────── export async function listSubscriptionPlans(): Promise { const result = await portalFetch<{ plans: PlanDefinition[] }>('/admin-api/subscriptions/plans'); return result.plans; } export async function createSubscriptionPlan( planType: string, data: Partial, ): Promise<{ plan: PlanDefinition; sync?: PlanSyncResult }> { const result = await portalFetch<{ plan: PlanDefinition; sync?: PlanSyncResult }>('/admin-api/subscriptions/plans', { method: 'POST', body: JSON.stringify({ planType, ...data }), }); return result; } export async function updateSubscriptionPlan( planType: string, data: Partial, ): Promise<{ plan: PlanDefinition; sync?: PlanSyncResult }> { const result = await portalFetch<{ plan: PlanDefinition; sync?: PlanSyncResult }>(`/admin-api/subscriptions/plans/${planType}`, { method: 'PUT', body: JSON.stringify(data), }); return result; } export async function deleteSubscriptionPlan(planType: string): Promise<{ ok: boolean; sync?: PlanSyncResult }> { return portalFetch(`/admin-api/subscriptions/plans/${planType}`, { method: 'DELETE' }); } export async function syncSubscriptionPlansToProduction(): Promise { const result = await portalFetch<{ sync: PlanSyncResult }>('/admin-api/subscriptions/plans/sync-production', { method: 'POST', }); return result.sync; } export async function listAdminSubscriptions(opts?: { userId?: string; status?: string; page?: number; pageSize?: number; }): Promise<{ items: AdminSubscription[]; total: number; page: number; totalPages: number; pageSize: number }> { const q = new URLSearchParams(); if (opts?.userId) q.set('userId', opts.userId); if (opts?.status) q.set('status', opts.status); if (opts?.page) q.set('page', String(opts.page)); if (opts?.pageSize) q.set('pageSize', String(opts.pageSize)); const result = await portalFetch<{ items: AdminSubscription[]; total: number; page: number; pageSize: number; }>(`/admin-api/subscriptions${q.toString() ? `?${q}` : ''}`); return { items: result.items, total: result.total, page: result.page, totalPages: Math.ceil(result.total / result.pageSize) || 1, pageSize: result.pageSize, }; } export async function grantUserSubscription( userId: string, planType: string, durationDays?: number, note?: string, ): Promise { const result = await portalFetch<{ subscription: AdminSubscription }>(`/admin-api/users/${userId}/subscription`, { method: 'POST', body: JSON.stringify({ planType, durationDays, note }), }); return result.subscription; } export async function cancelUserSubscription(userId: string): Promise { await portalFetch(`/admin-api/users/${userId}/subscription`, { method: 'DELETE' }); } export async function getUserSubscription(userId: string): Promise { const result = await portalFetch<{ subscription: AdminSubscription | null }>( `/admin-api/users/${userId}/subscription`, ); return result.subscription; }