// ============================================================ // 自动更新模块 — 自定义 API 版本检查 + OSS 文件下载 // // 架构: // 客户端 → GET /api/v1/update/check → ECS (API) → 返回 JSON // 客户端 → GET → OSS → 下载安装包 // 下载完成 → SHA512 校验 → spawn 安装器 → app.quit() // // 与旧版 (electron-updater generic provider) 的区别: // - 版本检查走自定义 API,后端可做灰度/强更/统计 // - 不再依赖 latest.yml 静态文件 // - 安装包仍托管于 OSS,下载地址由 API 动态返回 // // 环境变量: // UPDATE_API_URL — 版本检查 API 基地址(默认 https://www.heixiu.com) // ============================================================ import { BrowserWindow, net, app, ipcMain } from 'electron'; import { createWriteStream } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createHash } from 'node:crypto'; import { spawn } from 'node:child_process'; import { unlink } from 'node:fs/promises'; import type { UpdateCheckResult } from '../../shared/types'; import { APP_VERSION } from '../../shared/constants/version'; // ---------- 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', } as const; // ---------- 配置 ---------- function getApiBaseUrl(): string { return process.env.UPDATE_API_URL || 'https://www.heixiu.com'; } // ---------- 状态 ---------- let updateWindow: BrowserWindow | null = null; let updateChecked = false; /** 当前正在下载的临时文件路径(用于安装后清理) */ let pendingInstallerPath: string | null = null; // ============================================================ // 初始化 // ============================================================ export function initUpdater(mainWindow: BrowserWindow): void { updateWindow = mainWindow; if (import.meta.env.DEV) { console.log('[Updater] 开发模式,跳过自动检查更新。API:', getApiBaseUrl()); return; } // 启动 5 秒后静默检查 setTimeout(() => { checkForUpdates(); }, 5000); } // ============================================================ // 版本检查(调用 API) // ============================================================ async function fetchUpdateInfo(): Promise { const platform = process.platform; // 'win32' | 'darwin' | 'linux' const edition = process.env.EDITION || 'personal'; const apiUrl = `${getApiBaseUrl()}/api/v1/update/check?platform=${platform}&version=${APP_VERSION}&edition=${edition}`; console.log('[Updater] 请求版本检查 API:', apiUrl); return new Promise((resolve, reject) => { const req = net.request({ method: 'GET', url: apiUrl }); req.on('response', (response) => { let body = ''; response.on('data', (chunk) => (body += chunk.toString())); response.on('end', () => { try { const json = JSON.parse(body); // 兼容两种 API 响应格式: // 格式 A: { code: 0, data: UpdateCheckResult } // 格式 B: UpdateCheckResult 直接返回 const data: UpdateCheckResult = json.data ?? json; if (!data.hasUpdate) { console.log('[Updater] 当前已是最新版本'); return resolve(null); } console.log('[Updater] 发现新版本:', data.latestVersion); resolve(data); } catch (err) { reject(new Error(`API 响应解析失败: ${(err as Error).message}`)); } }); }); req.on('error', (err) => reject(new Error(`API 请求失败: ${err.message}`))); req.end(); }); } // ============================================================ // 下载安装包 // ============================================================ async function downloadInstaller(info: UpdateCheckResult): Promise { const ext = process.platform === 'win32' ? '.exe' : process.platform === 'darwin' ? '.dmg' : '.AppImage'; const tempPath = join(tmpdir(), `HeiXiu-${info.latestVersion}-update${ext}`); console.log('[Updater] 开始下载:', info.downloadUrl); console.log('[Updater] 临时文件:', tempPath); return new Promise((resolve, reject) => { const req = net.request({ method: 'GET', url: info.downloadUrl }); const fileStream = createWriteStream(tempPath); const hash = createHash('sha512'); const totalSize = info.fileSize || 0; let downloadedSize = 0; let lastReportTime = 0; req.on('response', (response) => { const statusCode = response.statusCode; if (statusCode !== 200) { fileStream.close(); unlink(tempPath).catch(() => {}); return reject(new Error(`下载失败,HTTP ${statusCode}`)); } response.on('data', (chunk: Buffer) => { fileStream.write(chunk); hash.update(chunk); downloadedSize += chunk.length; // 限频:最多每 250ms 推送一次进度 const now = Date.now(); if (totalSize > 0 && now - lastReportTime >= 250) { lastReportTime = now; const percent = Math.round((downloadedSize / totalSize) * 100); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_PROGRESS, { percent, bytesPerSecond: 0, // 简化处理,不计算实时速度 transferred: downloadedSize, total: totalSize, }); } }); response.on('end', () => { fileStream.end(); fileStream.on('finish', () => { // SHA512 校验 const actualSha512 = hash.digest('base64'); if (info.sha512 && actualSha512 !== info.sha512) { unlink(tempPath).catch(() => {}); return reject( new Error( `SHA512 校验失败\n期望: ${info.sha512.slice(0, 16)}...\n实际: ${actualSha512.slice(0, 16)}...`, ), ); } console.log('[Updater] 下载完成,SHA512 校验通过'); pendingInstallerPath = tempPath; resolve(tempPath); }); }); response.on('error', (err) => { fileStream.close(); unlink(tempPath).catch(() => {}); reject(new Error(`下载中断: ${err.message}`)); }); }); req.on('error', (err) => { fileStream.close(); unlink(tempPath).catch(() => {}); reject(new Error(`下载请求失败: ${err.message}`)); }); req.end(); }); } // ============================================================ // 操作方法 // ============================================================ export async function checkForUpdates(): Promise { if (updateChecked) { console.log('[Updater] 已检查过更新,跳过'); return; } if (import.meta.env.DEV) { console.log('[Updater] 开发模式,跳过更新检查'); return; } try { updateChecked = true; const info = await fetchUpdateInfo(); if (!info) { // 无更新,不发通知(静默) return; } // 通知渲染进程:有新版本 updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, { version: info.latestVersion, releaseDate: info.releaseDate, releaseNotes: info.changelog, forceUpdate: info.forceUpdate, }); // 自动开始下载 await downloadInstaller(info); // 下载完成 → 通知渲染进程 updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED); } catch (error) { console.error('[Updater] 更新流程失败:', (error as Error).message); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, { message: (error as Error).message || '更新检查失败', }); updateChecked = false; } } export async function checkForUpdatesManual(): Promise { try { updateChecked = false; const info = await fetchUpdateInfo(); if (!info) { updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE); return; } updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, { version: info.latestVersion, releaseDate: info.releaseDate, releaseNotes: info.changelog, forceUpdate: info.forceUpdate, }); await downloadInstaller(info); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED); } catch (error) { console.error('[Updater] 手动更新失败:', (error as Error).message); updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, { message: (error as Error).message || '检查更新失败', }); } } export function installUpdateNow(): void { if (!pendingInstallerPath) { console.error('[Updater] 没有待安装的更新文件'); return; } console.log('[Updater] 开始安装更新:', pendingInstallerPath); if (process.platform === 'win32') { // Windows: NSIS 安装器,/S 静默安装 spawn(pendingInstallerPath, ['/S'], { detached: true, stdio: 'ignore', }); } else if (process.platform === 'darwin') { // macOS: 挂载 DMG spawn('hdiutil', ['attach', pendingInstallerPath], { detached: true, stdio: 'ignore', }); } else { // Linux: AppImage,chmod +x 后执行(通常需要手动替换) console.log('[Updater] Linux 更新请手动替换 AppImage 文件'); } // 退出应用,让安装器接管 setImmediate(() => { app.quit(); }); } // ============================================================ // IPC 注册 // ============================================================ export function registerUpdateIpcHandlers(): void { ipcMain.handle(UPDATE_CHANNELS.CHECK_FOR_UPDATE, async () => { await checkForUpdatesManual(); }); ipcMain.on(UPDATE_CHANNELS.INSTALL_UPDATE, () => { installUpdateNow(); }); }