diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 7af4ad04..b0bfac6c 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -4437,6 +4437,12 @@ const monitorState = { lastFetchedAt: null, retentionDays: 0, selectedExecutions: new Set(), + renderKeys: { + stats: '', + executions: '', + pagination: '', + timeline: '' + }, pagination: { page: 1, pageSize: (() => { @@ -4450,28 +4456,43 @@ const monitorState = { }; let monitorPollTimer = null; +let monitorPollGeneration = 0; const MONITOR_POLL_INTERVAL_MS = 3000; function startMonitorPoll() { stopMonitorPoll(); - monitorPollTimer = setInterval(function () { + scheduleMonitorPoll(monitorPollGeneration); +} + +function scheduleMonitorPoll(generation) { + monitorPollTimer = setTimeout(async function pollMonitor() { + monitorPollTimer = null; + if (generation !== monitorPollGeneration) return; const page = document.getElementById('page-mcp-monitor'); if (!page || !page.classList.contains('active')) { - stopMonitorPoll(); return; } - if (document.hidden) { - return; - } - if (typeof refreshMonitorPanel === 'function') { - refreshMonitorPanel().catch(function () { /* ignore */ }); + + try { + if (!document.hidden && typeof refreshMonitorPanel === 'function') { + // 等待本轮完成后再安排下一轮,避免 WSL 或慢网络下请求重叠。 + await refreshMonitorPanel(); + } + } catch (error) { + // refreshMonitorPanel 已负责展示错误;轮询仍应继续。 + } finally { + const activePage = document.getElementById('page-mcp-monitor'); + if (generation === monitorPollGeneration && activePage && activePage.classList.contains('active')) { + scheduleMonitorPoll(generation); + } } }, MONITOR_POLL_INTERVAL_MS); } function stopMonitorPoll() { + monitorPollGeneration++; if (monitorPollTimer) { - clearInterval(monitorPollTimer); + clearTimeout(monitorPollTimer); monitorPollTimer = null; } } @@ -4547,7 +4568,8 @@ async function refreshMonitorPanel(page = null) { } const range = getMcpMonitorTimelineRange(); - monitorState.timelineLoading = true; + // 后台轮询时保留当前趋势图,避免每 3 秒重新进入加载态并触发闪烁。 + monitorState.timelineLoading = monitorState.timeline == null && !monitorState.timelineError; const timelinePromise = fetchMonitorTimeline(range); const monitorResp = await apiFetch(url, { method: 'GET' }); @@ -4565,10 +4587,7 @@ async function refreshMonitorPanel(page = null) { if (mySeq !== monitorPanelFetchSeq) { return; } - monitorState.timeline = timeline; - monitorState.timelineError = timelineError; - monitorState.timelineLoading = false; - updateMonitorTimelineSection(); + applyMonitorTimelinePayload(timeline, timelineError, range); initializeMonitorPageSize(); } catch (error) { console.error('刷新监控面板失败:', error); @@ -4783,7 +4802,7 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = } const range = getMcpMonitorTimelineRange(); - monitorState.timelineLoading = true; + monitorState.timelineLoading = monitorState.timeline == null && !monitorState.timelineError; const timelinePromise = fetchMonitorTimeline(range); const monitorResp = await apiFetch(url, { method: 'GET' }); @@ -4801,10 +4820,7 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = if (mySeq !== monitorPanelFetchSeq) { return; } - monitorState.timeline = timeline; - monitorState.timelineError = timelineError; - monitorState.timelineLoading = false; - updateMonitorTimelineSection(); + applyMonitorTimelinePayload(timeline, timelineError, range); initializeMonitorPageSize(); } catch (error) { console.error('刷新监控面板失败:', error); @@ -4837,9 +4853,68 @@ function applyMonitorPayload(result, statusFilter) { }; } - renderMonitorStats(monitorState.summary, monitorState.topTools, monitorState.lastFetchedAt); - renderMonitorExecutions(monitorState.executions, statusFilter); - renderMonitorPagination(); + const locale = typeof window.__locale === 'string' ? window.__locale : ''; + const toolFilterEl = document.getElementById('monitor-tool-filter'); + const currentToolFilter = toolFilterEl ? toolFilterEl.value.trim() : ''; + const statsKey = monitorRenderKey([ + monitorState.summary, + monitorState.topTools, + monitorState.retentionDays, + currentToolFilter, + locale + ]); + const executionsKey = monitorRenderKey([ + monitorState.executions, + statusFilter || 'all', + currentToolFilter, + locale + ]); + const paginationKey = monitorRenderKey(monitorState.pagination); + + if (statsKey !== monitorState.renderKeys.stats) { + monitorState.renderKeys.stats = statsKey; + renderMonitorStats(monitorState.summary, monitorState.topTools, monitorState.lastFetchedAt); + } else if (document.querySelector('#monitor-stats .mcp-exec-stats')) { + const toolCount = monitorState.summary && typeof monitorState.summary.toolCount === 'number' + ? monitorState.summary.toolCount + : monitorState.topTools.length; + updateMonitorStatsSubtitle(monitorState.lastFetchedAt, toolCount, monitorState.retentionDays); + } + + const executionsChanged = executionsKey !== monitorState.renderKeys.executions; + if (executionsChanged) { + monitorState.renderKeys.executions = executionsKey; + renderMonitorExecutions(monitorState.executions, statusFilter); + } else { + updateMonitorExecutionDurations(monitorState.executions); + } + + // 空列表渲染会清空执行区,因此这种情况下也需要恢复分页控件。 + if (executionsChanged || paginationKey !== monitorState.renderKeys.pagination) { + monitorState.renderKeys.pagination = paginationKey; + renderMonitorPagination(); + } +} + +function monitorRenderKey(value) { + try { + return JSON.stringify(value); + } catch (error) { + return String(Date.now()); + } +} + +function applyMonitorTimelinePayload(timeline, timelineError, range) { + const wasLoading = monitorState.timelineLoading; + const timelineKey = monitorRenderKey([timeline, timelineError || null, range || '']); + const timelineChanged = timelineKey !== monitorState.renderKeys.timeline; + monitorState.timeline = timeline; + monitorState.timelineError = timelineError; + monitorState.timelineLoading = false; + if (wasLoading || timelineChanged) { + monitorState.renderKeys.timeline = timelineKey; + updateMonitorTimelineSection(); + } } async function fetchMonitorTimeline(range) { @@ -5178,14 +5253,9 @@ async function setMcpMonitorTimelineRange(range) { updateMonitorTimelineSection(); try { const { timeline, timelineError } = await fetchMonitorTimeline(range); - monitorState.timeline = timeline; - monitorState.timelineError = timelineError; - monitorState.timelineLoading = false; - updateMonitorTimelineSection(); + applyMonitorTimelinePayload(timeline, timelineError, range); } catch (err) { - monitorState.timelineError = err.message || 'error'; - monitorState.timelineLoading = false; - updateMonitorTimelineSection(); + applyMonitorTimelinePayload(null, err.message || 'error', range); } } window.setMcpMonitorTimelineRange = setMcpMonitorTimelineRange; @@ -6143,7 +6213,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { const terminateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.terminateExecution') : '终止'; const statusKeyMap = { pending: 'statusPending', running: 'statusRunning', completed: 'statusCompleted', failed: 'statusFailed', cancelled: 'statusCancelled' }; const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined; - const rows = executions + const rowEntries = executions .map(exec => { const status = (exec.status || 'unknown').toLowerCase(); const statusClass = `monitor-status-chip ${status}`; @@ -6159,15 +6229,19 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { : ''; const jsExecId = rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const isSelected = monitorState.selectedExecutions.has(rawExecId); - return ` -
| - - | -${escapeHtml(colTool)} | -${escapeHtml(colStatus)} | -${escapeHtml(colStartTime)} | -${escapeHtml(colDuration)} | -${escapeHtml(colActions)} | -
|---|