- 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)
151 lines
5.2 KiB
JavaScript
151 lines
5.2 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 (/* 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);
|
||
});
|
||
}
|