Files
story/docs/device-api-reference.md
T
Codex Local 0c2151ace9
ci / verify (push) Canceled after 0s
chore: checkpoint 第一版
2026-09-06 17:04:11 +08:00

23 KiB
Raw Permalink Blame History

设备接口文档

适用范围:平台设备管理、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. 通用响应格式

成功响应:

{
  "request_id": "req-...",
  "data": {}
}

失败响应:

{
  "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(含查询字符串):

UPPERCASE_HTTP_METHOD
REQUEST_URI_WITH_EXACT_QUERY_STRING
X-Device-Timestamp原值
X-Device-Nonce原值
X-Device-Content-SHA256原值

伪代码:

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. 设备激活状态机

主流程:

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 示例:

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 申请火山一机一密

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。

请求:

{
  "hardware_id": "echoear-b43a451634a8"
}

hardware_id 必须来自稳定硬件身份(如 eFuse/Chip ID 的规范化值),设备重启、升级和恢复出厂后保持不变。

成功响应:

{
  "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 创建本次硬件智能体会话

POST /device/v3/agent-sessions
Idempotency-Key: d7f08e7a-9e1d-455f-b8ec-a745a34d8790
Content-Type: application/json

状态要求:仅 ACTIVE。每次聊天创建一个新会话,不复用上一通响应。

请求示例:

{
  "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 返回的版本。

成功响应核心字段:

{
  "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 上报会话事件

POST /device/v3/agent-sessions/{conversation_id}/events
Content-Type: application/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。

响应:

{
  "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 批量上传最终话轮

POST /device/v3/agent-sessions/{conversation_id}/turns:batch
Idempotency-Key: 1710b8cc-9bf2-45e8-a16d-f32c3b80cbbe
Content-Type: application/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 的重试必须保持正文完全一致。

成功响应:

{
  "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 登记硬件智能体设备

{
  "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 人工迁移激活状态

{
  "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 为机器可读基准;本文件是设备研发和联调的实施说明。

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。