bookworm-smart-assistant/hooks/log-rotator.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

151 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});
}