Wails v3项目 GitHub Release 应用版本更新

背景

ZGit 是基于 Wails v3 + Go + Vue 3 的 Git 桌面工具。

安装包通过 GitHub Actions 构建并发布到独立仓库 psvmc/z-git-releases

应用需要具备:

  • 启动后自动检查 GitHub Release 是否有新版本
  • 系统设置中支持手动「检查更新」
  • 用户确认后再下载安装,支持「稍后 / 跳过该版本 / 立即更新」
  • Windows 下载 NSIS 安装包后弹出安装向导(非静默),无需管理员权限;默认安装到 %LOCALAPPDATA%\Programs\z-git,并记住上次安装目录;安装完成页可勾选「立即运行」
  • 下载过程中显示进度条(百分比、速率、校验/准备安装阶段)
  • macOS 尽量做到一键替换并重启;Linux 走 deb/rpm 包安装

本文记录 ZGit 项目中的具体实现方式,便于后续维护或在其它 Wails v3 项目中复用。

配置要求

Windows下 安装包的 staging 目录要放在系统的临时文件夹

下载超时时长设置为10分钟,Wails GitHub 更新 provider 默认 HTTP 客户端也设置为10分钟。

NSIS 安装包的安装作用域设置为 **user **

整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────┐
│ 前端 Vue │
│ useAppUpdate / UpdateConfirmDialog / UpdateProgressDialog │
└───────────────────────────┬─────────────────────────────────┘
│ Wails Bindings
┌───────────────────────────▼─────────────────────────────────┐
│ UpdateService(Go Service) │
│ CheckForUpdate / ApplyUpdate / SkipUpdateVersion │
└───────────────────────────┬─────────────────────────────────┘

┌───────────────────────────▼─────────────────────────────────┐
│ Wails v3 app.Updater + GitHub Provider │
│ GET .../releases/latest → 下载 → 校验 → 按平台安装 │
└───────────────────────────┬─────────────────────────────────┘

┌───────────────────────────▼─────────────────────────────────┐
│ GitHub Release(psvmc/z-git-releases) │
│ ZGit-amd64-installer.exe / ZGit-macos-universal.zip / .deb/.rpm │
└───────────────────────────────────────────────────────────────┘

核心思路:下载与替换逻辑交给 Wails 内置 Updater,业务 UI 自己实现(Window: WindowNone 不用框架自带小窗),通过 UpdateService 暴露给前端。

版本号与构建标签

当前版本定义在 version.go,需与 build/config.ymlinfo.version 及各平台打包配置保持一致:

1
2
3
// version.go
const Version = "1.2.3"
const WindowTitle = "ZGit v" + Version

自动更新仅在 production 构建 中启用(wails3 dev 不会弹更新):

1
2
3
4
5
// internal/update/enabled_prod.go  (//go:build production)
const Enabled = true

// internal/update/enabled_dev.go (//go:build !production)
const Enabled = false

Release 构建需带 -tags production,与项目 build/windows/Taskfile.yml 等配置一致。

发版前可用 scripts\set-version.bat 同步版本号:

1
2
3
scripts\set-version.bat              REM 交互:当前 1.4.1 时提示 e.g. 1.4.2,回车即写入
scripts\set-version.bat 1.5.0 REM 直接指定
scripts\set-version.bat --show REM 查看当前版本

脚本会更新 version.gobuild/config.yml,并执行 wails3 task common:update:build-assets 同步 NSIS / deb / macOS 等打包配置。

初始化 Updater

main.go 中注册 UpdateService,并在启动时初始化 Updater:

1
2
3
4
5
if err := update.InitUpdater(app, Version); err != nil {
log.Fatal(err)
}
updateService := services.NewUpdateService(app, Version)
app.RegisterService(application.NewService(updateService))

internal/update/setup.go 使用 GitHub Releases Provider,数据源为:

1
const GitHubRepository = "psvmc/z-git-releases"

等价于请求:

1
https://api.github.com/repos/psvmc/z-git-releases/releases/latest

初始化时关闭框架自带更新窗口,UI 完全自定义:

1
2
3
4
5
app.Updater.Init(updater.Config{
CurrentVersion: currentVersion,
Providers: []updater.Provider{provider},
Window: updater.WindowNone,
})

按平台匹配 Release 资源

GitHub Release 上各平台产物文件名固定,通过自定义 AssetMatcher 精确匹配,避免默认规则误选:

平台 资源文件名
Windows amd64 ZGit-amd64-installer.exe
Windows arm64 ZGit-arm64-installer.exe
Windows 386 ZGit-386-installer.exe
macOS ZGit-macos-universal.zip
Linux deb 系 ZGit.deb
Linux rpm 系 ZGit.rpm

Linux 根据 /etc/os-release 等判断优先 deb 还是 rpm(internal/update/asset.go)。

UpdateService 对外方法

方法 作用
GetCurrentVersion() 返回当前运行版本
CheckForUpdate() 调用 app.Updater.Check,有新版本则返回 release 信息
ApplyUpdate() 下载并安装,各平台策略见下节
SkipUpdateVersion(version) 跳过指定版本,持久化到本地配置

CheckForUpdate 返回结构体包含:availableenabledcurrentVersionlatestVersionreleaseNamenotesreleaseURL 等字段,供前端展示更新日志。

各平台安装策略

Windows

Windows 发布产物为 NSIS 安装包wails3 task windows:package 或本地 wails3 task package),不再单独发布裸 ZGit.exe

NSIS 安装包配置

本地与 GitHub Actions 共用同一套配置(build/windows/Taskfile.yml + build/windows/nsis/project.nsi),默认 user 作用域

项目 配置
安装作用域 userINSTALL_SCOPE 默认 user)
执行级别 RequestExecutionLevel user,不触发 UAC
默认安装路径 %LOCALAPPDATA%\Programs\z-git
注册表 HKCU 写入 InstallLocation
完成页 勾选「立即运行 ZGit」(默认勾选;LangString 多语言,中文系统不乱码)

build/windows/Taskfile.yml 在 user 模式下调用 makensis 时传入:

1
/INPUTCHARSET UTF8 -DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user

/INPUTCHARSET UTF8 强制按 UTF-8 解析脚本,避免中文 LangString 在本地与 GitHub Actions 上编译乱码。

project.nsi!ifndef WAILS_INSTALL_SCOPE 兜底为 user,避免与 makensis 命令行重复定义冲突。完成页文案勿在 !define 里直接写中文,应使用 LangString + SimpChinese / English 语言包。

应用内更新流程

  1. DownloadAndInstall — 从 GitHub 下载 NSIS 安装包到系统 Temp 目录(前端订阅 wails:updater:download-progress 等事件,弹出 UpdateProgressDialog 显示进度)
  2. 同盘 staging — 将安装包复制到当前 exe 同目录(RelocateBesideExecutable),避免 Temp 路径或跨盘导致安装程序异常
  3. 可见 NSIS 向导 — 不带 /S,以独立进程启动安装程序(ApplyVisibleDetachAttrs);不能用 CommandContext,否则 ApplyUpdate 返回后 context 被 cancel 会杀掉安装进程
  4. app.Quit() — 主进程退出,用户在 NSIS 向导中完成覆盖安装;勾选「立即运行」则安装完成后自动启动新版本

NSIS 脚本会写入并读取注册表 InstallLocation,升级时自动预填上次安装目录;旧版本若无该字段,则从 UninstallString 解析路径作为回退。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
!ifndef WAILS_INSTALL_SCOPE
!define WAILS_INSTALL_SCOPE "user"
!endif

!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_EXECUTABLE}"
!define MUI_FINISHPAGE_RUN_TEXT "$(ZGIT_FINISH_RUN)"
!define MUI_FINISHPAGE_RUN_CHECKED

!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_LANGUAGE "English"

LangString ZGIT_FINISH_RUN ${LANG_SIMPCHINESE} "立即运行 ${INFO_PRODUCTNAME}"
LangString ZGIT_FINISH_RUN ${LANG_ENGLISH} "Launch ${INFO_PRODUCTNAME}"

!if "${WAILS_INSTALL_SCOPE}" == "user"
InstallDir "$LOCALAPPDATA\Programs\z-git"
InstallDirRegKey HKCU "${UNINST_KEY}" "InstallLocation"
!endif

Function .onInit
!insertmacro wails.checkArchitecture
Call RestorePreviousInstallDir ; 无 InstallLocation 时从 UninstallString 回退
FunctionEnd

; 安装完成后写入(user 作用域为 HKCU)
WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR"

注意:若用户曾用旧版 machine 作用域装到 Program Files,首次切到 user 安装包时会装到 %LOCALAPPDATA%\Programs\z-git;之后升级会记住该目录。建议卸载旧版 machine 安装残留。

macOS

流程:

  1. DownloadAndInstall — 从 GitHub 下载到系统 Temp 目录
  2. 同盘 staging — 将文件复制到 exe 同目录(见下节「跨盘修复」)
  3. 启动 Wails Helper 进程 — 等待主进程退出后 Rename 替换二进制
  4. app.Quit() — 主进程退出,Helper 完成替换并拉起新版本

macOS 的 zip 内含 .app,Wails Updater 会先解压再交给 Helper;swapTarget 会定位到 .app bundle 路径。

Linux

Linux 不直接替换正在运行的二进制,而是下载 deb/rpm 后:

  1. 优先 pkexec dpkg -i / pkexec rpm -U
  2. 其次尝试 sudo
  3. 都不可用则 xdg-open 打开安装包,提示用户手动安装

安装命令在独立进程中执行,随后 app.Quit()

跳过版本

用户可在更新弹窗点击 「跳过该版本」。实现要点:

  1. 调用 app.Updater.SkipVersion(version) — 内存中生效
  2. 写入 %APPDATA%\z-git-tools\config.jsonskippedUpdateVersion — 重启后仍生效
  3. 应用启动时 applySkippedVersion 读配置并恢复 Skip 状态

仅跳过指定版本号;若之后发布更高版本(如跳过 1.2.2,后来有 1.2.3),仍会正常提醒。

前端实现

启动时检查

App.vue 在恢复标签页与批量刷新完成后触发(waitForUiReady 延迟,避免与 WebView 初始化竞争):

1
2
3
4
5
6
7
onMounted(async () => {
await loadSettings()
await waitForUiReady(200)
await repo.restoreSavedTabs()
await waitForUiReady(400)
void checkOnStartup()
})

useAppUpdate.ts 封装检查逻辑;有新版本时弹出 UpdateConfirmDialog,无新版本时启动检查静默跳过(手动检查才 Toast「已是最新」)。

下载进度弹窗

用户点击「立即更新」后,不再用全局 Loading 遮罩,改为 UpdateProgressDialog(360px 宽,与批量刷新进度弹窗风格一致):

阶段 显示内容
下载中 百分比、已下载/总大小、下载速率
校验中 正在校验数字签名…
准备安装 正在解压并准备安装程序…

useUpdateProgress.ts 订阅 Wails Updater 事件:

  • wails:updater:download-started / download-progress
  • wails:updater:verifying / installing

通过 runWithProgress(() => UpdateService.ApplyUpdate()) 在调用前后自动打开/关闭进度状态。

更新确认弹窗

UpdateConfirmDialog.vue(680px 宽)展示:

  • 版本对比:v1.2.1 → v1.2.3
  • Release 标题
  • 可滚动更新日志区域(最高 320px)
  • 三个按钮:跳过该版本 | 稍后 | 立即更新

弹出更新确认前会关闭 批量刷新结果弹窗;系统设置里点「检查更新」会先 关闭设置弹窗 再检查。

系统设置入口

SystemSettingsDialog.vue 增加「软件更新」区块,显示当前版本与「检查更新」按钮,复用同一套 useAppUpdate 逻辑。

与 CI 发版的配合

Release 由 GitHub Actions 构建(workflow 在 scripts/release/github-workflow-release-all.yml),从 Gitee 拉 tag 源码,三平台产物上传到 psvmc/z-git-releases

发版流程(简要):

1
2
3
git tag 1.2.3
git push origin 1.2.3
scripts\publish-release.bat

Updater 依赖 Release 资源文件名与 AssetMatcher 一致,发版 workflow 中 copy 产物时需保持命名,例如:

  • Windows: ZGit-amd64-installer.exe(NSIS 安装包)
  • macOS: ZGit-macos-universal.zip
  • Linux: ZGit.deb / ZGit.rpm

发版前可用 scripts\set-version.bat 一键同步 version.gobuild/config.yml 及各平台打包配置中的版本号。交互模式下会根据当前版本自动建议 patch +1(如 1.4.11.4.2),直接回车即采用该版本;也可传参指定,例如 scripts\set-version.bat 1.5.0

本地 Windows 验证更新流程时,可执行 scripts\build-release.bat 生成 bin\ZGit-amd64-installer.exe(需 NSIS),与 CI 产物一致。两者均走 build/windows/Taskfile.yml,默认 INSTALL_SCOPE=user,NSIS 脚本为 build/windows/nsis/project.nsi

关键代码

后端

注册服务

main.go

1
2
3
4
5
if err := update.InitUpdater(app, Version); err != nil {
log.Fatal(err)
}
updateService := services.NewUpdateService(app, Version)
app.RegisterService(application.NewService(updateService))

初始化 GitHub Provider

internal/update/setup.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func InitUpdater(app *application.App, currentVersion string) error {
if !Enabled {
return nil
}

provider, err := github.New(github.Config{
Repository: GitHubRepository, // "psvmc/z-git-releases"
AssetMatcher: ZGitAssetMatcher,
})
if err != nil {
return fmt.Errorf("init github update provider: %w", err)
}

if err := app.Updater.Init(updater.Config{
CurrentVersion: currentVersion,
Providers: []updater.Provider{provider},
Window: updater.WindowNone, // 不用框架自带更新窗
}); err != nil {
return err
}

applySkippedVersion(app)
return nil
}

按平台匹配 Release 资源

internal/update/asset.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func PreferredAssetName(platform, arch string) string {
switch platform {
case "windows":
if arch == "386" {
return "ZGit-386-installer.exe"
}
if arch == "arm64" {
return "ZGit-arm64-installer.exe"
}
return "ZGit-amd64-installer.exe"
case "darwin":
return "ZGit-macos-universal.zip"
case "linux":
return linuxPackageAssetName() // deb 或 rpm
default:
return ""
}
}

func ZGitAssetMatcher(req updater.CheckRequest, assets []github.ReleaseAsset) int {
platform := req.Platform
if platform == "" {
platform = runtime.GOOS
}
arch := req.Arch
if arch == "" {
arch = runtime.GOARCH
}

preferred := PreferredAssetName(platform, arch)
for i, asset := range assets {
if asset.Name == preferred {
return i
}
}
return -1
}

暴露给前端的 Service

services/update_service.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func (s *UpdateService) CheckForUpdate() (models.UpdateCheckResult, error) {
result := models.UpdateCheckResult{
CurrentVersion: s.currentVersion,
Enabled: update.Enabled,
}
if !update.Enabled {
return result, nil
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

rel, err := s.app.Updater.Check(ctx)
if err != nil {
return result, fmt.Errorf("检查更新失败: %w", err)
}
if rel == nil {
return result, nil
}

result.Available = true
result.LatestVersion = rel.Version
result.ReleaseName = rel.Name
result.Notes = rel.Notes
if rel.Metadata != nil {
if url, ok := rel.Metadata["github.release.htmlURL"].(string); ok {
result.ReleaseURL = url
}
}
return result, nil
}

func (s *UpdateService) SkipUpdateVersion(version string) error {
version = strings.TrimSpace(version)
if version == "" {
return fmt.Errorf("版本号不能为空")
}
s.app.Updater.SkipVersion(version)
return s.store.SetSkippedUpdateVersion(version)
}

macOS 安装(Helper 替换)

services/update_apply_other.go//go:build darwin):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (s *UpdateService) applyPlatformUpdate(ctx context.Context) error {
if err := s.app.Updater.DownloadAndInstall(ctx); err != nil {
return fmt.Errorf("下载或安装更新失败: %w", err)
}

staged := s.app.Updater.DownloadedPath()
if staged == "" {
return fmt.Errorf("未找到已下载的更新包")
}

if err := update.ApplyStagedUpdate(staged); err != nil {
return fmt.Errorf("应用更新失败: %w", err)
}

s.app.Quit()
return nil
}

Windows 安装(NSIS 可见向导)

services/update_apply_windows.go//go:build windows):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func (s *UpdateService) applyPlatformUpdate(ctx context.Context) error {
if err := s.app.Updater.DownloadAndInstall(ctx); err != nil {
return fmt.Errorf("下载更新包失败: %w", err)
}

installerPath := s.app.Updater.DownloadedPath()
if installerPath == "" {
return fmt.Errorf("未找到已下载的更新包")
}

self, err := os.Executable()
if err != nil {
return fmt.Errorf("定位当前程序失败: %w", err)
}

localInstaller, err := update.RelocateBesideExecutable(self, installerPath)
if err != nil {
return fmt.Errorf("准备安装包失败: %w", err)
}

if err := runWindowsInstaller(localInstaller); err != nil {
return err
}

s.app.Quit()
return nil
}

func runWindowsInstaller(installerPath string) error {
// 不能用 CommandContext:ApplyUpdate 返回后 context 会被 cancel,安装进程会被杀掉。
cmd := exec.Command(installerPath)
update.ApplyVisibleDetachAttrs(cmd)
return cmd.Start()
}

Linux 安装 deb/rpm

services/update_apply_linux.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (s *UpdateService) applyPlatformUpdate(ctx context.Context) error {
if err := s.app.Updater.DownloadAndInstall(ctx); err != nil {
return fmt.Errorf("下载更新包失败: %w", err)
}

packagePath := s.app.Updater.DownloadedPath()
if packagePath == "" {
return fmt.Errorf("未找到已下载的更新包")
}

if err := installLinuxPackage(ctx, packagePath); err != nil {
return err
}

s.app.Quit()
return nil
}

func installLinuxPackage(ctx context.Context, packagePath string) error {
ext := filepath.Ext(packagePath)
switch ext {
case ".deb":
return runLinuxInstaller(ctx, []string{"dpkg", "-i", packagePath}, packagePath)
case ".rpm":
return runLinuxInstaller(ctx, []string{"rpm", "-U", packagePath}, packagePath)
default:
return openPackageWithDesktop(packagePath)
}
}

跨盘 staging 与 Helper

Windows 将 NSIS 安装包复制到 exe 同目录后再启动;macOS 将更新包 staging 后交给 Helper 替换。共用 RelocateBesideExecutable

internal/update/staging.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// RelocateBesideExecutable — 复制到 exe 同目录,避免跨盘或 Temp 路径问题
func RelocateBesideExecutable(exePath, artifactPath string) (string, error) {
return relocateNextToExecutable(exePath, artifactPath)
}

func relocateNextToExecutable(exePath, stagedPath string) (string, error) {
info, err := os.Stat(stagedPath)
if err != nil {
return "", fmt.Errorf("stat staged artifact: %w", err)
}

exeDir := filepath.Dir(exePath)
stagingDir := filepath.Join(exeDir, fmt.Sprintf(".zgit-update-%d", os.Getpid()))
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
return "", fmt.Errorf("create staging dir: %w", err)
}

dest := filepath.Join(stagingDir, filepath.Base(stagedPath))
// 文件或 .app 目录均 copy 到 dest ...
return dest, nil
}

internal/update/helper_restart.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// helper_restart.go — 启动 Wails Helper 替换二进制
func ApplyStagedUpdate(stagedPath string) error {
self, err := os.Executable()
if err != nil {
return fmt.Errorf("resolve executable: %w", err)
}

localPath, err := relocateNextToExecutable(self, stagedPath)
if err != nil {
return err
}

target := swapTarget(self)
logPath := filepath.Join(os.TempDir(), fmt.Sprintf("wails-update-%d.log", os.Getpid()))
return spawnUpdaterHelper(self, target, localPath, os.Getpid(), logPath)
}

func spawnUpdaterHelper(self, target, newPath string, pid int, logPath string) error {
cmd := exec.Command(self)
cmd.Env = append(os.Environ(),
"WAILS_UPDATER_HELPER=1",
"WAILS_UPDATER_HELPER_TARGET="+target,
"WAILS_UPDATER_HELPER_NEW="+newPath,
"WAILS_UPDATER_HELPER_PID="+strconv.Itoa(pid),
"WAILS_UPDATER_HELPER_LOG="+logPath,
)
applyDetachAttrs(cmd)
return cmd.Start()
}

启动时恢复跳过版本

internal/update/skip.go

1
2
3
4
5
6
7
8
9
func applySkippedVersion(app *application.App) {
store, err := config.NewStore()
if err != nil {
return
}
if version := store.GetSkippedUpdateVersion(); version != "" {
app.Updater.SkipVersion(version)
}
}

配置写入 %APPDATA%\z-git-tools\config.json

1
2
3
{
"skippedUpdateVersion": "1.2.2"
}

前端

检查、确认、安装

frontend/src/composables/useAppUpdate.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const promptAndApply = async (result: UpdateCheckResult) => {
options.dismissBlockingDialogs?.() // 关闭批量刷新结果弹窗
const choice = await promptUpdateConfirm(result)

if (choice === 'skip') {
await UpdateService.SkipUpdateVersion(result.latestVersion!)
toast.info(`已跳过 v${result.latestVersion},该版本不再提醒`)
return
}

if (choice !== 'confirm') {
return
}

await runWithProgress(() => UpdateService.ApplyUpdate())
}

const checkOnStartup = async () => {
const result = await UpdateService.CheckForUpdate()
await handleCheckResult(result, false)
}

更新确认弹窗

frontend/src/composables/useUpdateConfirm.ts

1
2
3
4
5
6
7
8
export type UpdatePromptChoice = 'confirm' | 'later' | 'skip'

const prompt = (result: UpdateCheckResult): Promise<UpdatePromptChoice> =>
new Promise((resolve) => {
state.result = result
state.show = true
resolvePrompt = resolve
})

frontend/src/components/dialogs/UpdateConfirmDialog.vue 底部按钮:

1
2
3
4
5
6
7
<NSpace justify="space-between" align="center" class="update-footer">
<NButton secondary @click="skip">跳过该版本</NButton>
<NSpace :size="12">
<NButton secondary @click="cancel">稍后</NButton>
<NButton type="primary" @click="confirm">立即更新</NButton>
</NSpace>
</NSpace>

更新日志区域使用可滚动 <pre>,弹窗宽度 680px、max-height: 320px

启动时检查

frontend/src/App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const dismissBlockingDialogs = () => {
showBatchRefreshResult.value = false
}

const { checkOnStartup } = useAppUpdate({ dismissBlockingDialogs })
const { updateProgress } = useUpdateProgress()

onMounted(async () => {
await loadSettings()
await waitForUiReady(200)
await repo.restoreSavedTabs() // 内含批量刷新
await waitForUiReady(400)
void checkOnStartup()
})

// template 中挂载:
// <UpdateProgressDialog v-model:progress="updateProgress" />

系统设置里点击「检查更新」时先 show.value = false 关闭设置弹窗,再调用 checkForUpdate()frontend/src/components/dialogs/SystemSettingsDialog.vue)。

调试建议

场景 处理方式
开发模式不检查更新 正常,需 production 构建
macOS 下载成功但未替换 查看 %TEMP%\wails-update-*.log
macOS 跨盘 Rename 失败 确认已包含 relocateNextToExecutable 逻辑
Windows 更新无反应 确认 Release 上传的是 *-installer.exe;不要用 CommandContext 启动安装程序
Windows 更新要求管理员 确认安装包为 user 作用域INSTALL_SCOPE=user),勿用 machine;旧版 machine 安装需先卸载
Windows 安装程序闪退 旧版用 /S 静默安装或 context cancel 杀进程;现改为可见 NSIS + 独立 exec.Command
升级时安装目录被重置 确认 NSIS 脚本写入/读取 InstallLocation(user 为 HKCU,build/windows/nsis/project.nsi
makensis 报重复定义 WAILS_INSTALL_SCOPE 勿在 project.nsi 中无条件 !define,应用 !ifndef 包裹
完成页「立即运行」乱码 勿在 MUI_FINISHPAGE_RUN_TEXT 直接写中文;用 LangString + SimpChinese/English,makensis 加 /INPUTCHARSET UTF8
手动验证 version.go 改为低于 Release 的版本后构建测试

小结

ZGit 的自动更新基于 Wails v3 内置 Updater + GitHub Releases Provider,业务层补充了:

  • 自定义更新确认 UI(含跳过版本、更新日志)
  • 按平台精确匹配 Release 资源(Windows 为 NSIS 安装包)
  • Windows NSIS user 作用域可见安装向导(无需管理员)+ 默认路径 %LOCALAPPDATA%\Programs\z-git + 完成页「立即运行」+ 下载进度条 + 记住安装目录;macOS 同盘 staging + Helper 替换
  • Linux deb/rpm 安装策略
  • 跳过版本持久化
  • 与批量刷新、系统设置等弹窗的联动关闭

若在新项目中复用,最少需要:InitUpdater + UpdateService + 前端确认弹窗 + 下载进度弹窗 + Release 资源命名约定;Windows 生产环境建议发布 user 作用域 NSIS 安装包(默认路径 %LOCALAPPDATA%\Programs\<app>),更新时用独立进程启动可见安装向导(勿用 CommandContext、勿用 /S 静默);macOS 建议保留同盘 staging 逻辑。