mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-23 22:40:05 +02:00
Add files via upload
This commit is contained in:
@@ -2456,16 +2456,68 @@ header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mcp-call-buttons {
|
||||
.mcp-call-buttons,
|
||||
.mcp-call-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mcp-tool-list {
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mcp-tool-list.expanded {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mcp-tools-toggle-btn {
|
||||
background: rgba(25, 118, 210, 0.1) !important;
|
||||
border-color: rgba(25, 118, 210, 0.35) !important;
|
||||
color: #1976d2 !important;
|
||||
}
|
||||
|
||||
.mcp-call-toolbar .process-detail-btn,
|
||||
.mcp-call-toolbar .mcp-tools-toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mcp-call-toolbar .process-detail-btn span,
|
||||
.mcp-call-toolbar .mcp-tools-toggle-btn span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.mcp-tools-toggle-btn:hover {
|
||||
background: rgba(25, 118, 210, 0.18) !important;
|
||||
border-color: #1976d2 !important;
|
||||
color: #1565c0 !important;
|
||||
}
|
||||
|
||||
.process-detail-btn {
|
||||
background: rgba(156, 39, 176, 0.1) !important;
|
||||
border-color: rgba(156, 39, 176, 0.3) !important;
|
||||
color: #9c27b0 !important;
|
||||
}
|
||||
|
||||
.mcp-call-toolbar .process-detail-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
@@ -530,6 +530,8 @@
|
||||
"noMatchTools": "No matching tools",
|
||||
"penetrationTestDetail": "Penetration test details",
|
||||
"expandDetail": "Expand details",
|
||||
"toolExecutionsCount": "{{n}} tool runs",
|
||||
"collapseToolExecutions": "Collapse tool runs",
|
||||
"noProcessDetail": "No process details (execution may be too fast or no detailed events)",
|
||||
"copyMessageTitle": "Copy message",
|
||||
"deleteTurnTitle": "Delete this turn",
|
||||
|
||||
@@ -518,6 +518,8 @@
|
||||
"noMatchTools": "没有匹配的工具",
|
||||
"penetrationTestDetail": "渗透测试详情",
|
||||
"expandDetail": "展开详情",
|
||||
"toolExecutionsCount": "{{n}}次工具执行",
|
||||
"collapseToolExecutions": "收起工具执行",
|
||||
"noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)",
|
||||
"copyMessageTitle": "复制消息内容",
|
||||
"deleteTurnTitle": "删除本轮对话",
|
||||
|
||||
+392
-103
@@ -1,6 +1,39 @@
|
||||
let currentConversationId = null;
|
||||
let loadConversationRequestSeq = 0;
|
||||
|
||||
/** 轻量会话 LRU 缓存:来回切换已加载会话时避免重复网络 + 全量 DOM 重建 */
|
||||
const CONVERSATION_LITE_CACHE_MAX = 12;
|
||||
const conversationLiteCache = new Map();
|
||||
|
||||
function getConversationLiteFromCache(conversationId) {
|
||||
if (!conversationId) return null;
|
||||
const hit = conversationLiteCache.get(conversationId);
|
||||
if (!hit) return null;
|
||||
conversationLiteCache.delete(conversationId);
|
||||
conversationLiteCache.set(conversationId, hit);
|
||||
return hit;
|
||||
}
|
||||
|
||||
function putConversationLiteCache(conversationId, data) {
|
||||
if (!conversationId || !data) return;
|
||||
conversationLiteCache.delete(conversationId);
|
||||
conversationLiteCache.set(conversationId, data);
|
||||
while (conversationLiteCache.size > CONVERSATION_LITE_CACHE_MAX) {
|
||||
const oldest = conversationLiteCache.keys().next().value;
|
||||
conversationLiteCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
function invalidateConversationLiteCache(conversationId) {
|
||||
if (conversationId) {
|
||||
conversationLiteCache.delete(conversationId);
|
||||
} else {
|
||||
conversationLiteCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
window.invalidateConversationLiteCache = invalidateConversationLiteCache;
|
||||
|
||||
// @ 提及相关状态
|
||||
let mentionTools = [];
|
||||
let mentionToolsLoaded = false;
|
||||
@@ -886,6 +919,9 @@ async function sendMessage() {
|
||||
window.CyberStrikeChatScroll.onUserSendMessage();
|
||||
}
|
||||
addMessage('user', displayMessage, null, null, null, { scroll: 'none' });
|
||||
if (currentConversationId) {
|
||||
invalidateConversationLiteCache(currentConversationId);
|
||||
}
|
||||
|
||||
// 清除防抖定时器,防止在清空输入框后重新保存草稿
|
||||
if (draftSaveTimer) {
|
||||
@@ -2027,31 +2063,13 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
|
||||
// 有 MCP 执行记录且非流式占位消息时展示调用按钮;带 progressId 的流式占位不挂此条(与进度卡片一致,结束时 integrate 再创建)
|
||||
if (role === 'assistant' && (mcpExecutionIds && Array.isArray(mcpExecutionIds) && mcpExecutionIds.length > 0) && !progressId) {
|
||||
const mcpSection = document.createElement('div');
|
||||
mcpSection.className = 'mcp-call-section';
|
||||
|
||||
const mcpLabel = document.createElement('div');
|
||||
mcpLabel.className = 'mcp-call-label';
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
mcpSection.appendChild(mcpLabel);
|
||||
|
||||
const buttonsContainer = document.createElement('div');
|
||||
buttonsContainer.className = 'mcp-call-buttons';
|
||||
|
||||
mcpExecutionIds.forEach((execId, index) => {
|
||||
const detailBtn = document.createElement('button');
|
||||
detailBtn.className = 'mcp-detail-btn';
|
||||
detailBtn.dataset.execId = execId;
|
||||
detailBtn.dataset.execIndex = String(index + 1);
|
||||
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
|
||||
detailBtn.onclick = () => showMCPDetail(execId);
|
||||
buttonsContainer.appendChild(detailBtn);
|
||||
});
|
||||
// 使用批量 API 一次性获取所有工具名称(消除 N 次单独请求)
|
||||
batchUpdateButtonToolNames(buttonsContainer, mcpExecutionIds);
|
||||
|
||||
mcpSection.appendChild(buttonsContainer);
|
||||
contentWrapper.appendChild(mcpSection);
|
||||
if (options && options.deferMcpButtons) {
|
||||
try {
|
||||
messageDiv.dataset.pendingMcpExecutionIds = JSON.stringify(mcpExecutionIds);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
appendMcpCallButtons(messageDiv, mcpExecutionIds);
|
||||
}
|
||||
}
|
||||
|
||||
messageDiv.appendChild(contentWrapper);
|
||||
@@ -2151,11 +2169,13 @@ function copyMessageToClipboard(messageDiv, button) {
|
||||
function showCopySuccess(button) {
|
||||
if (button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.dataset.copySuccessActive = '1';
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>' + (typeof window.t === 'function' ? window.t('common.copied') : '已复制') + '</span>';
|
||||
button.style.color = '#10b981';
|
||||
button.style.background = 'rgba(16, 185, 129, 0.1)';
|
||||
button.style.borderColor = 'rgba(16, 185, 129, 0.3)';
|
||||
setTimeout(() => {
|
||||
delete button.dataset.copySuccessActive;
|
||||
button.innerHTML = originalText;
|
||||
button.style.color = '';
|
||||
button.style.background = '';
|
||||
@@ -2301,47 +2321,20 @@ function processDetailRowFingerprint(d) {
|
||||
}
|
||||
|
||||
// 渲染过程详情
|
||||
function renderProcessDetails(messageId, processDetails) {
|
||||
// options.append=true 时分页追加;options.markLoaded=false 时保留 lazy 标记(分页加载中)
|
||||
function renderProcessDetails(messageId, processDetails, options) {
|
||||
const renderOpts = options || {};
|
||||
const appendMode = !!renderOpts.append;
|
||||
const markLoaded = renderOpts.markLoaded !== false;
|
||||
const messageElement = document.getElementById(messageId);
|
||||
if (!messageElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找或创建MCP调用区域
|
||||
let mcpSection = messageElement.querySelector('.mcp-call-section');
|
||||
if (!mcpSection) {
|
||||
mcpSection = document.createElement('div');
|
||||
mcpSection.className = 'mcp-call-section';
|
||||
|
||||
const contentWrapper = messageElement.querySelector('.message-content');
|
||||
if (contentWrapper) {
|
||||
contentWrapper.appendChild(mcpSection);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保有标签和按钮容器(统一结构)
|
||||
let mcpLabel = mcpSection.querySelector('.mcp-call-label');
|
||||
let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons');
|
||||
|
||||
// 如果没有标签,创建一个(当没有工具调用时)
|
||||
if (!mcpLabel && !buttonsContainer) {
|
||||
mcpLabel = document.createElement('div');
|
||||
mcpLabel.className = 'mcp-call-label';
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
mcpSection.appendChild(mcpLabel);
|
||||
} else if (mcpLabel && mcpLabel.textContent !== ('📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情'))) {
|
||||
// 如果标签存在但不是统一格式,更新它
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
}
|
||||
|
||||
// 如果没有按钮容器,创建一个
|
||||
if (!buttonsContainer) {
|
||||
buttonsContainer = document.createElement('div');
|
||||
buttonsContainer.className = 'mcp-call-buttons';
|
||||
mcpSection.appendChild(buttonsContainer);
|
||||
}
|
||||
// 查找或创建 MCP 区域(工具栏 + 工具列表 + 迭代时间线 分区)
|
||||
const chrome = ensureMcpCallSectionChrome(messageElement, messageId);
|
||||
if (!chrome) return;
|
||||
const { mcpSection, toolbar: buttonsContainer } = chrome;
|
||||
|
||||
// 添加过程详情按钮(如果还没有)
|
||||
let processDetailBtn = buttonsContainer.querySelector('.process-detail-btn');
|
||||
@@ -2352,17 +2345,20 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
processDetailBtn.onclick = () => toggleProcessDetails(null, messageId);
|
||||
buttonsContainer.appendChild(processDetailBtn);
|
||||
}
|
||||
syncMcpToolsToggleButton(messageElement);
|
||||
|
||||
// 创建过程详情容器(放在按钮容器之后)
|
||||
// 创建过程详情容器(放在工具列表之后)
|
||||
const detailsId = 'process-details-' + messageId;
|
||||
let detailsContainer = document.getElementById(detailsId);
|
||||
const toolListEl = chrome.toolList;
|
||||
|
||||
if (!detailsContainer) {
|
||||
detailsContainer = document.createElement('div');
|
||||
detailsContainer.id = detailsId;
|
||||
detailsContainer.className = 'process-details-container';
|
||||
// 确保容器在按钮容器之后
|
||||
if (buttonsContainer.nextSibling) {
|
||||
if (toolListEl) {
|
||||
toolListEl.after(detailsContainer);
|
||||
} else if (buttonsContainer.nextSibling) {
|
||||
mcpSection.insertBefore(detailsContainer, buttonsContainer.nextSibling);
|
||||
} else {
|
||||
mcpSection.appendChild(detailsContainer);
|
||||
@@ -2391,17 +2387,21 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
if (isLazyNotLoaded && !reasoningFromMessage) {
|
||||
detailsContainer.dataset.lazyNotLoaded = '1';
|
||||
detailsContainer.dataset.loaded = '0';
|
||||
timeline.innerHTML = '<div class="progress-timeline-empty">' +
|
||||
(typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
|
||||
'(点击后加载)</div>';
|
||||
const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
|
||||
let lazyHint = expandLabel + '(点击后加载迭代详情)';
|
||||
timeline.innerHTML = '<div class="progress-timeline-empty">' + lazyHint + '</div>';
|
||||
timeline.classList.remove('expanded');
|
||||
prefetchProcessDetailsSummaryHint(messageId, messageElement);
|
||||
return;
|
||||
}
|
||||
if (isLazyNotLoaded) {
|
||||
detailsContainer.dataset.lazyNotLoaded = '1';
|
||||
detailsContainer.dataset.loaded = '0';
|
||||
processDetails = [];
|
||||
} else {
|
||||
if (!appendMode) {
|
||||
prefetchProcessDetailsSummaryHint(messageId, messageElement);
|
||||
}
|
||||
} else if (markLoaded) {
|
||||
detailsContainer.dataset.lazyNotLoaded = '0';
|
||||
detailsContainer.dataset.loaded = '1';
|
||||
}
|
||||
@@ -2413,15 +2413,16 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
}
|
||||
// 如果没有processDetails或为空,显示空状态
|
||||
if (!processDetails || processDetails.length === 0) {
|
||||
// 显示空状态提示
|
||||
timeline.innerHTML = '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>';
|
||||
// 默认折叠
|
||||
timeline.classList.remove('expanded');
|
||||
if (!appendMode) {
|
||||
timeline.innerHTML = '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>';
|
||||
timeline.classList.remove('expanded');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空时间线并重新渲染
|
||||
timeline.innerHTML = '';
|
||||
if (!appendMode) {
|
||||
timeline.innerHTML = '';
|
||||
}
|
||||
|
||||
|
||||
function processDetailAgentPrefix(d) {
|
||||
@@ -2430,14 +2431,12 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
return s ? ('[' + s + '] ') : '';
|
||||
}
|
||||
|
||||
// 渲染每个过程详情事件
|
||||
processDetails.forEach(detail => {
|
||||
function renderOneProcessDetail(detail) {
|
||||
const eventType = detail.eventType || '';
|
||||
const title = detail.message || '';
|
||||
const data = detail.data || {};
|
||||
const agPx = processDetailAgentPrefix(data);
|
||||
|
||||
// 根据事件类型渲染不同的内容
|
||||
let itemTitle = title;
|
||||
if (eventType === 'iteration') {
|
||||
const n = data.iteration || 1;
|
||||
@@ -2530,15 +2529,38 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
title: itemTitle,
|
||||
message: detail.message || '',
|
||||
data: data,
|
||||
createdAt: detail.createdAt // 传递实际的事件创建时间
|
||||
createdAt: detail.createdAt
|
||||
};
|
||||
if (eventType === 'tool_call' && data._mergedResult) {
|
||||
timelineOpts.mergedResult = data._mergedResult;
|
||||
}
|
||||
addTimelineItem(timeline, eventType, timelineOpts);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLazyNotLoaded && reasoningFromMessage) {
|
||||
const TIMELINE_RENDER_BATCH = 40;
|
||||
const renderTimelineBatch = (startIdx) => {
|
||||
const endIdx = Math.min(startIdx + TIMELINE_RENDER_BATCH, processDetails.length);
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
renderOneProcessDetail(processDetails[i]);
|
||||
}
|
||||
if (endIdx < processDetails.length) {
|
||||
requestAnimationFrame(() => renderTimelineBatch(endIdx));
|
||||
} else if (markLoaded) {
|
||||
finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoaded, timeline);
|
||||
}
|
||||
};
|
||||
if (processDetails.length > TIMELINE_RENDER_BATCH) {
|
||||
renderTimelineBatch(0);
|
||||
} else {
|
||||
processDetails.forEach(renderOneProcessDetail);
|
||||
if (markLoaded) {
|
||||
finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoaded, timeline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoaded, timeline) {
|
||||
if (isLazyNotLoaded && getMessageReasoningContent(messageElement)) {
|
||||
const lazyHint = document.createElement('div');
|
||||
lazyHint.className = 'progress-timeline-empty progress-timeline-lazy-hint';
|
||||
lazyHint.textContent = (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
|
||||
@@ -2546,15 +2568,12 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
timeline.appendChild(lazyHint);
|
||||
}
|
||||
|
||||
// 检查是否有错误或取消事件,如果有,确保详情默认折叠(但仍有待审批 HITL 时保持展开,由 restoreHitlInlineForConversation 处理)
|
||||
const hasPendingHitlInDetails = processDetails.some(d => d && d.eventType === 'hitl_interrupt');
|
||||
const hasErrorOrCancelled = processDetails.some(d =>
|
||||
d.eventType === 'error' || d.eventType === 'cancelled'
|
||||
);
|
||||
if (hasErrorOrCancelled && !hasPendingHitlInDetails) {
|
||||
// 确保时间线是折叠的
|
||||
timeline.classList.remove('expanded');
|
||||
// 更新按钮文本为"展开详情"
|
||||
const processDetailBtn = messageElement.querySelector('.process-detail-btn');
|
||||
if (processDetailBtn) {
|
||||
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
@@ -2562,6 +2581,36 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 懒加载折叠态:后台拉摘要,提示迭代规模而不加载全量详情 */
|
||||
function prefetchProcessDetailsSummaryHint(messageId, messageElement) {
|
||||
if (!messageElement || !messageElement.dataset || !messageElement.dataset.backendMessageId) return;
|
||||
const backendId = String(messageElement.dataset.backendMessageId).trim();
|
||||
if (!backendId || typeof apiFetch !== 'function') return;
|
||||
const detailsContainer = document.getElementById('process-details-' + messageId);
|
||||
if (!detailsContainer || detailsContainer.dataset.summaryFetched === '1') return;
|
||||
detailsContainer.dataset.summaryFetched = '1';
|
||||
apiFetch('/api/messages/' + encodeURIComponent(backendId) + '/process-details?summary=1')
|
||||
.then(async (res) => {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !j.summary) return;
|
||||
const s = j.summary;
|
||||
const timeline = detailsContainer.querySelector('.progress-timeline');
|
||||
if (!timeline || detailsContainer.dataset.loaded === '1') return;
|
||||
const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
|
||||
let hint = expandLabel + '(点击后加载迭代详情)';
|
||||
if (s.maxIteration > 0) {
|
||||
hint = expandLabel + '(共 ' + s.maxIteration + ' 轮迭代,' + (s.total || 0) + ' 条详情)';
|
||||
} else if (s.total > 0) {
|
||||
hint = expandLabel + '(共 ' + (s.total || 0) + ' 条详情)';
|
||||
}
|
||||
const empty = timeline.querySelector('.progress-timeline-empty');
|
||||
if (empty) {
|
||||
empty.textContent = hint;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// 移除消息
|
||||
function removeMessage(id) {
|
||||
const messageDiv = document.getElementById(id);
|
||||
@@ -2623,6 +2672,201 @@ async function updateButtonWithToolName(button, executionId, index) {
|
||||
}
|
||||
}
|
||||
|
||||
function getPendingMcpExecutionCount(messageElement) {
|
||||
if (!messageElement || !messageElement.dataset || !messageElement.dataset.pendingMcpExecutionIds) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
const ids = JSON.parse(messageElement.dataset.pendingMcpExecutionIds);
|
||||
return Array.isArray(ids) ? ids.length : 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getMcpExecutionCount(messageElement) {
|
||||
const pending = getPendingMcpExecutionCount(messageElement);
|
||||
if (pending > 0) return pending;
|
||||
const toolList = messageElement && messageElement.querySelector('.mcp-tool-list');
|
||||
if (toolList) {
|
||||
return toolList.querySelectorAll('.mcp-detail-btn[data-exec-id]').length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function formatMcpToolsToggleLabel(count, expanded) {
|
||||
if (expanded) {
|
||||
if (typeof window.t === 'function') {
|
||||
const s = window.t('chat.collapseToolExecutions');
|
||||
if (s && s !== 'chat.collapseToolExecutions') return s;
|
||||
}
|
||||
return '收起工具执行';
|
||||
}
|
||||
if (typeof window.t === 'function') {
|
||||
const s = window.t('chat.toolExecutionsCount', { n: count });
|
||||
if (s && s !== 'chat.toolExecutionsCount') return s;
|
||||
}
|
||||
return count + '次工具执行';
|
||||
}
|
||||
|
||||
/** 渗透测试区:工具栏(展开详情 | N次工具执行)+ 独立工具列表 + 迭代时间线 */
|
||||
function ensureMcpCallSectionChrome(messageElement, messageId) {
|
||||
const contentWrapper = messageElement && messageElement.querySelector('.message-content');
|
||||
if (!contentWrapper) return null;
|
||||
|
||||
let mcpSection = messageElement.querySelector('.mcp-call-section');
|
||||
if (!mcpSection) {
|
||||
mcpSection = document.createElement('div');
|
||||
mcpSection.className = 'mcp-call-section';
|
||||
const mcpLabel = document.createElement('div');
|
||||
mcpLabel.className = 'mcp-call-label';
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
mcpSection.appendChild(mcpLabel);
|
||||
contentWrapper.appendChild(mcpSection);
|
||||
} else {
|
||||
const mcpLabel = mcpSection.querySelector('.mcp-call-label');
|
||||
const labelText = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
if (mcpLabel && mcpLabel.textContent !== labelText) {
|
||||
mcpLabel.textContent = labelText;
|
||||
}
|
||||
}
|
||||
|
||||
let toolbar = mcpSection.querySelector('.mcp-call-toolbar');
|
||||
const legacyButtons = mcpSection.querySelector('.mcp-call-buttons');
|
||||
if (!toolbar) {
|
||||
toolbar = document.createElement('div');
|
||||
toolbar.className = 'mcp-call-toolbar';
|
||||
if (legacyButtons) {
|
||||
const processBtn = legacyButtons.querySelector('.process-detail-btn');
|
||||
if (processBtn) toolbar.appendChild(processBtn);
|
||||
mcpSection.replaceChild(toolbar, legacyButtons);
|
||||
} else {
|
||||
mcpSection.appendChild(toolbar);
|
||||
}
|
||||
}
|
||||
|
||||
let toolList = mcpSection.querySelector('.mcp-tool-list');
|
||||
if (!toolList) {
|
||||
toolList = document.createElement('div');
|
||||
toolList.className = 'mcp-tool-list';
|
||||
const detailsContainer = mcpSection.querySelector('.process-details-container');
|
||||
if (detailsContainer) {
|
||||
mcpSection.insertBefore(toolList, detailsContainer);
|
||||
} else {
|
||||
toolbar.after(toolList);
|
||||
}
|
||||
}
|
||||
|
||||
if (legacyButtons && legacyButtons.parentNode === mcpSection) {
|
||||
legacyButtons.querySelectorAll('.mcp-detail-btn[data-exec-id]').forEach((btn) => toolList.appendChild(btn));
|
||||
legacyButtons.remove();
|
||||
}
|
||||
|
||||
const clientId = messageId || messageElement.id;
|
||||
if (clientId && !toolbar.querySelector('.process-detail-btn')) {
|
||||
const processDetailBtn = document.createElement('button');
|
||||
processDetailBtn.className = 'mcp-detail-btn process-detail-btn';
|
||||
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
processDetailBtn.onclick = () => toggleProcessDetails(null, clientId);
|
||||
toolbar.appendChild(processDetailBtn);
|
||||
}
|
||||
|
||||
return { mcpSection, toolbar, toolList };
|
||||
}
|
||||
|
||||
function syncMcpToolsToggleButton(messageElement) {
|
||||
if (!messageElement) return;
|
||||
const chrome = ensureMcpCallSectionChrome(messageElement, messageElement.id);
|
||||
if (!chrome) return;
|
||||
const { toolbar, toolList } = chrome;
|
||||
const count = getMcpExecutionCount(messageElement);
|
||||
let toolsToggle = toolbar.querySelector('.mcp-tools-toggle-btn');
|
||||
if (count <= 0) {
|
||||
if (toolsToggle) toolsToggle.remove();
|
||||
return;
|
||||
}
|
||||
if (!toolsToggle) {
|
||||
toolsToggle = document.createElement('button');
|
||||
toolsToggle.type = 'button';
|
||||
toolsToggle.className = 'mcp-detail-btn mcp-tools-toggle-btn';
|
||||
toolsToggle.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
toggleMcpToolList(messageElement.id);
|
||||
};
|
||||
toolbar.appendChild(toolsToggle);
|
||||
}
|
||||
const expanded = toolList.classList.contains('expanded');
|
||||
toolsToggle.innerHTML = '<span>' + formatMcpToolsToggleLabel(count, expanded) + '</span>';
|
||||
}
|
||||
|
||||
function toggleMcpToolList(assistantMessageId) {
|
||||
const messageEl = document.getElementById(assistantMessageId);
|
||||
if (!messageEl) return;
|
||||
const chrome = ensureMcpCallSectionChrome(messageEl, assistantMessageId);
|
||||
if (!chrome) return;
|
||||
const { toolList } = chrome;
|
||||
const willExpand = !toolList.classList.contains('expanded');
|
||||
if (willExpand) {
|
||||
ensureMcpCallButtons(messageEl);
|
||||
toolList.classList.add('expanded');
|
||||
} else {
|
||||
toolList.classList.remove('expanded');
|
||||
}
|
||||
syncMcpToolsToggleButton(messageEl);
|
||||
}
|
||||
|
||||
window.toggleMcpToolList = toggleMcpToolList;
|
||||
window.syncMcpToolsToggleButton = syncMcpToolsToggleButton;
|
||||
window.ensureMcpCallSectionChrome = ensureMcpCallSectionChrome;
|
||||
|
||||
/** 将 MCP 工具按钮挂到独立工具列表,并批量解析工具名 */
|
||||
function appendMcpCallButtons(messageElement, executionIds) {
|
||||
if (!messageElement || !Array.isArray(executionIds) || executionIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const chrome = ensureMcpCallSectionChrome(messageElement, messageElement.id);
|
||||
if (!chrome) return;
|
||||
const toolList = chrome.toolList;
|
||||
|
||||
executionIds.forEach((execId, index) => {
|
||||
if (toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(execId)) + '"]')) {
|
||||
return;
|
||||
}
|
||||
const detailBtn = document.createElement('button');
|
||||
detailBtn.className = 'mcp-detail-btn';
|
||||
detailBtn.dataset.execId = execId;
|
||||
detailBtn.dataset.execIndex = String(index + 1);
|
||||
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
|
||||
detailBtn.onclick = () => showMCPDetail(execId);
|
||||
toolList.appendChild(detailBtn);
|
||||
});
|
||||
batchUpdateButtonToolNames(toolList, executionIds);
|
||||
syncMcpToolsToggleButton(messageElement);
|
||||
}
|
||||
|
||||
/** 历史会话懒加载:用户展开工具列表时再渲染工具按钮 */
|
||||
function ensureMcpCallButtons(messageElement) {
|
||||
if (!messageElement || !messageElement.dataset || !messageElement.dataset.pendingMcpExecutionIds) {
|
||||
return;
|
||||
}
|
||||
let executionIds;
|
||||
try {
|
||||
executionIds = JSON.parse(messageElement.dataset.pendingMcpExecutionIds);
|
||||
} catch (e) {
|
||||
delete messageElement.dataset.pendingMcpExecutionIds;
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(executionIds) || executionIds.length === 0) {
|
||||
delete messageElement.dataset.pendingMcpExecutionIds;
|
||||
return;
|
||||
}
|
||||
appendMcpCallButtons(messageElement, executionIds);
|
||||
delete messageElement.dataset.pendingMcpExecutionIds;
|
||||
}
|
||||
|
||||
window.ensureMcpCallButtons = ensureMcpCallButtons;
|
||||
window.appendMcpCallButtons = appendMcpCallButtons;
|
||||
|
||||
// 批量获取工具名称并更新按钮(消除 N 次单独 API 请求,合并为 1 次)
|
||||
async function batchUpdateButtonToolNames(buttonsContainer, executionIds) {
|
||||
if (!executionIds || executionIds.length === 0) return;
|
||||
@@ -3182,40 +3426,63 @@ function getConversationGroup(dateObj, todayStart, sevenDaysCutoff, yesterdaySta
|
||||
}
|
||||
|
||||
// 加载对话
|
||||
/** 轻量加载会话后,拉取最后一条助手消息的 process_details(机器人等无 SSE 场景) */
|
||||
/** 轻量加载会话后,仅对「处理中…」占位回复拉取过程详情(机器人等非 SSE 场景);已完成会话不预取全量 */
|
||||
async function prefetchLastAssistantProcessDetails() {
|
||||
const nodes = document.querySelectorAll('#chat-messages .message.assistant');
|
||||
if (!nodes.length) return;
|
||||
const last = nodes[nodes.length - 1];
|
||||
if (!last || !last.id) return;
|
||||
const bubble = last.querySelector('.message-bubble');
|
||||
const visibleText = bubble ? String(bubble.textContent || '').trim() : '';
|
||||
const isPlaceholder = visibleText === '处理中...' || visibleText === 'Processing...';
|
||||
if (!isPlaceholder) return;
|
||||
const container = document.getElementById('process-details-' + last.id);
|
||||
if (!container || container.dataset.lazyNotLoaded !== '1') return;
|
||||
const backendId = last.dataset && last.dataset.backendMessageId;
|
||||
if (!backendId || typeof apiFetch !== 'function') return;
|
||||
if (typeof window.loadProcessDetailsPaginated === 'function') {
|
||||
await window.loadProcessDetailsPaginated(last.id, backendId);
|
||||
return;
|
||||
}
|
||||
const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !Array.isArray(j.processDetails) || j.processDetails.length === 0) return;
|
||||
if (typeof renderProcessDetails === 'function') {
|
||||
renderProcessDetails(last.id, j.processDetails);
|
||||
}
|
||||
if (typeof window.expandProcessDetailsTimeline === 'function') {
|
||||
window.expandProcessDetailsTimeline(last.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConversation(conversationId) {
|
||||
const seq = ++loadConversationRequestSeq;
|
||||
try {
|
||||
// 轻量加载:不带 processDetails,避免历史会话切换卡顿;展开详情时再按需拉取
|
||||
const response = await apiFetch(`/api/conversations/${conversationId}?include_process_details=0`);
|
||||
if (seq !== loadConversationRequestSeq) {
|
||||
return;
|
||||
}
|
||||
const conversation = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
showChatToast('加载对话失败: ' + (conversation.error || '未知错误'), 'error');
|
||||
return;
|
||||
const cachedConversation = getConversationLiteFromCache(conversationId);
|
||||
const fetchPromise = apiFetch(`/api/conversations/${conversationId}?include_process_details=0`)
|
||||
.then(async (response) => {
|
||||
const data = await response.json();
|
||||
return { response, data };
|
||||
});
|
||||
|
||||
let conversation;
|
||||
let response;
|
||||
if (cachedConversation) {
|
||||
conversation = cachedConversation;
|
||||
fetchPromise.then(({ response: freshResp, data }) => {
|
||||
if (freshResp.ok && data && seq === loadConversationRequestSeq && currentConversationId === conversationId) {
|
||||
putConversationLiteCache(conversationId, data);
|
||||
}
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
const fetched = await fetchPromise;
|
||||
response = fetched.response;
|
||||
conversation = fetched.data;
|
||||
if (seq !== loadConversationRequestSeq) {
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
showChatToast('加载对话失败: ' + (conversation.error || '未知错误'), 'error');
|
||||
return;
|
||||
}
|
||||
putConversationLiteCache(conversationId, conversation);
|
||||
}
|
||||
if (seq !== loadConversationRequestSeq) {
|
||||
return;
|
||||
@@ -3265,11 +3532,15 @@ async function loadConversation(conversationId) {
|
||||
if (typeof refreshChatProjectSelector === 'function') {
|
||||
refreshChatProjectSelector();
|
||||
}
|
||||
if (typeof window.syncHitlConfigFromServer === 'function') {
|
||||
await window.syncHitlConfigFromServer(conversationId);
|
||||
} else {
|
||||
refreshHitlConfigByCurrentConversation();
|
||||
}
|
||||
refreshHitlConfigByCurrentConversation();
|
||||
const hitlSyncPromise = (typeof window.syncHitlConfigFromServer === 'function')
|
||||
? window.syncHitlConfigFromServer(conversationId).then(() => {
|
||||
if (seq === loadConversationRequestSeq && currentConversationId === conversationId) {
|
||||
refreshHitlConfigByCurrentConversation();
|
||||
}
|
||||
}).catch(() => {})
|
||||
: Promise.resolve();
|
||||
void hitlSyncPromise;
|
||||
updateActiveConversation();
|
||||
|
||||
// 如果攻击链模态框打开且显示的不是当前对话,关闭它
|
||||
@@ -3336,7 +3607,9 @@ async function loadConversation(conversationId) {
|
||||
// - user: createdAt 即可(发送后不会再更新)
|
||||
// - assistant: 如果后端提供 updatedAt(任务完成时写回),优先用它,避免占位消息“任务开始时间”误导
|
||||
const msgTime = (msg && msg.role === 'assistant' && msg.updatedAt) ? msg.updatedAt : (msg ? msg.createdAt : null);
|
||||
const messageId = addMessage(msg.role, displayContent, msg.mcpExecutionIds || [], null, msgTime);
|
||||
const mcpIds = (msg.mcpExecutionIds && Array.isArray(msg.mcpExecutionIds)) ? msg.mcpExecutionIds : [];
|
||||
const addOpts = (msg.role === 'assistant' && mcpIds.length > 0) ? { deferMcpButtons: true } : null;
|
||||
const messageId = addMessage(msg.role, displayContent, mcpIds, null, msgTime, addOpts);
|
||||
const messageEl = document.getElementById(messageId);
|
||||
if (messageEl && msg && msg.id) {
|
||||
messageEl.dataset.backendMessageId = String(msg.id);
|
||||
@@ -3504,6 +3777,7 @@ async function deleteConversationTurnFromUI(anchorBackendMessageId) {
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.message || 'delete failed');
|
||||
}
|
||||
invalidateConversationLiteCache(currentConversationId);
|
||||
await loadConversation(currentConversationId);
|
||||
if (typeof loadConversationsWithGroups === 'function') {
|
||||
loadConversationsWithGroups();
|
||||
@@ -3550,6 +3824,7 @@ async function deleteConversation(conversationId, skipConfirm = false) {
|
||||
|
||||
// 更新缓存 - 立即删除,确保后续加载时能正确识别
|
||||
delete conversationGroupMappingCache[conversationId];
|
||||
invalidateConversationLiteCache(conversationId);
|
||||
// 同时从待保留映射中移除
|
||||
delete pendingGroupMappings[conversationId];
|
||||
|
||||
@@ -7693,6 +7968,20 @@ function refreshChatPanelI18n() {
|
||||
const expanded = timeline && timeline.classList.contains('expanded');
|
||||
span.textContent = expanded ? t('tasks.collapseDetail') : t('chat.expandDetail');
|
||||
});
|
||||
const copyLabel = t('common.copy');
|
||||
const copyTitle = t('chat.copyMessageTitle');
|
||||
messagesEl.querySelectorAll('.message-copy-btn').forEach(function (btn) {
|
||||
if (btn.dataset.copySuccessActive === '1') return;
|
||||
const span = btn.querySelector('span');
|
||||
if (span) span.textContent = copyLabel;
|
||||
btn.title = copyTitle;
|
||||
btn.setAttribute('aria-label', copyTitle);
|
||||
});
|
||||
messagesEl.querySelectorAll('.message.assistant').forEach(function (msgEl) {
|
||||
if (typeof window.syncMcpToolsToggleButton === 'function') {
|
||||
window.syncMcpToolsToggleButton(msgEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isAppModalOpen('mcp-detail-modal')) {
|
||||
|
||||
+89
-82
@@ -1259,101 +1259,60 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找或创建 MCP 区域
|
||||
let mcpSection = assistantElement.querySelector('.mcp-call-section');
|
||||
if (!mcpSection) {
|
||||
mcpSection = document.createElement('div');
|
||||
mcpSection.className = 'mcp-call-section';
|
||||
const mcpLabel = document.createElement('div');
|
||||
mcpLabel.className = 'mcp-call-label';
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
mcpSection.appendChild(mcpLabel);
|
||||
const buttonsContainerInit = document.createElement('div');
|
||||
buttonsContainerInit.className = 'mcp-call-buttons';
|
||||
mcpSection.appendChild(buttonsContainerInit);
|
||||
contentWrapper.appendChild(mcpSection);
|
||||
// 查找或创建 MCP 区域(工具栏 + 工具列表 + 迭代时间线)
|
||||
if (typeof window.ensureMcpCallSectionChrome === 'function') {
|
||||
window.ensureMcpCallSectionChrome(assistantElement, assistantMessageId);
|
||||
}
|
||||
|
||||
// 获取时间线内容
|
||||
const hasContent = timelineHTML.trim().length > 0;
|
||||
|
||||
// 检查时间线中是否有错误项
|
||||
const hasError = timeline && timeline.querySelector('.timeline-item-error');
|
||||
|
||||
// 确保按钮容器存在
|
||||
let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons');
|
||||
if (!buttonsContainer) {
|
||||
buttonsContainer = document.createElement('div');
|
||||
buttonsContainer.className = 'mcp-call-buttons';
|
||||
mcpSection.appendChild(buttonsContainer);
|
||||
const mcpSection = assistantElement.querySelector('.mcp-call-section');
|
||||
if (!mcpSection) {
|
||||
removeMessage(progressId);
|
||||
return;
|
||||
}
|
||||
|
||||
let maxExecIndex = 0;
|
||||
const existingExecBtns = buttonsContainer.querySelectorAll('.mcp-detail-btn:not(.process-detail-btn)');
|
||||
existingExecBtns.forEach(function (btn) {
|
||||
const n = parseInt(btn.dataset.execIndex, 10);
|
||||
if (!isNaN(n) && n > maxExecIndex) maxExecIndex = n;
|
||||
});
|
||||
const seenExec = new Set();
|
||||
existingExecBtns.forEach(function (btn) {
|
||||
if (btn.dataset.execId) seenExec.add(String(btn.dataset.execId).trim());
|
||||
});
|
||||
let appendedAny = false;
|
||||
if (mcpIds.length > 0) {
|
||||
mcpIds.forEach(function (execId) {
|
||||
const id = execId != null ? String(execId).trim() : '';
|
||||
if (!id || seenExec.has(id)) return;
|
||||
seenExec.add(id);
|
||||
maxExecIndex += 1;
|
||||
appendedAny = true;
|
||||
const detailBtn = document.createElement('button');
|
||||
detailBtn.className = 'mcp-detail-btn';
|
||||
detailBtn.dataset.execId = id;
|
||||
detailBtn.dataset.execIndex = String(maxExecIndex);
|
||||
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: maxExecIndex }) : '调用 #' + maxExecIndex) + '</span>';
|
||||
detailBtn.onclick = function () { showMCPDetail(id); };
|
||||
buttonsContainer.appendChild(detailBtn);
|
||||
});
|
||||
if (appendedAny && typeof batchUpdateButtonToolNames === 'function') {
|
||||
batchUpdateButtonToolNames(buttonsContainer, mcpIds);
|
||||
}
|
||||
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');
|
||||
if (toolList) toolList.classList.remove('expanded');
|
||||
}
|
||||
if (!buttonsContainer.querySelector('.process-detail-btn')) {
|
||||
if (typeof window.syncMcpToolsToggleButton === 'function') {
|
||||
window.syncMcpToolsToggleButton(assistantElement);
|
||||
}
|
||||
|
||||
const toolbar = mcpSection.querySelector('.mcp-call-toolbar');
|
||||
if (toolbar && !toolbar.querySelector('.process-detail-btn')) {
|
||||
const progressDetailBtn = document.createElement('button');
|
||||
progressDetailBtn.className = 'mcp-detail-btn process-detail-btn';
|
||||
progressDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
progressDetailBtn.onclick = () => toggleProcessDetails(null, assistantMessageId);
|
||||
buttonsContainer.appendChild(progressDetailBtn);
|
||||
toolbar.appendChild(progressDetailBtn);
|
||||
}
|
||||
|
||||
// 创建详情容器,放在MCP按钮区域下方(统一结构)
|
||||
|
||||
const detailsId = 'process-details-' + assistantMessageId;
|
||||
let detailsContainer = document.getElementById(detailsId);
|
||||
const toolListEl = mcpSection.querySelector('.mcp-tool-list');
|
||||
|
||||
if (!detailsContainer) {
|
||||
detailsContainer = document.createElement('div');
|
||||
detailsContainer.id = detailsId;
|
||||
detailsContainer.className = 'process-details-container';
|
||||
// 确保容器在按钮容器之后
|
||||
if (buttonsContainer.nextSibling) {
|
||||
mcpSection.insertBefore(detailsContainer, buttonsContainer.nextSibling);
|
||||
if (toolListEl) {
|
||||
toolListEl.after(detailsContainer);
|
||||
} else {
|
||||
mcpSection.appendChild(detailsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置详情内容(如果有错误,默认折叠;否则默认折叠)
|
||||
detailsContainer.innerHTML = `
|
||||
<div class="process-details-content">
|
||||
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 确保初始状态是折叠的(默认折叠,特别是错误时)
|
||||
if (hasContent) {
|
||||
const timeline = document.getElementById(detailsId + '-timeline');
|
||||
if (timeline) {
|
||||
// 如果有错误,确保折叠;否则也默认折叠
|
||||
timeline.classList.remove('expanded');
|
||||
}
|
||||
|
||||
@@ -1363,10 +1322,47 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut
|
||||
});
|
||||
}
|
||||
|
||||
// 移除原来的进度消息(详情已快照到助手消息下的 process-details)
|
||||
removeMessage(progressId);
|
||||
}
|
||||
|
||||
const PROCESS_DETAILS_PAGE_SIZE = 100;
|
||||
|
||||
/**
|
||||
* 分页加载过程详情并增量渲染,避免数百轮迭代一次性阻塞主线程。
|
||||
*/
|
||||
async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId) {
|
||||
if (!assistantMessageId || !backendMessageId || typeof apiFetch !== 'function' || typeof renderProcessDetails !== 'function') {
|
||||
return;
|
||||
}
|
||||
const PAGE = PROCESS_DETAILS_PAGE_SIZE;
|
||||
let offset = 0;
|
||||
let isFirst = true;
|
||||
while (true) {
|
||||
const res = await apiFetch(
|
||||
'/api/messages/' + encodeURIComponent(String(backendMessageId)) +
|
||||
'/process-details?limit=' + PAGE + '&offset=' + offset
|
||||
);
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error((j && j.error) ? j.error : String(res.status));
|
||||
}
|
||||
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
|
||||
const hasMore = !!(j && j.hasMore);
|
||||
renderProcessDetails(assistantMessageId, details, {
|
||||
append: !isFirst,
|
||||
markLoaded: !hasMore
|
||||
});
|
||||
if (!hasMore || details.length === 0) {
|
||||
break;
|
||||
}
|
||||
offset += details.length;
|
||||
isFirst = false;
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
window.loadProcessDetailsPaginated = loadProcessDetailsPaginated;
|
||||
|
||||
// 切换过程详情显示
|
||||
function toggleProcessDetails(progressId, assistantMessageId) {
|
||||
const detailsId = 'process-details-' + assistantMessageId;
|
||||
@@ -1383,26 +1379,17 @@ function toggleProcessDetails(progressId, assistantMessageId) {
|
||||
// 正在加载中,避免重复请求
|
||||
} else {
|
||||
detailsContainer.dataset.loading = '1';
|
||||
// 先展开容器,显示加载态
|
||||
const timeline = detailsContainer.querySelector('.progress-timeline');
|
||||
if (timeline) {
|
||||
timeline.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('common.loading') : '加载中…') + '</div>';
|
||||
}
|
||||
apiFetch(`/api/messages/${encodeURIComponent(String(backendMessageId))}/process-details`)
|
||||
.then(async (res) => {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error((j && j.error) ? j.error : res.status);
|
||||
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
|
||||
// 重新渲染详情(renderProcessDetails 会清掉 lazy 标记并写入 loaded)
|
||||
renderProcessDetails(assistantMessageId, details);
|
||||
})
|
||||
loadProcessDetailsPaginated(assistantMessageId, backendMessageId)
|
||||
.catch((e) => {
|
||||
console.error('加载过程详情失败:', e);
|
||||
const tl = detailsContainer.querySelector('.progress-timeline');
|
||||
if (tl) {
|
||||
tl.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('chat.noProcessDetail') : '暂无过程详情(加载失败)') + '</div>';
|
||||
}
|
||||
// 失败时保留 lazy 状态,允许用户重试
|
||||
detailsContainer.dataset.lazyNotLoaded = '1';
|
||||
detailsContainer.dataset.loaded = '0';
|
||||
})
|
||||
@@ -2756,12 +2743,16 @@ async function restoreHitlInlineForConversation(conversationId) {
|
||||
if (detailsContainer.dataset.lazyNotLoaded === '1' && detailsContainer.dataset.loaded !== '1') {
|
||||
try {
|
||||
detailsContainer.dataset.loading = '1';
|
||||
const res = await apiFetch('/api/messages/' + encodeURIComponent(backendMsgId) + '/process-details');
|
||||
const j = await res.json().catch(function () { return {}; });
|
||||
if (!res.ok) throw new Error((j && j.error) ? j.error : String(res.status));
|
||||
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
|
||||
if (typeof renderProcessDetails === 'function') {
|
||||
renderProcessDetails(clientMsgId, details);
|
||||
if (typeof loadProcessDetailsPaginated === 'function') {
|
||||
await loadProcessDetailsPaginated(clientMsgId, backendMsgId);
|
||||
} else {
|
||||
const res = await apiFetch('/api/messages/' + encodeURIComponent(backendMsgId) + '/process-details');
|
||||
const j = await res.json().catch(function () { return {}; });
|
||||
if (!res.ok) throw new Error((j && j.error) ? j.error : String(res.status));
|
||||
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
|
||||
if (typeof renderProcessDetails === 'function') {
|
||||
renderProcessDetails(clientMsgId, details);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载过程详情失败(HITL 恢复):', e);
|
||||
@@ -5468,6 +5459,22 @@ function refreshProgressAndTimelineI18n() {
|
||||
const expanded = timeline && timeline.classList.contains('expanded');
|
||||
span.textContent = expanded ? _t('tasks.collapseDetail') : _t('chat.expandDetail');
|
||||
});
|
||||
|
||||
document.querySelectorAll('#chat-messages .message.assistant').forEach(function (msgEl) {
|
||||
if (typeof window.syncMcpToolsToggleButton === 'function') {
|
||||
window.syncMcpToolsToggleButton(msgEl);
|
||||
}
|
||||
});
|
||||
|
||||
const copyLabel = _t('common.copy');
|
||||
const copyTitle = _t('chat.copyMessageTitle');
|
||||
document.querySelectorAll('#chat-messages .message-copy-btn').forEach(function (btn) {
|
||||
if (btn.dataset.copySuccessActive === '1') return;
|
||||
const span = btn.querySelector('span');
|
||||
if (span) span.textContent = copyLabel;
|
||||
btn.title = copyTitle;
|
||||
btn.setAttribute('aria-label', copyTitle);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
|
||||
Reference in New Issue
Block a user