# 嵌入式设备与平台交互接口文档 > 文档范围:嵌入式语音终端与平台、RTC 和对象存储的接口及交互流程 > 设备示例:`10000000-0000-0000-0000-000000000512` > 设备接口版本:`/device/v2` > OpenAPI 契约版本:`0.70.0` > 本地平台地址:`http://localhost:58080` > 公网测试地址:`https://121.41.46.238` > 编制日期:2026-08-29 ## 1. 适用范围和重要前提 本文档面向嵌入式固件、BSP、音频和设备联调开发人员,说明设备从启动登记到完成一次语音通话并上传录音的完整过程,包括: - 主通话链路使用的 `/device/v2` 平台接口; - 每个接口的请求参数、响应参数和示例; - 设备 HMAC 签名、Nonce 和幂等键的生成规则; - 火山 RTC 和对象存储的非平台 API 交互; - 正常流程、轮询、重试和异常清理顺序。 仓库本地种子数据中的示例设备 `10000000-0000-0000-0000-000000000512` 当前是注册但未绑定设备: ```text status = registered credential_ref = local-dev://unbound-terminal-2 binding.bound = false ``` 因此,在未绑定状态下: - `GET /device/v2/bootstrap` 和 `GET /device/v2/config` 可以通过; - `POST /device/v2/conversations` 将返回 `409 DEVICE_NOT_BOUND`; - 后续成功通话示例是假设设备 `...0512` 已经绑定到有效家庭和服务对象,并取得 `voice_chat` 授权后的结果。 设备 ID 和设备密钥必须成对。使用设备 `...0511` 的密钥为 `...0512` 签名会返回 `401 DEVICE_AUTHENTICATION_FAILED`。 本文使用“必须”“应”“可以”区分协议要求、推荐实现和可选能力。Windows 模拟器仅作为已验证参考实现,不构成嵌入式设备的运行时约束。 ## 2. 主通话链路接口 | 阶段 | 顺序 | 方法与路径 | 调用方式 | |---|---:|---|---| | 环境检测 | 1 | `GET /device/v2/bootstrap` | 调用一次 | | 环境检测 | 2 | `GET /device/v2/config` | 调用一次 | | 创建通话 | 3 | `POST /device/v2/conversations` | 调用一次,幂等 | | 确认入房 | 4 | `POST /device/v2/conversations/{id}/activate` | RTC 入房成功并满足产品前置条件后调用,幂等 | | 等待智能体 | 5 | `GET /device/v2/conversations/{id}` | 首次约 500 ms,之后建议每 1 秒轮询,不超过房间预留期 | | 通话中 | 6 | `POST /device/v2/conversations/{id}/heartbeat` | 激活后立即一次,之后通常每 15 秒一次 | | 通话中 | 7 | `POST /device/v2/conversations/{id}/turns:batch` | 每次收到最终字幕时调用;失败后在心跳或结束前重试 | | 结束通话 | 8 | `POST /device/v2/conversations/{id}/stop` | 本地 RTC 离房后调用,幂等 | | 等待结束 | 9 | `GET /device/v2/conversations/{id}` | 有录音时建议每 1 秒轮询,并设置本地上限 | | 录音上传 | 10 | `POST /device/v2/conversations/{id}/upload-batches` | 有授权录音时调用,幂等 | | 录音上传 | 外部 | `PUT {upload_url}` | 录音字节直接上传对象存储 | | 录音上传 | 11 | `POST /device/v2/upload-batches/{batch_id}/complete` | 所有对象上传后调用 | | 录音校验 | 12 | `GET /device/v2/upload-batches/{batch_id}` | 建议每 1~2 秒轮询,直到终态、批次到期或本地上限 | | 本地清理 | 13 | `POST /device/v2/media-segments/{segment_id}/local-delete-ack` | 到 `local_delete_after` 后删除本地密文并回报 | 以下能力不在本文主通话流程中展开,嵌入式产品应按产品需求实现: - `GET /device/v2/commands` - `POST /device/v2/commands/{id}/ack` - `GET /device/v2/firmware-releases/{id}/download` - `POST /device/v2/telemetry:batch` ## 3. 总体时序 ```mermaid sequenceDiagram participant D as 嵌入式语音终端 participant A as 平台API participant R as 火山RTC participant W as Worker participant B as 豆包智能体 participant O as 对象存储 D->>A: GET /device/v2/bootstrap A-->>D: 设备、绑定、固件和配置状态 D->>A: GET /device/v2/config A-->>D: 期望配置和配置版本 D->>R: 可选的TLS 443连通性预检 D->>A: POST /device/v2/conversations A-->>D: conversation_id、RTC房间和短期Token D->>R: 加入房间、发布麦克风、启动字幕 D->>A: POST /device/v2/conversations/{id}/activate A->>W: agent_start_requested W->>B: StartVoiceChat B->>R: 智能体加入RTC房间 loop 首次约500ms,之后建议每1秒,不超过房间预留期 D->>A: GET /device/v2/conversations/{id} A-->>D: wait / continue / leave_room end loop 通话期间 D->>A: POST /device/v2/conversations/{id}/heartbeat A-->>D: actions、配置版本、命令 D->>A: POST /device/v2/conversations/{id}/turns:batch A-->>D: 最终字幕接收结果 D->>R: 上行麦克风音频 R-->>D: 下行豆包音频 end D->>R: 停字幕、离房并销毁RTC引擎 D->>A: POST /device/v2/conversations/{id}/stop A->>W: agent_stop_requested W->>B: StopVoiceChat opt 本次存在已授权录音 loop 等待通话终态 D->>A: GET /device/v2/conversations/{id} A-->>D: STOPPING / COMPLETED / FAILED end D->>A: POST /device/v2/conversations/{id}/upload-batches A-->>D: 批次、分片和短期PUT地址 D->>O: PUT near/far WAV分片 D->>A: POST /device/v2/upload-batches/{batch_id}/complete loop 等待校验 D->>A: GET /device/v2/upload-batches/{batch_id} A-->>D: VERIFYING / COMPLETED / FAILED end D->>D: 到local_delete_after后删除本地密文 D->>A: POST /device/v2/media-segments/{id}/local-delete-ack end ``` ## 4. 通用协议 ### 4.1 成功和错误包装 一般成功响应: ```json { "request_id": "01JTESTREQUEST", "data": {} } ``` 统一错误响应: ```json { "request_id": "01JTESTERROR", "error": { "code": "VALIDATION_FAILED", "message": "请求字段不符合契约", "retryable": false, "field_errors": [ { "field": "body.device_time", "message": "必须是RFC3339时间" } ], "details": {} } } ``` 设备应该使用 `error.code` 和 `retryable` 决定动作,不应匹配中文 `message`。 ### 4.2 全部设备接口的 HMAC 请求头 每一个 `/device/v2` 请求都必须携带: | 请求头 | 示例 | 生成方式 | |---|---|---| | `X-Device-Id` | `10000000-0000-0000-0000-000000000512` | 平台为设备分配的公开 UUID | | `X-Device-Timestamp` | `2026-08-29T08:30:00.123Z` | 每次请求生成当前 UTC RFC3339 时间 | | `X-Device-Nonce` | `5d4c79fa3f014e9cacb68eeb481be2c8` | 每次 HTTP 尝试生成新的 16~128 字符随机数 | | `X-Device-Content-SHA256` | 64 位小写十六进制 | 对实际发送的原始正文 UTF-8 字节计算 SHA-256 | | `X-Device-Signature` | Base64URL,无 `=` | 使用设备密钥计算 HMAC-SHA256 | 设备密钥不发送给平台。量产设备应把密钥保存在安全元件、可信执行环境或受保护的加密存储中,签名运算应尽量在安全边界内完成。 #### 4.2.1 `X-Device-Id` 该值来自平台设备注册/生产烧录流程,是设备的公开 UUID,不需要计算。本文示例固定为: ```text 10000000-0000-0000-0000-000000000512 ``` 设备必须把 Device ID 与对应的 `device_secret` 成对保存。Device ID 可以出现在普通日志中,`device_secret` 不可以。 #### 4.2.2 `X-Device-Timestamp` 该值由设备在每次 HTTP 尝试前读取当前 UTC 时间生成,格式必须为 RFC3339。推荐统一使用带毫秒的 UTC `Z` 格式: ```text 2026-08-29T08:31:00.050Z ``` 生成要求: 1. 设备启动后通过可信 RTC/NTP、蜂窝网络时间或平台 `bootstrap.data.server_time` 校时。 2. 默认服务端允许时间偏差为前后 5 分钟,量产固件应把本地允许偏差控制得更小。 3. 写入请求头的字符串必须原样写入签名原文,不能在签名前后改变小数位、时区或大小写。 4. 网络重试时必须生成新的 Timestamp、Nonce 和 Signature。 伪代码: ```c // utc_now_ms() 必须返回已经校准的 UTC 时间。 format_rfc3339_utc(utc_now_ms(), timestamp, sizeof(timestamp)); // 期望输出示例:2026-08-29T08:31:00.050Z ``` #### 4.2.3 `X-Device-Nonce` Nonce 用于阻止签名请求被重放。服务端会按设备 ID 保存近期已经使用的 Nonce,同一个 Nonce 再次出现会被拒绝。 推荐生成方式:使用密码学安全随机数生成器产生 16 个随机字节,再编码成 32 个小写十六进制字符: ```c uint8_t nonce_raw[16]; char nonce_hex[33]; // rng 必须是芯片 TRNG 或已经正确播种的 CSPRNG,不能使用 rand()。 secure_random(rng, nonce_raw, sizeof(nonce_raw)); hex_lower(nonce_raw, sizeof(nonce_raw), nonce_hex, sizeof(nonce_hex)); ``` 示例: ```text 5d4c79fa3f014e9cacb68eeb481be2c8 ``` 要求: - 长度 16~128 个字符,推荐 32 个十六进制字符; - 每一次 HTTP 尝试都重新生成,包括相同幂等请求的重试; - 不能使用开机计数器、时间戳或 `rand()` 作为唯一随机源; - Nonce 不等于 `Idempotency-Key`,两者生命周期不同。 #### 4.2.4 `X-Device-Content-SHA256` 该值是“真正发送到网络上的 HTTP 正文字节”的 SHA-256,编码为 64 个小写十六进制字符。 正确处理顺序: 1. 把请求对象序列化一次,得到最终 UTF-8 字节数组 `body_bytes`。 2. 对 `body_bytes[0..body_length-1]` 计算 SHA-256。 3. 把 32 字节摘要编码为 64 字符小写十六进制。 4. 使用同一个 `body_bytes` 和相同长度发送 HTTP 请求,不能计算完摘要后再次序列化 JSON。 伪代码: ```c uint8_t digest_raw[32]; char digest_hex[65]; sha256(body_bytes, body_length, digest_raw); hex_lower(digest_raw, sizeof(digest_raw), digest_hex, sizeof(digest_hex)); ``` mbedTLS 示例: ```c #include "mbedtls/sha256.h" int make_content_sha256( const uint8_t *body, size_t body_len, char output_hex[65] ) { static const char hex[] = "0123456789abcdef"; uint8_t digest[32]; if (mbedtls_sha256_ret(body, body_len, digest, 0) != 0) { return -1; } for (size_t i = 0; i < sizeof(digest); ++i) { output_hex[i * 2] = hex[digest[i] >> 4]; output_hex[i * 2 + 1] = hex[digest[i] & 0x0f]; } output_hex[64] = '\0'; return 0; } ``` GET 和真正空正文的输入长度是 0 字节,因此 SHA-256 固定为: ```text e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ``` 以下内容都不是空正文,摘要与上面的固定值不同: | 正文 | UTF-8 字节数 | 说明 | |---|---:|---| | 真正空正文 | 0 | 使用固定空摘要 | | `{}` | 2 | 对字节 `7b 7d` 计算 | | `""` | 2 | 对两个双引号字节计算 | | `\n` | 1 | 对换行字节 `0a` 计算 | | 一个空格 | 1 | 对字节 `20` 计算 | JSON 中字段顺序、空格、换行、转义方式或非 ASCII 字符的 UTF-8 编码只要变化,摘要就会变化。平台比较的是原始字节,不是解析后的 JSON 对象。 #### 4.2.5 Request URI 的确定方法 签名使用 Request URI,不包含协议、域名和端口: ```text request_uri = path + 可选的问号和原始查询串 ``` 示例: ```text POST https://121.41.46.238/device/v2/conversations 签名URI:/device/v2/conversations ``` ```text GET https://121.41.46.238/device/v2/bootstrap?firmware_version=1.2.0&network=wifi 签名URI:/device/v2/bootstrap?firmware_version=1.2.0&network=wifi ``` 查询参数顺序、重复参数、百分号编码、`+` 与 `%20`、十六进制转义大小写都必须与最终发送的 URI 完全一致。推荐先由 HTTP 客户端构建最终 URI,再从同一缓冲区生成签名;不能签名后重新排序查询参数。 #### 4.2.6 `X-Device-Signature` 签名原文按以下五行拼接,行与行之间只有一个字节 `0x0A`,最后一行末尾不能再添加换行: ```text UPPERCASE_HTTP_METHOD REQUEST_URI_WITH_EXACT_QUERY_STRING X-Device-Timestamp原值 X-Device-Nonce原值 X-Device-Content-SHA256原值 ``` 计算公式: ```text signature_raw = HMAC-SHA256(device_secret_raw_bytes, canonical_utf8_bytes) X-Device-Signature = Base64URL_NoPadding(signature_raw) ``` Base64URL 转换要求: 1. 先得到标准 Base64; 2. 把 `+` 替换为 `-`; 3. 把 `/` 替换为 `_`; 4. 删除末尾所有 `=`; 5. 不能输出十六进制 HMAC,必须输出 Base64URL。 mbedTLS HMAC 核心示例: ```c #include "mbedtls/md.h" int hmac_sha256( const uint8_t *secret, size_t secret_len, const uint8_t *canonical, size_t canonical_len, uint8_t output[32] ) { const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); if (info == NULL) { return -1; } return mbedtls_md_hmac( info, secret, secret_len, canonical, canonical_len, output ); } ``` 构造签名原文时可以使用: ```c int written = snprintf( canonical, sizeof(canonical), "%s\n%s\n%s\n%s\n%s", method_upper, request_uri, timestamp, nonce, content_sha256 ); if (written < 0 || (size_t)written >= sizeof(canonical)) { return SIGN_BUFFER_TOO_SMALL; } ``` `snprintf` 产生的结尾 `\0` 不能包含在 HMAC 输入长度中,HMAC 长度应为 `written`。 #### 4.2.7 可复现签名测试向量 下面的密钥仅用于验证算法,绝不能用于真实设备: ```text device_id = 10000000-0000-0000-0000-000000000512 test_secret = embedded-doc-example-secret-32-bytes-minimum method = POST request_uri = /device/v2/conversations timestamp = 2026-08-29T08:31:00.050Z nonce = 5d4c79fa3f014e9cacb68eeb481be2c8 ``` 请求正文必须是下面这一行的 246 个 UTF-8 字节,末尾没有换行: ```json {"device_time":"2026-08-29T08:31:00.000Z","monotonic_ms":15820,"capabilities":{"rtc":true,"audio_tracks":["near","far"],"upload_resume":true,"rtc_credential_renewal":true},"network":{"type":"wifi","rssi":-55},"requested_goal":"open_conversation"} ``` 正文 SHA-256 应得到: ```text 6ab32d795cf68a2f48d3db879e4811db683309b7a3f631003b47e727646143c9 ``` 签名原文的 UTF-8 内容必须准确为: ```text POST /device/v2/conversations 2026-08-29T08:31:00.050Z 5d4c79fa3f014e9cacb68eeb481be2c8 6ab32d795cf68a2f48d3db879e4811db683309b7a3f631003b47e727646143c9 ``` 使用测试密钥计算后,`X-Device-Signature` 必须得到: ```text _spDPNobaFkrX8hInZ26ULuoKsYhSzExpSK7Ww_cVR4 ``` 对应请求头为: ```http X-Device-Id: 10000000-0000-0000-0000-000000000512 X-Device-Timestamp: 2026-08-29T08:31:00.050Z X-Device-Nonce: 5d4c79fa3f014e9cacb68eeb481be2c8 X-Device-Content-SHA256: 6ab32d795cf68a2f48d3db879e4811db683309b7a3f631003b47e727646143c9 X-Device-Signature: _spDPNobaFkrX8hInZ26ULuoKsYhSzExpSK7Ww_cVR4 Idempotency-Key: create-51200000-0000-4000-8000-000000000001 ``` 嵌入式实现应先离线运行该测试向量。如果摘要或签名不一致,不要开始平台联调,应先检查正文长度、换行、URI、Base64URL 和密钥字节。 #### 4.2.8 常见计算错误 | 现象 | 常见原因 | |---|---| | 正文摘要不一致 | 计算后重新序列化 JSON、末尾多换行、发送长度与计算长度不同 | | GET 签名失败 | 忘记使用空正文摘要,或签名 URI 没有包含查询串 | | HMAC 长度不对 | 把 C 字符串结尾 `\0` 也计入 HMAC | | 签名包含 `+`、`/`、`=` | 使用了标准 Base64,没有转换为无填充 Base64URL | | 同一请求第一次成功、第二次 401 | 重用了同一个 Nonce,被重放保护拒绝 | | 相同幂等操作创建了两条记录 | 重试时更换了 `Idempotency-Key` | | `IDEMPOTENCY_KEY_REUSED` | 同一幂等键对应的正文原始字节发生变化 | | Timestamp 过期 | 设备 UTC 未校准,或请求在离线队列停留过久 | #### 4.2.9 一次请求的完整生成顺序 设备应严格按下面的顺序组装请求,避免参与签名的内容与实际发送内容不一致: 1. 确定 HTTP Method,并转换为大写,例如 `POST`。 2. 构建最终 Request URI;如果有查询参数,此时就完成排序和百分号编码,之后不再修改。 3. 把请求对象序列化一次,得到最终 `body_bytes` 和 `body_length`;GET 或无正文请求使用长度为 0 的字节数组。 4. 计算 `SHA256(body_bytes[0..body_length-1])`,转成 64 位小写十六进制,得到 `X-Device-Content-SHA256`。 5. 读取已经校准的 UTC 时间,生成 `X-Device-Timestamp`。 6. 从 TRNG/CSPRNG 读取随机数,生成新的 `X-Device-Nonce`。 7. 用 Method、Request URI、Timestamp、Nonce 和正文摘要构造五行签名原文。 8. 使用当前设备的 `device_secret` 计算 HMAC-SHA256,并转换成无填充 Base64URL,得到 `X-Device-Signature`。 9. 对要求幂等的接口,读取本次业务操作已经持久化的 `Idempotency-Key`;首次操作才生成新值。 10. 设置 HTTP 请求头并发送第 3 步保存的同一份 `body_bytes`,发送长度必须是同一个 `body_length`。 推荐把“最终 URI、最终正文缓冲区、五个签名请求头”封装成不可变请求对象。请求进入发送阶段后不得再修改 URI 或正文。 重试时按错误发生位置处理: | 场景 | 正文 | `Idempotency-Key` | Timestamp | Nonce | Signature | |---|---|---|---|---|---| | 同一业务操作因超时/断网重试 | 保持完全相同 | 保持相同 | 重新生成 | 重新生成 | 重新计算 | | 用户发起新的业务操作 | 生成新正文 | 生成新值 | 重新生成 | 重新生成 | 重新计算 | #### 4.2.10 其他相关 HTTP 请求头 JSON 请求还应设置: ```http Content-Type: application/json; charset=utf-8 Content-Length: 246 ``` - `Content-Length` 是最终正文的字节数,不是字符数。测试向量正文是 246 个 UTF-8 字节,所以值为 `246`。 - 如果 HTTP 库自动生成 `Content-Length`,设备只需保证传给 HTTP 库的缓冲区和长度就是参与 SHA-256 的同一份数据。 - GET/无正文请求正文长度为 0;HTTP 库可以发送 `Content-Length: 0`,也可以省略该头,二者都不改变空正文摘要。 - `Content-Type`、`Content-Length` 和 `Idempotency-Key` 不属于五行 HMAC 签名原文,不能擅自把它们加入签名字符串。 - 所有设备接口都必须使用 HTTPS;HMAC 不能替代 TLS 证书校验。 ### 4.3 设备 `...0512` 的密钥来源与保存 本地环境通过 `DEVICE_LOCAL_SECRET` 把演示密钥导入 `device_credential` 表;服务端数据库保存 AES-256-GCM 加密后的密文,不保存明文。公网或生产环境应使用 `device-credential provision/rotate` 为设备生成或轮换独立密钥。 嵌入式设备应在生产或受控测试烧录阶段接收设备 `...0512` 对应的明文密钥,并写入设备安全区。设备 ID 与密钥必须成对保存。明文密钥不能写进本文档、普通 Flash 分区、串口日志、崩溃转储或升级包;如果密钥丢失或疑似泄露,应在平台执行轮换,不能从数据库反查明文。 ### 4.4 幂等键 以下接口要求 `Idempotency-Key`: - 创建通话; - 激活通话; - 字幕批量补传; - 停止通话; - 创建录音上传批次。 规则: 1. 长度 16~128 字符。 2. 同一次业务操作重试时保持原键和原正文。 3. 每一次 HTTP 重试仍要生成新的 Timestamp、Nonce 和签名。 4. 相同键和相同正文返回原结果,响应可能带 `Idempotency-Replayed: true`。 5. 相同键对应不同正文返回 `409 IDEMPOTENCY_KEY_REUSED`。 推荐生成方法:首次发起一项业务操作时,从 TRNG/CSPRNG 读取 16 个随机字节,编码为 32 个小写十六进制字符,再加上便于排查的接口前缀: ```c uint8_t operation_random[16]; char operation_hex[33]; char idempotency_key[64]; secure_random(rng, operation_random, sizeof(operation_random)); hex_lower(operation_random, sizeof(operation_random), operation_hex, sizeof(operation_hex)); snprintf(idempotency_key, sizeof(idempotency_key), "create-%s", operation_hex); ``` 示例: ```text create-51200000000040008000000000000001 ``` 前缀建议按接口区分:`create-`、`activate-`、`turns-`、`stop-`、`upload-`。前缀只是便于日志定位,不参与业务判断;整个字符串必须满足 16~128 字符。 设备在首次发送前必须把下面的数据写入掉电可恢复存储: - 业务操作类型; - `Idempotency-Key`; - 最终正文原始字节或能够逐字节恢复同一正文的数据; - 当前操作状态。 收到明确的成功响应或明确的不可重试失败后,才能结束该幂等操作。HTTP 超时、连接断开或未收到完整响应时,结果属于“未知”,设备应使用原 `Idempotency-Key` 和原正文重试,但必须重新生成 Timestamp、Nonce 和 Signature。 不要使用 Nonce 作为幂等键,也不要从 Timestamp 推导幂等键:Nonce 每次 HTTP 尝试都要变化,而幂等键在同一业务操作的所有重试中必须稳定。`Idempotency-Key` 也不属于五行 HMAC 签名原文。 ## 5. 阶段一:启动登记、配置同步和网络准备 设备开机、网络恢复或固件升级后,应先完成平台启动登记和配置同步,再开放通话能力。 ### 5.1 设备启动登记 ```http GET /device/v2/bootstrap ``` #### 查询参数 | 参数 | 必填 | 嵌入式示例值 | 说明 | |---|---:|---|---| | `firmware_version` | 是 | `1.2.0` | 当前固件版本 | | `board_rev` | 否 | `board-rev-a` | PCB/硬件版本 | | `rtc_sdk_version` | 否 | `embedded-rtc-3.0.0` | 设备集成的 RTC SDK 版本 | | `codec` | 否 | `opus` | RTC 音频编码能力 | | `sample_rate` | 否 | `16000` | 当前首选采样率 | | `network` | 否 | `wifi` | 当前网络类型或描述 | | `device_time` | 是 | 当前 UTC 时间 | RFC3339 | #### 请求示例 ```http GET /device/v2/bootstrap?firmware_version=1.2.0&board_rev=board-rev-a&rtc_sdk_version=embedded-rtc-3.0.0&codec=opus&sample_rate=16000&network=wifi&device_time=2026-08-29T08%3A30%3A00.000Z HTTP/1.1 Host: 121.41.46.238 X-Device-Id: 10000000-0000-0000-0000-000000000512 X-Device-Timestamp: 2026-08-29T08:30:00.050Z X-Device-Nonce: 5d4c79fa3f014e9cacb68eeb481be2c8 X-Device-Content-SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 X-Device-Signature: <按包含完整查询串的Request URI计算> ``` #### 本地种子中未绑定设备的响应示例 ```json { "request_id": "01J512BOOTSTRAP", "data": { "server_time": "2026-08-29T08:30:00.080Z", "clock_skew_ms": 80, "device": { "device_id": "10000000-0000-0000-0000-000000000512", "status": "registered", "model": "simulated-terminal", "hardware_revision": "sim-rev-a", "minimum_security_version": "dev-signature-v1", "minimum_firmware_version": "0.1.0-sim", "firmware_compatible": true, "config_revision": 1, "binding": { "bound": false } } } } ``` 设备应检查: - `status` 必须为 `registered`; - `firmware_compatible` 必须为 `true`; - 正常创建通话前 `binding.bound` 应为 `true`; - 使用 `server_time` 和 `clock_skew_ms` 检测本机时钟偏差。 主要状态码:`200`、`400`、`401`、`503`。 ### 5.2 获取设备配置 ```http GET /device/v2/config ``` 设备应持久化上一次成功配置的强 ETag,并在后续请求中发送 `If-None-Match`,以减少响应流量。首次启动或没有缓存时可以省略。 #### 请求示例 ```http GET /device/v2/config HTTP/1.1 Host: 121.41.46.238 X-Device-Id: 10000000-0000-0000-0000-000000000512 X-Device-Timestamp: 2026-08-29T08:30:00.100Z X-Device-Nonce: c84bdfef37594423a62f8ac870c27f10 X-Device-Content-SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 X-Device-Signature: <计算结果> If-None-Match: "v1" ``` #### 响应示例 ```http HTTP/1.1 200 OK ETag: "v1" Content-Type: application/json; charset=utf-8 { "request_id": "01J512CONFIG", "data": { "device_id": "10000000-0000-0000-0000-000000000512", "revision": 1, "desired_config": { "codec": "opus", "sample_rate": 16000, "heartbeat_interval_seconds": 15 }, "binding": { "bound": false } } } ``` 配置未变化且客户端发送匹配的 `If-None-Match` 时返回 `304`,无响应正文。平台暂时不可用时,设备应保留上一次有效配置。 ### 5.3 火山 RTC 网络探测 完成两个平台接口后,设备可以按网络栈和 RTC SDK 能力预检下列火山 RTC 域名的 443 端口: ```text access.rtc.volcvideo.com access2-hl.rtc.volcvideo.com common.rtc.volcvideo.com rtcg-access.volcvideos.com rtcg.volcvideos.com web-log-report.rtc.volcvideo.com web-log-report.volcvideos.com ``` 这一步不是平台 HTTP API。产品可以将预检失败作为阻止通话的条件,也可以交由 RTC SDK 在入房阶段返回网络错误;无论采用哪种方式,都应提供稳定错误码而不是上传网络堆栈或密钥。 ## 6. 阶段二:创建通话并等待智能体 以下成功示例假设设备 `...0512` 已经完成绑定并具有有效语音通话授权。 ### 6.1 创建通话 ```http POST /device/v2/conversations ``` 本接口只分配 RTC 房间和短期凭证,不启动豆包智能体。 #### 请求字段 | 字段 | 必填 | 嵌入式实现要求或示例 | |---|---:|---| | `device_time` | 是 | 当前 UTC RFC3339 | | `monotonic_ms` | 否 | RTOS/系统启动后的单调毫秒计数,≥0 | | `capabilities.rtc` | 是 | `true` | | `capabilities.audio_tracks` | 是 | `['near','far']` | | `capabilities.upload_resume` | 是 | `true` | | `capabilities.rtc_credential_renewal` | 否 | `true` | | `network.type` | 是 | 示例为 `wifi`,也可为 `ethernet`、`4g` 等 | | `network.rssi` | 否 | 无线网络建议上报,例如 `-55` | | `requested_goal` | 是 | 固定 `open_conversation` | `audio_tracks` 是设备能力声明,不是本次通话的录音授权: | 值 | 含义 | 嵌入式音频来源 | |---|---|---| | `near` | 设备近端轨道 | 麦克风采集的用户语音 | | `far` | 设备远端轨道 | RTC 解码后的智能体下行语音 | 设备可以声明 `['near']`、`['far']` 或 `['near','far']`,至少一项且不能重复。即使设备声明支持某轨道,也只有创建响应中的 `privacy.record_near`、`privacy.record_far` 和 `privacy.upload_allowed` 同时允许时,才能采集、落盘和上传对应录音。 额外请求头: ```http Idempotency-Key: create-<设备生成的稳定业务UUID> ``` #### 请求正文示例 ```json { "device_time": "2026-08-29T08:31:00.000Z", "monotonic_ms": 15820, "capabilities": { "rtc": true, "audio_tracks": ["near", "far"], "upload_resume": true, "rtc_credential_renewal": true }, "network": { "type": "wifi", "rssi": -55 }, "requested_goal": "open_conversation" } ``` #### 201 响应示例 ```json { "request_id": "01J512CREATE", "data": { "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "ROOM_ISSUED", "rtc": { "app_id": "", "room_id": "r_51200000000040008000000000000001", "user_id": "d_10000000000000000000000000000512", "agent_user_id": "a_51200000000040008000000000000001", "token": "", "expires_at": "2026-08-29T08:36:00Z" }, "privacy": { "retention_mode": "transcript_and_audio", "record_near": true, "record_far": true, "upload_allowed": true }, "session": { "engine_mode": "s2s_hybrid", "heartbeat_interval_sec": 15, "allocation_expires_at": "2026-08-29T08:33:00Z" } } } ``` 关键处理: - `rtc.token` 只保存在内存中; - 必须在 `allocation_expires_at` 之前入房并激活; - `privacy` 决定设备是否允许采集和保存 near/far 原始音频; - `engine_mode` 只用于诊断,设备不能据此改变音频逻辑。 主要错误: | HTTP | 错误码 | 含义 | |---:|---|---| | 400 | `VALIDATION_FAILED` | 字段、正文或幂等键不符合契约 | | 401 | `DEVICE_AUTHENTICATION_FAILED` | 设备 ID、时间、摘要、密钥或签名不正确 | | 403 | `CONSENT_REQUIRED` | 缺少语音通话授权 | | 409 | `DEVICE_NOT_BOUND` | 设备未绑定;本地种子中的 `...0512` 默认会命中此错误 | | 409 | `CONVERSATION_ALREADY_ACTIVE` | 设备已有未结束通话 | | 409 | `IDEMPOTENCY_KEY_REUSED` | 同一键用于不同正文 | | 422 | `AGENT_CONTEXT_PREPARATION_FAILED` | 背景资料准备失败,不会创建 RTC 房间 | | 426 | `FIRMWARE_UPDATE_REQUIRED` | 固件低于最低版本 | | 429 | `USAGE_QUOTA_EXCEEDED` | 费用、用量或并发额度不足 | | 503 | `RUNTIME_CONFIG_UNAVAILABLE` | 没有可用语音路由配置,可按 `retryable` 退避重试 | ### 6.2 嵌入式设备直接加入火山 RTC 创建成功后,设备不经过平台传输实时音频,而是直接调用火山 RTC SDK: 1. 使用 `rtc.app_id` 创建 RTC Engine; 2. 初始化麦克风、音频编解码和播放链路; 3. 使用 `rtc.token + rtc.room_id + rtc.user_id` 加入房间; 4. 发布近端音频; 5. 订阅并播放智能体远端音频; 6. 如果设备侧承担最终字幕采集,则启动 RTC 字幕/ASR 并确认可用; 7. 只有设备要求“字幕可用后才能通话”时,字幕失败才阻止激活;具体门禁由产品方案确定。 实时麦克风和豆包音频不经过平台 API 或数据库。 ### 6.3 确认入房并启动智能体 ```http POST /device/v2/conversations/51200000-0000-4000-8000-000000000001/activate ``` 只有 RTC 入房和字幕启动成功后调用。 额外请求头: ```http Idempotency-Key: activate-<设备生成的稳定业务UUID> ``` #### 请求正文示例 ```json { "joined_at": "2026-08-29T08:31:03.000Z", "room_id": "r_51200000000040008000000000000001", "rtc_user_id": "d_10000000000000000000000000000512", "rtc_join_result": "success", "config_revision": 1, "connection_reference": "embedded-rtc-session-0001" } ``` #### 202 响应示例 ```json { "request_id": "01J512ACTIVATE", "data": { "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "STARTING_AGENT", "accepted_at": "2026-08-29T08:31:03.100Z" } } ``` `202 STARTING_AGENT` 只表示平台已经受理。平台随后通过可靠任务让 Worker 调用豆包 `StartVoiceChat`。 主要错误:`400`、`401`、`404 CONVERSATION_NOT_FOUND`、`409 ROOM_ALLOCATION_EXPIRED`、`409 RTC_ALLOCATION_MISMATCH`、`409 CONFIG_VERSION_CONFLICT`、`409 CONVERSATION_STATE_CONFLICT`、`503`。 ### 6.4 轮询通话状态 ```http GET /device/v2/conversations/51200000-0000-4000-8000-000000000001 ``` 推荐在激活后约 500 ms 首次查询,之后每 1 秒查询,并确保总等待时间不超过 `session.allocation_expires_at`。设备应设置有限次数和本地超时,不能无限轮询。 #### 等待响应 ```json { "request_id": "01J512STATE1", "data": { "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "STARTING_AGENT", "action": "wait" } } ``` #### 激活响应 ```json { "request_id": "01J512STATE2", "data": { "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "ACTIVE", "action": "continue" } } ``` #### 失败响应 ```json { "request_id": "01J512STATE3", "data": { "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "FAILED", "action": "leave_room", "failure_code": "provider_start_rejected" } } ``` | `action` | 设备动作 | |---|---| | `wait` | 保持 RTC 房间并继续有限轮询 | | `continue` | 进入正常通话并启动心跳 | | `leave_room` | 退出 RTC 并执行失败清理 | ## 7. 阶段三:通话期间 ### 7.1 通话心跳 ```http POST /device/v2/conversations/51200000-0000-4000-8000-000000000001/heartbeat ``` 设备进入 `ACTIVE` 后应立即发送一次,之后按: ```text max(5秒, session.heartbeat_interval_sec) ``` 周期调用。当前通常为 15 秒。 #### 请求正文示例 ```json { "sequence_no": 1, "device_time": "2026-08-29T08:31:05.000Z", "monotonic_ms": 20820, "conversation_status": "ACTIVE", "rtc": { "joined": true, "uplink": true, "downlink": true }, "network": { "type": "wifi", "rssi": -55 }, "firmware_version": "1.2.0", "reported_config_revision": 1, "rtc_credential_expires_at": "2026-08-29T08:36:00Z", "recording_queue": { "pending_segments": 0, "failed_segments": 0 }, "command_results": [] } ``` #### 200 响应示例 ```json { "request_id": "01J512HEARTBEAT", "data": { "lease_expires_at": "2026-08-29T08:31:50Z", "actions": [ { "type": "continue" } ], "desired_config_revision": 1, "commands": [] } } ``` 可能的动作: | 动作 | 设备处理 | |---|---| | `continue` | 继续通话 | | `stop` | 尽快停止并离房 | | `stop_after_turn` | 当前话轮完成后停止 | | `renew_rtc_credential` | 使用动作中的新 `rtc.token` 调用 RTC SDK 更新 Token,并保存新到期时间 | 心跳 `sequence_no` 从 1 开始严格递增,设备重启后应从耐久确认点恢复。重复或乱序心跳不会延长租约。该接口不要求 `Idempotency-Key`,由序号完成去重。 ### 7.2 批量上传最终字幕 ```http POST /device/v2/conversations/51200000-0000-4000-8000-000000000001/turns:batch ``` 如果设备 RTC SDK 或本地 ASR 能产生最终字幕,设备应在收到最终结果后加入可靠队列并调用该接口。每批最多 500 条;不具备字幕能力的固件可以不调用,但应与平台确认最终转写由供应商回调提供。 推荐生成规则: - `turn_key = rtc-subtitle-{speaker}-{RTC sequence}`; - `sequence_no` 为本通话内从 1 开始的严格递增编号; - `speaker` 为 `subject` 或 `agent`; - 幂等键可以使用 `turns-{请求正文SHA-256前32位}`。 #### 请求正文示例 ```json { "turns": [ { "turn_key": "rtc-subtitle-subject-1001", "sequence_no": 1, "speaker": "subject", "start_at": "2026-08-29T08:31:20.000Z", "end_at": "2026-08-29T08:31:23.000Z", "final_text": "给我讲一个小时候的故事。", "interrupted": false }, { "turn_key": "rtc-subtitle-agent-2001", "sequence_no": 2, "speaker": "agent", "start_at": "2026-08-29T08:31:24.000Z", "end_at": "2026-08-29T08:31:30.000Z", "final_text": "好呀,我们从您小时候生活的地方说起。", "interrupted": false } ] } ``` #### 200 响应示例 ```json { "request_id": "01J512TURNS", "data": { "conversation_id": "51200000-0000-4000-8000-000000000001", "accepted_count": 2, "duplicate_count": 0 } } ``` 相同终稿重放会计入 `duplicate_count`。相同 `turn_key` 或 `sequence_no` 对应不同内容时,整批返回 `409 TRANSCRIPT_TURN_CONFLICT`。 上传失败时设备应保留可靠队列,并在以下时机重试: - 下一次心跳之后; - 结束通话之前,最多连续重试 3 次。 如果产品要求“字幕不丢失”,仍有字幕未上传时设备不应直接清空队列;可以延迟结束流程或把加密队列持久化后异步补传。 ## 8. 阶段四:结束通话 ### 8.1 本地结束前处理 用户挂断、平台下发停止动作、RTC 失败或设备策略触发结束时,设备建议按以下顺序处理: 1. 停止心跳定时器; 2. 停止 RTC 实时字幕; 3. 等待音频/字幕管线吐出最后的最终结果; 4. 按本地重试策略上传剩余最终字幕; 5. 将 near/far PCM 音频封装为单声道 WAV PCM16 分片; 6. 退出 RTC 房间、停止采集并销毁 RTC Engine; 7. 然后调用平台停止接口。 ### 8.2 停止通话 ```http POST /device/v2/conversations/51200000-0000-4000-8000-000000000001/stop ``` 额外请求头: ```http Idempotency-Key: stop-<设备生成的稳定业务UUID> ``` #### 请求正文示例 ```json { "reason": "user_hangup", "device_time": "2026-08-29T08:35:00.000Z", "last_heartbeat_sequence": 16, "rtc_left": true, "recording": { "near_segments": 1, "far_segments": 1 } } ``` #### 202 响应示例 ```json { "request_id": "01J512STOP", "data": { "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "STOPPING", "accepted_at": "2026-08-29T08:35:00.100Z" } } ``` 响应状态可能为 `STOPPING`、`COMPLETED`、`FAILED` 或 `EXPIRED`。相同停止请求重放不会重复触发供应商 Stop。 启动流程任何一步失败时,只要已经拿到 `conversation_id`,设备也应释放 RTC,并使用稳定原因码清理通话,例如: ```text reason = device_start_failed ``` ### 8.3 等待通话终态 仅当本次实际生成了录音分片时,设备需要等待通话进入允许上传的终态: ```http GET /device/v2/conversations/51200000-0000-4000-8000-000000000001 ``` 建议每 1 秒查询一次,并设置不超过 60 秒的本地等待上限: - `COMPLETED` 或 `FAILED`:进入录音上传; - `EXPIRED`:停止上传并报错; - 超时:本次录音上传失败。 ## 9. 阶段五:录音上传和校验 只有创建通话响应满足以下条件时,设备才允许采集和上传录音: ```text privacy.upload_allowed = true privacy.record_near = true 或 privacy.record_far = true ``` 当前平台接受的嵌入式录音基线: | 项目 | 当前值 | |---|---| | 编码 | `wav_pcm_s16le` | | Content-Type | `audio/wav` | | 声道 | 单声道 | | near | 设备麦克风原始音频帧 | | far | 智能体远端原始音频帧 | | 单片最大时长 | 9 分钟,低于平台 10 分钟上限 | | 单片最大大小 | 64 MiB | ### 9.1 创建上传批次 ```http POST /device/v2/conversations/51200000-0000-4000-8000-000000000001/upload-batches ``` 设备先对每个本地 WAV 分片计算真实字节数和小写 SHA-256,然后只把清单提交给平台,不把音频字节发送给业务 API。 幂等键: ```text media-{清单JSON的SHA-256前40位} ``` #### 请求正文示例 ```json { "segments": [ { "track": "near", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 239000, "expected_size": 7648044, "expected_digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" }, { "track": "far", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 238500, "expected_size": 7632044, "expected_digest": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" } ] } ``` #### 201 响应示例 ```json { "request_id": "01J512MEDIA1", "data": { "id": "51200000-0000-4000-8000-000000000101", "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "UPLOADING", "segment_count": 2, "expected_total_size": 15280088, "verified_count": 0, "rejected_count": 0, "verification_attempts": 0, "expires_at": "2026-08-29T09:05:00Z", "segments": [ { "id": "51200000-0000-4000-8000-000000000201", "track": "near", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 239000, "expected_size": 7648044, "expected_digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "status": "AUTHORIZED", "upload_url": "https://object-store.example/temporary/near.wav?", "upload_until": "2026-08-29T08:55:00Z" }, { "id": "51200000-0000-4000-8000-000000000202", "track": "far", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 238500, "expected_size": 7632044, "expected_digest": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", "status": "AUTHORIZED", "upload_url": "https://object-store.example/temporary/far.wav?", "upload_until": "2026-08-29T08:55:00Z" } ] } } ``` 主要错误:`400`、`401`、`403`(通话、归属或录音授权不允许)、`409`(幂等键或分片元数据冲突)。 ### 9.2 直接 PUT 到对象存储 设备对每个响应中的新分片执行: ```http PUT HTTP/1.1 Content-Type: audio/wav <该分片原始WAV字节> ``` 注意: - 这是设备到对象存储的直接请求,不经过平台业务 API; - 不携带设备 HMAC 请求头; - `upload_url` 自带短期签名,只能上传对应单一对象; - 不得持久化或记录完整 `upload_url`; - 返回非 2xx 时,本次上传流程失败并按外层逻辑重试。 ### 9.3 声明上传完成 ```http POST /device/v2/upload-batches/51200000-0000-4000-8000-000000000101/complete ``` 该接口没有请求正文。设备应发送真正的空正文,并使用空正文 SHA-256: ```text e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ``` #### 202 响应示例 ```json { "request_id": "01J512MEDIA2", "data": { "id": "51200000-0000-4000-8000-000000000101", "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "VERIFYING", "segment_count": 2, "expected_total_size": 15280088, "verified_count": 0, "rejected_count": 0, "verification_attempts": 0, "expires_at": "2026-08-29T09:05:00Z", "segments": [ { "id": "51200000-0000-4000-8000-000000000201", "track": "near", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 239000, "expected_size": 7648044, "expected_digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "status": "VERIFYING" }, { "id": "51200000-0000-4000-8000-000000000202", "track": "far", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 238500, "expected_size": 7632044, "expected_digest": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", "status": "VERIFYING" } ] } } ``` 重复调用不会重复创建后台校验任务。主要状态码:`202`、`401`、`404`、`409`、`410`。 ### 9.4 查询校验结果 ```http GET /device/v2/upload-batches/51200000-0000-4000-8000-000000000101 ``` 建议每 1~2 秒查询一次,直到批次进入终态、达到 `expires_at`,或达到设备本地等待上限(建议不超过 90 秒)。 #### 完成响应示例 ```json { "request_id": "01J512MEDIA3", "data": { "id": "51200000-0000-4000-8000-000000000101", "conversation_id": "51200000-0000-4000-8000-000000000001", "status": "COMPLETED", "segment_count": 2, "expected_total_size": 15280088, "verified_count": 2, "rejected_count": 0, "verification_attempts": 1, "expires_at": "2026-08-29T09:05:00Z", "segments": [ { "id": "51200000-0000-4000-8000-000000000201", "track": "near", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 239000, "expected_size": 7648044, "expected_digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "status": "VERIFIED", "verified_at": "2026-08-29T08:35:05Z", "local_delete_after": "2026-08-29T08:40:05Z" }, { "id": "51200000-0000-4000-8000-000000000202", "track": "far", "sequence_no": 0, "codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": 238500, "expected_size": 7632044, "expected_digest": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", "status": "VERIFIED", "verified_at": "2026-08-29T08:35:05Z", "local_delete_after": "2026-08-29T08:40:05Z" } ] } } ``` 遇到以下结果时设备应判定上传失败: - 批次状态为 `FAILED` 或 `EXPIRED`; - 任一分片状态为 `REJECTED`; - 等待超过 90 秒。 ### 9.5 删除本地录音密文并回报 每个分片达到 `VERIFIED` 后,平台返回 `local_delete_after`。设备必须等到该时间之后,才能删除本地加密录音副本,并调用: ```http POST /device/v2/media-segments/51200000-0000-4000-8000-000000000201/local-delete-ack ``` 删除成功: ```json { "status": "DELETED" } ``` 删除失败: ```json { "status": "FAILED", "error_code": "LOCAL_STORAGE_DELETE_FAILED" } ``` `DELETED` 重复回报是幂等的。过早回报、分片尚未验证或状态冲突返回 `409`。此接口只证明设备副本已删除,不删除云端已验证对象。 ## 10. 重试和异常处理汇总 | 场景 | 嵌入式设备建议行为 | |---|---| | 平台登记或配置同步失败 | 保持不可通话状态,记录稳定错误码并按错误策略重试 | | RTC 网络不可达 | 保持不可通话状态,记录稳定网络错误码并按网络恢复策略处理 | | 创建后 RTC/激活失败 | 释放 RTC;已有通话编号时调用 `/stop`,原因使用稳定错误码 | | 等待 ACTIVE 超过房间预留期或本地上限 | 作为启动失败清理通话 | | 心跳失败 | 保存待确认序号和状态,按退避策略继续;不得用旧序号覆盖新状态 | | 字幕上传失败 | 保留可靠队列,在网络恢复、后续心跳或结束流程中重试 | | 结束时字幕仍上传失败 | 按产品留存策略延迟结束,或持久化加密队列后异步补传 | | 录音上传失败 | 保留本地加密分片,执行有上限的退避重试或进入补偿队列 | | 等待通话终态超过本地上限 | 保留加密录音并进入受控补偿或失败队列 | | 等待录音校验超过批次或本地上限 | 保留加密录音并进入受控补偿或失败队列 | 通用重试原则: - `400/401/403/404/409/422/426` 通常不能盲目重试,应先修正状态或参数; - `429` 按 `Retry-After` 或业务额度提示处理; - `503` 且 `error.retryable=true` 时使用指数退避; - 同一幂等业务重试时保留原 `Idempotency-Key` 和原正文; - 每次 HTTP 尝试都重新生成 HMAC Timestamp、Nonce 和 Signature。 ## 11. 代码与契约依据 - OpenAPI 契约:`openapi/openapi.yaml` - 设备接口总文档:`docs/设备终端接口文档.md` - 设备 HMAC 验签:`internal/deviceauth/verifier.go` - 通话 HTTP 处理:`internal/httpapi/conversation_handlers.go` - 通话业务编排:`internal/orchestration/service.go` - 本地设备种子:`test/fixtures/local_seed.sql` - 已验证参考实现(非规范):`device-simulator-windows/src/main.ts` - 参考实现签名与请求代码:`device-simulator-windows/electron/main.cjs` - 参考实现录音分片代码:`device-simulator-windows/src/recording.ts` ## 12. 联调检查清单 - [ ] 烧录/配置的设备 ID 为 `10000000-0000-0000-0000-000000000512` - [ ] 设备安全区保存的是设备 `...0512` 对应密钥 - [ ] 设备 UTC 时钟与平台相差不超过允许窗口 - [ ] Bootstrap 返回 `registered` 和 `firmware_compatible=true` - [ ] 设备已经绑定有效家庭和服务对象 - [ ] 服务对象具有有效 `voice_chat` 授权 - [ ] 火山 RTC 相关域名 TCP/TLS 443 可达 - [ ] RTC AppID 已开启实时字幕和流式语音识别 - [ ] 创建响应中的 Token 未被写入日志或磁盘 - [ ] RTC 入房成功并满足产品前置条件后才调用 Activate - [ ] 心跳序号严格递增 - [ ] 最终字幕全部上传后才结束通话 - [ ] 只有 `privacy` 明确授权的轨道才采集和上传 - [ ] PUT 的实际 WAV 大小和 SHA-256 与声明一致 - [ ] 上传批次最终达到 `COMPLETED` - [ ] 到 `local_delete_after` 后删除本地密文并回报删除结果