Add files via upload

This commit is contained in:
公明
2026-06-12 19:36:45 +08:00
committed by GitHub
parent 4661862a1a
commit bf0ce33e3f
8 changed files with 228 additions and 58 deletions
+71
View File
@@ -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);
+6
View File
@@ -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",
+6
View File
@@ -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": "目标",
+3 -1
View File
@@ -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, '待处理');
}
+2 -1
View File
@@ -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 `<span class="status-badge status-${escapeHtml(cls)}">${escapeHtml(label)}</span>`;
}
+135 -55
View File
@@ -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 = `
<span>${t('mcp.filterBySource', { name: escapeHtml(toolsExternalMcpFilter) })}</span>
<button type="button" class="tools-source-filter-clear" onclick="clearExternalMcpFilter()" title="${escapeHtml(t('mcp.clearSourceFilter'))}">×</button>
`;
}
// 渲染工具列表分页控件
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 += `
<div class="external-mcp-item">
<div class="${cardClass}${selectedClass}" data-mcp-name="${escapeHtml(name)}"${hasTools ? ` onclick="scrollToExternalMCPTools('${escapeHtml(name)}', event)" title="${cardClickTitle}"` : ''}>
<div class="external-mcp-item-header">
<div class="external-mcp-item-info">
<h4>${transportIcon} ${escapeHtml(name)}${server.tool_count !== undefined && server.tool_count > 0 ? `<span class="tool-count-badge" title="${escapeHtml(statusT('mcp.toolCount'))}">🔧 ${server.tool_count}</span>` : ''}</h4>
@@ -1866,6 +1942,7 @@ function renderExternalMCPList(servers) {
}
html += '</div>';
list.innerHTML = html;
updateExternalMcpCardSelection();
}
// 渲染外部MCP统计信息
@@ -2226,6 +2303,7 @@ async function toggleExternalMCP(name, currentStatus) {
}
// 轮询直到该 MCP 工具数量已更新(每秒拉一次,无固定延迟)
pollExternalMCPToolCount(name, 10);
await reloadMcpToolsAfterExternalChange(true);
return;
}
}
@@ -2238,6 +2316,7 @@ async function toggleExternalMCP(name, currentStatus) {
} else {
// 停止操作,直接刷新
await loadExternalMCPs();
await reloadMcpToolsAfterExternalChange(false);
// 刷新对话界面的工具列表
if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') {
window.refreshMentionTools();
@@ -2289,6 +2368,7 @@ async function pollExternalMCPStatus(name, maxAttempts = 30) {
}
// 轮询直到该 MCP 工具数量已更新(每秒拉一次,无固定延迟)
pollExternalMCPToolCount(name, 10);
await reloadMcpToolsAfterExternalChange(true);
return;
} else if (status === 'error' || status === 'disconnected') {
// 连接失败,刷新列表并显示错误
+2 -1
View File
@@ -33,7 +33,8 @@ function vulnStatusLabel(code) {
open: 'vulnerabilityPage.statusOpen',
confirmed: 'vulnerabilityPage.statusConfirmed',
fixed: 'vulnerabilityPage.statusFixed',
false_positive: 'vulnerabilityPage.statusFalsePositive'
false_positive: 'vulnerabilityPage.statusFalsePositive',
ignored: 'vulnerabilityPage.statusIgnored'
};
return m[code] ? vulnT(m[code]) : code;
}
+3
View File
@@ -1613,6 +1613,7 @@
<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>
<option value="ignored" data-i18n="vulnerabilityPage.statusIgnored">已忽略</option>
</select>
</label>
</div>
@@ -1811,6 +1812,7 @@
<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>
<option value="ignored" data-i18n="vulnerabilityPage.statusIgnored">已忽略</option>
</select>
</label>
<div class="vulnerability-filter-actions">
@@ -3943,6 +3945,7 @@
<option value="confirmed" data-i18n="vulnerabilityModal.statusConfirmed">已确认</option>
<option value="fixed" data-i18n="vulnerabilityModal.statusFixed">已修复</option>
<option value="false_positive" data-i18n="vulnerabilityModal.statusFalsePositive">误报</option>
<option value="ignored" data-i18n="vulnerabilityModal.statusIgnored">已忽略</option>
</select>
</div>
<div class="form-group">