bookworm-smart-assistant/scripts/browserbase-mcp-wrapper.js

202 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});