feat: DevTools 数据库监看面板 + 隧道控制 + 多项 Bug 修复
**DevTools 新增功能 (Tasks 13-14):** - 首页仪表盘添加数据库实时监看卡片 (5端口状态 + 记忆数) - 侧边栏新增数据库面板,支持自动 5 秒刷新 - 数据库面板显示 PostgreSQL/Redis/Qdrant/MinIO/NATS 端口状态 - 隧道控制按钮 (启动/停止/重启/查看状态) - 新增 API 端点: GET /api/database/status, POST /api/tunnel/:action - 更新 docs/api-reference/ API 文档 **Bug 修复 (Task 15):** - 修复 pgrep -f 自匹配导致隧道状态误判 (添加 ^ssh 锚点) - devtools/src/index.js (dashboard + database/status) - scripts/tunnel.sh (is_tunnel_running + show_status) - 修复数据库面板缺少自动刷新定时器 - 修复侧边栏数据库徽章永远 display:none - 修复僵尸进程场景下按钮死锁问题 **其他改进:** - .gitignore 添加 backend/cmd, backend/iot-debug-service/main - 前端多项改进 (登录/注册/会话/流式动画等)
This commit is contained in:
@@ -13,11 +13,15 @@ import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
|
||||
import { processManager } from './process-manager.js';
|
||||
import { performanceMonitor } from './performance.js';
|
||||
import { SERVICES, DEVTOOLS_PORT, LOGS_DIR, logFile, GATEWAY_URL, ADMIN_USERNAME, ADMIN_PASSWORD } from './config.js';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const TUNNEL_SCRIPT = path.join(ROOT, 'scripts/tunnel.sh');
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -194,6 +198,19 @@ app.get('/api/dashboard', async (_req, res) => {
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
|
||||
// 数据库状态(快速检查,不阻塞)
|
||||
let dbStatus = { checked: false };
|
||||
try {
|
||||
let tunnelRunning = false;
|
||||
try {
|
||||
// ^ssh 锚点确保只匹配实际的 SSH 进程,排除 pgrep 自身的 shell 包装器
|
||||
const out = execSync('pgrep -f "^ssh .*cyrene-tunnel"', { encoding: 'utf-8', timeout: 2000 });
|
||||
tunnelRunning = out.trim().length > 0;
|
||||
} catch { /* 未运行 */ }
|
||||
const port5432Alive = checkPort(5432);
|
||||
dbStatus = { checked: true, tunnelRunning, postgresAlive: port5432Alive };
|
||||
} catch { /* 忽略 */ }
|
||||
|
||||
const sysMem = process.memoryUsage();
|
||||
|
||||
res.json({
|
||||
@@ -202,6 +219,7 @@ app.get('/api/dashboard', async (_req, res) => {
|
||||
performance: { totalCpu: Math.round(totalCpu * 100) / 100, totalMem: Math.round(totalMem * 100) / 100, perService: perfSnapshot },
|
||||
sessions: { active: activeSessions, totalMessages },
|
||||
memory: { total: memoryCount },
|
||||
database: dbStatus,
|
||||
system: { heapUsedMB: Math.round(sysMem.heapUsed / 1024 / 1024 * 100) / 100, heapTotalMB: Math.round(sysMem.heapTotal / 1024 / 1024 * 100) / 100, uptime: process.uptime() },
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -461,6 +479,125 @@ app.get('/api/proxy/:id/health', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 数据库状态检查 ----
|
||||
// 需要检查的远程数据库端口映射 (对应 tunnel.sh 中的 SERVICES)
|
||||
const DB_PORTS = [
|
||||
{ port: 5432, name: 'PostgreSQL' },
|
||||
{ port: 6379, name: 'Redis' },
|
||||
{ port: 6334, name: 'Qdrant HTTP' },
|
||||
{ port: 9000, name: 'MinIO API' },
|
||||
{ port: 4222, name: 'NATS' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 检查本地端口是否在监听 (对应 tunnel 转发的远程服务)
|
||||
*/
|
||||
function checkPort(port) {
|
||||
try {
|
||||
const hexPort = port.toString(16).padStart(4, '0').toUpperCase();
|
||||
const tcpContent = fs.readFileSync('/proc/net/tcp', 'utf-8');
|
||||
for (const line of tcpContent.split('\n')) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length > 1 && parts[1]) {
|
||||
const localAddr = parts[1].split(':')[1];
|
||||
if (localAddr === hexPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* fallback: try TCP connect */ }
|
||||
// 备用方案: 使用 /dev/tcp (在 bash 中可用)
|
||||
try {
|
||||
execSync(`timeout 1 bash -c "echo >/dev/tcp/127.0.0.1/${port}" 2>/dev/null`, { timeout: 1500 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/api/database/status', (_req, res) => {
|
||||
// 检查 SSH 隧道进程是否运行
|
||||
let tunnelRunning = false;
|
||||
try {
|
||||
const out = execSync('pgrep -f "^ssh .*cyrene-tunnel"', { encoding: 'utf-8', timeout: 3000 });
|
||||
tunnelRunning = out.trim().length > 0;
|
||||
} catch { /* 没找到进程 */ }
|
||||
|
||||
// 检查各端口
|
||||
const ports = DB_PORTS.map(({ port, name }) => {
|
||||
const alive = checkPort(port);
|
||||
return { port, name, alive };
|
||||
});
|
||||
|
||||
const allAlive = ports.every(p => p.alive);
|
||||
const aliveCount = ports.filter(p => p.alive).length;
|
||||
|
||||
// 尝试获取 PostgreSQL 详情
|
||||
let pgDetails = null;
|
||||
if (ports.find(p => p.port === 5432)?.alive) {
|
||||
try {
|
||||
// 读取 .env 获取凭据
|
||||
const envPath = path.join(ROOT, 'backend', '.env');
|
||||
let pgUser = 'cyrene', pgPass = 'change_me', pgDb = 'cyrene_ai';
|
||||
if (fs.existsSync(envPath)) {
|
||||
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||
const mUser = envContent.match(/^POSTGRES_USER=(.+)$/m);
|
||||
const mPass = envContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
|
||||
const mDb = envContent.match(/^POSTGRES_DB=(.+)$/m);
|
||||
if (mUser) pgUser = mUser[1];
|
||||
if (mPass) pgPass = mPass[1];
|
||||
if (mDb) pgDb = mDb[1];
|
||||
}
|
||||
const out = execSync(
|
||||
`PGPASSWORD="${pgPass}" psql -h localhost -p 5432 -U "${pgUser}" -d "${pgDb}" -t -c "SELECT count(*) FROM memories;" 2>/dev/null`,
|
||||
{ encoding: 'utf-8', timeout: 5000 }
|
||||
);
|
||||
const match = out.match(/(\d+)/);
|
||||
pgDetails = { memories: match ? parseInt(match[1]) : 0, database: pgDb };
|
||||
} catch { /* pg query failed */ }
|
||||
}
|
||||
|
||||
res.json({
|
||||
timestamp: Date.now(),
|
||||
tunnelRunning,
|
||||
ports,
|
||||
allAlive,
|
||||
aliveCount,
|
||||
totalPorts: DB_PORTS.length,
|
||||
pgDetails,
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 隧道控制 ----
|
||||
app.post('/api/tunnel/:action', (req, res) => {
|
||||
const { action } = req.params;
|
||||
if (!['start', 'stop', 'restart', 'status'].includes(action)) {
|
||||
return res.status(400).json({ error: `不支持的操作: ${action},支持: start/stop/restart/status` });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(TUNNEL_SCRIPT)) {
|
||||
return res.status(404).json({ error: `隧道脚本不存在: ${TUNNEL_SCRIPT}` });
|
||||
}
|
||||
|
||||
try {
|
||||
const out = execSync(`bash "${TUNNEL_SCRIPT}" ${action}`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 20000,
|
||||
cwd: path.join(ROOT, 'scripts'),
|
||||
});
|
||||
res.json({ success: true, action, output: out.trim() });
|
||||
} catch (err) {
|
||||
// tunnel.sh 可能返回非零退出码但仍成功(如 start 时发现已在运行)
|
||||
const output = err.stdout || err.stderr || err.message;
|
||||
res.json({
|
||||
success: false,
|
||||
action,
|
||||
output: output.trim(),
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 启动 ==========
|
||||
// 启动性能监控
|
||||
performanceMonitor.start();
|
||||
|
||||
Reference in New Issue
Block a user