mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-18 14:04:52 +02:00
Add files via upload
This commit is contained in:
+1090
-19
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+378
-46
@@ -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;
|
||||
|
||||
+518
-35
@@ -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 (
|
||||
'<button type="button" class="vulnerability-filter-chip" role="listitem" data-filter-key="' +
|
||||
escapeHtml(chip.key) + '" title="' + escapeHtml(removeLabel) + '">' +
|
||||
'<span>' + escapeHtml(chip.text) + '</span>' +
|
||||
'<span class="vulnerability-filter-chip-remove" aria-hidden="true">×</span>' +
|
||||
'</button>'
|
||||
);
|
||||
}).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 '<option value="' + escapeHtml(v) + '"></option>';
|
||||
}).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();
|
||||
}
|
||||
});
|
||||
|
||||
+97
-59
@@ -1082,10 +1082,13 @@
|
||||
<div class="page-content">
|
||||
<div class="monitor-sections">
|
||||
<section class="monitor-section monitor-overview">
|
||||
<div class="section-header">
|
||||
<h3 data-i18n="mcp.execStats">执行统计</h3>
|
||||
<div class="section-header monitor-stats-section-header">
|
||||
<div class="monitor-stats-header-text">
|
||||
<h3 data-i18n="mcp.execStats">执行统计</h3>
|
||||
<p id="monitor-stats-subtitle" class="monitor-stats-subtitle" hidden></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="monitor-stats" class="monitor-stats-grid">
|
||||
<div id="monitor-stats" class="mcp-exec-stats-root">
|
||||
<div class="monitor-empty" data-i18n="mcpMonitor.loading">加载中...</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1385,89 +1388,124 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="vulnerability.title">漏洞管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
||||
<button class="btn-secondary" onclick="refreshVulnerabilities()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-primary" onclick="showAddVulnerabilityModal()" data-i18n="vulnerability.addVuln">添加漏洞</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<!-- 统计看板 -->
|
||||
<!-- 统计看板:点击卡片筛选严重度,与下方下拉/地址栏 hash 同步 -->
|
||||
<div class="vulnerability-dashboard" id="vulnerability-dashboard">
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-card">
|
||||
<div class="dashboard-stats" id="vulnerability-stat-cards" role="group" aria-label="漏洞严重度统计">
|
||||
<div class="stat-card stat-card-total is-clickable is-active" data-severity="" role="button" tabindex="0"
|
||||
data-i18n="vulnerabilityPage.statClickAll" data-i18n-attr="title" title="查看全部(清除严重度筛选)">
|
||||
<div class="stat-label" data-i18n="vulnerabilityPage.statTotal">总漏洞数</div>
|
||||
<div class="stat-value" id="stat-total">-</div>
|
||||
<div class="stat-stacked-bar" id="stat-stacked-bar" aria-hidden="true">
|
||||
<span class="stat-stacked-seg critical" data-sev="critical"></span>
|
||||
<span class="stat-stacked-seg high" data-sev="high"></span>
|
||||
<span class="stat-stacked-seg medium" data-sev="medium"></span>
|
||||
<span class="stat-stacked-seg low" data-sev="low"></span>
|
||||
<span class="stat-stacked-seg info" data-sev="info"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-critical">
|
||||
<div class="stat-card stat-critical is-clickable" data-severity="critical" role="button" tabindex="0"
|
||||
data-i18n="vulnerabilityPage.statClickFilter" data-i18n-attr="title" title="点击按此严重度筛选;再次点击清除">
|
||||
<div class="stat-label" data-i18n="dashboard.severityCritical">严重</div>
|
||||
<div class="stat-value" id="stat-critical">-</div>
|
||||
<div class="stat-pct" id="stat-critical-pct" aria-hidden="true">—</div>
|
||||
</div>
|
||||
<div class="stat-card stat-high">
|
||||
<div class="stat-card stat-high is-clickable" data-severity="high" role="button" tabindex="0"
|
||||
data-i18n="vulnerabilityPage.statClickFilter" data-i18n-attr="title" title="点击按此严重度筛选;再次点击清除">
|
||||
<div class="stat-label" data-i18n="dashboard.severityHigh">高危</div>
|
||||
<div class="stat-value" id="stat-high">-</div>
|
||||
<div class="stat-pct" id="stat-high-pct" aria-hidden="true">—</div>
|
||||
</div>
|
||||
<div class="stat-card stat-medium">
|
||||
<div class="stat-card stat-medium is-clickable" data-severity="medium" role="button" tabindex="0"
|
||||
data-i18n="vulnerabilityPage.statClickFilter" data-i18n-attr="title" title="点击按此严重度筛选;再次点击清除">
|
||||
<div class="stat-label" data-i18n="dashboard.severityMedium">中危</div>
|
||||
<div class="stat-value" id="stat-medium">-</div>
|
||||
<div class="stat-pct" id="stat-medium-pct" aria-hidden="true">—</div>
|
||||
</div>
|
||||
<div class="stat-card stat-low">
|
||||
<div class="stat-card stat-low is-clickable" data-severity="low" role="button" tabindex="0"
|
||||
data-i18n="vulnerabilityPage.statClickFilter" data-i18n-attr="title" title="点击按此严重度筛选;再次点击清除">
|
||||
<div class="stat-label" data-i18n="dashboard.severityLow">低危</div>
|
||||
<div class="stat-value" id="stat-low">-</div>
|
||||
<div class="stat-pct" id="stat-low-pct" aria-hidden="true">—</div>
|
||||
</div>
|
||||
<div class="stat-card stat-info">
|
||||
<div class="stat-card stat-info is-clickable" data-severity="info" role="button" tabindex="0"
|
||||
data-i18n="vulnerabilityPage.statClickFilter" data-i18n-attr="title" title="点击按此严重度筛选;再次点击清除">
|
||||
<div class="stat-label" data-i18n="dashboard.severityInfo">信息</div>
|
||||
<div class="stat-value" id="stat-info">-</div>
|
||||
<div class="stat-pct" id="stat-info-pct" aria-hidden="true">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="vulnerability-controls">
|
||||
<div class="vulnerability-filters">
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.vulnId">漏洞ID</span>
|
||||
<input type="text" id="vulnerability-id-filter" data-i18n="vulnerabilityPage.searchVulnId" data-i18n-attr="placeholder" placeholder="搜索漏洞ID" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.conversationId">会话ID</span>
|
||||
<input type="text" id="vulnerability-conversation-filter" data-i18n="vulnerabilityPage.filterConversation" data-i18n-attr="placeholder" placeholder="筛选特定会话" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.taskOrQueueId">任务ID/队列ID</span>
|
||||
<input type="text" id="vulnerability-task-filter" data-i18n="vulnerabilityPage.filterTaskOrQueue" data-i18n-attr="placeholder" placeholder="筛选任务ID或队列ID" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.conversationTag">对话标签</span>
|
||||
<input type="text" id="vulnerability-conversation-tag-filter" data-i18n="vulnerabilityPage.filterConversationTag" data-i18n-attr="placeholder" placeholder="筛选对话标签" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.taskTag">任务标签</span>
|
||||
<input type="text" id="vulnerability-task-tag-filter" data-i18n="vulnerabilityPage.filterTaskTag" data-i18n-attr="placeholder" placeholder="筛选任务标签" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.severity">严重程度</span>
|
||||
<select id="vulnerability-severity-filter">
|
||||
<option value="" data-i18n="knowledgePage.all">全部</option>
|
||||
<option value="critical" data-i18n="dashboard.severityCritical">严重</option>
|
||||
<option value="high" data-i18n="dashboard.severityHigh">高危</option>
|
||||
<option value="medium" data-i18n="dashboard.severityMedium">中危</option>
|
||||
<option value="low" data-i18n="dashboard.severityLow">低危</option>
|
||||
<option value="info" data-i18n="dashboard.severityInfo">信息</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.status">状态</span>
|
||||
<select id="vulnerability-status-filter">
|
||||
<option value="" data-i18n="knowledgePage.all">全部</option>
|
||||
<option value="open" data-i18n="vulnerabilityPage.statusOpen">待处理</option>
|
||||
<option value="confirmed" data-i18n="vulnerabilityPage.statusConfirmed">已确认</option>
|
||||
<option value="fixed" data-i18n="vulnerabilityPage.statusFixed">已修复</option>
|
||||
<option value="false_positive" data-i18n="vulnerabilityPage.statusFalsePositive">误报</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn-secondary" onclick="filterVulnerabilities()" data-i18n="vulnerabilityPage.filter">筛选</button>
|
||||
<button class="btn-secondary" onclick="clearVulnerabilityFilters()" data-i18n="vulnerabilityPage.clear">清除</button>
|
||||
<button class="btn-primary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
||||
<!-- 筛选 -->
|
||||
<div class="vulnerability-controls" id="vulnerability-filter-panel">
|
||||
<div class="vulnerability-filter-toolbar">
|
||||
<div class="vulnerability-filter-primary">
|
||||
<label class="vulnerability-filter-field vulnerability-filter-field--grow">
|
||||
<span class="sr-only" data-i18n="vulnerabilityPage.vulnId">漏洞ID</span>
|
||||
<input type="search" id="vulnerability-id-filter" autocomplete="off"
|
||||
data-i18n="vulnerabilityPage.searchVulnId" data-i18n-attr="placeholder" placeholder="搜索漏洞 ID,回车筛选" />
|
||||
</label>
|
||||
<label class="vulnerability-filter-field vulnerability-filter-field--status">
|
||||
<span class="sr-only" data-i18n="vulnerabilityPage.status">状态</span>
|
||||
<select id="vulnerability-status-filter" data-i18n-attr="title" title="状态">
|
||||
<option value="" data-i18n="knowledgePage.all">全部状态</option>
|
||||
<option value="open" data-i18n="vulnerabilityPage.statusOpen">待处理</option>
|
||||
<option value="confirmed" data-i18n="vulnerabilityPage.statusConfirmed">已确认</option>
|
||||
<option value="fixed" data-i18n="vulnerabilityPage.statusFixed">已修复</option>
|
||||
<option value="false_positive" data-i18n="vulnerabilityPage.statusFalsePositive">误报</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="vulnerability-filter-clear-btn" onclick="clearVulnerabilityFilters()" data-i18n="vulnerabilityPage.clear">清除</button>
|
||||
</div>
|
||||
<select id="vulnerability-severity-filter" class="vulnerability-severity-sync" hidden aria-hidden="true" tabindex="-1">
|
||||
<option value=""></option>
|
||||
<option value="critical">critical</option>
|
||||
<option value="high">high</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="low">low</option>
|
||||
<option value="info">info</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="vulnerability-filter-advanced-wrap">
|
||||
<button type="button" class="vulnerability-filter-advanced-toggle" id="vulnerability-advanced-toggle"
|
||||
aria-expanded="false" aria-controls="vulnerability-advanced-filters"
|
||||
onclick="toggleVulnerabilityAdvancedFilters(event)">
|
||||
<span class="vulnerability-filter-advanced-chevron" aria-hidden="true"></span>
|
||||
<span data-i18n="vulnerabilityPage.advancedFilters">高级筛选</span>
|
||||
<span class="vulnerability-filter-advanced-badge" id="vulnerability-advanced-badge" hidden></span>
|
||||
</button>
|
||||
<div class="vulnerability-filter-advanced" id="vulnerability-advanced-filters" hidden>
|
||||
<label class="vulnerability-filter-field">
|
||||
<span data-i18n="vulnerabilityPage.conversationId">会话 ID</span>
|
||||
<input type="text" id="vulnerability-conversation-filter" list="vulnerability-conversation-suggestions" placeholder="回车筛选" />
|
||||
</label>
|
||||
<label class="vulnerability-filter-field">
|
||||
<span data-i18n="vulnerabilityPage.taskOrQueueId">任务 / 队列 ID</span>
|
||||
<input type="text" id="vulnerability-task-filter" list="vulnerability-task-suggestions" placeholder="回车筛选" />
|
||||
</label>
|
||||
<label class="vulnerability-filter-field">
|
||||
<span data-i18n="vulnerabilityPage.conversationTag">对话标签</span>
|
||||
<input type="text" id="vulnerability-conversation-tag-filter" list="vulnerability-conversation-tag-suggestions" placeholder="回车筛选" />
|
||||
</label>
|
||||
<label class="vulnerability-filter-field">
|
||||
<span data-i18n="vulnerabilityPage.taskTag">任务标签</span>
|
||||
<input type="text" id="vulnerability-task-tag-filter" list="vulnerability-task-tag-suggestions" placeholder="回车筛选" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vulnerability-filter-chips" id="vulnerability-filter-chips" hidden>
|
||||
<div class="vulnerability-filter-chips-list" id="vulnerability-filter-chips-list" role="list"></div>
|
||||
</div>
|
||||
<datalist id="vulnerability-conversation-suggestions"></datalist>
|
||||
<datalist id="vulnerability-task-suggestions"></datalist>
|
||||
<datalist id="vulnerability-conversation-tag-suggestions"></datalist>
|
||||
<datalist id="vulnerability-task-tag-suggestions"></datalist>
|
||||
</div>
|
||||
|
||||
<!-- 漏洞列表 -->
|
||||
@@ -3515,7 +3553,7 @@
|
||||
<script src="/static/js/terminal.js"></script>
|
||||
<script src="/static/js/knowledge.js"></script>
|
||||
<script src="/static/js/skills.js"></script>
|
||||
<script src="/static/js/vulnerability.js?v=7"></script>
|
||||
<script src="/static/js/vulnerability.js?v=12"></script>
|
||||
<script src="/static/js/webshell.js"></script>
|
||||
<script src="/static/js/chat-files.js"></script>
|
||||
<script src="/static/js/tasks.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user