把 AgentServer 的对话能力(Claude / Codex、流式输出、审批与提问、文件上传与产出)接入自己的平台(Vue 3 + TS)。可以直接对照完整的示例项目 examples/platform-demo:它由一个 Vue 前端和一个约 50 行的 Node 后端组成,同时演示了单个助手的对话和机器人工作组(§8)。
浏览器(平台前端)
│ ① POST /api/agent-token(平台自己的接口,校验平台登录态)
▼
平台后端 ── ② POST /v1/browser-tokens(携带 Agent Key)──► AgentServer
│ 返回 { token: "bt_…", expiresAt }(默认 1 小时)
▼
浏览器 ── ③ 用 bt_ 令牌直接调用 AgentServer 的 /v1(跨域,SSE 流式)──► AgentServer
https://tools.example.com(只写协议、域名和端口,不带路径)。任何语言都可以实现,只需要转发一次请求。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)。
SDK 由 AgentServer 自己提供,三个包需要一起安装:
pnpm add https://agent.zenpower.cc/sdk/agent-server-shared-0.6.0.tgz \
https://agent.zenpower.cc/sdk/agent-server-client-0.6.0.tgz \
https://agent.zenpower.cc/sdk/agent-server-chat-ui-0.6.0.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 中的版本号即可,旧版本会一直保留。
// 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 }),用于自己的预览。不监听时直接下载文件 |
组件已经处理了:
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 也可以单独使用,它是已经整理好的时间线。
用户在对话中说"每个工作日早上 9 点帮我出日报",AI 会创建定时任务:对话中出现确认卡片,用户确认后生效,之后到点由 AgentServer 自动往同一个会话(或工作组)发出指令并执行,结果留在时间线中。设计见 design/14-schedules.md。
<AgentChat>、<TeamChat> 已经处理了确认卡片,并在顶部显示定时任务栏(可以暂停、恢复、删除),平台不需要额外开发。自己渲染界面时:
await client.sessions.schedules(sessionId) // 会话的定时任务
await client.schedules.setStatus(scheduleId, 'paused') // 暂停或恢复('active')
await client.schedules.remove(scheduleId)
await client.schedules.runs(scheduleId) // 执行记录
await bots.teams.schedules(teamId) // 工作组的定时任务(BotClient)
定时执行时没有人在场:需要审批的操作会在等待超时后被拒绝。希望定时任务稳定执行的场景,使用"自动执行"或"只读"的 Profile。
每次执行有时长和成本上限(后台"设置 › 定时任务"),超出成本上限时定时任务自动暂停。
接入的外部工具(MCP 服务)中执行时间很长的调用(导出、跑批等)会转到后台:AI 先告诉用户"已提交",结束这一轮;服务端继续等待,完成后把结果作为一条新消息发到会话(或工作组中发起调用的机器人),AI 收到后继续处理。设计见 design/15-background-calls.md。
<AgentChat>、<TeamChat> 顶部会显示"后台执行中",可以取消;结果到达后自动出现在时间线中。client.sessions.backgroundCalls(id)、client.backgroundCalls.cancel(callId);工作组用 bots.teams.backgroundCalls(teamId)、bots.teams.cancelBackgroundCall(teamId, callId)。会话的 Profile 勾选的 MCP 服务(设计见 design/16-mcp-support.md)可以:
interaction.required,kind 为 form)。mode 为 form 时按 fields 填写;为 url 时请用户自己打开网址(授权、付款等)后点"我已完成",不要自动打开。长任务中的询问不属于某一轮(事件的 runId 为 null),会话可能是空闲状态。<AgentChat> 的输入框会多出"资料"、"快捷指令"两个按钮(只在有相应服务时显示)。附加的资料作为附件发出;快捷指令填好参数后放进输入框,用户可以修改后再发送。自己渲染界面时:
// 表单卡片的回复:提交(带内容;url 模式表示已完成)、拒绝、暂不处理
await client.interactions.reply(itx.interactionId, { action: 'accept', content: { name: '张三', count: 2 } })
await client.interactions.reply(itx.interactionId, { action: 'decline' })
const { mcp } = await client.sessions.get(sessionId) // { resources: boolean, prompts: boolean }
const list = await client.sessions.mcpResources(sessionId) // 资源目录(不含内容)
const files = await client.sessions.attachMcpResource(sessionId, 'docs', 'docs://manual/guide.md')
await client.sessions.send(sessionId, { text: '看看这份手册', attachments: files.map(f => f.id) })
const { prompts } = await client.sessions.mcpPrompts(sessionId)
const { text, attachments } = await client.sessions.getMcpPrompt(sessionId, 'docs', 'weekly', { week: '44' })
AI 用脚本批量调用外部工具(run_script,见 design/17-run-script.md)时,事件流中有 script.progress(只推送、不保存,每秒最多一次):已调用次数、失败次数、脚本报告的进度(done / total / message)、用时、状态。<AgentChat> 会在时间线底部显示;自己渲染界面时可以按 scriptId 更新一行进度。
<html> 上设置 data-theme="light" 或 "dark" 可以强制指定。--app-* CSS 变量,平台可以在自己的样式里覆盖,例如 :root { --app-button-background: #2563eb; }。完整列表见 @agent-server/ui-tokens 的 variables.css。| 现象 | 原因与处理 |
|---|---|
| 浏览器控制台报 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) |
机器人工作组让多个机器人在一个"群"里协作:成员(平台的用户)发消息,组长理解需求、开任务并把子任务分派给组员,组员完成后组长汇总交付。设计见 design/13-bot-teams.md,完整接口见后台"API 协议 › Bot API"。
接入方式与单个助手相同,只是换成 Bot Key 和 /bot/v1:
POST /bot/v1/browser-tokens,参数和返回值与 §2 相同。前端:
<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),平台用户之间不共享工作组。.team/jobs/ 下的任务纪要了解。