Files
story/docs/设备注册与绑定接口简版.md
T
2026-09-12 08:57:29 +08:00

477 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设备注册与绑定接口简版
按当前 SaaS 代码整理,适用协议 `ONBOARDING-1.2-SAAS`。
正常流程:**后台登记并放行 → 设备联网认证 → 打开手机绑定页 → 用户登录并提交绑定 → 设备按键确认 → 保存绑定结果 → 激活语音服务。**
`register` 成功表示设备身份认证通过;`binding_state=BOUND` 才表示绑定成功;`activation_state=READY` 表示语音服务已就绪。
## 阅读约定
- 每个接口只列用途、输入和输出。输入示例为请求 JSON;输出示例为响应 `data` 的主要字段,已省略部分状态和时间字段。
- `<…>` 都是占位值,须替换为实际返回值或设备生成值;UUID、密钥、签名不能照抄占位值。版本号也必须使用最新返回值。
- 路径前加实际 SaaS 域名,例如 `https://saas.example`。请求使用 `Content-Type: application/json`;下面 POST 请求统一携带 `X-Request-ID`(UUID v4)。
- 设备接口除校时外,需要设备签名请求头;手机接口使用浏览器 Cookie,兑换入口后的 POST 还需 `X-CSRF-Token`,并按同源请求调用;运维接口需要运维登录凭证和 `X-Tenant-ID`。具体签名见[设备注册接口说明](设备注册接口说明.md)。
- 这是代码中的接口清单,实际可用性取决于部署开关;短信登录需要已配置并开启短信服务。
响应外层统一如下,后文的“输出”放在 `data` 中:
```json
{"ok":true,"code":"OK","message":"","request_id":"<本次请求UUID>","server_time_ms":1789092000000,"recovery_action":"NONE","retry_after_ms":0,"data":{}}
```
## 一、后台预登记:运维后台或生产工装调用
### 1. 登记一台生产设备
`POST /api/v1/ops/device-inventory`
**用途:** 在平台建立设备档案,生成设备编号和凭据交付任务。MAC、型号、硬件版本用于识别设备;批次用于追溯。另需请求头 `Idempotency-Key`,避免重复建档。
输入:
```json
{"factory_mac":"AABBCCDDEEFF","hardware_model":"KP01","hardware_revision":"A1","batch_no":"20260911-01"}
```
输出:设备生产编号、平台编号、交付任务和是否放行。
```json
{"device_id":"KP01-AABBCCDDEEFF","platform_device_id":"<平台设备UUID>","enrollment_job_id":"<交付任务UUID>","released":false}
```
### 2. 获取设备认证凭据交付包
`POST /api/v1/ops/device-inventory/{id}/credential-deliveries`
**用途:** 将设备认证密钥加密交给生产工装。`{id}` 使用上一步的 `platform_device_id`;公钥属于接收工装。
输入:
```json
{"enrollment_job_id":"<交付任务UUID>","receiver_public_key_pem":"<工装RSA公钥PEM>"}
```
输出:加密交付包。工装解密后可取得 `device_id`、`device_key_b64`、`key_version` 等设备认证资料。
```json
{"algorithm":"RSA-OAEP-SHA256+A256GCM","enrollment_job_id":"<交付任务UUID>","wrapped_key_b64":"<加密的数据密钥>","ciphertext_b64":"<凭据密文>","expires_at_ms":1789092600000}
```
### 3. 确认凭据写入并放行
`POST /api/v1/ops/device-inventory/{id}/release`
**用途:** 确认设备凭据已写入并读回校验,允许设备开始联网认证。`evidence_ref` 是校验记录的引用。
输入:
```json
{"enrollment_job_id":"<交付任务UUID>","evidence_ref":"factory-check/20260911/001","expected_key_version":1}
```
输出:该设备已放行。
```json
{"device_id":"KP01-AABBCCDDEEFF","platform_device_id":"<平台设备UUID>","enrollment_job_id":"<交付任务UUID>","released":true}
```
## 二、设备认证与绑定:设备调用
### 4. 获取平台时间和注册挑战
`POST /api/v1/device/time-challenges`
**用途:** 取得可信时间和本次注册需要的一次性凭证。`boot_id` 标识本次启动,`client_nonce` 是设备生成的 16 字节随机数,以 Base64URL 无填充编码。
输入:
```json
{"device_id":"KP01-AABBCCDDEEFF","key_version":1,"boot_id":"<本次启动UUID>","client_nonce":"<16字节随机数的Base64URL>"}
```
输出:带签名的校时数据包。设备验签并解码后读取 `challenge_id`、`server_time_ms`、`expires_at_ms`,同时核对设备编号、启动编号和随机数。
```json
{"payload_b64":"<校时数据的Base64URL>","mac_hex":"<平台响应签名>"}
```
### 5. 设备注册/启动认证
`POST /api/v1/device/register`
**用途:** 校验设备身份,记录本次启动和软件版本,返回绑定及激活状态。设备编号通过请求头 `X-Device-ID` 传入。首次联网和重新启动都调用。
输入:
```json
{"challenge_id":"<上一步challenge_id>","boot_id":"<本次启动UUID>","hardware_model":"KP01","hardware_revision":"A1","firmware_version":"1.0.0","sdk_version":"1.0.0"}
```
输出:协议版本和当前设备状态。下面表示认证通过,但尚未绑定。
```json
{"contract_version":"ONBOARDING-1.2-SAAS","state":{"device_id":"KP01-AABBCCDDEEFF","binding_state":"UNBOUND","activation_state":"NOT_STARTED","next_action":"WAIT_BIND"}}
```
### 6. 创建一次绑定设置会话
`POST /api/v1/device/setup-sessions`
**用途:** 为本次手机绑定建立临时会话。`local_setup_id` 由设备生成,表示本次设置入口;`transport_profile` 固定为 `LOCAL_WEB`。
输入:
```json
{"boot_id":"<本次启动UUID>","local_setup_id":"<本次设置入口UUID>","transport_profile":"LOCAL_WEB"}
```
输出:云端会话编号,有效期最长 15 分钟;`poll_after_ms` 表示建议多久后查询状态。
```json
{"setup_id":"<设置会话UUID>","expires_at_ms":1789092900000,"poll_after_ms":2000,"mode":"ENROLL"}
```
### 7. 获取手机绑定页面入口
`POST /api/v1/device/handoffs`
**用途:** 获取手机绑定页地址和一次性入口令牌。设备生成 `verifier`,将其 SHA256 结果以 Base64URL 无填充编码后,作为 `code_challenge`。
输入:
```json
{"setup_id":"<设置会话UUID>","local_setup_id":"<本次设置入口UUID>","code_challenge":"<verifier的SHA256结果的Base64URL>"}
```
输出:页面地址、一次性令牌和到期时间。令牌最长 90 秒有效,只能兑换一次;`verifier` 保留在设备侧,由设备一并交给手机。
```json
{"entry_url":"https://saas.example/onboarding/entry","handoff_token":"<一次性令牌>","expires_at_ms":1789092090000}
```
手机实际打开:
```text
https://saas.example/onboarding/entry#token=<handoff_token>&verifier=<设备生成的verifier>
```
### 8. 查询绑定进度/领取平台命令
`POST /api/v1/device/poll`
**用途:** 设备定期查询是否有人申请绑定,以及是否有激活配置。`known_revision` 是设备已知的状态版本,首次可传 0。
输入:
```json
{"setup_id":"<设置会话UUID>","boot_id":"<本次启动UUID>","known_revision":0}
```
输出示例:手机已经提交绑定申请,平台要求设备提示用户按键确认。`payload` 中的确认字段用于下一接口。
```json
{"state":{"binding_state":"CLAIM_PENDING"},"command":{"command_id":"<绑定申请UUID>","type":"CONFIRM_CLAIM","expires_at_ms":1789092120000,"payload":{"claim_id":"<绑定申请UUID>","challenge":"<平台按键挑战>","binding_digest":"<平台绑定摘要>","expected_binding_generation":0,"masked_account":"尾号1234"}},"poll_after_ms":2000}
```
没有命令时 `command=null`。绑定后可能返回 `APPLY_ACTIVATION`,其中包含激活任务、配置版本、配置包及校验值;见第四部分。
### 9. 设备按键确认绑定
`POST /api/v1/device/claim-confirmations`
**用途:** 用户真实按下设备确认键后,设备上报确认,平台正式建立归属关系。前四个字段取自上一步命令,`press_event_id` 标识本次实际按键。
输入:
```json
{"claim_id":"<绑定申请UUID>","challenge":"<平台按键挑战>","binding_digest":"<平台绑定摘要>","expected_binding_generation":0,"press_event_id":"<本次按键事件UUID>"}
```
输出:`BOUND` 表示正式绑定成功;`binding_generation` 是绑定代数,绑定或解绑发生后会变化。
```json
{"claim_id":"<绑定申请UUID>","already_applied":false,"state":{"binding_state":"BOUND","binding_generation":1,"next_action":"RESUME_ACTIVATION"}}
```
### 10. 设备确认已保存绑定结果
`POST /api/v1/device/state-acks`
**用途:** 设备将最新平台状态保存到本地后回执。状态版本、绑定代数和摘要使用平台返回值;`local_generation` 是本次启动的本地保存序号,不能倒退。
输入:
```json
{"device_revision":5,"binding_generation":1,"local_generation":1,"state_hash":"<平台返回的state_hash>"}
```
输出:平台已收到保存回执;设备继续查询状态,等允许后关闭临时设置入口。
```json
{"accepted_revision":5,"setup_can_close":false,"close_after_ms":30000,"next_action":"POLL_FOR_CLOSE"}
```
### 11. 关闭本次临时设置入口
`POST /api/v1/device/setup-close`
**用途:** 绑定流程收尾,关闭临时设置入口。正常完成时等待 `poll` 返回 `setup_can_close=true`,再传 `SUCCESS`;其他原因可为 `TIMEOUT`、`CANCELLED`、`NETWORK_SWITCH`。
输入:
```json
{"setup_id":"<设置会话UUID>","reason":"SUCCESS","final_revision":5}
```
输出:允许关闭;关闭入口不等于语音激活完成,激活可继续进行。
```json
{"close_allowed":true,"state":{"binding_state":"BOUND"}}
```
## 三、手机绑定页面:SaaS 前端调用
用户看到的页面是 `/onboarding/setup`,正常由带一次性凭证的 `/onboarding/entry` 进入。登录账号及目标设备由会话确定,无需前端在绑定请求中再传手机号或设备编号。
### 12. 兑换设备提供的一次性入口
`POST /api/v1/web/handoffs/exchange`
**用途:** 手机用设备提供的令牌和校验值建立绑定会话。服务端通过 Cookie 保持该会话,并返回后续 POST 使用的 `csrf_token`。
输入:
```json
{"token":"<设备提供的handoff_token>","verifier":"<设备提供的verifier>"}
```
输出:注意此接口的响应 `data` 内还有一层 `data`,这是当前代码的实际结构。
```json
{"csrf_token":"<后续请求使用的CSRF值>","data":{"setup_id":"<设置会话UUID>","device_id":"KP01-AABBCCDDEEFF","sms_enabled":true,"state":{"binding_state":"UNBOUND","binding_generation":0}}}
```
### 13. 发送手机登录验证码
`POST /api/v1/web/auth/sms/send`
**用途:** 给用户手机号发送登录验证码。`phone_e164` 含国家区号,`purpose` 固定为 `LOGIN`。
输入:
```json
{"phone_e164":"+8613800000000","purpose":"LOGIN"}
```
输出:验证码任务编号、过期时间、多久后可重发;不会在接口响应中返回验证码。
```json
{"sms_challenge_id":"<短信验证UUID>","expires_at_ms":1789092300000,"resend_after_ms":60000}
```
### 14. 验证短信并登录
`POST /api/v1/web/auth/sms/verify`
**用途:** 验证用户输入的短信验证码,建立登录状态。
输入:
```json
{"sms_challenge_id":"<短信验证UUID>","code":"123456"}
```
输出:登录成功,并返回更新后的 CSRF 值;前端后续请求改用该值。
```json
{"csrf_token":"<更新后的CSRF值>","data":{"authenticated":true,"setup_id":"<设置会话UUID>"}}
```
### 15. 查询可以绑定的家庭和使用者
`POST /api/v1/web/profiles`
**用途:** 列出当前用户有权选择的家庭和设备使用者。
输入:
```json
{}
```
输出:家庭和使用者列表。选择目标后,后续绑定使用 `elder_id`。
```json
{"items":[{"household_id":"<家庭UUID>","household_name":"我的家庭","elder_id":"<使用者UUID>","preferred_name":"爸爸"}]}
```
### 16. 创建家庭/设备使用者
`POST /api/v1/web/profiles/create`
**用途:** 没有可选使用者时创建档案。新建家庭传 `household_name`;已有家庭则改传 `household_id`。`preferred_name` 是使用者称呼。
输入示例:新建家庭和使用者。
```json
{"household_name":"我的家庭","preferred_name":"爸爸"}
```
输出:新建的家庭和使用者编号。
```json
{"household_id":"<家庭UUID>","elder_id":"<使用者UUID>"}
```
### 17. 提交设备绑定申请
`POST /api/v1/web/claims`
**用途:** 申请把当前设备绑定给选中的使用者。`elder_id` 来自上面两个接口,`expected_binding_generation` 来自最新设备状态。
输入:
```json
{"elder_id":"<使用者UUID>","expected_binding_generation":0}
```
输出:申请编号和状态。`CLAIM_PENDING` 表示等待设备按键确认,还没有正式绑定。
```json
{"claim_id":"<绑定申请UUID>","expires_at_ms":1789092120000,"state":{"binding_state":"CLAIM_PENDING","binding_generation":0}}
```
### 18. 查询手机页面上的绑定进度
`POST /api/v1/web/setup/status`
**用途:** 手机刷新等待确认、已绑定和激活进度。`device_saved` 表示设备是否已确认保存绑定结果。
输入:
```json
{}
```
输出示例:设备已经绑定并保存结果。
```json
{"binding_id":"<正式绑定UUID>","device_saved":true,"authenticated":true,"state":{"device_revision":5,"binding_generation":1,"binding_state":"BOUND"}}
```
### 19. 手机确认已收到绑定成功结果
`POST /api/v1/web/setup/ack`
**用途:** 告诉平台手机页面已收到绑定成功结果,配合设备完成设置收尾。三个字段均取自上一步响应。
输入:
```json
{"binding_id":"<正式绑定UUID>","binding_generation":1,"received_revision":5}
```
输出:回执已接收。
```json
{"accepted":true}
```
## 四、绑定后的语音激活:设备继续调用
配置由平台后台任务准备,通过前面的 `POST /api/v1/device/poll` 下发。设备不用直接调用供应商注册接口。
`poll` 的激活命令输出示例(仍是响应 `data` 内的字段):
```json
{"command":{"command_id":"<激活任务UUID>","type":"APPLY_ACTIVATION","payload":{"activation_id":"<激活任务UUID>","config_version":1,"config_digest":"<配置摘要>","config_b64":"<编码后的配置包>","config_mac":"<配置签名>","key_version":1,"config_saved":false,"engine_initialized":false}}}
```
配置包解码后包含设备编号、绑定代数、配置版本、到期时间、IoT 连接资料、单设备密钥、机器人及 RTC 配置。设备校验后保存,用于连接语音服务。
### 20. 确认已保存激活配置
`POST /api/v1/device/activation-acks`
**用途:** 设备校验并保存配置后回执。任务、版本和摘要取自激活命令;绑定代数取自当前状态。
输入:
```json
{"activation_id":"<激活任务UUID>","config_version":1,"binding_generation":1,"local_generation":2,"config_digest":"<配置摘要>"}
```
输出:配置保存已确认。
```json
{"accepted":true,"config_version":1}
```
### 21. 申请执行初始化/首次通话测试
`POST /api/v1/device/activation-attempts`
**用途:** 执行 SDK 初始化或首次通话测试前,申请本次执行许可。`attempt_id` 由设备生成;`stage` 为 `ENGINE_INIT` 或 `CONVERSATION_TEST`。首次通话必须由用户主动发起,并传 `user_initiated=true`。
输入示例:申请初始化 SDK。
```json
{"activation_id":"<激活任务UUID>","config_version":1,"attempt_id":"<本次执行UUID>","stage":"ENGINE_INIT","user_initiated":false}
```
输出:本次执行许可和到期时间。许可最长 120 秒,首次通话测试最长 60 秒。
```json
{"activation_id":"<激活任务UUID>","attempt_id":"<本次执行UUID>","stage":"ENGINE_INIT","permit_token":"<短时执行许可>","permit_expires_at_ms":1789092120000,"maximum_test_seconds":60}
```
### 22. 上报初始化/首次通话测试结果
`POST /api/v1/device/activation-results`
**用途:** 上报执行结果。`outcome` 为 `SUCCESS`、`DEFINITE_FAILURE` 或 `UNKNOWN`;成功时 `sdk_result` 必须为 0。
输入示例:真实首次通话完成后上报成功,`evidence` 引用平台已记录的会话和事件。初始化结果使用 `stage=ENGINE_INIT`,`evidence` 可传 `{}`。
```json
{"activation_id":"<激活任务UUID>","attempt_id":"<首次通话执行UUID>","stage":"CONVERSATION_TEST","sdk_result":0,"outcome":"SUCCESS","evidence":{"conversation_id":"<测试会话UUID>","started_event_id":"<开始事件ID>","first_audio_event_id":"<首音频事件ID>","ended_event_id":"<结束事件ID>"}}
```
输出示例:首次通话验收通过,设备可正常使用。仅初始化成功不会直接返回 `READY`。
```json
{"duplicate":false,"state":{"binding_state":"BOUND","activation_state":"READY","next_action":"ENTER_READY"}}
```
首次通话本身复用既有 `POST /device/v3/agent-sessions` 及会话事件接口;不能只调用本节的结果接口就完成验收。原有通话字段和新增测试许可字段见[实施说明中的首通流程](onboarding-implementation.md#6-激活首通和人工恢复)。
## 五、页面恢复和可选操作
这些操作按需要使用,不是每次首次绑定都必须调用。
| 接口 | 用途 | 输入示例 | 输出示例(响应 data,节选) |
|---|---|---|---|
| `GET /api/v1/web/setup/session` | 页面刷新后恢复设置会话,取得新的 CSRF 值 | 无正文,浏览器携带设置 Cookie | `{"csrf_token":"<新CSRF值>","data":{"authenticated":true,"state":{"binding_state":"CLAIM_PENDING"}}}` |
| `POST /api/v1/web/setup/auth-link` | 使用已有 OIDC 登录状态,作为短信登录的另一种方式 | 正文 `{}`,另带 `Authorization: Bearer <登录令牌>` | `{"csrf_token":"<新CSRF值>","data":{"authenticated":true,"setup_id":"<设置会话UUID>"}}` |
| `POST /api/v1/web/claims/cancel` | 用户取消自己的待确认绑定申请 | `{"claim_id":"<绑定申请UUID>","expected_binding_generation":0}` | `{"state":{"binding_state":"UNBOUND"}}` |
| `GET /api/v1/web/onboarding/devices` | 设置完成后,通过登录账号查询已绑定设备 | 无正文,携带账号 Cookie 或 OIDC 登录令牌 | `{"items":[{"device_id":"KP01-AABBCCDDEEFF","activation_state":"READY","household_name":"我的家庭","preferred_name":"爸爸"}]}` |
## 最常用字段怎么理解
| 字段 | 含义/来源 |
|---|---|
| `device_id` | 生产设备编号,例如 `KP01-AABBCCDDEEFF` |
| `platform_device_id` | 平台设备 UUID,用于后台设备接口中的 `{id}` |
| `boot_id` | 设备本次启动编号,重启后重新生成 |
| `setup_id` | 本次临时绑定流程编号,由平台创建 |
| `elder_id` | 使用者档案编号,由手机选择或创建 |
| `claim_id` | 待设备按键确认的绑定申请编号 |
| `binding_id` | 已正式生效的绑定记录编号,由手机状态接口返回 |
| `binding_generation` | 绑定代数,用于区分绑定、解绑后的不同归属状态 |
| `device_revision` | 平台状态版本,用于判断拿到的是哪一版状态 |
| `activation_id` | 激活任务编号,通过设备 `poll` 命令取得 |
| `attempt_id` | 设备生成的一次初始化或首次通话执行编号 |
接口路径与请求字段依据[当前 HTTP 路由](../internal/onboarding/http.go)、[设备流程](../internal/onboarding/setup.go)、[手机流程](../internal/onboarding/web.go)、[短信登录](../internal/onboarding/sms.go)、[激活流程](../internal/onboarding/activation.go)及[生产登记](../internal/onboarding/inventory.go)核对。完整 31 项首次接入操作(含管理、退出和异常处理)见[字段清单](onboarding-api-fields.md)。