Add files via upload

This commit is contained in:
公明
2026-05-18 17:28:14 +08:00
committed by GitHub
parent 8733f3a2d2
commit ead2ce3ecc
6 changed files with 2143 additions and 159 deletions
+378 -46
View File
@@ -2982,6 +2982,9 @@ async function applyMonitorFilters() {
const toolFilter = document.getElementById('monitor-tool-filter');
const status = statusFilter ? statusFilter.value : 'all';
const tool = toolFilter ? (toolFilter.value.trim() || 'all') : 'all';
if (toolFilter) {
toolFilter.classList.toggle('is-filter-active', tool !== 'all');
}
// 当筛选条件改变时,从后端重新获取数据
await refreshMonitorPanelWithFilter(status, tool);
}
@@ -3045,20 +3048,244 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter =
}
const MCP_STATS_TOP_N = 6;
function mcpMonitorT(key, params) {
if (typeof window.t !== 'function') return '';
return window.t('mcpMonitor.' + key, {
...(params || {}),
interpolation: { escapeValue: false },
});
}
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 = '<svg class="mcp-stats-tool-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>';
function getMcpStatsRateTone(rateNum) {
if (rateNum >= 95) return 'is-success';
if (rateNum >= 80) return 'is-warning';
return 'is-danger';
}
function getMcpStatsRingStrokeClass(rateNum) {
if (rateNum >= 95) return '';
if (rateNum >= 80) return 'is-warning';
return 'is-danger';
}
function renderMcpStatsSuccessRing(percent) {
const p = Math.min(100, Math.max(0, parseFloat(percent) || 0));
const r = 15.9155;
const circumference = 2 * Math.PI * r;
const offset = circumference - (p / 100) * circumference;
const strokeClass = getMcpStatsRingStrokeClass(p);
return `<div class="mcp-stats-ring-wrap" aria-hidden="true">
<svg class="mcp-stats-ring-svg" viewBox="0 0 36 36">
<circle class="mcp-stats-ring-track" cx="18" cy="18" r="${r}" fill="none" stroke-width="3"/>
<circle class="mcp-stats-ring-fill ${strokeClass}" cx="18" cy="18" r="${r}" fill="none" stroke-width="3"
stroke-dasharray="${circumference}" stroke-dashoffset="${offset}"/>
</svg>
</div>`;
}
function renderMcpStatsToolVolumeBar(total, success, failed, maxTotal) {
const volumePct = maxTotal > 0 && total > 0 ? (total / maxTotal) * 100 : 0;
const successPct = total > 0 ? (success / total) * 100 : 0;
const failPct = total > 0 ? (failed / total) * 100 : 0;
const legend = mcpMonitorT('barVolumeLegend') || '条长表示相对调用量';
const volumeTitle = `${total} / ${maxTotal}`;
return `<div class="mcp-stats-tool-bar-track" title="${escapeHtml(legend)} · ${escapeHtml(volumeTitle)}">
<div class="mcp-stats-tool-bar-fill" style="width:${volumePct.toFixed(2)}%">
<div class="mcp-stats-tool-bar-inner">
<span class="mcp-stats-tool-bar-seg mcp-stats-tool-bar-seg--success" style="width:${successPct.toFixed(2)}%"></span>
<span class="mcp-stats-tool-bar-seg mcp-stats-tool-bar-seg--fail" style="width:${failPct.toFixed(2)}%"></span>
</div>
</div>
</div>`;
}
function getMcpToolRateClass(rateNum) {
if (rateNum >= 95) return 'is-success';
if (rateNum >= 80) return 'is-warning';
return 'is-danger';
}
const MCP_STATS_DIST_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#14b8a6', '#ec4899'];
function renderMcpStatsInsightPanel(topTools, totals) {
const distTitle = mcpMonitorT('distTitle') || '调用分布';
const distLegend = mcpMonitorT('distLegend') || '扇区面积为占全部调用比例';
const top6ShareLabel = mcpMonitorT('distTop6Share', { n: MCP_STATS_TOP_N }) || `Top ${MCP_STATS_TOP_N} 占全部调用`;
const othersLabel = mcpMonitorT('distOthers') || '其他工具';
const callsUnit = (n) => mcpMonitorT('distCallsUnit', { n }) || `${n}`;
const top6Total = topTools.reduce((s, t) => s + (t.totalCalls || 0), 0);
const top6SharePct = totals.total > 0 ? ((top6Total / totals.total) * 100).toFixed(1) : '0.0';
const otherCalls = Math.max(0, totals.total - top6Total);
let acc = 0;
const segments = [];
topTools.forEach((tool, i) => {
const calls = tool.totalCalls || 0;
if (calls <= 0 || totals.total <= 0) return;
const pct = (calls / totals.total) * 100;
segments.push({
color: MCP_STATS_DIST_COLORS[i % MCP_STATS_DIST_COLORS.length],
start: acc,
end: acc + pct,
name: tool.toolName || '',
calls,
pct: pct.toFixed(1),
});
acc += pct;
});
if (otherCalls > 0 && totals.total > 0) {
const pct = (otherCalls / totals.total) * 100;
segments.push({
color: '#cbd5e1',
start: acc,
end: acc + pct,
name: othersLabel,
calls: otherCalls,
pct: pct.toFixed(1),
});
}
const conic = segments.length > 0
? segments.map(s => `${s.color} ${s.start.toFixed(2)}% ${s.end.toFixed(2)}%`).join(', ')
: '#e2e8f0 0% 100%';
const legendHtml = segments.map(s => `
<li class="mcp-stats-dist-legend-item">
<span class="mcp-stats-dist-swatch" style="--swatch-color:${s.color}"></span>
<span class="mcp-stats-dist-legend-name" title="${escapeHtml(s.name)}">${escapeHtml(s.name)}</span>
<span class="mcp-stats-dist-legend-meta"><em>${s.pct}%</em><span>${escapeHtml(callsUnit(s.calls))}</span></span>
</li>
`).join('');
const centerLabel = `Top ${MCP_STATS_TOP_N}`;
const distHint = totals.total > 0
? (mcpMonitorT('distTotalCalls', { n: totals.total }) || `${totals.total} 次调用`)
: '';
return `
<div class="mcp-stats-tools-panel mcp-stats-dist-panel" aria-label="${escapeHtml(distTitle)}">
<div class="mcp-stats-tools-header">
<div class="mcp-stats-tools-heading">
<h4 class="mcp-stats-tools-title">${escapeHtml(distTitle)}</h4>
<span class="mcp-stats-tools-legend">${escapeHtml(distLegend)}</span>
</div>
${distHint ? `<span class="mcp-stats-tools-hint">${escapeHtml(distHint)}</span>` : ''}
</div>
<div class="mcp-stats-dist-body mcp-stats-dist-body--stacked">
<div class="mcp-stats-dist-chart-stage">
<div class="mcp-stats-dist-chart-wrap">
<div class="mcp-stats-dist-donut" style="background:conic-gradient(${conic})" role="img" aria-label="${escapeHtml(top6ShareLabel)} ${top6SharePct}%"></div>
<div class="mcp-stats-dist-donut-hole">
<span class="mcp-stats-dist-donut-label">${centerLabel}</span>
<span class="mcp-stats-dist-donut-value">${top6SharePct}<span class="mcp-stats-dist-donut-unit">%</span></span>
</div>
</div>
</div>
<ul class="mcp-stats-dist-legend mcp-stats-dist-legend--grid">${legendHtml}</ul>
</div>
</div>
`;
}
function renderMcpStatsStackedBar(success, failed) {
const total = success + failed;
if (total <= 0) {
return '<div class="mcp-stats-stacked-bar" role="presentation"><div class="mcp-stats-stacked-bar-seg mcp-stats-stacked-bar-seg--success" style="flex:1"></div></div>';
}
const successFlex = Math.max(0, (success / total) * 100);
const failFlex = Math.max(0, (failed / total) * 100);
return `<div class="mcp-stats-stacked-bar" role="presentation">
<div class="mcp-stats-stacked-bar-seg mcp-stats-stacked-bar-seg--success" style="flex:${successFlex}"></div>
<div class="mcp-stats-stacked-bar-seg mcp-stats-stacked-bar-seg--fail" style="flex:${failFlex}"></div>
</div>`;
}
function updateMonitorStatsSubtitle(lastFetchedAt, toolCount) {
const subtitle = document.getElementById('monitor-stats-subtitle');
if (!subtitle) return;
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const timeText = lastFetchedAt
? (lastFetchedAt.toLocaleString ? lastFetchedAt.toLocaleString(locale) : String(lastFetchedAt))
: '—';
const text = mcpMonitorT('statsSubtitle', { time: timeText, count: toolCount })
|| `最后刷新 ${timeText} · 共 ${toolCount} 个工具`;
subtitle.textContent = text;
subtitle.hidden = false;
}
function filterMonitorByTool(toolName) {
const toolFilter = document.getElementById('monitor-tool-filter');
if (!toolFilter || !toolName) return;
toolFilter.value = toolName;
toolFilter.classList.add('is-filter-active');
applyMonitorFilters();
const execSection = document.querySelector('.monitor-executions');
if (execSection && typeof execSection.scrollIntoView === 'function') {
execSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function clearMonitorToolFilter() {
const toolFilter = document.getElementById('monitor-tool-filter');
if (!toolFilter) return;
toolFilter.value = '';
toolFilter.classList.remove('is-filter-active');
applyMonitorFilters();
}
let monitorStatsPanelEventsBound = false;
function bindMonitorStatsPanelEvents() {
if (monitorStatsPanelEventsBound) return;
const root = document.getElementById('monitor-stats');
if (!root) return;
root.addEventListener('click', function (e) {
const clearBtn = e.target.closest('.mcp-stats-clear-filter');
if (clearBtn) {
e.preventDefault();
clearMonitorToolFilter();
return;
}
const row = e.target.closest('.mcp-stats-tool-row');
if (!row) return;
const tool = row.getAttribute('data-tool-name');
if (tool) {
e.preventDefault();
filterMonitorByTool(tool);
}
});
monitorStatsPanelEventsBound = true;
}
function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
const container = document.getElementById('monitor-stats');
if (!container) {
return;
}
const entries = Object.values(statsMap);
const entries = normalizeMonitorStatsEntries(statsMap);
if (entries.length === 0) {
const noStats = typeof window.t === 'function' ? window.t('mcpMonitor.noStatsData') : '暂无统计数据';
const noStats = mcpMonitorT('noStatsData') || '暂无统计数据';
container.innerHTML = '<div class="monitor-empty">' + escapeHtml(noStats) + '</div>';
const subtitle = document.getElementById('monitor-stats-subtitle');
if (subtitle) subtitle.hidden = true;
return;
}
// 计算总体汇总
const totals = entries.reduce(
(acc, item) => {
acc.total += item.totalCalls || 0;
@@ -3073,59 +3300,154 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
{ total: 0, success: 0, failed: 0, lastCallTime: null }
);
const successRate = totals.total > 0 ? ((totals.success / totals.total) * 100).toFixed(1) : '0.0';
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
const lastUpdatedText = lastFetchedAt ? (lastFetchedAt.toLocaleString ? lastFetchedAt.toLocaleString(locale || 'en-US') : String(lastFetchedAt)) : 'N/A';
const noCallsYet = typeof window.t === 'function' ? window.t('mcpMonitor.noCallsYet') : '暂无调用';
const lastCallText = totals.lastCallTime ? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale || 'en-US') : String(totals.lastCallTime)) : noCallsYet;
const totalCallsLabel = typeof window.t === 'function' ? window.t('mcpMonitor.totalCalls') : '总调用次数';
const successFailedLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successFailed', { success: totals.success, failed: totals.failed }) : `成功 ${totals.success} / 失败 ${totals.failed}`;
const successRateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successRate') : '成功率';
const statsFromAll = typeof window.t === 'function' ? window.t('mcpMonitor.statsFromAllTools') : '统计自全部工具调用';
const lastCallLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastCall') : '最近一次调用';
const lastRefreshLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastRefreshTime') : '最后刷新时间';
const successRateNum = totals.total > 0 ? (totals.success / totals.total) * 100 : 0;
const successRate = successRateNum.toFixed(1);
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const noCallsYet = mcpMonitorT('noCallsYet') || '暂无调用';
const lastCallText = totals.lastCallTime
? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale) : String(totals.lastCallTime))
: noCallsYet;
let html = `
<div class="monitor-stat-card">
<h4>${escapeHtml(totalCallsLabel)}</h4>
<div class="monitor-stat-value">${totals.total}</div>
<div class="monitor-stat-meta">${escapeHtml(successFailedLabel)}</div>
</div>
<div class="monitor-stat-card">
<h4>${escapeHtml(successRateLabel)}</h4>
<div class="monitor-stat-value">${successRate}%</div>
<div class="monitor-stat-meta">${escapeHtml(statsFromAll)}</div>
</div>
<div class="monitor-stat-card">
<h4>${escapeHtml(lastCallLabel)}</h4>
<div class="monitor-stat-value" style="font-size:1rem;">${escapeHtml(lastCallText)}</div>
<div class="monitor-stat-meta">${escapeHtml(lastRefreshLabel)}${escapeHtml(lastUpdatedText)}</div>
</div>
`;
const totalCallsLabel = mcpMonitorT('totalCalls') || '总调用次数';
const successRateLabel = mcpMonitorT('successRate') || '成功率';
const lastCallLabel = mcpMonitorT('lastCall') || '最近一次调用';
const statsFromAll = mcpMonitorT('statsFromAllTools') || '统计自全部工具调用';
const successPill = mcpMonitorT('successCount', { n: totals.success }) || `成功 ${totals.success}`;
const failedPill = mcpMonitorT('failedCount', { n: totals.failed }) || `失败 ${totals.failed}`;
const rateTone = getMcpStatsRateTone(successRateNum);
let rateSubText = mcpMonitorT('rateHealthy') || '运行平稳';
if (successRateNum < 80) rateSubText = mcpMonitorT('rateCritical') || '失败率偏高';
else if (successRateNum < 95) rateSubText = mcpMonitorT('rateWarning') || '存在失败调用';
const toolFilterEl = document.getElementById('monitor-tool-filter');
const activeToolFilter = toolFilterEl ? toolFilterEl.value.trim() : '';
// 显示最多前4个工具的统计(过滤掉 totalCalls 为 0 的工具)
const topTools = entries
.filter(tool => (tool.totalCalls || 0) > 0)
.slice()
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
.slice(0, 4);
.slice(0, MCP_STATS_TOP_N);
const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具';
topTools.forEach(tool => {
const toolSuccessRate = tool.totalCalls > 0 ? ((tool.successCalls || 0) / tool.totalCalls * 100).toFixed(1) : '0.0';
const toolMeta = typeof window.t === 'function' ? window.t('mcpMonitor.successFailedRate', { success: tool.successCalls || 0, failed: tool.failedCalls || 0, rate: toolSuccessRate }) : `成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%`;
html += `
<div class="monitor-stat-card">
<h4>${escapeHtml(tool.toolName || unknownToolLabel)}</h4>
<div class="monitor-stat-value">${tool.totalCalls || 0}</div>
<div class="monitor-stat-meta">
${escapeHtml(toolMeta)}
</div>
</div>
const maxToolCalls = topTools.length > 0 ? (topTools[0].totalCalls || 0) : 0;
const unknownToolLabel = mcpMonitorT('unknownTool') || '未知工具';
const topToolsTitle = mcpMonitorT('topToolsTitle', { n: MCP_STATS_TOP_N }) || `工具调用 Top ${MCP_STATS_TOP_N}`;
const toolsHint = mcpMonitorT('clickToFilterTool') || '点击行筛选下方执行记录';
const barLegend = mcpMonitorT('barVolumeLegend') || '条长表示相对调用量';
const successRateAria = mcpMonitorT('successRateAria', { rate: successRate }) || `成功率 ${successRate}%`;
const iconCalls = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
const iconRate = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
const iconTime = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
let toolRowsHtml = '';
topTools.forEach((tool, index) => {
const name = tool.toolName || unknownToolLabel;
const total = tool.totalCalls || 0;
const success = tool.successCalls || 0;
const failed = tool.failedCalls || 0;
const toolRateNum = total > 0 ? (success / total) * 100 : 0;
const toolRate = toolRateNum.toFixed(1);
const isActive = activeToolFilter && activeToolFilter === name;
const rowAria = mcpMonitorT('toolRowAriaLabel', { name, total, rate: toolRate })
|| `${name}${total} 次调用,成功率 ${toolRate}%`;
const rateClass = getMcpToolRateClass(toolRateNum);
toolRowsHtml += `
<li class="mcp-stats-tool-item">
<button type="button" class="mcp-stats-tool-row${isActive ? ' is-active' : ''}"
data-tool-name="${escapeHtml(name)}"
aria-label="${escapeHtml(rowAria)}"
aria-pressed="${isActive ? 'true' : 'false'}">
<span class="mcp-stats-tool-rank">${index + 1}</span>
<div class="mcp-stats-tool-main">
<div class="mcp-stats-tool-top">
<span class="mcp-stats-tool-name" title="${escapeHtml(name)}">${escapeHtml(name)}</span>
<span class="mcp-stats-tool-metrics">
<span class="mcp-stats-tool-count">${total}</span>
<span aria-hidden="true">·</span>
<span class="mcp-stats-tool-rate ${rateClass}">${toolRate}%</span>
${failed > 0 ? `<span class="mcp-stats-tool-fail-badge">${escapeHtml(mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`)}</span>` : ''}
</span>
</div>
${renderMcpStatsToolVolumeBar(total, success, failed, maxToolCalls)}
</div>
${MCP_STATS_TOOL_CHEVRON}
</button>
</li>
`;
});
container.innerHTML = `<div class="monitor-stats-grid">${html}</div>`;
const clearFilterBtn = activeToolFilter
? `<button type="button" class="mcp-stats-clear-filter">${escapeHtml(mcpMonitorT('clearToolFilter') || '清除工具筛选')}</button>`
: '';
const html = `
<div class="mcp-exec-stats">
<div class="mcp-stats-kpi-row">
<article class="mcp-stats-kpi-card mcp-stats-kpi-card--calls">
<div class="mcp-stats-kpi-head">
<span class="mcp-stats-kpi-label">${escapeHtml(totalCallsLabel)}</span>
<span class="mcp-stats-kpi-icon mcp-stats-kpi-icon--calls" aria-hidden="true">${iconCalls}</span>
</div>
<div class="mcp-stats-kpi-value">${totals.total}</div>
${renderMcpStatsStackedBar(totals.success, totals.failed)}
<div class="mcp-stats-kpi-sub">
<span class="mcp-stats-pill mcp-stats-pill--success">${escapeHtml(successPill)}</span>
<span class="mcp-stats-pill mcp-stats-pill--fail">${escapeHtml(failedPill)}</span>
</div>
</article>
<article class="mcp-stats-kpi-card mcp-stats-kpi-card--rate">
<div class="mcp-stats-kpi-head">
<span class="mcp-stats-kpi-label">${escapeHtml(successRateLabel)}</span>
<span class="mcp-stats-kpi-icon mcp-stats-kpi-icon--rate" aria-hidden="true">${iconRate}</span>
</div>
<div class="mcp-stats-kpi-body" role="img" aria-label="${escapeHtml(successRateAria)}">
<div class="mcp-stats-kpi-value">${successRate}%</div>
${renderMcpStatsSuccessRing(successRate)}
</div>
<div class="mcp-stats-kpi-sub">
<span class="mcp-stats-kpi-sub-text ${rateTone}">${escapeHtml(rateSubText)}</span>
<span class="mcp-stats-kpi-sub-text">${escapeHtml(statsFromAll)}</span>
</div>
</article>
<article class="mcp-stats-kpi-card mcp-stats-kpi-card--time">
<div class="mcp-stats-kpi-head">
<span class="mcp-stats-kpi-label">${escapeHtml(lastCallLabel)}</span>
<span class="mcp-stats-kpi-icon mcp-stats-kpi-icon--time" aria-hidden="true">${iconTime}</span>
</div>
<div class="mcp-stats-kpi-value mcp-stats-kpi-value--time">${escapeHtml(lastCallText)}</div>
</article>
</div>
${topTools.length > 0 ? `
<div class="mcp-stats-split">
<div class="mcp-stats-split-left">
<div class="mcp-stats-tools-panel">
<div class="mcp-stats-tools-header">
<div class="mcp-stats-tools-heading">
<h4 class="mcp-stats-tools-title">${escapeHtml(topToolsTitle)}</h4>
<span class="mcp-stats-tools-legend">${escapeHtml(barLegend)}</span>
</div>
<span class="mcp-stats-tools-hint">${escapeHtml(toolsHint)}</span>
</div>
<ol class="mcp-stats-tool-list" aria-label="${escapeHtml(topToolsTitle)}">${toolRowsHtml}</ol>
${clearFilterBtn}
</div>
</div>
<div class="mcp-stats-split-right">
${renderMcpStatsInsightPanel(topTools, totals)}
</div>
</div>
` : ''}
</div>
`;
container.innerHTML = html;
bindMonitorStatsPanelEvents();
if (toolFilterEl && activeToolFilter) {
toolFilterEl.classList.add('is-filter-active');
} else if (toolFilterEl) {
toolFilterEl.classList.remove('is-filter-active');
}
updateMonitorStatsSubtitle(lastFetchedAt, entries.length);
}
function renderMonitorExecutions(executions = [], statusFilter = 'all') {
@@ -3622,4 +3944,14 @@ document.addEventListener('languagechange', function () {
updateBatchActionsState();
loadActiveTasks();
refreshProgressAndTimelineI18n();
if (monitorState.stats && Object.keys(monitorState.stats).length > 0) {
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
}
});
document.addEventListener('DOMContentLoaded', function () {
bindMonitorStatsPanelEvents();
});
window.filterMonitorByTool = filterMonitorByTool;
window.clearMonitorToolFilter = clearMonitorToolFilter;