| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- // electron/modules/updater.ts - 自动更新模块
- import { autoUpdater, UpdateInfo } from 'electron-updater'
- import { BrowserWindow, app } from 'electron'
- import * as path from 'path'
- import * as fs from 'fs'
- import logger from './logger'
- // 更新状态
- export type UpdateStatus =
- | 'checking'
- | 'available'
- | 'not-available'
- | 'downloading'
- | 'downloaded'
- | 'error'
- // 更新进度信息
- export interface UpdateProgress {
- percent: number
- bytesPerSecond: number
- total: number
- transferred: number
- }
- // 更新信息
- export interface UpdateResult {
- status: UpdateStatus
- info?: UpdateInfo
- progress?: UpdateProgress
- error?: string
- }
- // 主窗口引用
- let mainWindow: BrowserWindow | null = null
- /**
- * 检测是否为 Windows Portable 模式(启动器模式)
- * 启动器模式的特征:exe 位于 app 子目录中
- */
- export function isPortableMode(): boolean {
- const exePath = process.execPath.toLowerCase()
- // 检查是否在 app 子目录中(启动器模式)
- const isInAppDir = exePath.includes(`${path.sep}app${path.sep}`)
- // 或者文件名包含 portable
- const isPortableName = exePath.includes('portable')
- return isInAppDir || isPortableName
- }
- /**
- * 获取 Portable 根目录(启动器所在目录)
- */
- function getPortableRootDir(): string {
- const exeDir = path.dirname(process.execPath)
- // 如果在 app 子目录中,返回上级目录
- if (exeDir.toLowerCase().endsWith(`${path.sep}app`)) {
- return path.dirname(exeDir)
- }
- return exeDir
- }
- /**
- * 获取更新目录(Portable 模式专用)
- */
- function getUpdateDir(): string {
- return path.join(getPortableRootDir(), 'update')
- }
- /**
- * 获取应用目录(Portable 模式专用)
- */
- function getAppDir(): string {
- return path.join(getPortableRootDir(), 'app')
- }
- /**
- * 发送更新状态到渲染进程
- */
- function sendUpdateStatus(result: UpdateResult): void {
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.webContents.send('updater:status', result)
- }
- }
- /**
- * 初始化自动更新器
- */
- export function initAutoUpdater(window: BrowserWindow): void {
- mainWindow = window
- // 配置
- autoUpdater.autoDownload = false // 不自动下载,让用户确认
- autoUpdater.autoInstallOnAppQuit = true
- autoUpdater.autoRunAppAfterInstall = true
- // 检查更新中
- autoUpdater.on('checking-for-update', () => {
- logger.info('正在检查更新...')
- sendUpdateStatus({ status: 'checking' })
- })
- // 有可用更新
- autoUpdater.on('update-available', (info: UpdateInfo) => {
- logger.info('发现新版本', { version: info.version })
- sendUpdateStatus({ status: 'available', info })
- })
- // 没有更新
- autoUpdater.on('update-not-available', (info: UpdateInfo) => {
- logger.info('当前已是最新版本', { version: info.version })
- sendUpdateStatus({ status: 'not-available', info })
- })
- // 下载进度
- autoUpdater.on('download-progress', (progress) => {
- logger.debug('下载进度', { percent: progress.percent.toFixed(2) })
- sendUpdateStatus({
- status: 'downloading',
- progress: {
- percent: progress.percent,
- bytesPerSecond: progress.bytesPerSecond,
- total: progress.total,
- transferred: progress.transferred
- }
- })
- })
- // 下载完成
- autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
- logger.info('更新下载完成', { version: info.version })
- sendUpdateStatus({ status: 'downloaded', info })
- })
- // 错误
- autoUpdater.on('error', (error) => {
- logger.error('更新错误', error)
- sendUpdateStatus({ status: 'error', error: error.message })
- })
- logger.info('自动更新器已初始化', {
- isPortable: isPortableMode(),
- currentVersion: app.getVersion()
- })
- }
- /**
- * 检查更新
- */
- export async function checkForUpdates(): Promise<UpdateResult> {
- try {
- logger.info('手动检查更新')
- const result = await autoUpdater.checkForUpdates()
- if (result?.updateInfo) {
- return { status: 'available', info: result.updateInfo }
- }
- return { status: 'not-available' }
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error)
- logger.error('检查更新失败', error)
- return { status: 'error', error: message }
- }
- }
- /**
- * 下载更新
- */
- export async function downloadUpdate(): Promise<UpdateResult> {
- try {
- logger.info('开始下载更新')
- await autoUpdater.downloadUpdate()
- return { status: 'downloading' }
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error)
- logger.error('下载更新失败', error)
- return { status: 'error', error: message }
- }
- }
- /**
- * 安装更新并重启
- * 自动检测是否为 Portable 模式并使用对应的更新方式
- */
- export function installUpdate(): void {
- logger.info('安装更新并重启', { isPortable: isPortableMode() })
- if (isPortableMode()) {
- // Portable 模式:通过启动器更新
- installPortableUpdate()
- } else {
- // 标准模式:使用 electron-updater
- autoUpdater.quitAndInstall(false, true)
- }
- }
- /**
- * 获取当前版本
- */
- export function getCurrentVersion(): string {
- return app.getVersion()
- }
- /**
- * Portable 模式:将下载的更新文件移动到 update 目录
- * 启动器会在下次启动时自动应用更新
- */
- export async function preparePortableUpdate(): Promise<boolean> {
- if (!isPortableMode()) {
- logger.info('非 Portable 模式,跳过 Portable 更新准备')
- return false
- }
- try {
- const updateDir = getUpdateDir()
- // 确保更新目录存在
- if (!fs.existsSync(updateDir)) {
- fs.mkdirSync(updateDir, { recursive: true })
- }
- // electron-updater 下载的文件位置
- const downloadedUpdatePath = path.join(app.getPath('userData'), 'pending')
- if (fs.existsSync(downloadedUpdatePath)) {
- // 查找下载的 exe 文件
- const files = fs.readdirSync(downloadedUpdatePath)
- const exeFile = files.find(f => f.endsWith('.exe'))
- if (exeFile) {
- const srcPath = path.join(downloadedUpdatePath, exeFile)
- const destPath = path.join(updateDir, exeFile)
- // 复制到 update 目录
- fs.copyFileSync(srcPath, destPath)
- logger.info('Portable 更新文件已准备', { destPath })
- return true
- }
- }
- logger.warn('未找到下载的更新文件')
- return false
- } catch (error) {
- logger.error('准备 Portable 更新失败', error)
- return false
- }
- }
- /**
- * Portable 模式:安装更新
- * 通过重启应用让启动器完成更新
- */
- export function installPortableUpdate(): void {
- if (!isPortableMode()) {
- // 非 Portable 模式,使用标准更新
- autoUpdater.quitAndInstall(false, true)
- return
- }
- logger.info('Portable 模式:准备重启以应用更新')
- // 查找启动器
- const rootDir = getPortableRootDir()
- const launcherPath = path.join(rootDir, 'Claude-AI-Installer.exe')
- if (fs.existsSync(launcherPath)) {
- // 通过启动器重启
- const { spawn } = require('child_process')
- spawn(launcherPath, [], {
- detached: true,
- stdio: 'ignore',
- cwd: rootDir
- }).unref()
- logger.info('已启动启动器,准备退出当前应用')
- app.quit()
- } else {
- // 没有启动器,直接退出(用户需要手动重启)
- logger.warn('未找到启动器,请手动重启应用')
- app.quit()
- }
- }
- /**
- * 获取 Portable 更新信息
- */
- export function getPortableUpdateInfo(): { hasUpdate: boolean; updateDir: string; appDir: string } {
- const updateDir = getUpdateDir()
- const appDir = getAppDir()
- let hasUpdate = false
- if (fs.existsSync(updateDir)) {
- const files = fs.readdirSync(updateDir)
- hasUpdate = files.some(f => f.endsWith('.exe'))
- }
- return { hasUpdate, updateDir, appDir }
- }
|