通过 WebSocket 接入智能体
WebSocket 接入适合把自有消息系统连接到智能体。完成接入时,需要先选择消息进入哪个智能体,再为客户端创建 Token、建立连接并处理回复、中断和心跳。连接采用 Bridge 协议 infini.bridge.v1;帧字段、鉴权方式和错误码见 WebSocket API 参考。
创建 WebSocket 渠道账号
按钮会在新标签页打开控制台。若先进入登录页,控制台目前不会在登录后自动返回目标页面;请登录后返回本文,再次选择按钮。
打开 渠道账号。
也可以从控制台左侧导航进入 渠道账号。
选择 新建账号,在 渠道类型 中选择 WebSocket,并填写 账号名称。
在 绑定模式 中选择 绑定 Agent 实例(默认) 或 绑定 Agent 模板,再通过 绑定对象 选择目标智能体实例或智能体模板。
按需选择 同步创建 Token(可一次创建多个)。为不同客户端填写不同的 Token 名称和有效期;同一个渠道账号内的 WebSocket Token 名称不能重复。
选择 创建。
信息
“同步创建”指随渠道账号一并签发
新建 WebSocket 渠道账号时,该选项默认选中;不需要立即签发 Token 时,请取消选择。每项配置都会生成一个独立的 wst_ Token,所有 Token 均绑定到新建的渠道账号;平台不会把 Token 自动分发给客户端。
创建成功后,所有 Token 明文会在同一弹窗中一次性显示。请分别复制到对应客户端的密钥存储;关闭弹窗后无法再次查看原值。不要把 Token 写入前端代码、日志或文档。
同一租户内,一个智能体实例或智能体模板只能绑定一个尚未删除的 WebSocket 渠道账号。已经停用的账号仍保留这个绑定目标。如果多个客户端需要连接同一个绑定目标,应在同一个渠道账号下分别创建 Token,而不是为同一个绑定目标创建多个渠道账号。
为客户端创建 Token
每个客户端应使用能够单独轮换和停用的 Token。如果创建渠道账号时已经为当前客户端同步创建并保存了 Token,可以直接进入下一节;否则从渠道账号详情创建:
- 打开目标 WebSocket 渠道账号,进入 Token。
- 选择 新建 Token,填写能够识别客户端和环境的 Token 名称,并按需设置 有效期。从账号详情创建时,Token 会自动绑定当前渠道账号。
- 选择 创建 Token,立即把一次性显示的
wst_明文保存到客户端使用的密钥存储。
需要跨账号查找或创建 Token 时,也可以打开 凭证管理,进入 WebSocket Token,再选择目标渠道账号。Token 的拆分、轮换和停用方法见管理 API 调用和 WebSocket Token。
wst_ 绑定到 WebSocket 渠道账号,不直接绑定智能体模板或智能体实例。账号的绑定模式和绑定对象负责路由;因此 Token 创建页只要求选择渠道账号。
连接并完成鉴权
打开目标渠道账号详情的 连接信息,复制目标部署提供的 WebSocket 地址。不要根据控制台地址自行拼接连接主机名。
推荐在 HTTP Upgrade 阶段通过子协议传递 Token:
language-textSec-WebSocket-Protocol: infini.bridge.v1, bearer.<base64url_token>base64url_token是wst_Token 的无填充 Base64URL 编码。确认连接成功,并且服务端只回显
infini.bridge.v1。
只有客户端无法设置自定义 WebSocket 子协议时,才改用帧内鉴权。帧内鉴权建立连接时不请求 Sec-WebSocket-Protocol,连接建立后再发送 auth 帧。两种方式的完整请求和错误处理见 WebSocket API 参考。
运行最小客户端
下面的 Node.js 22 或更高版本示例使用推荐的 Subprotocol 鉴权,发送一条不应触发工具的测试消息,并等待对应回复以 done: true 结束。脚本会在交互式终端中隐藏 Token 输入;如果收到中断,它会停止并要求先检查智能体配置,不会自动批准操作。
将以下内容保存为 websocket-smoke.mjs:
import { randomUUID } from "node:crypto";
function readHidden(prompt) {
return new Promise((resolve, reject) => {
const input = process.stdin;
if (!input.isTTY || typeof input.setRawMode !== "function") {
reject(new Error("请在交互式终端中运行此脚本"));
return;
}
let value = "";
const cleanup = () => {
input.off("data", onData);
input.setRawMode(false);
input.pause();
};
const onData = (chunk) => {
for (const character of chunk) {
if (character === "\u0003") {
cleanup();
process.stderr.write("\n");
reject(new Error("已取消"));
return;
}
if (character === "\r" || character === "\n") {
cleanup();
process.stderr.write("\n");
resolve(value);
return;
}
if (character === "\u007f") {
value = value.slice(0, -1);
continue;
}
value += character;
}
};
process.stderr.write(prompt);
input.setEncoding("utf8");
input.setRawMode(true);
input.resume();
input.on("data", onData);
});
}
const endpoint = process.argv[2];
if (!endpoint?.startsWith("wss://")) {
throw new Error("请把“连接信息”中复制的 wss:// 地址作为第一个参数");
}
if (typeof WebSocket === "undefined") {
throw new Error("需要 Node.js 22 或更高版本");
}
let token = await readHidden("WebSocket Token: ");
if (!token.startsWith("wst_")) {
throw new Error("请输入当前渠道账号的 WebSocket Token");
}
let bearer = Buffer.from(token, "utf8").toString("base64url");
const messageId = randomUUID();
const socket = new WebSocket(endpoint, [
"infini.bridge.v1",
`bearer.${bearer}`,
]);
token = "";
bearer = "";
let finished = false;
let responseText = "";
let timeout;
const finish = (passed, message) => {
if (finished) return;
finished = true;
if (timeout) clearTimeout(timeout);
const output = passed ? console.log : console.error;
output(`\n${passed ? "PASS" : "FAIL"}: ${message}`);
if (!passed) process.exitCode = 1;
if (socket.readyState === WebSocket.OPEN) socket.close(1000, "client_done");
};
const finishWithReply = (message) => {
if (responseText.trim() !== "WS_ROUND_TRIP_OK") {
finish(false, "回复内容不是预期的 WS_ROUND_TRIP_OK");
return;
}
finish(true, message);
};
timeout = setTimeout(
() => finish(false, "120 秒内没有收到本次运行的结束帧"),
120_000,
);
socket.addEventListener("open", () => {
if (socket.protocol !== "infini.bridge.v1") {
finish(false, "服务端没有选择 infini.bridge.v1");
return;
}
socket.send(JSON.stringify({
type: "message",
message: {
meta: {
msg_id: messageId,
user_id: "websocket-smoke-user",
user_name: "WebSocket smoke test",
chat_type: "p2p",
chat_id: "websocket-smoke-chat",
},
content: "不要调用工具,只回复 WS_ROUND_TRIP_OK",
},
}));
});
socket.addEventListener("message", async (event) => {
const raw = typeof event.data === "string" ? event.data : await event.data.text();
let frame;
try {
frame = JSON.parse(raw);
} catch {
finish(false, "服务端返回了非 JSON 文本帧");
return;
}
if (frame.type === "PING") {
socket.send(JSON.stringify({ type: "PONG" }));
return;
}
if (frame.type === "error") {
finish(false, `${frame.error?.code ?? "error"}: ${frame.error?.message ?? ""}`);
return;
}
const reply = frame.message_stream_reply ?? frame.message_reply;
if (!reply || reply.meta?.msg_id !== messageId) return;
if (reply.interrupt) {
finish(false, "收到意外中断;请先检查测试智能体的工具和审批配置");
return;
}
if (reply.tool_call) {
finish(false, "智能体意外调用了工具;请改用不含工具的测试智能体");
return;
}
if (frame.type === "message_reply") {
responseText = reply.content ?? "";
console.log(responseText);
finishWithReply("收到本次消息的完整回复");
return;
}
if (reply.content?.content) {
responseText += reply.content.content;
process.stdout.write(reply.content.content);
}
if (reply.done === true) finishWithReply("收到本次运行的 done: true");
});
socket.addEventListener("error", () => finish(false, "WebSocket 连接出错"));
socket.addEventListener("close", (event) => {
if (!finished) finish(false, `连接提前关闭,Close Code ${event.code}`);
});使用 连接信息 中复制的地址运行脚本,并在隐藏提示中粘贴当前客户端的 wst_ Token:
node websocket-smoke.mjs 'wss://目标部署提供的地址/ws/v1'看到 WS_ROUND_TRIP_OK 和 PASS 后,再把相同的鉴权、消息关联、心跳和结束判断移入业务客户端。不要把 Token 写入脚本、命令行参数或测试输出。
验证消息往返
先使用唯一的 msg_id、稳定的 user_id 和预期不触发工具的固定文本验证基础消息链路:
{
"type": "message",
"message": {
"meta": {
"msg_id": "3eeec74d-a4d4-4517-89ec-ef8aeb676666",
"user_id": "user-001",
"user_name": "Alice",
"chat_type": "p2p",
"chat_id": "chat-001"
},
"content": "不要调用工具,只回复 WS_ROUND_TRIP_OK"
}
}检查结果:
- 收到与请求
msg_id和chat_id对应的message_reply或message_stream_reply。 - 确认这次基础消息测试没有
tool_call、interrupt或error。如果出现意外的tool_call或interrupt,改用不含工具的专用测试智能体重新验证;如果收到error,记录错误码和消息,按照处理错误排查,修复后重新完成基础消息测试。 - 流式文本能够按顺序累积,并以
done: true结束本次运行。 - 同一个渠道账号有多个客户端时,各客户端能够按消息元数据筛选自己的回复。
- 收到
PING时返回PONG;断开连接后能够退避重连并重新鉴权。
实现业务客户端时,可以在渠道账号详情的 消息协议 中快速查看帧示例;完整消息字段、事件类型、结束条件和心跳约定以 WebSocket API 参考为准。
验证中断和用户工具包
智能体可能通过 message_stream_reply.interrupt 等待用户确认、用户输入或用户工具包执行结果。基础消息链路通过后,再使用包含相应能力的专用测试智能体:
- 发送测试消息时,按
msg_id保存包含user_id、chat_type和chat_id的完整原始meta,并在收到中断时另外保存中断帧及其中断 ID。 - 通过中断的
msg_id找到原始消息。如果当前客户端没有对应的待处理消息,不要响应该中断;同一渠道账号上的其他连接可能会处理它。如果中断属于当前客户端,并且中断帧缺少user_id,只从同一条原始消息补回该值;原记录已经丢失,或者两个非空user_id不一致时,停止发送决定并联系平台支持。具体兼容方法见响应中断。 - 使用两条新的测试消息,对无副作用的单项待审批调用分别返回
APPROVE和REJECT。每次运行都应在原消息上下文对应的回复中返回done: true,原 Trace 也应出现与决定一致的后续事件;两次都通过后,再完成审批验收。 - 如果决定已经发出但连接保持正常且没有收到
done: true,把本次结果记为未知。保留原始消息和中断的meta、中断 ID 与发生时间,并在原 Trace、MCP 服务或实际写入的业务系统中核对是否已经执行。只有PING和PONG表示连接仍然存活,不表示审批已经生效。不要重复发送决定或原消息;没有幂等保证时,也不要重试有副作用的操作,并联系平台支持。 - 对用户工具包中断校验工具名称和参数,在业务系统中执行操作,再返回
RESPOND或ERROR,并确认智能体使用结果继续运行且以done: true结束。
响应帧见 WebSocket API 参考,工具执行责任和结果格式见定义用户工具包。
轮换或撤销 Token
为同一渠道账号创建第二个 wst_,并使用不同于旧 Token 的 Token 名称。让客户端使用新 Token 建立并验证新连接,再选择 停用旧 Token。客户端必须重连;现有连接不能在原连接中更换 Token。
停用、删除或重置旧 Token 会阻止下一次鉴权,但不会主动关闭已经鉴权的连接。需要立即切断时,对渠道账号选择 停用,关闭该账号的全部连接。这个操作会同时影响该账号下使用其他 Token 的客户端。随后刷新 WebSocket Token 列表,逐个核对绑定 Token,并单独停用任何仍显示为启用的 Token。恢复步骤参见停用或删除 WebSocket 渠道账号。
选择 编辑并修改渠道账号的 绑定对象 后,应让客户端重连并重新执行消息往返测试,不要依赖保持在线的旧连接自动切换路由。
停用或删除 WebSocket 渠道账号
选择 停用 WebSocket 渠道账号会关闭该账号的全部连接。停用后刷新 WebSocket Token 列表,逐个核对绑定 Token,并单独停用任何仍显示为启用的 Token。
临时停用后恢复服务:
- 打开 凭证管理 的 WebSocket Token,只选择 启用或替换仍需要使用的 Token,并停用其他仍显示为启用的 Token。
- 对渠道账号选择 启用。
- 让客户端重新连接,并完成鉴权、心跳和消息往返测试。
永久删除账号时,先选择 停用并确认连接已经结束,再刷新 Token 列表并单独停用任何仍显示为启用的绑定 Token,最后选择 删除。删除前应盘点客户端保存的 Token 和连接配置;删除平台内的渠道账号不会修改客户端配置。
处理模板绑定
WebSocket 账号绑定智能体模板时,业务系统需要代表渠道用户发送 /init 消息。user_id 是用户绑定和专属实例创建的关键标识,必须稳定且不可被不同终端随意复用。
如果只需要把所有消息转发到一个现有智能体实例,选择实例绑定,避免引入动态实例生命周期。