Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 3 additions & 96 deletions backend/app/utils/toolkit/hybrid_browser_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,102 +78,9 @@ async def _receive_loop(self):
self.websocket = None

async def start(self):
# Check if node_modules exists (dependencies installed)
node_modules_path = os.path.join(self.ts_dir, "node_modules")
if not os.path.exists(node_modules_path):
logger.warning("Node modules not found. Running npm install...")
install_result = subprocess.run(
[uv(), "run", "npm", "install"],
cwd=self.ts_dir,
capture_output=True,
text=True,
)
if install_result.returncode != 0:
logger.error(f"npm install failed: {install_result.stderr}")
raise RuntimeError(
f"Failed to install npm dependencies: {install_result.stderr}\n" # noqa:E501
f"Please run 'npm install' in {self.ts_dir} manually."
)
logger.info("npm dependencies installed successfully")

# Ensure the TypeScript code is built
build_result = subprocess.run(
[uv(), "run", "npm", "run", "build"],
cwd=self.ts_dir,
capture_output=True,
text=True,
)
if build_result.returncode != 0:
logger.error(f"TypeScript build failed: {build_result.stderr}")
raise RuntimeError(f"TypeScript build failed: {build_result.stderr}")
else:
# Log warnings but don't fail on them
if build_result.stderr:
logger.warning(f"TypeScript build warnings: {build_result.stderr}")
logger.info("TypeScript build completed successfully")

# Start the WebSocket server
self.process = subprocess.Popen(
[uv(), "run", "node", "websocket-server.js"], # bun not support playwright, use uv nodejs-bin
cwd=self.ts_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)

# Wait for server to output the port
server_ready = False
timeout = 10 # 10 seconds timeout
start_time = time.time()

while not server_ready and time.time() - start_time < timeout:
if self.process.poll() is not None:
# Process died
stderr = self.process.stderr.read() # type: ignore
raise RuntimeError(f"WebSocket server failed to start: {stderr}")

try:
line = self.process.stdout.readline() # type: ignore
logger.debug(f"WebSocket server output: {line}")
if line.startswith("SERVER_READY:"):
self.server_port = int(line.split(":")[1].strip())
server_ready = True
logger.info(f"WebSocket server ready on port {self.server_port}")
except (ValueError, IndexError):
continue

if not server_ready:
self.process.kill()
raise RuntimeError("WebSocket server failed to start within timeout")

# Connect to the WebSocket server
try:
self.websocket = await websockets.connect(
f"ws://localhost:{self.server_port}",
ping_interval=30,
ping_timeout=10,
max_size=50 * 1024 * 1024, # 50MB limit to match server
)
logger.info("Connected to WebSocket server")
except Exception as e:
self.process.kill()
raise RuntimeError(f"Failed to connect to WebSocket server: {e}") from e

# Start the background receiver task - THIS WAS MISSING!
self._receive_task = asyncio.create_task(self._receive_loop())
logger.debug("Started WebSocket receiver task")

# Initialize the browser toolkit
logger.debug(f"send init {self.config}")
try:
await self._send_command("init", self.config)
logger.debug("WebSocket server initialized successfully")
except RuntimeError as e:
if "Timeout waiting for response to command: init" in str(e):
logger.warning("Init timeout - continuing anyway (CDP connection may be slow)")
# Continue without error - the WebSocket server is likely still initializing
else:
raise
# Simply use the parent implementation which uses system npm/node
logger.info("Starting WebSocket server using parent implementation (system npm/node)")
await super().start()

async def _send_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""Send a command to the WebSocket server with enhanced error handling."""
Expand Down
6 changes: 6 additions & 0 deletions electron/main/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,17 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
setPort(port);
}

const npmCacheDir = path.join(venvPath, '.npm-cache');
if (!fs.existsSync(npmCacheDir)) {
fs.mkdirSync(npmCacheDir, { recursive: true });
}

const env = {
...process.env,
SERVER_URL: "https://dev.eigent.ai/api",
PYTHONIOENCODING: 'utf-8',
UV_PROJECT_ENVIRONMENT: venvPath,
npm_config_cache: npmCacheDir,
}

//Redirect output
Expand Down
201 changes: 201 additions & 0 deletions electron/main/install-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,199 @@ export async function installDependencies(version: string): Promise<PromiseRetur
log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates');
}
return success;
},
installHybridBrowserDependencies: async (): Promise<boolean> => {
try {
// Find the hybrid_browser_toolkit ts directory in the virtual environment
// Need to determine the Python version to construct the correct path
let sitePackagesPath: string | null = null;
const libPath = path.join(venvPath, 'lib');

// Try to find the site-packages directory (it varies by Python version)
if (fs.existsSync(libPath)) {
const libContents = fs.readdirSync(libPath);
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');
return true; // Not an error if the venv structure is different
}

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');
return true; // Not an error if the toolkit isn't installed
}

log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit npm dependencies...');
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
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<boolean>(resolve => {
testNpm.on('close', (code) => resolve(code === 0));
testNpm.on('error', () => resolve(false));
});

if (npmExists) {
// Use system npm directly
npmCommand = ['npm'];
log.info('[DEPS INSTALL] Using system npm for installation');
} else {
// Try uv run npm (might not work if nodejs-wheel isn't properly set up)
npmCommand = [uv_path, 'run', 'npm'];
log.info('[DEPS INSTALL] Attempting to use uv run npm');
}

// Run npm install
const npmCacheDir = path.join(venvPath, '.npm-cache');
if (!fs.existsSync(npmCacheDir)) {
fs.mkdirSync(npmCacheDir, { recursive: true });
}

const npmInstall = spawn(npmCommand[0], [...npmCommand.slice(1), 'install'], {
cwd: toolkitPath,
env: {
...process.env,
UV_PROJECT_ENVIRONMENT: venvPath,
npm_config_cache: npmCacheDir,
},
shell: true // Important for Windows
});

await new Promise<void>((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() });
});
}

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() });
});
}

npmInstall.on('close', (code) => {
if (code === 0) {
log.info('[DEPS INSTALL] npm install completed successfully');
resolve();
} else {
log.error(`[DEPS INSTALL] npm install failed with code ${code}`);
reject(new Error(`npm install failed with code ${code}`));
}
});

npmInstall.on('error', (err) => {
log.error(`[DEPS INSTALL] npm install process error: ${err}`);
reject(err);
});
});

// Run npm build (use the same npm command as install)
log.info('[DEPS INSTALL] Building hybrid_browser_toolkit TypeScript...');
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: 'Building browser toolkit TypeScript...\n'
});

const buildArgs = npmCommand[0] === 'npm' ? ['run', 'build'] : [...npmCommand.slice(1), 'run', 'build'];
const npmBuild = spawn(npmCommand[0], buildArgs, {
cwd: toolkitPath,
env: {
...process.env,
UV_PROJECT_ENVIRONMENT: venvPath,
npm_config_cache: npmCacheDir,
},
shell: true // Important for Windows
});

await new Promise<void>((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() });
});
}

if (npmBuild.stderr) {
npmBuild.stderr.on('data', (data) => {
// 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() });
});
}

npmBuild.on('close', (code) => {
if (code === 0) {
log.info('[DEPS INSTALL] TypeScript build completed successfully');
resolve();
} else {
log.error(`[DEPS INSTALL] TypeScript build failed with code ${code}`);
reject(new Error(`TypeScript build failed with code ${code}`));
}
});

npmBuild.on('error', (err) => {
log.error(`[DEPS INSTALL] npm build process error: ${err}`);
reject(err);
});
});

// Optionally install Playwright browsers
try {
log.info('[DEPS INSTALL] Installing Playwright browsers...');
const npxCommand = npmCommand[0] === 'npm' ? ['npx'] : [uv_path, 'run', 'npx'];
const playwrightInstall = spawn(npxCommand[0], [...npxCommand.slice(1), 'playwright', 'install'], {
cwd: toolkitPath,
env: {
...process.env,
UV_PROJECT_ENVIRONMENT: venvPath,
},
shell: true
});

await new Promise<void>((resolve) => {
playwrightInstall.on('close', (code) => {
if (code === 0) {
log.info('[DEPS INSTALL] Playwright browsers installed successfully');
// Create marker file
const markerPath = path.join(toolkitPath, '.playwright_installed');
fs.writeFileSync(markerPath, 'installed');
} else {
log.warn('[DEPS INSTALL] Playwright installation failed, but continuing anyway');
}
resolve();
});

playwrightInstall.on('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);
// Non-critical, continue
}

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);
// Don't fail the entire installation if this fails
return false;
}
}
}

Expand All @@ -352,6 +545,10 @@ export async function installDependencies(version: string): Promise<PromiseRetur
// try default install
const installSuccess = await runInstall([], version)
if (installSuccess.success) {
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...')
await handleInstallOperations.installHybridBrowserDependencies()

handleInstallOperations.spawnBabel()

// Clean up old venvs after successful installation
Expand All @@ -369,6 +566,10 @@ export async function installDependencies(version: string): Promise<PromiseRetur
mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs, version) :await runInstall([], version)

if (mirrorInstallSuccess.success) {
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...')
await handleInstallOperations.installHybridBrowserDependencies()

handleInstallOperations.spawnBabel("mirror")

// Clean up old venvs after successful installation
Expand Down