155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
/**
|
||
* 自动更新:electron-updater(generic provider,消费静态托管的 latest*.yml + 安装包)
|
||
* Windows 消费 latest.yml + exe;mac 消费 latest-mac.yml + zip
|
||
* mac 未签名包无法通过更新签名校验:捕获后降级为「前往下载页手动安装」提示
|
||
*/
|
||
import type { UpdateInfo } from 'electron-updater';
|
||
|
||
import { app, dialog, shell } from 'electron';
|
||
import log from 'electron-log/main';
|
||
import electronUpdater from 'electron-updater';
|
||
|
||
import { DOWNLOAD_PAGE_URL, PRODUCT_NAME } from './config';
|
||
import { appState } from './state';
|
||
import { getMainWindow } from './window';
|
||
|
||
// electron-updater 是 CJS 包,ESM 下先整体导入再解构(named import 在部分打包场景会失败)
|
||
const { autoUpdater } = electronUpdater;
|
||
|
||
/** 是否用户手动触发的检查:手动才弹「已是最新/失败」反馈,启动时的静默检查不打扰用户 */
|
||
let manualCheck = false;
|
||
/** 防止重复初始化事件监听 */
|
||
let initialized = false;
|
||
|
||
/** 把更新流程事件转发给渲染层(web 侧可用 desktopAPI.onUpdateEvent 展示下载进度) */
|
||
function sendUpdateEvent(type: string, payload?: unknown): void {
|
||
getMainWindow()?.webContents.send('desktop:update-event', { payload, type });
|
||
}
|
||
|
||
/**
|
||
* 初始化更新器:注册事件 + 启动延时静默检查
|
||
* 只在打包环境生效(开发态没有 app-update.yml,检查必然报错)
|
||
*/
|
||
export function initUpdater(): void {
|
||
if (!app.isPackaged || initialized) return;
|
||
initialized = true;
|
||
|
||
autoUpdater.logger = log;
|
||
autoUpdater.autoDownload = true;
|
||
// 用户选「退出时安装」时靠这个兜底自动装
|
||
autoUpdater.autoInstallOnAppQuit = true;
|
||
|
||
autoUpdater.on('checking-for-update', () => sendUpdateEvent('checking'));
|
||
autoUpdater.on('update-available', (info: UpdateInfo) =>
|
||
sendUpdateEvent('available', { version: info.version }),
|
||
);
|
||
autoUpdater.on('update-not-available', () => {
|
||
sendUpdateEvent('not-available');
|
||
if (manualCheck) {
|
||
dialog.showMessageBox({
|
||
message: '当前已是最新版本',
|
||
title: PRODUCT_NAME,
|
||
type: 'info',
|
||
});
|
||
}
|
||
manualCheck = false;
|
||
});
|
||
autoUpdater.on('download-progress', (progress) =>
|
||
sendUpdateEvent('progress', {
|
||
percent: progress.percent,
|
||
transferred: progress.transferred,
|
||
total: progress.total,
|
||
}),
|
||
);
|
||
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
|
||
sendUpdateEvent('downloaded', { version: info.version });
|
||
void promptInstall(info);
|
||
});
|
||
autoUpdater.on('error', (error) => {
|
||
const message = String(error?.message ?? error);
|
||
sendUpdateEvent('error', { message });
|
||
log.error('[updater] 更新出错:', message);
|
||
// mac 未签名包的签名校验失败:降级为手动下载引导
|
||
const isMacSignatureIssue =
|
||
process.platform === 'darwin' &&
|
||
/code signature|codesign|not signed/i.test(message);
|
||
if (isMacSignatureIssue) {
|
||
void promptManualDownload();
|
||
} else if (manualCheck) {
|
||
dialog.showMessageBox({
|
||
detail: message,
|
||
message: '检查更新失败',
|
||
title: PRODUCT_NAME,
|
||
type: 'warning',
|
||
});
|
||
}
|
||
manualCheck = false;
|
||
});
|
||
|
||
// 启动 8 秒后静默检查:等窗口/协议就绪,也避免抢启动期资源
|
||
setTimeout(() => {
|
||
autoUpdater
|
||
.checkForUpdates()
|
||
.catch((error) => log.warn('[updater] 启动静默检查失败:', error));
|
||
}, 8000);
|
||
}
|
||
|
||
/** 手动检查更新(托盘菜单 / desktopAPI.checkUpdate 共用入口) */
|
||
export function checkUpdateManually(): void {
|
||
if (!app.isPackaged) {
|
||
dialog.showMessageBox({
|
||
message: '开发模式不支持检查更新',
|
||
title: PRODUCT_NAME,
|
||
type: 'info',
|
||
});
|
||
return;
|
||
}
|
||
manualCheck = true;
|
||
autoUpdater
|
||
.checkForUpdates()
|
||
.catch((error) => log.warn('[updater] 手动检查失败:', error));
|
||
}
|
||
|
||
/** 立即重启安装(更新属于真正退出,先置 quitting 绕过「关闭进托盘」拦截) */
|
||
export function quitAndInstallNow(): void {
|
||
appState.quitting = true;
|
||
// isSilent=false 走正常安装界面;isForceRunAfter=true 装完自动拉起新版本
|
||
autoUpdater.quitAndInstall(false, true);
|
||
}
|
||
|
||
/** 下载完成后的安装确认弹窗 */
|
||
async function promptInstall(info: UpdateInfo): Promise<void> {
|
||
const options = {
|
||
buttons: ['立即重启安装', '退出时安装'],
|
||
cancelId: 1,
|
||
defaultId: 0,
|
||
detail: '选择「退出时安装」则下次退出应用时自动完成升级。',
|
||
message: `新版本 ${info.version} 已下载完成,是否立即重启安装?`,
|
||
title: PRODUCT_NAME,
|
||
type: 'info' as const,
|
||
};
|
||
const win = getMainWindow();
|
||
const { response } = win
|
||
? await dialog.showMessageBox(win, options)
|
||
: await dialog.showMessageBox(options);
|
||
if (response === 0) {
|
||
quitAndInstallNow();
|
||
}
|
||
}
|
||
|
||
/** mac 未签名降级:引导用户去下载页手动装新版本 */
|
||
async function promptManualDownload(): Promise<void> {
|
||
const { response } = await dialog.showMessageBox({
|
||
buttons: ['打开下载页', '取消'],
|
||
cancelId: 1,
|
||
defaultId: 0,
|
||
detail: '当前安装包未进行代码签名,无法自动更新,请前往下载页手动安装最新版本。',
|
||
message: '检测到新版本',
|
||
title: PRODUCT_NAME,
|
||
type: 'info',
|
||
});
|
||
if (response === 0) {
|
||
void shell.openExternal(DOWNLOAD_PAGE_URL);
|
||
}
|
||
}
|