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, 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'; import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps' import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process' const userData = app.getPath('userData'); // ==================== 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 + ''); }); // ==================== 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'; log.transports.console.format = '[{level}]{text}'; log.transports.file.format = '[{level}]{text}'; // 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; }; function registerIpcHandlers() { // ==================== basic info handler ==================== ipcMain.handle('get-browser-port', () => { log.info('Starting new task') return browser_port }); ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-backend-port', () => backendPort); ipcMain.handle('restart-backend', async () => { try { if (backendPort) { log.info('Restarting backend service...'); await cleanupPythonProcess(); await checkAndStartBackend(); log.info('Backend restart completed successfully'); return { success: true }; } else { log.warn('No backend port found, starting fresh backend'); await checkAndStartBackend(); return { success: true }; } } catch (error) { log.error('Failed to restart backend:', error); return { success: false, error: String(error) }; } }); 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) => { // Convert args from JSON string to array if needed if (mcp.args && typeof mcp.args === 'string') { try { mcp.args = JSON.parse(mcp.args); } catch (e) { // If parsing fails, split by comma as fallback mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); } } 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) => { // Convert args from JSON string to array if needed if (mcp.args && typeof mcp.args === 'string') { try { mcp.args = JSON.parse(mcp.args); } catch (e) { // If parsing fails, split by comma as fallback mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); } } 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', (_, data) => { if(data.isForceQuit) { return app?.quit() } return 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 { const stats = await fs.promises.stat(filePath.replace(/\/$/, '')).catch(() => null); if (stats && stats.isDirectory()) { shell.openPath(filePath); } else { 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); }); ipcMain.handle('get-log-folder', async (_, email: string) => { const manager = checkManagerInstance(fileReader, 'FileReader'); return manager.getLogFolder(email); }); // ==================== 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', async () => { try { if(win === null) throw new Error("Window is null"); //Force installation even if versionFile exists const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true}); return { success: true, isInstalled }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('check-tool-installed', async () => { try { const isInstalled = await checkToolInstalled(); return { success: true, isInstalled: isInstalled.success }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('get-installation-status', async () => { try { const { isInstalling, hasLockFile } = await getInstallationStatus(); return { success: true, isInstalling, hasLockFile, timestamp: Date.now() }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // ==================== register update related handler ==================== registerUpdateIpcHandlers(); } // ==================== ensure eigent directories ==================== const ensureEigentDirectories = () => { const eigentBase = path.join(os.homedir(), '.eigent'); const requiredDirs = [ eigentBase, path.join(eigentBase, 'bin'), path.join(eigentBase, 'cache'), path.join(eigentBase, 'venvs'), path.join(eigentBase, 'runtime'), ]; for (const dir of requiredDirs) { if (!fs.existsSync(dir)) { log.info(`Creating directory: ${dir}`); fs.mkdirSync(dir, { recursive: true }); } } log.info('.eigent directory structure ensured'); }; // ==================== window create ==================== async function createWindow() { const isMac = process.platform === 'darwin'; // Ensure .eigent directories exist before anything else ensureEigentDirectories(); win = new BrowserWindow({ title: 'Eigent', width: 1200, height: 800, minWidth: 1050, minHeight: 650, 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()); } // ==================== set event listeners ==================== setupWindowEventListeners(); setupDevToolsShortcuts(); setupExternalLinkHandling(); handleBeforeClose(); // ==================== auto update ==================== update(win); // ==================== CHECK IF INSTALLATION IS NEEDED BEFORE LOADING CONTENT ==================== log.info('Pre-checking if dependencies need to be installed...'); // Check version and tools status synchronously const currentVersion = app.getVersion(); const versionFile = path.join(app.getPath('userData'), 'version.txt'); const versionExists = fs.existsSync(versionFile); let savedVersion = ''; if (versionExists) { savedVersion = fs.readFileSync(versionFile, 'utf-8').trim(); } const uvExists = await isBinaryExists('uv'); const bunExists = await isBinaryExists('bun'); // Check if installation was previously completed const backendPath = getBackendPath(); const installedLockPath = path.join(backendPath, 'uv_installed.lock'); const installationCompleted = fs.existsSync(installedLockPath); // Check if venv path exists for current version const venvPath = getVenvPath(currentVersion); const venvExists = fs.existsSync(venvPath); const needsInstallation = !versionExists || savedVersion !== currentVersion || !uvExists || !bunExists || !installationCompleted || !venvExists; log.info('Installation check result:', { needsInstallation, versionExists, versionMatch: savedVersion === currentVersion, uvExists, bunExists, installationCompleted, venvExists, venvPath }); // Handle localStorage based on installation state if (needsInstallation) { log.info('Installation needed - clearing auth storage to force carousel state'); // Clear the persisted auth storage file to force fresh initialization with carousel const localStoragePath = path.join(app.getPath('userData'), 'Local Storage'); const leveldbPath = path.join(localStoragePath, 'leveldb'); try { // Delete the localStorage database to force fresh init if (fs.existsSync(leveldbPath)) { log.info('Removing localStorage database to force fresh state...'); fs.rmSync(leveldbPath, { recursive: true, force: true }); log.info('Successfully cleared localStorage'); } } catch (error) { log.error('Error clearing localStorage:', error); } // Set up the injection for when page loads win.webContents.once('dom-ready', () => { if (!win || win.isDestroyed()) { log.warn('Window destroyed before DOM ready - skipping localStorage injection'); return; } log.info('DOM ready - creating auth-storage with carousel state'); win.webContents.executeJavaScript(` (function() { try { // Create fresh auth storage with carousel state const newAuthStorage = { state: { token: null, username: null, email: null, user_id: null, appearance: 'light', language: 'system', isFirstLaunch: true, modelType: 'cloud', cloud_model_type: 'gpt-4.1', initState: 'carousel', share_token: null, workerListData: {} }, version: 0 }; localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage)); console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state'); } catch (e) { console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e); } })(); `).catch(err => { log.error('Failed to inject script:', err); }); }); } else { // Installation is complete - ensure initState is set to 'done' log.info('Installation already complete - ensuring initState is done'); win.webContents.once('dom-ready', () => { if (!win || win.isDestroyed()) { log.warn('Window destroyed before DOM ready - skipping localStorage update'); return; } log.info('DOM ready - checking and updating auth-storage to done state'); win.webContents.executeJavaScript(` (function() { try { const authStorage = localStorage.getItem('auth-storage'); if (authStorage) { const parsed = JSON.parse(authStorage); if (parsed.state && parsed.state.initState !== 'done') { console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done'); // Only update the initState field, preserve all other data const updatedStorage = { ...parsed, state: { ...parsed.state, initState: 'done' } }; localStorage.setItem('auth-storage', JSON.stringify(updatedStorage)); console.log('[ELECTRON] initState updated to done, reloading page...'); return true; // Signal that we need to reload } } return false; // No reload needed } catch (e) { console.error('[ELECTRON] Failed to update initState:', e); // Don't modify localStorage if there's an error to prevent data corruption return false; } })(); `).then(needsReload => { if (needsReload) { log.info('Reloading window after localStorage update'); win!.reload(); } }).catch(err => { log.error('Failed to inject script:', err); }); }); } // Load content if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL); win.webContents.openDevTools(); } else { win.loadFile(indexHtml); } // Wait for window to be ready await new Promise(resolve => { win!.webContents.once('did-finish-load', () => { log.info('Window content loaded, starting dependency check immediately...'); resolve(); }); }); // Now check and install dependencies let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); if (!res.success) { log.info("[DEPS INSTALL] Dependency Error: ", res.message); win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message }); return; } log.info("[DEPS INSTALL] Dependency Success: ", res.message); // Start backend after dependencies are ready 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...'); try { const isToolInstalled = await checkToolInstalled(); if (isToolInstalled.success) { 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'); } } catch (error) { log.debug("Cannot Start Backend due to ", error) } }; // ==================== 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); } }; // brefore close const handleBeforeClose = () => { win?.on("close", (event) => { event.preventDefault(); win?.webContents.send("before-close"); }) } // ==================== 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(); });