api.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. // 后台 API:同源 Cookie 鉴权,复用 AgentClient 的请求与错误处理
  2. import { AgentClient, BotClient } from '@agent-server/client'
  3. import type {
  4. 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,
  5. ProviderQuotaDto, ProviderStatusDto, RecentErrorDto, ResolvedAccessDto, SkillInfo, UsageSummaryDto,
  6. } from '@agent-server/shared'
  7. export const client = new AgentClient({ baseUrl: '' })
  8. /** Bot Playground:以后台身份调用 /bot/v1(ADR-0024) */
  9. export const botClient = new BotClient({ baseUrl: '' })
  10. const req = <T>(method: string, path: string, body?: unknown) => client.request<T>(method, `/admin/v1${path}`, body)
  11. const fsq = (root: string, path: string) => `root=${encodeURIComponent(root)}&path=${encodeURIComponent(path)}`
  12. export interface AuditRow { id: number; ts: string; actor: string; sessionId: string | null; kind: string; summary: string; detail: unknown }
  13. export type KnowledgeInput = Pick<KnowledgeRootDto, 'name' | 'path' | 'description' | 'access' | 'enabled'>
  14. export const admin = {
  15. me: () => req<{ loggedIn: boolean }>('GET', '/auth/me'),
  16. login: (password: string) => req<{ ok: true }>('POST', '/auth/login', { password }),
  17. logout: () => req<{ ok: true }>('POST', '/auth/logout'),
  18. changePassword: (current: string, next: string) => req<{ ok: true }>('POST', '/auth/password', { current, next }),
  19. claudeLogin: {
  20. current: () => req<{ id: string; url: string; expiresAt: string } | null>('GET', '/providers/claude/login'),
  21. start: (body: { sso?: boolean; email?: string }) => req<{ id: string; url: string; expiresAt: string }>('POST', '/providers/claude/login', body),
  22. submit: (id: string, code: string) => req<{ ok: boolean; message: string; retry?: boolean }>('POST', `/providers/claude/login/${id}/code`, { code }),
  23. cancel: (id: string) => req<void>('DELETE', `/providers/claude/login/${id}`),
  24. logout: () => req<{ ok: true }>('POST', '/providers/claude/logout'),
  25. },
  26. codexLogin: {
  27. current: () => req<CodexLoginState | null>('GET', '/providers/codex/login'),
  28. start: () => req<CodexLoginState>('POST', '/providers/codex/login'),
  29. cancel: () => req<void>('DELETE', '/providers/codex/login'),
  30. logout: () => req<{ ok: true }>('POST', '/providers/codex/logout'),
  31. },
  32. providerQuota: (id: string) => req<ProviderQuotaDto>('GET', `/providers/${id}/quota`),
  33. apiReference: () => req<ApiReferenceDto>('GET', '/api-reference.json'),
  34. apiKeyDebugToken: (id: string) => req<BrowserTokenDto>('POST', `/api-keys/${id}/debug-token`),
  35. apiReferenceMeta: () => req<{ pdfAvailable: boolean }>('GET', '/api-reference/meta'),
  36. usage: (days = 30) => req<UsageSummaryDto>('GET', `/usage?days=${days}`),
  37. usageErrors: (limit = 10) => req<RecentErrorDto[]>('GET', `/usage/errors?limit=${limit}`),
  38. overview: () => req<{ providers: ProviderStatusDto[]; runs: { running: number; queued: number; active: number }; version: string }>('GET', '/overview'),
  39. settings: () => req<{ settings: AppSettings; defaults: AppSettings; builtinGlobalDeny: string[] }>('GET', '/settings'),
  40. updateSettings: (patch: DeepPartial<AppSettings>) => req<{ settings: AppSettings }>('PATCH', '/settings', patch),
  41. checkPath: (path: string, purpose: 'knowledge' | 'generic' = 'knowledge') => req<PathCheck>('POST', '/paths/check', { path, purpose }),
  42. skills: (engine: 'claude' | 'codex' = 'claude') => req<SkillInfo[]>('GET', `/skills?engine=${engine}`),
  43. codexSkills: {
  44. catalog: () => req<CodexSkillCatalogDto>('GET', '/codex-skills'),
  45. refresh: () => req<CodexSkillCatalogDto>('POST', '/codex-skills/refresh'),
  46. install: (name: string) => req<CodexSkillCatalogDto>('POST', `/codex-skills/${encodeURIComponent(name)}`),
  47. uninstall: (name: string) => req<CodexSkillCatalogDto>('DELETE', `/codex-skills/${encodeURIComponent(name)}`),
  48. },
  49. codexPlugins: {
  50. catalog: (q: { q?: string; category?: string; installed?: boolean; offset?: number; refresh?: boolean }) => req<CodexPluginCatalogDto>('GET', `/codex-plugins?${new URLSearchParams({
  51. q: q.q ?? '', category: q.category ?? '', installed: q.installed ? '1' : '', offset: String(q.offset ?? 0), refresh: q.refresh ? '1' : '',
  52. })}`),
  53. detail: (name: string) => req<CodexPluginDetailDto>('GET', `/codex-plugins/${encodeURIComponent(name)}`),
  54. install: (name: string) => req<CodexPluginInstallResultDto>('POST', `/codex-plugins/${encodeURIComponent(name)}/install`),
  55. uninstall: (name: string) => req<{ ok: true }>('POST', `/codex-plugins/${encodeURIComponent(name)}/uninstall`),
  56. },
  57. fs: {
  58. roots: () => req<{ roots: FsRootDto[]; shortcuts: FsShortcutDto[] }>('GET', '/fs/roots'),
  59. list: (root: string, path: string) => req<{ entries: FsEntryDto[]; truncated: boolean; sessionTitle: string | null }>('GET', `/fs/list?${fsq(root, path)}`),
  60. readText: (root: string, path: string) => req<{ content: string; mtime: string }>('GET', `/fs/text?${fsq(root, path)}`),
  61. writeText: (root: string, path: string, content: string, baseMtime: string | null) => req<{ mtime: string }>('PUT', `/fs/text?${fsq(root, path)}`, { content, baseMtime }),
  62. mkdir: (root: string, path: string) => req<{ ok: true }>('POST', `/fs/mkdir?${fsq(root, path)}`),
  63. move: (root: string, path: string, to: string) => req<{ ok: true }>('POST', `/fs/move?${fsq(root, path)}`, { to }),
  64. remove: (root: string, path: string) => req<{ ok: true }>('DELETE', `/fs/entry?${fsq(root, path)}`),
  65. upload: (root: string, dir: string, files: File[]) => {
  66. const form = new FormData()
  67. for (const f of files) form.append('file', f, f.name)
  68. return req<FsEntryDto[]>('POST', `/fs/upload?${fsq(root, dir)}`, form)
  69. },
  70. downloadUrl: (root: string, path: string, inline = false) => `/admin/v1/fs/download?${fsq(root, path)}${inline ? '&inline=1' : ''}`,
  71. zipUrl: (root: string, path: string) => `/admin/v1/fs/zip?${fsq(root, path)}`,
  72. },
  73. customSkills: {
  74. list: () => req<CustomSkillDto[]>('GET', '/custom-skills'),
  75. create: (name: string, description: string) => req<CustomSkillDetailDto>('POST', '/custom-skills', { name, description }),
  76. get: (id: string) => req<CustomSkillDetailDto>('GET', `/custom-skills/${id}`),
  77. setFlags: (id: string, flags: { enabled?: boolean; includeInAll?: boolean }) => req<CustomSkillDetailDto>('PATCH', `/custom-skills/${id}`, flags),
  78. remove: (id: string) => req<void>('DELETE', `/custom-skills/${id}`),
  79. fileUrl: (id: string, path: string, version?: number) => `/admin/v1/custom-skills/${id}/file?path=${encodeURIComponent(path)}${version === undefined ? '' : `&version=${version}`}`,
  80. readText: async (id: string, path: string, version?: number) => {
  81. const r = await fetch(`/admin/v1/custom-skills/${id}/file?path=${encodeURIComponent(path)}${version === undefined ? '' : `&version=${version}`}`, { credentials: 'same-origin' })
  82. if (!r.ok) throw new Error((await r.json().catch(() => null))?.error?.message ?? `HTTP ${r.status}`)
  83. return r.text()
  84. },
  85. writeText: (id: string, path: string, content: string) => req<CustomSkillDetailDto>('PUT', `/custom-skills/${id}/file?path=${encodeURIComponent(path)}`, { content }),
  86. upload: (id: string, path: string, file: File) => {
  87. const form = new FormData()
  88. form.append('file', file, file.name)
  89. return req<CustomSkillDetailDto>('PUT', `/custom-skills/${id}/file?path=${encodeURIComponent(path)}`, form)
  90. },
  91. deleteFile: (id: string, path: string) => req<CustomSkillDetailDto>('DELETE', `/custom-skills/${id}/file?path=${encodeURIComponent(path)}`),
  92. move: (id: string, from: string, to: string) => req<CustomSkillDetailDto>('POST', `/custom-skills/${id}/move`, { from, to }),
  93. discard: (id: string) => req<CustomSkillDetailDto>('POST', `/custom-skills/${id}/discard`),
  94. save: (id: string, baseVersion: number, publish: boolean) => req<CustomSkillDetailDto>('POST', `/custom-skills/${id}/save`, { baseVersion, publish }),
  95. publish: (id: string, version: number) => req<CustomSkillDetailDto>('POST', `/custom-skills/${id}/publish`, { version }),
  96. restore: (id: string, version: number) => req<CustomSkillDetailDto>('POST', `/custom-skills/${id}/versions/${version}/restore`),
  97. diff: (id: string, from: number | 'working', to: number | 'working') => req<FileDiffDto[]>('GET', `/custom-skills/${id}/diff?from=${from}&to=${to}`),
  98. exportUrl: (id: string, version?: number) => `/admin/v1/custom-skills/${id}/export${version === undefined ? '' : `?version=${version}`}`,
  99. detach: (id: string) => req<CustomSkillDetailDto>('POST', `/custom-skills/${id}/detach`),
  100. },
  101. skillSources: {
  102. list: () => req<SkillSourceDto[]>('GET', '/skill-sources'),
  103. create: (input: SkillSourceInput) => req<SkillSourceDto>('POST', '/skill-sources', input),
  104. update: (id: string, input: SkillSourceInput) => req<SkillSourceDto>('PUT', `/skill-sources/${id}`, input),
  105. remove: (id: string) => req<void>('DELETE', `/skill-sources/${id}`),
  106. scan: (id: string) => req<SkillScanDto>('POST', `/skill-sources/${id}/scan`),
  107. import: (id: string, skills: string[], digest: string) => req<{ imported: { name: string; version: number; created: boolean }[]; scan: SkillScanDto }>('POST', `/skill-sources/${id}/import`, { skills, digest }),
  108. upload: (file: File) => {
  109. const form = new FormData()
  110. form.append('file', file, file.name)
  111. return req<{ source: SkillSourceDto; scan: SkillScanDto }>('POST', '/skill-sources/upload', form)
  112. },
  113. },
  114. mcp: {
  115. state: () => req<McpStateDto>('GET', '/mcp'),
  116. create: (input: McpServerInput) => req<McpServerDto>('POST', '/mcp/servers', input),
  117. update: (id: string, input: McpServerInput) => req<McpServerDto>('PUT', `/mcp/servers/${id}`, input),
  118. remove: (id: string) => req<void>('DELETE', `/mcp/servers/${id}`),
  119. refresh: (id: string) => req<McpServerDto>('POST', `/mcp/servers/${id}/refresh`),
  120. setToolPolicy: (id: string, tool: string, policy: McpToolPolicy) => req<McpServerDto>('PUT', `/mcp/servers/${id}/tools/${encodeURIComponent(tool)}`, { policy }),
  121. test: (input: McpServerInput, id: string | null) => req<McpTestResultDto>('POST', '/mcp/test', { input, id }),
  122. },
  123. communitySkills: {
  124. list: () => req<CommunitySkillDto[]>('GET', '/community-skills'),
  125. search: (q: string) => req<CommunitySearchResultDto[]>('GET', `/community-skills/search?q=${encodeURIComponent(q)}`),
  126. preview: (source: string, skillId: string) => req<CommunitySkillPreviewDto>('POST', '/community-skills/preview', { source, skillId }),
  127. install: (source: string, skillId: string, sha: string) => req<CommunitySkillDto[]>('POST', '/community-skills/install', { source, skillId, sha }),
  128. uninstall: (name: string) => req<CommunitySkillDto[]>('DELETE', `/community-skills/${encodeURIComponent(name)}`),
  129. },
  130. plugins: (refresh = false) => req<PluginStateDto>('GET', `/plugins${refresh ? '?refresh=1' : ''}`),
  131. pluginAction: (a: PluginAction) => req<{ message: string; state: PluginStateDto }>('POST', '/plugins/actions', a),
  132. profiles: () => req<ProfileDto[]>('GET', '/profiles'),
  133. createProfile: (p: ProfileInput) => req<ProfileDto>('POST', '/profiles', p),
  134. updateProfile: (id: string, p: ProfileInput) => req<ProfileDto>('PUT', `/profiles/${id}`, p),
  135. deleteProfile: (id: string) => req<void>('DELETE', `/profiles/${id}`),
  136. profileVersions: (id: string) => req<ProfileVersionDto[]>('GET', `/profiles/${id}/versions`),
  137. restoreProfile: (id: string, version: number) => req<ProfileDto>('POST', `/profiles/${id}/versions/${version}/restore`),
  138. previewAccess: (fileAccess: FileAccessPolicy, knowledgeRootIds: string[]) => req<ResolvedAccessDto>('POST', '/access/preview', { fileAccess, knowledgeRootIds }),
  139. knowledge: () => req<KnowledgeRootDto[]>('GET', '/knowledge'),
  140. createKnowledge: (k: KnowledgeInput) => req<KnowledgeRootDto>('POST', '/knowledge', k),
  141. updateKnowledge: (id: string, k: KnowledgeInput) => req<KnowledgeRootDto>('PUT', `/knowledge/${id}`, k),
  142. deleteKnowledge: (id: string) => req<void>('DELETE', `/knowledge/${id}`),
  143. bots: () => req<BotDto[]>('GET', '/bots'),
  144. createBot: (input: BotInput) => req<BotDto>('POST', '/bots', input),
  145. updateBot: (id: string, input: BotInput) => req<BotDto>('PUT', `/bots/${id}`, input),
  146. deleteBot: (id: string) => req<void>('DELETE', `/bots/${id}`),
  147. botKeys: () => req<BotKeyDto[]>('GET', '/bot-keys'),
  148. createBotKey: (name: string, allowedBotIds: string[]) => req<{ key: string; botKey: BotKeyDto }>('POST', '/bot-keys', { name, allowedBotIds }),
  149. setBotKeyEnabled: (id: string, enabled: boolean) => req<{ ok: true }>('PATCH', `/bot-keys/${id}`, { enabled }),
  150. setBotKeyBots: (id: string, allowedBotIds: string[]) => req<{ ok: true }>('PUT', `/bot-keys/${id}/bots`, { allowedBotIds }),
  151. deleteBotKey: (id: string) => req<void>('DELETE', `/bot-keys/${id}`),
  152. botKeyDebugToken: (id: string) => req<BrowserTokenDto>('POST', `/bot-keys/${id}/debug-token`),
  153. apiKeys: () => req<ApiKeyDto[]>('GET', '/api-keys'),
  154. createApiKey: (name: string, access: ApiKeyProfiles) => req<{ key: string; apiKey: ApiKeyDto }>('POST', '/api-keys', { name, ...access }),
  155. setApiKeyProfiles: (id: string, access: ApiKeyProfiles) => req<{ ok: true }>('PUT', `/api-keys/${id}/profiles`, access),
  156. setApiKeyEnabled: (id: string, enabled: boolean) => req<{ ok: true }>('PATCH', `/api-keys/${id}`, { enabled }),
  157. deleteApiKey: (id: string) => req<void>('DELETE', `/api-keys/${id}`),
  158. audit: (q: { kind?: string; sessionId?: string; limit?: number } = {}) => {
  159. const s = new URLSearchParams(Object.entries(q).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => [k, String(v)]))
  160. return req<AuditRow[]>('GET', `/audit${s.size ? `?${s}` : ''}`)
  161. },
  162. }
  163. export interface CodexLoginState { loginId: string; verificationUrl: string; userCode: string; status: 'pending' | 'success' | 'failed' | 'cancelled'; error: string | null }
  164. export const errorMessage = (err: unknown) => (err instanceof Error ? err.message : String(err))