feat: DevTools调试工具 + 前端样式修复 + 管理员登录系统

DevTools (新增):
- 进程管理器: 启动/停止/重启/编译 + 端口自动释放
- 服务接管 (tryAdopt): 检测已运行服务,健康检查通过则直接接管
- 一键启动 (startAllSequential): 按 ai-core→gateway→frontend 顺序启动
- 日志布局切换: 标签页模式 ↔ 三栏并列模式
- 性能监控: CPU/内存采样 + SVG 折线图
- Web UI + WebSocket 实时推送

前端修复:
- tailwind.config.ts: 修复空配置导致 CSS 不加载 (增加 content/colors/fontFamily)
- postcss.config.js: 新建缺失的 PostCSS 配置
- App.tsx: 移除注册功能,仅保留管理员登录 (admin / cyrene-dev-admin)

后端新增:
- config.go: AdminUsername/AdminPassword/RegistrationEnabled 环境变量
- auth_handler.go: 管理员登录 + 注册邮箱验证码 + 注册开关控制
- 管理员凭据: admin / cyrene-dev-admin (默认)

其他:
- .gitignore: 新增 devtools/node_modules/ devtools/logs/ devtools/package-lock.json
- devtools.sh: DevTools 一键启动脚本
This commit is contained in:
2026-05-16 10:49:43 +08:00
parent 86b70b1613
commit cd60b01cf3
32 changed files with 4569 additions and 2845 deletions
+298
View File
@@ -0,0 +1,298 @@
/**
* Cyrene DevTools - 主入口
*
* 提供:
* - REST API: 服务管理、状态查询、性能分析、健康检查代理
* - WebSocket: 实时日志推送
* - Web UI: 管理控制台
*/
import express from 'express';
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { processManager } from './process-manager.js';
import { performanceMonitor } from './performance.js';
import { SERVICES, DEVTOOLS_PORT, LOGS_DIR, logFile } from './config.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ========== 初始化 ==========
const app = express();
app.use(express.json());
// 静态文件 - Web控制台
app.use(express.static(path.join(__dirname, '../public')));
// ========== WebSocket ==========
const server = http.createServer(app);
const wss = new WebSocketServer({ server, path: '/ws' });
/** @type {Set<WebSocket>} */
const wsClients = new Set();
wss.on('connection', (ws) => {
wsClients.add(ws);
ws.on('close', () => wsClients.delete(ws));
});
/** 广播到所有WebSocket客户端 */
function broadcast(type, data) {
const msg = JSON.stringify({ type, data, ts: Date.now() });
for (const ws of wsClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(msg);
}
}
}
// 日志事件 -> WebSocket广播
processManager.on('log', (serviceId, stream, text) => {
broadcast('log', { service: serviceId, stream, text: text.trimEnd() });
});
// 状态变化
processManager.on('log', (serviceId, stream, text) => {
if (stream === 'system') {
broadcast('status', processManager.getStatus());
}
});
// ========== REST API 路由 ==========
// ---- 健康检查 ----
app.get('/api/health', (_req, res) => {
res.json({
status: 'ok',
service: 'cyrene-devtools',
uptime: process.uptime(),
wsClients: wsClients.size,
});
});
// ---- 服务状态 ----
app.get('/api/services', (_req, res) => {
res.json(processManager.getStatus());
});
app.get('/api/services/:id', (req, res) => {
const status = processManager.getServiceStatus(req.params.id);
if (!status) return res.status(404).json({ error: '未知服务' });
res.json(status);
});
// ---- 服务控制 ----
app.post('/api/services/:id/start', async (req, res) => {
try {
const result = await processManager.start(req.params.id);
broadcast('status', processManager.getStatus());
res.json(result);
} catch (err) {
res.status(400).json({ success: false, message: err.message });
}
});
app.post('/api/services/:id/stop', async (req, res) => {
try {
const result = await processManager.stop(req.params.id);
broadcast('status', processManager.getStatus());
res.json(result);
} catch (err) {
res.status(400).json({ success: false, message: err.message });
}
});
app.post('/api/services/:id/restart', async (req, res) => {
try {
// 异步重启,因为可能耗时较长
res.json({ success: true, message: '重启中...' });
const result = await processManager.restart(req.params.id);
broadcast('status', processManager.getStatus());
} catch (err) {
// 已经在上面res了
}
});
app.post('/api/services/:id/build', async (req, res) => {
try {
const result = await processManager.build(req.params.id);
res.json(result);
} catch (err) {
res.status(400).json({ success: false, message: err.message });
}
});
// 批量操作
// 一键按顺序启动 (接管已运行 + 健康检查)
app.post('/api/services/start-all', async (_req, res) => {
const results = await processManager.startAllSequential();
broadcast('status', processManager.getStatus());
res.json(results);
});
// 强制重启全部 (先杀后启)
app.post('/api/services/start-all-fresh', async (_req, res) => {
await processManager.stopAll();
await new Promise((r) => setTimeout(r, 1000));
const results = await processManager.startAllSequential();
broadcast('status', processManager.getStatus());
res.json(results);
});
app.post('/api/services/stop-all', async (_req, res) => {
const results = await processManager.stopAll();
broadcast('status', processManager.getStatus());
res.json(results);
});
// ---- 性能监控 ----
app.get('/api/performance', async (_req, res) => {
const snapshot = await performanceMonitor.getSnapshot();
res.json(snapshot);
});
app.get('/api/performance/history', (_req, res) => {
res.json(performanceMonitor.getAllHistory());
});
app.get('/api/performance/:id', (req, res) => {
const history = performanceMonitor.getHistory(req.params.id);
res.json(history);
});
app.get('/api/performance/:id/summary', async (req, res) => {
const snap = await performanceMonitor.getSnapshot();
const history = performanceMonitor.getHistory(req.params.id);
const svc = snap[req.params.id];
if (!svc) return res.status(404).json({ error: '未知服务' });
// 计算汇总统计
let maxCpu = 0, maxMem = 0;
const cpuValues = [], memValues = [];
for (const h of history) {
if (h.cpu > maxCpu) maxCpu = h.cpu;
if (h.mem > maxMem) maxMem = h.mem;
cpuValues.push(h.cpu);
memValues.push(h.mem);
}
const avgCpu = cpuValues.length > 0 ? Math.round(cpuValues.reduce((a, b) => a + b, 0) / cpuValues.length * 100) / 100 : 0;
const avgMem = memValues.length > 0 ? Math.round(memValues.reduce((a, b) => a + b, 0) / memValues.length * 100) / 100 : 0;
res.json({
current: svc,
history: { count: history.length, maxCpu, maxMem, avgCpu, avgMem },
});
});
// ---- 日志查询 ----
app.get('/api/logs/:id', (req, res) => {
const id = req.params.id;
if (!SERVICES[id]) return res.status(404).json({ error: '未知服务' });
const filePath = logFile(id);
const lines = req.query.lines ? parseInt(req.query.lines) : 200;
const offset = req.query.offset ? parseInt(req.query.offset) : 0;
if (!fs.existsSync(filePath)) {
return res.json({ service: id, lines: [], total: 0 });
}
try {
// 使用tail方式读取
const content = fs.readFileSync(filePath, 'utf-8');
const allLines = content.split('\n').filter(Boolean);
const total = allLines.length;
const sliced = allLines.slice(Math.max(0, total - lines - offset), total - offset);
res.json({ service: id, lines: sliced, total });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/logs/:id/recent', (req, res) => {
const id = req.params.id;
if (!SERVICES[id]) return res.status(404).json({ error: '未知服务' });
const filePath = logFile(id);
if (!fs.existsSync(filePath)) {
return res.json({ service: id, lines: [], total: 0 });
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
const allLines = content.split('\n').filter(Boolean);
const total = allLines.length;
res.json({ service: id, lines: allLines.slice(-100), total });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 删除日志
app.delete('/api/logs/:id', (req, res) => {
const id = req.params.id;
if (!SERVICES[id]) return res.status(404).json({ error: '未知服务' });
const filePath = logFile(id);
try {
if (fs.existsSync(filePath)) {
fs.writeFileSync(filePath, '');
}
res.json({ success: true, message: '日志已清空' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ---- 健康检查代理 ----
app.get('/api/proxy/:id/health', async (req, res) => {
const svc = SERVICES[req.params.id];
if (!svc || !svc.healthUrl) {
return res.status(404).json({ error: '未知服务或无健康检查端点' });
}
try {
const resp = await fetch(svc.healthUrl, { signal: AbortSignal.timeout(5000) });
const data = await resp.json();
res.json({ proxy: true, service: req.params.id, status: resp.status, data });
} catch {
res.json({ proxy: true, service: req.params.id, status: 'unreachable', data: null });
}
});
// ========== 启动 ==========
// 启动性能监控
performanceMonitor.start();
// 确保日志目录存在
fs.mkdirSync(LOGS_DIR, { recursive: true });
server.listen(DEVTOOLS_PORT, () => {
console.log(`🛠️ Cyrene DevTools 已启动: http://localhost:${DEVTOOLS_PORT}`);
console.log(` API: http://localhost:${DEVTOOLS_PORT}/api/health`);
console.log(` WebSocket: ws://localhost:${DEVTOOLS_PORT}/ws`);
console.log(` Web控制台: http://localhost:${DEVTOOLS_PORT}`);
console.log('');
console.log(' 可用服务:');
for (const [id, svc] of Object.entries(SERVICES)) {
console.log(` - ${svc.name} (${id}): ${svc.healthUrl || 'N/A'}`);
}
});
// 优雅退出
process.on('SIGINT', async () => {
console.log('\n关闭所有服务...');
await processManager.stopAll();
performanceMonitor.stop();
process.exit(0);
});
process.on('SIGTERM', async () => {
await processManager.stopAll();
performanceMonitor.stop();
process.exit(0);
});