15 KiB
15 KiB
WS v1 协议完整说明(中文)
本文档描述 /ws 端点的 WebSocket v1 协议,覆盖:
- 客户端输入(JSON 文本消息 + 二进制音频);
- 服务端输出(JSON 事件 + 二进制音频);
- 每个参数的类型、约束、含义与使用方式;
- 握手顺序、状态机、错误语义与实现细节。
实现对照来源:
models/ws_v1.pycore/session.pycore/duplex_pipeline.pyapp/main.py
1. 传输与基础规则
- 连接地址:
ws://<host>/ws - 单连接双通道承载:
- 文本帧:JSON 控制消息(严格校验 schema)
- 二进制帧:原始 PCM 音频
- JSON 校验策略:
- 所有已定义客户端消息都
extra="forbid",即不允许未声明字段; hello.version固定必须是"v1";- 缺失
type或未知type会返回协议错误。
- 所有已定义客户端消息都
2. 状态机与消息顺序
2.1 服务端状态
WAIT_HELLO:等待helloWAIT_START:已通过握手,等待session.startACTIVE:会话运行中,可收发文本/音频STOPPED:会话结束
2.2 正确顺序
- 客户端发送
hello - 服务端返回
hello.ack - 客户端发送
session.start - 服务端返回
session.started - 客户端可持续发送:
- 二进制音频
input.text(可选)response.cancel(可选)tool_call.results(可选)
- 客户端发送
session.stop或直接断开连接
顺序错误会返回 error,code = "protocol.order"。
3. 客户端 -> 服务端消息(输入)
3.1 hello
示例:
{
"type": "hello",
"version": "v1"
}
字段说明:
| 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 |
|---|---|---|---|---|---|
type |
string | 是 | 固定 "hello" |
消息类型 | 握手第一条消息 |
version |
string | 是 | 固定 "v1" |
协议版本 | 版本不匹配会 protocol.version_unsupported 并断开 |
3.2 session.start
示例:
{
"type": "session.start",
"audio": {
"encoding": "pcm_s16le",
"sample_rate_hz": 16000,
"channels": 1
},
"metadata": {
"appId": "assistant_123",
"channel": "web",
"configVersionId": "cfg_20260217_01",
"client": "web-debug",
"output": {
"mode": "audio"
},
"systemPrompt": "你是简洁助手",
"greeting": "你好,我能帮你什么?",
"dynamicVariables": {
"customer_name": "Alice",
"plan_tier": "Pro"
}
}
}
字段说明:
| 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 |
|---|---|---|---|---|---|
type |
string | 是 | 固定 "session.start" |
启动会话 | 握手后第二阶段消息 |
audio |
object | null | 否 | 仅支持固定值 | 音频格式描述 | 仅用于声明;MVP 实际只接受固定 PCM |
audio.encoding |
string | 否 | 固定 "pcm_s16le" |
编码格式 | 非该值会在模型校验层报错 |
audio.sample_rate_hz |
number | 否 | 固定 16000 |
采样率 | 16kHz |
audio.channels |
number | 否 | 固定 1 |
声道数 | 单声道 |
metadata |
object | null | 否 | 任意对象(会被白名单过滤) | 运行时配置 | 用于 app/channel/提示词/输出模式等覆盖 |
metadata 白名单策略(关键):
- 允许透传的标识字段(ID 类):
appId/app_idchannelconfigVersionId/config_version_id
- 允许透传的覆盖字段:
firstTurnModegreetinggeneratedOpenerEnabledsystemPromptoutputbargeInknowledgeknowledgeBaseIdopenerAudiohistoryuserIdassistantIdsourcedynamicVariables
- 客户端传入
metadata.services会被忽略(服务端会记录 warning),服务配置由后端/环境变量决定。
metadata.dynamicVariables 规则:
- 可选;必须是
object<string,string>。 - key 正则:
^[a-zA-Z_][a-zA-Z0-9_]{0,63}$ - 最多 30 个变量,单个 value 最长 1000 字符。
systemPrompt/greeting中支持占位符语法:{{variable_name}}。- 内置系统变量(始终可用):
{{system__time}}、{{system_utc}}、{{system_timezone}}。system__time:会话开始时的本地时间(YYYY-MM-DD HH:mm:ss)system_utc:会话开始时的 UTC 时间(YYYY-MM-DD HH:mm:ss)system_timezone:会话开始时的本地时区
- 若模板引用了缺失变量,
session.start会被拒绝,错误码protocol.dynamic_variables_missing。 - 若
dynamicVariables结构/内容非法,session.start会被拒绝,错误码protocol.dynamic_variables_invalid。
output.mode 用法:
"audio"(默认语音输出)"text"(纯文本输出)- 纯文本模式下仍会收到
assistant.response.delta/final; - 不会收到 TTS 音频帧与
output.audio.start/end。
- 纯文本模式下仍会收到
3.3 input.text
示例:
{
"type": "input.text",
"text": "你能做什么?"
}
字段说明:
| 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 |
|---|---|---|---|---|---|
type |
string | 是 | 固定 "input.text" |
文本输入 | 跳过 ASR,直接触发 LLM 回答 |
text |
string | 是 | 非空字符串为佳 | 用户文本 | 用于文本聊天或调试 |
3.4 response.cancel
示例:
{
"type": "response.cancel",
"graceful": false
}
字段说明:
| 字段 | 类型 | 必填 | 默认值 | 含义 | 使用说明 |
|---|---|---|---|---|---|
type |
string | 是 | - | 固定 "response.cancel" |
请求中断当前回答 |
graceful |
boolean | 否 | false |
取消方式 | false 立即打断;true 当前实现主要用于记录日志,不强制中断 |
3.5 tool_call.results
仅在工具执行端为客户端时使用(assistant.tool_call.executor == "client")。
示例:
{
"type": "tool_call.results",
"results": [
{
"tool_call_id": "call_abc123",
"name": "weather",
"output": { "temp_c": 21, "condition": "sunny" },
"status": { "code": 200, "message": "ok" }
}
]
}
字段说明:
| 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 |
|---|---|---|---|---|---|
type |
string | 是 | 固定 "tool_call.results" |
工具执行回传 | 客户端工具结果上送 |
results |
array | 否 | 默认为空数组 | 多个工具结果 | 可批量回传 |
results[].tool_call_id |
string | 是 | 任意字符串 | 工具调用ID | 必须与 assistant.tool_call.tool_call_id 对应 |
results[].name |
string | 是 | 任意字符串 | 工具名 | 建议与请求一致 |
results[].output |
any | 否 | 任意 JSON | 工具输出 | 供模型后续组织回答 |
results[].status |
object | 是 | 包含 code、message |
执行状态 | 用于判定成功/失败 |
results[].status.code |
number | 是 | HTTP 风格状态码 | 状态码 | 200-299 判定成功 |
results[].status.message |
string | 是 | 任意字符串 | 状态描述 | 例如 "ok" / "timeout" |
处理规则:
- 未请求过的
tool_call_id会被忽略(防止伪造/串话); - 重复回传会被忽略;
- 超时未回传会由服务端合成超时结果(
504)。
3.6 session.stop
示例:
{
"type": "session.stop",
"reason": "client_disconnect"
}
字段说明:
| 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 |
|---|---|---|---|---|---|
type |
string | 是 | 固定 "session.stop" |
结束会话 | 正常结束推荐发送 |
reason |
string | null | 否 | 任意字符串 | 结束原因 | 服务端会回传到 session.stopped.reason |
4. 二进制音频输入(客户端 -> 服务端)
在 session.started 之后可持续发送二进制音频。
固定格式(MVP):
- 编码:
pcm_s16le - 采样率:
16000 - 声道:
1 - 帧长:20ms =
640 bytes
分包规则:
- 单个 WebSocket 二进制消息可包含 1 帧或多帧;
- 长度必须是
640的整数倍; - 不是
640倍数会触发audio.frame_size_mismatch,该消息整包丢弃; - 奇数字节长度会触发
audio.invalid_pcm。
5. 服务端 -> 客户端事件(输出)
所有 JSON 事件都包含统一包络字段。
5.1 统一包络(Envelope)
{
"type": "event.name",
"timestamp": 1730000000000,
"sessionId": "sess_xxx",
"seq": 42,
"source": "asr",
"trackId": "audio_in",
"data": {}
}
字段说明:
| 字段 | 类型 | 含义 | 使用说明 |
|---|---|---|---|
type |
string | 事件类型 | 见下方事件清单 |
timestamp |
number | 事件时间戳(毫秒) | 由 ev() 生成 |
sessionId |
string | 会话ID | 同一连接固定 |
seq |
number | 单会话递增序号 | 可用于重放、去重、排序 |
source |
string | 事件来源 | 常见:asr/llm/tts/tool/system/client/server |
trackId |
string | 事件轨道 | 常用:audio_in/audio_out/control |
data |
object | 结构化数据 | 顶层业务字段会镜像进 data 以兼容旧客户端 |
关联ID(在 data 内自动注入,存在时):
turn_id:一次用户-助手对话轮次utterance_id:一次用户语音话语response_id:一次助手生成响应tool_call_id:一次工具调用tts_id:一次 TTS 播放段
5.2 事件类型与参数
5.2.1 会话与控制类
hello.ack
- 关键字段:
version - 含义:握手成功,应紧接着发送
session.start
session.started
- 关键字段:
trackIdtracks.audio_intracks.audio_outtracks.controlaudio(回显客户端声明的音频元信息)
- 含义:会话进入 ACTIVE,可发音频/文本
config.resolved
- 关键字段:
config.channelconfig.output.modeconfig.tools.enabledconfig.tools.countconfig.tracks
- 含义:服务端公开配置快照(SaaS 安全),便于前端展示与排错
- 发送策略:可选调试事件,默认关闭(
ws_emit_config_resolved=false) - 不应返回:
assistantId/appId/configVersionIdservices(provider/model/baseUrl 等内部运行细节)- 系统提示词原文及其它内部编排细节
heartbeat
- 关键字段:无业务字段(仅 envelope)
- 含义:保活心跳
- 默认间隔:
heartbeat_interval_sec(默认 50s)
session.stopped
- 关键字段:
reason - 含义:会话结束确认
error
- 关键字段:
sendercodemessagestageretryabletrackIddata.error(结构化错误镜像)
- 含义:统一错误事件
5.2.2 识别与输入侧(ASR/VAD)
input.speech_started
- 字段:
probability - 含义:检测到语音开始
input.speech_stopped
- 字段:
probability - 含义:检测到语音结束
transcript.delta
- 字段:
text - 含义:ASR 增量识别文本(节流发送)
transcript.final
- 字段:
text - 含义:ASR 最终识别文本
5.2.3 输出侧(LLM/TTS/Tool)
assistant.response.delta
- 字段:
text - 含义:助手增量文本输出(节流发送)
assistant.response.final
- 字段:
text - 含义:助手完整文本输出
assistant.tool_call
- 字段:
tool_call_idtool_namearguments(对象)executor(client或server)timeout_mstool_call(完整工具调用对象)
- 含义:通知客户端发生工具调用(用于可视化或客户端执行)
assistant.tool_result
- 字段:
source(client或server)tool_call_idtool_nameok(boolean)error(失败时{code,message,retryable})result(原始结果对象)
- 含义:工具调用结果回执
output.audio.start
- 含义:TTS 音频输出开始边界
output.audio.end
- 含义:TTS 音频输出结束边界
response.interrupted
- 含义:当前回答被打断(barge-in 或 cancel)
metrics.ttfb
- 字段:
latencyMs - 含义:首包音频时延(TTFB)
5.2.4 工作流扩展事件(可选)
若 metadata.workflow 生效,会额外出现:
workflow.startedworkflow.node.enteredworkflow.edge.takenworkflow.tool.requestedworkflow.human_transferworkflow.ended
这些事件用于外部可视化工作流状态,不影响基础语音会话协议。
6. 服务端二进制音频输出(服务端 -> 客户端)
- 音频为 PCM 二进制帧;
- 发送单位对齐到
640 bytes(不足会补零后发送); - 前端通常结合
output.audio.start/end做播放边界控制; - 收到
response.interrupted后应丢弃队列中未播放完的旧音频。
7. 错误模型与常见错误码
统一结构(error 事件):
{
"type": "error",
"sender": "client",
"code": "protocol.invalid_message",
"message": "Invalid message: ...",
"stage": "protocol",
"retryable": false,
"trackId": "control",
"data": {
"error": {
"stage": "protocol",
"code": "protocol.invalid_message",
"message": "Invalid message: ...",
"retryable": false
}
}
}
字段语义:
sender:错误来源角色(如client/server)code:机器可读错误码message:人类可读描述stage:阶段(protocol|audio|asr|llm|tts|tool)retryable:是否建议重试trackId:错误归属轨道
常见错误码:
protocol.invalid_jsonprotocol.invalid_messageprotocol.orderprotocol.version_unsupportedprotocol.unsupportedaudio.invalid_pcmaudio.frame_size_mismatchaudio.processing_failedserver.internal
8. 心跳与超时
服务端后台任务逻辑:
- 每隔约 5 秒检查一次连接;
- 超过
inactivity_timeout_sec(默认 60 秒)未收到任何客户端消息则关闭会话; - 每隔
heartbeat_interval_sec(默认 50 秒)发送一次heartbeat。
客户端建议:
- 持续上行音频或定期发送轻量文本消息,避免被判定闲置;
- 用
heartbeat+seq检测连接活性和事件乱序。
9. 实战接入建议
- 建连后立即发送
hello,收到hello.ack后再发session.start。 - 语音输入严格按 16k/16bit/mono,并保证每个 WS 二进制消息长度是
640*n。 - UI 层把
assistant.response.delta当作流式显示,把assistant.response.final当作收敛结果。 - 播放器用
output.audio.start/end管理一轮播报生命周期。 - 工具调用场景下,若
executor=client,务必按tool_call_id回传tool_call.results。 - 出现
error时优先按code分流处理,而不是仅看message。
10. 最小完整时序示例
Client -> hello
Server <- hello.ack
Client -> session.start
Server <- session.started
Server <- (optional) config.resolved
Client -> (binary pcm frames...)
Server <- input.speech_started / transcript.delta / transcript.final
Server <- assistant.response.delta / assistant.response.final
Server <- output.audio.start
Server <- (binary pcm frames...)
Server <- output.audio.end
Client -> session.stop
Server <- session.stopped