From a0d56429a7624b453a94b8b9450644646737a9ff Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Wed, 17 Jun 2026 15:35:55 +0000 Subject: [PATCH] fix browser use --- package-lock.json | 26 ++++++++++++ package.json | 3 +- server/browser-use-mcp.ts | 10 ++++- server/cli.js | 13 +++--- server/index.js | 12 +++++- .../browser-use/browser-use-mcp.routes.ts | 4 +- .../modules/browser-use/browser-use.routes.ts | 16 +++++--- .../browser-use/browser-use.service.ts | 41 +++++++++++++++---- 8 files changed, 100 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3faa74aa..28bdd8a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "auto-changelog": "^2.5.0", "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", + "cross-env": "^10.1.0", "electron": "^38.0.0", "electron-builder": "^26.15.3", "eslint": "^9.39.3", @@ -1748,6 +1749,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -8694,6 +8702,24 @@ "optional": true, "peer": true }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/package.json b/package.json index ae75f9c6..f378f79b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "server:dev-watch": "tsx watch --tsconfig server/tsconfig.json server/index.js", "client": "vite", "desktop": "electron electron/main.js", - "desktop:dev": "ELECTRON_DEV_URL=http://127.0.0.1:5173 electron electron/main.js", + "desktop:dev": "cross-env ELECTRON_DEV_URL=http://127.0.0.1:5173 electron electron/main.js", "desktop:pack": "npm run build && electron-builder --dir", "desktop:dist:mac": "npm run build && electron-builder --mac dmg zip", "build": "npm run build:client && npm run build:server", @@ -194,6 +194,7 @@ "auto-changelog": "^2.5.0", "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", + "cross-env": "^10.1.0", "electron": "^38.0.0", "electron-builder": "^26.15.3", "eslint": "^9.39.3", diff --git a/server/browser-use-mcp.ts b/server/browser-use-mcp.ts index 22a4c3e4..55c448ad 100644 --- a/server/browser-use-mcp.ts +++ b/server/browser-use-mcp.ts @@ -35,6 +35,7 @@ const readNumber = (value: unknown): number | undefined => const apiUrl = (process.env.CLOUDCLI_BROWSER_USE_API_URL || 'http://127.0.0.1:3001/api/browser-use-mcp').replace(/\/$/, ''); const apiToken = process.env.CLOUDCLI_BROWSER_USE_MCP_TOKEN || ''; +const API_TIMEOUT_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_API_TIMEOUT_MS || '60000', 10); async function callBrowserUseApi(toolName: string, input: Record) { if (!apiToken) { @@ -48,6 +49,7 @@ async function callBrowserUseApi(toolName: string, input: Record { buffer = buffer.slice(messageEnd); void (async () => { - const request = JSON.parse(rawMessage) as JsonRpcRequest; + let request: JsonRpcRequest; + try { + request = JSON.parse(rawMessage) as JsonRpcRequest; + } catch (error) { + sendError(null, error); + return; + } try { const result = await handleMessage(request); sendResult(request.id, result); diff --git a/server/cli.js b/server/cli.js index e6daacc4..83c04411 100755 --- a/server/cli.js +++ b/server/cli.js @@ -155,12 +155,13 @@ Usage: cloudcli [command] [options] Commands: - start Start the CloudCLI server (default) - sandbox Manage Docker sandbox environments - status Show configuration and data locations - update Update to the latest version - help Show this help information - version Show version information + start Start the CloudCLI server (default) + sandbox Manage Docker sandbox environments + browser-use-mcp Run the Browser Use MCP stdio server + status Show configuration and data locations + update Update to the latest version + help Show this help information + version Show version information Options: -p, --port Set server port (default: 3001) diff --git a/server/index.js b/server/index.js index 3fcd438a..ce35c542 100755 --- a/server/index.js +++ b/server/index.js @@ -1714,8 +1714,16 @@ async function startServer() { await closeSessionsWatcher(); // Clean up plugin processes on shutdown const shutdownRuntimeServices = async () => { - await browserUseService.stopAllSessions(); - await stopAllPlugins(); + try { + await browserUseService.stopAllSessions(); + } catch (err) { + console.error('[Browser Use] Error stopping sessions during shutdown:', err?.message || err); + } + try { + await stopAllPlugins(); + } catch (err) { + console.error('[Plugins] Error stopping plugins during shutdown:', err?.message || err); + } process.exit(0); }; process.on('SIGTERM', () => void shutdownRuntimeServices()); diff --git a/server/modules/browser-use/browser-use-mcp.routes.ts b/server/modules/browser-use/browser-use-mcp.routes.ts index 335ffa18..2899fd74 100644 --- a/server/modules/browser-use/browser-use-mcp.routes.ts +++ b/server/modules/browser-use/browser-use-mcp.routes.ts @@ -8,8 +8,8 @@ function readBearerToken(header: unknown): string | null { if (typeof header !== 'string') { return null; } - const match = /^Bearer\s+(.+)$/i.exec(header.trim()); - return match?.[1] || null; + const match = /^Bearer\s+(\S.*)$/i.exec(header.trim()); + return match?.[1]?.trim() || null; } router.use((req, res, next) => { diff --git a/server/modules/browser-use/browser-use.routes.ts b/server/modules/browser-use/browser-use.routes.ts index 16f65d7e..167912cd 100644 --- a/server/modules/browser-use/browser-use.routes.ts +++ b/server/modules/browser-use/browser-use.routes.ts @@ -121,10 +121,12 @@ router.post('/sessions/:sessionId/navigate', async (req: AuthenticatedRequest, r router.post('/sessions/:sessionId/click', async (req: AuthenticatedRequest, res) => { try { - const session = await browserUseService.userClick(requireUser(req), readParam(req.params.sessionId), { - x: Number(req.body?.x), - y: Number(req.body?.y), - }); + const x = Number(req.body?.x); + const y = Number(req.body?.y); + if (!Number.isFinite(x) || !Number.isFinite(y)) { + throw new Error('Click requires numeric x and y coordinates.'); + } + const session = await browserUseService.userClick(requireUser(req), readParam(req.params.sessionId), { x, y }); res.json({ success: true, data: { session } }); } catch (error) { res.status(400).json({ @@ -136,7 +138,11 @@ router.post('/sessions/:sessionId/click', async (req: AuthenticatedRequest, res) router.post('/sessions/:sessionId/press-key', async (req: AuthenticatedRequest, res) => { try { - const session = await browserUseService.userPressKey(requireUser(req), readParam(req.params.sessionId), String(req.body?.key || '')); + const key = String(req.body?.key || '').trim(); + if (!key) { + throw new Error('A key is required.'); + } + const session = await browserUseService.userPressKey(requireUser(req), readParam(req.params.sessionId), key); res.json({ success: true, data: { session } }); } catch (error) { res.status(400).json({ diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 06fe255b..7906b49e 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -7,7 +7,7 @@ import os from 'node:os'; import net from 'node:net'; import path from 'node:path'; -import { appConfigDb } from '@/modules/database/repositories/app-config.js'; +import { appConfigDb } from '@/modules/database/index.js'; import { providerMcpService } from '@/modules/providers/services/mcp.service.js'; import { getModuleDir } from '@/utils/runtime-paths.js'; @@ -220,6 +220,11 @@ function getRuntimeReadiness(): RuntimeReadiness { return readiness; } +const INSTALL_COMMAND_TIMEOUT_MS = Number.parseInt( + process.env.CLOUDCLI_BROWSER_USE_INSTALL_TIMEOUT_MS || String(10 * 60 * 1000), + 10, +); + function runCommand(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { @@ -229,18 +234,36 @@ function runCommand(command: string, args: string[]): Promise { stdio: ['ignore', 'pipe', 'pipe'], }); const output: string[] = []; + let settled = false; + const finish = (fn: () => void) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + fn(); + }; + + // Guard against a stuck npm/playwright process hanging the install request forever. + const timer = setTimeout(() => { + child.kill('SIGKILL'); + finish(() => reject(new Error( + `${command} ${args.join(' ')} timed out after ${INSTALL_COMMAND_TIMEOUT_MS}ms.`, + ))); + }, INSTALL_COMMAND_TIMEOUT_MS); + timer.unref?.(); child.stdout.on('data', (chunk) => output.push(String(chunk))); child.stderr.on('data', (chunk) => output.push(String(chunk))); - child.on('error', reject); - child.on('close', (code) => { + child.on('error', (error) => finish(() => reject(error))); + child.on('close', (code) => finish(() => { if (code === 0) { resolve(); return; } reject(new Error(output.join('').trim() || `${command} ${args.join(' ')} exited with code ${code}`)); - }); + })); }); } @@ -386,8 +409,10 @@ async function assertAllowedBrowserRequest(rawUrl: string): Promise { await assertPublicHttpTarget(parsed); } -async function attachRequestGuard(page: any): Promise { - await page.route('**/*', async (route: any) => { +async function attachRequestGuard(context: any): Promise { + // Attach at the context level so the guard also covers popups, window.open targets, + // and any replacement pages created during the session lifecycle. + await context.route('**/*', async (route: any) => { try { await assertAllowedBrowserRequest(route.request().url()); await route.continue(); @@ -637,7 +662,7 @@ export const browserUseService = { context = await browser.newContext(contextOptions); page = await context.newPage(); } - await attachRequestGuard(page); + await attachRequestGuard(context); session.status = 'ready'; session.message = 'Browser session is ready.'; sessions.set(session.id, session); @@ -886,7 +911,7 @@ export const browserUseService = { if (action === 'new') { const page = await handle.context.newPage(); handles.set(sessionId, { ...handle, page }); - await attachRequestGuard(page); + // Request guard is attached at the context level, so new pages are already covered. if (input.url) { await this.agentNavigate(sessionId, input.url); }