feat: 多功能升级 — 流式逐字渲染、对话缓存、会话组织优化、记忆管理修复、性能仪表盘
- 前端消息流式逐字渲染 (AI-Core ChatStream → SSE → Gateway → WebSocket stream_chunk → fadeInUp + cursorBlink) - 后端对话缓存 (conversationCache sync.Map, GET /sessions/:id/messages) - 前端侧边栏历史多轮对话显示 - DevTools 性能监控图标移至首页仪表盘 - DevTools 用户记忆查询/删减功能修复 (补全 DELETE 数据链路) - 后端和 DevTools 按用户分类组织实时活动会话 (map[userID]map[sessionID]*Client) - 新增 docs/api-reference/ 路由参考文档 - 新增 docs/message-flow-architecture.md 消息链路架构文档
This commit is contained in:
+215
-50
@@ -221,6 +221,23 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
|
||||
.chart-area.cpu { fill: var(--blue); }
|
||||
.chart-area.mem { fill: var(--green); }
|
||||
.legend { display: flex; gap: 14px; font-size: 11px; color: var(--text2); }
|
||||
|
||||
/* 性能仪表盘进度条 */
|
||||
.perf-dashboard { display: flex; flex-direction: column; gap: 14px; }
|
||||
.perf-row { display: flex; align-items: center; gap: 12px; }
|
||||
.perf-label { min-width: 60px; font-size: 12px; color: var(--text2); }
|
||||
.perf-value { min-width: 52px; font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 600; text-align: right; }
|
||||
.perf-bar-wrap { flex: 1; background: var(--bg); border-radius: 4px; height: 10px; overflow: hidden; }
|
||||
.perf-bar { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
|
||||
.perf-bar.cpu-low, .perf-bar.cpu-mid, .perf-bar.cpu-high, .perf-bar.mem-low, .perf-bar.mem-mid, .perf-bar.mem-high { background: var(--blue); }
|
||||
.perf-bar.cpu-mid, .perf-bar.mem-mid { background: var(--yellow); }
|
||||
.perf-bar.cpu-high, .perf-bar.mem-high { background: var(--red); }
|
||||
.perf-bar.mem-low { background: var(--green); }
|
||||
.perf-stat { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--border); }
|
||||
.perf-stat:last-child { border-bottom: none; }
|
||||
.perf-stat-icon { font-size: 16px; width: 24px; text-align: center; }
|
||||
.perf-stat-label { font-size: 12px; color: var(--text2); flex: 1; }
|
||||
.perf-stat-value { font-family: 'JetBrains Mono', monospace; font-size: 14px; font-weight: 600; }
|
||||
.legend-item { display: flex; align-items: center; gap: 5px; }
|
||||
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.legend-dot.cpu { background: var(--blue); }
|
||||
@@ -592,22 +609,30 @@ async function renderDashboard() {
|
||||
<div class="cards-grid cards-3" id="dashboard-svc-cards"></div>
|
||||
</div>
|
||||
|
||||
<!-- 性能快照 + 系统信息 -->
|
||||
<!-- 性能快照 + 性能仪表盘 -->
|
||||
<div class="cards-grid cards-2">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">⚡ 资源使用</span></div>
|
||||
<div id="dashboard-perf"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">💻 系统信息</span></div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;font-size:12px">
|
||||
<div><span style="color:var(--text2)">运行时间:</span> ${formatUptime((data.system?.uptime || 0) * 1000)}</div>
|
||||
<div><span style="color:var(--text2)">堆内存:</span> ${data.system?.heapUsedMB ?? '—'} MB / ${data.system?.heapTotalMB ?? '—'} MB</div>
|
||||
<div><span style="color:var(--text2)">总消息数:</span> ${data.sessions?.totalMessages ?? 0}</div>
|
||||
<div><span style="color:var(--text2)">更新时间:</span> ${formatTime(data.timestamp)}</div>
|
||||
<div class="card-header"><span class="card-title">📊 性能仪表盘</span></div>
|
||||
<div id="performance-dashboard">
|
||||
<div class="empty-state"><div class="icon">📊</div>等待性能数据...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">💻 系统信息</span></div>
|
||||
<div style="display:flex;gap:20px;font-size:12px;flex-wrap:wrap">
|
||||
<div><span style="color:var(--text2)">运行时间:</span> ${formatUptime((data.system?.uptime || 0) * 1000)}</div>
|
||||
<div><span style="color:var(--text2)">堆内存:</span> ${data.system?.heapUsedMB ?? '—'} MB / ${data.system?.heapTotalMB ?? '—'} MB</div>
|
||||
<div><span style="color:var(--text2)">总消息数:</span> ${data.sessions?.totalMessages ?? 0}</div>
|
||||
<div><span style="color:var(--text2)">更新时间:</span> ${formatTime(data.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 渲染服务卡片
|
||||
@@ -624,6 +649,112 @@ async function renderDashboard() {
|
||||
</span>
|
||||
</div>
|
||||
`).join('') || '<div class="empty-state"><div class="icon">📊</div>等待采样数据...</div>';
|
||||
|
||||
// 渲染性能仪表盘
|
||||
updatePerformanceDashboard(data.performance?.perService || {});
|
||||
}
|
||||
|
||||
// ========== 性能仪表盘渲染 ==========
|
||||
async function updatePerformanceDashboard(perfData) {
|
||||
const container = document.getElementById('performance-dashboard');
|
||||
if (!container) return; // 静默跳过:用户在其他页面
|
||||
|
||||
const entries = Object.entries(perfData);
|
||||
if (entries.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">📊</div>等待性能数据...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 聚合数据
|
||||
let totalCpu = 0, totalMem = 0, activeCount = 0;
|
||||
for (const [, p] of entries) {
|
||||
totalCpu += p.cpu || 0;
|
||||
totalMem += p.mem || 0;
|
||||
if (p.pid) activeCount++;
|
||||
}
|
||||
|
||||
const avgCpu = entries.length > 0 ? Math.round(totalCpu / entries.length * 10) / 10 : 0;
|
||||
const cpuLevel = avgCpu > 80 ? 'cpu-high' : avgCpu > 50 ? 'cpu-mid' : 'cpu-low';
|
||||
const memLevel = totalMem > 1024 ? 'mem-high' : totalMem > 512 ? 'mem-mid' : 'mem-low';
|
||||
|
||||
// 计算平均延迟 (基于活跃连接和服务数估算,或使用 perf 数据中的 elapsed)
|
||||
let avgLatency = '—';
|
||||
let totalElapsed = 0, elapsedCount = 0;
|
||||
for (const [, p] of entries) {
|
||||
if (p.elapsed && p.elapsed > 0) { totalElapsed += p.elapsed; elapsedCount++; }
|
||||
}
|
||||
if (elapsedCount > 0) {
|
||||
avgLatency = Math.round(totalElapsed / elapsedCount) + 'ms';
|
||||
}
|
||||
|
||||
// 获取趋势数据 (从性能仪表盘 API)
|
||||
let trendCpu = '→', trendMem = '→';
|
||||
try {
|
||||
const dashResp = await api('/api/performance/dashboard');
|
||||
if (!dashResp.error && dashResp.summary?.trend) {
|
||||
const t = dashResp.summary.trend;
|
||||
trendCpu = t.cpu === 'up' ? '↑' : t.cpu === 'down' ? '↓' : '→';
|
||||
trendMem = t.mem === 'up' ? '↑' : t.mem === 'down' ? '↓' : '→';
|
||||
// 使用 API 返回的更精确的延迟数据
|
||||
if (dashResp.summary.avgLatencyMs != null) {
|
||||
avgLatency = dashResp.summary.avgLatencyMs + 'ms';
|
||||
}
|
||||
}
|
||||
} catch { /* 忽略: 使用本地计算的数据 */ }
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="perf-dashboard">
|
||||
<!-- CPU 使用率 -->
|
||||
<div class="perf-row">
|
||||
<span class="perf-label">🖥 CPU</span>
|
||||
<div class="perf-bar-wrap">
|
||||
<div class="perf-bar ${cpuLevel}" style="width:${Math.min(avgCpu, 100)}%"></div>
|
||||
</div>
|
||||
<span class="perf-value">${avgCpu}% ${trendCpu}</span>
|
||||
</div>
|
||||
|
||||
<!-- 内存使用 -->
|
||||
<div class="perf-row">
|
||||
<span class="perf-label">💾 内存</span>
|
||||
<div class="perf-bar-wrap">
|
||||
<div class="perf-bar ${memLevel}" style="width:${Math.min(totalMem / 1024 * 100, 100)}%"></div>
|
||||
</div>
|
||||
<span class="perf-value">${Math.round(totalMem)} MB ${trendMem}</span>
|
||||
</div>
|
||||
|
||||
<!-- 详细统计 -->
|
||||
<div style="margin-top:8px">
|
||||
<div class="perf-stat">
|
||||
<span class="perf-stat-icon">⏱</span>
|
||||
<span class="perf-stat-label">平均请求延迟</span>
|
||||
<span class="perf-stat-value" style="color:var(--yellow)">${avgLatency}</span>
|
||||
</div>
|
||||
<div class="perf-stat">
|
||||
<span class="perf-stat-icon">🔗</span>
|
||||
<span class="perf-stat-label">活跃连接数</span>
|
||||
<span class="perf-stat-value" style="color:var(--accent)">${activeCount}</span>
|
||||
</div>
|
||||
<div class="perf-stat">
|
||||
<span class="perf-stat-icon">📦</span>
|
||||
<span class="perf-stat-label">监控服务数</span>
|
||||
<span class="perf-stat-value" style="color:var(--blue)">${entries.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 各服务简要 -->
|
||||
<div style="margin-top:6px;border-top:1px solid var(--border);padding-top:8px">
|
||||
${entries.map(([id, p]) => `
|
||||
<div class="perf-stat">
|
||||
<span class="perf-stat-icon" style="font-size:12px">${p.pid ? '🟢' : '🔴'}</span>
|
||||
<span class="perf-stat-label">${escapeId(id)}</span>
|
||||
<span class="perf-stat-value" style="font-size:11px;color:var(--text2)">
|
||||
CPU ${p.cpu || 0}% · MEM ${p.mem || 0}MB
|
||||
</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDashboardSvcCards(svcs) {
|
||||
@@ -699,9 +830,9 @@ function renderMemoryPanel() {
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>内容</th><th>分类</th><th>优先级</th><th>用户</th><th>创建时间</th></tr></thead>
|
||||
<thead><tr><th>内容</th><th>分类</th><th>优先级</th><th>用户</th><th>创建时间</th><th style="width:50px">操作</th></tr></thead>
|
||||
<tbody id="mem-table-body">
|
||||
<tr><td colspan="5"><div class="empty-state"><div class="icon">🧠</div>使用搜索或列表按钮加载记忆</div></td></tr>
|
||||
<tr><td colspan="6"><div class="empty-state"><div class="icon">🧠</div>使用搜索或列表按钮加载记忆</div></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -732,7 +863,7 @@ function renderMemoryResults(data) {
|
||||
const countEl = document.getElementById('mem-result-count');
|
||||
|
||||
if (data.error) {
|
||||
tbody.innerHTML = `<tr><td colspan="5"><div class="empty-state"><div class="icon">⚠️</div>${escHtml(data.error)}</div></td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="6"><div class="empty-state"><div class="icon">⚠️</div>${escHtml(data.error)}</div></td></tr>`;
|
||||
countEl.textContent = '';
|
||||
return;
|
||||
}
|
||||
@@ -746,7 +877,7 @@ function renderMemoryResults(data) {
|
||||
countEl.textContent = `共 ${memories.length} 条`;
|
||||
|
||||
if (memories.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5"><div class="empty-state"><div class="icon">📭</div>没有找到记忆</div></td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><div class="icon">📭</div>没有找到记忆</div></td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -757,6 +888,7 @@ function renderMemoryResults(data) {
|
||||
<td>${m.priority ?? 1}</td>
|
||||
<td style="color:var(--text2)">${escHtml(m.user_id || '—')}</td>
|
||||
<td style="color:var(--text2);font-size:11px">${formatTime(m.created_at)}</td>
|
||||
<td><button class="btn btn-xs btn-red" onclick="deleteMemory('${escHtml(m.id || m.ID || '')}')" title="删除">🗑</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
@@ -782,10 +914,23 @@ async function addMemory() {
|
||||
listMemory();
|
||||
}
|
||||
|
||||
async function deleteMemory(memoryId) {
|
||||
if (!memoryId) { showToast('无效的记忆ID', 'error'); return; }
|
||||
if (!confirm(`确定要删除记忆 ${memoryId.substring(0, 8)}... 吗?`)) return;
|
||||
|
||||
const data = await api(`/api/memory/${encodeURIComponent(memoryId)}`, { method: 'DELETE' });
|
||||
|
||||
if (data.error) { showToast(`删除失败: ${data.error}`, 'error'); return; }
|
||||
|
||||
showToast('记忆删除成功!', 'success');
|
||||
// 自动刷新列表
|
||||
listMemory();
|
||||
}
|
||||
|
||||
// ========== 面板3: 会话监看 ==========
|
||||
function renderSessionsPanel() {
|
||||
document.getElementById('panel-actions').innerHTML = `
|
||||
<button class="btn btn-sm" onclick="loadSessions()" id="sessions-refresh-btn">🔄 刷新</button>
|
||||
<button class="btn btn-sm" onclick="fetchActiveSessions()" id="sessions-refresh-btn">🔄 刷新</button>
|
||||
<span style="font-size:11px;color:var(--text2)">⏱ 每5秒自动刷新</span>
|
||||
`;
|
||||
document.getElementById('panel-sessions').innerHTML = `
|
||||
@@ -794,70 +939,90 @@ function renderSessionsPanel() {
|
||||
<span class="card-title">💬 活跃 WebSocket 会话</span>
|
||||
<span id="sessions-count" style="font-size:12px;color:var(--text2)"></span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th style="width:30px"></th><th>Session ID</th><th>User ID</th><th>状态</th><th>消息数</th><th>连接时间</th><th>最后活跃</th></tr></thead>
|
||||
<tbody id="sessions-table-body">
|
||||
<tr><td colspan="7"><div class="empty-state"><div class="icon">💬</div>加载中...</div></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="sessions-grouped-container">
|
||||
<div class="empty-state"><div class="icon">💬</div>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
loadSessions();
|
||||
fetchActiveSessions();
|
||||
}
|
||||
|
||||
async function loadSessions() {
|
||||
async function fetchActiveSessions() {
|
||||
const btn = document.getElementById('sessions-refresh-btn');
|
||||
if (btn) btn.classList.add('spinning');
|
||||
|
||||
const data = await api('/api/sessions');
|
||||
const data = await api('/api/sessions/active');
|
||||
|
||||
if (btn) btn.classList.remove('spinning');
|
||||
|
||||
const sessions = data.sessions || [];
|
||||
STATE.sessionsData = sessions;
|
||||
const users = data.users || {};
|
||||
|
||||
// 计算总会话数
|
||||
let totalSessions = 0;
|
||||
const flatSessions = [];
|
||||
for (const [userID, sessions] of Object.entries(users)) {
|
||||
totalSessions += sessions.length;
|
||||
for (const s of sessions) {
|
||||
flatSessions.push({ ...s, _userID: userID });
|
||||
}
|
||||
}
|
||||
STATE.sessionsData = flatSessions;
|
||||
|
||||
// 更新侧边栏徽章
|
||||
const badge = document.getElementById('sessions-badge');
|
||||
badge.textContent = sessions.length;
|
||||
badge.style.display = sessions.length > 0 ? 'inline-block' : 'none';
|
||||
badge.textContent = totalSessions;
|
||||
badge.style.display = totalSessions > 0 ? 'inline-block' : 'none';
|
||||
|
||||
// 更新计数
|
||||
const countEl = document.getElementById('sessions-count');
|
||||
if (countEl) countEl.textContent = `共 ${sessions.length} 个活跃会话`;
|
||||
if (countEl) countEl.textContent = `共 ${Object.keys(users).length} 个用户,${totalSessions} 个活跃会话`;
|
||||
|
||||
const tbody = document.getElementById('sessions-table-body');
|
||||
if (!tbody) return;
|
||||
const container = document.getElementById('sessions-grouped-container');
|
||||
if (!container) return;
|
||||
|
||||
if (data.error) {
|
||||
tbody.innerHTML = `<tr><td colspan="7"><div class="empty-state"><div class="icon">⚠️</div>${escHtml(data.error)}<br><span style="font-size:11px">请确认 Gateway 服务已启动</span></div></td></tr>`;
|
||||
container.innerHTML = `<div class="empty-state"><div class="icon">⚠️</div>${escHtml(data.error)}<br><span style="font-size:11px">请确认 Gateway 服务已启动</span></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7"><div class="empty-state"><div class="icon">💤</div>当前没有活跃会话</div></td></tr>';
|
||||
if (Object.keys(users).length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">💤</div>当前没有活跃会话</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = sessions.map((s, i) => `
|
||||
<tr id="session-row-${i}" class="session-row" data-index="${i}" style="cursor:pointer" onclick="toggleSessionDetail(${i})">
|
||||
<td><span class="collapse-arrow" id="session-arrow-${i}">▶</span></td>
|
||||
<td><code style="font-size:11px;color:var(--accent)">${escHtml((s.session_id || '').substring(0, 20))}${(s.session_id || '').length > 20 ? '...' : ''}</code></td>
|
||||
<td>${escHtml(s.user_id || '—')}</td>
|
||||
<td><span class="badge ${statusBadge(s.state || 'idle')}">${s.state || 'idle'}</span></td>
|
||||
<td>${s.message_count || 0}</td>
|
||||
<td style="font-size:11px;color:var(--text2)">${timeAgo(s.connected_at)}</td>
|
||||
<td style="font-size:11px;color:var(--text2)">${timeAgo(s.last_activity)}</td>
|
||||
</tr>
|
||||
<tr id="session-detail-${i}" style="display:none">
|
||||
<td colspan="7">
|
||||
<div class="session-detail" id="session-detail-content-${i}">
|
||||
<div style="text-align:center;color:var(--text2);padding:8px">加载详情中...</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
let html = '';
|
||||
let globalIndex = 0;
|
||||
for (const [userID, sessions] of Object.entries(users)) {
|
||||
html += `<div style="margin-bottom:16px">`;
|
||||
html += `<div style="font-weight:600;font-size:14px;padding:8px 0;border-bottom:1px solid var(--border);margin-bottom:8px;color:var(--accent)">👤 User: ${escHtml(userID)}</div>`;
|
||||
|
||||
for (const s of sessions) {
|
||||
const idx = globalIndex++;
|
||||
html += `
|
||||
<div style="padding:6px 0 6px 20px">
|
||||
<div id="session-row-${idx}" class="session-row" data-index="${idx}" style="cursor:pointer;display:flex;align-items:center;gap:10px;padding:6px 10px;background:var(--bg3);border-radius:var(--radius-sm)" onclick="toggleSessionDetail(${idx})">
|
||||
<span class="collapse-arrow" id="session-arrow-${idx}">▶</span>
|
||||
<span style="flex:1">💬 <strong>Session:</strong> <code style="font-size:11px;color:var(--accent)">${escHtml((s.session_id || '').substring(0, 24))}${(s.session_id || '').length > 24 ? '...' : ''}</code></span>
|
||||
<span class="badge ${statusBadge(s.state || 'idle')}">${s.state || 'idle'}</span>
|
||||
<span style="font-size:11px;color:var(--text2)">最近活动: ${timeAgo(s.last_activity)}</span>
|
||||
</div>
|
||||
<div id="session-detail-${idx}" style="display:none;margin-top:4px;margin-left:20px">
|
||||
<div class="session-detail" id="session-detail-content-${idx}">
|
||||
<div style="text-align:center;color:var(--text2);padding:8px">加载详情中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 保留旧 loadSessions 兼容其他调用
|
||||
async function loadSessions() {
|
||||
fetchActiveSessions();
|
||||
}
|
||||
|
||||
async function toggleSessionDetail(index) {
|
||||
|
||||
@@ -198,6 +198,12 @@ app.get('/api/sessions', async (_req, res) => {
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/sessions/active — 获取按用户分组的活跃会话
|
||||
app.get('/api/sessions/active', async (_req, res) => {
|
||||
const result = await proxyToGateway('/api/v1/sessions/active');
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
app.get('/api/sessions/:id', async (req, res) => {
|
||||
const result = await proxyToGateway(`/api/v1/admin/sessions/${req.params.id}`);
|
||||
res.status(result.status).json(result.body);
|
||||
@@ -230,6 +236,14 @@ app.post('/api/memory/add', async (req, res) => {
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
app.delete('/api/memory/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
if (!id) return res.status(400).json({ error: '缺少 memory id' });
|
||||
const qs = new URLSearchParams({ id }).toString();
|
||||
const result = await proxyToGateway(`/api/v1/memory?${qs}`, { method: 'DELETE' });
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 服务状态 ----
|
||||
app.get('/api/services', (_req, res) => {
|
||||
res.json(processManager.getStatus());
|
||||
@@ -311,6 +325,16 @@ app.get('/api/performance', async (_req, res) => {
|
||||
res.json(snapshot);
|
||||
});
|
||||
|
||||
// 性能仪表盘聚合数据 (供首页仪表盘使用)
|
||||
app.get('/api/performance/dashboard', async (_req, res) => {
|
||||
try {
|
||||
const dashboardData = await performanceMonitor.updateDashboard();
|
||||
res.json(dashboardData);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: `获取性能仪表盘数据失败: ${err.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/performance/history', (_req, res) => {
|
||||
res.json(performanceMonitor.getAllHistory());
|
||||
});
|
||||
|
||||
@@ -103,6 +103,63 @@ class PerformanceMonitor {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新仪表盘数据 — 返回聚合的性能摘要供首页仪表盘使用
|
||||
* 调用方负责将数据渲染到 #performance-dashboard 元素
|
||||
* @returns {object} 仪表盘性能摘要
|
||||
*/
|
||||
async updateDashboard() {
|
||||
const snapshot = await this.getSnapshot();
|
||||
const entries = Object.entries(snapshot);
|
||||
|
||||
let totalCpu = 0, totalMem = 0, activeCount = 0;
|
||||
for (const [, p] of entries) {
|
||||
totalCpu += p.cpu || 0;
|
||||
totalMem += p.mem || 0;
|
||||
if (p.pid) activeCount++;
|
||||
}
|
||||
|
||||
const avgCpu = entries.length > 0 ? Math.round(totalCpu / entries.length * 10) / 10 : 0;
|
||||
const totalMemRounded = Math.round(totalMem * 100) / 100;
|
||||
|
||||
// 计算平均延迟 (基于各服务进程的 elapsed 时间)
|
||||
let avgLatencyMs = null;
|
||||
let totalElapsed = 0, elapsedCount = 0;
|
||||
for (const [, p] of entries) {
|
||||
if (p.elapsed && p.elapsed > 0) { totalElapsed += p.elapsed; elapsedCount++; }
|
||||
}
|
||||
if (elapsedCount > 0) {
|
||||
avgLatencyMs = Math.round(totalElapsed / elapsedCount);
|
||||
}
|
||||
|
||||
// 获取最近历史用于趋势判断
|
||||
const recentHistory = this.getAllHistory();
|
||||
let trendCpu = 'stable', trendMem = 'stable';
|
||||
for (const [, hist] of Object.entries(recentHistory)) {
|
||||
if (hist.length < 5) continue;
|
||||
const recent = hist.slice(-5);
|
||||
const firstCpu = recent[0].cpu, lastCpu = recent[recent.length - 1].cpu;
|
||||
const firstMem = recent[0].mem, lastMem = recent[recent.length - 1].mem;
|
||||
if (lastCpu > firstCpu * 1.15) trendCpu = 'up';
|
||||
else if (lastCpu < firstCpu * 0.85) trendCpu = 'down';
|
||||
if (lastMem > firstMem * 1.15) trendMem = 'up';
|
||||
else if (lastMem < firstMem * 0.85) trendMem = 'down';
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
summary: {
|
||||
avgCpu,
|
||||
totalMemMB: totalMemRounded,
|
||||
activeProcesses: activeCount,
|
||||
monitoredServices: entries.length,
|
||||
avgLatencyMs,
|
||||
trend: { cpu: trendCpu, mem: trendMem },
|
||||
},
|
||||
perService: snapshot,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const performanceMonitor = new PerformanceMonitor();
|
||||
|
||||
Reference in New Issue
Block a user