258 lines
7.9 KiB
JavaScript
258 lines
7.9 KiB
JavaScript
#!/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.<string, 'critical'|'high'|'medium'|'low'>}
|
||
*/
|
||
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}`);
|
||
}
|