#!/usr/bin/env node /** * Bookworm 活动日志实时查看器 * * 使用方式: * node watch-activity.js # 实时 tail 今日日志 * node watch-activity.js --date 2026-02-19 # 查看历史 * node watch-activity.js --filter mcp,bash # 分类过滤 * node watch-activity.js --stats # 统计摘要 * * 特性: * - ANSI 彩色输出 (skill=紫, agent=青, mcp=黄, bash=绿, write=蓝) * - fs.watchFile 轮询 (500ms),兼容 WSL+NTFS * - 增量读取:只读文件新增字节 * - --stats 模式:事件类型直方图 + Top 10 工具排名 */ const fs = require('fs'); const path = require('path'); // 路径检测: 优先 paths.config.js,回退环境检测 let CLAUDE_ROOT; try { CLAUDE_ROOT = require('./paths.config.js').PATHS.root; } catch { const IS_WSL = process.platform === 'linux' && fs.existsSync('/mnt/c'); CLAUDE_ROOT = IS_WSL ? '/mnt/c/Users/janson9527us/.claude' : 'C:/Users/janson9527us/.claude'; } const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug'); // ANSI 颜色 const C = { skill: '\x1b[35m', // 紫 agent: '\x1b[36m', // 青 mcp: '\x1b[33m', // 黄 bash: '\x1b[32m', // 绿 write: '\x1b[34m', // 蓝 reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', }; // 解析命令行参数 function parseArgs() { const args = process.argv.slice(2); const opts = { date: null, filter: null, stats: false }; for (let i = 0; i < args.length; i++) { if (args[i] === '--date' && args[i + 1]) { opts.date = args[++i]; } else if (args[i] === '--filter' && args[i + 1]) { opts.filter = new Set(args[++i].split(',')); } else if (args[i] === '--stats') { opts.stats = true; } } return opts; } // 获取日志文件路径 function getLogFile(dateStr) { return path.join(DEBUG_DIR, `activity-${dateStr}.jsonl`); } // UTC ISO 字符串 → 本地时间 HH:MM:SS function toLocalTime(isoStr) { if (!isoStr) return '??:??:??'; const d = new Date(isoStr); if (isNaN(d.getTime())) return '??:??:??'; return d.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // 格式化单行日志输出 function formatEntry(entry) { const color = C[entry.event] || ''; const time = toLocalTime(entry.ts); const tag = (entry.event || '?').toUpperCase().padEnd(5); const detail = entry.detail || ''; return `${C.dim}${time}${C.reset} ${color}${tag}${C.reset} ${entry.tool} ${C.dim}${detail}${C.reset}`; } // --stats 统计模式 function showStats(logFile) { if (!fs.existsSync(logFile)) { console.log('日志文件不存在:', logFile); process.exit(1); } const content = fs.readFileSync(logFile, 'utf8').trim(); if (!content) { console.log('日志为空'); process.exit(0); } const lines = content.split('\n'); const eventCounts = {}; const toolCounts = {}; let validCount = 0; for (const line of lines) { try { const entry = JSON.parse(line); eventCounts[entry.event] = (eventCounts[entry.event] || 0) + 1; toolCounts[entry.tool] = (toolCounts[entry.tool] || 0) + 1; validCount++; } catch { /* 跳过无效行 */ } } if (validCount === 0) { console.log('无有效日志条目'); process.exit(0); } console.log(`\n${C.bold}活动统计 — ${path.basename(logFile)}${C.reset}`); console.log(`总计: ${validCount} 条事件\n`); // 事件类型直方图 console.log(`${C.bold}事件类型分布:${C.reset}`); const maxCount = Math.max(...Object.values(eventCounts)); const barWidth = 30; const sortedEvents = Object.entries(eventCounts).sort((a, b) => b[1] - a[1]); for (const [event, count] of sortedEvents) { const color = C[event] || ''; const bar = '█'.repeat(Math.max(1, Math.ceil(count / maxCount * barWidth))); console.log(` ${color}${event.padEnd(6)}${C.reset} ${String(count).padStart(4)} ${color}${bar}${C.reset}`); } // Top 10 工具排名 console.log(`\n${C.bold}Top 10 工具:${C.reset}`); const sortedTools = Object.entries(toolCounts).sort((a, b) => b[1] - a[1]).slice(0, 10); for (let i = 0; i < sortedTools.length; i++) { const [tool, count] = sortedTools[i]; console.log(` ${String(i + 1).padStart(2)}. ${tool.padEnd(50)} ${count}`); } console.log(''); } // 实时 tail 模式 function tailWatch(logFile, filterSet) { console.log(`${C.bold}活动日志 — ${path.basename(logFile)}${C.reset}`); console.log(`${C.dim}Ctrl+C 退出${filterSet ? ` | 过滤: ${[...filterSet].join(',')}` : ''}${C.reset}\n`); // 读取现有内容(最后 20 行作为上下文) let fileSize = 0; if (fs.existsSync(logFile)) { const content = fs.readFileSync(logFile, 'utf8'); const lines = content.trim().split('\n').filter(Boolean); const recent = lines.slice(-20); fileSize = Buffer.byteLength(content, 'utf8'); for (const line of recent) { try { const entry = JSON.parse(line); if (!filterSet || filterSet.has(entry.event)) { console.log(formatEntry(entry)); } } catch { /* 跳过 */ } } if (recent.length > 0) { console.log(`${C.dim}--- 以上为历史 (最近 ${recent.length} 条) ---${C.reset}\n`); } } // 增量轮询 (500ms),兼容 WSL+NTFS const pollInterval = setInterval(() => { try { if (!fs.existsSync(logFile)) return; const stat = fs.statSync(logFile); if (stat.size <= fileSize) return; // 只读新增字节,不重读全文件 const fd = fs.openSync(logFile, 'r'); const newSize = stat.size - fileSize; const buffer = Buffer.alloc(newSize); fs.readSync(fd, buffer, 0, newSize, fileSize); fs.closeSync(fd); fileSize = stat.size; const newContent = buffer.toString('utf8'); const newLines = newContent.trim().split('\n').filter(Boolean); for (const line of newLines) { try { const entry = JSON.parse(line); if (!filterSet || filterSet.has(entry.event)) { console.log(formatEntry(entry)); } } catch { /* 跳过 */ } } } catch { /* 轮询异常静默处理 */ } }, 500); // 优雅退出 process.on('SIGINT', () => { clearInterval(pollInterval); console.log(`\n${C.dim}已停止监听${C.reset}`); process.exit(0); }); } // 主程序 function main() { const opts = parseArgs(); const dateStr = opts.date || new Date().toISOString().slice(0, 10); const logFile = getLogFile(dateStr); if (opts.stats) { showStats(logFile); } else { tailWatch(logFile, opts.filter); } } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { module.exports = { C, parseArgs, getLogFile, toLocalTime, formatEntry, showStats, tailWatch, main }; } if (require.main === module) { main(); }