updater.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // electron/modules/updater.ts - 自动更新模块
  2. import { autoUpdater, UpdateInfo } from 'electron-updater'
  3. import { BrowserWindow, app } from 'electron'
  4. import * as path from 'path'
  5. import * as fs from 'fs'
  6. import logger from './logger'
  7. // 更新状态
  8. export type UpdateStatus =
  9. | 'checking'
  10. | 'available'
  11. | 'not-available'
  12. | 'downloading'
  13. | 'downloaded'
  14. | 'error'
  15. // 更新进度信息
  16. export interface UpdateProgress {
  17. percent: number
  18. bytesPerSecond: number
  19. total: number
  20. transferred: number
  21. }
  22. // 更新信息
  23. export interface UpdateResult {
  24. status: UpdateStatus
  25. info?: UpdateInfo
  26. progress?: UpdateProgress
  27. error?: string
  28. }
  29. // 主窗口引用
  30. let mainWindow: BrowserWindow | null = null
  31. /**
  32. * 检测是否为 Windows Portable 模式(启动器模式)
  33. * 启动器模式的特征:exe 位于 app 子目录中
  34. */
  35. export function isPortableMode(): boolean {
  36. const exePath = process.execPath.toLowerCase()
  37. // 检查是否在 app 子目录中(启动器模式)
  38. const isInAppDir = exePath.includes(`${path.sep}app${path.sep}`)
  39. // 或者文件名包含 portable
  40. const isPortableName = exePath.includes('portable')
  41. return isInAppDir || isPortableName
  42. }
  43. /**
  44. * 获取 Portable 根目录(启动器所在目录)
  45. */
  46. function getPortableRootDir(): string {
  47. const exeDir = path.dirname(process.execPath)
  48. // 如果在 app 子目录中,返回上级目录
  49. if (exeDir.toLowerCase().endsWith(`${path.sep}app`)) {
  50. return path.dirname(exeDir)
  51. }
  52. return exeDir
  53. }
  54. /**
  55. * 获取更新目录(Portable 模式专用)
  56. */
  57. function getUpdateDir(): string {
  58. return path.join(getPortableRootDir(), 'update')
  59. }
  60. /**
  61. * 获取应用目录(Portable 模式专用)
  62. */
  63. function getAppDir(): string {
  64. return path.join(getPortableRootDir(), 'app')
  65. }
  66. /**
  67. * 发送更新状态到渲染进程
  68. */
  69. function sendUpdateStatus(result: UpdateResult): void {
  70. if (mainWindow && !mainWindow.isDestroyed()) {
  71. mainWindow.webContents.send('updater:status', result)
  72. }
  73. }
  74. /**
  75. * 初始化自动更新器
  76. */
  77. export function initAutoUpdater(window: BrowserWindow): void {
  78. mainWindow = window
  79. // 配置
  80. autoUpdater.autoDownload = false // 不自动下载,让用户确认
  81. autoUpdater.autoInstallOnAppQuit = true
  82. autoUpdater.autoRunAppAfterInstall = true
  83. // 检查更新中
  84. autoUpdater.on('checking-for-update', () => {
  85. logger.info('正在检查更新...')
  86. sendUpdateStatus({ status: 'checking' })
  87. })
  88. // 有可用更新
  89. autoUpdater.on('update-available', (info: UpdateInfo) => {
  90. logger.info('发现新版本', { version: info.version })
  91. sendUpdateStatus({ status: 'available', info })
  92. })
  93. // 没有更新
  94. autoUpdater.on('update-not-available', (info: UpdateInfo) => {
  95. logger.info('当前已是最新版本', { version: info.version })
  96. sendUpdateStatus({ status: 'not-available', info })
  97. })
  98. // 下载进度
  99. autoUpdater.on('download-progress', (progress) => {
  100. logger.debug('下载进度', { percent: progress.percent.toFixed(2) })
  101. sendUpdateStatus({
  102. status: 'downloading',
  103. progress: {
  104. percent: progress.percent,
  105. bytesPerSecond: progress.bytesPerSecond,
  106. total: progress.total,
  107. transferred: progress.transferred
  108. }
  109. })
  110. })
  111. // 下载完成
  112. autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
  113. logger.info('更新下载完成', { version: info.version })
  114. sendUpdateStatus({ status: 'downloaded', info })
  115. })
  116. // 错误
  117. autoUpdater.on('error', (error) => {
  118. logger.error('更新错误', error)
  119. sendUpdateStatus({ status: 'error', error: error.message })
  120. })
  121. logger.info('自动更新器已初始化', {
  122. isPortable: isPortableMode(),
  123. currentVersion: app.getVersion()
  124. })
  125. }
  126. /**
  127. * 检查更新
  128. */
  129. export async function checkForUpdates(): Promise<UpdateResult> {
  130. try {
  131. logger.info('手动检查更新')
  132. const result = await autoUpdater.checkForUpdates()
  133. if (result?.updateInfo) {
  134. return { status: 'available', info: result.updateInfo }
  135. }
  136. return { status: 'not-available' }
  137. } catch (error) {
  138. const message = error instanceof Error ? error.message : String(error)
  139. logger.error('检查更新失败', error)
  140. return { status: 'error', error: message }
  141. }
  142. }
  143. /**
  144. * 下载更新
  145. */
  146. export async function downloadUpdate(): Promise<UpdateResult> {
  147. try {
  148. logger.info('开始下载更新')
  149. await autoUpdater.downloadUpdate()
  150. return { status: 'downloading' }
  151. } catch (error) {
  152. const message = error instanceof Error ? error.message : String(error)
  153. logger.error('下载更新失败', error)
  154. return { status: 'error', error: message }
  155. }
  156. }
  157. /**
  158. * 安装更新并重启
  159. * 自动检测是否为 Portable 模式并使用对应的更新方式
  160. */
  161. export function installUpdate(): void {
  162. logger.info('安装更新并重启', { isPortable: isPortableMode() })
  163. if (isPortableMode()) {
  164. // Portable 模式:通过启动器更新
  165. installPortableUpdate()
  166. } else {
  167. // 标准模式:使用 electron-updater
  168. autoUpdater.quitAndInstall(false, true)
  169. }
  170. }
  171. /**
  172. * 获取当前版本
  173. */
  174. export function getCurrentVersion(): string {
  175. return app.getVersion()
  176. }
  177. /**
  178. * Portable 模式:将下载的更新文件移动到 update 目录
  179. * 启动器会在下次启动时自动应用更新
  180. */
  181. export async function preparePortableUpdate(): Promise<boolean> {
  182. if (!isPortableMode()) {
  183. logger.info('非 Portable 模式,跳过 Portable 更新准备')
  184. return false
  185. }
  186. try {
  187. const updateDir = getUpdateDir()
  188. // 确保更新目录存在
  189. if (!fs.existsSync(updateDir)) {
  190. fs.mkdirSync(updateDir, { recursive: true })
  191. }
  192. // electron-updater 下载的文件位置
  193. const downloadedUpdatePath = path.join(app.getPath('userData'), 'pending')
  194. if (fs.existsSync(downloadedUpdatePath)) {
  195. // 查找下载的 exe 文件
  196. const files = fs.readdirSync(downloadedUpdatePath)
  197. const exeFile = files.find(f => f.endsWith('.exe'))
  198. if (exeFile) {
  199. const srcPath = path.join(downloadedUpdatePath, exeFile)
  200. const destPath = path.join(updateDir, exeFile)
  201. // 复制到 update 目录
  202. fs.copyFileSync(srcPath, destPath)
  203. logger.info('Portable 更新文件已准备', { destPath })
  204. return true
  205. }
  206. }
  207. logger.warn('未找到下载的更新文件')
  208. return false
  209. } catch (error) {
  210. logger.error('准备 Portable 更新失败', error)
  211. return false
  212. }
  213. }
  214. /**
  215. * Portable 模式:安装更新
  216. * 通过重启应用让启动器完成更新
  217. */
  218. export function installPortableUpdate(): void {
  219. if (!isPortableMode()) {
  220. // 非 Portable 模式,使用标准更新
  221. autoUpdater.quitAndInstall(false, true)
  222. return
  223. }
  224. logger.info('Portable 模式:准备重启以应用更新')
  225. // 查找启动器
  226. const rootDir = getPortableRootDir()
  227. const launcherPath = path.join(rootDir, 'Claude-AI-Installer.exe')
  228. if (fs.existsSync(launcherPath)) {
  229. // 通过启动器重启
  230. const { spawn } = require('child_process')
  231. spawn(launcherPath, [], {
  232. detached: true,
  233. stdio: 'ignore',
  234. cwd: rootDir
  235. }).unref()
  236. logger.info('已启动启动器,准备退出当前应用')
  237. app.quit()
  238. } else {
  239. // 没有启动器,直接退出(用户需要手动重启)
  240. logger.warn('未找到启动器,请手动重启应用')
  241. app.quit()
  242. }
  243. }
  244. /**
  245. * 获取 Portable 更新信息
  246. */
  247. export function getPortableUpdateInfo(): { hasUpdate: boolean; updateDir: string; appDir: string } {
  248. const updateDir = getUpdateDir()
  249. const appDir = getAppDir()
  250. let hasUpdate = false
  251. if (fs.existsSync(updateDir)) {
  252. const files = fs.readdirSync(updateDir)
  253. hasUpdate = files.some(f => f.endsWith('.exe'))
  254. }
  255. return { hasUpdate, updateDir, appDir }
  256. }