481 lines
20 KiB
Plaintext
481 lines
20 KiB
Plaintext
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* UserPromptSubmit Hook: 路由注入主管道 (v5.2 Neural Gateway)
|
|||
|
|
*
|
|||
|
|
* 处理流程:
|
|||
|
|
* 1. 解析用户 prompt
|
|||
|
|
* 2. 意图分类 → 三级分流 (simple/medium/complex)
|
|||
|
|
* 3. 运行路由引擎 (BM25 + 上下文融合)
|
|||
|
|
* 4. 生成 [BWR] 指令注入 additionalContext
|
|||
|
|
* 5. 写入 route-state-current.json (供下游 hook 消费)
|
|||
|
|
*
|
|||
|
|
* stdin: { session_id, transcript_path, cwd, prompt, hook_event_name }
|
|||
|
|
* stdout: JSON { hookSpecificOutput: { additionalContext } }
|
|||
|
|
* 退出码: 0 (始终放行)
|
|||
|
|
*
|
|||
|
|
* [P2-7] skills-index-lite.json mtime 缓存: 避免每次路由调用都重复 JSON.parse
|
|||
|
|
*
|
|||
|
|
* 性能预算: < 2000ms 总计
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
const { safeAppendJsonl } = require('./lib/safe-append.js');
|
|||
|
|
|
|||
|
|
const readStdin = require('./lib/read-stdin.js');
|
|||
|
|
|
|||
|
|
// === P3-1 BUNDLE: preload routing deps ===
|
|||
|
|
// Phase 0 宪法合规拆分: 核心逻辑提取到独立模块
|
|||
|
|
const { runRouteEngine, loadSkillsIndex, safeRequire: _engineRequire } = require('../scripts/route-engine.js');
|
|||
|
|
const { buildBWRDirective, MUST_INVOKE_EXEMPT_INTENTS: _EXEMPT } = require('../scripts/bwr-builder.js');
|
|||
|
|
const { writeRouteState: _writeRouteState } = require('../scripts/route-state.js');
|
|||
|
|
|
|||
|
|
// H13: 意图分类器立即加载 (每次必用)
|
|||
|
|
const _preloaded = {};
|
|||
|
|
try { _preloaded['intent-classifier.js'] = require('../scripts/intent-classifier.js'); } catch {}
|
|||
|
|
// 次要模块 — 首次访问时延迟加载
|
|||
|
|
function _getLazy(name) {
|
|||
|
|
if (!_preloaded[name]) {
|
|||
|
|
try { _preloaded[name] = require('../scripts/' + name); } catch { _preloaded[name] = null; }
|
|||
|
|
}
|
|||
|
|
return _preloaded[name];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 动态检测 Claude 配置根目录
|
|||
|
|
|
|||
|
|
const CLAUDE_ROOT = require('./lib/root.js');
|
|||
|
|
const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug');
|
|||
|
|
const SCRIPTS_DIR = path.join(CLAUDE_ROOT, 'scripts');
|
|||
|
|
const STATE_FILE = path.join(DEBUG_DIR, 'route-state-current.json');
|
|||
|
|
const SESSION_LOCK = path.join(DEBUG_DIR, 'session-active.lock');
|
|||
|
|
let _currentSessionId = null;
|
|||
|
|
|
|||
|
|
// MUST_INVOKE 豁免白名单 (来源: bwr-builder.js)
|
|||
|
|
const MUST_INVOKE_EXEMPT_INTENTS = _EXEMPT;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 日志脱敏
|
|||
|
|
*/
|
|||
|
|
const sanitizePrompt = (() => {
|
|||
|
|
try { return require('../scripts/sanitize.js').sanitize; }
|
|||
|
|
catch { return (text) => text || ''; }
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
// === 会话首次激活横幅 (v5.3) ===
|
|||
|
|
// 返回 null (非首条消息) 或横幅文本 (首条消息, 注入 additionalContext)
|
|||
|
|
function showActivationBanner(sessionId) {
|
|||
|
|
// 检查是否已有同 session 的锁文件
|
|||
|
|
try {
|
|||
|
|
if (fs.existsSync(SESSION_LOCK)) {
|
|||
|
|
const lock = JSON.parse(fs.readFileSync(SESSION_LOCK, 'utf8'));
|
|||
|
|
if (lock.sessionId === sessionId) return null; // 同会话,不重复显示
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// 写入新会话锁
|
|||
|
|
try {
|
|||
|
|
if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true });
|
|||
|
|
fs.writeFileSync(SESSION_LOCK, JSON.stringify({ sessionId, ts: new Date().toISOString() }));
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// 从 stats-compiled.json 读取系统指标 (唯一真相源)
|
|||
|
|
let skillCount = 0, hookCount = 0, mcpCount = 0, agentCount = 0, sysVersion = 'v5.9';
|
|||
|
|
try {
|
|||
|
|
const stats = JSON.parse(fs.readFileSync(path.join(CLAUDE_ROOT, 'stats-compiled.json'), 'utf8'));
|
|||
|
|
const s = stats.summary || {};
|
|||
|
|
skillCount = s.skills || 0;
|
|||
|
|
hookCount = s.hooks || 0;
|
|||
|
|
mcpCount = s.mcp || 0;
|
|||
|
|
agentCount = s.agents || 0;
|
|||
|
|
sysVersion = stats.version || 'v5.9';
|
|||
|
|
} catch {
|
|||
|
|
// stats-compiled.json 不存在时回退扫描 (P2-7: 使用 mtime 缓存加载)
|
|||
|
|
try {
|
|||
|
|
const idx = loadSkillsIndex(path.join(CLAUDE_ROOT, 'skills-index-lite.json'));
|
|||
|
|
if (idx) skillCount = idx.skills ? idx.skills.length : 0;
|
|||
|
|
} catch {}
|
|||
|
|
try {
|
|||
|
|
const sJson = JSON.parse(fs.readFileSync(path.join(CLAUDE_ROOT, 'settings.json'), 'utf8'));
|
|||
|
|
mcpCount = Object.keys(sJson.mcpServers || {}).length;
|
|||
|
|
} catch {}
|
|||
|
|
try {
|
|||
|
|
hookCount = fs.readdirSync(path.join(CLAUDE_ROOT, 'hooks')).filter(f => f.endsWith('.js')).length;
|
|||
|
|
} catch {}
|
|||
|
|
try {
|
|||
|
|
agentCount = fs.readdirSync(path.join(CLAUDE_ROOT, 'agents')).filter(f => f.endsWith('.md')).length;
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
// 从 route-stats.json 读取活跃技能数 (真实用户查询命中过的技能)
|
|||
|
|
let activeSkillCount = 0;
|
|||
|
|
try {
|
|||
|
|
const statsFile = path.join(DEBUG_DIR, 'route-stats.json');
|
|||
|
|
if (fs.existsSync(statsFile)) {
|
|||
|
|
const routeStats = JSON.parse(fs.readFileSync(statsFile, 'utf8'));
|
|||
|
|
activeSkillCount = Object.keys(routeStats.stats || {}).length;
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// /* ROUTE-ACC-3D-V1 */ near-3-day route accuracy
|
|||
|
|
let routeAccuracy3d = 'N/A';
|
|||
|
|
try {
|
|||
|
|
const cutoff = Date.now() - 3 * 86400 * 1000;
|
|||
|
|
const files = fs.readdirSync(DEBUG_DIR).filter(function(f){return /^route-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f);});
|
|||
|
|
let total = 0, hit = 0;
|
|||
|
|
for (const f of files) {
|
|||
|
|
const lines = fs.readFileSync(path.join(DEBUG_DIR, f), 'utf8').split('\n');
|
|||
|
|
for (const L of lines) {
|
|||
|
|
if (!L) continue;
|
|||
|
|
try {
|
|||
|
|
const j = JSON.parse(L);
|
|||
|
|
const ts = new Date(j.ts).getTime();
|
|||
|
|
if (!Number.isFinite(ts) || ts < cutoff) continue;
|
|||
|
|
/* ROUTE-ACC-3D-V2-FILTER */
|
|||
|
|
const q = (j.query || '').trim();
|
|||
|
|
if (q.length <= 3 || q.startsWith('[Image') || !q) continue;
|
|||
|
|
if (j.topResult === 'none' && (!j.candidates || j.candidates.length === 0)) continue;
|
|||
|
|
total++;
|
|||
|
|
if (j.topConfidence && j.topConfidence > 0) hit++;
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (total > 0) routeAccuracy3d = (hit / total * 100).toFixed(1) + '%';
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
const now = new Date();
|
|||
|
|
const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
|
|||
|
|
|
|||
|
|
// 纯数据上下文 (不含任何指令, 显示规则在 CLAUDE.md 中定义)
|
|||
|
|
const banner = [
|
|||
|
|
`[BOOKWORM_SESSION_START]`,
|
|||
|
|
`version: ${sysVersion}`,
|
|||
|
|
`skills: ${skillCount}`,
|
|||
|
|
`agents: ${agentCount}`,
|
|||
|
|
`hooks: ${hookCount}`,
|
|||
|
|
`mcp: ${mcpCount}`,
|
|||
|
|
`active_skills: ${activeSkillCount}/${skillCount}`,
|
|||
|
|
`route_accuracy_3d: ${routeAccuracy3d}`,
|
|||
|
|
`timestamp: ${ts}`,
|
|||
|
|
].join('\n');
|
|||
|
|
|
|||
|
|
return banner;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// === Phase 0 宪法拆分: 核心函数委托到独立模块 ===
|
|||
|
|
// runRouteEngine → scripts/route-engine.js
|
|||
|
|
// buildBWRDirective → scripts/bwr-builder.js
|
|||
|
|
// writeRouteState + appendRouteLog → scripts/route-state.js
|
|||
|
|
|
|||
|
|
function safeRequire(modulePath) {
|
|||
|
|
const basename = path.basename(modulePath);
|
|||
|
|
if (_preloaded[basename] !== undefined) return _preloaded[basename] || null;
|
|||
|
|
try { return require(modulePath); } catch { return null; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// runRouteEngine: 薄代理 → scripts/route-engine.js (宪法 2.2 拆分)
|
|||
|
|
// buildBWRDirective: 薄代理 → scripts/bwr-builder.js
|
|||
|
|
// writeRouteState: 薄代理 → scripts/route-state.js (注入 sessionId)
|
|||
|
|
|
|||
|
|
function writeRouteState(traceId, prompt, intent, routing) {
|
|||
|
|
return _writeRouteState(traceId, prompt, intent, routing, _currentSessionId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// === 主流程 ===
|
|||
|
|
function main() {
|
|||
|
|
// v5.9: 硬 timeout 保护 — 超过 2000ms 强制退出并返回无路由建议
|
|||
|
|
const HARD_TIMEOUT_MS = 2000; // [PERF v6.1] 从 2500→2000ms
|
|||
|
|
const timeoutTimer = setTimeout(() => {
|
|||
|
|
// 超时时静默退出,等同于无路由建议 (fallback 到 developer-expert)
|
|||
|
|
process.exit(0);
|
|||
|
|
}, HARD_TIMEOUT_MS);
|
|||
|
|
// 允许 Node.js 在 timer 未触发时正常退出
|
|||
|
|
if (timeoutTimer.unref) timeoutTimer.unref();
|
|||
|
|
|
|||
|
|
readStdin({ maxSize: 256 * 1024 }).then(input => {
|
|||
|
|
try {
|
|||
|
|
const prompt = input.prompt;
|
|||
|
|
const cwd = input.cwd || process.cwd();
|
|||
|
|
|
|||
|
|
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|||
|
|
process.exit(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// XC14 修复: task-notification 系统消息提前退出,不写 route-state-current.json
|
|||
|
|
// appendRouteLog 已有同类过滤,但 writeRouteState 调用早于它,state 文件会被污染
|
|||
|
|
if (prompt.includes('<task-notification>')) {
|
|||
|
|
process.exit(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// v5.3: 会话首次激活横幅 (返回横幅文本或 null)
|
|||
|
|
const sessionId = (typeof input.session_id === 'string' && input.session_id.length >= 8) ? input.session_id : 'transient-' + process.pid; // H4 修复: session_id 无效时用进程级标识
|
|||
|
|
_currentSessionId = sessionId; // RL-V01: 同步到模块级变量供 writeRouteState 使用
|
|||
|
|
const bannerText = showActivationBanner(sessionId);
|
|||
|
|
|
|||
|
|
// === Phase 0: 逃生舱命令检测 (在 /skill-name 检测之前) ===
|
|||
|
|
const escapeMatch = prompt.trim().match(/^\/(force|checks|reset)(?:\s+(.*))?$/i);
|
|||
|
|
if (escapeMatch) {
|
|||
|
|
try {
|
|||
|
|
const { isEnabled } = require('../scripts/feature-flags.js');
|
|||
|
|
const userOverrides = require('../scripts/user-overrides.js');
|
|||
|
|
const cmd = escapeMatch[1].toLowerCase();
|
|||
|
|
const arg = (escapeMatch[2] || '').trim();
|
|||
|
|
let message = '';
|
|||
|
|
|
|||
|
|
if (cmd === 'force' && isEnabled('escape-hatch-force')) {
|
|||
|
|
userOverrides.setForce(arg || undefined);
|
|||
|
|
message = `[BWR:override] Force mode ON${arg ? ` (skill: ${arg})` : ''} — next routing will be bypassed`;
|
|||
|
|
} else if (cmd === 'checks' && isEnabled('escape-hatch-checks')) {
|
|||
|
|
const enabled = arg.toLowerCase() !== 'off';
|
|||
|
|
userOverrides.setChecks(enabled);
|
|||
|
|
message = enabled
|
|||
|
|
? '[BWR:override] Quality checks ON'
|
|||
|
|
: '[BWR:override] Quality checks OFF (1h expiry)';
|
|||
|
|
} else if (cmd === 'reset' && isEnabled('escape-hatch-reset')) {
|
|||
|
|
userOverrides.resetAll();
|
|||
|
|
message = '[BWR:override] All overrides cleared';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (message) {
|
|||
|
|
const ctx = bannerText ? bannerText + '\n\n' + message : message;
|
|||
|
|
const output = {
|
|||
|
|
hookSpecificOutput: {
|
|||
|
|
hookEventName: 'UserPromptSubmit',
|
|||
|
|
additionalContext: ctx,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
process.stdout.write(JSON.stringify(output));
|
|||
|
|
process.exit(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Warning#3 优化: 一次性读取 STATE_FILE,供隐式反馈检测 + simple 继承共享
|
|||
|
|
let _cachedPrevState = null;
|
|||
|
|
try {
|
|||
|
|
if (fs.existsSync(STATE_FILE)) {
|
|||
|
|
_cachedPrevState = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// P1-4: Skill 隐式反馈检测 — 上一轮 Skill 调用后用户是否表示不满
|
|||
|
|
try {
|
|||
|
|
if (_cachedPrevState) {
|
|||
|
|
const prevTs = _cachedPrevState.ts ? new Date(_cachedPrevState.ts).getTime() : 0;
|
|||
|
|
const elapsed = Date.now() - prevTs;
|
|||
|
|
if (elapsed < 3 * 60 * 1000 && _cachedPrevState.routing && _cachedPrevState.routing.primary) {
|
|||
|
|
// R2#1 修复: 仅匹配 prompt 前 30 字符,避免技术描述中的词误触发
|
|||
|
|
// LV-06: 使用更精确的短语匹配,减少 "不对称"/"是不是" 等误判
|
|||
|
|
const head = prompt.slice(0, 30).toLowerCase();
|
|||
|
|
const negativeSignals = /^(不对|不是|换个|错了|重来|不要|别用|不行|太差)|^(no|not what|wrong|try again)/;
|
|||
|
|
const positiveSignals = /^(很好|不错|对的|好的|可以|继续)|^(exactly|perfect|great|yes)/;
|
|||
|
|
const isNegative = negativeSignals.test(head);
|
|||
|
|
const isPositive = positiveSignals.test(head) && !isNegative;
|
|||
|
|
if (isNegative || isPositive) {
|
|||
|
|
const feedbackFile = path.join(DEBUG_DIR, 'skill-implicit-feedback.jsonl');
|
|||
|
|
safeAppendJsonl(feedbackFile, {
|
|||
|
|
ts: new Date().toISOString(),
|
|||
|
|
prevTraceId: _cachedPrevState.traceId,
|
|||
|
|
prevSkill: _cachedPrevState.routing.primary,
|
|||
|
|
prevConfidence: _cachedPrevState.routing.confidence,
|
|||
|
|
signal: isNegative ? 'negative' : 'positive',
|
|||
|
|
promptSnippet: sanitizePrompt(prompt.slice(0, 100)),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// 用户显式调用 /skill-name → 直接放行不干预
|
|||
|
|
if (/^\/[\w-]+/.test(prompt.trim())) {
|
|||
|
|
// 即使是显式调用,首条消息也要输出横幅
|
|||
|
|
if (bannerText) {
|
|||
|
|
const output = {
|
|||
|
|
hookSpecificOutput: {
|
|||
|
|
hookEventName: 'UserPromptSubmit',
|
|||
|
|
additionalContext: bannerText,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
process.stdout.write(JSON.stringify(output));
|
|||
|
|
}
|
|||
|
|
process.exit(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成 traceId
|
|||
|
|
const traceId = crypto.randomUUID().slice(0, 8);
|
|||
|
|
|
|||
|
|
// 意图分类
|
|||
|
|
const intentClassifier = safeRequire(path.join(SCRIPTS_DIR, 'intent-classifier.js'));
|
|||
|
|
let intent;
|
|||
|
|
if (intentClassifier) {
|
|||
|
|
intent = intentClassifier.classifyIntent(prompt);
|
|||
|
|
} else {
|
|||
|
|
intent = { intents: ['general'], modifiers: [], entities: [], complexity: 'medium' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 三级分流 + 斧二(短查询继承) + 斧四(图片继承)
|
|||
|
|
let routing;
|
|||
|
|
let inherited = false;
|
|||
|
|
|
|||
|
|
// 斧四: 图片/附件查询检测 — [Image #N] 模式自动继承上轮 (占 none 的 24.3%)
|
|||
|
|
// v6.5.2: 移除 ^ 锚定,支持 "检查[Image #1]..." 等非行首位置
|
|||
|
|
const isImageQuery = /\[Image\s*#?\d+\]/.test(prompt);
|
|||
|
|
|
|||
|
|
// 继承尝试函数 (simple + 斧二 + 斧四 共用)
|
|||
|
|
const INHERIT_WINDOW_MS = 5 * 60 * 1000;
|
|||
|
|
function tryInherit() {
|
|||
|
|
if (!_cachedPrevState) return null;
|
|||
|
|
const prevTs = _cachedPrevState.ts ? new Date(_cachedPrevState.ts).getTime() : 0;
|
|||
|
|
const elapsed = Date.now() - prevTs;
|
|||
|
|
if (
|
|||
|
|
elapsed > INHERIT_WINDOW_MS ||
|
|||
|
|
!_cachedPrevState.routing?.primary ||
|
|||
|
|
_cachedPrevState.routing.primary === 'none'
|
|||
|
|
) return null;
|
|||
|
|
const prevRouting = _cachedPrevState.routing;
|
|||
|
|
return {
|
|||
|
|
primary: prevRouting.primary,
|
|||
|
|
candidates: (prevRouting.candidates || []).map(c => ({
|
|||
|
|
...c,
|
|||
|
|
confidence: Math.round(c.confidence * 0.7 * 100) / 100,
|
|||
|
|
})),
|
|||
|
|
confidence: Math.round((prevRouting.confidence || 0) * 0.7 * 100) / 100,
|
|||
|
|
chain: prevRouting.chain || [],
|
|||
|
|
// 宪法 13.1: 继承路由保留 mustInvoke 标记
|
|||
|
|
_inheritedMustInvoke: _cachedPrevState.mustInvoke || false,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isImageQuery) {
|
|||
|
|
// 斧四: 图片查询 → 强制继承上轮,不走 TF-IDF
|
|||
|
|
routing = tryInherit() || { primary: 'none', candidates: [], confidence: 0, chain: [] };
|
|||
|
|
inherited = routing.primary !== 'none';
|
|||
|
|
} else if (intent.complexity === 'simple') {
|
|||
|
|
// simple: 继承上一次路由 (continue/select/confirm + general/explain)
|
|||
|
|
const inheritResult = tryInherit();
|
|||
|
|
if (inheritResult && inheritResult.primary !== 'none') {
|
|||
|
|
routing = inheritResult;
|
|||
|
|
inherited = true;
|
|||
|
|
} else {
|
|||
|
|
// v6.5.2 追问兜底: CJK 3-14 字的 general 查询继承失败时走 TF-IDF
|
|||
|
|
// 避免 "再美化一下"/"系统自检" 等追问直接 none
|
|||
|
|
const cjkCount = (prompt.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
|
|||
|
|
const isPureSimple = intent.intents.some(i => i === 'confirm' || i === 'select' || i === 'continue');
|
|||
|
|
if (!isPureSimple && cjkCount >= 3 && cjkCount < 15) {
|
|||
|
|
routing = null; // 落到 TF-IDF 路由引擎
|
|||
|
|
} else {
|
|||
|
|
routing = { primary: 'none', candidates: [], confidence: 0, chain: [] };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else if (intent.complexity === 'medium') {
|
|||
|
|
// 斧二: medium 短查询尝试继承 (V-03: CJK 独立阈值)
|
|||
|
|
// CJK 字符数 < 6 且前轮有效 → 继承; 否则走 TF-IDF
|
|||
|
|
const cjkCount = (prompt.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
|
|||
|
|
const isShortCJK = cjkCount > 0 && cjkCount < 6 && prompt.length < 20;
|
|||
|
|
if (isShortCJK) {
|
|||
|
|
const inheritResult = tryInherit();
|
|||
|
|
// 继承质量门控: 衰减后置信度 >= 0.5 才继承
|
|||
|
|
if (inheritResult && inheritResult.confidence >= 0.5) {
|
|||
|
|
routing = inheritResult;
|
|||
|
|
inherited = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!routing) {
|
|||
|
|
// medium / complex: 运行完整路由引擎
|
|||
|
|
routing = runRouteEngine(prompt, cwd, intent);
|
|||
|
|
|
|||
|
|
// v5.3: 会话级路由记忆 — 注入会话偏好加成
|
|||
|
|
const sessionMemory = safeRequire(path.join(SCRIPTS_DIR, 'session-memory.js'));
|
|||
|
|
if (sessionMemory && routing.candidates.length > 0) {
|
|||
|
|
try {
|
|||
|
|
const sessionId = sessionMemory.getSessionId();
|
|||
|
|
for (const c of routing.candidates) {
|
|||
|
|
const boost = sessionMemory.getSessionBoost(sessionId, c.name);
|
|||
|
|
if (boost > 0) c.confidence = Math.min(1.0, c.confidence + boost);
|
|||
|
|
}
|
|||
|
|
// 重新排序并更新 primary
|
|||
|
|
routing.candidates.sort((a, b) => b.confidence - a.confidence);
|
|||
|
|
if (routing.candidates[0]) {
|
|||
|
|
routing.primary = routing.candidates[0].name;
|
|||
|
|
routing.confidence = routing.candidates[0].confidence;
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// v5.3: A/B 实验 — 低置信差时随机探索
|
|||
|
|
const abTest = safeRequire(path.join(SCRIPTS_DIR, 'route-ab-test.js'));
|
|||
|
|
if (abTest && routing.candidates.length >= 2) {
|
|||
|
|
try {
|
|||
|
|
const top2 = routing.candidates.slice(0, 2);
|
|||
|
|
if (abTest.shouldExperiment(top2)) {
|
|||
|
|
const { selected, experiment } = abTest.selectVariant(top2[0].name, top2[1].name);
|
|||
|
|
routing.primary = selected;
|
|||
|
|
routing.experiment = experiment; // 记录实验信息供审计
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// v5.3: 记录技能使用到会话记忆
|
|||
|
|
if (sessionMemory && routing.primary && routing.primary !== 'none') {
|
|||
|
|
try {
|
|||
|
|
sessionMemory.recordSessionSkill(sessionMemory.getSessionId(), routing.primary);
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 写入 route-state
|
|||
|
|
writeRouteState(traceId, prompt, intent, routing);
|
|||
|
|
|
|||
|
|
// GH-6: 路由决策 trace event (提升 trace 覆盖率)
|
|||
|
|
try {
|
|||
|
|
const sessionTrace = require('../scripts/session-trace.js');
|
|||
|
|
sessionTrace.appendTraceEvent('route-interceptor-bundle', 'route-decision', {
|
|||
|
|
traceId, primary: routing.primary, confidence: routing.confidence
|
|||
|
|
});
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// 构建 [BWR] 指令
|
|||
|
|
const directive = buildBWRDirective(traceId, intent, routing, inherited);
|
|||
|
|
|
|||
|
|
// 输出 additionalContext (横幅 + 路由指令)
|
|||
|
|
const fullContext = bannerText ? bannerText + '\n\n' + directive : directive;
|
|||
|
|
const output = {
|
|||
|
|
hookSpecificOutput: {
|
|||
|
|
hookEventName: 'UserPromptSubmit',
|
|||
|
|
additionalContext: fullContext,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 写入 stdout (JSON 格式供 Claude Code 消费)
|
|||
|
|
process.stdout.write(JSON.stringify(output));
|
|||
|
|
} catch (e) {
|
|||
|
|
try { process.stderr.write('[route-err] ' + (e.message || '') + '\n'); } catch {}
|
|||
|
|
// 异常时静默放行
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
process.exit(0);
|
|||
|
|
}).catch(() => { clearTimeout(timeoutTimer); process.exit(0); });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模块导出 (供测试)
|
|||
|
|
if (typeof module !== 'undefined') {
|
|||
|
|
module.exports = {
|
|||
|
|
detectClaudeRoot: CLAUDE_ROOT,
|
|||
|
|
runRouteEngine,
|
|||
|
|
buildBWRDirective,
|
|||
|
|
writeRouteState,
|
|||
|
|
showActivationBanner,
|
|||
|
|
safeRequire,
|
|||
|
|
loadSkillsIndex,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (require.main === module) {
|
|||
|
|
main();
|
|||
|
|
}
|