483 lines
17 KiB
JavaScript
483 lines
17 KiB
JavaScript
|
|
#!/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,
|
|||
|
|
};
|
|||
|
|
}
|