fix: 第二轮修复 — 数据库启动检查、会话持久化、URL路由、设备排序等

1. DevTools 启动前检查数据库状态,失败时自动尝试启动
2. ai-core 添加数据库断线重连机制 (30秒间隔)
3. Dashboard 添加数据库状态卡片 (启动/停止/重启)
4. Gateway 会话空闲超时管理 (30分钟标记空闲)
5. 会话/消息 PostgreSQL 持久化 (SessionStore + REST API)
6. 前端服务端会话持久化 + URL hash 路由 + 侧边栏管理
7. 管理员回到主对话按钮
8. IoT 设备卡片固定排序
9. 更新相关文档
This commit is contained in:
2026-05-17 17:18:02 +08:00
parent 745b1c6aad
commit e7b7eff0d8
21 changed files with 1735 additions and 284 deletions
+76
View File
@@ -696,6 +696,82 @@ app.post('/api/tunnel/:action', (req, res) => {
}
});
// ---- 数据库控制 (Docker Compose) ----
const DB_COMPOSE_FILE = path.join(ROOT, 'docker-compose.dev.db.yml');
const DB_PORT = 5432;
// GET /api/db/status
app.get('/api/db/status', (_req, res) => {
try {
const online = checkPort(DB_PORT);
res.json({
online,
port: DB_PORT,
checked_at: new Date().toISOString(),
});
} catch (err) {
res.json({
online: false,
port: DB_PORT,
checked_at: new Date().toISOString(),
});
}
});
// POST /api/db/start
app.post('/api/db/start', (_req, res) => {
try {
const out = execSync(`docker compose -f "${DB_COMPOSE_FILE}" up -d`, {
encoding: 'utf-8',
timeout: 60000,
stdio: 'pipe',
});
res.json({ success: true, action: 'start', output: out.trim() });
} catch (err) {
const stderr = err.stderr?.toString() || err.message;
res.status(500).json({ success: false, action: 'start', error: stderr });
}
});
// POST /api/db/stop
app.post('/api/db/stop', (_req, res) => {
try {
const out = execSync(`docker compose -f "${DB_COMPOSE_FILE}" down`, {
encoding: 'utf-8',
timeout: 30000,
stdio: 'pipe',
});
res.json({ success: true, action: 'stop', output: out.trim() });
} catch (err) {
const stderr = err.stderr?.toString() || err.message;
res.status(500).json({ success: false, action: 'stop', error: stderr });
}
});
// POST /api/db/restart
app.post('/api/db/restart', (_req, res) => {
try {
const downOut = execSync(`docker compose -f "${DB_COMPOSE_FILE}" down`, {
encoding: 'utf-8',
timeout: 30000,
stdio: 'pipe',
});
const upOut = execSync(`docker compose -f "${DB_COMPOSE_FILE}" up -d`, {
encoding: 'utf-8',
timeout: 60000,
stdio: 'pipe',
});
res.json({
success: true,
action: 'restart',
output: `down: ${downOut.trim()}\nup: ${upOut.trim()}`,
});
} catch (err) {
const stderr = err.stderr?.toString() || err.message;
res.status(500).json({ success: false, action: 'restart', error: stderr });
}
});
// ========== 启动 ==========
// 启动性能监控
performanceMonitor.start();
+72
View File
@@ -7,8 +7,16 @@ import { spawn, execSync } from 'child_process';
import { EventEmitter } from 'events';
import fs from 'fs';
import net from 'net';
import path from 'path';
import { fileURLToPath } from 'url';
import { SERVICES, logFile } from './config.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT = path.resolve(__dirname, '../..');
const DB_PORTS = [5432, 5433, 5434];
const DB_COMPOSE_FILE = path.join(ROOT, 'docker-compose.dev.db.yml');
/**
* 通过 TCP 连接尝试判断端口是否被占用,若被占用则尝试用 fuser 释放
*/
@@ -36,6 +44,64 @@ function releasePort(port) {
});
}
/**
* 检查端口是否可连接 (TCP connect, 超时2秒)
*/
function isPortOpen(port) {
return new Promise((resolve) => {
const sock = new net.Socket();
sock.setTimeout(2000);
sock.on('connect', () => { sock.destroy(); resolve(true); });
sock.on('error', () => { sock.destroy(); resolve(false); });
sock.on('timeout', () => { sock.destroy(); resolve(false); });
sock.connect(port, '127.0.0.1');
});
}
/**
* 确保数据库在线
* 检查 DB_PORTS 中至少有一个端口可用,若不可用则尝试 docker compose up
* 等待最多 30 秒检查数据库就绪
* @param {string} serviceId - 正在启动的服务 ID
* @param {EventEmitter} emitter - 用于发送日志事件
*/
async function ensureDBOnline(serviceId, emitter) {
// 1. 快速检查:任意数据库端口是否已在线
for (const port of DB_PORTS) {
if (await isPortOpen(port)) {
emitter.emit('log', serviceId, 'system', `数据库端口 ${port} 已在线`);
return;
}
}
// 2. 数据库不在线,尝试 docker compose up
emitter.emit('log', serviceId, 'system', '数据库未启动,正在通过 Docker Compose 启动...');
try {
execSync(`docker compose -f "${DB_COMPOSE_FILE}" up -d`, {
timeout: 60000,
stdio: 'pipe',
});
emitter.emit('log', serviceId, 'system', 'Docker Compose 启动命令已执行,等待数据库就绪...');
} catch (err) {
const stderr = err.stderr?.toString() || err.message;
emitter.emit('log', serviceId, 'error', `Docker Compose 启动失败: ${stderr}`);
}
// 3. 等待最多 30 秒检查数据库就绪
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 1000));
for (const port of DB_PORTS) {
if (await isPortOpen(port)) {
emitter.emit('log', serviceId, 'system', `数据库端口 ${port} 已就绪 (等待 ${i + 1}s)`);
return;
}
}
}
// 4. 30 秒后仍不可用
emitter.emit('log', serviceId, 'error', '⚠️ 数据库无法启动,请手动检查 Docker。将继续启动后端服务...');
}
class ProcessManager extends EventEmitter {
constructor() {
super();
@@ -65,6 +131,12 @@ class ProcessManager extends EventEmitter {
throw new Error(`${svc.name} 已在运行中`);
}
// 对 gateway 和 ai-core 做数据库前置检查
if (serviceId === 'gateway' || serviceId === 'ai-core') {
this.emit('log', serviceId, 'system', '检查数据库连接状态...');
await ensureDBOnline(serviceId, this);
}
// 启动前释放端口,避免 "address already in use"
if (svc.port) {
this.emit('log', serviceId, 'system', `检查端口 ${svc.port}...`);