Add files via upload

This commit is contained in:
公明
2026-07-12 13:07:20 +08:00
committed by GitHub
parent 987bd0a03c
commit 46cea9459a
7 changed files with 218 additions and 69 deletions
+24 -26
View File
@@ -1,7 +1,13 @@
let currentConversationId = null;
let loadConversationRequestSeq = 0;
/** 轻量会话 LRU 缓存:来回切换已加载会话时避免重复网络 + 全量 DOM 重建 */
/**
* 轻量会话 LRU 缓存
*
* 缓存只用作请求失败时的降级数据不能先于服务端响应直接渲染
* 运行中会话的 process details 会持续写入直接渲染旧快照会让
* UI 暂时回退到旧轮次 task-events 接管后又突然跳到最新轮次
*/
const CONVERSATION_LITE_CACHE_MAX = 12;
const conversationLiteCache = new Map();
@@ -4131,32 +4137,24 @@ async function loadConversation(conversationId) {
const seq = ++loadConversationRequestSeq;
try {
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) {
let conversation = null;
let response = null;
try {
response = await apiFetch(`/api/conversations/${conversationId}?include_process_details=0`);
conversation = await response.json();
} catch (fetchError) {
if (!cachedConversation) throw fetchError;
console.warn('加载最新对话失败,使用本地缓存:', fetchError);
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;
}
}
if (seq !== loadConversationRequestSeq) {
return;
}
if (response && !response.ok) {
showChatToast('加载对话失败: ' + (conversation.error || '未知错误'), 'error');
return;
}
if (response && response.ok) {
putConversationLiteCache(conversationId, conversation);
}
if (seq !== loadConversationRequestSeq) {
+72 -13
View File
@@ -319,7 +319,10 @@ function finalizeOutstandingToolCallsForProgress(progressId, finalStatus) {
if (!mapping) continue;
if (mapping.progressId != null && String(mapping.progressId) !== pid) continue;
const tcid = mapping.toolCallId || (String(mapKey).includes('::') ? String(mapKey).split('::').slice(1).join('::') : String(mapKey));
updateToolCallStatus(mapping.progressId || progressId, tcid, finalStatus);
// 已收到终态结果的调用仅清理索引,不能在任务收尾时被改写成失败。
if (!mapping.terminalStatus) {
updateToolCallStatus(mapping.progressId || progressId, tcid, finalStatus);
}
toolCallStatusMap.delete(mapKey);
}
}
@@ -1866,15 +1869,30 @@ function handleStreamEvent(event, progressElement, progressId,
? String(prevMainIter.workflowNodeId).trim()
: '';
const curNode = d.workflowNodeId != null ? String(d.workflowNodeId).trim() : '';
const prevAgent = prevMainIter && prevMainIter.einoAgent != null
? String(prevMainIter.einoAgent).trim()
: '';
const curAgent = d.einoAgent != null ? String(d.einoAgent).trim() : '';
const prevOrchestration = prevMainIter && prevMainIter.orchestration != null
? String(prevMainIter.orchestration).trim()
: '';
const curOrchestration = d.orchestration != null ? String(d.orchestration).trim() : '';
const duplicateMainIteration = prevN != null && String(prevN) === String(n) &&
prevNode === curNode && prevAgent === curAgent &&
prevOrchestration === curOrchestration;
mainIterationStateByProgressId.set(String(progressId), {
iteration: n,
orchestration: d.orchestration != null ? d.orchestration : '',
workflowNodeId: curNode
workflowNodeId: curNode,
einoAgent: curAgent
});
// 主通道进入新轮次或图编排切换到新 Agent 节点后,不复用上一段的流式时间线条目
if (prevN != null && (n < prevN || prevN !== n || (curNode && prevNode && curNode !== prevNode))) {
clearTimelineStreamStates(progressId);
}
// 同一主代理可能从“进入模型”和“检测到工具批次”两条路径补发同一轮次。
// 状态缓存仍更新,但时间线只保留一个幂等条目。
if (duplicateMainIteration) break;
}
let iterTitle;
if (d.orchestration === 'plan_execute' && d.einoScope === 'main') {
@@ -2306,7 +2324,9 @@ function handleStreamEvent(event, progressElement, progressId,
const existingItem = document.getElementById(existing.itemId);
if (existingItem) {
// 同一 toolCallId 的重复 tool_call(重试/补发)只更新状态,不重复追加条目。
updateToolCallStatus(progressId, toolCallId, 'running');
if (!existing.terminalStatus) {
updateToolCallStatus(progressId, toolCallId, 'running');
}
break;
}
}
@@ -2359,7 +2379,7 @@ function handleStreamEvent(event, progressElement, progressId,
const mapKey = toolCallMapKey(progressId, resultToolCallId);
if (toolCallStatusMap.has(mapKey)) {
updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed');
toolCallStatusMap.delete(mapKey);
toolCallStatusMap.get(mapKey).terminalStatus = success ? 'completed' : 'failed';
}
break;
}
@@ -2367,7 +2387,7 @@ function handleStreamEvent(event, progressElement, progressId,
const mapKey = toolCallMapKey(progressId, resultToolCallId);
if (toolCallStatusMap.has(mapKey)) {
updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed');
toolCallStatusMap.delete(mapKey);
toolCallStatusMap.get(mapKey).terminalStatus = success ? 'completed' : 'failed';
}
break;
}
@@ -2375,7 +2395,7 @@ function handleStreamEvent(event, progressElement, progressId,
if (resultToolCallId && toolCallStatusMap.has(toolCallMapKey(progressId, resultToolCallId))) {
updateToolCallStatus(progressId, resultToolCallId, success ? 'completed' : 'failed');
toolCallStatusMap.delete(toolCallMapKey(progressId, resultToolCallId));
toolCallStatusMap.get(toolCallMapKey(progressId, resultToolCallId)).terminalStatus = success ? 'completed' : 'failed';
}
addTimelineItem(timeline, 'tool_result', {
title: timelineAgentBracketPrefix(resultInfo) + statusIcon + ' ' + resultExecText,
@@ -3948,6 +3968,25 @@ function isLiveProgressTimeline(timeline) {
return !!(timeline && timeline.id && /^progress-\d+-\d+-timeline$/.test(timeline.id));
}
function formatLiveTimelinePrunedMarker(marker) {
const count = parseInt(marker.dataset.prunedCount || '0', 10) || 0;
const minIteration = parseInt(marker.dataset.prunedMainIterationMin || '0', 10) || 0;
const maxIteration = parseInt(marker.dataset.prunedMainIterationMax || '0', 10) || 0;
const translate = typeof window.t === 'function' ? window.t : null;
const baseText = translate
? translate('chat.liveTimelinePruned', { count: count })
: ('已收起前 ' + count + ' 条实时过程详情,任务完成后可按页查看完整记录');
if (minIteration <= 0 || maxIteration < minIteration) return baseText;
if (minIteration === maxIteration) {
return baseText + ' · ' + (translate
? translate('chat.liveTimelinePrunedSingleRound', { n: minIteration })
: ('主代理第 ' + minIteration + ' 轮'));
}
return baseText + ' · ' + (translate
? translate('chat.liveTimelinePrunedRoundRange', { from: minIteration, to: maxIteration })
: ('主代理第 ' + minIteration + '' + maxIteration + ' 轮'));
}
function pruneLiveTimelineIfNeeded(timeline) {
if (!isLiveProgressTimeline(timeline)) return;
const items = Array.from(timeline.children).filter(function (el) {
@@ -3970,16 +4009,37 @@ function pruneLiveTimelineIfNeeded(timeline) {
if (!marker) {
marker = document.createElement('div');
marker.className = 'timeline-live-pruned-marker';
marker.setAttribute('role', 'status');
marker.setAttribute('aria-live', 'polite');
timeline.insertBefore(marker, timeline.firstChild);
}
let prunedMainIterationMin = parseInt(marker.dataset.prunedMainIterationMin || '0', 10) || 0;
let prunedMainIterationMax = parseInt(marker.dataset.prunedMainIterationMax || '0', 10) || 0;
for (let i = 0; i < removeCount; i++) {
removable[i].remove();
const removed = removable[i];
if (removed && removed.dataset && removed.dataset.timelineType === 'iteration' &&
removed.dataset.einoScope !== 'sub') {
const iteration = parseInt(removed.dataset.iterationN || '0', 10) || 0;
if (iteration > 0) {
if (prunedMainIterationMin === 0 || iteration < prunedMainIterationMin) {
prunedMainIterationMin = iteration;
}
if (iteration > prunedMainIterationMax) {
prunedMainIterationMax = iteration;
}
}
}
removed.remove();
}
pruned += removeCount;
marker.dataset.prunedCount = String(pruned);
marker.textContent = typeof window.t === 'function'
? window.t('chat.liveTimelinePruned', { count: pruned })
: ('已收起前 ' + pruned + ' 条实时过程详情,任务完成后可按页查看完整记录');
marker.dataset.prunedMainIterationMin = String(prunedMainIterationMin);
marker.dataset.prunedMainIterationMax = String(prunedMainIterationMax);
marker.textContent = formatLiveTimelinePrunedMarker(marker);
// 始终放在已保留详情之前;配合 sticky 样式,查看最新内容时也能感知历史已裁剪。
if (timeline.firstChild !== marker) {
timeline.insertBefore(marker, timeline.firstChild);
}
}
function addTimelineItem(timeline, type, options) {
@@ -4816,7 +4876,7 @@ function updateMonitorTimelineSection() {
}
const MCP_STATS_TOP_N = 6;
const MCP_STATS_TOP_N = 3;
const MCP_TIMELINE_RANGES = ['24h', '7d', '30d'];
function getMcpMonitorTimelineRange() {
@@ -6565,8 +6625,7 @@ function refreshProgressAndTimelineI18n() {
});
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 });
marker.textContent = formatLiveTimelinePrunedMarker(marker);
});
// 详情区「展开/收起」按钮
+17 -1
View File
@@ -1178,7 +1178,7 @@ function renderGraphEdgesListHtml(factKey, graphData, selectedEdgeId) {
const synthetic = isSyntheticGraphEdge(e);
const deleteBtn = synthetic
? `<span class="project-fact-graph-edge-synthetic" title="${escapeHtml(tp('projects.graphEdgeSynthetic'))}">—</span>`
: `<button type="button" class="project-fact-graph-edge-delete" data-edge-id="${escapeHtml(e.id)}" onclick="event.stopPropagation(); deleteProjectFactEdge(this.dataset.edgeId)" title="${escapeHtml(tp('projects.graphDeleteEdge'))}">×</button>`;
: `<button type="button" class="project-fact-graph-edge-delete" data-edge-id="${escapeHtml(e.id)}" onclick="event.stopPropagation(); deleteProjectFactEdge(this.dataset.edgeId)" title="${escapeHtml(tp('projects.graphDeleteEdge'))}"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>`;
return `<div class="project-fact-graph-edge-item${selected}" data-edge-id="${escapeHtml(e.id)}" onclick="focusProjectFactGraphEdge(${JSON.stringify(e.id)})">
<span class="project-fact-graph-edge-dir">${escapeHtml(dirLabel)}</span>
<span class="project-fact-graph-edge-type">${escapeHtml(e.type || '')}</span>
@@ -2675,15 +2675,31 @@ function initChatProjectSelector() {
});
}
function initProjectGraphFooterWheelScroll() {
const footer = document.querySelector('.project-fact-graph-footer');
if (!footer || footer.dataset.wheelScrollBound === 'true') return;
footer.dataset.wheelScrollBound = 'true';
footer.addEventListener('wheel', (event) => {
if (footer.scrollWidth <= footer.clientWidth || Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
const maxScrollLeft = footer.scrollWidth - footer.clientWidth;
const nextScrollLeft = Math.max(0, Math.min(maxScrollLeft, footer.scrollLeft + event.deltaY));
if (nextScrollLeft === footer.scrollLeft) return;
footer.scrollLeft = nextScrollLeft;
event.preventDefault();
}, { passive: false });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initChatProjectSelector();
initProjectListActionMenu();
initProjectGraphFooterWheelScroll();
refreshProjectsFilterSelects();
});
} else {
initChatProjectSelector();
initProjectListActionMenu();
initProjectGraphFooterWheelScroll();
refreshProjectsFilterSelects();
}