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