#!/usr/bin/env node /** * Bookworm Smart Assistant - Browserbase MCP 代理 Patch 脚本 * * 功能: * WSL 环境下 Clash 代理劫持 DNS,Stagehand 内部 fetch/ws 不走代理环境变量。 * 本脚本对 @browserbasehq/stagehand/dist/index.js 应用 3 个 patch: * 1. Proxy Bootstrap 头部注入 — undici ProxyAgent + http/https globalAgent * 2. WebSocket 代理注入 — CdpConnection.connect 中 ws 走 https-proxy-agent * 3. API 降级容错 — apiClient.init() 失败时降级继续 * * 使用方式: * node patch-browserbase-proxy.js # 等同 --check * node patch-browserbase-proxy.js --check # 检查 patch 状态 * node patch-browserbase-proxy.js --apply # 应用 patch (幂等) * node patch-browserbase-proxy.js --restore # 从备份恢复原始文件 * * 集成点: * - 每次 npx -y @browserbasehq/mcp-server-browserbase@latest 更新后运行 * - 可配合 post-tool hook 自动触发 * * 创建: 2026-02-20 */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const { glob } = require('path'); // ─── 配置 ─────────────────────────────────────────────── const IS_WSL = process.platform === 'linux' && fs.existsSync('/mnt/c'); const HOME = process.env.HOME || process.env.USERPROFILE; // npx 缓存中的目标文件 glob 模式 const NPX_CACHE_GLOB = path.join(HOME, '.npm/_npx/*/node_modules/@browserbasehq/stagehand/dist/index.js'); // patch 检测标记 const MARKERS = { proxyBootstrap: '/* === Bookworm Proxy Bootstrap (injected) === */', wsProxy: 'const _proxyUrl = process.env.https_proxy', apiFallback: '[Stagehand] WARN - API client init failed', }; // ─── Patch 内容定义 ───────────────────────────────────── // Patch 1: Proxy Bootstrap IIFE — 在文件最前面 var __create 之前注入 const PATCH1_INJECT = `/* === Bookworm Proxy Bootstrap (injected) === */ (function _setupGlobalProxy() { var _pUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY; if (!_pUrl) return; // 1) Patch undici global fetch to use proxy (native fetch) try { var _undiciPaths = [ require("path").join(process.execPath, "../../lib/node_modules/undici"), require("path").join(process.env.HOME || "", ".nvm/versions/node", "v" + process.versions.node, "lib/node_modules/undici") ]; var _undici; for (var _up of _undiciPaths) { try { _undici = require(_up); break; } catch(e) {} } if (_undici && _undici.ProxyAgent && _undici.setGlobalDispatcher) { _undici.setGlobalDispatcher(new _undici.ProxyAgent(_pUrl)); } } catch(e) {} // 2) Patch Node.js http/https globalAgent for node-fetch and other libs try { var _hpa = require("https-proxy-agent"); var _httpsAgent = new _hpa.HttpsProxyAgent(_pUrl); require("https").globalAgent = _httpsAgent; var _httpPa = require("http-proxy-agent"); var _httpAgent = new _httpPa.HttpProxyAgent(_pUrl); require("http").globalAgent = _httpAgent; } catch(e) {} })(); /* === End Proxy Bootstrap === */ `; // Patch 1 匹配锚点 (原始文件开头) const PATCH1_ANCHOR = 'var __create = Object.create;'; // Patch 2: WebSocket 代理注入 — 替换 CdpConnection.connect 内的 ws 创建 const PATCH2_ORIGINAL = ` static connect(wsUrl) { return __async(this, null, function* () { const ws = new import_ws.default(wsUrl);`; const PATCH2_REPLACE = ` static connect(wsUrl) { return __async(this, null, function* () { let wsOpts = void 0; const _proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY; if (_proxyUrl && wsUrl.startsWith("wss://")) { try { const { HttpsProxyAgent } = require("https-proxy-agent"); wsOpts = { agent: new HttpsProxyAgent(_proxyUrl) }; } catch (e) {} } const ws = wsOpts ? new import_ws.default(wsUrl, wsOpts) : new import_ws.default(wsUrl);`; // Patch 3: API 降级容错 — 在 apiClient 初始化块外包裹 try-catch const PATCH3_ORIGINAL = `if (!this.disableAPI && !this.experimental) { this.apiClient = new StagehandAPIClient({`; const PATCH3_REPLACE = `if (!this.disableAPI && !this.experimental) { try { this.apiClient = new StagehandAPIClient({`; // Patch 3 闭合部分: 在 sessionID 赋值后加 catch const PATCH3_CLOSE_ORIGINAL = `this.opts.browserbaseSessionID = sessionId2; }`; const PATCH3_CLOSE_REPLACE = `this.opts.browserbaseSessionID = sessionId2; } catch (_apiErr) { process.stderr.write("[Stagehand] WARN - API client init failed (" + (_apiErr && _apiErr.message || _apiErr) + "), falling back to direct Browserbase session\\n"); this.apiClient = null; } }`; // ─── 工具函数 ─────────────────────────────────────────── /** * 查找所有 npx 缓存中的 stagehand index.js */ function findTargetFiles() { const npxDir = path.join(HOME, '.npm/_npx'); const results = []; if (!fs.existsSync(npxDir)) return results; try { const entries = fs.readdirSync(npxDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const candidate = path.join(npxDir, entry.name, 'node_modules/@browserbasehq/stagehand/dist/index.js'); if (fs.existsSync(candidate)) { results.push(candidate); } } } catch { /* 忽略 */ } return results; } /** * 检查单个文件的 patch 状态 */ function checkPatchStatus(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); return { filePath, fileSize: (fs.statSync(filePath).size / 1024).toFixed(0) + ' KB', proxyBootstrap: content.includes(MARKERS.proxyBootstrap), wsProxy: content.includes(MARKERS.wsProxy), apiFallback: content.includes(MARKERS.apiFallback), get allPatched() { return this.proxyBootstrap && this.wsProxy && this.apiFallback; }, get nonePatched() { return !this.proxyBootstrap && !this.wsProxy && !this.apiFallback; }, }; } /** * 创建备份 */ function createBackup(filePath) { const backupPath = filePath + '.bak'; fs.copyFileSync(filePath, backupPath); return backupPath; } /** * 语法验证 */ function validateSyntax(filePath) { try { execSync(`node -c "${filePath}"`, { stdio: 'pipe' }); return { valid: true, error: null }; } catch (err) { return { valid: false, error: err.stderr ? err.stderr.toString().trim() : '未知语法错误' }; } } /** * 应用所有 patch (幂等) */ function applyPatches(filePath) { let content = fs.readFileSync(filePath, 'utf-8'); const applied = []; const skipped = []; const errors = []; // Patch 1: Proxy Bootstrap if (content.includes(MARKERS.proxyBootstrap)) { skipped.push('Patch 1 (Proxy Bootstrap) — 已存在'); } else if (content.includes(PATCH1_ANCHOR)) { content = content.replace(PATCH1_ANCHOR, PATCH1_INJECT + PATCH1_ANCHOR); applied.push('Patch 1 (Proxy Bootstrap)'); } else { errors.push('Patch 1 (Proxy Bootstrap) — 找不到锚点: "var __create = Object.create;"'); } // Patch 2: WebSocket 代理 if (content.includes(MARKERS.wsProxy)) { skipped.push('Patch 2 (WebSocket Proxy) — 已存在'); } else if (content.includes(PATCH2_ORIGINAL)) { content = content.replace(PATCH2_ORIGINAL, PATCH2_REPLACE); applied.push('Patch 2 (WebSocket Proxy)'); } else { errors.push('Patch 2 (WebSocket Proxy) — 找不到锚点: "static connect(wsUrl)" 代码块'); } // Patch 3: API 降级容错 if (content.includes(MARKERS.apiFallback)) { skipped.push('Patch 3 (API Fallback) — 已存在'); } else if (content.includes(PATCH3_ORIGINAL)) { // 先替换开头 content = content.replace(PATCH3_ORIGINAL, PATCH3_REPLACE); // 再替换闭合 — 找到 disableAPI 块结尾 if (content.includes(PATCH3_CLOSE_ORIGINAL)) { content = content.replace(PATCH3_CLOSE_ORIGINAL, PATCH3_CLOSE_REPLACE); applied.push('Patch 3 (API Fallback)'); } else { errors.push('Patch 3 (API Fallback) — 找不到闭合锚点: "this.opts.browserbaseSessionID = sessionId2;"'); } } else { errors.push('Patch 3 (API Fallback) — 找不到锚点: "if (!this.disableAPI && !this.experimental)" 代码块'); } // 写回文件 if (applied.length > 0) { fs.writeFileSync(filePath, content, 'utf-8'); } return { applied, skipped, errors }; } // ─── 命令: --check ────────────────────────────────────── function cmdCheck() { const files = findTargetFiles(); if (files.length === 0) { console.log(' 未找到 stagehand dist/index.js'); console.log(` 查找路径: ${path.join(HOME, '.npm/_npx/*/...')}`); console.log(' 提示: 先运行 npx -y @browserbasehq/mcp-server-browserbase@latest 安装'); return; } for (const filePath of files) { const status = checkPatchStatus(filePath); const hash = filePath.split('/_npx/')[1].split('/')[0]; console.log(` 文件: .../_npx/${hash}/.../dist/index.js (${status.fileSize})`); console.log(` Patch 1 (Proxy Bootstrap): ${status.proxyBootstrap ? '✓ 已应用' : '✗ 未应用'}`); console.log(` Patch 2 (WebSocket Proxy): ${status.wsProxy ? '✓ 已应用' : '✗ 未应用'}`); console.log(` Patch 3 (API Fallback): ${status.apiFallback ? '✓ 已应用' : '✗ 未应用'}`); const backupExists = fs.existsSync(filePath + '.bak'); console.log(` 备份文件: ${backupExists ? '✓ 存在' : '- 无'}`); if (status.allPatched) { console.log(` 状态: 全部 patch 已就绪`); } else if (status.nonePatched) { console.log(` 状态: 未 patch (运行 --apply 应用)`); } else { console.log(` 状态: 部分 patch (运行 --apply 补全)`); } console.log(''); } } // ─── 命令: --apply ────────────────────────────────────── function cmdApply() { const files = findTargetFiles(); if (files.length === 0) { console.log(' 未找到 stagehand dist/index.js'); console.log(' 提示: 先运行 npx -y @browserbasehq/mcp-server-browserbase@latest 安装'); process.exit(1); return; } let allSuccess = true; for (const filePath of files) { const hash = filePath.split('/_npx/')[1].split('/')[0]; console.log(` 目标: .../_npx/${hash}/.../dist/index.js`); // 创建备份 (如果还没有) const backupPath = filePath + '.bak'; if (!fs.existsSync(backupPath)) { createBackup(filePath); console.log(` 备份: 已创建 .bak`); } else { console.log(` 备份: 已存在 .bak (跳过)`); } // 应用 patch const result = applyPatches(filePath); for (const msg of result.applied) console.log(` [应用] ${msg}`); for (const msg of result.skipped) console.log(` [跳过] ${msg}`); for (const msg of result.errors) { console.log(` [错误] ${msg}`); allSuccess = false; } // 验证 console.log(''); console.log(' 验证:'); // 标记检查 const status = checkPatchStatus(filePath); console.log(` 标记检查: ${status.allPatched ? '✓ 3/3 标记均存在' : '✗ 部分标记缺失'}`); // 语法检查 const syntax = validateSyntax(filePath); console.log(` 语法检查: ${syntax.valid ? '✓ node -c 通过' : '✗ ' + syntax.error}`); if (!syntax.valid) { allSuccess = false; console.log(' ⚠ 语法错误! 建议运行 --restore 恢复备份'); } if (status.allPatched && syntax.valid) { console.log(' 结果: 全部 patch 成功'); } console.log(''); } process.exit(allSuccess ? 0 : 1); } // ─── 命令: --restore ──────────────────────────────────── function cmdRestore() { const files = findTargetFiles(); let restored = 0; if (files.length === 0) { console.log(' 未找到 stagehand dist/index.js'); return; } for (const filePath of files) { const backupPath = filePath + '.bak'; const hash = filePath.split('/_npx/')[1].split('/')[0]; if (!fs.existsSync(backupPath)) { console.log(` .../_npx/${hash}/...: 无备份文件,跳过`); continue; } fs.copyFileSync(backupPath, filePath); restored++; console.log(` .../_npx/${hash}/...: 已从 .bak 恢复`); // 验证恢复后状态 const status = checkPatchStatus(filePath); console.log(` 恢复后状态: ${status.nonePatched ? '✓ 已清除所有 patch' : '仍有 patch 残留'}`); } if (restored === 0) { console.log(' 没有找到任何备份文件'); } else { console.log(`\n 已恢复 ${restored} 个文件`); } } // ─── 主程序 ───────────────────────────────────────────── function main() { const args = process.argv.slice(2); const command = args.find(a => a.startsWith('--')) || '--check'; console.log(`\n${'═'.repeat(50)}`); console.log(' Bookworm Browserbase Proxy Patcher'); console.log(` ${new Date().toISOString().slice(0, 19)}`); console.log(`${'═'.repeat(50)}\n`); switch (command) { case '--check': cmdCheck(); break; case '--apply': cmdApply(); break; case '--restore': cmdRestore(); break; default: console.log(' 用法:'); console.log(' node patch-browserbase-proxy.js --check 检查 patch 状态'); console.log(' node patch-browserbase-proxy.js --apply 应用 patch (幂等)'); console.log(' node patch-browserbase-proxy.js --restore 从备份恢复'); break; } console.log(`${'═'.repeat(50)}\n`); } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { module.exports = { MARKERS, PATCH1_INJECT, PATCH1_ANCHOR, PATCH2_ORIGINAL, PATCH2_REPLACE, PATCH3_ORIGINAL, PATCH3_REPLACE, PATCH3_CLOSE_ORIGINAL, PATCH3_CLOSE_REPLACE, findTargetFiles, checkPatchStatus, createBackup, validateSyntax, applyPatches, cmdCheck, cmdApply, cmdRestore, main, }; } if (require.main === module) { main(); }