bookworm-smart-assistant/hooks/log-rotator.js

151 lines
5.2 KiB
JavaScript
Raw Permalink Normal View History

#!/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 (/* W3_LOG_EXTEND_v1 */ /* AGENT_RETURNS_ROTATE_V66 */ (/^(activity|trace|outcome|compliance|security|route-stats-daily|ab-experiments|hook-timing|skill-outcome|pre-agent-gate|agent-returns)-?/.test(f) || /^route-\d{4}-\d{2}-\d{2}\.jsonl$/.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);
});
}