174 lines
5.4 KiB
JavaScript
174 lines
5.4 KiB
JavaScript
|
|
#!/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();
|