fix: DevTools Windows兼容性修复 + 日志UI重构 (7服务并排显示)

- process-manager: 移除ESM中的require(), 跨平台spawn(.exe/.cmd), path.dirname修复
- config: 自动检测Go二进制路径, Windows构建产物使用.exe后缀
- index: 移除SSH隧道代码, 改用Docker容器检查数据库状态
- index.html: 日志默认并排网格布局, 7个服务横向滚动, 数据库面板改用Docker控制
- docs: 更新Migration.md启动顺序(7服务+DevTools自动编译), README添加Windows用法

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 21:05:07 +08:00
parent 697ed72db4
commit e4d2eab9ad
6 changed files with 228 additions and 203 deletions
+37 -106
View File
@@ -23,7 +23,6 @@ const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || 'http://localhost:8
const VOICE_SERVICE_URL = process.env.VOICE_SERVICE_URL || 'http://localhost:8093';
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);
@@ -222,17 +221,11 @@ app.get('/api/dashboard', async (_req, res) => {
}
} catch { /* 忽略 */ }
// 数据库状态(快速检查,不阻塞
// 数据库状态(通过 TCP 端口检查 Docker 容器是否在运行
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 };
const port5432Alive = await isPortOpen(5432);
dbStatus = { checked: true, postgresAlive: port5432Alive };
} catch { /* 忽略 */ }
const sysMem = process.memoryUsage();
@@ -1036,65 +1029,42 @@ 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' },
];
import net from 'net';
/**
* 检查本地端口是否在监听 (对应 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;
}
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');
});
}
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 { /* 没找到进程 */ }
app.get('/api/database/status', async (_req, res) => {
const DB_PORTS = [
{ port: 5432, name: 'PostgreSQL' },
{ port: 6379, name: 'Redis' },
{ port: 6334, name: 'Qdrant gRPC' },
{ port: 9000, name: 'MinIO API' },
{ port: 4222, name: 'NATS' },
];
// 检查各端口
const ports = DB_PORTS.map(({ port, name }) => {
const alive = checkPort(port);
const results = await Promise.all(DB_PORTS.map(async ({ port, name }) => {
const alive = await isPortOpen(port);
return { port, name, alive };
});
}));
const allAlive = ports.every(p => p.alive);
const aliveCount = ports.filter(p => p.alive).length;
const allAlive = results.every(p => p.alive);
const aliveCount = results.filter(p => p.alive).length;
// 尝试获取 PostgreSQL 详情
let pgDetails = null;
if (ports.find(p => p.port === 5432)?.alive) {
const pgPort = results.find(p => p.port === 5432);
if (pgPort?.alive) {
try {
// 读取 .env 获取凭据
const envPath = path.join(ROOT, 'backend', '.env');
let pgUser = 'cyrene', pgPass = 'change_me', pgDb = 'cyrene_ai';
let pgUser = 'cyrene', pgPass = 'cyrene_pass', pgDb = 'cyrene_ai';
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8');
const mUser = envContent.match(/^POSTGRES_USER=(.+)$/m);
@@ -1105,8 +1075,8 @@ app.get('/api/database/status', (_req, res) => {
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 }
`docker exec cyrene_postgres psql -U "${pgUser}" -d "${pgDb}" -t -c "SELECT count(*) FROM memories;" 2>nul`,
{ encoding: 'utf-8', timeout: 5000, windowsHide: true }
);
const match = out.match(/(\d+)/);
pgDetails = { memories: match ? parseInt(match[1]) : 0, database: pgDb };
@@ -1115,8 +1085,7 @@ app.get('/api/database/status', (_req, res) => {
res.json({
timestamp: Date.now(),
tunnelRunning,
ports,
ports: results,
allAlive,
aliveCount,
totalPorts: DB_PORTS.length,
@@ -1124,55 +1093,17 @@ app.get('/api/database/status', (_req, res) => {
});
});
// ---- 隧道控制 ----
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,
});
}
});
// ---- 数据库控制 (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) => {
app.get('/api/db/status', async (_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(),
});
const online = await isPortOpen(DB_PORT);
res.json({ online, port: DB_PORT, checked_at: new Date().toISOString() });
} catch {
res.json({ online: false, port: DB_PORT, checked_at: new Date().toISOString() });
}
});