Organize config

This commit is contained in:
Xin Wang
2026-02-25 15:52:55 +08:00
parent 2b2193557d
commit 8b9064f6e6
12 changed files with 1248 additions and 92 deletions

520
docs/ws_v1_schema_zh.md Normal file
View File

@@ -0,0 +1,520 @@
# 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",
"auth": {
"apiKey": "optional-api-key",
"jwt": "optional-jwt"
}
}
```
字段说明:
| 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 |
|---|---|---|---|---|---|
| `type` | string | 是 | 固定 `"hello"` | 消息类型 | 握手第一条消息 |
| `version` | string | 是 | 固定 `"v1"` | 协议版本 | 版本不匹配会 `protocol.version_unsupported` 并断开 |
| `auth` | object \| null | 否 | 仅允许 `apiKey``jwt` | 认证载荷 | 认证策略由服务端配置决定 |
| `auth.apiKey` | string \| null | 否 | 任意字符串 | API Key | 若服务端配置 `WS_API_KEY`,必须精确匹配 |
| `auth.jwt` | string \| null | 否 | 任意字符串 | JWT 字符串 | 当 `WS_REQUIRE_AUTH=true` 时可用于满足“有认证信息”条件 |
认证行为:
- 若设置了 `WS_API_KEY`:必须提供且匹配 `auth.apiKey`,否则 `auth.invalid_api_key` 并关闭连接。
-`WS_REQUIRE_AUTH=true` 且未设置 `WS_API_KEY``auth.apiKey``auth.jwt` 至少一个非空,否则 `auth.required` 并关闭连接。
## 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": "你好,我能帮你什么?"
}
}
```
字段说明:
| 字段 | 类型 | 必填 | 约束 | 含义 | 使用说明 |
|---|---|---|---|---|---|
| `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`
- `history`
- `userId`
- `assistantId`
- `source`
- 客户端传入 `metadata.services` 会被忽略(服务端会记录 warning服务配置由后端/环境变量决定。
`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` / `auth`
- `code`:机器可读错误码
- `message`:人类可读描述
- `stage`:阶段(`protocol|audio|asr|llm|tts|tool`
- `retryable`:是否建议重试
- `trackId`:错误归属轨道
常见错误码:
- `protocol.invalid_json`
- `protocol.invalid_message`
- `protocol.order`
- `protocol.version_unsupported`
- `protocol.unsupported`
- `auth.invalid_api_key`
- `auth.required`
- `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
```