Files

577 lines
23 KiB
Markdown
Raw Permalink Normal View History

2026-09-06 17:04:11 +08:00
# 设备接口文档
> 适用范围:平台设备管理、EchoEar/ESP32-S3 设备接入、火山引擎硬件对话智能体。
> 契约基线:当前工作区实际路由、处理器与 `openapi/openapi.yaml`,更新日期 2026-09-05。
> 生产 API:`https://121.41.46.238`(正式域名启用后只替换 Base URL,不改变路径)。
## 1. 接入模型
设备同时使用两套彼此独立的身份:
| 身份/密钥 | 用途 | 生成与保存位置 | 是否下发设备 |
|---|---|---|---|
| 平台 `device_id` | 标识平台登记设备 | 平台生成;设备保存公开编号 | 是 |
| 平台 Device Secret | 调用 `/device/*` 时做 HMAC 鉴权 | 平台逐设备生成;平台加密保存,设备安全区保存 | 是,只在登记/轮换时显示一次 |
| 火山 `instance_id` / `product_key` | 标识火山实例和产品 | 平台配置 | 可随预配响应返回,非秘密 |
| 火山 Product Secret | 调用 DynamicRegister 的产品级秘密 | 只保存在平台 KMS/Secret | **否** |
| 火山 `device_name` / `device_secret` | 设备直连火山时使用的一机一密 | 平台代设备动态注册;平台加密保存,设备安全区保存 | 是,只返回给已认证的对应设备 |
| Bot ID | 选择硬件对话智能体 | 平台按设备绑定 | 会话响应返回,非秘密 |
核心原则:设备固件不内置 Product Secret。设备先用平台 Device Secret 认证,由平台代设备调用火山动态注册,再把该设备专属的火山 Device Secret 返回给设备。
### 1.1 火山 License 的准确含义
火山硬件对话智能体的 License 是产品侧可用额度。平台调用 DynamicRegister 时,火山自动消耗当前产品的一份有效额度并返回一机一密;设备不接收一段可安装的 License 明文,也不需要向平台提交 License 安装回执。
因此,当前有效契约中:
- 没有 `/device/v3/hardware-agent-license:ack` 路由;
- 没有设备端 `license` 字段;
- 没有 `AVAILABLE → ALLOCATED → ACTIVE` 的设备 License 安装状态机;
- 平台可以维护额度监控或影子账本,但不能用它替代火山的真实额度,也不能阻断已正确取得一机一密的设备。
火山返回额度耗尽、过期等错误时,平台统一转换为 `HARDWARE_AGENT_PROVISIONING_FAILED`,同时通过内部日志和请求编号保留供应商错误供运维定位。
## 2. 通用响应格式
成功响应:
```json
{
"request_id": "req-...",
"data": {}
}
```
失败响应:
```json
{
"request_id": "req-...",
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数不正确",
"retryable": false,
"field_errors": [],
"details": {}
}
}
```
排障时应记录 `request_id`、HTTP 状态码和稳定错误码,不记录任何 Secret、Token、完整人设或完整对话正文。
## 3. 设备 HMAC 鉴权
所有 `/device/v2/*` 和 `/device/v3/*` 请求必须携带:
| 请求头 | 说明 |
|---|---|
| `X-Device-Id` | 平台设备公开 UUID |
| `X-Device-Timestamp` | RFC3339 UTC 时间,默认允许前后 5 分钟 |
| `X-Device-Nonce` | 本次请求唯一的 16—128 字符随机值 |
| `X-Device-Content-SHA256` | 原始 HTTP 正文的 SHA-256 小写十六进制;空正文也要计算 |
| `X-Device-Signature` | HMAC-SHA256 结果的无填充 Base64URL |
签名原文必须逐字使用实际请求 URI(含查询字符串):
```text
UPPERCASE_HTTP_METHOD
REQUEST_URI_WITH_EXACT_QUERY_STRING
X-Device-Timestamp原值
X-Device-Nonce原值
X-Device-Content-SHA256原值
```
伪代码:
```text
bodyHash = hexLower(SHA256(rawBody))
canonical = methodUpper + "\n" + requestURI + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash
signature = base64UrlNoPadding(HMAC_SHA256(platformDeviceSecret, canonical))
```
注意事项:
- JSON 签名后不得重新格式化或改变字段顺序;服务端校验的是收到的原始字节。
- 每次网络重试都生成新的 `timestamp`、`nonce` 和签名。
- 需要幂等的业务请求保留原 `Idempotency-Key` 和原正文。
- 时间未同步时先通过安全时间源校时;不能关闭时间窗或重放保护。
## 4. 设备激活状态机
主流程:
```text
FACTORY → CLAIMABLE → BOUND → PROVISIONING → ACTIVE
│
├─→ LOST
└─→ REVOKED
```
| 状态 | 含义 | 允许的设备操作 |
|---|---|---|
| `FACTORY` | 只有出厂身份,等待质检放行 | 不允许设备 API 鉴权 |
| `CLAIMABLE` | 可生成/消费绑定码 | 启动、配置、命令、遥测;不能通话 |
| `BOUND` | 已绑定家庭和使用人 | 可申请火山一机一密;不能创建会话 |
| `PROVISIONING` | 正在注册或恢复火山运行凭据 | 可重复预配;不能创建会话 |
| `ACTIVE` | 平台身份、绑定和运行凭据就绪 | 全部设备接口 |
| `LOST` | 设备挂失 | 设备鉴权和新会话被拒绝 |
| `REVOKED` | 永久吊销 | 终态,不可恢复 |
代码允许的恢复/解绑边包括:`BOUND → CLAIMABLE`、`PROVISIONING → BOUND`、`ACTIVE → CLAIMABLE/PROVISIONING/LOST`、`LOST → CLAIMABLE/ACTIVE`;任意非 `REVOKED` 状态均可受控转为 `REVOKED`。状态变更必须由平台业务或运维接口执行,设备不能自行声明已激活。
每次变更增加 `activation_revision` 并保存审计历史。进入 `ACTIVE` 时,硬件智能体设备必须已经绑定且存在有效火山一机一密。
## 5. 首次激活完整流程
1. 运维调用 `POST /api/v1/ops/devices` 登记设备,取得一次性平台 Device Secret。
2. 工厂烧录/安全注入 `device_id`、平台 Device Secret、API Base URL 和稳定 `hardware_id`;不得烧录 Product Secret。
3. 运维将设备从 `FACTORY` 放行到 `CLAIMABLE`,并生成绑定码。
4. 用户 App/Web 调用 `POST /api/v1/device-claims` 消费绑定码,平台把设备变为 `BOUND`。
5. 设备使用平台 HMAC 调用 `POST /device/v3/hardware-agent-credentials:provision`。
6. 平台把状态推进到 `PROVISIONING`,自动建立产品默认 Bot/传输/最低 SDK 绑定,再使用服务端 Product Secret 调用火山 DynamicRegister。
7. 火山消耗产品 License 额度并返回该 `device_name` 对应的 `device_secret`。
8. 平台加密保存火山 Device Secret,并通过 `no-store` 响应返回给同一设备,然后自动完成到 `ACTIVE` 的迁移。
9. 设备把火山一机一密写入 NVS 加密分区/安全芯片;以后重复预配会返回已保存的同一凭据,不重复注册。
10. 每次用户发起聊天时,设备先创建平台硬件智能体会话,注入本次人设和上下文后,再直连火山。
## 6. 设备端接口总览
### 6.1 启动、配置、命令和固件(v2 公共能力)
| 方法 | 路径 | 状态要求 | 幂等键 | 作用 |
|---|---|---|---|---|
| `GET` | `/device/v2/bootstrap` | `CLAIMABLE/BOUND/PROVISIONING/ACTIVE` | 否 | 上报启动版本并取得绑定和兼容性 |
| `GET` | `/device/v2/config` | 同上 | 否 | 拉取期望配置;支持 `If-None-Match` |
| `GET` | `/device/v2/commands` | 同上 | 否 | 拉取待执行命令 |
| `POST` | `/device/v2/commands/{id}/ack` | 同上 | 否 | 上报命令阶段或终态 |
| `POST` | `/device/v2/telemetry:batch` | 同上 | 是 | 批量上报设备遥测 |
| `GET` | `/device/v2/firmware-releases/{id}/download` | 同上 | 否 | 下载受控固件制品 |
`bootstrap` 示例:
```http
GET /device/v2/bootstrap?firmware_version=1.0.0&board_rev=echoear-v1&rtc_sdk_version=1.0.0&codec=pcm_s16le&sample_rate=16000&device_time=2026-09-03T08%3A00%3A00Z&network=wifi
```
配置拉取建议:首次不带 `If-None-Match`;保存响应 `ETag`,后续携带该值。HTTP `304` 表示继续使用本地有效配置。
### 6.2 旧 RTC 通话(v2,仅兼容旧设备)
| 方法 | 路径 | 说明 |
|---|---|---|
| `POST` | `/device/v2/conversations` | 创建旧 RTC 通话 |
| `POST` | `/device/v2/conversations/{id}/activate` | 入房成功后激活供应商任务 |
| `GET` | `/device/v2/conversations/{id}` | 查询通话权威状态 |
| `POST` | `/device/v2/conversations/{id}/heartbeat` | 通话心跳 |
| `POST` | `/device/v2/conversations/{id}/stop` | 停止通话 |
| `POST` | `/device/v2/conversations/{id}/turns:batch` | 补传最终字幕 |
新 EchoEar 固件的 `voice_runtime=volc_hardware_agent` 时不得调用这组 RTC 创建/激活接口。
### 6.3 录音上传(v2 公共媒体能力)
| 方法 | 路径 | 作用 |
|---|---|---|
| `POST` | `/device/v2/conversations/{id}/upload-batches` | 创建分片上传批次 |
| `GET` | `/device/v2/upload-batches/{id}` | 查询上传校验状态 |
| `POST` | `/device/v2/upload-batches/{id}/complete` | 通知平台分片已上传 |
| `POST` | `/device/v2/media-segments/{id}/local-delete-ack` | 回报本地密文已删除 |
只有 `ACTIVE` 设备且本次隐私策略允许时才能使用;没有录音授权不得采集、缓存或上传 PCM。
## 7. 火山硬件智能体接口(v3)
### 7.1 申请火山一机一密
```http
POST /device/v3/hardware-agent-credentials:provision
Idempotency-Key: 2ee3b675-59c0-4d93-a189-6db7f8db3750
Cache-Control: no-store
Content-Type: application/json
```
状态要求:`BOUND`、`PROVISIONING` 或 `ACTIVE`。
请求:
```json
{
"hardware_id": "echoear-b43a451634a8"
}
```
`hardware_id` 必须来自稳定硬件身份(如 eFuse/Chip ID 的规范化值),设备重启、升级和恢复出厂后保持不变。
成功响应:
```json
{
"request_id": "req-...",
"data": {
"instance_id": "<instance-id>",
"product_key": "<product-key>",
"device_name": "echoear-...",
"hardware_id": "echoear-b43a451634a8",
"device_secret": "<volc-device-secret>",
"credential_ref": "<non-secret-ref>",
"credential_version": 1,
"credential_source": "platform_dynamic_registration",
"issued_at": "2026-09-03T08:00:00Z"
}
}
```
安全要求:
- 响应带 `Cache-Control: no-store` 和 `Pragma: no-cache`。
- `device_secret` 只写入设备安全存储,不进入普通配置文件、串口日志、崩溃转储或平台遥测。
- `credential_source=platform_stored` 表示幂等返回平台已保存的凭据,不代表重新消耗 License。
- `hardware_id` 与已注册记录不一致时返回冲突,不允许覆盖原凭据。
常见错误:
| HTTP | 错误码 | 处理 |
|---|---|---|
| `400` | `VALIDATION_FAILED` | 修正 `hardware_id` 或幂等键 |
| `409` | `HARDWARE_AGENT_BINDING_REQUIRED` | 先在平台绑定 Bot/产品配置 |
| `409` | `HARDWARE_AGENT_PRODUCT_MISMATCH` | 运维核对实例、产品和设备绑定 |
| `409` | `HARDWARE_AGENT_HARDWARE_MISMATCH` | 停止自动重试,核对硬件身份 |
| `409` | `DEVICE_ACTIVATION_REQUIRED` | 先完成用户绑定,进入 `BOUND` |
| `503` | `HARDWARE_AGENT_PROVISIONING_UNAVAILABLE` | 平台未配置动态注册;联系运维 |
| `503` | `HARDWARE_AGENT_PROVISIONING_FAILED` | 使用原幂等键、原正文和新签名退避重试;由 `request_id` 查火山原始错误 |
### 7.2 创建本次硬件智能体会话
```http
POST /device/v3/agent-sessions
Idempotency-Key: d7f08e7a-9e1d-455f-b8ec-a745a34d8790
Content-Type: application/json
```
状态要求:仅 `ACTIVE`。每次聊天创建一个新会话,不复用上一通响应。
请求示例:
```json
{
"device_time": "2026-09-03T08:05:00Z",
"monotonic_ms": 123456,
"capabilities": {
"hardware_agent": true,
"sdk_version": "1.0.0-echoear-platform.1",
"transports": ["low_load"],
"input_audio_format": "pcm_s16le_16000_mono",
"output_audio_format": "pcm_s16le_16000_mono",
"server_event_callback": false,
"transcript_upload": true,
"dynamic_session_config": true,
"credential_rotation": true,
"near_pcm_recording": false,
"far_pcm_recording": false
},
"network": {
"type": "wifi",
"rssi": -52
},
"requested_goal": "open_conversation",
"config_revision": 1
}
```
约束:
- `transports` 只能包含 `low_load`、`high_quality`,至少一个。
- `transcript_upload`、`dynamic_session_config`、`credential_rotation` 必须为 `true`。
- EchoEar 当前使用 `low_load`;`server_event_callback=false` 时设备事件和最终字幕上传是权威回流。
- `config_revision` 必须使用最近一次 `/device/v2/config` 返回的版本。
成功响应核心字段:
```json
{
"request_id": "req-...",
"data": {
"conversation_id": "<uuid>",
"status": "SESSION_READY",
"runtime_type": "volc_hardware_agent",
"runtime": {
"provider": "volcengine",
"bot_id": "<bot-id>",
"transport": "low_load",
"provider_session_ref": "<provider-session-ref>",
"session_nonce": "<memory-only-secret>",
"config_digest": "<sha256-hex>",
"wait_for_session_update": true
},
"session_config": {
"dialog_id": "<uuid>",
"system_prompt": "<本次冻结的人设提示>",
"persona_tone": "<语气>",
"authorized_context": "<已授权背景/近期摘要/长期记忆>",
"prompt_bundle_version": "<version>"
},
"privacy": {
"retention_mode": "transcript_only",
"record_near": false,
"record_far": false,
"upload_allowed": false
},
"session": {
"engine_mode": "volc_hardware_agent",
"heartbeat_interval_sec": 10,
"allocation_expires_at": "2026-09-03T08:10:00Z"
}
}
}
```
设备处理顺序:
1. 只在运行内存保存 `session_nonce`。
2. 使用预配取得的一机一密连接响应指定的 Bot 和传输模式。
3. 以 `dialog_id` 作为本次业务对话标识。
4. 在开启麦克风前通过 SDK 动态配置或 WebSocket `session.update` 注入 `system_prompt`、`persona_tone` 和 `authorized_context`。
5. 收到火山配置已应用确认后,再上报 `session_started`。
`tool_scope_token` 如存在也只能保存在运行内存,并且仅用于响应指定的 `tool_url` 和本次会话。
### 7.3 上报会话事件
```http
POST /device/v3/agent-sessions/{conversation_id}/events
Content-Type: application/json
```
开始事件:
```json
{
"event_id": "evt-start-0001",
"sequence_no": 1,
"event_type": "session_started",
"occurred_at": "2026-09-03T08:05:03Z",
"provider_session_ref": "<response-runtime-provider-session-ref>",
"session_nonce": "<response-runtime-session-nonce>",
"provider_connection_ref": "<actual-volc-connection-ref>",
"bot_id": "<response-runtime-bot-id>",
"transport": "low_load",
"config_digest": "<response-runtime-config-digest>",
"dynamic_config_applied": true
}
```
后续事件类型:
- `session_updated`:按 `heartbeat_interval_sec` 上报存活;`sequence_no` 严格递增。
- `session_ended`:正常结束。
- `session_failed`:异常结束,可附加稳定、脱敏的 `error_code`。
响应:
```json
{
"request_id": "req-...",
"data": {
"conversation_id": "<uuid>",
"status": "ACTIVE",
"action": "continue",
"duplicate": false,
"accepted_at": "2026-09-03T08:05:03Z"
}
}
```
`action=stop_session` 时,设备必须立即关闭麦克风和火山连接,然后补报 `session_ended` 或 `session_failed`。相同 `event_id` 重放时必须保持原正文一致。
### 7.4 批量上传最终话轮
```http
POST /device/v3/agent-sessions/{conversation_id}/turns:batch
Idempotency-Key: 1710b8cc-9bf2-45e8-a16d-f32c3b80cbbe
Content-Type: application/json
```
请求:
```json
{
"turns": [
{
"turn_key": "user-final-0001",
"sequence_no": 1,
"speaker": "subject",
"start_at": "2026-09-03T08:05:10Z",
"end_at": "2026-09-03T08:05:12Z",
"final_text": "今天天气怎么样?",
"interrupted": false
},
{
"turn_key": "agent-final-0002",
"sequence_no": 2,
"speaker": "agent",
"start_at": "2026-09-03T08:05:12Z",
"end_at": "2026-09-03T08:05:15Z",
"final_text": "我来帮您看看。",
"interrupted": false
}
]
}
```
只上传最终字幕;不得上传增量识别结果。单批 1—500 条,`speaker` 只能为 `subject` 或 `agent`。同一 `turn_key`/`sequence_no` 的重试必须保持正文完全一致。
成功响应:
```json
{
"request_id": "req-...",
"data": {
"conversation_id": "<uuid>",
"accepted_count": 2,
"duplicate_count": 0
}
}
```
## 8. App/Web 的设备接口
以下接口使用用户 OIDC Bearer Token,不使用设备 HMAC:
| 方法 | 路径 | 作用 |
|---|---|---|
| `POST` | `/api/v1/device-claims` | 消费绑定码并绑定设备 |
| `GET` | `/api/v1/devices` | 列出当前用户可管理设备 |
| `GET` | `/api/v1/devices/{id}` | 查看设备详情 |
| `GET` | `/api/v1/devices/{id}/binding-impact` | 变更绑定前评估影响 |
| `POST` | `/api/v1/devices/{id}/binding-change` | 解绑或改绑 |
| `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` | 下发设备命令 |
绑定码由平台/运维为 `CLAIMABLE` 设备生成,App 只消费,不自行生成。解绑会影响设备激活状态和火山运行凭据,必须先调用 `binding-impact` 展示影响并要求用户确认。
## 9. 平台运维设备接口
所有接口要求 OIDC Bearer Token 和有效的 `X-Tenant-Id: <tenant-uuid>`,且仅独立运维账号可调用。
| 方法 | 路径 | 作用 |
|---|---|---|
| `GET` | `/api/v1/ops/devices` | 设备列表 |
| `POST` | `/api/v1/ops/devices` | 登记设备并一次性返回平台 Device Secret |
| `GET` | `/api/v1/ops/device-assignment-options` | 可选家庭/长者列表 |
| `PATCH` | `/api/v1/ops/devices/{id}` | 修改设备;必须携带 `If-Match` |
| `DELETE` | `/api/v1/ops/devices/{id}` | 吊销设备;必须携带 `If-Match` |
| `POST` | `/api/v1/ops/devices/{id}/credentials/rotate` | 轮换平台 Device Secret,只返回一次明文 |
| `POST` | `/api/v1/ops/devices/{id}/claims` | 生成一次性绑定码 |
| `POST` | `/api/v1/ops/devices/{id}/activation-transitions` | 人工迁移激活状态 |
| `GET` | `/api/v1/ops/devices/{id}/activation-transitions` | 查询激活迁移历史 |
### 9.1 登记硬件智能体设备
```json
{
"model": "echoear",
"hardware_revision": "esp32-s3",
"minimum_security_version": "1",
"minimum_firmware_version": "1.0.0",
"household_id": "",
"subject_id": "",
"voice_runtime": "volc_hardware_agent",
"activation_state": "FACTORY"
}
```
Instance ID、Product Key、默认 Bot、传输模式和最低 SDK 由平台产品配置统一持有。设备第一次预配时自动生成绑定,运维创建设备或设备自身都不能覆盖这些字段。
响应中的 `data.credential.secret` 只显示一次。运维系统应立即通过受控注入工具写入设备安全区,不得复制到工单、聊天记录或普通数据库字段。
### 9.2 人工迁移激活状态
```json
{
"target_state": "CLAIMABLE",
"reason": "工厂质检通过",
"expected_revision": 1
}
```
并发控制:列表/详情返回的设备版本对应 `ETag`;修改和删除使用 `If-Match`。激活迁移单独使用 `expected_revision`。版本不一致返回 `412 VERSION_CONFLICT` 或 `409 DEVICE_ACTIVATION_CONFLICT`,刷新后再决定,不能强制覆盖。
## 10. 幂等、重试和本地持久化
| 数据 | 本地策略 |
|---|---|
| 平台 Device Secret | 安全区长期保存;轮换时原子替换 |
| 火山 Device Secret | 安全区长期保存;不要每次启动都预配 |
| `session_nonce` | 仅本次通话运行内存 |
| `tool_scope_token` | 仅本次通话运行内存 |
| 最终字幕待上传队列 | 授权允许时加密持久化,成功确认后删除 |
| 增量字幕/原始音频 | 默认不长期保存 |
重试规则:
- `Idempotency-Key` 长度 16—128 字符。
- HTTP 超时或 `retryable=true`:原业务正文 + 原幂等键 + 新 HMAC nonce/signature,指数退避。
- `401`:先检查平台 Device Secret、时钟、请求 URI、正文摘要和 Base64URL 编码,不要盲目轮换密钥。
- `409 IDEMPOTENCY_KEY_REUSED`:同一幂等键配了不同正文;这是客户端错误。
- `HARDWARE_SESSION_PROOF_INVALID`:丢弃当前会话内存数据,重新创建新会话,不尝试其他设备的 nonce。
- `action=stop_session`:优先停止音频和供应商连接,再做结束确认及本地队列收尾。
## 11. EchoEar 固件接入清单
出厂只写入:
- 平台 `device_id`;
- 平台 Device Secret;
- `hardware_id`;
- API Base URL 和服务器信任锚;
- 固件签名信任锚。
首次绑定后设备取得并安全保存:
- 火山 `instance_id`;
- 火山 `product_key`;
- 火山 `device_name`;
- 火山 Device Secret;
- `credential_version`。
绝不写入设备:
- 火山 Product Secret;
- 平台数据库主密钥/KMS 密钥;
- 其他设备的 Device Secret;
- 固定的会话 nonce 或工具 Token;
- 人设、长期记忆和聊天记录的永久明文副本。
会话侧必须做到:
- 每通先请求平台会话配置,再连接火山;
- 人设、近期摘要和长期记忆以本次 `session_config` 为准;
- 动态配置确认前不打开麦克风;
- 设备事件和最终字幕可靠回流平台;
- 平台下发 `stop_session` 时可在一个心跳周期内停止。
## 12. 当前实现边界
- 当前 API 已实现平台 HMAC、激活状态机、火山动态注册、v3 会话控制面、事件回流和最终字幕上传。
- 设备音频是设备直连火山,平台不代理每一帧音频;平台通过设备事件和字幕回流维护会话权威状态。
- Windows/EchoEar 客户端仍需按目标 SDK 完成真实音频 HAL、火山协议、断线重连和安全存储验收。
- `/device/v3/hardware-agent-license:ack` 处理器代码虽曾存在,但没有路由、没有生产契约,也不应由设备调用。
- 完整字段定义以 [`openapi/openapi.yaml`](../openapi/openapi.yaml) 为机器可读基准;本文件是设备研发和联调的实施说明。
## 13. 联调最小验收用例
1. 错误平台签名返回 `401`,日志不泄露设备是否存在。
2. 同 nonce 重放被拒绝;相同业务幂等键配新 nonce 可安全重试。
3. `CLAIMABLE` 设备不能预配,`BOUND` 设备可以预配但不能创建会话。
4. 首次预配成功取得火山一机一密;重复预配不重复注册。
5. Product Secret 在设备固件、NVS、日志和网络响应中均不存在。
6. `ACTIVE` 设备创建 v3 会话并得到正确 Bot ID、人设和授权上下文。
7. 未确认动态配置的 `session_started` 被拒绝。
8. 正常开始、心跳、最终字幕、结束全部回流平台。
9. 平台返回 `stop_session` 后设备关闭连接并补报结束。
10. 火山 License 额度耗尽时预配失败,扩容后使用原幂等请求可恢复;设备无需安装 License 或 ACK。