Files
AI-VideoAssistant/engine/docs/ws_v1_schema_zh.md

525 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WS v1 协议完整说明(中文)
本文档描述 `/ws` 端点的 WebSocket v1 协议,覆盖:
- 客户端输入JSON 文本消息 + 二进制音频);
- 服务端输出JSON 事件 + 二进制音频);
- 每个参数的类型、约束、含义与使用方式;
- 握手顺序、状态机、错误语义与实现细节。
实现对照来源:
- `models/ws_v1.py`
- `core/session.py`
- `core/duplex_pipeline.py`
- `app/main.py`
---
## 1. 传输与基础规则
- 连接地址:`ws://<host>/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`(可选)
- `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<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`
示例:
```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 `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.6 `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.appId`
- `config.channel`
- `config.configVersionId`
- `config.prompt.sha256`
- `config.output`
- `config.services`(去密钥后的有效服务配置)
- `config.tools.allowlist`
- `config.tracks`
- 含义:服务端最终生效配置快照,便于前端展示与排错
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` 做播放边界控制;
- 收到 `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. 工具调用场景下,若 `executor=client`,务必按 `tool_call_id` 回传 `tool_call.results`
6. 出现 `error` 时优先按 `code` 分流处理,而不是仅看 `message`
---
## 10. 最小完整时序示例
```text
Client -> hello
Server <- hello.ack
Client -> session.start
Server <- session.started
Server <- 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
```