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

1224 lines
38 KiB
Markdown
Raw Permalink Normal View History

# 数据采集终端—云端系统设备接口文档
> 文档版本: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 猜测接口字段。