#!/usr/bin/env node // browserbase-mcp-wrapper.js v2.2 // 本地安装 + 代理健康检查 + 启动重试 // 替代旧版 npx 模式,消除下载超时和缓存碎片问题 // v2.1: 修复环境变量泄露 -- 只传递业务必需的变量 (P1-11) // v2.2: 添加 STAGEHAND_AGENT_PROVIDER + Gemini CUA env 支持 const { spawn } = require('child_process'); const path = require('path'); const http = require('http'); // 路径常量 const BOOTSTRAP_PATH = path.join(__dirname, 'undici-proxy-bootstrap.js'); const LOCAL_MCP_BIN = path.join(__dirname, '..', 'mcp-servers', 'browserbase', 'node_modules', '@browserbasehq', 'mcp-server-browserbase', 'cli.js'); const LOCAL_MCP_DIR = path.join(__dirname, '..', 'mcp-servers', 'browserbase'); // 配置 const PROXY_URL = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || ''; const MAX_RETRIES = 2; const PROXY_CHECK_TIMEOUT = 3000; // ms /** * 检查代理是否可达 (仅在配置了代理时检查) */ function checkProxy() { return new Promise((resolve) => { if (!PROXY_URL) { process.stderr.write('[bb-wrapper] 未配置代理,跳过检查\n'); return resolve(true); } try { const url = new URL(PROXY_URL); const req = http.request({ hostname: url.hostname, port: url.port || 7893, method: 'CONNECT', path: 'api.browserbase.com:443', timeout: PROXY_CHECK_TIMEOUT }); req.on('connect', (res) => { res.socket.destroy(); resolve(true); }); req.on('error', () => resolve(false)); req.on('timeout', () => { req.destroy(); resolve(false); }); req.end(); } catch { resolve(false); } }); } /** * 启动 MCP 服务器 * 安全: 只传递 Browserbase MCP 实际需要的环境变量 (最小权限原则) */ function startServer() { const modelApiKey = process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY || process.env.MODEL_API_KEY || ''; const modelName = process.env.STAGEHAND_MODEL_NAME || 'claude-sonnet-4-6'; // --experimental: 禁用 Stagehand 云端 API,改为本地 Playwright + 本地 LLM 调用 // 原因: 中转站 ANTHROPIC_API_KEY 在云端 API 不可用,本地模式可通过 ANTHROPIC_BASE_URL 正确路由 const args = ['--modelApiKey', modelApiKey, '--modelName', modelName, '--experimental']; const child = spawn('node', [LOCAL_MCP_BIN, ...args], { stdio: 'inherit', cwd: LOCAL_MCP_DIR, env: { // ---- 系统运行必需 ---- PATH: process.env.PATH || process.env.Path || '', HOME: process.env.HOME || process.env.USERPROFILE || '', USERPROFILE: process.env.USERPROFILE || '', TEMP: process.env.TEMP || process.env.TMP || '', TMP: process.env.TMP || process.env.TEMP || '', APPDATA: process.env.APPDATA || '', SystemRoot: process.env.SystemRoot || '', // ---- Node.js 运行必需 ---- NODE_OPTIONS: `--require ${BOOTSTRAP_PATH}`, NODE_PATH: path.join(LOCAL_MCP_DIR, 'node_modules'), // ---- Browserbase 业务必需 ---- BROWSERBASE_API_KEY: process.env.BROWSERBASE_API_KEY || '', BROWSERBASE_PROJECT_ID: process.env.BROWSERBASE_PROJECT_ID || '', GEMINI_API_KEY: process.env.GEMINI_API_KEY || process.env.MODEL_API_KEY || '', GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || '', GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || '', // ---- Stagehand Agent 配置 ---- STAGEHAND_AGENT_PROVIDER: process.env.STAGEHAND_AGENT_PROVIDER || 'google', STAGEHAND_MODEL_NAME: process.env.STAGEHAND_MODEL_NAME || 'claude-sonnet-4-6', // ---- Anthropic 中转站 (Stagehand AI 功能) ---- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '', ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || '', // ---- 代理配置 ---- http_proxy: process.env.http_proxy || process.env.HTTP_PROXY || '', https_proxy: process.env.https_proxy || process.env.HTTPS_PROXY || '', no_proxy: process.env.no_proxy || process.env.NO_PROXY || '', // ---- TLS 配置 (如需自签证书) ---- NODE_TLS_REJECT_UNAUTHORIZED: process.env.NODE_TLS_REJECT_UNAUTHORIZED || '' } }); return child; } /** * 启动前环境变量校验 (P0: 快速失败,避免无密钥静默启动) */ function validateEnv() { const apiKey = process.env.BROWSERBASE_API_KEY; const projectId = process.env.BROWSERBASE_PROJECT_ID; const missing = []; if (!apiKey) missing.push('BROWSERBASE_API_KEY'); if (!projectId) missing.push('BROWSERBASE_PROJECT_ID'); if (missing.length > 0) { process.stderr.write(`[bb-wrapper] FATAL: 缺少必需环境变量: ${missing.join(', ')}\n`); process.stderr.write('[bb-wrapper] 请在 .claude.json 的 browserbase env 段中配置密钥\n'); process.exit(1); } } /** * 带重试的主流程 */ async function main() { // 0. 环境变量校验 (快速失败) validateEnv(); // 1. 代理健康检查 const proxyOk = await checkProxy(); if (!proxyOk) { process.stderr.write(`[bb-wrapper] WARNING: 代理 ${PROXY_URL} 不可达,MCP 可能无法连接 Browserbase API\n`); process.stderr.write('[bb-wrapper] 请确认代理软件 (Clash/V2Ray) 已启动\n'); // 仍然尝试启动,万一有直连能力 } else if (PROXY_URL) { process.stderr.write(`[bb-wrapper] 代理 ${PROXY_URL} 连通性 OK\n`); } // 2. 启动服务器 (带重试) let retries = 0; function launch() { const child = startServer(); child.on('error', (err) => { process.stderr.write(`[bb-wrapper] 启动错误: ${err.message}\n`); if (retries < MAX_RETRIES) { retries++; process.stderr.write(`[bb-wrapper] 第 ${retries}/${MAX_RETRIES} 次重试...\n`); setTimeout(launch, 1000 * retries); } else { process.stderr.write('[bb-wrapper] 已达最大重试次数,退出\n'); process.exit(1); } }); child.on('exit', (code) => { if (code !== 0 && code !== null && retries < MAX_RETRIES) { retries++; process.stderr.write(`[bb-wrapper] 进程退出 (code=${code}),第 ${retries}/${MAX_RETRIES} 次重试...\n`); setTimeout(launch, 1000 * retries); } else { process.exit(code || 0); } }); } launch(); } // 退出时尝试清理泄漏 session (best-effort, 不阻塞退出) function cleanupOnExit() { try { const cleanup = require(path.join(__dirname, 'browserbase-session-cleanup.js')); const config = cleanup.loadConfig(); const sessions = cleanup.listRunningSessions(config); const stale = sessions.filter(s => { const created = new Date(s.createdAt || s.created_at || 0).getTime(); return (Date.now() - created) > 30 * 60 * 1000; }); for (const s of stale) { cleanup.closeSession(s.id || s.sessionId, config); } if (stale.length > 0) { process.stderr.write(`[bb-wrapper] 退出清理: 关闭 ${stale.length} 个泄漏 session\n`); } } catch { // best-effort, 不阻塞退出 } } process.on('SIGINT', () => { cleanupOnExit(); process.exit(0); }); process.on('SIGTERM', () => { cleanupOnExit(); process.exit(0); }); main().catch((err) => { process.stderr.write(`[bb-wrapper] 致命错误: ${err.message}\n`); cleanupOnExit(); process.exit(1); });