From bf0ce33e3f4fc28ec11888ec9422507f9492666f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:36:45 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 71 ++++++++++++ web/static/i18n/en-US.json | 6 ++ web/static/i18n/zh-CN.json | 6 ++ web/static/js/dashboard.js | 4 +- web/static/js/projects.js | 3 +- web/static/js/settings.js | 190 +++++++++++++++++++++++---------- web/static/js/vulnerability.js | 3 +- web/templates/index.html | 3 + 8 files changed, 228 insertions(+), 58 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 8f7def4c..7dcad05e 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -5596,6 +5596,66 @@ header { animation: mcpHighlight 2s ease-out; } +.external-mcp-item.clickable { + cursor: pointer; +} + +.external-mcp-item.selected { + border-color: var(--accent-color); + box-shadow: 0 0 0 1px var(--accent-color); +} + +.tool-item.highlight { + animation: mcpHighlight 2s ease-out; +} + +.tools-source-filter-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 10px; + border-radius: 999px; + background: rgba(var(--accent-rgb, 59, 130, 246), 0.12); + border: 1px solid var(--accent-color); + color: var(--text-primary); + font-size: 0.8125rem; + line-height: 1.2; +} + +.tools-source-filter-clear { + display: inline-flex; + align-items: center; + justify-content: center; + width: auto; + min-width: 0; + height: auto; + padding: 0; + margin: 0; + border: none !important; + border-radius: 0; + background: transparent !important; + box-shadow: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 1rem; + line-height: 1; + flex-shrink: 0; +} + +.tools-actions .tools-source-filter-clear { + padding: 0; + border: none; + background: transparent; +} + +.tools-source-filter-clear:hover, +.tools-actions .tools-source-filter-clear:hover { + background: transparent !important; + border: none !important; + box-shadow: none; + color: var(--text-primary); +} + @keyframes mcpHighlight { 0% { box-shadow: 0 0 0 3px var(--accent-color); border-color: var(--accent-color); } 100% { box-shadow: none; border-color: var(--border-color); } @@ -16636,6 +16696,12 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { border-color: rgba(148, 163, 184, 0.25); } +.dashboard-recent-vuln-status.st-ignored { + background: rgba(108, 117, 125, 0.12); + color: #868e96; + border-color: rgba(108, 117, 125, 0.22); +} + @media (max-width: 720px) { .dashboard-recent-vuln-item { grid-template-columns: 56px minmax(0, 1fr) auto 8.25rem; @@ -18710,6 +18776,11 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { color: #dc3545; } +.status-badge.status-ignored { + background: rgba(108, 117, 125, 0.12); + color: #868e96; +} + .vulnerability-date { font-size: 0.75rem; color: var(--text-muted); diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 8b409896..56134afe 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -205,6 +205,7 @@ "statusConfirmed": "Confirmed", "statusFixed": "Fixed", "statusFalsePositive": "False positive", + "statusIgnored": "Ignored", "fixRate": "Fix rate", "dataStale": "Data may be stale — please refresh", "recommendedActions": "Recommended Actions", @@ -960,6 +961,9 @@ "externalBadge": "External", "externalFrom": "External ({{name}})", "externalToolFrom": "External MCP - Source: {{name}}", + "clickToViewTools": "Click to view tools from {{name}}", + "filterBySource": "Source: {{name}}", + "clearSourceFilter": "Clear source filter", "noDescription": "No description", "paginationInfo": "{{start}}-{{end}} of {{total}} tools", "perPage": "Per page:", @@ -1805,6 +1809,7 @@ "statusConfirmed": "Confirmed", "statusFixed": "Fixed", "statusFalsePositive": "False positive", + "statusIgnored": "Ignored", "searchVulnId": "Search vuln ID", "searchKeyword": "Search title, description, type, target…", "searchKeywordShort": "Keyword", @@ -2467,6 +2472,7 @@ "statusConfirmed": "Confirmed", "statusFixed": "Fixed", "statusFalsePositive": "False positive", + "statusIgnored": "Ignored", "type": "Vulnerability type", "typePlaceholder": "e.g. SQL injection, XSS, CSRF", "target": "Target", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index ad18109e..372249e9 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -198,6 +198,7 @@ "statusConfirmed": "已确认", "statusFixed": "已修复", "statusFalsePositive": "误报", + "statusIgnored": "已忽略", "fixRate": "修复率", "dataStale": "数据可能已过期,请手动刷新", "recommendedActions": "推荐操作", @@ -948,6 +949,9 @@ "externalBadge": "外部", "externalFrom": "外部 ({{name}})", "externalToolFrom": "外部MCP工具 - 来源:{{name}}", + "clickToViewTools": "点击查看 {{name}} 的工具", + "filterBySource": "来源: {{name}}", + "clearSourceFilter": "清除来源筛选", "noDescription": "无描述", "paginationInfo": "显示 {{start}}-{{end}} / 共 {{total}} 个工具", "perPage": "每页:", @@ -1793,6 +1797,7 @@ "statusConfirmed": "已确认", "statusFixed": "已修复", "statusFalsePositive": "误报", + "statusIgnored": "已忽略", "searchVulnId": "搜索漏洞 ID", "searchKeyword": "搜索标题、描述、类型、目标…", "searchKeywordShort": "关键词", @@ -2455,6 +2460,7 @@ "statusConfirmed": "已确认", "statusFixed": "已修复", "statusFalsePositive": "误报", + "statusIgnored": "已忽略", "type": "漏洞类型", "typePlaceholder": "如:SQL注入、XSS、CSRF等", "target": "目标", diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index fdd69ce1..2e1f4ee8 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -131,7 +131,7 @@ async function refreshDashboard() { openVulnQuery('low'), // 拉取 MCP 工具的「配置总数」用于「能力总览」(区别于 monitor/stats 的「有调用记录」)。 // 仅取 total 字段,page_size=1 减少传输;total 已涵盖内部 + 外部 MCP + 直接注册的工具。 - fetchJson('/api/config/tools?page=1&page_size=1'), + fetchJson('/api/config/tools?page=1&page_size=1&include_external=false'), // HITL 待审批:用于「需要立即处理」告警条 + 推荐操作 fetchJson('/api/hitl/pending'), // 通知摘要:since=0 拿最新一批,limit 控制大小;用于「最近事件」内联展示 @@ -1459,6 +1459,7 @@ function statusKey(s) { if (s === 'fixed' || s === 'closed' || s === 'resolved') return 'fixed'; if (s === 'confirmed') return 'confirmed'; if (s === 'false_positive' || s === 'false-positive' || s === 'fp') return 'fp'; + if (s === 'ignored') return 'ignored'; return 'open'; } @@ -1467,6 +1468,7 @@ function statusShortLabel(s) { if (k === 'fixed') return dt('dashboard.statusFixed', null, '已修复'); if (k === 'confirmed') return dt('dashboard.statusConfirmed', null, '已确认'); if (k === 'fp') return dt('dashboard.statusFalsePositive', null, '误报'); + if (k === 'ignored') return dt('dashboard.statusIgnored', null, '已忽略'); return dt('dashboard.statusOpen', null, '待处理'); } diff --git a/web/static/js/projects.js b/web/static/js/projects.js index f7f2e46e..35b03b78 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -463,9 +463,10 @@ function formatVulnStatusBadge(status) { confirmed: 'vulnerabilityPage.statusConfirmed', fixed: 'vulnerabilityPage.statusFixed', false_positive: 'vulnerabilityPage.statusFalsePositive', + ignored: 'vulnerabilityPage.statusIgnored', }; const label = labelMap[s] ? tp(labelMap[s]) : status || '—'; - const cls = ['open', 'confirmed', 'fixed', 'false_positive'].includes(s) ? s : 'open'; + const cls = ['open', 'confirmed', 'fixed', 'false_positive', 'ignored'].includes(s) ? s : 'open'; return `${escapeHtml(label)}`; } diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 451f26e1..d94c38d0 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -442,8 +442,11 @@ let toolsSearchKeyword = ''; // 工具状态筛选: '' = 全部, 'true' = 已启用, 'false' = 已停用 let toolsStatusFilter = ''; +// 按外部 MCP 来源筛选(点击左侧卡片时设置) +let toolsExternalMcpFilter = ''; + // 加载工具列表(分页) -async function loadToolsList(page = 1, searchKeyword = '') { +async function loadToolsList(page = 1, searchKeyword = '', options = {}) { // 等待 i18n 就绪,避免快速刷新时翻译函数未初始化导致显示占位符 if (window.i18nReady) await window.i18nReady; const toolsList = document.getElementById('tools-list'); @@ -466,6 +469,12 @@ async function loadToolsList(page = 1, searchKeyword = '') { if (toolsStatusFilter !== '') { url += `&enabled=${toolsStatusFilter}`; } + if (options.refreshExternal) { + url += '&refresh_external=true'; + } + if (toolsExternalMcpFilter) { + url += `&external_mcp=${encodeURIComponent(toolsExternalMcpFilter)}`; + } // 使用较短的超时时间(10秒),避免长时间等待 const controller = new AbortController(); @@ -486,6 +495,7 @@ async function loadToolsList(page = 1, searchKeyword = '') { page: result.page || page, pageSize: result.page_size || pageSize, total: result.total || 0, + totalEnabled: result.total_enabled ?? 0, totalPages: result.total_pages || 1 }; @@ -504,6 +514,8 @@ async function loadToolsList(page = 1, searchKeyword = '') { renderToolsList(); renderToolsPagination(); + renderExternalMcpFilterChip(); + updateExternalMcpCardSelection(); } catch (error) { console.error('加载工具列表失败:', error); if (toolsList) { @@ -763,8 +775,7 @@ function scrollToExternalMCP(mcpName, event) { event.stopPropagation(); const items = document.querySelectorAll('.external-mcp-item'); for (const item of items) { - const h4 = item.querySelector('h4'); - if (h4 && h4.textContent.includes(mcpName)) { + if (item.dataset.mcpName === mcpName) { item.scrollIntoView({ behavior: 'smooth', block: 'center' }); item.classList.add('highlight'); setTimeout(() => item.classList.remove('highlight'), 2000); @@ -773,6 +784,94 @@ function scrollToExternalMCP(mcpName, event) { } } +// 点击左侧外部 MCP 卡片,筛选并定位右侧工具列表 +async function scrollToExternalMCPTools(mcpName, event) { + if (event) { + if (event.target.closest('.external-mcp-item-actions, button, a, input, label')) { + return; + } + event.stopPropagation(); + } + + if (toolsExternalMcpFilter === mcpName) { + await clearExternalMcpFilter(); + return; + } + + toolsExternalMcpFilter = mcpName; + updateExternalMcpCardSelection(); + renderExternalMcpFilterChip(); + await loadToolsList(1, toolsSearchKeyword); + + requestAnimationFrame(() => { + highlightExternalMcpTools(mcpName); + }); +} + +function highlightExternalMcpTools(mcpName) { + const toolsList = document.querySelector('.mcp-tools-panel .tools-list'); + if (toolsList) { + toolsList.scrollTop = 0; + } + + document.querySelectorAll('#tools-list .tool-item.highlight').forEach(el => { + el.classList.remove('highlight'); + }); + + const selector = `#tools-list .tool-item[data-external-mcp="${CSS.escape(mcpName)}"]`; + const matchingTools = document.querySelectorAll(selector); + if (matchingTools.length === 0) { + return; + } + + matchingTools[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); + matchingTools.forEach(el => { + el.classList.add('highlight'); + setTimeout(() => el.classList.remove('highlight'), 2000); + }); +} + +async function clearExternalMcpFilter() { + toolsExternalMcpFilter = ''; + updateExternalMcpCardSelection(); + renderExternalMcpFilterChip(); + await loadToolsList(1, toolsSearchKeyword); +} + +function updateExternalMcpCardSelection() { + document.querySelectorAll('.external-mcp-item').forEach(item => { + item.classList.toggle('selected', item.dataset.mcpName === toolsExternalMcpFilter); + }); +} + +function renderExternalMcpFilterChip() { + let chip = document.getElementById('tools-source-filter-chip'); + const toolsActions = document.querySelector('.mcp-tools-panel .tools-actions'); + if (!toolsActions) { + return; + } + + if (!chip) { + chip = document.createElement('div'); + chip.id = 'tools-source-filter-chip'; + chip.className = 'tools-source-filter-chip'; + toolsActions.appendChild(chip); + } + + if (!toolsExternalMcpFilter) { + chip.style.display = 'none'; + chip.innerHTML = ''; + return; + } + + const t = typeof window.t === 'function' ? window.t : (k) => k; + chip.style.display = 'inline-flex'; + chip.innerHTML = ` + ${t('mcp.filterBySource', { name: escapeHtml(toolsExternalMcpFilter) })} + + `; +} + // 渲染工具列表分页控件 function renderToolsPagination() { const toolsList = document.getElementById('tools-list'); @@ -964,60 +1063,22 @@ async function updateToolsStats() { return checkbox ? checkbox.checked : tool.enabled; }).length; } else { - // 没有搜索时,需要获取所有工具的状态 - // 先使用全局状态映射和当前页的checkbox状态 - const localStateMap = new Map(); - - // 从当前页的checkbox获取状态(如果全局映射中没有) - allTools.forEach(tool => { - const toolKey = getToolKey(tool); - const savedState = toolStateMap.get(toolKey); - if (savedState !== undefined) { - localStateMap.set(toolKey, savedState.enabled); - } else { - const checkboxId = `tool-${toolKey.replace(/::/g, '--')}`; - const checkbox = document.getElementById(checkboxId); - if (checkbox) { - localStateMap.set(toolKey, checkbox.checked); - } else { - // 如果checkbox不存在(不在当前页),使用工具原始状态 - localStateMap.set(toolKey, tool.enabled); + // 使用服务端统计,避免为统计翻页触发多次外部 MCP ListTools + totalEnabled = toolsPagination.totalEnabled ?? 0; + if (toolStateMap.size > 0) { + let delta = 0; + allTools.forEach(tool => { + const toolKey = getToolKey(tool); + const savedState = toolStateMap.get(toolKey); + if (savedState === undefined) { + return; } - } - }); - - // 如果总工具数大于当前页,需要获取所有工具的状态 - if (totalTools > allTools.length) { - // 遍历所有页面获取完整状态 - let page = 1; - let hasMore = true; - const pageSize = 100; // 使用较大的页面大小以减少请求次数 - - while (hasMore && page <= 10) { // 限制最多10页,避免无限循环 - const url = `/api/config/tools?page=${page}&page_size=${pageSize}`; - const pageResponse = await apiFetch(url); - if (!pageResponse.ok) break; - - const pageResult = await pageResponse.json(); - pageResult.tools.forEach(tool => { - // 优先使用全局状态映射,否则使用服务器返回的状态 - const toolKey = getToolKey(tool); - if (!localStateMap.has(toolKey)) { - const savedState = toolStateMap.get(toolKey); - localStateMap.set(toolKey, savedState ? savedState.enabled : tool.enabled); - } - }); - - if (page >= pageResult.total_pages) { - hasMore = false; - } else { - page++; + if (savedState.enabled !== tool.enabled) { + delta += savedState.enabled ? 1 : -1; } - } + }); + totalEnabled = Math.max(0, totalEnabled + delta); } - - // 计算启用的工具数 - totalEnabled = Array.from(localStateMap.values()).filter(enabled => enabled).length; } } catch (error) { console.warn('获取工具统计失败,使用当前页数据', error); @@ -1750,6 +1811,13 @@ async function loadExternalMCPs() { } } +async function reloadMcpToolsAfterExternalChange(refreshExternal = false) { + if (typeof loadToolsList === 'function') { + const page = (toolsPagination && toolsPagination.page) ? toolsPagination.page : 1; + await loadToolsList(page, toolsSearchKeyword, { refreshExternal }); + } +} + // 轮询列表直到指定 MCP 的工具数量已更新(每秒拉一次,拿到即停,无固定延迟) // name 为 null 时仅按 maxAttempts 次数轮询,不判断 tool_count async function pollExternalMCPToolCount(name, maxAttempts = 10) { @@ -1768,6 +1836,7 @@ async function pollExternalMCPToolCount(name, maxAttempts = 10) { console.warn('轮询工具数量失败:', e); } } + await reloadMcpToolsAfterExternalChange(true); if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } @@ -1802,8 +1871,15 @@ function renderExternalMCPList(servers) { const transport = server.config.type || server.config.transport || (server.config.command ? 'stdio' : 'http'); const transportIcon = transport === 'stdio' ? '⚙️' : '🌐'; + const hasTools = server.tool_count !== undefined && server.tool_count > 0; + const cardClickTitle = hasTools + ? escapeHtml(statusT('mcp.clickToViewTools', { name })) + : ''; + const cardClass = hasTools ? 'external-mcp-item clickable' : 'external-mcp-item'; + const selectedClass = toolsExternalMcpFilter === name ? ' selected' : ''; + html += ` -