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

417 lines
15 KiB
JavaScript
Raw Permalink 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
/**
* 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();
}