diff --git a/electron-builder.json b/electron-builder.json index 3b0752ac..0bc2b12c 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -6,16 +6,30 @@ "directories": { "output": "release" }, - "files": ["dist-electron", "dist", "resources", "!backend", "!dist/images","!**/__pycache__","!server","!docs"], + "files": [ + "dist-electron", + "dist", + "resources", + "!backend", + "!dist/images", + "!**/__pycache__", + "!server", + "!docs" + ], "extraResources": [ { - "from": "backend", + "from": "backend", "to": "backend", "filter": ["**/*", "!.venv/**/*"] }, { "from": "utils", "to": "utils" + }, + { + "from": "resources/prebuilt", + "to": "prebuilt", + "filter": ["**/*"] } ], "protocols": [ @@ -70,7 +84,7 @@ "deleteAppDataOnUninstall": false, "installerIcon": "build/icon.ico", "uninstallerIcon": "build/icon.ico", - "installerHeaderIcon": "build/icon.ico", + "installerHeaderIcon": "build/icon.ico", "include": "build/installer.nsh" } } diff --git a/electron/main/index.ts b/electron/main/index.ts index 7e84d4ab..611d5fcd 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1359,6 +1359,36 @@ async function createWindow() { // ==================== CHECK IF INSTALLATION IS NEEDED BEFORE LOADING CONTENT ==================== log.info('Pre-checking if dependencies need to be installed...'); + // Check if prebuilt dependencies are available (for packaged app) + let hasPrebuiltDeps = false; + if (app.isPackaged) { + const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin'); + const prebuiltVenvDir = path.join( + process.resourcesPath, + 'prebuilt', + 'venv' + ); + const uvPath = path.join( + prebuiltBinDir, + process.platform === 'win32' ? 'uv.exe' : 'uv' + ); + const bunPath = path.join( + prebuiltBinDir, + process.platform === 'win32' ? 'bun.exe' : 'bun' + ); + const pyvenvCfg = path.join(prebuiltVenvDir, 'pyvenv.cfg'); + + hasPrebuiltDeps = + fs.existsSync(uvPath) && + fs.existsSync(bunPath) && + fs.existsSync(pyvenvCfg); + if (hasPrebuiltDeps) { + log.info( + '[PRE-CHECK] Prebuilt dependencies found, skipping installation check' + ); + } + } + // Check version and tools status synchronously const currentVersion = app.getVersion(); const versionFile = path.join(app.getPath('userData'), 'version.txt'); @@ -1380,13 +1410,15 @@ async function createWindow() { const venvPath = getVenvPath(currentVersion); const venvExists = fs.existsSync(venvPath); - const needsInstallation = - !versionExists || - savedVersion !== currentVersion || - !uvExists || - !bunExists || - !installationCompleted || - !venvExists; + // If prebuilt deps are available, skip installation + const needsInstallation = hasPrebuiltDeps + ? false + : !versionExists || + savedVersion !== currentVersion || + !uvExists || + !bunExists || + !installationCompleted || + !venvExists; log.info('Installation check result:', { needsInstallation, diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 84efae6d..675985f7 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -1,12 +1,21 @@ -import { app, BrowserWindow } from 'electron' -import path from 'node:path' -import log from 'electron-log' -import { getMainWindow } from './init' -import fs from 'node:fs' -import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process' -import { spawn } from 'child_process' -import { safeMainWindowSend } from './utils/safeWebContentsSend' -import os from 'node:os' +import { app, BrowserWindow } from 'electron'; +import path from 'node:path'; +import log from 'electron-log'; +import { getMainWindow } from './init'; +import fs from 'node:fs'; +import { + getBackendPath, + getBinaryPath, + getCachePath, + getVenvPath, + getUvEnv, + cleanupOldVenvs, + isBinaryExists, + runInstallScript, +} from './utils/process'; +import { spawn } from 'child_process'; +import { safeMainWindowSend } from './utils/safeWebContentsSend'; +import os from 'node:os'; const userData = app.getPath('userData'); const versionFile = path.join(userData, 'version.txt'); @@ -14,19 +23,55 @@ const versionFile = path.join(userData, 'version.txt'); export type PromiseReturnType = { message: string; success: boolean; -} +}; interface checkInstallProps { - win:BrowserWindow|null; - forceInstall?:boolean + win: BrowserWindow | null; + forceInstall?: boolean; } // Read last run version and install dependencies on update -export const checkAndInstallDepsOnUpdate = async ({win, forceInstall=false}:checkInstallProps): -Promise => { +export const checkAndInstallDepsOnUpdate = async ({ + win, + forceInstall = false, +}: checkInstallProps): Promise => { const currentVersion = app.getVersion(); let savedVersion = ''; + + // Check if prebuilt dependencies are available + const hasPrebuiltDeps = (): boolean => { + if (!app.isPackaged) { + return false; + } + const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin'); + const prebuiltVenvDir = path.join( + process.resourcesPath, + 'prebuilt', + 'venv' + ); + const uvPath = path.join( + prebuiltBinDir, + process.platform === 'win32' ? 'uv.exe' : 'uv' + ); + const bunPath = path.join( + prebuiltBinDir, + process.platform === 'win32' ? 'bun.exe' : 'bun' + ); + const pyvenvCfg = path.join(prebuiltVenvDir, 'pyvenv.cfg'); + + const hasBinaries = fs.existsSync(uvPath) && fs.existsSync(bunPath); + const hasVenv = fs.existsSync(pyvenvCfg); + + if (hasBinaries && hasVenv) { + log.info( + '[DEPS INSTALL] Prebuilt dependencies found, skipping installation' + ); + return true; + } + return false; + }; + const checkInstallOperations = { - getSavedVersion: ():boolean => { + getSavedVersion: (): boolean => { // Check if version file exists const versionExists = fs.existsSync(versionFile); if (versionExists) { @@ -38,34 +83,50 @@ Promise => { } return versionExists; }, - handleUpdateNotification: (versionExists:boolean) => { + handleUpdateNotification: (versionExists: boolean) => { 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' + reason: !versionExists + ? 'version file not exist' + : 'version not match', }); } else { - log.warn('[DEPS INSTALL] Cannot send update notification - window not available'); + log.warn( + '[DEPS INSTALL] Cannot send update notification - window not available' + ); } }, createVersionFile: () => { fs.writeFileSync(versionFile, currentVersion); log.info('[DEPS INSTALL] version file updated', { currentVersion }); - } - } + }, + }; return new Promise(async (resolve, reject) => { try { + // If prebuilt dependencies are available, use them and skip installation + if (hasPrebuiltDeps()) { + log.info( + '[DEPS INSTALL] Using prebuilt dependencies, creating version file' + ); + checkInstallOperations.createVersionFile(); + resolve({ message: 'Using prebuilt dependencies', success: true }); + return; + } + // Clean up cache in production environment BEFORE any checks // This ensures users always get fresh dependencies in production if (app.isPackaged) { - log.info('[CACHE CLEANUP] Production environment detected, cleaning cache before dependency check...'); + log.info( + '[CACHE CLEANUP] Production environment detected, cleaning cache before dependency check...' + ); cleanupCacheInProduction(); } - const versionExists:boolean = checkInstallOperations.getSavedVersion(); + const versionExists: boolean = checkInstallOperations.getSavedVersion(); // Check if command tools are installed const uvExists = await isBinaryExists('uv'); @@ -74,18 +135,31 @@ Promise => { // If version file does not exist or version does not match, reinstall dependencies // Or if command tools are missing, need to install them - if (forceInstall || !versionExists || savedVersion !== currentVersion || toolsMissing) { + if ( + forceInstall || + !versionExists || + savedVersion !== currentVersion || + toolsMissing + ) { if (toolsMissing) { - log.info('[DEPS INSTALL] Command tools missing, starting installation...', { - uvExists, - bunExists - }); + log.info( + '[DEPS INSTALL] Command tools missing, starting installation...', + { + uvExists, + bunExists, + } + ); } else { - log.info('[DEPS INSTALL] version changed, prepare to reinstall uv dependencies...', { - currentVersion, - savedVersion: versionExists ? savedVersion : 'none', - reason: !versionExists ? 'version file not exist' : 'version not match' - }); + log.info( + '[DEPS INSTALL] 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 @@ -95,67 +169,71 @@ Promise => { const result = await installDependencies(currentVersion); if (!result.success) { log.error(' install dependencies failed'); - resolve({ message: `Install dependencies failed, msg ${result.message}`, success: false }); - return + resolve({ + message: `Install dependencies failed, msg ${result.message}`, + success: false, + }); + return; } // Update version file ONLY after successful installation checkInstallOperations.createVersionFile(); - resolve({ message: "Dependencies installed successfully after update", success: true }); + resolve({ + message: 'Dependencies installed successfully after update', + success: true, + }); log.info('[DEPS INSTALL] install dependencies complete'); - return + return; } else { - log.info('[DEPS INSTALL] version not changed and tools installed, skip install dependencies', { currentVersion }); - resolve({ message: "Version not changed and tools installed, skipped installation", success: true }); - return + log.info( + '[DEPS INSTALL] version not changed and tools installed, skip install dependencies', + { currentVersion } + ); + resolve({ + message: + 'Version not changed and tools installed, skipped installation', + success: true, + }); + return; } } catch (error) { log.error(' check version and install dependencies error:', error); resolve({ message: `Error checking version: ${error}`, success: false }); - return + return; } - }) -} + }); +}; /** * Check if command line tools are installed, install if not */ export async function installCommandTool(): Promise { try { - const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => { - if (await isBinaryExists(toolName)) { - return { message: `${toolName} already installed`, success: true }; - } + const ensureInstalled = async ( + toolName: 'uv' | 'bun', + scriptName: string + ): Promise => { + if (await isBinaryExists(toolName)) { + return { message: `${toolName} already installed`, success: true }; + } - console.log(`start install ${toolName}`); - try { - await runInstallScript(scriptName); - const installed = await isBinaryExists(toolName); + console.log(`start install ${toolName}`); + try { + await runInstallScript(scriptName); + const installed = await isBinaryExists(toolName); - if (installed) { - safeMainWindowSend('install-dependencies-log', { - type: 'stdout', - data: `${toolName} installed successfully`, - }); - return { - message: `${toolName} installed successfully`, - success: true - }; - } else { - const errorMsg = `${toolName} installation failed: binary not found after installation`; - safeMainWindowSend('install-dependencies-complete', { - success: false, - code: 2, - error: errorMsg, - }); - return { - message: errorMsg, - success: false - }; - } - } catch (scriptError) { - const errorMsg = `${toolName} installation failed: ${scriptError instanceof Error ? scriptError.message : String(scriptError)}`; + if (installed) { + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: `${toolName} installed successfully`, + }); + return { + message: `${toolName} installed successfully`, + success: true, + }; + } else { + const errorMsg = `${toolName} installation failed: binary not found after installation`; safeMainWindowSend('install-dependencies-complete', { success: false, code: 2, @@ -163,35 +241,54 @@ export async function installCommandTool(): Promise { }); return { message: errorMsg, - success: false + success: false, }; } - }; - - const uvResult = await ensureInstalled('uv', 'install-uv.js'); - if (!uvResult.success) { - return { message: uvResult.message, success: false }; + } catch (scriptError) { + const errorMsg = `${toolName} installation failed: ${ + scriptError instanceof Error + ? scriptError.message + : String(scriptError) + }`; + safeMainWindowSend('install-dependencies-complete', { + success: false, + code: 2, + error: errorMsg, + }); + return { + message: errorMsg, + success: false, + }; } + }; - const bunResult = await ensureInstalled('bun', 'install-bun.js'); - if (!bunResult.success) { - return { message: bunResult.message, success: false }; - } + const uvResult = await ensureInstalled('uv', 'install-uv.js'); + if (!uvResult.success) { + return { message: uvResult.message, success: false }; + } - return { message: "Command tools installed successfully", success: true }; + const bunResult = await ensureInstalled('bun', 'install-bun.js'); + if (!bunResult.success) { + return { message: bunResult.message, success: false }; + } + + return { message: 'Command tools installed successfully', success: true }; } catch (error) { - const errorMessage = `Command tool installation failed: ${error}`; - log.error('[DEPS INSTALL] Exception during command tool installation:', error); - safeMainWindowSend('install-dependencies-complete', { - success: false, - code: 2, - error: errorMessage - }); - return { message: errorMessage, success: false }; + const errorMessage = `Command tool installation failed: ${error}`; + log.error( + '[DEPS INSTALL] Exception during command tool installation:', + error + ); + safeMainWindowSend('install-dependencies-complete', { + success: false, + code: 2, + error: errorMessage, + }); + return { message: errorMessage, success: false }; } } -let uv_path:string; +let uv_path: string; const mainWindow = getMainWindow(); const backendPath = getBackendPath(); @@ -201,10 +298,13 @@ if (!fs.existsSync(backendPath)) { fs.mkdirSync(backendPath, { recursive: true }); } -const installingLockPath = path.join(backendPath, 'uv_installing.lock') -const installedLockPath = path.join(backendPath, 'uv_installed.lock') +const installingLockPath = path.join(backendPath, 'uv_installing.lock'); +const installedLockPath = path.join(backendPath, 'uv_installed.lock'); // const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple'] -const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/'] +const proxyArgs = [ + '--default-index', + 'https://mirrors.aliyun.com/pypi/simple/', +]; /** * Get current installation status by checking lock files @@ -218,20 +318,23 @@ export async function getInstallationStatus(): Promise<{ try { const installingExists = fs.existsSync(installingLockPath); const installedExists = fs.existsSync(installedLockPath); - + // If installing lock exists, installation is in progress // If installed lock exists, installation completed previously return { isInstalling: installingExists, hasLockFile: installingExists || installedExists, - installedExists: installedExists + installedExists: installedExists, }; } catch (error) { - console.error('[getInstallationStatus] Error checking installation status:', error); + console.error( + '[getInstallationStatus] Error checking installation status:', + error + ); return { isInstalling: false, hasLockFile: false, - installedExists: false + installedExists: false, }; } } @@ -240,52 +343,67 @@ class InstallLogs { private node_process; private version: string; - constructor(extraArgs:string[], version: string) { - console.log('start install dependencies', extraArgs, 'version:', version) + constructor(extraArgs: string[], version: string) { + console.log('start install dependencies', extraArgs, 'version:', version); this.version = version; - this.node_process = spawn(uv_path, [ + this.node_process = spawn( + uv_path, + [ 'sync', '--no-dev', - '--cache-dir', getCachePath('uv_cache'), - ...extraArgs], { + '--cache-dir', + getCachePath('uv_cache'), + ...extraArgs, + ], + { cwd: backendPath, env: { - ...process.env, - ...getUvEnv(version), - } - }) + ...process.env, + ...getUvEnv(version), + }, + } + ); } /**Display filtered logs based on severity */ - displayFilteredLogs(data:String) { - if (!data) return; - const msg = data.toString().trimEnd(); - if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) { - log.error(`BACKEND: [DEPS INSTALL] ${msg}`); - safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() }); - } else { - log.info(`BACKEND: [DEPS INSTALL] ${msg}`); - safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() }); - } + displayFilteredLogs(data: String) { + if (!data) return; + const msg = data.toString().trimEnd(); + if ( + msg.toLowerCase().includes('error') || + msg.toLowerCase().includes('traceback') + ) { + log.error(`BACKEND: [DEPS INSTALL] ${msg}`); + safeMainWindowSend('install-dependencies-log', { + type: 'stderr', + data: data.toString(), + }); + } else { + log.info(`BACKEND: [DEPS INSTALL] ${msg}`); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: data.toString(), + }); + } } - + /**Handle stdout data */ - onStdout() { - this.node_process.stdout.on('data', (data:any) => { - this.displayFilteredLogs(data); - }) + onStdout() { + this.node_process.stdout.on('data', (data: any) => { + this.displayFilteredLogs(data); + }); } /**Handle stderr data */ onStderr() { - this.node_process.stderr.on('data', (data:any) => { - this.displayFilteredLogs(data); - }) + this.node_process.stderr.on('data', (data: any) => { + this.displayFilteredLogs(data); + }); } /**Handle process close event */ - onClose(resolveInner:(code: number | null) => void) { + onClose(resolveInner: (code: number | null) => void) { this.node_process.on('close', resolveInner); } @@ -296,15 +414,15 @@ class InstallLogs { */ static setLockPath() { if (!fs.existsSync(backendPath)) { - fs.mkdirSync(backendPath, { recursive: true }) + fs.mkdirSync(backendPath, { recursive: true }); } - fs.writeFileSync(installingLockPath, '') + fs.writeFileSync(installingLockPath, ''); } /**Clean installing Lock Path */ static cleanLockPath() { if (fs.existsSync(installingLockPath)) { - fs.unlinkSync(installingLockPath); + fs.unlinkSync(installingLockPath); } } } @@ -319,7 +437,9 @@ function cleanupCacheInProduction(): void { const cacheBaseDir = path.join(os.homedir(), '.eigent', 'cache'); if (!fs.existsSync(cacheBaseDir)) { - log.info('[CACHE CLEANUP] Cache directory does not exist, nothing to clean'); + log.info( + '[CACHE CLEANUP] Cache directory does not exist, nothing to clean' + ); return; } @@ -331,7 +451,6 @@ function cleanupCacheInProduction(): void { fs.mkdirSync(cacheBaseDir, { recursive: true }); log.info('[CACHE CLEANUP] Empty cache directory recreated'); - } catch (error) { log.error('[CACHE CLEANUP] Failed to clean cache directory:', error); } @@ -341,47 +460,56 @@ const runInstall = (extraArgs: string[], version: string) => { const installLogs = new InstallLogs(extraArgs, version); return new Promise((resolveInner, rejectInner) => { try { - installLogs.onStdout(); - installLogs.onStderr(); - installLogs.onClose((code) => { - console.log('install dependencies end', code === 0) - InstallLogs.cleanLockPath() - resolveInner({ - message: code === 0 ? "Installation completed successfully" : `Installation failed with code ${code}`, - success: code === 0 - }) - }) - } catch (err) { - log.error('run install failed', err) - // Clean up uv_installing.lock file if installation fails + installLogs.onStdout(); + installLogs.onStderr(); + installLogs.onClose((code) => { + console.log('install dependencies end', code === 0); InstallLogs.cleanLockPath(); - rejectInner({ message: `Installation failed: ${err}`, success: false }) + resolveInner({ + message: + code === 0 + ? 'Installation completed successfully' + : `Installation failed with code ${code}`, + success: code === 0, + }); + }); + } catch (err) { + log.error('run install failed', err); + // Clean up uv_installing.lock file if installation fails + InstallLogs.cleanLockPath(); + rejectInner({ message: `Installation failed: ${err}`, success: false }); } - }) -} + }); +}; -export async function installDependencies(version: string): Promise { +export async function installDependencies( + version: string +): Promise { uv_path = await getBinaryPath('uv'); const venvPath = getVenvPath(version); const handleInstallOperations = { - spawnBabel: (message:"mirror"|"main"="main") => { - fs.writeFileSync(installedLockPath, '') - log.info('[DEPS INSTALL] Script completed successfully') - console.log(`Install Dependencies completed ${message} for version ${version}`) - console.log(`Virtual environment path: ${venvPath}`) + spawnBabel: (message: 'mirror' | 'main' = 'main') => { + fs.writeFileSync(installedLockPath, ''); + log.info('[DEPS INSTALL] Script completed successfully'); + console.log( + `Install Dependencies completed ${message} for version ${version}` + ); + console.log(`Virtual environment path: ${venvPath}`); spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath, env: { ...process.env, UV_PROJECT_ENVIRONMENT: venvPath, - } - }) + }, + }); }, - notifyInstallDependenciesPage: ():boolean => { + notifyInstallDependenciesPage: (): boolean => { const success = safeMainWindowSend('install-dependencies-start'); if (!success) { - log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates'); + log.warn( + '[DEPS INSTALL] Main window not available, continuing installation without UI updates' + ); } return success; }, @@ -395,57 +523,89 @@ export async function installDependencies(version: string): Promise name.startsWith('python')); + const pythonDir = libContents.find((name) => + name.startsWith('python') + ); if (pythonDir) { sitePackagesPath = path.join(libPath, pythonDir, 'site-packages'); } } if (!sitePackagesPath || !fs.existsSync(sitePackagesPath)) { - log.warn('[DEPS INSTALL] site-packages directory not found in venv, skipping npm install'); + log.warn( + '[DEPS INSTALL] site-packages directory not found in venv, skipping npm install' + ); return true; // Not an error if the venv structure is different } - const toolkitPath = path.join(sitePackagesPath, 'camel', 'toolkits', 'hybrid_browser_toolkit', 'ts'); + const toolkitPath = path.join( + sitePackagesPath, + 'camel', + 'toolkits', + 'hybrid_browser_toolkit', + 'ts' + ); if (!fs.existsSync(toolkitPath)) { - log.warn('[DEPS INSTALL] hybrid_browser_toolkit ts directory not found at ' + toolkitPath + ', skipping npm install'); + log.warn( + '[DEPS INSTALL] hybrid_browser_toolkit ts directory not found at ' + + toolkitPath + + ', skipping npm install' + ); return true; // Not an error if the toolkit isn't installed } // Check if npm dependencies are already installed - const npmMarkerPath = path.join(toolkitPath, '.npm_dependencies_installed'); + const npmMarkerPath = path.join( + toolkitPath, + '.npm_dependencies_installed' + ); const nodeModulesPath = path.join(toolkitPath, 'node_modules'); const distPath = path.join(toolkitPath, 'dist'); // Check if marker exists and verify version - if (fs.existsSync(npmMarkerPath) && fs.existsSync(nodeModulesPath) && fs.existsSync(distPath)) { + if ( + fs.existsSync(npmMarkerPath) && + fs.existsSync(nodeModulesPath) && + fs.existsSync(distPath) + ) { try { - const markerContent = JSON.parse(fs.readFileSync(npmMarkerPath, 'utf-8')); + const markerContent = JSON.parse( + fs.readFileSync(npmMarkerPath, 'utf-8') + ); if (markerContent.version === version) { - log.info('[DEPS INSTALL] hybrid_browser_toolkit npm dependencies already installed for current version, skipping...'); + log.info( + '[DEPS INSTALL] hybrid_browser_toolkit npm dependencies already installed for current version, skipping...' + ); return true; } else { - log.info('[DEPS INSTALL] npm dependencies installed for different version, will reinstall...'); + log.info( + '[DEPS INSTALL] npm dependencies installed for different version, will reinstall...' + ); // Clean up old installation fs.unlinkSync(npmMarkerPath); } } catch (error) { - log.warn('[DEPS INSTALL] Could not read npm marker file, will reinstall...', error); + log.warn( + '[DEPS INSTALL] Could not read npm marker file, will reinstall...', + error + ); // If we can't read the marker, assume we need to reinstall } } - log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit npm dependencies...'); + log.info( + '[DEPS INSTALL] Installing hybrid_browser_toolkit npm dependencies...' + ); safeMainWindowSend('install-dependencies-log', { type: 'stdout', - data: 'Installing browser toolkit dependencies...\n' + data: 'Installing browser toolkit dependencies...\n', }); // Try to find npm - first try system npm, then try uv run npm let npmCommand: string[]; const testNpm = spawn('npm', ['--version'], { shell: true }); - const npmExists = await new Promise(resolve => { + const npmExists = await new Promise((resolve) => { testNpm.on('close', (code) => resolve(code === 0)); testNpm.on('error', () => resolve(false)); }); @@ -465,29 +625,39 @@ export async function installDependencies(version: string): Promise((resolve, reject) => { if (npmInstall.stdout) { npmInstall.stdout.on('data', (data) => { log.info(`[DEPS INSTALL] npm install: ${data}`); - safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() }); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: data.toString(), + }); }); } if (npmInstall.stderr) { npmInstall.stderr.on('data', (data) => { log.warn(`[DEPS INSTALL] npm install stderr: ${data}`); - safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() }); + safeMainWindowSend('install-dependencies-log', { + type: 'stderr', + data: data.toString(), + }); }); } @@ -508,13 +678,18 @@ export async function installDependencies(version: string): Promise((resolve, reject) => { if (npmBuild.stdout) { npmBuild.stdout.on('data', (data) => { log.info(`[DEPS INSTALL] npm build: ${data}`); - safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() }); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: data.toString(), + }); }); } @@ -537,16 +715,23 @@ export async function installDependencies(version: string): Promise { // TypeScript build warnings are common, don't treat as errors log.info(`[DEPS INSTALL] npm build output: ${data}`); - safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() }); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: data.toString(), + }); }); } npmBuild.on('close', (code) => { if (code === 0) { - log.info('[DEPS INSTALL] TypeScript build completed successfully'); + log.info( + '[DEPS INSTALL] TypeScript build completed successfully' + ); resolve(); } else { - log.error(`[DEPS INSTALL] TypeScript build failed with code ${code}`); + log.error( + `[DEPS INSTALL] TypeScript build failed with code ${code}` + ); reject(new Error(`TypeScript build failed with code ${code}`)); } }); @@ -560,74 +745,107 @@ export async function installDependencies(version: string): Promise((resolve) => { playwrightInstall.on('close', (code) => { if (code === 0) { - log.info('[DEPS INSTALL] Playwright browsers installed successfully'); + log.info( + '[DEPS INSTALL] Playwright browsers installed successfully' + ); // Create marker file - const markerPath = path.join(toolkitPath, '.playwright_installed'); + const markerPath = path.join( + toolkitPath, + '.playwright_installed' + ); fs.writeFileSync(markerPath, 'installed'); } else { - log.warn('[DEPS INSTALL] Playwright installation failed, but continuing anyway'); + log.warn( + '[DEPS INSTALL] Playwright installation failed, but continuing anyway' + ); } resolve(); }); playwrightInstall.on('error', (err) => { - log.warn('[DEPS INSTALL] Playwright installation process error:', err); + log.warn( + '[DEPS INSTALL] Playwright installation process error:', + err + ); resolve(); // Non-critical, continue }); }); } catch (error) { - log.warn('[DEPS INSTALL] Failed to install Playwright browsers:', error); + log.warn( + '[DEPS INSTALL] Failed to install Playwright browsers:', + error + ); // Non-critical, continue } // Create marker file to indicate npm dependencies are installed - fs.writeFileSync(npmMarkerPath, JSON.stringify({ - installedAt: new Date().toISOString(), - version: version - })); + fs.writeFileSync( + npmMarkerPath, + JSON.stringify({ + installedAt: new Date().toISOString(), + version: version, + }) + ); log.info('[DEPS INSTALL] Created npm dependencies marker file'); - log.info('[DEPS INSTALL] hybrid_browser_toolkit dependencies installed successfully'); + log.info( + '[DEPS INSTALL] hybrid_browser_toolkit dependencies installed successfully' + ); return true; } catch (error) { - log.error('[DEPS INSTALL] Failed to install hybrid_browser_toolkit dependencies:', error); + log.error( + '[DEPS INSTALL] Failed to install hybrid_browser_toolkit dependencies:', + error + ); // Don't fail the entire installation if this fails return false; } - } - } + }, + }; return new Promise(async (resolve, reject) => { - console.log('start install dependencies') - const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage(); - + console.log('start install dependencies'); + const mainWindowAvailable = + handleInstallOperations.notifyInstallDependenciesPage(); + if (!mainWindowAvailable) { - log.info('[DEPS INSTALL] Proceeding with installation without UI notifications'); + log.info( + '[DEPS INSTALL] Proceeding with installation without UI notifications' + ); } - const isInstalCommandTool = await installCommandTool() + const isInstalCommandTool = await installCommandTool(); if (!isInstalCommandTool.success) { - log.error('[DEPS INSTALL] Command tool installation failed:', isInstalCommandTool.message); - safeMainWindowSend('install-dependencies-complete', { - success: false, - code: 2, - error: isInstalCommandTool.message || 'Command tool installation failed' - }); - resolve({ message: "Command tool installation failed", success: false }) - return + log.error( + '[DEPS INSTALL] Command tool installation failed:', + isInstalCommandTool.message + ); + safeMainWindowSend('install-dependencies-complete', { + success: false, + code: 2, + error: + isInstalCommandTool.message || 'Command tool installation failed', + }); + resolve({ message: 'Command tool installation failed', success: false }); + return; } // Set Installing Lock Files @@ -641,17 +859,26 @@ export async function installDependencies(version: string): Promise name.startsWith('python')); + const pythonDir = libContents.find((name) => name.startsWith('python')); if (pythonDir) { sitePackagesPath = path.join(libPath, pythonDir, 'site-packages'); } } if (sitePackagesPath) { - const npmMarkerPath = path.join(sitePackagesPath, 'camel', 'toolkits', 'hybrid_browser_toolkit', 'ts', '.npm_dependencies_installed'); + const npmMarkerPath = path.join( + sitePackagesPath, + 'camel', + 'toolkits', + 'hybrid_browser_toolkit', + 'ts', + '.npm_dependencies_installed' + ); if (fs.existsSync(npmMarkerPath)) { fs.unlinkSync(npmMarkerPath); - log.info('[DEPS INSTALL] Removed npm dependencies marker for fresh installation'); + log.info( + '[DEPS INSTALL] Removed npm dependencies marker for fresh installation' + ); } } } catch (error) { @@ -660,48 +887,67 @@ export async function installDependencies(version: string): Promise { return new Promise((resolve, reject) => { - const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath) - log.info(`Running script at: ${installScriptPath}`) + const installScriptPath = path.join( + getResourcePath(), + 'scripts', + scriptPath + ); + log.info(`Running script at: ${installScriptPath}`); const nodeProcess = spawn(process.execPath, [installScriptPath], { - env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } - }) + env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, + }); let stderrOutput = ''; nodeProcess.stdout.on('data', (data) => { - log.info(`Script output: ${data}`) - }) + log.info(`Script output: ${data}`); + }); nodeProcess.stderr.on('data', (data) => { const errorMsg = data.toString(); stderrOutput += errorMsg; - log.error(`Script error: ${errorMsg}`) - }) + log.error(`Script error: ${errorMsg}`); + }); nodeProcess.on('close', (code) => { if (code === 0) { - log.info('Script completed successfully') - resolve(true) + log.info('Script completed successfully'); + resolve(true); } else { - log.error(`Script exited with code ${code}`) - const errorMessage = stderrOutput.trim() || `Script exited with code ${code}`; - reject(new Error(errorMessage)) + log.error(`Script exited with code ${code}`); + const errorMessage = + stderrOutput.trim() || `Script exited with code ${code}`; + reject(new Error(errorMessage)); } - }) - }) + }); + }); } export async function getBinaryName(name: string): Promise { if (process.platform === 'win32') { - return `${name}.exe` + return `${name}.exe`; } - return name + return name; } -export async function getBinaryPath(name?: string): Promise { - const binariesDir = path.join(os.homedir(), '.eigent', 'bin') +/** + * Get path to prebuilt binary (if available in packaged app) + */ +export function getPrebuiltBinaryPath(name?: string): string | null { + if (!app.isPackaged) { + return null; + } - // Ensure .eigent/bin directory exists - if (!fs.existsSync(binariesDir)) { - fs.mkdirSync(binariesDir, { recursive: true }) + const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin'); + if (!fs.existsSync(prebuiltBinDir)) { + return null; } if (!name) { - return binariesDir + return prebuiltBinDir; } - const binaryName = await getBinaryName(name) - return path.join(binariesDir, binaryName) + const binaryName = process.platform === 'win32' ? `${name}.exe` : name; + const binaryPath = path.join(prebuiltBinDir, binaryName); + return fs.existsSync(binaryPath) ? binaryPath : null; +} + +export async function getBinaryPath(name?: string): Promise { + // First check for prebuilt binary in packaged app + if (app.isPackaged) { + const prebuiltPath = getPrebuiltBinaryPath(name); + if (prebuiltPath) { + log.info(`Using prebuilt binary: ${prebuiltPath}`); + return prebuiltPath; + } + } + + const binariesDir = path.join(os.homedir(), '.eigent', 'bin'); + + // Ensure .eigent/bin directory exists + if (!fs.existsSync(binariesDir)) { + fs.mkdirSync(binariesDir, { recursive: true }); + } + + if (!name) { + return binariesDir; + } + + const binaryName = await getBinaryName(name); + return path.join(binariesDir, binaryName); } export function getCachePath(folder: string): string { - const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder) + // For packaged app, try to use prebuilt cache first + if (app.isPackaged) { + const prebuiltCachePath = path.join( + process.resourcesPath, + 'prebuilt', + 'cache', + folder + ); + if (fs.existsSync(prebuiltCachePath)) { + log.info(`Using prebuilt cache: ${prebuiltCachePath}`); + return prebuiltCachePath; + } + } + + const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder); // Ensure cache directory exists if (!fs.existsSync(cacheDir)) { - fs.mkdirSync(cacheDir, { recursive: true }) + fs.mkdirSync(cacheDir, { recursive: true }); } - return cacheDir + return cacheDir; +} + +/** + * Get path to prebuilt venv (if available in packaged app) + */ +export function getPrebuiltVenvPath(): string | null { + if (!app.isPackaged) { + return null; + } + + const prebuiltVenvPath = path.join(process.resourcesPath, 'prebuilt', 'venv'); + if (fs.existsSync(prebuiltVenvPath)) { + const pyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg'); + if (fs.existsSync(pyvenvCfg)) { + log.info(`Using prebuilt venv: ${prebuiltVenvPath}`); + return prebuiltVenvPath; + } + } + return null; } export function getVenvPath(version: string): string { - const venvDir = path.join(os.homedir(), '.eigent', 'venvs', `backend-${version}`) - - // Ensure venvs directory exists (parent of the actual venv) - const venvsBaseDir = path.dirname(venvDir) - if (!fs.existsSync(venvsBaseDir)) { - fs.mkdirSync(venvsBaseDir, { recursive: true }) + // First check for prebuilt venv in packaged app + if (app.isPackaged) { + const prebuiltVenv = getPrebuiltVenvPath(); + if (prebuiltVenv) { + return prebuiltVenv; + } } - return venvDir + const venvDir = path.join( + os.homedir(), + '.eigent', + 'venvs', + `backend-${version}` + ); + + // Ensure venvs directory exists (parent of the actual venv) + const venvsBaseDir = path.dirname(venvDir); + if (!fs.existsSync(venvsBaseDir)) { + fs.mkdirSync(venvsBaseDir, { recursive: true }); + } + + return venvDir; } export function getVenvsBaseDir(): string { - return path.join(os.homedir(), '.eigent', 'venvs') + return path.join(os.homedir(), '.eigent', 'venvs'); } export async function cleanupOldVenvs(currentVersion: string): Promise { - const venvsBaseDir = getVenvsBaseDir() + const venvsBaseDir = getVenvsBaseDir(); // Check if venvs directory exists if (!fs.existsSync(venvsBaseDir)) { - return + return; } try { - const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true }) + const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && entry.name.startsWith('backend-')) { - const versionMatch = entry.name.match(/^backend-(.+)$/) + const versionMatch = entry.name.match(/^backend-(.+)$/); if (versionMatch && versionMatch[1] !== currentVersion) { - const oldVenvPath = path.join(venvsBaseDir, entry.name) - console.log(`Cleaning up old venv: ${oldVenvPath}`) + const oldVenvPath = path.join(venvsBaseDir, entry.name); + console.log(`Cleaning up old venv: ${oldVenvPath}`); try { // Remove old venv directory recursively - fs.rmSync(oldVenvPath, { recursive: true, force: true }) - console.log(`Successfully removed old venv: ${entry.name}`) + fs.rmSync(oldVenvPath, { recursive: true, force: true }); + console.log(`Successfully removed old venv: ${entry.name}`); } catch (err) { - console.error(`Failed to remove old venv ${entry.name}:`, err) + console.error(`Failed to remove old venv ${entry.name}:`, err); } } } } } catch (err) { - console.error('Error during venv cleanup:', err) + console.error('Error during venv cleanup:', err); } } export async function isBinaryExists(name: string): Promise { - const cmd = await getBinaryPath(name) + const cmd = await getBinaryPath(name); - return await fs.existsSync(cmd) + return await fs.existsSync(cmd); } /** @@ -155,36 +236,36 @@ export function getUvEnv(version: string): Record { UV_TOOL_DIR: getCachePath('uv_tool'), UV_PROJECT_ENVIRONMENT: getVenvPath(version), UV_HTTP_TIMEOUT: '300', - } + }; } export async function killProcessByName(name: string): Promise { - const platform = process.platform + const platform = process.platform; try { if (platform === 'win32') { await new Promise((resolve, reject) => { // /F = force, /IM = image name - const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`]) + const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`]); cmd.on('close', (code) => { // code 0 = success, code 128 = process not found (which is fine) - if (code === 0 || code === 128) resolve() - else reject(new Error(`taskkill exited with code ${code}`)) - }) - cmd.on('error', reject) - }) + if (code === 0 || code === 128) resolve(); + else reject(new Error(`taskkill exited with code ${code}`)); + }); + cmd.on('error', reject); + }); } else { await new Promise((resolve, reject) => { - const cmd = spawn('pkill', ['-9', name]) + const cmd = spawn('pkill', ['-9', name]); cmd.on('close', (code) => { // code 0 = success, code 1 = no process found (which is fine) - if (code === 0 || code === 1) resolve() - else reject(new Error(`pkill exited with code ${code}`)) - }) - cmd.on('error', reject) - }) + if (code === 0 || code === 1) resolve(); + else reject(new Error(`pkill exited with code ${code}`)); + }); + cmd.on('error', reject); + }); } } catch (err) { // Ignore errors, just best effort - log.warn(`Failed to kill process ${name}:`, err) + log.warn(`Failed to kill process ${name}:`, err); } } diff --git a/package.json b/package.json index c218962e..de9de1a4 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,11 @@ "compile-babel": "cd backend && uv run pybabel compile -d lang", "clean-cache": "rimraf node_modules/.vite", "dev": "npm run clean-cache && vite", - "build": "npm run compile-babel && tsc && vite build && electron-builder -- --publish always", - "build:mac": "npm run compile-babel && tsc && vite build && electron-builder --mac", - "build:win": "npm run compile-babel && tsc && vite build && electron-builder --win", - "build:all": "npm run compile-babel && tsc && vite build && electron-builder --mac --win", + "preinstall-deps": "node scripts/preinstall-deps.js", + "build": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder -- --publish always", + "build:mac": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac", + "build:win": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --win", + "build:all": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac --win", "preview": "vite preview", "pretest": "vite build --mode=test", "test": "vitest run", diff --git a/resources/scripts/download.js b/resources/scripts/download.js index 4b760343..5245ea21 100644 --- a/resources/scripts/download.js +++ b/resources/scripts/download.js @@ -35,23 +35,52 @@ export async function downloadWithRedirects(url, destinationPath) { }; const request = (url) => { - https + // Support both http and https + const httpModule = url.startsWith('https://') ? https : require('http'); + + httpModule .get(url, (response) => { - if (response.statusCode == 301 || response.statusCode == 302) { - request(response.headers.location) - return + const statusCode = response.statusCode || 0; + + // Handle redirects (301, 302, 307, 308) + if (statusCode >= 301 && statusCode <= 308 && response.headers.location) { + const redirectUrl = response.headers.location; + console.log(`Following redirect to: ${redirectUrl}`); + request(redirectUrl); + return; } - if (response.statusCode !== 200) { - safeReject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) + if (statusCode !== 200) { + safeReject(new Error(`Download failed: ${statusCode} ${response.statusMessage || 'Unknown error'}`)) return } const file = fs.createWriteStream(destinationPath) let downloadedBytes = 0 const expectedBytes = parseInt(response.headers['content-length'] || '0') + const startTime = Date.now() + let lastProgressTime = Date.now() + + if (expectedBytes > 0) { + console.log(`Downloading ${(expectedBytes / 1024 / 1024).toFixed(2)} MB...`) + } else { + console.log('Downloading...') + } response.on('data', (chunk) => { downloadedBytes += chunk.length + + // Show progress every 1 second + const now = Date.now() + if (now - lastProgressTime >= 1000) { + if (expectedBytes > 0) { + const percent = ((downloadedBytes / expectedBytes) * 100).toFixed(1) + const speed = downloadedBytes / ((now - startTime) / 1000) / 1024 / 1024 + console.log(`Progress: ${percent}% (${(downloadedBytes / 1024 / 1024).toFixed(2)} MB) - ${speed.toFixed(2)} MB/s`) + } else { + console.log(`Downloaded: ${(downloadedBytes / 1024 / 1024).toFixed(2)} MB`) + } + lastProgressTime = now + } }) response.pipe(file) diff --git a/scripts/preinstall-deps.js b/scripts/preinstall-deps.js new file mode 100644 index 00000000..7d21ca50 --- /dev/null +++ b/scripts/preinstall-deps.js @@ -0,0 +1,683 @@ +#!/usr/bin/env node +/** + * Pre-install dependencies script + * This script installs all necessary dependencies before packaging the app + * so users don't have to wait for installation on first run + */ + +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { execSync } from 'child_process'; +import { fileURLToPath } from 'url'; +import https from 'https'; +import http from 'http'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, '..'); + +const BIN_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'bin'); +const VENV_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'venv'); +const BACKEND_DIR = path.join(projectRoot, 'backend'); + +console.log('🚀 Starting pre-installation of dependencies...'); +console.log(`📦 Binaries will be installed to: ${BIN_DIR}`); +console.log(`🐍 Python venv will be installed to: ${VENV_DIR}`); + +// Ensure directories exist +fs.mkdirSync(BIN_DIR, { recursive: true }); +fs.mkdirSync(VENV_DIR, { recursive: true }); + +/** + * 检测是否配置了代理 + */ +function hasProxy() { + const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy; + const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; + return !!(httpProxy || httpsProxy); +} + +const PROXY_DETECTED = hasProxy(); +if (PROXY_DETECTED) { + console.log('🔍 Proxy detected, will use GitHub official sources for better compatibility'); + console.log(` HTTP_PROXY: ${process.env.HTTP_PROXY || process.env.http_proxy || 'not set'}`); + console.log(` HTTPS_PROXY: ${process.env.HTTPS_PROXY || process.env.https_proxy || 'not set'}`); +} + +/** + * 验证下载的文件是否是有效的 ZIP 文件 + */ +function isValidZip(filePath) { + try { + const buffer = fs.readFileSync(filePath); + return buffer.length > 4 && + buffer[0] === 0x50 && + buffer[1] === 0x4B && + buffer[2] === 0x03 && + buffer[3] === 0x04; + } catch { + return false; + } +} + +/** + * 验证下载的文件是否是有效的 tar.gz 文件 + */ +function isValidTarGz(filePath) { + try { + const buffer = fs.readFileSync(filePath); + return buffer.length > 2 && + buffer[0] === 0x1F && + buffer[1] === 0x8B; + } catch { + return false; + } +} + +/** + * 下载文件并验证完整性 + */ +async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType = 'file') { + const maxRetries = 2; + + for (const { url, name } of urlsToTry) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + console.log(` Trying ${name} (attempt ${attempt + 1}/${maxRetries})`); + console.log(` URL: ${url}`); + + await new Promise((resolve, reject) => { + const protocol = url.startsWith('https') ? https : http; + const timeout = 180000; // 3 minutes + + const request = protocol.get(url, { + timeout: timeout, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' + } + }, (response) => { + // 处理重定向 + if (response.statusCode === 301 || response.statusCode === 302) { + const redirectUrl = response.headers.location; + console.log(` Following redirect...`); + + const redirectProtocol = redirectUrl.startsWith('https') ? https : http; + const redirectRequest = redirectProtocol.get(redirectUrl, { + timeout: timeout, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' + } + }, (redirectResponse) => { + if (redirectResponse.statusCode === 200) { + const file = fs.createWriteStream(dest); + let downloadedSize = 0; + const totalSize = parseInt(redirectResponse.headers['content-length'] || '0'); + + redirectResponse.on('data', (chunk) => { + downloadedSize += chunk.length; + if (totalSize > 0) { + const progress = ((downloadedSize / totalSize) * 100).toFixed(1); + process.stdout.write(`\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)`); + } + }); + + redirectResponse.pipe(file); + file.on('finish', () => { + file.close(); + console.log(''); // 新行 + resolve(); + }); + file.on('error', (err) => { + file.close(); + if (fs.existsSync(dest)) fs.unlinkSync(dest); + reject(err); + }); + } else { + reject(new Error(`HTTP ${redirectResponse.statusCode}`)); + } + }); + + redirectRequest.on('error', reject); + redirectRequest.on('timeout', () => { + redirectRequest.destroy(); + reject(new Error('Request timeout')); + }); + + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`HTTP ${response.statusCode}`)); + return; + } + + const file = fs.createWriteStream(dest); + let downloadedSize = 0; + const totalSize = parseInt(response.headers['content-length'] || '0'); + + response.on('data', (chunk) => { + downloadedSize += chunk.length; + if (totalSize > 0) { + const progress = ((downloadedSize / totalSize) * 100).toFixed(1); + process.stdout.write(`\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)`); + } + }); + + response.pipe(file); + file.on('finish', () => { + file.close(); + console.log(''); // 新行 + resolve(); + }); + file.on('error', (err) => { + file.close(); + if (fs.existsSync(dest)) fs.unlinkSync(dest); + reject(err); + }); + }); + + request.on('error', (err) => { + if (fs.existsSync(dest)) fs.unlinkSync(dest); + reject(err); + }); + + request.on('timeout', () => { + request.destroy(); + if (fs.existsSync(dest)) fs.unlinkSync(dest); + reject(new Error('Request timeout')); + }); + }); + + // 验证下载的文件 + if (!fs.existsSync(dest)) { + throw new Error('Downloaded file does not exist'); + } + + const fileSize = fs.statSync(dest).size; + console.log(` Downloaded file size: ${(fileSize / 1024 / 1024).toFixed(2)}MB`); + + if (fileSize < 1024) { + const content = fs.readFileSync(dest, 'utf-8'); + console.log(` ⚠️ File too small, content: ${content.substring(0, 200)}`); + throw new Error('Downloaded file is too small (likely an error page)'); + } + + if (!validateFn(dest)) { + throw new Error(`Downloaded file is not a valid ${fileType}`); + } + + console.log(` ✅ Successfully downloaded and validated from ${name}`); + return true; + + } catch (error) { + console.log(`\n ⚠️ Failed: ${error.message}`); + if (fs.existsSync(dest)) { + try { + fs.unlinkSync(dest); + } catch (e) { + // Ignore + } + } + } + } + } + + throw new Error(`Failed to download ${fileType} from all mirrors`); +} + +/** + * 获取 Bun 的下载 URL 列表 + * 如果检测到代理,优先使用 GitHub 官方;否则使用中国镜像 + */ +function getBunUrls(platform, arch) { + const filename = `bun-${platform}-${arch}.zip`; + const urls = []; + + if (PROXY_DETECTED) { + // 有代理时,直接使用 GitHub 官方(代理通常能访问 GitHub) + urls.push({ + url: `https://github.com/oven-sh/bun/releases/latest/download/${filename}`, + name: 'GitHub (官方 via proxy)' + }); + + // 备选:镜像(可能代理访问镜像反而慢) + urls.push({ + url: `https://mirror.ghproxy.com/https://github.com/oven-sh/bun/releases/latest/download/${filename}`, + name: 'ghproxy (备选)' + }); + } else { + // 无代理时,使用中国镜像 + urls.push({ + url: `https://mirror.ghproxy.com/https://github.com/oven-sh/bun/releases/latest/download/${filename}`, + name: 'ghproxy.net (GitHub镜像)' + }); + + urls.push({ + url: `https://gh-proxy.com/https://github.com/oven-sh/bun/releases/latest/download/${filename}`, + name: 'gh-proxy.com' + }); + + urls.push({ + url: `https://github.moeyy.xyz/https://github.com/oven-sh/bun/releases/latest/download/${filename}`, + name: 'moeyy.xyz (CDN)' + }); + + urls.push({ + url: `https://github.com/oven-sh/bun/releases/latest/download/${filename}`, + name: 'GitHub (官方)' + }); + } + + return urls; +} + +/** + * 获取 UV 的下载 URL 列表 + */ +function getUvUrls(archStr, platformStr) { + const filename = `uv-${archStr}-${platformStr}.tar.gz`; + const urls = []; + + if (PROXY_DETECTED) { + // 有代理时,优先使用 GitHub 官方 + urls.push({ + url: `https://github.com/astral-sh/uv/releases/latest/download/${filename}`, + name: 'GitHub (官方 via proxy)' + }); + + urls.push({ + url: `https://mirror.ghproxy.com/https://github.com/astral-sh/uv/releases/latest/download/${filename}`, + name: 'ghproxy (备选)' + }); + } else { + // 无代理时,使用中国镜像 + urls.push({ + url: `https://mirror.ghproxy.com/https://github.com/astral-sh/uv/releases/latest/download/${filename}`, + name: 'ghproxy.net (GitHub镜像)' + }); + + urls.push({ + url: `https://gh-proxy.com/https://github.com/astral-sh/uv/releases/latest/download/${filename}`, + name: 'gh-proxy.com' + }); + + urls.push({ + url: `https://github.com/astral-sh/uv/releases/latest/download/${filename}`, + name: 'GitHub (官方)' + }); + } + + return urls; +} + +/** + * Install uv binary + */ +async function installUv() { + console.log('\n📥 Installing uv...'); + const uvPath = path.join(BIN_DIR, process.platform === 'win32' ? 'uv.exe' : 'uv'); + + if (fs.existsSync(uvPath)) { + console.log('✅ uv already installed'); + return uvPath; + } + + // Check manual path + const manualUvPath = process.env.MANUAL_UV_PATH; + if (manualUvPath && fs.existsSync(manualUvPath)) { + console.log(`📋 Using manually provided uv binary: ${manualUvPath}`); + fs.copyFileSync(manualUvPath, uvPath); + if (process.platform !== 'win32') { + fs.chmodSync(uvPath, '755'); + } + return uvPath; + } + + // Try pip first + const usePipEnv = process.env.USE_PIP_INSTALL_UV; + const shouldTryPip = usePipEnv !== 'false'; + + if (shouldTryPip) { + console.log('\n🐍 Trying to install uv via pip (fastest for China)...'); + + try { + const pypiMirror = 'https://pypi.tuna.tsinghua.edu.cn/simple'; + let pipCommand = null; + + try { + execSync('pip3 --version', { stdio: 'ignore' }); + pipCommand = 'pip3'; + } catch { + try { + execSync('pip --version', { stdio: 'ignore' }); + pipCommand = 'pip'; + } catch { + throw new Error('pip not found'); + } + } + + const isMacOS = process.platform === 'darwin'; + const pipArgs = isMacOS + ? `install --user --break-system-packages uv -i ${pypiMirror}` + : `install --user uv -i ${pypiMirror}`; + + console.log(` Installing via ${pipCommand}...`); + execSync(`${pipCommand} ${pipArgs}`, { stdio: 'inherit' }); + + // Find installed uv + const possiblePaths = [ + path.join(os.homedir(), '.local', 'bin', 'uv'), + path.join(os.homedir(), 'Library', 'Python', '3.11', 'bin', 'uv'), + path.join(os.homedir(), 'Library', 'Python', '3.12', 'bin', 'uv'), + path.join(os.homedir(), 'Library', 'Python', '3.13', 'bin', 'uv'), + '/usr/local/bin/uv', + ]; + + let foundUvPath = null; + try { + foundUvPath = execSync('which uv', { encoding: 'utf-8' }).trim(); + } catch { + for (const p of possiblePaths) { + if (fs.existsSync(p)) { + foundUvPath = p; + break; + } + } + } + + if (foundUvPath && fs.existsSync(foundUvPath)) { + fs.copyFileSync(foundUvPath, uvPath); + if (process.platform !== 'win32') { + fs.chmodSync(uvPath, '755'); + } + console.log('✅ uv installed via pip'); + return uvPath; + } + } catch (error) { + console.log(` ⚠️ pip install failed: ${error.message}`); + } + } + + // Download from mirrors + console.log('\n📥 Downloading uv...'); + + const platform = process.platform; + const arch = process.arch; + let platformStr, archStr; + + archStr = arch === 'x64' ? 'x86_64' : arch === 'arm64' ? 'aarch64' : arch; + + if (platform === 'darwin') { + platformStr = 'apple-darwin'; + } else if (platform === 'linux') { + platformStr = 'unknown-linux-gnu'; + } else if (platform === 'win32') { + platformStr = 'pc-windows-msvc'; + archStr = 'x86_64'; + } else { + throw new Error(`Unsupported platform: ${platform}`); + } + + const tempFilename = path.join(BIN_DIR, `uv-download-${Date.now()}.tar.gz`); + + console.log(` Platform: ${platform}-${arch}`); + + const urlsToTry = getUvUrls(archStr, platformStr); + await downloadFileWithValidation(urlsToTry, tempFilename, isValidTarGz, 'tar.gz'); + + // Extract + console.log(' Extracting...'); + const tar = await import('tar'); + await tar.extract({ file: tempFilename, cwd: BIN_DIR }); + + const extractedUvPath = path.join(BIN_DIR, 'uv'); + if (fs.existsSync(extractedUvPath)) { + fs.renameSync(extractedUvPath, uvPath); + if (process.platform !== 'win32') { + fs.chmodSync(uvPath, '755'); + } + } + + fs.unlinkSync(tempFilename); + console.log('✅ uv installed successfully'); + return uvPath; +} + +/** + * Install bun binary + */ +async function installBun() { + console.log('\n📥 Installing bun...'); + const platform = process.platform; + const arch = process.arch; + const bunPath = path.join(BIN_DIR, platform === 'win32' ? 'bun.exe' : 'bun'); + + if (fs.existsSync(bunPath)) { + console.log('✅ bun already installed'); + return bunPath; + } + + // Check manual path + const manualBunPath = process.env.MANUAL_BUN_PATH; + if (manualBunPath && fs.existsSync(manualBunPath)) { + console.log(`📋 Using manually provided bun binary: ${manualBunPath}`); + fs.copyFileSync(manualBunPath, bunPath); + if (platform !== 'win32') { + fs.chmodSync(bunPath, '755'); + } + return bunPath; + } + + // Determine platform and architecture + let bunPlatform, bunArch; + + if (platform === 'darwin') { + bunPlatform = 'darwin'; + bunArch = arch === 'arm64' ? 'aarch64' : 'x64'; + } else if (platform === 'linux') { + bunPlatform = 'linux'; + bunArch = arch === 'arm64' ? 'aarch64' : 'x64'; + } else if (platform === 'win32') { + bunPlatform = 'windows'; + bunArch = 'x64'; + } else { + throw new Error(`Unsupported platform: ${platform}`); + } + + const tempFilename = path.join(BIN_DIR, `bun-download-${Date.now()}.zip`); + + console.log(` Platform: ${bunPlatform}-${bunArch}`); + + const urlsToTry = getBunUrls(bunPlatform, bunArch); + await downloadFileWithValidation(urlsToTry, tempFilename, isValidZip, 'ZIP'); + + // Extract + console.log(' Extracting...'); + + try { + const AdmZip = (await import('adm-zip')).default; + const zip = new AdmZip(tempFilename); + const entries = zip.getEntries(); + + for (const entry of entries) { + const name = entry.entryName; + if (name === 'bun' || name === 'bun.exe' || name.endsWith('/bun') || name.endsWith('/bun.exe')) { + zip.extractEntryTo(entry, BIN_DIR, false, true); + break; + } + } + } catch (admZipError) { + console.log(' Using system unzip...'); + if (platform === 'win32') { + execSync(`powershell -command "Expand-Archive -Path '${tempFilename}' -DestinationPath '${BIN_DIR}' -Force"`, { stdio: 'inherit' }); + } else { + execSync(`unzip -o "${tempFilename}" -d "${BIN_DIR}"`, { stdio: 'inherit' }); + } + } + + if (platform !== 'win32' && fs.existsSync(bunPath)) { + fs.chmodSync(bunPath, '755'); + } + + fs.unlinkSync(tempFilename); + + if (fs.existsSync(bunPath)) { + console.log('✅ bun installed successfully'); + return bunPath; + } else { + throw new Error('bun binary not found after extraction'); + } +} + +/** + * Install Python dependencies + */ +async function installPythonDeps(uvPath) { + console.log('\n🐍 Installing Python dependencies...'); + + const venvPath = VENV_DIR; + const cacheDir = path.join(projectRoot, 'resources', 'prebuilt', 'cache', 'uv_cache'); + const pythonCacheDir = path.join(projectRoot, 'resources', 'prebuilt', 'cache', 'uv_python'); + const toolCacheDir = path.join(projectRoot, 'resources', 'prebuilt', 'cache', 'uv_tool'); + + fs.mkdirSync(cacheDir, { recursive: true }); + fs.mkdirSync(pythonCacheDir, { recursive: true }); + fs.mkdirSync(toolCacheDir, { recursive: true }); + + const env = { + ...process.env, + UV_PYTHON_INSTALL_DIR: pythonCacheDir, + UV_TOOL_DIR: toolCacheDir, + UV_PROJECT_ENVIRONMENT: venvPath, + UV_HTTP_TIMEOUT: '300', + }; + + const pyvenvCfg = path.join(venvPath, 'pyvenv.cfg'); + if (fs.existsSync(pyvenvCfg)) { + console.log('✅ Python venv exists, syncing...'); + } else { + console.log('📦 Creating Python venv...'); + } + + const usePypiMirrorEnv = process.env.USE_PYPI_MIRROR; + const shouldUseMirror = usePypiMirrorEnv !== 'false'; + + const proxyArgs = shouldUseMirror + ? ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple/'] + : []; + + if (shouldUseMirror) { + console.log(' Using PyPI mirror: https://pypi.tuna.tsinghua.edu.cn/simple/'); + } + + execSync( + `"${uvPath}" sync --no-dev --cache-dir "${cacheDir}" ${proxyArgs.join(' ')}`, + { cwd: BACKEND_DIR, env: env, stdio: 'inherit' } + ); + + console.log('✅ Python dependencies installed'); + + console.log('📝 Compiling babel...'); + execSync(`"${uvPath}" run pybabel compile -d lang`, { + cwd: BACKEND_DIR, + env: env, + stdio: 'inherit' + }); + + console.log('✅ Babel compiled'); +} + +/** + * Install browser toolkit deps + */ +async function installBrowserToolkitDeps(uvPath, venvPath) { + console.log('\n🌐 Installing browser toolkit...'); + + try { + const libPath = path.join(venvPath, 'lib'); + if (!fs.existsSync(libPath)) { + console.log('⚠️ Skipping browser toolkit'); + return; + } + + const pythonDir = fs.readdirSync(libPath).find(n => n.startsWith('python')); + if (!pythonDir) { + console.log('⚠️ Skipping browser toolkit'); + return; + } + + const toolkitPath = path.join(libPath, pythonDir, 'site-packages', 'camel', 'toolkits', 'hybrid_browser_toolkit', 'ts'); + if (!fs.existsSync(toolkitPath)) { + console.log('⚠️ Toolkit not found'); + return; + } + + const nodeModulesPath = path.join(toolkitPath, 'node_modules'); + const distPath = path.join(toolkitPath, 'dist'); + if (fs.existsSync(nodeModulesPath) && fs.existsSync(distPath)) { + console.log('✅ Browser toolkit already installed'); + return; + } + + const npmCacheDir = path.join(venvPath, '.npm-cache'); + fs.mkdirSync(npmCacheDir, { recursive: true }); + + const env = { + ...process.env, + UV_PROJECT_ENVIRONMENT: venvPath, + npm_config_cache: npmCacheDir, + }; + + let npmCommand = 'npm'; + try { + execSync('npm --version', { stdio: 'ignore' }); + } catch { + npmCommand = `"${uvPath}" run npm`; + } + + console.log('📦 Installing npm deps...'); + execSync(`${npmCommand} install`, { cwd: toolkitPath, env: env, stdio: 'inherit' }); + + console.log('🔨 Building TS...'); + execSync(`${npmCommand} run build`, { cwd: toolkitPath, env: env, stdio: 'inherit' }); + + console.log('🎭 Installing Playwright...'); + try { + const npxCommand = npmCommand === 'npm' ? 'npx' : `"${uvPath}" run npx`; + execSync(`${npxCommand} playwright install`, { + cwd: toolkitPath, + env: env, + stdio: 'inherit', + timeout: 600000 + }); + } catch (e) { + console.log('⚠️ Playwright install failed (non-critical)'); + } + + console.log('✅ Browser toolkit installed'); + } catch (error) { + console.error('❌ Browser toolkit failed:', error.message); + } +} + +/** + * Main + */ +async function main() { + try { + const uvPath = await installUv(); + await installBun(); + await installPythonDeps(uvPath); + await installBrowserToolkitDeps(uvPath, VENV_DIR); + + console.log('\n✅ All dependencies installed!'); + console.log(`📦 Binaries: ${BIN_DIR}`); + console.log(`🐍 Python venv: ${VENV_DIR}`); + } catch (error) { + console.error('\n❌ Failed:', error); + process.exit(1); + } +} + +main();