Files
memind_adm/src/api/client.ts
T
john 731d93843d feat(admin): add Memory V2 personal controls and Skill Runtime page
Wire admin UI and API routes to restored Memind personal memory and skill
runtime modules, including candidate review and runtime status display.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:00:20 +08:00

1087 lines
35 KiB
TypeScript

import type {
AssetGatewayConfig,
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,
PersonalMemoryCandidateListResponse,
PolicyDefinition,
PolicyMap,
PortalUser,
MindSpaceAdminConfig,
MemoryV2AdminConfig,
MemoryV2AdminConfigResponse,
MemoryV2ModelApiType,
MemoryV2RuntimeStatusResponse,
SkillRuntimeAdminConfig,
SkillRuntimeAdminConfigResponse,
SkillRuntimeCatalogItem,
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<string, unknown>;
constructor(
status: number,
message: string,
code?: string,
details?: InsufficientBalanceDetails | Record<string, unknown>,
) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
this.details = details;
}
}
async function parseErrorResponse(res: Response) {
const text = await res.text().catch(() => '');
try {
const body = JSON.parse(text) as Record<string, unknown>;
const nested =
body.error && typeof body.error === 'object'
? (body.error as Record<string, unknown>)
: body;
const message =
typeof nested.message === 'string'
? nested.message
: typeof body.message === 'string'
? body.message
: text;
const code =
typeof nested.code === 'string'
? nested.code
: typeof body.code === 'string'
? body.code
: undefined;
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<T>(path: string, init?: RequestInit): Promise<T> {
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<T>;
}
// ── Auth ──────────────────────────────────────────────
export async function checkAuth(): Promise<AuthStatus> {
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<PortalUser | null> {
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<void> {
await fetch('/auth/logout', { method: 'POST', credentials: 'include' });
}
// ── Pagination helpers ────────────────────────────────
export type PagedResult<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
};
// ── Admin dashboard ───────────────────────────────────
export async function getAdminDashboardSummary(): Promise<AdminDashboardSummary> {
const result = await portalFetch<{ summary: AdminDashboardSummary }>('/admin-api/summary');
return result.summary;
}
export async function restartAdminService(
action: AdminServiceRestartAction,
): Promise<AdminServiceRestartResult> {
return portalFetch<AdminServiceRestartResult>(`/admin-api/service-restart/${action}`, {
method: 'POST',
});
}
export async function listAdminUsage(params?: {
userId?: string;
page?: number;
pageSize?: number;
}): Promise<PagedResult<UsageRecord>> {
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<PagedResult<LedgerEntry>> {
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<WechatAdminSummary> {
const summary = await portalFetch<Partial<WechatAdminSummary>>('/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<WechatScheduleLlmConfig, 'scheduleLlmEnabled'>,
): Promise<WechatScheduleLlmConfig> {
return portalFetch<WechatScheduleLlmConfig>('/admin-api/wechat/schedule-llm-config', {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
export async function getAssetGatewayConfig(): Promise<AssetGatewayConfig> {
return portalFetch('/admin-api/asset-gateway/config');
}
export async function updateAssetGatewayConfig(
payload: Pick<AssetGatewayConfig, 'enabled'>,
): Promise<AssetGatewayConfig> {
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;
},
): 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<MindSpaceAdminConfig> {
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
return result.config;
}
export async function updateMindSpaceAdminConfig(
payload: Partial<MindSpaceAdminConfig>,
): Promise<MindSpaceAdminConfig> {
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config', {
method: 'PATCH',
body: JSON.stringify(payload),
});
return result.config;
}
export async function getMemoryV2AdminConfig(): Promise<MemoryV2AdminConfigResponse> {
return portalFetch('/admin-api/memory-v2/config');
}
export async function getMemoryV2RuntimeStatus(): Promise<MemoryV2RuntimeStatusResponse> {
return portalFetch('/admin-api/memory-v2/status');
}
export async function listPersonalMemoryCandidates(
status = 'candidate',
): Promise<PersonalMemoryCandidateListResponse> {
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<MemoryV2AdminConfig>,
): Promise<MemoryV2AdminConfigResponse> {
return portalFetch('/admin-api/memory-v2/config', {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
export async function getSkillRuntimeAdminConfig(): Promise<SkillRuntimeAdminConfigResponse> {
return portalFetch('/admin-api/skill-runtime/config');
}
export async function updateSkillRuntimeAdminConfig(
config: SkillRuntimeAdminConfig,
): Promise<SkillRuntimeAdminConfigResponse> {
return portalFetch('/admin-api/skill-runtime/config', {
method: 'PUT',
body: JSON.stringify({ config }),
});
}
export async function listSkillRuntimeCatalog(): Promise<SkillRuntimeCatalogItem[]> {
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<AdminSystemTestReport> {
return portalFetch('/admin-api/system-tests/skill-validation', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function listSystemTestAccounts(): Promise<AdminSystemTestAccount[]> {
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<AdminSystemTestAccount> {
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<void> {
await portalFetch(`/admin-api/system-tests/accounts/${encodeURIComponent(accountId)}`, {
method: 'DELETE',
});
}
export async function listWechatBindings(params?: {
search?: string;
limit?: number;
}): Promise<WechatBinding[]> {
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<WechatMessage[]> {
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<WechatDigestSubscription[]> {
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<WechatDeliveryLog[]> {
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<WechatWebNotification[]> {
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<PagedResult<AdminUserRow>> {
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<PortalUser> {
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<PortalUser> {
const result = await portalFetch<{ user: PortalUser }>('/admin-api/users', {
method: 'POST',
body: JSON.stringify(payload),
});
return result.user;
}
export async function updateAdminUser(
userId: string,
payload: Partial<{
displayName: string;
status: 'active' | 'suspended' | 'disabled';
workspaceRoot: string;
balanceCents: number;
spaceQuotaBytes: number;
role: 'user' | 'admin';
}>,
): Promise<PortalUser> {
const result = await portalFetch<{ user: PortalUser }>(`/admin-api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
return result.user;
}
export async function rechargeUser(
userId: string,
amountCents: number,
note?: string,
): Promise<PortalUser> {
const result = await portalFetch<{ user: PortalUser }>(`/admin-api/users/${userId}/recharge`, {
method: 'POST',
body: JSON.stringify({ amountCents, note }),
});
return result.user;
}
// ── Capabilities ──────────────────────────────────────
export async function listCapabilityCatalog(): Promise<CapabilityDefinition[]> {
const result = await portalFetch<{ catalog: CapabilityDefinition[] }>(
'/admin-api/capabilities/catalog',
);
return result.catalog ?? [];
}
export async function getRoleCapabilities(role: 'user' = 'user'): Promise<{
role: string;
capabilities: CapabilityMap;
}> {
return portalFetch(`/admin-api/capabilities/role/${role}`);
}
export async function updateRoleCapabilities(
role: 'user',
capabilities: CapabilityMap,
): Promise<{ role: string; capabilities: CapabilityMap }> {
return portalFetch(`/admin-api/capabilities/role/${role}`, {
method: 'PUT',
body: JSON.stringify({ capabilities }),
});
}
export async function getUserCapabilities(userId: string): Promise<{
userId: string;
role: string;
unrestricted: boolean;
capabilities: CapabilityMap;
overrides: CapabilityMap;
}> {
return portalFetch(`/admin-api/users/${userId}/capabilities`);
}
export async function updateUserCapabilities(
userId: string,
capabilities: CapabilityMap,
): Promise<{ userId: string; capabilities: CapabilityMap }> {
return portalFetch(`/admin-api/users/${userId}/capabilities`, {
method: 'PUT',
body: JSON.stringify({ capabilities }),
});
}
export async function clearUserCapabilityOverrides(userId: string): Promise<void> {
await portalFetch(`/admin-api/users/${userId}/capabilities`, { method: 'DELETE' });
}
// ── Policies ──────────────────────────────────────────
export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
const result = await portalFetch<{ catalog: PolicyDefinition[] }>('/admin-api/policies/catalog');
return result.catalog ?? [];
}
export async function getRolePolicies(role: 'user' = 'user'): Promise<{
role: string;
policies: PolicyMap;
}> {
return portalFetch(`/admin-api/policies/role/${role}`);
}
export async function updateRolePolicies(
role: 'user',
policies: PolicyMap,
): Promise<{ role: string; policies: PolicyMap }> {
return portalFetch(`/admin-api/policies/role/${role}`, {
method: 'PUT',
body: JSON.stringify({ policies }),
});
}
export async function getUserPolicies(userId: string): Promise<{
userId: string;
role: string;
unrestricted: boolean;
policies: PolicyMap;
overrides: PolicyMap;
}> {
return portalFetch(`/admin-api/users/${userId}/policies`);
}
export async function updateUserPolicies(
userId: string,
policies: PolicyMap,
): Promise<{ userId: string; policies: PolicyMap }> {
return portalFetch(`/admin-api/users/${userId}/policies`, {
method: 'PUT',
body: JSON.stringify({ policies }),
});
}
export async function clearUserPolicyOverrides(userId: string): Promise<void> {
await portalFetch(`/admin-api/users/${userId}/policies`, { method: 'DELETE' });
}
// ── Skills ────────────────────────────────────────────
export async function listSkillCatalog(): Promise<SkillDefinition[]> {
const result = await portalFetch<{ catalog: SkillDefinition[] }>('/admin-api/skills/catalog');
return result.catalog ?? [];
}
export async function getRoleSkills(role: 'user' = 'user'): Promise<{
role: string;
skills: SkillMap;
}> {
return portalFetch(`/admin-api/skills/role/${role}`);
}
export async function updateRoleSkills(
role: 'user',
skills: SkillMap,
): Promise<{ role: string; skills: SkillMap }> {
return portalFetch(`/admin-api/skills/role/${role}`, {
method: 'PUT',
body: JSON.stringify({ skills }),
});
}
export async function getUserSkills(userId: string): Promise<{
userId: string;
role: string;
skills: SkillMap;
grantedSkills: string[];
overrides: SkillMap;
}> {
return portalFetch(`/admin-api/users/${userId}/skills`);
}
export async function updateUserSkills(
userId: string,
skills: SkillMap,
): Promise<{ userId: string; skills: SkillMap; grantedSkills: string[] }> {
return portalFetch(`/admin-api/users/${userId}/skills`, {
method: 'PUT',
body: JSON.stringify({ skills }),
});
}
export async function clearUserSkillOverrides(userId: string): Promise<void> {
await portalFetch(`/admin-api/users/${userId}/skills`, { method: 'DELETE' });
}
// ── LLM Providers ─────────────────────────────────────
export async function listLlmProviderCatalog(): Promise<LlmProviderDefinition[]> {
const result = await portalFetch<{ catalog: LlmProviderDefinition[] }>(
'/admin-api/llm-providers/catalog',
);
return result.catalog ?? [];
}
export async function listLlmProviderKeys(): Promise<LlmProviderKeyRow[]> {
const result = await portalFetch<{ keys: LlmProviderKeyRow[] }>('/admin-api/llm-providers/keys');
return result.keys ?? [];
}
export async function createLlmProviderKey(payload: {
providerId: string;
name: string;
apiKey: string;
defaultModel?: string;
apiUrl?: string;
basePath?: string;
models?: string | string[];
engine?: string;
relayProvider?: string;
}): Promise<LlmProviderKeyRow> {
const result = await portalFetch<{ key: LlmProviderKeyRow }>('/admin-api/llm-providers/keys', {
method: 'POST',
body: JSON.stringify(payload),
});
return result.key;
}
export async function updateLlmProviderKey(
keyId: string,
payload: {
name?: string;
apiKey?: string;
defaultModel?: string;
status?: 'active' | 'disabled';
apiUrl?: string;
basePath?: string;
models?: string | string[];
engine?: string;
relayProvider?: string;
},
): Promise<LlmProviderKeyRow> {
const result = await portalFetch<{ key: LlmProviderKeyRow }>(
`/admin-api/llm-providers/keys/${keyId}`,
{ method: 'PATCH', body: JSON.stringify(payload) },
);
return result.key;
}
export async function selectLlmProviderKey(keyId: string): Promise<LlmProviderKeyRow> {
const result = await portalFetch<{ key: LlmProviderKeyRow }>(
`/admin-api/llm-providers/keys/${keyId}/select`,
{ method: 'POST' },
);
return result.key;
}
export async function deleteLlmProviderKey(keyId: string): Promise<void> {
await portalFetch(`/admin-api/llm-providers/keys/${keyId}`, { method: 'DELETE' });
}
export async function syncLlmProviderToGoosed(): Promise<{ synced: boolean }> {
return portalFetch('/admin-api/llm-providers/sync', { method: 'POST' });
}
export async function getLlmGlobalSettings(): Promise<LlmGlobalSettings> {
const result = await portalFetch<{ global: LlmGlobalSettings }>('/admin-api/llm-providers/global');
return result.global;
}
export async function getLlmVisionSettings(): Promise<LlmVisionSettings> {
const result = await portalFetch<{ vision: LlmVisionSettings }>('/admin-api/llm-providers/vision');
return result.vision;
}
export async function setLlmVisionKey(keyId: string, model: string): Promise<LlmVisionSettings> {
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<void> {
await portalFetch('/admin-api/llm-providers/vision', { method: 'DELETE' });
}
export async function listLlmExecutorBindings(): Promise<LlmExecutorBinding[]> {
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<LlmExecutorBinding> {
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<LlmExecutorRuntime[]> {
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<LlmExecutorLaunchPlan[]> {
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<Array<LlmExecutorLaunchState | null>> {
const q = new URLSearchParams();
if (params.purpose) q.set('purpose', params.purpose);
const result = await portalFetch<{ launches: Array<LlmExecutorLaunchState | null> }>(
`/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<LlmExecutorLaunchState> {
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<LlmExecutorLaunchState | null> {
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<LlmExecutorLaunchState> {
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<LlmConnectionTestResult> {
return portalFetch('/admin-api/llm-providers/test', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function testLlmProviderKey(
keyId: string,
model?: string,
): Promise<LlmConnectionTestResult> {
return portalFetch(`/admin-api/llm-providers/keys/${keyId}/test`, {
method: 'POST',
body: JSON.stringify(model ? { model } : {}),
});
}
// ── Blocked words ─────────────────────────────────────
export async function listBlockedWords(): Promise<BlockedWord[]> {
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<BlockedWord> {
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<BlockedWord> {
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<void> {
await portalFetch(`/admin-api/blocked-words/${id}`, { method: 'DELETE' });
}
// ── Subscriptions ──────────────────────────────────────
export async function listSubscriptionPlans(): Promise<PlanDefinition[]> {
const result = await portalFetch<{ plans: PlanDefinition[] }>('/admin-api/subscriptions/plans');
return result.plans;
}
export async function createSubscriptionPlan(
planType: string,
data: Partial<PlanDefinition>,
): 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<PlanDefinition>,
): 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<PlanSyncResult> {
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<AdminSubscription> {
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<void> {
await portalFetch(`/admin-api/users/${userId}/subscription`, { method: 'DELETE' });
}
export async function getUserSubscription(userId: string): Promise<AdminSubscription | null> {
const result = await portalFetch<{ subscription: AdminSubscription | null }>(
`/admin-api/users/${userId}/subscription`,
);
return result.subscription;
}