import type { AssetGatewayConfig, AdminDashboardSummary, AdminServiceRestartAction, AdminServiceRestartResult, AdminSystemTestReport, AdminSystemTestAccount, AdminSubscription, AdminUserRow, AuthStatus, BlockedWord, CapabilityDefinition, CapabilityMap, InsufficientBalanceDetails, ImageQuotaLedgerEntry, ImageQuotaView, LedgerEntry, LlmConnectionTestResult, LlmExecutorBinding, LlmExecutorId, LlmExecutorLaunchPlan, LlmExecutorLaunchState, LlmExecutorRuntime, LlmGlobalSettings, LlmVisionSettings, LlmProviderDefinition, LlmProviderKeyRow, PlanDefinition, PlanSyncResult, PersonalMemoryCandidateListResponse, PolicyDefinition, PolicyMap, PortalUser, MindSpaceAdminConfig, MindSpaceAnalyticsUpdate, MemoryV2AdminConfig, MemoryV2AdminConfigResponse, MemoryV2ModelApiType, MemoryV2RuntimeStatusResponse, MemoryV2ConfigRuntimeState, SkillRuntimeAdminConfig, SkillRuntimeAdminConfigResponse, SkillRuntimeCatalogItem, SkillDefinition, SkillMap, UsageRecord, UsageStatsBucket, UsageStatsResult, UsageSummaryResult, UsageTotals, WechatAdminSummary, WechatBinding, WechatDeliveryLog, WechatDigestSubscription, WechatScheduleLlmConfig, WechatIntentRouterAdminConfig, WechatIntentRouterConfigState, WechatIntentRouterRuntimeState, WechatMessage, WechatWebNotification, MindSearchConfig, MindSearchServiceTestResult, } from '../types'; import type { OrchestratorCanaryReadiness, OrchestratorConfig, OrchestratorConfigState, OrchestratorExecutionPlanList, OrchestratorRuntimeState, OrchestratorShadowRunDetail, OrchestratorShadowRunList, } from '../types/orchestrator'; 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; startAt?: number; endAt?: 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)); if (params?.startAt) q.set('startAt', String(params.startAt)); if (params?.endAt) q.set('endAt', String(params.endAt)); 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) }; } function toMillis(ts: number) { return ts >= 1_000_000_000_000 ? ts : ts * 1000; } function usageDayKey(createdAt: number) { const d = new Date(toMillis(createdAt)); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } async function aggregateUsageStatsFromRecords(params: { userId?: string; startAt: number; endAt: number; }): Promise { const bucketMap = new Map(); let page = 1; const pageSize = 200; const maxPages = 50; const startMs = params.startAt * 1000; const endMs = params.endAt * 1000 + 999; while (page <= maxPages) { const result = await listAdminUsage({ userId: params.userId, page, pageSize, startAt: params.startAt, endAt: params.endAt, }); for (const record of result.items) { const createdMs = toMillis(record.createdAt); if (createdMs < startMs || createdMs > endMs) continue; const day = usageDayKey(record.createdAt); const bucket = bucketMap.get(day) ?? { day, count: 0, inputTokens: 0, outputTokens: 0, costCents: 0, }; bucket.count += 1; bucket.inputTokens += record.inputTokens; bucket.outputTokens += record.outputTokens; bucket.costCents += record.costCents; bucketMap.set(day, bucket); } if (page >= result.totalPages || result.items.length === 0) break; page += 1; } return { buckets: [...bucketMap.values()].sort((a, b) => a.day.localeCompare(b.day)), startAt: params.startAt, endAt: params.endAt, }; } function sumUsageBuckets(buckets: UsageStatsBucket[]): UsageTotals { return buckets.reduce( (acc, b) => ({ count: acc.count + b.count, inputTokens: acc.inputTokens + b.inputTokens, outputTokens: acc.outputTokens + b.outputTokens, costCents: acc.costCents + b.costCents, }), { count: 0, inputTokens: 0, outputTokens: 0, costCents: 0 }, ); } export async function getAdminUsageSummary(params?: { userId?: string; startAt?: number; endAt?: number; }): Promise { const q = new URLSearchParams(); if (params?.userId) q.set('userId', params.userId); if (params?.startAt) q.set('startAt', String(params.startAt)); if (params?.endAt) q.set('endAt', String(params.endAt)); const suffix = q.toString() ? `?${q}` : ''; try { return await portalFetch(`/admin-api/usage/summary${suffix}`); } catch (err) { if (!(err instanceof ApiError) || err.status !== 404) throw err; } const allTime = sumUsageBuckets( (await aggregateUsageStatsFromRecords({ userId: params?.userId, startAt: 0, endAt: Math.floor(Date.now() / 1000), })).buckets, ); if (!params?.startAt || !params?.endAt) { return { range: null, allTime }; } const rangeRows = await getAdminUsageStats({ userId: params.userId, startAt: params.startAt, endAt: params.endAt, }); return { range: { ...sumUsageBuckets(rangeRows.buckets), startAt: params.startAt, endAt: params.endAt, }, allTime, }; } export async function getAdminUsageStats(params: { userId?: string; startAt: number; endAt: number; }): Promise { const q = new URLSearchParams(); if (params.userId) q.set('userId', params.userId); q.set('startAt', String(params.startAt)); q.set('endAt', String(params.endAt)); const attempts = [ `/admin-api/usage/stats?${q}`, `/admin-api/usage?${q}&stats=1`, ]; for (const path of attempts) { try { return await portalFetch(path); } catch (err) { if (err instanceof ApiError && err.status === 404) continue; throw err; } } return aggregateUsageStatsFromRecords(params); } 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 getWechatIntentRouterConfig(): Promise { const result = await portalFetch('/admin-api/wechat/intent-router/config'); return result.config; } export async function patchWechatIntentRouterConfig( patch: Partial>, ): Promise { const result = await portalFetch('/admin-api/wechat/intent-router/config', { method: 'PATCH', body: JSON.stringify(patch), }); return result.config; } export async function getWechatIntentRouterRuntime(): Promise { return portalFetch('/admin-api/wechat/intent-router/runtime'); } export async function getAssetGatewayConfig(): Promise { return portalFetch('/admin-api/asset-gateway/config'); } export async function updateAssetGatewayConfig( payload: Pick, ): Promise { return portalFetch('/admin-api/asset-gateway/config', { method: 'PUT', body: JSON.stringify(payload), }); } export async function updateAssetPluginConfig( pluginId: string, payload: { enabled: boolean; provider?: string | null; llmProviderKeyId?: string | null; llmModel?: string | null; purposes?: Record; }, ): Promise<{ ok: boolean; config: AssetGatewayConfig }> { return portalFetch(`/admin-api/asset-gateway/plugins/${encodeURIComponent(pluginId)}`, { method: 'PUT', 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 getUmamiSsoUrl(): Promise { const result = await portalFetch<{ url: string }>('/admin-api/analytics/sso'); return result.url; } export async function getRybbitSsoUrl(): Promise { const result = await portalFetch<{ url: string }>('/admin-api/analytics/rybbit-sso'); return result.url; } export async function updateMindSpaceAdminConfig( payload: Partial & { analytics?: MindSpaceAnalyticsUpdate }, ): 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 getMemoryV2RuntimeStatus(): Promise { return portalFetch('/admin-api/memory-v2/status'); } export async function getMemoryV2ConfigRuntime(): Promise { return portalFetch('/admin-api/memory-v2/runtime'); } export async function listPersonalMemoryCandidates( status = 'candidate', ): Promise { return portalFetch(`/admin-api/memory-v2/candidates?status=${encodeURIComponent(status)}&limit=50`); } export async function reviewPersonalMemoryCandidate( id: string, action: 'accept' | 'reject', ): Promise<{ updated: boolean; status: string; reviewedAt: number }> { return portalFetch(`/admin-api/memory-v2/candidates/${encodeURIComponent(id)}/${action}`, { method: 'POST', }); } export async function updateMemoryV2AdminConfig( payload: Partial, ): Promise { return portalFetch('/admin-api/memory-v2/config', { method: 'PATCH', body: JSON.stringify(payload), }); } export async function getSkillRuntimeAdminConfig(): Promise { return portalFetch('/admin-api/skill-runtime/config'); } export async function updateSkillRuntimeAdminConfig( config: SkillRuntimeAdminConfig, ): Promise { return portalFetch('/admin-api/skill-runtime/config', { method: 'PUT', body: JSON.stringify({ config }), }); } export async function listSkillRuntimeCatalog(): Promise { const result = await portalFetch<{ catalog: SkillRuntimeCatalogItem[] }>('/admin-api/skill-runtime/catalog'); return result.catalog ?? []; } 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' }); } export async function getMindSearchConfig(): Promise<{ config: MindSearchConfig; source: string }> { return portalFetch('/admin-api/mindsearch/config'); } export async function updateMindSearchConfig(config: Partial): Promise<{ config: MindSearchConfig; source: string }> { return portalFetch('/admin-api/mindsearch/config', { method: 'PATCH', body: JSON.stringify(config) }); } export async function testMindSearchService(serviceId: string): Promise { return portalFetch(`/admin-api/mindsearch/services/${encodeURIComponent(serviceId)}/test`, { method: 'POST' }); } // ── Workflow Orchestrator ───────────────────────────── export async function getOrchestratorConfig() { return portalFetch('/admin-api/orchestrator/config'); } export async function updateOrchestratorConfig(config: OrchestratorConfig) { return portalFetch('/admin-api/orchestrator/config', { method: 'PUT', body: JSON.stringify({ config }), }); } export async function getOrchestratorRuntime() { return portalFetch('/admin-api/orchestrator/runtime'); } export async function getOrchestratorShadowRuns(params: { hours?: number; limit?: number; status?: 'all' | 'succeeded' | 'failed'; } = {}) { const query = new URLSearchParams(); if (params.hours) query.set('hours', String(params.hours)); if (params.limit) query.set('limit', String(params.limit)); if (params.status) query.set('status', params.status); return portalFetch( `/admin-api/orchestrator/shadow-runs?${query}`, ); } export async function getOrchestratorExecutionPlans(params: { hours?: number; limit?: number; selection?: 'all' | 'candidate' | 'native'; } = {}) { const query = new URLSearchParams(); if (params.hours) query.set('hours', String(params.hours)); if (params.limit) query.set('limit', String(params.limit)); if (params.selection) query.set('selection', params.selection); return portalFetch( `/admin-api/orchestrator/execution-plans?${query}`, ); } export async function getOrchestratorCanaryReadiness() { return portalFetch( '/admin-api/orchestrator/canary-readiness', ); } export async function getOrchestratorShadowRun(runId: string) { return portalFetch( `/admin-api/orchestrator/shadow-runs/${encodeURIComponent(runId)}`, ); } // ── 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; } export async function fetchImageQuotaConfig() { return portalFetch<{ plans: PlanDefinition[] }>('/admin-api/image-quota/config'); } export async function patchImageQuotaPlan(planType: string, periodImages: number) { return portalFetch<{ ok: boolean; plan: PlanDefinition }>(`/admin-api/image-quota/config/${planType}`, { method: 'PATCH', body: JSON.stringify({ periodImages }), }); } export async function fetchUserImageQuota(userId: string) { return portalFetch<{ subscription: AdminSubscription; quota: ImageQuotaView; ledger: ImageQuotaLedgerEntry[]; }>(`/admin-api/users/${userId}/image-quota`); } export async function grantUserImageQuota(userId: string, delta: number, note = '') { return portalFetch<{ ok: boolean; subscription: AdminSubscription; quota: ImageQuotaView; }>(`/admin-api/users/${userId}/image-quota/grant`, { method: 'POST', body: JSON.stringify({ delta, note }), }); } export async function fetchImageQuotaLedger(params: { userId?: string; page?: number; pageSize?: number; } = {}) { 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)); return portalFetch<{ entries: ImageQuotaLedgerEntry[]; total: number; page: number; pageSize: number; totalPages: number; }>(`/admin-api/image-quota/ledger${q.toString() ? `?${q}` : ''}`); }