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