Files
memind_adm/src/api/client.ts
T
2026-06-20 17:02:16 +08:00

630 lines
20 KiB
TypeScript

import type {
AdminDashboardSummary,
AdminUserRow,
AuthStatus,
CapabilityDefinition,
CapabilityMap,
InsufficientBalanceDetails,
LedgerEntry,
LlmConnectionTestResult,
LlmGlobalSettings,
LlmProviderDefinition,
LlmProviderKeyRow,
PolicyDefinition,
PolicyMap,
PortalUser,
SkillDefinition,
SkillMap,
UsageRecord,
WechatAdminSummary,
WechatBinding,
WechatDeliveryLog,
WechatDigestSubscription,
WechatMessage,
} 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 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,
},
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 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 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 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;
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 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 } : {}),
});
}