Files
soft_quay/docs/api.md
T

335 lines
20 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.
# 协议合约(Catalog / 软件包 / 许可证 / 事件 / CLI)
> 本项目没有自建在线 API;盒子只消费静态签名文件。本文定义所有跨边界数据结构和交互合约的目标形状。
> 实现前可细化,但不要在代码里另起一套不兼容结构。字段变化必须同步更新本文、[04-architecture.md](04-architecture.md) 和 `schemas/` 下的 JSON Schema。
## 通用约定
- 传输:HTTPS;所有 JSON 使用 UTF-8。
- 时间格式:ISO 8601(UTC)。
- 签名:Ed25519;客户端只内置公钥;示例中的 `"signature": "..."` 均为占位符。
- 版本号:语义化版本(SemVer)。
- 软件 ID:`^[a-z0-9-]+$`,永久稳定,发布后不得更改。
- 软件包控制的路径统一使用 UTF-8 与 `/` 分隔,并由客户端共享的 Windows 安全相对路径策略校验。JSON Schema 只表达基础形状;运行时还逐段拒绝空段、`.`/`..`、首尾 ASCII 空格、尾随句点、控制字符、Windows 禁止字符和 DOS 设备名及其兼容变体。
## 1. Catalog 清单(远端 → 盒子)
现代版与 Win7 版使用不同 channel 文件:`manifest-modern.json` / `manifest-win7.json`。更新器必须校验 channel,禁止 Win7 版下载现代版包。
```json
{
"schema_version": 1,
"channel": "modern",
"generated_at": "2026-07-16T00:00:00Z",
"min_box_version": "1.0.0",
"apps": [
{
"id": "json-parser",
"name": "JSON解析工具",
"description": "示例简介",
"version": "1.2.0",
"channel": "stable",
"status": "active",
"category": "开发工具",
"tags": ["工具", "JSON"],
"icon": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"homepage": "https://example.com",
"tutorial": "https://example.com/tutorial",
"min_os": "windows-7-sp1",
"architectures": ["386", "amd64"],
"entry_exe": "JsonParser.exe",
"requires_admin": false,
"packages": {
"amd64": {
"url": "https://download.example.com/json-parser-1.2.0-amd64.zip",
"size": 12345678,
"sha256": "...",
"signature": "..."
}
}
}
],
"signature": "..."
}
```
行为要求:
- 网络成功:按“HTTPS 获取 → 验签 → Schema/字段/目标 channel 校验 → 替换缓存”处理;任一步失败都**拒绝**新内容,继续用最后一次验证且客户端可消费的缓存。
- 网络失败:用缓存;清单过期给提示,但保留已安装软件的启动能力。
- 下架:显式 `status: deprecated | hidden`,不用名称前缀。
- 分类:`category` 是单一稳定分类,`tags` 是搜索/多标签展示数据;两者都不得为空。
- 过滤:先校验 manifest `channel`,再按 `min_os`、`architectures` 和 `packages` 过滤;不兼容软件可见说明但不可下载。
- 状态:`active` 可安装;`deprecated` 可见但不可新装/更新;`hidden` 不进入目录结果,但本地已安装记录仍由本地状态模块保留。
- URL:Catalog、package、homepage、tutorial 只接受无用户信息、无 fragment 的绝对 HTTPS URL。
- 架构:MVP Schema 接受 `386` / `amd64`;一个 app 的 `architectures` 必须与 `packages` 键一一对应。当前两个客户端发布目标仍是 amd64。
- 系统版本:`min_os` v1 只允许 `windows-7-sp1`、`windows-10`、`windows-11`;更高系统可消费更低最低版本的软件。
- Schema:客户端协议文件为 `schemas/manifest.schema.json`;标准包元数据为 `schemas/app.schema.json`。运行时还会执行 JSON Schema 难以表达的重复 ID、架构映射和 channel 目标一致性检查。
### 1.1 Catalog 签名域
客户端采用以下签名域,供发布器实现对齐:
1. 输入必须是单个 UTF-8 JSON object;重复字段、尾随 JSON、浮点/指数数字直接拒绝。
2. 读取顶层 `signature`(标准 Base64 编码的 64 字节 Ed25519 签名),然后从对象中移除该字段。
3. 对剩余值递归规范化:对象键按 Unicode 字符串升序排列;数组保持原顺序;字符串按 JSON 转义;数字仅允许 JSON 整数并保持其合法十进制写法;不保留无意义空白。
4. Ed25519 直接签名/验证上述规范 JSON 字节。
T-201 已把该签名域接入正式客户端加载链路并用客户端测试向量覆盖。`softbox-catalog` 发布端仍必须补跨实现向量测试;密钥 ID/轮换字段尚未定稿,在单公钥协议升级前不得另造签名域。
### 1.2 图标内容引用与本地缓存
Catalog `icon` v1 是 `sha256:<64 hex>` 内容引用,不是可直接请求的 URL。最终分发字段仍由发布端定稿;客户端通过注入的 IconFetcher 把引用解析为图标字节,不得在 UI 中拼接或猜测 URL。
客户端缓存合约:
1. 请求键为 `(icon digest, DPI)`;DPI 接受 48~768 的整数值。
2. 加载顺序为有界 memory LRU → 磁盘 → 注入的流式 Fetcher;磁盘文件名为 `<digest>-<dpi>.icon`。memory 默认同时限制为 32 MiB 与 256 个 key,命中提升 LRU recency。
3. 同一 key 的并发 miss 由一个 in-flight leader 执行 disk/fetch/validate/store,等待者复用结果;不同 key 不互相串行。等待者取消只结束自身等待,leader 失败/取消后必须释放 key 供后续重试。
4. Fetcher 返回与请求 context 绑定的 `io.ReadCloser` 和可选声明长度。声明长度超过 2 MiB 默认上限时不读 body;未知或伪造长度仍只读 `maxBytes+1`,所有成功/失败路径都关闭 body。
5. 远端和磁盘字节都必须复核 SHA-256,并通过图片完整解码、2 MiB 默认字节上限与 2048×2048 默认尺寸上限。
6. 只有验证成功的远端字节可用同目录临时文件原子写入磁盘;损坏的普通缓存文件删除后可重新获取,symlink/非普通文件按不安全布局拒绝。
7. 新进程断网时可读取再次验证成功的磁盘缓存;缓存损坏且远端不可用时返回 `no valid icon available`,UI 使用稳定占位图。
8. `IconEventDelivery` 在调用方拥有的后台 context 中完成 `IconCache.Load` 与 `DecodeIcon`,成功发布 `IconReady`,失败只发布稳定分类的 `IconFailed`;取消直接结束且不发布迟到失败。事件身份为 request_id + app_id + icon_ref + DPI,不得把原始 URL/query 或 Gio 类型放进 payload。
9. application event relay 是有界 FIFO,队列满时执行可取消的 lossless backpressure,不静默丢图标结果。后台 pump 成功入队后只调用并发安全的 `Window.Invalidate`;Gio Frame/UI goroutine 在 Layout 前 drain 并执行 `ApplyEvent`/`ApplyIcon`。
10. shell 只接受当前 app 最新且 icon_ref/DPI 匹配的 request_id;删除 app、替换 IconRef、DPI 变化或取消请求后丢弃迟到 ready/failed。同一 AppID 更换 IconRef 时先清除旧 ImageOp,Layout 始终只复用内存 `paint.ImageOp`。
11. symlink、目录和其他非普通 cache entry 必须返回 `ErrIconCacheUnsafe` 并立即停止;不得读取/跟随、删除、改写或重命名该 entry,不得回退 Fetcher。该错误由 delivery 映射成唯一稳定码 `unsafe_cache`,后台返回链仍保留 `ErrIconCacheUnsafe` 供调用方分类。
12. shell 只为当前最新身份保存 `IconEventIdentity + IconFailureCode`;仅 `unsafe_cache` 在选中详情显示安全警告、稳定 code、app ID 与 `<digest>-<dpi>.icon` locator。不得显示绝对 cache root、原始 error、URL/query、token 或 link target;人工处理遵循 [故障排查](troubleshooting.md),不提供自动清理/隔离/重试操作。
当前生产 `cmd` 尚未装配 `NewIconCache`、真实 `IconFetcher` 或 `IconEventDelivery.LoadAndPublish` 调用方。以上是已由 core/双端适配器测试冻结的安全契约,不是生产网络图标链已经启用的声明。
## 2. 标准软件包协议 v1(ZIP)
```text
json-parser_1.4.2_windows_amd64.zip
├─ app.json # 身份、版本、入口、兼容性
├─ files.json # 解压后文件清单(v1.1 强制,v1 推荐)
└─ payload/ # 实际安装到 current/ 的程序文件
```
### 2.1 app.json
```json
{
"schema_version": 1,
"id": "json-parser",
"name": "JSON解析工具",
"vendor": "MyCompany",
"version": "1.4.2",
"channel": "stable",
"min_os": "windows-7-sp1",
"architecture": "amd64",
"entrypoint": "JsonParser.exe",
"working_directory": ".",
"product_id": "product-json-parser",
"supports_trial": true,
"requires_admin": false,
"data_policy": "local-app-data",
"update_policy": "managed-by-softbox"
}
```
校验规则:`entrypoint`/`working_directory` 必须通过共享 Windows 安全相对路径策略(`working_directory` 额外允许 `.`),并位于 payload 内;`id`、`version`、`channel`、`architecture` 必须与 Catalog 记录一致;`schema_version` 高于盒子支持范围时拒绝安装并提示升级盒子(盒子始终支持当前与前一个 Schema)。
### 2.2 files.json
```json
{
"schema_version": 1,
"files": [
{ "path": "JsonParser.exe", "size": 3456789, "sha256": "..." }
]
}
```
完整 ZIP 的 SHA-256 由签名 Catalog 保存(不写入包内部);files.json 用于解压后复核关键文件与修复功能。
### 2.3 安全限制(必须拒绝)
绝对路径;`../` 与 Windows `".. "` 等归一化穿越;首尾 ASCII 空格/尾随句点造成的路径别名;DOS 设备名;符号链接/重解析点逃出 staging;写入其他软件或盒子目录;覆盖 `data/` 与 `licenses/`;包内自动执行脚本(install.bat/PowerShell 钩子);未验证 SHA-256/签名的包被执行;解压文件数、总体积或压缩比无上限;entrypoint 指向 payload 之外。
T-102 Phase 1 原型进一步固定:
- ZIP 名称只接受 UTF-8 `/` 分隔的规范 Windows 安全相对路径;逐段拒绝反斜杠、盘符、冒号/NTFS ADS、NUL/控制字符、Windows 禁止字符、`.`/`..`、首尾 ASCII 空格、尾随句点、DOS 设备名及大小写折叠后的重复输出路径。
- 顶层只允许必需的 `app.json`、可选 `files.json` 与 `payload/`;只把 `payload/` 内容写入全新的 staging。
- 拒绝符号链接、设备/管道等特殊文件和加密条目。
- 原型默认上限:10,000 个条目、总展开 4 GiB、单条及总体压缩比 200:1。T-302 按真实包体分布复核后再冻结。
- entrypoint 使用 payload 内相对路径表示,不得自带 `payload/` 前缀,且必须精确对应 ZIP 中的普通文件。
- 所有输出路径在创建 staging 前完成规划,并在逐段名称校验后再次验证 native `filepath.Join` 结果仍位于 destination 内;包含性检查是纵深防御,不能替代 Windows 名称规则。
### 2.4 安装记录 installed-app.json(本地)
记录实际安装的软件 ID、版本、架构、channel 和文件清单;与 `current/`、`staging/`、`backup/` 同级存放于 `apps/<id>/`。
```json
{
"schema_version": 1,
"id": "json-parser",
"version": "1.4.2",
"architecture": "amd64",
"channel": "stable",
"files": [
{
"path": "JsonParser.exe",
"size": 3456789,
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
}
]
}
```
规则:
- Schema 位于 `schemas/installed-app.schema.json`;未知字段、非法 SemVer、ID/目录不匹配、非 `386|amd64` 架构、非 stable channel、共享 Windows 安全相对路径以外的文件名、大小写折叠重复路径和非法 SHA-256 均拒绝。
- `files` 必须是数组;v1 可以为空,完整文件清单由 T-302 安装整合时从已验证包写入。
- 写入使用 app 目录内临时文件 + `installed-app.json.backup` 原子替换;主文件缺失时可读取中断遗留 backup,但所有读取都重新严格校验。
- SemVer 比较遵循 2.0.0:major/minor/patch 与 prerelease 参与 precedence,build metadata 不影响更新判断。
### 2.4.1 本地可见状态推导
状态事实由后台/存储层预先收集,UI Layout 不扫描磁盘。单一可见状态按以下优先级推导:
1. 存在 `install-transaction.json` 或其 backup → `rollback_pending`。
2. 存在活跃任务状态 → `queued|downloading|verifying|extracting|installing|failed`。
3. 已检测到进程运行 → `running`。
4. Catalog 判定不可兼容 → `incompatible`。
5. 无 installed-app.json → `not_installed`。
6. 本地 SemVer 低于 Catalog → `update_available`;否则 → `installed`。Catalog 中已隐藏/下架且无可比较版本时,保留 `installed`。
### 2.5 安装切换事务(Phase 1 原型)
`apps/<id>/install-transaction.json` 用于断电恢复:
```json
{
"schema_version": 1,
"phase": "staging_activated",
"had_current": true
}
```
phase 只允许:`prepared`、`current_backed_up`、`staging_activated`、`rollback_required`、`committed`。日志使用临时文件 + 同目录 backup 原子替换;恢复时同时检查日志与 current/staging/backup 实际状态。未完成健康检查的 current 不视为可信:有旧版时恢复 backup,首次安装则撤销 current。
### 2.6 下载任务元数据 download-task.json(本地)
每个任务以稳定 `request_id` 为主键,元数据位于 `downloads/tasks/<request_id>.json`,字节文件位于 `downloads/files/<request_id>.part|.download`。本地路径只由客户端从 request_id 派生,不接受 URL 或 Content-Disposition 提供的文件名。
```json
{
"schema_version": 1,
"request_id": "download-json-parser-001",
"app_id": "json-parser",
"url": "https://download.example.com/json-parser.zip",
"status": "paused",
"attempt": 2,
"done": 1048576,
"total_known": true,
"total": 12345678,
"validator": {
"etag": "\"release-1.4.2\""
},
"error_code": "",
"created_at": "2026-07-16T00:00:00Z"
}
```
规则:
- Schema 为 `schemas/download-task.schema.json`;内部状态只允许 queued/downloading/paused/failed/completed。paused 对 UI 映射为 queued + DownloadPaused 事件,不增加第 13 个 AppStatus。
- 默认最多 2 个 downloading;同一 request_id 幂等,身份/URL/size 不同则冲突;同一 app 同时只允许一个非 completed 任务。
- 暂停保留 `.part` 和元数据;失败 retry 保留 `.part`;取消固定删除该 request_id 精确派生的 part/final/metadata,不使用递归删除。取消已线性化后,body close/sync 等非致命错误只记录日志,不能把取消反转成 failed 或跳过清理。
- 续传只在存在 strong ETag 或合法 Last-Modified 时发送 Range + If-Range;无可靠 validator 时从 0 重下。请求固定 `Accept-Encoding: identity`,所有 redirect 重新校验 HTTPS。
- `206` 必须严格匹配 Content-Range 起点、长度、total 与期望 size;已有 part 但服务端返回 `200` 时截断并创建新 attempt,不能 old+new 拼接。
- known total 使用已验签 Catalog package.size 作为精确传输长度;unknown total 使用显式 `total_known: false` 并受 4 GiB 默认硬上限保护。
- known total 的 `.part` 已达到精确 total 时,同进程 resume/retry 与重启恢复都直接 sync、核对身份并 finalize,不得再请求 `Range: bytes=<total>-`。
- 完成顺序为 part sync/close → 核对实际写入文件身份 → rename `.download` → metadata completed → DownloadCompleted。rename 前后必须仍是同一普通文件;恢复时以普通文件实际长度对账,不信任 metadata.done。
- 事件投递是 best-effort,失败不得反向把已完成/已取消的持久任务改成失败。队列必须把投递错误交给 `OnObserverError` 记录;application/UI 在启动或事件通道重连后必须用 `Queue.Tasks()` 对账持久状态,不能只依赖某一次终态事件。
- `.download` 仍是**不可信隔离字节**。T-302 必须重新从已验签 Catalog 取得 app/version/arch/size/SHA-256/signature 并验证,通过后才可读取 app.json 或解压。
## 3. 许可证(服务端签发 → 本地离线验证)
```json
{
"schema_version": 1,
"license_id": "lic-...",
"machine_hash": "...",
"products": ["product-json-parser"],
"issued_at": "2026-07-16T00:00:00Z",
"perpetual": true,
"update_policy": "updates-until-2027-12-31",
"rebind_policy": "self-service-1-per-90d",
"signature": "..."
}
```
- machine_hash 由平台层多个稳定硬件标识清洗生成;许可证中**不保存**原始序列号和 MAC。
- 客户端用内置 Ed25519 公钥离线验签;许可证保存于 `licenses/`,与程序文件、用户配置分离;更新不得覆盖。
- 撤销名单同样签名并缓存,网络失败保留宽限期。
- 盒子负责导入/展示/管理;**子软件必须用 sdk 的 licensing 逻辑独立再验证**(签名 + machine_hash + product_id),决定正式版/试用版/授权错误。
## 4. application 事件合约(core → UI)
后台任务不直接修改 Gio 控件,只发布事件;UI 按 `request_id` + `app_id` 更新 ViewModel 并 `Invalidate`。
| 事件 | 触发时机 | 负载 | 结果 |
| --- | --- | --- | --- |
| CatalogRefreshed | 清单验签并缓存成功 | catalog 摘要、generated_at | 列表刷新 |
| CatalogRejected | 清单验签失败 | 原因码 | 提示 + 继续用缓存 |
| DownloadStarted | 一个下载 attempt 开始/因安全重下而重置 | request_id, app_id, attempt, done, total_known, total | 状态 → downloading |
| DownloadProgress | 当前 attempt 进度更新 | request_id, app_id, attempt, done, total_known, total, speed | 进度条刷新;done 在同 attempt 内单调 |
| DownloadPaused | 用户暂停且旧 worker 已完全退出 | request_id, app_id, attempt, done | 状态 → queued(暂停态) |
| DownloadCompleted | 字节完整落盘,尚未通过 SHA/签名 | request_id, app_id, attempt, done, path | 状态 → verifying;T-302 接管信任校验 |
| DownloadFailed | 传输/Range/存储失败 | request_id, app_id, attempt, done, error_code | 状态 → failed + 可重试 |
| DownloadCanceled | 取消清理完成且旧 attempt 不再发事件 | request_id, app_id, attempt | 从任务视图移除并回到本地基础状态 |
| InstallCompleted | 原子切换成功 + 健康检查通过 | app_id, version | 状态 → installed |
| InstallRolledBack | 切换失败恢复 backup | app_id, error_code | 状态 → rollback 完成提示 |
| AppStarted / AppExited | 进程启动/退出检测 | app_id, pid | 状态 → running / installed |
| LicenseChanged | 许可证导入/撤销 | products | 授权视图刷新 |
| IconReady | 图标已完成可信加载与后台解码 | request_id, app_id, icon_ref, DPI, image.Image | 有界 relay 唤醒窗口;UI Frame 验证仍为最新请求后创建 ImageOp |
| IconFailed | 图标加载、校验或解码失败(取消不发布) | request_id, app_id, icon_ref, DPI, error_code | UI 记录诊断;仅保留同 icon_ref/DPI 的既有可信图标,否则继续占位 |
错误码为稳定英文枚举(如 `hash_mismatch`, `zip_path_escape`, `disk_full`, `app_running`, `signature_invalid`),UI 负责本地化文案。图标事件只使用 `unavailable`、`invalid_content`、`unsafe_cache`,原始网络错误只返回后台调用方/日志,不得进入 UI payload。`unsafe_cache` 详情只使用 event 中已经验证的 app ID、icon_ref 与 DPI 生成 `<digest>-<dpi>.icon` locator;不得把 cache root、原始错误或 link target 补进 event/UI。
## 5. CLI 参数合约
### 5.1 SoftBox.exe
```bash
SoftBox.exe # 正常启动
SoftBox.exe --open-app <id> # 打开并定位到指定软件
SoftBox.exe --update-app <id> # 触发指定软件更新流程
SoftBox.exe --repair-app <id> # 按 files.json 修复安装(V1.1)
```
子软件的「检查更新」按钮调用以上参数,不自建下载器;盒子未运行时子软件可启动它。
### 5.2 SoftBoxUpdater.exe(盒子自更新助手)
```bash
SoftBoxUpdater.exe --pid <主程序PID> --staging <暂存目录> --target <目标目录>
```
等待主进程退出 → 备份旧版 → 切换新版 → 启动新版 SoftBox → 失败时恢复备份。退出码:0 成功;非 0 失败并写日志。
### 5.3 子软件推荐参数(v1 推荐,非强制)
```bash
<App>.exe --softbox-info # 输出软件 ID、版本、架构、协议版本 JSON,退出码 0
<App>.exe --softbox-health # 基本环境自检,成功退出码 0
```
产品自定义参数不得占用 `--softbox-` 前缀。
## 6. 进程退出协议(更新前)
v1:进程快照判断运行 → 提示用户保存关闭 → 等待正常退出 → 超时取消更新,**不默认强杀**。
V1.1:命名管道 `\\.\pipe\softbox.<app-id>`,盒子发送 `{"command": "prepare_update", "request_id": "..."}`,子软件保存数据回复 `ready` 后自行退出。
## 待实现时确认
- 清单密钥 ID/轮换字段定稿后同步 `schemas/`。
- 错误码完整枚举表。
- 图标资源从内容哈希到下载位置/分辨率变体的发布端映射格式。
- 撤销名单的结构与宽限期时长。
- machine_hash 的标识来源清单与加权算法(平台层内部文档)。