// 后台 API:同源 Cookie 鉴权,复用 AgentClient 的请求与错误处理 import { AgentClient, BotClient } from '@agent-server/client' import type { ApiKeyDto, ApiKeyProfiles, BotDto, BotInput, BotKeyDto, ApiReferenceDto, AppSettings, BrowserTokenDto, CodexPluginCatalogDto, CodexPluginDetailDto, CodexPluginInstallResultDto, CodexSkillCatalogDto, CommunitySearchResultDto, CustomSkillDetailDto, CustomSkillDto, FileDiffDto, SkillScanDto, SkillSourceDto, SkillSourceInput, CommunitySkillDto, CommunitySkillPreviewDto, DeepPartial, FileAccessPolicy, FsEntryDto, FsRootDto, FsShortcutDto, KnowledgeRootDto, McpServerDto, McpServerInput, McpStateDto, McpTestResultDto, McpToolPolicy, PathCheck, PluginAction, PluginStateDto, ProfileDto, ProfileInput, ProfileVersionDto, ProviderQuotaDto, ProviderStatusDto, RecentErrorDto, ResolvedAccessDto, SkillInfo, UsageSummaryDto, } from '@agent-server/shared' export const client = new AgentClient({ baseUrl: '' }) /** Bot Playground:以后台身份调用 /bot/v1(ADR-0024) */ export const botClient = new BotClient({ baseUrl: '' }) const req = (method: string, path: string, body?: unknown) => client.request(method, `/admin/v1${path}`, body) const fsq = (root: string, path: string) => `root=${encodeURIComponent(root)}&path=${encodeURIComponent(path)}` export interface AuditRow { id: number; ts: string; actor: string; sessionId: string | null; kind: string; summary: string; detail: unknown } export type KnowledgeInput = Pick export const admin = { me: () => req<{ loggedIn: boolean }>('GET', '/auth/me'), login: (password: string) => req<{ ok: true }>('POST', '/auth/login', { password }), logout: () => req<{ ok: true }>('POST', '/auth/logout'), changePassword: (current: string, next: string) => req<{ ok: true }>('POST', '/auth/password', { current, next }), claudeLogin: { current: () => req<{ id: string; url: string; expiresAt: string } | null>('GET', '/providers/claude/login'), start: (body: { sso?: boolean; email?: string }) => req<{ id: string; url: string; expiresAt: string }>('POST', '/providers/claude/login', body), submit: (id: string, code: string) => req<{ ok: boolean; message: string; retry?: boolean }>('POST', `/providers/claude/login/${id}/code`, { code }), cancel: (id: string) => req('DELETE', `/providers/claude/login/${id}`), logout: () => req<{ ok: true }>('POST', '/providers/claude/logout'), }, codexLogin: { current: () => req('GET', '/providers/codex/login'), start: () => req('POST', '/providers/codex/login'), cancel: () => req('DELETE', '/providers/codex/login'), logout: () => req<{ ok: true }>('POST', '/providers/codex/logout'), }, providerQuota: (id: string) => req('GET', `/providers/${id}/quota`), apiReference: () => req('GET', '/api-reference.json'), apiKeyDebugToken: (id: string) => req('POST', `/api-keys/${id}/debug-token`), apiReferenceMeta: () => req<{ pdfAvailable: boolean }>('GET', '/api-reference/meta'), usage: (days = 30) => req('GET', `/usage?days=${days}`), usageErrors: (limit = 10) => req('GET', `/usage/errors?limit=${limit}`), overview: () => req<{ providers: ProviderStatusDto[]; runs: { running: number; queued: number; active: number }; version: string }>('GET', '/overview'), settings: () => req<{ settings: AppSettings; defaults: AppSettings; builtinGlobalDeny: string[] }>('GET', '/settings'), updateSettings: (patch: DeepPartial) => req<{ settings: AppSettings }>('PATCH', '/settings', patch), checkPath: (path: string, purpose: 'knowledge' | 'generic' = 'knowledge') => req('POST', '/paths/check', { path, purpose }), skills: (engine: 'claude' | 'codex' = 'claude') => req('GET', `/skills?engine=${engine}`), codexSkills: { catalog: () => req('GET', '/codex-skills'), refresh: () => req('POST', '/codex-skills/refresh'), install: (name: string) => req('POST', `/codex-skills/${encodeURIComponent(name)}`), uninstall: (name: string) => req('DELETE', `/codex-skills/${encodeURIComponent(name)}`), }, codexPlugins: { catalog: (q: { q?: string; category?: string; installed?: boolean; offset?: number; refresh?: boolean }) => req('GET', `/codex-plugins?${new URLSearchParams({ q: q.q ?? '', category: q.category ?? '', installed: q.installed ? '1' : '', offset: String(q.offset ?? 0), refresh: q.refresh ? '1' : '', })}`), detail: (name: string) => req('GET', `/codex-plugins/${encodeURIComponent(name)}`), install: (name: string) => req('POST', `/codex-plugins/${encodeURIComponent(name)}/install`), uninstall: (name: string) => req<{ ok: true }>('POST', `/codex-plugins/${encodeURIComponent(name)}/uninstall`), }, fs: { roots: () => req<{ roots: FsRootDto[]; shortcuts: FsShortcutDto[] }>('GET', '/fs/roots'), list: (root: string, path: string) => req<{ entries: FsEntryDto[]; truncated: boolean; sessionTitle: string | null }>('GET', `/fs/list?${fsq(root, path)}`), readText: (root: string, path: string) => req<{ content: string; mtime: string }>('GET', `/fs/text?${fsq(root, path)}`), writeText: (root: string, path: string, content: string, baseMtime: string | null) => req<{ mtime: string }>('PUT', `/fs/text?${fsq(root, path)}`, { content, baseMtime }), mkdir: (root: string, path: string) => req<{ ok: true }>('POST', `/fs/mkdir?${fsq(root, path)}`), move: (root: string, path: string, to: string) => req<{ ok: true }>('POST', `/fs/move?${fsq(root, path)}`, { to }), remove: (root: string, path: string) => req<{ ok: true }>('DELETE', `/fs/entry?${fsq(root, path)}`), upload: (root: string, dir: string, files: File[]) => { const form = new FormData() for (const f of files) form.append('file', f, f.name) return req('POST', `/fs/upload?${fsq(root, dir)}`, form) }, downloadUrl: (root: string, path: string, inline = false) => `/admin/v1/fs/download?${fsq(root, path)}${inline ? '&inline=1' : ''}`, zipUrl: (root: string, path: string) => `/admin/v1/fs/zip?${fsq(root, path)}`, }, customSkills: { list: () => req('GET', '/custom-skills'), create: (name: string, description: string) => req('POST', '/custom-skills', { name, description }), get: (id: string) => req('GET', `/custom-skills/${id}`), setFlags: (id: string, flags: { enabled?: boolean; includeInAll?: boolean }) => req('PATCH', `/custom-skills/${id}`, flags), remove: (id: string) => req('DELETE', `/custom-skills/${id}`), fileUrl: (id: string, path: string, version?: number) => `/admin/v1/custom-skills/${id}/file?path=${encodeURIComponent(path)}${version === undefined ? '' : `&version=${version}`}`, readText: async (id: string, path: string, version?: number) => { const r = await fetch(`/admin/v1/custom-skills/${id}/file?path=${encodeURIComponent(path)}${version === undefined ? '' : `&version=${version}`}`, { credentials: 'same-origin' }) if (!r.ok) throw new Error((await r.json().catch(() => null))?.error?.message ?? `HTTP ${r.status}`) return r.text() }, writeText: (id: string, path: string, content: string) => req('PUT', `/custom-skills/${id}/file?path=${encodeURIComponent(path)}`, { content }), upload: (id: string, path: string, file: File) => { const form = new FormData() form.append('file', file, file.name) return req('PUT', `/custom-skills/${id}/file?path=${encodeURIComponent(path)}`, form) }, deleteFile: (id: string, path: string) => req('DELETE', `/custom-skills/${id}/file?path=${encodeURIComponent(path)}`), move: (id: string, from: string, to: string) => req('POST', `/custom-skills/${id}/move`, { from, to }), discard: (id: string) => req('POST', `/custom-skills/${id}/discard`), save: (id: string, baseVersion: number, publish: boolean) => req('POST', `/custom-skills/${id}/save`, { baseVersion, publish }), publish: (id: string, version: number) => req('POST', `/custom-skills/${id}/publish`, { version }), restore: (id: string, version: number) => req('POST', `/custom-skills/${id}/versions/${version}/restore`), diff: (id: string, from: number | 'working', to: number | 'working') => req('GET', `/custom-skills/${id}/diff?from=${from}&to=${to}`), exportUrl: (id: string, version?: number) => `/admin/v1/custom-skills/${id}/export${version === undefined ? '' : `?version=${version}`}`, detach: (id: string) => req('POST', `/custom-skills/${id}/detach`), }, skillSources: { list: () => req('GET', '/skill-sources'), create: (input: SkillSourceInput) => req('POST', '/skill-sources', input), update: (id: string, input: SkillSourceInput) => req('PUT', `/skill-sources/${id}`, input), remove: (id: string) => req('DELETE', `/skill-sources/${id}`), scan: (id: string) => req('POST', `/skill-sources/${id}/scan`), import: (id: string, skills: string[], digest: string) => req<{ imported: { name: string; version: number; created: boolean }[]; scan: SkillScanDto }>('POST', `/skill-sources/${id}/import`, { skills, digest }), upload: (file: File) => { const form = new FormData() form.append('file', file, file.name) return req<{ source: SkillSourceDto; scan: SkillScanDto }>('POST', '/skill-sources/upload', form) }, }, mcp: { state: () => req('GET', '/mcp'), create: (input: McpServerInput) => req('POST', '/mcp/servers', input), update: (id: string, input: McpServerInput) => req('PUT', `/mcp/servers/${id}`, input), remove: (id: string) => req('DELETE', `/mcp/servers/${id}`), refresh: (id: string) => req('POST', `/mcp/servers/${id}/refresh`), setToolPolicy: (id: string, tool: string, policy: McpToolPolicy) => req('PUT', `/mcp/servers/${id}/tools/${encodeURIComponent(tool)}`, { policy }), test: (input: McpServerInput, id: string | null) => req('POST', '/mcp/test', { input, id }), }, communitySkills: { list: () => req('GET', '/community-skills'), search: (q: string) => req('GET', `/community-skills/search?q=${encodeURIComponent(q)}`), preview: (source: string, skillId: string) => req('POST', '/community-skills/preview', { source, skillId }), install: (source: string, skillId: string, sha: string) => req('POST', '/community-skills/install', { source, skillId, sha }), uninstall: (name: string) => req('DELETE', `/community-skills/${encodeURIComponent(name)}`), }, plugins: (refresh = false) => req('GET', `/plugins${refresh ? '?refresh=1' : ''}`), pluginAction: (a: PluginAction) => req<{ message: string; state: PluginStateDto }>('POST', '/plugins/actions', a), profiles: () => req('GET', '/profiles'), createProfile: (p: ProfileInput) => req('POST', '/profiles', p), updateProfile: (id: string, p: ProfileInput) => req('PUT', `/profiles/${id}`, p), deleteProfile: (id: string) => req('DELETE', `/profiles/${id}`), profileVersions: (id: string) => req('GET', `/profiles/${id}/versions`), restoreProfile: (id: string, version: number) => req('POST', `/profiles/${id}/versions/${version}/restore`), previewAccess: (fileAccess: FileAccessPolicy, knowledgeRootIds: string[]) => req('POST', '/access/preview', { fileAccess, knowledgeRootIds }), knowledge: () => req('GET', '/knowledge'), createKnowledge: (k: KnowledgeInput) => req('POST', '/knowledge', k), updateKnowledge: (id: string, k: KnowledgeInput) => req('PUT', `/knowledge/${id}`, k), deleteKnowledge: (id: string) => req('DELETE', `/knowledge/${id}`), bots: () => req('GET', '/bots'), createBot: (input: BotInput) => req('POST', '/bots', input), updateBot: (id: string, input: BotInput) => req('PUT', `/bots/${id}`, input), deleteBot: (id: string) => req('DELETE', `/bots/${id}`), botKeys: () => req('GET', '/bot-keys'), createBotKey: (name: string, allowedBotIds: string[]) => req<{ key: string; botKey: BotKeyDto }>('POST', '/bot-keys', { name, allowedBotIds }), setBotKeyEnabled: (id: string, enabled: boolean) => req<{ ok: true }>('PATCH', `/bot-keys/${id}`, { enabled }), setBotKeyBots: (id: string, allowedBotIds: string[]) => req<{ ok: true }>('PUT', `/bot-keys/${id}/bots`, { allowedBotIds }), deleteBotKey: (id: string) => req('DELETE', `/bot-keys/${id}`), botKeyDebugToken: (id: string) => req('POST', `/bot-keys/${id}/debug-token`), apiKeys: () => req('GET', '/api-keys'), createApiKey: (name: string, access: ApiKeyProfiles) => req<{ key: string; apiKey: ApiKeyDto }>('POST', '/api-keys', { name, ...access }), setApiKeyProfiles: (id: string, access: ApiKeyProfiles) => req<{ ok: true }>('PUT', `/api-keys/${id}/profiles`, access), setApiKeyEnabled: (id: string, enabled: boolean) => req<{ ok: true }>('PATCH', `/api-keys/${id}`, { enabled }), deleteApiKey: (id: string) => req('DELETE', `/api-keys/${id}`), audit: (q: { kind?: string; sessionId?: string; limit?: number } = {}) => { const s = new URLSearchParams(Object.entries(q).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => [k, String(v)])) return req('GET', `/audit${s.size ? `?${s}` : ''}`) }, } export interface CodexLoginState { loginId: string; verificationUrl: string; userCode: string; status: 'pending' | 'success' | 'failed' | 'cancelled'; error: string | null } export const errorMessage = (err: unknown) => (err instanceof Error ? err.message : String(err))