38 KiB
数据采集终端—云端系统设备接口文档
文档版本: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. 总体通信关系
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
重要边界:
- 设备的控制请求经过本项目 API;实时麦克风音频不经过本项目 API 和数据库。
- 设备和豆包智能体分别直接加入同一个火山 RTC 房间。
- 录音文件使用短期单对象地址直接上传对象存储,不经过业务 API 传输文件字节。
- 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、固件下载重定向等特殊响应外,成功响应统一为:
{
"request_id": "01J6...",
"data": {}
}
request_id 是本次云端请求关联编号,设备日志应保存该值,但不得把密钥、签名、Token 或临时对象地址写入日志。
4.3 统一错误响应
{
"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 或空正文请求也必须计算空字节摘要:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
5.2 签名原文
使用单个换行符按以下顺序拼接,最后一行后不增加换行:
UPPERCASE_HTTP_METHOD
REQUEST_URI_WITH_EXACT_QUERY_STRING
X-Device-Timestamp原值
X-Device-Nonce原值
X-Device-Content-SHA256原值
例如:
GET
/device/v2/bootstrap?firmware_version=1.2.0&device_time=2026-08-27T06%3A30%3A00Z
2026-08-27T06:30:01Z
62d4b688f77b4baaa87a38d315c2f3b1
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
计算方法:
signature = Base64URL_NoPadding(
HMAC_SHA256(device_secret, canonical_string)
)
注意事项:
- 查询参数的顺序、百分号编码和大小写必须与实际请求 URI 完全一致。
Nonce不是业务幂等键。业务请求重试时保留原Idempotency-Key和正文,但必须生成新Nonce、时间戳和签名。- 不允许在设备镜像、日志或前端页面中明文输出设备密钥。
- 云端 Redis 或设备身份数据库不可用时鉴权关闭失败并返回 503,不会绕过签名。
5.3 幂等键
需要幂等保护的接口必须包含:
Idempotency-Key: 3ca4e514-cbde-41fc-aa32-6f1ed460cb01
长度为 16~128 字符。规则如下:
- 同一设备、同一接口、同一幂等键和相同正文:返回第一次的业务结果。
- 同一幂等键对应不同正文:返回 409
IDEMPOTENCY_KEY_REUSED。 - 网络超时且无法确认结果时:使用原幂等键、原正文重试,但重新生成 HMAC 请求头。
- 返回已有结果时,响应可能包含
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 | 网络类型或描述 |
成功响应示例:
{
"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:
If-None-Match: "v3"
成功响应头:
ETag: "v4"
成功响应示例:
{
"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 |
设备技术目标,不作为豆包提示词 |
请求示例:
{
"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 响应示例:
{
"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"
}
}
}
设备处理:
- Token 只保存在内存中,不写日志和磁盘。
- 在
allocation_expires_at前使用rtc.room_id + rtc.user_id + rtc.token加入 RTC。 - 发布麦克风近端音频,并订阅
agent_user_id的智能体音频。 - 成功入房后调用激活接口。
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 | 设备侧连接关联号,不能包含凭证 |
请求示例:
{
"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 响应示例:
{
"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 响应示例:
{
"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 |
可选稳定错误码 | 仅用于展示和诊断,不显示供应商原始错误 |
失败示例:
{
"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 项;当前可为空数组 |
请求示例:
{
"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 响应示例:
{
"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 |
是 | 本地远端录音分片数量 |
请求示例:
{
"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 响应示例:
{
"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 同名的一个对象 |
状态事件:
{
"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 }
}
}
错误事件:
{
"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"
}
}
命令结果事件:
{
"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"
}
}
}
完整请求:
{
"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 响应:
{
"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。只返回当前设备未过期、未终结的命令。
响应示例:
{
"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 |
下载、验签、安装并确认固件 |
命令主状态机:
QUEUED → DELIVERED → ACKNOWLEDGED → SUCCEEDED / FAILED
└────────→ EXPIRED / CANCELLED
设备进程重启后可以重新拉取 ACKNOWLEDGED 但未终结的命令,用于恢复执行。
主要状态码:200、401、503。
9.2 命令执行回执
POST /device/v2/commands/{id}/ack
请求:
{
"status": "ACKNOWLEDGED",
"result_summary": {
"phase": "running",
"progress_percent": 10
}
}
status 只允许 ACKNOWLEDGED、SUCCEEDED、FAILED。同阶段或相同终态重复提交保持幂等,禁止状态回退。
固件命令可用的阶段包括:
downloadinginstallingstagingreboot_pendingboot_verifyingconfirmedrolled_back
成功结果应包含 installed_version;A/B 启动健康失败并自动回退时应提交:
{
"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 字节~256 MiB;
- 下载后校验命令清单中的小写 SHA-256;
- 使用安全区预置的 Ed25519 公钥验证
firmware-manifest-v1; - 校验型号、硬件版本、制品大小、版本和发布编号;
- 全部校验通过后才允许写入非活动分区或执行安装。
不要信任命令 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 |
是 | 是否被打断 |
请求示例:
{
"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
}
]
}
响应:
{
"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. 双轨录音上传接口
录音上传只在创建响应满足以下条件时允许:
privacy.retention_mode = transcript_and_audio
privacy.upload_allowed = true
对应 track 的 record_near / record_far = true
当前编码基线只接受可解析的 WAV PCM 16 位:
codec=wav_pcm_s16lecontent_type=audio/wav- 单片时长 1~600000 ms
- 单片大小 1~67108864 字节
- 每批最多 1000 片
12.1 声明上传批次
POST /device/v2/conversations/{id}/upload-batches
必须携带 Idempotency-Key。
请求:
{
"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}
分片状态:
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 之后删除设备本地加密录音副本并回报。
成功:
{
"status": "DELETED"
}
失败:
{
"status": "FAILED",
"error_code": "LOCAL_STORAGE_DELETE_FAILED"
}
FAILED时error_code必填;DELETED时禁止携带。- 过早提交成功回执返回 409。
DELETED重复回报幂等。- 此回执只代表设备副本已删除,不删除云端已验证对象。
主要状态码:200、400、401、404、409。
13. 业务流程
13.1 设备开机与配置同步
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 双向语音通话
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 设备命令
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 录音上传
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 猜测接口字段。