Files
story/docs/设备终端接口文档.md

1224 lines
38 KiB
Markdown
Raw Permalink 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.
# 数据采集终端—云端系统设备接口文档
> 文档版本:1.0
> 设备接口版本:`/device/v2`
> OpenAPI 契约版本:`0.68.0`
> 本地联调地址:`http://localhost:58080`
> 契约来源:`openapi/openapi.yaml`、实际 HTTP 路由及设备处理代码
> 编制日期:2026-08-27
## 1. 文档范围
本文档面向终端固件、Windows 模拟设备和设备联调人员,说明终端直接调用云端的全部接口,包括:
- 设备启动、身份校验和配置同步;
- 双向语音通话创建、入房确认、智能体启动状态、心跳和停止;
- 设备遥测、错误和命令结果上报;
- 云端命令拉取与执行回执;
- 固件私有下载;
- 通话终稿补传;
- 双轨录音分片声明、对象存储直传、校验查询和本地删除回执。
本文档详细描述的是设备使用 HMAC 身份直接调用的 `/device/v2` 接口。家庭或运营人员使用的 `/api/v1/devices`、设备绑定、固件发布和灰度管理接口在第 15 章列出边界,但它们不由设备固件直接调用。火山 RTC/豆包回调也不属于设备接口。
## 2. 总体通信关系
```mermaid
flowchart LR
D[终端设备] -->|HTTPS 控制请求 + HMAC| API[云端 API]
API --> PG[(PostgreSQL)]
API --> REDIS[(Redis)]
API -->|可靠任务| W[Worker]
W -->|StartVoiceChat / StopVoiceChat| V[火山语音服务]
D <-->|RTC 实时音频| RTC[火山 RTC 房间]
V -->|豆包智能体加入| RTC
D -->|录音文件 PUT| OSS[对象存储]
API -->|只签发短期地址| D
W -->|流式校验录音| OSS
```
重要边界:
1. 设备的控制请求经过本项目 API;实时麦克风音频不经过本项目 API 和数据库。
2. 设备和豆包智能体分别直接加入同一个火山 RTC 房间。
3. 录音文件使用短期单对象地址直接上传对象存储,不经过业务 API 传输文件字节。
4. RTC Token、上传地址和固件下载地址都是短期凭证,只允许保存在设备运行内存中。
## 3. 接口总览
| 序号 | 方法 | 路径 | 用途 | 幂等键 |
|---:|---|---|---|---|
| 1 | GET | `/device/v2/bootstrap` | 启动登记、版本与时钟检查 | 否 |
| 2 | GET | `/device/v2/config` | 拉取期望配置和绑定信息 | 否,支持 ETag |
| 3 | GET | `/device/v2/commands` | 拉取本设备待执行命令 | 否 |
| 4 | POST | `/device/v2/commands/{id}/ack` | 回报命令接收、成功或失败 | 否,业务状态幂等 |
| 5 | GET | `/device/v2/firmware-releases/{id}/download` | 获取固件私有短期下载地址 | 否 |
| 6 | POST | `/device/v2/conversations` | 创建通话并取得 RTC 房间资料 | 是 |
| 7 | POST | `/device/v2/conversations/{id}/activate` | 确认设备入房并启动豆包智能体 | 是 |
| 8 | GET | `/device/v2/conversations/{id}` | 查询权威通话和智能体状态 | 否 |
| 9 | POST | `/device/v2/conversations/{id}/heartbeat` | 通话心跳和租约续期 | 否,按序号去重 |
| 10 | POST | `/device/v2/conversations/{id}/stop` | 停止通话和豆包任务 | 是 |
| 11 | POST | `/device/v2/conversations/{id}/turns:batch` | 补传设备侧最终字幕 | 是 |
| 12 | POST | `/device/v2/telemetry:batch` | 批量上报状态、错误和命令结果 | 是 |
| 13 | POST | `/device/v2/conversations/{id}/upload-batches` | 声明录音分片并取得上传地址 | 是 |
| 14 | POST | `/device/v2/upload-batches/{id}/complete` | 声明上传完成并触发校验 | 否,业务状态幂等 |
| 15 | GET | `/device/v2/upload-batches/{id}` | 查询录音校验结果 | 否 |
| 16 | POST | `/device/v2/media-segments/{id}/local-delete-ack` | 回报本地录音密文删除结果 | 否,成功回执幂等 |
## 4. 通用协议约定
### 4.1 传输与数据格式
- 生产环境必须使用 HTTPS;本机开发环境可使用 HTTP。
- JSON 请求使用 `Content-Type: application/json; charset=utf-8`。
- 时间统一使用 RFC3339 UTC,例如 `2026-08-27T06:30:00Z`。
- UUID 使用带连字符的标准格式。
- 未声明可为空的字段不要发送 `null`,直接省略。
- 设备必须忽略响应中的未知非必填字段,以保持 `/device/v2` 向后兼容。
- 请求对象声明 `additionalProperties: false` 时,不得发送未定义字段。
### 4.2 统一成功响应
除 304、固件下载重定向等特殊响应外,成功响应统一为:
```json
{
"request_id": "01J6...",
"data": {}
}
```
`request_id` 是本次云端请求关联编号,设备日志应保存该值,但不得把密钥、签名、Token 或临时对象地址写入日志。
### 4.3 统一错误响应
```json
{
"request_id": "01J6...",
"error": {
"code": "VALIDATION_FAILED",
"message": "请求字段不符合契约",
"retryable": false,
"field_errors": [
{
"field": "body.device_time",
"message": "must be a valid RFC3339 timestamp"
}
],
"details": {}
}
}
```
设备应优先使用 `error.code` 和 `retryable` 决定动作,不解析或匹配 `message` 文本。
## 5. 设备 HMAC 身份认证
### 5.1 必填请求头
所有 `/device/v2` 请求都必须包含:
| 请求头 | 格式 | 说明 |
|---|---|---|
| `X-Device-Id` | UUID | 已登记的设备公开编号 |
| `X-Device-Timestamp` | RFC3339 | 本次请求 UTC 时间;默认允许前后 5 分钟误差 |
| `X-Device-Nonce` | 16~128 字符 | 每次 HTTP 尝试都必须重新生成的随机数 |
| `X-Device-Content-SHA256` | 64 位小写十六进制 | 原始 HTTP 正文字节的 SHA-256 |
| `X-Device-Signature` | Base64URL,无 `=` 填充 | HMAC-SHA256 签名结果 |
POST JSON 的摘要必须针对真正发送的原始 UTF-8 字节计算。GET 或空正文请求也必须计算空字节摘要:
```text
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
### 5.2 签名原文
使用单个换行符按以下顺序拼接,最后一行后不增加换行:
```text
UPPERCASE_HTTP_METHOD
REQUEST_URI_WITH_EXACT_QUERY_STRING
X-Device-Timestamp原值
X-Device-Nonce原值
X-Device-Content-SHA256原值
```
例如:
```text
GET
/device/v2/bootstrap?firmware_version=1.2.0&device_time=2026-08-27T06%3A30%3A00Z
2026-08-27T06:30:01Z
62d4b688f77b4baaa87a38d315c2f3b1
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
计算方法:
```text
signature = Base64URL_NoPadding(
HMAC_SHA256(device_secret, canonical_string)
)
```
注意事项:
- 查询参数的顺序、百分号编码和大小写必须与实际请求 URI 完全一致。
- `Nonce` 不是业务幂等键。业务请求重试时保留原 `Idempotency-Key` 和正文,但必须生成新 `Nonce`、时间戳和签名。
- 不允许在设备镜像、日志或前端页面中明文输出设备密钥。
- 云端 Redis 或设备身份数据库不可用时鉴权关闭失败并返回 503,不会绕过签名。
### 5.3 幂等键
需要幂等保护的接口必须包含:
```http
Idempotency-Key: 3ca4e514-cbde-41fc-aa32-6f1ed460cb01
```
长度为 16~128 字符。规则如下:
1. 同一设备、同一接口、同一幂等键和相同正文:返回第一次的业务结果。
2. 同一幂等键对应不同正文:返回 409 `IDEMPOTENCY_KEY_REUSED`。
3. 网络超时且无法确认结果时:使用原幂等键、原正文重试,但重新生成 HMAC 请求头。
4. 返回已有结果时,响应可能包含 `Idempotency-Replayed: true`。
## 6. 启动与配置接口
### 6.1 设备启动登记
`GET /device/v2/bootstrap`
用于设备开机、重连或固件升级后向云端上报运行版本,并取得服务器时间、设备状态、绑定、最低版本策略和期望配置版本。
查询参数:
| 参数 | 必填 | 类型/限制 | 说明 |
|---|---|---|---|
| `firmware_version` | 是 | string,1~80 | 当前固件版本 |
| `device_time` | 是 | RFC3339 | 设备当前时间 |
| `board_rev` | 否 | string,≤80 | 硬件板卡版本 |
| `rtc_sdk_version` | 否 | string,≤80 | RTC SDK 版本 |
| `codec` | 否 | string,≤40 | 当前首选音频编码 |
| `sample_rate` | 否 | integer,≥1 | 当前采样率 |
| `network` | 否 | string,≤80 | 网络类型或描述 |
成功响应示例:
```json
{
"request_id": "01J6BOOTSTRAP",
"data": {
"server_time": "2026-08-27T06:30:01Z",
"clock_skew_ms": 850,
"device": {
"device_id": "10000000-0000-0000-0000-000000000511",
"status": "registered",
"model": "simulated-terminal",
"hardware_revision": "sim-rev-a",
"minimum_security_version": "security-v1",
"minimum_firmware_version": "0.1.0",
"firmware_compatible": true,
"config_revision": 3,
"binding": {
"bound": true,
"household_id": "20000000-0000-0000-0000-000000000001",
"subject_id": "30000000-0000-0000-0000-000000000001",
"subject_name": "演示长者"
}
}
}
}
```
设备处理:
- `status=registered` 且 `binding.bound=true` 才能进入正常业务。
- `quarantined`:停止通话能力,只保留受控诊断或升级。
- `revoked`:立即停止业务并进入重新登记/售后流程。
- `firmware_compatible=false`:进入升级提示,不要创建新通话;云端仍会在创建通话时二次阻断。
- 使用 `server_time` 和 `clock_skew_ms` 校正后续签名时间。
主要状态码:200、400、401、503。
### 6.2 获取设备配置
`GET /device/v2/config`
请求可携带上次响应的强 ETag:
```http
If-None-Match: "v3"
```
成功响应头:
```http
ETag: "v4"
```
成功响应示例:
```json
{
"request_id": "01J6CONFIG",
"data": {
"device_id": "10000000-0000-0000-0000-000000000511",
"revision": 4,
"desired_config": {
"volume": { "speaker": 60 },
"audio": { "sample_rate": 16000 },
"interaction": {},
"upload": {},
"rtc": {},
"policy_refs": {}
},
"reported_revision": 3,
"binding": {
"bound": true,
"household_id": "20000000-0000-0000-0000-000000000001",
"subject_id": "30000000-0000-0000-0000-000000000001",
"subject_name": "演示长者"
}
}
}
```
- 配置未变化返回 304,无响应正文。
- 设备只在完整校验新配置成功后切换本地生效版本。
- 应保留上一次有效配置;503 时不能清空本地配置。
- 应通过心跳或遥测上报实际应用的 `reported_config_revision`。
主要状态码:200、304、401、503。
## 7. 双向语音通话接口
### 7.1 创建通话并分配 RTC 房间
`POST /device/v2/conversations`
必须携带 `Idempotency-Key`。本接口只分配房间和短期凭证,不调用豆包 `StartVoiceChat`。
请求字段:
| 字段 | 必填 | 类型/限制 | 说明 |
|---|---|---|---|
| `device_time` | 是 | RFC3339 | 设备时间 |
| `monotonic_ms` | 否 | int64,≥0 | 开机后单调时钟毫秒数 |
| `capabilities.rtc` | 是 | `true` | 支持 RTC |
| `capabilities.audio_tracks` | 是 | `near`/`far` 数组 | 支持的音频轨道,至少一项且唯一 |
| `capabilities.upload_resume` | 是 | boolean | 是否支持断点/恢复上传 |
| `capabilities.rtc_credential_renewal` | 否 | boolean | 明确为 true 才允许心跳续签 RTC 凭证 |
| `network.type` | 是 | string,1~40 | 网络类型,如 wifi/ethernet/4g |
| `network.rssi` | 否 | integer | 信号强度 |
| `requested_goal` | 是 | 固定 `open_conversation` | 设备技术目标,不作为豆包提示词 |
请求示例:
```json
{
"device_time": "2026-08-27T06:31:00Z",
"monotonic_ms": 15820,
"capabilities": {
"rtc": true,
"audio_tracks": ["near", "far"],
"upload_resume": true,
"rtc_credential_renewal": true
},
"network": {
"type": "wifi",
"rssi": -48
},
"requested_goal": "open_conversation"
}
```
201 响应示例:
```json
{
"request_id": "01J6CREATE",
"data": {
"conversation_id": "0829059e-4485-46b9-87ea-5a2fcefc6c35",
"status": "ROOM_ISSUED",
"rtc": {
"app_id": "<rtc-app-id>",
"room_id": "r_25a5f0e5825645d099540604e17ee15c",
"user_id": "d_10000000000000000000000000000511",
"agent_user_id": "a_0829059e448546b987ea5a2fcefc6c35",
"token": "<short-lived-rtc-token>",
"expires_at": "2026-08-27T06:41:00Z"
},
"privacy": {
"retention_mode": "transcript_only",
"record_near": false,
"record_far": false,
"upload_allowed": false
},
"session": {
"engine_mode": "s2s_hybrid",
"heartbeat_interval_sec": 10,
"allocation_expires_at": "2026-08-27T06:33:00Z"
}
}
}
```
设备处理:
1. Token 只保存在内存中,不写日志和磁盘。
2. 在 `allocation_expires_at` 前使用 `rtc.room_id + rtc.user_id + rtc.token` 加入 RTC。
3. 发布麦克风近端音频,并订阅 `agent_user_id` 的智能体音频。
4. 成功入房后调用激活接口。
5. `session.engine_mode` 仅供云端诊断,终端不得据此切换采集、编码、发布或播放逻辑。
主要状态码:
| HTTP | 错误码/情况 | 设备动作 |
|---:|---|---|
| 201 | 创建成功 | 加入 RTC 后激活 |
| 403 | `CONSENT_REQUIRED` | 停止并等待授权 |
| 409 | `DEVICE_NOT_BOUND` | 提示先绑定设备 |
| 409 | `CONVERSATION_ALREADY_ACTIVE` | 查询或恢复已有通话 |
| 409 | `IDEMPOTENCY_KEY_REUSED` | 更正调用逻辑,不能换正文重用同键 |
| 422 | `AGENT_CONTEXT_PREPARATION_FAILED` | 不入房,提示背景资料准备失败 |
| 426 | `FIRMWARE_UPDATE_REQUIRED` | 进入固件升级流程 |
| 429 | `USAGE_QUOTA_EXCEEDED` | 停止自动重试并提示额度/并发限制 |
| 503 | `RUNTIME_CONFIG_UNAVAILABLE` 等 | 按 `retryable` 退避重试 |
### 7.2 确认入房并启动智能体
`POST /device/v2/conversations/{id}/activate`
必须携带 `Idempotency-Key`。只有 RTC 加入成功后调用。
请求字段:
| 字段 | 必填 | 限制 | 说明 |
|---|---|---|---|
| `joined_at` | 是 | RFC3339 | 实际入房时间 |
| `room_id` | 是 | 1~200 | 必须等于创建响应的房间号 |
| `rtc_user_id` | 是 | 1~200 | 必须等于创建响应的设备 RTC 用户号 |
| `rtc_join_result` | 是 | 固定 `success` | 只接受成功入房确认 |
| `config_revision` | 是 | int64,≥1 | 本通话使用的配置版本 |
| `connection_reference` | 否 | ≤255 | 设备侧连接关联号,不能包含凭证 |
请求示例:
```json
{
"joined_at": "2026-08-27T06:31:03Z",
"room_id": "r_25a5f0e5825645d099540604e17ee15c",
"rtc_user_id": "d_10000000000000000000000000000511",
"rtc_join_result": "success",
"config_revision": 4,
"connection_reference": "rtc-session-00001"
}
```
202 响应示例:
```json
{
"request_id": "01J6ACTIVATE",
"data": {
"conversation_id": "0829059e-4485-46b9-87ea-5a2fcefc6c35",
"status": "STARTING_AGENT",
"accepted_at": "2026-08-27T06:31:04Z"
}
}
```
响应表示云端已经受理,不表示豆包一定已经进房。随后必须调用通话状态查询接口,不能不断更换幂等键重复激活。
主要状态码:202、400、401、404、409、503。
### 7.3 查询权威通话状态
`GET /device/v2/conversations/{id}`
200 响应示例:
```json
{
"request_id": "01J6STATE",
"data": {
"conversation_id": "0829059e-4485-46b9-87ea-5a2fcefc6c35",
"status": "ACTIVE",
"action": "continue"
}
}
```
字段说明:
| 字段 | 取值 | 设备动作 |
|---|---|---|
| `status` | `PREPARING`、`ROOM_ISSUED`、`STARTING_AGENT`、`ACTIVE`、`STOPPING`、`COMPLETED`、`FAILED`、`EXPIRED` | 云端权威状态 |
| `action` | `wait` | 保持 RTC 房间并有限次数继续查询 |
| `action` | `continue` | 豆包已激活,进入正常双向语音和心跳 |
| `action` | `leave_room` | 立即退出 RTC 房间并结束本地通话流程 |
| `failure_code` | 可选稳定错误码 | 仅用于展示和诊断,不显示供应商原始错误 |
失败示例:
```json
{
"request_id": "01J6STATEFAILED",
"data": {
"conversation_id": "0829059e-4485-46b9-87ea-5a2fcefc6c35",
"status": "FAILED",
"action": "leave_room",
"failure_code": "provider_start_rejected"
}
}
```
推荐轮询:首次等待 500 ms,之后每 1 秒查询,累计不超过房间预留有效期;达到上限仍未收敛时按本地超时处理并调用停止接口。
主要状态码:200、400、401、404、503。
### 7.4 通话心跳
`POST /device/v2/conversations/{id}/heartbeat`
进入 `STARTING_AGENT` 或 `ACTIVE` 后,按创建响应的 `heartbeat_interval_sec` 调用。
请求主要字段:
| 字段 | 必填 | 说明 |
|---|---|---|
| `sequence_no` | 是 | 从 1 开始严格递增;进程重启后应从耐久状态恢复 |
| `device_time` | 是 | RFC3339 UTC |
| `monotonic_ms` | 是 | 单调时钟毫秒数 |
| `conversation_status` | 是 | 设备当前观察的通话状态 |
| `rtc.joined/uplink/downlink` | 是 | RTC 入房、上行和下行状态 |
| `network.type` | 是 | 当前网络类型 |
| `network.rssi` | 否 | 信号强度 |
| `firmware_version` | 是 | 当前固件版本 |
| `reported_config_revision` | 是 | 已实际应用的配置版本 |
| `recording_queue.pending_segments` | 是 | 待上传录音数 |
| `recording_queue.failed_segments` | 是 | 上传失败录音数 |
| `recording_queue.last_failure_code/at` | 否 | 最近稳定失败码和时间 |
| `rtc_credential_expires_at` | 否 | 当前 RTC Token 到期时间;声明续签能力时使用 |
| `last_error` | 否 | ≤80 的脱敏稳定错误码,不得传自由文本堆栈 |
| `command_results` | 是 | 最多 20 项;当前可为空数组 |
请求示例:
```json
{
"sequence_no": 1,
"device_time": "2026-08-27T06:31:15Z",
"monotonic_ms": 30820,
"conversation_status": "ACTIVE",
"rtc": {
"joined": true,
"uplink": true,
"downlink": true
},
"network": {
"type": "wifi",
"rssi": -50
},
"firmware_version": "1.2.0",
"reported_config_revision": 4,
"recording_queue": {
"pending_segments": 0,
"failed_segments": 0
},
"rtc_credential_expires_at": "2026-08-27T06:41:00Z",
"command_results": []
}
```
200 响应示例:
```json
{
"request_id": "01J6HEARTBEAT",
"data": {
"lease_expires_at": "2026-08-27T06:32:05Z",
"actions": [
{ "type": "continue" }
],
"desired_config_revision": 4,
"commands": []
}
}
```
`actions[].type`:
- `continue`:继续通话;
- `stop`:尽快停止并离房;
- `stop_after_turn`:当前话轮完成后停止;
- `renew_rtc_credential`:响应同一动作的 `rtc` 字段包含新 RTCGrant,设备先替换内存凭证,再在下一次心跳报告新到期时间。
同序号或更小序号不会续租,也不会覆盖较新的设备状态。报告配置版本高于云端期望版本会返回 409。
主要状态码:200、400、401、404、409、503。
### 7.5 停止通话
`POST /device/v2/conversations/{id}/stop`
必须携带 `Idempotency-Key`。
请求字段:
| 字段 | 必填 | 说明 |
|---|---|---|
| `reason` | 是 | 1~64 字符稳定原因码,字符集 `[A-Za-z0-9_.:-]` |
| `device_time` | 是 | RFC3339 |
| `last_heartbeat_sequence` | 是 | 最后成功心跳序号,可为 0 |
| `rtc_left` | 是 | 请求时是否已经退出 RTC |
| `recording.near_segments` | 是 | 本地近端录音分片数量 |
| `recording.far_segments` | 是 | 本地远端录音分片数量 |
请求示例:
```json
{
"reason": "user_hangup",
"device_time": "2026-08-27T06:35:00Z",
"last_heartbeat_sequence": 23,
"rtc_left": true,
"recording": {
"near_segments": 0,
"far_segments": 0
}
}
```
202 响应示例:
```json
{
"request_id": "01J6STOP",
"data": {
"conversation_id": "0829059e-4485-46b9-87ea-5a2fcefc6c35",
"status": "STOPPING",
"accepted_at": "2026-08-27T06:35:01Z"
}
}
```
允许的响应状态为 `STOPPING`、`COMPLETED`、`FAILED` 或 `EXPIRED`。相同停止请求不会重复调用供应商 Stop。
主要状态码:202、400、401、404、409、503。
## 8. 设备遥测接口
### 8.1 批量上报遥测
`POST /device/v2/telemetry:batch`
必须携带 `Idempotency-Key`。每批 1~500 条,同一批内 `event_key` 和 `sequence_no` 分别唯一。
事件公共字段:
| 字段 | 必填 | 说明 |
|---|---|---|
| `event_key` | 是 | 设备内稳定唯一键,1~255 字符 |
| `sequence_no` | 是 | 设备内严格正整数单调序号 |
| `occurred_at` | 是 | 事件发生时间 |
| `event_type` | 是 | `status`、`error`、`command_result` |
| `conversation_id` | 否 | 一旦填写,必须属于当前签名设备 |
| `status/error/command_result` | 条件必填 | 只允许携带与 `event_type` 同名的一个对象 |
状态事件:
```json
{
"event_key": "status.boot.000001",
"sequence_no": 1,
"occurred_at": "2026-08-27T06:30:00Z",
"event_type": "status",
"status": {
"firmware_version": "1.2.0",
"reported_config_revision": 4,
"network": { "type": "wifi", "rssi": -48 },
"rtc": { "joined": false, "uplink": false, "downlink": false },
"recording_queue": { "pending_segments": 0, "failed_segments": 0 }
}
}
```
错误事件:
```json
{
"event_key": "error.audio.000002",
"sequence_no": 2,
"occurred_at": "2026-08-27T06:30:10Z",
"event_type": "error",
"error": {
"code": "AUDIO_DEVICE_UNAVAILABLE",
"severity": "error"
}
}
```
命令结果事件:
```json
{
"event_key": "command.9e70.result",
"sequence_no": 3,
"occurred_at": "2026-08-27T06:30:20Z",
"event_type": "command_result",
"command_result": {
"command_id": "9e7006eb-6cd2-4e60-82ba-e24ba45158bd",
"status": "SUCCEEDED",
"result_summary": {
"result_code": "OK"
}
}
}
```
完整请求:
```json
{
"events": [
{
"event_key": "status.boot.000001",
"sequence_no": 1,
"occurred_at": "2026-08-27T06:30:00Z",
"event_type": "status",
"status": {
"firmware_version": "1.2.0",
"reported_config_revision": 4,
"network": { "type": "wifi", "rssi": -48 },
"rtc": { "joined": false, "uplink": false, "downlink": false },
"recording_queue": { "pending_segments": 0, "failed_segments": 0 }
}
}
]
}
```
200 响应:
```json
{
"request_id": "01J6TELEMETRY",
"data": {
"accepted_count": 1,
"duplicate_count": 0,
"desired_config_revision": 4,
"actions": [
{ "type": "pull_commands" }
]
}
}
```
响应动作包括 `refresh_config`、`pull_commands` 和 `stop_conversation`。结果对象递归禁止出现 `secret`、`token`、`password`、`credential`、`private_key`、`api_key` 等敏感字段。
主要状态码:200、400、401、404、409、503。
## 9. 云端设备命令接口
### 9.1 拉取命令
`GET /device/v2/commands?limit=10`
`limit` 可选,范围 1~20,默认 10。只返回当前设备未过期、未终结的命令。
响应示例:
```json
{
"request_id": "01J6COMMANDS",
"data": {
"items": [
{
"id": "9e7006eb-6cd2-4e60-82ba-e24ba45158bd",
"type": "diagnostics",
"payload": {
"scope": ["audio", "network", "storage"]
},
"status": "DELIVERED",
"expires_at": "2026-08-27T06:40:00Z",
"delivery_attempts": 1,
"created_at": "2026-08-27T06:35:00Z"
}
]
}
}
```
命令类型:
| 类型 | 用途 |
|---|---|
| `apply_config` | 拉取并应用指定配置 |
| `reboot` | 受控重启设备 |
| `diagnostics` | 执行音频、网络、存储等有限诊断 |
| `firmware_update` | 下载、验签、安装并确认固件 |
命令主状态机:
```text
QUEUED → DELIVERED → ACKNOWLEDGED → SUCCEEDED / FAILED
└────────→ EXPIRED / CANCELLED
```
设备进程重启后可以重新拉取 `ACKNOWLEDGED` 但未终结的命令,用于恢复执行。
主要状态码:200、401、503。
### 9.2 命令执行回执
`POST /device/v2/commands/{id}/ack`
请求:
```json
{
"status": "ACKNOWLEDGED",
"result_summary": {
"phase": "running",
"progress_percent": 10
}
}
```
`status` 只允许 `ACKNOWLEDGED`、`SUCCEEDED`、`FAILED`。同阶段或相同终态重复提交保持幂等,禁止状态回退。
固件命令可用的阶段包括:
- `downloading`
- `installing`
- `staging`
- `reboot_pending`
- `boot_verifying`
- `confirmed`
- `rolled_back`
成功结果应包含 `installed_version`;A/B 启动健康失败并自动回退时应提交:
```json
{
"status": "FAILED",
"result_summary": {
"phase": "rolled_back",
"progress_percent": 95,
"result_code": "BOOT_HEALTH_FAILED_ROLLED_BACK",
"rollback_applied": true,
"active_slot": "A",
"failed_slot": "B",
"rollback_version": "1.1.0"
}
}
```
主要状态码:200、400、401、409。
## 10. 固件下载接口
### 10.1 获取私有固件地址
`GET /device/v2/firmware-releases/{id}/download`
只有属于当前灰度批次且版本仍可用的设备能够取得地址。响应签发约 5 分钟有效的私有下载地址,设备必须:
1. 只接受云端固定固件下载接口返回的地址;
2. 限制下载对象大小为 1 字节~256 MiB;
3. 下载后校验命令清单中的小写 SHA-256;
4. 使用安全区预置的 Ed25519 公钥验证 `firmware-manifest-v1`;
5. 校验型号、硬件版本、制品大小、版本和发布编号;
6. 全部校验通过后才允许写入非活动分区或执行安装。
不要信任命令 `payload` 临时携带的公钥,不要把下载地址写入持久日志。
主要状态码:200、401、403。
## 11. 通话终稿补传接口
### 11.1 批量补传最终话轮
`POST /device/v2/conversations/{id}/turns:batch`
必须携带 `Idempotency-Key`。用于供应商最终转写缺失或不完整时补交设备侧终稿,每批 1~500 条。
话轮字段:
| 字段 | 必填 | 限制 |
|---|---|---|
| `turn_key` | 是 | 1~255,通话内稳定唯一 |
| `sequence_no` | 是 | 正整数,批内唯一 |
| `speaker` | 是 | `subject` 或 `agent` |
| `start_at` | 是 | RFC3339 |
| `end_at` | 是 | RFC3339,不早于 `start_at` |
| `final_text` | 是 | 1~20000 字符 |
| `interrupted` | 是 | 是否被打断 |
请求示例:
```json
{
"turns": [
{
"turn_key": "device-turn-0001",
"sequence_no": 1,
"speaker": "subject",
"start_at": "2026-08-27T06:32:00Z",
"end_at": "2026-08-27T06:32:03Z",
"final_text": "今天天气怎么样?",
"interrupted": false
}
]
}
```
响应:
```json
{
"request_id": "01J6TURNS",
"data": {
"conversation_id": "0829059e-4485-46b9-87ea-5a2fcefc6c35",
"accepted_count": 1,
"duplicate_count": 0
}
}
```
相同终稿计入重复数;相同 `turn_key` 或 `sequence_no` 对应不同内容时整批返回 409 `TRANSCRIPT_TURN_CONFLICT`,设备不得覆盖云端内容。
主要状态码:200、400、401、403、404、409、503。
## 12. 双轨录音上传接口
录音上传只在创建响应满足以下条件时允许:
```text
privacy.retention_mode = transcript_and_audio
privacy.upload_allowed = true
对应 track 的 record_near / record_far = true
```
当前编码基线只接受可解析的 WAV PCM 16 位:
- `codec=wav_pcm_s16le`
- `content_type=audio/wav`
- 单片时长 1~600000 ms
- 单片大小 1~67108864 字节
- 每批最多 1000 片
### 12.1 声明上传批次
`POST /device/v2/conversations/{id}/upload-batches`
必须携带 `Idempotency-Key`。
请求:
```json
{
"segments": [
{
"track": "near",
"sequence_no": 0,
"codec": "wav_pcm_s16le",
"content_type": "audio/wav",
"duration_ms": 10000,
"expected_size": 320044,
"expected_digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
]
}
```
201 响应的 `data` 为上传批次:
| 字段 | 说明 |
|---|---|
| `id` | 批次 UUID |
| `conversation_id` | 所属通话 |
| `status` | `PENDING/UPLOADING/VERIFYING/COMPLETED/FAILED/EXPIRED` |
| `segment_count` | 分片数 |
| `expected_total_size` | 声明总字节数 |
| `verified_count/rejected_count` | 校验成功/拒绝数量 |
| `verification_attempts` | 校验尝试次数 |
| `last_error_code` | 批次稳定失败码 |
| `expires_at` | 批次到期时间 |
| `segments` | 分片结果数组 |
新分片包含短期 `upload_url` 和 `upload_until`。设备使用 HTTP PUT 将该分片原始字节直接上传,不得把多个分片或其他内容写入同一 URL。
主要状态码:201、400、401、403、409。
### 12.2 声明批次上传完成
`POST /device/v2/upload-batches/{id}/complete`
请求无 JSON 正文,但仍需发送空正文 SHA-256。平台返回 202,并把批次置为 `VERIFYING` 或返回已经完成的结果。重复提交不会重复创建校验任务。
主要状态码:202、401、404、409、410。
### 12.3 查询批次校验结果
`GET /device/v2/upload-batches/{id}`
分片状态:
```text
DECLARED → AUTHORIZED → UPLOADED → VERIFYING → VERIFIED / REJECTED
```
关键字段:
| 字段 | 说明 |
|---|---|
| `id` | 分片 UUID |
| `track` | `near` 或 `far` |
| `sequence_no` | 轨道内序号 |
| `expected_size/digest` | 声明大小和 SHA-256 |
| `status` | 当前分片状态 |
| `last_error_code` | 校验失败稳定码 |
| `verified_at` | 校验成功时间 |
| `local_delete_after` | 只有 VERIFIED 才返回;此时间之后才能删除本地密文 |
| `local_delete_status` | 最近一次本地删除回执 `DELETED/FAILED` |
建议每 1~2 秒轮询,达到批次 `expires_at` 或本地等待上限后停止。对象暂时不可见时平台可能保持 `VERIFYING` 并后台重试。
主要状态码:200、401、404。
### 12.4 回报本地密文删除结果
`POST /device/v2/media-segments/{id}/local-delete-ack`
只能在 `local_delete_after` 之后删除设备本地加密录音副本并回报。
成功:
```json
{
"status": "DELETED"
}
```
失败:
```json
{
"status": "FAILED",
"error_code": "LOCAL_STORAGE_DELETE_FAILED"
}
```
- `FAILED` 时 `error_code` 必填;`DELETED` 时禁止携带。
- 过早提交成功回执返回 409。
- `DELETED` 重复回报幂等。
- 此回执只代表设备副本已删除,不删除云端已验证对象。
主要状态码:200、400、401、404、409。
## 13. 业务流程
### 13.1 设备开机与配置同步
```mermaid
sequenceDiagram
participant D as 设备
participant A as 云端API
D->>A: GET /bootstrap + HMAC
A-->>D: server_time、状态、绑定、版本门禁
alt 固件不兼容
D->>D: 进入升级流程,禁止新通话
else 固件兼容
D->>A: GET /config + If-None-Match
A-->>D: 200 新配置或 304 未变化
D->>D: 校验并原子应用配置
D->>A: POST /telemetry:batch 上报实际版本
end
```
### 13.2 双向语音通话
```mermaid
sequenceDiagram
participant D as 设备
participant A as 云端API
participant R as 火山RTC
participant W as Worker
participant B as 豆包智能体
D->>A: POST /conversations
A-->>D: conversation_id + room/token
D->>R: 加入房间并发布麦克风
D->>A: POST /conversations/{id}/activate
A-->>D: 202 STARTING_AGENT
A->>W: agent_start_requested
W->>B: StartVoiceChat
B->>R: 智能体加入同一房间
loop 有限轮询
D->>A: GET /conversations/{id}
A-->>D: wait / continue / leave_room
end
loop 通话中
D->>A: POST /heartbeat
A-->>D: continue/stop/renew + config/commands
D->>R: 上行麦克风音频
R-->>D: 下行豆包语音
end
D->>A: POST /stop
A->>W: agent_stop_requested
W->>B: StopVoiceChat
D->>R: 退出房间
```
### 13.3 设备命令
```mermaid
sequenceDiagram
participant D as 设备
participant A as 云端API
D->>A: GET /commands
A-->>D: 待执行命令
D->>A: POST /commands/{id}/ack ACKNOWLEDGED
D->>D: 执行命令并保存本地状态
D->>A: POST /commands/{id}/ack SUCCEEDED/FAILED
A-->>D: 最终命令状态
```
### 13.4 录音上传
```mermaid
sequenceDiagram
participant D as 设备
participant A as 云端API
participant O as 对象存储
participant W as Worker
D->>A: POST /conversations/{id}/upload-batches
A-->>D: 分片ID + 短期PUT地址
D->>O: PUT 分片字节
D->>A: POST /upload-batches/{id}/complete
A->>W: media.verify_requested
W->>O: 校验大小、摘要、WAV与时长
loop 查询结果
D->>A: GET /upload-batches/{id}
A-->>D: VERIFYING/VERIFIED/REJECTED
end
D->>D: 到 local_delete_after 后删除本地密文
D->>A: POST /media-segments/{id}/local-delete-ack
```
## 14. 错误码与重试策略
| HTTP | 错误码 | 是否自动重试 | 处理建议 |
|---:|---|---|---|
| 400 | `VALIDATION_FAILED` | 否 | 修正字段,重新生成随机数和签名 |
| 401 | `DEVICE_AUTHENTICATION_FAILED` | 否 | 检查设备登记、时钟、摘要、签名或重放 |
| 403 | `CONSENT_REQUIRED` | 否 | 等待用户有效授权 |
| 403 | `TRANSCRIPT_STORAGE_NOT_ALLOWED` | 否 | 停止补传,不能绕过留存策略 |
| 404 | `TELEMETRY_CONVERSATION_NOT_FOUND` | 否 | 删除错误通话引用 |
| 409 | `DEVICE_NOT_BOUND` | 否 | 完成设备绑定 |
| 409 | `CONVERSATION_ALREADY_ACTIVE` | 否 | 恢复或查询已有通话 |
| 409 | `IDEMPOTENCY_KEY_REUSED` | 否 | 不同正文必须使用新业务键 |
| 409 | `ROOM_ALLOCATION_EXPIRED` | 否 | 重新创建通话和房间 |
| 409 | `RTC_ALLOCATION_MISMATCH` | 否 | 丢弃本地房间资料并重新创建 |
| 409 | `CONFIG_VERSION_CONFLICT` | 否 | 重新获取设备配置 |
| 409 | `CONVERSATION_STATE_CONFLICT` | 否 | 查询权威状态,禁止强制覆盖 |
| 409 | `TRANSCRIPT_TURN_CONFLICT` | 否 | 保存冲突明细,转人工对账 |
| 409 | `TELEMETRY_EVENT_CONFLICT` | 否 | 停止推进确认点,检查事件键和序号 |
| 409 | `DEVICE_COMMAND_STATE_CONFLICT` | 否 | 重新拉取命令,禁止状态回退 |
| 422 | `AGENT_CONTEXT_PREPARATION_FAILED` | 否 | 等待修正长者背景资料后新建通话 |
| 426 | `FIRMWARE_UPDATE_REQUIRED` | 否 | 执行固件升级 |
| 429 | `USAGE_QUOTA_EXCEEDED` | 延迟/人工 | 提示额度或并发限制 |
| 503 | `DEVICE_AUTHENTICATION_UNAVAILABLE` | 是 | 指数退避,使用新随机数重试 |
| 503 | `DEVICE_CONFIG_UNAVAILABLE` | 是 | 保留旧配置并退避重试 |
| 503 | `RUNTIME_CONFIG_UNAVAILABLE` | 有条件 | 停止创建通话,等待平台恢复路由 |
| 503 | `TRANSCRIPT_BATCH_UNAVAILABLE` | 是 | 原正文、原幂等键,新 HMAC 头重试 |
| 503 | `DEVICE_TELEMETRY_UNAVAILABLE` | 是 | 保留事件队列,原正文和幂等键重试 |
| 503 | `CONVERSATION_HEARTBEAT_UNAVAILABLE` | 是 | 同序号、新 HMAC 头退避重试 |
| 503 | `CONVERSATION_STOP_UNAVAILABLE` | 是 | 原正文和幂等键、新 HMAC 头重试 |
建议重试参数:
- 初始等待 500 ms;
- 指数退避 0.5、1、2、4、8、15、30 秒;
- 加入 0~20% 随机抖动;
- 单次业务的最大重试时间受 RTC 房间、上传批次或命令过期时间约束;
- `retryable=false` 时不得自动循环重试。
## 15. 与设备相关但不由终端直接调用的管理接口
下列接口由 SaaS 管理后台或运营后台调用,使用 OIDC 用户身份和权限控制,不使用设备 HMAC:
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | `/api/v1/device-claims` | 家庭成员使用设备屏幕显示的绑定码绑定设备 |
| GET | `/api/v1/devices` | 查询当前家庭可管理设备 |
| GET | `/api/v1/devices/{id}` | 查询设备详情 |
| GET | `/api/v1/devices/{id}/presence` | 查询设备在线和当前通话状态 |
| GET | `/api/v1/devices/{id}/twin` | 查询设备期望/实际配置影子 |
| PATCH | `/api/v1/devices/{id}/twin` | 修改期望配置,需 `If-Match` |
| GET | `/api/v1/devices/{id}/commands` | 查询设备命令记录 |
| POST | `/api/v1/devices/{id}/commands` | 创建配置、重启、诊断或固件命令 |
| GET/POST | `/api/v1/households/{id}/firmware-signing-keys` | 管理固件 Ed25519 信任根 |
| POST | `/api/v1/households/{id}/firmware-signing-keys/revoke` | 吊销签名公钥 |
| GET/POST | `/api/v1/households/{id}/firmware-releases` | 查询或创建固件版本 |
| POST | `/api/v1/firmware-releases/{id}/upload-session` | 创建固件上传会话 |
| POST | `/api/v1/firmware-releases/{id}/finalize` | 完成固件制品校验与发布 |
| GET | `/api/v1/firmware-releases/{id}/impact` | 查询版本、设备和命令影响 |
| POST | `/api/v1/firmware-releases/{id}/pause` | 暂停版本 |
| POST | `/api/v1/firmware-releases/{id}/resume` | 恢复版本 |
| POST | `/api/v1/firmware-releases/{id}/retire` | 退役版本 |
| GET/POST | `/api/v1/households/{id}/firmware-rollouts` | 查询或创建灰度批次 |
| GET | `/api/v1/firmware-rollouts/{id}` | 查询灰度批次状态 |
| POST | `/api/v1/firmware-rollouts/{id}/pause` | 暂停灰度 |
| POST | `/api/v1/firmware-rollouts/{id}/rollback` | 回滚到已验证版本 |
## 16. 设备实现检查清单
### 16.1 身份与安全
- [ ] 每台设备使用独立密钥,密钥位于安全存储区。
- [ ] 每次请求生成新 Nonce,网络重试也不复用。
- [ ] 对实际发送的原始正文字节计算 SHA-256。
- [ ] 签名 URI 与实际请求路径、查询串完全一致。
- [ ] 不记录设备密钥、HMAC 签名、RTC Token、上传地址和下载地址。
- [ ] 生产环境强制 HTTPS 并校验证书。
### 16.2 离线与幂等
- [ ] 关键请求正文和幂等键在成功确认前持久化。
- [ ] 重试保留原正文与 Idempotency-Key,但更新 HMAC 请求头。
- [ ] 遥测事件键和序号跨进程重启保持单调或稳定唯一。
- [ ] 命令 `ACKNOWLEDGED` 后保存本地执行进度,重启可恢复。
- [ ] 保留最近一次有效配置,配置拉取失败不清空。
### 16.3 实时语音
- [ ] 创建通话后先加入 RTC,再调用 activate。
- [ ] 识别 `wait/continue/leave_room`,失败时及时离房。
- [ ] 按服务器心跳间隔发送递增序号。
- [ ] 支持续签时只在内存替换 RTC Token。
- [ ] 收到 `stop` 或终态后停止采集、退出 RTC 并调用停止接口。
- [ ] 不根据 `engine_mode` 改变设备媒体行为。
### 16.4 录音与隐私
- [ ] 只有云端明确允许时才录制和上传对应轨道。
- [ ] 上传前计算真实字节大小和 SHA-256。
- [ ] 每个短期 URL 只 PUT 对应单一对象。
- [ ] 只有校验为 VERIFIED 且到达 `local_delete_after` 后才删除本地密文。
- [ ] 删除失败上报稳定错误码,不上传包含个人信息的自由文本堆栈。
## 17. 契约与联调依据
- 权威机器可读契约:`openapi/openapi.yaml`
- 设备接入实现说明:`docs/device-integration.md`
- 设备命令可靠下发:`docs/device-commands.md`
- 录音上传和访问:`docs/media-upload-and-access.md`
- 设备 API 兼容策略:`docs/device-api-compatibility.md`
- A/B 固件协议:`docs/device-firmware-ab-protocol.md`
若本文档与当前 OpenAPI 发生字段差异,应以同一构建版本中的 `openapi/openapi.yaml` 为准,并同步修订本文档;不得根据固件版本、设备型号或 User-Agent 猜测接口字段。