Add files via upload

This commit is contained in:
公明
2026-07-06 17:27:29 +08:00
committed by GitHub
parent 404feb372e
commit c40a1f82a8
2 changed files with 432 additions and 21 deletions
+363
View File
@@ -9574,6 +9574,369 @@ html[data-theme="dark"] .openapi-doc-btn:hover {
display: none;
}
/* MCP 监控统计重构:工具卡 + 柱线趋势 */
.mcp-stats-combined {
border-radius: 8px;
}
.mcp-stats-combined__head {
padding: 14px 18px;
}
.mcp-stats-combined__body--full {
min-height: 248px;
}
.mcp-stats-combined__main {
padding: 14px 16px 16px;
}
.mcp-stats-combined__body--full .mcp-stats-combined__timeline {
gap: 8px;
padding: 14px 16px 12px;
}
.mcp-stats-tools-panel {
gap: 12px;
}
.mcp-stats-proportion-bar {
height: 12px;
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.05);
}
.mcp-stats-tools-panel__caption {
gap: 6px;
}
.mcp-stats-tools-panel__list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
}
.mcp-stats-tool-item {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
min-width: 0;
min-height: 132px;
padding: 12px;
border: 1px solid rgba(148, 163, 184, 0.16);
background:
linear-gradient(180deg, rgba(248, 250, 252, 0.92), rgba(255, 255, 255, 0.96));
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8) inset;
}
.mcp-stats-tool-item:hover,
.mcp-stats-tool-item.is-highlighted {
background:
linear-gradient(180deg, rgba(239, 246, 255, 0.96), rgba(255, 255, 255, 0.98));
border-color: rgba(59, 130, 246, 0.26);
}
.mcp-stats-tool-item.is-active {
background: rgba(59, 130, 246, 0.08);
border-color: rgba(59, 130, 246, 0.38);
}
.mcp-stats-tool-item__top,
.mcp-stats-tool-item__middle,
.mcp-stats-tool-item__bottom {
display: flex;
align-items: center;
min-width: 0;
}
.mcp-stats-tool-item__top {
gap: 8px;
}
.mcp-stats-tool-item__rank {
flex: 0 0 auto;
}
.mcp-stats-tool-item__name {
font-size: 0.8125rem;
}
.mcp-stats-tool-item__share {
margin-left: auto;
padding: 2px 7px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.05);
}
.mcp-stats-tool-item__middle {
gap: 8px;
}
.mcp-stats-tool-item__calls {
font-size: 1.45rem;
line-height: 1;
}
.mcp-stats-tool-item__calls-label {
flex: 0 0 auto;
font-size: 0.6875rem;
color: var(--text-muted);
}
.mcp-stats-tool-item__track {
flex: 1 1 auto;
width: auto;
height: 6px;
background: rgba(15, 23, 42, 0.07);
}
.mcp-stats-tool-item__bottom {
gap: 6px;
flex-wrap: wrap;
margin-top: auto;
}
.mcp-stats-tool-item__pill {
display: inline-flex;
align-items: center;
min-height: 20px;
padding: 2px 7px;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
color: var(--text-secondary);
background: rgba(148, 163, 184, 0.13);
}
.mcp-stats-tool-item__pill.is-success {
color: #15803d;
background: rgba(34, 197, 94, 0.13);
}
.mcp-stats-tool-item__pill.is-danger {
color: #b91c1c;
background: rgba(239, 68, 68, 0.12);
}
.mcp-stats-tool-item__rate {
margin-left: auto;
flex-direction: row;
align-items: center;
gap: 4px;
min-width: 0;
}
.mcp-stats-tool-item__fail {
font-size: 0.625rem;
}
.mcp-stats-timeline-moments {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
margin: 2px 0 0;
}
.mcp-stats-timeline-moments__label {
flex: 0 0 auto;
font-size: 0.6875rem;
font-weight: 600;
color: var(--text-muted);
}
.mcp-stats-timeline-moments__list {
display: flex;
gap: 6px;
min-width: 0;
overflow: hidden;
flex: 1 1 auto;
flex-wrap: nowrap;
}
.mcp-stats-timeline-moment {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
max-width: 144px;
padding: 4px 8px;
border-radius: 999px;
background: rgba(59, 130, 246, 0.08);
color: #1d4ed8;
font-size: 0.6875rem;
font-weight: 600;
flex: 0 0 auto;
}
.mcp-stats-timeline-moment__time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mcp-stats-timeline-moment__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
color: #fff;
background: #2563eb;
box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.04);
}
.mcp-stats-timeline-moment__fail {
display: inline-flex;
align-items: center;
min-height: 18px;
padding: 0 6px;
border-radius: 999px;
color: #b91c1c;
background: rgba(239, 68, 68, 0.12);
font-size: 0.625rem;
font-weight: 700;
white-space: nowrap;
}
.mcp-stats-timeline-moment--more {
color: var(--text-secondary);
background: rgba(148, 163, 184, 0.14);
font-weight: 700;
}
.mcp-stats-timeline__chart-wrap {
min-height: 146px;
padding: 4px 0 0;
}
.mcp-stats-combined__timeline .mcp-stats-timeline__chart-wrap,
.mcp-stats-combined__timeline .mcp-stats-timeline__chart {
min-height: 132px;
}
.mcp-stats-timeline-bar {
fill: rgba(59, 130, 246, 0.14);
cursor: crosshair;
transition: fill 0.12s ease, opacity 0.12s ease;
}
.mcp-stats-timeline-bar.is-active {
fill: rgba(59, 130, 246, 0.42);
}
.mcp-stats-timeline-bar.is-hover {
fill: rgba(37, 99, 235, 0.68);
}
.mcp-stats-timeline-bar-fail {
fill: rgba(239, 68, 68, 0.72);
cursor: crosshair;
}
.mcp-stats-timeline-bar-fail.is-hover {
fill: rgba(220, 38, 38, 0.9);
}
.mcp-stats-timeline-area {
opacity: 0.55;
}
.mcp-stats-timeline-line {
stroke-width: 2;
}
.mcp-stats-timeline-dot {
opacity: 0;
}
@media (max-width: 1180px) {
.mcp-stats-tools-panel__list {
grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
}
}
@media (max-width: 720px) {
.mcp-stats-tools-panel__list {
grid-template-columns: 1fr;
}
.mcp-stats-combined__main {
max-width: none;
}
.mcp-stats-combined__timeline {
max-width: none !important;
}
.mcp-stats-timeline-moments {
align-items: center;
flex-direction: row;
}
.mcp-stats-timeline-moments__list {
flex-wrap: nowrap;
overflow: hidden;
}
}
html[data-theme="dark"] .mcp-stats-tool-item {
background: linear-gradient(180deg, #111827 0%, #0f172a 100%);
border-color: #263244;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset;
}
html[data-theme="dark"] .mcp-stats-tool-item:hover,
html[data-theme="dark"] .mcp-stats-tool-item.is-highlighted {
background: linear-gradient(180deg, #172033 0%, #111827 100%);
border-color: rgba(96, 165, 250, 0.36);
}
html[data-theme="dark"] .mcp-stats-tool-item.is-active {
background: rgba(96, 165, 250, 0.12);
border-color: rgba(96, 165, 250, 0.46);
}
html[data-theme="dark"] .mcp-stats-tool-item__share,
html[data-theme="dark"] .mcp-stats-tool-item__track {
background: rgba(148, 163, 184, 0.12);
}
html[data-theme="dark"] .mcp-stats-tool-item__pill {
background: rgba(148, 163, 184, 0.14);
}
html[data-theme="dark"] .mcp-stats-tool-item__pill.is-success {
color: #86efac;
background: rgba(34, 197, 94, 0.14);
}
html[data-theme="dark"] .mcp-stats-tool-item__pill.is-danger,
html[data-theme="dark"] .mcp-stats-timeline-moment__fail {
color: #fca5a5;
background: rgba(239, 68, 68, 0.14);
}
html[data-theme="dark"] .mcp-stats-timeline-moment {
color: #93c5fd;
background: rgba(96, 165, 250, 0.14);
}
html[data-theme="dark"] .mcp-stats-timeline-moment--more {
color: var(--text-secondary);
background: rgba(148, 163, 184, 0.14);
}
html[data-theme="dark"] .mcp-stats-timeline-bar {
fill: rgba(96, 165, 250, 0.16);
}
html[data-theme="dark"] .mcp-stats-timeline-bar.is-active {
fill: rgba(96, 165, 250, 0.46);
}
html[data-theme="dark"] .mcp-stats-timeline-bar.is-hover {
fill: rgba(96, 165, 250, 0.78);
}
/* ── 工具统计面板:表格 + 饼图 ── */
.mcp-stats-panel {
background: #fff;
+69 -21
View File
@@ -4377,6 +4377,8 @@ function buildMcpTimelineSvg(points, rangeKey) {
const maxVal = Math.max(1, ...points.map((p) => p.total || 0));
const hasFailed = points.some((p) => (p.failed || 0) > 0);
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const barGap = points.length > 48 ? 1 : 2;
const barW = Math.max(1.6, Math.min(8, (plotW / Math.max(1, points.length)) - barGap));
const coords = points.map((p, i) => {
const x = padL + (points.length <= 1 ? plotW / 2 : (i / (points.length - 1)) * plotW);
@@ -4433,6 +4435,21 @@ function buildMcpTimelineSvg(points, rangeKey) {
data-failed="${c.p.failed || 0}" />`;
}).join('');
const bars = coords.map((c) => {
const total = c.p.total || 0;
const failed = c.p.failed || 0;
const h = total > 0 ? Math.max(3, (total / maxVal) * plotH) : 1;
const y = baseY - h;
const failedH = failed > 0 ? Math.max(2, (failed / maxVal) * plotH) : 0;
const tipTime = formatMcpTimelineLabel(c.p.t, rangeKey, locale);
return `<g class="mcp-stats-timeline-bar-group">
<rect class="mcp-stats-timeline-bar${total > 0 ? ' is-active' : ''}" x="${(c.x - barW / 2).toFixed(2)}" y="${y.toFixed(2)}" width="${barW.toFixed(2)}" height="${h.toFixed(2)}" rx="1.6"
data-time="${escapeHtml(tipTime)}" data-total="${total}" data-failed="${failed}" />
${failedH > 0 ? `<rect class="mcp-stats-timeline-bar-fail" x="${(c.x - barW / 2).toFixed(2)}" y="${(baseY - failedH).toFixed(2)}" width="${barW.toFixed(2)}" height="${failedH.toFixed(2)}" rx="1.6"
data-time="${escapeHtml(tipTime)}" data-total="${total}" data-failed="${failed}" />` : ''}
</g>`;
}).join('');
const peakC = coords[peakIdx];
const peakMarker = (peakC.p.total || 0) > 0
? `<circle class="mcp-stats-timeline-peak-glow" cx="${peakC.x.toFixed(2)}" cy="${peakC.y.toFixed(2)}" r="5" />`
@@ -4453,6 +4470,7 @@ function buildMcpTimelineSvg(points, rangeKey) {
</defs>
${yLines}
<path class="mcp-stats-timeline-area" d="${areaPath}" fill="url(#mcpTimelineAreaFill)" />
${bars}
${peakMarker}
<path class="mcp-stats-timeline-line" d="${linePath}" stroke="url(#mcpTimelineLineStroke)" />
${hasFailed ? `<path class="mcp-stats-timeline-line mcp-stats-timeline-line--fail" d="${failPath}" />` : ''}
@@ -4484,14 +4502,17 @@ function bindMcpStatsTimelineEvents() {
}
root.addEventListener('mousemove', function (e) {
const dot = e.target.closest('.mcp-stats-timeline-dot');
const dot = e.target.closest('.mcp-stats-timeline-dot, .mcp-stats-timeline-bar, .mcp-stats-timeline-bar-fail');
if (!dot || !mcpTimelineTooltipEl) {
root.querySelectorAll('.mcp-stats-timeline-dot.is-active').forEach((d) => d.classList.remove('is-active'));
root.querySelectorAll('.mcp-stats-timeline-bar.is-hover, .mcp-stats-timeline-bar-fail.is-hover').forEach((d) => d.classList.remove('is-hover'));
mcpTimelineTooltipEl.style.display = 'none';
return;
}
root.querySelectorAll('.mcp-stats-timeline-dot.is-active').forEach((d) => d.classList.remove('is-active'));
root.querySelectorAll('.mcp-stats-timeline-bar.is-hover, .mcp-stats-timeline-bar-fail.is-hover').forEach((d) => d.classList.remove('is-hover'));
dot.classList.add('is-active');
dot.classList.add('is-hover');
const time = dot.getAttribute('data-time') || '';
const total = dot.getAttribute('data-total') || '0';
const failed = dot.getAttribute('data-failed') || '0';
@@ -4507,6 +4528,7 @@ function bindMcpStatsTimelineEvents() {
if (!e.target.closest || !e.target.closest('.mcp-stats-combined__timeline, .mcp-stats-timeline')) return;
if (e.relatedTarget && root.contains(e.relatedTarget)) return;
root.querySelectorAll('.mcp-stats-timeline-dot.is-active').forEach((d) => d.classList.remove('is-active'));
root.querySelectorAll('.mcp-stats-timeline-bar.is-hover, .mcp-stats-timeline-bar-fail.is-hover').forEach((d) => d.classList.remove('is-hover'));
if (mcpTimelineTooltipEl) mcpTimelineTooltipEl.style.display = 'none';
});
@@ -4568,6 +4590,37 @@ function buildTimelineSparseHint(points, timeline) {
|| `该时段多数时间为 0,峰值 ${peak} 次出现在 ${peakTime}`;
}
function renderMcpTimelineActiveMoments(points, rangeKey) {
if (!Array.isArray(points) || points.length === 0) return '';
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const active = points
.map((p, i) => ({ ...p, i }))
.filter((p) => (p.total || 0) > 0)
.sort((a, b) => (b.total || 0) - (a.total || 0) || b.i - a.i)
const shown = active.slice(0, 4);
const hiddenCount = Math.max(0, active.length - shown.length);
if (!active.length) return '';
const label = mcpMonitorT('timelineActiveMoments') || monitorFallback('活跃时段', 'Active moments');
const moreLabel = mcpMonitorT('timelineMoreMoments', { n: hiddenCount }) || `+${hiddenCount}`;
const chips = shown.map((p) => {
const time = formatMcpTimelineLabel(p.t, rangeKey, locale);
const failed = p.failed || 0;
const failedLabel = mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`;
return `<span class="mcp-stats-timeline-moment" title="${escapeHtml(time)}">
<span class="mcp-stats-timeline-moment__time">${escapeHtml(time)}</span>
<span class="mcp-stats-timeline-moment__count">${p.total || 0}</span>
${failed > 0 ? `<span class="mcp-stats-timeline-moment__fail">${escapeHtml(failedLabel)}</span>` : ''}
</span>`;
}).join('');
const moreChip = hiddenCount > 0
? `<span class="mcp-stats-timeline-moment mcp-stats-timeline-moment--more" title="${escapeHtml(mcpMonitorT('timelineMoreMomentsTitle', { n: hiddenCount }) || `还有 ${hiddenCount} 个活跃时段`)}">${escapeHtml(moreLabel)}</span>`
: '';
return `<div class="mcp-stats-timeline-moments">
<span class="mcp-stats-timeline-moments__label">${escapeHtml(label)}</span>
<div class="mcp-stats-timeline-moments__list">${chips}${moreChip}</div>
</div>`;
}
async function setMcpMonitorTimelineRange(range) {
if (!MCP_TIMELINE_RANGES.includes(range)) return;
localStorage.setItem('mcpMonitorTimelineRange', range);
@@ -4643,12 +4696,14 @@ function renderMcpStatsTimelineBody(timeline, timelineError, compactEmpty, loadi
const failLegend = mcpMonitorT('timelineFailedLegend') || '失败';
const hasFailed = points.some((p) => (p.failed || 0) > 0);
const sparseHint = buildTimelineSparseHint(points, timeline);
const momentsHtml = renderMcpTimelineActiveMoments(points, rangeKey);
const sparseHtml = sparseHint
? `<p class="mcp-stats-timeline__sparse-hint">${escapeHtml(sparseHint)}</p>`
: '';
return `
<p class="mcp-stats-timeline__inline-meta">${escapeHtml(hint)} · ${escapeHtml(summaryText)}</p>
${momentsHtml}
<div class="mcp-stats-timeline__chart-wrap">${chartSvg}</div>
${sparseHtml}
<div class="mcp-stats-timeline__legend">
@@ -5294,10 +5349,6 @@ function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') {
const caption = mcpMonitorT('rankingSummary', { n: MCP_STATS_TOP_N, pct: topNSharePct, total: totals.total })
|| `Top ${MCP_STATS_TOP_N}${topNSharePct}% · 共 ${totals.total}`;
const unknownToolLabel = mcpMonitorT('unknownTool') || '未知工具';
const colTool = mcpMonitorT('columnTool') || '工具';
const colCalls = mcpMonitorT('columnCalls') || '调用';
const colShare = mcpMonitorT('columnShare') || '占比';
const colRate = mcpMonitorT('columnSuccessRate') || '成功率';
const distAria = mcpMonitorT('distTitle') || '调用分布';
const stackedHtml = segments.map((s) => {
@@ -5336,20 +5387,27 @@ function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') {
const failNote = failed > 0
? `<span class="mcp-stats-tool-item__fail">${escapeHtml(mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`)}</span>`
: '';
const successLabel = mcpMonitorT('successCount', { n: success }) || `成功 ${success}`;
const failedLabel = mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`;
return `<li class="mcp-stats-tool-item${isActive ? ' is-active' : ''}"
data-tool-name="${escapeHtml(rawName)}" tabindex="0" role="button"
aria-label="${escapeHtml(rowAria)}" aria-pressed="${isActive ? 'true' : 'false'}">
<span class="mcp-stats-tool-item__rank mcp-stats-rank${rankClass}">${index + 1}</span>
<span class="mcp-stats-tool-item__dot" style="background:${color}" aria-hidden="true"></span>
<div class="mcp-stats-tool-item__body">
<div class="mcp-stats-tool-item__top">
<span class="mcp-stats-tool-item__rank mcp-stats-rank${rankClass}">${index + 1}</span>
<span class="mcp-stats-tool-item__dot" style="background:${color}" aria-hidden="true"></span>
<span class="mcp-stats-tool-item__name" title="${escapeHtml(name)}">${escapeHtml(name)}</span>
<span class="mcp-stats-tool-item__share">${sharePct}%</span>
</div>
<div class="mcp-stats-tool-item__middle">
<strong class="mcp-stats-tool-item__calls">${total}</strong>
<span class="mcp-stats-tool-item__calls-label">${escapeHtml(mcpMonitorT('columnCalls') || '调用')}</span>
<span class="mcp-stats-tool-item__track" aria-hidden="true">
<span class="mcp-stats-tool-item__fill" style="width:${barPct}%;background:${color}"></span>
</span>
</div>
<div class="mcp-stats-tool-item__metrics">
<span class="mcp-stats-tool-item__share">${sharePct}%</span>
<span class="mcp-stats-tool-item__calls">${total}</span>
<div class="mcp-stats-tool-item__bottom">
<span class="mcp-stats-tool-item__pill is-success">${escapeHtml(successLabel)}</span>
<span class="mcp-stats-tool-item__pill${failed > 0 ? ' is-danger' : ''}">${escapeHtml(failedLabel)}</span>
<span class="mcp-stats-tool-item__rate ${rateClass}">${toolRate}%${failNote}</span>
</div>
</li>`;
@@ -5364,16 +5422,6 @@ function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') {
${escapeHtml(caption)}
</p>
</div>
<div class="mcp-stats-tools-panel__list-head" aria-hidden="true">
<span>#</span>
<span></span>
<span>${escapeHtml(colTool)}</span>
<span class="mcp-stats-tool-item__metrics-head">
<span>${escapeHtml(colShare)}</span>
<span>${escapeHtml(colCalls)}</span>
<span>${escapeHtml(colRate)}</span>
</span>
</div>
<ol class="mcp-stats-tools-panel__list">${listHtml}</ol>
</div>`;
}