#!/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 文件。');