#!/usr/bin/env node /** * 日志轮转脚本 (Stop hook) * 清理 debug/ 目录中超过 7 天的日志文件 * 截断大于 500KB 的反馈/指标文件 */ const fs = require('fs'); const path = require('path'); const ROOT = path.dirname(__dirname).includes('.claude') ? path.dirname(__dirname) : path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude'); const DEBUG = path.join(ROOT, 'debug'); /** * 执行日志轮转逻辑 (供 dispatcher 调用) * 不调用 process.exit,不读取 stdin * @returns {number} 清理的文件数 */ function runRotation() { const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 天 const MAX_FILE_SIZE = 500 * 1024; // 500KB const now = Date.now(); let cleaned = 0; if (!fs.existsSync(DEBUG)) return 0; try { const files = fs.readdirSync(DEBUG); for (const f of files) { const fp = path.join(DEBUG, f); try { const stat = fs.statSync(fp); // 删除旧的日期分片日志 (activity-*, trace-*, outcome-*, compliance-*, route-2*, security-*) if (/^(activity|trace|outcome|compliance|route-2|security|route-stats-daily)-/.test(f) && f.endsWith('.jsonl')) { if (now - stat.mtimeMs > MAX_AGE_MS) { fs.unlinkSync(fp); cleaned++; } } // 截断超大的累积日志文件 // P1-V04: route-metrics/ab-experiments 轮转已移至 auto-cleanup.js (5000/2000 行策略) // RT-V5: route-feedback.jsonl 保留 5000 行 (学习历史),其他保留 200 行 if (['route-feedback.jsonl', 'constitution-report.jsonl', 'route-blind-spots.jsonl', 'skill-implicit-feedback.jsonl', 'actual-skills.jsonl'].includes(f)) { if (stat.size > MAX_FILE_SIZE) { const keepLines = (f === 'route-feedback.jsonl') ? 5000 : 200; const content = fs.readFileSync(fp, 'utf8'); const lines = content.trim().split('\n'); const kept = lines.slice(-keepLines).join('\n') + '\n'; const tmpFile = fp + '.tmp.' + process.pid; fs.writeFileSync(tmpFile, kept); fs.renameSync(tmpFile, fp); cleaned++; } } } catch {} } // V13/V20 修复: weights-history 快照清理 + evolution-log 轮转 try { const histDir = path.join(ROOT, 'debug', 'weights-history'); if (fs.existsSync(histDir)) { const hFiles = fs.readdirSync(histDir).filter(x => x.startsWith('weights-')).sort(); const MAX_HIST = 20; if (hFiles.length > MAX_HIST) { for (let hi = 0; hi < hFiles.length - MAX_HIST; hi++) { fs.unlinkSync(path.join(histDir, hFiles[hi])); cleaned++; } } } } catch {} // P1-V03: evolution-log 轮转已移至 auto-cleanup.js (2000 行策略),此处不再重复处理 // v6.4: 清理孤儿 .tmp 文件 (W3 审计发现) // safe-append.js 和轮转操作创建 .tmp.PID 文件,进程异常退出时残留 try { const tmpFiles = fs.readdirSync(DEBUG).filter(tf => /\.tmp\.\d+$/.test(tf)); const ONE_HOUR = 60 * 60 * 1000; for (const tf of tmpFiles) { const tfp = path.join(DEBUG, tf); try { const stat = fs.statSync(tfp); if (now - stat.mtimeMs > ONE_HOUR) { fs.unlinkSync(tfp); cleaned++; } } catch {} } } catch {} if (cleaned > 0) { console.error('[log-rotator] cleaned ' + cleaned + ' files'); } } catch {} // P2-12: hook-errors.log 轮转 (保留最近 100KB) try { const hookErrLog = path.join(DEBUG, 'hook-errors.log'); if (fs.existsSync(hookErrLog)) { const stat = fs.statSync(hookErrLog); if (stat.size > 100 * 1024) { const content = fs.readFileSync(hookErrLog, 'utf8'); const lines = content.split('\n'); const kept = lines.slice(-500).join('\n'); const tmpFile = hookErrLog + '.tmp.' + process.pid; fs.writeFileSync(tmpFile, kept); fs.renameSync(tmpFile, hookErrLog); } } } catch {} // P3+: hook-slow.log 轮转 (与 hook-errors 相同策略: >100KB 保留 tail-500) try { const hookSlowLog = path.join(DEBUG, 'hook-slow.log'); if (fs.existsSync(hookSlowLog)) { const stat = fs.statSync(hookSlowLog); if (stat.size > 100 * 1024) { const content = fs.readFileSync(hookSlowLog, 'utf8'); const lines = content.split('\n'); const kept = lines.slice(-500).join('\n'); const tmpFile = hookSlowLog + '.tmp.' + process.pid; fs.writeFileSync(tmpFile, kept); fs.renameSync(tmpFile, hookSlowLog); } } } catch {} return cleaned; } // 模块导出 (供 dispatcher 和测试使用) if (typeof module !== 'undefined') { module.exports = { runRotation }; } // 独立运行模式: 消费 stdin 后执行 if (require.main === module) { const readStdin = require('./lib/read-stdin.js'); readStdin({ maxSize: 512 * 1024 }).then(() => { runRotation(); process.exit(0); }).catch(() => { runRotation(); process.exit(0); }); }