#!/usr/bin/env node /** * W3 补丁 — 2026-04-16 audit-2026-04-16 WARNING * * 问题: stop-dispatcher.js L172 慢 Stop 事件 (>2s) 静默写入 hook-slow.log, * 无 stderr 输出,Claude Code 界面无法感知;log-rotator 已轮转 (P3+), * 但用户不知道问题存在。 * * 修复方案: * (a) 在 appendFileSync 后追加 process.stderr.write 告警 * (b) 基于文件 mtime 的 60s 冷却窗口防刷屏 * (c) 冷却 sentinel 路径: debug/.slow-alert.sentinel * * 冷却逻辑: * - 读 sentinel mtime,若 now - mtime < 60000ms 则只写文件不输出 stderr * - 否则输出 stderr + 更新 sentinel mtime * - 文件不存在视为冷却已过 * * 文件: hooks/stop-dispatcher.js (L168-179 块) * * 幂等: sentinel = 'slow-alert.sentinel' */ 'use strict'; const fs = require('fs'); const path = require('path'); const TARGET = path.join(__dirname, '..', '..', 'hooks', 'stop-dispatcher.js'); const SENTINEL = 'slow-alert.sentinel'; function main() { if (!fs.existsSync(TARGET)) { console.error('[patch-w3] 目标文件不存在'); process.exit(1); } const before = fs.readFileSync(TARGET, 'utf8'); if (before.includes(SENTINEL)) { console.log('[patch-w3] 已打过补丁,跳过'); process.exit(0); } const anchor = ' // P2.1 总耗时记录: runAll 超过 2s 视为慢 Stop,写 slow.log 辅助诊断\n' + ' try {\n' + ' const _total = Date.now() - _perf_start;\n' + ' if (_total > 2000) {\n' + ' const errLog = path.join(require(\'./lib/root.js\'), \'debug\', \'hook-slow.log\');\n' + ' fs.appendFileSync(errLog, JSON.stringify({\n' + ' ts: new Date().toISOString(),\n' + ' hook: \'stop-dispatcher\',\n' + ' totalMs: _total,\n' + ' }) + \'\\n\');\n' + ' }\n' + ' } catch {}'; const replacement = ' // P2.1 总耗时记录: runAll 超过 2s 视为慢 Stop,写 slow.log 辅助诊断\n' + ' // W3 (2026-04-16): 追加 stderr 告警 + 60s 冷却窗口防刷屏\n' + ' try {\n' + ' const _total = Date.now() - _perf_start;\n' + ' if (_total > 2000) {\n' + ' const debugDir = path.join(require(\'./lib/root.js\'), \'debug\');\n' + ' const errLog = path.join(debugDir, \'hook-slow.log\');\n' + ' fs.appendFileSync(errLog, JSON.stringify({\n' + ' ts: new Date().toISOString(),\n' + ' hook: \'stop-dispatcher\',\n' + ' totalMs: _total,\n' + ' }) + \'\\n\');\n' + ' // W3: 冷却聚合 — 60s 内仅告警 1 次防刷屏\n' + ' try {\n' + ' const alertSentinel = path.join(debugDir, \'.slow-alert.sentinel\');\n' + ' const now = Date.now();\n' + ' let lastAlert = 0;\n' + ' try {\n' + ' if (fs.existsSync(alertSentinel)) lastAlert = fs.statSync(alertSentinel).mtimeMs || 0;\n' + ' } catch {}\n' + ' if (now - lastAlert > 60000) {\n' + ' process.stderr.write(\'[stop-dispatcher] 慢 Stop 事件 (\' + _total + \'ms) — 详见 debug/hook-slow.log\\n\');\n' + ' try {\n' + ' const _tmp = alertSentinel + \'.tmp.\' + process.pid;\n' + ' fs.writeFileSync(_tmp, String(now));\n' + ' fs.renameSync(_tmp, alertSentinel);\n' + ' } catch {}\n' + ' }\n' + ' } catch {}\n' + ' }\n' + ' } catch {}'; if (!before.includes(anchor)) { console.error('[patch-w3] 锚点未匹配'); process.exit(2); } const after = before.replace(anchor, replacement); if (after === before) { console.error('[patch-w3] 替换无效果'); process.exit(3); } const backup = TARGET + '.bak.w3.' + Date.now(); fs.writeFileSync(backup, before); const _tmp = TARGET + '.tmp.' + process.pid; fs.writeFileSync(_tmp, after); fs.renameSync(_tmp, TARGET); console.log('[patch-w3] ✓ stop-dispatcher.js 慢 Stop 告警通道已加装'); console.log('[patch-w3] 备份: ' + path.basename(backup)); console.log('[patch-w3] 冷却窗口: 60s, sentinel = debug/.slow-alert.sentinel'); } try { main(); } catch (e) { console.error('[patch-w3] 异常:', e.message); process.exit(99); }