Skip to content

feat(desktop): 应用内更新支持增量更新,补丁失败时自动改下完整包 - #1916

Merged
xiaoyumuxi merged 2 commits into
previewfrom
feat/desktop-delta-updates
Nov 14, 2026
Merged

xiaoyumuxi merged 2 commits into
previewfrom
feat/desktop-delta-updates

Conversation

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator

关联 Issue / Linked issue

PR 类型 / Type

  • feat:新功能或用户可见的改进
  • fix:缺陷修复
  • docs:文档、Agent Note、Skill 等规则文件
  • release:稳定版发布准备

改动说明 / What changes

背景 / Why

Viva 桌面端的应用内更新每次都下载完整安装包。旧 macOS 客户端用 Sparkle 增量包,只下载改动的部分。上游 Viva 的 update 包已经支持签名增量补丁(上游 27429b5,"feat(update): apply signed delta updates to the installed payload"),所有平台走同一套流程。本 PR 升级 Viva,在客户端接入增量更新:发布源为当前版本列出补丁时只下载补丁,从已安装的应用生成新版本后安装;补丁任一步失败,自动改下完整包。

改动 / Changes

增量更新流程(desktop/internal/workbench/updates.go)

  • 检查到新版本时,用 Asset.PatchFrom(当前版本) 查发布源是否列出了从当前版本出发的补丁。如果有,状态里记下 Delta、补丁大小和完整包大小。
  • 点“更新”时:
    1. 先用 DownloadPatch 下载补丁,同样校验大小和 SHA-256,下载到用户缓存目录 Lithe/Updates/<版本>。
    2. 再用 update.ApplyPatch 在系统临时目录里,从已安装的应用生成新版本。这一步只读取已安装的应用,不写入。
    3. 最后通过 InstallOptions.Payload 交给安装器,校验与完整包相同。
  • 补丁下载、应用、安装任一步失败时:记录 WARN update.delta.fallback_selected(字段为 stage 和 error),同一版本以后不再尝试补丁,并自动改下完整包、安装。
  • 有些安装失败,换成完整包也一样会失败:由包管理器管理的安装(ErrManagedExternally),以及只读位置(ErrNotWritable)。这两种情况不改下完整包,只显示原因。
  • 下载补丁时取消,不算失败,也不改下完整包。生成新版本的过程无法中断,cancelUpdate 在这期间不起作用。
  • 生成的副本只用一次,以下时机删除:
    • 安装器复制完之后(退出应用前)
    • 安装失败时
    • 检查到其他版本时
    • 窗口关闭时
  • 删除副本的方式:
    • 从窗口线程发起的删除在后台任务里执行(removeUpdatePayload),不阻塞窗口线程,退出时会等它完成。
    • 下载 goroutine 在生成副本时被取消,就直接在该 goroutine 上同步删除(deleteUpdatePayload)。
    • 删除失败记录 WARN update.delta.cleanup_failed。
  • 安装前重新读取要安装的内容,因为关闭标签页期间,检查可能已经换成了别的版本。窗口关闭后才到达的下载结果,不再记为待安装(current 增加 !u.stopped)。
  • 进度回调抽成 updateProgress,补丁下载和完整包下载共用。
  • applyUpdatePatch、updateQuit、removeUpdateDir 改成包级变量,方便测试替换。

更新页(desktop/internal/settingswindow/updates_view.go)

  • UpdateStatus 新增 Delta、DeltaSize、FullSize、Applying、FellBack。
  • 更新行的文案:
    • 有增量可用时,显示增量大小和完整包大小。
    • 下载增量、生成新版本、补丁不可用改下完整包,各有一条状态文案。
  • 生成新版本期间不显示进度条,也不显示“取消”。
  • local_history_view.go 的 historySize 改名为 byteSize,本地历史和更新页显示大小时共用。

文案(shared/i18n/)

  • 新增中英词条:
    • desktop.settings.updates.applying
    • desktop.settings.updates.availableDelta
    • desktop.settings.updates.downloadingDelta
    • desktop.settings.updates.downloadingFull
  • 更新 desktop.settings.updates.help,加上“发布源提供增量更新时只下载改动的部分”。
  • 重新生成 desktop/internal/i18n/shared.generated.go。

依赖与固定版本

功能矩阵

  • shared/platform-feature-matrix/features/updates-check-download.json 的 verification 和 notes 补充了增量更新的行为、平台范围和测试命令。两端的实现状态和验证状态保持不变:partial / pending。

测试

  • desktop/internal/workbench/updates_test.go 新增:
    • TestDeltaUpdateInstallsTheRebuiltApplication
    • TestDeltaUpdateFallsBackToTheFullArchive:download、apply、install 三步分别失败
    • TestDeltaUpdateRefusalIsNotRetriedInFull
    • TestCancelledPatchDownloadIsNotAFallback
    • TestRebuiltApplicationIsDeletedWhenNothingWillInstallIt
    • TestRebuiltApplicationIsDeletedOffTheWindowThread:先挂起删除,确认检查结果照常显示;放行后副本被删除
  • 已有用例在断言前加 a.wait(),等后台删除完成。
  • desktop/internal/settingswindow/about_view_test.go:
    • TestUpdateLineSaysTheStepUnderWay 增加增量相关的各个状态。
    • 新增 TestApplyingADeltaUpdateOffersNoCancel。

不在本 PR 范围内 / Out of scope


类型专属:feat

多平台对齐

Lithe 的目标是 macOS 与 Windows 能力对齐。必须选下面一项:

补充:客户端代码与平台无关,Linux 同样会走增量流程。Windows 缺的只是 Viva 一侧的补丁支持:发布源没有补丁时,客户端照常下载完整安装包,不会报错。


范围检查 / Scope checks

功能矩阵 / Feature matrix

  • 已更新 shared/platform-feature-matrix/features/updates-check-download.json:补充了增量更新的 verification 和 notes。两端的实现状态 partial、验证状态 pending 保持不变,因为还没有实机和签名发布源的验收。
  • 已运行 ./scripts/platform-feature-matrix/verify-platform-feature-matrix.sh(validated 224 platform capabilities),以及 ./scripts/platform-feature-matrix/verify-platform-feature-matrix-change.sh origin/preview HEAD(passed)。
  • 没有提交 .artifacts/ 下生成的矩阵视图
  • [N/A] 没有更新矩阵的理由:已更新矩阵

统一日志系统 / Structured logging

  • 已接入统一日志系统。两个新事件都在 Core 的事件目录 rust/logging/events/update.json 中声明:update.delta.fallback_selected(Lithe-core-rust#38)和 update.delta.cleanup_failed(Lithe-core-rust#40),均已在固定的 c1df34b 中。本仓库没有 shared/logging/,桌面端也没有生成的事件类型;事件 ID 和现有的 operationFailedEvent 一样,写成字符串常量后经 a.emit 记录。没有使用 print,也没有直接写 stderr。
  • 一个操作只有一条终态:每次回退只在失败的那一步记录一条 WARN。用户取消不记录(沿用已有的 log-exempt)。高频路径(下载进度)不打日志。
  • 字段只有 stage(固定的三个值)和 error(logwindow.ErrorFields),不记录路径内容或命令输出
  • [N/A] node scripts/logging/generate-log-events.mjs --check 和 node scripts/logging/verify-log-usage.mjs:本仓库没有 scripts/logging/。事件目录的生成和检查在 Core 仓库,已随 Lithe-core-rust#38、Add Windows CI workflow configuration #40 运行。

界面文案 / i18n

  • 文案都走共享 i18n:在 shared/i18n/ 补齐中英词条,运行 node scripts/i18n/generate-i18n.mjs 并提交了生成结果,没有手改生成文件
  • 已运行 node scripts/i18n/verify-app-language-localization.mjs(passed)和 node scripts/i18n/generate-i18n.mjs --check(en-US、zh-CN 均为 11130/11130),没有放宽脚本或扩大豁免名单

逻辑归属 / Logic placement

  • [N/A] 可共享的逻辑放在 Rust Core:更新的选择、签名校验、补丁应用和安装都在 Viva 的 update 包里,各平台用的是同一份 Go 代码。Lithe 只负责状态编排和界面,Core 不参与更新。
  • [N/A] 迁移平台层已有的可共享逻辑:没有修改这类逻辑
  • 工具或运行时专属的知识放在插件或成熟上游:补丁格式和安装细节都在上游 Viva,宿主没有按工具名加分支

性能与内存 / Performance and memory

  • 补丁只在用户点“更新”后才下载和应用。没有新增轮询(沿用已有的每小时检查)。生成的副本用完立即删除,关闭窗口时也会删除。
  • 主线程没有文件读写:补丁的下载和应用都在下载 goroutine 上,删除副本在后台任务里;TestRebuiltApplicationIsDeletedOffTheWindowThread 守住这一点
  • 没有新增第三方运行时依赖。go.sum 多出的 github.com/dsnet/compress 和 github.com/gabstv/go-bsdiff 只供 Viva 的 tools/patchgen 使用,没有链接进 lithe:scripts/desktop/desktop-go.sh list -deps ./cmd/... 只列出 Viva 自带的 internal/bsdiff 和 update。不涉及启动、输入、滚动等路径。

设置 / Settings

N/A:只改了更新卡片的状态文案和按钮,没有新增或修改设置项,也没有新增或移动设置页。

插件契约 / Plugin contract

N/A:没有改动插件契约、能力版本或插件函数协议。Core 固定版本只是为了日志事件目录而升级。

发行包与资源 / Bundle and resources

  • 运行时不向已安装的 app bundle 或 Windows 安装目录写入:
    • 补丁下载到用户缓存目录 Lithe/Updates/<版本>。
    • 生成的新版本由 Viva ApplyPatch 放在系统临时目录(os.MkdirTemp),只读取已安装的应用。
    • 副本的生命周期(何时生成、谁删除、删除失败怎么办)写在 updates.go 的注释里。
    • 已运行 ./scripts/desktop/verify/verify-runtime-bundle-immutability.sh(macOS 和 Windows 源码的静态检查都通过)。
  • [N/A] 同步 worktree 资源复用清单:没有新增构建期的下载、解压或生成资源;更新下载和临时副本都属于运行期的用户数据,不跨 worktree 复用

无用代码与重构 / Dead code and refactoring

  • 本次改动导致变成无用的代码已在本 PR 删除:原来内联的下载进度闭包已由 updateProgress 取代
  • [N/A] 本次之前就存在的无用代码或可重构之处:这次涉及的范围里没有发现

验证 / Verification

命令或操作 平台 结果
make -C desktop fmt-check macOS arm64 通过
scripts/desktop/desktop-go.sh test ./... macOS arm64 55 个包全部 ok
scripts/desktop/desktop-go.sh test -race -count=3 ./internal/workbench -run 'TestAutomaticUpdateCheck|TestUpdateCheckFailure|TestCancelledUpdateDownload|TestTurningOffTheAutomaticCheck|TestDeltaUpdate|TestCancelledPatchDownload|TestRebuiltApplicationIsDeleted' macOS arm64 ok
scripts/desktop/desktop-go.sh test -race -count=3 ./internal/settingswindow -run 'TestUpdateLineSaysTheStepUnderWay|TestApplyingADeltaUpdateOffersNoCancel' macOS arm64 ok
反向验证:把 removeUpdatePayload 改回在窗口线程同步删除,再跑 TestRebuiltApplicationIsDeletedOffTheWindowThread macOS arm64 失败(窗口线程等待了删除),恢复后通过
node scripts/i18n/verify-app-language-localization.mjs — passed
node scripts/i18n/generate-i18n.mjs --check — en-US、zh-CN 11130/11130
./scripts/platform-feature-matrix/verify-platform-feature-matrix.sh — validated 224 platform capabilities
./scripts/platform-feature-matrix/verify-platform-feature-matrix-change.sh origin/preview HEAD — passed
./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh --base origin/preview — passed
./scripts/desktop/verify/verify-runtime-bundle-immutability.sh macOS 和 Windows 源码静态检查 passed
当前固定的 Viva 609dd47:GOOS=windows go build Windows amd64(在 macOS 上交叉编译) 失败:duplicate case wmSetTitle,见“不在本 PR 范围内”
Lithe-IDEA/Lithe-viva#17:GOOS=windows GOARCH=amd64|arm64 CGO_ENABLED=0 go vet ./internal/platform/windows/ ./app/... ./headless/... ./update/... 和 go build ./... Windows amd64/arm64(在 macOS 上交叉编译) 通过

未能执行的检查和原因:

  • Windows 真机,以及 Lithe 桌面端整体的 Windows 交叉编译:core 包依赖 cgo,本机没有 Windows C 工具链。updates.go 没有平台构建标签,只用到 Viva update 包的跨平台 API,这个包修复后在 Windows 上 vet 通过。
  • Linux 没有在本地跑,由 Draft 的 Linux CI 覆盖。
  • 签名发布源的端到端演练(上一版到下一版的增量更新、补丁不可用时回退、运行后安装包内容不变):发布流程还没有签名的 latest.json 和补丁,被 [Feature] 发布流程生成签名更新源与增量补丁,并在发行构建中注入更新源 #1913 阻塞。所以没有做 macOS 整机验收,也没有界面截图;更新页的各个状态由 about_view_test.go 覆盖。

提交前确认 / Final checks

  • 分支基于最新的 preview(b582e206a);目标分支:preview
  • 以 Draft 提交。标记 ready 前,先把 Viva 固定版本升到 fix(windows): give wmSetTitle its own message number Lithe-IDEA/Lithe-viva#17 合入后的 commit
  • 没有无关改动,没有提交 .build/、target/、dist/、.artifacts/ 等产物,没有凭据、个人路径和未脱敏的日志或截图
  • 本次没有启动 Lithe 应用,测试进程都已结束,没有残留

- 升级 Viva 到带增量更新的版本(Lithe-IDEA/viva 609dd47),发布源为当前版本列出补丁时只下载补丁,在临时目录里从已安装的应用生成新版本后安装
- 补丁下载、应用、安装任一步失败时记录 update.delta.fallback_selected,改下完整包,此后同一版本只提供完整包;只读位置和包管理器管理的安装被拒时不重试完整包
- 生成的副本只用一次:安装器取走后、安装失败、检查到其他版本和窗口关闭时删除,删除失败记录 update.delta.cleanup_failed
- 更新页显示增量与完整包大小、下载增量、生成新版本和改下完整包的状态,生成新版本时不提供取消
- 新增中英文案,更新跨平台功能矩阵;Core 固定到 #40 合入后的 c1df34b(含 update.delta.cleanup_failed)
- removeUpdatePayload 改为后台任务,退出时等待删除完成
- fetchDelta 在后台 goroutine 中取消时直接同步删除
- 新增测试:删除被挂起时检查结果照常显示,放行后临时应用被删除
- 功能矩阵说明同步
@ghfind-review ghfind-review Bot added the review: high ghfind author score; see https://ghfind.com label Nov 14, 2026
@xiaoyumuxi
xiaoyumuxi marked this pull request as ready for review November 14, 2026 10:27
@xiaoyumuxi
xiaoyumuxi merged commit 7126dbe into preview Nov 14, 2026
5 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review: high ghfind author score; see https://ghfind.com

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant