Initial commit: tkmind admin frontend (React + Vite).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,483 @@
|
||||
import type {
|
||||
AdminDashboardSummary,
|
||||
AdminUserRow,
|
||||
AuthStatus,
|
||||
CapabilityDefinition,
|
||||
CapabilityMap,
|
||||
InsufficientBalanceDetails,
|
||||
LedgerEntry,
|
||||
LlmConnectionTestResult,
|
||||
LlmGlobalSettings,
|
||||
LlmProviderDefinition,
|
||||
LlmProviderKeyRow,
|
||||
PolicyDefinition,
|
||||
PolicyMap,
|
||||
PortalUser,
|
||||
SkillDefinition,
|
||||
SkillMap,
|
||||
UsageRecord,
|
||||
} 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,
|
||||
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');
|
||||
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',
|
||||
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> {
|
||||
if (import.meta.env.DEV) return;
|
||||
await fetch('/auth/logout', { method: 'POST' });
|
||||
}
|
||||
|
||||
// ── Admin dashboard ───────────────────────────────────
|
||||
|
||||
export async function getAdminDashboardSummary(): Promise<AdminDashboardSummary> {
|
||||
const result = await portalFetch<{ summary: AdminDashboardSummary }>('/admin-api/summary');
|
||||
return result.summary;
|
||||
}
|
||||
|
||||
export async function listAdminUsage(userId?: string): Promise<UsageRecord[]> {
|
||||
const query = userId ? `?userId=${encodeURIComponent(userId)}` : '';
|
||||
const result = await portalFetch<{ records: UsageRecord[] }>(`/admin-api/usage${query}`);
|
||||
return result.records ?? [];
|
||||
}
|
||||
|
||||
export async function listAdminLedger(userId?: string): Promise<LedgerEntry[]> {
|
||||
const query = userId ? `?userId=${encodeURIComponent(userId)}` : '';
|
||||
const result = await portalFetch<{ entries: LedgerEntry[] }>(`/admin-api/ledger${query}`);
|
||||
return result.entries ?? [];
|
||||
}
|
||||
|
||||
// ── Admin users ───────────────────────────────────────
|
||||
|
||||
export async function listAdminUsers(): Promise<AdminUserRow[]> {
|
||||
const result = await portalFetch<{ users: AdminUserRow[] }>('/admin-api/users');
|
||||
return result.users ?? [];
|
||||
}
|
||||
|
||||
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 } : {}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user