bookworm-smart-assistant/scripts/archive/patch-browserbase-proxy.js

417 lines
15 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* Bookworm Smart Assistant - Browserbase MCP 代理 Patch 脚本
*
* 功能:
* WSL 环境下 Clash 代理劫持 DNSStagehand 内部 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();
}