#!/usr/bin/env node /** * PostToolUse Hook: 统一编辑派遣器 (v5.2 性能优化) * 匹配器: Edit|Write|NotebookEdit * * 合并 5 个 PostToolUse 钩子为单进程,核心优化: * 1. 单次 stdin 读取 (避免 5× JSON 解析开销) * 2. check-typescript + check-lint 并行执行 (省 ~10s) * 3. 轻量级检查 (suggest-tests/drift-detector/integrity-check) 同步内联 * * 退出码: 0=通过, 2=有反馈(continue=true) * 容错: 任何子检查异常不影响其他检查,fail-open */ const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const { isEnabled } = require('../scripts/feature-flags.js'); const MAX_STDIN_SIZE = 1024 * 1024; const HOOKS_DIR = __dirname; // === 共享常量 === const TS_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts']; const LINT_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.vue']; const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs']; // === 主入口 === function main() { let rawInput = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { rawInput += chunk; if (rawInput.length > MAX_STDIN_SIZE) { process.exit(0); } }); process.stdin.on('end', async () => { try { const input = JSON.parse(rawInput); // [v6.1-PATCH] hook-priority-scheduler // 初始化优先级调度器,高工具调用次数时自动跳过低优先级钩子 let _toolCallCount = 0; let _scheduler = null; try { _scheduler = require(require('path').join( __dirname.replace(/[/\\]hooks$/, ''), 'scripts', 'hook-priority-scheduler.js' )); _toolCallCount = _scheduler.readToolCallCount(); } catch {} const filePath = (input.tool_input && (input.tool_input.file_path || input.tool_input.filePath || input.tool_input.notebook_path)) || ''; if (!filePath) { process.exit(0); return; } // [OPT-1] 临时文件路径跳过:Desktop/ 下的修复脚本/验证脚本不需要质量检查 if (filePath.includes('Desktop') || filePath.includes('desktop')) { process.exit(0); return; } const ext = path.extname(filePath).toLowerCase(); const messages = []; const dispatchStartMs = Date.now(); // [P3-6] 计时起点 // 并行分组: // 组 A (重型, 并行): check-typescript + check-lint // 组 B (轻型, 同步): suggest-tests + drift-detector + integrity-check const heavyChecks = []; // --- 重型检查: TypeScript 编译 --- if (TS_EXTENSIONS.includes(ext)) { // [v6.1-PATCH] hook-priority-scheduler: check-typescript 为 medium 优先级 if (!_scheduler || _scheduler.shouldExecute('check-typescript', _toolCallCount)) { heavyChecks.push(runSubHook('check-typescript.js', rawInput)); } } // --- 重型检查: ESLint --- if (LINT_EXTENSIONS.includes(ext)) { // [v6.1-PATCH] hook-priority-scheduler: check-lint 为 medium 优先级 if (!_scheduler || _scheduler.shouldExecute('check-lint', _toolCallCount)) { heavyChecks.push(runSubHook('check-lint.js', rawInput)); } } // --- 轻型检查: 测试提醒 (委托 suggest-tests.js) --- const testMsg = checkSuggestTests(filePath, ext, input.tool_name); if (testMsg) messages.push(testMsg); // --- 轻型检查: 漂移检测 (委托 drift-detector.js) --- const driftMsg = checkDriftDetector(filePath); if (driftMsg) messages.push(driftMsg); // --- 轻型检查: 完整性校验 (内联) --- const integrityMsg = checkIntegrity(filePath); if (integrityMsg) messages.push(integrityMsg); // --- Phase 2.2: 大变更质量提醒 (轻型, <1ms) --- try { const content = (input.tool_input && (input.tool_input.content || input.tool_input.new_string)) || ''; const isHookFile = filePath && /[/\\]hooks[/\\]/.test(filePath); if (content.length > 500 || isHookFile) { const lineCount = content.split(String.fromCharCode(10)).length; const isSourceCode = SOURCE_EXTENSIONS.includes(ext); let hint = '[quality-reminder] 较大变更 (' + content.length + ' chars' + (isHookFile ? ', hooks基础设施' : '') + '),建议完成后运行构建验证。'; if (isSourceCode && lineCount > 200) { hint += ' 大规模源码变更 (' + lineCount + ' 行),建议运行 /review 进行代码审查。'; } messages.push(hint); } } catch {} // --- #4 合并: 反模式检测 (原 post-edit-quality-check.js) --- // [v6.2-PATCH] feature-flag 检查: 仅在 post-edit-quality-check 启用时调用 if (isEnabled('post-edit-quality-check')) { try { const qc = require('./post-edit-quality-check.js'); if (qc && qc.detectAntiPatterns && qc.extractContent) { const qcContent = qc.extractContent(input); if (qcContent && /.(?:js|ts|jsx|tsx)$/.test(filePath)) { const findings = qc.detectAntiPatterns(qcContent); if (findings.length > 0) { const items = findings.map(f => ' [' + f.severity.toUpperCase() + '][' + f.id + '] L' + f.line + ': ' + f.label).join('\n'); messages.push('[quality-check] ' + path.basename(filePath) + ':\n' + items); try { qc.updateDetectionStats(findings, filePath); } catch {} } } } } catch {} } // --- #4 合并: 宪法反腐败检测 (原 constitution-guard.js) --- // [v6.2-PATCH] feature-flag 检查: 仅在 constitution-guard 启用时调用 if (isEnabled('constitution-guard')) { try { const cg = require('./constitution-guard.js'); if (cg && cg.detectConstitutionViolations && cg.extractContent) { const cgContent = cg.extractContent(input); if (cgContent) { const violations = cg.detectConstitutionViolations(cgContent, filePath); if (violations.length > 0) { const items = violations.map(v => ' [' + v.severity.toUpperCase() + '][' + v.id + '] ' + (v.line ? 'L' + v.line + ': ' : '') + v.label).join('\n'); messages.push('[constitution-guard] ' + path.basename(filePath) + ':\n' + items); } } } } catch {} } // 等待重型检查完成 (并行) if (heavyChecks.length > 0) { const results = await Promise.all(heavyChecks); for (const r of results) { if (r) messages.push(r); } } // 合并输出 if (messages.length === 0) { process.exit(0); return; } // [P2-4] 输出截断: 防止大量 lint/ts 错误撑爆上下文 const MAX_SYSTEM_MESSAGE_CHARS = 2000; let finalMessage = messages.join('\n\n'); if (finalMessage.length > MAX_SYSTEM_MESSAGE_CHARS) { const origLen = finalMessage.length; finalMessage = finalMessage.slice(0, MAX_SYSTEM_MESSAGE_CHARS) + '\n\n[...截断: ' + origLen + ' chars, 详见 debug/hook-output.log]'; try { const logPath = require('path').join(__dirname, '..', 'debug', 'hook-output.log'); require('fs').appendFileSync(logPath, '[' + new Date().toISOString() + ']\n' + messages.join('\n') + '\n---\n'); } catch {} } // [P3-6] 记录 post-edit 总耗时 try { const elapsed = Date.now() - dispatchStartMs; const tPath = require('path').join(__dirname, '..', 'debug', 'hook-timing.jsonl'); require('fs').appendFileSync(tPath, JSON.stringify({ ts: new Date().toISOString(), hook: 'post-edit-dispatcher', elapsed, msgCount: messages.length, msgChars: finalMessage.length }) + '\n'); } catch {} process.stderr.write(JSON.stringify({ continue: true, suppressOutput: false, systemMessage: finalMessage, })); process.exit(2); } catch (e) { // P2.3 错误日志化: 静默 fail-open 前先 append 到 hook-errors.log,保留可观测性 try { const errLog = require('path').join(require('./lib/root.js'), 'debug', 'hook-errors.log'); require('fs').appendFileSync(errLog, JSON.stringify({ ts: new Date().toISOString(), hook: 'post-edit-dispatcher', level: 'FATAL', msg: (e && e.message) || String(e), stack: (e && e.stack) ? e.stack.slice(0, 500) : null, }) + '\n'); } catch {} process.exit(0); // fail-open } }); } // === 运行子钩子进程 (并行) === function runSubHook(hookFile, stdinData) { return new Promise((resolve) => { const hookPath = path.join(HOOKS_DIR, hookFile); if (!fs.existsSync(hookPath)) { resolve(null); return; } const child = spawn('node', [hookPath], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 15000, }); let stderr = ''; child.stderr.on('data', (d) => { stderr += d; }); child.stdout.on('data', () => {}); // 消耗 stdout 防阻塞 child.on('close', (code) => { if (code === 2 && stderr) { try { const parsed = JSON.parse(stderr); resolve(parsed.systemMessage || null); } catch { resolve(null); } } else { resolve(null); } }); child.on('error', () => resolve(null)); // 将原始 stdin 传递给子进程 child.stdin.write(stdinData); child.stdin.end(); }); } // === 委托: 测试提醒 (suggest-tests.js — 8 路径候选 + 16 跳过模式) === function checkSuggestTests(filePath, ext, toolName) { try { const st = require('./suggest-tests.js'); if (st && st.inlineCheck) { return st.inlineCheck(filePath, ext, toolName); } } catch {} return null; } // === 委托: 漂移检测 (drift-detector.js — 含 generate-stats 自动触发 + 宪法变更检测) === function checkDriftDetector(filePath) { try { const dd = require('./drift-detector.js'); if (dd && dd.inlineCheck) { return dd.inlineCheck(filePath); } } catch {} return null; } // === 内联: 完整性校验 (P1-4: 委托 integrity-check.js 含 HMAC 验证) === function checkIntegrity(filePath) { try { const integrityMod = require('./integrity-check.js'); if (integrityMod && integrityMod.inlineCheck) { return integrityMod.inlineCheck(filePath); } } catch {} return null; } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { module.exports = { checkSuggestTests, checkDriftDetector, checkIntegrity, runSubHook }; } if (require.main === module) { main(); }