ipc-handlers.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. // electron/modules/ipc-handlers.ts - IPC 处理模块
  2. import { dialog, BrowserWindow } from 'electron'
  3. import { registerHandler } from './ipc-registry'
  4. import * as path from 'path'
  5. import * as os from 'os'
  6. import { execa } from 'execa'
  7. import type { SoftwareType, SoftwareTypeWithAll, Platform, InstallOptions, GitMirrorType, NodejsMirrorType } from './types'
  8. import { ERROR_MESSAGES } from './constants'
  9. import { commandExists, checkNetworkConnection, getVscodeCliPath } from './utils'
  10. import { installVscodeExtension } from './vscode-extension'
  11. import { installClaudeCode as installClaudeCodeWithStatus } from './claude-code-installer'
  12. import { getVersions, setGitMirror, getGitMirrorConfig, setNodejsMirror, getNodejsMirrorConfig } from './version-fetcher'
  13. import {
  14. checkInstalled,
  15. checkAllInstalled,
  16. cancelInstall,
  17. aptUpdate,
  18. installNodejs,
  19. installPnpm,
  20. installVscode,
  21. installGit,
  22. installAll,
  23. uninstallSoftware
  24. } from './installer'
  25. import { addInstallHistory, getInstallHistory } from './config'
  26. import logger from './logger'
  27. import {
  28. checkForUpdates,
  29. downloadUpdate,
  30. installUpdate,
  31. getCurrentVersion,
  32. isPortableMode
  33. } from './updater'
  34. /**
  35. * 检测管理员权限
  36. */
  37. async function checkAdminPrivilege(): Promise<boolean> {
  38. const platform = os.platform()
  39. try {
  40. if (platform === 'win32') {
  41. const result = await execa('powershell', [
  42. '-Command',
  43. '([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)'
  44. ])
  45. return result.stdout.trim().toLowerCase() === 'true'
  46. } else {
  47. return process.getuid ? process.getuid() === 0 : false
  48. }
  49. } catch (error) {
  50. logger.warn('检测管理员权限失败', error)
  51. return false
  52. }
  53. }
  54. /**
  55. * 检测 brew 是否安装
  56. */
  57. async function checkBrew(): Promise<boolean> {
  58. return await commandExists('brew')
  59. }
  60. /**
  61. * 安装 Homebrew
  62. */
  63. async function installBrew(): Promise<void> {
  64. const installScript = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
  65. await execa('bash', ['-c', installScript])
  66. }
  67. /**
  68. * 发送状态到渲染进程
  69. */
  70. function sendToRenderer(channel: string, data: unknown): void {
  71. const windows = BrowserWindow.getAllWindows()
  72. windows.forEach((win) => {
  73. win.webContents.send(channel, data)
  74. })
  75. }
  76. /**
  77. * 注册所有 IPC 处理器
  78. */
  79. export function registerHandlers(): void {
  80. // 检测管理员权限
  81. registerHandler('check-admin', async () => {
  82. return await checkAdminPrivilege()
  83. })
  84. // 检测包管理器
  85. // Windows 使用直接下载方式安装,不需要包管理器
  86. // macOS 需要 brew,Linux 使用 apt
  87. registerHandler('check-package-manager', async () => {
  88. const platform = os.platform()
  89. if (platform === 'win32') {
  90. // Windows 不需要包管理器,直接下载安装
  91. return { exists: true, manager: 'none' as const }
  92. } else if (platform === 'darwin') {
  93. return { exists: await checkBrew(), manager: 'brew' }
  94. } else {
  95. return { exists: true, manager: 'apt' }
  96. }
  97. })
  98. // 安装包管理器 (仅 macOS 需要安装 brew)
  99. registerHandler('install-package-manager', async (_event, manager: string) => {
  100. try {
  101. logger.info(`开始安装包管理器: ${manager}`)
  102. if (manager === 'brew') {
  103. await installBrew()
  104. logger.info(`包管理器安装成功: ${manager}`)
  105. return { success: true }
  106. }
  107. // Windows 不需要包管理器,Linux apt 已预装
  108. return { success: true }
  109. } catch (error) {
  110. logger.error(`包管理器安装失败: ${manager}`, error)
  111. return { success: false, error: (error as Error).message }
  112. }
  113. })
  114. // 获取平台信息
  115. registerHandler('get-platform', (): Platform => {
  116. return os.platform() as Platform
  117. })
  118. // 检测网络连接
  119. registerHandler('check-network', async () => {
  120. return await checkNetworkConnection()
  121. })
  122. // 获取软件版本列表
  123. registerHandler('get-versions', async (_event, software: SoftwareType) => {
  124. try {
  125. return await getVersions(software)
  126. } catch (error) {
  127. logger.error(`获取 ${software} 版本列表失败`, error)
  128. throw error
  129. }
  130. })
  131. // 检查更新
  132. registerHandler('check-update', async (_event, software: SoftwareType) => {
  133. try {
  134. const installed = await checkInstalled(software)
  135. if (!installed.installed || !installed.version) {
  136. return { hasUpdate: false }
  137. }
  138. const versions = await getVersions(software)
  139. const latestVersion = versions.versions.find((v) => !v.disabled && !v.separator)
  140. if (!latestVersion) {
  141. return { hasUpdate: false }
  142. }
  143. const hasUpdate = latestVersion.value !== installed.version
  144. return { hasUpdate, latestVersion: latestVersion.value }
  145. } catch (error) {
  146. logger.error(`检查 ${software} 更新失败`, error)
  147. return { hasUpdate: false }
  148. }
  149. })
  150. // 检测软件是否已安装
  151. registerHandler('check-installed', async (_event, software: SoftwareTypeWithAll) => {
  152. if (software === 'all') {
  153. return await checkAllInstalled()
  154. }
  155. return await checkInstalled(software)
  156. })
  157. // 取消安装
  158. registerHandler('cancel-install', () => {
  159. return cancelInstall()
  160. })
  161. // 卸载软件
  162. registerHandler('uninstall', async (_event, software: SoftwareType) => {
  163. try {
  164. return await uninstallSoftware(software)
  165. } catch (error) {
  166. logger.error(`卸载 ${software} 失败`, error)
  167. return false
  168. }
  169. })
  170. // 获取安装历史
  171. registerHandler('get-install-history', (_event, limit?: number) => {
  172. return getInstallHistory(limit)
  173. })
  174. // 获取日志
  175. registerHandler('get-logs', () => {
  176. return logger.getRecentLogs(200)
  177. })
  178. // 写入安装日志(供渲染进程调用)
  179. registerHandler('write-install-log', (_event, message: string, level: 'info' | 'warn' | 'error' = 'info') => {
  180. switch (level) {
  181. case 'warn':
  182. logger.installWarn(message)
  183. break
  184. case 'error':
  185. logger.installError(message)
  186. break
  187. default:
  188. logger.installInfo(message)
  189. }
  190. })
  191. // 获取日志文件路径
  192. registerHandler('get-log-paths', () => {
  193. return {
  194. appLog: logger.getAppLogPath(),
  195. installLog: logger.getInstallLogPath()
  196. }
  197. })
  198. // 设置窗口标题
  199. registerHandler('set-window-title', (_event, title: string) => {
  200. const windows = BrowserWindow.getAllWindows()
  201. windows.forEach((win) => {
  202. win.setTitle(title)
  203. })
  204. })
  205. // 窗口最小化
  206. registerHandler('window-minimize', () => {
  207. const win = BrowserWindow.getFocusedWindow()
  208. win?.minimize()
  209. })
  210. // 窗口最大化/还原
  211. registerHandler('window-maximize', () => {
  212. const win = BrowserWindow.getFocusedWindow()
  213. if (win?.isMaximized()) {
  214. win.unmaximize()
  215. } else {
  216. win?.maximize()
  217. }
  218. return win?.isMaximized() ?? false
  219. })
  220. // 关闭窗口
  221. registerHandler('window-close', () => {
  222. const win = BrowserWindow.getFocusedWindow()
  223. win?.close()
  224. })
  225. // 获取窗口最大化状态
  226. registerHandler('window-is-maximized', () => {
  227. const win = BrowserWindow.getFocusedWindow()
  228. return win?.isMaximized() ?? false
  229. })
  230. // 检测 Claude Code 是否已安装
  231. registerHandler('check-claude-code', async () => {
  232. try {
  233. // 使用 checkInstalled 函数,它会刷新 PATH 环境变量来检测新安装的软件
  234. const result = await checkInstalled('claudeCode')
  235. if (result.installed) {
  236. logger.info(`检测到 Claude Code: ${result.version}`)
  237. } else {
  238. logger.info('未检测到 Claude Code')
  239. }
  240. return result
  241. } catch (error) {
  242. logger.warn('检测 Claude Code 失败', error)
  243. return { installed: false, version: null }
  244. }
  245. })
  246. // 启动 Claude Code (打开 Git Bash 并执行 claude 命令)
  247. registerHandler('launch-claude-code', async () => {
  248. const platform = os.platform()
  249. if (platform !== 'win32') {
  250. throw new Error('此功能仅支持 Windows 系统')
  251. }
  252. // 查找 Git Bash 路径
  253. const possiblePaths = [
  254. 'C:\\Program Files\\Git\\git-bash.exe',
  255. 'C:\\Program Files (x86)\\Git\\git-bash.exe',
  256. path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Git', 'git-bash.exe')
  257. ]
  258. let gitBashPath = ''
  259. for (const p of possiblePaths) {
  260. try {
  261. const fs = await import('fs')
  262. if (fs.existsSync(p)) {
  263. gitBashPath = p
  264. break
  265. }
  266. } catch {
  267. continue
  268. }
  269. }
  270. if (!gitBashPath) {
  271. throw new Error('未找到 Git Bash,请确保已安装 Git')
  272. }
  273. logger.info(`启动 Git Bash: ${gitBashPath}`)
  274. // 获取 pnpm 全局安装路径(claude 命令所在目录)
  275. // Windows 上 pnpm 全局安装的可执行文件在 %LOCALAPPDATA%\pnpm 目录下
  276. const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
  277. const pnpmGlobalBin = path.join(localAppData, 'pnpm')
  278. // 转换为 Git Bash 可识别的路径格式(将反斜杠转为正斜杠,将 C: 转为 /c)
  279. const pnpmGlobalBinUnix = pnpmGlobalBin.replace(/\\/g, '/').replace(/^([A-Za-z]):/, '/$1').toLowerCase()
  280. logger.info(`pnpm 全局路径: ${pnpmGlobalBin} -> ${pnpmGlobalBinUnix}`)
  281. // 获取 npm 全局安装路径(备用)
  282. // Windows 上 npm 全局安装的可执行文件在 %APPDATA%\npm 目录下
  283. const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
  284. const npmGlobalBin = path.join(appData, 'npm')
  285. const npmGlobalBinUnix = npmGlobalBin.replace(/\\/g, '/').replace(/^([A-Za-z]):/, '/$1').toLowerCase()
  286. logger.info(`npm 全局路径: ${npmGlobalBin} -> ${npmGlobalBinUnix}`)
  287. // 获取 Node.js 路径
  288. // 常见的 Node.js 安装路径
  289. const fs = await import('fs')
  290. const possibleNodePaths = [
  291. 'C:\\Program Files\\nodejs',
  292. 'C:\\Program Files (x86)\\nodejs',
  293. path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'nodejs'),
  294. // fnm 安装的 Node.js
  295. path.join(os.homedir(), 'AppData', 'Local', 'fnm_multishells'),
  296. // nvm-windows 安装的 Node.js
  297. path.join(os.homedir(), 'AppData', 'Roaming', 'nvm')
  298. ]
  299. let nodePath = ''
  300. for (const p of possibleNodePaths) {
  301. if (fs.existsSync(p)) {
  302. // 检查是否有 node.exe
  303. const nodeExe = path.join(p, 'node.exe')
  304. if (fs.existsSync(nodeExe)) {
  305. nodePath = p
  306. break
  307. }
  308. }
  309. }
  310. // 如果没找到,尝试从 PATH 环境变量中获取
  311. if (!nodePath) {
  312. const pathEnv = process.env.PATH || ''
  313. const pathDirs = pathEnv.split(';')
  314. for (const dir of pathDirs) {
  315. const nodeExe = path.join(dir, 'node.exe')
  316. if (fs.existsSync(nodeExe)) {
  317. nodePath = dir
  318. break
  319. }
  320. }
  321. }
  322. // 构建额外的 PATH(pnpm 优先,因为 claude 是通过 pnpm 安装的)
  323. const pathParts: string[] = [pnpmGlobalBinUnix, npmGlobalBinUnix]
  324. if (nodePath) {
  325. const nodePathUnix = nodePath.replace(/\\/g, '/').replace(/^([A-Za-z]):/, '/$1').toLowerCase()
  326. pathParts.unshift(nodePathUnix)
  327. logger.info(`Node.js 路径: ${nodePath} -> ${nodePathUnix}`)
  328. } else {
  329. logger.warn('未找到 Node.js 路径,可能会导致 claude 命令无法运行')
  330. }
  331. const extraPaths = pathParts.join(':')
  332. logger.info(`额外 PATH: ${extraPaths}`)
  333. // 使用 execa 启动 Git Bash 并执行 claude 命令
  334. // 先将 Node.js、pnpm 和 npm 全局路径添加到 PATH,然后执行 claude 命令
  335. const bashCommand = `export PATH="${extraPaths}:$PATH"; claude; exec bash`
  336. execa(gitBashPath, ['-c', bashCommand], {
  337. detached: true,
  338. stdio: 'ignore'
  339. }).unref()
  340. return { success: true }
  341. })
  342. // 安装 Claude Code(复用 installClaudeCodeWithStatus 函数)
  343. registerHandler('install-claude-code', async () => {
  344. return await installClaudeCodeWithStatus()
  345. })
  346. // 检查 VS Code 插件是否已安装
  347. registerHandler('check-vscode-extension', async (_event, extensionId: string) => {
  348. try {
  349. const codePath = await getVscodeCliPath()
  350. logger.info(`使用 VS Code CLI: ${codePath}`)
  351. // 使用 --show-versions 参数获取插件版本信息
  352. const result = await execa(codePath, ['--list-extensions', '--show-versions'], {
  353. encoding: 'utf8',
  354. stdout: 'pipe',
  355. stderr: 'pipe'
  356. })
  357. // 输出格式为 "extensionId@version",例如 "[email protected]"
  358. const extensions = result.stdout.split('\n').map(ext => ext.trim())
  359. const targetExtLower = extensionId.toLowerCase()
  360. let installed = false
  361. let version: string | undefined
  362. for (const ext of extensions) {
  363. const [extId, extVersion] = ext.split('@')
  364. if (extId && extId.toLowerCase() === targetExtLower) {
  365. installed = true
  366. version = extVersion
  367. break
  368. }
  369. }
  370. logger.info(`检查 VS Code 插件 ${extensionId}: ${installed ? `已安装 v${version}` : '未安装'}`)
  371. return { installed, version }
  372. } catch (error) {
  373. logger.warn(`检查 VS Code 插件失败: ${extensionId}`, error)
  374. return { installed: false }
  375. }
  376. })
  377. // 安装 VS Code 插件(复用 installVscodeExtension 函数)
  378. registerHandler('install-vscode-extension', async (_event, extensionId: string) => {
  379. return await installVscodeExtension(extensionId)
  380. })
  381. // ==================== 文件夹选择 ====================
  382. // 选择文件夹
  383. registerHandler('select-directory', async (_event, defaultPath?: string) => {
  384. const win = BrowserWindow.getFocusedWindow()
  385. const result = await dialog.showOpenDialog(win!, {
  386. properties: ['openDirectory'],
  387. defaultPath: defaultPath || os.homedir()
  388. })
  389. if (result.canceled || result.filePaths.length === 0) {
  390. return { canceled: true, path: null }
  391. }
  392. return { canceled: false, path: result.filePaths[0] }
  393. })
  394. // ==================== Git 镜像配置 ====================
  395. // 设置 Git 镜像
  396. registerHandler('set-git-mirror', (_event, mirror: GitMirrorType) => {
  397. logger.info(`设置 Git 镜像: ${mirror}`)
  398. setGitMirror(mirror)
  399. })
  400. // 获取 Git 镜像配置
  401. registerHandler('get-git-mirror-config', () => {
  402. return getGitMirrorConfig()
  403. })
  404. // ==================== Node.js 镜像配置 ====================
  405. // 设置 Node.js 镜像
  406. registerHandler('set-nodejs-mirror', (_event, mirror: NodejsMirrorType) => {
  407. logger.info(`设置 Node.js 镜像: ${mirror}`)
  408. setNodejsMirror(mirror)
  409. })
  410. // 获取 Node.js 镜像配置
  411. registerHandler('get-nodejs-mirror-config', () => {
  412. return getNodejsMirrorConfig()
  413. })
  414. // ==================== 自动更新相关 ====================
  415. // 检查应用更新
  416. registerHandler('updater:check', async () => {
  417. return await checkForUpdates()
  418. })
  419. // 下载更新
  420. registerHandler('updater:download', async () => {
  421. return await downloadUpdate()
  422. })
  423. // 安装更新并重启
  424. registerHandler('updater:install', () => {
  425. installUpdate()
  426. })
  427. // 获取当前版本
  428. registerHandler('updater:version', () => {
  429. return getCurrentVersion()
  430. })
  431. // 检测是否为 Portable 模式
  432. registerHandler('updater:is-portable', () => {
  433. return isPortableMode()
  434. })
  435. // ==================== 软件安装相关 ====================
  436. // 统一安装入口
  437. registerHandler('install', async (_event, software: SoftwareTypeWithAll, options: InstallOptions = {}) => {
  438. const { version, nodejsPath, vscodePath, gitPath } = options
  439. const startTime = Date.now()
  440. // 状态回调
  441. const onStatus = (sw: SoftwareTypeWithAll, message: string, progress: number, skipLog?: boolean): void => {
  442. sendToRenderer('install-status', { software: sw, message, progress, skipLog })
  443. if (!skipLog) {
  444. logger.installInfo(`[${sw}] ${message} (${progress}%)`)
  445. }
  446. }
  447. try {
  448. logger.installInfo(`开始安装: ${software}`, options)
  449. // Linux 下先 apt update
  450. if (os.platform() === 'linux' && ['nodejs', 'git', 'all'].includes(software)) {
  451. await aptUpdate(onStatus, software)
  452. }
  453. switch (software) {
  454. case 'nodejs':
  455. await installNodejs(version, false, onStatus, nodejsPath)
  456. sendToRenderer('install-complete', {
  457. software: 'nodejs',
  458. message: '✅ Node.js 安装完成!',
  459. i18nKey: 'log.nodejsComplete'
  460. })
  461. break
  462. case 'vscode':
  463. await installVscode(version, onStatus, vscodePath)
  464. sendToRenderer('install-complete', {
  465. software: 'vscode',
  466. message: '✅ VS Code 安装完成!',
  467. i18nKey: 'log.vscodeComplete'
  468. })
  469. break
  470. case 'git':
  471. await installGit(version, onStatus, gitPath)
  472. sendToRenderer('install-complete', {
  473. software: 'git',
  474. message: '✅ Git 安装完成!',
  475. i18nKey: 'log.gitComplete'
  476. })
  477. break
  478. case 'pnpm':
  479. await installPnpm(onStatus)
  480. sendToRenderer('install-complete', {
  481. software: 'pnpm',
  482. message: '✅ pnpm 安装完成!',
  483. i18nKey: 'log.pnpmComplete'
  484. })
  485. break
  486. case 'all': {
  487. const installed = await installAll(options, onStatus)
  488. sendToRenderer('install-complete', {
  489. software: 'all',
  490. message: `✅ ${installed.join('、')} 安装完成!`,
  491. i18nKey: 'log.allComplete',
  492. i18nParams: { software: installed.join(', ') }
  493. })
  494. break
  495. }
  496. default:
  497. throw new Error(`${ERROR_MESSAGES.UNKNOWN_SOFTWARE}: ${software}`)
  498. }
  499. // 记录安装历史
  500. const duration = Date.now() - startTime
  501. addInstallHistory({
  502. software,
  503. version: version || 'latest',
  504. options,
  505. success: true,
  506. duration
  507. })
  508. } catch (error) {
  509. const errorMessage = (error as Error).message
  510. const isCancelled = errorMessage.includes('取消')
  511. if (!isCancelled) {
  512. // 注意:dialog.showErrorBox 在主进程中无法使用 i18n,保留中文作为后备
  513. dialog.showErrorBox('Installation Failed / 安装失败', errorMessage || 'Unknown error / 未知错误')
  514. }
  515. sendToRenderer('install-error', {
  516. software,
  517. message: isCancelled ? '⚠️ 安装已取消' : `❌ 安装失败:${errorMessage}`,
  518. i18nKey: isCancelled ? 'log.installCancelled' : 'log.installError',
  519. i18nParams: isCancelled ? undefined : { error: errorMessage }
  520. })
  521. // 记录失败历史
  522. addInstallHistory({
  523. software,
  524. version: version || 'latest',
  525. options,
  526. success: false,
  527. error: errorMessage,
  528. cancelled: isCancelled
  529. })
  530. }
  531. })
  532. }
  533. /**
  534. * 移除所有 IPC 处理器
  535. * 使用自动注册模块,无需手动维护 handler 列表
  536. */
  537. export { removeAllHandlers as removeHandlers } from './ipc-registry'