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);
|
|||
|
|
}
|
|||
|
|
}
|