# WS v1 协议完整说明(中文) 本文档描述 `/ws` 端点的 WebSocket v1 协议,覆盖: - 客户端输入(JSON 文本消息 + 二进制音频); - 服务端输出(JSON 事件 + 二进制音频); - 每个参数的类型、约束、含义与使用方式; - 握手顺序、状态机、错误语义与实现细节。 实现对照来源: - `models/ws_v1.py` - `core/session.py` - `core/duplex_pipeline.py` - `app/main.py` --- ## 1. 传输与基础规则 - 连接地址:`ws:///ws` - 单连接双通道承载: - 文本帧:JSON 控制消息(严格校验 schema) - 二进制帧:原始 PCM 音频 - JSON 校验策略: - 所有已定义客户端消息都 `extra="forbid"`,即不允许未声明字段; - `hello.version` 固定必须是 `"v1"`; - 缺失 `type` 或未知 `type` 会返回协议错误。 --- ## 2. 状态机与消息顺序 ### 2.1 服务端状态 - `WAIT_HELLO`:等待 `hello` - `WAIT_START`:已通过握手,等待 `session.start` - `ACTIVE`:会话运行中,可收发文本/音频 - `STOPPED`:会话结束 ### 2.2 正确顺序 1. 客户端发送 `hello` 2. 服务端返回 `hello.ack` 3. 客户端发送 `session.start` 4. 服务端返回 `session.started` 5. 客户端可持续发送: - 二进制音频 - `input.text`(可选) - `response.cancel`(可选) - `output.audio.played`(可选) - `tool_call.results`(可选) 6. 客户端发送 `session.stop` 或直接断开连接 顺序错误会返回 `error`,`code = "protocol.order"`。 --- ## 3. 客户端 -> 服务端消息(输入) ## 3.1 `hello` 示例: ```json { "type": "hello", "version": "v1" } ``` 字段说明: | 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 | |---|---|---|---|---|---| | `type` | string | 是 | 固定 `"hello"` | 消息类型 | 握手第一条消息 | | `version` | string | 是 | 固定 `"v1"` | 协议版本 | 版本不匹配会 `protocol.version_unsupported` 并断开 | ## 3.2 `session.start` 示例: ```json { "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_id` - `channel` - `configVersionId` / `config_version_id` - 允许透传的覆盖字段: - `firstTurnMode` - `greeting` - `generatedOpenerEnabled` - `systemPrompt` - `output` - `bargeIn` - `knowledge` - `knowledgeBaseId` - `openerAudio` - `history` - `userId` - `assistantId` - `source` - `dynamicVariables` - 客户端传入 `metadata.services` 会被忽略(服务端会记录 warning),服务配置由后端/环境变量决定。 `metadata.dynamicVariables` 规则: - 可选;必须是 `object`。 - 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` 示例: ```json { "type": "input.text", "text": "你能做什么?" } ``` 字段说明: | 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 | |---|---|---|---|---|---| | `type` | string | 是 | 固定 `"input.text"` | 文本输入 | 跳过 ASR,直接触发 LLM 回答 | | `text` | string | 是 | 非空字符串为佳 | 用户文本 | 用于文本聊天或调试 | ## 3.4 `response.cancel` 示例: ```json { "type": "response.cancel", "graceful": false } ``` 字段说明: | 字段 | 类型 | 必填 | 默认值 | 含义 | 使用说明 | |---|---|---|---|---|---| | `type` | string | 是 | - | 固定 `"response.cancel"` | 请求中断当前回答 | | `graceful` | boolean | 否 | `false` | 取消方式 | `false` 立即打断;`true` 当前实现主要用于记录日志,不强制中断 | ## 3.5 `output.audio.played` 客户端在本地扬声器真正播完后回执(含 jitter buffer / 播放队列)。 示例: ```json { "type": "output.audio.played", "tts_id": "tts_001", "response_id": "resp_001", "turn_id": "turn_001", "played_at_ms": 1730000018450, "played_ms": 2520 } ``` 字段说明: | 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 | |---|---|---|---|---|---| | `type` | string | 是 | 固定 `"output.audio.played"` | 播放完成回执 | 客户端播完后上送 | | `tts_id` | string | 是 | 非空字符串 | TTS 段 ID | 建议使用 `output.audio.start/end` 中同一 `tts_id` | | `response_id` | string \| null | 否 | 任意字符串 | 回复 ID | 建议回传,便于聚合 | | `turn_id` | string \| null | 否 | 任意字符串 | 轮次 ID | 建议回传,便于聚合 | | `played_at_ms` | number \| null | 否 | 毫秒时间戳 | 客户端播放完成时间 | 用于时延分析 | | `played_ms` | number \| null | 否 | 非负数 | 客户端播放耗时 | 用于播放器统计 | ## 3.6 `tool_call.results` 仅在工具执行端为客户端时使用(`assistant.tool_call.executor == "client"`)。 示例: ```json { "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.7 `session.stop` 示例: ```json { "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) ```json { "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 会话与控制类 1. `hello.ack` - 关键字段:`version` - 含义:握手成功,应紧接着发送 `session.start` 2. `session.started` - 关键字段: - `trackId` - `tracks.audio_in` - `tracks.audio_out` - `tracks.control` - `audio`(回显客户端声明的音频元信息) - 含义:会话进入 ACTIVE,可发音频/文本 3. `config.resolved` - 关键字段: - `config.channel` - `config.output.mode` - `config.tools.enabled` - `config.tools.count` - `config.tracks` - 含义:服务端公开配置快照(SaaS 安全),便于前端展示与排错 - 发送策略:可选调试事件,默认关闭(`ws_emit_config_resolved=false`) - 不应返回: - `assistantId` / `appId` / `configVersionId` - `services`(provider/model/baseUrl 等内部运行细节) - 系统提示词原文及其它内部编排细节 4. `heartbeat` - 关键字段:无业务字段(仅 envelope) - 含义:保活心跳 - 默认间隔:`heartbeat_interval_sec`(默认 50s) 5. `session.stopped` - 关键字段:`reason` - 含义:会话结束确认 6. `error` - 关键字段: - `sender` - `code` - `message` - `stage` - `retryable` - `trackId` - `data.error`(结构化错误镜像) - 含义:统一错误事件 ### 5.2.2 识别与输入侧(ASR/VAD) 1. `input.speech_started` - 字段:`probability` - 含义:检测到语音开始 2. `input.speech_stopped` - 字段:`probability` - 含义:检测到语音结束 3. `transcript.delta` - 字段:`text` - 含义:ASR 增量识别文本(节流发送) 4. `transcript.final` - 字段:`text` - 含义:ASR 最终识别文本 ### 5.2.3 输出侧(LLM/TTS/Tool) 1. `assistant.response.delta` - 字段:`text` - 含义:助手增量文本输出(节流发送) 2. `assistant.response.final` - 字段:`text` - 含义:助手完整文本输出 3. `assistant.tool_call` - 字段: - `tool_call_id` - `tool_name` - `arguments`(对象) - `executor`(`client` 或 `server`) - `timeout_ms` - `tool_call`(完整工具调用对象) - 含义:通知客户端发生工具调用(用于可视化或客户端执行) 4. `assistant.tool_result` - 字段: - `source`(`client` 或 `server`) - `tool_call_id` - `tool_name` - `ok`(boolean) - `error`(失败时 `{code,message,retryable}`) - `result`(原始结果对象) - 含义:工具调用结果回执 5. `output.audio.start` - 含义:TTS 音频输出开始边界 6. `output.audio.end` - 含义:TTS 音频输出结束边界(服务端发送完成,不等价于扬声器已播完) 7. `response.interrupted` - 含义:当前回答被打断(barge-in 或 cancel) 8. `metrics.ttfb` - 字段:`latencyMs` - 含义:首包音频时延(TTFB) ### 5.2.4 工作流扩展事件(可选) 若 `metadata.workflow` 生效,会额外出现: - `workflow.started` - `workflow.node.entered` - `workflow.edge.taken` - `workflow.tool.requested` - `workflow.human_transfer` - `workflow.ended` 这些事件用于外部可视化工作流状态,不影响基础语音会话协议。 --- ## 6. 服务端二进制音频输出(服务端 -> 客户端) - 音频为 PCM 二进制帧; - 发送单位对齐到 `640 bytes`(不足会补零后发送); - 前端通常结合 `output.audio.start/end` 做播放边界控制; - 若需要“扬声器真实播完”语义,前端应在播完后发送 `output.audio.played`; - 收到 `response.interrupted` 后应丢弃队列中未播放完的旧音频。 --- ## 7. 错误模型与常见错误码 统一结构(`error` 事件): ```json { "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_json` - `protocol.invalid_message` - `protocol.order` - `protocol.version_unsupported` - `protocol.unsupported` - `audio.invalid_pcm` - `audio.frame_size_mismatch` - `audio.processing_failed` - `server.internal` --- ## 8. 心跳与超时 服务端后台任务逻辑: - 每隔约 5 秒检查一次连接; - 超过 `inactivity_timeout_sec`(默认 60 秒)未收到任何客户端消息则关闭会话; - 每隔 `heartbeat_interval_sec`(默认 50 秒)发送一次 `heartbeat`。 客户端建议: - 持续上行音频或定期发送轻量文本消息,避免被判定闲置; - 用 `heartbeat` + `seq` 检测连接活性和事件乱序。 --- ## 9. 实战接入建议 1. 建连后立即发送 `hello`,收到 `hello.ack` 后再发 `session.start`。 2. 语音输入严格按 16k/16bit/mono,并保证每个 WS 二进制消息长度是 `640*n`。 3. UI 层把 `assistant.response.delta` 当作流式显示,把 `assistant.response.final` 当作收敛结果。 4. 播放器用 `output.audio.start/end` 管理一轮播报生命周期。 5. 若业务依赖“扬声器真实播完”,请在播完时上送 `output.audio.played`。 6. 工具调用场景下,若 `executor=client`,务必按 `tool_call_id` 回传 `tool_call.results`。 7. 出现 `error` 时优先按 `code` 分流处理,而不是仅看 `message`。 --- ## 10. 最小完整时序示例 ```text 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 -> output.audio.played (optional) Client -> session.stop Server <- session.stopped ```