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
+64 -54
View File
@@ -695,11 +695,11 @@ const STATE = {
serviceStatus: {},
// 日志
activeLogTab: 'ai-core',
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [], 'iot-debug-service': [] },
logLines: { 'ai-core': [], 'gateway': [], 'frontend': [], 'iot-debug-service': [], 'memory-service': [], 'tool-engine': [], 'voice-service': [] },
maxLogLines: 500,
logLayout: 'tabs',
logLayout: 'grid',
// 性能
perfHistory: { 'ai-core': [], 'gateway': [], 'iot-debug-service': [], 'frontend': [] },
perfHistory: { 'ai-core': [], 'gateway': [], 'iot-debug-service': [], 'frontend': [], 'memory-service': [], 'tool-engine': [], 'voice-service': [] },
// 会话
sessionsData: [],
sessionsAutoRefresh: null,
@@ -868,8 +868,10 @@ function statusBadge(status) {
return map[status] || 'badge-stopped';
}
const ALL_SVC_IDS = ['ai-core', 'gateway', 'frontend', 'iot-debug-service', 'memory-service', 'tool-engine', 'voice-service'];
function escapeId(id) {
const map = { 'ai-core': 'AI-Core', 'gateway': 'Gateway', 'frontend': 'Frontend', 'iot-debug-service': 'IoT Debug' };
const map = { 'ai-core': 'AI-Core', 'gateway': 'Gateway', 'frontend': 'Frontend', 'iot-debug-service': 'IoT Debug', 'memory-service': 'Memory', 'tool-engine': 'Tool Engine', 'voice-service': 'Voice' };
return map[id] || id;
}
@@ -2033,21 +2035,24 @@ function renderServicesPanel() {
<span class="card-title">📋 实时日志</span>
<div class="btn-group">
<div class="log-tabs" id="services-log-tabs" style="margin:0"></div>
<button class="btn btn-xs" onclick="toggleSvcLogLayout()" id="btn-svc-log-layout">📐 并列</button>
<button class="btn btn-xs" onclick="toggleSvcLogLayout()" id="btn-svc-log-layout">📋 标签页</button>
<button class="btn btn-xs" onclick="clearSvcLogs()">🗑 清空</button>
</div>
</div>
<div id="services-log-tabs-panel">
<div id="services-log-tabs-panel" style="display:none">
<div class="log-container" id="services-log-panel">
<div class="empty-state"><div class="icon">📝</div>等待日志输出...</div>
</div>
</div>
<div id="services-log-grid" style="display:none">
<div class="cards-grid cards-4">
<div class="log-container" id="log-panel-ai-core" style="height:280px"><div class="empty-state"><div class="icon">📝</div>AI-Core</div></div>
<div class="log-container" id="log-panel-gateway" style="height:280px"><div class="empty-state"><div class="icon">📝</div>Gateway</div></div>
<div class="log-container" id="log-panel-iot-debug-service" style="height:280px"><div class="empty-state"><div class="icon">📝</div>IoT Debug</div></div>
<div class="log-container" id="log-panel-frontend" style="height:280px"><div class="empty-state"><div class="icon">📝</div>Frontend</div></div>
<div id="services-log-grid">
<div class="svc-log-grid" style="display:flex;gap:12px;overflow-x:auto;padding-bottom:8px">
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-ai-core" style="height:300px"><div class="empty-state"><div class="icon">📝</div>AI-Core</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-gateway" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Gateway</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-iot-debug-service" style="height:300px"><div class="empty-state"><div class="icon">📝</div>IoT Debug</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-frontend" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Frontend</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-memory-service" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Memory</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-tool-engine" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Tool Engine</div></div></div>
<div class="svc-log-col" style="min-width:320px;flex:1"><div class="log-container" id="log-panel-voice-service" style="height:300px"><div class="empty-state"><div class="icon">📝</div>Voice</div></div></div>
</div>
</div>
</div>
@@ -2055,14 +2060,19 @@ function renderServicesPanel() {
renderServiceCards();
initSvcLogTabs();
renderServiceLog();
if (STATE.logLayout === 'grid') {
document.getElementById('services-log-tabs').style.display = 'none';
ALL_SVC_IDS.forEach(id => renderGridLog(id));
} else {
renderServiceLog();
}
}
function renderServiceCards() {
const container = document.getElementById('services-svc-cards');
if (!container) return;
const status = STATE.serviceStatus;
const ids = Object.keys(status).length > 0 ? Object.keys(status) : ['ai-core', 'gateway', 'iot-debug-service', 'frontend'];
const ids = Object.keys(status).length > 0 ? Object.keys(status) : ALL_SVC_IDS;
container.innerHTML = ids.map(id => {
const svc = status[id] || { name: escapeId(id), status: 'unknown', pid: null, port: '—', uptime: 0, healthUrl: null };
@@ -2096,7 +2106,7 @@ function renderServiceCards() {
function initSvcLogTabs() {
const tabs = document.getElementById('services-log-tabs');
if (!tabs) return;
tabs.innerHTML = ['ai-core', 'gateway', 'iot-debug-service', 'frontend'].map(id =>
tabs.innerHTML = ALL_SVC_IDS.map(id =>
`<button class="log-tab ${id === STATE.activeLogTab ? 'active' : ''}" onclick="switchSvcLogTab('${id}')">${escapeId(id)}</button>`
).join('');
}
@@ -2141,20 +2151,20 @@ function toggleSvcLogLayout() {
const gridPanel = document.getElementById('services-log-grid');
const logTabs = document.getElementById('services-log-tabs');
if (STATE.logLayout === 'tabs') {
if (STATE.logLayout === 'grid') {
STATE.logLayout = 'tabs';
gridPanel.style.display = 'none';
tabsPanel.style.display = '';
logTabs.style.display = '';
btn.textContent = '📐 并列';
renderServiceLog();
} else {
STATE.logLayout = 'grid';
tabsPanel.style.display = 'none';
gridPanel.style.display = '';
logTabs.style.display = 'none';
btn.textContent = '📋 标签页';
['ai-core', 'gateway', 'iot-debug-service', 'frontend'].forEach(id => renderGridLog(id));
} else {
STATE.logLayout = 'tabs';
tabsPanel.style.display = '';
gridPanel.style.display = 'none';
logTabs.style.display = '';
btn.textContent = '📐 并列';
renderServiceLog();
ALL_SVC_IDS.forEach(id => renderGridLog(id));
}
}
@@ -2233,7 +2243,7 @@ async function refreshPerf() {
function renderPerfPanels(snap) {
const container = document.getElementById('perf-panels');
if (!container) return;
const ids = Object.keys(snap).length > 0 ? Object.keys(snap) : ['ai-core', 'gateway', 'iot-debug-service', 'frontend'];
const ids = Object.keys(snap).length > 0 ? Object.keys(snap) : ALL_SVC_IDS;
// Bug 7: 增量更新 — 首次创建完整 DOM,后续只更新图表和数值
const isFirstRender = !container.querySelector('.perf-card');
@@ -2315,7 +2325,6 @@ async function renderDatabasePanel() {
}
const ports = data.ports || [];
const tunnelRunning = data.tunnelRunning;
const allAlive = data.allAlive;
const aliveCount = data.aliveCount;
const totalPorts = data.totalPorts;
@@ -2342,13 +2351,13 @@ async function renderDatabasePanel() {
'<!-- 概览 -->' +
'<div class="card">' +
'<div class="card-header">' +
'<span class="card-title">🔌 SSH 隧道状态</span>' +
'<span class="badge ' + (tunnelRunning ? 'badge-running' : 'badge-stopped') + '" id="db-tunnel-badge">' + (tunnelRunning ? '运行中' : '未运行') + '</span>' +
'<span class="card-title">🐳 数据库连接状态</span>' +
'<span class="badge ' + (allAlive ? 'badge-running' : (aliveCount > 0 ? 'badge-starting' : 'badge-error')) + '" id="db-tunnel-badge">' + (allAlive ? '全部在线' : (aliveCount > 0 ? '部分在线' : '离线')) + '</span>' +
'</div>' +
'<div class="db-summary">' +
'<div class="db-summary-stat">' +
'<div class="val" id="db-alive-count" style="color:' + (allAlive ? 'var(--green)' : 'var(--red)') + '">' + aliveCount + '/' + totalPorts + '</div>' +
'<div class="lbl">数据库端口通联</div>' +
'<div class="lbl">容器端口通联</div>' +
'</div>' +
(pg ?
'<div class="db-summary-stat">' +
@@ -2377,16 +2386,15 @@ async function renderDatabasePanel() {
'</div>' +
'</div>' +
'<!-- 隧道操作 -->' +
'<!-- 数据库容器控制 -->' +
'<div class="card">' +
'<div class="card-header"><span class="card-title">🕹️ 隧道控制</span></div>' +
'<div class="card-header"><span class="card-title">🕹️ 数据库容器控制</span></div>' +
'<div class="btn-group" style="margin-bottom:8px">' +
'<button class="btn btn-green btn-sm" id="db-tunnel-start" onclick="tunnelAction(\'start\')"' + (tunnelRunning && allAlive ? ' disabled' : '') + '>▶ 启动隧道</button>' +
'<button class="btn btn-red btn-sm" id="db-tunnel-stop" onclick="tunnelAction(\'stop\')"' + (!tunnelRunning ? ' disabled' : '') + '>⏹ 停止隧道</button>' +
'<button class="btn btn-sm" onclick="tunnelAction(\'restart\')">🔄 重启隧道</button>' +
'<button class="btn btn-sm" onclick="tunnelAction(\'status\')">📋 查看状态</button>' +
'<button class="btn btn-green btn-sm" id="db-tunnel-start" onclick="dbContainerAction(\'start\')"' + (allAlive ? ' disabled' : '') + '>▶ 启动</button>' +
'<button class="btn btn-red btn-sm" id="db-tunnel-stop" onclick="dbContainerAction(\'stop\')"' + (aliveCount === 0 ? ' disabled' : '') + '>⏹ 停止</button>' +
'<button class="btn btn-sm" onclick="dbContainerAction(\'restart\')">🔄 重启</button>' +
'</div>' +
'<div id="db-zombie-warn" style="font-size:11px;color:var(--yellow);margin-bottom:8px;display:' + (tunnelRunning && !allAlive ? 'block' : 'none') + '">⚠️ 隧道进程存在但部分端口不通,可能是僵尸进程,请尝试重启隧道</div>' +
'<div id="db-zombie-warn" style="font-size:11px;color:var(--yellow);margin-bottom:8px;display:' + (aliveCount > 0 && !allAlive ? 'block' : 'none') + '">⚠️ 部分容器端口不通,请尝试重启 Docker 容器</div>' +
'<div id="tunnel-log-container" style="display:none">' +
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">' +
'<span style="font-size:11px;color:var(--text2)">操作日志</span>' +
@@ -2396,13 +2404,13 @@ async function renderDatabasePanel() {
'</div>' +
'</div>' +
'<!-- 数据库连接信息 -->' +
'<!-- 连接信息 -->' +
'<div class="card">' +
'<div class="card-header"><span class="card-title">📋 连接说明</span></div>' +
'<div style="font-size:12px;color:var(--text2);line-height:1.8">' +
'<div>🔑 SSH 服务器: <code style="color:var(--text)">root@cd.yeij.top</code></div>' +
'<div>📁 隧道脚本: <code style="color:var(--text)">scripts/tunnel.sh</code></div>' +
'<div>💡 所有数据库端口通过 SSH 转发至 <code style="color:var(--text)">localhost</code>,无需修改 .env</div>' +
'<div>🐳 Docker Compose: <code style="color:var(--text)">docker compose -f docker-compose.dev.db.yml up -d</code></div>' +
'<div>📁 配置文件: <code style="color:var(--text)">docker-compose.dev.db.yml</code></div>' +
'<div>💡 所有数据库服务运行在本地 Docker 容器中,端口映射至 <code style="color:var(--text)">localhost</code></div>' +
'</div>' +
'</div>';
STATE.dbInitialized = true;
@@ -2410,11 +2418,11 @@ async function renderDatabasePanel() {
// Bug 7: 增量更新 — 只更新状态徽章、计数器、端口卡片、检查时间
var el;
// 隧道状态徽章
// 数据库状态徽章
el = document.getElementById('db-tunnel-badge');
if (el) {
el.className = 'badge ' + (tunnelRunning ? 'badge-running' : 'badge-stopped');
el.textContent = tunnelRunning ? '运行中' : '未运行';
el.className = 'badge ' + (allAlive ? 'badge-running' : (aliveCount > 0 ? 'badge-starting' : 'badge-error'));
el.textContent = allAlive ? '全部在线' : (aliveCount > 0 ? '部分在线' : '离线');
}
// 端口通联计数
@@ -2453,13 +2461,13 @@ async function renderDatabasePanel() {
// 更新按钮 disable 状态
el = document.getElementById('db-tunnel-start');
if (el) el.disabled = !!(tunnelRunning && allAlive);
if (el) el.disabled = !!allAlive;
el = document.getElementById('db-tunnel-stop');
if (el) el.disabled = !tunnelRunning;
if (el) el.disabled = !(aliveCount > 0);
// 僵尸警告
// 部分在线警告
el = document.getElementById('db-zombie-warn');
if (el) el.style.display = (tunnelRunning && !allAlive) ? 'block' : 'none';
if (el) el.style.display = (aliveCount > 0 && !allAlive) ? 'block' : 'none';
}
}
@@ -2467,23 +2475,25 @@ function refreshDatabasePanel() {
renderDatabasePanel();
}
async function tunnelAction(action) {
showToast(`正在执行: ${action} 隧道...`, 'info');
async function dbContainerAction(action) {
var labelMap = { start: '启动', stop: '停止', restart: '重启' };
var label = labelMap[action] || action;
showToast('正在' + label + '数据库容器...', 'info');
const logContainer = document.getElementById('tunnel-log-container');
const logEl = document.getElementById('tunnel-log');
logEl.textContent = '执行中...';
logContainer.style.display = 'block';
const data = await api(`/api/tunnel/${action}`, { method: 'POST' });
const data = await api('/api/db/' + action, { method: 'POST' });
if (data.error && !data.output) {
logEl.textContent = `错误: ${data.error}`;
showToast(`操作失败: ${data.error}`, 'error');
logEl.textContent = '错误: ' + data.error;
showToast('操作失败: ' + data.error, 'error');
} else {
logEl.textContent = data.output || data.error || '(无输出)';
if (data.success) {
showToast(`${action} 隧道完成`, 'success');
showToast(label + '数据库完成', 'success');
} else {
showToast(`${action} 完成 (查看日志)`, 'info');
showToast(label + '完成 (查看日志)', 'info');
}
setTimeout(refreshDatabasePanel, 1500);
}