#!/usr/bin/env node /** * 钩子优先级调度器 (v6.0 Phase 3) * * 为钩子数量增长提供优先级调度能力。 * 当会话工具调用次数增加时,自动跳过低优先级钩子, * 减少高频场景下的调度开销。 * * 优先级定义: * critical — 始终执行(安全相关,绝不跳过) * high — 正常执行(路由合规、编辑派遣、宪法守卫) * medium — 可跳过(工具调用次数 > 50 时跳过) * low — 高频场景跳过(工具调用次数 > 20 时跳过) * * 调度策略: * toolCallCount <= 20 : 全部执行 * toolCallCount 21~50 : 跳过 low * toolCallCount > 50 : 跳过 low + medium * critical / high : 始终执行(不受阈值限制) * * 用法: * const scheduler = require('./hook-priority-scheduler.js'); * scheduler.shouldExecute('activity-logger', 25); // → false * scheduler.filterHooks(['post-edit-dispatcher', 'activity-logger'], 25); // → ['post-edit-dispatcher'] */ // ===================================================== // 钩子优先级注册表 // ===================================================== /** * 钩子名称 → 优先级映射 * 新增钩子时在此处注册,未注册的钩子默认为 'high' * * @type {Object.} */ const HOOK_PRIORITIES = { // === critical: 安全屏蔽,始终执行 === 'block-sensitive-files': 'critical', 'block-dangerous-commands': 'critical', // === high: 核心功能,正常执行 === 'route-interceptor': 'high', 'route-compliance-gate': 'high', 'post-edit-dispatcher': 'high', 'constitution-guard': 'high', 'stop-hook': 'high', 'route-auditor': 'high', // === medium: 质量辅助,高负载可跳过 === 'post-edit-quality-check': 'medium', 'constitution-delivery-reminder': 'medium', 'suggest-tests': 'medium', 'memory-persistence-trigger': 'medium', 'check-typescript': 'medium', 'check-lint': 'medium', 'drift-detector': 'medium', 'integrity-check': 'medium', 'self-auditor': 'medium', // === low: 日志记录,高频场景跳过 === 'activity-logger': 'high', // V14 修复: 提升优先级,避免长会话断链 'build-outcome-tracker': 'low', 'route-stats-updater': 'low', 'evolution-log-writer': 'low', }; // 工具调用次数阈值 const THRESHOLD_SKIP_LOW = 20; // 超过此值跳过 low 优先级 const THRESHOLD_SKIP_MEDIUM = 50; // 超过此值跳过 medium 优先级 // 优先级数字映射(数值越小优先级越高) const PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, }; // ===================================================== // 公共 API // ===================================================== /** * 判断指定钩子在当前工具调用次数下是否应该执行 * * @param {string} hookName - 钩子名称(不含路径和扩展名) * @param {number} toolCallCount - 当前会话工具调用次数 * @returns {boolean} true 表示应该执行 */ function shouldExecute(hookName, toolCallCount) { try { const priority = getPriority(hookName); const count = (typeof toolCallCount === 'number' && toolCallCount >= 0) ? toolCallCount : 0; // critical 和 high 始终执行 if (priority === 'critical' || priority === 'high') return true; // medium: 超过 50 次跳过 if (priority === 'medium' && count > THRESHOLD_SKIP_MEDIUM) return false; // low: 超过 20 次跳过 if (priority === 'low' && count > THRESHOLD_SKIP_LOW) return false; return true; } catch { // fail-open: 异常时默认执行 return true; } } /** * 获取钩子的优先级 * * @param {string} hookName - 钩子名称 * @returns {'critical'|'high'|'medium'|'low'} 优先级字符串,未注册返回 'high' */ function getPriority(hookName) { if (!hookName || typeof hookName !== 'string') return 'high'; // 支持带路径和扩展名的钩子名(取文件名去扩展名) const baseName = hookName .replace(/\\/g, '/') .split('/').pop() .replace(/\.js$/, ''); return HOOK_PRIORITIES[baseName] || 'high'; } /** * 从钩子名列表中过滤出在当前工具调用次数下应执行的钩子 * 供 post-edit-dispatcher.js 使用 * * @param {string[]} hookNames - 钩子名称列表 * @param {number} toolCallCount - 当前会话工具调用次数 * @returns {string[]} 过滤后的钩子名称列表 */ function filterHooks(hookNames, toolCallCount) { if (!Array.isArray(hookNames)) return []; return hookNames.filter(h => shouldExecute(h, toolCallCount)); } /** * 按优先级对钩子列表排序(critical → high → medium → low) * * @param {string[]} hookNames - 钩子名称列表 * @returns {string[]} 排序后的列表 */ function sortByPriority(hookNames) { if (!Array.isArray(hookNames)) return []; return [...hookNames].sort((a, b) => { const rankA = PRIORITY_RANK[getPriority(a)] ?? 1; const rankB = PRIORITY_RANK[getPriority(b)] ?? 1; return rankA - rankB; }); } /** * 读取当前工具调用计数 * 从 debug/tool-call-counter.json 读取;文件不存在或读取失败返回 0 * * @param {string} [sessionId] - 会话 ID(可选,用于 per-session 计数) * @returns {number} 工具调用次数 */ function readToolCallCount(sessionId) { try { const fs = require('fs'); const path = require('path'); const counterFile = path.join( require('./paths.config.js').PATHS.root, 'debug', 'tool-call-counter.json' ); if (!fs.existsSync(counterFile)) return 0; const data = JSON.parse(fs.readFileSync(counterFile, 'utf8')); // 如果提供了 sessionId,优先读取 per-session 计数 if (sessionId && data.sessions && data.sessions[sessionId]) { return data.sessions[sessionId].count || 0; } // 否则读取全局计数 return data.totalCount || data.count || 0; } catch { return 0; } } /** * 生成调度摘要报告(供调试使用) * * @param {number} toolCallCount - 工具调用次数 * @returns {Object} 包含各优先级执行情况的摘要 */ function getScheduleSummary(toolCallCount) { const count = toolCallCount || 0; const result = { toolCallCount: count, thresholds: { skipLow: THRESHOLD_SKIP_LOW, skipMedium: THRESHOLD_SKIP_MEDIUM, }, hooks: {}, }; for (const [name, priority] of Object.entries(HOOK_PRIORITIES)) { result.hooks[name] = { priority, willExecute: shouldExecute(name, count), }; } return result; } // ===================================================== // 模块导出 // ===================================================== if (typeof module !== 'undefined') { module.exports = { // 核心调度接口 shouldExecute, getPriority, filterHooks, sortByPriority, // 工具函数 readToolCallCount, getScheduleSummary, // 常量导出(供测试和外部引用) HOOK_PRIORITIES, THRESHOLD_SKIP_LOW, THRESHOLD_SKIP_MEDIUM, }; } // CLI 入口 if (require.main === module) { const count = parseInt(process.argv[2]) || 0; console.log(`=== 钩子优先级调度器 (toolCallCount=${count}) ===\n`); const summary = getScheduleSummary(count); const groups = { critical: [], high: [], medium: [], low: [] }; for (const [name, info] of Object.entries(summary.hooks)) { const marker = info.willExecute ? '✓' : '✗'; groups[info.priority].push(` ${marker} ${name}`); } for (const [p, lines] of Object.entries(groups)) { if (lines.length > 0) { console.log(`[${p.toUpperCase()}]`); console.log(lines.join('\n')); console.log(); } } console.log(`阈值: low 跳过 > ${THRESHOLD_SKIP_LOW}, medium 跳过 > ${THRESHOLD_SKIP_MEDIUM}`); }