#!/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'); // === 维度检查器 === 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 = (Date.now() - 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((Date.now() - genDate.getTime()) / 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((Date.now() - genDate.getTime()) / 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 (Date.now() - 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(); }