#!/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, }; }