// 自动更新 — HeiXiuProvider + electron-updater 增量下载 // 流程图:调用自有 API → 获取版本信息 → electron-updater 差分下载 → SHA512 校验 → 安装 import {app, BrowserWindow, ipcMain} from 'electron'; import {createHash} from 'node:crypto'; import {URL} from 'node:url'; import { NsisUpdater, MacUpdater, AppImageUpdater, Provider, } from 'electron-updater'; import type {ProviderRuntimeOptions} from 'electron-updater/out/providers/Provider'; import type {UpdateInfo, UpdateFileInfo} from 'builder-util-runtime'; import type {ResolvedUpdateFileInfo} from 'electron-updater/out/types'; import type {UpdateCheckResult} from '../../shared/types'; import {APP_VERSION} from '../../shared/constants/version'; import {logger} from './logger'; import {netRequest} from './net-request'; // ---------- 构建时注入的渠道信息 ---------- /** 构建渠道(由 build.mjs 通过 VITE_BUILD_CHANNEL 注入,Vite 编译时内联) */ const BUILD_CHANNEL: string = (typeof import.meta !== 'undefined' && (import.meta as Record).env ? ((import.meta as Record).env as Record).VITE_BUILD_CHANNEL : undefined) || process.env.VITE_BUILD_CHANNEL || 'stable'; /** 构建版本类型(由 build.mjs 通过 VITE_BUILD_EDITION 注入) */ const BUILD_EDITION: string = (typeof import.meta !== 'undefined' && (import.meta as Record).env ? ((import.meta as Record).env as Record).VITE_BUILD_EDITION : undefined) || process.env.VITE_BUILD_EDITION || 'personal'; // ---------- 设备指纹(稳定的机器标识,用于灰度分组)---------- let _deviceFingerprint: string | null = null; /** 获取设备指纹(基于机器特定信息的 SHA-256 前 16 位) */ function getDeviceFingerprint(): string { if (_deviceFingerprint) return _deviceFingerprint; try { const parts = [ process.platform, process.arch, app.getPath('home'), ]; _deviceFingerprint = createHash('sha256') .update(parts.join('|')) .digest('hex') .slice(0, 16); } catch { _deviceFingerprint = 'unknown-device'; } return _deviceFingerprint; } // ---------- IPC 通道(对渲染进程暴露,与旧版兼容)---------- export const UPDATE_CHANNELS = { UPDATE_AVAILABLE: 'update-available', UPDATE_PROGRESS: 'update-progress', UPDATE_DOWNLOADED: 'update-downloaded', UPDATE_ERROR: 'update-error', UPDATE_NOT_AVAILABLE: 'update-not-available', INSTALL_UPDATE: 'install-update', CHECK_FOR_UPDATE: 'check-for-update', DOWNLOAD_UPDATE: 'download-update', } as const; // ---------- 配置 ---------- function getApiBaseUrl(): string { const url = (process.env.UPDATE_API_URL || 'https://www.heixiu.com').trim(); console.log(`[updater] UPDATE_API_URL=${process.env.UPDATE_API_URL} → 使用: ${url}`); return url; } // ============================================================ // 自定义 Provider // ============================================================ class HeiXiuProvider extends Provider { private apiBaseUrl: string; /** 短时缓存:避免 checkForUpdates 预检 + electron-updater 内部调用导致两次 API 请求 */ private _cachedResult: { data: UpdateInfo; ts: number } | null = null; constructor(runtimeOptions: ProviderRuntimeOptions) { super(runtimeOptions); this.apiBaseUrl = getApiBaseUrl(); } /** * 调用自有版本检查 API,返回 electron-updater 格式的 UpdateInfo。 * blockMapSize > 0 时 electron-updater 自动启用差分下载。 * 无更新时返回当前版本号(APP_VERSION)。 */ async getLatestVersion(): Promise { // 短时缓存(500ms):预检和 electron-updater 内部调用共享同一结果 if (this._cachedResult && Date.now() - this._cachedResult.ts < 500) { return this._cachedResult.data; } const data = await this._doGetLatestVersion(); this._cachedResult = { data, ts: Date.now() }; return data; } private async _doGetLatestVersion(): Promise { const {data} = await netRequest.get<{ code: number; data?: UpdateCheckResult } & UpdateCheckResult>( '/api/v1/update/check', { baseURL: this.apiBaseUrl, params: { platform: process.platform, version: APP_VERSION, channel: BUILD_CHANNEL, edition: BUILD_EDITION, deviceFingerprint: getDeviceFingerprint(), }, }, ); // 兼容两种 API 响应格式 const result: UpdateCheckResult = (data as { data?: UpdateCheckResult }).data ?? (data as UpdateCheckResult); // 无更新 → 返回当前版本号 if (!result.hasUpdate) { return { version: APP_VERSION, files: [], path: '', sha512: '', releaseDate: new Date().toISOString(), }; } const file: UpdateFileInfo = { url: result.downloadUrl, sha512: result.sha512, size: result.fileSize, }; // 后端 API 返回 blockMapSize 时启用增量更新 if (result.blockMapSize) { file.blockMapSize = result.blockMapSize; } logger.info('updater', 'Provider 获取到新版本', { latestVersion: result.latestVersion, hasBlockmap: file.blockMapSize != null, }); return { version: result.latestVersion, files: [file], path: result.downloadUrl, sha512: result.sha512, releaseDate: result.releaseDate, releaseNotes: result.changelog, }; } /** * 将 UpdateInfo 中的相对路径解析为完整的下载 URL。 * electron-updater 在下载前调用此方法。 */ resolveFiles(updateInfo: UpdateInfo): Array { return updateInfo.files.map((f) => { // 使用 downloadUrl 作为 base URL 来解析文件路径 const baseUrl = new URL(updateInfo.path); const fileUrl = new URL(f.url, baseUrl.origin); return { url: fileUrl, info: f, }; }); } } // ============================================================ // 平台特定 Updater(注入自定义 Provider) // ============================================================ /** * 覆盖 getUpdateInfoAndProvider() 以注入 HeiXiuProvider。 * 其他行为(NSIS 安装、签名校验等)完全由父类处理。 */ class HeiXiuNsisUpdater extends NsisUpdater { private heiXiuProvider: HeiXiuProvider; constructor(heiXiuProvider: HeiXiuProvider) { // 传 undefined 跳过内置 Provider 创建,由我们自己注入 super(undefined); this.heiXiuProvider = heiXiuProvider; this.autoDownload = false; // 由我们手动控制流程,保持 IPC 兼容 this.autoInstallOnAppQuit = false; this.forceDevUpdateConfig = true; // 绕过 electron-updater 的 dev 模式检查 } /** @internal — 注入自定义 Provider */ protected override async getUpdateInfoAndProvider(): Promise<{ info: UpdateInfo; // eslint-disable-next-line @typescript-eslint/no-explicit-any provider: Provider; }> { const info = await this.heiXiuProvider.getLatestVersion(); // eslint-disable-next-line @typescript-eslint/no-explicit-any return {info, provider: this.heiXiuProvider as Provider}; } } class HeiXiuMacUpdater extends MacUpdater { private heiXiuProvider: HeiXiuProvider; constructor(heiXiuProvider: HeiXiuProvider) { super(undefined); this.heiXiuProvider = heiXiuProvider; this.autoDownload = false; this.autoInstallOnAppQuit = false; } protected override async getUpdateInfoAndProvider(): Promise<{ info: UpdateInfo; // eslint-disable-next-line @typescript-eslint/no-explicit-any provider: Provider; }> { const info = await this.heiXiuProvider.getLatestVersion(); // eslint-disable-next-line @typescript-eslint/no-explicit-any return {info, provider: this.heiXiuProvider as Provider}; } } class HeiXiuAppImageUpdater extends AppImageUpdater { private heiXiuProvider: HeiXiuProvider; constructor(heiXiuProvider: HeiXiuProvider) { super(null); this.heiXiuProvider = heiXiuProvider; this.autoDownload = false; this.autoInstallOnAppQuit = false; } protected override async getUpdateInfoAndProvider(): Promise<{ info: UpdateInfo; // eslint-disable-next-line @typescript-eslint/no-explicit-any provider: Provider; }> { const info = await this.heiXiuProvider.getLatestVersion(); // eslint-disable-next-line @typescript-eslint/no-explicit-any return {info, provider: this.heiXiuProvider as Provider}; } } /** 工厂:按平台创建对应的 Updater 实例 */ function createPlatformUpdater(provider: HeiXiuProvider) { switch (process.platform) { case 'win32': return new HeiXiuNsisUpdater(provider); case 'darwin': return new HeiXiuMacUpdater(provider); default: return new HeiXiuAppImageUpdater(provider); } } // ---------- 状态 ---------- let updateWindow: BrowserWindow | null = null; let provider: HeiXiuProvider | null = null; let platformUpdater: NsisUpdater | MacUpdater | AppImageUpdater | null = null; // ============================================================ // 初始化 // ============================================================ export function initUpdater(mainWindow: BrowserWindow): void { updateWindow = mainWindow; console.log('[updater] initUpdater 被调用'); console.log(`[updater] API URL: ${getApiBaseUrl()}`); console.log(`[updater] 渠道: ${BUILD_CHANNEL}`); console.log(`[updater] 版本类型: ${BUILD_EDITION}`); console.log(`[updater] 当前版本: ${APP_VERSION}`); console.log(`[updater] 设备指纹: ${getDeviceFingerprint()}`); // if (import.meta.env.DEV) { // logger.info('updater', '开发模式,跳过自动检查更新', { apiUrl: getApiBaseUrl() }); // return; // } // 创建自定义 Provider provider = new HeiXiuProvider({ isUseMultipleRangeRequest: true, platform: process.platform as ProviderRuntimeOptions['platform'], executor: null as unknown as ProviderRuntimeOptions['executor'], }); // 创建平台 Updater platformUpdater = createPlatformUpdater(provider); // 将 electron-updater 事件桥接到现有 IPC 通道 platformUpdater.on('checking-for-update', () => { logger.debug('updater', '正在检查更新'); }); platformUpdater.on('update-available', (info: UpdateInfo) => { // releaseNotes 可能是 string | ReleaseNoteInfo[] | null,统一转为 string let notes = ''; if (typeof info.releaseNotes === 'string') { notes = info.releaseNotes; } else if (Array.isArray(info.releaseNotes)) { notes = info.releaseNotes.map((n) => n.note).filter(Boolean).join('\n'); } updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, { version: info.version, releaseDate: info.releaseDate, releaseNotes: notes, forceUpdate: false, }); logger.info('updater', '发现新版本', { version: info.version, releaseNotes: notes ? `${notes.slice(0, 50)}...` : '(空)', }); }); platformUpdater.on('update-not-available', (info: UpdateInfo) => { updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE); logger.info('updater', '当前已是最新版本', {currentVersion: info.version}); }); platformUpdater.on('download-progress', (progress: { percent: number; bytesPerSecond: number; transferred: number; total: number }) => { updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_PROGRESS, { percent: Math.round(progress.percent), bytesPerSecond: progress.bytesPerSecond, transferred: progress.transferred, total: progress.total, }); }); platformUpdater.on('update-downloaded', () => { updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED); logger.info('updater', '更新已下载完成'); }); platformUpdater.on('error', (error: Error) => { logger.error('updater', '更新流程出错', error); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, { message: error.message || '更新失败', }); }); // 启动 5 秒后静默检查 setTimeout(() => { checkForUpdates(); }, 5000); } // ============================================================ // 操作方法 // ============================================================ let updateChecked = false; /** 静默检查更新(自动下载) */ export async function checkForUpdates(): Promise { if (updateChecked) { logger.debug('updater', '已检查过更新,跳过'); return; } if (!platformUpdater || !provider) return; try { updateChecked = true; // ① 先自行检查有无更新(不经过 electron-updater) const info = await provider.getLatestVersion(); if (info.version === APP_VERSION) { // 无更新 → 直接通知 UI,不触发 electron-updater 的任何事件 updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE); logger.debug('updater', '当前已是最新版本', {version: APP_VERSION}); return; } // ② 有新版本 → 交给 electron-updater 触发 'update-available' 事件(不自动下载) await platformUpdater.checkForUpdates(); } catch (error) { updateChecked = false; logger.error('updater', '更新流程失败', error as Error, {apiUrl: getApiBaseUrl()}); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, { message: (error as Error).message || '更新检查失败', }); } } /** 手动检查更新(用户点击触发) */ export async function checkForUpdatesManual(): Promise { if (!platformUpdater || !provider) return; try { updateChecked = false; // ① 先自行检查有无更新 const info = await provider.getLatestVersion(); if (info.version === APP_VERSION) { // 无更新 → 直接通知 UI updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE); logger.debug('updater', '手动检查:当前已是最新版本', {version: APP_VERSION}); return; } // ② 有新版本 → 交给 electron-updater 触发 'update-available' 事件(不自动下载) await platformUpdater.checkForUpdates(); } catch (error) { logger.error('updater', '手动更新失败', error as Error); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, { message: (error as Error).message || '检查更新失败', }); } } /** 用户确认后开始下载更新 */ export async function downloadUpdate(): Promise { if (!platformUpdater) { logger.warn('updater', 'downloadUpdate: Updater 未初始化'); return; } try { await platformUpdater.downloadUpdate(); } catch (error) { logger.error('updater', '下载更新失败', error as Error); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, { message: (error as Error).message || '下载更新失败', }); } } /** 安装已下载的更新并退出应用 */ export function installUpdateNow(): void { if (!platformUpdater) { logger.warn('updater', 'Updater 未初始化'); return; } logger.info('updater', '开始安装更新并退出应用'); setImmediate(() => { platformUpdater!.quitAndInstall(false, true); }); } // ============================================================ // IPC 注册 // ============================================================ export function registerUpdateIpcHandlers(): void { ipcMain.handle(UPDATE_CHANNELS.CHECK_FOR_UPDATE, async () => { await checkForUpdatesManual(); }); ipcMain.handle(UPDATE_CHANNELS.DOWNLOAD_UPDATE, async () => { await downloadUpdate(); }); ipcMain.on(UPDATE_CHANNELS.INSTALL_UPDATE, () => { installUpdateNow(); }); }