Browse Source

merge: 合并新代码

master
Senor-Liu 6 hours ago
parent
commit
82fcc433c9
  1. 13
      api/package-lock.json
  2. 2
      web/client/src/sections/aichat/router.js
  3. 16
      web/client/src/sections/superAgent/actions/api.js
  4. 54
      web/client/src/sections/superAgent/components/SuperAgentConversation.jsx
  5. 5
      web/client/src/sections/superAgent/components/TaskProgressCard.jsx
  6. 2
      web/client/src/sections/superAgent/constants.js
  7. 756
      web/client/src/sections/superAgent/container/index.jsx
  8. 249
      web/client/src/sections/superAgent/hooks/useSuperAgentChat.js
  9. 7
      web/client/src/sections/superAgent/hooks/useSuperAgentSession.js
  10. 697
      web/client/src/sections/superAgent/hooks/useSuperAgentStream.js
  11. 2
      web/client/src/sections/superAgent/router.js
  12. 6
      web/client/src/sections/superAgent/utils/streamMessage.js
  13. 3781
      web/package-lock.json

13
api/package-lock.json

@ -15,6 +15,7 @@
"@modelcontextprotocol/sdk": "^1.17.2",
"@peace/node_server": "^2.0.1",
"cheerio": "^1.0.0",
"dotenv": "^17.4.2",
"html-docx-js": "^0.3.1",
"iconv-lite": "^0.6.3",
"image-size": "^2.0.2",
@ -1614,6 +1615,18 @@
"tslib": "^2.0.3"
}
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dottie": {
"version": "2.0.6",
"resolved": "https://nexus.ngaiot.com/repository/fs-npm/dottie/-/dottie-2.0.6.tgz",

2
web/client/src/sections/aichat/router.js

@ -4,7 +4,7 @@ export default {
type: 'outer',
route: {
key: 'aichat',
path: "/aichat",
path: "/aichatv1",
component: AiChat
}
};

16
web/client/src/sections/superAgent/actions/api.js

@ -1,7 +1,7 @@
/**
* 文件作用superAgent 超级智能体模块文件
* 职责范围
* 1. 承载 superAgent 业务 sessionworkspace附件以及保留的旧聊天能力
* 1. 承载 superAgent 业务 sessionworkspace 附件能力
* 2. 在不影响 newAiChat 的前提下复用现有组件和接口
* 3. 支撑标书任务工作区的前端编排
*
@ -11,7 +11,7 @@
* - 封装 v2 useStream 协议新版流由 useSuperAgentStream 直接交给 SDK 管理
*
* 维护说明
* - sendChatcheckpoint 和旧 history action 为回滚能力不得由新版容器调用
* - 不负责消息流thread 快照或 interrupt 恢复这些能力统一由 useStream 管理
*/
"use strict";
@ -84,18 +84,6 @@ export const createSuperAgentSession = (options = {}) => async () => {
return { payload: { data } };
};
export const getSuperAgentSessionHistory = createDirectJsonAction({
method: "GET",
path: superAgentServiceApiTable.sessionHistory,
fallbackMessage: "获取超级智能体历史消息失败",
});
export const getSuperAgentSessionState = createDirectJsonAction({
method: "GET",
path: superAgentServiceApiTable.sessionState,
fallbackMessage: "获取超级智能体会话状态失败",
});
export const getSuperAgentSessionSections = createDirectJsonAction({
method: "GET",
path: superAgentServiceApiTable.sessionSections,

54
web/client/src/sections/superAgent/components/SuperAgentConversation.jsx

@ -213,6 +213,7 @@ const SuperAgentConversation = ({
chatWelcomeContent,
taskWelcomeTitle,
taskWelcomeContent,
isWaitingForInterrupt = false,
agentCategory,
setAgentCategory,
agentKeyword,
@ -535,12 +536,22 @@ const SuperAgentConversation = ({
const checkpoint = normalizeCheckpoint(item);
const progressTask = latestProgressTask || item.task;
if (isBodyProgressCheckpoint(item, progressTask)) return null;
if (isStaleCatalogCheckpoint(item, progressTask)) return null;
//[LangGraph interrupt suspended ]
const checkpointProgressTask =
isWaitingForInterrupt &&
String(progressTask?.status || "").toLowerCase() === "suspended"
? {
...progressTask,
status: "waiting_interrupt",
session_status: "waiting_interrupt",
}
: progressTask;
if (isBodyProgressCheckpoint(item, checkpointProgressTask)) return null;
if (isStaleCatalogCheckpoint(item, checkpointProgressTask)) return null;
return (
<CheckpointHost
checkpoint={checkpoint}
progressTask={progressTask}
progressTask={checkpointProgressTask}
submitting={item.checkpointStatus === "submitting"}
onAction={(action) => handleCheckpointAction(item, action)}
/>
@ -629,7 +640,8 @@ const SuperAgentConversation = ({
(Array.isArray(item.runLog?.entries) &&
item.runLog.entries.some((entry) => entry?.status === "running"));
const shouldShowActiveLoading =
!hasStructuredCard && (showLoadingIndicator || hasRunningTool);
!hasStructuredCard &&
(showLoadingIndicator || (isStreamingAnswer && hasRunningTool));
const shouldShowLoading =
!hasStructuredCard &&
!item.content &&
@ -686,9 +698,20 @@ const SuperAgentConversation = ({
const renderConversationMessages = () => {
const messageNodeList = [];
const displayConversationMessages = mergeDisplayConversationMessages(conversationMessages);
const latestAssistantIndex = [...displayConversationMessages]
.map((item) => item.role)
.lastIndexOf("assistant");
const latestAssistantIndex = displayConversationMessages.reduce(
(latestIndex, item, index) => {
const isRenderableAssistantMessage =
item.role === "assistant" &&
!["taskProgress", "checkpoint", "content"].includes(item.type);
return isRenderableAssistantMessage ? index : latestIndex;
},
-1
);
const lastConversationMessage =
displayConversationMessages[displayConversationMessages.length - 1];
const hasTrailingLoadingMessage =
isStreamingAnswer &&
(lastConversationMessage?.role === "user" || latestAssistantIndex < 0);
let messageIndex = 0;
while (messageIndex < displayConversationMessages.length) {
@ -701,12 +724,27 @@ const SuperAgentConversation = ({
messageNodeList.push(
renderSingleConversationMessage(currentMessage, {
showLoadingIndicator:
isStreamingAnswer && messageIndex === latestAssistantIndex,
isStreamingAnswer &&
!hasTrailingLoadingMessage &&
messageIndex === latestAssistantIndex,
})
);
messageIndex += 1;
}
if (hasTrailingLoadingMessage) {
messageNodeList.push(
renderSingleConversationMessage(
{
key: "stream-loading-placeholder",
role: "assistant",
content: "",
},
{ showLoadingIndicator: true }
)
);
}
return messageNodeList;
};

5
web/client/src/sections/superAgent/components/TaskProgressCard.jsx

@ -18,6 +18,7 @@ import React from "react";
import { Button } from "antd";
import {
CheckCircleOutlined,
CloseCircleOutlined,
DownloadOutlined,
FileWordOutlined,
LoadingOutlined,
@ -61,7 +62,7 @@ const TaskProgressCard = ({ item = {}, onOpenWorkspace, onDownloadFile }) => {
? "已生成"
: "正在生成";
const statusIcon = isFailed
? null
? <CloseCircleOutlined />
: isSuspended
? <PauseCircleOutlined />
: isCompleted
@ -96,7 +97,7 @@ const TaskProgressCard = ({ item = {}, onOpenWorkspace, onDownloadFile }) => {
<div className="super-body-artifact-title" title={fileName}>
{fileName}
</div>
<div className={`super-body-artifact-status is-${isCompleted ? "completed" : isFailed ? "failed" : isSuspended ? "suspended" : "running"}`}>
<div className={`super-body-artifact-status is-${isFailed ? "failed" : isSuspended ? "suspended" : isCompleted ? "completed" : "running"}`}>
{statusIcon}
<span>{statusText}</span>
</div>

2
web/client/src/sections/superAgent/constants.js

@ -192,7 +192,7 @@ export const quickTaskList = [
key: "legal",
title: "法务评估",
desc: "归集项目和材料,生成评估报告和起诉状。",
enabled: true,
enabled: false,
icon: KnowledgeIcon,
},
{

756
web/client/src/sections/superAgent/container/index.jsx

File diff suppressed because it is too large

249
web/client/src/sections/superAgent/hooks/useSuperAgentChat.js

@ -21,7 +21,6 @@ import {
cancelSuperAgentSession,
createSuperAgentSession,
deleteSuperAgentSession,
getSuperAgentSessionHistory,
respondSuperAgentCheckpointStream,
sendSuperAgentChat,
uploadSuperAgentFile,
@ -46,7 +45,6 @@ import {
isSuperAgentRunLogTool,
markTenderSessionId,
markPendingEmptySession,
normalizeHistoryMessages,
removeTenderSessionId,
resolveApiFolderId,
saveActiveSessionId,
@ -117,7 +115,6 @@ const useSuperAgentChat = ({
setWorkspaceOpen,
actions,
dispatch,
protocolMode = "legacy",
}) => {
const [routeType, setRouteType] = useState("chat");
const [inputValue, setInputValue] = useState("");
@ -158,7 +155,6 @@ const useSuperAgentChat = ({
const chatRunActiveRef = useRef(false);
const creatingSessionRef = useRef(false);
const sidebarRefreshTimerRef = useRef(null);
const historyRefreshTimerRef = useRef(null);
const artifactProgressSessionRef = useRef("");
const taskProgressUpsertAtRef = useRef(0);
const taskProgressPendingRef = useRef(null);
@ -287,24 +283,6 @@ const useSuperAgentChat = ({
}
setConversationMessages((prev) => {
if (messageData.type === "content") {
const hasCurrentContentEntry = prev.some(
(item) => item.key === messageKey && item.type === "content"
);
const hasHistoryContentWorkspace = prev.some((item) => {
const checkpoint = item?.checkpoint?.checkpoint || item?.checkpoint || {};
const checkpointType = checkpoint.checkpoint_type || checkpoint.type || "";
const title = checkpoint.title || item.title || "";
return (
item.type === "checkpoint" &&
(checkpointType === "content_edit" ||
checkpointType === "progress_view" ||
title === "正文工作区")
);
});
if (!hasCurrentContentEntry && hasHistoryContentWorkspace) return prev;
}
const hasMessage = prev.some((item) => item.key === messageKey);
if (!hasMessage) {
return [
@ -371,147 +349,6 @@ const useSuperAgentChat = ({
setConversationMessages((prev) => prev.filter((item) => item.key !== messageKey));
};
//[生成稳定签名,用于轮询 history 时判断消息是否真的变化]
const buildConversationMessageSignature = (item = {}) => {
const runLogEntries = Array.isArray(item.runLog?.entries)
? item.runLog.entries.map((entry) => ({
id: entry.id || "",
name: entry.name || "",
kind: entry.kind || "",
status: entry.status || "",
arguments: entry.arguments || "",
result: entry.result || "",
}))
: [];
return JSON.stringify({
key: item.key || "",
role: item.role || "",
type: item.type || "",
title: item.title || "",
content: item.content || "",
loadingText: item.loadingText || "",
checkpointStatus: item.checkpointStatus || "",
checkpoint: item.checkpoint || null,
task: item.task || null,
files: item.files || [],
runLogEntries,
});
};
//[按 key 做 history diff,未变化的消息沿用原对象,减少轮询导致的重绘]
const mergeHistoryMessagesByDiff = (currentMessages = [], historyMessages = []) => {
const currentMap = new Map(currentMessages.map((item) => [item.key, item]));
const historyKeySet = new Set(historyMessages.map((item) => item.key));
const shouldPreserveLocalRuntimeMessage = (item = {}) => {
if (!item.key || historyKeySet.has(item.key)) return false;
if (item.type === "taskProgress") return true;
return String(item.key).startsWith("session_running_");
};
const nextHistoryMessages = historyMessages.map((historyItem) => {
const currentItem = currentMap.get(historyItem.key);
if (
currentItem &&
buildConversationMessageSignature(currentItem) ===
buildConversationMessageSignature(historyItem)
) {
return currentItem;
}
return historyItem;
});
const localRuntimeMessages = currentMessages.filter(shouldPreserveLocalRuntimeMessage);
const nextMessages = [...nextHistoryMessages, ...localRuntimeMessages];
const currentSignature = currentMessages.map(buildConversationMessageSignature).join("\n");
const nextSignature = nextMessages.map(buildConversationMessageSignature).join("\n");
if (currentSignature === nextSignature) return currentMessages;
return nextMessages;
};
/**
* 功能重新读取当前会话 history并用持久化消息更新对话区
* 使用场景正文生成完成后恢复后端写入的正文工作区 checkpoint
*
* 入参sessionId 当前正文任务会话 ID
* 返回是否成功更新对话消息
*
* 注意 history 或用户已切换会话时保留当前消息避免异步响应覆盖其他会话
*/
const refreshSessionHistoryMessages = useCallback(async (
sessionId,
{ requireBodyWorkspace = false } = {}
) => {
if (!sessionId || activeSessionIdRef.current !== sessionId) return false;
try {
const data = await dispatch(getSuperAgentSessionHistory({
params: buildSessionParams(sessionId),
query: { userId: currentUserId },
})).then((res) => ensureActionSuccess(res, "获取任务历史失败"));
const historyData = data?.data || data || {};
const historyMessages = normalizeHistoryMessages(
historyData?.messages || [],
sessionId
);
if (
!historyMessages.length ||
activeSessionIdRef.current !== sessionId
) {
return false;
}
const hasBodyWorkspace = historyMessages.some((item) => {
const checkpoint = item?.checkpoint?.checkpoint || item?.checkpoint || {};
const checkpointType = checkpoint.checkpoint_type || checkpoint.type || "";
const title = checkpoint.title || item.title || "";
return (
checkpointType === "content_edit" ||
checkpointType === "progress_view" ||
title === "正文工作区"
);
});
if (requireBodyWorkspace && !hasBodyWorkspace) return false;
setConversationMessages((prev) => mergeHistoryMessagesByDiff(prev, historyMessages));
return true;
} catch (error) {
superAgentWarn("chat.history", "refresh_after_artifact_failed", {
sessionId,
error,
});
return false;
}
}, [actions, currentUserId, dispatch, protocolMode]);
//[正文完成后以 SSE 生命周期为准刷新 history,不依赖可能被 checkpoint 替换的进度卡]
const scheduleArtifactHistoryRefresh = useCallback((sessionId, attempt = 0) => {
if (!sessionId || activeSessionIdRef.current !== sessionId) return;
if (historyRefreshTimerRef.current) {
window.clearTimeout(historyRefreshTimerRef.current);
}
historyRefreshTimerRef.current = window.setTimeout(async () => {
historyRefreshTimerRef.current = null;
superAgentLog("chat.history", "refresh_after_artifact_start", {
sessionId,
attempt,
});
const refreshed = await refreshSessionHistoryMessages(sessionId, {
requireBodyWorkspace: true,
});
if (refreshed) {
superAgentLog("chat.history", "refresh_after_artifact_done", {
sessionId,
attempt,
});
return;
}
if (attempt < 2 && activeSessionIdRef.current === sessionId) {
scheduleArtifactHistoryRefresh(sessionId, attempt + 1);
}
}, attempt === 0 ? 800 : 1200);
}, [refreshSessionHistoryMessages]);
useEffect(() => {
return () => {
if (chatAbortControllerRef.current) {
@ -520,9 +357,6 @@ const useSuperAgentChat = ({
if (sidebarRefreshTimerRef.current) {
window.clearTimeout(sidebarRefreshTimerRef.current);
}
if (historyRefreshTimerRef.current) {
window.clearTimeout(historyRefreshTimerRef.current);
}
if (taskProgressTimerRef.current) {
window.clearTimeout(taskProgressTimerRef.current);
}
@ -914,7 +748,6 @@ const useSuperAgentChat = ({
artifactProgressSessionRef.current = options.sessionId;
}
if (isArtifactCompleted) {
scheduleArtifactHistoryRefresh(options.sessionId);
}
updateConversationMessage(assistantMessageKey, (item) => ({
title: progress >= 100 ? "正文生成完成" : `正在生成:${sectionTitle}`,
@ -1056,7 +889,6 @@ const useSuperAgentChat = ({
if (eventName === "done") {
if (artifactProgressSessionRef.current === options.sessionId) {
artifactProgressSessionRef.current = "";
scheduleArtifactHistoryRefresh(options.sessionId);
}
clearWaitingNoticeMessages();
updateConversationMessage(assistantMessageKey, (item) => {
@ -1226,10 +1058,9 @@ const useSuperAgentChat = ({
},
});
//[部分服务不会发送 done 事件;流结束后仍需按本轮 artifact 进度刷新 history]
//[部分服务不会发送 done 事件;本轮进度仍由 useStream 业务事件维护]
if (artifactProgressSessionRef.current === sessionId) {
artifactProgressSessionRef.current = "";
scheduleArtifactHistoryRefresh(sessionId);
}
superAgentLog("chat.send", "run.done", {
@ -1406,7 +1237,6 @@ const useSuperAgentChat = ({
if (artifactProgressSessionRef.current === sessionId) {
artifactProgressSessionRef.current = "";
scheduleArtifactHistoryRefresh(sessionId);
}
superAgentLog("chat.checkpoint", "respond.done", {
@ -1915,8 +1745,6 @@ const useSuperAgentChat = ({
};
const handleSelectTask = async (task) => {
//[切换会话时中止当前SSE并重置流式状态,避免running状态泄漏到新会话]
handleStopAnswer();
activeSessionIdRef.current = task.id;
setActiveTaskId(task.id);
setActiveSessionId(task.id);
@ -1927,51 +1755,12 @@ const useSuperAgentChat = ({
setUploadedFileList([]);
setSelectedTaskType(null);
setConversationMessages([]);
if (protocolMode === "v2") {
return { success: true, isEmptyHistory: false };
}
try {
const data = await dispatch(getSuperAgentSessionHistory({
params: buildSessionParams(task.id),
query: { userId: currentUserId },
})).then((res) => ensureActionSuccess(res, "获取任务历史失败"));
//[陈旧会话检查:异步操作完成后确认当前激活的会话ID未改变]
if (activeSessionIdRef.current !== task.id) {
return { success: false, isEmptyHistory: false, reason: "stale_session" };
}
const historyData = data?.data || data || {};
const rawHistoryMessages = Array.isArray(historyData?.messages)
? historyData.messages
: [];
const historyMessages = normalizeHistoryMessages(
rawHistoryMessages,
task.id
);
//[空任务会话:展示任务大厅,而非直接进入标书引导]
if (!rawHistoryMessages.length) {
//[useStream 是唯一会话恢复来源,stream.messages 负责在当前 thread 完成快照加载后还原消息]
setConversationMessages([buildEmptyTaskStartMessage()]);
return { success: true, isEmptyHistory: true };
} else {
setSelectedTaskType("tender");
setConversationMessages(historyMessages);
}
return { success: true, isEmptyHistory: false };
} catch (error) {
const isSessionMissing = /404|not found|不存在/i.test(String(error?.message || ""));
if (isSessionMissing) {
removeSidebarSessionLocally(task.id, "task");
clearActiveSidebarSession(task.id, true);
message.warning("该任务记录已不存在,已从列表移除");
return { success: false, isEmptyHistory: false };
}
message.error(error?.message || "获取任务历史失败");
return { success: false, isEmptyHistory: false };
}
return { success: true };
};
const handleSelectChat = async (chat) => {
//[切换会话时中止当前SSE并重置流式状态,避免running状态泄漏到新会话]
handleStopAnswer();
const sessionId = chat.id || chat.chatId;
activeSessionIdRef.current = sessionId;
setActiveSessionId(sessionId);
@ -1983,35 +1772,8 @@ const useSuperAgentChat = ({
setUploadedFileList([]);
setSelectedTaskType(null);
setConversationMessages([]);
if (protocolMode === "v2") {
return { success: true, isEmptyHistory: false };
}
try {
const data = await dispatch(getSuperAgentSessionHistory({
params: buildSessionParams(sessionId),
query: { userId: currentUserId },
})).then((res) => ensureActionSuccess(res, "获取对话历史失败"));
//[陈旧会话检查:异步操作完成后确认当前激活的会话ID未改变]
if (activeSessionIdRef.current !== sessionId) {
return { success: false, isEmptyHistory: false, reason: "stale_session" };
}
const historyData = data?.data || data || {};
const rawHistoryMessages = Array.isArray(historyData?.messages)
? historyData.messages
: [];
setConversationMessages(normalizeHistoryMessages(rawHistoryMessages, sessionId));
return { success: true, isEmptyHistory: rawHistoryMessages.length === 0 };
} catch (error) {
const isSessionMissing = /404|not found|不存在/i.test(String(error?.message || ""));
if (isSessionMissing) {
removeSidebarSessionLocally(sessionId, "chat");
clearActiveSidebarSession(sessionId, true);
message.warning("该对话记录已不存在,已从列表移除");
return { success: false, isEmptyHistory: false };
}
message.error(error?.message || "获取对话历史失败");
return { success: false, isEmptyHistory: false };
}
//[useStream 是唯一会话恢复来源,stream.messages 负责在当前 thread 完成快照加载后还原消息]
return { success: true };
};
const handleDeleteSidebarSession = async (item, itemType) => {
@ -2076,7 +1838,6 @@ const useSuperAgentChat = ({
updateActiveTask,
updateConversationMessage,
removeConversationMessage,
refreshSessionHistoryMessages,
createAndActivateSession,
sendMessageToAgent,
respondCheckpointToAgent,

7
web/client/src/sections/superAgent/hooks/useSuperAgentSession.js

@ -4,21 +4,20 @@
*
* 职责
* 1. 复用成熟的 session侧栏文件上传和业务页面状态
* 2. 固定启用 v2 模式禁止旧 history 在任务打开切换和刷新时发起请求
* 2. 会话切换只设置 useStream active thread不主动读取消息或任务快照
*
* 不负责
* 1. 消息流interrupttool call停止运行或 SSE 生命周期
*
* 维护说明
* v2 协议能力统一由 useSuperAgentStream 承担此适配层不可暴露旧发送和 checkpoint 方法给容器
* useStream 协议能力统一由 useSuperAgentStream 承担此适配层只保留 session/sidebar 能力
*/
"use strict";
import useSuperAgentChat from "./useSuperAgentChat";
const useSuperAgentSession = (options) => {
const legacySession = useSuperAgentChat({ ...options, protocolMode: "v2" });
const session = { ...legacySession };
const session = { ...useSuperAgentChat(options) };
[
"sendMessageToAgent",
"respondCheckpointToAgent",

697
web/client/src/sections/superAgent/hooks/useSuperAgentStream.js

@ -8,8 +8,8 @@
* 3. 订阅业务 custom channel并通知容器刷新 workspace/sections
*
* 不负责
* 1. 创建业务 session加载 workspace/sections 兼容旧聊天协议
* 2. 手写 SSE调用旧 history/checkpoint 或管理业务会话
* 1. 创建业务 session加载 workspace/sections 管理业务会话
* 2. 手写 SSE调用旧聊天协议或维护独立恢复接口
*
* 维护说明
* Agent v2 不可用时只暴露错误调用方不得回退 /api/chat runs/stream
@ -43,6 +43,21 @@ const TERMINAL_INTERRUPT_STATUSES = new Set([
"canceled",
]);
const ACTIVE_TASK_STATUSES = new Set([
"queued",
"starting",
"started",
"running",
"pending",
"processing",
"generating",
"writing",
"active",
"streaming",
"resuming",
"awaiting_interrupt",
]);
const getInterruptStatus = (item = {}) => {
const payload = item?.value || item?.payload || {};
return String(
@ -63,6 +78,11 @@ const isSuperAgentEventRequest = (input) => {
return /\/threads\/[^/]+\/stream\/events(?:\?|$)/.test(requestUrl);
};
const isUseStreamHistoryDiscoveryRequest = (input) => {
const requestUrl = String(input?.url || input || "");
return /\/(?:threads|tasks)\/[^/]+\/history(?:\?|$)/.test(requestUrl);
};
const isLifecycleWatcherRequest = (input, init = {}) => {
if (!isSuperAgentEventRequest(input) || typeof init.body !== "string") {
return false;
@ -182,14 +202,32 @@ const fetchEventStreamWithRecovery = async (input, init) => {
class SingleSubscriberSseTransportAdapter extends ProtocolSseTransportAdapter {
activeEventStream = null;
eventHandles = new Set();
latestState = null;
onStateChange = null;
stateRequestVersion = 0;
async getState() {
const requestVersion = ++this.stateRequestVersion;
const requestedThreadId = String(this.threadId || "").trim();
const state = await super.getState();
// getState 可能在切换 thread 后才返回;旧请求不能覆盖新 thread 的快照。
if (
requestVersion === this.stateRequestVersion &&
String(this.threadId || "").trim() === requestedThreadId
) {
this.latestState = state;
this.onStateChange?.();
}
return state;
}
openEventStream(params) {
const isLifecycleWatcher = isLifecycleWatcherFilter(params);
const threadId = String(this.threadId || "").trim();
if (isLifecycleWatcher) {
// fetchWithHistoryCompatibility 会把这个 watcher 转成本地空流,
// 不要让它参与主事件流的连接排他和释放等待。
// useStream 的 root pump 已经覆盖 lifecycle/input,额外 watcher
// 不参与主事件流的连接排他和释放等待。
return super.openEventStream(params);
}
@ -310,131 +348,6 @@ const getEventPayload = (event) =>
event?.params?.data ??
event;
//[兼容历史消息]旧会话中的 tool 消息可能缺少 tool_call_id,LangChain 无法将其转换为 ToolMessage。
const isInvalidSerializedToolMessage = (message) =>
message?.type === "tool" && !message.tool_call_id && !message.toolCallId;
//[兼容历史消息]仅清理无法被 SDK 转换的 tool 消息,保留其他状态字段和可用消息。
const sanitizeLangGraphPayload = (value, parentKey = "") => {
if (Array.isArray(value)) {
return value
.filter((item) => parentKey !== "messages" || !isInvalidSerializedToolMessage(item))
.map((item) => sanitizeLangGraphPayload(item, parentKey));
}
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, sanitizeLangGraphPayload(item, key)])
);
};
//[保留同一会话的最近有效消息,避免 hydrate 短暂返回不完整快照时丢失流式内容]
const mergeStreamMessageSnapshots = (previousMessages = [], currentMessages = []) => {
const previous = Array.isArray(previousMessages) ? previousMessages : [];
const current = Array.isArray(currentMessages) ? currentMessages : [];
if (!previous.length) return current;
if (!current.length) return previous;
const currentById = new Map(
current
.filter((message) => message?.id)
.map((message) => [String(message.id), message])
);
const merged = [];
const usedIds = new Set();
previous.forEach((message) => {
const messageId = String(message?.id || "");
if (!messageId) return;
merged.push(currentById.get(messageId) || message);
usedIds.add(messageId);
});
current.forEach((message) => {
const messageId = String(message?.id || "");
if (!messageId || usedIds.has(messageId)) return;
merged.push(message);
});
return merged;
};
//[直接消费 root messages 事件,避免 history hydrate 覆盖正在生成的增量消息]
const applyMessageEventToState = (currentState, event) => {
const data = event?.params?.data || {};
const eventType = String(data.event || "");
const nextMessages = new Map(currentState.messages);
let activeMessageId = currentState.activeMessageId;
const messageId = String(data.id || activeMessageId || "").trim();
if (!messageId) return currentState;
const previousMessage = nextMessages.get(messageId) || {
id: messageId,
type: "ai",
role: "ai",
content: "",
tool_calls: [],
};
const nextMessage = {
...previousMessage,
tool_calls: Array.isArray(previousMessage.tool_calls)
? [...previousMessage.tool_calls]
: [],
};
if (eventType === "message-start") {
activeMessageId = messageId;
nextMessage.role = data.role || nextMessage.role || "ai";
nextMessage.type = nextMessage.role;
}
if (eventType === "content-block-delta") {
const delta = data.delta || {};
if (delta.type === "text-delta") {
nextMessage.content = `${String(nextMessage.content || "")}${String(delta.text || "")}`;
}
if (delta.type === "tool-call-delta") {
const toolCallIndex = Number(data.index || 0);
const previousToolCall = nextMessage.tool_calls[toolCallIndex] || {};
const deltaArgs = delta.args;
const nextArgs =
previousToolCall.args &&
typeof previousToolCall.args === "object" &&
deltaArgs &&
typeof deltaArgs === "object"
? { ...previousToolCall.args, ...deltaArgs }
: deltaArgs ?? previousToolCall.args;
nextMessage.tool_calls[toolCallIndex] = {
...previousToolCall,
args: nextArgs,
status: "running",
};
}
}
if (eventType === "content-block-finish") {
const content = data.content || {};
if (content.type === "text") {
nextMessage.content = content.text || nextMessage.content || "";
}
if (content.type === "tool_call") {
const toolCallIndex = Number(data.index || 0);
nextMessage.tool_calls[toolCallIndex] = {
...(nextMessage.tool_calls[toolCallIndex] || {}),
...content,
status: "success",
};
}
}
nextMessages.set(messageId, nextMessage);
return {
messages: nextMessages,
activeMessageId,
};
};
const getEventDedupeKey = (event) => {
if (event?.event_id) return String(event.event_id);
const payload = getEventPayload(event) || {};
@ -474,20 +387,22 @@ const useSuperAgentStream = ({
const pendingInterruptRef = useRef(false);
const awaitingInterruptRef = useRef(false);
const [runPhase, setRunPhase] = useState("idle");
const [isLocalRunActive, setIsLocalRunActive] = useState(false);
const [isRespondingInterrupt, setIsRespondingInterrupt] = useState(false);
const [isBusinessRunning, setIsBusinessRunning] = useState(false);
const [runtimeCheckpointDeliveries, setRuntimeCheckpointDeliveries] = useState([]);
const [runtimeInterrupts, setRuntimeInterrupts] = useState([]);
const [dismissedInterruptIds, setDismissedInterruptIds] = useState(new Set());
const resolvedInterruptIdsRef = useRef(new Set());
const [currentInterruptId, setCurrentInterruptId] = useState("");
const [hydratedThreadId, setHydratedThreadId] = useState("");
const [transportStateRevision, setTransportStateRevision] = useState(0);
const eventRequestControllersRef = useRef(new Map());
const streamSnapshotCacheRef = useRef(new Map());
const eventMessageStateRef = useRef({
const pendingSubmitThreadIdRef = useRef("");
const hydrationSessionRef = useRef({
sessionId: "",
activeMessageId: "",
messages: new Map(),
hasSeenLoading: false,
});
const [eventStreamMessages, setEventStreamMessages] = useState([]);
const localRunActiveRef = useRef(false);
const localRunHasSeenLoadingRef = useRef(false);
const activeSessionIdRef = useRef("");
@ -504,8 +419,17 @@ const useSuperAgentStream = ({
}),
[token, userId]
);
const fetchWithHistoryCompatibility = useCallback(async (...args) => {
const fetchUseStreamTransport = useCallback(async (...args) => {
const [input, init = {}] = args;
if (isUseStreamHistoryDiscoveryRequest(input)) {
// 当前 SDK hydrate 会为 namespace discovery 自动读取 history。
// SuperAgent 只以 thread state 和 event stream 为准,禁止该
// 非业务读取落到服务端,避免 history 再次参与会话恢复。
return new Response("[]", {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (isLifecycleWatcherRequest(input, init)) {
// Agent 服务每个 thread 只允许一个 events subscriber。useStream
// 的 root pump 已覆盖 lifecycle/input,额外 watcher 会稳定触发 429。
@ -515,7 +439,9 @@ const useSuperAgentStream = ({
});
}
let requestInit = init;
const sessionKey = String(activeSessionIdRef.current || "").trim();
const sessionKey = String(
pendingSubmitThreadIdRef.current || activeSessionIdRef.current || ""
).trim();
if (sessionKey && isSuperAgentEventRequest(input)) {
let sessionController = eventRequestControllersRef.current.get(sessionKey);
if (!sessionController) {
@ -528,40 +454,31 @@ const useSuperAgentStream = ({
};
}
const response = await fetchEventStreamWithRecovery(input, requestInit);
const contentType = response.headers.get("content-type") || "";
if (!/json/i.test(contentType)) return response;
try {
const payload = await response.clone().json();
const sanitizedPayload = sanitizeLangGraphPayload(payload);
const headers = new Headers(response.headers);
headers.delete("content-length");
return new Response(JSON.stringify(sanitizedPayload), {
status: response.status,
statusText: response.statusText,
headers,
});
} catch {
return response;
}
return fetchEventStreamWithRecovery(input, requestInit);
}, []);
const eventTransport = useMemo(
() =>
new SingleSubscriberSseTransportAdapter({
() => {
const transport = new SingleSubscriberSseTransportAdapter({
apiUrl: getSuperAgentApiUrl(),
defaultHeaders,
fetch: fetchWithHistoryCompatibility,
fetch: fetchUseStreamTransport,
maxReconnectAttempts: 5,
}),
[defaultHeaders, fetchWithHistoryCompatibility]
});
transport.onStateChange = () => {
setTransportStateRevision((revision) => revision + 1);
};
return transport;
},
[activeSessionId, defaultHeaders, fetchUseStreamTransport]
);
// StreamController 在 hydrate 时会先通过 transport.getState() 取新 thread;
// 自定义 adapter 需在该 effect 运行前同步绑定目标 thread,避免读到旧会话。
eventTransport.setThreadId(activeSessionId || "");
eventTransport.setThreadId(
activeSessionId || pendingSubmitThreadIdRef.current || ""
);
const callerOptions = useMemo(
() => ({ fetch: fetchWithHistoryCompatibility }),
[fetchWithHistoryCompatibility]
() => ({ fetch: fetchUseStreamTransport }),
[fetchUseStreamTransport]
);
const stream = useStream({
assistantId: "freesun_agent",
@ -616,6 +533,7 @@ const useSuperAgentStream = ({
const handleStreamError = useCallback((error) => {
localRunActiveRef.current = false;
setIsLocalRunActive(false);
localRunHasSeenLoadingRef.current = false;
interruptResumeRef.current = { active: false, running: false };
pendingInterruptRef.current = false;
@ -641,14 +559,16 @@ const useSuperAgentStream = ({
}, [activeSessionId, eventTransport]);
useEffect(() => {
eventMessageStateRef.current = {
if (pendingSubmitThreadIdRef.current === String(activeSessionId || "")) {
pendingSubmitThreadIdRef.current = "";
}
hydrationSessionRef.current = {
sessionId: String(activeSessionId || ""),
activeMessageId: "",
messages: new Map(),
hasSeenLoading: false,
};
setEventStreamMessages([]);
handledEventKeysRef.current.clear();
localRunActiveRef.current = false;
setIsLocalRunActive(false);
localRunHasSeenLoadingRef.current = false;
interruptResumeRef.current = { active: false, running: false };
pendingInterruptRef.current = false;
@ -659,41 +579,58 @@ const useSuperAgentStream = ({
setRuntimeCheckpointDeliveries([]);
setRuntimeInterrupts([]);
setDismissedInterruptIds(new Set());
resolvedInterruptIdsRef.current.clear();
setCurrentInterruptId("");
setHydratedThreadId("");
}, [activeSessionId]);
const handleMessageEvent = useCallback((event) => {
if (event?.method !== "messages") return;
const namespace = event?.params?.namespace;
if (Array.isArray(namespace) && namespace.length > 0) return;
const sessionId = String(activeSessionIdRef.current || "").trim();
if (!sessionId) return;
if (eventMessageStateRef.current.sessionId !== sessionId) {
eventMessageStateRef.current = {
sessionId,
activeMessageId: "",
messages: new Map(),
};
}
useEffect(() => {
const sessionId = String(activeSessionId || "");
const threadId = String(stream.threadId || "");
if (!sessionId || sessionId !== threadId) return undefined;
let cancelled = false;
// isThreadLoading 在 useStream hydrate 切换的中间阶段可能仍是 false,
// 不能用它作为快照完成标志;hydrationPromise 才代表当前 thread 的 state
// 已经写入 root store。
Promise.resolve(stream.hydrationPromise)
.then(() => {
if (cancelled || activeSessionIdRef.current !== sessionId) return;
setHydratedThreadId(sessionId);
})
.catch(() => undefined);
const nextState = applyMessageEventToState(eventMessageStateRef.current, event);
if (nextState === eventMessageStateRef.current) return;
eventMessageStateRef.current = nextState;
setEventStreamMessages([...nextState.messages.values()]);
}, []);
return () => {
cancelled = true;
};
}, [
activeSessionId,
stream.hydrationPromise,
stream.threadId,
transportStateRevision,
]);
const checkpointDeliveries = useMemo(() => {
const deliveries = new Map();
const hydratedDeliveries = Array.isArray(stream.values?.checkpoint_deliveries)
const hydratedDeliveries = [
...(Array.isArray(stream.values?.checkpoint_deliveries)
? stream.values.checkpoint_deliveries
: [];
: []),
...(Array.isArray(eventTransport.latestState?.values?.checkpoint_deliveries)
? eventTransport.latestState.values.checkpoint_deliveries
: []),
];
[...hydratedDeliveries, ...runtimeCheckpointDeliveries].forEach((item) => {
if (!item?.checkpoint_id) return;
deliveries.set(item.checkpoint_id, item);
});
return [...deliveries.values()];
}, [runtimeCheckpointDeliveries, stream.values?.checkpoint_deliveries]);
}, [
eventTransport.latestState,
runtimeCheckpointDeliveries,
stream.values?.checkpoint_deliveries,
transportStateRevision,
]);
useEffect(() => {
if (stream.error) handleStreamError(stream.error);
@ -709,6 +646,7 @@ const useSuperAgentStream = ({
localRunActiveRef.current = false;
localRunHasSeenLoadingRef.current = false;
setIsLocalRunActive(false);
setRunPhase((currentPhase) =>
["running", "resuming"].includes(currentPhase) ? "completed" : currentPhase
);
@ -722,23 +660,27 @@ const useSuperAgentStream = ({
String(stream.threadId || "") === String(activeSessionId || "");
if (!isCurrentThread) return undefined;
const listenerSessionId = String(activeSessionId || "");
const currentThread = stream.getThread?.();
if (!currentThread) return undefined;
return currentThread.onEvent((event) => {
handleMessageEvent(event);
// 切换会话后旧 Thread 的监听器可能在清理前再收到一个事件,
// 不能把旧会话的事件写入新会话的流式消息状态。
if (activeSessionIdRef.current !== listenerSessionId) return;
if (event?.method === "input.requested") {
const data = event.params?.data || {};
const interruptId = String(data.interrupt_id || "").trim();
if (interruptId) {
if (interruptId && !resolvedInterruptIdsRef.current.has(interruptId)) {
localRunActiveRef.current = false;
setIsLocalRunActive(false);
setCurrentInterruptId(interruptId);
pendingInterruptRef.current = true;
awaitingInterruptRef.current = false;
setRunPhase("waiting_interrupt");
setRuntimeInterrupts((current) => {
if (current.some((item) => item.id === interruptId)) return current;
setRuntimeInterrupts((currentInterrupts) => {
const nextInterrupts = currentInterrupts.filter(
(item) => String(item?.id || "") !== interruptId
);
return [
...current,
...nextInterrupts,
{
id: interruptId,
value: data.payload,
@ -748,6 +690,9 @@ const useSuperAgentStream = ({
},
];
});
pendingInterruptRef.current = true;
awaitingInterruptRef.current = false;
setRunPhase("waiting_interrupt");
}
}
if (event?.method === "lifecycle" && Array.isArray(event?.params?.namespace) && event.params.namespace.length === 0) {
@ -767,6 +712,8 @@ const useSuperAgentStream = ({
stream.interrupts.length > 0 ||
threadInterrupts.length > 0;
if (hasRealInterrupt) {
localRunActiveRef.current = false;
setIsLocalRunActive(false);
awaitingInterruptRef.current = false;
setRunPhase("waiting_interrupt");
} else {
@ -780,6 +727,8 @@ const useSuperAgentStream = ({
setRunPhase("waiting_interrupt");
} else {
awaitingInterruptRef.current = false;
localRunActiveRef.current = false;
setIsLocalRunActive(false);
// aborted 表示本轮已结束,不能让 started 留下的 running 状态继续生效。
setRunPhase(lifecycleEvent === "aborted" ? "completed" : lifecycleEvent);
}
@ -793,10 +742,18 @@ const useSuperAgentStream = ({
if (!BUSINESS_CHANNELS.includes(event?.method)) return;
handleBusinessEvent(event);
});
}, [activeSessionId, handleBusinessEvent, handleMessageEvent, stream.isThreadLoading, stream.threadId]);
}, [activeSessionId, handleBusinessEvent, stream.threadId]);
const prepareForSessionSwitch = useCallback(() => {
// 先让旧 thread 的事件监听失效。activeSessionId 在 React 下一次渲染
// 前仍可能是旧值,单靠监听器里的 sessionId 判断不够。
activeSessionIdRef.current = "";
// pendingSubmitThreadId 只服务于“创建会话后首次发送”这一瞬间;
// 切换会话时必须清掉,否则新 transport 会继续绑定旧 thread。
pendingSubmitThreadIdRef.current = "";
setHydratedThreadId("");
localRunActiveRef.current = false;
setIsLocalRunActive(false);
localRunHasSeenLoadingRef.current = false;
interruptResumeRef.current = { active: false, running: false };
pendingInterruptRef.current = false;
@ -804,18 +761,21 @@ const useSuperAgentStream = ({
setRunPhase("idle");
setIsRespondingInterrupt(false);
setIsBusinessRunning(false);
setRuntimeInterrupts([]);
resolvedInterruptIdsRef.current.clear();
if (stream.isLoading) {
void stream.stop().catch(() => undefined);
}
// 会话切换只断开当前浏览器订阅,不能发送 stop/cancel 命令。
// stopRun 只由用户点击停止按钮触发,否则后端会把原会话改为 suspended。
// useStream 切换 threadId 时会等待旧 root pump 清理;直接关闭当前
// ThreadStream 可以立即中止旧 /events 请求,避免阻塞新会话接管。
const currentThread = stream.getThread?.();
if (currentThread) {
void currentThread.close().catch(() => undefined);
}
}, [stream]);
eventTransport.close();
return Promise.allSettled([
stream.disconnect(),
currentThread ? currentThread.close() : Promise.resolve(),
]);
}, [eventTransport, stream]);
const submitMessage = useCallback(async ({
content,
@ -830,12 +790,29 @@ const useSuperAgentStream = ({
if (!targetThreadId) throw new Error("请先创建任务");
const isBlockedByInterrupt = isRespondingInterrupt && !allowDuringInterruptResponse;
const isSwitchingThread = String(stream.threadId || "") !== targetThreadId;
if ((stream.isThreadLoading && !isSwitchingThread) || isBlockedByInterrupt) {
// isThreadLoading 只表示当前 thread 的快照还在 hydrate,不能阻止首条消息提交。
// 真正表示本轮任务仍在运行的是 isLoading;否则新建会话会先创建成功,
// 但首条消息被这里提前拒绝,页面最终只剩空会话。
if ((stream.isLoading && !isSwitchingThread) || isBlockedByInterrupt) {
throw new Error("任务正在处理,请稍后再试");
}
if (stream.isThreadLoading && !isSwitchingThread) {
// 新建会话的 state 可能还未写入 root store。等待 hydrate 完成后再提交,
// 避免 optimistic 消息被空快照或旧事件回放覆盖。
await Promise.resolve(stream.hydrationPromise).catch(() => undefined);
}
// activeSessionId 的 React 状态更新可能晚于首次提交;先绑定 transport
// 和事件请求所属会话,避免首轮响应仍落到旧 thread。
if (hydrationSessionRef.current.sessionId === targetThreadId) {
hydrationSessionRef.current.hasSeenLoading = true;
}
pendingSubmitThreadIdRef.current = targetThreadId;
eventTransport.setThreadId(targetThreadId);
localRunActiveRef.current = true;
setIsLocalRunActive(true);
localRunHasSeenLoadingRef.current = false;
setRunPhase("running");
try {
await stream.submit({
messages: [{
type: "human",
@ -849,11 +826,17 @@ const useSuperAgentStream = ({
multitaskStrategy,
metadata: { source: "super-agent-web" },
});
}, [activeSessionId, isRespondingInterrupt, stream]);
} catch (error) {
localRunActiveRef.current = false;
setIsLocalRunActive(false);
throw error;
}
}, [activeSessionId, eventTransport, isRespondingInterrupt, stream]);
const respondInterrupt = useCallback((response, options = {}) => {
const interruptId = String(options.interruptId || "").trim();
localRunActiveRef.current = true;
setIsLocalRunActive(true);
localRunHasSeenLoadingRef.current = false;
interruptResumeRef.current = { active: true, running: false };
pendingInterruptRef.current = false;
@ -878,8 +861,11 @@ const useSuperAgentStream = ({
namespace: options.namespace,
}).then((result) => {
if (interruptId) {
setRuntimeInterrupts((current) =>
current.filter((item) => item.id !== interruptId)
resolvedInterruptIdsRef.current.add(interruptId);
setRuntimeInterrupts((currentInterrupts) =>
currentInterrupts.filter(
(item) => String(item?.id || "") !== interruptId
)
);
}
// respond 只代表恢复命令已提交,不代表后端本轮运行完成。
@ -888,6 +874,7 @@ const useSuperAgentStream = ({
}).catch((error) => {
if (interruptId) {
// 恢复失败时撤销临时隐藏,让用户可以重新提交。
resolvedInterruptIdsRef.current.delete(interruptId);
setDismissedInterruptIds((current) => {
const next = new Set(current);
next.delete(interruptId);
@ -896,6 +883,8 @@ const useSuperAgentStream = ({
setCurrentInterruptId((currentId) => currentId || interruptId);
}
interruptResumeRef.current = { active: false, running: false };
localRunActiveRef.current = false;
setIsLocalRunActive(false);
pendingInterruptRef.current = true;
setRunPhase("waiting_interrupt");
setIsRespondingInterrupt(false);
@ -905,6 +894,7 @@ const useSuperAgentStream = ({
const stopRun = useCallback(async () => {
localRunActiveRef.current = false;
setIsLocalRunActive(false);
localRunHasSeenLoadingRef.current = false;
interruptResumeRef.current = { active: false, running: false };
pendingInterruptRef.current = false;
@ -917,48 +907,211 @@ const useSuperAgentStream = ({
const currentSessionId = String(activeSessionId || "");
const currentThreadId = String(stream.threadId || "");
const projectedThreadId = String(
pendingSubmitThreadIdRef.current || currentThreadId
);
const isThreadBound =
Boolean(currentSessionId) && currentSessionId === currentThreadId;
const cachedSnapshot = streamSnapshotCacheRef.current.get(currentSessionId) || {
messages: [],
toolCalls: [],
values: {},
const isProjectedThreadBound =
Boolean(currentSessionId) && currentSessionId === projectedThreadId;
if (hydrationSessionRef.current.sessionId !== currentSessionId) {
hydrationSessionRef.current = {
sessionId: currentSessionId,
hasSeenLoading: false,
};
const currentValues = isThreadBound && stream.values ? stream.values : {};
const currentValueMessages = Array.isArray(currentValues.messages)
? currentValues.messages
: [];
const currentMessages = isThreadBound ? stream.messages : [];
const hydratedMessages = mergeStreamMessageSnapshots(
mergeStreamMessageSnapshots(cachedSnapshot.messages, currentValueMessages),
currentMessages
}
if (isThreadBound && stream.isThreadLoading) {
hydrationSessionRef.current.hasSeenLoading = true;
}
const isThreadReady =
isThreadBound &&
!stream.isThreadLoading &&
hydratedThreadId === currentSessionId;
const isPendingSubmitThread =
isProjectedThreadBound &&
pendingSubmitThreadIdRef.current === currentSessionId;
const isStreamProjectionReady =
isThreadReady || isPendingSubmitThread;
const isInterruptProjectionReady =
isThreadBound && runtimeInterrupts.length > 0;
const isCurrentThread = isThreadReady;
const hydratedStateThreadId = String(
eventTransport.latestState?.checkpoint?.thread_id ||
eventTransport.latestState?.checkpoint?.threadId ||
eventTransport.latestState?.values?.task?.task_id ||
eventTransport.latestState?.values?.task?.taskId ||
""
);
const nextMessages = mergeStreamMessageSnapshots(
hydratedMessages,
isThreadBound ? eventStreamMessages : []
const hydratedState =
isThreadReady &&
hydratedStateThreadId === currentSessionId
? eventTransport.latestState
: null;
const rawStreamMessages = isStreamProjectionReady ? stream.messages : [];
const rawStreamValues = isStreamProjectionReady ? stream.values || {} : {};
const hydratedMessages = Array.isArray(hydratedState?.values?.messages)
? hydratedState.values.messages
: [];
// 只有 hydrate 真正拿到消息后,才能解除首次提交的 thread 投影保护。
// 仅有 task 快照不代表消息已完成落库,否则 task 先到时会让当前消息再次被隐藏。
const hasHydratedSessionContent = hydratedMessages.length > 0;
useEffect(() => {
if (!isThreadReady || !isPendingSubmitThread || !hasHydratedSessionContent) return;
pendingSubmitThreadIdRef.current = "";
}, [
hasHydratedSessionContent,
isPendingSubmitThread,
isThreadReady,
]);
const hydratedTailIds = hydratedMessages
.slice(-3)
.map((item) => String(item?.id || "").trim())
.filter(Boolean);
const rawStreamMessageIds = new Set(
rawStreamMessages
.map((item) => String(item?.id || "").trim())
.filter(Boolean)
);
const nextToolCalls = isThreadBound && stream.toolCalls.length
? stream.toolCalls
: cachedSnapshot.toolCalls;
const nextValues = isThreadBound
? { ...cachedSnapshot.values, ...currentValues }
: cachedSnapshot.values;
if (isThreadBound && (nextMessages.length || nextToolCalls.length || Object.keys(nextValues).length)) {
streamSnapshotCacheRef.current.set(currentSessionId, {
messages: nextMessages,
toolCalls: nextToolCalls,
values: {
...nextValues,
...(nextMessages.length ? { messages: nextMessages } : {}),
},
});
const hydratedTaskStatus = String(
hydratedState?.values?.status ||
hydratedState?.values?.session_status ||
hydratedState?.values?.sessionStatus ||
hydratedState?.values?.task?.session_status ||
hydratedState?.values?.task?.sessionStatus ||
hydratedState?.values?.task?.status ||
""
).toLowerCase();
const rawTaskStatus = String(
rawStreamValues.status ||
rawStreamValues.session_status ||
rawStreamValues.sessionStatus ||
rawStreamValues.task?.session_status ||
rawStreamValues.task?.sessionStatus ||
rawStreamValues.task?.status ||
""
).toLowerCase();
const hydratedCheckpointDeliveryCount = Array.isArray(
hydratedState?.values?.checkpoint_deliveries
)
? hydratedState.values.checkpoint_deliveries.length
: 0;
const rawCheckpointDeliveryCount = Array.isArray(rawStreamValues.checkpoint_deliveries)
? rawStreamValues.checkpoint_deliveries.length
: 0;
const shouldPreferHydratedRuntimeState =
Boolean(hydratedState) &&
Boolean(hydratedTaskStatus || hydratedCheckpointDeliveryCount) &&
(!rawTaskStatus && !rawCheckpointDeliveryCount ||
hydratedTaskStatus !== rawTaskStatus ||
hydratedCheckpointDeliveryCount > rawCheckpointDeliveryCount);
//[state 是当前 thread 的最新快照,SSE 重连可能先重放更早的 values]
// 消息和 task/value 的权威来源不能共用一个切换条件。
// hydrate 返回空 messages 时,仍可能带有较新的 task 状态;此时只能更新
// task/value,不能让空快照覆盖事件流已经收到的消息。
const shouldPreferHydratedMessages =
hydratedMessages.length > 0 &&
(rawStreamMessages.length < hydratedMessages.length ||
(hydratedTailIds.length > 0 &&
!hydratedTailIds.every((messageId) => rawStreamMessageIds.has(messageId))));
const shouldPreferHydratedState =
shouldPreferHydratedRuntimeState || shouldPreferHydratedMessages;
const streamMessages = shouldPreferHydratedMessages
? hydratedMessages
: rawStreamMessages;
const streamToolCalls = isStreamProjectionReady ? stream.toolCalls : [];
const streamValues = shouldPreferHydratedState
? hydratedState?.values || {}
: rawStreamValues;
const streamSessionStatus = String(
streamValues.status ||
streamValues.session_status ||
streamValues.sessionStatus ||
streamValues.task?.session_status ||
streamValues.task?.sessionStatus ||
""
).toLowerCase();
const streamTask = streamValues?.task
? {
...streamValues.task,
...(streamSessionStatus
? { session_status: streamSessionStatus }
: {}),
}
const isCurrentThread = isThreadBound && !stream.isThreadLoading;
const streamMessages = isThreadBound ? nextMessages : [];
const streamToolCalls = isThreadBound ? nextToolCalls : [];
const streamValues = isThreadBound ? nextValues : {};
: null;
const persistedTaskStatus = String(
streamSessionStatus || streamTask?.status || ""
).toLowerCase();
const isPersistedTaskActive = ACTIVE_TASK_STATUSES.has(persistedTaskStatus);
const hasPersistedNextNodes =
Array.isArray(hydratedState?.next) && hydratedState.next.length > 0;
const hydratedInterrupts = [
...(Array.isArray(hydratedState?.interrupts)
? hydratedState.interrupts
: []),
...(Array.isArray(hydratedState?.values?.__interrupt__)
? hydratedState.values.__interrupt__.map((item) => ({
...item,
id: item?.id || item?.interruptId,
value: item?.value ?? item?.payload,
namespace: Array.isArray(item?.namespace) ? item.namespace : [],
}))
: []),
];
const hasPersistedInterrupts = hydratedInterrupts.length > 0;
const persistedInterrupts =
hydratedState
? hydratedInterrupts
: isStreamProjectionReady
? stream.interrupts
: [];
const streamInterrupts = [
...(Array.isArray(persistedInterrupts) ? persistedInterrupts : []),
...runtimeInterrupts,
];
const shouldResetStaleWaitingPhase =
Boolean(hydratedState) &&
!hasPersistedInterrupts &&
["waiting_interrupt", "awaiting_interrupt"].includes(runPhase);
const isPersistedThreadActive =
isPersistedTaskActive || hasPersistedNextNodes || hasPersistedInterrupts;
const isPersistedExecutionActive =
isPersistedThreadActive &&
![
"suspended",
"waiting_user",
"awaiting_interrupt",
"waiting_interrupt",
].includes(persistedTaskStatus) &&
!hasPersistedInterrupts;
const hasRuntimeExecutionSignal =
localRunActiveRef.current ||
isLocalRunActive ||
isBusinessRunning ||
["running", "resuming"].includes(runPhase);
const isPersistedSuspended =
persistedTaskStatus === "suspended" &&
!hasPersistedInterrupts &&
!hasRuntimeExecutionSignal;
const isTerminalRunPhase = [
"completed",
"aborted",
"failed",
"cancelled",
"canceled",
].includes(runPhase);
const effectiveRunPhase =
isPersistedSuspended
? "idle"
: shouldResetStaleWaitingPhase
? "running"
: isPersistedExecutionActive && (runPhase === "idle" || isTerminalRunPhase)
? "running"
: runPhase;
const effectiveIsLocalRunActive = isPersistedSuspended ? false : isLocalRunActive;
const effectiveIsBusinessRunning = isPersistedSuspended ? false : isBusinessRunning;
const visibleInterrupts = useMemo(() => {
if (!isThreadBound) return [];
if (!isStreamProjectionReady && !isInterruptProjectionReady) return [];
const merged = new Map();
const appendInterrupt = (item, fallback = {}) => {
@ -977,35 +1130,61 @@ const useSuperAgentStream = ({
});
};
stream.interrupts.forEach((item) => appendInterrupt(item));
runtimeInterrupts.forEach((item) => appendInterrupt(item));
const threadInterrupts = stream.getThread?.()?.interrupts || [];
threadInterrupts.forEach((item) => appendInterrupt(item));
streamInterrupts.forEach((item) => appendInterrupt(item));
return [...merged.values()].filter(
(item) =>
!dismissedInterruptIds.has(String(item.id || "")) &&
!isTerminalInterrupt(item)
);
}, [dismissedInterruptIds, isCurrentThread, isThreadBound, runtimeInterrupts, stream]);
const isRunPhaseActive = ["running", "awaiting_interrupt", "resuming"].includes(runPhase);
}, [
dismissedInterruptIds,
isInterruptProjectionReady,
isStreamProjectionReady,
streamInterrupts,
]);
const isRunPhaseActive = ["running", "awaiting_interrupt", "resuming"].includes(
effectiveRunPhase
);
const effectiveIsLoading =
isPersistedSuspended
? isRespondingInterrupt
: stream.isLoading ||
isRespondingInterrupt ||
isBusinessRunning ||
isRunPhaseActive ||
isPersistedExecutionActive;
return {
messages: streamMessages,
toolCalls: streamToolCalls,
interrupt: visibleInterrupts[0] || null,
interrupts: visibleInterrupts,
values: streamValues,
checkpointDeliveries: isCurrentThread ? checkpointDeliveries : [],
task: streamValues?.task || null,
isLoading: stream.isLoading || isRespondingInterrupt || isBusinessRunning || isRunPhaseActive,
runPhase,
currentInterruptId,
isWaitingForInterrupt: runPhase === "waiting_interrupt" || visibleInterrupts.length > 0,
isAwaitingInterrupt: runPhase === "awaiting_interrupt",
isBusinessRunning,
checkpointDeliveries: isStreamProjectionReady ? checkpointDeliveries : [],
task: streamTask,
sessionStatus: streamSessionStatus,
isLoading: effectiveIsLoading,
isLocalRunActive: effectiveIsLocalRunActive,
runPhase: effectiveRunPhase,
isPersistedThreadActive,
currentInterruptId:
hydratedState &&
hydratedInterrupts.length === 0 &&
runtimeInterrupts.length === 0
? ""
: currentInterruptId,
isWaitingForInterrupt:
visibleInterrupts.length > 0 ||
(effectiveRunPhase === "waiting_interrupt" && !isPersistedThreadActive),
isAwaitingInterrupt: effectiveRunPhase === "awaiting_interrupt",
isBusinessRunning: effectiveIsBusinessRunning,
isThreadLoading: stream.isThreadLoading,
isThreadReady,
isThreadProjectionReady:
isStreamProjectionReady || isInterruptProjectionReady,
error: isCurrentThread && stream.error ? normalizeStreamError(stream.error) : null,
threadId: stream.threadId,
threadId: projectedThreadId,
submitMessage,
respondInterrupt,
respondAllInterrupts: stream.respondAll,

2
web/client/src/sections/superAgent/router.js

@ -18,7 +18,7 @@ export default {
type: "outer",
route: {
key: "superAgent",
path: "/superAgent",
path: "/aichat",
component: SuperAgent,
nav: false,
},

6
web/client/src/sections/superAgent/utils/streamMessage.js

@ -47,8 +47,12 @@ const normalizeMessageContent = (content) => {
const text = readTextContent(content);
const lines = text.split(/\r?\n/);
const fileUrls = lines.filter(isUploadedFileUrl);
const fileNames = fileUrls.map(getUploadedFileName).filter(Boolean);
const messageText = lines.filter((line) => !isUploadedFileUrl(line)).join("\n").trim();
return {
content: lines.filter((line) => !isUploadedFileUrl(line)).join("\n").trim(),
// URL 继续放入 files 供附件 tag 使用;正文文本只展示解码后的文件名,
// 避免把带 token 的上传地址直接展示给用户。
content: [...fileNames, messageText].filter(Boolean).join("\n"),
files: fileUrls.map((url) => ({
id: url,
name: getUploadedFileName(url),

3781
web/package-lock.json

File diff suppressed because it is too large
Loading…
Cancel
Save