#!/usr/bin/env node /** * Bookworm Smart Assistant - 指标仪表盘 * * 用法: * node scripts/dashboard.js # 今日概览 * node scripts/dashboard.js --date 2026-02-20 # 指定日期 * node scripts/dashboard.js --range 7 # 最近 7 天汇总 * node scripts/dashboard.js --json # JSON 输出 (供其他脚本消费) * * 数据源: * debug/activity-YYYY-MM-DD.jsonl - 活动日志 * debug/security-YYYY-MM-DD.jsonl - 安全事件日志 * projects/{proj}/memory/evolution-log.jsonl - 进化日志 */ const fs = require('fs'); const path = require('path'); // 动态检测配置根目录 const detectClaudeRoot = () => require('./paths.config.js').PATHS.root; const CLAUDE_ROOT = detectClaudeRoot(); const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug'); // === 参数解析 === const args = process.argv.slice(2); const jsonMode = args.includes('--json'); const dateIdx = args.indexOf('--date'); const rangeIdx = args.indexOf('--range'); const today = new Date().toISOString().slice(0, 10); let targetDates = [today]; if (dateIdx >= 0 && args[dateIdx + 1]) { targetDates = [args[dateIdx + 1]]; } else if (rangeIdx >= 0) { const days = parseInt(args[rangeIdx + 1]) || 7; targetDates = []; for (let i = 0; i < days; i++) { const d = new Date(); d.setDate(d.getDate() - i); targetDates.push(d.toISOString().slice(0, 10)); } } // === 数据加载 === function loadJsonl(filePath) { if (!fs.existsSync(filePath)) return []; const lines = fs.readFileSync(filePath, 'utf8').trim().split('\n'); const entries = []; for (const line of lines) { if (!line) continue; try { entries.push(JSON.parse(line)); } catch {} } return entries; } function loadActivityLogs(dates) { const all = []; for (const d of dates) { all.push(...loadJsonl(path.join(DEBUG_DIR, `activity-${d}.jsonl`))); } return all; } function loadSecurityLogs(dates) { const all = []; for (const d of dates) { all.push(...loadJsonl(path.join(DEBUG_DIR, `security-${d}.jsonl`))); } return all; } function loadEvolutionLog() { // 搜索所有 projects 目录下的 evolution-log.jsonl const projectsDir = path.join(CLAUDE_ROOT, 'projects'); if (!fs.existsSync(projectsDir)) return []; const entries = []; try { for (const proj of fs.readdirSync(projectsDir)) { const elog = path.join(projectsDir, proj, 'memory', 'evolution-log.jsonl'); if (fs.existsSync(elog)) { entries.push(...loadJsonl(elog)); } } } catch {} return entries; } // === Phase 3 数据加载 === function loadDetectionStats() { const fp = path.join(DEBUG_DIR, 'detection-stats.json'); if (!fs.existsSync(fp)) return null; try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; } } function loadSkillCorrelation() { const fp = path.join(DEBUG_DIR, 'skill-outcome-correlation.json'); if (!fs.existsSync(fp)) return null; try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; } } function loadOutcomeAggregation() { const fp = path.join(DEBUG_DIR, 'outcome-aggregation.json'); if (!fs.existsSync(fp)) return null; try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; } } function loadTraceLogs(dates) { const all = []; for (const d of dates) { all.push(...loadJsonl(path.join(DEBUG_DIR, `trace-${d}.jsonl`))); } return all; } function loadRemediationLog() { return loadJsonl(path.join(DEBUG_DIR, 'remediation-log.jsonl')); } // === Phase 3 统计计算 === function computePhase3Stats(detectionStats, skillCorrelation, outcomeAgg, traceLogs, remediations) { const result = { detection: { trend: 'insufficient', todayCount: 0, topRules: [], topExtensions: [] }, skills: { entries: [], bestSkill: null, worstSkill: null }, builds: { totalCommands: 0, overallSuccessRate: 0, topFailures: [] }, traces: { totalEvents: 0, byHook: {}, byEventType: {} }, remediations: { total: 0, successful: 0, recent: [] }, }; // --- Detection --- if (detectionStats) { const ds = detectionStats; result.detection.trend = ds.trend || 'insufficient'; result.detection.todayCount = ds.todayCount || ds.total || 0; if (Array.isArray(ds.byRule)) { result.detection.topRules = ds.byRule .sort((a, b) => (b.total || b.count || 0) - (a.total || a.count || 0)) .slice(0, 5) .map(r => ({ id: r.id || r.rule, total: r.total || r.count || 0, severity: r.severity || 'unknown' })); } if (Array.isArray(ds.byExtension)) { result.detection.topExtensions = ds.byExtension .sort((a, b) => (b.count || 0) - (a.count || 0)) .slice(0, 5) .map(e => ({ ext: e.ext || e.extension, count: e.count || 0 })); } } // --- Skills --- if (skillCorrelation) { const sc = skillCorrelation; const entries = []; const skills = sc.skills || sc.correlations || sc; if (typeof skills === 'object' && !Array.isArray(skills)) { for (const [name, data] of Object.entries(skills)) { if (!data || typeof data !== 'object') continue; const total = data.total || (data.success || 0) + (data.failure || 0) || 0; const successRate = total > 0 ? Math.round(((data.success || 0) / total) * 100) : 0; entries.push({ name, total, successRate, trend: data.trend || 'stable' }); } } else if (Array.isArray(skills)) { for (const s of skills) { const total = s.total || (s.success || 0) + (s.failure || 0) || 0; const successRate = total > 0 ? Math.round(((s.success || 0) / total) * 100) : 0; entries.push({ name: s.name || s.skill, total, successRate, trend: s.trend || 'stable' }); } } entries.sort((a, b) => b.total - a.total); result.skills.entries = entries; if (entries.length > 0) { const sorted = [...entries].filter(e => e.total > 0).sort((a, b) => b.successRate - a.successRate); result.skills.bestSkill = sorted[0]?.name || null; result.skills.worstSkill = sorted[sorted.length - 1]?.name || null; } } // --- Builds --- if (outcomeAgg) { const oa = outcomeAgg; const commands = oa.commands || oa.outcomes || {}; let totalCmd = 0, totalSuccess = 0; const failures = []; if (typeof commands === 'object' && !Array.isArray(commands)) { for (const [cmd, data] of Object.entries(commands)) { if (!data || typeof data !== 'object') continue; const t = data.total || (data.success || 0) + (data.failure || 0) || 0; const f = data.failure || 0; totalCmd += t; totalSuccess += (data.success || 0); if (f > 0 && t > 0) { failures.push({ command: cmd, failureRate: Math.round((f / t) * 100), total: t }); } } } result.builds.totalCommands = totalCmd; result.builds.overallSuccessRate = totalCmd > 0 ? Math.round((totalSuccess / totalCmd) * 100) : 0; result.builds.topFailures = failures.sort((a, b) => b.failureRate - a.failureRate).slice(0, 5); } // --- Traces --- if (Array.isArray(traceLogs) && traceLogs.length > 0) { result.traces.totalEvents = traceLogs.length; for (const t of traceLogs) { const hook = t.hook || t.hookName || 'unknown'; result.traces.byHook[hook] = (result.traces.byHook[hook] || 0) + 1; const evType = t.eventType || t.event || t.type || 'unknown'; result.traces.byEventType[evType] = (result.traces.byEventType[evType] || 0) + 1; } } // --- Remediations --- if (Array.isArray(remediations) && remediations.length > 0) { result.remediations.total = remediations.length; result.remediations.successful = remediations.filter(r => r.success || r.result === 'success').length; result.remediations.recent = remediations .slice(-5) .reverse() .map(r => ({ ts: r.ts || r.timestamp || '', dimensionId: r.dimensionId || r.dimension || r.id || '', action: r.action || r.description || '', success: !!(r.success || r.result === 'success'), improved: r.improved != null ? r.improved : null, })); } return result; } // === 统计计算 === function computeStats(activities, securities) { const stats = { total: activities.length, byEvent: {}, topSkills: {}, topMcps: {}, topAgents: {}, bashCount: 0, writeCount: 0, security: { deny: 0, ask: 0, byHook: {} }, }; for (const a of activities) { // 按事件类型 stats.byEvent[a.event] = (stats.byEvent[a.event] || 0) + 1; // 技能排名 if (a.event === 'skill' && a.detail) { stats.topSkills[a.detail] = (stats.topSkills[a.detail] || 0) + 1; } // MCP 排名 if (a.event === 'mcp' && a.detail) { stats.topMcps[a.detail] = (stats.topMcps[a.detail] || 0) + 1; } // Agent 排名 if (a.event === 'agent' && a.detail) { stats.topAgents[a.detail] = (stats.topAgents[a.detail] || 0) + 1; } } stats.bashCount = stats.byEvent.bash || 0; stats.writeCount = stats.byEvent.write || 0; // 安全事件 for (const s of securities) { if (s.decision === 'deny') stats.security.deny++; else if (s.decision === 'ask') stats.security.ask++; const hook = s.hook || 'unknown'; if (!stats.security.byHook[hook]) { stats.security.byHook[hook] = { deny: 0, ask: 0 }; } stats.security.byHook[hook][s.decision === 'deny' ? 'deny' : 'ask']++; } return stats; } // === 磁盘统计 === function getDiskStats() { const result = { totalMB: 0, debugMB: 0, activityFiles: 0, securityFiles: 0 }; try { if (!fs.existsSync(DEBUG_DIR)) return result; let totalBytes = 0; for (const f of fs.readdirSync(DEBUG_DIR)) { const fp = path.join(DEBUG_DIR, f); const st = fs.statSync(fp); if (st.isFile()) { totalBytes += st.size; if (f.startsWith('activity-')) result.activityFiles++; if (f.startsWith('security-')) result.securityFiles++; } } result.debugMB = Math.round(totalBytes / 1024 / 1024 * 10) / 10; // 整个 .claude 目录大小 (只统计一层深度的子目录) let claudeBytes = 0; for (const sub of fs.readdirSync(CLAUDE_ROOT)) { const subPath = path.join(CLAUDE_ROOT, sub); try { const st = fs.statSync(subPath); if (st.isFile()) { claudeBytes += st.size; } else if (st.isDirectory()) { // 粗略统计子目录 for (const f2 of fs.readdirSync(subPath)) { const f2Path = path.join(subPath, f2); try { const st2 = fs.statSync(f2Path); if (st2.isFile()) claudeBytes += st2.size; } catch {} } } } catch {} } result.totalMB = Math.round(claudeBytes / 1024 / 1024 * 10) / 10; } catch {} return result; } // === CLI 渲染 === function bar(count, max, width = 20) { const filled = max > 0 ? Math.round((count / max) * width) : 0; return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled); } function topN(obj, n = 5) { return Object.entries(obj) .sort((a, b) => b[1] - a[1]) .slice(0, n); } function renderPhase3(p3) { const W = 60; const line = '\u2500'.repeat(W); console.log('\u251C' + line + '\u2524'); console.log('\u2502' + ' Phase 3 Intelligence'.padEnd(W) + '\u2502'); console.log('\u2502' + ''.padEnd(W) + '\u2502'); // Detection Trend const trendArrow = p3.detection.trend === 'improving' ? '\u2193' : p3.detection.trend === 'worsening' ? '\u2191' : '\u2192'; console.log('\u2502' + ` Detection Trend: ${p3.detection.trend} ${trendArrow} (Today: ${p3.detection.todayCount})`.padEnd(W) + '\u2502'); if (p3.detection.topRules.length > 0) { const rulesStr = p3.detection.topRules.map(r => `${r.id}(${r.total})`).join(' '); console.log('\u2502' + ` Top Rules: ${rulesStr}`.slice(0, W).padEnd(W) + '\u2502'); } if (p3.detection.topExtensions.length > 0) { const extStr = p3.detection.topExtensions.map(e => `${e.ext}(${e.count})`).join(' '); console.log('\u2502' + ` Top Files: ${extStr}`.slice(0, W).padEnd(W) + '\u2502'); } console.log('\u2502' + ''.padEnd(W) + '\u2502'); // Skill Outcomes if (p3.skills.entries.length > 0) { console.log('\u2502' + ' Skill Outcomes:'.padEnd(W) + '\u2502'); for (const [i, s] of p3.skills.entries.slice(0, 5).entries()) { const filled = Math.round(s.successRate / 10); const skillBar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled); const row = ` ${(i + 1)}. ${s.name.padEnd(18)} ${skillBar} ${s.successRate}% (${s.total}) ${s.trend}`; console.log('\u2502' + row.slice(0, W).padEnd(W) + '\u2502'); } console.log('\u2502' + ''.padEnd(W) + '\u2502'); } // Build Success if (p3.builds.totalCommands > 0) { const succCount = Math.round(p3.builds.totalCommands * p3.builds.overallSuccessRate / 100); console.log('\u2502' + ` Build Success: ${p3.builds.overallSuccessRate}% (${succCount}/${p3.builds.totalCommands})`.padEnd(W) + '\u2502'); if (p3.builds.topFailures.length > 0) { const failStr = p3.builds.topFailures.map(f => `${f.command}(${f.failureRate}%)`).join(' '); console.log('\u2502' + ` Top Failures: ${failStr}`.slice(0, W).padEnd(W) + '\u2502'); } console.log('\u2502' + ''.padEnd(W) + '\u2502'); } // Trace Events if (p3.traces.totalEvents > 0) { const hookCount = Object.keys(p3.traces.byHook).length; console.log('\u2502' + ` Trace Events: ${p3.traces.totalEvents} (${hookCount} hooks)`.padEnd(W) + '\u2502'); const evtStr = Object.entries(p3.traces.byEventType).map(([k, v]) => `${k}: ${v}`).join(' '); console.log('\u2502' + ` ${evtStr}`.slice(0, W).padEnd(W) + '\u2502'); console.log('\u2502' + ''.padEnd(W) + '\u2502'); } // Auto-Remediation if (p3.remediations.total > 0) { console.log('\u2502' + ` Auto-Remediation: ${p3.remediations.successful}/${p3.remediations.total} successful`.padEnd(W) + '\u2502'); for (const r of p3.remediations.recent) { const icon = r.success ? '[+]' : '[X]'; const imp = r.improved != null ? ` (${r.improved})` : ''; const desc = ` ${icon} ${r.dimensionId} ${r.action}${imp}`; console.log('\u2502' + desc.slice(0, W).padEnd(W) + '\u2502'); } } } function renderCli(stats, disk, evoLog, dates, phase3Stats) { const W = 60; const line = '\u2500'.repeat(W); const dateLabel = dates.length === 1 ? dates[0] : `${dates[dates.length - 1]} ~ ${dates[0]}`; console.log(); console.log('\u250C' + '\u2500'.repeat(W) + '\u2510'); console.log('\u2502' + ` Bookworm Dashboard \u2014 ${dateLabel}`.padEnd(W) + '\u2502'); console.log('\u251C' + line + '\u2524'); // 事件总览 const skillCount = stats.byEvent.skill || 0; const agentCount = stats.byEvent.agent || 0; const mcpCount = stats.byEvent.mcp || 0; const row1 = ` Events: ${stats.total}`.padEnd(20) + `Skills: ${skillCount}`.padEnd(14) + `Agents: ${agentCount}`.padEnd(14) + `MCP: ${mcpCount}`; const row2 = ` `.padEnd(20) + `Bash: ${stats.bashCount}`.padEnd(14) + `Write: ${stats.writeCount}`.padEnd(14); console.log('\u2502' + row1.padEnd(W) + '\u2502'); console.log('\u2502' + row2.padEnd(W) + '\u2502'); // TOP 5 Skills const top5 = topN(stats.topSkills, 5); if (top5.length > 0) { console.log('\u251C' + line + '\u2524'); console.log('\u2502' + ' TOP 5 Skills:'.padEnd(W) + '\u2502'); const maxVal = top5[0]?.[1] || 1; for (const [i, [name, count]] of top5.entries()) { const b = bar(count, maxVal, 16); const row = ` ${i + 1}. ${name.padEnd(28)} ${b} ${count}`; console.log('\u2502' + row.padEnd(W) + '\u2502'); } } // 安全事件 console.log('\u251C' + line + '\u2524'); const secTotal = stats.security.deny + stats.security.ask; console.log('\u2502' + ` Security Events: ${secTotal} (${stats.security.deny} deny / ${stats.security.ask} ask)`.padEnd(W) + '\u2502'); for (const [hook, counts] of Object.entries(stats.security.byHook)) { const shortHook = hook.replace('block-', '').slice(0, 20); const row = ` ${shortHook}: ${counts.deny} deny / ${counts.ask} ask`; console.log('\u2502' + row.padEnd(W) + '\u2502'); } // 磁盘健康 console.log('\u251C' + line + '\u2524'); const diskStatus = disk.totalMB > 4096 ? 'CRITICAL' : disk.totalMB > 2048 ? 'WARNING' : 'GOOD'; const diskRow = ` Disk: ${disk.debugMB} MB (debug/)`.padEnd(30) + `Health: ${diskStatus}`; console.log('\u2502' + diskRow.padEnd(W) + '\u2502'); console.log('\u2502' + ` Log files: ${disk.activityFiles} activity + ${disk.securityFiles} security`.padEnd(W) + '\u2502'); // 进化日志摘要 if (evoLog.length > 0) { console.log('\u251C' + line + '\u2524'); const latest = evoLog[evoLog.length - 1]; console.log('\u2502' + ` Evolution: ${evoLog.length} entries, latest ${latest.version} (${latest.ts})`.padEnd(W) + '\u2502'); // 按版本统计修复数 const versionFixes = {}; for (const e of evoLog) { versionFixes[e.version] = (versionFixes[e.version] || 0) + (e.fix_count || 0); } const versionRow = Object.entries(versionFixes) .map(([v, c]) => `${v}:${c}`) .join(' '); console.log('\u2502' + ` Fixes by version: ${versionRow}`.padEnd(W) + '\u2502'); } // Phase 3 Intelligence if (phase3Stats) { renderPhase3(phase3Stats); } console.log('\u2514' + '\u2500'.repeat(W) + '\u2518'); console.log(); } // === 主流程 === function main() { const activities = loadActivityLogs(targetDates); const securities = loadSecurityLogs(targetDates); const stats = computeStats(activities, securities); const disk = getDiskStats(); const evoLog = loadEvolutionLog(); // Phase 3 数据 const detectionStats = loadDetectionStats(); const skillCorrelation = loadSkillCorrelation(); const outcomeAgg = loadOutcomeAggregation(); const traceLogs = loadTraceLogs(targetDates); const remediations = loadRemediationLog(); const phase3Stats = computePhase3Stats(detectionStats, skillCorrelation, outcomeAgg, traceLogs, remediations); if (jsonMode) { console.log(JSON.stringify({ dates: targetDates, stats, disk, evolution: { total: evoLog.length, latestVersion: evoLog[evoLog.length - 1]?.version || 'unknown', }, phase3: phase3Stats, }, null, 2)); } else { renderCli(stats, disk, evoLog, targetDates, phase3Stats); } } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { module.exports = { detectClaudeRoot, loadJsonl, loadActivityLogs, loadSecurityLogs, loadEvolutionLog, loadDetectionStats, loadSkillCorrelation, loadOutcomeAggregation, loadTraceLogs, loadRemediationLog, computeStats, computePhase3Stats, getDiskStats, bar, topN, renderPhase3, renderCli, main, }; } if (require.main === module) { main(); }