mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-21 18:26:38 +02:00
Add files via upload
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "加载示例"
|
||||
|
||||
+131
-23
@@ -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 = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
|
||||
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 = `<span class="external-tool-badge clickable" onclick="scrollToExternalMCP('${escapeHtml(externalMcpName)}', event)" title="${badgeTitle}">${badgeText}</span>`;
|
||||
} else {
|
||||
externalBadge = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 生成唯一的checkbox id,使用工具唯一标识符
|
||||
const checkboxId = `tool-${escapeHtml(toolKey).replace(/::/g, '--')}`;
|
||||
|
||||
|
||||
toolItem.innerHTML = `
|
||||
<input type="checkbox" id="${checkboxId}" ${toolState.enabled ? 'checked' : ''} ${toolState.is_external || tool.is_external ? 'data-external="true"' : ''} onchange="handleToolCheckboxChange('${escapeHtml(toolKey)}', this.checked)" />
|
||||
<div class="tool-item-info">
|
||||
<div class="tool-item-info" onclick="toggleToolDetail(this, '${escapeHtml(toolKey)}', ${tool.is_external ? 'true' : 'false'}, '${escapeHtml(tool.external_mcp || '')}', event)">
|
||||
<div class="tool-item-name">
|
||||
${escapeHtml(tool.name)}
|
||||
${externalBadge}
|
||||
<span class="tool-expand-icon">▶</span>
|
||||
</div>
|
||||
<div class="tool-item-desc">${escapeHtml(tool.description || (typeof window.t === 'function' ? window.t('mcp.noDescription') : '无描述'))}</div>
|
||||
<div class="tool-item-detail" style="display:none"></div>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="tool-detail-desc">${escapeHtml(fullDesc)}</div>
|
||||
<div class="tool-detail-section-title">参数定义</div>
|
||||
<div style="color:var(--text-tertiary);font-size:0.8125rem;padding:4px 0;">加载中...</div>
|
||||
`;
|
||||
|
||||
// 解析工具名(外部工具 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 = `<table class="tool-schema-table">
|
||||
<thead><tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr></thead>
|
||||
<tbody>`;
|
||||
paramKeys.forEach(key => {
|
||||
const p = props[key] || {};
|
||||
const type = p.type || (p.enum ? 'enum' : '—');
|
||||
const isReq = required.includes(key);
|
||||
const desc = p.description || '';
|
||||
schemaHTML += `<tr>
|
||||
<td><code>${escapeHtml(key)}</code></td>
|
||||
<td>${escapeHtml(String(type))}</td>
|
||||
<td>${isReq ? '<span style="color:#28a745">✔</span>' : ''}</td>
|
||||
<td>${escapeHtml(desc)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
schemaHTML += '</tbody></table>';
|
||||
}
|
||||
}
|
||||
if (!schemaHTML) {
|
||||
schemaHTML = '<div style="color:var(--text-tertiary);font-size:0.8125rem;padding:4px 0;">无参数定义</div>';
|
||||
}
|
||||
detail.innerHTML = `
|
||||
<div class="tool-detail-desc">${escapeHtml(fullDesc)}</div>
|
||||
<div class="tool-detail-section-title">参数定义</div>
|
||||
${schemaHTML}
|
||||
`;
|
||||
})
|
||||
.catch(() => {
|
||||
detail.innerHTML = `
|
||||
<div class="tool-detail-desc">${escapeHtml(fullDesc)}</div>
|
||||
<div class="tool-detail-section-title">参数定义</div>
|
||||
<div style="color:var(--text-tertiary);font-size:0.8125rem;padding:4px 0;">加载失败</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部工具徽章跳转到对应的外部 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) {
|
||||
<span class="external-mcp-status ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="external-mcp-item-actions">
|
||||
${status === 'connected' || status === 'disconnected' || status === 'error' ?
|
||||
${status === 'connected' || status === 'disconnected' || status === 'error' || status === 'disabled' ?
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" onclick="toggleExternalMCP('${escapeHtml(name)}', '${status}')" title="${status === 'connected' ? statusT('mcp.stopConnection') : statusT('mcp.startConnection')}">
|
||||
${status === 'connected' ? '⏸ ' + statusT('mcp.stop') : '▶ ' + statusT('mcp.start')}
|
||||
</button>` :
|
||||
status === 'connecting' ?
|
||||
</button>` :
|
||||
status === 'connecting' ?
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" disabled style="opacity: 0.6; cursor: not-allowed;">
|
||||
⏳ ${statusT('mcp.connecting')}
|
||||
</button>` : ''}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user