539 lines
19 KiB
JavaScript
539 lines
19 KiB
JavaScript
|
|
#!/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();
|
||
|
|
}
|