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

202 lines
7.2 KiB
JavaScript
Raw Normal View History

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