From ead2ce3ecc7d697178b3b2232328c42ea8412276 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=85=AC=E6=98=8E?=
<83812544+Ed1s0nZ@users.noreply.github.com>
Date: Mon, 18 May 2026 17:28:14 +0800
Subject: [PATCH] Add files via upload
---
web/static/css/style.css | 1109 +++++++++++++++++++++++++++++++-
web/static/i18n/en-US.json | 30 +
web/static/i18n/zh-CN.json | 30 +
web/static/js/monitor.js | 424 ++++++++++--
web/static/js/vulnerability.js | 553 +++++++++++++++-
web/templates/index.html | 156 +++--
6 files changed, 2143 insertions(+), 159 deletions(-)
diff --git a/web/static/css/style.css b/web/static/css/style.css
index c4f6e012..2d4f2c3c 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -5640,10 +5640,738 @@ header {
border-color: var(--accent-color);
}
+.monitor-section .section-actions input#monitor-tool-filter.is-filter-active {
+ border-color: rgba(0, 102, 255, 0.45);
+ background: rgba(0, 102, 255, 0.04);
+ box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.12);
+}
+
.monitor-section .section-actions input[type="text"]::placeholder {
color: var(--text-muted);
}
+/* MCP 执行统计:概览 KPI + 工具排行 */
+.monitor-stats-section-header {
+ align-items: flex-start;
+}
+
+.monitor-stats-header-text h3 {
+ margin: 0;
+}
+
+.monitor-stats-subtitle {
+ margin: 4px 0 0;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.mcp-exec-stats-root {
+ width: 100%;
+ min-width: 0;
+}
+
+.mcp-exec-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ width: 100%;
+}
+
+.mcp-stats-kpi-row {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+}
+
+@media (max-width: 900px) {
+ .mcp-stats-kpi-row {
+ grid-template-columns: 1fr;
+ }
+}
+
+.mcp-stats-kpi-card {
+ background: #fff;
+ border-radius: 14px;
+ padding: 18px 20px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-height: 118px;
+ min-width: 0;
+ transition: transform 0.2s ease, box-shadow 0.25s ease, border-color 0.2s ease;
+}
+
+.mcp-stats-kpi-card--calls {
+ background: linear-gradient(145deg, #fff 0%, #f0f9ff 100%);
+}
+
+.mcp-stats-kpi-card--rate {
+ background: linear-gradient(145deg, #fff 0%, #f0fdfa 100%);
+}
+
+.mcp-stats-kpi-card--time {
+ background: linear-gradient(145deg, #fff 0%, #faf5ff 100%);
+}
+
+.mcp-stats-kpi-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.mcp-stats-kpi-label {
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.mcp-stats-kpi-icon {
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.mcp-stats-kpi-icon--calls { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
+.mcp-stats-kpi-icon--rate { background: rgba(20, 184, 166, 0.1); color: #14b8a6; }
+.mcp-stats-kpi-icon--time { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; }
+
+.mcp-stats-kpi-body {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ min-width: 0;
+}
+
+.mcp-stats-kpi-value {
+ font-size: 1.875rem;
+ font-weight: 800;
+ color: var(--text-primary);
+ line-height: 1.1;
+ letter-spacing: -0.03em;
+ font-variant-numeric: tabular-nums;
+}
+
+.mcp-stats-kpi-value--time {
+ font-size: 1.05rem;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+ line-height: 1.35;
+ word-break: break-word;
+}
+
+.mcp-stats-kpi-sub {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ min-height: 22px;
+}
+
+.mcp-stats-kpi-sub-text {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.mcp-stats-kpi-sub-text.is-success { color: #15803d; }
+.mcp-stats-kpi-sub-text.is-warning { color: #ca8a04; }
+.mcp-stats-kpi-sub-text.is-danger { color: #dc2626; }
+
+.mcp-stats-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+
+.mcp-stats-pill--success {
+ background: rgba(34, 197, 94, 0.12);
+ color: #15803d;
+}
+
+.mcp-stats-pill--fail {
+ background: rgba(239, 68, 68, 0.1);
+ color: #b91c1c;
+}
+
+.mcp-stats-stacked-bar {
+ display: flex;
+ height: 6px;
+ border-radius: 999px;
+ overflow: hidden;
+ background: rgba(0, 0, 0, 0.06);
+ gap: 1px;
+ margin-top: 2px;
+}
+
+.mcp-stats-stacked-bar-seg {
+ min-width: 0;
+ transition: flex-grow 0.35s ease;
+}
+
+.mcp-stats-stacked-bar-seg--success {
+ background: linear-gradient(90deg, #22c55e, #16a34a);
+}
+
+.mcp-stats-stacked-bar-seg--fail {
+ background: linear-gradient(90deg, #f87171, #dc2626);
+}
+
+.mcp-stats-ring-wrap {
+ position: relative;
+ width: 56px;
+ height: 56px;
+ flex-shrink: 0;
+}
+
+.mcp-stats-ring-svg {
+ width: 56px;
+ height: 56px;
+ transform: rotate(-90deg);
+}
+
+.mcp-stats-ring-track {
+ stroke: rgba(0, 0, 0, 0.08);
+}
+
+.mcp-stats-ring-fill {
+ stroke: #14b8a6;
+ stroke-linecap: round;
+ transition: stroke-dashoffset 0.5s ease, stroke 0.2s ease;
+}
+
+.mcp-stats-ring-fill.is-warning { stroke: #eab308; }
+.mcp-stats-ring-fill.is-danger { stroke: #ef4444; }
+
+.mcp-stats-split {
+ display: grid;
+ grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr);
+ gap: 16px;
+ align-items: stretch;
+}
+
+@media (max-width: 1024px) {
+ .mcp-stats-split {
+ grid-template-columns: 1fr;
+ }
+
+ .mcp-stats-dist-chart-wrap {
+ width: min(72vw, 200px);
+ height: min(72vw, 200px);
+ }
+
+ .mcp-stats-dist-legend--grid {
+ grid-template-columns: 1fr;
+ }
+
+ .mcp-stats-tool-list {
+ justify-content: flex-start;
+ }
+}
+
+.mcp-stats-split-left,
+.mcp-stats-split-right {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.mcp-stats-insight-panel {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 100%;
+}
+
+.mcp-stats-insight-block {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
+ padding: 14px 16px;
+}
+
+.mcp-stats-dist-body--stacked {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-height: 0;
+}
+
+.mcp-stats-dist-chart-stage {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 140px;
+ padding: 2px 0;
+}
+
+.mcp-stats-dist-chart-wrap {
+ position: relative;
+ width: min(100%, 220px);
+ height: min(100%, 220px);
+ max-width: 220px;
+ max-height: 220px;
+ aspect-ratio: 1;
+ flex-shrink: 0;
+}
+
+.mcp-stats-dist-donut {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ box-shadow:
+ 0 2px 12px rgba(0, 0, 0, 0.08),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.8);
+}
+
+.mcp-stats-dist-donut-hole {
+ position: absolute;
+ inset: 22%;
+ border-radius: 50%;
+ background: var(--bg-primary);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ box-shadow: inset 0 0 0 1px var(--border-color);
+}
+
+.mcp-stats-dist-donut-label {
+ font-size: 0.6875rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-muted);
+}
+
+.mcp-stats-dist-donut-value {
+ font-size: 1.625rem;
+ font-weight: 800;
+ color: var(--text-primary);
+ font-variant-numeric: tabular-nums;
+ line-height: 1;
+ letter-spacing: -0.04em;
+}
+
+.mcp-stats-dist-donut-unit {
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: var(--text-secondary);
+}
+
+.mcp-stats-dist-legend--grid {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+.mcp-stats-dist-legend-item {
+ display: grid;
+ grid-template-columns: 6px minmax(0, 1fr);
+ grid-template-rows: auto auto;
+ gap: 2px 8px;
+ align-items: center;
+ padding: 7px 8px;
+ border-radius: 8px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.mcp-stats-dist-legend-item:hover {
+ border-color: rgba(0, 102, 255, 0.22);
+ background: rgba(0, 102, 255, 0.03);
+}
+
+.mcp-stats-dist-swatch {
+ grid-row: 1 / 3;
+ align-self: stretch;
+ width: 6px;
+ border-radius: 3px;
+ background: var(--swatch-color, #94a3b8);
+}
+
+.mcp-stats-dist-legend-name {
+ grid-column: 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--text-primary);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.625rem;
+ font-weight: 600;
+ line-height: 1.2;
+}
+
+.mcp-stats-dist-legend-meta {
+ grid-column: 2;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.625rem;
+ font-variant-numeric: tabular-nums;
+}
+
+.mcp-stats-dist-legend-meta em {
+ font-style: normal;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+.mcp-stats-dist-legend-meta span {
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.mcp-stats-risk-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.mcp-stats-risk-btn {
+ width: 100%;
+ text-align: left;
+ padding: 8px 10px;
+ border-radius: 8px;
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ background: rgba(254, 242, 242, 0.6);
+ color: #b91c1c;
+ font-size: 0.75rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease;
+ font: inherit;
+}
+
+.mcp-stats-risk-btn:hover {
+ background: rgba(254, 242, 242, 1);
+ border-color: rgba(239, 68, 68, 0.35);
+}
+
+.mcp-stats-risk-empty,
+.mcp-stats-selected-empty {
+ margin: 0;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ line-height: 1.5;
+}
+
+.mcp-stats-selected-card {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.mcp-stats-selected-name {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ color: var(--text-primary);
+ word-break: break-all;
+}
+
+.mcp-stats-selected-meta {
+ margin: 0;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.mcp-stats-tools-panel {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
+ padding: 16px 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ flex: 1;
+ min-height: 100%;
+ box-sizing: border-box;
+}
+
+.mcp-stats-tools-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.mcp-stats-tools-heading {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.mcp-stats-tools-title {
+ margin: 0;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.mcp-stats-tools-legend {
+ font-size: 0.6875rem;
+ color: var(--text-muted);
+ font-weight: 500;
+ line-height: 1.4;
+}
+
+.mcp-stats-tools-hint {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-weight: 500;
+ white-space: nowrap;
+ padding-top: 2px;
+}
+
+.mcp-stats-tool-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ flex: 1;
+ justify-content: space-between;
+}
+
+.mcp-stats-tool-item {
+ margin: 0;
+ padding: 0;
+}
+
+.mcp-stats-tool-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 1px solid transparent;
+ background: var(--bg-primary);
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
+ text-align: left;
+ width: 100%;
+ box-sizing: border-box;
+ font: inherit;
+ color: var(--text-primary);
+}
+
+.mcp-stats-tool-row:hover {
+ border-color: rgba(0, 102, 255, 0.22);
+ box-shadow: 0 2px 10px rgba(0, 102, 255, 0.1);
+ transform: translateX(2px);
+}
+
+.mcp-stats-tool-row:hover .mcp-stats-tool-chevron {
+ color: var(--accent-color);
+ opacity: 1;
+}
+
+.mcp-stats-tool-row:focus-visible {
+ outline: 2px solid rgba(0, 102, 255, 0.45);
+ outline-offset: 2px;
+}
+
+.mcp-stats-tool-row.is-active {
+ border-color: rgba(0, 102, 255, 0.35);
+ background: rgba(0, 102, 255, 0.05);
+ box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
+}
+
+.mcp-stats-tool-row.is-active .mcp-stats-tool-chevron {
+ color: var(--accent-color);
+ opacity: 1;
+}
+
+.mcp-stats-tool-rank {
+ flex-shrink: 0;
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ background: rgba(0, 102, 255, 0.08);
+ color: #2563eb;
+ font-size: 0.75rem;
+ font-weight: 700;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-variant-numeric: tabular-nums;
+}
+
+.mcp-stats-tool-item:first-child .mcp-stats-tool-rank {
+ background: rgba(234, 179, 8, 0.15);
+ color: #a16207;
+}
+
+.mcp-stats-tool-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+}
+
+.mcp-stats-tool-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ min-width: 0;
+}
+
+.mcp-stats-tool-name {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: #1a1a1a;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ min-width: 4rem;
+ flex: 1 1 auto;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+}
+
+.mcp-stats-tool-metrics {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.mcp-stats-tool-count {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.mcp-stats-tool-rate.is-success { color: #15803d; font-weight: 600; }
+.mcp-stats-tool-rate.is-warning { color: #ca8a04; font-weight: 600; }
+.mcp-stats-tool-rate.is-danger { color: #dc2626; font-weight: 600; }
+
+.mcp-stats-tool-fail-badge {
+ display: inline-flex;
+ padding: 1px 6px;
+ border-radius: 999px;
+ font-size: 0.625rem;
+ font-weight: 600;
+ background: rgba(239, 68, 68, 0.1);
+ color: #b91c1c;
+}
+
+.mcp-stats-tool-chevron {
+ flex-shrink: 0;
+ color: var(--text-muted);
+ opacity: 0.45;
+ transition: color 0.15s ease, opacity 0.15s ease, transform 0.15s ease;
+}
+
+.mcp-stats-tool-row:hover .mcp-stats-tool-chevron {
+ transform: translateX(2px);
+}
+
+.mcp-stats-tool-bar-track {
+ width: 100%;
+ height: 8px;
+ border-radius: 999px;
+ background: rgba(0, 0, 0, 0.06);
+ overflow: hidden;
+}
+
+.mcp-stats-tool-bar-fill {
+ height: 100%;
+ min-width: 3px;
+ border-radius: 999px;
+ overflow: hidden;
+ transition: width 0.4s ease;
+}
+
+.mcp-stats-tool-bar-inner {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ gap: 1px;
+}
+
+.mcp-stats-tool-bar-seg {
+ height: 100%;
+ min-width: 0;
+ transition: width 0.35s ease;
+}
+
+.mcp-stats-tool-bar-seg--success {
+ background: linear-gradient(90deg, #22c55e, #16a34a);
+}
+
+.mcp-stats-tool-bar-seg--fail {
+ background: linear-gradient(90deg, #f87171, #dc2626);
+}
+
+.mcp-stats-clear-filter {
+ align-self: flex-start;
+ padding: 4px 12px;
+ font-size: 0.75rem;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-primary);
+ color: var(--accent-color);
+ cursor: pointer;
+ font-weight: 500;
+ transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.mcp-stats-clear-filter:hover {
+ background: rgba(0, 102, 255, 0.06);
+ border-color: rgba(0, 102, 255, 0.25);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .mcp-stats-tool-row,
+ .mcp-stats-stacked-bar-seg,
+ .mcp-stats-tool-bar-seg,
+ .mcp-stats-tool-bar-fill,
+ .mcp-stats-tool-chevron,
+ .mcp-stats-ring-fill {
+ transition: none;
+ }
+
+ .mcp-stats-tool-row:hover {
+ transform: none;
+ }
+}
+
+/* 兼容 Skills 监控等复用 monitor-stats-grid 的页面 */
.monitor-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
@@ -11057,9 +11785,25 @@ header {
.dashboard-stats {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 16px;
- margin-bottom: 24px;
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+@media (max-width: 1200px) {
+ .dashboard-stats {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 640px) {
+ .dashboard-stats {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .stat-card-total {
+ grid-column: 1 / -1;
+ }
}
.stat-card {
@@ -11107,8 +11851,118 @@ header {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
+ line-height: 1.1;
+ transition: color 0.2s ease;
}
+.stat-card.is-clickable {
+ cursor: pointer;
+ user-select: none;
+}
+
+.stat-card.is-clickable:focus-visible {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+}
+
+.stat-card.is-clickable:hover:not(.is-active) {
+ border-color: rgba(59, 130, 246, 0.35);
+}
+
+/* 选中态:浅底 + 外发光 ring,避免深色描边过硬 */
+.stat-card.is-clickable.is-active {
+ transform: none;
+ border-color: rgba(59, 130, 246, 0.35);
+ box-shadow: var(--shadow-sm), 0 0 0 2px rgba(59, 130, 246, 0.14);
+ background: rgba(59, 130, 246, 0.06);
+}
+
+.stat-card.stat-card-total.is-active {
+ border-color: rgba(148, 163, 184, 0.5);
+ box-shadow: var(--shadow-sm), 0 0 0 2px rgba(148, 163, 184, 0.18);
+ background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
+}
+
+.stat-card.stat-card-total.is-active .stat-label {
+ color: #475569;
+ font-weight: 600;
+}
+
+.stat-card.stat-critical.is-active {
+ border-color: rgba(220, 38, 38, 0.35);
+ box-shadow: var(--shadow-sm), 0 0 0 2px rgba(220, 38, 38, 0.12);
+ background: rgba(254, 242, 242, 0.65);
+}
+
+.stat-card.stat-high.is-active {
+ border-color: rgba(234, 88, 12, 0.35);
+ box-shadow: var(--shadow-sm), 0 0 0 2px rgba(234, 88, 12, 0.12);
+ background: rgba(255, 247, 237, 0.75);
+}
+
+.stat-card.stat-medium.is-active {
+ border-color: rgba(202, 138, 4, 0.4);
+ box-shadow: var(--shadow-sm), 0 0 0 2px rgba(202, 138, 4, 0.12);
+ background: rgba(254, 252, 232, 0.75);
+}
+
+.stat-card.stat-low.is-active {
+ border-color: rgba(15, 118, 110, 0.35);
+ box-shadow: var(--shadow-sm), 0 0 0 2px rgba(15, 118, 110, 0.12);
+ background: rgba(240, 253, 250, 0.75);
+}
+
+.stat-card.stat-info.is-active {
+ border-color: rgba(37, 99, 235, 0.35);
+ box-shadow: var(--shadow-sm), 0 0 0 2px rgba(37, 99, 235, 0.12);
+ background: rgba(239, 246, 255, 0.75);
+}
+
+.stat-pct {
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+ margin-top: 6px;
+ font-weight: 500;
+ letter-spacing: 0.02em;
+}
+
+.stat-card .stat-value.is-zero {
+ color: var(--text-secondary);
+ opacity: 0.5;
+}
+
+.stat-card.stat-critical .stat-value:not(.is-zero) { color: #dc2626; }
+.stat-card.stat-high .stat-value:not(.is-zero) { color: #ea580c; }
+.stat-card.stat-medium .stat-value:not(.is-zero) { color: #ca8a04; }
+.stat-card.stat-low .stat-value:not(.is-zero) { color: #0f766e; }
+.stat-card.stat-info .stat-value:not(.is-zero) { color: #2563eb; }
+
+.stat-stacked-bar {
+ display: flex;
+ height: 5px;
+ border-radius: 3px;
+ overflow: hidden;
+ margin-top: 12px;
+ background: rgba(0, 0, 0, 0.06);
+ gap: 1px;
+}
+
+.stat-stacked-bar.is-empty {
+ opacity: 0.35;
+}
+
+.stat-stacked-seg {
+ flex: 0 0 auto;
+ min-width: 0;
+ transition: flex-grow 0.35s ease, opacity 0.2s ease;
+}
+
+.stat-stacked-seg.critical { background: #f87171; }
+.stat-stacked-seg.high { background: #fb923c; }
+.stat-stacked-seg.medium { background: #facc15; }
+.stat-stacked-seg.low { background: #2dd4bf; }
+.stat-stacked-seg.info { background: #60a5fa; }
+
/* WebShell 管理页面样式 - 美化版 */
.webshell-page-content {
height: calc(100vh - 140px);
@@ -15715,37 +16569,254 @@ header {
}
}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
.vulnerability-controls {
- margin-bottom: 24px;
+ margin-bottom: 16px;
+ padding: 10px 12px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 10px;
+ box-shadow: var(--shadow-sm);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
-.vulnerability-filters {
+.vulnerability-controls.is-filtered {
+ border-color: rgba(59, 130, 246, 0.4);
+ box-shadow: var(--shadow-sm), 0 0 0 1px rgba(59, 130, 246, 0.08);
+}
+
+.vulnerability-filter-toolbar {
+ display: block;
+}
+
+.vulnerability-filter-primary {
display: flex;
- gap: 16px;
flex-wrap: wrap;
- align-items: flex-end;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
}
-.vulnerability-filters label {
+.vulnerability-filter-field {
display: flex;
- flex-direction: column;
- gap: 4px;
- font-size: 0.875rem;
- color: var(--text-secondary);
+ align-items: center;
+ min-width: 0;
+ margin: 0;
}
-.vulnerability-filters input,
-.vulnerability-filters select {
+.vulnerability-filter-field--grow {
+ flex: 1 1 220px;
+ min-width: 160px;
+}
+
+.vulnerability-filter-field--status {
+ flex: 0 0 auto;
+}
+
+.vulnerability-filter-field input,
+.vulnerability-filter-field select {
+ width: 100%;
+ min-width: 0;
padding: 8px 12px;
border: 1px solid var(--border-color);
- border-radius: 6px;
+ border-radius: 8px;
font-size: 0.875rem;
- min-width: 150px;
+ background: var(--bg-primary);
+ color: var(--text-primary);
}
-/* Keep action buttons visually aligned in vulnerability filters */
-.vulnerability-filters .btn-primary {
- border: 1px solid transparent;
+.vulnerability-filter-field--status select {
+ min-width: 120px;
+ cursor: pointer;
+}
+
+.vulnerability-filter-field input[type="search"] {
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.vulnerability-filter-field input:focus,
+.vulnerability-filter-field select:focus {
+ outline: none;
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12);
+}
+
+.vulnerability-filter-clear-btn {
+ flex-shrink: 0;
+ padding: 8px 12px;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: color 0.15s ease, background 0.15s ease;
+}
+
+.vulnerability-filter-clear-btn:hover {
+ color: #2563eb;
+ background: rgba(59, 130, 246, 0.06);
+}
+
+/* tasks-filters 的 display:flex 会覆盖 [hidden],必须显式隐藏 */
+#vulnerability-advanced-filters[hidden] {
+ display: none !important;
+}
+
+.vulnerability-filter-advanced-wrap {
+ margin-top: 6px;
+}
+
+.vulnerability-filter-advanced-wrap.is-expanded {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px dashed var(--border-color);
+}
+
+.vulnerability-filter-advanced {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px 14px;
+}
+
+.vulnerability-filter-advanced .vulnerability-filter-field {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 4px;
+}
+
+.vulnerability-filter-advanced .vulnerability-filter-field > span {
+ max-width: none;
+ font-size: 0.75rem;
+}
+
+.vulnerability-filter-advanced .vulnerability-filter-field input {
+ width: 100%;
+}
+
+@media (max-width: 768px) {
+ .vulnerability-filter-advanced {
+ grid-template-columns: 1fr;
+ }
+}
+
+.vulnerability-filter-advanced-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ margin: 0 0 8px;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: color 0.15s ease, background 0.15s ease;
+}
+
+.vulnerability-filter-advanced-toggle:hover {
+ color: var(--text-primary);
+ background: rgba(0, 0, 0, 0.04);
+}
+
+/* 高级筛选生效数量:弱提示文字,避免实心蓝点过于抢眼 */
+.vulnerability-filter-advanced-badge {
+ display: inline;
+ margin-left: 2px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ line-height: 1;
+ color: #64748b;
+ letter-spacing: 0.01em;
+}
+
+.vulnerability-filter-advanced-badge[hidden] {
+ display: none !important;
+}
+
+.vulnerability-filter-advanced-chevron {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 5px solid currentColor;
+ transition: transform 0.2s ease;
+}
+
+.vulnerability-filter-advanced-toggle[aria-expanded="true"] .vulnerability-filter-advanced-chevron {
+ transform: rotate(180deg);
+}
+
+.vulnerability-filter-chips {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid var(--border-color);
+}
+
+.vulnerability-filter-chips-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.vulnerability-filter-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px 4px 10px;
+ font-size: 0.75rem;
+ line-height: 1.3;
+ color: #1e40af;
+ background: rgba(59, 130, 246, 0.1);
+ border: 1px solid rgba(59, 130, 246, 0.25);
+ border-radius: 999px;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.vulnerability-filter-chip:hover {
+ background: rgba(59, 130, 246, 0.16);
+ border-color: rgba(59, 130, 246, 0.4);
+}
+
+.vulnerability-filter-chip-remove {
+ font-size: 0.875rem;
+ line-height: 1;
+ opacity: 0.7;
+}
+
+@media (max-width: 768px) {
+ .vulnerability-filter-primary {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .vulnerability-filter-field--grow,
+ .vulnerability-filter-field--status {
+ flex: 1 1 100%;
+ }
+
+ .vulnerability-filter-clear-btn {
+ align-self: flex-end;
+ }
}
.vulnerabilities-list {
diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index f2a87da9..2121fff3 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -1306,6 +1306,31 @@
"noCallsYet": "No calls yet",
"unknownTool": "Unknown tool",
"successFailedRate": "Success {{success}} / Failed {{failed}} · {{rate}}% success rate",
+ "topToolsTitle": "Top {{n}} tools by calls",
+ "barVolumeLegend": "Bar length: relative call volume; green/red: success vs failure share",
+ "clickToFilterTool": "Click a row to filter records below",
+ "toolRowAriaLabel": "{{name}}, {{total}} calls, {{rate}}% success rate. Click to filter records.",
+ "successRateAria": "Success rate {{rate}}%",
+ "filterByToolTitle": "Filtered by: {{tool}}",
+ "clearToolFilter": "Clear tool filter",
+ "successCount": "Success {{n}}",
+ "failedCount": "Failed {{n}}",
+ "rateHealthy": "Running smoothly",
+ "rateWarning": "Some failures detected",
+ "rateCritical": "High failure rate",
+ "statsSubtitle": "Refreshed {{time}} · {{count}} tools",
+ "distTitle": "Call distribution",
+ "distLegend": "Slice area shows share of all calls",
+ "distTotalCalls": "{{n}} total calls",
+ "distTop6Share": "Top {{n}} share of all calls",
+ "distOthers": "Other tools",
+ "distCallsUnit": "{{n}} calls",
+ "riskTitle": "Failure alerts",
+ "riskNone": "No recent failures",
+ "riskItem": "{{name}}: {{failed}} / {{total}} failed",
+ "selectedToolTitle": "Active filter",
+ "selectedToolEmpty": "Click a tool on the left to filter records below",
+ "selectedToolStats": "{{total}} calls · {{success}} ok · {{failed}} failed · {{rate}}% success",
"columnTool": "Tool",
"columnStatus": "Status",
"columnStartTime": "Start time",
@@ -1486,6 +1511,11 @@
},
"vulnerabilityPage": {
"statTotal": "Total",
+ "statClickAll": "View all (clear severity filter)",
+ "statClickFilter": "Click to filter by this severity; click again to clear",
+ "advancedFilters": "Advanced filters",
+ "activeFilters": "Active filters",
+ "chipRemove": "Remove",
"filter": "Filter",
"clear": "Clear",
"vulnId": "Vuln ID",
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index 6a6568ac..dff884aa 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -1295,6 +1295,31 @@
"noCallsYet": "暂无调用",
"unknownTool": "未知工具",
"successFailedRate": "成功 {{success}} / 失败 {{failed}} · 成功率 {{rate}}%",
+ "topToolsTitle": "工具调用 Top {{n}}",
+ "barVolumeLegend": "条长表示相对调用量,条内绿/红为成功/失败占比",
+ "clickToFilterTool": "点击行筛选下方执行记录",
+ "toolRowAriaLabel": "{{name}},{{total}} 次调用,成功率 {{rate}}%,点击查看执行记录",
+ "successRateAria": "成功率 {{rate}}%",
+ "filterByToolTitle": "筛选工具:{{tool}}",
+ "clearToolFilter": "清除工具筛选",
+ "successCount": "成功 {{n}}",
+ "failedCount": "失败 {{n}}",
+ "rateHealthy": "运行平稳",
+ "rateWarning": "存在失败调用",
+ "rateCritical": "失败率偏高",
+ "statsSubtitle": "最后刷新 {{time}} · 共 {{count}} 个工具",
+ "distTitle": "调用分布",
+ "distLegend": "扇区面积为占全部调用比例",
+ "distTotalCalls": "共 {{n}} 次调用",
+ "distTop6Share": "Top {{n}} 占全部调用",
+ "distOthers": "其他工具",
+ "distCallsUnit": "{{n}} 次",
+ "riskTitle": "失败提醒",
+ "riskNone": "近期无失败调用",
+ "riskItem": "{{name}}:失败 {{failed}} / {{total}} 次",
+ "selectedToolTitle": "当前筛选",
+ "selectedToolEmpty": "点击左侧工具行,可筛选下方执行记录",
+ "selectedToolStats": "调用 {{total}} 次 · 成功 {{success}} · 失败 {{failed}} · 成功率 {{rate}}%",
"columnTool": "工具",
"columnStatus": "状态",
"columnStartTime": "开始时间",
@@ -1475,6 +1500,11 @@
},
"vulnerabilityPage": {
"statTotal": "总漏洞数",
+ "statClickAll": "查看全部(清除严重度筛选)",
+ "statClickFilter": "点击按此严重度筛选;再次点击清除",
+ "advancedFilters": "高级筛选",
+ "activeFilters": "已选条件",
+ "chipRemove": "移除",
"filter": "筛选",
"clear": "清除",
"vulnId": "漏洞ID",
diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js
index dab04583..2c0bf3cd 100644
--- a/web/static/js/monitor.js
+++ b/web/static/js/monitor.js
@@ -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 = '';
+
+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 `
+
+
`;
+}
+
+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 ``;
+}
+
+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 => `
+
+
+ ${escapeHtml(s.name)}
+ ${s.pct}%${escapeHtml(callsUnit(s.calls))}
+
+ `).join('');
+
+ const centerLabel = `Top ${MCP_STATS_TOP_N}`;
+ const distHint = totals.total > 0
+ ? (mcpMonitorT('distTotalCalls', { n: totals.total }) || `共 ${totals.total} 次调用`)
+ : '';
+
+ return `
+
+ `;
+}
+
+
+function renderMcpStatsStackedBar(success, failed) {
+ const total = success + failed;
+ if (total <= 0) {
+ return '';
+ }
+ const successFlex = Math.max(0, (success / total) * 100);
+ const failFlex = Math.max(0, (failed / total) * 100);
+ return ``;
+}
+
+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 = '' + escapeHtml(noStats) + '
';
+ 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 = `
-
-
${escapeHtml(totalCallsLabel)}
-
${totals.total}
-
${escapeHtml(successFailedLabel)}
-
-
-
${escapeHtml(successRateLabel)}
-
${successRate}%
-
${escapeHtml(statsFromAll)}
-
-
-
${escapeHtml(lastCallLabel)}
-
${escapeHtml(lastCallText)}
-
${escapeHtml(lastRefreshLabel)}:${escapeHtml(lastUpdatedText)}
-
- `;
+ 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 += `
-
-
${escapeHtml(tool.toolName || unknownToolLabel)}
-
${tool.totalCalls || 0}
-
- ${escapeHtml(toolMeta)}
-
-
+ 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 = '';
+ const iconRate = '';
+ const iconTime = '';
+
+ 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 += `
+
+
+
`;
});
- container.innerHTML = `${html}
`;
+ const clearFilterBtn = activeToolFilter
+ ? ``
+ : '';
+
+ const html = `
+
+
+
+
+ ${escapeHtml(totalCallsLabel)}
+ ${iconCalls}
+
+ ${totals.total}
+ ${renderMcpStatsStackedBar(totals.success, totals.failed)}
+
+ ${escapeHtml(successPill)}
+ ${escapeHtml(failedPill)}
+
+
+
+
+ ${escapeHtml(successRateLabel)}
+ ${iconRate}
+
+
+
${successRate}%
+ ${renderMcpStatsSuccessRing(successRate)}
+
+
+ ${escapeHtml(rateSubText)}
+ ${escapeHtml(statsFromAll)}
+
+
+
+
+ ${escapeHtml(lastCallLabel)}
+ ${iconTime}
+
+ ${escapeHtml(lastCallText)}
+
+
+ ${topTools.length > 0 ? `
+
+
+
+ ${renderMcpStatsInsightPanel(topTools, totals)}
+
+
+ ` : ''}
+
+ `;
+
+ 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;
diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js
index 82711ee3..42c171ea 100644
--- a/web/static/js/vulnerability.js
+++ b/web/static/js/vulnerability.js
@@ -61,6 +61,24 @@ let vulnerabilityPagination = {
totalPages: 1
};
+const VULN_STAT_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
+let vulnerabilityStatCardsBound = false;
+let vulnerabilityFilterPanelBound = false;
+let vulnerabilityFilterOptionsCache = null;
+const VULNERABILITY_ADVANCED_OPEN_KEY = 'vulnerabilityAdvancedFiltersOpen';
+const VULNERABILITY_DATALIST_MAX = 8;
+const VULNERABILITY_DATALIST_MIN_QUERY = 2;
+
+const VULN_FILTER_CHIP_FIELDS = [
+ { key: 'id', labelKey: 'vulnerabilityPage.vulnId' },
+ { key: 'status', labelKey: null, format: 'status' },
+ { key: 'severity', labelKey: null, format: 'severity' },
+ { key: 'conversation_id', labelKey: 'vulnerabilityPage.conversationId' },
+ { key: 'task_id', labelKey: 'vulnerabilityPage.taskOrQueueId' },
+ { key: 'conversation_tag', labelKey: 'vulnerabilityPage.conversationTag' },
+ { key: 'task_tag', labelKey: 'vulnerabilityPage.taskTag' }
+];
+
// 从地址栏 #vulnerabilities?conversation_id= / ?task_id= / ?id= 同步筛选(通知/对话菜单/任务管理联动)
function syncVulnerabilityFiltersFromLocationHash() {
const hash = window.location.hash.slice(1);
@@ -74,23 +92,31 @@ function syncVulnerabilityFiltersFromLocationHash() {
const tid = (params.get('task_id') || '').trim();
const sev = (params.get('severity') || '').trim();
const st = (params.get('status') || '').trim();
- if (!vid && !cid && !tid && !sev && !st) {
+ const convTag = (params.get('conversation_tag') || '').trim();
+ const taskTag = (params.get('task_tag') || '').trim();
+ if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag) {
return;
}
vulnerabilityFilters.id = '';
vulnerabilityFilters.conversation_id = '';
vulnerabilityFilters.task_id = '';
+ vulnerabilityFilters.conversation_tag = '';
+ vulnerabilityFilters.task_tag = '';
vulnerabilityFilters.severity = '';
vulnerabilityFilters.status = '';
const idEl = document.getElementById('vulnerability-id-filter');
const convEl = document.getElementById('vulnerability-conversation-filter');
const taskEl = document.getElementById('vulnerability-task-filter');
+ const convTagEl = document.getElementById('vulnerability-conversation-tag-filter');
+ const taskTagEl = document.getElementById('vulnerability-task-tag-filter');
const sevEl = document.getElementById('vulnerability-severity-filter');
const stEl = document.getElementById('vulnerability-status-filter');
if (idEl) idEl.value = '';
if (convEl) convEl.value = '';
if (taskEl) taskEl.value = '';
+ if (convTagEl) convTagEl.value = '';
+ if (taskTagEl) taskTagEl.value = '';
if (sevEl) sevEl.value = '';
if (stEl) stEl.value = '';
@@ -106,6 +132,14 @@ function syncVulnerabilityFiltersFromLocationHash() {
vulnerabilityFilters.task_id = tid;
if (taskEl) taskEl.value = tid;
}
+ if (convTag) {
+ vulnerabilityFilters.conversation_tag = convTag;
+ if (convTagEl) convTagEl.value = convTag;
+ }
+ if (taskTag) {
+ vulnerabilityFilters.task_tag = taskTag;
+ if (taskTagEl) taskTagEl.value = taskTag;
+ }
if (sev) {
vulnerabilityFilters.severity = sev;
if (sevEl) sevEl.value = sev;
@@ -115,17 +149,457 @@ function syncVulnerabilityFiltersFromLocationHash() {
if (stEl) stEl.value = st;
}
vulnerabilityPagination.currentPage = 1;
+ if (hasVulnerabilityAdvancedFiltersActive()) {
+ setVulnerabilityAdvancedFiltersOpen(true, false);
+ }
+ syncVulnerabilityStatCardActiveState();
+ updateVulnerabilityFilterPanelState();
+ renderVulnerabilityFilterChips();
}
// 初始化漏洞管理页面
function initVulnerabilityPage() {
// 从localStorage加载每页条数设置
vulnerabilityPagination.pageSize = getVulnerabilityPageSize();
+ initVulnerabilityStatCards();
+ initVulnerabilityFilterPanel();
syncVulnerabilityFiltersFromLocationHash();
+ updateVulnerabilityFilterPanelState();
+ renderVulnerabilityFilterChips();
+ loadVulnerabilityFilterOptions();
loadVulnerabilityStats();
loadVulnerabilities();
}
+function initVulnerabilityStatCards() {
+ if (vulnerabilityStatCardsBound) {
+ syncVulnerabilityStatCardActiveState();
+ return;
+ }
+ const root = document.getElementById('vulnerability-stat-cards');
+ if (!root) return;
+ vulnerabilityStatCardsBound = true;
+ root.addEventListener('click', onVulnerabilityStatCardClick);
+ root.addEventListener('keydown', onVulnerabilityStatCardKeydown);
+}
+
+function onVulnerabilityStatCardClick(ev) {
+ const totalCard = ev.target.closest('.stat-card.stat-card-total');
+ if (totalCard) {
+ applyVulnerabilitySeverityFilter('');
+ return;
+ }
+ const card = ev.target.closest('.stat-card.is-clickable[data-severity]');
+ if (!card) return;
+ const sev = card.getAttribute('data-severity');
+ if (!sev) return;
+ const sevEl = document.getElementById('vulnerability-severity-filter');
+ const current = sevEl ? sevEl.value : vulnerabilityFilters.severity;
+ applyVulnerabilitySeverityFilter(current === sev ? '' : sev);
+}
+
+function onVulnerabilityStatCardKeydown(ev) {
+ if (ev.key !== 'Enter' && ev.key !== ' ') return;
+ const card = ev.target.closest('.stat-card.is-clickable');
+ if (!card || !card.contains(ev.target)) return;
+ ev.preventDefault();
+ card.click();
+}
+
+function applyVulnerabilitySeverityFilter(severity) {
+ const sevEl = document.getElementById('vulnerability-severity-filter');
+ if (sevEl) sevEl.value = severity || '';
+ applyVulnerabilityFilters();
+}
+
+function readVulnerabilityFiltersFromForm() {
+ vulnerabilityFilters.id = (document.getElementById('vulnerability-id-filter')?.value || '').trim();
+ vulnerabilityFilters.conversation_id = (document.getElementById('vulnerability-conversation-filter')?.value || '').trim();
+ vulnerabilityFilters.task_id = (document.getElementById('vulnerability-task-filter')?.value || '').trim();
+ vulnerabilityFilters.conversation_tag = (document.getElementById('vulnerability-conversation-tag-filter')?.value || '').trim();
+ vulnerabilityFilters.task_tag = (document.getElementById('vulnerability-task-tag-filter')?.value || '').trim();
+ vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter')?.value || '';
+ vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter')?.value || '';
+ return vulnerabilityFilters;
+}
+
+function hasVulnerabilityAdvancedFiltersActive() {
+ const f = vulnerabilityFilters;
+ return Boolean(f.conversation_id || f.task_id || f.conversation_tag || f.task_tag);
+}
+
+function hasAnyVulnerabilityFilterActive() {
+ const f = vulnerabilityFilters;
+ return Boolean(
+ f.id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status
+ );
+}
+
+function applyVulnerabilityFilters() {
+ readVulnerabilityFiltersFromForm();
+ vulnerabilityPagination.currentPage = 1;
+ syncVulnerabilityStatCardActiveState();
+ updateVulnerabilityLocationHashFromFilters();
+ updateVulnerabilityFilterPanelState();
+ renderVulnerabilityFilterChips();
+ loadVulnerabilityStats();
+ loadVulnerabilities();
+}
+
+function updateVulnerabilityLocationHashFromFilters() {
+ const hash = window.location.hash.slice(1);
+ const hashParts = hash.split('?');
+ if (hashParts[0] !== 'vulnerabilities') return;
+ const params = new URLSearchParams(hashParts.length >= 2 ? hashParts.slice(1).join('?') : '');
+ const f = vulnerabilityFilters;
+ const pairs = [
+ ['id', f.id],
+ ['conversation_id', f.conversation_id],
+ ['task_id', f.task_id],
+ ['conversation_tag', f.conversation_tag],
+ ['task_tag', f.task_tag],
+ ['severity', f.severity],
+ ['status', f.status]
+ ];
+ pairs.forEach(function (pair) {
+ if (pair[1]) {
+ params.set(pair[0], pair[1]);
+ } else {
+ params.delete(pair[0]);
+ }
+ });
+ const qs = params.toString();
+ const newHash = qs ? 'vulnerabilities?' + qs : 'vulnerabilities';
+ if (window.location.hash.slice(1) === newHash) return;
+ const newFull = '#' + newHash;
+ if (typeof history.replaceState === 'function') {
+ history.replaceState(null, '', window.location.pathname + window.location.search + newFull);
+ } else {
+ window.location.hash = newHash;
+ }
+}
+
+function toggleVulnerabilityAdvancedFilters(ev) {
+ if (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+ const toggleBtn = document.getElementById('vulnerability-advanced-toggle');
+ if (!toggleBtn) return;
+ const expanded = toggleBtn.getAttribute('aria-expanded') === 'true';
+ setVulnerabilityAdvancedFiltersOpen(!expanded, true);
+}
+window.toggleVulnerabilityAdvancedFilters = toggleVulnerabilityAdvancedFilters;
+
+function initVulnerabilityFilterPanel() {
+ const panel = document.getElementById('vulnerability-filter-panel');
+ if (!panel) return;
+
+ if (vulnerabilityFilterPanelBound) {
+ updateVulnerabilityFilterPanelState();
+ return;
+ }
+ vulnerabilityFilterPanelBound = true;
+
+ let savedOpen = false;
+ try {
+ savedOpen = localStorage.getItem(VULNERABILITY_ADVANCED_OPEN_KEY) === 'true';
+ } catch (e) { /* ignore */ }
+ setVulnerabilityAdvancedFiltersOpen(savedOpen, false);
+
+ const stEl = document.getElementById('vulnerability-status-filter');
+ if (stEl) stEl.addEventListener('change', applyVulnerabilityFilters);
+
+ const textIds = [
+ 'vulnerability-id-filter',
+ 'vulnerability-conversation-filter',
+ 'vulnerability-task-filter',
+ 'vulnerability-conversation-tag-filter',
+ 'vulnerability-task-tag-filter'
+ ];
+ textIds.forEach(function (id) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ el.addEventListener('keydown', function (ev) {
+ if (ev.key === 'Enter') {
+ ev.preventDefault();
+ applyVulnerabilityFilters();
+ }
+ });
+ });
+
+ bindVulnerabilityFilterTypeaheads();
+}
+
+function setVulnerabilityAdvancedFiltersOpen(open, persist) {
+ const toggleBtn = document.getElementById('vulnerability-advanced-toggle');
+ const advanced = document.getElementById('vulnerability-advanced-filters');
+ const wrap = document.querySelector('#vulnerability-filter-panel .vulnerability-filter-advanced-wrap');
+ if (!toggleBtn || !advanced) return;
+ toggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
+ advanced.hidden = !open;
+ advanced.classList.toggle('is-open', open);
+ if (wrap) wrap.classList.toggle('is-expanded', open);
+ if (persist) {
+ try {
+ localStorage.setItem(VULNERABILITY_ADVANCED_OPEN_KEY, open ? 'true' : 'false');
+ } catch (e) { /* ignore */ }
+ }
+}
+
+function countVulnerabilityAdvancedFiltersActive() {
+ const f = vulnerabilityFilters;
+ let n = 0;
+ if (f.conversation_id) n++;
+ if (f.task_id) n++;
+ if (f.conversation_tag) n++;
+ if (f.task_tag) n++;
+ return n;
+}
+
+function updateVulnerabilityAdvancedBadge() {
+ const badge = document.getElementById('vulnerability-advanced-badge');
+ if (!badge) return;
+ readVulnerabilityFiltersFromForm();
+ const n = countVulnerabilityAdvancedFiltersActive();
+ if (n > 0) {
+ badge.hidden = false;
+ badge.textContent = '(' + n + ')';
+ badge.setAttribute('aria-label', String(n));
+ } else {
+ badge.hidden = true;
+ badge.textContent = '';
+ badge.removeAttribute('aria-label');
+ }
+}
+
+function updateVulnerabilityFilterPanelState() {
+ const panel = document.getElementById('vulnerability-filter-panel');
+ if (!panel) return;
+ readVulnerabilityFiltersFromForm();
+ panel.classList.toggle('is-filtered', hasAnyVulnerabilityFilterActive());
+ updateVulnerabilityAdvancedBadge();
+}
+
+function formatVulnerabilityFilterChipValue(key, value) {
+ if (key === 'severity') return vulnSeverityLabel(value);
+ if (key === 'status') return vulnStatusLabel(value);
+ return value;
+}
+
+function renderVulnerabilityFilterChips() {
+ const wrap = document.getElementById('vulnerability-filter-chips');
+ const list = document.getElementById('vulnerability-filter-chips-list');
+ if (!wrap || !list) return;
+
+ readVulnerabilityFiltersFromForm();
+ const chips = [];
+ VULN_FILTER_CHIP_FIELDS.forEach(function (field) {
+ const val = vulnerabilityFilters[field.key];
+ if (!val) return;
+ const label = field.labelKey ? vulnT(field.labelKey) : '';
+ const displayVal = formatVulnerabilityFilterChipValue(field.key, val);
+ const text = label ? label + ': ' + displayVal : displayVal;
+ chips.push({ key: field.key, text: text });
+ });
+
+ if (!chips.length) {
+ wrap.hidden = true;
+ list.innerHTML = '';
+ return;
+ }
+
+ wrap.hidden = false;
+ const removeLabel = vulnT('vulnerabilityPage.chipRemove');
+ list.innerHTML = chips.map(function (chip) {
+ return (
+ ''
+ );
+ }).join('');
+
+ list.querySelectorAll('.vulnerability-filter-chip').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ const key = btn.getAttribute('data-filter-key');
+ if (key) removeVulnerabilityFilterByKey(key);
+ });
+ });
+}
+
+function removeVulnerabilityFilterByKey(key) {
+ const map = {
+ id: 'vulnerability-id-filter',
+ conversation_id: 'vulnerability-conversation-filter',
+ task_id: 'vulnerability-task-filter',
+ conversation_tag: 'vulnerability-conversation-tag-filter',
+ task_tag: 'vulnerability-task-tag-filter',
+ severity: 'vulnerability-severity-filter',
+ status: 'vulnerability-status-filter'
+ };
+ const elId = map[key];
+ if (elId) {
+ const el = document.getElementById(elId);
+ if (el) el.value = '';
+ }
+ if (Object.prototype.hasOwnProperty.call(vulnerabilityFilters, key)) {
+ vulnerabilityFilters[key] = '';
+ }
+ applyVulnerabilityFilters();
+}
+
+async function loadVulnerabilityFilterOptions() {
+ if (typeof apiFetch === 'undefined') return;
+ try {
+ const response = await apiFetch('/api/vulnerabilities/filter-options');
+ if (!response.ok) return;
+ vulnerabilityFilterOptionsCache = await response.json();
+ populateVulnerabilityDatalist(
+ 'vulnerability-conversation-tag-suggestions',
+ vulnerabilityFilterOptionsCache.conversation_tags,
+ { max: 20 }
+ );
+ populateVulnerabilityDatalist(
+ 'vulnerability-task-tag-suggestions',
+ vulnerabilityFilterOptionsCache.task_tags,
+ { max: 20 }
+ );
+ clearVulnerabilityDatalist('vulnerability-conversation-suggestions');
+ clearVulnerabilityDatalist('vulnerability-task-suggestions');
+ } catch (e) {
+ console.warn('加载漏洞筛选建议失败', e);
+ }
+}
+
+function clearVulnerabilityDatalist(listId) {
+ const list = document.getElementById(listId);
+ if (list) list.innerHTML = '';
+}
+
+function populateVulnerabilityDatalist(listId, values, opts) {
+ const list = document.getElementById(listId);
+ if (!list || !Array.isArray(values)) return;
+ const max = (opts && opts.max) || VULNERABILITY_DATALIST_MAX;
+ const seen = new Set();
+ const unique = [];
+ values.forEach(function (v) {
+ const s = String(v || '').trim();
+ if (!s || seen.has(s)) return;
+ seen.add(s);
+ unique.push(s);
+ if (unique.length >= max) return;
+ });
+ list.innerHTML = unique.slice(0, max).map(function (v) {
+ return '';
+ }).join('');
+}
+
+function filterVulnerabilitySuggestionPool(pool, query) {
+ if (!Array.isArray(pool) || !query) return [];
+ const q = query.toLowerCase();
+ const out = [];
+ for (let i = 0; i < pool.length && out.length < VULNERABILITY_DATALIST_MAX; i++) {
+ const s = String(pool[i] || '').trim();
+ if (s && s.toLowerCase().indexOf(q) !== -1) out.push(s);
+ }
+ return out;
+}
+
+function updateVulnerabilityTypeaheadDatalist(inputId, listId, poolKey) {
+ const el = document.getElementById(inputId);
+ if (!el || !vulnerabilityFilterOptionsCache) return;
+ const q = el.value.trim();
+ if (q.length < VULNERABILITY_DATALIST_MIN_QUERY) {
+ clearVulnerabilityDatalist(listId);
+ return;
+ }
+ let pool = vulnerabilityFilterOptionsCache[poolKey] || [];
+ if (poolKey === 'task_ids') {
+ pool = (vulnerabilityFilterOptionsCache.task_ids || []).concat(vulnerabilityFilterOptionsCache.queue_ids || []);
+ }
+ populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(pool, q));
+}
+
+function bindVulnerabilityFilterTypeaheads() {
+ const pairs = [
+ { inputId: 'vulnerability-conversation-filter', listId: 'vulnerability-conversation-suggestions', poolKey: 'conversation_ids' },
+ { inputId: 'vulnerability-task-filter', listId: 'vulnerability-task-suggestions', poolKey: 'task_ids' }
+ ];
+ pairs.forEach(function (pair) {
+ const el = document.getElementById(pair.inputId);
+ if (!el) return;
+ el.addEventListener('input', function () {
+ updateVulnerabilityTypeaheadDatalist(pair.inputId, pair.listId, pair.poolKey);
+ });
+ el.addEventListener('blur', function () {
+ setTimeout(function () { clearVulnerabilityDatalist(pair.listId); }, 150);
+ });
+ });
+
+ ['vulnerability-conversation-tag-filter', 'vulnerability-task-tag-filter'].forEach(function (inputId) {
+ const el = document.getElementById(inputId);
+ if (!el) return;
+ el.addEventListener('focus', function () {
+ if (!vulnerabilityFilterOptionsCache) return;
+ const listId = inputId === 'vulnerability-conversation-tag-filter'
+ ? 'vulnerability-conversation-tag-suggestions'
+ : 'vulnerability-task-tag-suggestions';
+ const key = inputId === 'vulnerability-conversation-tag-filter' ? 'conversation_tags' : 'task_tags';
+ const q = el.value.trim();
+ if (q.length >= VULNERABILITY_DATALIST_MIN_QUERY) {
+ populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(vulnerabilityFilterOptionsCache[key], q), { max: 20 });
+ }
+ });
+ });
+}
+
+function syncVulnerabilityStatCardActiveState() {
+ const sevEl = document.getElementById('vulnerability-severity-filter');
+ const sev = (sevEl && sevEl.value) || vulnerabilityFilters.severity || '';
+ const root = document.getElementById('vulnerability-stat-cards');
+ if (!root) return;
+ root.querySelectorAll('.stat-card.is-clickable').forEach(function (card) {
+ if (card.classList.contains('stat-card-total')) {
+ card.classList.toggle('is-active', !sev);
+ card.setAttribute('aria-pressed', sev ? 'false' : 'true');
+ } else {
+ const cardSev = card.getAttribute('data-severity');
+ const active = Boolean(sev && cardSev === sev);
+ card.classList.toggle('is-active', active);
+ card.setAttribute('aria-pressed', active ? 'true' : 'false');
+ }
+ });
+}
+
+function updateVulnerabilityStatStackedBar(bySeverity, total) {
+ const bar = document.getElementById('stat-stacked-bar');
+ if (!bar) return;
+ const segs = bar.querySelectorAll('.stat-stacked-seg');
+ if (!total) {
+ bar.classList.add('is-empty');
+ segs.forEach(function (seg) {
+ seg.style.flex = '0 0 0';
+ seg.style.display = 'none';
+ });
+ return;
+ }
+ bar.classList.remove('is-empty');
+ segs.forEach(function (seg) {
+ const sev = seg.getAttribute('data-sev');
+ const count = bySeverity[sev] || 0;
+ if (count <= 0) {
+ seg.style.display = 'none';
+ seg.style.flex = '0 0 0';
+ return;
+ }
+ seg.style.display = '';
+ const pct = Math.max((count / total) * 100, 0);
+ seg.style.flex = '1 1 ' + pct + '%';
+ });
+}
+
// 加载漏洞统计
async function loadVulnerabilityStats() {
try {
@@ -169,15 +643,33 @@ function updateVulnerabilityStats(stats) {
by_status: {}
};
}
-
- document.getElementById('stat-total').textContent = stats.total || 0;
-
+
+ const total = stats.total || 0;
const bySeverity = stats.by_severity || {};
- document.getElementById('stat-critical').textContent = bySeverity.critical || 0;
- document.getElementById('stat-high').textContent = bySeverity.high || 0;
- document.getElementById('stat-medium').textContent = bySeverity.medium || 0;
- document.getElementById('stat-low').textContent = bySeverity.low || 0;
- document.getElementById('stat-info').textContent = bySeverity.info || 0;
+
+ const totalEl = document.getElementById('stat-total');
+ if (totalEl) {
+ totalEl.textContent = String(total);
+ totalEl.classList.toggle('is-zero', total === 0);
+ }
+
+ VULN_STAT_SEVERITIES.forEach(function (sev) {
+ const count = bySeverity[sev] || 0;
+ const valEl = document.getElementById('stat-' + sev);
+ const pctEl = document.getElementById('stat-' + sev + '-pct');
+ if (valEl) {
+ valEl.textContent = String(count);
+ valEl.classList.toggle('is-zero', count === 0);
+ }
+ if (pctEl) {
+ const pct = total > 0 ? Math.round((count / total) * 100) : 0;
+ pctEl.textContent = pct + '%';
+ pctEl.setAttribute('aria-hidden', total === 0 ? 'true' : 'false');
+ }
+ });
+
+ updateVulnerabilityStatStackedBar(bySeverity, total);
+ syncVulnerabilityStatCardActiveState();
}
// 加载漏洞列表
@@ -591,32 +1083,26 @@ function closeVulnerabilityModal() {
currentVulnerabilityId = null;
}
-// 筛选漏洞
+// 筛选漏洞(应用当前表单条件)
function filterVulnerabilities() {
- vulnerabilityFilters.id = document.getElementById('vulnerability-id-filter').value.trim();
- vulnerabilityFilters.conversation_id = document.getElementById('vulnerability-conversation-filter').value.trim();
- vulnerabilityFilters.task_id = document.getElementById('vulnerability-task-filter').value.trim();
- vulnerabilityFilters.conversation_tag = document.getElementById('vulnerability-conversation-tag-filter').value.trim();
- vulnerabilityFilters.task_tag = document.getElementById('vulnerability-task-tag-filter').value.trim();
- vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value;
- vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value;
-
- // 重置到第一页
- vulnerabilityPagination.currentPage = 1;
-
- loadVulnerabilityStats();
- loadVulnerabilities();
+ applyVulnerabilityFilters();
}
// 清除筛选
function clearVulnerabilityFilters() {
- document.getElementById('vulnerability-id-filter').value = '';
- document.getElementById('vulnerability-conversation-filter').value = '';
- document.getElementById('vulnerability-task-filter').value = '';
- document.getElementById('vulnerability-conversation-tag-filter').value = '';
- document.getElementById('vulnerability-task-tag-filter').value = '';
- document.getElementById('vulnerability-severity-filter').value = '';
- document.getElementById('vulnerability-status-filter').value = '';
+ const fields = [
+ 'vulnerability-id-filter',
+ 'vulnerability-conversation-filter',
+ 'vulnerability-task-filter',
+ 'vulnerability-conversation-tag-filter',
+ 'vulnerability-task-tag-filter',
+ 'vulnerability-severity-filter',
+ 'vulnerability-status-filter'
+ ];
+ fields.forEach(function (id) {
+ const el = document.getElementById(id);
+ if (el) el.value = '';
+ });
vulnerabilityFilters = {
id: '',
@@ -628,11 +1114,7 @@ function clearVulnerabilityFilters() {
status: ''
};
- // 重置到第一页
- vulnerabilityPagination.currentPage = 1;
-
- loadVulnerabilityStats();
- loadVulnerabilities();
+ applyVulnerabilityFilters();
}
// 刷新漏洞
@@ -908,6 +1390,7 @@ window.onclick = function(event) {
document.addEventListener('languagechange', function () {
const page = document.getElementById('page-vulnerabilities');
if (page && page.classList.contains('active')) {
+ renderVulnerabilityFilterChips();
loadVulnerabilities();
}
});
diff --git a/web/templates/index.html b/web/templates/index.html
index 85248cd7..2c3c5cbc 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -1082,10 +1082,13 @@