- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
182 lines
5.9 KiB
JavaScript
182 lines
5.9 KiB
JavaScript
#!/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();
|