483 lines
19 KiB
JavaScript
483 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* v6.1 模块集成补丁脚本
|
||
*
|
||
* 将 Phase 2-3 三个热路径孤岛模块接入路由管线:
|
||
* 1. route-interceptor.js ← adaptive-disambiguator + embedding-router
|
||
* 2. post-edit-dispatcher.js ← hook-priority-scheduler
|
||
* 3. route-auditor.js ← adaptive-disambiguator 反馈闭环
|
||
*
|
||
* 约束:
|
||
* - hooks/ 文件受 integrity-check 保护,补丁只追加,不重写原逻辑
|
||
* - 所有集成使用 safeRequire + try-catch (fail-open)
|
||
* - 补丁为幂等操作: 检测 PATCH_MARKER 防止重复注入
|
||
*
|
||
* 用法:
|
||
* node scripts/apply-v61-integration-patches.js # 应用所有补丁
|
||
* node scripts/apply-v61-integration-patches.js --check # 检查补丁状态
|
||
* node scripts/apply-v61-integration-patches.js --dry-run # 预览变更,不写入
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// ─── 路径检测 ──────────────────────────────────────────────────────────────
|
||
function detectClaudeRoot() {
|
||
if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
|
||
const selfDir = path.dirname(__filename);
|
||
if (selfDir.includes('.claude')) return selfDir.replace(/[/\\]scripts$/, '');
|
||
try { return require('./paths.config.js').PATHS.root; } catch {}
|
||
return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\/g, '/') + '/.claude';
|
||
}
|
||
|
||
const CLAUDE_ROOT = detectClaudeRoot();
|
||
const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks');
|
||
const SCRIPTS_DIR = path.join(CLAUDE_ROOT, 'scripts');
|
||
|
||
const DRY_RUN = process.argv.includes('--dry-run');
|
||
const CHECK = process.argv.includes('--check');
|
||
|
||
// ─── 补丁标记(用于幂等检测)──────────────────────────────────────────────
|
||
const PATCH_MARKERS = {
|
||
'route-interceptor.js': '// [v6.1-PATCH] adaptive-disambiguator + embedding-router',
|
||
'post-edit-dispatcher.js': '// [v6.1-PATCH] hook-priority-scheduler',
|
||
'route-auditor.js': '// [v6.1-PATCH] adaptive-disambiguator feedback',
|
||
};
|
||
|
||
// ─── 工具函数 ──────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 读取文件内容,失败时返回 null
|
||
*/
|
||
function readFile(filePath) {
|
||
try {
|
||
return fs.readFileSync(filePath, 'utf8');
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 写入文件(dry-run 模式只打印)
|
||
*/
|
||
function writeFile(filePath, content) {
|
||
if (DRY_RUN) {
|
||
console.log(`[DRY-RUN] 将写入: ${filePath}`);
|
||
return true;
|
||
}
|
||
try {
|
||
// 原子写入: 先写 .tmp 再 rename,防止写入中断
|
||
const tmpPath = filePath + '.patch-tmp.' + process.pid;
|
||
fs.writeFileSync(tmpPath, content, 'utf8');
|
||
fs.renameSync(tmpPath, filePath);
|
||
return true;
|
||
} catch (e) {
|
||
console.error(`[ERROR] 写入失败: ${filePath} — ${e.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查文件中是否已存在补丁标记
|
||
*/
|
||
function isPatchApplied(content, marker) {
|
||
return content.includes(marker);
|
||
}
|
||
|
||
// ─── 补丁 1: route-interceptor.js ────────────────────────────────────────
|
||
//
|
||
// 集成点 A: 消歧规则应用后,注入 adaptive-disambiguator (Bayesian 融合)
|
||
// 集成点 B: BM25 评分后、融合权重计算前,注入 embedding-router (tie-breaker)
|
||
//
|
||
// 原始结构 (runRouteEngine 函数内):
|
||
// ① results = index.skills.map(...).sort() ← BM25 评分完成
|
||
// ② disambResult = routeAnalyzer.applyDisambiguation() ← 消歧规则
|
||
// ③ coldStartBoost ← 冷启动
|
||
// ④ rerankTopK ← reranking
|
||
//
|
||
// 插入顺序:
|
||
// - embedding-router: 在 ① 之后(results 存在),②之前
|
||
// - adaptive-disambiguator: 在 ② 之后,③ 之前
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 补丁代码: embedding-router 注入块
|
||
* 插入锚点: results 排序完成后,进入消歧之前
|
||
*/
|
||
const EMBEDDING_PATCH = `
|
||
// [v6.1-PATCH] adaptive-disambiguator + embedding-router
|
||
// Phase 3 集成: 当 BM25 top-2 差距 < 15% 时,激活向量路由作为 tie-breaker
|
||
const embeddingRouter = safeRequire(path.join(SCRIPTS_DIR, 'embedding-router.js'));
|
||
if (embeddingRouter && results.length >= 2) {
|
||
try {
|
||
const top2Scores = [results[0].score, results[1].score];
|
||
if (embeddingRouter.shouldActivate(top2Scores)) {
|
||
// 仅对 top-10 候选的技能名计算向量相似度(降低开销)
|
||
const top10Names = results.slice(0, 10).map(r => r.name);
|
||
const embeddingScores = embeddingRouter.computeSimilarity(prompt, top10Names);
|
||
// 将 embedding 相似度以 10% 权重融合到 BM25 分数
|
||
const embMap = new Map(embeddingScores.map(es => [es.skill, es.similarity]));
|
||
for (const r of results) {
|
||
const embSim = embMap.get(r.name) || 0;
|
||
if (embSim > 0) {
|
||
// 微调: embedding 信号最多影响原始分数的 ±10%
|
||
r.score = Math.round((r.score + embSim * r.score * 0.10) * 100) / 100;
|
||
}
|
||
}
|
||
// 重新排序(embedding 融合后分数可能变化)
|
||
results.sort((a, b) => b.score - a.score);
|
||
}
|
||
} catch {}
|
||
}
|
||
`;
|
||
|
||
/**
|
||
* 补丁代码: adaptive-disambiguator 注入块
|
||
* 插入锚点: applyDisambiguation 调用之后,applyColdStartBoost 之前
|
||
*/
|
||
const ADAPTIVE_DISAMB_PATCH = `
|
||
// [v6.1-PATCH] adaptive-disambiguator: Bayesian 后验融合硬规则结果
|
||
// 在消歧规则之后调用,将学习到的 Dirichlet 后验概率与硬规则结果融合
|
||
const adaptiveDisamb = safeRequire(path.join(SCRIPTS_DIR, 'adaptive-disambiguator.js'));
|
||
if (adaptiveDisamb && disambiguated.length > 0) {
|
||
try {
|
||
// 构造 hardRuleResults 结构供 adaptiveDisambiguate 使用
|
||
const hardRuleResults = {
|
||
boosted: firedRules.filter(r => r.action === 'boost' || r.winner).map(r => r.winner || r.skill).filter(Boolean),
|
||
penalized: firedRules.filter(r => r.action === 'penalize').map(r => r.skill).filter(Boolean),
|
||
firedRules: firedRules.map(r => r.id || r.rule || '').filter(Boolean),
|
||
};
|
||
const intentResult = precomputedIntent || { intents: [], entities: [] };
|
||
const bayesianResult = adaptiveDisamb.adaptiveDisambiguate(
|
||
disambiguated.slice(0, 5), // 仅对 top-5 候选进行 Bayesian 调整
|
||
{ prompt, domain: domainInfo, intent: intentResult },
|
||
hardRuleResults
|
||
);
|
||
if (bayesianResult && bayesianResult.length > 0) {
|
||
// 用 Bayesian 融合结果替代纯硬规则结果(替换 top-5,保留后续候选)
|
||
const tail = disambiguated.slice(5);
|
||
disambiguated = [...bayesianResult, ...tail];
|
||
}
|
||
} catch {}
|
||
}
|
||
`;
|
||
|
||
/**
|
||
* 应用 route-interceptor.js 补丁
|
||
*/
|
||
function patchRouteInterceptor() {
|
||
const filePath = path.join(HOOKS_DIR, 'route-interceptor.js');
|
||
const content = readFile(filePath);
|
||
if (!content) {
|
||
console.error('[SKIP] route-interceptor.js 读取失败');
|
||
return false;
|
||
}
|
||
|
||
const marker = PATCH_MARKERS['route-interceptor.js'];
|
||
if (isPatchApplied(content, marker)) {
|
||
console.log('[SKIP] route-interceptor.js 补丁已存在,跳过');
|
||
return true;
|
||
}
|
||
|
||
// ── 插入点 A: embedding-router ──
|
||
// 锚点: ` // v5.8: 冲突消歧 (加权投票 + specificity)`
|
||
// 在此行之前插入 embedding patch
|
||
const ANCHOR_EMBEDDING = ' // v5.8: 冲突消歧 (加权投票 + specificity)';
|
||
if (!content.includes(ANCHOR_EMBEDDING)) {
|
||
console.error('[ERROR] route-interceptor.js 锚点 A 未找到: 消歧注释行');
|
||
return false;
|
||
}
|
||
|
||
// ── 插入点 B: adaptive-disambiguator ──
|
||
// 锚点: ` // v5.8: 冷启动防护 (epsilon-greedy + 新技能 boost)`
|
||
// 在此行之前插入 adaptive disambiguator patch
|
||
const ANCHOR_ADAPTIVE = ' // v5.8: 冷启动防护 (epsilon-greedy + 新技能 boost)';
|
||
if (!content.includes(ANCHOR_ADAPTIVE)) {
|
||
console.error('[ERROR] route-interceptor.js 锚点 B 未找到: 冷启动注释行');
|
||
return false;
|
||
}
|
||
|
||
// 先插 B(adaptive disambiguator),再插 A(embedding)
|
||
// 这样两次字符串替换都基于不同的锚点,不会互相干扰
|
||
let patched = content
|
||
// 插入 B: adaptive-disambiguator(在冷启动之前)
|
||
.replace(
|
||
ANCHOR_ADAPTIVE,
|
||
ADAPTIVE_DISAMB_PATCH + '\n ' + ANCHOR_ADAPTIVE.trimStart()
|
||
)
|
||
// 插入 A: embedding-router(在消歧之前)
|
||
.replace(
|
||
ANCHOR_EMBEDDING,
|
||
EMBEDDING_PATCH + '\n ' + ANCHOR_EMBEDDING.trimStart()
|
||
);
|
||
|
||
if (DRY_RUN) {
|
||
console.log('[DRY-RUN] route-interceptor.js 补丁预览:');
|
||
console.log(' + embedding-router 集成(BM25 tie-breaker)');
|
||
console.log(' + adaptive-disambiguator 集成(Bayesian 融合)');
|
||
return true;
|
||
}
|
||
|
||
const ok = writeFile(filePath, patched);
|
||
if (ok) console.log('[OK] route-interceptor.js 补丁已应用');
|
||
return ok;
|
||
}
|
||
|
||
// ─── 补丁 2: post-edit-dispatcher.js ────────────────────────────────────
|
||
//
|
||
// 集成点: 在 runSubHook 调用之前,使用 hook-priority-scheduler
|
||
// 检查子钩子是否需要执行(高工具调用次数时跳过低优先级钩子)
|
||
//
|
||
// 原始结构 (main 函数的 async 回调内):
|
||
// heavyChecks.push(runSubHook('check-typescript.js', rawInput));
|
||
// heavyChecks.push(runSubHook('check-lint.js', rawInput));
|
||
//
|
||
// 补丁策略:
|
||
// 在 main 函数开头初始化 scheduler 和 toolCallCount,
|
||
// 在每个 runSubHook 调用前包裹条件检查
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 补丁代码: hook-priority-scheduler 初始化(注入到 main async 回调开头)
|
||
*/
|
||
const SCHEDULER_INIT_PATCH = ` // [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 {}
|
||
|
||
`;
|
||
|
||
/**
|
||
* 应用 post-edit-dispatcher.js 补丁
|
||
*/
|
||
function patchPostEditDispatcher() {
|
||
const filePath = path.join(HOOKS_DIR, 'post-edit-dispatcher.js');
|
||
const content = readFile(filePath);
|
||
if (!content) {
|
||
console.error('[SKIP] post-edit-dispatcher.js 读取失败');
|
||
return false;
|
||
}
|
||
|
||
const marker = PATCH_MARKERS['post-edit-dispatcher.js'];
|
||
if (isPatchApplied(content, marker)) {
|
||
console.log('[SKIP] post-edit-dispatcher.js 补丁已存在,跳过');
|
||
return true;
|
||
}
|
||
|
||
// ── 插入点: async 回调内、filePath 解析之前 ──
|
||
// 锚点: ` const filePath = (`
|
||
const ANCHOR_INIT = ' const filePath = (';
|
||
if (!content.includes(ANCHOR_INIT)) {
|
||
console.error('[ERROR] post-edit-dispatcher.js 锚点未找到: filePath 赋值行');
|
||
return false;
|
||
}
|
||
|
||
// ── 替换 TypeScript 检查的 runSubHook 调用,加入调度检查 ──
|
||
// 原始: ` heavyChecks.push(runSubHook('check-typescript.js', rawInput));`
|
||
const ORIG_TS_CHECK = ` heavyChecks.push(runSubHook('check-typescript.js', rawInput));`;
|
||
const PATCHED_TS_CHECK = ` // [v6.1-PATCH] hook-priority-scheduler: check-typescript 为 medium 优先级
|
||
if (!_scheduler || _scheduler.shouldExecute('check-typescript', _toolCallCount)) {
|
||
heavyChecks.push(runSubHook('check-typescript.js', rawInput));
|
||
}`;
|
||
|
||
// ── 替换 ESLint 检查的 runSubHook 调用,加入调度检查 ──
|
||
const ORIG_LINT_CHECK = ` heavyChecks.push(runSubHook('check-lint.js', rawInput));`;
|
||
const PATCHED_LINT_CHECK = ` // [v6.1-PATCH] hook-priority-scheduler: check-lint 为 medium 优先级
|
||
if (!_scheduler || _scheduler.shouldExecute('check-lint', _toolCallCount)) {
|
||
heavyChecks.push(runSubHook('check-lint.js', rawInput));
|
||
}`;
|
||
|
||
if (!content.includes(ORIG_TS_CHECK) || !content.includes(ORIG_LINT_CHECK)) {
|
||
console.error('[ERROR] post-edit-dispatcher.js runSubHook 调用行未找到,代码结构可能已变更');
|
||
return false;
|
||
}
|
||
|
||
let patched = content
|
||
// 1. 在 filePath 解析前注入调度器初始化
|
||
.replace(ANCHOR_INIT, SCHEDULER_INIT_PATCH + ' ' + ANCHOR_INIT.trimStart())
|
||
// 2. 包裹 TypeScript 检查
|
||
.replace(ORIG_TS_CHECK, PATCHED_TS_CHECK)
|
||
// 3. 包裹 ESLint 检查
|
||
.replace(ORIG_LINT_CHECK, PATCHED_LINT_CHECK);
|
||
|
||
if (DRY_RUN) {
|
||
console.log('[DRY-RUN] post-edit-dispatcher.js 补丁预览:');
|
||
console.log(' + hook-priority-scheduler 初始化');
|
||
console.log(' + check-typescript: 高负载时跳过(medium 优先级)');
|
||
console.log(' + check-lint: 高负载时跳过(medium 优先级)');
|
||
return true;
|
||
}
|
||
|
||
const ok = writeFile(filePath, patched);
|
||
if (ok) console.log('[OK] post-edit-dispatcher.js 补丁已应用');
|
||
return ok;
|
||
}
|
||
|
||
// ─── 补丁 3: route-auditor.js ─────────────────────────────────────────────
|
||
//
|
||
// 集成点: 在 writeAuditEntry 调用之后、autoLearn 检查之前,
|
||
// 当检测到路由纠正(compliant === false)时,
|
||
// 调用 adaptive-disambiguator.updateFromFeedback() 更新学习权重
|
||
//
|
||
// 原始结构 (main 函数的 stdin 'end' 回调内):
|
||
// writeAuditEntry(state, judgment);
|
||
// (回写 actualSkill)
|
||
// A/B 实验回写
|
||
// 累计检查 checkAutoLearn
|
||
// runImplicitFeedback
|
||
// cleanupState
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 补丁代码: adaptive-disambiguator 反馈闭环
|
||
* 当 judgment.compliant === false 时,说明用户实际使用了不同的技能 → 路由纠正
|
||
*/
|
||
const FEEDBACK_PATCH = `
|
||
// [v6.1-PATCH] adaptive-disambiguator feedback
|
||
// 反馈闭环: 检测到路由纠正时,更新 Bayesian Dirichlet 先验
|
||
// compliant===false 表示用户实际使用的技能与路由推荐不符
|
||
if (judgment.compliant === false && judgment.actualSkill && state.routing) {
|
||
try {
|
||
const adaptiveDisamb = require(require('path').join(
|
||
__dirname.replace(/[/\\\\]hooks$/, ''), 'scripts', 'adaptive-disambiguator.js'
|
||
));
|
||
const routedTo = state.routing.primary;
|
||
const actualSkill = judgment.actualSkill;
|
||
// 竞争技能: 路由时的候选列表(排除 routedTo 和 actualSkill 本身)
|
||
const competingSkills = (state.routing.candidates || [])
|
||
.map(c => c.name)
|
||
.filter(n => n !== routedTo && n !== actualSkill);
|
||
adaptiveDisamb.updateFromFeedback(routedTo, actualSkill, competingSkills);
|
||
} catch {}
|
||
}
|
||
`;
|
||
|
||
/**
|
||
* 应用 route-auditor.js 补丁
|
||
*/
|
||
function patchRouteAuditor() {
|
||
const filePath = path.join(HOOKS_DIR, 'route-auditor.js');
|
||
const content = readFile(filePath);
|
||
if (!content) {
|
||
console.error('[SKIP] route-auditor.js 读取失败');
|
||
return false;
|
||
}
|
||
|
||
const marker = PATCH_MARKERS['route-auditor.js'];
|
||
if (isPatchApplied(content, marker)) {
|
||
console.log('[SKIP] route-auditor.js 补丁已存在,跳过');
|
||
return true;
|
||
}
|
||
|
||
// 锚点: 写入审计记录后的那一行
|
||
// ` // 回写 actualSkill 到当日路由日志 (供 implicit-feedback 消费)`
|
||
const ANCHOR_FEEDBACK = ' // 回写 actualSkill 到当日路由日志 (供 implicit-feedback 消费)';
|
||
if (!content.includes(ANCHOR_FEEDBACK)) {
|
||
console.error('[ERROR] route-auditor.js 锚点未找到: 回写 actualSkill 注释行');
|
||
return false;
|
||
}
|
||
|
||
let patched = content.replace(
|
||
ANCHOR_FEEDBACK,
|
||
FEEDBACK_PATCH + '\n ' + ANCHOR_FEEDBACK.trimStart()
|
||
);
|
||
|
||
if (DRY_RUN) {
|
||
console.log('[DRY-RUN] route-auditor.js 补丁预览:');
|
||
console.log(' + adaptive-disambiguator 反馈闭环(路由纠正时更新 Bayesian 权重)');
|
||
return true;
|
||
}
|
||
|
||
const ok = writeFile(filePath, patched);
|
||
if (ok) console.log('[OK] route-auditor.js 补丁已应用');
|
||
return ok;
|
||
}
|
||
|
||
// ─── 检查模式: 报告每个文件的补丁状态 ──────────────────────────────────
|
||
function checkPatchStatus() {
|
||
console.log('=== v6.1 补丁状态检查 ===\n');
|
||
|
||
const files = [
|
||
{ file: path.join(HOOKS_DIR, 'route-interceptor.js'), key: 'route-interceptor.js' },
|
||
{ file: path.join(HOOKS_DIR, 'post-edit-dispatcher.js'), key: 'post-edit-dispatcher.js' },
|
||
{ file: path.join(HOOKS_DIR, 'route-auditor.js'), key: 'route-auditor.js' },
|
||
];
|
||
|
||
let allApplied = true;
|
||
for (const { file, key } of files) {
|
||
const content = readFile(file);
|
||
if (!content) {
|
||
console.log(` [MISSING] ${key} — 文件不存在`);
|
||
allApplied = false;
|
||
continue;
|
||
}
|
||
const marker = PATCH_MARKERS[key];
|
||
const applied = isPatchApplied(content, marker);
|
||
console.log(` [${applied ? 'APPLIED' : 'PENDING'}] ${key}`);
|
||
if (!applied) allApplied = false;
|
||
}
|
||
|
||
console.log(`\n总状态: ${allApplied ? '全部已应用' : '有待应用的补丁'}`);
|
||
return allApplied;
|
||
}
|
||
|
||
// ─── 主入口 ───────────────────────────────────────────────────────────────
|
||
function main() {
|
||
if (CHECK) {
|
||
const allOk = checkPatchStatus();
|
||
process.exit(allOk ? 0 : 1);
|
||
}
|
||
|
||
console.log(`=== v6.1 模块集成补丁${DRY_RUN ? ' (DRY-RUN)' : ''} ===\n`);
|
||
|
||
let successCount = 0;
|
||
const total = 3;
|
||
|
||
// 应用三个补丁
|
||
if (patchRouteInterceptor()) successCount++;
|
||
if (patchPostEditDispatcher()) successCount++;
|
||
if (patchRouteAuditor()) successCount++;
|
||
|
||
console.log(`\n完成: ${successCount}/${total} 个文件已处理`);
|
||
|
||
if (!DRY_RUN && successCount > 0) {
|
||
// 语法检查(node -c)
|
||
console.log('\n─── 语法检查 ───');
|
||
const { execFileSync } = require('child_process');
|
||
const targets = [
|
||
path.join(HOOKS_DIR, 'route-interceptor.js'),
|
||
path.join(HOOKS_DIR, 'post-edit-dispatcher.js'),
|
||
path.join(HOOKS_DIR, 'route-auditor.js'),
|
||
];
|
||
let syntaxOk = true;
|
||
for (const target of targets) {
|
||
try {
|
||
execFileSync(process.execPath, ['--check', target], {
|
||
timeout: 5000,
|
||
stdio: ['pipe', 'pipe', 'pipe'],
|
||
});
|
||
console.log(` [OK] ${path.basename(target)}`);
|
||
} catch (e) {
|
||
console.error(` [FAIL] ${path.basename(target)} — 语法错误: ${(e.stderr || e.message || '').toString().trim()}`);
|
||
syntaxOk = false;
|
||
}
|
||
}
|
||
if (!syntaxOk) {
|
||
console.error('\n[WARNING] 存在语法错误,请检查补丁代码');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
process.exit(successCount === total ? 0 : 1);
|
||
}
|
||
|
||
main();
|