Files
soft_quay/docs/04-architecture.md
ilaandClaude Fable 5 63dc40275f Move trial authorization out of MVP scope
Resolve the requirements-vs-architecture conflict found in the Phase 5
review. License v1 is frozen at nine fields with no expiry field
(perpetual is a bool; update/rebind policy are opaque labels), so it
cannot express a trial period. The implementation and architecture docs
correctly recorded that boundary, but 02-requirements still listed trial
as a P0 capability and an MVP acceptance criterion.

Decision: trial moves to V1.1.

- 02-requirements: drop trial from the user role and P0 feature list;
  acceptance now reads 'authorization state consistent between box and
  sub-apps'; add a V1.1 trial row naming the protocol prerequisite; add
  a scope row stating perpetual:false is display-only and never expires,
  and supports_trial does not grant local trial.
- 00-ai-start-here: remove trial from 'MVP does', add it to 'MVP does
  not' with the protocol reason.
- 04-architecture: development order step 5 no longer lists trial/rebind
  as MVP work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:43:21 +08:00

225 lines
24 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.
# 架构设计
> 本文讲"怎么把技术栈搭起来":系统结构、职责划分、数据模型、技术难点、开发顺序。
> 具体用了哪些框架 / 库 / 工具链,见 [技术栈](03-tech-stack.md);清单、软件包、许可证等协议字段见 [协议合约](api.md)。
## 一、系统结构
```text
用户
|
v
SoftBox.exe(Gio 桌面客户端,现代版 / Win7 遗留版)
|
├─> core 业务核心(清单、下载、安装、更新、授权、存储)
| |
| ├─> HTTPS:签名 Catalog 清单 + ZIP 软件包(对象存储 / CDN)
| └─> 本地文件:SoftBoxData(apps / data / licenses / cache / downloads / staging / backups / logs)
|
├─> platform/windows(进程、版本、图标、机器指纹、托盘、目录选择)
|
└─> SoftBoxUpdater.exe(盒子自更新助手)
```
真实组件:
- 客户端 UI:Gio,入口 `app-modern/cmd/softbox` 与 `app-win7/cmd/softbox`。
- 业务核心:`core/`(独立 go.mod,Go 1.20 兼容,无 UI、无 Windows 强依赖)。
- 外部服务:仅静态的签名清单与软件包下载(HTTPS);无自建在线 API、无账号服务。
- 存储:JSON 文件 + 原子替换;无数据库。
## 二、分层与职责划分
```text
domain 纯模型、状态和规则
↓
application 用例、任务调度、事件总线
↓
infrastructure 网络、存储、清单、下载、安装、授权
↓
platform/windows 进程、版本、图标、机器信息、更新助手
↓
ui/gio Gio 控件、布局和交互
↓
cmd/softbox 依赖装配和程序入口
```
硬约束:
- domain 不依赖 Gio、SQLite 或 Windows API。
- application 只依赖接口(下载、存储、时间、进程、系统信息均注入)。
- infrastructure 不操作 UI。
- platform/windows 通过接口暴露能力,并提供非 Windows stub,保证 core 可在 CI 无头测试;仅 Win10 存在的 API 必须动态加载、失败降级,不进 EXE 导入表。
- ui/gio 只提交用例并消费事件;Layout 中不读磁盘、不访问网络、不算哈希。
- cmd 负责现代版和 Win7 版的具体装配。
UI 固定交互模式:
1. 每帧先 drain 点击事件 → 2. 向 application runtime 提交任务 → 3. 保存 RequestID 与软件 ID → 4. 后台 goroutine 执行 → 5. runtime 发布 application.Event → 6. UI 的 ApplyEvent 更新 ViewModel → 7. `Window.Invalidate` 请求重绘。
控件状态按**软件 ID**保存,不按列表序号;列表用惰性 `layout.List`;图标走内存 + 磁盘缓存。
T-203 已把共享列表状态落在 `core/application.CatalogListModel`:源快照、搜索、单分类、all/installed/updates 视图和 selected app ID 都是无 IO 纯内存状态。两个 Gio 适配分别保存 Editor、`layout.List` 与以 app ID 为键的 Clickable;主循环或后台用例通过 `SetItems` 替换准备好的快照,Layout 不扫描 installed-app.json、不获取 Catalog。T-616 新增纯 core `CatalogBootstrap`:composition 注入的 loader 只能返回已验签、协议校验和目标过滤后的内存快照,再经既有 runtime/relay 投递 `CatalogRefreshed`/`CatalogRejected`;Gio Frame 解析强类型 payload 后才 `SetItems`,并以 loading、source_unconfigured、load_failed、loaded-empty、filter-empty 区分空态,失败不清除最后成功快照。当前 cmd 没有真实发布 URL/公钥,所以明确注入 fail-closed 未配置 loader,而不读取 testdata、环境变量或未签名本地文件;可信 client/cache 的实际 composition 仍待发布配置。T-608 在两个隔离 workspace 以同场景交互契约验证 Editor/Clickable 经 `Layout`/`drainInput` 更新共享 model、重排后行点击仍按 AppID、关闭详情保留筛选与列表位置、500 项只布局 `layout.List.Position.Count` 所示可见子集,并通过语义树区分空 Catalog 与过滤无结果;不重复 ViewModel 纯逻辑。T-609 让每次实际 refilter 在局部新 backing array 完整构造后发布 `VisibleItems` generation,旧 generation 可安全保留到后续帧且每帧读取不复制;返回值严格只读,model 仍由单 owner goroutine 串行操作,不承诺并发安全。
T-610 在两个隔离 `ui/gio` package 内采用相同文件职责:`shell.go` 只保存 AppShell 状态/生命周期和根编排,`shell_header.go` 保存 header/navigation,`shell_catalog.go` 保存 content/list/row/icon/empty state,`shell_detail.go` 保存详情,`shell_style.go` 保存主题与绘制 helper。该拆分没有增加 package/API/状态边界,modern 与 Win7 的 Gio 版本特有布局继续分别实现;适配器交互契约仍负责证明两端事件接线和可见行为一致。
T-204/T-606/T-607 图标链路为 `Catalog icon digest + DPI → 32 MiB/256-key memory LRU → verified disk → 流式 IconFetcher(maxBytes+1) → SHA-256/图片资源限制校验 → 原子磁盘缓存 → 后台 DecodeIcon → IconReady/IconFailed application event → bounded FIFO relay + Window.Invalidate → Frame/UI ApplyEvent → ApplyIcon(paint.ImageOp)`。同一 key 由一个 in-flight leader 去重,不同 key 的磁盘/网络工作并行;全局锁只保护 memory/LRU/in-flight 元数据。relay 队列满时无损背压且可由 context/close 取消,后台从不修改 shell map。UI 只接受当前 app 最新且 icon_ref/DPI 匹配的 request_id;删除 app、替换 IconRef 或取消会使迟到结果失效,替换 IconRef 同时清除旧 ImageOp。磁盘与远端都重新校验,断网只使用已验证磁盘缓存;详情右栏只读取 `CatalogListModel.SelectedItem` 与内存 ImageOp,关闭详情不清空筛选或列表位置。
T-611 固定该链路的 fail-closed 诊断:磁盘 entry 为 symlink/非普通文件时 `IconCache` 不读取、不删除且不 fetch,`IconEventDelivery` 只发布安全分类 `unsafe_cache`,同时把包含 `ErrIconCacheUnsafe` 的错误留给后台调用方。双端 shell 在 UI owner goroutine 保存完整失败 identity + code,只对当前选中软件显示 code/app ID/`<digest>-<dpi>.icon` locator 与人工恢复提示;生命周期与最新请求、reference/DPI、ready、取消和 app 删除绑定。正式安装目标 root 为 `%LOCALAPPDATA%\OwnSoftBox\cache\icons\`,但当前生产 `cmd` 尚未装配 IconCache/Delivery/Fetcher,constructor root 才是代码事实;人工步骤见 [故障排查](troubleshooting.md)。
## 三、仓库目录结构
```text
soft_quay/
├─ core/ # 共享业务核心(go.mod,Go 1.20 兼容)
│ ├─ domain/
│ ├─ application/
│ ├─ catalog/ # 清单获取、验签、缓存、过滤
│ ├─ downloader/ # 下载队列、断点续传、任务持久化
│ ├─ internal/safepath/ # 跨 catalog/installer/storage 的 Windows 安全相对路径规则
│ ├─ installer/ # ZIP 校验、安全解压、staging/backup/回滚
│ ├─ licensing/ # Ed25519 许可证验证、machine_hash
│ ├─ storage/ # JSON 原子读写、目录布局
│ └─ updater/ # 盒子自更新编排
├─ app-modern/ # 现代版(go.mod,Go 1.25 + Gio v0.10.1)
│ ├─ cmd/softbox/
│ ├─ cmd/softboxupdater/# 无 Gio 的受限自更新助手
│ ├─ ui/gio/ # shell 根编排 + header/catalog/detail/style 职责文件
│ └─ platform/windows/
├─ app-win7/ # Win7 遗留版(go.mod,Go 1.20 + Gio v0.6.0)
│ ├─ cmd/softbox/
│ ├─ ui/gio/ # 同名职责文件,保留 Legacy Gio 实现差异
│ └─ platform/windows/
├─ schemas/ # manifest / app.json / files.json / license JSON Schema
├─ scripts/ # 构建、打包、签名、校验脚本
├─ testdata/ # 测试用清单、ZIP、许可证样例(全部为假数据)
├─ docs/ # 本文档集
└─ go.work
```
依赖方向:`app-modern` → `core` ← `app-win7`,不得反向。
## 四、数据模型
### 4.1 远端只读数据(签名后消费)
| 数据 | 来源 | 说明 |
| --- | --- | --- |
| Catalog 清单 | `manifest-modern.json` / `manifest-win7.json`(HTTPS) | 软件列表、版本、packages(url/size/sha256)、Ed25519 签名;字段见 [api.md](api.md) |
| ZIP 软件包 | 清单中的 `packages[arch].url` | 内含 `app.json` + `files.json` + `payload/`;协议见 [api.md](api.md) |
| 撤销名单 | 签名的 revocation 列表 | 网络失败时使用缓存并保留宽限期 |
关键事实:
- 软件主键是永久稳定的 `id`(小写英文/数字/短横线),不用名称;下架用 `status`,不用名称前缀。
- 清单验签失败时**拒绝**,回退到最后一次验证成功的缓存,绝不接受未验证的新内容。
- Catalog 正式加载顺序为 HTTPS 获取 → 验签 → 严格字段/Schema/channel 校验 → 缓存替换 → 目标过滤;签名正确但结构或目标通道不匹配的远端内容不能挤掉最后可消费缓存。
- 清单解析拒绝未知字段、重复字段/软件 ID、尾随 JSON、`-0`/非整数数字、非法 Unicode surrogate、非唯一 Base64 signature、非 HTTPS URL 与 architectures/packages 映射不一致;签名域为只移除顶层 `signature` 的受限规范 JSON。对象键 Unicode 排序、字符串 JSON 转义、原始大整数 token 和静态跨实现向量是同一协议的一部分,细节见 [api.md](api.md)。
- Catalog `entry_exe` 与后续 app.json/files.json 路径必须复用 `core/internal/safepath`,不能由各协议解析器分别维护 Windows 路径规则。
- channel 分 `modern` / `win7`,更新器必须校验 channel + min_os,禁止交叉升级。
- `category` 提供稳定单分类,`tags` 用于搜索和多标签展示;hidden 项从远端目录隐藏,deprecated 与不兼容项保留可见原因但不提供安装包操作。
### 4.2 本地动态数据(JSON + 原子写入)
安装模式根目录 `%LOCALAPPDATA%/OwnSoftBox/`(便携模式为程序目录 `.softbox/`,V2):
```text
├─ app/ # 盒子自身程序
├─ apps/<id>/ # current/ staging/ backup/ installed-app.json
├─ data/<id>/ # 子软件用户数据(更新永不覆盖)
├─ licenses/ # 许可证(更新永不覆盖):v1/<sha256>.license 与 revocations-v1.json
├─ cache/ # 清单缓存
│ └─ icons/ # 正式装配的目标图标缓存根;当前以 NewIconCache(root, ...) 实参为事实
├─ downloads/ # 下载临时文件 + 任务元数据
├─ staging/ # 盒子自更新暂存
├─ backups/ # 盒子自更新备份
└─ logs/
```
软件本地状态机(domain 层枚举):
`not_installed → queued → downloading → verifying → extracting → installing → installed`,
另有 `update_available`、`running`、`failed`、`rollback_pending`、`incompatible`。
每次状态写入使用临时文件 + 原子替换;崩溃后可识别 staging、backup 和未完成事务。
T-202 将本地识别拆为两层:
- `core/storage`:严格读写 `installed-app.json` v1,读取主文件或中断遗留 backup,并只读检测安装 transaction/journal 是否存在;`files[].path` 与 ZIP/Catalog 共用 Windows 安全相对路径规则。T-401/T-503 新记录同时保存已验证的 `entrypoint`、`working_directory`、`min_os`、`product_id`、`supports_trial` 与 `requires_admin`;旧 v1 记录仍可读取,但缺少启动或 product 授权元数据不能启动。`core/storage.LicenseStore` 只在验签/机器绑定后原子保存 1 MiB 以下的 license document,并在每次读取重新验证;它同样原子缓存已验签的撤销列表,未知/链接/损坏布局 fail closed。
- `core/domain`:纯 SemVer 2.0.0 比较与 `ResolveAppStatus`,按“恢复事务 → 活跃操作 → 运行 → 不兼容 → 是否安装 → 是否有更新”推导单一状态。
磁盘扫描结果必须在进入 Gio Layout 前准备好;UI 不直接读取 installed-app.json。完整字段见 [api.md](api.md),Schema 为 `schemas/installed-app.schema.json`。
T-301 下载队列:
- `core/downloader.Queue` 默认并发 2,用 request_id 主键和 app 非终态唯一约束串行化 enqueue/pause/resume/cancel/retry;网络/磁盘 IO 与事件发布都在全局锁外。
- `.part` / `.download` / metadata 路径只由受限 request_id 派生。元数据通过 `core/storage.DownloadTaskStore` 临时文件 + backup 原子替换。
- Range 必须绑定 strong ETag 或 Last-Modified 并发送 If-Range;无 validator 从 0 重下,`200` 忽略 Range 时创建新 attempt。进度只在 request_id+attempt 内单调。
- pause/cancel/close 先取消 generation context,等待 body 关闭、part sync/close 和旧 worker 完全退出,再发布终态/释放并发槽,阻断迟到 progress。
- cancel 一旦先于 completion 取得线性化点,即使 body close/sync 报错也记录后继续精确清理;若 completion 先完成,后续 cancel 明确拒绝。known-total 完整 part 在同进程 resume/retry 与启动恢复中都直接 finalize,不发送 offset==total 的 Range。
- 写入句柄的文件身份贯穿 sync/close 与 rename 前后核对,防止活跃 `.part` 路径被替换后发布错误文件。崩溃恢复对账 metadata、part、final 三份事实:完整 part 可 finalize,已 rename 的 final 可补 completed metadata;缺 final 的 completed、part+final、超出 expected/unknown 上限均 fail closed。
- 事件投递失败通过 `OnObserverError` 显式报告,不改变 durable transfer 结果;application/UI 启动或重连后用 `Queue.Tasks()` 对账终态,避免 DownloadCompleted 等一次性通知丢失后永久停链。
- DownloadCompleted 仅证明传输字节完整落盘。下载元数据和 `.download` 可被本地篡改,T-302 的 `core/application/install.InstallService` 不得把它们当信任根,而是接收已验签/过滤 Catalog `Entry` + architecture,确认其与 `App.Packages[architecture]` 精确对应。它将该 Catalog size/SHA-256 交给 `Extractor.ExtractVerifiedFile`,后者以同一打开的普通完成文件先 size、再 SHA-256、再 ZIP 预扫描/解析;package `signature` 仅由外层 Catalog 签名覆盖,当前没有独立包级验签域。
### 4.3 事件模型
后台任务只发布事件(`DownloadStarted / DownloadProgress / DownloadPaused / DownloadCompleted / DownloadFailed` 等),UI 按 RequestID 和软件 ID 回填,见 [api.md](api.md) 事件合约。
## 五、关键安全流程
安装/更新一款软件的强制顺序:
```text
读取已签名/过滤 Catalog → 选择并交叉核对 OS/架构匹配的 Package → 下载到 downloads
→ 以同一普通完成文件核对实际长度 = Catalog size → 以同句柄校验 SHA-256
→ 有界 EOCD/ZIP64/中央目录预扫描 → 用同句柄构造 ZIP reader → 有界严格读取根 app.json
→ 比对 ID/版本/通道/系统/架构/入口/管理员标记 → 检查 ZIP 路径与解压上限 → 解压 payload 到 staging 并记录实际文件 hash → 校验 entry_exe
→ 恢复旧 transaction(如有)→ 更新时在 current 改名 backup 紧邻前复查精确 entrypoint 未运行 → current 改名 backup → staging 原子切换为 current
→ 必需 health check → 原子写 installed-app.json → 成功延迟清理 backup / 任一失败恢复 backup
```
必须防止:绝对路径、`../` 与 Windows dot-space 归一化穿越、首尾空格/尾随句点路径别名、DOS 设备名、符号链接逃逸、写入其他软件目录、覆盖 data 与 licenses、运行中强替换 EXE、未验证包被执行、解压数量/体积/压缩比无上限、包内自动执行脚本。
Phase 1 ZIP 原型采用“两阶段解压”:第 0 阶段在任何 `zip.Reader` 构造前,从同一普通文件句柄核对 `expectedPackageSize` 与实际长度,并只读有界 EOCD 尾部及固定 ZIP64 end 记录以限制原始包、中央目录和声明条目数;第 1 阶段才由标准库解析完整中央目录,继续预检协议顶层、共享 Windows 安全路径、类型、重复项、entrypoint 与展开资源上限,再规划并确认所有 native 输出路径仍在 destination 内。全部通过后才创建新的 staging 并只写 `payload/`;任一复制/CRC 失败删除本次 staging。T-302 把这一原型封装为同句柄 `size → SHA-256 → scan → app.json → extract` 的生产安装链:app.json 读取有 1 MiB 上限并严格对齐可信 Catalog,实际写入文件 hash 进入 installed-app record;原型默认限制见 [api.md](api.md),真实包分布复核仍是本任务验收的一部分。T-303 在严格验证和 extraction 之间加入唯一的 pre-extract 边界:提取器只输出已规划 payload 的 bytes/files 与安全 entrypoint,`core/application/install` 注入容量/目标状态 checker 并在创建 staging 前要求 `payload bytes + 64 MiB` 可用空间及目标未运行;checker 故障 fail closed。T-615 进一步将 ZIP entry 输入的 open/read/CRC/close 与 staging 输出的创建/write/sync/close 分开:后者保留底层错误链,由必需的、平台注入的 `StorageFailureClassifier` 仅对 `ErrStagingOutput` 判断 `disk_full`,其他输出 I/O 稳定为 `install_failed`;清理失败不吞掉,下一次 Recover 仅在已验证 app layout 内删除残留 staging。T-401 在 `Switcher` 的 current→backup 临界区重复精确运行状态检查,仅明确运行中映射 `app_running` 并清理 staging,checker 故障映射 `target_state_unavailable`;没有旧 current 的首次安装跳过该 hook。`core/application/launch` 保持纯 core 接口边界,按受控 current/记录、兼容、授权、运行状态、平台启动器的顺序 fail closed;Toolhelp、`RtlGetVersion` 和无参数进程创建/固定 `runas` elevation 仅在两端 `platform/windows`,并有非 Windows 不支持 stub。T-402 的 `core/application/update` 先验证已装记录与严格更新版本,再在后台按“检测运行→取得关闭确认→有限自然退出等待→委托 InstallService”的顺序编排;未运行时直接委托安装器。等待器只以 Toolhelp 全路径身份轮询,context 取消、超时和检测错误均 fail closed,绝不强杀。InstallService 保留其 staging 前和 switch 临界区两次复查,覆盖等待结束后的重新启动竞态;更新 use case 从不改 transaction 或 data/licenses。当前 cmd 没有可信 Catalog、许可证或下载完成文件的生产装配来源,所以没有注入 allow-all 授权或伪装端到端启动/更新按钮;Gio Layout 仍只处理内存事件。
Phase 1 原子切换原型把 `install-transaction.json` 与目录现实共同作为恢复依据。阶段写入顺序为 `prepared → current_backed_up → staging_activated → committed`,健康失败写 `rollback_required`;崩溃恢复不自动信任未健康检查的新 current,而是恢复旧 backup 或撤销首次安装。日志结构见 [api.md](api.md)。
T-613 已把该状态机的代码层耐久顺序收敛为:payload 的 CRC/长度检查后 `Sync`/`Close` → staging 子目录到根及其父目录同步 → prepared journal 的临时文件 `Sync`/`Close` 与 root 栅栏 → 每次目录 rename 的 root 栅栏与下一 phase journal → committed journal 的 root 栅栏 → backup/journal 清理的 root 栅栏。所有栅栏通过 installer 内部接口复用;非 Windows 使用目录 `File.Sync`,Windows 用 Win7 已有的 `CreateFile(FILE_FLAG_BACKUP_SEMANTICS)` 读写目录句柄和 `FlushFileBuffers`,失败一律 fail closed。测试可注入失败并验证状态机保留可恢复 journal,但这仍不是物理掉电证明。
真实断电时的硬件/驱动缓存、杀毒软件/文件锁干扰和目标文件系统行为仍需 T-601 在 Windows VM/真机做故障注入;T-302 的代码级链路与单元测试不替代硬件级断电验证。
T-403 的盒子自更新由独立、无 Gio 的 `SoftBoxUpdater.exe` 完成。它仅接受正 PID、绝对 `<root>/app` 和绝对 `<root>/staging/<request-id>`;request ID 不走 CLI,而是从已准备 staging 的规范目录名派生并复验。旧 PID 自然退出后才检查/恢复上次 journal,随后以 `prepared → target_backed_up → staging_activated → launched → committed` 把 `app` 与同 root staging 受限 rename。T-617 固定 `prepared` 的恢复依据为经 `Lstat` 验证的实际 target/backup 拓扑:仅 target 存在且 backup 不存在才删除 journal;target 缺失且 managed backup 存在必须先 restore;其余组合保留 journal/材料并 fail closed。因此首次 `app → backup` rename 已完成但目录 sync 失败时,当前调用会立即尝试该受限收敛,下一次 Recover 也不会再盲删 journal。`core/updater` 只依赖 PID waiter、固定 launcher、health waiter 和目录同步接口;Windows 的 `OpenProcess(SYNCHRONIZE)`/短等待、无 shell 固定 health 启动和 `FlushFileBuffers` 均保留在两端 `platform/windows`,非 Windows stub fail closed。新版仅能从自身 `<root>/app/SoftBox.exe` 在匹配的已激活 transaction 下写最小 `<root>/self-update-health.json` 确认;确认前旧 backup 不删除,失败优先 restore,文件锁导致 restore 失败则保留 journal/backup 而不强杀。它不提供下载、签名校验、版本选择或 UI 触发,可信 package→staging 链继续后置。
授权:平台层短暂读取严格规范化的 `MachineGuid` 与 Windows 目录所在卷序列号两个必需来源 → 用固定域分隔 SHA-256 生成 `machine_hash` v1(不保存原始 GUID、序列号或 MAC;任一来源失败即拒绝)→ 服务端 Ed25519 私钥对删除顶层 `signature` 后的受限 canonical License v1 JSON 与 Revocation List v1 JSON 签名 → 客户端 core 以调用方显式注入的 32 字节授权公钥离线验签、严格比对 machine_hash/product ID,并在 `licenses/v1/` 重新验证缓存;撤销为 current、最多 7 天 grace 或 unavailable/revoked,unavailable/revoked 一律不可授权。安装记录从已验证 `app.json` 携带 product/trial 元数据,启动按 product ID 检查而非 app ID。`core/application.AuthorizationService` 只向 Gio relay 发布净化状态快照,Gio 点击仅入队导入请求,后台 source/read/verify/store 后才发布结果。受限 canonical JSON 为 Catalog/License/Revocation 共用的 `core/internal` 实现;core 不含生产 key,Windows 采集留在双端 `platform/windows`,非 Windows stub fail closed。当前 cmd 明确注入未配置授权 loader;真实 trust root、撤销获取和文件选择 composition 留给发布配置。试用/到期/自助换绑/申诉仍不在客户端实现,子软件必须独立再次验证,不能只信盒子。
## 六、关键技术难点
| 难点 | 说明 | 应对 |
| --- | --- | --- |
| 清单签名与缓存回退 | 验签、缓存、拒绝逻辑错了等于远程代码执行 | Phase 1 先做最小原型 + 恶意样例测试(testdata 伪造清单) |
| ZIP 安全解压 | 路径穿越/符号链接/zip bomb | 独立模块 + 攻击样例表驱动测试 |
| 原子切换与回滚 | Windows 文件锁、断电恢复 | staging/backup 状态机 + 崩溃恢复测试;更新前确认进程退出 |
| 双 Gio 版本 API 差异 | v0.6.0 与 v0.10.1 控件 API 不同 | 共享 ViewModel 与交互语义,Gio 适配层各自实现 |
| Win7 API 兼容 | 新 API 进导入表则 Win7 无法启动 | 动态加载 + 降级;Toolhelp32/GetFileVersionInfo 等 Win7 已有 API 优先 |
| 断点续传与任务恢复 | 重启后恢复未完成任务 | 任务元数据持久化 + HTTP Range;超时指数退避 |
| 机器指纹稳定性 | 系统重装/卷或设备变动导致正版误判 | v1 固定双来源、缺失即拒绝;不在客户端做部分匹配,后续按签发端 rebind/申诉流程处理 |
高风险模块先做最小原型(Phase 1),不要等整个系统搭完才验证。
## 七、推荐开发顺序
1. 工程骨架:core / app-modern / app-win7 + go.work + 空 Gio 窗口 + 平台 stub + CI 双目标编译。
2. 最高风险原型:签名清单验签与缓存回退、ZIP 安全解压、staging/backup 原子切换。
3. 核心用户流程:清单 → 列表/搜索 → 下载队列 → 安装 → 启动。
4. 更新与自更新:子软件更新、SoftBoxUpdater、健康检查与回滚。
5. 授权:机器指纹、许可证验证、离线导入与撤销状态(试用/换绑不在 MVP,见 `02-requirements.md`)。
6. Win7 加固与发布:API 降级、真机矩阵、双通道发布流水线。
## 八、架构纪律
- 业务事实和协议变化必须同步更新本文与 [api.md](api.md)。
- 不在代码里发明文档没有的接口、字段和状态。
- 清单缓存、用户数据、许可证、下载临时文件分目录存放,不混在一个模型里。
- 高风险模块先单独验证,再接入完整页面或流程。
- core 的 Go 1.20 兼容性由 CI 用 Go 1.20 工具链编译检查,不靠口头约定。