/** * 自动更新: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 { 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 { 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); } }