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

174 lines
5.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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