#!/usr/bin/env node 'use strict'; /** * Phase 1 · T1.3 补丁 — stats-compiled.json 扩展 MCP 观测字段 * * 目的: * - generate-stats.js 纳入 mcpUtilization + mcpHealth 顶级字段 * - 数据源: scripts/mcp-usage-tracker.js 产出 + hooks/session-start-mcp-probe.js 产出 * - 不改 summary 结构 (保持向后兼容) * * 修改文件: * - scripts/generate-stats.js (单点插入 scanMcpObservability + 调用) * * 幂等: * - sentinel: PHASE1_T1_3_MCP_OBSERVABILITY_FIELDS_2026_04_24 * - 检测函数已存在则跳过 * * 原子性: * - .bak.phase1-t1.3 + tmp + rename * * 回滚: * - cp scripts/generate-stats.js.bak.phase1-t1.3 scripts/generate-stats.js * - 下次 generate-stats 运行将重写 stats-compiled.json 为旧格式 */ const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = path.join(__dirname, '..', '..'); const TARGET = path.join(CLAUDE_ROOT, 'scripts', 'generate-stats.js'); const BAK = TARGET + '.bak.phase1-t1.3'; const SENTINEL = 'PHASE1_T1_3_MCP_OBSERVABILITY_FIELDS_2026_04_24'; const SCAN_FUNCTION = ` // === ${SENTINEL} === // Phase 1 · T1.3: 读取 MCP 使用率 + 每日健康快照 function scanMcpObservability() { const obs = { mcpUtilization: null, mcpHealth: null }; const usageFile = path.join(ROOT, 'mcp-usage-week.json'); const logsDir = path.join(ROOT, 'logs'); const today = new Date().toISOString().slice(0, 10); const healthFile = path.join(logsDir, 'mcp-health-' + today + '.json'); try { if (fs.existsSync(usageFile)) { const u = JSON.parse(fs.readFileSync(usageFile, 'utf8')); obs.mcpUtilization = { schema_version: u.schema_version || 1, generated: u.generated, windowDays: u.windowDays, totalEvents: u.totalEvents, activeCount: Object.values(u.mcpStats || {}).filter(s => s.totalCalls > 0).length, pruneCandidateCount: (u.pruneCandidates || []).length, pruneCandidates: (u.pruneCandidates || []).map(p => p.server), criticalCount: (u.criticalSet || []).length }; } } catch {} try { if (fs.existsSync(healthFile)) { const h = JSON.parse(fs.readFileSync(healthFile, 'utf8')); obs.mcpHealth = { schema_version: h.schema_version || 1, date: h.date, probedAt: h.probedAt, probeKind: h.probeKind, totalMcps: h.totalMcps, reachable: h.reachable, unreachable: h.unreachable, unreachableList: h.unreachableList }; } } catch {} return obs; } // === END ${SENTINEL} === `; // 在 stats 对象中插入两个顶级字段 const INSERT_FIELDS = ` // Phase 1 · T1.3: MCP 观测字段 (${SENTINEL}) mcpUtilization: mcpObs.mcpUtilization, mcpHealth: mcpObs.mcpHealth, `; function main() { if (!fs.existsSync(TARGET)) { console.error('[patch-phase1-T1.3] 目标文件不存在:', TARGET); process.exit(1); } const before = fs.readFileSync(TARGET, 'utf8'); // 幂等检查 if (before.includes(SENTINEL)) { console.log('[patch-phase1-T1.3] 已打过补丁,跳过'); process.exit(0); } // 锚点 1: 在 function generateStats() 前插入 scanMcpObservability const anchor1 = 'function generateStats() {'; if (!before.includes(anchor1)) { console.error('[patch-phase1-T1.3] 锚点 1 缺失:', anchor1); process.exit(2); } // 锚点 2: 在 generateStats() 内调用 const anchor2 = 'const skillsIndex = readSkillsIndex();'; if (!before.includes(anchor2)) { console.error('[patch-phase1-T1.3] 锚点 2 缺失:', anchor2); process.exit(3); } // 锚点 3: stats 对象中 details 前插入新字段 (CRLF 无关) const anchor3Regex = /(^|\r?\n)(\s*)\/\/ 详细列表\s*\r?\n(\s*)details:\s*\{/; const anchor3Match = before.match(anchor3Regex); if (!anchor3Match) { console.error('[patch-phase1-T1.3] 锚点 3 正则未命中 details 段'); process.exit(4); } // 备份 fs.copyFileSync(TARGET, BAK); console.log('[patch-phase1-T1.3] 已备份:', BAK); let after = before; // 插入 1: scanMcpObservability 函数定义 after = after.replace(anchor1, SCAN_FUNCTION + anchor1); // 插入 2: generateStats 内调用 (在 readSkillsIndex 之后) after = after.replace( anchor2, anchor2 + '\n const mcpObs = scanMcpObservability(); // T1.3' ); // 插入 3: stats 对象字段 (在 details 之前) — 用正则保留原 CRLF after = after.replace(anchor3Regex, (_full, lead, indent1, indent2) => lead + indent1 + '// Phase 1 · T1.3: MCP 观测字段 (' + SENTINEL + ')\r\n' + indent1 + 'mcpUtilization: mcpObs.mcpUtilization,\r\n' + indent1 + 'mcpHealth: mcpObs.mcpHealth,\r\n' + '\r\n' + indent1 + '// 详细列表\r\n' + indent2 + 'details: {' ); // 验证插入成功 if (!after.includes(SENTINEL)) { console.error('[patch-phase1-T1.3] sentinel 注入失败'); process.exit(5); } if (!after.includes('const mcpObs = scanMcpObservability();')) { console.error('[patch-phase1-T1.3] 函数调用注入失败'); process.exit(6); } // 语法检查 (node --check 要求 .js 扩展才按 CJS 解析) const tmp = TARGET + '.tmp.js'; fs.writeFileSync(tmp, after, 'utf8'); try { const { execFileSync } = require('child_process'); execFileSync(process.execPath, ['--check', tmp], { stdio: 'pipe' }); } catch (e) { try { fs.unlinkSync(tmp); } catch {} console.error('[patch-phase1-T1.3] 语法检查失败:', (e.stderr || e.message || '').toString().slice(0, 500)); process.exit(7); } fs.renameSync(tmp, TARGET); console.log('[patch-phase1-T1.3] 已写入:', TARGET); console.log('[patch-phase1-T1.3] sentinel:', SENTINEL); console.log('[patch-phase1-T1.3] 完成。验证: node scripts/generate-stats.js --json | node -e \"const d=JSON.parse(require(\'fs\').readFileSync(0,\'utf8\')); console.log(JSON.stringify({mcpUtilization:d.mcpUtilization, mcpHealth:d.mcpHealth}, null, 2))\"'); process.exit(0); } main();