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

177 lines
5.5 KiB
JavaScript
Raw 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
// 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);
}
}