bookworm-smart-assistant/scripts/patches/patch-phase1-t1.3-stats-mcp-fields.js

182 lines
5.9 KiB
JavaScript
Raw Normal View History

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