187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* 会话上下文追踪器 (v5.0)
|
|||
|
|
*
|
|||
|
|
* 维护最近 10 次技能调用的滑动窗口,
|
|||
|
|
* 计算候选技能的上下文相关分数。
|
|||
|
|
*
|
|||
|
|
* 核心函数:
|
|||
|
|
* recordSkillUsage(skillName) → 记录技能使用到滑动窗口
|
|||
|
|
* computeContextScore(candidateSkill, composableIndex) → 上下文分数 (0~1.0)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
|
|||
|
|
|
|||
|
|
const ROOT = detectClaudeRoot();
|
|||
|
|
const STATE_FILE = path.join(ROOT, 'debug', 'context-state.json');
|
|||
|
|
|
|||
|
|
// v5.9: 多项目隔离 — 按 cwd 分离状态文件
|
|||
|
|
let _isolator = null;
|
|||
|
|
try { _isolator = require('./project-isolator.js'); } catch {}
|
|||
|
|
function getStateFile(cwd) {
|
|||
|
|
if (_isolator && cwd) return _isolator.getIsolatedPath('context-state.json', cwd);
|
|||
|
|
return STATE_FILE;
|
|||
|
|
}
|
|||
|
|
const MAX_WINDOW = 10;
|
|||
|
|
const DECAY_FACTOR = 0.70; // v5.9: 降低衰减因子 (0.85→0.70),半衰期从 4.3 次降到 2.3 次
|
|||
|
|
const ANTI_STICKY_THRESHOLD = 3; // 连续同技能超过此次数时降低同技能加成
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加载上下文状态
|
|||
|
|
* @param {string} [cwd] - 工作目录 (v5.9 多项目隔离)
|
|||
|
|
* @returns {{ recentSkills: string[], updatedAt: string }}
|
|||
|
|
*/
|
|||
|
|
function loadState(cwd) {
|
|||
|
|
try {
|
|||
|
|
const file = getStateFile(cwd);
|
|||
|
|
if (fs.existsSync(file)) {
|
|||
|
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
return { recentSkills: [], updatedAt: new Date().toISOString() };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存上下文状态
|
|||
|
|
* @param {Object} state
|
|||
|
|
* @param {string} [cwd] - 工作目录 (v5.9 多项目隔离)
|
|||
|
|
*/
|
|||
|
|
function saveState(state, cwd) {
|
|||
|
|
const file = getStateFile(cwd);
|
|||
|
|
const dir = path.dirname(file);
|
|||
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|||
|
|
// P1-2: O_EXCL 文件锁 + 原子写入 (防并发竞态)
|
|||
|
|
const lockFile = file + '.lock';
|
|||
|
|
try {
|
|||
|
|
const lockFd = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|||
|
|
fs.writeSync(lockFd, String(process.pid));
|
|||
|
|
fs.closeSync(lockFd);
|
|||
|
|
} catch {
|
|||
|
|
try {
|
|||
|
|
const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs;
|
|||
|
|
if (lockAge < 10000) return; // 未过期,跳过
|
|||
|
|
fs.unlinkSync(lockFile);
|
|||
|
|
const lockFd2 = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|||
|
|
fs.writeSync(lockFd2, String(process.pid));
|
|||
|
|
fs.closeSync(lockFd2);
|
|||
|
|
} catch { return; }
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
const tmpFile = file + '.tmp.' + process.pid;
|
|||
|
|
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2) + '\n');
|
|||
|
|
fs.renameSync(tmpFile, file);
|
|||
|
|
} finally {
|
|||
|
|
try { fs.unlinkSync(lockFile); } catch {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 记录技能使用到滑动窗口
|
|||
|
|
* @param {string} skillName - 技能名称
|
|||
|
|
* @param {string} [cwd] - 工作目录 (v5.9 多项目隔离)
|
|||
|
|
*/
|
|||
|
|
function recordSkillUsage(skillName, cwd) {
|
|||
|
|
const state = loadState(cwd);
|
|||
|
|
state.recentSkills.push(skillName);
|
|||
|
|
// 保持窗口大小
|
|||
|
|
if (state.recentSkills.length > MAX_WINDOW) {
|
|||
|
|
state.recentSkills = state.recentSkills.slice(-MAX_WINDOW);
|
|||
|
|
}
|
|||
|
|
state.updatedAt = new Date().toISOString();
|
|||
|
|
saveState(state, cwd);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算候选技能的上下文相关分数
|
|||
|
|
* @param {string} candidateSkill - 候选技能名
|
|||
|
|
* @param {Object} composableIndex - { skillName → { enhances, requires, conflicts } }
|
|||
|
|
* @param {Object} [preloadedState] - 预加载的状态 (避免重复 I/O,由调用方一次性加载)
|
|||
|
|
* @returns {number} 上下文分数 (0~1.0)
|
|||
|
|
*/
|
|||
|
|
function computeContextScore(candidateSkill, composableIndex, preloadedState) {
|
|||
|
|
const state = preloadedState || loadState();
|
|||
|
|
const recent = state.recentSkills;
|
|||
|
|
if (recent.length === 0) return 0;
|
|||
|
|
|
|||
|
|
let score = 0;
|
|||
|
|
|
|||
|
|
// v5.9: 计算最近连续同技能次数 (反粘滞检测)
|
|||
|
|
let consecutiveSame = 0;
|
|||
|
|
for (let j = recent.length - 1; j >= 0; j--) {
|
|||
|
|
if (recent[j] === candidateSkill) consecutiveSame++;
|
|||
|
|
else break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (let i = 0; i < recent.length; i++) {
|
|||
|
|
const recentSkill = recent[recent.length - 1 - i]; // 最新的在前
|
|||
|
|
const decay = Math.pow(DECAY_FACTOR, i);
|
|||
|
|
const comp = composableIndex[recentSkill] || {};
|
|||
|
|
|
|||
|
|
// 同技能重复使用: 反粘滞 — 连续 >= 3 次时降低加成 (0.3→0.1)
|
|||
|
|
if (recentSkill === candidateSkill) {
|
|||
|
|
const sameBoost = consecutiveSame >= ANTI_STICKY_THRESHOLD ? 0.1 : 0.3;
|
|||
|
|
score += sameBoost * decay;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// composable enhances 关系 +0.5
|
|||
|
|
if (comp.enhances && comp.enhances.includes(candidateSkill)) {
|
|||
|
|
score += 0.5 * decay;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// composable requires 关系 +0.4
|
|||
|
|
if (comp.requires && comp.requires.includes(candidateSkill)) {
|
|||
|
|
score += 0.4 * decay;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 反向: 候选技能 enhances 最近使用的技能
|
|||
|
|
const candidateComp = composableIndex[candidateSkill] || {};
|
|||
|
|
if (candidateComp.enhances && candidateComp.enhances.includes(recentSkill)) {
|
|||
|
|
score += 0.3 * decay;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 上限 1.0
|
|||
|
|
// P2-2: sigmoid 归一化替代硬截断,保留高分区间区分度
|
|||
|
|
return score <= 0 ? 0 : Math.round((2.0 / (1.0 + Math.exp(-score)) - 1.0) * 100) / 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从 skills-index.json 构建 composable 索引
|
|||
|
|
* @param {Object} index - skills-index.json
|
|||
|
|
* @returns {Object} { skillName → composable }
|
|||
|
|
*/
|
|||
|
|
function buildComposableIndex(index) {
|
|||
|
|
const result = {};
|
|||
|
|
for (const skill of (index.skills || [])) {
|
|||
|
|
if (skill.composable) {
|
|||
|
|
result[skill.name] = skill.composable;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模块导出
|
|||
|
|
if (typeof module !== 'undefined') {
|
|||
|
|
module.exports = {
|
|||
|
|
loadState, saveState, recordSkillUsage,
|
|||
|
|
computeContextScore, buildComposableIndex,
|
|||
|
|
MAX_WINDOW, DECAY_FACTOR, ANTI_STICKY_THRESHOLD,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CLI 入口
|
|||
|
|
if (require.main === module) {
|
|||
|
|
const state = loadState();
|
|||
|
|
console.log('=== 上下文状态 ===');
|
|||
|
|
console.log(`最近技能 (${state.recentSkills.length}/${MAX_WINDOW}):`);
|
|||
|
|
for (let i = 0; i < state.recentSkills.length; i++) {
|
|||
|
|
const decay = Math.pow(DECAY_FACTOR, state.recentSkills.length - 1 - i);
|
|||
|
|
console.log(` ${i + 1}. ${state.recentSkills[i]} (decay: ${decay.toFixed(3)})`);
|
|||
|
|
}
|
|||
|
|
console.log(`更新时间: ${state.updatedAt}`);
|
|||
|
|
}
|