bookworm-smart-assistant/scripts/archive/dashboard-server.js

483 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// Bookworm Smart Assistant v5.3 — 可视化中控 API 服务器
// 零 npm 依赖,纯 Node.js 内置模块
// 启动: node dashboard-server.js
// 访问: http://localhost:3210
const http = require('http');
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const url = require('url');
// ─── 配置 ───────────────────────────────────────────────
const PORT = parseInt(process.env.DASHBOARD_PORT, 10) || 3210;
const SCRIPT_CACHE_TTL = 15000; // 15s 缓存 (< 前端 30s 轮询间隔)
const SCRIPT_TIMEOUT = 120000; // 120s execSync 超时 (WSL 跨文件系统扫描较慢)
// ─── 根目录检测 ─────────────────────────────────────────
function detectClaudeRoot() {
if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
if (process.env.CLAUDE_ROOT) return process.env.CLAUDE_ROOT;
const selfDir = path.dirname(__filename);
if (selfDir.includes('.claude')) {
return selfDir.replace(/[/\\]scripts$/, '');
}
const IS_WSL = process.platform === 'linux' && fs.existsSync('/mnt/c');
if (IS_WSL) {
try {
const usersDir = '/mnt/c/Users';
for (const u of fs.readdirSync(usersDir)) {
const candidate = path.join(usersDir, u, '.claude');
if (fs.existsSync(candidate)) return candidate;
}
} catch {}
}
try { return require('./paths.config.js').PATHS.root; } catch { return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\/g, '/') + '/.claude'; }
}
const ROOT = detectClaudeRoot();
const SCRIPTS_DIR = path.join(ROOT, 'scripts');
const DEBUG_DIR = path.join(ROOT, 'debug');
const PROJECTS_DIR = path.join(ROOT, 'projects');
// ─── 工具函数 ───────────────────────────────────────────
/** 读取 JSONL 文件,返回数组,容错跳过坏行 */
function readJsonl(filePath) {
if (!fs.existsSync(filePath)) return [];
try {
const content = fs.readFileSync(filePath, 'utf8').trim();
if (!content) return [];
const lines = content.split('\n');
const entries = [];
for (const line of lines) {
if (!line.trim()) continue;
try { entries.push(JSON.parse(line)); } catch {}
}
return entries;
} catch { return []; }
}
/** 按日期范围读取 prefix-YYYY-MM-DD.jsonl 文件 */
function readJsonlByDateRange(prefix, days) {
const result = [];
const now = new Date();
for (let i = 0; i < days; i++) {
const d = new Date(now);
d.setDate(d.getDate() - i);
const dateStr = d.toISOString().slice(0, 10);
const filePath = path.join(DEBUG_DIR, `${prefix}${dateStr}.jsonl`);
result.push(...readJsonl(filePath));
}
return result;
}
/** 内存缓存 */
const _cache = {};
/** 异步带 TTL 缓存的脚本执行 */
function cachedRunScript(key, script, args = '', ttlMs = SCRIPT_CACHE_TTL) {
return new Promise((resolve) => {
const now = Date.now();
if (_cache[key] && (now - _cache[key].ts) < ttlMs) {
return resolve(_cache[key].data);
}
// 如果已在执行中,返回 pending promise 避免重复调用
if (_cache[key] && _cache[key].pending) {
return _cache[key].pending.then(resolve);
}
const cmd = `node "${path.join(SCRIPTS_DIR, script)}" ${args}`;
const pending = new Promise((res) => {
exec(cmd, {
timeout: SCRIPT_TIMEOUT,
encoding: 'utf8',
cwd: SCRIPTS_DIR,
env: { ...process.env, CLAUDE_HOME: ROOT },
maxBuffer: 10 * 1024 * 1024,
}, (err, stdout) => {
delete (_cache[key] || {}).pending;
if (err) {
if (err.killed) {
return res({ error: 'timeout', message: `脚本 ${script} 超时 (${SCRIPT_TIMEOUT}ms)` });
}
// 尝试解析部分输出
if (stdout) { try { return res(JSON.parse(stdout)); } catch {} }
return res({ error: 'script_error', message: err.message || String(err) });
}
try {
const data = JSON.parse(stdout);
_cache[key] = { ts: Date.now(), data };
res(data);
} catch {
res({ error: 'parse_error', message: '脚本输出不是有效 JSON', raw: (stdout || '').slice(0, 200) });
}
});
});
_cache[key] = { ..._cache[key], pending };
pending.then(resolve);
});
}
/** 统一 JSON 响应 */
function respondJson(res, code, data) {
const body = JSON.stringify(data);
res.writeHead(code, {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'no-cache',
});
res.end(body);
}
/** 统一 HTML 响应 */
function respondHtml(res, html) {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache',
});
res.end(html);
}
/** 扫描所有项目的 evolution-log.jsonl */
function findEvolutionLogs() {
const entries = [];
if (!fs.existsSync(PROJECTS_DIR)) return entries;
try {
for (const proj of fs.readdirSync(PROJECTS_DIR)) {
const evoPath = path.join(PROJECTS_DIR, proj, 'memory', 'evolution-log.jsonl');
entries.push(...readJsonl(evoPath));
}
} catch {}
// 按 seq 排序
entries.sort((a, b) => (a.seq || 0) - (b.seq || 0));
return entries;
}
/** 安全读取 JSON 文件 */
function readJsonFile(filePath) {
if (!fs.existsSync(filePath)) return null;
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch { return null; }
}
/** 格式化当前日期 YYYY-MM-DD */
function today() {
return new Date().toISOString().slice(0, 10);
}
/** 获取 JSONL 文件最后修改时间 */
function getLastModified(prefix) {
const dateStr = today();
const filePath = path.join(DEBUG_DIR, `${prefix}${dateStr}.jsonl`);
try {
const stat = fs.statSync(filePath);
return stat.mtime.toISOString();
} catch { return null; }
}
// ─── API 路由 ───────────────────────────────────────────
const routes = {};
// GET / — 提供 dashboard.html
routes['/'] = (req, res) => {
const htmlPath = path.join(SCRIPTS_DIR, 'dashboard.html');
if (!fs.existsSync(htmlPath)) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('dashboard.html not found');
return;
}
const html = fs.readFileSync(htmlPath, 'utf8');
respondHtml(res, html);
};
// GET /api/health — 系统健康检查
routes['/api/health'] = async (req, res) => {
const data = await cachedRunScript('health', 'health-check.js', '--json');
respondJson(res, 200, data);
};
// GET /api/disk — 磁盘报告
routes['/api/disk'] = async (req, res) => {
const data = await cachedRunScript('disk', 'auto-cleanup.js', '--report');
respondJson(res, 200, data);
};
// GET /api/weekly — 周报
routes['/api/weekly'] = async (req, res) => {
const data = await cachedRunScript('weekly', 'weekly-report.js', '--json');
respondJson(res, 200, data);
};
// GET /api/dashboard?range=N — 仪表盘统计
routes['/api/dashboard'] = async (req, res, query) => {
const range = parseInt(query.range, 10) || 7;
const data = await cachedRunScript(`dashboard-${range}`, 'dashboard.js', `--json --range ${range}`);
respondJson(res, 200, data);
};
// GET /api/activity?days=7 — 活动日志
routes['/api/activity'] = (req, res, query) => {
const days = Math.min(parseInt(query.days, 10) || 7, 90);
const entries = readJsonlByDateRange('activity-', days);
respondJson(res, 200, entries);
};
// GET /api/security?days=7 — 安全事件
routes['/api/security'] = (req, res, query) => {
const days = Math.min(parseInt(query.days, 10) || 7, 90);
const entries = readJsonlByDateRange('security-', days);
respondJson(res, 200, entries);
};
// GET /api/compliance?days=7 — 合规日志
routes['/api/compliance'] = (req, res, query) => {
const days = Math.min(parseInt(query.days, 10) || 7, 90);
const entries = readJsonlByDateRange('compliance-', days);
respondJson(res, 200, entries);
};
// GET /api/evolution — 进化日志 (全部项目)
routes['/api/evolution'] = (req, res) => {
const entries = findEvolutionLogs();
respondJson(res, 200, entries);
};
// GET /api/skills — 技能索引
routes['/api/skills'] = (req, res) => {
const data = readJsonFile(path.join(ROOT, 'skills-index.json'));
respondJson(res, 200, data || { error: 'skills-index.json not found' });
};
// GET /api/route-feedback — 路由反馈
routes['/api/route-feedback'] = (req, res) => {
const entries = readJsonl(path.join(DEBUG_DIR, 'route-feedback.jsonl'));
respondJson(res, 200, entries);
};
// GET /api/weights — 路由权重
routes['/api/weights'] = (req, res) => {
const data = readJsonFile(path.join(DEBUG_DIR, 'route-weights.json'));
respondJson(res, 200, data || {});
};
// GET /api/status — 心跳 + 数据源活跃度 (无缓存, 用于前端动态检测)
routes['/api/status'] = (req, res) => {
const now = new Date().toISOString();
const activityMtime = getLastModified('activity-');
const securityMtime = getLastModified('security-');
const complianceMtime = getLastModified('compliance-');
// 会话锁
let sessionInfo = null;
try {
sessionInfo = JSON.parse(fs.readFileSync(path.join(DEBUG_DIR, 'session-active.lock'), 'utf8'));
} catch {}
// Phase 3 数据源时间戳
function getFileMtime(filePath) {
try { return fs.statSync(filePath).mtime.toISOString(); } catch { return null; }
}
const detectionMtime = getFileMtime(path.join(DEBUG_DIR, 'detection-stats.json'));
const skillCorrMtime = getFileMtime(path.join(DEBUG_DIR, 'skill-outcome-correlation.json'));
const outcomeAggMtime = getFileMtime(path.join(DEBUG_DIR, 'outcome-aggregation.json'));
const traceMtime = getLastModified('trace-');
const remediationMtime = getFileMtime(path.join(DEBUG_DIR, 'remediation-log.jsonl'));
respondJson(res, 200, {
serverTime: now,
uptime: process.uptime(),
cacheTTL: SCRIPT_CACHE_TTL,
pollInterval: 30,
dataSources: {
activity: { lastModified: activityMtime },
security: { lastModified: securityMtime },
compliance: { lastModified: complianceMtime },
detectionStats: { lastModified: detectionMtime },
skillCorrelation: { lastModified: skillCorrMtime },
outcomeAggregation: { lastModified: outcomeAggMtime },
traces: { lastModified: traceMtime },
remediations: { lastModified: remediationMtime },
},
session: sessionInfo,
});
};
// GET /api/detection-stats — Phase 3 检测统计
routes['/api/detection-stats'] = (req, res) => {
const data = readJsonFile(path.join(DEBUG_DIR, 'detection-stats.json'));
respondJson(res, 200, data || { error: 'detection-stats.json not found' });
};
// GET /api/skill-correlation — Phase 3 技能关联
routes['/api/skill-correlation'] = (req, res) => {
const data = readJsonFile(path.join(DEBUG_DIR, 'skill-outcome-correlation.json'));
respondJson(res, 200, data || { error: 'skill-outcome-correlation.json not found' });
};
// GET /api/outcome-aggregation — Phase 3 构建聚合
routes['/api/outcome-aggregation'] = (req, res) => {
const data = readJsonFile(path.join(DEBUG_DIR, 'outcome-aggregation.json'));
respondJson(res, 200, data || { error: 'outcome-aggregation.json not found' });
};
// GET /api/traces?days=N — Phase 3 会话追踪
routes['/api/traces'] = (req, res, query) => {
const days = Math.min(parseInt(query.days, 10) || 7, 90);
const entries = readJsonlByDateRange('trace-', days);
respondJson(res, 200, entries);
};
// GET /api/remediations — Phase 3 修复日志
routes['/api/remediations'] = (req, res) => {
const entries = readJsonl(path.join(DEBUG_DIR, 'remediation-log.jsonl'));
respondJson(res, 200, entries);
};
// GET /api/config-validate — 配置一致性检查
routes['/api/config-validate'] = async (req, res) => {
const data = await cachedRunScript('configValidate', 'config-validator.js', '--json', 60000);
respondJson(res, 200, data);
};
// GET /api/health-history — 健康历史
routes['/api/health-history'] = (req, res) => {
let data = readJsonFile(path.join(DEBUG_DIR, 'health-weight-history.json'));
if (Array.isArray(data)) {
data = data.slice(-30); // 最多30条
}
respondJson(res, 200, data || []);
};
// ─── SSE 实时推送 ────────────────────────────────────────
const sseClients = new Set();
let lastMtimes = {};
/** 收集数据源 mtime 指纹 */
function collectMtimes() {
const mtimes = {};
const files = [
['detection', path.join(DEBUG_DIR, 'detection-stats.json')],
['skillCorr', path.join(DEBUG_DIR, 'skill-outcome-correlation.json')],
['outcome', path.join(DEBUG_DIR, 'outcome-aggregation.json')],
['remediation', path.join(DEBUG_DIR, 'remediation-log.jsonl')],
['weightHistory', path.join(DEBUG_DIR, 'health-weight-history.json')],
];
// 日期类文件
const dateStr = today();
const dateFiles = [
['activity', path.join(DEBUG_DIR, `activity-${dateStr}.jsonl`)],
['security', path.join(DEBUG_DIR, `security-${dateStr}.jsonl`)],
['compliance', path.join(DEBUG_DIR, `compliance-${dateStr}.jsonl`)],
['trace', path.join(DEBUG_DIR, `trace-${dateStr}.jsonl`)],
];
for (const [key, fp] of [...files, ...dateFiles]) {
try { mtimes[key] = fs.statSync(fp).mtimeMs; } catch { mtimes[key] = 0; }
}
return mtimes;
}
/** 检测变更并推送 SSE */
function checkAndPush() {
if (sseClients.size === 0) return;
const current = collectMtimes();
const changed = [];
for (const key of Object.keys(current)) {
if (current[key] !== (lastMtimes[key] || 0)) changed.push(key);
}
if (changed.length > 0) {
lastMtimes = current;
const msg = `data: ${JSON.stringify({ type: 'data-changed', sources: changed, ts: new Date().toISOString() })}\n\n`;
for (const client of sseClients) {
try { client.write(msg); } catch { sseClients.delete(client); }
}
}
}
// 每 5 秒检查文件变更
setInterval(checkAndPush, 5000);
lastMtimes = collectMtimes();
// GET /api/events — SSE 实时推送
routes['/api/events'] = (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
res.write(`data: ${JSON.stringify({ type: 'connected', ts: new Date().toISOString() })}\n\n`);
sseClients.add(res);
req.on('close', () => sseClients.delete(res));
};
// ─── HTTP 服务器 ────────────────────────────────────────
const server = http.createServer((req, res) => {
// OPTIONS 预检
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
});
res.end();
return;
}
if (req.method !== 'GET') {
respondJson(res, 405, { error: 'Method not allowed' });
return;
}
const parsed = url.parse(req.url, true);
const pathname = parsed.pathname;
const query = parsed.query || {};
const handler = routes[pathname];
if (handler) {
Promise.resolve(handler(req, res, query)).catch(e => {
respondJson(res, 500, { error: 'Internal error', message: e.message });
});
} else {
respondJson(res, 404, { error: 'Not found', path: pathname });
}
});
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.error(`\n❌ 端口 ${PORT} 已被占用!`);
console.error(` 请设置环境变量: DASHBOARD_PORT=3211 node dashboard-server.js\n`);
process.exit(1);
}
throw e;
});
// 直接运行时启动服务器require 引入时不启动
if (require.main === module) {
server.listen(PORT, () => {
console.log(`\n🎛️ Bookworm 控制中心已启动`);
console.log(` 地址: http://localhost:${PORT}`);
console.log(` 根目录: ${ROOT}`);
console.log(` 缓存 TTL: ${SCRIPT_CACHE_TTL / 1000}s`);
console.log(` 按 Ctrl+C 停止\n`);
});
}
// ─── 模块导出 (供测试) ──────────────────────────────────
if (typeof module !== 'undefined') {
module.exports = {
detectClaudeRoot,
readJsonl,
readJsonlByDateRange,
cachedRunScript,
respondJson,
findEvolutionLogs,
readJsonFile,
routes,
ROOT,
};
}