Files
story/docs/首次配网设备认证与绑定_SaaS修改方案.md
2026-09-12 08:57:29 +08:00

474 lines
45 KiB
Markdown
Raw Permalink 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 修改方案
版本:方案草案 1.0 日期:2026 年 9 月 11 日
本方案用于把当前平台改造成支持设备首次认证、手机登录交接、实体按键确认绑定和独立 AI 激活的 SaaS。修改以现有 Go 服务、PostgreSQL、Redis、React、OIDC、设备绑定及供应商凭据模块为基础,分阶段交付,保留旧设备协议。
最终用户流程是:设备联网并认证,手机进入 SaaS 登录并选择使用者,用户按设备按键确认,SaaS 保存绑定,设备保存结果并回执,AI 服务随后独立开通。激活失败时显示“已绑定,AI 服务待处理”,用户不必重新配网或绑定。
本文是一份待实施的设计,不代表接口、数据库或页面已经修改。依据为用户提供的 ONBOARDING-1.1 首次接入文档、上一份差异分析和当前工作区代码。接口、数据表、配置项及模块名称标注为新增或拟新增的,均属于本方案建议。
## 1 方案采用的实现路线
| 决策 | 采用的方案 | 原因及影响 |
|---|---|---|
| 设备身份 | 保留数据库 UUID,新增唯一工厂编号和 MAC 映射 | 兼容现有业务外键、通话和管理接口 |
| 新设备认证 | 新增 DEVREQ1 验签,使用原始 32 字节密钥 | 与附件认证算法衔接;旧设备原签名和密钥解释不变 |
| 账号归属 | 延续租户、家庭、使用者模型;绑定人从当前登录态取得 | 复用权限服务,防止跨租户或任意指定 owner |
| 绑定方式 | 新设备必须走当前申请的设备按键确认 | 绑定码、运维分配等旧入口不能绕过该规则 |
| 手机登录 | 首先接入现有 OIDC 完成联调,完整交付补短信登录和注册 | 先验证绑定业务;短信未完成时不宣称覆盖附件的全部手机流程 |
| AI 激活 | 推荐保留 SaaS 代注册,再向设备下发 DeviceSecret | 复用当前供应商集成;ProductSecret 留在服务端 |
| 任务执行 | 绑定与 outbox 同事务,外部调用在独立 worker 中执行 | 回包丢失或进程退出仍能恢复原任务 |
| 上线方式 | 按设备或产线批次启用新协议,先发布兼容保护版本 | 保证旧设备持续运行,也保证新设备不能降级绕过确认 |
由于激活执行位置与原文档不同,建议把最终新契约暂命名为 `ONBOARDING-1.2-SAAS`,待双方联调后冻结。该名称是本方案的拟定版本,不是现有已发布协议。
### 1.1 与附件的明确调整
1. C01 至 C07、C11 的业务顺序和 DEVREQ1/TIME1/STATE1 计算保留;公共状态额外返回 platform_device_id,说明内部 UUID 与工厂编号的关系。
2. C08 至 C10 调整为设备引擎初始化或首通测试的许可、结果和配置保存回执;供应商注册由 SaaS worker 执行并记录证据。不得继续用设备上报 SDK_REGISTER 来表示服务端代注册结果。
3. 激活配置使用 device_secret,不下发 product_secret;完整性仍采用 CFG1,包含设备、绑定代数、任务、版本和期限。
4. OIDC 是第一阶段的账号接入路径;短信是完整交付的一部分。两者最后映射到同一平台账号和家庭权限模型。
5. 新设备进行日常通话时继续复用现有会话服务,设备入口增加显式协议路由与统一身份解析,不能让设备在进入通话阶段突然改用未经配置的另一套密钥。
若后续要求逐字段保持 ONBOARDING-1.1,则需要另行选择设备执行动态注册路线,恢复附件的 C08 至 C10 和 ProductSecret 配置语义;不能把两种执行模式混在同一个协议版本里。当前方案仅规划 SaaS 代注册这一条实施主线。
### 1.2 SaaS 与设备的工作边界
SaaS 实现库存、凭据、认证、会话交接、账号权限、绑定提交、状态回执、激活任务、对账和运维页面。设备配合生成及保存启动标识、调用新接口、按键确认指定申请、保存状态并 ACK,以及使用平台下发的运行凭据。
家庭 Wi-Fi 密码、AP/NAPT、SSID 扫描、GPIO 和本地 NVS 的实现不进入 SaaS。接口联调需要设备配合,但本方案不安排具体硬件开发。
## 2 修改后的流程及完成条件
| 阶段 | SaaS 提交的事实 | 进入下一阶段的条件 | 失败时保留什么 |
|---|---|---|---|
| 生产登记 | 库存、固定身份和受保护凭据 | 烧录校验并受控放行 | 原设备身份,不生成新设备副本 |
| 设备认证 | 时间挑战已消费、当前启动报告已更新 | 合法 HMAC、挑战、型号和设备状态 | 已有绑定和激活任务 |
| 手机交接 | setup 已建立、handoff 已兑换 | 有效入口和匹配 verifier | 设备和网络已有结果 |
| 申请绑定 | 固定账号和使用者的 PENDING claim | 当前账号有管理权限且设备未绑定 | 已完成登录,不提前建立归属 |
| 提交绑定 | 唯一有效绑定、代数、状态和激活待办 | 设备确认匹配当前 claim | 即使回包丢失,绑定仍成立 |
| 保存与收尾 | 设备和手机回执、设置入口关闭事实 | 回执条件满足或到达关闭上限 | 绑定、设备凭据和原激活任务 |
| AI 激活 | 供应商结果、运行配置和验收证据 | 按阶段检查,不以 HTTP 200 统一判断 | 账号绑定及可能已占用的供应商权益 |
手机可以在绑定完成后离开设置页。设备本地保存失败只修复落盘,AI 激活失败只修复激活。普通重配网保留绑定;解绑和转移使用独立业务流程。
## 3 数据库修改
数据库变更先增加字段和新表,再回填,再启用约束及新功能。以下为逻辑设计,正式 migration 编号在开发时按仓库最新序号分配。
### 3.1 扩展现有记录
| 现有记录 | 拟增加的数据 | 约束及用途 |
|---|---|---|
| device | enrollment_mode、onboarding_contract、factory_device_id、factory_mac、binding_generation、onboarding_revision | 旧数据默认为 legacy;工厂编号和 MAC 各自唯一;绑定代数和状态版本独立 |
| device_binding | generation、source、claim_id | device 与 generation 唯一;保留当前有效绑定唯一索引;claim_id 只对应一次提交 |
| 账号身份映射 | 统一的身份提供方、稳定主体和 user_account 关联 | 保留现有 OIDC 映射;短信身份不伪造成某个外部 OIDC 用户 |
| 审计及事件 | setup/claim/activation/attempt 的关联字段或结构化详情 | 追踪整条首次接入链路,不保存 token、验证码或密钥正文 |
onboarding_revision 在新协议中输出为 device_revision,避免更改现有 device.version、activation_revision、配置 revision 的语义。新表全部使用内部 UUID 做外键,并建立与 tenant_id 一致的组合约束。
### 3.2 新增的逻辑记录
| 拟新增表 | 关键字段 | 必须具备的约束 |
|---|---|---|
| device_inventory | device_id、factory_device_id、factory_mac、tenant_id、型号、批次、生产状态、放行记录 | MAC 和工厂编号唯一;一个库存只关联一个平台设备 |
| device_onboarding_credential | device_id、key_version、encrypted_key、nonce、key_ref、status、activated_at | device/key_version 唯一;只允许一个当前有效版本;密钥是原始 32 字节 |
| device_enrollment_job | job_id、device_id、request_digest、烧录包引用、校验及交付状态 | 重复登记不换身份;受保护交付结果可按原任务恢复 |
| device_time_challenge | device_id、key_version、boot_id、client_nonce、签发和到期时间、consumed_by_request | 绑定设备与启动;挑战只消费一次;可关联同一已提交注册请求 |
| device_onboarding_state | device_id、binding_state、activation_state、current_activation_id、last_boot_id、state_hash | 每设备一条;与 revision 和当前有效绑定在事务内一致更新 |
| device_setup_session | setup_id、device_id、boot_id、local_setup_id、mode、expires_at、absolute_expires_at、closed_at | 当前流程模式为 ENROLL 或 RECOVERY;已绑定设备只能恢复 |
| device_handoff | token_hash、setup_id、code_challenge、expires_at、consumed_at | token 哈希唯一;一次兑换;不得生成第二个可复活的入口 |
| web_setup_session | session_hash、setup_id、认证账号引用、CSRF 信息、期限、撤销时间 | 浏览器只访问当前流程;登录后旋转会话标识 |
| device_binding_claim | claim_id、device_id、setup_id、目标账号/家庭/使用者、challenge_hash、加密挑战、binding_digest、expected_generation、status、expires_at、press_event_id、result_binding_id | 每设备最多一条 PENDING;已成功申请长期去重;按键事件不能确认第二个申请 |
| device_state_snapshot / device_state_ack | device_id、revision、binding_generation、state_hash、local_generation、boot_id、保存及接收时间 | 快照不可变;重复相同 ACK 无副作用;旧 ACK 不覆盖当前状态 |
| device_setup_web_ack | setup_id、binding_id、binding_generation、received_revision、received_at | 记录页面看到了哪次绑定;与设备 ACK 独立 |
| device_activation_job | activation_id、device_id、provider_identity_id、状态、配置版本、当前 attempt、权益引用 | 每设备只有一个当前激活任务;终态和历史保留 |
| device_activation_attempt | attempt_id、activation_id、executor、stage、permit_deadline、fence_version、结果和证据 | executor 区分 SERVER/DEVICE;attempt 唯一;超时不自动授予第二次可能重复的调用 |
| provider_device_identity / activation_entitlement | 固定 device_name、供应商产品、状态、预留或对账证据 | 设备身份不因换用户重建;额度扣占必须事务化且有业务去重键 |
可以复用现有供应商凭据表保存加密 DeviceSecret,在请求供应商之前先持久化 provider_device_identity。权益模型与当前 License 内容表分开处理,不能把文件导入记录当成供应商已确认授权。
新 credential 表避免直接破坏现有 device_credential 按设备唯一的结构。旧密钥继续由旧解析器读取,新密钥由版本化解析器读取;共用加密基础时分别定义包含协议和版本的 AAD。
### 3.3 唯一约束示意
以下 SQL 用于表达约束,不是可直接执行的 migration:
```sql
CREATE UNIQUE INDEX device_binding_claim_one_pending
ON device_binding_claim (device_id)
WHERE status = 'PENDING';
CREATE UNIQUE INDEX device_binding_generation_unique
ON device_binding (device_id, generation);
CREATE UNIQUE INDEX device_activation_one_current
ON device_activation_job (device_id)
WHERE is_current = true;
```
不要在部分索引条件中使用 now()。创建新申请前,在设备锁内把已过期 PENDING 更新为 EXPIRED;后台清理只能辅助,不能作为并发正确性的前提。
### 3.4 绑定代数和历史回填
新设备初始 generation=0;首次绑定变为 1;解绑增加一次;重新绑定再增加一次。第一版转移走“原账号授权解绑,再由目标账号和设备确认新绑定”,不提供绕过确认的一步换主。
存量绑定按稳定的 bound_at、id 顺序建立迁移映射,考虑中间解绑事件,为当前归属确定唯一有效代数。不能把所有历史记录统一回填为 1。历史证据不足的设备保留 legacy 模式,待受控迁移时设置并审计基线;不凭推测补造过去的按键或验收证据。
## 4 生产登记和认证修改
### 4.1 新增生产管理入口
以下路径为本方案新增建议,由受控生产服务账号或具备相应能力的运维账号调用,鉴权与设备请求分开。
| 方法与路径 | 请求数据 | 返回和效果 |
|---|---|---|
| POST /api/v1/ops/device-inventory | factory_mac、hardware_model、hardware_revision、batch_no、目标租户;Idempotency-Key | 服务端生成 factory_device_id,建立或核对原库存;返回内部 UUID 和 enrollment_job_id |
| POST /api/v1/ops/device-inventory/{id}/credential-deliveries | enrollment_job_id、受控产线接收方和交付用途 | 返回仅产线可解密的烧录包或受控交付引用;不在普通列表返回密钥 |
| POST /api/v1/ops/device-inventory/{id}/release | 校验记录引用、expected_version | 已完成烧录校验后放行,允许首次云端认证 |
| POST /api/v1/ops/device-inventory/{id}/credential-rotations | reason、expected_key_version、交付接收方 | 创建新版本交付任务,确认后切换版本,旧版本撤销 |
同 MAC 再登记时返回同一库存和设备;型号、租户或板卡记录冲突返回 409,不静默覆盖。返修使用原登记任务或受控恢复任务;不因响应丢失重新生成密钥。烧录包有效期、访问授权和销毁均可审计,普通幂等响应只保存任务引用。
放行前设备可以处于现有 FACTORY 状态;放行后同步为 CLAIMABLE,避免被当前认证门禁拒绝。尚未通过生产校验的设备不得因请求 C01 而自动放行。
### 4.2 新鉴权组件
新增 `internal/onboardingauth`,读取以下头:X-Device-ID、X-Key-Version、X-Timestamp-Ms、X-Nonce、X-Request-ID、Authorization。DEVREQ1 九行签名按附件原始 body 字节计算;时间容差 ±120 秒;nonce 按设备和版本至少保存 5 分钟。
处理顺序固定为:限制体积和格式 → 查生产放行记录和凭据版本 → 恒定时间验签 → 时间窗口 → 原子 nonce 防重放 → 设备及租户当前状态 → 业务幂等 → 业务事务。新设备请求拒绝 query 参数,JSON 拒绝重复键、非法 UTF-8 和超过 12 层嵌套,普通报文 8 KiB、配置 32 KiB。
限流在昂贵验签之前先按来源执行,通过认证后再按设备执行。Redis 不可用时不能跳过防重放,返回可重试的服务不可用。公开工厂编号和 MAC 只用于定位记录,不构成认证证据。
身份解析器输出内部 device UUID、tenant UUID、工厂编号、协议和密钥版本。现有内部服务继续按 UUID 访问数据库,新协议对外的 device_id 使用工厂编号,platform_device_id 明确返回管理及历史资源所用的 UUID。
### 4.3 C01 与 C02
C01 允许专用 HTTP 校时入口,只返回 `payload_b64` 与 `mac_hex`,签名域为 TIME1;其他业务必须 HTTPS。部署时给 C01 单独路由,不能把整个 /api/v1/device/ 路径开放为明文业务入口。
挑战包含 device_id、key_version 的服务端关联、boot_id、client_nonce、challenge_id、server_time_ms、expires_at_ms;有效期 120 秒。设备校验新 nonce、回显和 RTT 是联调条件。服务器不接受将 C01 响应当成登录凭证。
C02 使用挑战完成已有身份的上线登记,保存固件和 SDK 版本、当前 boot_id。库存登记已经关联内部设备记录,因此此处更新既有记录,不让公开调用创建设备身份。注册时核对型号和生产记录,不以设备自报值覆盖库存型号。
C02 的 challenge 消费、注册结果和幂等提交在同一事务内完成。已成功请求重试先定位原业务结果,同时复核当前绑定代数和权限;挑战已消费不影响读取该成功结果,新 request_id 则须新挑战。
### 4.4 幂等执行器的必要修改
现有 `internal/idempotency/executor.go` 在命中缓存时直接返回 response_body。新协议必须增加事务内 replay guard,并支持调用方控制事务的 ExecuteTx,保证检查当前状态和读取重试结果之间没有换绑竞争。
作用域建议为 tenant/internal_device/contract/method/path/request_id,摘要采用原始 body。保存 24 小时的是短期请求结果;claim_id、activation_id、attempt_id 的业务唯一记录不随缓存过期删除。
固定操作结果与当前状态分别处理:操作未变化且代数仍有效时返回原结果;设备已换绑、停用或权限变化时返回 409/403 并要求查询最新状态,不把旧状态包装成新结果。对只读 poll/status 不缓存过时状态,每次查询使用新的请求号。
handoff token、设备密钥和 DeviceSecret 不明文存进通用 response_body。短时响应恢复采用独立加密存储,绑定有效期和当前权限;普通幂等记录只保存资源引用。
## 5 设备接口修改清单
以下 11 个 POST 路径保留附件的路径命名;字段标注“新增”的属于拟定 1.2 契约变化,不得对 1.1 客户端隐式启用。C01 使用专用校时协议,其余使用新 HMAC。
| 编号和路径 | body 核心字段 | 实施后的行为 |
|---|---|---|
| C01 /api/v1/device/time-challenges | device_id、key_version、boot_id、client_nonce | 返回签名时间及挑战,未知/未放行设备拒绝 |
| C02 /api/v1/device/register | challenge_id、boot_id、hardware_model、hardware_revision、firmware_version、sdk_version | 验证现有身份,返回契约版本及公共 state |
| C03 /api/v1/device/setup-sessions | boot_id、local_setup_id、transport_profile=LOCAL_WEB | 未绑定建立 ENROLL;已绑定建立 RECOVERY,恢复原激活任务 |
| C04 /api/v1/device/handoffs | setup_id、local_setup_id、code_challenge | 仅 ENROLL 签发 90 秒一次性手机入口 |
| C05 /api/v1/device/poll | setup_id、boot_id、known_revision | 当前 state、有效命令和 poll_after_ms;绑定阶段默认 2 秒 |
| C06 /api/v1/device/claim-confirmations | claim_id、challenge、binding_digest、expected_binding_generation、press_event_id | 指定申请确认;事务提交绑定或返回同一有效结果 |
| C07 /api/v1/device/state-acks | device_revision、binding_generation、local_generation、state_hash | 记录设备保存事实,返回 accepted_revision、setup_can_close |
| C08 /api/v1/device/activation-attempts | activation_id、config_version、attempt_id;新增 stage | 只签发 ENGINE_INIT/CONVERSATION_TEST 的设备许可;供应商注册许可由 worker 内部执行 |
| C09 /api/v1/device/activation-results | activation_id、attempt_id、stage、sdk_result、outcome;测试附新增 evidence | 核对同设备尝试和真实会话证据,更新相应阶段;拒绝设备伪报 SERVER_REGISTER |
| C10 /api/v1/device/activation-acks | activation_id、local_generation、config_version;新增 binding_generation、config_digest | 核对当前绑定和配置,记录设备保存运行配置 |
| C11 /api/v1/device/setup-close | setup_id、reason、final_revision | 关闭临时入口和未完成申请;保留已绑定关系及激活任务 |
新增协议版本通过生产记录固定,并在 C02 返回。固件只支持 1.1 时不得把 1.2 当作可忽略的小字段更新;必须完成版本能力校验后再使用。
### 5.1 公共状态示例
以下是拟定响应示例,UUID 为演示值,state_hash 由正式状态计算,不能把示例占位符作为测试向量。
```json
{
"ok": true,
"code": "OK",
"request_id": "f25fcabb-53e4-42c2-ad5b-048775f54414",
"server_time_ms": 1788681600000,
"data": {
"contract_version": "ONBOARDING-1.2-SAAS",
"state": {
"device_id": "KP01-84FCE6123456",
"platform_device_id": "b71dd44c-9a4d-4fdc-a4c3-e6921462c443",
"device_revision": 4,
"binding_generation": 1,
"binding_state": "BOUND",
"activation_state": "RESERVED",
"next_action": "RESUME_ACTIVATION",
"state_hash": "<STATE1计算得到的64位小写hex>"
}
}
}
```
新路径使用附件的 ok/code/data 响应封装,旧 API 保留原封装。不要全局修改 WriteJSON 使旧前端和旧设备同时发生响应变化。
### 5.2 首次接入命令
新增独立的 onboarding 命令队列,复用已有投递和去重经验,但不直接塞入只允许四种旧命令的 device_command 约束。
- CONFIRM_CLAIM:固定 claim_id、挑战、binding_digest、masked_account、expected_binding_generation 和期限。
- APPLY_ACTIVATION:固定 activation_id、config_version、binding_generation、config_digest、config_b64、config_mac 和期限;设备重复收到只同步同版配置。
- UPDATE_STATE:请求同步较新状态,不改变 owner。
- CLOSE_SETUP:请求结束临时设置流程,不等于解绑。
命令按设备、setup 模式、代数和到期时间过滤;取消或换绑后立刻失效。挑战及含 DeviceSecret 的命令载荷加密保存,重复投递可恢复原内容,普通日志仅记录命令 ID 和摘要。
## 6 手机接口和账号修改
### 6.1 七个目标接口
以下字段是完整方案拟定的数据契约,补齐附件未写全的短信及回执部分。新 Web 写接口均使用 HTTPS、当前会话、Origin/CSRF 检查与 X-Request-ID。
| POST 路径 | 请求数据 | 响应及限制 |
|---|---|---|
| /api/v1/web/handoffs/exchange | token、verifier | 设置临时 Cookie,返回 setup_id、脱敏设备信息及当前阶段;不要求预先登录 |
| /api/v1/web/auth/sms/send | phone_e164、purpose=LOGIN | 返回 sms_challenge_id、expires_at_ms、resend_after_ms;不泄露号码是否已有账号 |
| /api/v1/web/auth/sms/verify | sms_challenge_id、code | 原子消费验证码,登录或建立身份映射,旋转 Cookie;保留服务端 setup |
| /api/v1/web/claims | elder_id、expected_binding_generation | 服务端解析使用者所属家庭,返回 claim_id、期限、等待按键状态 |
| /api/v1/web/setup/status | 空对象,使用当前 Cookie | 当前 setup 的绑定、设备保存、激活进度和可恢复动作 |
| /api/v1/web/setup/ack | binding_id、binding_generation、received_revision | 记录页面已看到该绑定;旧绑定的回执不能用于新绑定收尾 |
| /api/v1/web/claims/cancel | claim_id、expected_binding_generation | 仅取消该会话有权管理的 PENDING;已提交时返回当前已绑定状态 |
第一阶段暂不开放 sms/send 和 sms/verify,改由 OIDC 登录接续接口完成身份衔接。完整发布前补短信提供方适配和真实联调,不能用模拟验证码对外发布。
### 6.2 OIDC 接续及账号权限
第一阶段拟新增 `POST /api/v1/web/setup/auth-link`:接收当前已验证的 OIDC Bearer 凭据,同时核对 setup Cookie 和 CSRF,将平台账号与本 setup 关联并旋转会话。它是 OIDC 适配接口,不替代 handoff 兑换。
完整方案增加统一平台会话解析层。OIDC 身份和已验证短信身份均映射到稳定的平台主体,再从 user_account、household_member 获取权限。仅向明确开放的家庭/档案路由增加平台会话鉴权;Cookie 写请求必须检查 CSRF。旧 Bearer 入口继续按现有方式工作。
现有部分服务直接按 issuer/subject 查询账号,需集中到共同的账号访问解析器,或在可信会话适配层使用明确的自有身份命名空间。不得把未经外部提供方签发的用户伪装为其 OIDC subject,也不得允许手机号自动合并另一账号。
设备库存确定目标租户。首次用户只可在允许自助入驻的消费租户中建立普通客户账号及家庭;企业租户没有邀请或授权时拒绝自动加入。注册不授予运维或租户管理能力。手机号绑定到已有 OIDC 账号须经过双方身份验证,不按相同显示名称合并。
用户已有家庭和使用者时直接选择;没有时通过现有家庭及档案服务创建。完成登记后必须再次校验使用者属于同租户且当前账号拥有设备管理能力。
### 6.3 入口页和浏览器会话
新建 `/onboarding/entry`、`/onboarding/setup`、`/onboarding/result` 页面。entry 读取 fragment 中的 token 和 verifier,立即 replaceState 清除地址,然后同源 HTTPS 兑换;禁用第三方脚本、统计和凭据缓存。登录页不访问设备私网 HTTP。
handoff 90 秒、claim 120 秒;setup 默认 15 分钟且绝对不超过 30 分钟。Cookie 建议为 `__Host-onboarding`,Secure、HttpOnly、Path=/、无 Domain,并按 OIDC 顶层回跳验证 SameSite=Lax 的实际行为。
登录成功后旋转 session ID 并保留数据库中的 setup;手机号、验证码、Token 不写 URL 或普通日志。发送验证码默认间隔 60 秒、验证码 5 分钟、单挑战最多 5 次验证,为拟定初值,另设手机号/IP/浏览器/全局发送限额。
兑换响应丢失时,有 Cookie 就查询当前状态;无 Cookie 就从同设备重新取入口,已经消费的 token 不复活。setup 关闭后普通账号会话仍可从设备列表查看进度,避免用户只能通过一次性入口访问已绑定设备。
## 7 绑定提交和所有归属写入口
### 7.1 统一的绑定服务
新增 `internal/devicebinding`,提供接收 pgx.Tx 的归属操作,供新 claim、旧绑定码、运维、正式解绑转移、恢复出厂和隐私删除共用。已有事务由调用方持有,公共服务不能私自另开事务导致部分提交。
新设备的 ConfirmClaim 应完成:
1. 验证设备请求,并按固定顺序锁定设备、setup 和 claim;目标权限行按统一顺序读取和锁定。多设备任务按 UUID 排序取锁。
2. 检查同申请是否已经成功:仅当前有效绑定和代数一致时返回原结果;归属已变化返回 409,要求同步当前状态。
3. 检查 setup 为有效 ENROLL、设备未绑定且未停用、claim 未取消且未过期、当前账号仍有使用者管理权限。
4. 核验 challenge、binding_digest、expected_binding_generation、press_event_id,并确保按键事件没有被另一申请使用。
5. 写入唯一有效绑定,binding_generation 增加一次,消费 claim,更新 onboarding state 和 revision,保存不可变快照。
6. 创建或关联固定激活任务,写入 `device.bound`/`device.activation.requested` outbox,事件去重键含设备与绑定代数;同时记录审计。
7. 提交后返回 BOUND。设备落盘 ACK 和供应商调用均发生在本事务之后。
取消和确认采用相同锁顺序。已绑定后再取消,只返回“绑定已完成”,不删除 owner。不同账号同时申请时返回 CLAIM_BUSY,不泄露另一个账号的完整信息。
### 7.2 其他写入口的处理
| 当前入口 | 新设备的规则 | 旧设备的兼容处理 |
|---|---|---|
| /api/v1/device-claims 绑定码 | 拒绝直接建归属,返回 DEVICE_CONFIRMATION_REQUIRED 和受控流程说明 | 保留原流程,但重试复核有效绑定 |
| 运维 Create/Patch 的预分配 | 对新模式禁止直接指定家庭/使用者;普通运维不能模拟设备按键 | 原设备按现有权限执行,并统一记录代数变化 |
| 运维删除/吊销 | 撤销归属及旧会话/命令,记录不可用状态;不能把设备重新开放给任意账号 | 保持原停用业务,复用新失效清理 |
| binding-change | 新模式走原账号授权解绑,目标账号重新申请并由设备确认 | 旧模式保持现有变更前检查,写入代数与审计 |
| /device/v3/factory-reset | 新模式只执行已获原账号或受控售后授权的完整重置;无授权不能解绑 | 保留旧产品行为并明确其语义;不可作为普通重配网接口 |
| 隐私删除处理器 | 解除对应使用者设备关系、递增代数、撤销旧流程与个性化配置 | 同样接入统一归属服务,避免绕过代数更新 |
最后一项来自当前 `internal/privacy/delete_processor.go` 的直接解绑写入,实施时必须覆盖。不能只修改可见绑定页面。
新设备普通重配网保留身份、owner 和供应商身份;需要云端记录时仅报告网络操作结果。受控售后解绑的申请、执行账号、理由和证据独立审计,不纳入首次绑定的快捷入口。
## 8 状态回执和收尾
binding_state 为 UNBOUND、CLAIM_PENDING、BOUND、TRANSFER_PENDING;activation_state 为 NOT_STARTED、RESERVED、CONTACT_POSSIBLE、REGISTERED_OBSERVED、READY、RECONCILE_REQUIRED、BLOCKED。现有 lifecycle 字段保留作兼容投影,由同一状态服务更新,避免多个模块各写一套最终事实。
device_revision 只随新协议公开状态变化;last_seen 更新不必增加该版本。STATE1 使用 device_id、device_revision、binding_generation、binding_state、activation_state 五个字段,前置 STATE1 后共六行;platform_device_id 等新增展示字段不加入原 hash 公式。
每次版本变更保存状态快照。C07 收到同版本同 hash 返回成功;过期代数或未知 hash 返回 409 并提示同步。local_generation 在设备和有效启动上下文中验证;若设备存储发生恢复,不以降低云端代数的方式迁就本地状态。
绑定与激活可继续并行:C07 ACK 只表示保存当前状态,Web ACK 表示看到了当前绑定。收尾以同一 binding_id/generation 的有效设备保存证据和页面接收证据为依据;页面收到较早 revision 不得被误当成看到了另一次绑定。
设备和页面都确认后等待 3 秒关闭;只有设备确认时最多 30 秒;AI 激活不延长该时间。C11 失败时设备可按本地安全上限关闭 AP,随后补报;SaaS 保存关闭事实但不删除原激活任务。
setup 过期或关闭后不再签发 handoff 和新 claim;认证设备可通过 C03 建 RECOVERY 上下文,再经 C05 获取自身状态、原任务和必要配置。恢复入口不授予新的账号所有权。
## 9 激活模块改造
### 9.1 将现有同步 Provision 拆成持久化阶段
当前 `internal/hardwareprovision/service.go` 在数据库事务持锁期间调用供应商注册。改造后分为:
1. 短事务内固定 provider_device_identity 和 activation_id,核验当前绑定与权益,创建 SERVER_REGISTER attempt,持久化 CONTACT_POSSIBLE,提交。
2. worker 在事务外调用供应商,携带稳定 device_name;不持有数据库长事务。
3. 新短事务保存明确结果和证据,加密 DeviceSecret,更新状态并发布 APPLY_ACTIVATION 命令。
worker 取到 outbox 重试事件时先检查业务任务状态。已有 CONTACT_POSSIBLE 但没有明确结果,进入对账;租约过期不能直接重新调用供应商。fence_version 用于拒绝旧 worker 覆盖新状态,但不把本地租约当成远端请求已取消的证据。
### 9.2 成功与未知结果
| 结果 | 处理 |
|---|---|
| 明确未发出请求的本地前置失败 | 保存错误;修复后可按策略创建新 attempt |
| 明确注册成功并取得合法凭据 | 保存供应商身份与 REGISTERED_OBSERVED;不直接写 READY |
| 明确授权不足/过期 | BLOCKED,显示服务待处理;保留绑定 |
| 注册请求超时、崩溃或回包未知 | RECONCILE_REQUIRED;保留原身份、任务和可能占用的权益 |
| 设备未保存配置 | 保持已知供应商结果,重发相同有效配置或生成新的配置版本 |
| 设备引擎初始化失败 | 记录 DEVICE/ENGINE_INIT 尝试,保留已知注册结果;不凭此释放供应商权益 |
| 已验证首通测试成功 | 满足证据、权限和保存条件后进入 READY |
对账通过经验证的供应商查询能力或受控人工证据完成。若供应商不提供可靠查询,不虚构自动对账 API;保持待处理,由运维基于供应商记录执行有审计的结论操作。
供应商身份按物理设备固定。换绑定时更新个性化配置版本并使旧代数失效,不重新分配 provider device_name 或盲目再注册。旧 attempt 的晚到结果可记录物理设备供应商事实,但不能恢复上一位用户的配置或推进新用户的 READY。
### 9.3 设备配置和 SDK 兼容验证
新的配置包含 contract_version、device_id、activation_id、config_version、binding_generation、expires_at_ms、config_digest、instance_id、product_key、device_name、device_secret、bot_id。通过设备 HTTPS 通道交付,并以 CFG1 HMAC 验证完整性。
ProductSecret 只在 SaaS worker 的供应商适配器中使用。配置不放进 Web status、普通 outbox payload 或明文幂等缓存。outbox 只记录引用,设备拉取时重新核对代数、任务和有效期。
必须验证当前 SDK 能使用已注册 DeviceSecret,且不会无条件触发另一轮动态注册。若该能力不成立,阻断此激活路线的发布并回到协议设计,不在代码里静默双重注册。此项只影响真实激活发布,不阻止先完成认证与绑定。
### 9.4 首通测试与日常通话
不能让“只有 READY 才能创建任何会话”和“必须完成首通测试才能 READY”互相阻塞。新设备达到 REGISTERED_OBSERVED、有效绑定、运行配置已 ACK 且用户明确发起测试后,允许携带 C08 的短时测试许可创建专用测试会话。
拟扩展现有 `/device/v3/agent-sessions`:仅新协议支持 intent=activation_test、activation_id、attempt_id、permit_token;正常设备通话仍检查当前绑定、配置、权限与服务状态。测试路径有时长/次数限制,不构成绕过普通服务权限的入口。
C09 的测试 evidence 至少关联 conversation_id、首次有效下行音频观测、音频检查结果、SDK/固件版本和结束状态;服务端与该次会话记录核对。只收到 session_started 或媒体连接事件不能写 READY。挂机准备不自动采集环境音,测试必须由用户明确触发。
### 9.5 新旧设备如何调用现有会话接口
新增统一 DevicePrincipal 适配:先根据已登记的设备身份与 enrollment_mode 选择唯一允许的认证方案,再交给现有内部服务。新设备在无 query 的现有 v3 会话/事件及 v2 上报路径可使用 DEVREQ1,旧设备继续旧五行签名;不根据 User-Agent 判断协议,不在验签失败后回退另一算法。
含 query 的旧 GET 接口不供新协议复用,新设备使用 C02/C05 等新 POST 路径。若以后需要 OTA 下载、媒体查询等功能,必须另列适配清单或提供无 query 的新入口;不能宣称本次首次接入方案已经覆盖所有旧设备能力。
现有 `/device/v3/hardware-agent-credentials:provision` 对旧模式保持原约定;新模式只读取已有已确认凭据,未准备完成返回当前任务状态或引导 C05,不能再次触发同步注册。凭据读取同样必须检查当前有效绑定。
## 10 前端和代码修改位置
| 模块或路径 | 修改内容 |
|---|---|
| internal/onboardingauth 新增 | DEVREQ1、TIME1、版本密钥解析、限长校验和新身份上下文 |
| internal/onboarding 新增 | C01 至 C07、C11、setup/handoff、状态快照和命令投递 |
| internal/devicebinding 新增 | 统一的事务内绑定、解绑、代数推进和旧上下文失效 |
| internal/activationjob 新增 | 固定任务、attempt、许可、权益、结果和对账 |
| internal/websession / internal/smsauth 新增 | OIDC 接续、短信验证、平台会话和 CSRF |
| internal/httpapi/router.go 及新增 handlers | 按契约挂载新路由、独立响应封装、显式鉴权分组 |
| internal/opsdevice/service.go | 工厂库存入口、凭据交付及所有归属写入的模式限制 |
| internal/saas/device_claim.go、device_binding_service.go | 旧入口新设备门禁、共同绑定事务和权限复核 |
| internal/devicereset/service.go | 区分普通重配网和获授权完整重置,撤销旧流程 |
| internal/privacy/delete_processor.go | 隐私解绑接入代数和状态失效流程 |
| internal/hardwareprovision/service.go | 从同步注册拆到 worker,保留已确认凭据读取和加密能力 |
| internal/idempotency/executor.go | ExecuteTx、事务内缓存命中权限/代数检查、敏感响应引用 |
| internal/outbox 和 worker 注册处 | 绑定/激活事件处理;网络重试与外部未知结果分开 |
| internal/identity、internal/app、internal/config | 账号解析统一、服务装配、新配置和按设备开关 |
| web/src/pages 下新增 Onboarding 页面 | 入口兑换、登录接续、档案选择、按键等待、保存和激活进度 |
| web/src/pages/SaaSWorkspace.tsx | 新设备不展示直接绑定码流程;正常账号设备列表显示分阶段状态 |
| web/src/pages/OpsDeviceWorkspace.tsx | 工厂编号/MAC、绑定历史、ACK、激活异常、受控对账操作 |
| web/src/api/client.ts、schema.d.ts | 从更新后的契约同步请求与返回类型 |
| openapi/openapi.yaml 和兼容说明 | 增加新契约及身份编码;保留 v2/v3 旧契约和旧测试 |
| internal/contractgovernance | 新增 ONBOARDING 契约检查、敏感字段边界和接口覆盖检查 |
前台文案按实际事实显示:正在认证、等待按键、已绑定但设备保存中、AI 服务准备中、待处理、可用。设备重连和手动刷新复用原 setup/claim/activation,不自动发新验证码或再建任务。
运维页面新增读取接口建议:`GET /api/v1/ops/devices/{id}/onboarding`、`GET /api/v1/ops/devices/{id}/activation-jobs`。受控对账建议 `POST /api/v1/ops/activation-jobs/{id}/reconciliations`,提交 conclusion、evidence_ref、reason、expected_version;权限限制到专门能力,不能用一个普通“重试”按钮解除未知结果。
## 11 错误与恢复规则
| 错误或状态 | HTTP | 客户端下一步 |
|---|---|---|
| DEVICE_AUTHENTICATION_FAILED | 401 | 停止敏感请求;检查密钥、时钟或受控恢复 |
| DEVICE_ACCESS_DENIED | 403 | 设备停用、租户或账号权限问题,显示联系支持 |
| REQUEST_BODY_CONFLICT | 409 | 同请求号不同内容,修复调用方 |
| DEVICE_CONFIRMATION_REQUIRED | 409 | 进入按键绑定流程,不能改用旧接口绕过 |
| CLAIM_BUSY | 409 | 等当前申请结束或由有权会话取消 |
| CLAIM_EXPIRED / HANDOFF_EXPIRED | 410 | 重新申请短期对象,保持设备和既有绑定 |
| BINDING_GENERATION_CONFLICT / STATE_MISMATCH | 409 | 查询当前状态,丢弃旧配置或旧确认 |
| CONTRACT_UNSUPPORTED | 426 | 使用双方支持的固件和契约,不猜测枚举含义 |
| RATE_LIMITED | 429 | 按 retry_after_ms 退避 |
| ACTIVATION_RECONCILE_REQUIRED | 200 状态查询或 409 新调用申请 | 展示待对账;禁止自动再注册 |
| DEPENDENCY_UNAVAILABLE | 503 | 保留业务 ID、原 body,更新时间和 nonce 后重试 |
错误响应携带 request_id、retry_after_ms、recovery_action 和可公开的错误原因,默认退避 2、5、15、30 秒,最大 60 秒并加抖动。配置不得通过错误详情泄露。
## 12 分阶段开发和交付物
每阶段完成其必需测试后进入下一阶段,工期由团队人数和供应商适配验证后排定,不用未经评估的天数替代验收条件。
| 阶段 | 工作项 | 可验收的交付物 | 依赖 |
|---|---|---|---|
| P0 兼容保护与契约 | 固定 ID/密钥/代数/租户规则;新增数据结构;所有旧归属入口加入模式门禁;OpenAPI 草案 | 数据迁移演练和旧接口回归通过,新模式尚未对产线开放 | 无 |
| P1 生产登记与认证 | 库存去重、凭据交付/放行、C01/C02、新 HMAC、幂等 guard | 模拟设备可受控登记并重复认证;测试向量与旧设备均通过 | P0 |
| P2 完整绑定流程 | C03 至 C07/C11、handoff、OIDC 接续、申请和确认事务、ACK、收尾页面 | 两个账号并发、回包丢失和断电恢复均不重复绑定;前台显示已绑定待开通 | P1 |
| P3 独立激活 | outbox worker、固定供应商身份、尝试与对账、C08 至 C10、配置 ACK、首通测试 | 明确成功与未知结果均可恢复,只有真实验收后 READY | P2;SDK 凭据模式及供应商行为验证 |
| P4 完整手机入口与发布 | 短信/账号注册、租户策略、正常列表恢复、运维对账、灰度观测 | 文档目标手机流程完成,真实短信和设备联调通过后扩大批次 | P2;P3 通过后才发布完整 AI 可用流程 |
建议按以上阶段拆 PR。P0/P1 可以独立评审,P2 可先用模拟激活验收,不等待真实火山调用。短信适配可在接口固定后与 P3 由团队独立实施,但完整验收必须等真实提供方接入。
完整交付应包含:迁移脚本、API 契约、后端与页面修改、设备调用示例和 HMAC 向量、故障测试记录、灰度及恢复手册、供应商模式验证结论。不能只以新增路由都返回 200 作为完成标准。
## 13 迁移 灰度和回退
1. 发布 P0 兼容保护版本,所有新开关默认关闭,旧设备正常回归。数据库只增加结构,保留原列和旧凭据。
2. 回填旧设备 legacy 标记、既有供应商身份和可验证的绑定历史。不能把旧 ACTIVE 自动当成新 READY;有真实证据才迁入 READY,否则保留旧模式直到完成迁移验收。
3. 在测试租户、单个受控工厂批次启用 enrollment_mode=physical_confirm。已有设备迁移必须核对实际 MAC、身份映射、密钥交付和固件能力,不能靠数据库批量切标记完成。
4. 分别控制新建入口、认领、激活外部执行、恢复查询四类开关。暂停新建不关闭已提交绑定的查询/ACK,不让设备因发布回退而失去恢复原任务的路径。
5. 灰度检查注册成功率、handoff 兑换率、确认成功率、状态保存延迟、激活对账积压、nonce 拒绝和 409 分类;指标只记录关联 ID,不包含秘密。
6. 回退到至少包含 P0 保护和恢复能力的版本;不执行删除新表的 down migration,不把 physical_confirm 设备改回 legacy 来绕过故障。
7. 停止新的供应商调用后,保留 CONTACT_POSSIBLE/RECONCILE_REQUIRED,等待已有调用和对账处理。不得回收可能已经占用的权益。
建议配置分别表达入组范围、入口启用、激活 worker 启用、恢复读取保留和短信发送启用。前端按钮隐藏只控制展示,所有权限与模式门禁在服务端执行。
## 14 测试和验收清单
| 测试类别 | 必测情形 | 通过标准 |
|---|---|---|
| 生产身份 | 同 MAC 重试、登记回包丢失、型号/租户冲突、返修、密钥轮换中断 | 同一设备不变;冲突不覆盖;可恢复原交付任务 |
| 新旧鉴权 | 附件 H 向量、旧 UTF-8 密钥、原始 JSON 空格变化、版本错误、query、超限 JSON | 各自按契约通过;错误不回退另一算法 |
| 时间和重放 | 首次错误时钟、旧挑战、同 nonce、不同 boot_id、Redis 中断 | 不接受旧挑战或跳过防重放,不关闭业务 TLS |
| 幂等和权限 | 同请求同 body、同请求不同 body、缓存命中前停用/换绑、24 小时后旧 claim | 有效操作不重复;旧权限和旧状态不可复活 |
| Handoff | 双兑换、过期、verifier 错误、Cookie 回包丢失、OIDC 回跳 | 一次兑换且可恢复;不把入口当所有权 |
| 短信与租户 | 重发、穷举、供应商超时、注册重试、已有 OIDC 账号、企业租户无邀请 | 不重复建账号、不自动越权、不错误合并身份 |
| 绑定竞争 | 双账号认领、确认与取消并发、权限撤销、申请过期后的按键 | 唯一有效归属;无当前设备确认不能提交 |
| 状态保存 | 提交后丢包、设备 ACK 重复或旧版本、页面 ACK 旧绑定、保存中断电 | 云端绑定不回滚;只承认对应代数和状态证据 |
| 旧入口覆盖 | 绑定码、运维预分配、解绑/转移、恢复出厂、隐私删除 | 新模式无确认绕过;代数和旧上下文一致失效 |
| 激活 worker | 外部调用前崩溃、调用中崩溃、成功后数据库失败、重复 outbox、租约过期 | 同一可能已调用的任务不被自动再次执行 |
| 供应商与权益 | 额度不足、已注册查询、未知结果、人工对账、换绑期间晚到结果 | 保留身份和可能占用权益,证据可追踪 |
| 首通及运行时 | DeviceSecret 模式、测试许可重放、媒体已连无首音频、正常首通、立即挂机 | 不重复注册;无真实测试证据不 READY;待机不采音 |
| 浏览器恢复 | 手机切网、门户关闭、普通浏览器重开、登录超时、设备已绑定后再进旧入口 | 恢复当前状态,不要求重做已成功步骤 |
| 发布回退 | 新旧设备同时在线、停用新建入口、P0 版本回退、恢复服务继续 | 旧设备可用,新设备既有绑定和任务不丢失 |
后端使用 PostgreSQL/Redis 集成测试验证并发与事务,不用 mock 替代唯一约束和进程中断测试。前端使用现有 Playwright 流程验证手机和桌面页面。真实供应商、短信和设备联调记录单列,静态代码或模拟服务通过不等于实机通过。
## 15 关键代码依据
- [运维建档与预分配](E:/Codex/story/platform/internal/opsdevice/service.go:244)
- [当前设备签名验证](E:/Codex/story/platform/internal/deviceauth/verifier.go:69)
- [现有幂等响应直接重放](E:/Codex/story/platform/internal/idempotency/executor.go:145)
- [当前绑定码提交](E:/Codex/story/platform/internal/saas/device_claim.go:27)
- [当前解绑和转移](E:/Codex/story/platform/internal/saas/device_binding_service.go:67)
- [恢复出厂的解绑写入](E:/Codex/story/platform/internal/devicereset/service.go:168)
- [隐私删除中的解绑写入](E:/Codex/story/platform/internal/privacy/delete_processor.go:302)
- [供应商注册当前执行方式](E:/Codex/story/platform/internal/hardwareprovision/service.go:127)
- [可复用的事务 outbox](E:/Codex/story/platform/internal/outbox/store.go:40)
- [现有 OIDC 入口](E:/Codex/story/platform/web/src/auth/oidc.ts:6)
- [当前账号与租户映射](E:/Codex/story/platform/internal/identity/repository.go:71)
- [原始差异分析](E:/Codex/story/platform/docs/首次配网设备认证与绑定_SaaS支撑功能分析.md)