bot.ts 5.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. // Bot API 客户端(/bot/v1,docs/design/13-bot-teams.md §9)。使用 Bot Key 或 Bot 浏览器令牌,与 AgentClient 的 Key 互不通用
  2. import type {
  3. BotCard,
  4. BrowserTokenDto,
  5. CreateTeamBody,
  6. FileDto,
  7. InteractionResponse,
  8. PatchTeamBody,
  9. SendTeamMessageBody,
  10. StubTaskBody,
  11. TeamDetailDto,
  12. TeamDto,
  13. TeamEvent,
  14. TeamJobsDto,
  15. TeamMessage,
  16. TeamStatus,
  17. TeamUsageDto,
  18. } from '@agent-server/shared'
  19. import { AgentApiError, HttpClient, query, type StreamHandlers } from './http'
  20. export class BotClient extends HttpClient {
  21. /** 平台后端调用(需要 Bot Key):签发交给浏览器使用的短期令牌 */
  22. issueBrowserToken = (ttlSeconds?: number) => this.request<BrowserTokenDto>('POST', '/bot/v1/browser-tokens', ttlSeconds === undefined ? {} : { ttlSeconds })
  23. /** 这个 Key 可以使用的机器人及其能力卡片 */
  24. bots = () => this.request<BotCard[]>('GET', '/bot/v1/bots')
  25. teams = {
  26. create: (body: CreateTeamBody) => this.request<TeamDetailDto>('POST', '/bot/v1/teams', body),
  27. list: (q: { scope?: 'all'; status?: TeamStatus; limit?: number } = {}) => this.request<TeamDto[]>('GET', `/bot/v1/teams${query(q)}`),
  28. get: (id: string) => this.request<TeamDetailDto>('GET', `/bot/v1/teams/${id}`),
  29. /** 标题、限额;debug 只有后台可以设置 */
  30. update: (id: string, body: PatchTeamBody) => this.request<TeamDetailDto>('PATCH', `/bot/v1/teams/${id}`, body),
  31. remove: (id: string) => this.request<void>('DELETE', `/bot/v1/teams/${id}`),
  32. pause: (id: string) => this.request<TeamDetailDto>('POST', `/bot/v1/teams/${id}/pause`),
  33. resume: (id: string) => this.request<TeamDetailDto>('POST', `/bot/v1/teams/${id}/resume`),
  34. close: (id: string) => this.request<TeamDetailDto>('POST', `/bot/v1/teams/${id}/close`),
  35. reopen: (id: string) => this.request<TeamDetailDto>('POST', `/bot/v1/teams/${id}/reopen`),
  36. addBot: (id: string, botId: string) => this.request<TeamDetailDto>('POST', `/bot/v1/teams/${id}/bots`, { botId }),
  37. removeBot: (id: string, botId: string) => this.request<TeamDetailDto>('DELETE', `/bot/v1/teams/${id}/bots/${botId}`),
  38. interruptBot: (id: string, botId: string) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/bots/${botId}/interrupt`),
  39. /** 让机器人开始新的会话(组长在当前任务结束后) */
  40. resetSession: (id: string, botId: string) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/bots/${botId}/reset-session`, {}),
  41. send: (id: string, body: SendTeamMessageBody) => this.request<TeamMessage>('POST', `/bot/v1/teams/${id}/messages`, body),
  42. messages: (id: string, after = 0) => this.request<{ items: TeamEvent[]; lastSeq: number }>('GET', `/bot/v1/teams/${id}/messages?after=${after}`),
  43. events: (id: string, after = 0) => this.request<{ items: TeamEvent[]; lastSeq: number }>('GET', `/bot/v1/teams/${id}/events?after=${after}`),
  44. files: (id: string) => this.request<FileDto[]>('GET', `/bot/v1/teams/${id}/files`),
  45. upload: (id: string, files: Blob[] | File[]) => {
  46. const form = new FormData()
  47. for (const f of files) form.append('file', f, (f as File).name ?? 'upload')
  48. return this.request<FileDto[]>('POST', `/bot/v1/teams/${id}/files`, form)
  49. },
  50. reply: (id: string, interactionId: string, body: InteractionResponse) =>
  51. this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/interactions/${interactionId}`, body),
  52. jobs: (id: string) => this.request<TeamJobsDto>('GET', `/bot/v1/teams/${id}/jobs`),
  53. cancelJob: (id: string, jobId: string) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/jobs/${jobId}/cancel`),
  54. continueJob: (id: string, jobId: string) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/jobs/${jobId}/continue`),
  55. usage: (id: string) => this.request<TeamUsageDto>('GET', `/bot/v1/teams/${id}/usage`),
  56. /** 仅后台:替身组员的子任务结果 */
  57. stubResult: (id: string, taskId: string, body: StubTaskBody) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/tasks/${taskId}/stub`, body),
  58. /** 仅后台:逐步执行时放行一步 */
  59. continueStep: (id: string, stepId: string) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/steps/${stepId}/continue`),
  60. }
  61. /** 文件下载地址(同源 Cookie 鉴权时可直接用于 <img>/<a>) */
  62. fileUrl(fileId: string, inline = false) {
  63. return `${this.opts.baseUrl}/bot/v1/files/${fileId}${inline ? '?inline=1' : ''}`
  64. }
  65. /** 带鉴权头下载文件为 Blob */
  66. async fileBlob(fileId: string): Promise<Blob> {
  67. const res = await this.send(`/bot/v1/files/${fileId}?inline=1`, {})
  68. if (!res.ok) throw new AgentApiError(res.status, 'http_error', `HTTP ${res.status}`)
  69. return res.blob()
  70. }
  71. /** 订阅工作组事件流;断线自动重连并从最后收到的 seq 续传。返回取消函数 */
  72. subscribe(teamId: string, handlers: StreamHandlers<TeamEvent>, opts: { after?: number } = {}): () => void {
  73. return this.stream(`/bot/v1/teams/${teamId}/stream`, handlers, opts.after ?? 0)
  74. }
  75. }