bookworm-smart-assistant/hooks/post-edit-dispatcher.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- 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)
2026-04-27 17:59:44 +08:00

292 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();
}