mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-27 17:52:28 +02:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 主对话区智能粘底滚动:流式输出时自动跟随,用户上滑阅读时不抢焦点。
|
||||
* 主 POST 流(sendMessage)与刷新后 task-events 补流共用同一策略。
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/** 距底部在此范围内才继续自动跟随(宜小,避免“差一点也被拽回去”) */
|
||||
const CHAT_SCROLL_FOLLOW_THRESHOLD_PX = 48;
|
||||
/** FAB 隐藏:用户已手动滚近底部 */
|
||||
const CHAT_SCROLL_FAB_HIDE_THRESHOLD_PX = 120;
|
||||
/** 用户上滑后的短暂锁,防止 SSE 与 scroll 事件竞态抢滚动 */
|
||||
const DETACH_LOCK_MS = 280;
|
||||
|
||||
/** @type {'following' | 'detached'} */
|
||||
let scrollMode = 'following';
|
||||
let scrollFollowRaf = 0;
|
||||
/** 用户脱离跟随后,下方是否有未读的新输出(不按 SSE 次数计) */
|
||||
let hasPendingNewBelow = false;
|
||||
let listenersBound = false;
|
||||
let lastScrollTop = 0;
|
||||
let programmaticScroll = false;
|
||||
let detachLockUntil = 0;
|
||||
|
||||
function getChatMessagesEl() {
|
||||
return document.getElementById('chat-messages');
|
||||
}
|
||||
|
||||
/** 主 POST 流 + 刷新后 task-events 补流均视为「流式进行中」 */
|
||||
function isStreamActive() {
|
||||
try {
|
||||
const live = window.__csAgentLiveStream;
|
||||
if (live && live.active) return true;
|
||||
const replay = window.__csTaskEventStream;
|
||||
return !!(replay && replay.active);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function distanceFromBottom(el) {
|
||||
if (!el) return 0;
|
||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||
return scrollHeight - clientHeight - scrollTop;
|
||||
}
|
||||
|
||||
function isNearBottom(thresholdPx) {
|
||||
const el = getChatMessagesEl();
|
||||
if (!el) return true;
|
||||
return distanceFromBottom(el) <= thresholdPx;
|
||||
}
|
||||
|
||||
function isChatMessagesPinnedToBottom() {
|
||||
return isNearBottom(CHAT_SCROLL_FAB_HIDE_THRESHOLD_PX);
|
||||
}
|
||||
|
||||
/** 已在底部时恢复 following(解决:手动滚到底但 scrollMode 仍为 detached) */
|
||||
function resumeFollowingIfAtBottom() {
|
||||
if (Date.now() < detachLockUntil) return false;
|
||||
if (!isNearBottom(CHAT_SCROLL_FOLLOW_THRESHOLD_PX)) return false;
|
||||
if (scrollMode === 'detached') setScrollFollowing();
|
||||
return true;
|
||||
}
|
||||
|
||||
function captureScrollPinState() {
|
||||
if (Date.now() < detachLockUntil) return false;
|
||||
if (resumeFollowingIfAtBottom()) return true;
|
||||
return scrollMode === 'following';
|
||||
}
|
||||
|
||||
function setScrollFollowing() {
|
||||
scrollMode = 'following';
|
||||
detachLockUntil = 0;
|
||||
hasPendingNewBelow = false;
|
||||
updateScrollToBottomFab();
|
||||
}
|
||||
|
||||
function markPendingNewBelow() {
|
||||
if (scrollMode !== 'detached') return;
|
||||
hasPendingNewBelow = true;
|
||||
updateScrollToBottomFab();
|
||||
}
|
||||
|
||||
function setScrollDetached() {
|
||||
scrollMode = 'detached';
|
||||
detachLockUntil = Date.now() + DETACH_LOCK_MS;
|
||||
cancelAnimationFrame(scrollFollowRaf);
|
||||
if (isStreamActive()) {
|
||||
hasPendingNewBelow = true;
|
||||
}
|
||||
updateScrollToBottomFab();
|
||||
}
|
||||
|
||||
function scrollChatToBottomInstant() {
|
||||
if (scrollMode !== 'following') return;
|
||||
const el = getChatMessagesEl();
|
||||
if (!el) return;
|
||||
programmaticScroll = true;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
lastScrollTop = el.scrollTop;
|
||||
requestAnimationFrame(function () {
|
||||
programmaticScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
function scrollChatToBottomSmooth() {
|
||||
const el = getChatMessagesEl();
|
||||
if (!el) return;
|
||||
programmaticScroll = true;
|
||||
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
||||
requestAnimationFrame(function () {
|
||||
programmaticScroll = false;
|
||||
const node = getChatMessagesEl();
|
||||
if (node) lastScrollTop = node.scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
function updateScrollToBottomFab() {
|
||||
const fab = document.getElementById('chat-scroll-to-bottom');
|
||||
if (!fab) return;
|
||||
|
||||
const show = scrollMode === 'detached' && !isNearBottom(CHAT_SCROLL_FAB_HIDE_THRESHOLD_PX);
|
||||
fab.classList.toggle('visible', show);
|
||||
|
||||
let label;
|
||||
if (hasPendingNewBelow) {
|
||||
label = typeof window.t === 'function'
|
||||
? window.t('chat.scrollToBottomHasNew')
|
||||
: '↓ 有新内容';
|
||||
} else {
|
||||
label = typeof window.t === 'function'
|
||||
? window.t('chat.scrollToBottom')
|
||||
: '回到底部';
|
||||
}
|
||||
fab.setAttribute('aria-label', label);
|
||||
fab.textContent = label;
|
||||
}
|
||||
|
||||
function canAutoScrollNow(wasPinnedBeforeDomUpdate) {
|
||||
if (Date.now() < detachLockUntil) return false;
|
||||
if (resumeFollowingIfAtBottom()) return true;
|
||||
if (scrollMode === 'detached') return false;
|
||||
if (wasPinnedBeforeDomUpdate === true) return true;
|
||||
return isNearBottom(CHAT_SCROLL_FOLLOW_THRESHOLD_PX);
|
||||
}
|
||||
|
||||
function scheduleChatScrollToBottomIfFollowing(wasPinnedBeforeDomUpdate) {
|
||||
if (!canAutoScrollNow(wasPinnedBeforeDomUpdate)) {
|
||||
markPendingNewBelow();
|
||||
return;
|
||||
}
|
||||
cancelAnimationFrame(scrollFollowRaf);
|
||||
scrollFollowRaf = requestAnimationFrame(scrollChatToBottomInstant);
|
||||
}
|
||||
|
||||
/** @param {boolean} wasPinned DOM 更新前是否应跟随(由 captureScrollPinState 传入) */
|
||||
function scrollChatMessagesToBottomIfPinned(wasPinned) {
|
||||
scheduleChatScrollToBottomIfFollowing(wasPinned);
|
||||
}
|
||||
|
||||
function forceScrollChatToBottom(smooth) {
|
||||
setScrollFollowing();
|
||||
cancelAnimationFrame(scrollFollowRaf);
|
||||
if (smooth) {
|
||||
scrollChatToBottomSmooth();
|
||||
} else {
|
||||
scrollChatToBottomInstant();
|
||||
}
|
||||
}
|
||||
|
||||
function onUserSendMessage() {
|
||||
setScrollFollowing();
|
||||
scrollChatToBottomInstant();
|
||||
}
|
||||
|
||||
function clearAllStreamingMarkers() {
|
||||
document.querySelectorAll('.progress-container.is-streaming, .process-details-container.is-streaming').forEach(function (el) {
|
||||
el.classList.remove('is-streaming');
|
||||
});
|
||||
}
|
||||
|
||||
function markProgressStreaming(active, progressId) {
|
||||
if (!active) {
|
||||
clearAllStreamingMarkers();
|
||||
return;
|
||||
}
|
||||
if (!progressId) return;
|
||||
const progressEl = document.getElementById(progressId);
|
||||
const container = progressEl && progressEl.querySelector('.progress-container');
|
||||
if (container) container.classList.add('is-streaming');
|
||||
}
|
||||
|
||||
function markProcessDetailsStreaming(active, assistantDomId) {
|
||||
if (!active) {
|
||||
document.querySelectorAll('.process-details-container.is-streaming').forEach(function (el) {
|
||||
el.classList.remove('is-streaming');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!assistantDomId) return;
|
||||
const container = document.getElementById('process-details-' + assistantDomId);
|
||||
if (!container) return;
|
||||
container.classList.add('is-streaming');
|
||||
const timeline = container.querySelector('.progress-timeline');
|
||||
if (timeline) timeline.classList.add('expanded');
|
||||
}
|
||||
|
||||
function onStreamEnd() {
|
||||
clearAllStreamingMarkers();
|
||||
try {
|
||||
window.__csTaskEventStream = { active: false, conversationId: null, assistantDomId: null, progressId: null };
|
||||
} catch (e) { /* ignore */ }
|
||||
updateScrollToBottomFab();
|
||||
}
|
||||
|
||||
/** 刷新后会话 task-events 补流开始时,与 sendMessage 主流程对齐 */
|
||||
function onTaskEventStreamBegin(conversationId, assistantDomId, progressId) {
|
||||
try {
|
||||
window.__csTaskEventStream = {
|
||||
active: true,
|
||||
conversationId: conversationId || null,
|
||||
assistantDomId: assistantDomId || null,
|
||||
progressId: progressId || null
|
||||
};
|
||||
} catch (e) { /* ignore */ }
|
||||
markProcessDetailsStreaming(true, assistantDomId);
|
||||
resumeFollowingIfAtBottom();
|
||||
updateScrollToBottomFab();
|
||||
}
|
||||
|
||||
function onTaskEventStreamEnd() {
|
||||
onStreamEnd();
|
||||
}
|
||||
|
||||
function applyMessageScrollOption(options) {
|
||||
const opt = (options && options.scroll) || 'follow';
|
||||
if (opt === 'none') return;
|
||||
if (opt === 'force') {
|
||||
forceScrollChatToBottom(false);
|
||||
return;
|
||||
}
|
||||
scheduleChatScrollToBottomIfFollowing(captureScrollPinState());
|
||||
}
|
||||
|
||||
/** 流式/用户未跟随时禁止 scrollIntoView 抢滚动 */
|
||||
function scrollElementIntoViewIfFollowing(el, options) {
|
||||
if (!el || !captureScrollPinState()) return;
|
||||
el.scrollIntoView(options || { behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function onChatMessagesScroll() {
|
||||
const el = getChatMessagesEl();
|
||||
if (!el) return;
|
||||
|
||||
if (programmaticScroll) {
|
||||
lastScrollTop = el.scrollTop;
|
||||
return;
|
||||
}
|
||||
|
||||
const st = el.scrollTop;
|
||||
const scrolledUp = st < lastScrollTop - 1;
|
||||
|
||||
if (scrolledUp) {
|
||||
setScrollDetached();
|
||||
} else if (resumeFollowingIfAtBottom()) {
|
||||
/* 拖滚动条/点击轨道跳到底部时也恢复跟随 */
|
||||
}
|
||||
|
||||
lastScrollTop = st;
|
||||
updateScrollToBottomFab();
|
||||
}
|
||||
|
||||
function bindChatScrollListeners() {
|
||||
if (listenersBound) return;
|
||||
const el = getChatMessagesEl();
|
||||
if (!el) return;
|
||||
listenersBound = true;
|
||||
lastScrollTop = el.scrollTop;
|
||||
|
||||
el.addEventListener('wheel', function (e) {
|
||||
if (e.deltaY < -1) setScrollDetached();
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener('touchmove', function (e) {
|
||||
if (e.touches && e.touches.length === 1) {
|
||||
el._csTouchLastY = el._csTouchLastY != null ? el._csTouchLastY : e.touches[0].clientY;
|
||||
if (e.touches[0].clientY > el._csTouchLastY + 4) {
|
||||
setScrollDetached();
|
||||
}
|
||||
el._csTouchLastY = e.touches[0].clientY;
|
||||
}
|
||||
}, { passive: true });
|
||||
el.addEventListener('touchstart', function (e) {
|
||||
if (e.touches && e.touches.length) {
|
||||
el._csTouchLastY = e.touches[0].clientY;
|
||||
}
|
||||
}, { passive: true });
|
||||
el.addEventListener('touchend', function () {
|
||||
el._csTouchLastY = null;
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener('scroll', onChatMessagesScroll, { passive: true });
|
||||
|
||||
const fab = document.getElementById('chat-scroll-to-bottom');
|
||||
if (fab) {
|
||||
fab.addEventListener('click', function () {
|
||||
forceScrollChatToBottom(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initChatScroll() {
|
||||
bindChatScrollListeners();
|
||||
const el = getChatMessagesEl();
|
||||
if (el) lastScrollTop = el.scrollTop;
|
||||
updateScrollToBottomFab();
|
||||
}
|
||||
|
||||
window.CyberStrikeChatScroll = {
|
||||
init: initChatScroll,
|
||||
onUserSendMessage: onUserSendMessage,
|
||||
onStreamEnd: onStreamEnd,
|
||||
onTaskEventStreamBegin: onTaskEventStreamBegin,
|
||||
onTaskEventStreamEnd: onTaskEventStreamEnd,
|
||||
captureScrollPinState: captureScrollPinState,
|
||||
scheduleScroll: scheduleChatScrollToBottomIfFollowing,
|
||||
scrollIfPinned: scrollChatMessagesToBottomIfPinned,
|
||||
forceScrollToBottom: forceScrollChatToBottom,
|
||||
applyMessageScroll: applyMessageScrollOption,
|
||||
scrollIntoViewIfFollowing: scrollElementIntoViewIfFollowing,
|
||||
isPinnedToBottom: isChatMessagesPinnedToBottom,
|
||||
markProgressStreaming: markProgressStreaming,
|
||||
markProcessDetailsStreaming: markProcessDetailsStreaming,
|
||||
setScrollFollowing: setScrollFollowing,
|
||||
setScrollDetached: setScrollDetached,
|
||||
};
|
||||
|
||||
window.isChatMessagesPinnedToBottom = isChatMessagesPinnedToBottom;
|
||||
window.captureScrollPinState = captureScrollPinState;
|
||||
window.scrollChatMessagesToBottomIfPinned = scrollChatMessagesToBottomIfPinned;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initChatScroll);
|
||||
} else {
|
||||
initChatScroll();
|
||||
}
|
||||
})();
|
||||
+32
-6
@@ -872,7 +872,10 @@ async function sendMessage() {
|
||||
const displayMessage = hasAttachments
|
||||
? message + '\n' + chatAttachments.map(a => '📎 ' + a.fileName).join('\n')
|
||||
: message;
|
||||
addMessage('user', displayMessage);
|
||||
if (window.CyberStrikeChatScroll) {
|
||||
window.CyberStrikeChatScroll.onUserSendMessage();
|
||||
}
|
||||
addMessage('user', displayMessage, null, null, null, { scroll: 'none' });
|
||||
|
||||
// 清除防抖定时器,防止在清空输入框后重新保存草稿
|
||||
if (draftSaveTimer) {
|
||||
@@ -930,6 +933,10 @@ async function sendMessage() {
|
||||
|
||||
// 创建进度消息容器(使用详细的进度展示)
|
||||
const progressId = addProgressMessage();
|
||||
if (window.CyberStrikeChatScroll) {
|
||||
window.CyberStrikeChatScroll.markProgressStreaming(true, progressId);
|
||||
window.CyberStrikeChatScroll.onUserSendMessage();
|
||||
}
|
||||
const progressElement = document.getElementById(progressId);
|
||||
registerProgressTask(progressId, currentConversationId);
|
||||
loadActiveTasks();
|
||||
@@ -1007,6 +1014,9 @@ async function sendMessage() {
|
||||
}
|
||||
} finally {
|
||||
window.__csAgentLiveStream = { active: false, conversationId: null, progressId: null };
|
||||
if (window.CyberStrikeChatScroll) {
|
||||
window.CyberStrikeChatScroll.onStreamEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// 消息发送成功后,再次确保草稿被清除
|
||||
@@ -2149,7 +2159,11 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
messageDiv.setAttribute('data-system-ready-message', '1');
|
||||
}
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
if (window.CyberStrikeChatScroll) {
|
||||
window.CyberStrikeChatScroll.applyMessageScroll(options);
|
||||
} else {
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -3296,7 +3310,11 @@ async function loadConversation(conversationId) {
|
||||
if (offset < rest.length) {
|
||||
requestAnimationFrame(renderNextBatch);
|
||||
} else {
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
if (window.CyberStrikeChatScroll) {
|
||||
window.CyberStrikeChatScroll.forceScrollToBottom(false);
|
||||
} else {
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
@@ -3304,7 +3322,11 @@ async function loadConversation(conversationId) {
|
||||
});
|
||||
}
|
||||
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
if (window.CyberStrikeChatScroll) {
|
||||
window.CyberStrikeChatScroll.forceScrollToBottom(false);
|
||||
} else {
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
addAttackChainButton(conversationId);
|
||||
await pendingMessageBatches;
|
||||
if (seq !== loadConversationRequestSeq) {
|
||||
@@ -3315,8 +3337,12 @@ async function loadConversation(conversationId) {
|
||||
}
|
||||
} else {
|
||||
const readyMsgEmpty = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgEmpty, null, null, null, { systemReadyMessage: true });
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
addMessage('assistant', readyMsgEmpty, null, null, null, { systemReadyMessage: true, scroll: 'force' });
|
||||
if (window.CyberStrikeChatScroll) {
|
||||
window.CyberStrikeChatScroll.forceScrollToBottom(false);
|
||||
} else {
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
addAttackChainButton(conversationId);
|
||||
if (seq !== loadConversationRequestSeq) {
|
||||
return;
|
||||
|
||||
+40
-25
@@ -530,23 +530,6 @@ function isConversationTaskRunning(conversationId) {
|
||||
return conversationExecutionTracker.isRunning(conversationId);
|
||||
}
|
||||
|
||||
/** 距底部该像素内视为「跟随底部」;流式输出时仅在此情况下自动滚到底部,避免用户上滑查看历史时被强制拉回 */
|
||||
const CHAT_SCROLL_PIN_THRESHOLD_PX = 120;
|
||||
|
||||
/** wasPinned 须在 DOM 追加内容之前计算,否则 scrollHeight 变大后会误判 */
|
||||
function scrollChatMessagesToBottomIfPinned(wasPinned) {
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
if (!messagesDiv || !wasPinned) return;
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function isChatMessagesPinnedToBottom() {
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
if (!messagesDiv) return true;
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesDiv;
|
||||
return scrollHeight - clientHeight - scrollTop <= CHAT_SCROLL_PIN_THRESHOLD_PX;
|
||||
}
|
||||
|
||||
/** 顶栏「停止任务」与进度条按钮对齐时,用会话 ID 反查当前页的 progress 块 ID(无则弹窗内仍可按会话取消) */
|
||||
function findProgressIdByConversationId(conversationId) {
|
||||
if (!conversationId) {
|
||||
@@ -788,8 +771,16 @@ function addProgressMessage() {
|
||||
messageDiv.appendChild(contentWrapper);
|
||||
messageDiv.dataset.conversationId = currentConversationId || '';
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
|
||||
bubble.classList.add('is-streaming');
|
||||
const progressWasPinned = typeof window.captureScrollPinState === 'function'
|
||||
? window.captureScrollPinState()
|
||||
: true;
|
||||
if (typeof window.scrollChatMessagesToBottomIfPinned === 'function') {
|
||||
window.scrollChatMessagesToBottomIfPinned(progressWasPinned);
|
||||
} else if (progressWasPinned) {
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1086,11 +1077,14 @@ function toggleProcessDetails(progressId, assistantMessageId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到展开的详情位置,而不是滚动到底部
|
||||
// 滚动到展开的详情位置(流式且用户上滑阅读时不抢主列表滚动)
|
||||
if (timeline && timeline.classList.contains('expanded')) {
|
||||
setTimeout(() => {
|
||||
// 使用 scrollIntoView 滚动到详情容器位置
|
||||
detailsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.scrollIntoViewIfFollowing === 'function') {
|
||||
window.CyberStrikeChatScroll.scrollIntoViewIfFollowing(detailsContainer, { behavior: 'smooth', block: 'nearest' });
|
||||
} else if (typeof window.captureScrollPinState === 'function' ? window.captureScrollPinState() : true) {
|
||||
detailsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
@@ -1177,7 +1171,9 @@ function convertProgressToDetails(progressId, assistantMessageId) {
|
||||
|
||||
// 将详情组件插入到助手消息之后
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
const insertWasPinned = isChatMessagesPinnedToBottom();
|
||||
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);
|
||||
@@ -1264,7 +1260,9 @@ function mergeMcpExecutionIDLists(prev, next) {
|
||||
// 处理流式事件
|
||||
function handleStreamEvent(event, progressElement, progressId,
|
||||
getAssistantId, setAssistantId, getMcpIds, setMcpIds) {
|
||||
const streamScrollWasPinned = isChatMessagesPinnedToBottom();
|
||||
const streamScrollWasPinned = typeof window.captureScrollPinState === 'function'
|
||||
? window.captureScrollPinState()
|
||||
: (typeof window.isChatMessagesPinnedToBottom === 'function' ? window.isChatMessagesPinnedToBottom() : true);
|
||||
|
||||
// 不依赖进度时间线;在首条 SSE 即可绑定用户消息 ID
|
||||
if (event.type === 'message_saved') {
|
||||
@@ -2277,7 +2275,11 @@ function expandProcessDetailsTimeline(assistantMessageId) {
|
||||
btn.innerHTML = '<span>' + collapseT + '</span>';
|
||||
});
|
||||
setTimeout(function () {
|
||||
detailsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.scrollIntoViewIfFollowing === 'function') {
|
||||
window.CyberStrikeChatScroll.scrollIntoViewIfFollowing(detailsContainer, { behavior: 'smooth', block: 'nearest' });
|
||||
} else if (typeof window.captureScrollPinState === 'function' ? window.captureScrollPinState() : true) {
|
||||
detailsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -2463,6 +2465,10 @@ async function attachRunningTaskEventStream(conversationId) {
|
||||
const progressId = taskReplayProgressId(conversationId);
|
||||
beginCsTaskReplay(progressId, asEl.id, conversationId);
|
||||
|
||||
if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.onTaskEventStreamBegin === 'function') {
|
||||
window.CyberStrikeChatScroll.onTaskEventStreamBegin(conversationId, asEl.id, progressId);
|
||||
}
|
||||
|
||||
const url = '/api/agent-loop/task-events?conversationId=' + encodeURIComponent(conversationId);
|
||||
const response = await apiFetch(url, {
|
||||
method: 'GET',
|
||||
@@ -2473,6 +2479,9 @@ async function attachRunningTaskEventStream(conversationId) {
|
||||
if (progressTaskState.has(progressId)) {
|
||||
progressTaskState.delete(progressId);
|
||||
}
|
||||
if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.onTaskEventStreamEnd === 'function') {
|
||||
window.CyberStrikeChatScroll.onTaskEventStreamEnd();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2508,6 +2517,9 @@ async function attachRunningTaskEventStream(conversationId) {
|
||||
if (progressTaskState.has(progressId)) {
|
||||
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成');
|
||||
}
|
||||
if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.onTaskEventStreamEnd === 'function') {
|
||||
window.CyberStrikeChatScroll.onTaskEventStreamEnd();
|
||||
}
|
||||
if (typeof loadActiveTasks === 'function') loadActiveTasks();
|
||||
if (typeof window.loadConversation === 'function' && window.currentConversationId === conversationId) {
|
||||
await window.loadConversation(conversationId);
|
||||
@@ -2516,6 +2528,9 @@ async function attachRunningTaskEventStream(conversationId) {
|
||||
} catch (e) {
|
||||
console.warn('attachRunningTaskEventStream', e);
|
||||
clearCsTaskReplay();
|
||||
if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.onTaskEventStreamEnd === 'function') {
|
||||
window.CyberStrikeChatScroll.onTaskEventStreamEnd();
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (taskEventReplayAttachState.inFlightPromise === attachPromise) {
|
||||
|
||||
Reference in New Issue
Block a user