143 lines
4.1 KiB
JavaScript
143 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 多项目上下文隔离器 (v5.9)
|
|
*
|
|
* 为不同工作目录 (cwd) 提供隔离的状态文件路径,
|
|
* 防止项目 A 的上下文/权重污染项目 B 的路由。
|
|
*
|
|
* 隔离文件:
|
|
* - context-state.json → context-state-{hash}.json
|
|
* - fusion-weights.json → fusion-weights-{hash}.json
|
|
* - route-weights.json → route-weights-{hash}.json
|
|
*
|
|
* 非隔离 (全局共享):
|
|
* - route-feedback.jsonl (全局学习语料)
|
|
* - skill-outcome.jsonl (全局观测)
|
|
* - skills-index.json (技能定义)
|
|
*
|
|
* 用法:
|
|
* const { getIsolatedPath } = require('./project-isolator.js');
|
|
* const stateFile = getIsolatedPath('context-state.json', cwd);
|
|
*/
|
|
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
|
|
|
|
const ROOT = detectClaudeRoot();
|
|
const DEBUG_DIR = path.join(ROOT, 'debug');
|
|
|
|
// 需要隔离的文件名列表
|
|
const ISOLATED_FILES = new Set([
|
|
'context-state.json',
|
|
'fusion-weights.json',
|
|
'route-weights.json',
|
|
'session-memory.json', // 防止跨项目会话偏好污染
|
|
]);
|
|
|
|
/**
|
|
* 为 cwd 生成短哈希 (6 字符)
|
|
* @param {string} cwd - 工作目录
|
|
* @returns {string} 6 位 hex 哈希
|
|
*/
|
|
function cwdHash(cwd) {
|
|
if (!cwd) return 'global';
|
|
const normalized = cwd.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
return crypto.createHash('md5').update(normalized).digest('hex').slice(0, 6);
|
|
}
|
|
|
|
/**
|
|
* 获取隔离后的文件路径
|
|
* @param {string} filename - 原始文件名 (如 'context-state.json')
|
|
* @param {string} [cwd] - 工作目录 (null/undefined 使用全局路径)
|
|
* @returns {string} 隔离后的完整路径
|
|
*/
|
|
function getIsolatedPath(filename, cwd) {
|
|
if (!cwd || !ISOLATED_FILES.has(filename)) {
|
|
return path.join(DEBUG_DIR, filename);
|
|
}
|
|
const hash = cwdHash(cwd);
|
|
const ext = path.extname(filename);
|
|
const base = path.basename(filename, ext);
|
|
return path.join(DEBUG_DIR, `${base}-${hash}${ext}`);
|
|
}
|
|
|
|
/**
|
|
* 列出某个基础文件的所有隔离实例
|
|
* @param {string} filename - 原始文件名
|
|
* @returns {Array<{path: string, hash: string, mtime: Date}>}
|
|
*/
|
|
function listInstances(filename) {
|
|
const ext = path.extname(filename);
|
|
const base = path.basename(filename, ext);
|
|
const pattern = new RegExp(`^${base}-([a-f0-9]{6})${ext.replace('.', '\\.')}$`);
|
|
|
|
try {
|
|
return fs.readdirSync(DEBUG_DIR)
|
|
.filter(f => pattern.test(f))
|
|
.map(f => {
|
|
const match = f.match(pattern);
|
|
const fullPath = path.join(DEBUG_DIR, f);
|
|
const stat = fs.statSync(fullPath);
|
|
return { path: fullPath, hash: match[1], mtime: stat.mtime };
|
|
})
|
|
.sort((a, b) => b.mtime - a.mtime);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 清理过期隔离文件 (超过 30 天未修改)
|
|
* @returns {number} 清理数量
|
|
*/
|
|
function cleanStale(maxAgeDays = 30) {
|
|
const cutoff = Date.now() - maxAgeDays * 86400000;
|
|
let cleaned = 0;
|
|
for (const filename of ISOLATED_FILES) {
|
|
for (const inst of listInstances(filename)) {
|
|
if (inst.mtime.getTime() < cutoff) {
|
|
try { fs.unlinkSync(inst.path); cleaned++; } catch {}
|
|
}
|
|
}
|
|
}
|
|
return cleaned;
|
|
}
|
|
|
|
// 模块导出
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = {
|
|
cwdHash, getIsolatedPath, listInstances, cleanStale,
|
|
ISOLATED_FILES, DEBUG_DIR,
|
|
};
|
|
}
|
|
|
|
// CLI 入口
|
|
if (require.main === module) {
|
|
console.log('=== 多项目隔离器 ===');
|
|
|
|
// 列出所有隔离实例
|
|
for (const filename of ISOLATED_FILES) {
|
|
const instances = listInstances(filename);
|
|
console.log(`\n${filename}: ${instances.length} 个实例`);
|
|
for (const inst of instances) {
|
|
console.log(` [${inst.hash}] ${inst.mtime.toISOString().slice(0, 19)}`);
|
|
}
|
|
}
|
|
|
|
// 测试
|
|
const testCwds = [
|
|
'C:\\Users\\home\\project-a',
|
|
'C:\\Users\\home\\project-b',
|
|
'/opt/xianyuzhushou',
|
|
];
|
|
console.log('\n测试路径映射:');
|
|
for (const cwd of testCwds) {
|
|
const hash = cwdHash(cwd);
|
|
console.log(` ${cwd} → ${hash}`);
|
|
console.log(` context-state: ${path.basename(getIsolatedPath('context-state.json', cwd))}`);
|
|
}
|
|
}
|