From d037647c2171dae89e53d7af4cd6fdcee20f9194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:13:08 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 91 +++++++++++++++++++++- web/static/i18n/en-US.json | 10 +-- web/static/i18n/zh-CN.json | 10 +-- web/static/js/settings.js | 154 +++++++++++++++++++++++++++++++------ 4 files changed, 231 insertions(+), 34 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index f4300451..46d0ba03 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -3965,7 +3965,7 @@ header { .tool-item { display: flex; - align-items: center; + align-items: flex-start; gap: 12px; padding: 10px 12px; border-radius: 6px; @@ -3980,8 +3980,10 @@ header { .tool-item input[type="checkbox"] { width: 18px; height: 18px; + margin-top: 2px; cursor: pointer; accent-color: var(--accent-color); + flex-shrink: 0; } .tool-item-info { @@ -4021,6 +4023,93 @@ header { white-space: nowrap; } +/* 展开图标 */ +.tool-expand-icon { + font-size: 0.625rem; + color: var(--text-tertiary); + transition: transform 0.2s; + user-select: none; + flex-shrink: 0; +} + +/* 展开后的详情面板 */ +.tool-item-detail { + margin-top: 8px; + padding: 12px; + background: var(--bg-tertiary); + border-radius: 8px; + border: 1px solid var(--border-color); + font-size: 0.8125rem; +} + +.tool-detail-desc { + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 8px; + white-space: pre-wrap; + word-break: break-word; +} + +.tool-detail-section-title { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; +} + +/* 参数表格 */ +.tool-schema-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; +} + +.tool-schema-table th { + text-align: left; + padding: 6px 10px; + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 600; + border-bottom: 1px solid var(--border-color); +} + +.tool-schema-table td { + padding: 6px 10px; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + vertical-align: top; +} + +.tool-schema-table code { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; + color: var(--accent-color); +} + +/* 可点击的外部工具徽章 */ +.external-tool-badge.clickable { + cursor: pointer; + transition: background 0.2s, border-color 0.2s; +} + +.external-tool-badge.clickable:hover { + background: rgba(255, 152, 0, 0.25); + border-color: rgba(255, 152, 0, 0.6); +} + +/* 外部 MCP 卡片高亮动画 */ +.external-mcp-item.highlight { + animation: mcpHighlight 2s ease-out; +} + +@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); } +} + .tool-item.hidden { display: none; } diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 2ba3f1ba..6b3cff14 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1564,15 +1564,15 @@ "externalMcpModal": { "configJson": "Config JSON", "formatLabel": "Format:", - "formatDesc": "JSON object; key = config name, value = config. Use Start/Stop buttons to control state.", + "formatDesc": "JSON object; key = config name, value = config. Use Start/Stop buttons to control state. Supports ${VAR} and ${VAR:-default} env variable syntax.", "configExample": "Configuration example:", "stdioMode": "stdio mode:", "httpMode": "HTTP mode:", "sseMode": "SSE mode:", - "placeholder": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\"],\n \"description\": \"Description\",\n \"timeout\": 300\n }\n}", - "exampleStdio": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\", \"--server\", \"http://example.com\"],\n \"description\": \"Description\",\n \"timeout\": 300\n }\n}", - "exampleHttp": "{\n \"cyberstrike-ai-http\": {\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:8081/mcp\"\n }\n}", - "exampleSse": "{\n \"cyberstrike-ai-sse\": {\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}", + "placeholder": "{\n \"my-server\": {\n \"command\": \"python3\",\n \"args\": [\"${HOME}/mcp/server.py\"],\n \"env\": { \"API_KEY\": \"${API_KEY}\" },\n \"timeout\": 300\n }\n}", + "exampleStdio": "{\n \"my-server\": {\n \"command\": \"python3\",\n \"args\": [\"${HOME}/mcp/server.py\"],\n \"env\": { \"API_KEY\": \"${API_KEY}\", \"LOG_LEVEL\": \"${LOG_LEVEL:-INFO}\" },\n \"timeout\": 300\n }\n}", + "exampleHttp": "{\n \"remote-mcp\": {\n \"type\": \"http\",\n \"url\": \"https://mcp.example.com/mcp\",\n \"headers\": { \"Authorization\": \"Bearer ${MCP_TOKEN}\" }\n }\n}", + "exampleSse": "{\n \"sse-mcp\": {\n \"type\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}", "exampleDescription": "Example description", "formatJson": "Format JSON", "loadExample": "Load example" diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 40831985..4102481c 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1564,15 +1564,15 @@ "externalMcpModal": { "configJson": "配置JSON", "formatLabel": "配置格式:", - "formatDesc": "JSON对象,key为配置名称,value为配置内容。状态通过\"启动/停止\"按钮控制,无需在JSON中配置。", + "formatDesc": "JSON对象,key为配置名称,value为配置内容。状态通过\"启动/停止\"按钮控制,无需在JSON中配置。支持 ${VAR} 和 ${VAR:-默认值} 环境变量语法。", "configExample": "配置示例:", "stdioMode": "stdio模式:", "httpMode": "HTTP模式:", "sseMode": "SSE模式:", - "placeholder": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\"],\n \"description\": \"描述\",\n \"timeout\": 300\n }\n}", - "exampleStdio": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\", \"--server\", \"http://example.com\"],\n \"description\": \"描述\",\n \"timeout\": 300\n }\n}", - "exampleHttp": "{\n \"cyberstrike-ai-http\": {\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:8081/mcp\"\n }\n}", - "exampleSse": "{\n \"cyberstrike-ai-sse\": {\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}", + "placeholder": "{\n \"my-server\": {\n \"command\": \"python3\",\n \"args\": [\"${HOME}/mcp/server.py\"],\n \"env\": { \"API_KEY\": \"${API_KEY}\" },\n \"timeout\": 300\n }\n}", + "exampleStdio": "{\n \"my-server\": {\n \"command\": \"python3\",\n \"args\": [\"${HOME}/mcp/server.py\"],\n \"env\": { \"API_KEY\": \"${API_KEY}\", \"LOG_LEVEL\": \"${LOG_LEVEL:-INFO}\" },\n \"timeout\": 300\n }\n}", + "exampleHttp": "{\n \"remote-mcp\": {\n \"type\": \"http\",\n \"url\": \"https://mcp.example.com/mcp\",\n \"headers\": { \"Authorization\": \"Bearer ${MCP_TOKEN}\" }\n }\n}", + "exampleSse": "{\n \"sse-mcp\": {\n \"type\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}", "exampleDescription": "示例描述", "formatJson": "格式化JSON", "loadExample": "加载示例" diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 48f35bca..4dd39ce8 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -501,26 +501,32 @@ function renderToolsList() { external_mcp: tool.external_mcp || '' }; - // 外部工具标签,显示来源信息 + // 外部工具标签,显示来源信息(可点击跳转到对应 MCP 卡片) let externalBadge = ''; if (toolState.is_external || tool.is_external) { const externalMcpName = toolState.external_mcp || tool.external_mcp || ''; const badgeText = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalFrom', { name: escapeHtml(externalMcpName) }) : `外部 (${escapeHtml(externalMcpName)})`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部'); - const badgeTitle = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalToolFrom', { name: escapeHtml(externalMcpName) }) : `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部MCP工具'); - externalBadge = `${badgeText}`; + const badgeTitle = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalToolFrom', { name: escapeHtml(externalMcpName) }) + ' — 点击跳转' : `外部MCP工具 - 来源:${escapeHtml(externalMcpName)} — 点击跳转`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部MCP工具'); + if (externalMcpName) { + externalBadge = `${badgeText}`; + } else { + externalBadge = `${badgeText}`; + } } - + // 生成唯一的checkbox id,使用工具唯一标识符 const checkboxId = `tool-${escapeHtml(toolKey).replace(/::/g, '--')}`; - + toolItem.innerHTML = ` -
+
${escapeHtml(tool.name)} ${externalBadge} +
${escapeHtml(tool.description || (typeof window.t === 'function' ? window.t('mcp.noDescription') : '无描述'))}
+
`; listContainer.appendChild(toolItem); @@ -534,6 +540,103 @@ function renderToolsList() { updateToolsStats(); } +// 展开/折叠工具详情面板(按需从后端加载 schema) +function toggleToolDetail(infoEl, toolKey, isExternal, externalMcp, event) { + // 点击 checkbox 或外部工具徽章时不展开 + if (event.target.tagName === 'INPUT' || event.target.closest('.external-tool-badge')) return; + + const detail = infoEl.querySelector('.tool-item-detail'); + const icon = infoEl.querySelector('.tool-expand-icon'); + if (!detail) return; + + const isOpen = detail.style.display !== 'none'; + detail.style.display = isOpen ? 'none' : 'block'; + if (icon) icon.textContent = isOpen ? '▶' : '▼'; + + // 首次展开时从后端按需加载 + if (!isOpen && !detail.dataset.rendered) { + detail.dataset.rendered = '1'; + const descEl = infoEl.querySelector('.tool-item-desc'); + const fullDesc = descEl ? descEl.textContent : ''; + + // 先显示加载状态 + detail.innerHTML = ` +
${escapeHtml(fullDesc)}
+
参数定义
+
加载中...
+ `; + + // 解析工具名(外部工具 toolKey 格式为 mcpName::toolName) + let apiToolName = toolKey; + let query = ''; + if (isExternal && externalMcp) { + const parts = toolKey.split('::'); + apiToolName = parts.length > 1 ? parts[1] : toolKey; + query = '?external_mcp=' + encodeURIComponent(externalMcp); + } + + apiFetch(`/api/config/tools/${encodeURIComponent(apiToolName)}/schema${query}`) + .then(r => r.json()) + .then(data => { + const schema = data.input_schema; + let schemaHTML = ''; + if (schema) { + const props = schema.properties || {}; + const required = schema.required || []; + const paramKeys = Object.keys(props); + if (paramKeys.length > 0) { + schemaHTML = ` + + `; + paramKeys.forEach(key => { + const p = props[key] || {}; + const type = p.type || (p.enum ? 'enum' : '—'); + const isReq = required.includes(key); + const desc = p.description || ''; + schemaHTML += ` + + + + + `; + }); + schemaHTML += '
参数类型必填说明
${escapeHtml(key)}${escapeHtml(String(type))}${isReq ? '' : ''}${escapeHtml(desc)}
'; + } + } + if (!schemaHTML) { + schemaHTML = '
无参数定义
'; + } + detail.innerHTML = ` +
${escapeHtml(fullDesc)}
+
参数定义
+ ${schemaHTML} + `; + }) + .catch(() => { + detail.innerHTML = ` +
${escapeHtml(fullDesc)}
+
参数定义
+
加载失败
+ `; + }); + } +} + +// 点击外部工具徽章跳转到对应的外部 MCP 卡片 +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)) { + item.scrollIntoView({ behavior: 'smooth', block: 'center' }); + item.classList.add('highlight'); + setTimeout(() => item.classList.remove('highlight'), 2000); + return; + } + } +} + // 渲染工具列表分页控件 function renderToolsPagination() { const toolsList = document.getElementById('tools-list'); @@ -1382,7 +1485,7 @@ function renderExternalMCPList(servers) { status === 'connecting' ? statusT('mcp.connecting') : status === 'error' ? statusT('mcp.connectionFailed') : status === 'disabled' ? statusT('mcp.disabled') : statusT('mcp.disconnected'); - const transport = server.config.transport || (server.config.command ? 'stdio' : 'http'); + const transport = server.config.type || server.config.transport || (server.config.command ? 'stdio' : 'http'); const transportIcon = transport === 'stdio' ? '⚙️' : '🌐'; html += ` @@ -1393,11 +1496,11 @@ function renderExternalMCPList(servers) { ${statusText}
- ${status === 'connected' || status === 'disconnected' || status === 'error' ? + ${status === 'connected' || status === 'disconnected' || status === 'error' || status === 'disabled' ? `` : - status === 'connecting' ? + ` : + status === 'connecting' ? `` : ''} @@ -1552,24 +1655,29 @@ function formatExternalMCPJSON() { // 加载示例 function loadExternalMCPExample() { - const desc = (typeof window.t === 'function' ? window.t('externalMcpModal.exampleDescription') : '示例描述'); const example = { - "hexstrike-ai": { + "my-stdio-server": { command: "python3", args: [ - "/path/to/script.py", - "--server", - "http://example.com" + "${HOME}/mcp-servers/main.py", + "--port", + "${MCP_PORT:-3000}" ], - description: desc, + env: { + "API_KEY": "${API_KEY}", + "LOG_LEVEL": "${LOG_LEVEL:-INFO}" + }, timeout: 300 }, - "cyberstrike-ai-http": { - transport: "http", - url: "http://127.0.0.1:8081/mcp" + "my-http-server": { + type: "http", + url: "https://mcp.example.com/mcp", + headers: { + "Authorization": "Bearer ${MCP_TOKEN}" + } }, - "cyberstrike-ai-sse": { - transport: "sse", + "my-sse-server": { + type: "sse", url: "http://127.0.0.1:8081/mcp/sse" } }; @@ -1642,8 +1750,8 @@ async function saveExternalMCP() { // 移除 external_mcp_enable 字段(由按钮控制,但保留 enabled/disabled 用于向后兼容) delete config.external_mcp_enable; - // 验证配置内容 - const transport = config.transport || (config.command ? 'stdio' : config.url ? 'http' : ''); + // 验证配置内容(同时支持官方 type 字段和旧版 transport 字段) + const transport = config.type || config.transport || (config.command ? 'stdio' : config.url ? 'http' : ''); if (!transport) { errorDiv.textContent = t('mcp.configNeedCommand', { name: name }); errorDiv.style.display = 'block';