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(waitingLabel) + '' + + '
' + escapeHtml(rawText) + '' + + (data.executionId ? '
' + escapeHtml(String(data.executionId)) + '';
+ 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 += `${escapeHtml(JSON.stringify(args, null, 2))}
' + escapeHtml(JSON.stringify(args, null, 2)) + '
' + - escapeHtml(JSON.stringify(args, null, 2)) + - '
' + + escapeHtml(JSON.stringify(args, null, 2)) + + '