Skip to content

RFC: 独立更新器重构方案 — 二进制 Diff + 静默更新 #301

Description

@1w1w11w1

概述

重构 AUTO-MAS 更新系统为独立更新器,支持二进制 diff 增量更新和静默更新。方案参考 BGI (Better Genshin Impact) 和 Starward 的更新架构。


设计原则(CRITICAL)

  • 使用 TypeScript严格模式,禁止 any, 所有类型显式声明
  • 前端 Vue 3 Composition API + <script setup lang="ts">,禁止 Options API
  • Ant Design Vue 4.x 组件库,CSS 使用 <style scoped> + CSS 变量 var(--ant-color-*)
  • 后端 Python 3.12+ FastAPI,所有函数加完整 type hints,模型用 Pydantic v2
  • 新代码遵循 AGPL-3.0 版权声明,文件头统一
  • 代码风格对齐现有项目,不做无意义的格式变更
  • 拒绝过度工程,非必要不引入的新东西

动机与目标

现状痛点

  1. 全量下载浪费带宽:每次更新下载 ~500MB 完整 ZIP,即使只改了一个文件
  2. 无静默更新:UpdateModal 的"后台更新"按钮实际是 // TBD 占位代码
  3. 耦合过重:更新逻辑在 Python 后端 (app/services/update.py),如果 Python 环境崩了就无法更新
  4. 无完整性校验:下载的 ZIP 没有 hash 验证,存在安全风险
  5. 无回滚机制:更新失败后应用可能处于损坏状态
  6. 未利用 electron-builder:package.json 已配置 publish.github 但完全没用它
  7. 进度推送依赖 Python WebSocket:下载进度通过 Python 后端 WebSocket 推送,架构笨重

目标

  • 独立更新器进程:脱离 Python 后端,纯 Node.js/Electron 侧独立运行
  • 二进制 Diff 增量:使用 HDiffPatch (hpatchz) 生成和应用 .patch 文件,典型更新从 500MB → 5-50MB
  • 静默更新:后台自动下载 + 下次启动自动应用,用户全程无感知
  • 完整性校验:全文件 SHA-256 hash + manifest 校验
  • 回滚支持:更新前自动备份旧文件,失败可回滚

当前架构分析

现有代码位置

文件 角色
frontend/src/composables/useUpdateChecker.ts 前端轮询检查 (4h),调 /api/update/check
frontend/src/composables/useUpdateDownload.ts 下载状态管理,WebSocket 进度接收
frontend/src/components/UpdateModal.vue 更新通知弹窗
frontend/src/components/UpdateDownloadModal.vue 下载进度弹窗
frontend/src/services/updateDownloadApi.ts 取消/切换下载源 API
frontend/src/composables/updateDownloadSpeed.ts 低速检测
app/services/update.py 核心:版本检查 + 下载 + 解压 + 安装
app/api/update.py FastAPI 路由
app/models/schema.py UpdateCheckIn / UpdateCheckOut
frontend/package.json electron-builder 配置

当前数据流

[App.vue] → useUpdateChecker (每4h)
  → POST /api/update/check  (Python FastAPI)
    → Mirror酱 API 获取版本信息
  → 有更新 → UpdateModal
    → 用户点"下载" → POST /api/update/download
      → Python 后端下载 ZIP → WebSocket 推送进度
        → UpdateDownloadModal
          → 下载完成 → 用户点"安装" → POST /api/update/install
            → 解压 ZIP → 启动 Inno Setup → KillSelf

关键缺陷

  1. UpdateModal.vue 只有"下载更新"和"暂不更新"两个按钮,没有后台更新按钮
  2. useUpdateDownload.tsbackground() 只是隐藏模态框,不启动后台任务
  3. install_update() 直接在当前进程解压 ZIP 然后 KillSelf,解压到一半被杀会导致文件不一致
  4. 多下载源都指向同一个命名格式的完整 ZIP,没有任何差异化

Agent Note: 实现时若发现以上描述与代码实际状态不一致,以实际代码为准并修复。


参考实现

Starward (C# .NET) 更新机制

  • 二进制 Diff: HDiffPatch/Snap.HPatch,HPatch.Patch() 应用 patch
  • 文件清单: ReleaseManifest 包含 Files: List<ReleaseFile> (Path, Size, Hash, CompressedSize, CompressedHash) + DeleteFiles: List<string>
  • Diff 模型: ReleaseInfoDiff (DiffVersion, DiffSize, ManifestUrl),per-file PatchSize, PatchHash, Offset, Length
  • 更新窗口: UpdateWindow 自动启动更新 (OnLoadedStartUpdate()),无需用户点击
  • 文件校验: 每个文件 SHA-256;CheckFilesAsync 全量校验
  • 原子替换: *.tmp → hash verify → File.Move (NTFS 原子操作)
  • 独立更新器: Starward.Setup 项目,通过 Process.Start 启动主程序
  • 自复制: CopySetupFile 确保更新器自身持久化
  • Zstandard 压缩: SharpCompress.Compressors.ZStandard
  • 参考仓库:https://github.com/Scighost/Starward

BGI / MicaSetup (C# .NET) 更新机制

  • GUI 框架: MicaSetup 提供安装/卸载 UI
  • 版本来源: OSS notice.json (灰度发布 hash%10) + Mirror酱 API (CDK)
  • 更新触发: BetterGI.update.exe -I 参数
  • 静默安装: 规划中 (/q silent, /a automate) 但未完成
  • Fork 进程: 避免文件锁
  • 参考仓库:https://github.com/babalae/better-genshin-impact

重构方案

总体架构:独立更新器进程

+--------------------------------------------------+
|  AUTO-MAS (主程序 - Electron)                     |
|                                                    |
|  main.ts 启动时:                                   |
|    -> spawn updater.exe --check-silent              |
|    -> updater.exe 返回: {updateAvailable, version}  |
|                                                    |
|  运行时:                                           |
|    -> 定期 spawn updater.exe --check (静默)         |
|    -> 或用户手动触发 (显示进度)                     |
|                                                    |
|  退出时:                                           |
|    -> 如果有待应用更新: updater.exe --apply         |
|                                                    |
+------------------+-------------------------------+
                   | spawn / JSON stdout
+------------------v-------------------------------+
|  updater.exe (独立 Node.js 进程, 无窗口)           |
|                                                    |
|  CLI 参数:                                         |
|    --check        检查更新 (输出 JSON)             |
|    --check-silent 静默检查 + 自动下载              |
|    --download     下载更新                         |
|    --apply        应用已下载的更新                 |
|    --rollback     回滚到上一个版本                  |
|    --version      显示版本                         |
|                                                    |
|  核心模块:                                         |
|    ReleaseFetcher  -> GitHub Releases API           |
|    ManifestParser  -> 解析 release manifest         |
|    DiffDownloader  -> 下载 .patch 文件              |
|    PatchApplier    -> HDiffPatch 应用 patch         |
|    FileVerifier    -> SHA-256 hash 校验             |
|    BackupManager   -> 更新前备份,失败回滚           |
+--------------------------------------------------+

发布流程(CI/CD 侧)

1. 构建新版本 -> 产生完整文件集合

2. 生成 ReleaseManifest.json
   -> 遍历所有文件 -> {path, size, sha256, compressedSize, compressedSha256}
   -> DeleteFiles: 比较上一版本,列出已删除文件

3. 对每个文件,与最近 N 个版本做二进制 diff
   -> hdiffz old_file new_file output.patch
   -> 生成 DiffManifest_{fromVersion}.json

4. 发布到 GitHub Release
   -> ReleaseManifest.json
   -> DiffManifest_v5.4.0-beta.1.json 等 (保留最近 5 个版本的 diff)
   -> FullPackage.zip (完整包,兜底)
   -> updater.exe (更新器自身)

5. 可选:同步到自建 CDN 和 CNB 镜像

数据模型设计(TypeScript)

// 版本信息
interface ReleaseInfo {
  version: string
  releaseDate: string                // ISO 8601
  releaseNotes: string               // Markdown
  isPrerelease: boolean
  manifestUrl: string                // ReleaseManifest.json URL
  fullPackageUrl: string             // 完整包 URL (兜底)
  fullPackageSize: number
  fullPackageHash: string            // sha256
  minUpdatableVersion: string        // 最低可从此版本 diff 更新
}

// 文件清单
interface ReleaseManifest {
  version: string
  files: ReleaseFile[]
  deleteFiles: string[]              // 相比上一版本要删除的文件
}

interface ReleaseFile {
  path: string                       // 相对于 app root
  size: number
  hash: string                       // SHA-256 hex
  compressedSize?: number            // Zstd 压缩后大小
  compressedHash?: string
  patch?: ReleaseFilePatch           // 从旧版本 patch 的信息
}

interface ReleaseFilePatch {
  fromVersion: string
  oldPath: string
  oldSize: number
  oldHash: string
  patchUrl: string
  patchSize: number
  patchHash: string
}

// 更新状态 (持久化到 updater-state.json)
interface UpdaterState {
  currentVersion: string
  pendingUpdate?: {
    version: string
    downloadedFiles: string[]
    manifestUrl: string
    ready: boolean
  }
  backupVersion?: string
  lastCheck: string
  failedAttempts: number
}

CLI 接口设计

# 静默检查(JSON 输出,无 UI)
updater.exe --check --json
# {"updateAvailable": true, "latestVersion": "v5.5.0", "downloadSize": 15728640, ...}

# 静默检查 + 自动下载
updater.exe --check-silent
# 后台下载 diff,状态写入 updater-state.json

# 应用已下载的更新
updater.exe --apply
# 备份 -> 应用 patches -> 校验 -> 清理 -> 启动主程序

# 回滚
updater.exe --rollback
# 还原 .rollback/ 中的文件

# 一步到位(手动触发)
updater.exe --update

静默更新策略

场景 行为
用户正在使用,有更新 后台静默下载 diff,下载完可选通知
用户关闭应用 检查 updater-state.json,如有 ready → 自动 apply
用户下次启动 如果上次已 apply → 正常启动;如有 pending → 提示
强制更新 minUpdatableVersion > 当前版本 → 必须全量更新
连续失败 > 3 次 自动回退到全量下载

实现阶段

Phase 1: 独立更新器基础框架

  • frontend/updater/ 目录结构 (独立 tsconfig, 独立构建)
  • 打包脚本:electron-builder 额外构建 updater.exe
  • CLI 参数解析 (--check, --apply, --rollback 等)
  • 日志模块 (独立日志文件 updater.log)

Phase 2: Release API + Manifest

  • ReleaseFetcher: 调用 GitHub Releases API 获取最新版本
    • 主源:GitHub Releases API (api.github.com/repos/AUTO-MAS-Project/AUTO-MAS/releases/latest)
    • 备用源:CNB / 自建站 / Mirror酱
    • 多源并发检查取最快响应
  • ManifestParser: 下载并解析 ReleaseManifest.json
  • UpdaterState 持久化读写

Phase 3: 二进制 Diff 核心

  • HDiffPatch 集成方案调研与选择
    • 方案 A(推荐):child_process 调用 hpatchz CLI
    • 方案 B:koffi 调用 hpatchz.dll
    • 方案 C:纯 JS bsdiff 实现作为 fallback
  • DiffDownloader: 根据当前文件 hash 匹配最佳 diff
  • PatchApplier: 应用 .patch 文件,原子替换
  • CI 脚本 (GitHub Actions):自动生成 diff manifest 和 .patch 文件
    • hpatchz 下载/缓存
    • diff 生成命令:hpatchz -f old_file new_file output.patch
    • Zstd 压缩

Phase 4: 文件校验与回滚

  • FileVerifier: SHA-256 hash 校验每个文件
  • BackupManager: 更新前自动备份 .rollback/ 目录
  • 原子替换:*.tmp → verify → fs.rename
  • 清理旧文件 (根据 DeleteFiles)

Phase 5: 静默更新

  • 主进程 main.ts 集成:启动时 spawn updater.exe --check-silent
  • 退出钩子:before-quit → 如有待应用更新 → spawn updater.exe --apply
  • 通知策略:可选托盘通知/Windows Toast

Phase 6: 迁移与清理

  • 移除 app/services/update.pyapp/api/update.py 中的下载/安装逻辑
  • 保留 Python 侧版本检查 API 作为 fallback
  • 更新前端:useUpdateChecker 改为调用 updater
  • 移除旧更新 UI 中不再需要的状态
  • 文档更新

风险与注意事项

风险

  1. HDiffPatch 平台兼容性:hpatchz 在 Windows x64/ARM64 的可用性
  2. 大文件 diff 性能environment/python/ 下 Python 解释器 (~100MB) 的 diff 耗时
  3. 版本跳跃:跳过多个版本时 diff 链过长,需 fallback 到全量
  4. 文件占用:更新时主程序文件可能被 Windows 锁定
  5. updater 自身更新:updater.exe 自己被更新时的鸡生蛋问题

缓解措施

  • 只保留最近 5 个版本的 diff,更老的版本直接全量
  • Python 解释器等大依赖单独判断:hash 变化则全量下载该文件,不做 diff
  • 参考 Starward 的 CheckProcessAsync 确保主进程已退出
  • updater 自身更新:先复制新 updater 到临时路径,由新 updater 继续执行

兼容性注意事项

  • 保留旧版 Inno Setup 安装路径注册表项的清理逻辑
  • 多下载源的 manifest/diff 文件也应同步到各源
  • DEBUG 模式下不启动更新检查(保持现有行为)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions