1016 lines
32 KiB
TypeScript
1016 lines
32 KiB
TypeScript
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<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 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 updateMemoryV2AdminConfig(
|
|
payload: Partial<MemoryV2AdminConfig>,
|
|
): Promise<MemoryV2AdminConfigResponse> {
|
|
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<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;
|
|
}
|