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