bookworm-smart-assistant/scripts/browserbase-session-cleanup.js

177 lines
5.5 KiB
JavaScript
Raw Normal View History

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