AgentConnect
连接与自动化

Agent 对话 API

通过一个服务端小代理,把 Agent 接到你自己的聊天界面上——文档站的 Ask AI 面板,或者客服窗口。

Agent 对话 API 可以把 Agent 接到任何基于 AI SDK useChat 的聊天界面上(UI message stream 协议 v1)。每一轮对话是一次 HTTP POST,回复以 server-sent events 流返回。API 密钥放在你服务器上运行的一个小代理里;浏览器既拿不到密钥,也拿不到 token。

常见用法是文档站上的 “Ask AI” 面板或客服窗口。本站的 Ask AI 面板就是这样实现的。

一共三步:

  1. 创建一个仅限 Agent 对话的 API 密钥。
  2. 每个会话开始时,代理用密钥换取一个短期 token。
  3. 每一轮对话,代理带着这个 token 转发 useChat 的请求,再把回复流式传回浏览器。

创建密钥

个人资料 → API 密钥 → 新建密钥:

  • 权限——Agent 对话。
  • Agent——指定 Agent,然后选中这个 Agent。

这种密钥只能调用一个接口,也就是下面的 token 签发接口,而且只对选中的 Agent 有效;其他任何请求都返回 403。和所有 API 密钥一样,它只显示一次,默认 90 天后过期。

签发 token

每个会话用密钥签发一个 token:

curl -X POST "https://api.agentconnect.md/v1/orgs/$ORG_ID/agents/$AGENT_ID/webchat/token" \
  -H "Authorization: Bearer $AGENTCONNECT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

请求体 {} 开启一个新会话;{ "conversationId": "<uuid>" } 继续已有会话。响应为:

{ "token": "…", "relayUrl": "…", "conversationId": "…", "expiresAt": "…" }

token 有效期 5 分钟(expiresAt)。在此之前,这个会话的每一轮都复用它,过期后重新签发。继续一个不存在的会话返回 404;409 表示 Agent 已迁移,这个会话需要重新开启。两种情况都开启新会话即可。

在 AgentConnect OSS 上,同一接口位于 <control-plane-url>/api/v1/orgs/…。

发送一轮对话

发送 POST {relayUrl}/ai-sdk/chat/{conversationId},带上 Authorization: Bearer <token>,请求体就是 useChat 的原样请求({ id, messages, trigger, … }):

curl -N -X POST "$RELAY_URL/ai-sdk/chat/$CONVERSATION_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"chat-1","trigger":"submit-message","messages":[{"id":"m1","role":"user","parts":[{"type":"text","text":"How do I install the daemon?"}]}]}'

响应为 text/event-stream,带有 x-vercel-ai-ui-message-stream: v1 头。各个 part 遵循 AI SDK 协议,流以 [DONE] 结束。

  • relay 只读取最后一条用户消息的文本 part。Agent 的会话本身已保存历史,所以更早的消息、文件 part 和 data-* part 都会被忽略。
  • 一轮对话可能持续几分钟,期间每 15 秒会收到一条 keepalive 注释。
  • 请求体最大 4 MiB,一轮的文本最大 128 KiB,超出返回 413。
  • 一个会话同一时间只运行一轮。上一轮还在运行时再次 POST,返回 409。

Agent 所在的 Daemon 拒绝这一轮时,状态码会在任何流之前返回:

状态码含义
409这个会话还有一轮在运行。
503Agent 离线、已暂停或正在排空。
404会话已不存在。
502其他拒绝。

流中包含什么

Part内容
text-*Agent 的回复。
reasoning-*Agent 的思考过程。
data-tool工具调用:只有标题和状态。
data-planAgent 的计划。
data-notice状态提示,例如 “Starting sandbox…”。
message-metadata会话标题。

只渲染文本的客户端只会显示回复。useChat 的 stop() 只中止 fetch:这一轮仍会跑完,输出写入控制台里这个会话的记录。中断的流目前还不能续传。

运行代理

代理位于 useChat 和 AgentConnect 之间,需要做到:

  • 把每个访客、每个打开的标签页对应到一个会话 id,例如存在 HttpOnly cookie 里。不要信任客户端发来的 chat id。
  • 历史中只有一条用户消息的请求,视为新会话。useChat 在客户端清空历史时不会通知服务端。
  • 原样转发响应体,以及 content-type 和 x-vercel-ai-ui-message-stream 两个头。
  • 按访客限流。AgentConnect 只按会话限制并发,不按访客限流。

一个最小的 Next.js 路由处理器 app/api/chat/route.ts,每个浏览器一个会话:

const API = 'https://api.agentconnect.md/v1'
const { AGENTCONNECT_API_KEY, AGENTCONNECT_ORG_ID, AGENTCONNECT_AGENT_ID } = process.env
type Minted = { token: string; relayUrl: string; conversationId: string; expiresAt: string }
const tokens = new Map<string, Minted>()

async function mint(conversationId?: string): Promise<Minted> {
  const cached = conversationId ? tokens.get(conversationId) : undefined
  if (cached && Date.parse(cached.expiresAt) - 30_000 > Date.now()) return cached
  const res = await fetch(`${API}/orgs/${AGENTCONNECT_ORG_ID}/agents/${AGENTCONNECT_AGENT_ID}/webchat/token`, {
    method: 'POST',
    headers: { authorization: `Bearer ${AGENTCONNECT_API_KEY}`, 'content-type': 'application/json' },
    body: JSON.stringify(conversationId ? { conversationId } : {})
  })
  if (!res.ok) throw Object.assign(new Error(`token mint failed: ${res.status}`), { status: res.status })
  const minted = (await res.json()) as Minted
  tokens.set(minted.conversationId, minted)
  return minted
}

export async function POST(req: Request) {
  const body = await req.json()
  const userTurns = body.messages.filter((m: { role: string }) => m.role === 'user').length
  const bound = req.headers.get('cookie')?.match(/(?:^|;\s*)chat_conversation=([0-9a-f-]{36})/)?.[1]
  // A lone first question starts over; an unknown (404) or moved (409) conversation does too. Any other failure surfaces.
  const reopen = (err: { status?: number }) => (err.status === 404 || err.status === 409 ? mint() : Promise.reject(err))
  const { token, relayUrl, conversationId } =
    userTurns > 1 && bound ? await mint(bound).catch(reopen) : await mint()

  const upstream = await fetch(`${relayUrl}/ai-sdk/chat/${conversationId}`, {
    method: 'POST',
    headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
    body: JSON.stringify(body),
    signal: req.signal
  })
  const headers = new Headers({ 'cache-control': 'no-store' })
  for (const name of ['content-type', 'x-vercel-ai-ui-message-stream']) {
    const value = upstream.headers.get(name)
    if (value) headers.set(name, value)
  }
  headers.append('set-cookie', `chat_conversation=${conversationId}; Path=/; HttpOnly; Secure; SameSite=Lax`)
  return new Response(upstream.body, { status: upstream.status, headers })
}

客户端把 useChat 的 transport 指向这个路由(new DefaultChatTransport({ api: '/api/chat' }))。

本站自己的代理在 agentconnect-md/docs 仓库中,另外做了按标签页区分的 cookie、有上限的 token 缓存和错误映射:见 lib/ask-ai.ts 和 app/api/chat/route.ts。面板用的是 Fumadocs 的 AI 搜索组件。

会话与安全

这样开启的会话是密钥所有者的普通 webchat 会话。它在控制台中对该用户私密,来源显示为这个密钥。用量和其他对话一样计入这个 Agent;见账单与用量。

Agent 每一轮收到的都是不可信输入。请据此配置它:

  • 不给它任何密钥,也不给仓库写权限。
  • 让它在能隔离不可信输入的沙箱中运行:microsandbox 或 Kubernetes。
  • 把它放在一个成员可以阅读这些会话记录的组织里。

故障排查

  • 403 “limited to agent:chat”——Agent 对话密钥被发到了 token 签发接口之外的接口。检查 URL,以及代理发送的是不是你想用的密钥。
  • 签发时 404 “agent not found”——密钥选中的 Agent 不包含这个 Agent。
  • 发送一轮时 409——这个会话还有一轮在运行。等它结束,或开启新会话。
  • 部署后立即出现 503——Agent 所在的 Daemon 正在重连。重试即可。

How is this guide?

本页目录

How is this guide?