- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
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();
|