Add files via upload

This commit is contained in:
公明
2026-07-21 20:42:43 +08:00
committed by GitHub
parent b707ea4e44
commit fa9229b435
10 changed files with 296 additions and 63 deletions
+6 -1
View File
@@ -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_idworker 继续后台执行,可用 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/<conversationId>/
reduction_enable: true # true:大工具输出截断/落盘以控上下文;后端会独立创建,不依赖 eino_skills 是否启用
+15
View File
@@ -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;
+14
View File
@@ -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",
+14
View File
@@ -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_idworker 继续后台执行,可用 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": "多代理预置编排",
+2 -2
View File
@@ -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;
}
+40 -15
View File
@@ -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') : '终止工具';
+130 -36
View File
@@ -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) {
'</div>'
);
}
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 (
'<div class="tool-result-section ' + (isError ? 'error' : 'success') + '">' +
'<div class="tool-result-section ' + sectionClass + '">' +
'<strong data-i18n="timeline.executionResult">' + escapeHtml(execResultLabel) + '</strong>' +
'<pre class="tool-result">' + escapeHtml(rawText) + '</pre>' +
(data.executionId ? '<div class="tool-execution-id"><span data-i18n="timeline.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 = ' <span class="tool-status-badge tool-status-running">' + escapeHtml(runningLabel) + '</span>';
statusText = ' <span class="tool-status-badge tool-status-running">' +
escapeHtml(status === 'background_running' ? backgroundRunningLabel : runningLabel) +
'</span>';
} else if (status === 'completed') {
item.classList.add('tool-call-completed');
statusText = ' <span class="tool-status-badge tool-status-completed">✅ ' + escapeHtml(completedLabel) + '</span>';
@@ -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') {
+22 -1
View File
@@ -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;
+25 -8
View File
@@ -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 += '<div class="webshell-ai-timeline-msg tool-result-slot">' + window.buildToolResultSectionHtml(data._mergedResult) + '</div>';
} 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 += '<div class="webshell-ai-timeline-msg"><div class="tool-result-section ' + (isError ? 'error' : 'success') + '"><strong>' + escapeHtml(execResultLabel) + '</strong><pre class="tool-result">' + escapeHtml(resultStr) + '</pre>' + (data.executionId ? '<div class="tool-execution-id"><span>' + escapeHtml(execIdLabel) + '</span> <code>' + escapeHtml(String(data.executionId)) + '</code></div>' : '') + '</div></div>';
var sectionClass = displayState.kind === 'background_running' ? 'pending' : (displayState.isError ? 'error' : 'success');
html += '<div class="webshell-ai-timeline-msg"><div class="tool-result-section ' + sectionClass + '"><strong>' + escapeHtml(execResultLabel) + '</strong><pre class="tool-result">' + escapeHtml(resultStr) + '</pre>' + (data.executionId ? '<div class="tool-execution-id"><span>' + escapeHtml(execIdLabel) + '</span> <code>' + escapeHtml(String(data.executionId)) + '</code></div>' : '') + '</div></div>';
} else if (detail.message && detail.message !== title) {
html += '<div class="webshell-ai-timeline-msg">' + escapeHtml(detail.message) + '</div>';
}
@@ -3140,14 +3154,17 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
html += '<div class="webshell-ai-timeline-msg"><pre style="white-space:pre-wrap;">' + escapeHtml(message) + '</pre></div>';
} 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 += '<div class="webshell-ai-timeline-msg"><div class="tool-result-section ' +
(isError ? 'error' : 'success') +
sectionClass +
'"><strong>' + escapeHtml(execResultLabel) + '</strong><pre class="tool-result">' +
escapeHtml(resultStr) +
'</pre>' +
+28
View File
@@ -1510,8 +1510,11 @@
<option value="all" data-i18n="mcp.filterAll">全部</option>
<option value="completed" data-i18n="mcpMonitor.statusCompleted">已完成</option>
<option value="running" data-i18n="mcpMonitor.statusRunning">执行中</option>
<option value="queued" data-i18n="mcpMonitor.statusQueued">排队中</option>
<option value="failed" data-i18n="mcpMonitor.statusFailed">失败</option>
<option value="hard_timeout" data-i18n="mcpMonitor.statusHardTimeout">执行超时</option>
<option value="cancelled" data-i18n="mcpMonitor.statusCancelled">已终止</option>
<option value="orphaned" data-i18n="mcpMonitor.statusOrphaned">孤儿任务</option>
</select>
</label>
</div>
@@ -3543,6 +3546,31 @@
<label for="agent-max-iterations" data-i18n="settingsBasic.maxIterations">最大迭代次数</label>
<input type="number" id="agent-max-iterations" min="1" data-i18n="settingsBasic.iterationsPlaceholder" data-i18n-attr="placeholder" placeholder="30" />
</div>
<div class="form-group">
<label for="agent-tool-wait-timeout-seconds" data-i18n="settingsBasic.toolWaitTimeoutSeconds">外部 MCP 单轮等待秒数</label>
<input type="number" id="agent-tool-wait-timeout-seconds" min="0" step="1" placeholder="60" />
<small class="form-hint" data-i18n="settingsBasic.toolWaitTimeoutSecondsHint">Agent 等待工具的单轮上限;到时返回 execution_idworker 继续后台执行,可用 wait_tool_execution 继续等待。0 表示等到完成。</small>
</div>
<div class="form-group">
<label for="agent-external-mcp-concurrency-server" data-i18n="settingsBasic.externalMcpConcurrencyServer">单 MCP 并发上限</label>
<input type="number" id="agent-external-mcp-concurrency-server" min="-1" step="1" placeholder="2" />
<small class="form-hint" data-i18n="settingsBasic.externalMcpConcurrencyServerHint">每个外部 MCP server 同时运行的工具数;0 使用默认 2,-1 不限制。</small>
</div>
<div class="form-group">
<label for="agent-external-mcp-concurrency-total" data-i18n="settingsBasic.externalMcpConcurrencyTotal">外部 MCP 全局并发上限</label>
<input type="number" id="agent-external-mcp-concurrency-total" min="-1" step="1" placeholder="16" />
<small class="form-hint" data-i18n="settingsBasic.externalMcpConcurrencyTotalHint">所有外部 MCP 工具的总并发;0 使用默认 16,-1 不限制。</small>
</div>
<div class="form-group">
<label for="agent-external-mcp-circuit-threshold" data-i18n="settingsBasic.externalMcpCircuitThreshold">MCP 熔断失败阈值</label>
<input type="number" id="agent-external-mcp-circuit-threshold" min="-1" step="1" placeholder="3" />
<small class="form-hint" data-i18n="settingsBasic.externalMcpCircuitThresholdHint">同一 MCP server 连续失败达到阈值后临时熔断;0 使用默认 3,-1 关闭熔断。</small>
</div>
<div class="form-group">
<label for="agent-external-mcp-circuit-cooldown" data-i18n="settingsBasic.externalMcpCircuitCooldown">MCP 熔断冷却秒数</label>
<input type="number" id="agent-external-mcp-circuit-cooldown" min="0" step="1" placeholder="60" />
<small class="form-hint" data-i18n="settingsBasic.externalMcpCircuitCooldownHint">server 熔断后的等待时间;0 使用默认 60 秒。</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="multi-agent-enabled" class="modern-checkbox" />