bookworm-smart-assistant/scripts/patches/patch-l1-agent-virtual-injection.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- 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)
2026-04-27 17:59:44 +08:00

152 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();