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 = `
-
- ${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 ? '' :
+ '';
+ 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 += `
-
- `;
+ 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 += `
-
- `;
+ 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 @@