diff --git a/web/static/css/style.css b/web/static/css/style.css index ad9ef9bb..7f94cde1 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -3753,6 +3753,27 @@ header { background: rgba(21, 101, 192, 0.07); } +.timeline-item-tool_call:has(.tool-result-section.success) { + border-left-color: var(--success-color); + background: rgba(40, 167, 69, 0.07); +} + +.timeline-item-tool_call:has(.tool-result-section.error) { + border-left-color: var(--error-color); + background: rgba(220, 53, 69, 0.07); +} + +.timeline-item-tool_call .tool-result-slot { + margin-top: 8px; + padding-top: 8px; + border-top: 1px dashed rgba(0, 0, 0, 0.12); +} + +.timeline-item-tool_call .tool-result-section.pending .tool-result-pending { + color: var(--text-muted); + font-style: italic; +} + .timeline-item-tool_result { border-left-color: #78909c; background: rgba(120, 144, 156, 0.06); diff --git a/web/static/js/chat.js b/web/static/js/chat.js index d551be19..73e41388 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2357,6 +2357,9 @@ function renderProcessDetails(messageId, processDetails) { detailsContainer.dataset.lazyNotLoaded = '0'; detailsContainer.dataset.loaded = '1'; processDetails = dedupeConsecutiveProcessDetailRows(processDetails); + if (typeof window.coalesceProcessDetailsToolPairs === 'function') { + processDetails = window.coalesceProcessDetailsToolPairs(processDetails); + } // 如果没有processDetails或为空,显示空状态 if (!processDetails || processDetails.length === 0) { // 显示空状态提示 @@ -2421,7 +2424,13 @@ function renderProcessDetails(messageId, processDetails) { const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); const index = data.index || 0; const total = data.total || 0; - itemTitle = agPx + '🔧 ' + (typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')'); + const argsHint = typeof window.toolCallArgHint === 'function' + ? window.toolCallArgHint(typeof window.parseToolCallArgsFromData === 'function' ? window.parseToolCallArgsFromData(data) : {}) + : ''; + const callTitle = typeof window.formatToolCallTimelineTitle === 'function' + ? window.formatToolCallTimelineTitle(toolName, index, total, argsHint) + : (typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')'); + 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; @@ -2451,12 +2460,16 @@ function renderProcessDetails(messageId, processDetails) { : '⏸️ 用户中断并继续'; } - addTimelineItem(timeline, eventType, { + const timelineOpts = { title: itemTitle, message: detail.message || '', data: data, createdAt: detail.createdAt // 传递实际的事件创建时间 - }); + }; + if (eventType === 'tool_call' && data._mergedResult) { + timelineOpts.mergedResult = data._mergedResult; + } + addTimelineItem(timeline, eventType, timelineOpts); }); // 检查是否有错误或取消事件,如果有,确保详情默认折叠(但仍有待审批 HITL 时保持展开,由 restoreHitlInlineForConversation 处理) diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 79527f98..b992e921 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1565,7 +1565,9 @@ function handleStreamEvent(event, progressElement, progressId, const index = toolInfo.index || 0; const total = toolInfo.total || 0; const toolCallId = toolInfo.toolCallId || null; - const toolCallTitle = typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')'; + const toolCallArgs = parseToolCallArgsFromData(toolInfo); + const toolCallHint = toolCallArgHint(toolCallArgs); + const toolCallTitle = formatToolCallTimelineTitle(toolName, index, total, toolCallHint); const toolCallItemId = addTimelineItem(timeline, 'tool_call', { title: timelineAgentBracketPrefix(toolInfo) + '🔧 ' + toolCallTitle, message: event.message, @@ -1593,44 +1595,33 @@ function handleStreamEvent(event, progressElement, progressId, const key = toolResultStreamKey(progressId, toolCallId); let state = toolResultStreamStateByKey.get(key); - const toolNameDelta = deltaInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); const deltaText = event.message || ''; if (!deltaText) break; if (!state) { - // 首次增量:创建一个 tool_result 占位条目,后续不断更新 pre 内容 - const runningLabel = typeof window.t === 'function' ? window.t('timeline.running') : '执行中...'; - const title = timelineAgentBracketPrefix(deltaInfo) + '⏳ ' + (typeof window.t === 'function' - ? window.t('timeline.running') - : runningLabel) + ' ' + (typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtmlLocal(toolNameDelta), index: deltaInfo.index || 0, total: deltaInfo.total || 0 }) : toolNameDelta); - - const itemId = addTimelineItem(timeline, 'tool_result', { - title: title, - message: '', - data: { - toolName: toolNameDelta, - success: true, - isError: false, - result: deltaText, - toolCallId: toolCallId, - index: deltaInfo.index, - total: deltaInfo.total, - iteration: deltaInfo.iteration, - einoAgent: deltaInfo.einoAgent, - source: deltaInfo.source - }, - expanded: false - }); - - state = { itemId, buffer: '' }; + const mapping = toolCallStatusMap.get(toolCallId); + let callItemId = mapping && mapping.itemId ? mapping.itemId : null; + if (callItemId) { + const callItem = document.getElementById(callItemId); + if (callItem) { + ensureToolCallResultSlot(callItem); + const section = callItem.querySelector('.tool-result-section'); + if (section) { + section.classList.remove('pending'); + section.className = 'tool-result-section success'; + } + } + } + state = { itemId: callItemId, buffer: '', onCallItem: !!callItemId }; toolResultStreamStateByKey.set(key, state); } state.buffer += deltaText; - const item = document.getElementById(state.itemId); + const item = state.itemId ? document.getElementById(state.itemId) : null; if (item) { const pre = item.querySelector('pre.tool-result'); if (pre) { + pre.classList.remove('tool-result-pending'); pre.textContent = state.buffer; } } @@ -1645,34 +1636,23 @@ function handleStreamEvent(event, progressElement, progressId, 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) + ' 执行失败'); - // 若此 tool 已经流式推送过增量,则复用占位条目并更新最终结果,避免重复添加一条 if (resultToolCallId) { const key = toolResultStreamKey(progressId, resultToolCallId); - const state = toolResultStreamStateByKey.get(key); - if (state && state.itemId) { - const item = document.getElementById(state.itemId); - if (item) { - const pre = item.querySelector('pre.tool-result'); - const resultVal = resultInfo.result || resultInfo.error || ''; - if (pre) pre.textContent = typeof resultVal === 'string' ? resultVal : JSON.stringify(resultVal); - - const section = item.querySelector('.tool-result-section'); - if (section) { - section.className = 'tool-result-section ' + (success ? 'success' : 'error'); - } - - const titleEl = item.querySelector('.timeline-item-title'); - if (titleEl) { - if (resultInfo.einoAgent != null && String(resultInfo.einoAgent).trim() !== '') { - item.dataset.einoAgent = String(resultInfo.einoAgent).trim(); - } - titleEl.textContent = timelineAgentBracketPrefix(resultInfo) + statusIcon + ' ' + resultExecText; - } + const streamState = toolResultStreamStateByKey.get(key); + if (streamState && streamState.itemId) { + const streamCallItem = document.getElementById(streamState.itemId); + if (streamCallItem) { + mergeToolResultIntoCallItem(streamCallItem, resultInfo); } toolResultStreamStateByKey.delete(key); - - // 同时更新 tool_call 的状态 - if (resultToolCallId && toolCallStatusMap.has(resultToolCallId)) { + if (toolCallStatusMap.has(resultToolCallId)) { + updateToolCallStatus(resultToolCallId, success ? 'completed' : 'failed'); + toolCallStatusMap.delete(resultToolCallId); + } + break; + } + if (attachToolResultToCall(resultToolCallId, resultInfo)) { + if (toolCallStatusMap.has(resultToolCallId)) { updateToolCallStatus(resultToolCallId, success ? 'completed' : 'failed'); toolCallStatusMap.delete(resultToolCallId); } @@ -2508,6 +2488,236 @@ async function attachRunningTaskEventStream(conversationId) { window.attachRunningTaskEventStream = attachRunningTaskEventStream; window.taskReplayProgressId = taskReplayProgressId; +/** 从工具参数提取短摘要(URL/命令等),便于同名工具批量调用时区分 */ +function parseToolCallArgsFromData(data) { + if (!data) return {}; + let args = data.argumentsObj; + if (args == null && data.arguments != null && String(data.arguments).trim() !== '') { + try { + args = JSON.parse(String(data.arguments)); + } catch (e) { + args = { _raw: String(data.arguments) }; + } + } + if (args == null || typeof args !== 'object') { + return {}; + } + return args; +} + +function toolCallArgHint(args) { + if (!args || typeof args !== 'object') return ''; + const method = args.method != null ? String(args.method).trim().toUpperCase() : ''; + const url = args.url || args.URL || args.target || args.uri; + if (url != null && String(url).trim() !== '') { + let s = String(url).trim(); + if (method) s = method + ' ' + s; + return s.length > 56 ? s.slice(0, 53) + '...' : s; + } + if (method) { + return method; + } + const cmd = args.command || args.cmd || args.script; + if (cmd != null && String(cmd).trim() !== '') { + const s = String(cmd).trim(); + return s.length > 48 ? s.slice(0, 45) + '...' : s; + } + return ''; +} + +function formatToolCallTimelineTitle(toolName, index, total, argsHint) { + const name = toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); + const idx = index || 0; + const tot = total || 0; + let base; + if (typeof window.t === 'function') { + base = window.t('chat.callTool', { name: name, index: idx, total: tot }); + } else { + base = '调用工具: ' + name + (tot ? ' (' + idx + '/' + tot + ')' : ''); + } + const hint = (argsHint && String(argsHint).trim()) ? String(argsHint).trim() : ''; + return hint ? (base + ' · ' + hint) : base; +} + +function buildToolResultSectionHtml(data, opts) { + opts = opts || {}; + const _t = function (k, o) { + return typeof window.t === 'function' ? window.t(k, o) : k; + }; + const execResultLabel = _t('timeline.executionResult'); + const execIdLabel = _t('timeline.executionId'); + const waitingLabel = _t('timeline.running'); + if (opts.pending) { + return ( + '
' + + '' + escapeHtml(execResultLabel) + '' + + '
' + escapeHtml(waitingLabel) + '
' + + '
' + ); + } + 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 resultStr = typeof result === 'string' ? result : JSON.stringify(result); + const rawText = opts.rawText != null ? String(opts.rawText) : resultStr; + return ( + '
' + + '' + escapeHtml(execResultLabel) + '' + + '
' + escapeHtml(rawText) + '
' + + (data.executionId ? '
' + + escapeHtml(execIdLabel) + ' ' + escapeHtml(String(data.executionId)) + '
' : '') + + '
' + ); +} + +function ensureToolCallResultSlot(item) { + if (!item) return null; + let section = item.querySelector('.tool-result-section'); + if (section) return section; + const content = item.querySelector('.timeline-item-content'); + if (!content) return null; + const wrap = document.createElement('div'); + wrap.className = 'tool-details tool-result-slot'; + wrap.innerHTML = buildToolResultSectionHtml({}, { pending: true }); + content.appendChild(wrap); + return wrap.querySelector('.tool-result-section'); +} + +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 resultStr = typeof result === 'string' ? result : JSON.stringify(result); + const text = options.rawText != null ? String(options.rawText) : resultStr; + + let section = item.querySelector('.tool-result-section'); + if (!section) { + ensureToolCallResultSlot(item); + section = item.querySelector('.tool-result-section'); + } + if (!section) return false; + + section.classList.remove('pending'); + section.className = 'tool-result-section ' + (isError ? 'error' : 'success'); + const pre = section.querySelector('pre.tool-result'); + if (pre) { + pre.classList.remove('tool-result-pending'); + pre.textContent = text; + } + + if (data.executionId) { + let execIdEl = section.querySelector('.tool-execution-id'); + if (!execIdEl) { + const execIdLabel = typeof window.t === 'function' ? window.t('timeline.executionId') : '执行ID:'; + execIdEl = document.createElement('div'); + execIdEl.className = 'tool-execution-id'; + execIdEl.innerHTML = '' + escapeHtml(execIdLabel) + + ' '; + section.appendChild(execIdEl); + } + const code = execIdEl.querySelector('code'); + if (code) code.textContent = String(data.executionId); + } + + 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'); + return true; +} + +function findToolCallItemById(root, toolCallId) { + if (!root || !toolCallId) return null; + const id = String(toolCallId).trim(); + if (!id) return null; + try { + return root.querySelector('[data-tool-call-id="' + CSS.escape(id) + '"]'); + } catch (e) { + return root.querySelector('[data-tool-call-id="' + id.replace(/"/g, '\\"') + '"]'); + } +} + +function attachToolResultToCall(toolCallId, data, options) { + if (!toolCallId || !data) return false; + const mapping = toolCallStatusMap.get(toolCallId); + let item = null; + if (mapping && mapping.itemId) { + item = document.getElementById(mapping.itemId); + } + if (!item && mapping && mapping.timeline) { + item = findToolCallItemById(mapping.timeline, toolCallId); + } + if (!item) return false; + mergeToolResultIntoCallItem(item, data, options); + return true; +} + +function coalesceProcessDetailsToolPairs(details) { + if (!Array.isArray(details) || details.length === 0) return details; + const callsById = new Map(); + const fifoCalls = []; + const out = []; + + function absorbResult(targetDetail, resultDetail) { + const rd = resultDetail.data || {}; + targetDetail.data = targetDetail.data || {}; + targetDetail.data._mergedResult = Object.assign({}, rd); + if (resultDetail.createdAt) { + targetDetail.data._mergedResultAt = resultDetail.createdAt; + } + } + + for (let i = 0; i < details.length; i++) { + const detail = details[i]; + const et = detail.eventType || ''; + const data = detail.data || {}; + const id = data.toolCallId != null ? String(data.toolCallId).trim() : ''; + + if (et === 'tool_call') { + const copy = { + eventType: detail.eventType, + message: detail.message, + createdAt: detail.createdAt, + data: Object.assign({}, data) + }; + if (id) callsById.set(id, copy); + fifoCalls.push(copy); + out.push(copy); + } else if (et === 'tool_result') { + let target = null; + if (id && callsById.has(id)) { + target = callsById.get(id); + } else { + for (let j = 0; j < fifoCalls.length; j++) { + const c = fifoCalls[j]; + if (c && c.data && !c.data._mergedResult) { + target = c; + break; + } + } + } + if (target) { + absorbResult(target, detail); + continue; + } + out.push(detail); + } else { + out.push(detail); + } + } + return out; +} + +window.coalesceProcessDetailsToolPairs = coalesceProcessDetailsToolPairs; +window.attachToolResultToCall = attachToolResultToCall; +window.mergeToolResultIntoCallItem = mergeToolResultIntoCallItem; +window.formatToolCallTimelineTitle = formatToolCallTimelineTitle; +window.parseToolCallArgsFromData = parseToolCallArgsFromData; +window.toolCallArgHint = toolCallArgHint; +window.buildToolResultSectionHtml = buildToolResultSectionHtml; + // 更新工具调用状态 function updateToolCallStatus(toolCallId, status) { const mapping = toolCallStatusMap.get(toolCallId); @@ -2574,6 +2784,16 @@ function addTimelineItem(timeline, type, options) { if (d.toolCallId != null && String(d.toolCallId).trim() !== '') { item.dataset.toolCallId = String(d.toolCallId).trim(); } + const callArgs = parseToolCallArgsFromData(d); + const argHint = toolCallArgHint(callArgs); + if (argHint) { + item.dataset.toolArgHint = argHint; + } + const merged = options.mergedResult || d._mergedResult; + if (merged) { + item.dataset.toolResultMerged = '1'; + item.dataset.toolSuccess = merged.success !== false ? '1' : '0'; + } } if (type === 'hitl_interrupt' && options.data && options.data.interruptId != null && String(options.data.interruptId).trim() !== '') { item.dataset.hitlInterruptId = String(options.data.interruptId).trim(); @@ -2635,18 +2855,20 @@ function addTimelineItem(timeline, type, options) { content += `
${formatMarkdown(streamBody)}
`; } else if (type === 'tool_call' && options.data) { const data = options.data; - let args = data.argumentsObj; - if (args == null && data.arguments != null && String(data.arguments).trim() !== '') { - try { - args = JSON.parse(String(data.arguments)); - } catch (e) { - args = { _raw: String(data.arguments) }; - } - } - if (args == null || typeof args !== 'object') { - args = {}; - } + const args = parseToolCallArgsFromData(data); + const merged = options.mergedResult || data._mergedResult; const paramsLabel = typeof window.t === 'function' ? window.t('timeline.params') : '参数:'; + let resultBlock = ''; + if (merged) { + resultBlock = '
' + buildToolResultSectionHtml(merged) + '
'; + if (merged.success !== false) { + item.classList.add('tool-call-completed'); + } else { + item.classList.add('tool-call-failed'); + } + } else if (!options.skipPendingResult) { + resultBlock = '
' + buildToolResultSectionHtml({}, { pending: true }) + '
'; + } content += `
@@ -2654,6 +2876,7 @@ function addTimelineItem(timeline, type, options) { ${escapeHtml(paramsLabel)}
${escapeHtml(JSON.stringify(args, null, 2))}
+ ${resultBlock}
`; @@ -4071,7 +4294,11 @@ function refreshProgressAndTimelineI18n() { const name = (item.dataset.toolName != null && item.dataset.toolName !== '') ? item.dataset.toolName : _t('chat.unknownTool'); const index = parseInt(item.dataset.toolIndex, 10) || 0; const total = parseInt(item.dataset.toolTotal, 10) || 0; - titleSpan.textContent = ap + '\uD83D\uDD27 ' + _t('chat.callTool', { name: name, index: index, total: total }); + const hint = item.dataset.toolArgHint || ''; + const callTitle = typeof formatToolCallTimelineTitle === 'function' + ? formatToolCallTimelineTitle(name, index, total, hint) + : _t('chat.callTool', { name: name, index: index, total: total }); + 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 success = item.dataset.toolSuccess === '1'; diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index cc5ffca1..35bca967 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -1666,7 +1666,13 @@ function buildWebshellTimelineItemFromDetail(detail) { var tn = data.toolName || ((typeof window.t === 'function') ? window.t('chat.unknownTool') : '未知工具'); var idx = data.index || 0; var total = data.total || 0; - title = ap + '🔧 ' + ((typeof window.t === 'function') ? window.t('chat.callTool', { name: tn, index: idx, total: total }) : ('调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : ''))); + var wsHint = typeof window.toolCallArgHint === 'function' + ? window.toolCallArgHint(typeof window.parseToolCallArgsFromData === 'function' ? window.parseToolCallArgsFromData(data) : {}) + : ''; + var wsCallTitle = typeof window.formatToolCallTimelineTitle === 'function' + ? window.formatToolCallTimelineTitle(tn, idx, total, wsHint) + : ((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 || '工具'; @@ -1695,6 +1701,9 @@ function buildWebshellTimelineItemFromDetail(detail) { html += '
' + escapeHtml(paramsLabel) + '
' + escapeHtml(JSON.stringify(args, null, 2)) + '
'; } } catch (e) {} + } + 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') : '无结果'; @@ -1712,6 +1721,9 @@ function buildWebshellTimelineItemFromDetail(detail) { // 渲染「执行过程及调用工具」折叠块(默认折叠,刷新后加载历史时保留并可展开) function renderWebshellProcessDetailsBlock(processDetails, defaultCollapsed) { if (!processDetails || processDetails.length === 0) return null; + if (typeof window.coalesceProcessDetailsToolPairs === 'function') { + processDetails = window.coalesceProcessDetailsToolPairs(processDetails); + } var expandLabel = (typeof window.t === 'function') ? window.t('chat.expandDetail') : '展开详情'; var collapseLabel = (typeof window.t === 'function') ? window.t('tasks.collapseDetail') : '收起详情'; var headerLabel = (typeof window.t === 'function') ? (window.t('chat.penetrationTestDetail') || '执行过程及调用工具') : '执行过程及调用工具'; @@ -2772,7 +2784,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { var html = '' + escapeHtml(title || message || '') + ''; - // 工具调用入参 + // 工具调用入参 + 结果同卡 if (type === 'tool_call' && data) { try { var args = data.argumentsObj; @@ -2783,14 +2795,20 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { args = { _raw: String(data.arguments) }; } } - if (args && typeof args === 'object') { - var paramsLabel = (typeof window.t === 'function') ? window.t('timeline.params') : '参数:'; - html += '
' + - escapeHtml(paramsLabel) + - '
' +
-                        escapeHtml(JSON.stringify(args, null, 2)) +
-                        '
'; + if (args == null || typeof args !== 'object') { + args = {}; } + var paramsLabel = (typeof window.t === 'function') ? window.t('timeline.params') : '参数:'; + var pendingResult = (typeof window.buildToolResultSectionHtml === 'function') + ? window.buildToolResultSectionHtml({}, { pending: true }) + : ''; + html += '
' + + escapeHtml(paramsLabel) + + '
' +
+                    escapeHtml(JSON.stringify(args, null, 2)) +
+                    '
' + + (pendingResult ? '
' + pendingResult + '
' : '') + + '
'; } catch (e) { // JSON 解析失败时忽略参数详情,避免打断主流程 } @@ -2829,6 +2847,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { var einoSubReplyStreams = new Map(); var wsThinkingStreams = new Map(); // streamId → { el, buf } var wsToolResultStreams = new Map(); // toolCallId → { el, buf } + var wsToolCallItems = new Map(); // toolCallId → DOM item(参数+结果同卡) if (inputEl) inputEl.value = ''; @@ -3035,11 +3054,16 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { var tn = _ed.toolName || '未知工具'; var idx = _ed.index || 0; var total = _ed.total || 0; - var callTitle = wsTOr('chat.callTool', '') || ('调用工具: ' + tn + (total ? ' (' + idx + '/' + total + ')' : '')); - if (typeof window.t === 'function') { - try { callTitle = window.t('chat.callTool', { name: tn, index: idx, total: total }); } catch (e) { /* */ } + var wsHintLive = typeof window.toolCallArgHint === 'function' + ? window.toolCallArgHint(typeof window.parseToolCallArgsFromData === 'function' ? window.parseToolCallArgsFromData(_ed) : {}) + : ''; + var callTitle = typeof window.formatToolCallTimelineTitle === 'function' + ? window.formatToolCallTimelineTitle(tn, idx, total, wsHintLive) + : (wsTOr('chat.callTool', '') || ('调用工具: ' + tn + (total ? ' (' + idx + '/' + total + ')' : ''))); + var callItem = appendTimelineItem('tool_call', webshellAgentPx(_ed) + '🔧 ' + callTitle, _em || '', _ed); + if (_ed.toolCallId && callItem) { + wsToolCallItems.set(_ed.toolCallId, callItem); } - appendTimelineItem('tool_call', webshellAgentPx(_ed) + '🔧 ' + callTitle, _em || '', _ed); if (!streamingTarget) assistantDiv.textContent = '…'; // ─── Tool result delta (streaming output) ─── @@ -3049,22 +3073,18 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { if (trdDelta) { var trdState = wsToolResultStreams.get(trdKey); if (!trdState) { - var trdName = _ed.toolName || '工具'; - var runLabel = wsTOr('timeline.running', '执行中...'); - var trdItem = document.createElement('div'); - trdItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-tool_result'; - trdItem.innerHTML = '' + - escapeHtml(webshellAgentPx(_ed) + '⏳ ' + runLabel + ' ' + trdName) + - '
' + - '
'; - timelineContainer.appendChild(trdItem); - timelineContainer.classList.add('has-items'); - trdState = { el: trdItem, buf: '' }; + var callEl = wsToolCallItems.get(trdKey); + trdState = { el: callEl || null, buf: '', onCall: !!callEl }; wsToolResultStreams.set(trdKey, trdState); } trdState.buf += trdDelta; - var trdPre = trdState.el.querySelector('pre.tool-result'); - if (trdPre) trdPre.textContent = trdState.buf; + if (trdState.el) { + var trdPre = trdState.el.querySelector('pre.tool-result'); + if (trdPre) { + trdPre.classList.remove('tool-result-pending'); + trdPre.textContent = trdState.buf; + } + } } if (!streamingTarget) assistantDiv.textContent = '…'; @@ -3072,25 +3092,23 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { } else if (_et === 'tool_result' && _ed) { var success = _ed.success !== false; var tname = _ed.toolName || '工具'; - var titleText = wsTOr(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', '') || - (tname + (success ? ' 执行完成' : ' 执行失败')); - if (typeof window.t === 'function') { - try { titleText = window.t(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', { name: tname }); } catch (e) { /* */ } + var merged = false; + if (_ed.toolCallId) { + var streamSt = wsToolResultStreams.get(_ed.toolCallId); + var callElRes = wsToolCallItems.get(_ed.toolCallId) || (streamSt && streamSt.el); + if (callElRes && typeof window.mergeToolResultIntoCallItem === 'function') { + window.mergeToolResultIntoCallItem(callElRes, _ed); + merged = true; + wsToolResultStreams.delete(_ed.toolCallId); + wsToolCallItems.delete(_ed.toolCallId); + } } - // 如果有流式占位条目,更新标题 - var trdExist = _ed.toolCallId ? wsToolResultStreams.get(_ed.toolCallId) : null; - if (trdExist) { - var trdTitleEl = trdExist.el.querySelector('.webshell-ai-timeline-title'); - if (trdTitleEl) trdTitleEl.textContent = webshellAgentPx(_ed) + (success ? '✅ ' : '❌ ') + titleText; - // 更新结果内容 - var resultText = _ed.result ? String(_ed.result) : (_em || ''); - var trdPreEl = trdExist.el.querySelector('pre.tool-result'); - if (trdPreEl && resultText) trdPreEl.textContent = resultText; - // 更新 section class - var trdSection = trdExist.el.querySelector('.tool-result-section'); - if (trdSection) { trdSection.className = 'tool-result-section ' + (success ? 'success' : 'error'); } - wsToolResultStreams.delete(_ed.toolCallId); - } else { + if (!merged) { + var titleText = wsTOr(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', '') || + (tname + (success ? ' 执行完成' : ' 执行失败')); + if (typeof window.t === 'function') { + try { titleText = window.t(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', { name: tname }); } catch (e) { /* */ } + } var title = webshellAgentPx(_ed) + (success ? '✅ ' : '❌ ') + titleText; var sub = _em || (_ed.result ? String(_ed.result).slice(0, 300) : ''); appendTimelineItem('tool_result', title, sub, _ed);