# WebSocket 协议 WebSocket 端点提供双向实时语音对话能力,支持音频流输入输出和文本消息交互。 ## 连接地址 ``` ws:///ws?assistant_id= ``` - `assistant_id` 为必填 query 参数,用于从数据库加载该助手的运行时配置。 ## 传输规则 - **文本帧**:JSON 格式控制消息 - **二进制帧**:PCM 音频数据(`pcm_s16le`, 16kHz, 单声道) - 帧长度必须是 640 字节的整数倍(20ms 音频 = 640 bytes) --- ## 消息流程 ``` 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 ``` --- ## 客户端 -> 服务端消息 `session.start` 客户端连接后发送的第一个消息,用于启动对话会话。 ```json { "type": "session.start", "audio": { "encoding": "pcm_s16le", "sample_rate_hz": 16000, "channels": 1 }, "metadata": { "channel": "web", "source": "web_debug", "history": { "userId": 1 }, "overrides": { "systemPrompt": "你是简洁助手", "greeting": "你好,我能帮你什么?", "output": { "mode": "audio" } }, "dynamicVariables": { "customer_name": "Alice", "plan_tier": "Pro" } } } ``` | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `type` | string | 是 | 固定为 `"session.start"` | | `audio` | object | 否 | 音频格式描述 | | `audio.encoding` | string | 否 | 固定为 `"pcm_s16le"` | | `audio.sample_rate_hz` | number | 否 | 固定为 `16000` | | `audio.channels` | number | 否 | 固定为 `1` | | `metadata` | object | 否 | 运行时配置 | **metadata 支持的字段**: - `channel` - 渠道标识 - `source` - 来源标识 - `history.userId` - 历史记录用户 ID - `overrides` - 可覆盖字段(仅限安全白名单) - `dynamicVariables` - 动态变量(支持 `{{variable}}` 占位符) **`metadata.overrides` 白名单字段**: - `systemPrompt` - `greeting` - `firstTurnMode` - `generatedOpenerEnabled` - `output` - `bargeIn` - `knowledgeBaseId` - `knowledge` - `tools` - `openerAudio` **限制**: - `metadata.workflow` 会被忽略(不触发 workflow 事件) - 禁止提交 `metadata.services` - 禁止提交 `assistantId` / `appId` / `app_id` / `configVersionId` / `config_version_id` - 禁止提交包含密钥语义的字段(如 `apiKey` / `token` / `secret` / `password` / `authorization`) --- `input.text` 发送文本输入,跳过 ASR 识别,直接触发 LLM 回复。 ```json { "type": "input.text", "text": "你能做什么?" } ``` | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `type` | string | 是 | 固定为 `"input.text"` | | `text` | string | 是 | 用户文本内容 | --- `response.cancel` 请求中断当前回答。 ```json { "type": "response.cancel", "graceful": false } ``` | 字段 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `type` | string | 是 | - | 固定为 `"response.cancel"` | | `graceful` | boolean | 否 | `false` | `false` 立即打断 | --- `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 | | `response_id` | string | 否 | 所属回复 ID(建议回传) | | `turn_id` | string | 否 | 所属轮次 ID(建议回传) | | `played_at_ms` | number | 否 | 客户端本地播放完成时间戳(毫秒) | | `played_ms` | number | 否 | 本次播放耗时(毫秒) | --- `tool_call.results` 回传客户端执行的工具结果。 ```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 | | `results[].name` | string | 是 | 工具名称 | | `results[].output` | any | 否 | 工具输出 | | `results[].status` | object | 是 | 执行状态 | | `results[].status.code` | number | 是 | HTTP 状态码(200-299 表示成功) | | `results[].status.message` | string | 是 | 状态描述 | --- `session.stop` 结束对话会话。 ```json { "type": "session.stop", "reason": "client_disconnect" } ``` | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `type` | string | 是 | 固定为 `"session.stop"` | | `reason` | string | 否 | 结束原因 | --- `Binary Audio` 在 `session.started` 之后可持续发送二进制 PCM 音频。 - **格式**:`pcm_s16le` - **采样率**:16000 Hz - **声道**:1(单声道) - **帧长**:20ms = 640 bytes --- ## 服务端 -> 客户端事件 ### 事件包络 所有 JSON 事件都包含统一包络字段: ```json { "type": "event.name", "timestamp": 1730000000000, "sessionId": "sess_xxx", "seq": 42, "source": "asr", "trackId": "audio_in", "data": {} } ``` | 字段 | 类型 | 说明 | |---|---|---| | `type` | string | 事件类型 | | `timestamp` | number | 事件时间戳(Unix 毫秒) | | `sessionId` | string | 会话 ID | | `seq` | number | 递增序号(用于重放/恢复) | | `source` | string | 事件来源:`asr` / `llm` / `tts` / `tool` / `system` / `client` / `server` | | `trackId` | string | 事件轨道:`audio_in` / `audio_out` / `control` | | `data` | object | 业务数据(可选) | **轨道 ID 说明**: | trackId | 说明 | 相关事件 | |---------|------|---------| | `audio_in` | ASR/VAD 输入侧事件 | `input.*`, `transcript.*` | | `audio_out` | 助手输出侧事件 | `assistant.*`, `output.audio.*`, `response.interrupted`, `metrics.ttfb` | | `control` | 会话控制事件 | `session.*`, `error`, `heartbeat`, `(optional) config.resolved` | --- ### 会话控制类事件 #### `session.started` 会话启动成功,客户端收到此事件后可以开始发送音频。 ```json { "type": "session.started", "timestamp": 1730000000000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 1, "trackId": "control", "tracks": { "audio_in": "audio_in", "audio_out": "audio_out", "control": "control" }, "audio": { "encoding": "pcm_s16le", "sample_rate_hz": 16000, "channels": 1 } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `sessionId` | string | 会话唯一标识符 | | `trackId` | string | 固定为 `"control"` | | `tracks` | object | 可用轨道列表 | | `tracks.audio_in` | string | 输入轨道 ID | | `tracks.audio_out` | string | 输出轨道 ID | | `tracks.control` | string | 控制轨道 ID | | `audio` | object | 音频格式配置 | | `audio.encoding` | string | 编码格式 | | `audio.sample_rate_hz` | number | 采样率 | | `audio.channels` | number | 声道数 | --- #### `config.resolved` 服务端返回的**公开配置快照**。 默认不发送(SaaS 公网模式建议关闭);仅在 `WS_EMIT_CONFIG_RESOLVED=true` 时发送。 ```json { "type": "config.resolved", "timestamp": 1730000000001, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 2, "trackId": "control", "config": { "channel": "web_debug", "output": { "mode": "audio" }, "tools": { "enabled": true, "count": 2 }, "tracks": { "audio_in": "audio_in", "audio_out": "audio_out", "control": "control" } } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"control"` | | `config` | object | SaaS 安全的公开配置快照 | | `config.channel` | string | 回显 `session.start.metadata.channel`(如提供) | | `config.output` | object | 输出配置 | | `config.output.mode` | string | 输出模式:`"audio"` / `"text"` | | `config.tools.enabled` | boolean | 是否启用工具能力 | | `config.tools.count` | number | 可用工具数量(不暴露工具清单) | | `config.tracks` | object | 可用轨道列表 | **不会返回以下内部字段**: - `assistantId` / `appId` / `configVersionId` - `services`(provider/model/baseUrl 等) - 系统提示词原文及其它内部编排细节 --- #### `heartbeat` 保活心跳事件,默认每 50 秒发送一次。 ```json { "type": "heartbeat", "timestamp": 1730000050000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 10 } ``` | 字段 | 类型 | 说明 | |---|---|---| | `timestamp` | number | 心跳时间戳 | --- #### `session.stopped` 会话结束确认。 ```json { "type": "session.stopped", "timestamp": 1730000100000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 50, "reason": "client_requested" } ``` | 字段 | 类型 | 说明 | |---|---|---| | `reason` | string | 结束原因:`"client_requested"` / `"timeout"` / `"error"` | --- ### ASR 识别事件 #### `input.speech_started` 检测到语音开始(VAD)。 ```json { "type": "input.speech_started", "timestamp": 1730000010000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 5, "source": "asr", "trackId": "audio_in", "probability": 0.95 } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_in"` | | `probability` | number | 语音检测置信度(0-1) | --- #### `input.speech_stopped` 检测到语音结束(VAD)。 ```json { "type": "input.speech_stopped", "timestamp": 1730000012000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 8, "source": "asr", "trackId": "audio_in", "probability": 0.92 } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_in"` | | `probability` | number | 静音检测置信度(0-1) | --- #### `transcript.delta` ASR 增量识别文本(实时转写)。 ```json { "type": "transcript.delta", "timestamp": 1730000011000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 6, "source": "asr", "trackId": "audio_in", "text": "你好", "data": { "text": "你好", "turn_id": "turn_001", "utterance_id": "utt_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_in"` | | `text` | string | 增量识别文本 | | `data.text` | string | 增量识别文本(同 `text`) | | `data.turn_id` | string | 当前对话轮次 ID | | `data.utterance_id` | string | 当前语句 ID | **节流说明**:服务端默认每 300ms 合并一次 delta 事件。 --- #### `transcript.final` ASR 最终识别文本(语句结束)。 ```json { "type": "transcript.final", "timestamp": 1730000012500, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 9, "source": "asr", "trackId": "audio_in", "text": "你好,请问今天天气怎么样", "data": { "text": "你好,请问今天天气怎么样", "turn_id": "turn_001", "utterance_id": "utt_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_in"` | | `text` | string | 最终识别文本 | | `data.text` | string | 最终识别文本(同 `text`) | | `data.turn_id` | string | 当前对话轮次 ID | | `data.utterance_id` | string | 当前语句 ID | --- ### LLM/TTS 输出事件 #### `assistant.response.delta` 助手增量文本输出(流式生成)。 ```json { "type": "assistant.response.delta", "timestamp": 1730000013000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 12, "source": "llm", "trackId": "audio_out", "text": "今天天气", "data": { "text": "今天天气", "turn_id": "turn_001", "response_id": "resp_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `source` | string | 固定为 `"llm"` | | `text` | string | 增量文本内容 | | `data.text` | string | 增量文本内容(同 `text`) | | `data.turn_id` | string | 当前对话轮次 ID | | `data.response_id` | string | 当前回复 ID | **节流说明**:服务端默认每 80ms 合并一次 delta 事件。 --- #### `assistant.response.final` 助手完整文本输出(回复结束)。 ```json { "type": "assistant.response.final", "timestamp": 1730000015000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 18, "source": "llm", "trackId": "audio_out", "text": "今天天气晴朗,气温25度,适合外出。", "data": { "text": "今天天气晴朗,气温25度,适合外出。", "turn_id": "turn_001", "response_id": "resp_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `source` | string | 固定为 `"llm"` | | `text` | string | 完整回复文本 | | `data.text` | string | 完整回复文本(同 `text`) | | `data.turn_id` | string | 当前对话轮次 ID | | `data.response_id` | string | 当前回复 ID | --- #### `assistant.tool_call` 工具调用通知,通知客户端 LLM 请求调用工具。 ```json { "type": "assistant.tool_call", "timestamp": 1730000014000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 14, "source": "llm", "trackId": "audio_out", "tool_call_id": "call_abc123", "tool_name": "weather", "arguments": { "city": "北京" }, "executor": "server", "timeout_ms": 30000, "data": { "tool_call": { "id": "call_abc123", "name": "weather", "arguments": "{\"city\":\"北京\"}" }, "turn_id": "turn_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `source` | string | 固定为 `"llm"` | | `tool_call_id` | string | 工具调用唯一 ID | | `tool_name` | string | 工具名称 | | `arguments` | object | 工具参数(已解析的 JSON) | | `executor` | string | 执行方:`"server"` 服务端执行 / `"client"` 客户端执行 | | `timeout_ms` | number | 超时时间(毫秒) | | `data.tool_call` | object | 原始工具调用信息 | | `data.tool_call.id` | string | 工具调用 ID | | `data.tool_call.name` | string | 工具名称 | | `data.tool_call.arguments` | string | 工具参数(JSON 字符串) | | `data.turn_id` | string | 当前对话轮次 ID | **注意**:当 `executor = "client"` 时,客户端需要执行工具并返回 `tool_call.results`。 --- #### `assistant.tool_result` 工具执行结果通知。 ```json { "type": "assistant.tool_result", "timestamp": 1730000014500, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 15, "source": "server", "trackId": "audio_out", "tool_call_id": "call_abc123", "tool_name": "weather", "tool_display_name": "天气查询", "ok": true, "error": null, "result": { "tool_call_id": "call_abc123", "name": "weather", "output": { "temperature": 25, "condition": "晴", "humidity": 40 }, "status": { "code": 200, "message": "ok" } } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `source` | string | 执行方:`"server"` / `"client"` | | `tool_call_id` | string | 工具调用 ID | | `tool_name` | string | 工具名称 | | `tool_display_name` | string | 工具显示名称 | | `ok` | boolean | 执行是否成功(状态码 200-299 为 true) | | `error` | object \| null | 错误信息(`ok=false` 时存在) | | `error.code` | number | 错误状态码 | | `error.message` | string | 错误描述 | | `error.retryable` | boolean | 是否可重试 | | `result` | object | 原始执行结果 | | `result.output` | any | 工具返回数据 | | `result.status` | object | 执行状态 | | `result.status.code` | number | HTTP 状态码 | | `result.status.message` | string | 状态描述 | --- #### `output.audio.start` TTS 音频播放开始标记。 ```json { "type": "output.audio.start", "timestamp": 1730000015500, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 19, "source": "tts", "trackId": "audio_out", "data": { "tts_id": "tts_001", "turn_id": "turn_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `source` | string | 固定为 `"tts"` | | `data.tts_id` | string | TTS 播放段 ID | | `data.turn_id` | string | 当前对话轮次 ID | **说明**:此事件后服务端将发送二进制 PCM 音频帧。 --- #### `output.audio.end` TTS 音频播放结束标记。 ```json { "type": "output.audio.end", "timestamp": 1730000018000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 25, "source": "tts", "trackId": "audio_out", "data": { "tts_id": "tts_001", "turn_id": "turn_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `source` | string | 固定为 `"tts"` | | `data.tts_id` | string | TTS 播放段 ID | | `data.turn_id` | string | 当前对话轮次 ID | **说明**:`output.audio.end` 表示服务端已发送完成,不代表客户端扬声器已播完。若需要“真实播完”信号,客户端应发送 `output.audio.played`。 --- #### `response.interrupted` 回答被打断(用户插话)。 ```json { "type": "response.interrupted", "timestamp": 1730000016000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 20, "source": "system", "trackId": "audio_out", "data": { "turn_id": "turn_001", "response_id": "resp_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `data.turn_id` | string | 被打断的对话轮次 ID | | `data.response_id` | string | 被打断的回复 ID | --- #### `metrics.ttfb` 首包音频时延指标(Time To First Byte)。 ```json { "type": "metrics.ttfb", "timestamp": 1730000015600, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 21, "source": "system", "trackId": "audio_out", "latencyMs": 1520, "data": { "latencyMs": 1520, "turn_id": "turn_001" } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `trackId` | string | 固定为 `"audio_out"` | | `latencyMs` | number | 首包音频时延(毫秒) | | `data.latencyMs` | number | 首包音频时延(同 `latencyMs`) | | `data.turn_id` | string | 当前对话轮次 ID | **说明**:从用户输入结束到第一个音频包发送的时间。 --- ### 错误事件 #### `error` 统一错误事件。 ```json { "type": "error", "timestamp": 1730000020000, "sessionId": "ea34e1ca-b417-4a57-b03e-f752cb82e97d", "seq": 30, "sender": "server", "code": "llm.timeout", "message": "LLM request timeout", "stage": "llm", "retryable": true, "trackId": "audio_out", "data": { "error": { "stage": "llm", "code": "llm.timeout", "message": "LLM request timeout", "retryable": true } } } ``` | 字段 | 类型 | 说明 | |---|---|---| | `sender` | string | 错误来源:`"server"` / `"client"` | | `code` | string | 错误码 | | `message` | string | 错误描述 | | `stage` | string | 错误阶段:`"protocol"` / `"asr"` / `"llm"` / `"tts"` / `"tool"` / `"audio"` | | `retryable` | boolean | 是否可重试 | | `trackId` | string | 错误关联的轨道 | | `data.error` | object | 结构化错误信息 | | `data.error.stage` | string | 错误阶段 | | `data.error.code` | string | 错误码 | | `data.error.message` | string | 错误描述 | | `data.error.retryable` | boolean | 是否可重试 | **trackId 约定**: - `audio_in`:ASR/音频输入相关错误 - `audio_out`:LLM/TTS/工具相关错误 - `control`:协议/会话控制相关错误 --- ## 关联 ID 说明 事件中的关联 ID 用于追踪对话流程: | ID 类型 | 说明 | 生命周期 | |---------|------|---------| | `turn_id` | 对话轮次 ID | 一次用户-助手交互 | | `utterance_id` | 语句 ID | 一次 ASR 最终识别结果 | | `response_id` | 回复 ID | 一次助手回复生成 | | `tool_call_id` | 工具调用 ID | 一次工具调用 | | `tts_id` | TTS 播放段 ID | 一段语音合成播放 | --- ## 心跳与超时 - **心跳间隔**:默认 50 秒(`heartbeat_interval_sec`) - **空闲超时**:默认 60 秒(`inactivity_timeout_sec`) - 客户端应持续发送音频或轻量消息避免被判定闲置 ## 事件节流 为保持客户端渲染和服务端负载稳定,v1 协议对部分事件进行节流: | 事件 | 默认节流间隔 | 说明 | |------|-------------|------| | `transcript.delta` | 300ms | ASR 增量文本 | | `assistant.response.delta` | 80ms | LLM 增量文本 | ## 错误处理 详细错误码请参考 [错误码](errors.md)。