Files

848 lines
35 KiB
Markdown
Raw Permalink Normal View History

2026-09-06 17:04:11 +08:00
# 设备相关接口完整说明
> 版本:2026-09-05
> 契约来源:`internal/httpapi/router.go`、实际处理器、`openapi/openapi.yaml`
> 生产 Base URL:`https://121.41.46.238`
> 适用对象:EchoEar/ESP32-S3 固件、Windows 模拟设备、用户 App/Web、平台运维端。
## 1. 文档范围
本文覆盖当前代码中全部直接或间接管理设备的接口:
| 接口面 | 方法数 | 鉴权 | 用途 |
|---|---:|---|---|
| 设备直连接口 `/device/v2`、`/device/v3` | 20 | 设备 HMAC | 启动、配置、命令、遥测、固件下载、会话、字幕、媒体、火山凭据 |
| 用户设备管理 `/api/v1/devices*` | 10 | OIDC Bearer | 绑定、设备列表、在线状态、影子、命令 |
| 运维设备管理 `/api/v1/ops/devices*` | 9 | OIDC Bearer + `X-Tenant-Id` | 登记、密钥轮换、绑定码、激活状态机 |
| 家庭端与运维端固件接口 | 31 | OIDC Bearer | 签名密钥、版本、上传、灰度、暂停、回滚、下载 |
| 设备诊断处置 | 2 | OIDC Bearer + `X-Tenant-Id` | 强制刷新配置、吊销设备 |
火山硬件智能体 WebSocket 不是平台 HTTP API,但设备聊天必须使用,故在第 10 节单独说明。
## 2. 通用约定
### 2.1 平台响应包络
成功:
```json
{
"request_id": "req-01J...",
"data": {}
}
```
失败:
```json
{
"request_id": "req-01J...",
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数不正确",
"retryable": false,
"field_errors": [{ "field": "hardware_id", "message": "不能为空" }],
"details": {}
}
}
```
### 2.2 ID、时间和分页
- `{id}`、`device_id`、`conversation_id`、`household_id`、`subject_id` 均为 UUID。
- 时间均为 RFC3339 UTC,例如 `2026-09-05T06:30:00Z`。
- `Idempotency-Key` 长度为 16—128 字符;业务重试保留原正文和原幂等键。
- `If-Match` 使用接口返回的强 ETag,例如 `"7"`。
- 密钥、Token、绑定码和下载地址不得写入普通日志。
### 2.3 设备 HMAC 请求头
所有 `/device/v2/*` 和 `/device/v3/*` 请求必须携带:
| Header | 输入说明 |
|---|---|
| `X-Device-Id` | 平台设备公开 UUID |
| `X-Device-Timestamp` | RFC3339 时间,允许时钟偏差默认 ±5 分钟 |
| `X-Device-Nonce` | 16—128 字符,一次请求只能使用一次 |
| `X-Device-Content-SHA256` | 原始请求正文 SHA-256 小写十六进制;空正文也计算 |
| `X-Device-Signature` | HMAC-SHA256 的 Base64URL 无填充编码 |
签名原文:
```text
HTTP_METHOD大写\n
完整REQUEST_URI(含查询字符串)\n
X-Device-Timestamp\n
X-Device-Nonce\n
X-Device-Content-SHA256
```
## 3. 设备启动、配置、命令和遥测
### 3.1 GET `/device/v2/bootstrap`
介绍:设备上电后报告固件和硬件信息,取得平台权威设备状态、绑定信息、配置版本和服务器时间。
输入:
| 位置 | 字段 | 必填 | 说明 |
|---|---|---:|---|
| Query | `firmware_version` | 是 | 当前固件版本,最长 80 |
| Query | `device_time` | 是 | RFC3339 设备时间 |
| Query | `board_rev` | 否 | 硬件版本 |
| Query | `rtc_sdk_version` | 否 | SDK 版本 |
| Query | `codec` | 否 | 编码,例如 `pcm_s16le` |
| Query | `sample_rate` | 否 | 采样率,例如 `16000` |
| Query | `network` | 否 | 网络类型,例如 `wifi` |
输出:`server_time`、`clock_skew_ms`、设备状态、最低版本、兼容性、`config_revision` 和绑定对象。
示例:
```http
GET /device/v2/bootstrap?firmware_version=1.0.0&board_rev=echoear-v1&codec=pcm_s16le&sample_rate=16000&device_time=2026-09-05T06%3A30%3A00Z&network=wifi
```
```json
{
"request_id": "req-1",
"data": {
"server_time": "2026-09-05T06:30:01Z",
"clock_skew_ms": 1000,
"device": {
"device_id": "10000000-0000-0000-0000-000000000512",
"status": "registered",
"model": "echoear",
"firmware_compatible": true,
"config_revision": 7,
"binding": { "bound": true, "household_id": "11111111-1111-1111-1111-111111111111", "subject_id": "22222222-2222-2222-2222-222222222222", "subject_name": "张大妈" }
}
}
}
```
### 3.2 GET `/device/v2/config`
介绍:读取云端期望配置和当前绑定。设备保存响应 `ETag`,后续用 `If-None-Match` 增量检查。
输入:无正文;可选 Header `If-None-Match: "7"`。
输出:HTTP `200` 返回 `device_id`、`revision`、`desired_config`、`reported_revision`、`binding`;配置未变化返回 `304` 且无正文。
```json
{
"request_id": "req-2",
"data": {
"device_id": "10000000-0000-0000-0000-000000000512",
"revision": 8,
"desired_config": { "voice_runtime": "volc_hardware_agent", "volume": { "speaker": 70 } },
"reported_revision": 7,
"binding": { "bound": true, "subject_id": "22222222-2222-2222-2222-222222222222" }
}
}
```
### 3.3 GET `/device/v2/commands`
介绍:拉取未过期的待执行命令,并增加投递次数。
输入:Query `limit`,范围 1—20,默认 10。
输出:`items[]`;每项包含 `id`、`type`、`payload`、`status`、`expires_at`、`delivery_attempts`、`created_at`。
```json
{
"request_id": "req-3",
"data": {
"items": [{
"id": "33333333-3333-3333-3333-333333333333",
"type": "apply_config",
"payload": { "revision": 8 },
"status": "DELIVERED",
"expires_at": "2026-09-05T07:30:00Z",
"delivery_attempts": 1,
"created_at": "2026-09-05T06:30:00Z"
}]
}
}
```
### 3.4 POST `/device/v2/commands/{id}/ack`
介绍:设备回报命令已接收、成功或失败。
输入:`status` 为 `ACKNOWLEDGED`、`SUCCEEDED`、`FAILED`;`result_summary` 只能包含有限诊断字段,不得含 Secret/Token。
```json
{
"status": "SUCCEEDED",
"result_summary": { "applied_revision": 8 }
}
```
输出:更新后的命令投递对象。
```json
{
"request_id": "req-4",
"data": { "id": "33333333-3333-3333-3333-333333333333", "type": "apply_config", "status": "SUCCEEDED", "result_summary": { "applied_revision": 8 } }
}
```
### 3.5 POST `/device/v2/telemetry:batch`
介绍:批量上报状态、脱敏错误或命令结果;按 `event_key` 和 `sequence_no` 去重,并返回平台动作。
输入:`events` 为 1—500 项;`event_type` 为 `status`、`error` 或 `command_result`,且只携带对应同名对象。
```json
{
"events": [{
"event_key": "boot.000008",
"sequence_no": 8,
"occurred_at": "2026-09-05T06:31:00Z",
"event_type": "status",
"status": {
"firmware_version": "1.0.0",
"reported_config_revision": 8,
"network": { "type": "wifi", "rssi": -51 },
"rtc": { "joined": false, "uplink": false, "downlink": false },
"recording_queue": { "pending_segments": 0, "failed_segments": 0 }
}
}]
}
```
输出:接收/重复数、最新期望配置版本和动作列表。
```json
{
"request_id": "req-5",
"data": { "accepted_count": 1, "duplicate_count": 0, "desired_config_revision": 8, "actions": [{ "type": "pull_commands" }] }
}
```
## 4. 火山硬件智能体平台控制面
### 4.1 POST `/device/v3/hardware-agent-credentials:provision`
介绍:平台使用 Product Secret 代设备调用火山动态注册,只向当前设备返回它自己的一机一密。允许状态:`BOUND`、`PROVISIONING`、`ACTIVE`。
输入:Header `Idempotency-Key`;正文只包含稳定 `hardware_id`。
```json
{ "hardware_id": "echoear-b43a451634a8" }
```
输出:`instance_id`、`product_key`、`device_name`、`hardware_id`、一次性敏感的 `device_secret`、版本和来源。响应必须 `Cache-Control: no-store`。
```json
{
"request_id": "req-6",
"data": {
"instance_id": "6a92945cc0c4689234aef656",
"product_key": "6a9568047dd4b23eacab9b1a",
"device_name": "echoear-100000000512",
"hardware_id": "echoear-b43a451634a8",
"device_secret": "<仅保存到设备安全区>",
"credential_ref": "cred-01J...",
"credential_version": 1,
"credential_source": "platform_dynamic_registration",
"issued_at": "2026-09-05T06:32:00Z"
}
}
```
### 4.2 POST `/device/v3/agent-sessions`
介绍:每次聊天前创建一次平台会话,冻结本次 Bot、授权、人设、记忆、隐私策略和会话证明。仅 `ACTIVE` 设备可调用。
输入:
```json
{
"device_time": "2026-09-05T06:35:00Z",
"monotonic_ms": 123456,
"capabilities": {
"hardware_agent": true,
"sdk_version": "1.1.0-echoear",
"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": 8
}
```
输出:
```json
{
"request_id": "req-7",
"data": {
"conversation_id": "44444444-4444-4444-4444-444444444444",
"status": "SESSION_READY",
"runtime_type": "volc_hardware_agent",
"runtime": {
"provider": "volcengine",
"bot_id": "botM4gWQ9A9y",
"transport": "low_load",
"provider_session_ref": "provider-ref",
"session_nonce": "<仅内存保存>",
"config_digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"wait_for_session_update": true
},
"session_config": {
"dialog_id": "55555555-5555-5555-5555-555555555555",
"system_prompt": "<本次人设>",
"persona_tone": "亲切、自然",
"authorized_context": "<本次授权背景与记忆>",
"prompt_bundle_version": "minimal-v2"
},
"privacy": { "retention_mode": "transcript_only", "record_near": false, "record_far": false, "upload_allowed": false },
"session": { "engine_mode": "s2s_hybrid", "heartbeat_interval_sec": 15, "allocation_expires_at": "2026-09-05T06:40:00Z" }
}
}
```
### 4.3 POST `/device/v3/agent-sessions/{id}/events`
介绍:设备回流 `session_started`、`session_updated`、`session_ended`、`session_failed`。`sequence_no` 严格递增。
开始事件输入:
```json
{
"event_id": "evt-start-1",
"sequence_no": 1,
"event_type": "session_started",
"occurred_at": "2026-09-05T06:35:03Z",
"provider_session_ref": "provider-ref",
"session_nonce": "<创建会话返回值>",
"provider_connection_ref": "event_LUVjX_wvR",
"bot_id": "botM4gWQ9A9y",
"transport": "low_load",
"config_digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"dynamic_config_applied": true
}
```
输出:`status`、平台动作、是否重复和接收时间。
```json
{
"request_id": "req-8",
"data": { "conversation_id": "44444444-4444-4444-4444-444444444444", "status": "ACTIVE", "action": "continue", "duplicate": false, "accepted_at": "2026-09-05T06:35:03Z" }
}
```
`action=stop_session` 时必须立即停麦、断开火山,再上报 `session_ended` 或 `session_failed`。
### 4.4 POST `/device/v3/agent-sessions/{id}/turns:batch`
介绍:可靠上传最终字幕;禁止上传增量字幕。单批 1—500 条。
输入:
```json
{
"turns": [{
"turn_key": "user-final-1",
"sequence_no": 1,
"speaker": "subject",
"start_at": "2026-09-05T06:35:10Z",
"end_at": "2026-09-05T06:35:12Z",
"final_text": "你好。",
"interrupted": false
}]
}
```
输出:
```json
{ "request_id": "req-9", "data": { "conversation_id": "44444444-4444-4444-4444-444444444444", "accepted_count": 1, "duplicate_count": 0 } }
```
## 5. 旧 RTC 会话兼容接口
新 EchoEar 使用硬件智能体 v3,不应调用本节的创建/激活接口。本节仅供存量 RTC 设备。
### 5.1 POST `/device/v2/conversations`
介绍:分配 RTC 房间和短期 Token,不启动智能体。
输入:
```json
{
"device_time": "2026-09-05T06:40:00Z",
"monotonic_ms": 130000,
"capabilities": { "rtc": true, "audio_tracks": ["near", "far"], "upload_resume": true, "rtc_credential_renewal": true },
"network": { "type": "wifi", "rssi": -50 },
"requested_goal": "open_conversation"
}
```
输出:`conversation_id`、`ROOM_ISSUED`、RTC 房间/用户/Token、隐私策略和心跳配置。
```json
{
"request_id": "req-10",
"data": {
"conversation_id": "66666666-6666-6666-6666-666666666666",
"status": "ROOM_ISSUED",
"rtc": { "app_id": "app", "room_id": "room", "user_id": "device-user", "agent_user_id": "agent-user", "token": "<仅内存>", "expires_at": "2026-09-05T06:45:00Z" },
"privacy": { "retention_mode": "none", "record_near": false, "record_far": false, "upload_allowed": false },
"session": { "engine_mode": "cascade", "heartbeat_interval_sec": 10, "allocation_expires_at": "2026-09-05T06:45:00Z" }
}
}
```
### 5.2 POST `/device/v2/conversations/{id}/activate`
介绍:设备确认已成功进入指定 RTC 房间后,触发平台启动旧语音任务。
输入:
```json
{ "joined_at": "2026-09-05T06:40:02Z", "room_id": "room", "rtc_user_id": "device-user", "rtc_join_result": "success", "config_revision": 8, "connection_reference": "rtc-conn-1" }
```
输出:
```json
{ "request_id": "req-11", "data": { "conversation_id": "66666666-6666-6666-6666-666666666666", "status": "STARTING_AGENT", "accepted_at": "2026-09-05T06:40:02Z" } }
```
### 5.3 GET `/device/v2/conversations/{id}`
介绍:读取平台权威会话状态和设备下一步动作。
输入:Path `id`。
输出示例:
```json
{ "request_id": "req-12", "data": { "conversation_id": "66666666-6666-6666-6666-666666666666", "status": "ACTIVE", "action": "continue" } }
```
失败时可能返回 `status=FAILED`、`action=leave_room`、稳定脱敏的 `failure_code`。
### 5.4 POST `/device/v2/conversations/{id}/heartbeat`
介绍:刷新会话租约、设备在线状态和配置状态;返回停止、续签或配置动作。
输入:
```json
{
"sequence_no": 2,
"device_time": "2026-09-05T06:40:12Z",
"monotonic_ms": 142000,
"conversation_status": "ACTIVE",
"rtc": { "joined": true, "uplink": true, "downlink": true },
"network": { "type": "wifi", "rssi": -49 },
"firmware_version": "1.0.0",
"reported_config_revision": 8,
"recording_queue": { "pending_segments": 0, "failed_segments": 0 },
"command_results": []
}
```
输出:
```json
{ "request_id": "req-13", "data": { "lease_expires_at": "2026-09-05T06:40:42Z", "actions": [{ "type": "continue" }], "desired_config_revision": 8, "commands": [] } }
```
### 5.5 POST `/device/v2/conversations/{id}/stop`
介绍:设备挂断并可靠停止旧供应商任务;支持幂等重放。
输入:
```json
{ "reason": "user_hangup", "device_time": "2026-09-05T06:42:00Z", "last_heartbeat_sequence": 5, "rtc_left": true, "recording": { "near_segments": 0, "far_segments": 0 } }
```
输出:
```json
{ "request_id": "req-14", "data": { "conversation_id": "66666666-6666-6666-6666-666666666666", "status": "STOPPING", "accepted_at": "2026-09-05T06:42:00Z" } }
```
### 5.6 POST `/device/v2/conversations/{id}/turns:batch`
介绍:旧 RTC 回调缺少终稿时,由设备补传最终字幕。输入输出与 4.4 完全相同。
```json
{ "turns": [{ "turn_key": "agent-final-2", "sequence_no": 2, "speaker": "agent", "start_at": "2026-09-05T06:41:00Z", "end_at": "2026-09-05T06:41:02Z", "final_text": "您好。", "interrupted": false }] }
```
## 6. 设备录音上传接口
仅当本次 `privacy.upload_allowed=true` 且对应轨道获授权时使用。
### 6.1 POST `/device/v2/conversations/{id}/upload-batches`
介绍:声明待上传录音分片,平台返回每个对象的短期 PUT 地址;业务 API 不接收音频字节。
输入:
```json
{
"segments": [{
"track": "near",
"sequence_no": 0,
"codec": "wav_pcm_s16le",
"content_type": "audio/wav",
"duration_ms": 10000,
"expected_size": 320044,
"expected_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}]
}
```
输出:批次状态和分片;创建响应中的分片包含 `upload_url`、`upload_until`。
```json
{
"request_id": "req-15",
"data": {
"id": "77777777-7777-7777-7777-777777777777",
"conversation_id": "66666666-6666-6666-6666-666666666666",
"status": "UPLOADING",
"segment_count": 1,
"expected_total_size": 320044,
"verified_count": 0,
"rejected_count": 0,
"verification_attempts": 0,
"expires_at": "2026-09-05T07:02:00Z",
"segments": [{ "id": "88888888-8888-8888-8888-888888888888", "track": "near", "sequence_no": 0, "status": "AUTHORIZED", "upload_url": "https://object.example/put?...", "upload_until": "2026-09-05T07:02:00Z" }]
}
}
```
### 6.2 POST `/device/v2/upload-batches/{id}/complete`
介绍:声明分片已上传,平台开始异步对象大小、摘要和容器校验。
输入:无正文;Path `id` 为上传批次 UUID。
输出:HTTP `202`,返回批次,`status` 通常为 `VERIFYING`。
```json
{ "request_id": "req-16", "data": { "id": "77777777-7777-7777-7777-777777777777", "status": "VERIFYING", "segment_count": 1, "verified_count": 0, "rejected_count": 0, "segments": [] } }
```
### 6.3 GET `/device/v2/upload-batches/{id}`
介绍:轮询后台校验结果。只有 `VERIFIED` 分片返回 `local_delete_after`。
输入:Path `id`。
输出示例:
```json
{ "request_id": "req-17", "data": { "id": "77777777-7777-7777-7777-777777777777", "status": "COMPLETED", "segment_count": 1, "verified_count": 1, "rejected_count": 0, "segments": [{ "id": "88888888-8888-8888-8888-888888888888", "status": "VERIFIED", "local_delete_after": "2026-09-05T07:10:00Z" }] } }
```
### 6.4 POST `/device/v2/media-segments/{id}/local-delete-ack`
介绍:到达平台允许删除时间后,设备删除本地密文并回报;不会删除云端对象。
输入:
```json
{ "status": "DELETED" }
```
失败示例:`{ "status": "FAILED", "error_code": "FLASH_DELETE_FAILED" }`。
输出:更新后的分片对象,包含 `local_delete_status` 和 `local_delete_reported_at`。
## 7. 设备固件下载接口
### GET `/device/v2/firmware-releases/{id}/download`
介绍:仅向当前灰度批次中的目标设备签发 5 分钟私有下载地址。
输入:Path `id` 为固件版本 UUID,无正文。
输出:
```json
{ "request_id": "req-18", "data": { "download_url": "https://object.example/firmware.bin?...", "download_until": "2026-09-05T07:05:00Z" } }
```
非目标设备或暂停/退役版本返回 `403 FIRMWARE_DOWNLOAD_DENIED`。
## 8. 用户 App/Web 设备管理接口
统一输入 Header:`Authorization: Bearer <OIDC access token>`。以下表中“示例输入/输出”均省略通用包络字段。
| 方法与路径 | 接口介绍 | 输入 | 输出 | 示例 |
|---|---|---|---|---|
| `POST /api/v1/device-claims` | 消费一次性绑定码 | Body:`claim_code`、`household_id`、`subject_id` | `binding_id`、`device_id`、归属、`bound_at` | 入:`{"claim_code":"CLAIM-...","household_id":"<uuid>","subject_id":"<uuid>"}`;出:`{"binding_id":"<uuid>","device_id":"<uuid>","bound_at":"2026-09-05T07:00:00Z"}` |
| `GET /api/v1/devices` | 列出当前账号可管理设备 | 无 | `items: ManagedDevice[]` | `{"items":[{"id":"<uuid>","model":"echoear","status":"registered","desired_revision":8}]}` |
| `GET /api/v1/devices/{id}` | 查询设备与绑定摘要 | Path:设备 UUID | `ManagedDevice` | `{"id":"<uuid>","binding_id":"<uuid>","subject_name":"张大妈","firmware_version":"1.0.0","version":3}` |
| `GET /api/v1/devices/{id}/binding-impact` | 解绑/转移前检查阻断项 | Query:`action=unbind\|transfer`;转移时 `target_subject_id` | 影响、`allowed`、`blockers`、当前 `binding_id` | `{"action":"transfer","allowed":true,"pending_command_count":0,"active_firmware_task_count":0}` |
| `POST /api/v1/devices/{id}/binding-change` | 执行解绑或转移 | Body:`action`、`expected_binding_id`、`reason`、`acknowledged=true`;转移时 `target_subject_id` | 旧/新绑定和变更时间 | 入:`{"action":"unbind","expected_binding_id":"<uuid>","reason":"用户解除绑定","acknowledged":true}`;出:`{"device_id":"<uuid>","action":"unbind","changed_at":"..."}` |
| `GET /api/v1/devices/{id}/presence` | 查询实时在线状态 | Path:设备 UUID | `online`、`status`、`last_seen_at`、`source` | `{"device_id":"<uuid>","online":true,"status":"ACTIVE","last_seen_at":"...","source":"redis"}` |
| `GET /api/v1/devices/{id}/twin` | 查询设备影子 | Path:设备 UUID | 期望/上报配置、版本、能力、同步状态;Header `ETag` | `{"device_id":"<uuid>","desired_revision":8,"desired_config":{"volume":{"speaker":70}},"sync_state":"applied"}` |
| `PATCH /api/v1/devices/{id}/twin` | 修改非敏感期望配置 | Header:`If-Match`;Body:`desired_config`,顶层仅 `volume/interaction/upload/rtc/policy_refs` | 更新后的 `DeviceTwin` 和新 `ETag` | `{"desired_config":{"volume":{"speaker":60}}}` |
| `GET /api/v1/devices/{id}/commands` | 查询最近 100 条命令 | Path:设备 UUID | `items: DeviceCommand[]` | `{"items":[{"id":"<uuid>","type":"reboot","status":"SUCCEEDED"}]}` |
| `POST /api/v1/devices/{id}/commands` | 下发可靠命令 | Header:`Idempotency-Key`;Body:`type`、`payload`、`expires_in_sec` | `DeviceCommand`,首次 `202`,重放 `200` | 入:`{"type":"reboot","payload":{"reason":"maintenance"},"expires_in_sec":300}`;出:`{"id":"<uuid>","status":"QUEUED","delivery_attempts":0}` |
`ManagedDevice` 主要输出字段:`id`、`binding_id`、`household_id`、`subject_id`、`subject_name`、`model`、`hardware_revision`、`status`、固件/配置版本、`last_reported_at`、`bound_at`、`registered_at`、`version`。
## 9. 平台运维设备管理接口
统一输入 Header:`Authorization: Bearer <operator token>`、`X-Tenant-Id: <tenant UUID>`。只有独立运维账号可调用。
| 方法与路径 | 接口介绍 | 输入 | 输出 | 示例 |
|---|---|---|---|---|
| `GET /api/v1/ops/devices` | 查询租户全部设备 | 无 | `items: OpsDevice[]`,不返回密钥 | `{"items":[{"id":"<uuid>","model":"echoear","activation_state":"ACTIVE","credential_status":"active","voice_runtime":"volc_hardware_agent"}]}` |
| `POST /api/v1/ops/devices` | 登记新设备并生成平台 HMAC 密钥 | Body:`model` 必填;可选硬件/最低版本/归属/运行时/初始激活状态 | `device`、只显示一次的 `credential`;可能同时返回 `claim` | 入:`{"model":"echoear","hardware_revision":"esp32-s3","voice_runtime":"volc_hardware_agent","activation_state":"FACTORY"}`;出:`{"device":{"id":"<uuid>"},"credential":{"device_id":"<uuid>","secret":"<64位hex>","key_version":1}}` |
| `GET /api/v1/ops/device-assignment-options` | 查询可分配家庭和长者 | 无 | `items[]` 含家庭/长者 ID 和名称 | `{"items":[{"household_id":"<uuid>","household_name":"张家","subject_id":"<uuid>","subject_name":"张大妈"}]}` |
| `PATCH /api/v1/ops/devices/{id}` | 更新设备型号、版本策略、状态、归属或运行时 | Header:`If-Match`;Body 至少一个可修改字段 | 更新后的 `OpsDevice` | `{"minimum_firmware_version":"1.1.0","status":"registered"}` |
| `DELETE /api/v1/ops/devices/{id}` | 软删除并吊销设备/凭据/绑定 | Header:`If-Match` | 吊销后的 `OpsDevice` | `{"id":"<uuid>","status":"revoked","activation_state":"REVOKED","credential_status":"revoked"}` |
| `POST /api/v1/ops/devices/{id}/credentials/rotate` | 轮换平台 HMAC 密钥,旧密钥立即失效 | 无正文 | 只显示一次的新 `secret`、`key_version`、`issued_at` | `{"device_id":"<uuid>","secret":"<64位hex>","key_version":2,"issued_at":"..."}` |
| `POST /api/v1/ops/devices/{id}/claims` | 为未绑定设备生成 24 小时绑定码 | 无正文 | 只显示一次的 `claim_code` 和过期时间 | `{"device_id":"<uuid>","claim_code":"CLAIM-...","issued_at":"...","expires_at":"..."}` |
| `POST /api/v1/ops/devices/{id}/activation-transitions` | 执行受控激活状态迁移 | Body:`target_state`、`reason`、`expected_revision` | 更新后的 `OpsDevice` | `{"target_state":"CLAIMABLE","reason":"工厂质检通过","expected_revision":1}` |
| `GET /api/v1/ops/devices/{id}/activation-transitions` | 查询完整激活审计历史 | Path:设备 UUID | `items: DeviceActivationTransition[]` | `{"items":[{"from_state":"FACTORY","to_state":"CLAIMABLE","reason":"工厂质检通过","activation_revision":2,"created_at":"..."}]}` |
激活状态:`FACTORY → CLAIMABLE → BOUND → PROVISIONING → ACTIVE → LOST/REVOKED`。设备不能自行修改该状态。
## 10. 火山硬件智能体 WebSocket
### 10.1 建连
```text
wss://ai-gateway.vei.volces.com/v1/realtime?bot=botM4gWQ9A9y&wait_for_session_update=true
```
输入 Header:
| Header | 说明 |
|---|---|
| `X-Auth-Type` | 固定 `1` |
| `X-Product-Key` | 平台预配返回的 Product Key |
| `X-Device-Name` | 平台预配返回的 Device Name |
| `X-Random-Num` | 本次随机整数 |
| `X-Timestamp` | Unix 秒 |
| `X-Instance-Id` | 平台预配返回的 Instance ID |
| `X-Signature` | 使用火山 Device Secret 计算的 HMAC-SHA256 Base64 |
| `X-Hardware-Id` | 稳定 Hardware ID |
签名正文顺序固定:
```text
auth_type=1&device_name={name}&random_num={random}&product_key={key}&timestamp={unix}&instance_id={instance}
```
### 10.2 动态配置输入
连接打开后立即发送:
```json
{
"event_id": "windows-session-update-<uuid>",
"type": "session.update",
"session": {
"object": "realtime.session",
"model": "",
"input_audio_format": "pcm16",
"output_audio_format": "pcm16",
"config": {
"ASRConfig": { "TurnDetectionMode": 0 },
"LLMConfig": { "SystemMessages": ["<平台冻结的人设与授权上下文>"] }
},
"agent_config": { "EnableConversationStateCallback": false }
}
}
```
输出:`session.created` 或 `session.updated`;在收到确认前不得开启麦克风。
### 10.3 音频和文本输入
上行音频:PCM16 little-endian、16kHz、单声道,建议每 20ms 320 个采样。
```json
{ "event_id": "audio-<uuid>", "type": "input_audio_buffer.append", "audio": "<PCM Base64>" }
```
文本测试:
```json
{
"event_id": "text-<uuid>",
"type": "conversation.item.create",
"item": { "type": "message", "role": "user", "content": [{ "type": "input_text", "text": "你好" }], "interrupt_mode": 1 }
}
```
### 10.4 设备消费的输出事件
| 事件 | 输出内容 | 设备动作 |
|---|---|---|
| `input_audio_buffer.speech_started/stopped` | 服务端 VAD 状态 | 更新交互状态 |
| `conversation.item.input_audio_transcription.completed` | 用户最终字幕 | 上传平台 `turns:batch` |
| `response.audio.delta` | Base64 PCM16、16kHz、单声道 | 连续缓冲并按声卡采样率重采样播放 |
| `response.audio.done` | 本轮音频结束 | 排空播放缓冲 |
| `response.audio_transcript.delta/done` | 智能体增量/最终字幕 | 只把最终字幕上传平台 |
| `response.function_call_arguments.done` | 工具名、参数和 `call_id` | 调用平台受控工具并回传结果 |
| `response.done` | 本轮完成 | 继续监听 |
| `error` | 供应商错误 | 脱敏后上报 `session_failed` |
注意:low-load WebSocket 下行是 16kHz。把它按 24kHz 播放会出现约 1.5 倍速、音调升高和明显电子感。
## 11. 固件管理接口
### 11.1 家庭/租户授权接口
| 方法与路径 | 介绍 | 输入 | 输出 |
|---|---|---|---|
| `GET /api/v1/households/{id}/firmware-signing-keys` | 查询可信 Ed25519 公钥 | Path `household id` | `FirmwareSigningKey[]` |
| `POST /api/v1/households/{id}/firmware-signing-keys` | 登记签名公钥 | `key_id`、`public_key_base64` | `201`,创建结果 |
| `POST /api/v1/households/{id}/firmware-signing-keys/revoke` | 吊销公钥 | `key_id` | `FirmwareSigningKey` |
| `GET /api/v1/households/{id}/firmware-releases` | 查询固件版本 | Path `household id` | `FirmwareRelease[]` |
| `POST /api/v1/households/{id}/firmware-releases` | 创建 `UPLOADING` 版本 | `PrepareFirmwareReleaseRequest` | 版本和短期上传地址 |
| `GET /api/v1/firmware-releases/{id}/impact` | 发布/暂停/退役前影响评估 | Path `release id` | `FirmwareReleaseImpact` |
| `POST /api/v1/firmware-releases/{id}/upload-session` | 重新签发上传地址 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/firmware-releases/{id}/finalize` | 校验大小、SHA-256、Ed25519 后发布 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/firmware-releases/{id}/pause` | 暂停版本和未投递命令 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/firmware-releases/{id}/resume` | 恢复版本分发 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/firmware-releases/{id}/retire` | 退役版本并保留审计 | 无正文 | `FirmwareRelease` |
| `GET /api/v1/households/{id}/firmware-rollouts` | 查询 OTA 批次 | Path `household id` | `FirmwareRollout[]` |
| `POST /api/v1/households/{id}/firmware-rollouts` | 启动灰度升级 | `StartFirmwareRolloutRequest` | `202 FirmwareRollout` |
| `GET /api/v1/firmware-rollouts/{id}` | 查询逐设备升级进度 | Path `rollout id` | `FirmwareRollout` |
| `POST /api/v1/firmware-rollouts/{id}/pause` | 暂停批次 | 无正文 | `FirmwareRollout` |
| `POST /api/v1/firmware-rollouts/{id}/rollback` | 创建回滚批次 | `rollback_release_id` | 新的 `FirmwareRollout` |
### 11.2 运维固件接口
以下接口全部额外要求 `X-Tenant-Id`:
| 方法与路径 | 介绍 | 输入 | 输出 |
|---|---|---|---|
| `GET /api/v1/ops/firmware-signing-keys` | 查询租户可信公钥 | 无 | `FirmwareSigningKey[]` |
| `POST /api/v1/ops/firmware-signing-keys` | 登记可信公钥 | `key_id`、`public_key_base64` | `201` |
| `POST /api/v1/ops/firmware-signing-keys/revoke` | 吊销公钥 | `key_id` | `FirmwareSigningKey` |
| `GET /api/v1/ops/firmware-releases` | 按 `model/status` 查询版本 | Query `model`、`status` 可选 | `FirmwareRelease[]` |
| `POST /api/v1/ops/firmware-releases` | 创建租户级版本 | `PrepareOpsFirmwareReleaseRequest` | `201 FirmwareRelease` |
| `POST /api/v1/ops/firmware-releases/{id}/upload-session` | 续签上传地址 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/ops/firmware-releases/{id}/finalize` | 正式发布 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/ops/firmware-releases/{id}/pause` | 暂停 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/ops/firmware-releases/{id}/resume` | 恢复 | 无正文 | `FirmwareRelease` |
| `POST /api/v1/ops/firmware-releases/{id}/retire` | 退役 | 无正文 | `FirmwareRelease` |
| `GET /api/v1/ops/firmware-releases/{id}/download` | 运维下载制品 | Path `release id` | 下载地址、摘要、签名元数据 |
| `GET /api/v1/ops/firmware-rollouts` | 查询 OTA 批次 | 无 | `FirmwareRollout[]` |
| `POST /api/v1/ops/firmware-rollouts` | 启动 OTA | `StartFirmwareRolloutRequest` | `202 FirmwareRollout` |
| `GET /api/v1/ops/firmware-rollouts/{id}` | 查询逐设备进度 | Path `rollout id` | `FirmwareRollout` |
| `POST /api/v1/ops/firmware-rollouts/{id}/pause` | 暂停 OTA | 无正文 | `FirmwareRollout` |
固件请求示例:
```json
{ "key_id": "release-key-2026-01", "public_key_base64": "<32字节Ed25519公钥的Base64>" }
```
```json
{
"model": "echoear",
"version": "1.1.0",
"expected_size": 2097152,
"expected_digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"signing_key_id": "release-key-2026-01",
"signature": "<firmware-manifest-v1签名Base64>"
}
```
```json
{ "release_id": "<uuid>", "name": "第一批灰度", "target_percentage": 10, "device_ids": ["<device-uuid>"] }
```
典型版本输出:
```json
{ "request_id": "req-fw", "data": { "id": "<uuid>", "model": "echoear", "version": "1.1.0", "status": "READY", "sha256": "bbbb...", "created_at": "2026-09-05T07:00:00Z" } }
```
典型批次输出:
```json
{ "request_id": "req-rollout", "data": { "id": "<uuid>", "release_id": "<uuid>", "name": "第一批灰度", "status": "RUNNING", "target_percentage": 10, "targeted": 1, "succeeded": 0, "failed": 0 } }
```
## 12. 运维诊断设备动作
| 方法与路径 | 介绍 | 输入 | 输出 | 示例 |
|---|---|---|---|---|
| `POST /api/v1/ops/diagnostics/devices/{id}/config-refresh` | 强制设备重新拉取配置 | `X-Tenant-Id`、`Idempotency-Key`、设备 UUID;Body:`reason` | `action=refresh_config`、命令 ID/状态、`queued` | 入:`{"reason":"远端配置与影子不一致"}`;出:`{"action":"refresh_config","device_id":"<uuid>","command_id":"<uuid>","command_status":"QUEUED","queued":true}` |
| `POST /api/v1/ops/diagnostics/devices/{id}/revoke` | 紧急吊销异常设备 | `X-Tenant-Id`、`Idempotency-Key`、设备 UUID;Body:`reason` | `action=revoke_device`、设备 ID、`queued` | 入:`{"reason":"设备密钥疑似泄露"}`;出:`{"action":"revoke_device","device_id":"<uuid>","queued":false}` |
## 13. 主要错误码
| HTTP | 错误码 | 含义与处理 |
|---:|---|---|
| 400 | `VALIDATION_FAILED` | 字段、Header、时间或幂等键不符合契约 |
| 401 | `DEVICE_AUTHENTICATION_FAILED` | 检查平台 Device Secret、时钟、URI、正文摘要和 Base64URL |
| 403 | `DEVICE_ACTIVATION_REQUIRED` | 当前激活状态不允许该操作 |
| 403 | `TRANSCRIPT_STORAGE_NOT_ALLOWED` | 当前授权不允许保存字幕 |
| 403 | `FIRMWARE_DOWNLOAD_DENIED` | 设备不是灰度目标或版本不可下载 |
| 404 | `RESOURCE_NOT_FOUND` | 资源不存在或不属于当前主体/设备 |
| 409 | `IDEMPOTENCY_KEY_REUSED` | 同一幂等键对应不同正文,客户端必须换业务请求 |
| 409 | `DEVICE_ACTIVATION_CONFLICT` | 激活状态或修订号冲突,刷新后再处理 |
| 409 | `HARDWARE_AGENT_HARDWARE_MISMATCH` | Hardware ID 与已注册记录不一致,禁止覆盖 |
| 409 | `HARDWARE_SESSION_PROOF_INVALID` | `session_nonce`/配置摘要/Bot/传输不匹配,丢弃本次会话重新创建 |
| 412 | `VERSION_CONFLICT` | `If-Match` 过期,先重新 GET |
| 426 | `FIRMWARE_UPDATE_REQUIRED` | 当前固件低于最低版本 |
| 429 | `USAGE_QUOTA_EXCEEDED` | 额度或并发限制达到上限 |
| 503 | `HARDWARE_AGENT_PROVISIONING_UNAVAILABLE` | 平台 Product Secret/产品配置不可用 |
| 503 | `HARDWARE_AGENT_PROVISIONING_FAILED` | 火山动态注册失败;按 `request_id` 排障,可退避重试 |
## 14. 设备端推荐调用顺序
```text
上电
→ GET /device/v2/bootstrap
→ GET /device/v2/config
→ GET /device/v2/commands
→ POST /device/v2/telemetry:batch
首次绑定后
→ POST /device/v3/hardware-agent-credentials:provision
→ 安全保存火山 Device Secret
每次聊天
→ POST /device/v3/agent-sessions
→ 连接火山 WebSocket
→ session.update
→ 收到 session.created/session.updated
→ POST /device/v3/agent-sessions/{id}/events (session_started)
→ 16k PCM 双向音频
→ POST /device/v3/agent-sessions/{id}/turns:batch
→ 周期 events (session_updated)
→ events (session_ended/session_failed)
```
## 15. 机器可读契约
- 完整 OpenAPI:[`../openapi/openapi.yaml`](../openapi/openapi.yaml)
- 设备实现与安全说明:[`device-api-reference.md`](device-api-reference.md)
- 设备接入说明:[`device-integration.md`](device-integration.md)
- 固件灰度说明:[`firmware-rollout.md`](firmware-rollout.md)
若本文与 OpenAPI 或运行时代码不一致,以同一发布版本的 OpenAPI 和处理器为准,并应同步修正文档。