199 lines
8.1 KiB
JavaScript
199 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 应用剩余手动补丁 (F1-2, F1-6, constitution-precheck 注册)
|
||
*
|
||
* 用法:
|
||
* node scripts/apply-remaining-patches.js # 预览
|
||
* node scripts/apply-remaining-patches.js --apply # 应用
|
||
*/
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const CLAUDE_ROOT = path.join(require('os').homedir(), '.claude');
|
||
const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks');
|
||
const dryRun = !process.argv.includes('--apply');
|
||
|
||
console.log('=== 剩余补丁应用工具 ===');
|
||
console.log(`模式: ${dryRun ? 'DRY-RUN (预览)' : 'APPLY'}`);
|
||
console.log('');
|
||
|
||
let patchCount = 0;
|
||
let errorCount = 0;
|
||
|
||
function backup(filePath) {
|
||
const bakPath = filePath + '.bak-remaining';
|
||
if (!fs.existsSync(bakPath)) {
|
||
fs.copyFileSync(filePath, bakPath);
|
||
console.log(` 已备份: ${bakPath}`);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// F1-2: route-interceptor.js — 消除 intent-classifier 双重调用
|
||
// ═══════════════════════════════════════════════════
|
||
console.log('[F1-2] route-interceptor.js — 消除 intent-classifier 双重调用');
|
||
try {
|
||
const riPath = path.join(HOOKS_DIR, 'route-interceptor.js');
|
||
let content = fs.readFileSync(riPath, 'utf8');
|
||
|
||
// 改动 1: 函数签名增加 precomputedIntent 参数
|
||
const sig1 = 'function runRouteEngine(prompt, cwd)';
|
||
const sig1New = 'function runRouteEngine(prompt, cwd, precomputedIntent)';
|
||
|
||
// 改动 2: 替换内部重复调用为使用传入参数
|
||
const dup1 = "const intentClassifier = safeRequire(path.join(SCRIPTS_DIR, 'intent-classifier.js'));\n";
|
||
const dup2 = " const intentResult = intentClassifier ? intentClassifier.classifyIntent(prompt) : { intents: [], entities: [] };";
|
||
const dupReplace = "// F1-2: 使用调用方传入的 intent,避免重复分类\n const intentResult = precomputedIntent || { intents: [], entities: [] };";
|
||
|
||
// 改动 3: main() 中传入 intent
|
||
const call1 = 'routing = runRouteEngine(prompt, cwd);';
|
||
const call1New = 'routing = runRouteEngine(prompt, cwd, intent);';
|
||
|
||
if (content.includes(sig1) && !content.includes(sig1New)) {
|
||
if (!dryRun) {
|
||
backup(riPath);
|
||
content = content.replace(sig1, sig1New);
|
||
// 替换内部双重调用(处理可能的缩进差异)
|
||
const dupPattern = /const intentClassifier = safeRequire\(path\.join\(SCRIPTS_DIR, 'intent-classifier\.js'\)\);\s*\n\s*const intentResult = intentClassifier \? intentClassifier\.classifyIntent\(prompt\) : \{ intents: \[\], entities: \[\] \};/;
|
||
content = content.replace(dupPattern, dupReplace);
|
||
content = content.replace(call1, call1New);
|
||
fs.writeFileSync(riPath, content);
|
||
console.log(` 已写入: ${riPath}`);
|
||
} else {
|
||
console.log(' [预览] 将修改函数签名、移除重复调用、传入已有 intent');
|
||
}
|
||
patchCount++;
|
||
} else if (content.includes(sig1New)) {
|
||
console.log(' [跳过] 已应用');
|
||
} else {
|
||
console.log(' [警告] 未找到目标代码,可能已修改');
|
||
errorCount++;
|
||
}
|
||
} catch (e) {
|
||
console.log(` [错误] ${e.message}`);
|
||
errorCount++;
|
||
}
|
||
console.log('');
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// F1-6: block-sensitive-files.js — self-healer 白名单
|
||
// ═══════════════════════════════════════════════════
|
||
console.log('[F1-6] block-sensitive-files.js — self-healer 安全白名单');
|
||
try {
|
||
const bsfPath = path.join(HOOKS_DIR, 'block-sensitive-files.js');
|
||
let content = fs.readFileSync(bsfPath, 'utf8');
|
||
|
||
const marker = 'SETTINGS_SAFE_KEYS';
|
||
if (content.includes(marker)) {
|
||
console.log(' [跳过] 白名单已存在');
|
||
} else {
|
||
// 在 "检查文件路径" 注释前插入白名单代码
|
||
const insertTarget = " // 检查文件路径\n for (const { pattern, reason } of SENSITIVE_PATH_PATTERNS) {\n if (pattern.test(filePath)) {";
|
||
const whitelistCode = ` // ─── F1-6: self-healer 白名单 ──────────────────────
|
||
// 允许 self-healer 修改 settings.json 中已知安全的配置键
|
||
const SETTINGS_SAFE_KEYS = new Set([
|
||
'skipDangerousModePermissionPrompt',
|
||
]);
|
||
function isSettingsSafeWrite(fp, c) {
|
||
if (!/[\\\\/]\\.claude[\\\\/]settings\\.json$/.test(fp)) return false;
|
||
if (!c) return false;
|
||
try {
|
||
const parsed = JSON.parse(c);
|
||
const keys = Object.keys(parsed);
|
||
return keys.length > 0 && keys.every(k => SETTINGS_SAFE_KEYS.has(k));
|
||
} catch { return false; }
|
||
}
|
||
// ──────────────────────────────────────────────────────
|
||
|
||
// 检查文件路径
|
||
for (const { pattern, reason } of SENSITIVE_PATH_PATTERNS) {
|
||
if (pattern.test(filePath)) {
|
||
// F1-6: self-healer 白名单豁免
|
||
if (isSettingsSafeWrite(filePath, content)) break;`;
|
||
|
||
if (content.includes(insertTarget)) {
|
||
if (!dryRun) {
|
||
backup(bsfPath);
|
||
content = content.replace(insertTarget, whitelistCode);
|
||
fs.writeFileSync(bsfPath, content);
|
||
console.log(` 已写入: ${bsfPath}`);
|
||
} else {
|
||
console.log(' [预览] 将在路径检查前插入 SETTINGS_SAFE_KEYS 白名单');
|
||
}
|
||
patchCount++;
|
||
} else {
|
||
console.log(' [警告] 未找到插入点,文件结构可能已变化');
|
||
errorCount++;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log(` [错误] ${e.message}`);
|
||
errorCount++;
|
||
}
|
||
console.log('');
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 注册 constitution-precheck.js 到 settings.json
|
||
// ═══════════════════════════════════════════════════
|
||
console.log('[注册] constitution-precheck.js → settings.json PreToolUse Write|Edit');
|
||
try {
|
||
const settingsPath = path.join(CLAUDE_ROOT, 'settings.json');
|
||
let content = fs.readFileSync(settingsPath, 'utf8');
|
||
const settings = JSON.parse(content);
|
||
|
||
const hookCmd = 'node C:/Users/janson9527us/.claude/hooks/constitution-precheck.js';
|
||
const preToolUse = settings.hooks?.PreToolUse || [];
|
||
|
||
// 检查是否已注册
|
||
const alreadyRegistered = preToolUse.some(entry =>
|
||
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('constitution-precheck'))
|
||
);
|
||
|
||
if (alreadyRegistered) {
|
||
console.log(' [跳过] 已注册');
|
||
} else {
|
||
// 找到 Write matcher,在其 hooks 数组末尾追加
|
||
let registered = false;
|
||
for (const entry of preToolUse) {
|
||
if (entry.matcher === 'Write' || entry.matcher === 'Edit') {
|
||
entry.hooks = entry.hooks || [];
|
||
entry.hooks.push({
|
||
type: 'command',
|
||
command: hookCmd,
|
||
timeout: 3000
|
||
});
|
||
registered = true;
|
||
}
|
||
}
|
||
|
||
if (registered) {
|
||
if (!dryRun) {
|
||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||
console.log(` 已写入: ${settingsPath}`);
|
||
} else {
|
||
console.log(' [预览] 将在 Write 和 Edit matcher 中注册 constitution-precheck');
|
||
}
|
||
patchCount++;
|
||
} else {
|
||
console.log(' [警告] 未找到 Write/Edit PreToolUse 配置');
|
||
errorCount++;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log(` [错误] ${e.message}`);
|
||
errorCount++;
|
||
}
|
||
|
||
console.log('');
|
||
console.log('=== 执行摘要 ===');
|
||
console.log(` 补丁数: ${patchCount}`);
|
||
console.log(` 错误数: ${errorCount}`);
|
||
if (dryRun) {
|
||
console.log('');
|
||
console.log('运行 --apply 以应用: node scripts/apply-remaining-patches.js --apply');
|
||
}
|
||
console.log('');
|
||
console.log('完成。回滚: 使用 .bak-remaining 文件。');
|