bookworm-smart-assistant/scripts/patches/patch-l1-agent-virtual-injection.js

152 lines
5.3 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* L1 修复 applyDisambiguation agent boost 失效根治 (2026-04-25)
*
* 问题: route-analyzer.js applyDisambiguation results.find(r => r.name === rule.boost)
* 仅在 BM25 results ( skills-index-lite.json skill 集合) 中查找
* 18 agent 不在 skills-index 导致 R81/R82/R84/R85/R86 5
* agent-boost 规则全部失效self-auditor 路径事实失能
*
* 实测证据: ~/.claude/debug/route-state-current.json self-auditor
* _bayesianAdj=0/coldStartBoost=0.048 disambiguated=true 标记
*
* 方案: 注入虚拟 agent 条目 (_virtual: true, _isAgent: true) results
* 使 boost/penalty/排名强制三阶段都能正常作用于 agent
* agent 名单从 ~/.claude/agents/*.md ()
*
* Fail-close: agent 名单扫描失败 stderr 警告但不阻断 (退化为修复前行为)
*
* 幂等: sentinel `// L1-AGENT-VIRTUAL-INJECTION` 已存在则跳过
* 原子: tmp + rename
* 备份: .bak.l1-agent-virtual.<ms>
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const TARGET = path.join(__dirname, '..', 'route-analyzer.js');
const SENTINEL = '// L1-AGENT-VIRTUAL-INJECTION';
const BAK_DIR = path.join(__dirname, 'bak');
const APPLY = process.argv.includes('--apply');
function syntaxCheck(file) {
// execFileSync 数组参数, 无 shell 拼接, 防注入
execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' });
}
function main() {
if (!fs.existsSync(TARGET)) {
console.error('[L1] route-analyzer.js not found at', TARGET);
process.exit(2);
}
let src = fs.readFileSync(TARGET, 'utf8');
if (src.includes(SENTINEL)) {
console.log('[L1] sentinel found — already patched, skipping (idempotent).');
process.exit(0);
}
const ANCHOR = ' // Phase 1: 收集所有匹配规则的投票';
if (!src.includes(ANCHOR)) {
console.error('[L1] anchor not found — abort to fail-close.');
process.exit(2);
}
const INJECTION = ` ${SENTINEL} (2026-04-25 D1 缺陷根治)
// 在投票阶段开始前, 为 agent-only boost 规则注入虚拟 results 条目,
// 使后续 boost/penalty/排名强制能正常作用于 agent (skills-index 不含 agent)。
// Fail-close: 加载失败仅打印警告, 不阻断主流程。
try {
const _agentNames = _loadAgentNamesCached();
if (_agentNames && _agentNames.size > 0 && results.length > 0) {
const _maxScore = Math.max.apply(null, results.map(function(r){return r.score||0;}).concat([0.001]));
const _existingNames = new Set(results.map(function(r){return r.name;}));
const _candidateAgents = new Set();
for (const _rule of DISAMBIGUATION_RULES) {
if (!_rule.trigger.test(queryText.toLowerCase())) continue;
if (_rule.boost && _agentNames.has(_rule.boost) && !_existingNames.has(_rule.boost)) {
_candidateAgents.add(_rule.boost);
}
}
for (const _agentName of _candidateAgents) {
results.push({
name: _agentName,
score: _maxScore * 0.6,
_virtual: true,
_isAgent: true,
matched: [],
weights: {}
});
}
}
} catch (_e) {
try { process.stderr.write('[route-analyzer] L1 virtual-agent injection skipped: ' + (_e && _e.message ? _e.message : String(_e)) + '\\n'); } catch (_) {}
}
`;
const newSrc = src.replace(ANCHOR, INJECTION + ANCHOR);
const HELPER_ANCHOR = 'function applyDisambiguation(results, queryText, index) {';
const HELPER = `// ${SENTINEL}-HELPER 加载 ~/.claude/agents/*.md 构建 agent 白名单 (惰性 + 缓存)
let _agentNamesCache = null;
function _loadAgentNamesCached() {
if (_agentNamesCache !== null) return _agentNamesCache;
try {
const _agentDir = path.join(CLAUDE_ROOT, 'agents');
if (!fs.existsSync(_agentDir)) {
_agentNamesCache = new Set();
return _agentNamesCache;
}
const _files = fs.readdirSync(_agentDir);
const _names = new Set();
for (const _f of _files) {
if (_f.endsWith('.md') && !_f.startsWith('_')) {
_names.add(_f.slice(0, -3));
}
}
_agentNamesCache = _names;
} catch (_e) {
_agentNamesCache = new Set(); // fail-close: 空集等价于关闭虚拟注入
}
return _agentNamesCache;
}
`;
const finalSrc = newSrc.replace(HELPER_ANCHOR, HELPER + HELPER_ANCHOR);
if (finalSrc === src) {
console.error('[L1] no replacement applied — abort.');
process.exit(2);
}
if (!fs.existsSync(BAK_DIR)) fs.mkdirSync(BAK_DIR, { recursive: true });
const ts = Date.now();
const bakPath = path.join(BAK_DIR, 'route-analyzer.js.bak.l1-agent-virtual.' + ts);
if (!APPLY) {
console.log('[L1] DRY RUN — pass --apply to write changes.');
console.log(' would backup to: ' + bakPath);
return;
}
fs.writeFileSync(bakPath, src);
const tmpPath = TARGET + '.tmp.' + ts;
fs.writeFileSync(tmpPath, finalSrc);
fs.renameSync(tmpPath, TARGET);
console.log('[L1] applied. backup at ' + bakPath);
try {
syntaxCheck(TARGET);
console.log('[L1] node --check syntax: PASS');
} catch (e) {
console.error('[L1] SYNTAX FAILED — rolling back!');
fs.copyFileSync(bakPath, TARGET);
process.exit(2);
}
}
main();