242 lines
7.2 KiB
JavaScript
242 lines
7.2 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* 技能使用报告 - 退役机制分析工具
|
||
|
|
*
|
||
|
|
* 用法:
|
||
|
|
* node scripts/skill-usage-report.js # CLI 报告
|
||
|
|
* node scripts/skill-usage-report.js --json # JSON 输出
|
||
|
|
* node scripts/skill-usage-report.js --days 90 # 指定分析天数 (默认 30)
|
||
|
|
*
|
||
|
|
* 分析内容:
|
||
|
|
* - 每个技能的调用次数和最后使用时间
|
||
|
|
* - 标记 dormant (30天未调用) 和 candidate-for-archive (90天未调用)
|
||
|
|
* - Agent 和 MCP 使用统计
|
||
|
|
*
|
||
|
|
* 数据源: debug/activity-*.jsonl
|
||
|
|
*/
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
// 动态检测配置根目录
|
||
|
|
function detectClaudeRoot() {
|
||
|
|
if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
|
||
|
|
const selfDir = path.dirname(__filename);
|
||
|
|
if (selfDir.includes('.claude')) return selfDir.replace(/[/\\]scripts$/, '');
|
||
|
|
try { return require('./paths.config.js').PATHS.root; } catch { return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\/g, '/') + '/.claude'; }
|
||
|
|
}
|
||
|
|
|
||
|
|
const CLAUDE_ROOT = detectClaudeRoot();
|
||
|
|
const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug');
|
||
|
|
const SKILLS_DIR = path.join(CLAUDE_ROOT, 'skills');
|
||
|
|
|
||
|
|
// === 参数解析 ===
|
||
|
|
const args = process.argv.slice(2);
|
||
|
|
const jsonMode = args.includes('--json');
|
||
|
|
const daysIdx = args.indexOf('--days');
|
||
|
|
const analysisDays = (daysIdx >= 0 && parseInt(args[daysIdx + 1])) || 30;
|
||
|
|
|
||
|
|
// === 数据加载 ===
|
||
|
|
function loadAllActivityLogs(days) {
|
||
|
|
const entries = [];
|
||
|
|
const now = new Date();
|
||
|
|
|
||
|
|
for (let i = 0; i < days; i++) {
|
||
|
|
const d = new Date(now);
|
||
|
|
d.setDate(d.getDate() - i);
|
||
|
|
const dateStr = d.toISOString().slice(0, 10);
|
||
|
|
const logFile = path.join(DEBUG_DIR, `activity-${dateStr}.jsonl`);
|
||
|
|
|
||
|
|
if (!fs.existsSync(logFile)) continue;
|
||
|
|
const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
|
||
|
|
for (const line of lines) {
|
||
|
|
if (!line) continue;
|
||
|
|
try { entries.push(JSON.parse(line)); } catch {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getRegisteredSkills() {
|
||
|
|
// 从 skills/ 目录读取所有已注册技能
|
||
|
|
const skills = new Set();
|
||
|
|
try {
|
||
|
|
for (const dir of fs.readdirSync(SKILLS_DIR)) {
|
||
|
|
const skillFile = path.join(SKILLS_DIR, dir, 'SKILL.md');
|
||
|
|
if (fs.existsSync(skillFile)) {
|
||
|
|
skills.add(dir);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch {}
|
||
|
|
return skills;
|
||
|
|
}
|
||
|
|
|
||
|
|
// === 分析 ===
|
||
|
|
function analyze(entries) {
|
||
|
|
const registeredSkills = getRegisteredSkills();
|
||
|
|
const today = new Date();
|
||
|
|
|
||
|
|
// 技能使用统计
|
||
|
|
const skillStats = {};
|
||
|
|
for (const skill of registeredSkills) {
|
||
|
|
skillStats[skill] = { count: 0, lastUsed: null };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Agent 统计
|
||
|
|
const agentStats = {};
|
||
|
|
// MCP 统计
|
||
|
|
const mcpStats = {};
|
||
|
|
|
||
|
|
for (const entry of entries) {
|
||
|
|
if (entry.event === 'skill' && entry.detail) {
|
||
|
|
const name = entry.detail;
|
||
|
|
if (!skillStats[name]) {
|
||
|
|
skillStats[name] = { count: 0, lastUsed: null };
|
||
|
|
}
|
||
|
|
skillStats[name].count++;
|
||
|
|
const ts = entry.ts;
|
||
|
|
if (!skillStats[name].lastUsed || ts > skillStats[name].lastUsed) {
|
||
|
|
skillStats[name].lastUsed = ts;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.event === 'agent' && entry.detail) {
|
||
|
|
const name = entry.detail;
|
||
|
|
agentStats[name] = (agentStats[name] || 0) + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.event === 'mcp' && entry.detail) {
|
||
|
|
const name = entry.detail;
|
||
|
|
mcpStats[name] = (mcpStats[name] || 0) + 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 分类技能状态
|
||
|
|
const dormantThreshold = 30;
|
||
|
|
const archiveThreshold = 90;
|
||
|
|
|
||
|
|
const results = {
|
||
|
|
period: `${analysisDays} days`,
|
||
|
|
totalEvents: entries.length,
|
||
|
|
skills: {
|
||
|
|
active: [],
|
||
|
|
dormant: [],
|
||
|
|
candidateForArchive: [],
|
||
|
|
neverUsed: [],
|
||
|
|
},
|
||
|
|
agents: agentStats,
|
||
|
|
mcps: mcpStats,
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const [name, stats] of Object.entries(skillStats)) {
|
||
|
|
const item = { name, count: stats.count, lastUsed: stats.lastUsed };
|
||
|
|
|
||
|
|
if (stats.count === 0) {
|
||
|
|
results.skills.neverUsed.push(item);
|
||
|
|
} else if (stats.lastUsed) {
|
||
|
|
const lastDate = new Date(stats.lastUsed);
|
||
|
|
const daysSince = Math.floor((today - lastDate) / (1000 * 60 * 60 * 24));
|
||
|
|
item.daysSinceLastUse = daysSince;
|
||
|
|
|
||
|
|
if (daysSince >= archiveThreshold) {
|
||
|
|
results.skills.candidateForArchive.push(item);
|
||
|
|
} else if (daysSince >= dormantThreshold) {
|
||
|
|
results.skills.dormant.push(item);
|
||
|
|
} else {
|
||
|
|
results.skills.active.push(item);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
results.skills.active.push(item);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 按调用次数排序
|
||
|
|
results.skills.active.sort((a, b) => b.count - a.count);
|
||
|
|
results.skills.dormant.sort((a, b) => (a.daysSinceLastUse || 0) - (b.daysSinceLastUse || 0));
|
||
|
|
results.skills.candidateForArchive.sort((a, b) => (a.daysSinceLastUse || 0) - (b.daysSinceLastUse || 0));
|
||
|
|
results.skills.neverUsed.sort((a, b) => a.name.localeCompare(b.name));
|
||
|
|
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
|
||
|
|
// === CLI 渲染 ===
|
||
|
|
function renderCli(results) {
|
||
|
|
console.log();
|
||
|
|
console.log(`=== Skill Usage Report (${results.period}, ${results.totalEvents} events) ===`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// Active
|
||
|
|
const active = results.skills.active;
|
||
|
|
console.log(`Active Skills (${active.length}):`);
|
||
|
|
if (active.length > 0) {
|
||
|
|
for (const s of active.slice(0, 15)) {
|
||
|
|
const last = s.lastUsed ? s.lastUsed.slice(0, 10) : 'N/A';
|
||
|
|
console.log(` ${s.name.padEnd(35)} ${String(s.count).padStart(4)} calls last: ${last}`);
|
||
|
|
}
|
||
|
|
if (active.length > 15) console.log(` ... and ${active.length - 15} more`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Never used
|
||
|
|
const neverUsed = results.skills.neverUsed;
|
||
|
|
if (neverUsed.length > 0) {
|
||
|
|
console.log();
|
||
|
|
console.log(`Never Used in ${results.period} (${neverUsed.length}):`);
|
||
|
|
for (const s of neverUsed) {
|
||
|
|
console.log(` ${s.name}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Dormant
|
||
|
|
const dormant = results.skills.dormant;
|
||
|
|
if (dormant.length > 0) {
|
||
|
|
console.log();
|
||
|
|
console.log(`Dormant - 30+ days idle (${dormant.length}):`);
|
||
|
|
for (const s of dormant) {
|
||
|
|
console.log(` ${s.name.padEnd(35)} ${s.daysSinceLastUse}d idle (${s.count} total calls)`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Archive candidates
|
||
|
|
const archive = results.skills.candidateForArchive;
|
||
|
|
if (archive.length > 0) {
|
||
|
|
console.log();
|
||
|
|
console.log(`Archive Candidates - 90+ days idle (${archive.length}):`);
|
||
|
|
for (const s of archive) {
|
||
|
|
console.log(` ${s.name.padEnd(35)} ${s.daysSinceLastUse}d idle (${s.count} total calls)`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Summary
|
||
|
|
console.log();
|
||
|
|
console.log('Summary:');
|
||
|
|
console.log(` Registered: ${active.length + neverUsed.length + dormant.length + archive.length}`);
|
||
|
|
console.log(` Active: ${active.length} | Never used: ${neverUsed.length} | Dormant: ${dormant.length} | Archive: ${archive.length}`);
|
||
|
|
console.log();
|
||
|
|
}
|
||
|
|
|
||
|
|
// === 主流程 ===
|
||
|
|
function main() {
|
||
|
|
const entries = loadAllActivityLogs(analysisDays);
|
||
|
|
const results = analyze(entries);
|
||
|
|
|
||
|
|
if (jsonMode) {
|
||
|
|
console.log(JSON.stringify(results, null, 2));
|
||
|
|
} else {
|
||
|
|
renderCli(results);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 模块导出 (供测试使用)
|
||
|
|
if (typeof module !== 'undefined') {
|
||
|
|
module.exports = {
|
||
|
|
loadAllActivityLogs,
|
||
|
|
getRegisteredSkills,
|
||
|
|
analyze,
|
||
|
|
main,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
if (require.main === module) {
|
||
|
|
main();
|
||
|
|
}
|