#!/usr/bin/env node /** * 周报自动生成器 (Weekly Report) * * 聚合 7 天数据生成周报: 技能使用、钩子拦截、磁盘、路由、演进。 * * 用法: * node scripts/weekly-report.js # 本周报告 (Markdown) * node scripts/weekly-report.js --json # JSON 输出 * node scripts/weekly-report.js --end 2026-02-20 # 指定截止日期 */ 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 FORECAST_FILE = path.join(DEBUG_DIR, 'forecast-history.jsonl'); const JSON_MODE = process.argv.includes('--json'); const endArg = process.argv.indexOf('--end'); const END_DATE = endArg >= 0 ? process.argv[endArg + 1] : new Date().toISOString().slice(0, 10); // 计算 7 天前 function dateOffset(dateStr, days) { const d = new Date(dateStr + 'T00:00:00Z'); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); } const START_DATE = dateOffset(END_DATE, -6); // === 数据加载 === function loadJsonlInRange(prefix) { const events = []; try { const files = fs.readdirSync(DEBUG_DIR) .filter(f => f.startsWith(prefix) && f.endsWith('.jsonl')) .sort(); for (const file of files) { const dateMatch = file.match(/(\d{4}-\d{2}-\d{2})/); if (!dateMatch) continue; if (dateMatch[1] < START_DATE || dateMatch[1] > END_DATE) continue; const lines = fs.readFileSync(path.join(DEBUG_DIR, file), 'utf8').trim().split('\n'); for (const line of lines) { try { events.push(JSON.parse(line)); } catch {} } } } catch {} return events; } // === 技能使用分析 === function analyzeSkills(events) { const skillCounts = {}; for (const e of events) { if (e.event === 'skill' && e.detail) { skillCounts[e.detail] = (skillCounts[e.detail] || 0) + 1; } } return Object.entries(skillCounts) .sort((a, b) => b[1] - a[1]) .map(([name, count]) => ({ name, count })); } // === 钩子拦截分析 === function analyzeSecurity(events) { let deny = 0, ask = 0; const byHook = {}; for (const e of events) { if (e.decision === 'deny') deny++; if (e.decision === 'ask') ask++; const hook = e.hook || 'unknown'; if (!byHook[hook]) byHook[hook] = { deny: 0, ask: 0 }; if (e.decision === 'deny') byHook[hook].deny++; if (e.decision === 'ask') byHook[hook].ask++; } return { total: deny + ask, deny, ask, byHook }; } // === 每日事件趋势 === function dailyTrend(events) { const daily = {}; for (const e of events) { const date = (e.ts || '').slice(0, 10); if (!date) continue; daily[date] = (daily[date] || 0) + 1; } // 填充零值天 const result = []; let d = START_DATE; while (d <= END_DATE) { result.push({ date: d, count: daily[d] || 0 }); d = dateOffset(d, 1); } return result; } // === 演进摘要 === function loadEvolution() { const candidates = [ path.join(CLAUDE_ROOT, 'debug', 'evolution-log.jsonl'), path.join(CLAUDE_ROOT, 'projects', 'C--Users-janson9527us', 'memory', 'evolution-log.jsonl'), path.join(CLAUDE_ROOT, 'projects', 'C--Users-Le-novo', 'memory', 'evolution-log.jsonl'), ]; for (const fp of candidates) { if (!fs.existsSync(fp)) continue; const lines = fs.readFileSync(fp, 'utf8').trim().split('\n'); const entries = []; for (const line of lines) { try { const e = JSON.parse(line); if (e.ts >= START_DATE && e.ts <= END_DATE) entries.push(e); } catch {} } return entries; } return []; } // === Browserbase Session 分析 (v6.5) === function analyzeBrowserbase(events) { const bbEvents = events.filter(e => e.event === 'mcp' && e.detail && e.detail.startsWith('browserbase/')); if (bbEvents.length === 0) return null; // 工具使用频率 const toolCounts = {}; for (const e of bbEvents) { const action = e.detail.replace('browserbase/', ''); toolCounts[action] = (toolCounts[action] || 0) + 1; } // Session 统计: 匹配 create/close 对 const creates = bbEvents.filter(e => e.detail.includes('session_create')); const closes = bbEvents.filter(e => e.detail.includes('session_close')); const failures = bbEvents.filter(e => !e.success); // 估算 session 时长(按时间戳配对) const durations = []; const createTimes = creates.map(e => new Date(e.ts).getTime()).sort(); const closeTimes = closes.map(e => new Date(e.ts).getTime()).sort(); const paired = Math.min(createTimes.length, closeTimes.length); for (let i = 0; i < paired; i++) { const dur = closeTimes[i] - createTimes[i]; if (dur > 0 && dur < 3600000) { // < 1h 视为合理 durations.push(dur); } } const avgDurationMs = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0; // 按日分布 const daily = {}; for (const e of bbEvents) { const date = (e.ts || '').slice(0, 10); if (date) daily[date] = (daily[date] || 0) + 1; } return { totalCalls: bbEvents.length, sessionsCreated: creates.length, sessionsClosed: closes.length, failedCalls: failures.length, failureRate: bbEvents.length > 0 ? Math.round(failures.length / bbEvents.length * 100) : 0, avgDurationSec: Math.round(avgDurationMs / 1000), topTools: Object.entries(toolCounts).sort((a, b) => b[1] - a[1]).slice(0, 8), daily, }; } // === 健康趋势预测 (P1-2) === function loadHealthForecast() { try { const { exponentialSmoothing, detectSpikes } = require('./predictive-audit.js'); // 从 health-check 历史或 evolution-log 获取修复密度序列 const evolutionEntries = loadEvolution(); if (evolutionEntries.length < 3) return null; const fixCounts = evolutionEntries.map(e => e.fix_count || 0); const smoothed = exponentialSmoothing(fixCounts); const spikes = detectSpikes(fixCounts); return { dataPoints: fixCounts.length, forecast: smoothed.forecast, series: smoothed.series, spikes: spikes.length, trend: fixCounts.length >= 2 ? (smoothed.series[smoothed.series.length - 1] > smoothed.series[smoothed.series.length - 2] ? 'rising' : 'falling') : 'stable', }; } catch { return null; } } function recordForecast(forecast) { if (!forecast) return; try { if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true }); const entry = { ts: new Date().toISOString(), period: `${START_DATE}~${END_DATE}`, forecast: forecast.forecast, trend: forecast.trend, dataPoints: forecast.dataPoints, spikes: forecast.spikes, }; fs.appendFileSync(FORECAST_FILE, JSON.stringify(entry) + '\n'); } catch { /* 写入失败静默忽略 */ } } // === 主流程 === function main() { const activityEvents = loadJsonlInRange('activity-'); const securityEvents = loadJsonlInRange('security-'); const evolutionEntries = loadEvolution(); const skills = analyzeSkills(activityEvents); const security = analyzeSecurity(securityEvents); const trend = dailyTrend(activityEvents); // 磁盘 let diskMB = 0; try { const files = fs.readdirSync(DEBUG_DIR); for (const f of files) { try { diskMB += fs.statSync(path.join(DEBUG_DIR, f)).size; } catch {} } } catch {} diskMB = Math.round(diskMB / 1024 / 1024); // 工具使用分布 const toolCounts = {}; for (const e of activityEvents) { const tool = e.event || 'other'; toolCounts[tool] = (toolCounts[tool] || 0) + 1; } // v6.5: Browserbase session 分析 const browserbase = analyzeBrowserbase(activityEvents); // P1-2: 健康趋势预测 const forecast = loadHealthForecast(); recordForecast(forecast); const report = { period: { start: START_DATE, end: END_DATE }, summary: { totalEvents: activityEvents.length, securityEvents: security.total, evolutionEntries: evolutionEntries.length, diskMB, }, skills: skills.slice(0, 10), security, trend, toolUsage: Object.entries(toolCounts).sort((a, b) => b[1] - a[1]), evolution: evolutionEntries.map(e => ({ version: e.version, summary: (e.summary || '').slice(0, 100), fixCount: e.fix_count, })), browserbase, forecast, }; if (JSON_MODE) { console.log(JSON.stringify(report, null, 2)); return; } // Markdown 周报 console.log(`# Bookworm 周报 ${START_DATE} ~ ${END_DATE}`); console.log(''); console.log('## 概览'); console.log(`- 总事件: ${report.summary.totalEvents}`); console.log(`- 安全事件: ${security.total} (${security.deny} deny / ${security.ask} ask)`); console.log(`- 版本变更: ${evolutionEntries.length} 条`); console.log(`- 磁盘占用: ${diskMB} MB`); console.log(''); // 每日趋势 console.log('## 每日事件趋势'); for (const day of trend) { const bar = '#'.repeat(Math.min(Math.round(day.count / 5), 40)); console.log(` ${day.date} ${String(day.count).padStart(4)} ${bar}`); } console.log(''); // 技能 TOP 10 if (skills.length > 0) { console.log('## 技能使用 TOP 10'); for (let i = 0; i < Math.min(skills.length, 10); i++) { const s = skills[i]; const bar = '#'.repeat(Math.min(s.count, 30)); console.log(` ${String(i + 1).padStart(2)}. ${s.name.padEnd(30)} ${String(s.count).padStart(3)} ${bar}`); } console.log(''); } // 工具分布 console.log('## 工具使用分布'); for (const [tool, count] of report.toolUsage) { console.log(` ${tool.padEnd(15)} ${count}`); } console.log(''); // 安全 if (security.total > 0) { console.log('## 安全事件'); for (const [hook, stats] of Object.entries(security.byHook)) { console.log(` ${hook.padEnd(30)} deny=${stats.deny} ask=${stats.ask}`); } console.log(''); } // 版本变更 if (evolutionEntries.length > 0) { console.log('## 版本变更'); for (const e of evolutionEntries) { console.log(` - **${e.version}** (${e.fix_count || 0} fixes): ${(e.summary || '').slice(0, 80)}`); } console.log(''); } // v6.5: Browserbase session 报告 if (browserbase) { console.log('## Browserbase Session 监控'); console.log(` 总调用: ${browserbase.totalCalls}`); console.log(` Session: ${browserbase.sessionsCreated} 创建 / ${browserbase.sessionsClosed} 关闭`); console.log(` 失败率: ${browserbase.failureRate}% (${browserbase.failedCalls} 失败)`); if (browserbase.avgDurationSec > 0) { console.log(` 平均时长: ${browserbase.avgDurationSec}s`); } console.log(' 工具 TOP:'); for (const [tool, count] of browserbase.topTools) { console.log(` ${tool.padEnd(35)} ${count}`); } console.log(''); } // P1-2: 健康趋势预测 if (forecast) { console.log('## 健康趋势预测'); console.log(` 数据点: ${forecast.dataPoints}`); console.log(` 下期预测: ${forecast.forecast} fixes`); console.log(` 趋势: ${forecast.trend === 'rising' ? '↑ 上升' : forecast.trend === 'falling' ? '↓ 下降' : '→ 稳定'}`); if (forecast.spikes > 0) { console.log(` 异常尖峰: ${forecast.spikes} 个`); } console.log(''); } console.log('---'); console.log(`*Generated at ${new Date().toISOString()}*`); } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { module.exports = { dateOffset, loadJsonlInRange, analyzeSkills, analyzeSecurity, dailyTrend, loadEvolution, analyzeBrowserbase, loadHealthForecast, recordForecast, main, }; } if (require.main === module) { main(); }