mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-18 22:57:31 +08:00
Merge branch 'pr889-fixes' into electron-app
# Conflicts: # server/index.js
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1755,9 +1755,21 @@ async function startServer() {
|
||||
await closeSessionsWatcher();
|
||||
// Clean up plugin processes on shutdown
|
||||
const shutdownRuntimeServices = async () => {
|
||||
await browserUseService.stopAllSessions();
|
||||
await stopAllPlugins();
|
||||
await removeLocalServerMarker();
|
||||
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);
|
||||
}
|
||||
try {
|
||||
await removeLocalServerMarker();
|
||||
} catch (err) {
|
||||
console.error('[Local Server] Error removing server marker during shutdown:', err?.message || err);
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGTERM', () => void shutdownRuntimeServices());
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user