bookworm-smart-assistant/scripts/project-isolator.js

143 lines
4.1 KiB
JavaScript
Raw Permalink Normal View History

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