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