diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 1f2b4556..eae3800d 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1667,6 +1667,7 @@ "timelineSummary": "{{total}} calls in range · peak {{peak}}", "timelineSparseHint": "Most buckets are empty; peak {{peak}} calls at {{peakTime}}", "timelineNoData": "No calls in this period", + "timelineLoading": "Loading trend…", "timelineEmptyHint": "Switch the time range or invoke MCP tools in chat or tasks", "timelineLoadError": "Failed to load call trend", "timelineTotalLegend": "Total calls", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index f40f8a03..0bbbec98 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1655,6 +1655,7 @@ "timelineSummary": "区间内 {{total}} 次 · 峰值 {{peak}}", "timelineSparseHint": "该时段多数时间为 0,峰值 {{peak}} 次出现在 {{peakTime}}", "timelineNoData": "该时段暂无调用", + "timelineLoading": "趋势加载中…", "timelineEmptyHint": "切换时间范围查看其他时段,或在对话/任务中调用 MCP 工具", "timelineLoadError": "无法加载调用趋势", "timelineTotalLegend": "总调用", diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 2e1f4ee8..44b96454 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -118,7 +118,7 @@ async function refreshDashboard() { fetchJson('/api/agent-loop/tasks'), fetchJson('/api/vulnerabilities/stats'), fetchJson('/api/batch-tasks?limit=500&page=1'), - fetchJson('/api/monitor/stats'), + fetchJson('/api/monitor/stats?top=30'), fetchJson('/api/knowledge/stats'), fetchJson('/api/skills/stats'), fetchJson('/api/vulnerabilities?limit=10&page=1'), @@ -301,36 +301,27 @@ async function refreshDashboard() { updateProgressBar('dashboard-batch-progress-done', '0'); } - // 工具调用:monitor/stats 为 { toolName: { totalCalls, successCalls, failedCalls, ... } } + // 工具调用:monitor/stats 为 { summary, topTools } let toolsCount = 0, toolsTotalCalls = 0, toolsSuccessRate = -1, toolsFailedCount = 0; - if (monitorRes && typeof monitorRes === 'object') { - const names = Object.keys(monitorRes); - let totalCalls = 0, totalSuccess = 0, totalFailed = 0; - names.forEach(k => { - const v = monitorRes[k]; - const n = v && (v.totalCalls ?? v.TotalCalls); - if (typeof n === 'number') totalCalls += n; - const s = v && (v.successCalls ?? v.SuccessCalls); - if (typeof s === 'number') totalSuccess += s; - const f = v && (v.failedCalls ?? v.FailedCalls); - if (typeof f === 'number') totalFailed += f; - }); - toolsCount = names.length; - toolsTotalCalls = totalCalls; - toolsFailedCount = totalFailed; - setEl('dashboard-kpi-tools-calls', formatNumber(totalCalls)); + if (monitorRes && monitorRes.summary) { + const s = monitorRes.summary; + toolsCount = s.toolCount || 0; + toolsTotalCalls = s.totalCalls || 0; + toolsFailedCount = s.failedCalls || 0; + const totalSuccess = s.successCalls || 0; + setEl('dashboard-kpi-tools-calls', formatNumber(toolsTotalCalls)); setKpiSubText('dashboard-kpi-tools-sub-text', dt('dashboard.toolsCountLabel', { count: toolsCount }, toolsCount + ' 个工具')); - if (totalCalls > 0) { - toolsSuccessRate = (totalSuccess / totalCalls) * 100; + if (toolsTotalCalls > 0) { + toolsSuccessRate = (totalSuccess / toolsTotalCalls) * 100; const rateStr = toolsSuccessRate.toFixed(1) + '%'; setEl('dashboard-kpi-success-rate', rateStr); - setKpiRateBadge('dashboard-kpi-rate-sub-text', toolsSuccessRate, totalFailed); + setKpiRateBadge('dashboard-kpi-rate-sub-text', toolsSuccessRate, toolsFailedCount); } else { setEl('dashboard-kpi-success-rate', '-'); setKpiSubText('dashboard-kpi-rate-sub-text', dt('dashboard.noCallYet', null, '暂无调用')); } - renderDashboardToolsBar(monitorRes); + renderDashboardToolsBar(monitorRes.topTools); } else { setEl('dashboard-kpi-tools-calls', '-'); setEl('dashboard-kpi-success-rate', '-'); @@ -1615,12 +1606,12 @@ function renderSeverityInsights(bySeverityOpen, totalOpen, recentVulnsRes) { } } -function renderDashboardToolsBar(monitorRes) { +function renderDashboardToolsBar(topTools) { const placeholder = document.getElementById('dashboard-tools-pie-placeholder'); const barChartEl = document.getElementById('dashboard-tools-bar-chart'); if (!placeholder || !barChartEl) return; - if (!monitorRes || typeof monitorRes !== 'object') { + if (!Array.isArray(topTools) || topTools.length === 0) { placeholder.style.removeProperty('display'); placeholder.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据'); barChartEl.style.display = 'none'; @@ -1628,11 +1619,12 @@ function renderDashboardToolsBar(monitorRes) { return; } - const entries = Object.keys(monitorRes).map(function (k) { - const v = monitorRes[k]; - const totalCalls = v && (v.totalCalls ?? v.TotalCalls); - return { name: k, totalCalls: typeof totalCalls === 'number' ? totalCalls : 0 }; - }).filter(function (e) { return e.totalCalls > 0; }) + const entries = topTools.map(function (t) { + return { + name: t.toolName || '', + totalCalls: typeof t.totalCalls === 'number' ? t.totalCalls : 0, + }; + }).filter(function (e) { return e.name && e.totalCalls > 0; }) .sort(function (a, b) { return b.totalCalls - a.totalCalls; }) .slice(0, 30); diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index a6c920ff..a1ac621d 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -3527,10 +3527,12 @@ let monitorPanelFetchSeq = 0; // 监控面板状态 const monitorState = { executions: [], - stats: {}, + summary: null, + topTools: [], timeline: null, timelineRange: null, timelineError: null, + timelineLoading: false, lastFetchedAt: null, retentionDays: 0, pagination: { @@ -3626,17 +3628,14 @@ async function refreshMonitorPanel(page = null) { try { const mySeq = ++monitorPanelFetchSeq; - // 如果指定了页码,使用指定页码,否则使用当前页码 const currentPage = page !== null ? page : monitorState.pagination.page; const pageSize = monitorState.pagination.pageSize; - - // 获取当前的筛选条件 + const statusFilter = document.getElementById('monitor-status-filter'); const toolFilter = document.getElementById('monitor-tool-filter'); const currentStatusFilter = statusFilter ? statusFilter.value : 'all'; const currentToolFilter = toolFilter ? (toolFilter.value.trim() || 'all') : 'all'; - - // 构建请求 URL + let url = `/api/monitor?page=${currentPage}&page_size=${pageSize}`; if (currentStatusFilter && currentStatusFilter !== 'all') { url += `&status=${encodeURIComponent(currentStatusFilter)}`; @@ -3644,37 +3643,34 @@ async function refreshMonitorPanel(page = null) { if (currentToolFilter && currentToolFilter !== 'all') { url += `&tool=${encodeURIComponent(currentToolFilter)}`; } - - const { result, timeline, timelineError } = await fetchMonitorAndTimeline(url); + + const range = getMcpMonitorTimelineRange(); + monitorState.timelineLoading = true; + const timelinePromise = fetchMonitorTimeline(range); + + const monitorResp = await apiFetch(url, { method: 'GET' }); + const result = await monitorResp.json().catch(() => ({})); + if (!monitorResp.ok) { + throw new Error(result.error || '获取监控数据失败'); + } if (mySeq !== monitorPanelFetchSeq) { return; } - monitorState.executions = Array.isArray(result.executions) ? result.executions : []; - monitorState.stats = result.stats || {}; + applyMonitorPayload(result, currentStatusFilter); + + const { timeline, timelineError } = await timelinePromise; + if (mySeq !== monitorPanelFetchSeq) { + return; + } monitorState.timeline = timeline; monitorState.timelineError = timelineError; - monitorState.lastFetchedAt = new Date(); - monitorState.retentionDays = typeof result.retention_days === 'number' ? result.retention_days : 0; - - // 更新分页信息 - if (result.total !== undefined) { - monitorState.pagination = { - page: result.page || currentPage, - pageSize: result.page_size || pageSize, - total: result.total || 0, - totalPages: result.total_pages || 1 - }; - } - - renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt); - renderMonitorExecutions(monitorState.executions, currentStatusFilter); - renderMonitorPagination(); - - // 初始化每页显示数量选择器 + monitorState.timelineLoading = false; + updateMonitorTimelineSection(); initializeMonitorPageSize(); } catch (error) { console.error('刷新监控面板失败:', error); + monitorState.timelineLoading = false; if (statsContainer) { statsContainer.innerHTML = `
${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}
`; } @@ -3717,10 +3713,9 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = try { const mySeq = ++monitorPanelFetchSeq; - const currentPage = 1; // 筛选时重置到第一页 + const currentPage = 1; const pageSize = monitorState.pagination.pageSize; - - // 构建请求 URL + let url = `/api/monitor?page=${currentPage}&page_size=${pageSize}`; if (statusFilter && statusFilter !== 'all') { url += `&status=${encodeURIComponent(statusFilter)}`; @@ -3728,37 +3723,34 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = if (toolFilter && toolFilter !== 'all') { url += `&tool=${encodeURIComponent(toolFilter)}`; } - - const { result, timeline, timelineError } = await fetchMonitorAndTimeline(url); + + const range = getMcpMonitorTimelineRange(); + monitorState.timelineLoading = true; + const timelinePromise = fetchMonitorTimeline(range); + + const monitorResp = await apiFetch(url, { method: 'GET' }); + const result = await monitorResp.json().catch(() => ({})); + if (!monitorResp.ok) { + throw new Error(result.error || '获取监控数据失败'); + } if (mySeq !== monitorPanelFetchSeq) { return; } - monitorState.executions = Array.isArray(result.executions) ? result.executions : []; - monitorState.stats = result.stats || {}; + applyMonitorPayload(result, statusFilter); + + const { timeline, timelineError } = await timelinePromise; + if (mySeq !== monitorPanelFetchSeq) { + return; + } monitorState.timeline = timeline; monitorState.timelineError = timelineError; - monitorState.lastFetchedAt = new Date(); - monitorState.retentionDays = typeof result.retention_days === 'number' ? result.retention_days : 0; - - // 更新分页信息 - if (result.total !== undefined) { - monitorState.pagination = { - page: result.page || currentPage, - pageSize: result.page_size || pageSize, - total: result.total || 0, - totalPages: result.total_pages || 1 - }; - } - - renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt); - renderMonitorExecutions(monitorState.executions, statusFilter); - renderMonitorPagination(); - - // 初始化每页显示数量选择器 + monitorState.timelineLoading = false; + updateMonitorTimelineSection(); initializeMonitorPageSize(); } catch (error) { console.error('刷新监控面板失败:', error); + monitorState.timelineLoading = false; if (statsContainer) { statsContainer.innerHTML = `
${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}
`; } @@ -3768,6 +3760,63 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = } } +function applyMonitorPayload(result, statusFilter) { + const currentPage = monitorState.pagination.page; + const pageSize = monitorState.pagination.pageSize; + + monitorState.executions = Array.isArray(result.executions) ? result.executions : []; + monitorState.summary = result.summary || null; + monitorState.topTools = Array.isArray(result.topTools) ? result.topTools : []; + monitorState.lastFetchedAt = new Date(); + monitorState.retentionDays = typeof result.retentionDays === 'number' ? result.retentionDays : 0; + + if (result.total !== undefined) { + monitorState.pagination = { + page: result.page || currentPage, + pageSize: result.pageSize || pageSize, + total: result.total || 0, + totalPages: result.totalPages || 1 + }; + } + + renderMonitorStats(monitorState.summary, monitorState.topTools, monitorState.lastFetchedAt); + renderMonitorExecutions(monitorState.executions, statusFilter); + renderMonitorPagination(); +} + +async function fetchMonitorTimeline(range) { + try { + const timelineResp = await apiFetch(`/api/monitor/calls-timeline?range=${encodeURIComponent(range)}`, { method: 'GET' }); + const timelineJson = await timelineResp.json().catch(() => ({})); + if (!timelineResp.ok) { + return { timeline: null, timelineError: timelineJson.error || 'timeline failed' }; + } + return { timeline: timelineJson, timelineError: null }; + } catch (err) { + return { timeline: null, timelineError: err && err.message ? err.message : 'timeline failed' }; + } +} + +function updateMonitorTimelineSection() { + const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); + if (timelineInner) { + const combined = timelineInner.closest('.mcp-stats-combined'); + const compactEmpty = combined && !!combined.querySelector('.mcp-stats-combined__main'); + timelineInner.innerHTML = renderMcpStatsTimelineBody( + monitorState.timeline, + monitorState.timelineError, + compactEmpty, + monitorState.timelineLoading + ); + bindMcpStatsTimelineEvents(); + syncMcpMonitorTimelineRangeUI(); + return; + } + if (monitorState.summary) { + renderMonitorStats(monitorState.summary, monitorState.topTools, monitorState.lastFetchedAt); + } +} + const MCP_STATS_TOP_N = 6; const MCP_TIMELINE_RANGES = ['24h', '7d', '30d']; @@ -3782,29 +3831,14 @@ function getMcpMonitorTimelineRange() { return range; } -async function fetchMonitorAndTimeline(monitorUrl) { - const range = getMcpMonitorTimelineRange(); - const [monitorResp, timelineResp] = await Promise.all([ - apiFetch(monitorUrl, { method: 'GET' }), - apiFetch(`/api/monitor/calls-timeline?range=${encodeURIComponent(range)}`, { method: 'GET' }) - ]); - const result = await monitorResp.json().catch(() => ({})); - if (!monitorResp.ok) { - throw new Error(result.error || '获取监控数据失败'); - } - let timeline = null; - let timelineError = null; - try { - const timelineJson = await timelineResp.json().catch(() => ({})); - if (timelineResp.ok) { - timeline = timelineJson; - } else { - timelineError = timelineJson.error || 'timeline failed'; - } - } catch (err) { - timelineError = err && err.message ? err.message : 'timeline failed'; - } - return { result, timeline, timelineError }; +function buildMonitorTotals(summary) { + const s = summary && typeof summary === 'object' ? summary : {}; + return { + total: s.totalCalls || 0, + success: s.successCalls || 0, + failed: s.failedCalls || 0, + lastCallTime: s.lastCallTime ? new Date(s.lastCallTime) : null, + }; } function formatMcpTimelineLabel(isoOrDate, rangeKey, locale) { @@ -4028,34 +4062,19 @@ async function setMcpMonitorTimelineRange(range) { localStorage.setItem('mcpMonitorTimelineRange', range); monitorState.timelineRange = range; monitorState.timelineError = null; + monitorState.timelineLoading = true; syncMcpMonitorTimelineRangeUI(range); + updateMonitorTimelineSection(); try { - const timelineResp = await apiFetch(`/api/monitor/calls-timeline?range=${encodeURIComponent(range)}`, { method: 'GET' }); - const timelineJson = await timelineResp.json().catch(() => ({})); - if (!timelineResp.ok) { - throw new Error(timelineJson.error || '加载趋势失败'); - } - monitorState.timeline = timelineJson; - const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); - if (timelineInner) { - const combined = timelineInner.closest('.mcp-stats-combined'); - const compactEmpty = combined && !!combined.querySelector('.mcp-stats-combined__main'); - timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError, compactEmpty); - bindMcpStatsTimelineEvents(); - syncMcpMonitorTimelineRangeUI(range); - } else if (monitorState.stats && Object.keys(monitorState.stats).length > 0) { - renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt); - } + const { timeline, timelineError } = await fetchMonitorTimeline(range); + monitorState.timeline = timeline; + monitorState.timelineError = timelineError; + monitorState.timelineLoading = false; + updateMonitorTimelineSection(); } catch (err) { monitorState.timelineError = err.message || 'error'; - const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); - if (timelineInner) { - const combined = timelineInner.closest('.mcp-stats-combined'); - const compactEmpty = combined && !!combined.querySelector('.mcp-stats-combined__main'); - timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError, compactEmpty); - bindMcpStatsTimelineEvents(); - syncMcpMonitorTimelineRangeUI(range); - } + monitorState.timelineLoading = false; + updateMonitorTimelineSection(); } } window.setMcpMonitorTimelineRange = setMcpMonitorTimelineRange; @@ -4084,7 +4103,12 @@ function renderMcpStatsTimelineEmptyState(compact) { `; } -function renderMcpStatsTimelineBody(timeline, timelineError, compactEmpty) { +function renderMcpStatsTimelineBody(timeline, timelineError, compactEmpty, loading) { + if (loading) { + const loadingText = mcpMonitorT('timelineLoading') || monitorFallback('趋势加载中…', 'Loading trend…'); + return `
${escapeHtml(loadingText)}
`; + } + const hint = mcpMonitorT('timelineHint') || monitorFallback('全部工具合计', 'All tools combined'); if (timelineError) { @@ -4152,7 +4176,7 @@ function renderMcpStatsCombinedSection(topTools, totals, activeToolFilter, timel const timelineCol = showTimeline ? `

${escapeHtml(timelineTitle)}

-
${renderMcpStatsTimelineBody(timeline, timelineError, hasTools)}
+
${renderMcpStatsTimelineBody(timeline, timelineError, hasTools, monitorState.timelineLoading)}
` : ''; @@ -4207,20 +4231,11 @@ function refreshMonitorPanelFromState() { if (!monitorState.lastFetchedAt) return; const statusFilter = document.getElementById('monitor-status-filter'); const currentStatusFilter = statusFilter ? statusFilter.value : 'all'; - renderMonitorStats(monitorState.stats || {}, monitorState.lastFetchedAt); + renderMonitorStats(monitorState.summary, monitorState.topTools, monitorState.lastFetchedAt); renderMonitorExecutions(monitorState.executions || [], currentStatusFilter); renderMonitorPagination(); } -function normalizeMonitorStatsEntries(statsMap) { - if (!statsMap || typeof statsMap !== 'object') return []; - return Object.entries(statsMap).map(([key, item]) => { - const stat = item && typeof item === 'object' ? { ...item } : {}; - if (!stat.toolName) stat.toolName = key; - return stat; - }); -} - const MCP_STATS_TOOL_CHEVRON = ''; function getMcpStatsRateTone(rateNum) { @@ -4915,15 +4930,19 @@ function renderMcpStatsToolRanking(topTools, totals, activeToolFilter = '', opti return renderMcpStatsDetailSection(topTools, totals, activeToolFilter); } -function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { +function renderMonitorStats(summary = null, topTools = [], lastFetchedAt = null) { const container = document.getElementById('monitor-stats'); if (!container) { return; } - const entries = normalizeMonitorStatsEntries(statsMap); - const showTimeline = monitorState.timeline != null || !!monitorState.timelineError; - if (entries.length === 0 && !showTimeline) { + const tools = Array.isArray(topTools) ? topTools : []; + const totals = buildMonitorTotals(summary); + const toolCount = summary && typeof summary.toolCount === 'number' ? summary.toolCount : tools.length; + const showTimeline = monitorState.timelineLoading || monitorState.timeline != null || !!monitorState.timelineError; + const hasSummaryData = toolCount > 0 || totals.total > 0; + + if (!hasSummaryData && !showTimeline) { const noStats = mcpMonitorT('noStatsData') || monitorFallback('暂无统计数据', 'No statistical data'); container.innerHTML = '
' + escapeHtml(noStats) + '
'; const subtitle = document.getElementById('monitor-stats-subtitle'); @@ -4931,20 +4950,6 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { return; } - const totals = entries.reduce( - (acc, item) => { - acc.total += item.totalCalls || 0; - acc.success += item.successCalls || 0; - acc.failed += item.failedCalls || 0; - const lastCall = item.lastCallTime ? new Date(item.lastCallTime) : null; - if (lastCall && (!acc.lastCallTime || lastCall > acc.lastCallTime)) { - acc.lastCallTime = lastCall; - } - return acc; - }, - { total: 0, success: 0, failed: 0, lastCallTime: null } - ); - const hasCalls = totals.total > 0; const successRateNum = hasCalls ? (totals.success / totals.total) * 100 : 0; const successRate = hasCalls ? successRateNum.toFixed(1) : '-'; @@ -4965,19 +4970,13 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { const toolFilterEl = document.getElementById('monitor-tool-filter'); const activeToolFilter = toolFilterEl ? toolFilterEl.value.trim() : ''; - const topTools = entries - .filter(tool => (tool.totalCalls || 0) > 0) - .slice() - .sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0)) - .slice(0, MCP_STATS_TOP_N); - const hasAnyCalls = totals.total > 0; - const showCombined = hasAnyCalls && (topTools.length > 0 || showTimeline); + const showCombined = hasAnyCalls && (tools.length > 0 || showTimeline); const html = `
${renderMcpStatsMetricsBar(totals, successRate, rateTone, rateSubText, lastCallText, hasCalls)} ${showCombined ? renderMcpStatsCombinedSection( - topTools, + tools, totals, activeToolFilter, monitorState.timeline, @@ -4995,7 +4994,7 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { } else if (toolFilterEl) { toolFilterEl.classList.remove('is-filter-active'); } - updateMonitorStatsSubtitle(lastFetchedAt, entries.length, monitorState.retentionDays); + updateMonitorStatsSubtitle(lastFetchedAt, toolCount, monitorState.retentionDays); } function renderMonitorExecutions(executions = [], statusFilter = 'all') {