bookworm-smart-assistant/scripts/weekly-report.js

381 lines
12 KiB
JavaScript
Raw Permalink Normal View History

#!/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();
}