# 设备接口文档 > 适用范围:平台设备管理、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": "", "product_key": "", "device_name": "echoear-...", "hardware_id": "echoear-b43a451634a8", "device_secret": "", "credential_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": "", "status": "SESSION_READY", "runtime_type": "volc_hardware_agent", "runtime": { "provider": "volcengine", "bot_id": "", "transport": "low_load", "provider_session_ref": "", "session_nonce": "", "config_digest": "", "wait_for_session_update": true }, "session_config": { "dialog_id": "", "system_prompt": "<本次冻结的人设提示>", "persona_tone": "<语气>", "authorized_context": "<已授权背景/近期摘要/长期记忆>", "prompt_bundle_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": "", "session_nonce": "", "provider_connection_ref": "", "bot_id": "", "transport": "low_load", "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": "", "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": "", "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: `,且仅独立运维账号可调用。 | 方法 | 路径 | 作用 | |---|---|---| | `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。