bookworm-smart-assistant/scripts/agent-usage-report.js

174 lines
5.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/* eslint-disable no-console */
'use strict';
// Agent 工具使用报告生成器
// 分析 debug/activity-*.jsonl 中的 Agent spawn 事件
const fs = require('fs');
const path = require('path');
// 路径配置
const CLAUDE_ROOT = path.resolve(__dirname, '..');
const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug');
// 解析命令行参数 --days N默认 7
function parseDays() {
const idx = process.argv.indexOf('--days');
if (idx !== -1 && process.argv[idx + 1]) {
const n = parseInt(process.argv[idx + 1], 10);
if (!isNaN(n) && n > 0) return n;
}
return 7;
}
// 获取 N 天前的截止日期字符串YYYY-MM-DD
function cutoffDate(days) {
const d = new Date();
d.setUTCDate(d.getUTCDate() - days + 1);
return d.toISOString().slice(0, 10);
}
// 读取单个 jsonl 文件,返回行数组(容错处理)
function readJsonlLines(filePath) {
try {
const text = fs.readFileSync(filePath, 'utf8');
return text.split('\n').filter(Boolean);
} catch (_) {
// 文件不存在或读取失败时静默跳过
return [];
}
}
// 从文件名提取日期activity-YYYY-MM-DD.jsonl → YYYY-MM-DD
function dateFromFilename(filename) {
const m = filename.match(/activity-(\d{4}-\d{2}-\d{2})\.jsonl$/);
return m ? m[1] : null;
}
// 主分析逻辑
function analyze() {
const days = parseDays();
const cutoff = cutoffDate(days);
// 列出所有 activity-*.jsonl 文件
let files = [];
try {
files = fs.readdirSync(DEBUG_DIR)
.filter(f => /^activity-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f))
.sort();
} catch (_) {
console.error(`无法读取目录: ${DEBUG_DIR}`);
process.exit(1);
}
// 按日期过滤:只保留窗口内的文件
files = files.filter(f => {
const d = dateFromFilename(f);
return d && d >= cutoff;
});
if (files.length === 0) {
console.log(`[agent-usage-report] 在最近 ${days} 天内未找到 activity 文件。`);
return;
}
// 聚合数据结构
const dailyCount = {}; // { 'YYYY-MM-DD': count }
const typeCount = {}; // { subagent_type: count }
const descCount = {}; // { description: count }
let totalSpawns = 0;
// 遍历所有文件、逐行解析
for (const file of files) {
const filePath = path.join(DEBUG_DIR, file);
const fileDate = dateFromFilename(file);
const lines = readJsonlLines(filePath);
for (const line of lines) {
let entry;
try {
entry = JSON.parse(line);
} catch (_) {
continue; // 跳过损坏行
}
// 仅处理 Agent spawn 事件
if (entry.event !== 'agent') continue;
// 以事件时间戳的日期为准优先fallback 用文件名日期
const dateKey = entry.ts ? entry.ts.slice(0, 10) : fileDate;
// 解析 detail 字段:"subagent_type:description"
const detail = typeof entry.detail === 'string' ? entry.detail : '';
const colonIdx = detail.indexOf(':');
const agentType = colonIdx > -1 ? detail.slice(0, colonIdx).trim() : detail.trim() || 'unknown';
const agentDesc = colonIdx > -1 ? detail.slice(colonIdx + 1).trim() : '';
// 累计统计
totalSpawns++;
dailyCount[dateKey] = (dailyCount[dateKey] || 0) + 1;
typeCount[agentType] = (typeCount[agentType] || 0) + 1;
if (agentDesc) {
descCount[agentDesc] = (descCount[agentDesc] || 0) + 1;
}
}
}
// ─── 渲染报告 ───────────────────────────────────────────
const line80 = '─'.repeat(60);
console.log('\n' + line80);
console.log(' Agent Tool Usage Report');
console.log(` 分析窗口: 最近 ${days} 天 (${cutoff} 起)`);
console.log(` 总计 Agent 调用次数: ${totalSpawns}`);
console.log(line80);
// 1. Agent 类型分布
console.log('\n[1] Agent 类型分布');
const sortedTypes = Object.entries(typeCount).sort((a, b) => b[1] - a[1]);
if (sortedTypes.length === 0) {
console.log(' (无数据)');
} else {
const maxTypeLen = Math.max(...sortedTypes.map(([k]) => k.length));
for (const [type, count] of sortedTypes) {
const bar = '█'.repeat(Math.round(count / totalSpawns * 20));
console.log(` ${type.padEnd(maxTypeLen)} ${String(count).padStart(4)} ${bar}`);
}
}
// 2. Top 10 最常见描述
console.log('\n[2] Top 10 最常见 Agent 描述');
const sortedDescs = Object.entries(descCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
if (sortedDescs.length === 0) {
console.log(' (无数据)');
} else {
sortedDescs.forEach(([desc, count], i) => {
const truncated = desc.length > 48 ? desc.slice(0, 45) + '...' : desc;
console.log(` ${String(i + 1).padStart(2)}. [${String(count).padStart(3)}x] ${truncated}`);
});
}
// 3. 每日趋势表
console.log('\n[3] 每日 Agent 调用趋势');
const sortedDays = Object.entries(dailyCount).sort((a, b) => a[0].localeCompare(b[0]));
if (sortedDays.length === 0) {
console.log(' (无数据)');
} else {
const maxDay = Math.max(...sortedDays.map(([, c]) => c));
console.log(` ${'日期'.padEnd(12)} ${'次数'.padStart(5)} 趋势`);
console.log(' ' + '─'.repeat(40));
for (const [date, count] of sortedDays) {
const bar = '▪'.repeat(Math.round(count / maxDay * 20));
console.log(` ${date} ${String(count).padStart(5)} ${bar}`);
}
}
console.log('\n' + line80 + '\n');
}
analyze();