bookworm-smart-assistant/scripts/health-check.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

1046 lines
38 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
/**
* 全链路健康检查 (Health Check)
*
* 聚合所有验证器为单一健康评分 (0-100)。
* 供 self-auditor D8 维度和日常运维使用。
*
* 用法:
* node scripts/health-check.js # 文本报告
* node scripts/health-check.js --json # JSON 输出 (供程序消费)
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
const CLAUDE_ROOT = detectClaudeRoot();
const JSON_MODE = process.argv.includes('--json');
// W7_SAFE_AGE_v1: 时间回拨防护 (NTP 回跳/跨时区/休眠恢复)
function safeAge(t) {
const ts = typeof t === 'number' ? t : (t && t.getTime ? t.getTime() : 0);
const d = Date.now() - ts;
return d < 0 ? 0 : d;
}
// === 维度检查器 ===
let dimensions = [];
function resetDimensions() { dimensions = []; return dimensions; }
function addDimension(id, name, score, status, detail) {
dimensions.push({ id, name, score, status, detail });
}
function runScript(scriptPath, args = '') {
try {
const result = execSync(
`node "${path.join(CLAUDE_ROOT, scriptPath)}" ${args}`,
{ encoding: 'utf8', timeout: 15000, cwd: CLAUDE_ROOT }
);
return { ok: true, output: result };
} catch (e) {
return { ok: false, output: e.stdout || '', error: e.stderr || e.message };
}
}
// H1: 配置一致性 (config-validator)
function checkConfig() {
const result = runScript('scripts/config-validator.js', '--json');
if (!result.ok) {
addDimension('H1', '配置一致性', 50, 'WARN', '验证器执行失败');
return;
}
try {
const data = JSON.parse(result.output);
if (data.errors > 0) {
addDimension('H1', '配置一致性', Math.max(0, 100 - data.errors * 25 - data.warnings * 5),
'FAIL', `${data.errors} errors, ${data.warnings} warnings`);
} else if (data.warnings > 0) {
addDimension('H1', '配置一致性', Math.max(70, 100 - data.warnings * 5),
'WARN', `${data.warnings} warnings`);
} else {
addDimension('H1', '配置一致性', 100, 'PASS', '全部检查通过');
}
} catch {
addDimension('H1', '配置一致性', 60, 'WARN', '输出解析失败');
}
}
// H2: 行为基线 (behavior-baseline)
function checkBehavior() {
const result = runScript('scripts/behavior-baseline.js', '--check --json');
if (!result.ok) {
addDimension('H2', '行为基线', 80, 'INFO', '基线数据不足');
return;
}
try {
const data = JSON.parse(result.output);
const check = data.check || {};
const anomalyCount = (check.anomalies || []).length;
const criticals = (check.anomalies || []).filter(a => a.severity === 'CRITICAL').length;
if (criticals > 0) {
addDimension('H2', '行为基线', 30, 'FAIL', `${criticals} 个严重异常`);
} else if (anomalyCount > 0) {
addDimension('H2', '行为基线', Math.max(60, 100 - anomalyCount * 15),
'WARN', `${anomalyCount} 个异常`);
} else {
addDimension('H2', '行为基线', 100, 'PASS', '行为正常');
}
} catch {
addDimension('H2', '行为基线', 80, 'INFO', '输出解析失败');
}
}
// H3: 磁盘健康
function checkDisk() {
const debugDir = path.join(CLAUDE_ROOT, 'debug');
let totalSize = 0;
try {
const files = fs.readdirSync(debugDir);
for (const f of files) {
try {
totalSize += fs.statSync(path.join(debugDir, f)).size;
} catch {}
}
} catch {}
const sizeMB = Math.round(totalSize / 1024 / 1024);
// P5: session-active.lock 过期检测 (>24h 视为残留)
let lockNote = '';
try {
const lockFile = path.join(debugDir, 'session-active.lock');
if (fs.existsSync(lockFile)) {
const lockAge = safeAge(fs.statSync(lockFile).mtimeMs) / 3600000;
if (lockAge > 24) {
lockNote = `, session-active.lock 残留 (${Math.round(lockAge)}h 前)`;
}
}
} catch {}
if (sizeMB > 16384) {
addDimension('H3', '磁盘健康', 20, 'FAIL', `${sizeMB} MB (>16GB CRITICAL)${lockNote}`);
} else if (sizeMB > 8192) {
addDimension('H3', '磁盘健康', 50, 'WARN', `${sizeMB} MB (>8GB WARNING)${lockNote}`);
} else if (sizeMB > 1024) {
addDimension('H3', '磁盘健康', 75, 'INFO', `${sizeMB} MB${lockNote}`);
} else {
addDimension('H3', '磁盘健康', lockNote ? 90 : 100, lockNote ? 'INFO' : 'PASS', `${sizeMB} MB${lockNote}`);
}
}
// H4: 钩子完整性 (checksums + designDecisions 识别)
function checkIntegrity() {
const checksumFile = path.join(CLAUDE_ROOT, 'hooks', 'checksums.json');
if (!fs.existsSync(checksumFile)) {
addDimension('H4', '钩子完整性', 60, 'WARN', 'checksums.json 不存在');
return;
}
try {
const crypto = require('crypto');
const checksums = JSON.parse(fs.readFileSync(checksumFile, 'utf8'));
const hooksDir = path.join(CLAUDE_ROOT, 'hooks');
let total = 0, match = 0;
for (const [file, expectedHash] of Object.entries(checksums)) {
total++;
// scripts/ 前缀文件从 CLAUDE_ROOT 解析,其余从 hooksDir 解析
const filePath = file.startsWith('scripts/') ? path.join(CLAUDE_ROOT, file) : path.join(hooksDir, file);
if (!fs.existsSync(filePath)) continue;
const content = fs.readFileSync(filePath);
const actualHash = crypto.createHash('sha256').update(content).digest('hex');
if (actualHash === expectedHash) match++;
}
// 读取 stats-compiled.json 的 designDecisions识别有意未注册的备用钩子
let designNote = '';
try {
const statsFile = path.join(CLAUDE_ROOT, 'stats-compiled.json');
if (fs.existsSync(statsFile)) {
const stats = JSON.parse(fs.readFileSync(statsFile, 'utf8'));
const dd = stats.designDecisions || {};
if (dd.unregisteredHooksIntentional && (stats.unregisteredHooks || []).length > 0) {
designNote = `, ${stats.unregisteredHooks.length} 备用钩子 (有意未注册)`;
}
}
} catch { /* stats 不可用时静默回退 */ }
// 检测 settings.json 注册的钩子文件是否实际存在
let missingHookFiles = 0;
let hookFileNotes = [];
try {
const settingsPath = path.join(CLAUDE_ROOT, 'settings.json');
if (fs.existsSync(settingsPath)) {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const hooksConfig = settings.hooks || {};
const checkedPaths = new Set(); // 去重,同一路径只检查一次
// 遍历所有 hook 事件类型UserPromptSubmit, PreToolUse 等)
for (const eventHookGroups of Object.values(hooksConfig)) {
if (!Array.isArray(eventHookGroups)) continue;
for (const group of eventHookGroups) {
const hookList = group.hooks || [];
for (const hook of hookList) {
if (hook.type !== 'command' || !hook.command) continue;
// 从 command 字段提取文件路径(格式: "node path/to/file.js [args]"
const cmdParts = hook.command.split(/\s+/);
// 跳过 "node",找到 .js 文件路径
const jsFile = cmdParts.find((p, i) => i > 0 && p.endsWith('.js'));
if (!jsFile) continue;
// 清理路径:移除可能的引号包裹
const cleanPath = jsFile.replace(/^["']|["']$/g, '');
if (checkedPaths.has(cleanPath)) continue;
checkedPaths.add(cleanPath);
// 解析为绝对路径
const absPath = path.isAbsolute(cleanPath)
? cleanPath
: path.join(CLAUDE_ROOT, cleanPath);
if (!fs.existsSync(absPath)) {
// 检查是否存在 .disabled 版本
const disabledPath = absPath + '.disabled';
if (fs.existsSync(disabledPath)) {
hookFileNotes.push(`${path.basename(absPath)} 已 .disabled`);
} else {
missingHookFiles++;
hookFileNotes.push(`settings.json 注册了 ${path.basename(absPath)} 但文件不存在`);
}
}
}
}
}
}
} catch { /* settings.json 读取异常时 fail-open不影响其他维度 */ }
// 将 hookFileNotes 追加到详情
const hookFileDetail = hookFileNotes.length > 0 ? `, ${hookFileNotes.join('; ')}` : '';
// 综合评分checksums 匹配度 + 缺失钩子文件扣分(每缺失一个扣 5 分)
const checksumScore = total > 0 ? Math.round(match / total * 100) : 100;
const hookFilePenalty = missingHookFiles * 5;
const finalScore = Math.max(0, Math.min(checksumScore, 100) - hookFilePenalty);
if (match === total && missingHookFiles === 0) {
addDimension('H4', '钩子完整性', finalScore, 'PASS', `${total}/${total} 校验通过${designNote}${hookFileDetail}`);
} else if (match === total && missingHookFiles > 0) {
addDimension('H4', '钩子完整性', finalScore, 'WARN', `${total}/${total} 校验通过, ${missingHookFiles} 个注册钩子文件缺失${designNote}${hookFileDetail}`);
} else {
addDimension('H4', '钩子完整性', Math.max(0, finalScore),
'FAIL', `${match}/${total} 校验通过, ${total - match} 个不匹配${missingHookFiles > 0 ? `, ${missingHookFiles} 个注册钩子文件缺失` : ''}${designNote}${hookFileDetail}`);
}
} catch (e) {
addDimension('H4', '钩子完整性', 50, 'WARN', e.message);
}
}
// H5: 技能索引新鲜度
function checkSkillIndex() {
const indexFile = path.join(CLAUDE_ROOT, 'skills-index.json');
if (!fs.existsSync(indexFile)) {
addDimension('H5', '技能索引', 50, 'WARN', '索引文件不存在');
return;
}
try {
const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
const skillsDir = path.join(CLAUDE_ROOT, 'skills');
const actualCount = fs.readdirSync(skillsDir)
.filter(d => fs.existsSync(path.join(skillsDir, d, 'SKILL.md'))).length;
if (index.skillCount === actualCount) {
addDimension('H5', '技能索引', 100, 'PASS', `${actualCount} 技能已索引`);
} else {
addDimension('H5', '技能索引', 70, 'WARN',
`索引 ${index.skillCount} vs 实际 ${actualCount}`);
}
} catch {
addDimension('H5', '技能索引', 60, 'WARN', '索引解析失败');
}
}
// H6: 规则缓存
function checkRulesCache() {
const compiled = path.join(CLAUDE_ROOT, 'hooks', 'rules', 'rules-compiled.json');
if (!fs.existsSync(compiled)) {
addDimension('H6', '规则缓存', 70, 'INFO', '无预编译缓存 (性能未优化)');
return;
}
try {
const cache = JSON.parse(fs.readFileSync(compiled, 'utf8'));
const rulesDir = path.join(CLAUDE_ROOT, 'hooks', 'rules');
let stale = false;
for (const [file, meta] of Object.entries(cache.sources || {})) {
const filePath = path.join(rulesDir, file);
if (!fs.existsSync(filePath)) { stale = true; break; }
const stat = fs.statSync(filePath);
if (Math.abs(stat.mtimeMs - meta.mtime) > 1000) { stale = true; break; }
}
if (stale) {
addDimension('H6', '规则缓存', 60, 'WARN', '缓存已过期 (运行 compile-rules.js)');
} else {
const totalPatterns = Object.values(cache.rules || {}).reduce((s, r) => s + r.length, 0);
addDimension('H6', '规则缓存', 100, 'PASS', `${totalPatterns} 条规则已缓存`);
}
} catch {
addDimension('H6', '规则缓存', 60, 'WARN', '缓存解析失败');
}
}
// H7: 路由准确率 (v5.0, P2 加固: 检测 feedback 文件缺失 + weights 过期)
function checkRouteAccuracy() {
const feedbackFile = path.join(CLAUDE_ROOT, 'debug', 'route-feedback.jsonl');
const weightsFile = path.join(CLAUDE_ROOT, 'debug', 'route-weights.json');
if (!fs.existsSync(feedbackFile)) {
// P2: 如果 weights 存在但 feedback 缺失,说明学习闭环断裂
if (fs.existsSync(weightsFile)) {
try {
const w = JSON.parse(fs.readFileSync(weightsFile, 'utf8'));
const genDate = w.generated ? new Date(w.generated) : null;
const daysSince = genDate ? Math.floor(safeAge(genDate) / 86400000) : -1;
addDimension('H7', '路由准确率', 50, 'WARN',
`学习闭环断裂: route-feedback.jsonl 缺失,权重固化于 ${daysSince >= 0 ? daysSince + ' 天前' : '未知时间'} (${w.feedbackCount || 0} 条历史反馈)`);
} catch {
addDimension('H7', '路由准确率', 50, 'WARN', '学习闭环断裂: feedback 文件缺失');
}
} else {
addDimension('H7', '路由准确率', 80, 'INFO', '无反馈数据 (尚未收集)');
}
return;
}
try {
const lines = fs.readFileSync(feedbackFile, 'utf8').trim().split('\n');
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
if (entries.length === 0) {
addDimension('H7', '路由准确率', 80, 'INFO', '无反馈数据');
return;
}
const corrections = entries.filter(f => f.routedTo !== f.correctedTo && f.routedTo !== 'unknown');
const total = entries.filter(f => f.routedTo !== 'unknown').length;
const accuracy = total > 0 ? (total - corrections.length) / total : 1;
const pct = Math.round(accuracy * 100);
if (pct >= 90) {
addDimension('H7', '路由准确率', 100, 'PASS', `${pct}% (${total} 条反馈)`);
} else if (pct >= 70) {
addDimension('H7', '路由准确率', pct, 'WARN', `${pct}% (${corrections.length}/${total} 纠正)`);
} else {
addDimension('H7', '路由准确率', Math.max(30, pct), 'FAIL', `${pct}% 准确率过低`);
}
} catch {
addDimension('H7', '路由准确率', 70, 'INFO', '数据解析失败');
}
}
// H8: 学习收敛趋势 (v5.0, P2 加固: 权重过期检测)
function checkLearningConvergence() {
const weightsFile = path.join(CLAUDE_ROOT, 'debug', 'route-weights.json');
if (!fs.existsSync(weightsFile)) {
addDimension('H8', '学习收敛', 70, 'INFO', '权重文件不存在 (未学习)');
return;
}
try {
const data = JSON.parse(fs.readFileSync(weightsFile, 'utf8'));
// P2: 权重过期检测 — 超过 7 天未更新则降级
const genDate = data.generated ? new Date(data.generated) : null;
const daysSince = genDate ? Math.floor(safeAge(genDate) / 86400000) : -1;
if (daysSince > 7) {
addDimension('H8', '学习收敛', 60, 'WARN',
`权重已过期: ${daysSince} 天未更新 (generated: ${data.generated || '未知'})`);
return;
}
const deltas = data.deltas || {};
const skillCount = Object.keys(deltas).length;
if (skillCount === 0) {
addDimension('H8', '学习收敛', 90, 'PASS', '无权重偏移 (稳定)');
return;
}
// 分析权重偏移幅度
let totalAbsDelta = 0;
let totalEntries = 0;
let maxAbsDelta = 0;
for (const skillDeltas of Object.values(deltas)) {
for (const delta of Object.values(skillDeltas)) {
totalAbsDelta += Math.abs(delta);
totalEntries++;
maxAbsDelta = Math.max(maxAbsDelta, Math.abs(delta));
}
}
const avgDelta = totalEntries > 0 ? totalAbsDelta / totalEntries : 0;
// 收敛判断: 平均偏移越小越好
if (avgDelta < 0.1 && maxAbsDelta < 0.3) {
addDimension('H8', '学习收敛', 100, 'PASS',
`已收敛 (avg=${avgDelta.toFixed(3)}, max=${maxAbsDelta.toFixed(3)})`);
} else if (avgDelta < 0.2) {
addDimension('H8', '学习收敛', 80, 'INFO',
`趋于收敛 (avg=${avgDelta.toFixed(3)}, ${totalEntries} 调整)`);
} else {
addDimension('H8', '学习收敛', 60, 'WARN',
`偏移较大 (avg=${avgDelta.toFixed(3)}, max=${maxAbsDelta.toFixed(3)})`);
}
} catch {
addDimension('H8', '学习收敛', 70, 'INFO', '权重文件解析失败');
}
}
// H9: 路由合规率 (v5.2)
function checkRouteCompliance() {
try {
const complianceAnalyzer = require('./compliance-analyzer.js');
const entries = complianceAnalyzer.loadComplianceLogs(7);
const metrics = complianceAnalyzer.computeComplianceRate(entries);
if (metrics.total === 0) {
addDimension('H9', '路由合规率', 80, 'INFO', '无合规数据 (尚未收集)');
return;
}
const pct = Math.round(metrics.rate * 100);
if (pct >= 95) {
addDimension('H9', '路由合规率', 100, 'PASS', `${pct}% (${metrics.total} 条决策)`);
} else if (pct >= 80) {
addDimension('H9', '路由合规率', pct, 'WARN', `${pct}% (${metrics.violated} 违规)`);
} else {
addDimension('H9', '路由合规率', Math.max(30, pct), 'FAIL', `${pct}% 合规率过低`);
}
} catch {
addDimension('H9', '路由合规率', 70, 'INFO', '合规分析器不可用');
}
}
// H10: Hook有效性 (v5.4 Phase 2)
function checkHookEffectiveness() {
const aggFile = path.join(CLAUDE_ROOT, 'debug', 'outcome-aggregation.json');
if (!fs.existsSync(aggFile)) {
addDimension('H10', 'Hook有效性', 80, 'INFO', '无聚合数据 (尚未收集)');
return;
}
try {
const agg = JSON.parse(fs.readFileSync(aggFile, 'utf8'));
const commands = Object.keys(agg);
if (commands.length === 0) {
addDimension('H10', 'Hook有效性', 80, 'INFO', '无构建记录');
return;
}
// 计算总体构建成功率 (unknown 不计入有效样本)
let totalBuilds = 0, totalSuccess = 0, totalUnknown = 0;
for (const cmd of commands) {
const entry = agg[cmd];
totalBuilds += entry.total || 0;
totalSuccess += entry.success || 0;
totalUnknown += entry.unknown || 0;
}
if (totalBuilds === 0) {
addDimension('H10', 'Hook有效性', 80, 'INFO', '无构建记录');
return;
}
// 有效样本 = 总数 - unknown; 全部 unknown 时报 INFO 而非 FAIL
const effective = totalBuilds - totalUnknown;
if (effective === 0) {
addDimension('H10', 'Hook有效性', 80, 'INFO',
`${totalBuilds} 次构建均无法判定结果 (outcome tracker 未捕获退出码)`);
return;
}
const successRate = totalSuccess / effective;
const pct = Math.round(successRate * 100);
if (successRate >= 0.8) {
addDimension('H10', 'Hook有效性', 100, 'PASS', `构建成功率 ${pct}% (${totalSuccess}/${effective}, ${totalUnknown} unknown)`);
} else if (successRate >= 0.6) {
addDimension('H10', 'Hook有效性', 75, 'WARN', `构建成功率 ${pct}% (${totalSuccess}/${effective})`);
} else {
addDimension('H10', 'Hook有效性', Math.max(40, pct), 'FAIL', `构建成功率 ${pct}% (${totalSuccess}/${effective})`);
}
} catch {
addDimension('H10', 'Hook有效性', 80, 'INFO', '聚合数据解析失败');
}
}
// H11: 宪法覆盖率 (v6.0)
function checkConstitutionCoverage() {
// 检查全局宪法副本存在性
const globalConstitution = path.join(CLAUDE_ROOT, 'constitution', 'AI-CONSTITUTION.md');
const globalTemplate = path.join(CLAUDE_ROOT, 'constitution', 'TEMPLATE-CONSTITUTION.md');
const claudeMd = path.join(CLAUDE_ROOT, 'CLAUDE.md');
if (!fs.existsSync(globalConstitution)) {
addDimension('H11', '宪法覆盖', 30, 'FAIL', '全局宪法文件缺失 (~/.claude/constitution/AI-CONSTITUTION.md)');
return;
}
let score = 60; // 基线: 全局宪法存在
const details = [];
// 检查全局 CLAUDE.md 包含交付质量宪章
try {
const claudeContent = (() => { try { return fs.readFileSync(claudeMd, 'utf8'); } catch { return ''; } })();
if (claudeContent.includes('交付质量宪章')) {
score += 15;
details.push('全局宪章已集成');
} else {
details.push('全局 CLAUDE.md 缺少交付质量宪章');
}
if (claudeContent.includes('项目宪法协议')) {
score += 10;
}
} catch {}
// 检查宪法模板存在
if (fs.existsSync(globalTemplate)) {
score += 5;
}
// 检查 constitution-guard hook 存在且启用
try {
const ff = JSON.parse((() => { try { return fs.readFileSync(path.join(CLAUDE_ROOT, 'feature-flags.json'), 'utf8'); } catch { return '{}'; } })());
if (ff.features && ff.features['constitution-guard'] && ff.features['constitution-guard'].enabled) {
score += 10;
details.push('guard hook 已启用');
} else {
details.push('guard hook 未启用');
}
} catch {}
score = Math.min(100, score);
const status = score >= 90 ? 'PASS' : score >= 70 ? 'INFO' : 'WARN';
addDimension('H11', '宪法覆盖', score, status, details.join(', ') || '宪法体系已部署');
}
// H12: Browserbase MCP 健康 (v6.5)
function checkBrowserbaseMcp() {
const wrapperPath = path.join(CLAUDE_ROOT, 'scripts', 'browserbase-mcp-wrapper.js');
const patchScript = path.join(CLAUDE_ROOT, 'mcp-servers', 'browserbase', 'postinstall-patch.js');
const bootstrapPath = path.join(CLAUDE_ROOT, 'scripts', 'undici-proxy-bootstrap.js');
const agentJs = path.join(CLAUDE_ROOT, 'mcp-servers', 'browserbase',
'node_modules', '@browserbasehq', 'mcp-server-browserbase', 'dist', 'tools', 'agent.js');
const stagehandIndex = path.join(CLAUDE_ROOT, 'mcp-servers', 'browserbase',
'node_modules', '@browserbasehq', 'stagehand', 'dist', 'index.js');
let score = 0;
const issues = [];
const passes = [];
// 1. Wrapper 脚本存在性
if (fs.existsSync(wrapperPath)) {
score += 20;
passes.push('wrapper');
} else {
issues.push('wrapper 缺失');
}
// 2. Proxy bootstrap 存在性
if (fs.existsSync(bootstrapPath)) {
score += 10;
passes.push('bootstrap');
} else {
issues.push('proxy-bootstrap 缺失');
}
// 3. node_modules 安装状态
if (fs.existsSync(agentJs)) {
score += 15;
passes.push('npm-installed');
} else {
issues.push('node_modules 未安装');
}
// 4. agent.js patch 状态 ([PATCHED] 标记)
if (fs.existsSync(agentJs)) {
try {
const content = fs.readFileSync(agentJs, 'utf8');
if (content.includes('[PATCHED]')) {
score += 20;
passes.push('agent-patched');
} else {
issues.push('agent.js 未 patch (Gemini CUA 不可用)');
}
} catch {
issues.push('agent.js 读取失败');
}
}
// 5. Stagehand model map patch 状态
if (fs.existsSync(stagehandIndex)) {
try {
const content = fs.readFileSync(stagehandIndex, 'utf8');
if (content.includes('claude-sonnet-4-6') || content.includes('claude-opus-4-6')) {
score += 15;
passes.push('model-map-patched');
} else {
issues.push('Stagehand model map 未 patch');
}
} catch {
issues.push('Stagehand index.js 读取失败');
}
}
// 6. postinstall 脚本存在npm install 后自动修复能力)
if (fs.existsSync(patchScript)) {
score += 10;
passes.push('postinstall');
} else {
issues.push('postinstall-patch.js 缺失 (npm install 后 patch 不会自动恢复)');
}
// 7. .claude.json 环境变量检查 (BROWSERBASE_API_KEY + PROJECT_ID)
try {
const claudeJsonPath = path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude.json');
if (fs.existsSync(claudeJsonPath)) {
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
const bbEnv = claudeJson?.mcpServers?.browserbase?.env || {};
if (bbEnv.BROWSERBASE_API_KEY && bbEnv.BROWSERBASE_PROJECT_ID) {
score += 10;
passes.push('env-keys');
} else {
issues.push('API Key 或 Project ID 未配置');
}
}
} catch {}
// 8. 用量查询 (可选, 网络不通时跳过)
let usageNote = '';
let sessionNote = '';
try {
const claudeJsonPath = path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude.json');
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
const bbEnv = claudeJson?.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) {
const { execFileSync: execF } = require('child_process');
// 用量查询
const curlArgs = ['-s', '-m', '5', '-H', `x-bb-api-key: ${apiKey}`,
`https://api.browserbase.com/v1/projects/${projectId}/usage`];
if (proxy) curlArgs.unshift('-x', proxy);
const usageResult = execF('curl', curlArgs, {
encoding: 'utf8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
const usage = JSON.parse(usageResult);
const minutes = usage.browserMinutes || 0;
const proxyMB = Math.round((usage.proxyBytes || 0) / 1024 / 1024);
usageNote = `, 用量: ${minutes}min/${proxyMB}MB`;
// 缓存用量到文件 (供周报使用)
const usageCacheFile = path.join(CLAUDE_ROOT, 'debug', 'browserbase-usage.json');
fs.writeFileSync(usageCacheFile, JSON.stringify({
ts: new Date().toISOString(), browserMinutes: minutes, proxyMB,
}, null, 2));
// Session 泄漏检测
const sessArgs = ['-s', '-m', '5', '-H', `x-bb-api-key: ${apiKey}`,
`https://api.browserbase.com/v1/sessions?status=RUNNING&limit=50`];
if (proxy) sessArgs.unshift('-x', proxy);
const sessResult = execF('curl', sessArgs, {
encoding: 'utf8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
const sessData = JSON.parse(sessResult);
const runningSessions = Array.isArray(sessData) ? sessData : (sessData?.sessions || []);
const staleCount = runningSessions.filter(s => {
const created = new Date(s.createdAt || s.created_at || 0).getTime();
return safeAge(created) > 30 * 60 * 1000;
}).length;
if (staleCount > 0) {
issues.push(`${staleCount} 个泄漏 session (>30min)`);
sessionNote = `, 泄漏: ${staleCount}`;
} else {
sessionNote = `, session: ${runningSessions.length} running`;
}
// 缓存 session 状态
const sessCacheFile = path.join(CLAUDE_ROOT, 'debug', 'browserbase-sessions.json');
fs.writeFileSync(sessCacheFile, JSON.stringify({
ts: new Date().toISOString(),
running: runningSessions.length,
stale: staleCount,
}, null, 2));
}
} catch {
// 网络不通或 API 异常时静默跳过,不影响评分
}
// 评分与状态
score = Math.min(100, score);
const detail = issues.length > 0
? `${passes.length}/7 通过, 问题: ${issues.join('; ')}${usageNote}${sessionNote}`
: `${passes.length}/7 全部通过${usageNote}${sessionNote}`;
const status = score >= 90 ? 'PASS' : score >= 60 ? 'WARN' : 'FAIL';
addDimension('H12', 'Browserbase', score, status, detail);
}
// === v5.1: 权重自优化 ===
const WEIGHT_HISTORY_FILE = path.join(CLAUDE_ROOT, 'debug', 'health-weight-history.json');
/**
* 基于瓶颈维度频率自动调整 H1-H8 权重
* @param {Object} currentWeights - 当前权重
* @returns {Object} 调整后的权重
*/
function autoTuneWeights(currentWeights) {
let history = [];
try {
if (fs.existsSync(WEIGHT_HISTORY_FILE)) {
history = JSON.parse(fs.readFileSync(WEIGHT_HISTORY_FILE, 'utf8'));
}
} catch {}
// 记录当前评分到历史
const currentRecord = {
ts: new Date().toISOString(),
dimensions: dimensions.map(d => ({ id: d.id, score: d.score, status: d.status })),
};
history.push(currentRecord);
// 仅保留最近 30 条
if (history.length > 30) history = history.slice(-30);
// 保存历史
try {
const dir = path.dirname(WEIGHT_HISTORY_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(WEIGHT_HISTORY_FILE, JSON.stringify(history, null, 2) + '\n');
} catch {}
// 需要至少 5 条历史才做调整
if (history.length < 5) return currentWeights;
// Phase 1+2: 差异化阈值 — 不同维度组使用不同的瓶颈判定标准
const BOTTLENECK_THRESHOLDS = {
H1: 85, H4: 85, H6: 85, // 配置类:应更灵敏
H7: 75, H9: 75, // 准确率类:波动正常
H3: 80, // 磁盘类:宽松
H2: 70, H5: 70, H8: 70, // 其余:保持默认
H10: 70, // Hook有效性保持默认
};
// Phase 1: 告警抑制 — 24 小时内同维度低分不重复告警
const now = new Date();
const suppressWindow = 24 * 60 * 60 * 1000; // 24h in ms
const recentAlerts = {};
for (const record of history) {
const recordTs = new Date(record.ts);
if (now - recordTs < suppressWindow) {
for (const d of (record.dimensions || [])) {
const threshold = BOTTLENECK_THRESHOLDS[d.id] || 70;
if (d.score < threshold) {
recentAlerts[d.id] = true;
}
}
}
}
// 统计各维度低分频率 (使用差异化阈值)
const bottleneckFreq = {};
for (const record of history) {
for (const d of (record.dimensions || [])) {
const threshold = BOTTLENECK_THRESHOLDS[d.id] || 70;
if (d.score < threshold) {
bottleneckFreq[d.id] = (bottleneckFreq[d.id] || 0) + 1;
}
}
}
// 频繁瓶颈维度提升权重, 稳定维度降低权重
const adjusted = { ...currentWeights };
const totalRecords = history.length;
for (const id of Object.keys(adjusted)) {
const freq = (bottleneckFreq[id] || 0) / totalRecords;
if (freq > 0.5) {
// 经常是瓶颈: 提升权重 (+3)
adjusted[id] = Math.min(25, adjusted[id] + 3);
} else if (freq === 0) {
// 从不是瓶颈: 降低权重 (-2)
adjusted[id] = Math.max(5, adjusted[id] - 2);
}
}
// Phase 1: 计算被抑制的告警
const alertsSuppressed = [];
for (const d of dimensions) {
const threshold = BOTTLENECK_THRESHOLDS[d.id] || 70;
if (d.score < threshold && recentAlerts[d.id]) {
alertsSuppressed.push(d.id);
}
}
adjusted._alertsSuppressed = alertsSuppressed;
return adjusted;
}
// === Phase 1: 告警分级 ===
/**
* 根据偏离程度分级告警
* @param {Object} dim - 维度 { id, score, status }
* @param {number} threshold - 该维度的瓶颈阈值
* @returns {'NORMAL'|'ALERT'|'AUTO-REPAIR'}
*/
function classifyAlert(dim, threshold) {
if (dim.score >= threshold) return 'NORMAL';
const deviation = threshold - dim.score;
const deviationPct = deviation / threshold;
if (deviationPct > 0.10) return 'AUTO-REPAIR'; // >10% 偏离
return 'ALERT'; // 5-10% 偏离
}
// === Phase 3: 自动修复引擎 (D4) ===
const REMEDIATION_MAP = {
H1: { label: '配置验证修复', script: 'scripts/config-validator.js', args: '--fix' },
H3: { label: '磁盘清理', script: 'scripts/auto-cleanup.js', args: '--execute' },
H4: { label: '完整性修复', script: 'hooks/integrity-check.js', args: '--generate' },
H5: { label: '技能索引重建', script: 'scripts/generate-skill-index.js', args: '--quiet' },
H6: { label: '规则缓存重建', script: 'scripts/compile-rules.js', args: '' },
};
const REMEDIATION_LOG_FILE = path.join(CLAUDE_ROOT, 'debug', 'remediation-log.jsonl');
/**
* 记录修复操作
* @param {Object} entry - 修复记录
*/
function logRemediation(entry) {
try {
const debugDir = path.join(CLAUDE_ROOT, 'debug');
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
fs.appendFileSync(REMEDIATION_LOG_FILE, JSON.stringify(entry) + '\n');
} catch {}
}
/**
* 修复后重跑该维度检查函数,确认分数提升
* @param {string} dimensionId - 维度 ID (H3|H4|H6)
* @returns {{ improved: boolean, newScore: number }|null}
*/
function verifyRemediation(dimensionId) {
try {
const checkFns = { H3: checkDisk, H4: checkIntegrity, H6: checkRulesCache };
const fn = checkFns[dimensionId];
if (!fn) return null;
// 保存当前 dimensions运行单维度检查
const savedDimensions = [...dimensions];
const idx = dimensions.findIndex(d => d.id === dimensionId);
const oldScore = idx >= 0 ? dimensions[idx].score : 0;
// 移除旧维度,重新检查
if (idx >= 0) dimensions.splice(idx, 1);
fn();
const newDim = dimensions.find(d => d.id === dimensionId);
const newScore = newDim ? newDim.score : oldScore;
// 恢复 (保留新检查结果)
return { improved: newScore > oldScore, newScore };
} catch {
return null;
}
}
/**
* 对 AUTO-REPAIR 级别维度执行自动修复
* @param {Array} gradedDimensions - 带 alertLevel 的维度列表
* @returns {Array} 修复结果数组
*/
function autoRemediate(gradedDimensions) {
const results = [];
for (const d of gradedDimensions) {
if (d.alertLevel !== 'AUTO-REPAIR' || d.suppressed) continue;
const remedy = REMEDIATION_MAP[d.id];
if (!remedy) continue;
const entry = {
ts: new Date().toISOString(),
dimensionId: d.id,
dimensionName: d.name,
action: remedy.label,
script: remedy.script,
scoreBefore: d.score,
};
try {
const scriptResult = runScript(remedy.script, remedy.args);
entry.success = scriptResult.ok;
entry.output = (scriptResult.output || '').slice(0, 200);
if (!scriptResult.ok) entry.error = (scriptResult.error || '').slice(0, 200);
// 验证修复效果
if (scriptResult.ok) {
const verification = verifyRemediation(d.id);
if (verification) {
entry.scoreAfter = verification.newScore;
entry.improved = verification.improved;
}
}
} catch (e) {
entry.success = false;
entry.error = (e.message || '').slice(0, 200);
}
logRemediation(entry);
results.push(entry);
}
return results;
}
// === 主流程 ===
function main() {
checkConfig();
checkBehavior();
checkDisk();
checkIntegrity();
checkSkillIndex();
checkRulesCache();
checkRouteAccuracy();
checkLearningConvergence();
checkRouteCompliance();
checkHookEffectiveness();
checkConstitutionCoverage();
checkBrowserbaseMcp();
// v6.5: 默认权重 + 自优化 (H12 Browserbase 加入)
let weights = { H1: 10, H2: 10, H3: 8, H4: 10, H5: 8, H6: 8, H7: 10, H8: 8, H9: 8, H10: 8, H11: 7, H12: 5 };
weights = autoTuneWeights(weights);
let totalWeight = 0, weightedSum = 0;
for (const d of dimensions) {
const w = weights[d.id] || 10;
totalWeight += w;
weightedSum += d.score * w;
}
const overallScore = Math.round(weightedSum / totalWeight);
let overallStatus = 'HEALTHY';
if (overallScore < 60) overallStatus = 'CRITICAL';
else if (overallScore < 80) overallStatus = 'DEGRADED';
// Phase 1: 告警分级与抑制
const alertsSuppressed = weights._alertsSuppressed || [];
delete weights._alertsSuppressed;
const BOTTLENECK_THRESHOLDS = {
H1: 85, H4: 85, H6: 85,
H7: 75, H9: 75,
H3: 80,
H2: 70, H5: 70, H8: 70,
H10: 70,
H12: 70,
};
// 为每个维度附加告警级别
const gradedDimensions = dimensions.map(d => {
const threshold = BOTTLENECK_THRESHOLDS[d.id] || 70;
const alertLevel = classifyAlert(d, threshold);
const suppressed = alertsSuppressed.includes(d.id);
return { ...d, alertLevel, suppressed };
});
// Phase 3: 自动修复 (D4)
const remediations = autoRemediate(gradedDimensions);
const report = {
ts: new Date().toISOString(),
overallScore,
overallStatus,
dimensions: gradedDimensions,
alertsSuppressed,
remediations,
};
if (JSON_MODE) {
console.log(JSON.stringify(report, null, 2));
return;
}
// 文本输出
const statusIcon = { PASS: '+', WARN: '~', FAIL: 'X', INFO: 'i' };
console.log('=== Bookworm Health Check ===');
console.log(`Overall: ${overallScore}/100 (${overallStatus})`);
console.log('');
for (const d of gradedDimensions) {
const icon = statusIcon[d.status] || '?';
const alertTag = d.alertLevel !== 'NORMAL' ? ` [${d.alertLevel}]` : '';
const suppressTag = d.suppressed ? ' (suppressed)' : '';
console.log(` [${icon}] ${d.id} ${d.name.padEnd(12)} ${String(d.score).padStart(3)}/100 ${d.detail}${alertTag}${suppressTag}`);
}
if (alertsSuppressed.length > 0) {
console.log(`\n (${alertsSuppressed.length} alert(s) suppressed — same dimension alerted within 24h)`);
}
// Phase 3: Auto-Remediation 输出
if (remediations.length > 0) {
console.log('\n--- Auto-Remediation ---');
for (const r of remediations) {
const icon = r.success ? '+' : 'X';
const scoreChange = r.scoreAfter !== undefined ? ` (${r.scoreBefore}${r.scoreAfter})` : '';
console.log(` [${icon}] ${r.dimensionId} ${r.action}${scoreChange}`);
}
}
console.log('');
if (overallStatus === 'HEALTHY') {
console.log('System is healthy. No action required.');
} else {
const issues = gradedDimensions.filter(d => d.alertLevel === 'AUTO-REPAIR' && !d.suppressed);
const alerts = gradedDimensions.filter(d => d.alertLevel === 'ALERT' && !d.suppressed);
if (issues.length > 0) {
console.log(`Action needed: ${issues.length} dimension(s) require auto-repair.`);
}
if (alerts.length > 0) {
console.log(`Alert: ${alerts.length} dimension(s) slightly below threshold (monitor only).`);
}
if (issues.length === 0 && alerts.length === 0) {
console.log('All alerts suppressed (already reported within 24h).');
}
}
}
// 导出核心函数供测试使用
if (typeof module !== 'undefined') {
module.exports = {
addDimension, checkConfig, checkBehavior, checkDisk, checkIntegrity,
checkSkillIndex, checkRulesCache, checkRouteAccuracy, checkLearningConvergence,
checkRouteCompliance, checkHookEffectiveness, checkConstitutionCoverage,
checkBrowserbaseMcp, autoTuneWeights, classifyAlert,
autoRemediate, logRemediation, verifyRemediation,
get dimensions() { return dimensions; }, resetDimensions,
};
}
if (require.main === module) {
main();
}