diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index d702b699..3e0de1fe 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -31,6 +31,25 @@ function shouldSkipTaskEventReplayAttach(conversationId) { return false; } } +/** 监控页展示:内部 mcp::tool → 模型侧 mcp__tool */ +function formatMonitorToolName(name) { + if (!name || typeof name !== 'string') return name || ''; + return name.includes('::') ? name.replace('::', '__') : name; +} + +/** 筛选/API:mcp__tool → 内部 mcp::tool(与库存一致) */ +function canonicalMonitorToolName(name) { + if (!name || typeof name !== 'string') return name || ''; + if (name.includes('::')) return name; + const idx = name.indexOf('__'); + if (idx > 0) return `${name.slice(0, idx)}::${name.slice(idx + 2)}`; + return name; +} + +function monitorToolNamesEqual(a, b) { + return canonicalMonitorToolName(a) === canonicalMonitorToolName(b); +} + if (typeof window !== 'undefined') { window.shouldSkipTaskEventReplayAttach = shouldSkipTaskEventReplayAttach; } @@ -3632,9 +3651,10 @@ async function applyMonitorFilters() { const statusFilter = document.getElementById('monitor-status-filter'); const toolFilter = document.getElementById('monitor-tool-filter'); const status = statusFilter ? statusFilter.value : 'all'; - const tool = toolFilter ? (toolFilter.value.trim() || 'all') : 'all'; + const toolRaw = toolFilter ? (toolFilter.value.trim() || 'all') : 'all'; + const tool = toolRaw === 'all' ? 'all' : canonicalMonitorToolName(toolRaw); if (toolFilter) { - toolFilter.classList.toggle('is-filter-active', tool !== 'all'); + toolFilter.classList.toggle('is-filter-active', toolRaw !== 'all'); } // 当筛选条件改变时,从后端重新获取数据 await refreshMonitorPanelWithFilter(status, tool); @@ -4041,9 +4061,10 @@ function renderMcpStatsCombinedSection(topTools, totals, activeToolFilter, timel if (!hasTools && !showTimeline) return ''; + const filterChipLabel = activeToolFilter ? formatMonitorToolName(activeToolFilter) : ''; const filterChip = activeToolFilter - ? ` - ${escapeHtml(mcpMonitorT('filterActive', { tool: activeToolFilter }) || `已筛选:${activeToolFilter}`)} + ? ` + ${escapeHtml(mcpMonitorT('filterActive', { tool: filterChipLabel }) || `已筛选:${filterChipLabel}`)} ` : ''; @@ -4483,7 +4504,7 @@ function updateMonitorStatsSubtitle(lastFetchedAt, toolCount) { function filterMonitorByTool(toolName) { const toolFilter = document.getElementById('monitor-tool-filter'); if (!toolFilter || !toolName) return; - toolFilter.value = toolName; + toolFilter.value = formatMonitorToolName(toolName); toolFilter.classList.add('is-filter-active'); applyMonitorFilters(); const execSection = document.querySelector('.monitor-executions'); @@ -4615,7 +4636,8 @@ function renderMcpStatsToolTable(topTools, totals, activeToolFilter = '') { let rowsHtml = ''; topTools.forEach((tool, index) => { - const name = tool.toolName || unknownToolLabel; + const rawName = tool.toolName || unknownToolLabel; + const name = formatMonitorToolName(rawName); const total = tool.totalCalls || 0; const success = tool.successCalls || 0; const failed = tool.failedCalls || 0; @@ -4623,14 +4645,14 @@ function renderMcpStatsToolTable(topTools, totals, activeToolFilter = '') { const toolRate = toolRateNum.toFixed(1); const sharePct = totals.total > 0 ? ((total / totals.total) * 100).toFixed(1) : '0.0'; const dotColor = MCP_STATS_DIST_COLORS[index % MCP_STATS_DIST_COLORS.length]; - const isActive = activeToolFilter && activeToolFilter === name; + const isActive = activeToolFilter && monitorToolNamesEqual(activeToolFilter, rawName); const rateClass = getMcpToolRateClass(toolRateNum); const rankClass = index === 0 ? ' rank-1' : index === 1 ? ' rank-2' : index === 2 ? ' rank-3' : ''; const rowAria = mcpMonitorT('toolRowAriaLabel', { name, total, rate: toolRate }) || `${name},${total} 次调用,成功率 ${toolRate}%`; rowsHtml += ` { - const isActive = !s.isOthers && activeToolFilter && activeToolFilter === s.name; - const title = `${s.name} · ${s.pct}% · ${s.calls}`; + const isActive = !s.isOthers && activeToolFilter && monitorToolNamesEqual(activeToolFilter, s.name); + const displayName = s.isOthers ? s.name : formatMonitorToolName(s.name); + const title = `${displayName} · ${s.pct}% · ${s.calls}`; if (s.isOthers) { return ``; } - const segAria = mcpMonitorT('distSegmentAria', { name: s.name, pct: s.pct, calls: s.calls }) - || `${s.name},占 ${s.pct}%,${s.calls} 次`; + const segAria = mcpMonitorT('distSegmentAria', { name: displayName, pct: s.pct, calls: s.calls }) + || `${displayName},占 ${s.pct}%,${s.calls} 次`; return ` t.totalCalls || 0)); const listHtml = topTools.map((tool, index) => { - const name = tool.toolName || unknownToolLabel; + const rawName = tool.toolName || unknownToolLabel; + const name = formatMonitorToolName(rawName); const total = tool.totalCalls || 0; const success = tool.successCalls || 0; const failed = tool.failedCalls || 0; @@ -4704,7 +4728,7 @@ function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') { const sharePct = totals.total > 0 ? ((total / totals.total) * 100).toFixed(1) : '0.0'; const color = MCP_STATS_DIST_COLORS[index % MCP_STATS_DIST_COLORS.length]; const barPct = maxCalls > 0 ? ((total / maxCalls) * 100).toFixed(1) : '0'; - const isActive = activeToolFilter && activeToolFilter === name; + const isActive = activeToolFilter && monitorToolNamesEqual(activeToolFilter, rawName); const rateClass = getMcpToolRateClass(toolRateNum); const rankClass = index === 0 ? ' rank-1' : index === 1 ? ' rank-2' : index === 2 ? ' rank-3' : ''; const rowAria = mcpMonitorT('toolRowAriaLabel', { name, total, rate: toolRate }) @@ -4713,7 +4737,7 @@ function renderMcpStatsToolsPanel(topTools, totals, activeToolFilter = '') { ? `${escapeHtml(mcpMonitorT('failedCount', { n: failed }) || `失败 ${failed}`)}` : ''; return `
  • ${index + 1} @@ -4947,7 +4971,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { const statusLabel = (typeof window.t === 'function' && statusKey) ? window.t('mcpMonitor.' + statusKey) : getStatusText(status); const startTime = exec.startTime ? (new Date(exec.startTime).toLocaleString ? new Date(exec.startTime).toLocaleString(locale || 'en-US') : String(exec.startTime)) : unknownLabel; const duration = formatExecutionDuration(exec.startTime, exec.endTime); - const toolName = escapeHtml(exec.toolName || unknownToolLabel); + const toolName = escapeHtml(formatMonitorToolName(exec.toolName) || unknownToolLabel); const rawExecId = exec.id || ''; const executionId = escapeHtml(rawExecId); const terminateBtn = status === 'running' diff --git a/web/static/js/router.js b/web/static/js/router.js index b9b5b7b5..2e18373b 100644 --- a/web/static/js/router.js +++ b/web/static/js/router.js @@ -315,6 +315,9 @@ function showSubmenuPopup(navItem, menuId) { async function initPage(pageId) { // 等待 i18n 就绪,避免快速刷新时翻译函数未初始化导致页面显示原始占位符 key if (window.i18nReady) await window.i18nReady; + if (typeof stopExternalMcpPoll === 'function') { + stopExternalMcpPoll(); + } switch(pageId) { case 'dashboard': if (typeof refreshDashboard === 'function') { @@ -372,21 +375,26 @@ async function initPage(pageId) { }, 100); } }; - // 先拉取全局配置,确保 tool_search 常驻状态按后端生效集合展示 + const afterMcpConfigReady = () => { + startLoadMcpTools(); + if (typeof loadExternalMCPs === 'function') { + loadExternalMCPs().catch(err => { + console.warn('加载外部MCP列表失败:', err); + }); + } + if (typeof startExternalMcpPoll === 'function') { + startExternalMcpPoll(); + } + }; + // 先拉取配置(含 tool_search 常驻列表),再加载工具与外部 MCP if (typeof loadConfig === 'function') { loadConfig(false) .catch(err => { - console.warn('加载配置失败(将继续加载工具列表):', err); + console.warn('加载配置失败(将继续加载 MCP 列表):', err); }) - .finally(startLoadMcpTools); + .finally(afterMcpConfigReady); } else { - startLoadMcpTools(); - } - // 先加载外部MCP列表(快速),然后加载工具列表 - if (typeof loadExternalMCPs === 'function') { - loadExternalMCPs().catch(err => { - console.warn('加载外部MCP列表失败:', err); - }); + afterMcpConfigReady(); } break; case 'projects': diff --git a/web/static/js/settings.js b/web/static/js/settings.js index d94c38d0..22b23cdb 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -16,6 +16,96 @@ function getToolKey(tool) { } return tool.name; } + +// 常驻工具配置存储键(外部工具用 mcp::tool,与后端 tool_search 白名单一致) +function getAlwaysVisibleStorageKey(tool) { + return getToolKey(tool); +} + +function addAlwaysVisibleAliases(name) { + const n = (name || '').trim(); + if (!n) return; + alwaysVisibleToolNames.add(n); + if (n.includes('::')) { + const sep = n.indexOf('::'); + const mcp = n.slice(0, sep); + const tool = n.slice(sep + 2); + if (mcp && tool) { + alwaysVisibleToolNames.add(`${mcp}__${tool}`); + } + return; + } + if (n.includes('__')) { + const sep = n.lastIndexOf('__'); + const mcp = n.slice(0, sep); + const tool = n.slice(sep + 2); + if (mcp && tool) { + alwaysVisibleToolNames.add(`${mcp}::${tool}`); + } + } +} + +function removeAlwaysVisibleAliases(name) { + const n = (name || '').trim(); + if (!n) return; + alwaysVisibleToolNames.delete(n); + if (n.includes('::')) { + const sep = n.indexOf('::'); + const mcp = n.slice(0, sep); + const tool = n.slice(sep + 2); + if (mcp && tool) { + alwaysVisibleToolNames.delete(`${mcp}__${tool}`); + } + return; + } + if (n.includes('__')) { + const sep = n.lastIndexOf('__'); + const mcp = n.slice(0, sep); + const tool = n.slice(sep + 2); + if (mcp && tool) { + alwaysVisibleToolNames.delete(`${mcp}::${tool}`); + } + } +} + +function isToolAlwaysVisible(tool) { + const key = getAlwaysVisibleStorageKey(tool); + if (alwaysVisibleToolNames.has(key)) return true; + if (alwaysVisibleToolNames.has(tool.name)) return true; + if (tool.is_external && tool.external_mcp) { + if (alwaysVisibleToolNames.has(`${tool.external_mcp}__${tool.name}`)) return true; + } + return false; +} + +function isToolAlwaysVisibleBuiltin(tool) { + if (alwaysVisibleBuiltinToolNames.has(tool.name)) return true; + return alwaysVisibleBuiltinToolNames.has(getAlwaysVisibleStorageKey(tool)); +} + +function getAlwaysVisibleForSave() { + const out = new Set(); + for (const name of alwaysVisibleToolNames) { + if (alwaysVisibleBuiltinToolNames.has(name)) continue; + if (name.includes('::')) { + out.add(name); + continue; + } + if (name.includes('__')) { + const sep = name.lastIndexOf('__'); + const mcp = name.slice(0, sep); + const tool = name.slice(sep + 2); + if (mcp && tool) out.add(`${mcp}::${tool}`); + continue; + } + out.add(name); + } + return Array.from(out); +} + +function countUserAlwaysVisibleTools() { + return getAlwaysVisibleForSave().length; +} // 从localStorage读取每页显示数量,默认为20 const getToolsPageSize = () => { const saved = localStorage.getItem('toolsPageSize'); @@ -158,14 +248,21 @@ async function loadConfig(loadTools = true) { } currentConfig = await response.json(); - const alwaysVisibleList = currentConfig?.multi_agent?.tool_search_always_visible_effective_tools; const alwaysVisibleConfigured = currentConfig?.multi_agent?.tool_search_always_visible_tools; - alwaysVisibleToolNames = new Set(Array.isArray(alwaysVisibleList) ? alwaysVisibleList.filter(Boolean) : []); - alwaysVisibleBuiltinToolNames = new Set( - alwaysVisibleToolNames.size > 0 && Array.isArray(alwaysVisibleConfigured) - ? Array.from(alwaysVisibleToolNames).filter(name => !alwaysVisibleConfigured.includes(name)) - : [] - ); + const alwaysVisibleEffective = currentConfig?.multi_agent?.tool_search_always_visible_effective_tools; + alwaysVisibleToolNames = new Set(); + if (Array.isArray(alwaysVisibleConfigured)) { + alwaysVisibleConfigured.filter(Boolean).forEach(addAlwaysVisibleAliases); + } + alwaysVisibleBuiltinToolNames = new Set(); + if (Array.isArray(alwaysVisibleEffective)) { + const configuredSet = new Set(Array.isArray(alwaysVisibleConfigured) ? alwaysVisibleConfigured : []); + alwaysVisibleEffective.filter(Boolean).forEach(name => { + if (!configuredSet.has(name)) { + alwaysVisibleBuiltinToolNames.add(name); + } + }); + } // 填充OpenAI配置 const providerEl = document.getElementById('openai-provider'); @@ -634,8 +731,8 @@ function renderToolsList() { is_external: tool.is_external || false, external_mcp: tool.external_mcp || '' }; - const alwaysVisibleChecked = alwaysVisibleToolNames.has(tool.name); - const alwaysVisibleLocked = alwaysVisibleBuiltinToolNames.has(tool.name); + const alwaysVisibleChecked = isToolAlwaysVisible(tool); + const alwaysVisibleLocked = isToolAlwaysVisibleBuiltin(tool); // 外部工具标签,显示来源信息(可点击跳转到对应 MCP 卡片) let externalBadge = ''; @@ -660,7 +757,7 @@ function renderToolsList() { ${escapeHtml(tool.name)} ${externalBadge} ${alwaysVisibleLocked ? `${typeof window.t === 'function' ? window.t('mcp.alwaysVisibleBuiltinLabel') : '内置默认'}` : ''} @@ -946,14 +1043,15 @@ function handleToolCheckboxChange(toolKey, enabled) { updateToolsStats(); } -function handleToolAlwaysVisibleChange(toolName, alwaysVisible) { - const name = (toolName || '').trim(); - if (!name) return; +function handleToolAlwaysVisibleChange(toolKey, alwaysVisible) { + const key = (toolKey || '').trim(); + if (!key) return; if (alwaysVisible) { - alwaysVisibleToolNames.add(name); + addAlwaysVisibleAliases(key); } else { - alwaysVisibleToolNames.delete(name); + removeAlwaysVisibleAliases(key); } + updateToolsStats(); } // 全选工具 @@ -1088,7 +1186,7 @@ async function updateToolsStats() { } const tStats = typeof window.t === 'function' ? window.t : (k) => k; - const pinnedCount = alwaysVisibleToolNames.size; + const pinnedCount = countUserAlwaysVisibleTools(); statsEl.innerHTML = ` ✅ ${tStats('mcp.currentPageEnabled')}: ${currentPageEnabled} / ${currentPageTotal} 📊 ${tStats('mcp.totalEnabled')}: ${totalEnabled} / ${totalTools} @@ -1596,7 +1694,7 @@ async function saveToolsConfig() { robot_default_agent_mode: currentConfig?.multi_agent?.robot_default_agent_mode || 'eino_single', batch_use_multi_agent: currentConfig?.multi_agent?.batch_use_multi_agent === true, plan_execute_loop_max_iterations: Number(currentConfig?.multi_agent?.plan_execute_loop_max_iterations || 0), - tool_search_always_visible_tools: Array.from(alwaysVisibleToolNames).filter(name => !alwaysVisibleBuiltinToolNames.has(name)) + tool_search_always_visible_tools: getAlwaysVisibleForSave() }, tools: [] }; @@ -1793,6 +1891,32 @@ async function fetchExternalMCPs() { return response.json(); } +// MCP 管理页定时刷新外部 MCP 状态(感知后台断连/自动重连) +let externalMcpPollTimer = null; +const EXTERNAL_MCP_POLL_INTERVAL_MS = 8000; + +function startExternalMcpPoll() { + stopExternalMcpPoll(); + externalMcpPollTimer = setInterval(function () { + const mcpPage = document.getElementById('page-mcp-management'); + if (!mcpPage || !mcpPage.classList.contains('active')) { + stopExternalMcpPoll(); + return; + } + if (document.hidden) { + return; + } + loadExternalMCPs().catch(function () { /* ignore */ }); + }, EXTERNAL_MCP_POLL_INTERVAL_MS); +} + +function stopExternalMcpPoll() { + if (externalMcpPollTimer) { + clearInterval(externalMcpPollTimer); + externalMcpPollTimer = null; + } +} + // 加载外部MCP列表并渲染 async function loadExternalMCPs() { try { @@ -1898,9 +2022,9 @@ function renderExternalMCPList(servers) { - ${status === 'error' && server.error ? ` -
    - ❌ ${statusT('mcp.connectionErrorLabel')}${escapeHtml(server.error)} + ${(status === 'error' || status === 'disconnected') && server.error ? ` +
    + ${status === 'error' ? '❌' : '⚠️'} ${statusT('mcp.connectionErrorLabel')}${escapeHtml(server.error)}
    ` : ''}