🐛 fix(update): 修复桌面端更新安装与重启流程

- macOS 下载完成后改为直接走安装更新链路,不再只打开安装目录
- Windows 更新脚本在重启时显式带上目标工作目录,提升安装后拉起稳定性
- 删除失效的 winget 发布工作流,并补充前端与脚本回归测试

Fixes #585
This commit is contained in:
Syngnat
2026-06-23 22:18:07 +08:00
parent a2c1b4a7d8
commit 6bf05c9ed7
6 changed files with 186 additions and 60 deletions

View File

@@ -1,22 +0,0 @@
name: Publish to WinGet
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
release_tag:
required: true
description: 'Tag of release you want to publish'
type: string
jobs:
publish:
runs-on: windows-2025-vs2026
steps:
- uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: Syngnat.GoNavi
installers-regex: 'GoNavi-windows-(amd64|arm64)\.exe$'
release-tag: ${{ inputs.release_tag || github.ref_name }}
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -4114,7 +4114,7 @@ function App() {
) : null,
isLatestUpdateDownloaded ? (
<Button key="install-direct" type="primary" icon={<DownloadOutlined />} onClick={handleInstallFromProgress}>
{isMacRuntime ? t('app.about.action.open_install_directory') : t('app.about.action.install_update')}
{t('app.about.action.install_update')}
</Button>
) : null,
].filter(Boolean)}
@@ -5111,7 +5111,7 @@ function App() {
] : (updateDownloadProgress.status === 'done' ? [
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>,
<Button key="install" type="primary" onClick={handleInstallFromProgress}>
{isMacRuntime ? t('app.about.action.open_install_directory') : t('app.about.action.install_update')}
{t('app.about.action.install_update')}
</Button>
] : (updateDownloadProgress.status === 'error' ? [
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>

View File

@@ -0,0 +1,159 @@
import React from 'react';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useAppUpdateManager } from './useAppUpdateManager';
const runtimeApi = vi.hoisted(() => ({
EventsOn: vi.fn(() => vi.fn()),
}));
const messageApi = vi.hoisted(() => ({
info: vi.fn(),
success: vi.fn(),
error: vi.fn(),
}));
vi.mock('../../wailsjs/runtime', () => runtimeApi);
vi.mock('antd', () => ({
message: messageApi,
}));
type BackendAppMock = {
CheckForUpdates: ReturnType<typeof vi.fn>;
CheckForUpdatesSilently: ReturnType<typeof vi.fn>;
DownloadUpdate: ReturnType<typeof vi.fn>;
InstallUpdateAndRestart: ReturnType<typeof vi.fn>;
OpenDownloadedUpdateDirectory: ReturnType<typeof vi.fn>;
GetAppInfo: ReturnType<typeof vi.fn>;
};
const createBackendAppMock = (): BackendAppMock => ({
CheckForUpdates: vi.fn(),
CheckForUpdatesSilently: vi.fn(),
DownloadUpdate: vi.fn(),
InstallUpdateAndRestart: vi.fn(),
OpenDownloadedUpdateDirectory: vi.fn(),
GetAppInfo: vi.fn(async () => ({ success: true, data: { version: '0.8.1', author: 'Syngnat' } })),
});
describe('useAppUpdateManager', () => {
let backendApp: BackendAppMock;
let hook: ReturnType<typeof useAppUpdateManager> | null = null;
let renderer: ReactTestRenderer | null = null;
const t = (key: string, params?: Record<string, any>) => {
if (params?.version) return `${key}:${params.version}`;
if (params?.path) return `${key}:${params.path}`;
if (params?.error) return `${key}:${params.error}`;
return key;
};
const renderHook = (isMacRuntime: boolean) => {
const Harness = () => {
hook = useAppUpdateManager({
isMacRuntime,
runtimeBuildType: 'release',
t,
});
return null;
};
act(() => {
renderer = create(<Harness />);
});
};
beforeEach(() => {
backendApp = createBackendAppMock();
hook = null;
renderer = null;
runtimeApi.EventsOn.mockClear();
messageApi.info.mockReset();
messageApi.success.mockReset();
messageApi.error.mockReset();
vi.useFakeTimers();
vi.stubGlobal('window', {
setTimeout,
clearTimeout,
setInterval,
clearInterval,
go: {
app: {
App: backendApp,
},
},
});
});
afterEach(() => {
act(() => {
renderer?.unmount();
});
vi.useRealTimers();
vi.unstubAllGlobals();
});
it('uses InstallUpdateAndRestart for downloaded macOS updates', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
hasUpdate: true,
currentVersion: '0.8.1',
latestVersion: '0.8.2',
downloaded: true,
assetSize: 1024,
},
});
backendApp.InstallUpdateAndRestart.mockResolvedValue({ success: true });
backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true });
renderHook(true);
await act(async () => {
await hook?.checkForUpdates(false);
});
await act(async () => {
await hook?.handleInstallFromProgress();
});
expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1);
expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
});
it('does not auto-open the downloaded macOS package directory after download succeeds', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
hasUpdate: true,
currentVersion: '0.8.1',
latestVersion: '0.8.2',
downloaded: false,
assetSize: 2048,
},
});
backendApp.DownloadUpdate.mockResolvedValue({
success: true,
data: {
downloadPath: '/Users/test/Desktop/GoNavi-0.8.2-MacOS-Arm64.dmg',
},
});
backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true });
renderHook(true);
await act(async () => {
await hook?.checkForUpdates(false);
});
await act(async () => {
await hook?.downloadUpdate(hook?.lastUpdateInfo!, false);
});
expect(backendApp.DownloadUpdate).toHaveBeenCalledTimes(1);
expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
});
});

View File

@@ -167,16 +167,6 @@ export const useAppUpdateManager = ({
void message.success({ content: t('app.about.message.download_completed'), duration: 2 });
}
setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, downloaded: true }));
if (isMacRuntime && !updateUserDismissedRef.current) {
try {
const openRes = await (window as any).go.app.App.OpenDownloadedUpdateDirectory();
if (openRes?.success) {
void message.success(openRes?.message || t('app.about.message.install_directory_opened_manual_replace'));
}
} catch (e) {
console.warn('自动打开下载目录失败', e);
}
}
} else {
setUpdateDownloadProgress((prev) => ({
...prev,
@@ -218,28 +208,6 @@ export const useAppUpdateManager = ({
if (!canInstall) {
return;
}
if (isMacRuntime) {
const res = await (window as any).go.app.App.OpenDownloadedUpdateDirectory();
if (!res?.success) {
void message.error(t('app.about.message.open_install_directory_failed_with_error', { error: res?.message || t('common.unknown') }));
updateDownloadedVersionRef.current = null;
updateDownloadMetaRef.current = null;
setUpdateDownloadProgress((prev) => ({
...prev,
status: 'idle',
percent: 0,
downloaded: 0,
open: false,
}));
setLastUpdateInfo((prev) => prev ? { ...prev, downloaded: false, downloadPath: undefined } : prev);
setAboutUpdateStatus((prev) => lastUpdateInfo ? formatAboutUpdateStatus({ ...lastUpdateInfo, downloaded: false, downloadPath: undefined }) : prev);
return;
}
updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null;
hideUpdateDownloadProgress();
void message.success(res?.message || t('app.about.message.install_directory_opened_manual_replace'));
return;
}
const res = await (window as any).go.app.App.InstallUpdateAndRestart();
if (!res?.success) {
void message.error(t('app.about.message.install_failed_with_error', { error: res?.message || t('common.unknown') }));
@@ -247,7 +215,7 @@ export const useAppUpdateManager = ({
}
updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null;
hideUpdateDownloadProgress();
}, [formatAboutUpdateStatus, hideUpdateDownloadProgress, isMacRuntime, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]);
}, [hideUpdateDownloadProgress, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]);
const checkForUpdates = useCallback(async (silent: boolean) => {
if (updateCheckInFlightRef.current) return;

View File

@@ -1076,6 +1076,7 @@ if not exist "%SOURCE%" (
)
for %%I in ("%TARGET%") do set "TARGET_NAME=%%~nxI"
for %%I in ("%TARGET%") do set "TARGET_DIR=%%~dpI"
for %%I in ("%SOURCE%") do set "SOURCE_EXT=%%~xI"
set "SOURCE_EXE="
@@ -1161,10 +1162,10 @@ exit /b 1
:move_done
del /F /Q "%TARGET_OLD%" >> "%LOG_FILE%" 2>&1
start "" "%TARGET%" >> "%LOG_FILE%" 2>&1
start "" /D "%TARGET_DIR%" "%TARGET%" >> "%LOG_FILE%" 2>&1
if %ERRORLEVEL% NEQ 0 (
call :log cmd start failed, trying powershell Start-Process
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%'" >> "%LOG_FILE%" 2>&1
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'" >> "%LOG_FILE%" 2>&1
if !ERRORLEVEL! NEQ 0 (
call :log relaunch failed
exit /b 1

View File

@@ -102,7 +102,7 @@ func TestBuildWindowsScriptUsesDelayedErrorlevelInsideBlocks(t *testing.T) {
for _, token := range []string{
`if !ERRORLEVEL! NEQ 0 (`,
`powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%'" >> "%LOG_FILE%" 2>&1`,
`powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'" >> "%LOG_FILE%" 2>&1`,
`set "TARGET_OLD=%TARGET%.old"`,
} {
if !strings.Contains(script, token) {
@@ -111,6 +111,26 @@ func TestBuildWindowsScriptUsesDelayedErrorlevelInsideBlocks(t *testing.T) {
}
}
func TestBuildWindowsScriptRelaunchUsesTargetDirectory(t *testing.T) {
script := buildWindowsScript(
`C:\tmp\GoNavi-v0.5.0-windows-amd64.exe`,
`C:\Program Files\GoNavi\GoNavi.exe`,
`C:\Program Files\GoNavi\.gonavi-update-windows-v0.5.0`,
`C:\Program Files\GoNavi\logs\update-install.log`,
99999,
)
for _, token := range []string{
`for %%I in ("%TARGET%") do set "TARGET_DIR=%%~dpI"`,
`start "" /D "%TARGET_DIR%" "%TARGET%" >> "%LOG_FILE%" 2>&1`,
`Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'`,
} {
if !strings.Contains(script, token) {
t.Fatalf("windows update relaunch missing token: %s\nscript:\n%s", token, script)
}
}
}
func TestBuildWindowsLaunchCommandUsesDirectHiddenCall(t *testing.T) {
cmd := buildWindowsLaunchCommand(`C:\tmp\gonavi-update\update.cmd`)