151 lines
5.0 KiB
JavaScript
151 lines
5.0 KiB
JavaScript
|
|
#!/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);
|
|||
|
|
});
|
|||
|
|
}
|