import { app, BrowserWindow, shell, ipcMain, Menu, dialog, nativeTheme, protocol, session } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' import os, { homedir } from 'node:os' import log from 'electron-log' import { update, registerUpdateIpcHandlers } from './update' import { checkToolInstalled, installDependencies, killProcessOnPort, startBackend } from './init' import { WebViewManager } from './webview' import { FileReader } from './fileReader' import { ChildProcessWithoutNullStreams } from 'node:child_process' import fs, { existsSync, readFileSync } from 'node:fs' import fsp from 'fs/promises' import { addMcp, removeMcp, updateMcp, readMcpConfig } from './utils/mcpConfig' import { getEnvPath, updateEnvBlock, removeEnvKey, getEmailFolderPath } from './utils/envUtil' import { copyBrowserData } from './copy' import { findAvailablePort } from './init' import kill from 'tree-kill'; import { zipFolder } from './utils/log' import axios from 'axios'; import FormData from 'form-data'; const userData = app.getPath('userData'); const versionFile = path.join(userData, 'version.txt'); // ==================== constants ==================== const __dirname = path.dirname(fileURLToPath(import.meta.url)); const MAIN_DIST = path.join(__dirname, '../..'); const RENDERER_DIST = path.join(MAIN_DIST, 'dist'); const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; const VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(MAIN_DIST, 'public') : RENDERER_DIST; // ==================== global variables ==================== let win: BrowserWindow | null = null; let webViewManager: WebViewManager | null = null; let fileReader: FileReader | null = null; let python_process: ChildProcessWithoutNullStreams | null = null; let backendPort: number = 5001; let browser_port = 9222; // ==================== path config ==================== const preload = path.join(__dirname, '../preload/index.mjs'); const indexHtml = path.join(RENDERER_DIST, 'index.html'); const logPath = log.transports.file.getFile().path; // Set remote debugging port findAvailablePort(browser_port).then(port => { browser_port = port; app.commandLine.appendSwitch('remote-debugging-port', port + ''); }); // Read last run version and install dependencies on update async function checkAndInstallDepsOnUpdate(): Promise { const currentVersion = app.getVersion(); return new Promise(async (resolve, reject) => { try { log.info(' start check version', { currentVersion }); // Check if version file exists const versionExists = fs.existsSync(versionFile); let savedVersion = ''; if (versionExists) { savedVersion = fs.readFileSync(versionFile, 'utf-8').trim(); log.info(' read saved version', { savedVersion }); } else { log.info(' version file not exist, will create new file'); } // If version file does not exist or version does not match, reinstall dependencies if (!versionExists || savedVersion !== currentVersion) { log.info(' version changed, prepare to reinstall uv dependencies...', { currentVersion, savedVersion: versionExists ? savedVersion : 'none', reason: !versionExists ? 'version file not exist' : 'version not match' }); // Notify frontend to update if (win && !win.isDestroyed()) { win.webContents.send('update-notification', { type: 'version-update', currentVersion, previousVersion: versionExists ? savedVersion : 'none', reason: !versionExists ? 'version file not exist' : 'version not match' }); } // Update version file fs.writeFileSync(versionFile, currentVersion); log.info(' version file updated', { currentVersion }); // Install dependencies const result = await installDependencies(); if (!result) { log.error(' install dependencies failed'); resolve(false); return } resolve(true); log.info(' install dependencies complete'); return } else { log.info(' version not changed, skip install dependencies', { currentVersion }); resolve(true); return } } catch (error) { log.error(' check version and install dependencies error:', error); resolve(false); return } }) } // ==================== app config ==================== process.env.APP_ROOT = MAIN_DIST; process.env.VITE_PUBLIC = VITE_PUBLIC; // Disable system theme nativeTheme.themeSource = 'light'; // Set log level log.transports.console.level = 'info'; log.transports.file.level = 'info'; // Disable GPU Acceleration for Windows 7 if (os.release().startsWith('6.1')) app.disableHardwareAcceleration() // Set application name for Windows 10+ notifications if (process.platform === 'win32') app.setAppUserModelId(app.getName()) if (!app.requestSingleInstanceLock()) { app.quit() process.exit(0) } // ==================== protocol config ==================== const setupProtocolHandlers = () => { if (process.env.NODE_ENV === 'development') { const isDefault = app.isDefaultProtocolClient('eigent', process.execPath, [path.resolve(process.argv[1])]); if (!isDefault) { app.setAsDefaultProtocolClient('eigent', process.execPath, [path.resolve(process.argv[1])]); } } else { app.setAsDefaultProtocolClient('eigent'); } }; // ==================== protocol url handle ==================== function handleProtocolUrl(url: string) { log.info('enter handleProtocolUrl', url); const urlObj = new URL(url); const code = urlObj.searchParams.get('code'); const share_token = urlObj.searchParams.get('share_token'); log.info('urlObj', urlObj); log.info('code', code); log.info('share_token', share_token); if (win && !win.isDestroyed()) { log.info('urlObj.pathname', urlObj.pathname); if (urlObj.pathname === '/oauth') { log.info('oauth'); const provider = urlObj.searchParams.get('provider'); const code = urlObj.searchParams.get('code'); log.info("protocol oauth", provider, code); win.webContents.send('oauth-authorized', { provider, code }); return; } if (code) { log.error('protocol code:', code); win.webContents.send('auth-code-received', code); } if (share_token) { win.webContents.send('auth-share-token-received', share_token); } } else { log.error('window not available'); } } // ==================== single instance lock ==================== const setupSingleInstanceLock = () => { const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { log.info("no-lock"); app.quit(); } else { app.on('second-instance', (event, argv) => { log.info("second-instance", argv); const url = argv.find(arg => arg.startsWith('eigent://')); if (url) handleProtocolUrl(url); if (win) win.show(); }); app.on('open-url', (event, url) => { log.info("open-url"); event.preventDefault(); handleProtocolUrl(url); }); } }; // ==================== initialize config ==================== const initializeApp = () => { setupProtocolHandlers(); setupSingleInstanceLock(); }; /** * Registers all IPC handlers once when the app starts * This prevents "Attempted to register a second handler" errors * when windows are reopened */ // Get backup log path const getBackupLogPath = () => { const userDataPath = app.getPath('userData') return path.join(userDataPath, 'logs', 'main.log') } // Constants define const BROWSER_PATHS = { win32: { chrome: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', edge: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', firefox: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', qq: 'C:\\Program Files\\Tencent\\QQBrowser\\QQBrowser.exe', '360': path.join(homedir(), 'AppData\\Local\\360Chrome\\Chrome\\Application\\360chrome.exe'), arc: path.join(homedir(), 'AppData\\Local\\Arc\\User Data\\Arc.exe'), dia: path.join(homedir(), 'AppData\\Local\\Dia\\Application\\dia.exe'), fellou: path.join(homedir(), 'AppData\\Local\\Fellou\\Application\\fellou.exe'), }, darwin: { chrome: '/Applications/Google Chrome.app', edge: '/Applications/Microsoft Edge.app', firefox: '/Applications/Firefox.app', safari: '/Applications/Safari.app', arc: '/Applications/Arc.app', dia: '/Applications/Dia.app', fellou: '/Applications/Fellou.app', }, } as const; // Tool function const getSystemLanguage = async () => { const locale = app.getLocale(); return locale === 'zh-CN' ? 'zh-cn' : 'en'; }; const checkManagerInstance = (manager: any, name: string) => { if (!manager) { throw new Error(`${name} not initialized`); } return manager; }; const handleDependencyInstallation = async () => { try { log.info(' start install dependencies...'); const isSuccess = await installDependencies(); if (!isSuccess) { log.error(' install dependencies failed'); return { success: false, error: 'install dependencies failed' }; } log.info(' install dependencies success, check tool installed status...'); const isToolInstalled = await checkToolInstalled(); log.info('isToolInstalled && !python_process', isToolInstalled && !python_process); if (isToolInstalled && !python_process) { log.info(' tool installed, start backend service...'); python_process = await startBackend((port) => { backendPort = port; log.info(' backend service start success', { port }); }); // Notify frontend to install success if (win && !win.isDestroyed()) { win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); } python_process?.on('exit', (code, signal) => { log.info(' python process exit', { code, signal }); }); } else if (!isToolInstalled) { log.warn(' tool not installed, skip backend start'); } else { log.info(' backend process already exist, skip start'); } log.info(' install dependencies complete'); return { success: true }; } catch (error: any) { log.error(' install dependencies error:', error); if (win && !win.isDestroyed()) { win.webContents.send('install-dependencies-complete', { success: false, code: 2 }); } return { success: false, error: error.message }; } }; function registerIpcHandlers() { // ==================== basic info handler ==================== ipcMain.handle('get-browser-port', () => browser_port); ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-backend-port', () => backendPort); ipcMain.handle('get-system-language', getSystemLanguage); ipcMain.handle('is-fullscreen', () => win?.isFullScreen() || false); ipcMain.handle('get-home-dir', () => { const platform = process.platform; return platform === 'win32' ? process.env.USERPROFILE : process.env.HOME; }); // ==================== command execution handler ==================== ipcMain.handle('get-email-folder-path', async (event, email: string) => { return getEmailFolderPath(email); }); ipcMain.handle('execute-command', async (event, command: string, email: string) => { log.info("execute-command", command); const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); try { const { spawn } = await import('child_process'); // Add --host parameter const commandWithHost = `${command} --debug --host dev.eigent.ai/api/oauth/notion/callback?code=1`; // const commandWithHost = `${command}`; log.info(' start execute command:', commandWithHost); // Parse command and arguments const [cmd, ...args] = commandWithHost.split(' '); log.info('start execute command:', commandWithHost.split(' ')); console.log(cmd, args) return new Promise((resolve) => { const child = spawn(cmd, args, { cwd: process.cwd(), env: { ...process.env, MCP_REMOTE_CONFIG_DIR }, stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; // Realtime listen standard output child.stdout.on('data', (data) => { const output = data.toString(); stdout += output; log.info('Real-time output:', output.trim()); }); // Realtime listen error output child.stderr.on('data', (data) => { const output = data.toString(); stderr += output; if (output.includes('OAuth callback server running at')) { const url = output.split('OAuth callback server running at')[1].trim(); log.info(' detect OAuth callback URL:', url); // Notify frontend to callback URL if (win && !win.isDestroyed()) { const match = url.match(/^https?:\/\/[^:\n]+:\d+/); const cleanedUrl = match ? match[0] : null; log.info('cleanedUrl', cleanedUrl); win.webContents.send('oauth-callback-url', { url: cleanedUrl, provider: 'notion' // TODO: can be set dynamically according to actual situation }); } } if (output.includes('Press Ctrl+C to exit')) { child.kill(); } log.info(' real-time error output:', output.trim()); }); // Listen process exit child.on('close', (code) => { log.info(` command execute complete, exit code: ${code}`); resolve({ success: code === null, stdout, stderr }); }); // Listen process error child.on('error', (error) => { log.error(' command execute error:', error); resolve({ success: false, error: error.message }); }); }); } catch (error: any) { log.error(' command execute failed:', error); return { success: false, error: error.message }; } }); // ==================== log export handler ==================== ipcMain.handle('export-log', async () => { try { let targetLogPath = logPath; if (!fs.existsSync(targetLogPath)) { const backupPath = getBackupLogPath(); if (fs.existsSync(backupPath)) { targetLogPath = backupPath; } else { return { success: false, error: 'no log file' }; } } await fsp.access(targetLogPath, fs.constants.R_OK); const stats = await fsp.stat(targetLogPath); if (stats.size === 0) { return { success: true, data: 'log file is empty' }; } const logContent = await fsp.readFile(targetLogPath, 'utf-8'); // Get app version and system version const appVersion = app.getVersion(); const platform = process.platform; const arch = process.arch; const systemVersion = `${platform}-${arch}`; const defaultFileName = `eigent-${appVersion}-${systemVersion}-${Date.now()}.log`; // Show save dialog const { canceled, filePath } = await dialog.showSaveDialog({ title: 'save log file', defaultPath: defaultFileName, filters: [{ name: 'log file', extensions: ['log', 'txt'] }] }); if (canceled || !filePath) { return { success: false, error: '' }; } await fsp.writeFile(filePath, logContent, 'utf-8'); return { success: true, savedPath: filePath }; } catch (error: any) { return { success: false, error: error.message }; } }); ipcMain.handle('upload-log', async (event, email: string, taskId: string, baseUrl: string, token: string) => { let zipPath: string | null = null; try { // Validate required parameters if (!email || !taskId || !baseUrl || !token) { return { success: false, error: 'Missing required parameters' }; } // Sanitize taskId to prevent path traversal attacks const sanitizedTaskId = taskId.replace(/[^a-zA-Z0-9_-]/g, ''); if (!sanitizedTaskId) { return { success: false, error: 'Invalid task ID' }; } const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); const logFolderName = `task_${sanitizedTaskId}`; const logFolderPath = path.join(MCP_REMOTE_CONFIG_DIR, logFolderName); // Check if log folder exists if (!fs.existsSync(logFolderPath)) { return { success: false, error: 'Log folder not found' }; } zipPath = path.join(MCP_REMOTE_CONFIG_DIR, `${logFolderName}.zip`); await zipFolder(logFolderPath, zipPath); // Create form data with file stream const formData = new FormData(); const fileStream = fs.createReadStream(zipPath); formData.append('file', fileStream); formData.append('task_id', sanitizedTaskId); // Upload with timeout const response = await axios.post(baseUrl + '/api/chat/logs', formData, { headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` }, timeout: 60000, // 60 second timeout maxContentLength: Infinity, maxBodyLength: Infinity }); fileStream.destroy(); if (response.status === 200) { return { success: true, data: response.data }; } else { return { success: false, error: response.data }; } } catch (error: any) { log.error('Failed to upload log:', error); return { success: false, error: error.message || 'Upload failed' }; } finally { // Clean up zip file if (zipPath && fs.existsSync(zipPath)) { try { fs.unlinkSync(zipPath); } catch (cleanupError) { log.error('Failed to clean up zip file:', cleanupError); } } } }); // ==================== MCP manage handler ==================== ipcMain.handle('mcp-install', async (event, name, mcp) => { addMcp(name, mcp); return { success: true }; }); ipcMain.handle('mcp-remove', async (event, name) => { removeMcp(name); return { success: true }; }); ipcMain.handle('mcp-update', async (event, name, mcp) => { updateMcp(name, mcp); return { success: true }; }); ipcMain.handle('mcp-list', async () => { return readMcpConfig(); }); // ==================== browser related handler ==================== // TODO: next version implement ipcMain.handle('check-install-browser', async () => { try { const platform = process.platform; const results: Record = {}; const paths = BROWSER_PATHS[platform as keyof typeof BROWSER_PATHS]; if (!paths) { log.warn(`not support current platform: ${platform}`); return {}; } for (const [browser, execPath] of Object.entries(paths)) { results[browser] = existsSync(execPath); } return results; } catch (error: any) { log.error('Failed to check browser installation:', error); return {}; } }); ipcMain.handle('start-browser-import', async (event, args) => { const isWin = process.platform === 'win32'; const localAppData = process.env.LOCALAPPDATA || ''; const appData = process.env.APPDATA || ''; const home = os.homedir(); const candidates: Record = { chrome: isWin ? `${localAppData}\\Google\\Chrome\\User Data\\Default` : `${home}/Library/Application Support/Google/Chrome/Default`, edge: isWin ? `${localAppData}\\Microsoft\\Edge\\User Data\\Default` : `${home}/Library/Application Support/Microsoft Edge/Default`, firefox: isWin ? `${appData}\\Mozilla\\Firefox\\Profiles` : `${home}/Library/Application Support/Firefox/Profiles`, qq: `${localAppData}\\Tencent\\QQBrowser\\User Data\\Default`, '360': `${localAppData}\\360Chrome\\Chrome\\User Data\\Default`, arc: isWin ? `${localAppData}\\Arc\\User Data\\Default` : `${home}/Library/Application Support/Arc/Default`, dia: `${localAppData}\\Dia\\User Data\\Default`, fellou: `${localAppData}\\Fellou\\User Data\\Default`, safari: `${home}/Library/Safari`, }; // Filter unchecked browser Object.keys(candidates).forEach((key) => { const browser = args.find((item: any) => item.browserId === key); if (!browser || !browser.checked) { delete candidates[key]; } }); const result: Record = {}; for (const [name, p] of Object.entries(candidates)) { result[name] = fs.existsSync(p) ? p : null; } const electronUserDataPath = app.getPath('userData'); for (const [browserName, browserPath] of Object.entries(result)) { if (!browserPath) continue; await copyBrowserData(browserName, browserPath, electronUserDataPath); } return { success: true }; }); // ==================== window control handler ==================== ipcMain.on('window-close', () => win?.close()); ipcMain.on('window-minimize', () => win?.minimize()); ipcMain.on('window-toggle-maximize', () => { if (win?.isMaximized()) { win?.unmaximize(); } else { win?.maximize(); } }); // ==================== file operation handler ==================== ipcMain.handle('select-file', async (event, options = {}) => { const result = await dialog.showOpenDialog(win!, { properties: ['openFile', 'multiSelections'], ...options }); if (!result.canceled && result.filePaths.length > 0) { const files = result.filePaths.map(filePath => ({ filePath, fileName: filePath.split(/[/\\]/).pop() || '' })); return { success: true, files, fileCount: files.length }; } return { success: false, canceled: result.canceled }; }); ipcMain.handle("reveal-in-folder", async (event, filePath: string) => { try { shell.showItemInFolder(filePath); } catch (e) { log.error("reveal in folder failed", e); } }); // ==================== read file handler ==================== ipcMain.handle('read-file', async (event, filePath: string) => { try { log.info('Reading file:', filePath); // Check if file exists if (!fs.existsSync(filePath)) { log.error('File does not exist:', filePath); return { success: false, error: 'File does not exist' }; } // Read file content const fileContent = await fsp.readFile(filePath); log.info('File read successfully:', filePath); return { success: true, data: fileContent, size: fileContent.length }; } catch (error: any) { log.error('Failed to read file:', filePath, error); return { success: false, error: error.message || 'Failed to read file' }; } }); // ==================== delete folder handler ==================== ipcMain.handle('delete-folder', async (event, email: string) => { const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); try { log.info('Deleting folder:', MCP_REMOTE_CONFIG_DIR); // Check if folder exists if (!fs.existsSync(MCP_REMOTE_CONFIG_DIR)) { log.error('Folder does not exist:', MCP_REMOTE_CONFIG_DIR); return { success: false, error: 'Folder does not exist' }; } // Check if it's actually a directory const stats = await fsp.stat(MCP_REMOTE_CONFIG_DIR); if (!stats.isDirectory()) { log.error('Path is not a directory:', MCP_REMOTE_CONFIG_DIR); return { success: false, error: 'Path is not a directory' }; } // Delete folder recursively await fsp.rm(MCP_REMOTE_CONFIG_DIR, { recursive: true, force: true }); log.info('Folder deleted successfully:', MCP_REMOTE_CONFIG_DIR); return { success: true, message: 'Folder deleted successfully' }; } catch (error: any) { log.error('Failed to delete folder:', MCP_REMOTE_CONFIG_DIR, error); return { success: false, error: error.message || 'Failed to delete folder' }; } }); // ==================== get MCP config path handler ==================== ipcMain.handle('get-mcp-config-path', async (event, email: string) => { try { const { MCP_REMOTE_CONFIG_DIR, tempEmail } = getEmailFolderPath(email); log.info('Getting MCP config path for email:', email); log.info('MCP config path:', MCP_REMOTE_CONFIG_DIR); return { success: MCP_REMOTE_CONFIG_DIR, path: MCP_REMOTE_CONFIG_DIR, tempEmail: tempEmail, }; } catch (error: any) { log.error('Failed to get MCP config path:', error); return { success: false, error: error.message || 'Failed to get MCP config path' }; } }); // ==================== env handler ==================== ipcMain.handle('get-env-path', async (_event, email) => { return getEnvPath(email); }); ipcMain.handle('get-env-has-key', async (_event, email, key) => { const ENV_PATH = getEnvPath(email); let content = ''; try { content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : ''; } catch (error) { log.error("env-remove error:", error); } let lines = content.split(/\r?\n/); return { success: lines.some(line => line.startsWith(key + '=')) }; }); ipcMain.handle('env-write', async (_event, email, { key, value }) => { const ENV_PATH = getEnvPath(email); let content = ''; try { content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : ''; } catch (error) { log.error("env-write error:", error); } let lines = content.split(/\r?\n/); lines = updateEnvBlock(lines, { [key]: value }); fs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8'); return { success: true }; }); ipcMain.handle('env-remove', async (_event, email, key) => { log.info("env-remove", key); const ENV_PATH = getEnvPath(email); let content = ''; try { content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : ''; } catch (error) { log.error("env-remove error:", error); } let lines = content.split(/\r?\n/); lines = removeEnvKey(lines, key); fs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8'); log.info("env-remove success", ENV_PATH); return { success: true }; }); // ==================== new window handler ==================== ipcMain.handle('open-win', (_, arg) => { const childWindow = new BrowserWindow({ webPreferences: { preload, nodeIntegration: true, contextIsolation: false, }, }); if (VITE_DEV_SERVER_URL) { childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`); } else { childWindow.loadFile(indexHtml, { hash: arg }); } }); // ==================== FileReader handler ==================== ipcMain.handle('open-file', async (_, type: string, filePath: string, isShowSourceCode: boolean) => { const manager = checkManagerInstance(fileReader, 'FileReader'); return manager.openFile(type, filePath, isShowSourceCode); }); ipcMain.handle('download-file', async (_, url: string) => { try { const https = await import('https'); const http = await import('http'); // extract file name from URL const urlObj = new URL(url); const fileName = urlObj.pathname.split('/').pop() || 'download'; // get download directory const downloadPath = path.join(app.getPath('downloads'), fileName); // create write stream const fileStream = fs.createWriteStream(downloadPath); // choose module according to protocol const client = url.startsWith('https:') ? https : http; return new Promise((resolve, reject) => { const request = client.get(url, (response) => { if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}`)); return; } response.pipe(fileStream); fileStream.on('finish', () => { fileStream.close(); shell.showItemInFolder(downloadPath); resolve({ success: true, path: downloadPath }); }); fileStream.on('error', (err) => { reject(err); }); }); request.on('error', (err) => { reject(err); }); }); } catch (error: any) { log.error('Download file error:', error); return { success: false, error: error.message }; } }); ipcMain.handle('get-file-list', async (_, email: string, taskId: string) => { const manager = checkManagerInstance(fileReader, 'FileReader'); return manager.getFileList(email, taskId); }); // ==================== WebView handler ==================== const webviewHandlers = [ { name: 'capture-webview', method: 'captureWebview' }, { name: 'create-webview', method: 'createWebview' }, { name: 'hide-webview', method: 'hideWebview' }, { name: 'show-webview', method: 'showWebview' }, { name: 'change-view-size', method: 'changeViewSize' }, { name: 'hide-all-webview', method: 'hideAllWebview' }, { name: 'get-active-webview', method: 'getActiveWebview' }, { name: 'set-size', method: 'setSize' }, { name: 'get-show-webview', method: 'getShowWebview' }, { name: 'webview-destroy', method: 'destroyWebview' }, ]; webviewHandlers.forEach(({ name, method }) => { ipcMain.handle(name, async (_, ...args) => { const manager = checkManagerInstance(webViewManager, 'WebViewManager'); return manager[method as keyof typeof manager](...args); }); }); // ==================== dependency install handler ==================== ipcMain.handle('install-dependencies', handleDependencyInstallation); ipcMain.handle('frontend-ready', handleDependencyInstallation); ipcMain.handle('check-tool-installed', async () => { try { const isInstalled = await checkToolInstalled(); return { success: true, isInstalled }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // ==================== register update related handler ==================== registerUpdateIpcHandlers(); } // ==================== window create ==================== async function createWindow() { const isMac = process.platform === 'darwin'; win = new BrowserWindow({ title: 'Eigent', width: 1200, height: 800, minWidth: 1200, minHeight: 800, frame: false, transparent: true, vibrancy: 'sidebar', visualEffectState: 'active', backgroundColor: '#00000000', titleBarStyle: isMac ? 'hidden' : undefined, trafficLightPosition: isMac ? { x: 10, y: 10 } : undefined, icon: path.join(VITE_PUBLIC, 'favicon.ico'), roundedCorners: true, webPreferences: { webSecurity: false, preload, nodeIntegration: true, contextIsolation: true, webviewTag: true, spellcheck: false, }, }); // ==================== initialize manager ==================== fileReader = new FileReader(win); webViewManager = new WebViewManager(win); // create multiple webviews for (let i = 1; i <= 8; i++) { webViewManager.createWebview(i === 1 ? undefined : i.toString()); } // ==================== load content ==================== if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL); win.webContents.openDevTools(); } else { win.loadFile(indexHtml); } // ==================== set event listeners ==================== setupWindowEventListeners(); setupDevToolsShortcuts(); setupExternalLinkHandling(); // ==================== auto update ==================== update(win); // ==================== check tool installed ==================== let res = await checkAndInstallDepsOnUpdate(); if (!res) { log.info('checkAndInstallDepsOnUpdate,install dependencies failed'); win.webContents.send('install-dependencies-complete', { success: false, code: 2 }); return; } await checkAndStartBackend(); } // ==================== window event listeners ==================== const setupWindowEventListeners = () => { if (!win) return; // close default menu Menu.setApplicationMenu(null); }; // ==================== devtools shortcuts ==================== const setupDevToolsShortcuts = () => { if (!win) return; const toggleDevTools = () => win?.webContents.toggleDevTools(); win.webContents.on('before-input-event', (event, input) => { // F12 key if (input.key === 'F12' && input.type === 'keyDown') { toggleDevTools(); } // Ctrl+Shift+I (Windows/Linux) or Cmd+Shift+I (Mac) if (input.control && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') { toggleDevTools(); } // Mac Cmd+Shift+I if (input.meta && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') { toggleDevTools(); } }); }; // ==================== external link handle ==================== const setupExternalLinkHandling = () => { if (!win) return; // handle new window open win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('https:') || url.startsWith('http:')) { shell.openExternal(url); } return { action: 'deny' }; }); // handle navigation win.webContents.on('will-navigate', (event, url) => { event.preventDefault(); shell.openExternal(url); }); }; // ==================== check and start backend ==================== const checkAndStartBackend = async () => { log.info('Checking and starting backend service...'); const isToolInstalled = await checkToolInstalled(); if (isToolInstalled) { log.info('Tool installed, starting backend service...'); // Notify frontend installation success if (win && !win.isDestroyed()) { win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); } python_process = await startBackend((port) => { backendPort = port; log.info('Backend service started successfully', { port }); }); python_process?.on('exit', (code, signal) => { log.info('Python process exited', { code, signal }); }); } else { log.warn('Tool not installed, cannot start backend service'); } }; // ==================== process cleanup ==================== const cleanupPythonProcess = async () => { try { // First attempt: Try to kill using PID if (python_process?.pid) { const pid = python_process.pid; log.info('Cleaning up Python process', { pid }); await new Promise((resolve) => { kill(pid, 'SIGINT', (err) => { if (err) { log.error('Failed to clean up process tree:', err); } else { log.info('Successfully cleaned up Python process tree'); } resolve(); }); }); } // Second attempt: Use port-based cleanup as fallback const portFile = path.join(userData, 'port.txt'); if (fs.existsSync(portFile)) { try { const port = parseInt(fs.readFileSync(portFile, 'utf-8').trim(), 10); if (!isNaN(port) && port > 0 && port < 65536) { log.info(`Attempting to kill process on port: ${port}`); await killProcessOnPort(port); } fs.unlinkSync(portFile); } catch (error) { log.error('Error handling port file:', error); } } python_process = null; } catch (error) { log.error('Error occurred while cleaning up process:', error); } }; // ==================== app event handle ==================== app.whenReady().then(() => { // ==================== download handle ==================== session.defaultSession.on('will-download', (event, item, webContents) => { item.once('done', (event, state) => { shell.showItemInFolder(item.getURL().replace('localfile://', '')); }); }); // ==================== protocol handle ==================== protocol.handle('localfile', async (request) => { const url = decodeURIComponent(request.url.replace('localfile://', '')); const filePath = path.normalize(url); try { const data = await fsp.readFile(filePath); // set correct Content-Type according to file extension const ext = path.extname(filePath).toLowerCase(); let contentType = 'application/octet-stream'; switch (ext) { case '.pdf': contentType = 'application/pdf'; break; case '.html': case '.htm': contentType = 'text/html'; break; } return new Response(new Uint8Array(data), { headers: { 'Content-Type': contentType, }, }); } catch (err) { return new Response('Not Found', { status: 404 }); } }); // ==================== initialize app ==================== initializeApp(); registerIpcHandlers(); createWindow(); }); // ==================== window close event ==================== app.on('window-all-closed', () => { log.info('window-all-closed'); webViewManager = null; win = null; if (process.platform !== 'darwin') { app.quit(); } }); // ==================== app activate event ==================== app.on('activate', () => { const allWindows = BrowserWindow.getAllWindows(); log.info('activate', allWindows.length); if (allWindows.length) { allWindows[0].focus(); } else { cleanupPythonProcess(); createWindow(); } }); // ==================== app exit event ==================== app.on('before-quit', () => { log.info('before-quit'); log.info('quit python_process.pid: ' + python_process?.pid); if (win) { win.destroy(); } cleanupPythonProcess(); });