#!/usr/bin/env node /** * patch-stop-dispatcher-24h-dedup.js * P2: stop-dispatcher.js consistency-sentinel 加 24h 日级去重 * 修复: (1) 同日不重复写 sentinel (2) version 从 stats-compiled.json 动态读取 * Idempotent: sentinel 防重复 */ 'use strict'; const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = path.join(process.env.USERPROFILE || process.env.HOME, '.claude'); const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks'); const STATE_DIR = path.join(CLAUDE_ROOT, 'session-state'); const SENTINEL = path.join(STATE_DIR, 'patch-stop-dispatcher-24h-dedup.done'); const TARGET = path.join(HOOKS_DIR, 'stop-dispatcher.js'); if (fs.existsSync(SENTINEL)) { console.log('[SKIP] patch-stop-dispatcher-24h-dedup already applied'); process.exit(0); } if (!fs.existsSync(TARGET)) { console.error('[FAIL] Target not found: ' + TARGET); process.exit(1); } const ANCHOR_DEDUP = ` if (findings.length > 0 && fs.existsSync(evoLog)) {`; const PATCH_DEDUP = ` if (findings.length > 0 && fs.existsSync(evoLog)) { // 24h 日级去重: 同日已有 consistency-sentinel 则跳过 (fix_count>0 的除外) const today = new Date().toISOString().slice(0, 10); try { const tail = fs.readFileSync(evoLog, 'utf8').trim().split('\\n').slice(-30); const fixable = findings.some(f => f.fix === 'auto'); const hasTodaySentinel = tail.some(l => { try { const e = JSON.parse(l); return e.scope === 'consistency-sentinel' && e.ts === today; } catch { return false; } }); if (hasTodaySentinel && !fixable) return; } catch {}`; const ANCHOR_VER = ` version: 'v6.5.1',`; const PATCH_VER = ` version: (() => { try { return JSON.parse(fs.readFileSync(path.join(sRoot, 'stats-compiled.json'), 'utf8')).version || 'v6.6.0'; } catch { return 'v6.6.0'; } })(),`; try { const bak = TARGET + '.bak-24h-dedup-' + Date.now(); fs.copyFileSync(TARGET, bak); console.log('[OK] Backup: ' + path.basename(bak)); let src = fs.readFileSync(TARGET, 'utf8'); let patches = 0; // Patch 1: 24h dedup if (src.includes('hasTodaySentinel')) { console.log('[SKIP] 24h dedup already present'); } else { const anchorLF = ANCHOR_DEDUP; const anchorCRLF = ANCHOR_DEDUP.replace(/\n/g, '\r\n'); if (src.includes(anchorCRLF)) { src = src.replace(anchorCRLF, PATCH_DEDUP.replace(/\n/g, '\r\n')); patches++; } else if (src.includes(anchorLF)) { src = src.replace(anchorLF, PATCH_DEDUP); patches++; } else { console.error('[WARN] Dedup anchor not found'); } } // Patch 2: dynamic version if (src.includes("version: 'v6.5.1'")) { const anchorLF = ANCHOR_VER; const anchorCRLF = ANCHOR_VER.replace(/\n/g, '\r\n'); if (src.includes(anchorCRLF)) { src = src.replace(anchorCRLF, PATCH_VER.replace(/\n/g, '\r\n')); patches++; } else if (src.includes(anchorLF)) { src = src.replace(anchorLF, PATCH_VER); patches++; } } else { console.log('[SKIP] version already dynamic or updated'); } if (patches === 0) { console.log('[SKIP] No changes needed'); process.exit(0); } const tmp = TARGET + '.tmp.' + process.pid; fs.writeFileSync(tmp, src, 'utf8'); fs.renameSync(tmp, TARGET); console.log('[OK] ' + path.basename(TARGET) + ' updated (' + patches + ' patches)'); if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); fs.writeFileSync(SENTINEL, JSON.stringify({ applied: new Date().toISOString(), target: path.basename(TARGET), backup: path.basename(bak), patches: patches, fixes: ['24h-dedup', 'dynamic-version'] }, null, 2), 'utf8'); console.log('[DONE] patch-stop-dispatcher-24h-dedup applied'); } catch (err) { console.error('[FAIL] ' + err.message); process.exit(1); }