bookworm-smart-assistant/hooks/post-edit-dispatcher.js

292 lines
11 KiB
JavaScript
Raw Permalink Normal View History

#!/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 {}
}
// --- REVIEW_REPORT_REQUIRED_v1: 宪法 2.1 审查报告强制提醒 (P1-3, 2026-04-25) ---
try {
const rr = require('./review-report-checker.js');
if (rr && rr.inlineCheck) {
const reviewMsg = rr.inlineCheck(filePath, input);
if (reviewMsg) messages.push(reviewMsg);
}
} catch (_e) {}
// 等待重型检查完成 (并行)
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();
}