From fa9229b435f8ab5187128d432a662f4d34c266b0 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 Jul 2026 20:42:43 +0800 Subject: [PATCH] Add files via upload --- config.example.yaml | 7 +- web/static/css/style.css | 15 ++++ web/static/i18n/en-US.json | 14 ++++ web/static/i18n/zh-CN.json | 14 ++++ web/static/js/auth.js | 4 +- web/static/js/chat.js | 55 ++++++++---- web/static/js/monitor.js | 166 +++++++++++++++++++++++++++++-------- web/static/js/settings.js | 23 ++++- web/static/js/webshell.js | 33 ++++++-- web/templates/index.html | 28 +++++++ 10 files changed, 296 insertions(+), 63 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 09aa1db2..47c76845 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -107,6 +107,11 @@ shodan: agent: max_iterations: 12000 # 全局最大迭代次数(单代理 / Deep / Supervisor / Plan-Execute 主执行器 / 子代理均沿用;agents/*.md 中 max_iterations>0 可单独覆盖) tool_timeout_minutes: 60 # 单次工具执行最大时长(分钟),超时自动终止;0 表示不限制(不推荐,易出现长时间挂起) + tool_wait_timeout_seconds: 300 # 工具本轮最多等待(秒);到时返回 execution_id,worker 继续后台执行,可用 wait_tool_execution 继续等待;0=等到完成 + external_mcp_max_concurrent_per_server: 5 # 单个外部 MCP server 同时运行的工具数;0=默认2;负数=不限制 + external_mcp_max_concurrent_total: 16 # 所有外部 MCP 工具全局并发上限;0=默认16;负数=不限制 + external_mcp_circuit_failure_threshold: 5 # 单个外部 MCP server 连续失败多少次后熔断;0=默认3;负数=关闭熔断 + external_mcp_circuit_cooldown_seconds: 60 # 熔断冷却秒数;0=默认60 shell_no_output_timeout_seconds: 1200 # execute/exec 连续无新输出则终止(秒);通用防挂死;0=默认300;-1=关闭 workspace_root_dir: "" # 会话工作目录根路径(curl/wget 下载、read_file/glob/grep 本地分析);空=tmp/workspace,其下按 projects/{id} 或 conversations/{id} 隔离;勿用系统 /tmp # system_prompt_path: prompts/single-agent.md # 可选:单代理系统提示文件(相对本配置文件所在目录);非空且可读时替换内置提示 @@ -214,7 +219,7 @@ multi_agent: tool_search_enable: true # true:工具数 ≥ min 时启用 tool_search,仅前 N 个工具常驻,其余按正则按需解锁,省 token、减误选;false:全量工具进上下文 tool_search_min_tools: 20 # 达到该数量才启用 tool_search(避免工具很少时多此一举);与 always_visible 配合使用 tool_search_always_visible: 12 # 始终直接暴露给模型的工具个数(顺序与角色工具列表一致);其余工具进入动态池,需 tool_search 解锁 - tool_search_always_visible_tools: [read_file, glob, grep, analyze_image, write_file, edit_file, execute, task, transfer_to_agent, exit, write_todos, skill, tool_search, TaskCreate, TaskGet, TaskUpdate, TaskList, record_vulnerability, list_vulnerabilities, get_vulnerability, list_knowledge_risk_types, search_knowledge_base, webshell_exec, webshell_file_list, webshell_file_read, webshell_file_write, manage_webshell_list, manage_webshell_add, manage_webshell_update, manage_webshell_delete, manage_webshell_test, batch_task_list, batch_task_get, batch_task_start, batch_task_rerun, batch_task_pause, batch_task_update_metadata, batch_task_update_schedule, batch_task_schedule_enabled, batch_task_update_task, batch_task_remove_task, batch_task_delete, batch_task_create, batch_task_add_task, http-framework-test, exec] # 后端内置常驻工具白名单(优先于 always_visible 数量策略) + tool_search_always_visible_tools: [read_file, glob, grep, analyze_image, write_file, edit_file, execute, task, transfer_to_agent, exit, write_todos, skill, tool_search, TaskCreate, TaskGet, TaskUpdate, TaskList, record_vulnerability, list_vulnerabilities, get_vulnerability, get_tool_execution, wait_tool_execution, cancel_tool_execution, list_knowledge_risk_types, search_knowledge_base, webshell_exec, webshell_file_list, webshell_file_read, webshell_file_write, manage_webshell_list, manage_webshell_add, manage_webshell_update, manage_webshell_delete, manage_webshell_test, batch_task_list, batch_task_get, batch_task_start, batch_task_rerun, batch_task_pause, batch_task_update_metadata, batch_task_update_schedule, batch_task_schedule_enabled, batch_task_update_task, batch_task_remove_task, batch_task_delete, batch_task_create, batch_task_add_task, http-framework-test, exec] # 后端内置常驻工具白名单(优先于 always_visible 数量策略) plantask_enable: true # P0:主代理挂载 TaskCreate/Get/Update/List 结构化任务板;需 eino_skills 可用且 skills_dir 存在 plantask_rel_dir: .eino/plantask # 任务文件相对 skills_dir,按会话分子目录:skills/.eino/plantask// reduction_enable: true # true:大工具输出截断/落盘以控上下文;后端会独立创建,不依赖 eino_skills 是否启用 diff --git a/web/static/css/style.css b/web/static/css/style.css index 4557a3f8..7f12e724 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -14014,16 +14014,31 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { color: var(--accent-color); } +.monitor-status-chip.queued { + background: rgba(124, 58, 237, 0.12); + color: #7c3aed; +} + .monitor-status-chip.failed { background: rgba(220, 53, 69, 0.12); color: var(--error-color); } +.monitor-status-chip.hard_timeout { + background: rgba(245, 158, 11, 0.14); + color: #b45309; +} + .monitor-status-chip.cancelled { background: rgba(108, 117, 125, 0.15); color: var(--text-muted, #6c757d); } +.monitor-status-chip.orphaned { + background: rgba(100, 116, 139, 0.14); + color: #64748b; +} + .monitor-execution-actions .btn-monitor-abort { border-color: rgba(253, 126, 20, 0.55); color: #fd7e14; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index b96bbb23..1df1ae23 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -854,6 +854,7 @@ "executionId": "Execution ID:", "noResult": "No result", "running": "Running...", + "backgroundRunning": "Running in background", "completed": "Completed", "execFailed": "Execution failed", "resultMissing": "Result record missing", @@ -2139,11 +2140,14 @@ "mcpMonitor": { "deselectAll": "Deselect all", "statusPending": "Pending", + "statusQueued": "Queued", "statusSuccess": "Succeeded", "statusCompleted": "Completed", "statusRunning": "Running", "statusFailed": "Failed", + "statusHardTimeout": "Timed out", "statusCancelled": "Cancelled", + "statusOrphaned": "Orphaned", "terminateExecution": "Stop", "loading": "Loading...", "noStatsData": "No statistical data", @@ -2595,6 +2599,16 @@ "fofaApiKeyHint": "Stored in server config (config.yaml) only.", "maxIterations": "Max iterations", "iterationsPlaceholder": "30", + "toolWaitTimeoutSeconds": "Tool wait seconds", + "toolWaitTimeoutSecondsHint": "Per-turn wait limit for tools. When reached, the agent receives an execution_id while the worker keeps running; use wait_tool_execution to continue waiting. 0 waits until completion.", + "externalMcpConcurrencyServer": "Per-MCP concurrency limit", + "externalMcpConcurrencyServerHint": "Concurrent tool runs per external MCP server. 0 uses default 2; -1 disables the limit.", + "externalMcpConcurrencyTotal": "Global external MCP concurrency", + "externalMcpConcurrencyTotalHint": "Total concurrent external MCP tool runs. 0 uses default 16; -1 disables the limit.", + "externalMcpCircuitThreshold": "MCP circuit failure threshold", + "externalMcpCircuitThresholdHint": "Temporarily opens the circuit after this many consecutive failures for one MCP server. 0 uses default 3; -1 disables circuit breaking.", + "externalMcpCircuitCooldown": "MCP circuit cooldown seconds", + "externalMcpCircuitCooldownHint": "How long a server stays circuit-open. 0 uses default 60 seconds.", "enableMultiAgent": "Enable Eino multi-agent", "enableMultiAgentHint": "After enabling, the chat page can use multi-agent mode; sub-agents are set in multi_agent.sub_agents or the agents/ directory. Orchestration is configured below.", "multiAgentOrchestration": "Multi-agent orchestration", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index c415e7c1..c1926ad6 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -842,6 +842,7 @@ "executionId": "执行ID:", "noResult": "无结果", "running": "执行中...", + "backgroundRunning": "后台执行中", "completed": "已完成", "execFailed": "执行失败", "resultMissing": "结果记录缺失", @@ -2127,11 +2128,14 @@ "mcpMonitor": { "deselectAll": "取消全选", "statusPending": "等待中", + "statusQueued": "排队中", "statusSuccess": "成功", "statusCompleted": "已完成", "statusRunning": "执行中", "statusFailed": "失败", + "statusHardTimeout": "执行超时", "statusCancelled": "已终止", + "statusOrphaned": "孤儿任务", "terminateExecution": "终止", "loading": "加载中...", "noStatsData": "暂无统计数据", @@ -2583,6 +2587,16 @@ "fofaApiKeyHint": "仅保存在服务器配置中(`config.yaml`)。", "maxIterations": "最大迭代次数", "iterationsPlaceholder": "30", + "toolWaitTimeoutSeconds": "工具单轮等待秒数", + "toolWaitTimeoutSecondsHint": "Agent 等待工具的单轮上限;到时返回 execution_id,worker 继续后台执行,可用 wait_tool_execution 继续等待。0 表示等到完成。", + "externalMcpConcurrencyServer": "单 MCP 并发上限", + "externalMcpConcurrencyServerHint": "每个外部 MCP server 同时运行的工具数;0 使用默认 2,-1 不限制。", + "externalMcpConcurrencyTotal": "外部 MCP 全局并发上限", + "externalMcpConcurrencyTotalHint": "所有外部 MCP 工具的总并发;0 使用默认 16,-1 不限制。", + "externalMcpCircuitThreshold": "MCP 熔断失败阈值", + "externalMcpCircuitThresholdHint": "同一 MCP server 连续失败达到阈值后临时熔断;0 使用默认 3,-1 关闭熔断。", + "externalMcpCircuitCooldown": "MCP 熔断冷却秒数", + "externalMcpCircuitCooldownHint": "server 熔断后的等待时间;0 使用默认 60 秒。", "enableMultiAgent": "启用 Eino 多代理", "enableMultiAgentHint": "开启后对话页可选「多代理」模式;子代理在 multi_agent.sub_agents 或 agents 目录配置;编排方式见下方「预置编排」。", "multiAgentOrchestration": "多代理预置编排", diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 7013aacc..d1b263de 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -604,10 +604,10 @@ function setUserMenuOpen(open) { function getStatusText(status) { const s = (status && String(status).toLowerCase()) || ''; if (typeof window.t !== 'function') { - const fallback = { pending: '等待中', running: '执行中', completed: '已完成', failed: '失败', cancelled: '已终止' }; + const fallback = { pending: '等待中', queued: '排队中', running: '执行中', background_running: '后台执行中', completed: '已完成', failed: '失败', cancelled: '已终止', hard_timeout: '硬超时', orphaned: '孤儿记录' }; return fallback[s] || status; } - const keyMap = { pending: 'mcpDetailModal.statusPending', running: 'mcpDetailModal.statusRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed', cancelled: 'mcpDetailModal.statusCancelled' }; + const keyMap = { pending: 'mcpDetailModal.statusPending', queued: 'mcpDetailModal.statusQueued', running: 'mcpDetailModal.statusRunning', background_running: 'timeline.backgroundRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed', cancelled: 'mcpDetailModal.statusCancelled', hard_timeout: 'mcpMonitor.statusHardTimeout', orphaned: 'mcpMonitor.statusOrphaned' }; const key = keyMap[s]; return key ? window.t(key) : status; } diff --git a/web/static/js/chat.js b/web/static/js/chat.js index bb9d8777..c676b53e 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2821,9 +2821,18 @@ function renderProcessDetails(messageId, processDetails, options) { itemTitle = agPx + '🔧 ' + callTitle; } else if (eventType === 'tool_result') { const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); - const success = data.success !== false; - const statusIcon = success ? '✅' : '❌'; - const execText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行失败'); + const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; + const result = data.result != null ? data.result : (data.error != null ? data.error : (data.resultPreview != null ? data.resultPreview : noResultText)); + const resultStr = typeof result === 'string' ? result : JSON.stringify(result); + const displayState = typeof window.getToolResultDisplayState === 'function' + ? window.getToolResultDisplayState(data, { rawText: resultStr }) + : { kind: (data.success !== false ? 'success' : 'error'), isError: data.success === false }; + const backgroundRunning = displayState.kind === 'background_running'; + const success = !displayState.isError && !backgroundRunning; + const statusIcon = backgroundRunning ? '⏳' : (success ? '✅' : '❌'); + const execText = backgroundRunning + ? ((typeof window.getBackgroundRunningToolLabel === 'function' ? window.getBackgroundRunningToolLabel() : '后台执行中') + ': ' + escapeHtml(toolName)) + : (success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行失败')); let execLine = statusIcon + ' ' + execText; if (toolName === BuiltinTools.SEARCH_KNOWLEDGE_BASE && success) { execLine = '📚 ' + execLine + ' - ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrievalTag') : '知识检索'); @@ -2862,11 +2871,17 @@ function renderProcessDetails(messageId, processDetails, options) { processDetailId: detail.id || '', createdAt: detail.createdAt }; - if (eventType === 'tool_call' && detail.id && toolStatusByProcessDetailId.has(String(detail.id))) { - timelineOpts.toolStatus = toolStatusByProcessDetailId.get(String(detail.id)); - } if (eventType === 'tool_call' && data._mergedResult) { timelineOpts.mergedResult = data._mergedResult; + if (typeof window.getToolResultDisplayState === 'function') { + const displayState = window.getToolResultDisplayState(data._mergedResult); + if (displayState && displayState.kind === 'background_running') { + timelineOpts.toolStatus = 'background_running'; + } + } + } + if (!timelineOpts.toolStatus && eventType === 'tool_call' && detail.id && toolStatusByProcessDetailId.has(String(detail.id))) { + timelineOpts.toolStatus = toolStatusByProcessDetailId.get(String(detail.id)); } const itemId = addTimelineItem(timeline, eventType, timelineOpts); if (prependMode && itemId) { @@ -3537,11 +3552,14 @@ async function fetchProcessDetailDataForModal(detailId) { function processToolResultToMCPResult(resultData, rawText) { if (!resultData) return null; - const isError = !!(resultData.isError || resultData.success === false); + const displayState = typeof window.getToolResultDisplayState === 'function' + ? window.getToolResultDisplayState(resultData, { rawText: rawText }) + : { kind: ((resultData.isError || resultData.success === false) ? 'error' : 'success'), isError: !!(resultData.isError || resultData.success === false) }; + const isError = !!displayState.isError; let text = rawText; if (text == null || String(text) === '') { const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; - const result = resultData.result != null ? resultData.result : (resultData.error != null ? resultData.error : noResultText); + const result = resultData.result != null ? resultData.result : (resultData.error != null ? resultData.error : (resultData.resultPreview != null ? resultData.resultPreview : noResultText)); text = typeof result === 'string' ? result : JSON.stringify(result); } return { @@ -3577,10 +3595,16 @@ async function showMCPDetailFromProcessToolItem(messageElement, summary, index) resultData = await fetchProcessDetailDataForModal(resultDetailId); } const toolName = (summary && summary.toolName) || target.dataset.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); - const success = resultData ? !(resultData.isError || resultData.success === false) : target.dataset.toolSuccess !== '0'; - const status = resultData || target.classList.contains('tool-call-completed') || target.classList.contains('tool-call-failed') - ? (success ? 'completed' : 'failed') - : 'running'; + const displayState = resultData && typeof window.getToolResultDisplayState === 'function' + ? window.getToolResultDisplayState(resultData, { rawText: rawText }) + : null; + const backgroundRunning = (displayState && displayState.kind === 'background_running') || target.dataset.toolDisplayStatus === 'background_running'; + const success = resultData ? !(displayState ? displayState.isError : (resultData.isError || resultData.success === false)) : target.dataset.toolSuccess !== '0'; + const status = backgroundRunning + ? 'background_running' + : (resultData || target.classList.contains('tool-call-completed') || target.classList.contains('tool-call-failed') + ? (success ? 'completed' : 'failed') + : 'running'); const exec = { id: (resultData && resultData.executionId) || (summary && summary.executionId) || '', toolName: toolName, @@ -3898,7 +3922,8 @@ function renderMCPDetailModal(exec) { const statusEl = document.getElementById('detail-status'); const normalizedStatus = (exec.status || 'unknown').toLowerCase(); statusEl.textContent = getStatusText(exec.status); - statusEl.className = `status-chip status-${normalizedStatus}`; + const statusClass = normalizedStatus === 'background_running' ? 'running' : normalizedStatus; + statusEl.className = `status-chip status-${statusClass}`; try { statusEl.dataset.detailStatus = (exec.status || '') + ''; } catch (e) { /* ignore */ } @@ -3960,7 +3985,7 @@ function renderMCPDetailModal(exec) { setMCPResultDetailTabs('success', true); } } else { - if (normalizedStatus === 'running') { + if (normalizedStatus === 'running' || normalizedStatus === 'background_running') { responseElement.textContent = typeof window.t === 'function' ? window.t('mcpDetailModal.runningNoResponseYet') : '尚无返回,工具可能仍在执行。若长时间无响应,可在下方终止本次调用。'; } else { responseElement.textContent = typeof window.t === 'function' ? window.t('chat.noResponseData') : '暂无响应数据'; @@ -3971,7 +3996,7 @@ function renderMCPDetailModal(exec) { const abortSection = document.getElementById('detail-abort-section'); const abortBtn = document.getElementById('detail-abort-btn'); if (abortSection && abortBtn) { - if (normalizedStatus === 'running' && exec.id) { + if ((normalizedStatus === 'running' || normalizedStatus === 'background_running') && exec.id) { abortSection.style.display = 'block'; abortBtn.dataset.execId = exec.id || ''; abortBtn.textContent = typeof window.t === 'function' ? window.t('mcpDetailModal.abortBtn') : '终止工具'; diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 34f3eeca..b83a791c 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -2567,9 +2567,14 @@ function handleStreamEvent(event, progressElement, progressId, const resultInfo = event.data || {}; const resultToolName = resultInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); const success = resultInfo.success !== false; - const statusIcon = success ? '✅' : '❌'; + const resultDisplayState = getToolResultDisplayState(resultInfo, { rawText: event.message || '' }); + const backgroundRunning = resultDisplayState.kind === 'background_running'; + const statusIcon = backgroundRunning ? '⏳' : (success ? '✅' : '❌'); const resultToolCallId = resultInfo.toolCallId || null; - const resultExecText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行失败'); + const resultStatusForCall = backgroundRunning ? 'background_running' : (success ? 'completed' : 'failed'); + const resultExecText = backgroundRunning + ? (getBackgroundRunningToolLabel() + ': ' + escapeHtml(resultToolName)) + : (success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行失败')); if (resultToolCallId) { const key = toolResultStreamKey(progressId, resultToolCallId); @@ -2582,24 +2587,24 @@ function handleStreamEvent(event, progressElement, progressId, toolResultStreamStateByKey.delete(key); const mapKey = toolCallMapKey(progressId, resultToolCallId); if (toolCallStatusMap.has(mapKey)) { - updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed'); - toolCallStatusMap.get(mapKey).terminalStatus = success ? 'completed' : 'failed'; + updateToolCallStatus(progressId, resultToolCallId, resultStatusForCall); + toolCallStatusMap.get(mapKey).terminalStatus = resultStatusForCall; } break; } if (attachToolResultToCall(progressId, resultToolCallId, resultInfo)) { const mapKey = toolCallMapKey(progressId, resultToolCallId); if (toolCallStatusMap.has(mapKey)) { - updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed'); - toolCallStatusMap.get(mapKey).terminalStatus = success ? 'completed' : 'failed'; + updateToolCallStatus(progressId, resultToolCallId, resultStatusForCall); + toolCallStatusMap.get(mapKey).terminalStatus = resultStatusForCall; } break; } } if (resultToolCallId && toolCallStatusMap.has(toolCallMapKey(progressId, resultToolCallId))) { - updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed'); - toolCallStatusMap.get(toolCallMapKey(progressId, resultToolCallId)).terminalStatus = success ? 'completed' : 'failed'; + updateToolCallStatus(progressId, resultToolCallId, resultStatusForCall); + toolCallStatusMap.get(toolCallMapKey(progressId, resultToolCallId)).terminalStatus = resultStatusForCall; } addTimelineItem(timeline, 'tool_result', { title: timelineAgentBracketPrefix(resultInfo) + statusIcon + ' ' + resultExecText, @@ -3770,6 +3775,66 @@ function formatToolCallTimelineTitle(toolName, index, total) { return '调用工具: ' + name + (tot ? ' (' + idx + '/' + tot + ')' : ''); } +function collectToolResultTextParts(value, parts, depth) { + if (value == null || depth > 4) return; + if (typeof value === 'string') { + parts.push(value); + return; + } + if (typeof value !== 'object') return; + if (Array.isArray(value)) { + value.forEach(function (v) { collectToolResultTextParts(v, parts, depth + 1); }); + return; + } + if (typeof value.text === 'string') parts.push(value.text); + if (typeof value.result === 'string') parts.push(value.result); + if (typeof value.error === 'string') parts.push(value.error); + if (value.content != null) collectToolResultTextParts(value.content, parts, depth + 1); +} + +function getToolResultDisplayState(data, opts) { + opts = opts || {}; + data = data || {}; + const toolName = String(data.toolName || data.name || '').trim().toLowerCase(); + const isObservationTool = toolName === 'wait_tool_execution' || toolName === 'get_tool_execution'; + const explicitStatus = String(data.displayStatus || data.status || '').toLowerCase(); + if (explicitStatus === 'background_running') { + if (isObservationTool) { + return { kind: 'success', isError: false, success: true }; + } + return { kind: 'background_running', isError: false, success: false }; + } + const parts = []; + if (opts.rawText != null) parts.push(String(opts.rawText)); + collectToolResultTextParts(data.result, parts, 0); + collectToolResultTextParts(data.error, parts, 0); + collectToolResultTextParts(data.content, parts, 0); + if (data.executionId != null) parts.push('execution_id: ' + String(data.executionId)); + if (data.status != null) parts.push('status: ' + String(data.status)); + const text = parts.join('\n'); + const errorLike = data.isError === true || data.success === false; + const hasExecutionId = !!data.executionId || + /execution[_-]?id\\?["']?\s*[:=]\s*\\?["']?[0-9a-f]{8}-[0-9a-f-]{12,}/i.test(text); + const hasRunningStatus = /status\\?["']?\s*[:=]\s*\\?["']?(running|queued)\\?["']?/i.test(text) || + /\bstatus:\s*(running|queued)\b/i.test(text); + const hasSoftWaitSignal = /(工具已提交到后台执行|本次等待已到达|wait_timeout|wait timeout|background execution|后台执行|仍未完成|still running)/i.test(text); + if (errorLike && hasExecutionId && hasRunningStatus && hasSoftWaitSignal) { + if (isObservationTool) { + return { kind: 'success', isError: false, success: true }; + } + return { kind: 'background_running', isError: false, success: false }; + } + return { kind: errorLike ? 'error' : 'success', isError: errorLike, success: !errorLike }; +} + +function getBackgroundRunningToolLabel() { + if (typeof window.t === 'function') { + const translated = window.t('timeline.backgroundRunning'); + if (translated && translated !== 'timeline.backgroundRunning') return translated; + } + return '后台执行中'; +} + function buildToolResultSectionHtml(data, opts) { opts = opts || {}; const _t = function (k, o) { @@ -3786,13 +3851,14 @@ function buildToolResultSectionHtml(data, opts) { '' ); } - const isError = data.isError || data.success === false; const noResultText = _t('timeline.noResult'); - const result = data.result != null ? data.result : (data.error != null ? data.error : noResultText); + const result = data.result != null ? data.result : (data.error != null ? data.error : (data.resultPreview != null ? data.resultPreview : noResultText)); const resultStr = typeof result === 'string' ? result : JSON.stringify(result); const rawText = opts.rawText != null ? String(opts.rawText) : resultStr; + const displayState = getToolResultDisplayState(data, { rawText: rawText }); + const sectionClass = displayState.kind === 'background_running' ? 'pending' : (displayState.isError ? 'error' : 'success'); return ( - '
' + + '
' + '' + escapeHtml(execResultLabel) + '' + '
' + escapeHtml(rawText) + '
' + (data.executionId ? '
' + @@ -3970,11 +4036,12 @@ function ensureToolCallResultSlot(item) { function mergeToolResultIntoCallItem(item, data, options) { if (!item || !data) return false; options = options || {}; - const isError = data.isError || data.success === false; const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; - const result = data.result != null ? data.result : (data.error != null ? data.error : noResultText); + const result = data.result != null ? data.result : (data.error != null ? data.error : (data.resultPreview != null ? data.resultPreview : noResultText)); const resultStr = typeof result === 'string' ? result : JSON.stringify(result); const text = options.rawText != null ? String(options.rawText) : resultStr; + const displayState = getToolResultDisplayState(data, { rawText: text }); + const backgroundRunning = displayState.kind === 'background_running'; if (item.classList.contains('tool-call-collapsible')) { const state = toolCallDetailStateByItemId.get(item.id) || {}; @@ -3982,6 +4049,8 @@ function mergeToolResultIntoCallItem(item, data, options) { state.rawText = text; state.resultDetailId = data.processDetailId || state.resultDetailId || ''; state.pending = false; + state.payloadDeferred = data._payloadDeferred === true; + state.payloadLoaded = data._payloadDeferred !== true; setToolCallDetailState(item, state); const expanded = item.classList.contains('tool-call-detail-expanded'); const content = item.querySelector('.timeline-item-content.tool-call-detail-content'); @@ -3991,9 +4060,10 @@ function mergeToolResultIntoCallItem(item, data, options) { renderToolCallDetailContent(item); } item.dataset.toolResultMerged = '1'; - item.dataset.toolSuccess = data.success !== false ? '1' : '0'; - item.classList.remove('tool-call-running'); - item.classList.add(data.success !== false ? 'tool-call-completed' : 'tool-call-failed'); + item.dataset.toolSuccess = (!displayState.isError && !backgroundRunning) ? '1' : '0'; + item.dataset.toolDisplayStatus = backgroundRunning ? 'background_running' : (displayState.isError ? 'failed' : 'completed'); + item.classList.remove('tool-call-running', 'tool-call-completed', 'tool-call-failed'); + item.classList.add(backgroundRunning ? 'tool-call-running' : (displayState.isError ? 'tool-call-failed' : 'tool-call-completed')); return true; } @@ -4005,7 +4075,7 @@ function mergeToolResultIntoCallItem(item, data, options) { if (!section) return false; section.classList.remove('pending'); - section.className = 'tool-result-section ' + (isError ? 'error' : 'success'); + section.className = 'tool-result-section ' + (backgroundRunning ? 'pending' : (displayState.isError ? 'error' : 'success')); const pre = section.querySelector('pre.tool-result'); if (pre) { pre.classList.remove('tool-result-pending'); @@ -4029,9 +4099,10 @@ function mergeToolResultIntoCallItem(item, data, options) { } item.dataset.toolResultMerged = '1'; - item.dataset.toolSuccess = data.success !== false ? '1' : '0'; - item.classList.remove('tool-call-running'); - item.classList.add(data.success !== false ? 'tool-call-completed' : 'tool-call-failed'); + item.dataset.toolSuccess = (!displayState.isError && !backgroundRunning) ? '1' : '0'; + item.dataset.toolDisplayStatus = backgroundRunning ? 'background_running' : (displayState.isError ? 'failed' : 'completed'); + item.classList.remove('tool-call-running', 'tool-call-completed', 'tool-call-failed'); + item.classList.add(backgroundRunning ? 'tool-call-running' : (displayState.isError ? 'tool-call-failed' : 'tool-call-completed')); return true; } @@ -4138,6 +4209,8 @@ window.attachToolResultToCall = attachToolResultToCall; window.mergeToolResultIntoCallItem = mergeToolResultIntoCallItem; window.formatToolCallTimelineTitle = formatToolCallTimelineTitle; window.parseToolCallArgsFromData = parseToolCallArgsFromData; +window.getToolResultDisplayState = getToolResultDisplayState; +window.getBackgroundRunningToolLabel = getBackgroundRunningToolLabel; window.buildToolResultSectionHtml = buildToolResultSectionHtml; // 更新工具调用状态 @@ -4157,10 +4230,13 @@ function updateToolCallStatus(progressId, toolCallId, status) { const runningLabel = typeof window.t === 'function' ? window.t('timeline.running') : '执行中...'; const completedLabel = typeof window.t === 'function' ? window.t('timeline.completed') : '已完成'; const failedLabel = typeof window.t === 'function' ? window.t('timeline.execFailed') : '执行失败'; + const backgroundRunningLabel = getBackgroundRunningToolLabel(); let statusText = ''; - if (status === 'running') { + if (status === 'running' || status === 'background_running') { item.classList.add('tool-call-running'); - statusText = ' ' + escapeHtml(runningLabel) + ''; + statusText = ' ' + + escapeHtml(status === 'background_running' ? backgroundRunningLabel : runningLabel) + + ''; } else if (status === 'completed') { item.classList.add('tool-call-completed'); statusText = ' ✅ ' + escapeHtml(completedLabel) + ''; @@ -4319,11 +4395,14 @@ function addTimelineItem(timeline, type, options) { item.dataset.toolCallId = String(d.toolCallId).trim(); } const merged = options.mergedResult || d._mergedResult; + const mergedDisplayState = merged ? getToolResultDisplayState(merged) : null; + const mergedBackgroundRunning = mergedDisplayState && mergedDisplayState.kind === 'background_running'; const terminalStatus = String(options.toolStatus || '').toLowerCase(); if (merged) { item.dataset.toolResultMerged = '1'; - item.dataset.toolSuccess = merged.success !== false ? '1' : '0'; - item.classList.add(merged.success !== false ? 'tool-call-completed' : 'tool-call-failed'); + item.dataset.toolSuccess = (!mergedDisplayState.isError && !mergedBackgroundRunning) ? '1' : '0'; + item.dataset.toolDisplayStatus = mergedBackgroundRunning ? 'background_running' : (mergedDisplayState.isError ? 'failed' : 'completed'); + item.classList.add(mergedBackgroundRunning ? 'tool-call-running' : (mergedDisplayState.isError ? 'tool-call-failed' : 'tool-call-completed')); if (d._mergedResultDetailId) { item.dataset.toolResultDetailId = String(d._mergedResultDetailId); } @@ -4346,11 +4425,16 @@ function addTimelineItem(timeline, type, options) { } if (type === 'tool_result' && options.data) { const d = options.data; + const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; + const result = d.result != null ? d.result : (d.error != null ? d.error : (d.resultPreview != null ? d.resultPreview : noResultText)); + const resultStr = typeof result === 'string' ? result : JSON.stringify(result); + const displayState = getToolResultDisplayState(d, { rawText: resultStr }); if (options.processDetailId) { item.dataset.processDetailId = String(options.processDetailId); } item.dataset.toolName = (d.toolName != null && d.toolName !== '') ? String(d.toolName) : ''; - item.dataset.toolSuccess = d.success !== false ? '1' : '0'; + item.dataset.toolSuccess = (!displayState.isError && displayState.kind !== 'background_running') ? '1' : '0'; + item.dataset.toolDisplayStatus = displayState.kind === 'background_running' ? 'background_running' : (displayState.isError ? 'failed' : 'completed'); } if (options.data && options.data.einoAgent != null && String(options.data.einoAgent).trim() !== '') { item.dataset.einoAgent = String(options.data.einoAgent).trim(); @@ -4406,15 +4490,13 @@ function addTimelineItem(timeline, type, options) { const data = options.data; const args = parseToolCallArgsFromData(data); const merged = options.mergedResult || data._mergedResult; + const mergedDisplayState = merged ? getToolResultDisplayState(merged) : null; + const mergedBackgroundRunning = mergedDisplayState && mergedDisplayState.kind === 'background_running'; const terminalStatus = String(options.toolStatus || '').toLowerCase(); const hasTerminalStatus = terminalStatus === 'completed' || terminalStatus === 'failed'; const hasHistoricalStatus = hasTerminalStatus || terminalStatus === 'result_missing'; if (merged) { - if (merged.success !== false) { - item.classList.add('tool-call-completed'); - } else { - item.classList.add('tool-call-failed'); - } + item.classList.add(mergedBackgroundRunning ? 'tool-call-running' : (mergedDisplayState.isError ? 'tool-call-failed' : 'tool-call-completed')); } else if (hasTerminalStatus) { item.classList.add(terminalStatus === 'completed' ? 'tool-call-completed' : 'tool-call-failed'); } else if (terminalStatus === 'result_missing') { @@ -4462,10 +4544,10 @@ function addTimelineItem(timeline, type, options) { content += buildWorkflowBranchDetailHtml(options.data); } else if (type === 'tool_result' && options.data) { const data = options.data; - const isError = data.isError || !data.success; const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; - const result = data.result || data.error || noResultText; + const result = data.result || data.error || data.resultPreview || noResultText; const resultStr = typeof result === 'string' ? result : JSON.stringify(result); + const displayState = getToolResultDisplayState(data, { rawText: resultStr }); setToolCallDetailState(item, { resultData: data, rawText: resultStr, @@ -4476,7 +4558,8 @@ function addTimelineItem(timeline, type, options) { payloadDeferred: data._payloadDeferred === true, payloadLoaded: data._payloadDeferred !== true }); - item.classList.add(isError ? 'tool-call-failed' : 'tool-call-completed'); + item.dataset.toolDisplayStatus = displayState.kind === 'background_running' ? 'background_running' : (displayState.isError ? 'failed' : 'completed'); + item.classList.add(displayState.kind === 'background_running' ? 'tool-call-running' : (displayState.isError ? 'tool-call-failed' : 'tool-call-completed')); } else if (type === 'cancelled') { const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消'; content += ` @@ -6455,7 +6538,16 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { const deleteLabel = typeof window.t === 'function' ? window.t('mcpMonitor.delete') : '删除'; const deleteExecTitle = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecTitle') : '删除此执行记录'; const terminateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.terminateExecution') : '终止'; - const statusKeyMap = { pending: 'statusPending', running: 'statusRunning', completed: 'statusCompleted', failed: 'statusFailed', cancelled: 'statusCancelled' }; + const statusKeyMap = { + pending: 'statusPending', + queued: 'statusQueued', + running: 'statusRunning', + completed: 'statusCompleted', + failed: 'statusFailed', + cancelled: 'statusCancelled', + hard_timeout: 'statusHardTimeout', + orphaned: 'statusOrphaned' + }; const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined; const rowEntries = executions .map(exec => { @@ -6997,9 +7089,11 @@ function refreshProgressAndTimelineI18n() { titleSpan.textContent = ap + '\uD83D\uDD27 ' + callTitle; } else if (type === 'tool_result' && (item.dataset.toolName !== undefined || item.dataset.toolSuccess !== undefined)) { const name = (item.dataset.toolName != null && item.dataset.toolName !== '') ? item.dataset.toolName : _t('chat.unknownTool'); + const displayStatus = item.dataset.toolDisplayStatus || ''; + const backgroundRunning = displayStatus === 'background_running'; const success = item.dataset.toolSuccess === '1'; - const icon = success ? '\u2705 ' : '\u274C '; - titleSpan.textContent = ap + icon + (success ? _t('chat.toolExecComplete', { name: name }) : _t('chat.toolExecFailed', { name: name })); + const icon = backgroundRunning ? '\u23F3 ' : (success ? '\u2705 ' : '\u274C '); + titleSpan.textContent = ap + icon + (backgroundRunning ? (getBackgroundRunningToolLabel() + ': ' + name) : (success ? _t('chat.toolExecComplete', { name: name }) : _t('chat.toolExecFailed', { name: name }))); } else if (type === 'eino_agent_reply') { titleSpan.textContent = ap + '\uD83D\uDCAC ' + _t('chat.einoAgentReplyTitle'); } else if (type === 'cancelled') { diff --git a/web/static/js/settings.js b/web/static/js/settings.js index aaea3070..59edb33e 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -757,6 +757,22 @@ async function loadConfig(loadTools = true, options = {}) { // 填充Agent配置 document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30; + const toolWaitTimeoutEl = document.getElementById('agent-tool-wait-timeout-seconds'); + if (toolWaitTimeoutEl) { + const v = currentConfig.agent.tool_wait_timeout_seconds; + toolWaitTimeoutEl.value = (v !== undefined && v !== null && !Number.isNaN(Number(v))) ? String(Number(v)) : '60'; + } + [ + ['agent-external-mcp-concurrency-server', 'external_mcp_max_concurrent_per_server', '2'], + ['agent-external-mcp-concurrency-total', 'external_mcp_max_concurrent_total', '16'], + ['agent-external-mcp-circuit-threshold', 'external_mcp_circuit_failure_threshold', '3'], + ['agent-external-mcp-circuit-cooldown', 'external_mcp_circuit_cooldown_seconds', '60'] + ].forEach(([id, key, fallback]) => { + const el = document.getElementById(id); + if (!el) return; + const v = currentConfig.agent[key]; + el.value = (v !== undefined && v !== null && !Number.isNaN(Number(v))) ? String(Number(v)) : fallback; + }); const ma = currentConfig.multi_agent || {}; const maEn = document.getElementById('multi-agent-enabled'); @@ -1941,7 +1957,12 @@ async function applySettings() { audit_agent_prompt_review_edit: document.getElementById('hitl-audit-agent-prompt-review-edit-settings')?.value.trim() || '' }, agent: { - max_iterations: parseInt(document.getElementById('agent-max-iterations').value) || 30 + max_iterations: parseInt(document.getElementById('agent-max-iterations').value) || 30, + tool_wait_timeout_seconds: Math.max(0, parseInt(document.getElementById('agent-tool-wait-timeout-seconds')?.value || '60', 10) || 0), + external_mcp_max_concurrent_per_server: parseInt(document.getElementById('agent-external-mcp-concurrency-server')?.value || '2', 10) || 0, + external_mcp_max_concurrent_total: parseInt(document.getElementById('agent-external-mcp-concurrency-total')?.value || '16', 10) || 0, + external_mcp_circuit_failure_threshold: parseInt(document.getElementById('agent-external-mcp-circuit-threshold')?.value || '3', 10) || 0, + external_mcp_circuit_cooldown_seconds: Math.max(0, parseInt(document.getElementById('agent-external-mcp-circuit-cooldown')?.value || '60', 10) || 0) }, multi_agent: (function () { const peRaw = document.getElementById('multi-agent-pe-loop')?.value; diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index 420fa8ff..615b3bec 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -1947,9 +1947,20 @@ function buildWebshellTimelineItemFromDetail(detail) { : ((typeof window.t === 'function') ? window.t('chat.callTool', { name: tn, index: idx, total: total }) : ('调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : ''))); title = ap + '🔧 ' + wsCallTitle; } else if (eventType === 'tool_result') { - var success = data.success !== false; var tname = data.toolName || '工具'; - title = ap + (success ? '✅ ' : '❌ ') + ((typeof window.t === 'function') ? (success ? window.t('chat.toolExecComplete', { name: tname }) : window.t('chat.toolExecFailed', { name: tname })) : (tname + (success ? ' 执行完成' : ' 执行失败'))); + var wsNoResultText = (typeof window.t === 'function') ? window.t('timeline.noResult') : '无结果'; + var wsResult = data.result != null ? data.result : (data.error != null ? data.error : (data.resultPreview != null ? data.resultPreview : wsNoResultText)); + var wsResultStr = (typeof wsResult === 'string') ? wsResult : JSON.stringify(wsResult); + var wsDisplayState = (typeof window.getToolResultDisplayState === 'function') + ? window.getToolResultDisplayState(data, { rawText: wsResultStr }) + : { kind: ((data.isError || data.success === false) ? 'error' : 'success'), isError: (data.isError || data.success === false) }; + var wsBackgroundRunning = wsDisplayState.kind === 'background_running'; + var success = !wsDisplayState.isError && !wsBackgroundRunning; + var wsIcon = wsBackgroundRunning ? '⏳ ' : (success ? '✅ ' : '❌ '); + var wsLabel = wsBackgroundRunning + ? (((typeof window.getBackgroundRunningToolLabel === 'function') ? window.getBackgroundRunningToolLabel() : '后台执行中') + ': ' + tname) + : ((typeof window.t === 'function') ? (success ? window.t('chat.toolExecComplete', { name: tname }) : window.t('chat.toolExecFailed', { name: tname })) : (tname + (success ? ' 执行完成' : ' 执行失败'))); + title = ap + wsIcon + wsLabel; } else if (eventType === 'eino_agent_reply') { title = ap + '💬 ' + ((typeof window.t === 'function') ? window.t('chat.einoAgentReplyTitle') : '子代理回复'); } else if (eventType === 'progress') { @@ -1978,13 +1989,16 @@ function buildWebshellTimelineItemFromDetail(detail) { if (eventType === 'tool_call' && data && data._mergedResult && typeof window.buildToolResultSectionHtml === 'function') { html += '
' + window.buildToolResultSectionHtml(data._mergedResult) + '
'; } else if (eventType === 'tool_result' && data) { - var isError = data.isError || data.success === false; var noResultText = (typeof window.t === 'function') ? window.t('timeline.noResult') : '无结果'; - var result = data.result != null ? data.result : (data.error != null ? data.error : noResultText); + var result = data.result != null ? data.result : (data.error != null ? data.error : (data.resultPreview != null ? data.resultPreview : noResultText)); var resultStr = (typeof result === 'string') ? result : JSON.stringify(result); + var displayState = (typeof window.getToolResultDisplayState === 'function') + ? window.getToolResultDisplayState(data, { rawText: resultStr }) + : { kind: ((data.isError || data.success === false) ? 'error' : 'success'), isError: (data.isError || data.success === false) }; var execResultLabel = (typeof window.t === 'function') ? window.t('timeline.executionResult') : '执行结果:'; var execIdLabel = (typeof window.t === 'function') ? window.t('timeline.executionId') : '执行ID:'; - html += '
' + escapeHtml(execResultLabel) + '
' + escapeHtml(resultStr) + '
' + (data.executionId ? '
' + escapeHtml(execIdLabel) + ' ' + escapeHtml(String(data.executionId)) + '
' : '') + '
'; + var sectionClass = displayState.kind === 'background_running' ? 'pending' : (displayState.isError ? 'error' : 'success'); + html += '
' + escapeHtml(execResultLabel) + '
' + escapeHtml(resultStr) + '
' + (data.executionId ? '
' + escapeHtml(execIdLabel) + ' ' + escapeHtml(String(data.executionId)) + '
' : '') + '
'; } else if (detail.message && detail.message !== title) { html += '
' + escapeHtml(detail.message) + '
'; } @@ -3140,14 +3154,17 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { html += '
' + escapeHtml(message) + '
'; } else if (type === 'tool_result' && data) { // 工具调用出参 - var isError = data.isError || data.success === false; var noResultText = (typeof window.t === 'function') ? window.t('timeline.noResult') : '无结果'; - var result = data.result != null ? data.result : (data.error != null ? data.error : noResultText); + var result = data.result != null ? data.result : (data.error != null ? data.error : (data.resultPreview != null ? data.resultPreview : noResultText)); var resultStr = (typeof result === 'string') ? result : JSON.stringify(result); + var displayState = (typeof window.getToolResultDisplayState === 'function') + ? window.getToolResultDisplayState(data, { rawText: resultStr }) + : { kind: ((data.isError || data.success === false) ? 'error' : 'success'), isError: (data.isError || data.success === false) }; var execResultLabel = (typeof window.t === 'function') ? window.t('timeline.executionResult') : '执行结果:'; var execIdLabel = (typeof window.t === 'function') ? window.t('timeline.executionId') : '执行ID:'; + var sectionClass = displayState.kind === 'background_running' ? 'pending' : (displayState.isError ? 'error' : 'success'); html += '
' + escapeHtml(execResultLabel) + '
' +
                 escapeHtml(resultStr) +
                 '
' + diff --git a/web/templates/index.html b/web/templates/index.html index 124191e1..5a9e29a2 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1510,8 +1510,11 @@ + + +
@@ -3543,6 +3546,31 @@
+
+ + + Agent 等待工具的单轮上限;到时返回 execution_id,worker 继续后台执行,可用 wait_tool_execution 继续等待。0 表示等到完成。 +
+
+ + + 每个外部 MCP server 同时运行的工具数;0 使用默认 2,-1 不限制。 +
+
+ + + 所有外部 MCP 工具的总并发;0 使用默认 16,-1 不限制。 +
+
+ + + 同一 MCP server 连续失败达到阈值后临时熔断;0 使用默认 3,-1 关闭熔断。 +
+
+ + + server 熔断后的等待时间;0 使用默认 60 秒。 +