#!/usr/bin/env node // browserbase-session-cleanup.js v1.0 // 清理 Browserbase 泄漏 session — 关闭超时的孤儿 session // 使用: node browserbase-session-cleanup.js [--max-age-min 30] [--dry-run] // 集成: health-check.js H12 子检查 / 手动运行 / wrapper 退出时调用 const path = require('path'); const { execFileSync } = require('child_process'); const fs = require('fs'); // === 配置 === const DEFAULT_MAX_AGE_MIN = 30; // 超过此分钟数的 RUNNING session 视为泄漏 const CLAUDE_ROOT = path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude'); const CACHE_FILE = path.join(CLAUDE_ROOT, 'debug', 'browserbase-sessions.json'); // === 解析命令行参数 === const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const maxAgeIdx = args.indexOf('--max-age-min'); const maxAgeMin = maxAgeIdx >= 0 ? parseInt(args[maxAgeIdx + 1], 10) || DEFAULT_MAX_AGE_MIN : DEFAULT_MAX_AGE_MIN; // === 从 .claude.json 读取凭证 === function loadConfig() { const claudeJsonPath = path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude.json'); if (!fs.existsSync(claudeJsonPath)) { throw new Error('.claude.json 不存在'); } const config = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); const bbEnv = config?.mcpServers?.browserbase?.env || {}; const apiKey = bbEnv.BROWSERBASE_API_KEY; const projectId = bbEnv.BROWSERBASE_PROJECT_ID; const proxy = bbEnv.https_proxy || bbEnv.http_proxy || ''; if (!apiKey || !projectId) { throw new Error('BROWSERBASE_API_KEY 或 BROWSERBASE_PROJECT_ID 未配置'); } return { apiKey, projectId, proxy }; } // === API 调用封装 === function apiCall(method, urlPath, config, body) { const curlArgs = [ '-s', '-m', '10', '-X', method, '-H', `x-bb-api-key: ${config.apiKey}`, '-H', 'Content-Type: application/json', ]; if (config.proxy) curlArgs.push('-x', config.proxy); if (body) curlArgs.push('-d', JSON.stringify(body)); curlArgs.push(`https://api.browserbase.com${urlPath}`); const result = execFileSync('curl', curlArgs, { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'], }).trim(); if (!result) return null; try { return JSON.parse(result); } catch { return result; } } // === 列出活跃 session === function listRunningSessions(config) { const data = apiCall('GET', `/v1/sessions?status=RUNNING&limit=50`, config); // API 返回 { sessions: [...] } 或直接数组 if (Array.isArray(data)) return data; if (data && Array.isArray(data.sessions)) return data.sessions; return []; } // === 关闭单个 session === function closeSession(sessionId, config) { return apiCall('POST', `/v1/sessions/${sessionId}`, config, { status: 'REQUEST_RELEASE', projectId: config.projectId, }); } // === 主流程 === function main() { const config = loadConfig(); const now = Date.now(); const maxAgeMs = maxAgeMin * 60 * 1000; const log = (msg) => process.stderr.write(`${msg}\n`); log(`[session-cleanup] 检查超过 ${maxAgeMin} 分钟的 RUNNING session${dryRun ? ' (dry-run)' : ''}`); // 列出所有 RUNNING session let sessions; try { sessions = listRunningSessions(config); } catch (err) { log(`[session-cleanup] API 调用失败: ${err.message}`); process.exit(1); } if (sessions.length === 0) { log('[session-cleanup] 无 RUNNING session,无需清理'); saveCacheReport({ ts: new Date().toISOString(), running: 0, stale: 0, closed: 0 }); return; } log(`[session-cleanup] 发现 ${sessions.length} 个 RUNNING session`); // 筛选超时 session const stale = sessions.filter(s => { const createdAt = new Date(s.createdAt || s.created_at || 0).getTime(); const age = now - createdAt; return age > maxAgeMs; }); if (stale.length === 0) { log(`[session-cleanup] 所有 session 均在 ${maxAgeMin} 分钟内,无需清理`); saveCacheReport({ ts: new Date().toISOString(), running: sessions.length, stale: 0, closed: 0 }); return; } log(`[session-cleanup] ${stale.length} 个超时 session 需要清理:`); let closed = 0; for (const s of stale) { const id = s.id || s.sessionId; const createdAt = s.createdAt || s.created_at || 'unknown'; const ageMin = Math.round((now - new Date(createdAt).getTime()) / 60000); log(` - ${id} (创建于 ${createdAt}, ${ageMin} 分钟前)`); if (!dryRun) { try { closeSession(id, config); log(` 已关闭`); closed++; } catch (err) { log(` 关闭失败: ${err.message}`); } } else { log(` [dry-run] 跳过`); } } const report = { ts: new Date().toISOString(), running: sessions.length, stale: stale.length, closed, dryRun, }; saveCacheReport(report); log(`[session-cleanup] 完成: ${closed}/${stale.length} 个超时 session 已关闭`); } // === 缓存报告(供 health-check / weekly-report 读取)=== function saveCacheReport(report) { try { const dir = path.dirname(CACHE_FILE); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(CACHE_FILE, JSON.stringify(report, null, 2)); } catch {} } // === 模块导出(供 health-check.js 内联调用)=== module.exports = { listRunningSessions, closeSession, loadConfig, CACHE_FILE }; // === 直接运行 === if (require.main === module) { try { main(); } catch (err) { console.error(`[session-cleanup] 致命错误: ${err.message}`); process.exit(1); } }