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}`);
|
|||
|
|
}
|