152 lines
5.3 KiB
JavaScript
152 lines
5.3 KiB
JavaScript
|
|
#!/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();
|