feat(update): 下载进度到 100% 后由用户点击「重启应用更新」

- 取消 macOS 下载完成后自动安装/退出,Win/Mac 统一交互
- 进度弹窗在 100% 显示就绪提示,主按钮改为「重启应用更新」
- 关于页安装入口同步文案;点击后才执行安装脚本并重启
- 补充多语言与前端回归测试
This commit is contained in:
Syngnat
2026-07-09 13:38:07 +08:00
parent d31bb978f2
commit cdfbdd5dd5
9 changed files with 192 additions and 66 deletions

View File

@@ -4201,8 +4201,8 @@ function App() {
</Button>
) : null,
isLatestUpdateDownloaded ? (
<Button key="install-direct" type="primary" icon={<DownloadOutlined />} onClick={handleInstallFromProgress}>
{t('app.about.action.install_update')}
<Button key="restart-to-update" type="primary" icon={<DownloadOutlined />} onClick={handleInstallFromProgress}>
{t('app.about.action.restart_to_update')}
</Button>
) : null,
].filter(Boolean);
@@ -7961,8 +7961,8 @@ function App() {
<Button key="open-install-directory" onClick={openDownloadedUpdateDirectory}>
{t('app.about.action.open_install_directory')}
</Button>,
<Button key="install" type="primary" onClick={handleInstallFromProgress}>
{t('app.about.action.install_update')}
<Button key="restart" type="primary" onClick={handleInstallFromProgress}>
{t('app.about.action.restart_to_update')}
</Button>
] : (updateDownloadProgress.status === 'error' ? [
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>
@@ -7974,10 +7974,20 @@ function App() {
status={updateDownloadProgress.status === 'error' ? 'exception' : (updateDownloadProgress.status === 'done' ? 'success' : 'active')}
/>
<div style={{ fontSize: 12, color: darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)' }}>
{`${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`}
{updateDownloadProgress.status === 'done'
? t('app.about.download_progress.complete_hint')
: `${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`}
</div>
{updateDownloadProgress.message ? (
<div style={{ fontSize: 12, color: '#ff4d4f' }}>{updateDownloadProgress.message}</div>
<div style={{
fontSize: 12,
color: updateDownloadProgress.status === 'error'
? '#ff4d4f'
: (darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(16,24,40,0.65)'),
}}
>
{updateDownloadProgress.message}
</div>
) : null}
</div>
</Modal>

View File

@@ -161,7 +161,7 @@ describe('useAppUpdateManager', () => {
expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
});
it('auto-installs a macOS update immediately after download when the backend reports auto relaunch support', async () => {
it('keeps download at 100% ready-to-restart without auto-installing after download completes', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
@@ -193,8 +193,39 @@ describe('useAppUpdateManager', () => {
});
expect(backendApp.DownloadUpdate).toHaveBeenCalledTimes(1);
expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1);
// 下载完成后不自动安装;用户需点击「重启应用更新」
expect(backendApp.InstallUpdateAndRestart).not.toHaveBeenCalled();
expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
expect(hook?.updateDownloadProgress.status).toBe('done');
expect(hook?.updateDownloadProgress.percent).toBe(100);
expect(hook?.updateDownloadProgress.open).toBe(true);
expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
});
it('installs and restarts only after the user confirms restart-to-update', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
hasUpdate: true,
currentVersion: '0.8.1',
latestVersion: '0.8.2',
downloaded: true,
assetSize: 2048,
},
});
backendApp.InstallUpdateAndRestart.mockResolvedValue({ success: true });
renderHook();
await act(async () => {
await hook?.checkForUpdates(false);
});
await act(async () => {
await hook?.handleInstallFromProgress();
});
expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1);
});
it('switches update channel and re-checks against the selected channel', async () => {

View File

@@ -129,11 +129,6 @@ const normalizeAboutInfo = (value: unknown): AboutInfo => {
};
};
const shouldAutoInstallDownloadedUpdate = (resultData: UpdateDownloadResultData | null | undefined): boolean => {
const platform = String(resultData?.platform || '').trim().toLowerCase();
return platform === 'darwin' && resultData?.autoRelaunch !== false;
};
export const useAppUpdateManager = ({
runtimeBuildType,
t,
@@ -191,7 +186,7 @@ export const useAppUpdateManager = ({
const localDownloaded = updateDownloadedVersionRef.current === buildUpdateKey(info);
const hasDownloaded = Boolean(info.downloaded) || localDownloaded;
return hasDownloaded
? t('app.about.update_status.new_version_downloaded', { version: info.latestVersion })
? t('app.about.update_status.new_version_ready_restart', { version: info.latestVersion })
: t('app.about.update_status.new_version_not_downloaded', { version: info.latestVersion });
}
return t('app.about.update_status.latest', { version: info.currentVersion || t('common.unknown') });
@@ -273,27 +268,22 @@ export const useAppUpdateManager = ({
downloadPath: resultData?.downloadPath || prev.downloadPath || info.downloadPath,
};
});
if (resultData?.downloadPath) {
void message.success({ content: t('app.about.message.download_completed_with_path', { path: resultData.downloadPath }), duration: 5 });
} else {
void message.success({ content: t('app.about.message.download_completed'), duration: 2 });
}
// 与 Terminus/Codex 一致:下载到 100% 后停留在就绪态,由用户点击「重启应用更新」
setUpdateDownloadProgress((prev) => ({
...prev,
open: true,
status: 'done',
percent: 100,
downloaded: prev.total > 0 ? prev.total : (info.assetSize || prev.downloaded),
message: t('app.about.download_progress.ready_to_restart'),
}));
void message.success({
content: resultData?.downloadPath
? t('app.about.message.download_ready_restart_with_path', { path: resultData.downloadPath })
: t('app.about.message.download_ready_restart'),
duration: 4,
});
setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, channel: normalizeUpdateChannel(info.channel), downloaded: true }));
if (shouldAutoInstallDownloadedUpdate(resultData)) {
let installRes: any = null;
try {
installRes = await (window as any).go?.app?.App?.InstallUpdateAndRestart?.();
} catch (error: any) {
installRes = { success: false, message: error?.message || t('common.unknown') };
}
if (!installRes?.success) {
void message.error(t('app.about.message.install_failed_with_error', { error: installRes?.message || t('common.unknown') }));
return;
}
updateInstallTriggeredVersionRef.current = targetKey || null;
setUpdateDownloadProgress((prev) => ({ ...prev, open: false }));
}
} else {
setUpdateDownloadProgress((prev) => ({
...prev,
@@ -335,14 +325,40 @@ export const useAppUpdateManager = ({
if (!canInstall) {
return;
}
const res = await (window as any).go.app.App.InstallUpdateAndRestart();
// 点击后进入「正在应用并重启」态,再拉起安装脚本并退出
setUpdateDownloadProgress((prev) => ({
...prev,
open: true,
status: 'downloading',
percent: 100,
message: t('app.about.download_progress.applying_restart'),
}));
let res: any = null;
try {
res = await (window as any).go?.app?.App?.InstallUpdateAndRestart?.();
} catch (error: any) {
res = { success: false, message: error?.message || t('common.unknown') };
}
if (!res?.success) {
setUpdateDownloadProgress((prev) => ({
...prev,
open: true,
status: 'error',
message: res?.message || t('common.unknown'),
}));
void message.error(t('app.about.message.install_failed_with_error', { error: res?.message || t('common.unknown') }));
return;
}
updateInstallTriggeredVersionRef.current = lastUpdateKey || null;
hideUpdateDownloadProgress();
}, [hideUpdateDownloadProgress, lastUpdateInfo, lastUpdateKey, t, updateDownloadProgress.status]);
// 后端会 Quit此处保持弹窗文案避免用户误以为失败
setUpdateDownloadProgress((prev) => ({
...prev,
open: true,
status: 'done',
percent: 100,
message: t('app.about.download_progress.restarting'),
}));
}, [lastUpdateInfo, lastUpdateKey, t, updateDownloadProgress.status]);
const openDownloadedUpdateDirectory = useCallback(async () => {
const backendApp = (window as any).go?.app?.App;
@@ -620,16 +636,31 @@ export const useAppUpdateManager = ({
? event.percent
: (total > 0 ? (downloaded / total) * 100 : 0);
const percent = Math.max(0, Math.min(100, percentRaw));
setUpdateDownloadProgress((prev) => ({
open: prev.open,
version: prev.version,
key: prev.key,
status: nextStatus,
percent,
downloaded,
total,
message: String(event.message || ''),
}));
setUpdateDownloadProgress((prev) => {
// 用户已点「重启应用更新」时,不让 downloading 事件把 100% 就绪态打回中间态文案
if (updateInstallTriggeredVersionRef.current && prev.key && updateInstallTriggeredVersionRef.current === prev.key) {
return prev;
}
const eventMessage = String(event.message || '');
let message = eventMessage;
if (!message) {
if (nextStatus === 'done') {
message = t('app.about.download_progress.ready_to_restart');
} else if (nextStatus === 'start' || nextStatus === 'downloading') {
message = t('app.about.download_progress.downloading');
}
}
return {
open: prev.open || nextStatus === 'start' || nextStatus === 'downloading' || nextStatus === 'done' || nextStatus === 'error',
version: prev.version,
key: prev.key,
status: nextStatus,
percent: nextStatus === 'done' ? 100 : percent,
downloaded: nextStatus === 'done' && total > 0 ? total : downloaded,
total: total > 0 ? total : prev.total,
message,
};
});
});
} catch (e) {
console.warn('Wails API: EventsOn unavailable', e);
@@ -637,7 +668,7 @@ export const useAppUpdateManager = ({
return () => {
if (offDownloadProgress) offDownloadProgress();
};
}, []);
}, [t]);
return {
aboutDisplayVersion,