# TalkingQ 硬件端注册、临时 Token、OTA 全流程接口文档 版本:2026-05-08 适用对象:硬件固件、产测工具、工厂烧录工具开发同事 代码依据:`gateway`、`register`、`token`、`ota` 当前源码 ## 1. 总览 默认统一从 Gateway 调用: ```text http://:8090 ``` 不建议设备或工厂工具直连下游服务端口。Gateway 会统一做外部鉴权、限流、单飞控制,并自动给下游服务补充内部调用签名 `X-Internal-*`。 下游服务端口仅用于定位问题: | 服务 | 默认端口 | Gateway 路由前缀 | |---|---:|---| | gateway | 8090 | - | | register | 8097 | `/register/**` | | token | 8095 | `/token/**` | | ota | 8096 | `/ota/**` | ## 2. 全流程逻辑示意图 ```mermaid flowchart TD A[工厂产测/烧录工具] --> B{是否已有设备记录} B -->|无| C[调用 /register/api/v1/devices 或 /batch 创建设备] C --> C1[Gateway X-Factory-* HMAC 鉴权] C1 --> C2[Register 校验 X-Api-Key 是否启用/配额是否足够] C2 --> D[写入 sys_device 和 device_app 配置] B -->|有| E[设备生成 P-256 密钥对] D --> E E --> F[调用 /register/api/auth/challenge] F --> F1[首绑/重绑阶段需要 station_id + sig_b64 工位私钥签名] F1 --> G[服务返回 challenge_id/challenge] G --> H[设备用设备私钥签名 challenge 消息] H --> I[调用 /register/api/auth/response] I --> I1[服务验签后签发设备 JWT access_token] I1 --> J[设备携带设备 JWT 调用 /token/api/device/token] J --> K[Token 服务返回阿里云 DashScope 临时 token/appId/workspaceId/voiceId] K --> L[设备连接阿里云 WebSocket/语音链路] L --> M[设备定期调用 /ota/api/ota/versionInfoReport 上报固件版本] M --> N{是否需要升级} N -->|否| O[HTTP 204 No Content] N -->|是| P[返回最新 version/url/sha256/sizeBytes/force/update] P --> Q[设备下载固件并校验 SHA-256] Q --> R[设备升级重启后再次 versionInfoReport] ``` 鉴权分层: ```mermaid flowchart LR U[外部调用方] --> G[Gateway] G -->|/register/api/v1/**| FH[X-Factory-* HMAC + X-Api-Key] G -->|/token/** /ota/api/ota/**| DJ[设备 JWT ES256 + scope] G -->|/ota/api/program/**| PJ[小程序设备 JWT HS256, type=device] G -->|转发到下游| IH[Gateway 自动注入 X-Internal-*] FH --> R[register] DJ --> T[token/ota] PJ --> O[ota program] IH --> R IH --> T ``` ## 3. 通用约定 | 项 | 说明 | |---|---| | Content-Type | JSON 接口统一使用 `application/json` | | 时间戳 | 除特别说明外,均为 Unix 秒级时间戳 | | Base64 | `sig_b64` 使用标准 Base64;设备公钥、challenge、设备签名使用 base64url 且无 padding | | Authorization | 设备 JWT 调用格式:`Authorization: Bearer ` | | 成功响应包装 | `AjaxResult`/`ResultMessage` 通常为 `{"code":200,"message":"...","data":...}` | | 业务错误包装 | Register 认证错误通常为 `{"error_code":"...","message":"..."}`;Gateway 鉴权失败通常为 HTTP 401/403 | ## 4. Gateway 工厂 HMAC 鉴权 当前代码中 Gateway 工厂鉴权只拦截: ```text /register/api/v1/** ``` 也就是以下设备注册接口必须带 `X-Factory-*`: ```text GET /register/api/v1/devices/{deviceId} POST /register/api/v1/devices POST /register/api/v1/devices/batch ``` `/register/api/factory/**` 当前不在 Gateway `factory-auth.include-paths` 内,这类接口主要依赖请求体里的工位私钥签名 `sig_b64`。如果未来希望它们也经过 Gateway HMAC,需要把 Gateway 配置里的 `gateway.factory-auth.include-paths` 扩展到 `/register/api/factory/**`。 ### 4.1 必填请求头 | Header | 必填 | 说明 | |---|---|---| | `X-Api-Key` | 是 | 工厂注册 API Key;Gateway 查 Redis 是否启用,Register 查数据库额度 | | `X-Factory-Timestamp` | 是 | Unix 秒级时间戳,默认允许偏移 300 秒 | | `X-Factory-Nonce` | 是 | 每次请求唯一随机串,默认 300 秒内不可重复 | | `X-Factory-Signature` | 是 | HMAC-SHA256 签名后标准 Base64 | ### 4.2 待签名 payload Gateway 按下面 6 行拼接,换行符是 `\n`: ```text ``` 字段规则: | 字段 | 说明 | |---|---| | `HTTP_METHOD` | 大写方法名,例如 `POST` | | `RAW_PATH` | 原始路径,例如 `/register/api/v1/devices` | | `RAW_QUERY` | 原始 query,不含 `?`;没有 query 时为空字符串 | | `BODY_SHA256_HEX` | 请求体字节的 SHA-256 小写 hex;请求体为空时是空字符串,不是空 body 的 SHA-256 | | `timestamp` | 与请求头完全一致 | | `nonce` | 与请求头完全一致 | 签名计算: ```text X-Factory-Signature = Base64( HMAC-SHA256(FACTORY_AUTH_SECRET, payload_utf8) ) ``` 注意: | 项 | 说明 | |---|---| | `FACTORY_AUTH_SECRET` | 后端环境变量 `FACTORY_AUTH_SECRET`,必须由服务端安全下发给工厂工具,不应写死在固件里 | | Redis 启用状态 | Gateway 会检查 `gateway:factory:api-key:enabled:{apiKey}` | | DB 启用/配额 | Register 会检查 `factory_register_quota` 是否启用、未过期、额度足够 | | 防重放 | 同一 `apiKey + nonce` 默认 300 秒内只能使用一次 | ### 4.3 Python 生成 Gateway 签名示例 ```python import base64 import hashlib import hmac import json import time import uuid api_key = "factory-api-key" secret = "factory-auth-secret" method = "POST" raw_path = "/register/api/v1/devices" raw_query = "" body_obj = { "deviceId": "550e8400-e29b-41d4-a716-446655440000", "deviceName": "0508-0001", "userId": 1, "appId": "mm_xxx", "workspaceId": "llm_xxx", "voiceId": "kenny", "product": "TQ_cat", "version": "1.0.0" } body = json.dumps(body_obj, ensure_ascii=False, separators=(",", ":")).encode("utf-8") timestamp = str(int(time.time())) nonce = uuid.uuid4().hex body_hash = hashlib.sha256(body).hexdigest() if body else "" payload = "\n".join([method, raw_path, raw_query, body_hash, timestamp, nonce]) signature = base64.b64encode( hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).digest() ).decode("ascii") headers = { "Content-Type": "application/json", "X-Api-Key": api_key, "X-Factory-Timestamp": timestamp, "X-Factory-Nonce": nonce, "X-Factory-Signature": signature, } ``` ## 5. 工位私钥签名 `sig_b64` 工位签名用于工厂刷写/解锁/上报,以及设备首次绑定或强制重绑时证明“这台设备是在合法工位上生产/绑定”。 涉及接口: ```text POST /register/api/factory/flash-key POST /register/api/factory/unlock POST /register/api/factory/report POST /register/api/auth/challenge 首绑/重绑或 pending 阶段需要 POST /register/api/auth/response pending 阶段需要 ``` ### 5.1 工位密钥配置 Register 服务通过环境变量 `DEVICE_AUTH_FACTORY_STATION_SECRETS_PATH` 指定工位公钥 JSON 文件。支持如下形式: ```json { "station-001": { "factory_id": "factory-a", "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" } } ``` 也支持 `public_key`、`public_key_pem_b64`、`public_key_b64` 字段。 ### 5.2 待签名 JSON 服务端校验规则: 1. 从请求体中移除 `sig_b64`。 2. 对剩余 JSON 按 key 排序序列化。 3. 使用工位公钥验签。 4. 校验 `timestamp` 默认 300 秒时间窗。 5. 将 `station_id + nonce` 写入数据库防重放。 推荐工厂工具按如下规则生成待签名 bytes: ```python canonical = json.dumps(payload_without_sig, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8") ``` 签名算法按工位公钥类型自动选择: | 工位公钥类型 | 服务端验签算法 | `sig_b64` 格式 | |---|---|---| | EC | `SHA256withECDSA` | DER ECDSA 签名字节再标准 Base64 | | RSA | `RSASSA-PSS` + SHA-256 | 签名字节标准 Base64 | | DSA | `SHA256withDSA` | 签名字节标准 Base64 | | Ed25519/Ed448 | `Ed25519`/`Ed448` | 签名字节标准 Base64 | 如果工位使用 P-256 EC 私钥,`sig_b64` 必须是 DER ECDSA 签名的标准 Base64,不是 raw `r||s`。 ### 5.3 Python 生成工位签名示例 ```python import base64 import json import time import uuid from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec station_private_key_pem = b"""-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----""" private_key = serialization.load_pem_private_key(station_private_key_pem, password=None) payload = { "station_id": "station-001", "timestamp": int(time.time()), "nonce": uuid.uuid4().hex, "device_id": "550e8400-e29b-41d4-a716-446655440000" } canonical = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8") sig_der = private_key.sign(canonical, ec.ECDSA(hashes.SHA256())) payload["sig_b64"] = base64.b64encode(sig_der).decode("ascii") ``` ## 6. 设备 P-256 认证协议 设备认证成功后,Register 服务签发 ES256 设备 JWT。该 JWT 后续用于调用 Token 和 OTA 设备接口。 ### 6.1 设备公钥 `pub_key` 设备使用 P-256,也就是 `secp256r1`。`pub_key` 格式: ```text base64url_no_padding( 0x04 || X[32] || Y[32] ) ``` 其中 `X`、`Y` 是 P-256 公钥坐标,固定 32 字节大端。 ### 6.2 公钥指纹 `pub_key_jkt` 服务端按 JWK thumbprint 思路计算: ```json {"crv":"P-256","kty":"EC","x":"","y":""} ``` 然后: ```text pub_key_jkt = base64url_no_padding( SHA256( compact_sorted_jwk_json_utf8 ) ) ``` ### 6.3 challenge 签名消息 设备调用 `/register/api/auth/challenge` 后,服务返回 `challenge_id` 和 `challenge`。设备用设备私钥签名下面的原始字节: ```text UTF8("robot-auth-v1") + UUID_TO_16_BYTES(device_id) + base64url_decode(challenge_id) + base64url_decode(challenge) + base64url_decode(pub_key_jkt) + int32_be(key_version) ``` 签名算法:P-256 ECDSA + SHA-256。 提交给服务端的 `signature` 格式: ```text base64url_no_padding( raw_r_32 || raw_s_32 ) ``` 注意这里和工位签名不同:设备签名要求 raw `r||s`,不是 DER。 ### 6.4 rotate 签名消息 密钥轮换时,设备用当前私钥签名: ```text UTF8("robot-rotate-v1") + UUID_TO_16_BYTES(device_id) + int32_be(current_key_version) + base64url_decode(new_pub_key_jkt) + base64url_decode(new_pub_key) ``` 提交格式仍是: ```text base64url_no_padding( raw_r_32 || raw_s_32 ) ``` ### 6.5 设备 JWT 内容 Register 签发的 JWT: | Claim | 说明 | |---|---| | `sub` | `device_id` | | `exp` | 默认当前时间 + 600 秒 | | `scope` | 默认 `ota:report ota:read ota:confirm voice:session` | | `kv` | 当前设备密钥版本 | Gateway 对 `/token/**` 和 `/ota/**` 会校验: 1. JWT ES256 签名正确。 2. `exp` 未过期。 3. scope 命中接口规则。 4. Redis 中 `gateway:device:auth:{deviceId}` 的 `auth_status=active`。 5. JWT `kv` 等于当前 `key_version`,或等于仍在宽限期的 `prev_key_version`。 ## 7. 推荐硬件调用顺序 ### 7.1 工厂生产阶段 1. 工厂工具生成设备 UUID `deviceId` 和序列号 `serialNumber`。 2. 调用 `POST /register/api/v1/devices` 或 `/batch` 创建设备和默认应用配置。 3. 设备生成 P-256 密钥对。 4. 工厂工具或设备调用 `POST /register/api/auth/challenge`,首绑时带 `station_id/timestamp/nonce/sig_b64/pub_key/key_version`。 5. 设备签名 challenge 后调用 `POST /register/api/auth/response` 获取设备 JWT。 6. 设备保存私钥、`device_id`、`key_version`。不要保存工厂 HMAC secret 或工位私钥到设备固件。 ### 7.2 设备运行阶段 1. 若没有有效设备 JWT,重新走 challenge/response。 2. 携带设备 JWT 调用 `POST /token/api/device/token` 获取阿里云临时 token。 3. 使用返回的 `token/appId/workspaceId/voiceId` 接入语音链路。 4. 定期或开机调用 `POST /ota/api/ota/versionInfoReport` 上报固件版本。 5. 收到 204 表示无需升级;收到 200 且 `data.notUptodate=1` 表示需要升级。 6. 下载固件,校验 `sha256` 和 `sizeBytes`,升级后再次上报版本。 ## 8. Register 接口 ### 8.1 创建设备 ```text POST /register/api/v1/devices ``` 鉴权: | 层级 | 要求 | |---|---| | Gateway | 必须通过 `X-Factory-*` HMAC | | Register | 必须通过 `X-Api-Key` 数据库启用/配额校验 | 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `deviceId` | string | 是 | 设备 UUID,后续设备认证要求 UUID 格式 | | `userId` | integer | 是 | 绑定用户 ID | | `deviceName` | string | 否 | 设备名称/序列号;为空时服务端生成 `MMdd` | | `batchId` | string | 否 | 批次 | | `clientId` | integer | 否 | 客户/渠道 ID | | `functionNames` | string | 否 | 功能描述 | | `ip` | string | 否 | 工厂或设备 IP | | `location` | string | 否 | 位置信息 | | `wifiName` | string | 否 | Wi-Fi 名称 | | `chipModelName` | string | 否 | 芯片型号 | | `type` | string | 否 | 设备类型 | | `version` | string | 否 | 初始固件版本 | | `state` | string | 否 | 状态,默认 `"1"` | | `product` | string | 否 | 产品标识,用于 OTA 版本匹配 | | `appId` | string | 否 | 阿里云应用 ID;为空使用默认值 | | `workspaceId` | string | 否 | 阿里云 workspace;为空使用默认值 | | `voiceId` | string | 否 | 声音 ID;为空使用默认值 `kenny` | 成功响应: ```json { "code": 200, "message": "设备新建成功,app配置添加成功。", "data": { "deviceId": "550e8400-e29b-41d4-a716-446655440000", "deviceName": "0508-0001", "product": "TQ_cat", "version": "1.0.0" } } ``` ### 8.2 查询设备 ```text GET /register/api/v1/devices/{deviceId} ``` 鉴权同创建设备,必须带 `X-Factory-*` 和 `X-Api-Key`。 响应: ```json { "code": 200, "message": "success", "data": { "deviceId": "550e8400-e29b-41d4-a716-446655440000", "deviceName": "0508-0001", "product": "TQ_cat", "version": "1.0.0" } } ``` ### 8.3 批量创建设备 ```text POST /register/api/v1/devices/batch ``` 鉴权同创建设备。 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `devices` | array | 是 | 设备列表 | | `devices[].deviceId` | string | 是 | 设备 ID | | `devices[].deviceName` | string | 否 | 设备名称 | | `devices[].serialNumber` | string | 是 | 序列号,会写入 `deviceName` 和 `device_app.serialNumber` | | `devices[].batchId` | string | 否 | 批次 | | `devices[].clientId` | integer | 否 | 客户/渠道 ID | | `devices[].product` | string | 否 | 产品标识 | | `userId` | integer | 是 | 用户 ID | | `appId` | string | 是 | 阿里云应用 ID | | `workspaceId` | string | 是 | workspace | | `voiceId` | string | 否 | 声音 ID | | `state` | string | 否 | 状态,默认 `"1"` | 成功响应: ```json { "code": 200, "message": "操作成功", "data": { "createdCount": 2, "appConfigCount": 2 } } ``` ### 8.4 工厂申请刷写密钥 ```text POST /register/api/factory/flash-key ``` 鉴权:请求体必须带工位私钥签名 `sig_b64`。当前 Gateway 不要求 `X-Factory-*`。 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `station_id` | string | 是 | 工位 ID | | `timestamp` | integer | 是 | Unix 秒 | | `nonce` | string | 是 | 防重放随机串,最长 128 | | `device_id` | string | 是 | 设备 ID,最长 128 | | `sig_b64` | string | 是 | 对移除 `sig_b64` 后的请求体做工位签名 | 成功响应: | 字段 | 类型 | 说明 | |---|---|---| | `key_b64` | string | 32 字节刷写密钥的标准 Base64 | | `issued_at` | string | UTC ISO 时间 | | `expires_at` | string | UTC ISO 过期时间 | | `expires_in` | integer | 默认 600 秒 | | `server_time` | integer | 服务端 Unix 秒 | | `factory_id` | string | 工位配置中的工厂 ID,可能为空 | | `station_id` | string | 工位 ID | | `sig_b64` | string | 服务端用工厂私钥签名响应 | | `alg` | string | 响应签名算法,例如 `ecdsa-sha256` | ### 8.5 工厂申请解锁凭据 ```text POST /register/api/factory/unlock ``` 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `station_id` | string | 是 | 工位 ID | | `timestamp` | integer | 是 | Unix 秒 | | `nonce` | string | 是 | 防重放随机串 | | `device_mac` | string | 是 | MAC,格式 `AA:BB:CC:DD:EE:FF` | | `purpose` | string | 是 | 默认必须为 `factory_unlock` | | `proto_version` | integer | 是 | 默认必须为 `1` | | `sig_b64` | string | 是 | 工位签名 | 成功响应包含: ```json { "device_mac": "aa:bb:cc:dd:ee:ff", "purpose": "factory_unlock", "proto_version": 1, "issued_at": "2026-05-08T01:00:00Z", "expires_at": "2026-05-08T01:05:00Z", "expires_in": 300, "server_time": 1778192400, "nonce": "base64url-random", "factory_id": "factory-a", "station_id": "station-001", "sig_b64": "...", "alg": "ecdsa-sha256" } ``` ### 8.6 工厂上报刷机/注册结果 ```text POST /register/api/factory/report ``` 请求体必填: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `station_id` | string | 是 | 工位 ID | | `timestamp` | integer | 是 | Unix 秒 | | `nonce` | string | 是 | 防重放随机串 | | `device_id` | string | 是 | 设备 ID | | `firmware_version` | string | 是 | 烧录固件版本 | | `steps.flash.status` | string | 是 | 刷写步骤状态 | | `steps.register.status` | string | 是 | 注册步骤状态 | | `success` | boolean/integer | 否 | 是否成功 | | `operator` | string | 否 | 操作员 | | `sig_b64` | string | 是 | 工位签名 | 成功响应: ```json { "status": "ok", "report_id": "uuid", "server_time": 1778192400 } ``` ### 8.7 发起设备认证挑战 ```text POST /register/api/auth/challenge ``` 鉴权: | 场景 | 是否需要工位签名 | |---|---| | 设备未绑定,首次写入 `pub_key` | 是 | | 设备 pending 首绑未完成 | 是 | | `force_rebind=true` 强制重绑 | 是 | | 设备已 active 且 `key_version/pub_key` 匹配 | 否 | 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `device_id` | string | 是 | UUID 字符串 | | `key_version` | integer | 是 | 正整数,首次一般为 `1` | | `pub_key` | string | 首绑/重绑必填 | P-256 未压缩点 base64url | | `force_rebind` | boolean | 否 | 强制重绑新公钥 | | `station_id` | string | 视场景 | 工位 ID | | `timestamp` | integer | 视场景 | Unix 秒 | | `nonce` | string | 视场景 | 防重放随机串 | | `sig_b64` | string | 视场景 | 工位签名 | challenge 工位签名 payload 只包含非空字段: ```json { "device_id": "...", "key_version": 1, "pub_key": "...", "force_rebind": false, "station_id": "station-001", "timestamp": 1778192400, "nonce": "..." } ``` 成功响应: ```json { "challenge_id": "base64url-16bytes", "challenge": "base64url-32bytes", "expires_in": 60, "server_time": 1778192400 } ``` ### 8.8 提交挑战响应并获取设备 JWT ```text POST /register/api/auth/response ``` 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `device_id` | string | 是 | UUID | | `challenge_id` | string | 是 | challenge 接口返回 | | `signature` | string | 是 | 设备私钥签名,base64url(raw r||s) | | `station_id` | string | pending 阶段必填 | 工位 ID | | `timestamp` | integer | pending 阶段必填 | Unix 秒 | | `nonce` | string | pending 阶段必填 | 防重放随机串 | | `sig_b64` | string | pending 阶段必填 | 工位签名 | 成功响应: ```json { "access_token": "", "token_ttl": 600 } ``` 如果使用上一把仍在宽限期内的 key,响应可能包含: ```json { "access_token": "", "token_ttl": 600, "rotate_required": true, "server_key_version": 2 } ``` ### 8.9 设备密钥轮换 ```text POST /register/api/auth/rotate ``` 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `device_id` | string | 是 | UUID | | `key_version` | integer | 是 | 当前 key version | | `new_pub_key` | string | 是 | 新 P-256 公钥 base64url | | `new_pub_key_jkt` | string | 是 | 新公钥指纹 | | `signature` | string | 是 | 当前私钥对 rotate 消息签名,base64url(raw r||s) | 成功响应: ```json { "key_version": 2 } ``` ## 9. Token 服务所有接口 Token 接口都需要设备 JWT,且必须包含 `voice:session` scope。 通用 Header: ```text Authorization: Bearer Content-Type: application/json ``` ### 9.1 获取阿里云临时 Token ```text POST /token/api/device/token ``` 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `deviceId` | string | 与 `serialNumber` 至少一个 | 设备 ID | | `serialNumber` | string | 与 `deviceId` 至少一个 | 设备序列号 | | `appId` | string | 否 | DTO 中存在,当前设备接口实际使用数据库 `device_app` 配置 | | `workspaceId` | string | 否 | DTO 中存在,当前设备接口实际使用数据库 `device_app` 配置 | | `voiceId` | string | 否 | DTO 中存在,当前设备接口实际使用数据库 `device_app` 配置 | 如果同时传 `deviceId` 和 `serialNumber`,服务端会校验两者是否匹配。 成功响应: ```json { "code": 200, "message": "获取token成功", "data": { "appId": "mm_xxx", "workspaceId": "llm_xxx", "voiceId": "kenny", "token": "dashscope-temp-token", "deviceId": "550e8400-e29b-41d4-a716-446655440000", "serialNumber": "0508-0001", "expires_at": 1778194200, "tokenExpireTime": "2026-05-08T09:30:00", "roleKey": "default", "roleName": "默认角色", "favoriteTeamCode": "", "favoriteTeamName": "", "favoriteTeamStatus": "" } } ``` 说明: | 字段 | 说明 | |---|---| | `token` | 阿里云 DashScope 临时 token,默认申请 1800 秒 | | `expires_at` | 阿里云真实过期秒级时间戳 | | `tokenExpireTime` | 本地时区格式化时间 | | `appId/workspaceId/voiceId` | 设备连接语音服务时需要使用 | | `roleKey/roleName` | 当前应用角色信息,可能为空字符串 | | `favoriteTeam*` | 收藏球队信息,未配置时为空字符串 | Token 服务会优先返回 Redis 缓存中的有效 token;距离过期不足 10 秒时刷新。 ### 9.2 强制刷新阿里云临时 Token ```text POST /token/api/device/token/refresh ``` 鉴权、请求体、响应字段与 `/token` 相同。区别是忽略现有缓存,强制调用阿里云刷新。 成功响应: ```json { "code": 200, "message": "刷新token成功", "data": { "appId": "mm_xxx", "workspaceId": "llm_xxx", "voiceId": "kenny", "token": "new-dashscope-temp-token", "deviceId": "550e8400-e29b-41d4-a716-446655440000", "serialNumber": "0508-0001", "expires_at": 1778194200, "tokenExpireTime": "2026-05-08T09:30:00", "roleKey": "", "roleName": "", "favoriteTeamCode": "", "favoriteTeamName": "", "favoriteTeamStatus": "" } } ``` ## 10. OTA 服务所有接口 ### 10.1 设备上报固件版本并检查升级 ```text POST /ota/api/ota/versionInfoReport ``` 鉴权: | 层级 | 要求 | |---|---| | Gateway | 设备 JWT,有 `ota:report` scope | | OTA 服务 | 设备 JWT 二次校验,有 `ota:report` scope | Header: ```text Authorization: Bearer Content-Type: application/json ``` 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `deviceId` | string | 是 | 设备 ID | | `serialNumber` | string | 否 | 设备序列号,当前代码未参与主要逻辑 | | `version` | string | 是 | 当前固件版本,例如 `1.0.0` 或 `v1.0.0` | | `isUpdate` | integer | 否 | 设备是否确认升级,版本上报接口当前主要使用 `deviceId/version` | 无升级响应: ```text HTTP/1.1 204 No Content Cache-Control: no-store ``` 需要升级响应: ```json { "code": 200, "message": "固件版本信息上报成功", "data": { "version": "1.0.1", "url": "https://download-url-or-short-link", "sha256": "64位hex", "sizeBytes": 1234567, "force": 0, "update": 0, "notUptodate": 1 } } ``` 字段说明: | 字段 | 说明 | |---|---| | `version` | 最新固件版本 | | `url` | 下载地址;如果启用 COS 预签名,会返回短期可用预签名 URL;如果启用短链,可能返回 `/ota/s/{token}` | | `sha256` | 固件 SHA-256 hex,设备下载后必须校验 | | `sizeBytes` | 固件大小,设备可用于下载完整性校验 | | `force` | `1` 强制升级,`0` 非强制 | | `update` | 设备是否已确认升级状态 | | `notUptodate` | `1` 需要升级,`0` 不需要升级 | 异常响应: | 场景 | HTTP | 响应 | |---|---:|---| | `deviceId` 为空 | 400 | `{"code":500,"message":"设备ID不能为空"}` | | `version` 为空 | 400 | `{"code":500,"message":"固件版本不能为空"}` | | OTA 下载链接生成失败且 strict-mode=true | 503 | `{"code":503,"message":"OTA下载链接生成失败","data":{"errorCode":"OTA_DOWNLOAD_URL_UNAVAILABLE","reason":"..."}}` | | JWT 缺失/过期/scope 不匹配 | 401 | Gateway 或服务过滤器返回未授权 | ### 10.2 小程序查询设备版本比对结果 ```text GET /ota/api/program/getVersionInfoCompare/{deviceId} ``` 该接口属于小程序/管理侧设备查询接口,不使用设备 JWT scope。Gateway 按 Program Token 校验: | 要求 | 说明 | |---|---| | Header | `Authorization: Bearer ` | | 签名 | HS256,密钥为 Gateway `jwt.secret` | | Claim `type` | 必须为 `device` | | Claim `sub` | 必须等于路径中的 `{deviceId}` | 成功响应: ```json { "code": 200, "message": "success", "data": { "notUptodate": 1, "version": "1.0.1" } } ``` ### 10.3 小程序上报升级确认 ```text POST /ota/api/program/versionUpdateConfirm ``` 鉴权同上一节 Program Token。Gateway 会从 JSON body 里读取 `deviceId`,并要求 JWT `sub` 与其一致。 请求体: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `deviceId` | string | 是 | 设备 ID | | `isUpdate` | integer | 是 | 是否确认升级,通常 `1` 是、`0` 否 | | `version` | string | 否 | DTO 中存在,当前确认接口不使用 | | `serialNumber` | string | 否 | DTO 中存在,当前确认接口不使用 | 成功响应: ```json { "code": 200, "message": "固件版本升级确认成功" } ``` ### 10.4 OTA 短链跳转 ```text GET /ota/s/{token} ``` 鉴权:无。Gateway 设备 JWT 忽略 `/ota/s/**`。 用途:当 OTA 开启短链时,`versionInfoReport` 返回短链接,设备访问短链接会跳转到真实 COS 预签名 URL。 响应: | 场景 | HTTP | 说明 | |---|---:|---| | token 格式非法 | 404 | 无响应体 | | token 不存在或已过期 | 410 | 无响应体 | | token 有效 | 307 | `Location` 为真实下载 URL | `token` 格式要求: ```text ^[A-Za-z0-9_-]{8,128}$ ``` ## 11. curl 调用示例 ### 11.1 创建设备 ```bash curl -X POST "http://:8090/register/api/v1/devices" \ -H "Content-Type: application/json" \ -H "X-Api-Key: " \ -H "X-Factory-Timestamp: " \ -H "X-Factory-Nonce: " \ -H "X-Factory-Signature: " \ -d '{"deviceId":"550e8400-e29b-41d4-a716-446655440000","deviceName":"0508-0001","userId":1,"appId":"mm_xxx","workspaceId":"llm_xxx","voiceId":"kenny","product":"TQ_cat","version":"1.0.0"}' ``` ### 11.2 设备获取 challenge ```bash curl -X POST "http://:8090/register/api/auth/challenge" \ -H "Content-Type: application/json" \ -d '{"device_id":"550e8400-e29b-41d4-a716-446655440000","key_version":1,"pub_key":"","station_id":"station-001","timestamp":1778192400,"nonce":"","sig_b64":""}' ``` ### 11.3 设备提交 challenge 签名 ```bash curl -X POST "http://:8090/register/api/auth/response" \ -H "Content-Type: application/json" \ -d '{"device_id":"550e8400-e29b-41d4-a716-446655440000","challenge_id":"","signature":""}' ``` ### 11.4 获取阿里云临时 Token ```bash curl -X POST "http://:8090/token/api/device/token" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"deviceId":"550e8400-e29b-41d4-a716-446655440000"}' ``` ### 11.5 OTA 版本上报 ```bash curl -X POST "http://:8090/ota/api/ota/versionInfoReport" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"deviceId":"550e8400-e29b-41d4-a716-446655440000","version":"1.0.0"}' ``` ## 12. 常见问题排查 | 现象 | 优先检查项 | |---|---| | `/register/api/v1/**` 返回 401 | `X-Factory-*` 是否缺失、timestamp 是否超过 300 秒、body hash 是否按真实字节计算、空 body hash 是否误用了 SHA256(empty) | | `/register/api/v1/**` 返回 403 | `X-Api-Key` 是否在 Redis `gateway:factory:api-key:enabled:{apiKey}` 中启用 | | Register 返回 `API_KEY_INVALID_OR_DISABLED` | 数据库 `factory_register_quota` 是否启用、是否过期 | | Register 返回 `API_KEY_QUOTA_EXCEEDED` | 工厂 API Key 注册额度不足 | | 工位签名失败 | canonical JSON 是否排序且紧凑、是否移除了 `sig_b64`、ECDSA 是否用了 DER 签名、`station_id` 是否存在于 secrets 文件 | | challenge 返回 `AUTH_PUB_KEY_MISMATCH` | 已绑定设备上报了不同 `pub_key`,需要确认是否应 `force_rebind=true` | | response 返回 `AUTH_SIG_INVALID` | 设备签名消息拼接顺序、UUID 16 字节、大端 key_version、raw `r||s` 编码是否正确 | | Token/OTA 返回 401 | 设备 JWT 是否过期、scope 是否包含 `voice:session` 或 `ota:report`、Gateway Redis auth 状态是否 active、`kv` 是否匹配 | | OTA 一直 204 | 当前版本已不低于最新版本,或产品 `product` 没有匹配到可比较最新版本 | | OTA 503 | COS/短链配置不完整,且 `OTA_DOWNLOAD_STRICT_MODE=true` | ## 13. 代码依据 | 模块 | 文件 | |---|---| | Gateway 工厂 HMAC | `gateway/src/main/java/com/talkingq/gateway/filter/FactoryAuthGlobalFilter.java` | | Gateway 设备 JWT | `gateway/src/main/java/com/talkingq/gateway/filter/DeviceJwtAuthGlobalFilter.java` | | Gateway Program JWT | `gateway/src/main/java/com/talkingq/gateway/filter/JwtAuthGlobalFilter.java` | | Gateway 路由与鉴权配置 | `gateway/src/main/resources/application.yml` | | 注册设备接口 | `register/src/main/java/com/talkingq/controller/DeviceController.java` | | 工位签名校验 | `register/src/main/java/com/talkingq/deviceauth/service/StationSignatureVerifier.java` | | 工厂接口 | `register/src/main/java/com/talkingq/deviceauth/controller/FactoryAuthController.java` | | 设备认证接口 | `register/src/main/java/com/talkingq/deviceauth/controller/DeviceAuthController.java` | | 设备认证实现 | `register/src/main/java/com/talkingq/deviceauth/service/impl/DeviceAuthServiceImpl.java` | | 设备 JWT 签发 | `register/src/main/java/com/talkingq/deviceauth/jwt/DeviceJwtSigner.java` | | Token 接口 | `token/src/main/java/com/talkingq/controller/DeviceTokenController.java` | | Token 实现 | `token/src/main/java/com/talkingq/service/impl/TokenServiceImpl.java` | | 阿里云 token 调用 | `token/src/main/java/com/talkingq/dialogue/token/providers/AliyunAppTokenService.java` | | OTA 设备接口 | `ota/src/main/java/com/talkingq/controller/OTAController.java` | | OTA 小程序接口 | `ota/src/main/java/com/talkingq/controller/OTAProgramController.java` | | OTA 短链接口 | `ota/src/main/java/com/talkingq/controller/OtaShortLinkController.java` | | OTA 版本逻辑 | `ota/src/main/java/com/talkingq/service/impl/DeviceUpdateServiceImpl.java` | | OTA 下载 URL | `ota/src/main/java/com/talkingq/service/impl/OtaDownloadUrlService.java` |