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:
+37
-106
@@ -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() });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user