integration.md 12 KB

业务平台接入指南

把 AgentServer 的对话能力(Claude / Codex、流式输出、审批与提问、文件上传与产出)接入自己的平台(Vue 3 + TS)。可以直接对照完整的示例项目 examples/platform-demo:它由一个 Vue 前端和一个约 50 行的 Node 后端组成,同时演示了单个助手的对话和机器人工作组(§8)。

0. 工作方式

浏览器(平台前端)
  │  ① POST /api/agent-token(平台自己的接口,校验平台登录态)
  ▼
平台后端 ── ② POST /v1/browser-tokens(携带 Agent Key)──► AgentServer
  │  返回 { token: "bt_…", expiresAt }(默认 1 小时)
  ▼
浏览器 ── ③ 用 bt_ 令牌直接调用 AgentServer 的 /v1(跨域,SSE 流式)──► AgentServer
  • Agent Key 只放在平台后端,不要写进前端代码或前端环境变量。
  • 浏览器令牌的权限与 Agent Key 相同:只能使用该 Key 可用的 Profile,只能看到该 Key 创建的会话。停用或删除 Key 后,令牌立即失效。
  • 一个平台一个 Agent Key,AgentServer 不区分平台内的具体用户。如果需要区分,在平台侧记录会话 ID 与用户的对应关系。

1. 在 AgentServer 后台准备

  1. API Keys › Agent Keys › 签发 Agent Key:勾选这个平台可以使用的 Profile,并设好默认 Profile。Key 只显示一次,存到平台后端的密钥配置里。
  2. 设置 › 安全 › CORS 来源:加入平台前端的地址,例如 https://tools.example.com(只写协议、域名和端口,不带路径)。
  3. Agent Keys 页面底部的"业务平台接入"卡片里有当前 SDK 版本的安装命令,可以直接复制。

2. 平台后端:签发浏览器令牌

任何语言都可以实现,只需要转发一次请求。Node 示例:

// POST /api/agent-token —— 先校验平台自己的登录态
app.post('/api/agent-token', requireLogin, async (req, res) => {
  const r = await fetch(`${process.env.AGENT_SERVER_URL}/v1/browser-tokens`, {
    method: 'POST',
    headers: { authorization: `Bearer ${process.env.AGENT_API_KEY}`, 'content-type': 'application/json' },
    body: JSON.stringify({ ttlSeconds: 3600 }),   // 60 ~ 43200,默认 3600
  })
  res.status(r.status).json(await r.json())        // { token, expiresAt }
})

如果后端也用 Node,可以改用 SDK:new AgentClient({ baseUrl, apiKey }).issueBrowserToken(3600)。

3. 前端:安装 SDK

SDK 由 AgentServer 自己提供,三个包需要一起安装:

pnpm add https://agent.zenpower.cc/sdk/agent-server-shared-0.3.1.tgz \
         https://agent.zenpower.cc/sdk/agent-server-client-0.3.1.tgz \
         https://agent.zenpower.cc/sdk/agent-server-chat-ui-0.3.1.tgz
包 内容
@agent-server/shared API 与事件的类型、常量
@agent-server/client AgentClient:会话、消息、SSE 订阅(断线续传)、交互回复、上传下载;BotClient:机器人工作组(§8)
@agent-server/chat-ui 对话组件 <AgentChat> 与状态 useChatSession;工作组组件 <TeamChat> 与状态 useTeamSession(依赖 vue@^3.5)

最新版本号见 https://agent.zenpower.cc/sdk/index.json。升级时修改上面 URL 中的版本号即可,旧版本会一直保留。

4. 前端:使用对话组件

// main.ts:只引入设计变量和组件自身的样式,不会改动平台页面的其他样式
import '@agent-server/chat-ui/style.css'
<script setup lang="ts">
import { onBeforeUnmount, shallowRef } from 'vue'
import { AgentClient, browserTokenProvider } from '@agent-server/client'
import { AgentChat, useChatSession, type ChatSession } from '@agent-server/chat-ui'

const client = new AgentClient({
  baseUrl: 'https://agent.zenpower.cc',
  // 缓存令牌、过期前自动换新;令牌失效(401)时会刷新并重试一次
  getToken: browserTokenProvider(async () => {
    const r = await fetch('/api/agent-token', { method: 'POST' })
    if (!r.ok) throw new Error('获取令牌失败')
    return r.json()
  }),
})

const chat = shallowRef<ChatSession | null>(null)
async function newChat() {
  chat.value?.dispose()
  const s = await client.sessions.create()          // 可传 { profileId, model, title }
  chat.value = useChatSession(client, s.id)          // 拉取历史事件 + 订阅实时事件
}
onBeforeUnmount(() => chat.value?.dispose())
</script>

<template>
  <div style="height: 100vh">
    <AgentChat v-if="chat" :client="client" :session="chat" placeholder="问点什么…" />
  </div>
</template>

<AgentChat> 会占满父元素的高度,请给它一个确定高度的容器。

属性 / 插槽 / 事件 说明
client、session 必填:AgentClient 实例和 useChatSession() 的返回值
readonly 只读回放:不显示输入框,不能回复审批
placeholder 输入框提示文字
#toolbar 插槽 输入框底部工具栏,可放模型选择等
#empty 插槽 没有消息时显示的内容
@open-file 点击文件卡片时触发({ fileId, path, mime, size }),用于自己的预览。不监听时直接下载文件

组件已经处理了:

  • 流式文本与思考过程
  • 工具调用卡片
  • 审批与提问卡片
  • 上传附件(点击、拖拽、粘贴截图)
  • Agent 产出文件的下载
  • 中断运行
  • 断线自动重连

5. 常用接口

await client.profiles()                                   // 本平台可用的 Profile
await client.sessions.list({ limit: 30 })                 // 本平台的会话(分页:nextCursor)
await client.sessions.update(id, { title: '…', archived: true })
await client.sessions.remove(id)                          // 删除会话及其工作区
await client.sessions.files(id)                           // 会话中的全部文件
await client.usage(30)                                    // 本平台最近 30 天的用量

不使用 <AgentChat>、自己渲染界面时:

  • 用 client.subscribe(sessionId, { onEvent }) 订阅事件
  • 用 client.sessions.send(id, { text, attachments }) 发送消息
  • 用 client.interactions.reply(itxId, …) 回复审批和提问

事件类型见 design/04-api.md。useChatSession 的 state 也可以单独使用,它是已经整理好的时间线。

6. 外观

  • 亮色/暗色默认跟随系统;在 <html> 上设置 data-theme="light" 或 "dark" 可以强制指定。
  • 配色来自 --app-* CSS 变量,平台可以在自己的样式里覆盖,例如 :root { --app-button-background: #2563eb; }。完整列表见 @agent-server/ui-tokens 的 variables.css。
  • 组件的原子类已经编译并隔离在组件内部,平台不需要在 UnoCSS 中扫描 SDK,也不需要引入 AgentServer 的 UnoCSS 预设。

7. 常见问题

现象 原因与处理
浏览器控制台报 CORS 错误 平台地址没有加入"设置 › 安全 › CORS 来源",或协议、端口写得不一致
所有请求都返回 401 invalid_token 令牌过期后刷新失败:检查平台的 /api/agent-token 接口,以及 Agent Key 是否已被停用
403 profile_not_allowed 创建会话时指定的 Profile 不在该 Key 的可用列表中
流式输出一段一段地出现,或很久才出现 平台与 AgentServer 之间有反向代理在缓冲响应:对 /v1/sessions/*/stream 关闭缓冲(nginx:proxy_buffering off)
安装时报找不到 @agent-server/shared 三个包要用 URL 一起安装(它们是对等依赖,不在 npm 仓库中)
Bot API 返回 401 Agent Key 和 Bot Key 互不通用:/bot/v1 要用 Bot Key 换的令牌(/bot/v1/browser-tokens)

8. 机器人工作组(Bot API)

机器人工作组让多个机器人在一个"群"里协作:成员(平台的用户)发消息,组长理解需求、开任务并把子任务分派给组员,组员完成后组长汇总交付。设计见 design/13-bot-teams.md,完整接口见后台"API 协议 › Bot API"。

接入方式与单个助手相同,只是换成 Bot Key 和 /bot/v1:

  1. 后台准备:在"机器人"页面配置机器人(组长 / 组员),在 API Keys › Bot Keys 签发 Bot Key 并勾选这个平台可以使用的机器人。CORS 来源与 §1 相同。
  2. 平台后端:用 Bot Key 换浏览器令牌,接口是 POST /bot/v1/browser-tokens,参数和返回值与 §2 相同。
  3. 前端:

    <script setup lang="ts">
    import { onBeforeUnmount, shallowRef } from 'vue'
    import { BotClient, browserTokenProvider } from '@agent-server/client'
    import { TeamChat, useTeamSession, type TeamSession } from '@agent-server/chat-ui'
    
    const bots = new BotClient({
    baseUrl: 'https://agent.zenpower.cc',
    getToken: browserTokenProvider(async () => (await fetch('/api/bot-token', { method: 'POST' })).json()),
    })
    
    const team = shallowRef<TeamSession | null>(null)
    async function newTeam(user: { id: string; name: string }) {
    const cards = await bots.bots()                         // 可用的机器人及其能力卡片
    const lead = cards.find(b => b.kind === 'lead')
    const t = await bots.teams.create({
    title: '周报',
    human: user,                                          // 成员:平台里的这个用户
    leadBotId: lead?.id,                                  // 有组长时,组员可以由组长申请加入(需成员批准)
    workerBotIds: cards.filter(b => b.kind === 'worker').map(b => b.id),
    })
    team.value?.dispose()
    team.value = useTeamSession(bots, t.id)                 // 拉取历史 + 订阅工作组事件
    }
    onBeforeUnmount(() => team.value?.dispose())
    </script>
    
    <template>
    <div style="height: 100vh">
    <TeamChat v-if="team" :client="bots" :session="team" placeholder="@ 某个机器人,或直接交给组长…" />
    </div>
    </template>
    

<TeamChat> 已经处理了:

  • 消息时间线
  • 输入框中的 @ 选择
  • 组长的任务与子任务进度
  • 超出限额时的"继续 / 取消"
  • 审批卡片(含组长申请加入机器人)
  • 文件卡片与上传
  • 正在工作的机器人和"停止"
  • 断线续传
属性 / 事件 说明
client、session 必填:BotClient 实例和 useTeamSession() 的返回值
readonly 只读:不显示输入框,不能处理审批和限额
placeholder 输入框提示文字
@open-file 同 <AgentChat>

常用接口:

await bots.teams.list({ status: 'active' })               // 本平台的工作组
await bots.teams.send(id, { text: '…', mentions: [botId] }) // @ 组员时只交给它,不 @ 时交给组长
await bots.teams.pause(id) / resume(id) / close(id)
await bots.teams.addBot(id, botId) / removeBot(id, botId)
await bots.teams.jobs(id)                                 // 任务与子任务
await bots.teams.usage(id)                                // 按机器人、任务、子任务统计的用量
await bots.teams.resetSession(id, botId)                  // 让机器人开始新的会话(组长在当前任务结束后)

说明:

  • 一个工作组只有一个成员(human),平台用户之间不共享工作组。
  • 限额(子任务数、组长轮数、时长、成本)以 AgentServer 的系统设置为上限,创建工作组时只能调低。
  • 机器人的会话上下文过长或闲置太久时会自动开始新的会话(组长在两项任务之间换),之前的工作从共享目录 .team/jobs/ 下的任务纪要了解。
  • 替身组员、逐步执行是后台 Bot Playground 的调试功能,平台不能设置。