/** * UpdateService — 轻量自动更新检查(v0.6.4 P4-2 引入,v0.8.0 P2-3 接入 electron-updater) * * 双轨设计: * 1. 手动检查(check()):feed 比对式 —— 拉取版本清单 JSON 与当前版本比较, * 结果经 IPC 直接返回(不依赖 electron-updater,dev 模式同样可用)。 * 2. 自动更新(startAutoUpdater,v0.8.0 P2-3):生产环境接入 electron-updater * (generic provider),启动 5s 后静默检查;available → 广播 update:status + * 系统通知;用户触发下载安装(app:updateInstall)→ downloadAndInstall 后重启。 * 未配置 feed / dev 模式 / electron-updater 不可用时静默禁用(可用性优先)。 */ import log from 'electron-log'; export interface UpdateFeedInfo { version: string; url?: string; notes?: string; } export type UpdateCheckResult = | { status: 'disabled'; message: string } | { status: 'error'; message: string } | { status: 'up-to-date'; latestVersion: string } | { status: 'available'; latestVersion: string; downloadUrl?: string; notes?: string }; /** v0.8.0 P2-3: 广播给渲染层的更新状态事件(update:status 通道载荷) */ export type UpdateStatusEvent = | { status: 'checking' } | { status: 'available'; latestVersion: string; notes?: string } | { status: 'not-available'; latestVersion: string } | { status: 'downloading'; percent: number } | { status: 'downloaded' } | { status: 'error'; message: string }; /** * 语义化版本比较(仅支持 Major.Minor.Patch 与可选的预发布后缀忽略策略: * 带预发布标签视为小于同号正式版)。非法输入返回 false(宁可不提示升级)。 */ export function isNewerVersion(candidate: string, current: string): boolean { const parse = (v: string): number[] | null => { if (typeof v !== 'string') return null; const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim()); if (!m) return null; return [Number(m[1]), Number(m[2]), Number(m[3])]; }; const a = parse(candidate); const b = parse(current); if (!a || !b) return false; // 预发布标记(如 1.2.3-beta)小于同号正式版 const preA = /-/.test(candidate.trim()); const preB = /-/.test(current.trim()); for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; } if (a.join('.') === b.join('.')) { // 同号:pre-release < stable return !preA && preB; } return false; } export class UpdateService { constructor( private readonly getCurrentVersion: () => string, private readonly getFeedUrl: () => string | null, ) {} async check(): Promise { const feedUrl = this.getFeedUrl(); if (!feedUrl) { return { status: 'disabled', message: '未配置更新源(app.updateFeedUrl)' }; } try { const response = await fetch(feedUrl, { signal: AbortSignal.timeout(10_000), headers: { Accept: 'application/json' }, }); if (!response.ok) { return { status: 'error', message: `更新源响应异常(HTTP ${response.status})` }; } const feed = (await response.json()) as Partial; const latestVersion = typeof feed.version === 'string' ? feed.version : ''; if (!latestVersion) { return { status: 'error', message: '更新源缺少 version 字段' }; } const current = this.getCurrentVersion(); if (!isNewerVersion(latestVersion, current)) { return { status: 'up-to-date', latestVersion }; } return { status: 'available', latestVersion, downloadUrl: typeof feed.url === 'string' && /^https?:\/\//i.test(feed.url) ? feed.url : undefined, notes: typeof feed.notes === 'string' ? feed.notes : undefined, }; } catch (err) { return { status: 'error', message: `检查失败:${(err as Error).message}` }; } } } // ===== v0.8.0 P2-3: electron-updater 集成(生产环境自动更新) ===== /** AutoUpdater 句柄(IPC 层消费;null = 未启用/dev 模式/未配置 feed) */ export interface AutoUpdaterHandle { checkForUpdates: () => Promise; downloadAndInstall: () => Promise; } let activeHandle: AutoUpdaterHandle | null = null; /** 获取已启动的 AutoUpdater 句柄(app:updateInstall 消费;未启用时返回 null) */ export function getAutoUpdaterHandle(): AutoUpdaterHandle | null { return activeHandle; } /** * 启动 electron-updater 自动更新(生产环境专用)。 * * @param feedUrl 更新源根目录(generic provider;app.updateFeedUrl 配置) * @param onEvent 状态事件回调(主进程广播 update:status 给渲染层) * @param autoCheckDelayMs 启动后延迟检查时间(默认 5s,让窗口先就绪) * @returns 句柄(未启用时返回 null —— dev / electron-updater 加载失败) */ export async function startAutoUpdater( feedUrl: string, onEvent: (event: UpdateStatusEvent) => void, autoCheckDelayMs = 5_000, ): Promise { if (!feedUrl) { log.info('[Update] auto-updater disabled: no feed URL configured'); return null; } try { // 动态导入:dev 模式(未打包)下 electron-updater 无法工作(无 app-update.yml), // 加载失败时静默禁用,保持可用性 const { autoUpdater } = await import('electron-updater'); autoUpdater.autoDownload = false; // 下载需用户触发(app:updateInstall) autoUpdater.autoInstallOnAppQuit = true; autoUpdater.logger = null; // 事件统一经 onEvent 广播,避免双通道日志噪声 autoUpdater.on('checking-for-update', () => onEvent({ status: 'checking' })); autoUpdater.on('update-available', (info) => { onEvent({ status: 'available', latestVersion: String(info.version ?? ''), notes: undefined }); }); autoUpdater.on('update-not-available', (info) => { onEvent({ status: 'not-available', latestVersion: String(info.version ?? '') }); }); autoUpdater.on('download-progress', (progress) => { onEvent({ status: 'downloading', percent: Math.round(progress.percent ?? 0) }); }); autoUpdater.on('update-downloaded', () => onEvent({ status: 'downloaded' })); autoUpdater.on('error', (err) => { onEvent({ status: 'error', message: (err as Error).message }); }); // generic provider feed(app-update.yml 缺失时显式 setFeedURL 兜底) autoUpdater.setFeedURL({ provider: 'generic', url: feedUrl }); const handle: AutoUpdaterHandle = { checkForUpdates: async () => { try { await autoUpdater.checkForUpdates(); } catch (err) { onEvent({ status: 'error', message: (err as Error).message }); } }, downloadAndInstall: async () => { try { await autoUpdater.downloadUpdate(); onEvent({ status: 'downloaded' }); autoUpdater.quitAndInstall(); } catch (err) { onEvent({ status: 'error', message: (err as Error).message }); } }, }; activeHandle = handle; // 启动 5s 后静默检查(不阻塞窗口就绪) const timer = setTimeout(() => { void handle.checkForUpdates(); }, autoCheckDelayMs); timer.unref?.(); log.info(`[Update] auto-updater started (feed=${feedUrl})`); return handle; } catch (err) { log.warn( '[Update] electron-updater unavailable, auto-update disabled:', (err as Error).message, ); return null; } }