fix browser use

This commit is contained in:
Simos Mikelatos
2026-06-17 15:35:55 +00:00
parent 6af4afe6f2
commit a0d56429a7
8 changed files with 100 additions and 25 deletions

26
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<string, unknown>) {
if (!apiToken) {
@@ -48,6 +49,7 @@ async function callBrowserUseApi(toolName: string, input: Record<string, unknown
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
signal: AbortSignal.timeout(API_TIMEOUT_MS),
});
const data = await response.json() as { success?: boolean; data?: unknown; error?: string };
if (!response.ok || data.success === false) {
@@ -378,7 +380,13 @@ process.stdin.on('data', (chunk) => {
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);

View File

@@ -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 <port> Set server port (default: 3001)

View File

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

View File

@@ -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) => {

View File

@@ -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({

View File

@@ -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<void> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
@@ -229,18 +234,36 @@ function runCommand(command: string, args: string[]): Promise<void> {
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<void> {
await assertPublicHttpTarget(parsed);
}
async function attachRequestGuard(page: any): Promise<void> {
await page.route('**/*', async (route: any) => {
async function attachRequestGuard(context: any): Promise<void> {
// 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);
}