12. 客户端资源分发¶
客户端资源分发统一管理安装包、模型、字库、词典、规则库等可版本化的客户端固定资源。设备固件与其共用私有对象存储、预签名传输、流式 SHA-256 和后台导航,但继续使用独立的 firmware_releases 表及 OTA API。
1. 设计边界¶
- S3 bucket 始终私有。上传使用短时预签名 PUT,下载使用短时预签名 GET;程序不设置公共读或对象 ACL。
- 前端静态文件由主程序提供,不作为客户端资源上传。
DownloadURLPrefix仅供已经自行配置好反向代理或 CDN 的部署替换签名 GET 前缀,程序不提供 CDN、缓存键或边缘鉴权适配。- 数据库区分资源身份、发布版本、文件 artifact 和适用 target。发布后的对象按 SHA-256 使用不可变 key,不允许覆盖。
- “通用”表示一个 artifact 显式选择多个平台/架构组合,不存在自动覆盖未来平台的
universal通配目标。 - 发布版本可以声明最低服务端版本、最低幽灵协议版本和所需服务端能力。当前服务端不能证明满足约束时禁止发布,历史发布也会从 manifest 和下载签发入口过滤。
2. 数据模型¶
| 表 | 职责 |
|---|---|
client_resources |
稳定的资源身份:数字 ID、resource_key、名称、分类、说明、是否必需、是否启用。 |
client_resource_releases |
资源版本:SemVer、stable/beta 频道、draft/published 状态、最低客户端/服务端版本、所需协议与能力和更新日志。 |
client_resource_artifacts |
实际文件或应用商店地址:格式、runtime、variant、大小、SHA-256、可选内容签名和 metadata。 |
client_resource_artifact_targets |
artifact 明确适用的平台/架构组合及可选系统版本条件。 |
firmware_releases |
独立设备固件版本;不加入通用 manifest。 |
resource_key 是客户端协议中的稳定机器标识,例如 app/draarl、model/denoise、font/cjk。它只能由安全的小写路径段组成;资源已有发布记录后不可修改。name 是可编辑的后台展示名称。required=true 表示客户端缺少该资源时不应继续依赖它的功能,不代表每次都必须下载。
3. 管理 API¶
以下接口均要求管理员 JWT:
| Method | Path | 说明 |
|---|---|---|
| GET / POST | /api/client-resources |
筛选/创建资源定义。 |
| GET / PATCH / DELETE | /api/client-resources/:resource_id |
查看、修改或级联删除资源;停用后不会出现在 manifest。 |
| GET / POST | /api/client-resources/:resource_id/releases |
列出发布或创建 draft。 |
| GET | /api/client-resources/:resource_id/releases/:release_id |
查看发布及全部 artifact/target。 |
| POST | .../:release_id/artifacts/complete |
校验直传对象、提升到 final key 并事务写入 artifact 和 targets;草稿和已发布版本均可调用。 |
| POST | .../:release_id/publish |
发布至少有一个完整 artifact 的 draft。 |
| DELETE | .../:release_id |
删除任意状态版本及其 artifact、target 和对象。 |
| GET | /api/client-resources/staging |
列出当前管理员已上传但尚未 complete 的 staging 对象。 |
| POST | /api/client-resources/staging/retry |
为当前管理员自己的 staging 对象重新签发 complete 用 upload_token。 |
| GET | /api/storage/audit |
只读审计 client-resources/ 和新固件 firmware/ final 对象与数据库引用。 |
3.1 登记资源和创建版本¶
POST /api/client-resources
Content-Type: application/json
{
"resource_key": "model/denoise",
"name": "通话降噪模型",
"category": "model",
"description": "客户端实时音频降噪",
"required": false,
"enabled": true
}
POST /api/client-resources/12/releases
Content-Type: application/json
{
"version": "1.3.0",
"channel": "stable",
"title": "降噪模型 1.3",
"changelog": "降低低信噪比环境中的伪影",
"min_client_version": "1.8.0",
"min_server_version": "1.1.5-alpha7",
"required_protocol_version": 1,
"required_capabilities": ["multi_receive_v1", "source_group_v1"]
}
资源版本、min_client_version 和 min_server_version 使用不带 v 前缀的 SemVer 2.0.0。同一资源、频道、版本只能有一条记录。能力名会转为小写、去重并排序,最多 32 项;声明能力时 required_protocol_version 必须大于 0。草稿可以提前声明未来约束,但发布时当前服务端必须满足全部约束。无法解析为 SemVer 的开发构建版本(通常为 dev)不能发布声明了 min_server_version 的版本。
stable 客户端只发现 stable;beta 客户端优先选择 beta,如果某项资源没有兼容的 beta 发布则回退 stable。
3.2 直传和完成 artifact¶
先申请上传:
POST /api/storage/presign-put
Content-Type: application/json
{
"file_type": "client_resource",
"file_name": "denoise-v1.3.0.onnx",
"size": 73400320,
"content_type": "application/octet-stream"
}
浏览器按响应要求 PUT 到 upload_url,再提交 object_key 和 upload_token:
POST /api/client-resources/12/releases/38/artifacts/complete
Content-Type: application/json
{
"format": "onnx",
"runtime": "onnxruntime",
"variant": "cpu",
"file_name": "denoise-v1.3.0.onnx",
"object_key": "staging/client_resource/7/2026/07/uuid.onnx",
"upload_token": "signed-upload-grant",
"metadata": {
"upstream_version": "2026.07",
"sample_rate": 48000
},
"targets": [
{ "platform": "windows", "arch": "x86_64", "min_os_version": "10.0.0" },
{ "platform": "macos", "arch": "arm64", "min_os_version": "13.0.0" },
{ "platform": "linux", "arch": "x86_64" }
]
}
服务端校验管理员、上传授权、用户隔离 staging key、声明与实际大小、扩展名、Content-Type 和全部目标组合,并从对象流重算 SHA-256。对象提升到类似下面的 key 后会再次核对大小与哈希:
client-resources/model/denoise/stable/1.3.0/onnx/onnxruntime/cpu/<sha256>/denoise-v1.3.0.onnx
同一文件可一次绑定多个 target,只保存一份对象。platform 和 arch 作为组合提交,不能分别传两个数组,也不能使用 universal。同一发布中,相同 format/runtime/variant 的目标组合不能重叠。已发布版本可继续追加不冲突的 artifact,因此 Android 先发布后可在同一版本追加 Windows artifact;已有 artifact 和 final 对象不可覆盖。
只有 category=application 且 format=app_store 的 artifact 可以不上传文件,改为提供 HTTPS external_url。内容签名使用 content_signature 与 signature_algorithm;它与短时 S3 URL 签名不是同一概念。
3.3 待完成上传重试¶
管理页刷新或网络中断后,如果浏览器 PUT 已经完成但 artifact complete 尚未提交,可调用:
GET /api/client-resources/staging
返回当前管理员自己的 staging/client_resource/<user-id>/... 对象。选中对象后再调用:
POST /api/client-resources/staging/retry
Content-Type: application/json
{ "object_key": "staging/client_resource/7/2026/07/uuid.onnx" }
服务端会重新读取对象大小和 Content-Type,并签发 15 分钟有效的 upload_token。该接口不提升、不删除对象,后续仍走正常 artifact complete 校验。
4. Manifest 和下载¶
4.1 查询 manifest¶
GET /api/public/client-resources/manifest?platform=windows&arch=x86_64&client_version=1.9.0&channel=stable&os_version=11.0.0
If-None-Match: "previous-etag"
| 参数 | 说明 |
|---|---|
platform / arch |
必填的明确目标;禁止 universal。常见值经过归一化,例如 amd64 为 x86_64、aarch64 为 arm64。 |
client_version |
可选 SemVer;未提供时不会返回设置了 min_client_version 的发布。 |
channel |
stable 或 beta,默认 stable。 |
os_version |
可选 SemVer,用于 target 的最低系统版本。 |
android_api |
Android 可选整数,用于最低 API 级别过滤。 |
响应 schema_version 仍为 1。新增契约字段是 schema 1 的可选扩展,不改变旧清单的主版本判断;根节点返回当前 server_version、幽灵 protocol_version 和 capabilities,每个发布返回自身约束。服务端只选择同时满足客户端条件和当前服务端契约的版本。
响应不包含 download_url、S3 key 或任何凭据,可使用稳定 ETag 和 If-None-Match 获得 304;缓存策略为 public, max-age=60, must-revalidate。
{
"code": 200,
"data": {
"schema_version": 1,
"server_version": "1.1.5-alpha7",
"protocol_version": 1,
"capabilities": ["multi_receive_v1", "source_group_v1"],
"resources": [
{
"resource": {
"id": 12,
"resource_key": "model/denoise",
"name": "通话降噪模型",
"category": "model",
"required": false
},
"release": {
"id": 38,
"version": "1.3.0",
"channel": "stable",
"min_client_version": "1.8.0",
"min_server_version": "1.1.5-alpha7",
"required_protocol_version": 1,
"required_capabilities": ["multi_receive_v1", "source_group_v1"]
},
"artifacts": [
{
"id": 91,
"format": "onnx",
"runtime": "onnxruntime",
"variant": "cpu",
"file_name": "denoise-v1.3.0.onnx",
"file_size": 73400320,
"sha256": "...",
"targets": [{ "platform": "windows", "arch": "x86_64" }]
}
]
}
]
}
}
4.2 获取短时下载地址¶
客户端选定 artifact 后单独请求:
GET /api/public/client-resources/artifacts/91/download
{
"code": 200,
"data": {
"artifact_id": 91,
"download_url": "https://storage.example.com/...X-Amz-Signature=...",
"url_expires_at": "2026-07-30T12:15:00Z"
}
}
下载响应使用 Cache-Control: private, no-store。接口只为启用资源下仍处于 published 状态、且当前服务端仍满足发布契约的 artifact 签发下载 URL;默认有效期为 15 分钟,启用 Storage.UseCDN 后改用 EdgeOne TypeA CDN 链接,有效期取 Storage.CDNExpirySeconds(默认 1 小时),并在 url_expires_at 返回对应过期时间。其他状态、停用资源、服务端不兼容或已删除版本返回 404。删除版本后不会再签发新 URL;源站对象清理后,已签发链接的可用性还受 CDN 已缓存内容及其 TTL 影响,需立即撤销时应同步刷新 CDN 缓存。对象清理失败会记录日志并可通过对象审计定位。客户端不得持久化 URL,过期后重新申请即可。
5. 完整示例¶
- 主程序安装包:登记
app/draarl、分类application;Windows、macOS、Android 的不同二进制分别建立 artifact,只有内容确实相同才多选 target。App Store/TestFlight 使用受控external_url。 - 降噪模型:登记
model/denoise、分类model;同一 ONNX 文件可明确绑定 Windows/x86_64、macOS/arm64、Linux/x86_64,GPU 模型使用另一个 variant。 - 中文字库:登记
font/cjk、分类font;同一 WOFF2/OTF 文件可绑定多个明确目标,metadata 记录字体家族、字重、许可证及上游来源。 - 设备固件:不创建
ClientResource。继续使用/api/firmware、/api/firmware/complete和/api/public/firmware/latest,按设备型号选择升级。
删除资源是不可恢复的管理操作。DELETE /api/client-resources/:resource_id 会在一个数据库事务中删除该资源的全部草稿、已发布/已撤回版本、artifact 和 target,随后清理所有关联对象文件;对象清理失败会记录日志并在响应中返回失败数量,可通过对象审计定位残留。
6. 客户端缓存与激活约定¶
客户端实现应遵守以下协议:
- 以 SHA-256 作为本地内容地址,不以 URL、文件名或 S3 ETag 作为缓存键;多个版本引用同一内容时只保存一份。
- 持久化最近一次有效 manifest 和已校验资源。断网或服务临时失败时继续使用本地版本;遇到不支持的 manifest 主版本也保留当前资源并报告错误。
- 下载写入
.part临时文件。使用 HTTP Range 从已有字节位置续传;URL 过期时重新申请 URL 后继续。 - 下载完成先核对文件大小和 SHA-256,再按需验证长期内容签名;全部通过后原子重命名并切换活动版本指针。
- 激活失败回退上一已校验版本。至少保留当前和上一版本,按磁盘配额/LRU 清理更旧且未被引用的内容;并发下载同一 SHA-256 时使用文件锁或任务去重。
required=true的资源缺失时阻止依赖它的功能;可选资源只降级对应能力。首次离线启动必需的最小模型、字体或词典应随安装包提供 fallback。- 字体只放客户端私有目录并在运行时注册,不写系统字体目录。
- 客户端在接受应用更新前应再次确认 manifest 根节点满足该发布的
min_server_version、required_protocol_version和required_capabilities。校验失败时不得安装该版本,并继续保留当前可用版本。
7. 安全与运维¶
/api/storage/get和/files/*都拒绝通用客户端资源、固件 final key 与 staging key,不能绕过业务状态检查。- manifest 和下载链接接口共享独立的匿名 IP 限流,每分钟 30 次。
/api/storage/audit只读扫描client-resources/与新固件firmware/,返回已扫描、已引用、未引用和缺失引用对象;不会删除任何对象。- 资源文件、模型和字体的版权与许可证由发布者负责;metadata 只记录信息,不代替合规审核。
- 旧的未上线
client_releases和client_release_artifacts表仅在确认两表均为空后删除;任一表非空会中止自动迁移并要求人工核对。 - final 对象应按数据库引用进行只读审计。自动清理未引用 final 对象默认不启用,避免误删。
设备固件的独立协议见 固件与 OTA 升级。