diff --git a/web/static/css/c2.css b/web/static/css/c2.css index f95048b1..236dcaf9 100644 --- a/web/static/css/c2.css +++ b/web/static/css/c2.css @@ -213,12 +213,12 @@ html[data-theme="dark"] .c2-session-main-empty__icon { min-width: 0; min-height: 40px; padding: 10px 14px; - border: 1px solid var(--c2-border); + border: 1.5px solid var(--c2-border); border-radius: var(--c2-radius-sm); background: var(--c2-surface); color: var(--c2-text); font-size: 14px; - line-height: 1.25; + line-height: 1.5; cursor: pointer; font-family: inherit; transition: border-color 0.15s ease, box-shadow 0.15s ease; @@ -230,7 +230,16 @@ html[data-theme="dark"] .c2-session-main-empty__icon { .c2-form-select-ui.open .c2-form-select-trigger { border-color: var(--c2-accent); - box-shadow: 0 0 0 2px var(--c2-accent-dim); + box-shadow: 0 0 0 3px var(--c2-accent-dim); +} + +html[data-theme="dark"] .c2-modal .c2-form-select-trigger { + background: var(--c2-surface); + border-color: var(--c2-border); +} + +html[data-theme="dark"] .c2-modal .c2-form-select-ui.open .c2-form-select-trigger { + border-color: var(--c2-accent); } .c2-form-select-ui.open { diff --git a/web/static/css/style.css b/web/static/css/style.css index cbf8c731..ae87311f 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -4190,6 +4190,41 @@ html[data-theme="dark"] .openapi-doc-btn:hover { transform: translateY(0); } +.mcp-detail-btn[data-exec-id] { + min-height: 32px; +} + +.mcp-detail-btn[data-status="completed"] { + color: var(--text-primary); + border-color: rgba(40, 167, 69, 0.34); + background: rgba(40, 167, 69, 0.055); +} + +.mcp-detail-btn[data-status="failed"] { + color: var(--text-primary); + border-color: rgba(220, 53, 69, 0.36); + background: rgba(220, 53, 69, 0.06); +} + +.mcp-detail-btn[data-status="running"], +.mcp-detail-btn[data-status="pending"] { + color: var(--text-primary); + border-color: rgba(0, 102, 255, 0.32); + background: rgba(0, 102, 255, 0.055); +} + +.mcp-detail-btn[data-status="cancelled"] { + color: var(--text-muted); + border-color: rgba(148, 163, 184, 0.32); + background: rgba(148, 163, 184, 0.065); +} + +.mcp-detail-btn .mcp-tool-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + /* 登录遮罩 */ .login-overlay { position: fixed; @@ -5163,7 +5198,14 @@ html[data-theme="dark"] .openapi-doc-btn:hover { word-break: break-word; } -/* 过程详情 Markdown:避免长 URL/代码/表格撑破紫/蓝时间线条目 */ +/* 过程详情纯文本:避免长 URL/代码/表格撑破时间线条目 */ +.timeline-item-content .timeline-plain-text { + margin: 0; + font: inherit; + color: inherit; + background: transparent; +} + .timeline-item-content p, .timeline-item-content li, .timeline-item-content td, @@ -5200,6 +5242,16 @@ html[data-theme="dark"] .openapi-doc-btn:hover { contain-intrinsic-size: auto 72px; } +.timeline-live-pruned-marker { + margin: 8px 0; + padding: 8px 12px; + border: 1px dashed rgba(148, 163, 184, 0.35); + border-radius: 6px; + color: var(--text-muted); + background: rgba(148, 163, 184, 0.08); + font-size: 0.8125rem; +} + .tool-details { display: flex; flex-direction: column; @@ -5481,20 +5533,73 @@ html[data-theme="dark"] .openapi-doc-btn:hover { border: 1px solid rgba(220, 53, 69, 0.3); } -/* 工具调用项状态:全程保持「信息蓝」系,完成态不用绿色(避免与工具成功结果混淆) */ +/* 工具调用项状态:摘要态直接表达成功/失败,避免必须展开才能判断状态 */ .timeline-item-tool_call.tool-call-running { - border-left-color: #42a5f5; - background: rgba(66, 165, 245, 0.1); + border: 1px solid rgba(66, 165, 245, 0.32); + border-left: 3px solid #42a5f5; + background: rgba(66, 165, 245, 0.075); } .timeline-item-tool_call.tool-call-completed { - border-left-color: #0d47a1; - background: rgba(13, 71, 161, 0.08); + border: 1px solid rgba(40, 167, 69, 0.34); + border-left: 3px solid var(--success-color); + background: rgba(40, 167, 69, 0.075); } .timeline-item-tool_call.tool-call-failed { - border-left-color: var(--error-color); - background: rgba(220, 53, 69, 0.1); + border: 1px solid rgba(220, 53, 69, 0.36); + border-left: 3px solid var(--error-color); + background: rgba(220, 53, 69, 0.085); +} + +.timeline-item.tool-detail-collapsible { + cursor: pointer; +} + +.timeline-item.tool-detail-collapsible .timeline-item-header { + cursor: pointer; + position: relative; + padding-right: 104px; +} + +.timeline-item.tool-detail-collapsible .timeline-item-header:hover .timeline-item-title { + color: var(--accent-color); +} + +.timeline-item.tool-detail-collapsible .timeline-item-header::after { + content: attr(data-tool-detail-label); + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + padding: 3px 8px; + border-radius: 999px; + border: 1px solid rgba(96, 165, 250, 0.35); + color: #93c5fd; + background: rgba(37, 99, 235, 0.12); + font-size: 0.75rem; + font-weight: 600; + line-height: 1.2; + white-space: nowrap; +} + +.timeline-item.tool-detail-collapsible .timeline-item-header:hover::after { + color: #dbeafe; + border-color: rgba(147, 197, 253, 0.65); + background: rgba(37, 99, 235, 0.22); +} + +@media (max-width: 640px) { + .timeline-item.tool-detail-collapsible .timeline-item-header { + padding-right: 0; + } + + .timeline-item.tool-detail-collapsible .timeline-item-header::after { + position: static; + transform: none; + margin-left: auto; + flex: 0 0 auto; + } } /* 参数块与卡片类型色弱对齐,扫读时一眼归到「调用」 */ @@ -29722,6 +29827,173 @@ html[data-theme="dark"] #skill-view-body { border-color: #263244; } +html[data-theme="dark"] .timeline-item-content .timeline-plain-text { + background: transparent !important; + border-color: transparent !important; + color: inherit; + padding: 0; +} + +html[data-theme="dark"] .timeline-item-content.timeline-stream-plain { + background: transparent; +} + +html[data-theme="dark"] .progress-container { + background: #0f172a; + border-color: #243044; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28); +} + +html[data-theme="dark"] .message.system .message-bubble.progress-container { + background: #0b1120 !important; + color: var(--text-primary); + border-color: #203047 !important; +} + +html[data-theme="dark"] .progress-container.completed { + background: #111827; + border-color: #263244; +} + +html[data-theme="dark"] .chat-scroll-to-bottom { + background: rgba(15, 23, 42, 0.94); + color: #dbeafe; + border-color: rgba(96, 165, 250, 0.36); + box-shadow: + 0 10px 24px rgba(0, 0, 0, 0.34), + 0 0 0 1px rgba(255, 255, 255, 0.03) inset; +} + +html[data-theme="dark"] .chat-scroll-to-bottom:hover { + background: #172033; + color: #ffffff; + border-color: rgba(147, 197, 253, 0.62); + box-shadow: + 0 12px 28px rgba(0, 0, 0, 0.42), + 0 0 0 1px rgba(147, 197, 253, 0.08) inset; +} + +html[data-theme="dark"] .chat-scroll-to-bottom:focus-visible { + outline-color: #93c5fd; +} + +html[data-theme="dark"] .process-details-container { + border-top-color: #263244; +} + +html[data-theme="dark"] .progress-header, +html[data-theme="dark"] .progress-footer { + border-color: #243044; +} + +html[data-theme="dark"] .progress-toggle { + background: #111827; + border-color: #2b374b; + color: #a7b0c0; +} + +html[data-theme="dark"] .progress-toggle:hover { + background: #172033; + border-color: rgba(96, 165, 250, 0.38); + color: #dbeafe; +} + +html[data-theme="dark"] .timeline-item { + background: #0f172a; + border-left-color: #334155; +} + +html[data-theme="dark"] .timeline-item:hover { + background: #132036; +} + +html[data-theme="dark"] .timeline-item-content { + border-top-color: rgba(148, 163, 184, 0.16); +} + +html[data-theme="dark"] .timeline-item-iteration, +html[data-theme="dark"] .timeline-item.timeline-item-iteration.timeline-eino-scope-main, +html[data-theme="dark"] .timeline-item.timeline-item-iteration.timeline-eino-scope-sub { + background: #101a2b !important; +} + +html[data-theme="dark"] .timeline-item-thinking { + background: rgba(139, 92, 246, 0.09); +} + +html[data-theme="dark"] .timeline-item-thinking[data-response-stream-placeholder="1"] { + background: rgba(96, 165, 250, 0.075); +} + +html[data-theme="dark"] .timeline-item-reasoning_chain { + background: rgba(124, 58, 237, 0.11); +} + +html[data-theme="dark"] .timeline-item-planning { + background: rgba(45, 212, 191, 0.075); +} + +html[data-theme="dark"] .timeline-item-tool_call, +html[data-theme="dark"] .timeline-item-tool_calls_detected { + background: rgba(96, 165, 250, 0.075); +} + +html[data-theme="dark"] .timeline-item-tool_result, +html[data-theme="dark"] .timeline-item-progress { + background: rgba(148, 163, 184, 0.075); +} + +html[data-theme="dark"] .timeline-item-eino_agent_reply { + background: rgba(168, 85, 247, 0.1); +} + +html[data-theme="dark"] .timeline-item-tool_call:has(.tool-result-section.success), +html[data-theme="dark"] .timeline-item-tool_result:has(.tool-result-section.success) { + background: rgba(52, 211, 153, 0.08); +} + +html[data-theme="dark"] .timeline-item-tool_call:has(.tool-result-section.error), +html[data-theme="dark"] .timeline-item-tool_result:has(.tool-result-section.error), +html[data-theme="dark"] .timeline-item-tool_result.error, +html[data-theme="dark"] .timeline-item-error { + background: rgba(248, 113, 113, 0.1); +} + +html[data-theme="dark"] .timeline-item-tool_call.tool-call-running { + border: 1px solid rgba(96, 165, 250, 0.32); + border-left: 3px solid #60a5fa; + background: rgba(37, 99, 235, 0.105); +} + +html[data-theme="dark"] .timeline-item-tool_call.tool-call-completed { + border: 1px solid rgba(52, 211, 153, 0.34); + border-left: 3px solid #34d399; + background: rgba(16, 185, 129, 0.105); +} + +html[data-theme="dark"] .timeline-item-tool_call.tool-call-failed { + border: 1px solid rgba(248, 113, 113, 0.38); + border-left: 3px solid #f87171; + background: rgba(239, 68, 68, 0.115); +} + +html[data-theme="dark"] .timeline-item-tool_call.tool-call-running:hover { + background: rgba(37, 99, 235, 0.14); +} + +html[data-theme="dark"] .timeline-item-tool_call.tool-call-completed:hover { + background: rgba(16, 185, 129, 0.14); +} + +html[data-theme="dark"] .timeline-item-tool_call.tool-call-failed:hover { + background: rgba(239, 68, 68, 0.16); +} + +html[data-theme="dark"] .timeline-item-warning, +html[data-theme="dark"] .timeline-item-user_interrupt_continue { + background: rgba(251, 191, 36, 0.08); +} + html[data-theme="dark"] .message.assistant .message-bubble { background: #111827; color: var(--text-primary); diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 1b581288..452d5e3f 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -22,6 +22,7 @@ "view": "View", "actions": "Actions", "loadFailed": "Load failed", + "loadMore": "Load more", "untitled": "Untitled", "justNow": "Just now", "minutesAgo": "{{n}} min ago", @@ -555,6 +556,10 @@ "noMatchTools": "No matching tools", "penetrationTestDetail": "Penetration test details", "expandDetail": "Expand details", + "expandDetailLazyHint": "Expand details (loads iteration details on click)", + "viewToolDetail": "View details", + "collapseToolDetail": "Collapse", + "liveTimelinePruned": "Collapsed the first {{count}} live process details. View the full record page by page after the task completes.", "toolExecutionsCount": "{{n}} tool runs", "collapseToolExecutions": "Collapse tool runs", "noProcessDetail": "No process details (execution may be too fast or no detailed events)", @@ -804,7 +809,9 @@ "noResult": "No result", "running": "Running...", "completed": "Completed", - "execFailed": "Execution failed" + "execFailed": "Execution failed", + "toolSucceeded": "Succeeded", + "toolFailed": "Failed" }, "tasks": { "title": "Task Management", @@ -1813,6 +1820,7 @@ "mcpMonitor": { "deselectAll": "Deselect all", "statusPending": "Pending", + "statusSuccess": "Succeeded", "statusCompleted": "Completed", "statusRunning": "Running", "statusFailed": "Failed", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 33038303..15c9b240 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -22,6 +22,7 @@ "view": "查看", "actions": "操作", "loadFailed": "加载失败", + "loadMore": "加载更多", "untitled": "未命名", "justNow": "刚刚", "minutesAgo": "{{n}} 分钟前", @@ -543,6 +544,10 @@ "noMatchTools": "没有匹配的工具", "penetrationTestDetail": "渗透测试详情", "expandDetail": "展开详情", + "expandDetailLazyHint": "展开详情(点击后加载迭代详情)", + "viewToolDetail": "查看详情", + "collapseToolDetail": "收起", + "liveTimelinePruned": "已收起前 {{count}} 条实时过程详情,任务完成后可按页查看完整记录", "toolExecutionsCount": "{{n}}次工具执行", "collapseToolExecutions": "收起工具执行", "noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)", @@ -792,7 +797,9 @@ "noResult": "无结果", "running": "执行中...", "completed": "已完成", - "execFailed": "执行失败" + "execFailed": "执行失败", + "toolSucceeded": "成功", + "toolFailed": "失败" }, "tasks": { "title": "任务管理", @@ -1801,6 +1808,7 @@ "mcpMonitor": { "deselectAll": "取消全选", "statusPending": "等待中", + "statusSuccess": "成功", "statusCompleted": "已完成", "statusRunning": "执行中", "statusFailed": "失败", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 556036f0..7c8d0c6e 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2397,7 +2397,10 @@ function isEinoAgentHeartbeatProgress(detail) { function filterNoiseProcessDetails(details) { if (!Array.isArray(details)) return details; - return details.filter(function (d) { return !isEinoAgentHeartbeatProgress(d); }); + return details.filter(function (d) { + if (isEinoAgentHeartbeatProgress(d)) return false; + return !(d && d.eventType === 'tool_calls_detected'); + }); } function dedupeConsecutiveProcessDetailRows(details) { @@ -2704,6 +2707,7 @@ function renderProcessDetails(messageId, processDetails, options) { title: itemTitle, message: detail.message || '', data: data, + processDetailId: detail.id || '', createdAt: detail.createdAt }; if (eventType === 'tool_call' && data._mergedResult) { @@ -3052,7 +3056,62 @@ function ensureMcpCallButtons(messageElement) { window.ensureMcpCallButtons = ensureMcpCallButtons; window.appendMcpCallButtons = appendMcpCallButtons; -// 批量获取工具名称并更新按钮(消除 N 次单独 API 请求,合并为 1 次) +function normalizeToolExecutionSummary(raw) { + if (typeof raw === 'string') { + return { toolName: raw, status: '' }; + } + if (raw && typeof raw === 'object') { + return { + toolName: raw.toolName || raw.name || '', + status: raw.status || '' + }; + } + return { toolName: '', status: '' }; +} + +function getToolExecutionStatusLabel(status) { + const normalized = String(status || '').toLowerCase(); + if (typeof window.t === 'function') { + const keyMap = { + completed: 'mcpMonitor.statusSuccess', + failed: 'mcpMonitor.statusFailed', + running: 'mcpMonitor.statusRunning', + cancelled: 'mcpMonitor.statusCancelled', + pending: 'mcpMonitor.statusPending' + }; + const key = keyMap[normalized]; + if (key) { + const translated = window.t(key); + if (translated && translated !== key) return translated; + } + } + const fallback = { + completed: '成功', + failed: '失败', + running: '运行中', + cancelled: '已取消', + pending: '等待中' + }; + return fallback[normalized] || ''; +} + +function renderToolExecutionButtonContent(btn, displayToolName, index, status) { + const safeToolName = escapeHtml(displayToolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具')); + const safeIndex = escapeHtml(index || ''); + const statusText = getToolExecutionStatusLabel(status); + const normalizedStatus = String(status || '').toLowerCase(); + const label = safeIndex ? `${safeToolName} #${safeIndex}` : safeToolName; + btn.innerHTML = '' + label + ''; + if (!statusText) { + btn.removeAttribute('data-status'); + btn.removeAttribute('title'); + return; + } + btn.dataset.status = normalizedStatus; + btn.title = statusText; +} + +// 批量获取工具摘要并更新按钮(消除 N 次单独 API 请求,合并为 1 次) async function batchUpdateButtonToolNames(buttonsContainer, executionIds) { if (!executionIds || executionIds.length === 0) return; try { @@ -3062,17 +3121,17 @@ async function batchUpdateButtonToolNames(buttonsContainer, executionIds) { body: JSON.stringify({ ids: executionIds }), }); if (!response.ok) return; - const nameMap = await response.json(); // { execId: toolName } + const nameMap = await response.json(); // { execId: toolName } 或 { execId: { toolName, status } } // 更新对应按钮的文本 const buttons = buttonsContainer.querySelectorAll('.mcp-detail-btn[data-exec-id]'); buttons.forEach(btn => { const execId = btn.dataset.execId; const index = btn.dataset.execIndex; - const toolName = nameMap[execId]; + const summary = normalizeToolExecutionSummary(nameMap[execId]); + const toolName = summary.toolName; if (toolName) { const displayToolName = toolName.includes('::') ? toolName.split('::')[1] : toolName; - const span = btn.querySelector('span'); - if (span) span.textContent = `${displayToolName} #${index}`; + renderToolExecutionButtonContent(btn, displayToolName, index, summary.status); } }); } catch (error) { diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 8643c815..7fa66102 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -236,11 +236,7 @@ function finalizeMainResponseStreamItem(streamState, finalMessage, responseData) const body = typeof formatTimelineStreamBody === 'function' ? formatTimelineStreamBody(fullText, meta) : fullText; - if (typeof formatMarkdown === 'function') { - setTimelineItemContentStreamRich(contentEl, formatMarkdown(body, timelineMarkdownOpts)); - } else { - setTimelineItemContentStreamPlain(contentEl, body); - } + setTimelineItemContentStreamPlain(contentEl, body); return true; } @@ -439,6 +435,9 @@ function toolResultStreamKey(progressId, toolCallId) { return String(progressId) + '::' + String(toolCallId); } +const LIVE_TIMELINE_MAX_ITEMS = 150; +const LIVE_TIMELINE_PRUNE_CHUNK = 50; + /** Eino 多代理:时间线标题前加 [agentId],标明哪一代理产生该工具调用/结果/回复 */ function timelineAgentBracketPrefix(data) { if (!data || data.einoAgent == null) return ''; @@ -461,9 +460,6 @@ function applyEinoTimelineRole(item, data) { } } -/** 过程详情时间线:更严消毒(无 img;整页 HTML 见 sanitize-markdown.js) */ -const timelineMarkdownOpts = { profile: 'timeline' }; - function escapeHtmlLocal(text) { if (!text) return ''; const div = document.createElement('div'); @@ -471,6 +467,10 @@ function escapeHtmlLocal(text) { return div.innerHTML; } +function formatTimelinePlainTextHtml(text) { + return '
' + escapeHtml(text == null ? '' : String(text)) + '
'; +} + /** fenced 块占位(BMP 私用区,正文几乎不会出现) */ const _MD_FENCE_PRE = '\n\uE000CSAI_FENCE_'; const _MD_FENCE_SUF = '_\uE000\n'; @@ -1280,23 +1280,14 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut const progressElement = document.getElementById(progressId); if (!progressElement) return; - // 快照 innerHTML 前刷掉尚未执行的 rAF 流式更新,避免过程详情少最后几帧 + // 只 flush 终态标题/正文;完成后的详情回看统一走后端分页,不再复制实时 DOM 快照。 flushAllPendingStreamPlainUpdates(); - // Ensure any "running" tool_call badges are closed before we snapshot timeline HTML. - // Otherwise, once the progress element is removed, later 'done' events may not be able - // to update the original timeline DOM and the copied HTML would stay "执行中". + // 进度 DOM 即将移除;先关闭仍处于 running 的工具摘要状态。 finalizeOutstandingToolCallsForProgress(progressId, 'failed'); const mcpIds = Array.isArray(mcpExecutionIds) ? mcpExecutionIds : []; - // 获取时间线内容 - const timeline = document.getElementById(progressId + '-timeline'); - let timelineHTML = ''; - if (timeline) { - timelineHTML = timeline.innerHTML; - } - // 获取助手消息元素 const assistantElement = document.getElementById(assistantMessageId); if (!assistantElement) { @@ -1320,8 +1311,6 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut return; } - const hasContent = timelineHTML.trim().length > 0; - if (mcpIds.length > 0 && typeof window.appendMcpCallButtons === 'function') { window.appendMcpCallButtons(assistantElement, mcpIds); const toolList = mcpSection.querySelector('.mcp-tool-list'); @@ -1354,40 +1343,87 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut mcpSection.appendChild(detailsContainer); } } - - detailsContainer.innerHTML = ` -
- ${hasContent ? `
${timelineHTML}
` : '
' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '
'} -
- `; - - if (hasContent) { - const timeline = document.getElementById(detailsId + '-timeline'); - if (timeline) { - timeline.classList.remove('expanded'); - } - + + if (typeof renderProcessDetails === 'function') { + renderProcessDetails(assistantMessageId, null); + } else { const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; - document.querySelectorAll(`#${assistantMessageId} .process-detail-btn`).forEach((btn) => { - btn.innerHTML = '' + expandLabel + ''; - }); + detailsContainer.dataset.lazyNotLoaded = '1'; + detailsContainer.dataset.loaded = '0'; + detailsContainer.innerHTML = ` +
+
+
${typeof window.t === 'function' ? window.t('chat.expandDetailLazyHint') : (expandLabel + '(点击后加载迭代详情)')}
+
+
+ `; } + + const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; + document.querySelectorAll(`#${assistantMessageId} .process-detail-btn`).forEach((btn) => { + btn.innerHTML = '' + expandLabel + ''; + }); removeMessage(progressId); } -const PROCESS_DETAILS_PAGE_SIZE = 100; +const PROCESS_DETAILS_PAGE_SIZE = 50; + +function getProcessDetailsLoadMoreLabel(hasMore) { + if (!hasMore) return ''; + return (typeof window.t === 'function' ? window.t('common.loadMore') : '加载更多') + ' · ' + + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '过程详情'); +} + +function updateProcessDetailsLoadMoreButton(assistantMessageId, backendMessageId, hasMore) { + const detailsContainer = document.getElementById('process-details-' + assistantMessageId); + if (!detailsContainer) return; + + let btn = detailsContainer.querySelector('.process-details-load-more-btn'); + if (!hasMore) { + if (btn) btn.remove(); + return; + } + if (!btn) { + btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'mcp-detail-btn process-details-load-more-btn'; + detailsContainer.appendChild(btn); + } + btn.textContent = getProcessDetailsLoadMoreLabel(true); + btn.disabled = false; + btn.onclick = async () => { + if (detailsContainer.dataset.loadingMore === '1') return; + detailsContainer.dataset.loadingMore = '1'; + btn.disabled = true; + btn.textContent = typeof window.t === 'function' ? window.t('common.loading') : '加载中…'; + try { + await loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { + append: true, + autoLoadAll: false + }); + } finally { + detailsContainer.dataset.loadingMore = '0'; + } + }; +} /** - * 分页加载过程详情并增量渲染,避免数百轮迭代一次性阻塞主线程。 + * 分页加载过程详情并增量渲染。默认全量加载供恢复流程使用; + * 用户手动展开时传 autoLoadAll=false,只加载一页并展示“加载更多”。 */ -async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId) { +async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId, options) { if (!assistantMessageId || !backendMessageId || typeof apiFetch !== 'function' || typeof renderProcessDetails !== 'function') { return; } + const opts = options || {}; + const autoLoadAll = opts.autoLoadAll !== false; + const detailsContainer = document.getElementById('process-details-' + assistantMessageId); const PAGE = PROCESS_DETAILS_PAGE_SIZE; - let offset = 0; - let isFirst = true; + let offset = opts.append && detailsContainer && detailsContainer.dataset.nextOffset + ? parseInt(detailsContainer.dataset.nextOffset, 10) || 0 + : 0; + let isFirst = !opts.append; while (true) { const res = await apiFetch( '/api/messages/' + encodeURIComponent(String(backendMessageId)) + @@ -1401,12 +1437,18 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId) const hasMore = !!(j && j.hasMore); renderProcessDetails(assistantMessageId, details, { append: !isFirst, - markLoaded: !hasMore + markLoaded: autoLoadAll ? !hasMore : true }); - if (!hasMore || details.length === 0) { + offset += details.length; + if (detailsContainer) { + detailsContainer.dataset.lazyNotLoaded = '0'; + detailsContainer.dataset.loaded = hasMore ? 'partial' : '1'; + detailsContainer.dataset.nextOffset = String(offset); + } + updateProcessDetailsLoadMoreButton(assistantMessageId, backendMessageId, hasMore && !autoLoadAll); + if (!hasMore || details.length === 0 || !autoLoadAll) { break; } - offset += details.length; isFirst = false; await new Promise((resolve) => requestAnimationFrame(resolve)); } @@ -1437,7 +1479,7 @@ function toggleProcessDetails(progressId, assistantMessageId) { if (timeline) { timeline.innerHTML = '
' + ((typeof window.t === 'function') ? window.t('common.loading') : '加载中…') + '
'; } - loadProcessDetailsPaginated(assistantMessageId, backendMessageId) + loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { autoLoadAll: false }) .catch((e) => { console.error('加载过程详情失败:', e); const tl = detailsContainer.querySelector('.progress-timeline'); @@ -1527,82 +1569,6 @@ async function cancelProgressTask(progressId) { openUserInterruptModal(progressId, state.conversationId); } -// 将进度消息转换为可折叠的详情组件 -function convertProgressToDetails(progressId, assistantMessageId) { - const progressElement = document.getElementById(progressId); - if (!progressElement) return; - - // 获取时间线内容 - const timeline = document.getElementById(progressId + '-timeline'); - // 即使时间线不存在,也创建详情组件(显示空状态) - let timelineHTML = ''; - if (timeline) { - timelineHTML = timeline.innerHTML; - } - - // 获取助手消息元素 - const assistantElement = document.getElementById(assistantMessageId); - if (!assistantElement) { - removeMessage(progressId); - return; - } - - // 创建详情组件 - const detailsId = 'details-' + Date.now() + '-' + messageCounter++; - const detailsDiv = document.createElement('div'); - detailsDiv.id = detailsId; - detailsDiv.className = 'message system progress-details'; - - const contentWrapper = document.createElement('div'); - contentWrapper.className = 'message-content'; - - const bubble = document.createElement('div'); - bubble.className = 'message-bubble progress-container completed'; - - // 获取时间线HTML内容 - const hasContent = timelineHTML.trim().length > 0; - - // 检查时间线中是否有错误项 - const hasError = timeline && timeline.querySelector('.timeline-item-error'); - - // 如果有错误,默认折叠;否则默认展开 - const shouldExpand = !hasError; - const expandedClass = shouldExpand ? 'expanded' : ''; - const collapseDetailText = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; - const expandDetailText = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; - const toggleText = shouldExpand ? collapseDetailText : expandDetailText; - const penetrationDetailText = typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情'; - const noProcessDetailText = typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)'; - bubble.innerHTML = ` -
- 📋 ${penetrationDetailText} - ${hasContent ? `` : ''} -
- ${hasContent ? `
${timelineHTML}
` : '
' + noProcessDetailText + '
'} - `; - - contentWrapper.appendChild(bubble); - detailsDiv.appendChild(contentWrapper); - - // 将详情组件插入到助手消息之后 - const messagesDiv = document.getElementById('chat-messages'); - const insertWasPinned = typeof window.captureScrollPinState === 'function' - ? window.captureScrollPinState() - : (typeof window.isChatMessagesPinnedToBottom === 'function' ? window.isChatMessagesPinnedToBottom() : true); - // assistantElement 是消息div,需要插入到它的下一个兄弟节点之前 - if (assistantElement.nextSibling) { - messagesDiv.insertBefore(detailsDiv, assistantElement.nextSibling); - } else { - // 如果没有下一个兄弟节点,直接追加 - messagesDiv.appendChild(detailsDiv); - } - - // 移除原来的进度消息 - removeMessage(progressId); - - scrollChatMessagesToBottomIfPinned(insertWasPinned); -} - /** 将后端消息 UUID 绑定到助手气泡,供删除本轮 / 过程详情懒加载(domId 为前端 msg-*) */ function applyBackendMessageIdToAssistantDom(domAssistantId, backendMessageId) { if (!domAssistantId || !backendMessageId) return; @@ -2065,11 +2031,7 @@ function handleStreamEvent(event, progressElement, progressId, const contentEl = item.querySelector('.timeline-item-content'); if (contentEl) { flushStreamPlainTextUpdate(contentEl); - if (typeof formatMarkdown === 'function') { - setTimelineItemContentStreamRich(contentEl, formatMarkdown(s.buffer, timelineMarkdownOpts)); - } else { - setTimelineItemContentStreamPlain(contentEl, s.buffer); - } + setTimelineItemContentStreamPlain(contentEl, s.buffer); } } break; @@ -2091,11 +2053,6 @@ function handleStreamEvent(event, progressElement, progressId, case 'tool_calls_detected': // 助手正文段结束、进入工具调用:下一段 response_start 应新建时间线条目 responseStreamStateByProgressId.delete(progressId); - addTimelineItem(timeline, 'tool_calls_detected', { - title: timelineAgentBracketPrefix(event.data) + '🔧 ' + (typeof window.t === 'function' ? window.t('chat.toolCallsDetected', { count: event.data?.count || 0 }) : '检测到 ' + (event.data?.count || 0) + ' 个工具调用'), - message: event.message, - data: event.data - }); break; case 'warning': @@ -2240,6 +2197,7 @@ function handleStreamEvent(event, progressElement, progressId, title: timelineAgentBracketPrefix(toolInfo) + '🔧 ' + toolCallTitle, message: event.message, data: toolInfo, + processDetailId: toolInfo.processDetailId || '', expanded: false }); @@ -2304,6 +2262,7 @@ function handleStreamEvent(event, progressElement, progressId, title: timelineAgentBracketPrefix(resultInfo) + statusIcon + ' ' + resultExecText, message: event.message, data: resultInfo, + processDetailId: resultInfo.processDetailId || '', expanded: false }); break; @@ -2391,11 +2350,7 @@ function handleStreamEvent(event, progressElement, progressId, item.appendChild(contentEl); } flushStreamPlainTextUpdate(contentEl); - if (typeof formatMarkdown === 'function') { - setTimelineItemContentStreamRich(contentEl, formatMarkdown(full, timelineMarkdownOpts)); - } else { - setTimelineItemContentStreamPlain(contentEl, full); - } + setTimelineItemContentStreamPlain(contentEl, full); if (d.einoAgent != null && String(d.einoAgent).trim() !== '') { item.dataset.einoAgent = String(d.einoAgent).trim(); } @@ -2640,17 +2595,20 @@ function handleStreamEvent(event, progressElement, progressId, // so the copied timeline HTML reflects the final status. finalizeOutstandingToolCallsForProgress(progressId, 'failed'); + const respMid = responseData.messageId; + if (respMid) { + applyBackendMessageIdToAssistantDom(assistantIdFinal, respMid); + } + const replayCtx = window.csTaskReplay; const directReplay = replayCtx && replayCtx.progressId === progressId; if (!directReplay) { - // 将进度详情集成到工具调用区域(放在最终 response 之后,保证时间线已完整) + // 将详情入口集成到工具调用区域;完整过程从后端分页加载,不依赖实时 DOM 快照。 integrateProgressToMCPSection(progressId, assistantIdFinal, mcpIds); } responseStreamStateByProgressId.delete(progressId); - const respMid = responseData.messageId; if (respMid) { - applyBackendMessageIdToAssistantDom(assistantIdFinal, respMid); if (typeof window.syncAssistantReasoningContentFromServer === 'function') { setTimeout(function () { window.syncAssistantReasoningContentFromServer(respMid, assistantIdFinal); @@ -3466,8 +3424,154 @@ function buildToolResultSectionHtml(data, opts) { ); } +const toolCallDetailStateByItemId = new Map(); + +function setToolCallDetailState(item, state) { + if (!item || !item.id) return; + toolCallDetailStateByItemId.set(item.id, Object.assign({}, state || {})); + item.classList.add('tool-detail-collapsible'); + item.classList.add('tool-call-collapsible'); + updateToolDetailToggleLabel(item); +} + +function toolDetailToggleText(expanded) { + if (typeof window.t === 'function') { + return expanded + ? window.t('chat.collapseToolDetail') + : window.t('chat.viewToolDetail'); + } + return expanded ? '收起' : '查看详情'; +} + +function updateToolDetailToggleLabel(item) { + if (!item) return; + const header = item.querySelector('.timeline-item-header'); + if (!header) return; + const expanded = item.classList.contains('tool-call-detail-expanded'); + header.setAttribute('data-tool-detail-label', toolDetailToggleText(expanded) + (expanded ? ' ▴' : ' ▾')); +} + +async function fetchFullProcessDetailData(detailId) { + const id = detailId != null ? String(detailId).trim() : ''; + if (!id || typeof apiFetch !== 'function') return null; + const res = await apiFetch('/api/process-details/' + encodeURIComponent(id)); + const j = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error((j && j.error) ? j.error : String(res.status)); + } + const detail = j && j.processDetail ? j.processDetail : null; + return detail && detail.data ? detail.data : null; +} + +function collapseOtherToolCallDetails(item) { + const timeline = item && item.closest ? item.closest('.progress-timeline') : null; + if (!timeline) return; + timeline.querySelectorAll('.timeline-item.tool-detail-collapsible.tool-call-detail-expanded').forEach(function (other) { + if (other === item) return; + other.classList.remove('tool-call-detail-expanded'); + const content = other.querySelector('.timeline-item-content.tool-call-detail-content'); + if (content) content.remove(); + updateToolDetailToggleLabel(other); + }); +} + +async function renderToolCallDetailContent(item) { + if (!item || !item.id) return; + const state = toolCallDetailStateByItemId.get(item.id) || {}; + collapseOtherToolCallDetails(item); + + let content = item.querySelector('.timeline-item-content.tool-call-detail-content'); + if (content) { + content.remove(); + item.classList.remove('tool-call-detail-expanded'); + updateToolDetailToggleLabel(item); + return; + } + + content = document.createElement('div'); + content.className = 'timeline-item-content tool-call-detail-content'; + if (state.payloadDeferred && !state.payloadLoaded && (state.processDetailId || state.resultDetailId)) { + content.innerHTML = '
' + + escapeHtml(typeof window.t === 'function' ? window.t('common.loading') : '加载中…') + + '
'; + item.appendChild(content); + item.classList.add('tool-call-detail-expanded'); + updateToolDetailToggleLabel(item); + try { + if (state.processDetailId && !state.hideArgs) { + const fullCall = await fetchFullProcessDetailData(state.processDetailId); + if (fullCall) { + state.args = parseToolCallArgsFromData(fullCall); + state.payloadDeferred = false; + } + } + if (state.resultDetailId) { + const fullResult = await fetchFullProcessDetailData(state.resultDetailId); + if (fullResult) { + state.resultData = fullResult; + const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; + const result = fullResult.result != null ? fullResult.result : (fullResult.error != null ? fullResult.error : noResultText); + state.rawText = typeof result === 'string' ? result : JSON.stringify(result); + } + } + state.payloadLoaded = true; + setToolCallDetailState(item, state); + content.remove(); + item.classList.remove('tool-call-detail-expanded'); + renderToolCallDetailContent(item); + } catch (e) { + content.innerHTML = '
' + escapeHtml(e && e.message ? e.message : '加载失败') + '
'; + } + return; + } + + const args = state.args != null ? state.args : {}; + let resultBlock = ''; + if (state.resultData) { + resultBlock = '
' + + buildToolResultSectionHtml(state.resultData, { rawText: state.rawText }) + + '
'; + } else if (state.pending !== false) { + resultBlock = '
' + + buildToolResultSectionHtml({}, { pending: true }) + + '
'; + } + + const paramsLabel = typeof window.t === 'function' ? window.t('timeline.params') : '参数:'; + const argsBlock = state.hideArgs ? '' : + '
' + + '' + escapeHtml(paramsLabel) + '' + + '
' + escapeHtml(JSON.stringify(args, null, 2)) + '
' + + '
'; + content.innerHTML = '
' + argsBlock + resultBlock + '
'; + item.appendChild(content); + item.classList.add('tool-call-detail-expanded'); + updateToolDetailToggleLabel(item); +} + +if (typeof document !== 'undefined' && !document.__cyberStrikeToolCallDetailToggleBound) { + document.__cyberStrikeToolCallDetailToggleBound = true; + document.addEventListener('click', function (event) { + const target = event.target; + if (target && target.closest && target.closest('button, a, input, textarea, select, pre, code, .timeline-item-content')) { + return; + } + const item = target && target.closest + ? target.closest('.timeline-item.tool-detail-collapsible') + : null; + if (!item) return; + renderToolCallDetailContent(item); + }); +} + function ensureToolCallResultSlot(item) { if (!item) return null; + if (item.classList.contains('tool-call-collapsible')) { + const state = toolCallDetailStateByItemId.get(item.id) || {}; + state.pending = true; + setToolCallDetailState(item, state); + if (!item.classList.contains('tool-call-detail-expanded')) return null; + } let section = item.querySelector('.tool-result-section'); if (section) return section; const content = item.querySelector('.timeline-item-content'); @@ -3488,6 +3592,27 @@ function mergeToolResultIntoCallItem(item, data, options) { const resultStr = typeof result === 'string' ? result : JSON.stringify(result); const text = options.rawText != null ? String(options.rawText) : resultStr; + if (item.classList.contains('tool-call-collapsible')) { + const state = toolCallDetailStateByItemId.get(item.id) || {}; + state.resultData = data; + state.rawText = text; + state.resultDetailId = data.processDetailId || state.resultDetailId || ''; + state.pending = false; + setToolCallDetailState(item, state); + const expanded = item.classList.contains('tool-call-detail-expanded'); + const content = item.querySelector('.timeline-item-content.tool-call-detail-content'); + if (content) content.remove(); + if (expanded) { + item.classList.remove('tool-call-detail-expanded'); + 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'); + return true; + } + let section = item.querySelector('.tool-result-section'); if (!section) { ensureToolCallResultSlot(item); @@ -3568,6 +3693,9 @@ function coalesceProcessDetailsToolPairs(details) { const rd = resultDetail.data || {}; targetDetail.data = targetDetail.data || {}; targetDetail.data._mergedResult = Object.assign({}, rd); + if (resultDetail.id) { + targetDetail.data._mergedResultDetailId = resultDetail.id; + } if (resultDetail.createdAt) { targetDetail.data._mergedResultAt = resultDetail.createdAt; } @@ -3581,6 +3709,7 @@ function coalesceProcessDetailsToolPairs(details) { if (et === 'tool_call') { const copy = { + id: detail.id, eventType: detail.eventType, message: detail.message, createdAt: detail.createdAt, @@ -3692,6 +3821,44 @@ function buildWorkflowBranchDetailHtml(data) { `; } +function isLiveProgressTimeline(timeline) { + return !!(timeline && timeline.id && /^progress-\d+-\d+-timeline$/.test(timeline.id)); +} + +function pruneLiveTimelineIfNeeded(timeline) { + if (!isLiveProgressTimeline(timeline)) return; + const items = Array.from(timeline.children).filter(function (el) { + return el && el.classList && el.classList.contains('timeline-item'); + }); + if (items.length <= LIVE_TIMELINE_MAX_ITEMS) return; + + let marker = timeline.querySelector('.timeline-live-pruned-marker'); + let pruned = marker ? (parseInt(marker.dataset.prunedCount || '0', 10) || 0) : 0; + let removable = items.filter(function (el) { + return !el.dataset.hitlInterruptId && + !el.dataset.workflowRunId && + !el.classList.contains('timeline-item-workflow_hitl_waiting') && + !el.classList.contains('timeline-item-hitl_interrupt'); + }); + const overflow = items.length - LIVE_TIMELINE_MAX_ITEMS; + const removeCount = Math.min(removable.length, Math.max(overflow, LIVE_TIMELINE_PRUNE_CHUNK)); + if (removeCount <= 0) return; + + if (!marker) { + marker = document.createElement('div'); + marker.className = 'timeline-live-pruned-marker'; + timeline.insertBefore(marker, timeline.firstChild); + } + for (let i = 0; i < removeCount; i++) { + removable[i].remove(); + } + pruned += removeCount; + marker.dataset.prunedCount = String(pruned); + marker.textContent = typeof window.t === 'function' + ? window.t('chat.liveTimelinePruned', { count: pruned }) + : ('已收起前 ' + pruned + ' 条实时过程详情,任务完成后可按页查看完整记录'); +} + function addTimelineItem(timeline, type, options) { const item = document.createElement('div'); // 生成唯一ID @@ -3715,6 +3882,9 @@ function addTimelineItem(timeline, type, options) { } if (type === 'tool_call' && options.data) { const d = options.data; + if (options.processDetailId) { + item.dataset.processDetailId = String(options.processDetailId); + } item.dataset.toolName = (d.toolName != null && d.toolName !== '') ? String(d.toolName) : ''; item.dataset.toolIndex = (d.index != null) ? String(d.index) : '0'; item.dataset.toolTotal = (d.total != null) ? String(d.total) : '0'; @@ -3725,6 +3895,10 @@ function addTimelineItem(timeline, type, options) { 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'); + if (d._mergedResultDetailId) { + item.dataset.toolResultDetailId = String(d._mergedResultDetailId); + } } } if (type === 'hitl_interrupt' && options.data && options.data.interruptId != null && String(options.data.interruptId).trim() !== '') { @@ -3738,6 +3912,9 @@ function addTimelineItem(timeline, type, options) { } if (type === 'tool_result' && options.data) { const d = options.data; + 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'; } @@ -3790,34 +3967,29 @@ function addTimelineItem(timeline, type, options) { const streamBody = typeof formatTimelineStreamBody === 'function' ? formatTimelineStreamBody(options.message, options.data) : options.message; - content += `
${formatMarkdown(streamBody, timelineMarkdownOpts)}
`; + content += `
${formatTimelinePlainTextHtml(streamBody)}
`; } else if (type === 'tool_call' && options.data) { const data = options.data; 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 }) + '
'; + item.classList.add('tool-call-running'); } - content += ` -
-
-
- ${escapeHtml(paramsLabel)} -
${escapeHtml(JSON.stringify(args, null, 2))}
-
- ${resultBlock} -
-
- `; + setToolCallDetailState(item, { + args: args, + resultData: merged || null, + pending: !merged && !options.skipPendingResult, + processDetailId: options.processDetailId || '', + resultDetailId: data._mergedResultDetailId || '', + payloadDeferred: data._payloadDeferred === true || (merged && merged._payloadDeferred === true), + payloadLoaded: !(data._payloadDeferred === true || (merged && merged._payloadDeferred === true)) + }); } else if ((type === 'eino_agent_reply' || type === 'workflow_agent_output') && options.message) { let prefix = ''; if (type === 'workflow_agent_output' && options.data) { @@ -3839,9 +4011,9 @@ function addTimelineItem(timeline, type, options) { const body = type === 'workflow_agent_output' ? `
输出
-
${formatMarkdown(options.message, timelineMarkdownOpts)}
+
${formatTimelinePlainTextHtml(options.message)}
` - : formatMarkdown(options.message, timelineMarkdownOpts); + : formatTimelinePlainTextHtml(options.message); content += `
${prefix}${body}
`; } else if (type === 'workflow_node_result' && options.data && String(options.data.nodeType || '').toLowerCase() === 'condition') { content += buildWorkflowConditionResultHtml(options.data); @@ -3853,17 +4025,17 @@ function addTimelineItem(timeline, type, options) { const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; const result = data.result || data.error || noResultText; const resultStr = typeof result === 'string' ? result : JSON.stringify(result); - const execResultLabel = typeof window.t === 'function' ? window.t('timeline.executionResult') : '执行结果:'; - const execIdLabel = typeof window.t === 'function' ? window.t('timeline.executionId') : '执行ID:'; - content += ` -
-
- ${escapeHtml(execResultLabel)} -
${escapeHtml(resultStr)}
- ${data.executionId ? `
${escapeHtml(execIdLabel)} ${escapeHtml(data.executionId)}
` : ''} -
-
- `; + setToolCallDetailState(item, { + resultData: data, + rawText: resultStr, + pending: false, + hideArgs: true, + processDetailId: options.processDetailId || '', + resultDetailId: options.processDetailId || '', + payloadDeferred: data._payloadDeferred === true, + payloadLoaded: data._payloadDeferred !== true + }); + item.classList.add(isError ? 'tool-call-failed' : 'tool-call-completed'); } else if (type === 'cancelled') { const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消'; content += ` @@ -3875,21 +4047,25 @@ function addTimelineItem(timeline, type, options) { const streamBody = typeof formatTimelineStreamBody === 'function' ? formatTimelineStreamBody(options.message, options.data) : options.message; - content += `
${formatMarkdown(streamBody, timelineMarkdownOpts)}
`; + content += `
${formatTimelinePlainTextHtml(streamBody)}
`; } else if (type === 'progress' && options.message) { content += `
${escapeHtml(options.message)}
`; } else if (type === 'user_interrupt_continue' && options.message) { const streamBody = typeof formatTimelineStreamBody === 'function' ? formatTimelineStreamBody(options.message, options.data) : options.message; - content += `
${formatMarkdown(streamBody, timelineMarkdownOpts)}
`; + content += `
${formatTimelinePlainTextHtml(streamBody)}
`; } item.innerHTML = content; + if (item.classList.contains('tool-detail-collapsible')) { + updateToolDetailToggleLabel(item); + } if (options.data) { applyEinoTimelineRole(item, options.data); } timeline.appendChild(item); + pruneLiveTimelineIfNeeded(timeline); // 自动展开详情 const expanded = timeline.classList.contains('expanded'); @@ -6245,6 +6421,14 @@ function refreshProgressAndTimelineI18n() { timeSpan.textContent = d.toLocaleTimeString(timeLocale, timeOpts); } } + if (item.classList.contains('tool-detail-collapsible') && typeof updateToolDetailToggleLabel === 'function') { + updateToolDetailToggleLabel(item); + } + }); + + document.querySelectorAll('.timeline-live-pruned-marker').forEach(function (marker) { + const count = parseInt(marker.dataset.prunedCount || '0', 10) || 0; + marker.textContent = _t('chat.liveTimelinePruned', { count: count }); }); // 详情区「展开/收起」按钮 diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 325dfaf9..6f4d91e2 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -2311,9 +2311,15 @@ function initModelListControls() { hitlAuditProv.dataset.modelListBound = '1'; hitlAuditProv.addEventListener('change', syncModelListFetchButtons); } + const knowledgeEmbeddingProv = document.getElementById('knowledge-embedding-provider'); + if (knowledgeEmbeddingProv && !knowledgeEmbeddingProv.dataset.modelListBound) { + knowledgeEmbeddingProv.dataset.modelListBound = '1'; + knowledgeEmbeddingProv.addEventListener('change', syncModelListFetchButtons); + } bindModelSelect('openai'); bindModelSelect('vision'); bindModelSelect('hitlAudit'); + bindModelSelect('knowledgeEmbedding'); syncModelListFetchButtons(); } @@ -2324,6 +2330,9 @@ function modelSelectIds(scope) { if (scope === 'hitlAudit') { return { selectId: 'hitl-audit-model-select', inputId: 'hitl-audit-model-name' }; } + if (scope === 'knowledgeEmbedding') { + return { selectId: 'knowledge-embedding-model-select', inputId: 'knowledge-embedding-model' }; + } return { selectId: 'openai-model-select', inputId: 'openai-model' }; } @@ -2359,6 +2368,15 @@ function resolveModelListCredentials(scope) { || (document.getElementById('openai-api-key')?.value || '').trim(); return { provider, base_url: baseUrl, api_key: apiKey }; } + if (scope === 'knowledgeEmbedding') { + const kp = (document.getElementById('knowledge-embedding-provider')?.value || '').trim(); + const provider = kp || document.getElementById('openai-provider')?.value || 'openai'; + const baseUrl = (document.getElementById('knowledge-embedding-base-url')?.value || '').trim() + || (document.getElementById('openai-base-url')?.value || '').trim(); + const apiKey = (document.getElementById('knowledge-embedding-api-key')?.value || '').trim() + || (document.getElementById('openai-api-key')?.value || '').trim(); + return { provider, base_url: baseUrl, api_key: apiKey }; + } return { provider: document.getElementById('openai-provider')?.value || 'openai', base_url: (document.getElementById('openai-base-url')?.value || '').trim(), @@ -2444,6 +2462,32 @@ function syncModelListFetchButtons() { hitlAuditHint.style.display = 'none'; } } + + const kp = (document.getElementById('knowledge-embedding-provider')?.value || '').trim(); + const knowledgeEmbeddingEffectiveProv = kp || openaiProv; + const knowledgeEmbeddingBtn = document.getElementById('fetch-knowledge-embedding-models-btn'); + const knowledgeEmbeddingHint = document.getElementById('fetch-knowledge-embedding-models-hint'); + const knowledgeEmbeddingSelect = document.getElementById('knowledge-embedding-model-select'); + const isClaudeKnowledgeEmbedding = knowledgeEmbeddingEffectiveProv === 'claude'; + if (knowledgeEmbeddingBtn) { + knowledgeEmbeddingBtn.style.display = isClaudeKnowledgeEmbedding ? 'none' : ''; + } + if (knowledgeEmbeddingSelect && isClaudeKnowledgeEmbedding) { + knowledgeEmbeddingSelect.style.display = 'none'; + const knowledgeEmbeddingWrap = modelPickSelectMap['knowledge-embedding-model-select']; + if (knowledgeEmbeddingWrap) knowledgeEmbeddingWrap.wrapper.style.display = 'none'; + } else if (knowledgeEmbeddingSelect && !isClaudeKnowledgeEmbedding) { + syncModelPickDropdown('knowledge-embedding-model-select'); + } + if (knowledgeEmbeddingHint) { + if (isClaudeKnowledgeEmbedding) { + knowledgeEmbeddingHint.textContent = tFn('settingsBasic.modelsListClaudeHint'); + knowledgeEmbeddingHint.style.display = ''; + } else { + knowledgeEmbeddingHint.textContent = ''; + knowledgeEmbeddingHint.style.display = 'none'; + } + } } function populateModelSelect(scope, models, currentValue) { @@ -2483,12 +2527,27 @@ function populateModelSelect(scope, models, currentValue) { async function fetchModelList(scope) { const tFn = typeof window.t === 'function' ? window.t : (k) => k; const creds = resolveModelListCredentials(scope); - const btnId = scope === 'vision' - ? 'fetch-vision-models-btn' - : (scope === 'hitlAudit' ? 'fetch-hitl-audit-models-btn' : 'fetch-openai-models-btn'); - const resultId = scope === 'vision' - ? 'fetch-vision-models-result' - : (scope === 'hitlAudit' ? 'fetch-hitl-audit-models-result' : 'fetch-openai-models-result'); + const modelListUiIds = { + openai: { + btnId: 'fetch-openai-models-btn', + resultId: 'fetch-openai-models-result' + }, + vision: { + btnId: 'fetch-vision-models-btn', + resultId: 'fetch-vision-models-result' + }, + hitlAudit: { + btnId: 'fetch-hitl-audit-models-btn', + resultId: 'fetch-hitl-audit-models-result' + }, + knowledgeEmbedding: { + btnId: 'fetch-knowledge-embedding-models-btn', + resultId: 'fetch-knowledge-embedding-models-result' + } + }; + const uiIds = modelListUiIds[scope] || modelListUiIds.openai; + const btnId = uiIds.btnId; + const resultId = uiIds.resultId; const inputId = modelSelectIds(scope).inputId; const btn = document.getElementById(btnId); const resultEl = document.getElementById(resultId); diff --git a/web/templates/index.html b/web/templates/index.html index a19d8392..06960717 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -3253,7 +3253,15 @@
- +
+ + + 获取列表 +
+ +