You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1873 lines
66 KiB
1873 lines
66 KiB
/**
|
|
* 文件作用:superAgent 会话与对话流状态 hook
|
|
* 职责范围:
|
|
* 1. 管理普通对话、标书任务会话和左侧列表状态
|
|
* 2. 处理流式消息、工具事件、checkpoint 事件
|
|
* 3. 处理新建会话、文件上传、发送消息和历史会话切换
|
|
*
|
|
* 不负责:
|
|
* - 渲染对话 UI
|
|
* - 渲染右侧工作区
|
|
* - 管理登录弹窗 UI
|
|
*
|
|
* 维护说明:
|
|
* - 标书任务身份由后端 session 加本地 tender session 标记共同维护
|
|
*/
|
|
"use strict";
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { message } from "antd";
|
|
import {
|
|
cancelSuperAgentSession,
|
|
createSuperAgentSession,
|
|
deleteSuperAgentSession,
|
|
respondSuperAgentCheckpointStream,
|
|
sendSuperAgentChat,
|
|
uploadSuperAgentFile,
|
|
} from "../actions/api";
|
|
import { createResourceFileFromUpload } from "../actions/resourceLibrary";
|
|
import useSuperAgentSidebar from "./useSuperAgentSidebar";
|
|
import {
|
|
TASK_STAGE,
|
|
TASK_WELCOME_CONTENT,
|
|
TASK_WELCOME_TITLE,
|
|
TENDER_TASK_WELCOME_CONTENT,
|
|
TENDER_TASK_WELCOME_TITLE,
|
|
} from "../constants";
|
|
import {
|
|
clearPendingEmptySession,
|
|
clearActiveSessionId,
|
|
getSuperAgentToolArguments,
|
|
getSuperAgentToolCallId,
|
|
getSuperAgentToolName,
|
|
getSuperAgentToolResult,
|
|
isSuperAgentQuestionTool,
|
|
isSuperAgentRunLogTool,
|
|
markTenderSessionId,
|
|
markPendingEmptySession,
|
|
removeTenderSessionId,
|
|
resolveApiFolderId,
|
|
saveActiveSessionId,
|
|
} from "../utils/session";
|
|
import {
|
|
createSuperAgentTraceId,
|
|
summarizeSuperAgentPayload,
|
|
superAgentError,
|
|
superAgentLog,
|
|
superAgentWarn,
|
|
} from "../utils/debugLog";
|
|
import {
|
|
mergeCheckpointEventData,
|
|
} from "../utils/workspace";
|
|
import { normalizeCheckpoint } from "../utils/checkpoint";
|
|
import { isWorkflowBodyText } from "../utils/tenderWorkflow";
|
|
|
|
const getActionData = (res) => {
|
|
const payload = res?.payload ?? res;
|
|
return payload?.data ?? payload;
|
|
};
|
|
|
|
const ensureActionSuccess = (res, fallbackMessage) => {
|
|
const payload = res?.payload ?? res;
|
|
if (payload?.success === false) {
|
|
throw new Error(payload?.message || payload?.error || fallbackMessage);
|
|
}
|
|
return getActionData(res);
|
|
};
|
|
|
|
const buildSessionParams = (sessionId) => ({
|
|
sessionId,
|
|
":sessionId": sessionId,
|
|
});
|
|
|
|
const hasFilePathPrefix = (filePath = "") => {
|
|
return /^(https?:)?\/\//i.test(filePath) || /^(data|blob):/i.test(filePath);
|
|
};
|
|
|
|
const buildQiniuFilePath = (filePath = "") => {
|
|
const cleanFilePath = String(filePath || "").trim();
|
|
if (!cleanFilePath || hasFilePathPrefix(cleanFilePath)) return cleanFilePath;
|
|
|
|
const qiniuDomain = window?.env?.FS_QINIU_DOMAIN || "";
|
|
const cleanDomain = String(qiniuDomain || "").replace(/\/+$/, "");
|
|
if (!cleanDomain) return cleanFilePath;
|
|
|
|
return `${cleanDomain}/${cleanFilePath.replace(/^\/+/, "")}`;
|
|
};
|
|
|
|
//[文件上下文] 上传文件只向智能体传递访问地址,不附加固定业务文案。
|
|
const buildUploadedFileReference = (fileList = []) => {
|
|
return fileList
|
|
.map((file) => {
|
|
const fileName = file?.name || file?.originalFilename || "未命名文件";
|
|
const rawFilePath = file?.url || file?.path || file?.objectKey || file?.localPath || "";
|
|
const filePath = buildQiniuFilePath(rawFilePath);
|
|
if (!filePath) return "";
|
|
return `(文件名:${fileName},文件路径:${filePath})`;
|
|
})
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
};
|
|
|
|
const useSuperAgentChat = ({
|
|
currentUserId,
|
|
openAuthModal,
|
|
setWorkspaceOpen,
|
|
actions,
|
|
dispatch,
|
|
}) => {
|
|
const [routeType, setRouteType] = useState("chat");
|
|
const [inputValue, setInputValue] = useState("");
|
|
const {
|
|
sidebarTree,
|
|
taskList,
|
|
ordinaryChatList,
|
|
isLoadingSidebar,
|
|
collapsedFolderMap,
|
|
loadSidebar,
|
|
patchSessionInTree,
|
|
removeSessionFromTree,
|
|
toggleFolderCollapsed,
|
|
handleCreateFolder,
|
|
handleRenameFolder,
|
|
handleDeleteFolder,
|
|
handleRenameSession,
|
|
handleMoveSession,
|
|
} = useSuperAgentSidebar({ currentUserId, actions, dispatch });
|
|
const [activeTaskId, setActiveTaskId] = useState("");
|
|
const [taskStage, setTaskStage] = useState(TASK_STAGE.IDLE);
|
|
const [conversationMessages, setConversationMessages] = useState([]);
|
|
const [activeSessionId, setActiveSessionId] = useState("");
|
|
const isLoadingSessions = isLoadingSidebar;
|
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
const [uploadProgress, setUploadProgress] = useState(null);
|
|
const [isStreamingAnswer, setIsStreamingAnswer] = useState(false);
|
|
const [isCreatingChat, setIsCreatingChat] = useState(false);
|
|
const [isCreatingTask, setIsCreatingTask] = useState(false);
|
|
const [uploadedFileList, setUploadedFileList] = useState([]);
|
|
const [agentCategory, setAgentCategory] = useState("全部应用");
|
|
const [agentKeyword, setAgentKeyword] = useState("");
|
|
/** 任务大厅已选类型:null 表示尚未选择;tender 表示已进入标书引导 */
|
|
const [selectedTaskType, setSelectedTaskType] = useState(null);
|
|
const chatAbortControllerRef = useRef(null);
|
|
const streamingMessageKeyRef = useRef("");
|
|
const chatRunPromiseRef = useRef(Promise.resolve());
|
|
const chatRunActiveRef = useRef(false);
|
|
const creatingSessionRef = useRef(false);
|
|
const sidebarRefreshTimerRef = useRef(null);
|
|
const artifactProgressSessionRef = useRef("");
|
|
const taskProgressUpsertAtRef = useRef(0);
|
|
const taskProgressPendingRef = useRef(null);
|
|
const taskProgressTimerRef = useRef(null);
|
|
const activeSessionIdRef = useRef("");
|
|
|
|
useEffect(() => {
|
|
activeSessionIdRef.current = activeSessionId;
|
|
}, [activeSessionId]);
|
|
|
|
const activeTask = useMemo(
|
|
() => taskList.find((task) => task.id === activeTaskId) || null,
|
|
[activeTaskId, taskList]
|
|
);
|
|
const activeChat = useMemo(
|
|
() =>
|
|
ordinaryChatList.find((chat) => {
|
|
const chatId = chat.id || chat.chatId;
|
|
return chatId === activeSessionId;
|
|
}) || null,
|
|
[activeSessionId, ordinaryChatList]
|
|
);
|
|
|
|
const findSessionTypeById = (sessionId) => {
|
|
if (!sessionId) return "";
|
|
const matchedTask = taskList.find((task) => {
|
|
const taskId = task.id || task.chatId;
|
|
return taskId === sessionId;
|
|
});
|
|
if (matchedTask) return "task";
|
|
|
|
const matchedChat = ordinaryChatList.find((chat) => {
|
|
const chatId = chat.id || chat.chatId;
|
|
return chatId === sessionId;
|
|
});
|
|
if (matchedChat) return "conversation";
|
|
|
|
return "";
|
|
};
|
|
|
|
const terminalCheckpointStatuses = new Set([
|
|
"confirmed",
|
|
"edited",
|
|
"completed",
|
|
"done",
|
|
"success",
|
|
"cancelled",
|
|
"canceled",
|
|
]);
|
|
const isTaskRoute = routeType === "task";
|
|
const isAgentRoute = routeType === "agents";
|
|
const isResourceLibraryRoute = routeType === "assets";
|
|
const breadcrumbTitle = isAgentRoute
|
|
? "发现智能体"
|
|
: isResourceLibraryRoute
|
|
? "资源库"
|
|
: isTaskRoute
|
|
? activeTask?.title || "新任务"
|
|
: activeChat?.title || "新对话";
|
|
|
|
const updateTaskById = (taskId, patch) => {
|
|
if (!taskId) return;
|
|
patchSessionInTree(taskId, { ...patch, updatedAt: Date.now() });
|
|
};
|
|
|
|
const updateActiveTask = (patch) => {
|
|
if (!activeTaskId) return;
|
|
updateTaskById(activeTaskId, patch);
|
|
};
|
|
|
|
const isDefaultSessionTitle = (title) => {
|
|
const value = String(title || "").trim().toLowerCase();
|
|
return (
|
|
!value ||
|
|
value === "新对话" ||
|
|
value === "未命名任务" ||
|
|
value === "new session"
|
|
);
|
|
};
|
|
|
|
const buildInstantSessionTitle = (text) => {
|
|
const title = String(text || "").replace(/\s+/g, " ").trim();
|
|
if (!title) return "";
|
|
return title.length > 24 ? `${title.slice(0, 24)}...` : title;
|
|
};
|
|
|
|
const patchSessionTitleAfterUserMessage = ({
|
|
sessionId,
|
|
isTaskSession = false,
|
|
text = "",
|
|
}) => {
|
|
if (!sessionId) return;
|
|
const currentSessionList = isTaskSession ? taskList : ordinaryChatList;
|
|
const currentSession = currentSessionList.find((item) => {
|
|
const itemId = item.id || item.chatId;
|
|
return itemId === sessionId;
|
|
});
|
|
if (currentSession && !isDefaultSessionTitle(currentSession.title)) return;
|
|
|
|
const title = buildInstantSessionTitle(text);
|
|
if (!title) return;
|
|
|
|
patchSessionInTree(sessionId, {
|
|
title,
|
|
time: Date.now(),
|
|
updatedAt: Date.now(),
|
|
});
|
|
};
|
|
|
|
const addAssistantMessage = (messageData) => {
|
|
setConversationMessages((prev) => [
|
|
...prev,
|
|
{
|
|
key: `assistant_${Date.now()}_${prev.length}`,
|
|
role: "assistant",
|
|
...messageData,
|
|
},
|
|
]);
|
|
};
|
|
|
|
const upsertAssistantMessage = (messageData) => {
|
|
const messageKey = messageData?.key;
|
|
if (!messageKey) {
|
|
addAssistantMessage(messageData);
|
|
return;
|
|
}
|
|
|
|
setConversationMessages((prev) => {
|
|
const hasMessage = prev.some((item) => item.key === messageKey);
|
|
if (!hasMessage) {
|
|
return [
|
|
...prev,
|
|
{
|
|
role: "assistant",
|
|
...messageData,
|
|
},
|
|
];
|
|
}
|
|
|
|
const shouldSyncBodyCheckpoint =
|
|
messageData.type === "taskProgress" && messageData.task;
|
|
return prev.map((item) => {
|
|
if (item.key === messageKey) return { ...item, ...messageData };
|
|
if (!shouldSyncBodyCheckpoint || item.type !== "checkpoint") return item;
|
|
|
|
const checkpoint = item?.checkpoint?.checkpoint || item?.checkpoint || {};
|
|
const uiType = String(checkpoint.ui_type || checkpoint.uiType || "");
|
|
if (uiType !== "tree-markdown") return item;
|
|
|
|
const nextTask = {
|
|
...(item.task || {}),
|
|
...messageData.task,
|
|
};
|
|
return {
|
|
...item,
|
|
task: nextTask,
|
|
checkpoint: {
|
|
...(item.checkpoint || {}),
|
|
task: nextTask,
|
|
},
|
|
};
|
|
});
|
|
});
|
|
};
|
|
|
|
const addUserMessage = (content, fileList = [], messageSessionId = "") => {
|
|
clearPendingEmptySession(activeSessionId || activeTaskId);
|
|
setConversationMessages((prev) => [
|
|
...prev,
|
|
{
|
|
key: `user_${Date.now()}_${prev.length}`,
|
|
role: "user",
|
|
content,
|
|
files: fileList,
|
|
sessionId: messageSessionId || activeSessionId || activeTaskId,
|
|
},
|
|
]);
|
|
};
|
|
|
|
const updateConversationMessage = (messageKey, updater) => {
|
|
setConversationMessages((prev) =>
|
|
prev.map((item) => {
|
|
if (item.key !== messageKey) return item;
|
|
const patch = typeof updater === "function" ? updater(item) : updater;
|
|
return { ...item, ...patch };
|
|
})
|
|
);
|
|
};
|
|
|
|
const removeConversationMessage = (messageKey) => {
|
|
if (!messageKey) return;
|
|
setConversationMessages((prev) => prev.filter((item) => item.key !== messageKey));
|
|
};
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (chatAbortControllerRef.current) {
|
|
chatAbortControllerRef.current.abort();
|
|
}
|
|
if (sidebarRefreshTimerRef.current) {
|
|
window.clearTimeout(sidebarRefreshTimerRef.current);
|
|
}
|
|
if (taskProgressTimerRef.current) {
|
|
window.clearTimeout(taskProgressTimerRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const removeSidebarSessionLocally = (sessionId, itemType) => {
|
|
if (!sessionId) return;
|
|
clearPendingEmptySession(sessionId);
|
|
if (itemType === "task") {
|
|
removeTenderSessionId(sessionId);
|
|
}
|
|
removeSessionFromTree(sessionId);
|
|
};
|
|
|
|
const clearActiveSidebarSession = (sessionId, shouldForce = false) => {
|
|
if (!shouldForce && activeSessionId !== sessionId) return;
|
|
clearActiveSessionId(sessionId);
|
|
setActiveSessionId("");
|
|
setActiveTaskId("");
|
|
setTaskStage(TASK_STAGE.IDLE);
|
|
setConversationMessages([]);
|
|
setUploadedFileList([]);
|
|
setRouteType("chat");
|
|
setWorkspaceOpen(false);
|
|
};
|
|
|
|
const createAndActivateSession = async ({
|
|
isTaskSession = false,
|
|
sessionKind,
|
|
folderId = null,
|
|
title = "",
|
|
shouldResetMessages = false,
|
|
} = {}) => {
|
|
if (!currentUserId) {
|
|
openAuthModal("login");
|
|
throw new Error("请先登录后再使用飞小尚超级智能体");
|
|
}
|
|
|
|
const sessionType = sessionKind || (isTaskSession ? "task" : "conversation");
|
|
const requestBody = { type: sessionType, reuse_empty: true };
|
|
const apiFolderId = resolveApiFolderId(folderId);
|
|
if (apiFolderId) requestBody.folder_id = apiFolderId;
|
|
if (title) requestBody.title = title;
|
|
|
|
const session = await dispatch(createSuperAgentSession({
|
|
query: { userId: currentUserId },
|
|
body: requestBody,
|
|
})).then((res) => ensureActionSuccess(res, "创建超级智能体会话失败"));
|
|
const sessionData = session?.data || session;
|
|
const nextSessionId = sessionData?.session_id || sessionData?.id;
|
|
if (!nextSessionId) throw new Error("创建会话失败,后端未返回 session_id");
|
|
|
|
const returnedType =
|
|
sessionData?.session_kind ||
|
|
(sessionData?.type === "task" || sessionData?.type === "conversation"
|
|
? sessionData.type
|
|
: sessionType);
|
|
|
|
const isReused = Boolean(sessionData?.reused);
|
|
superAgentLog("chat.session", "create_or_reuse.done", {
|
|
sessionId: nextSessionId,
|
|
reused: isReused,
|
|
sessionType: returnedType,
|
|
folderId: apiFolderId,
|
|
});
|
|
|
|
if (isReused) {
|
|
const tipText = returnedType === "task"
|
|
? "已有未开始的任务,已为您定位"
|
|
: "已有未开始的对话,已为您定位";
|
|
message.info(tipText);
|
|
}
|
|
|
|
setActiveSessionId(nextSessionId);
|
|
saveActiveSessionId(nextSessionId);
|
|
setWorkspaceOpen(false);
|
|
setUploadedFileList([]);
|
|
markPendingEmptySession(nextSessionId, returnedType, apiFolderId);
|
|
if (shouldResetMessages) setConversationMessages([]);
|
|
|
|
if (returnedType === "task") {
|
|
markTenderSessionId(nextSessionId);
|
|
setActiveTaskId(nextSessionId);
|
|
} else {
|
|
removeTenderSessionId(nextSessionId);
|
|
setActiveTaskId("");
|
|
}
|
|
|
|
await loadSidebar({ silent: true });
|
|
return nextSessionId;
|
|
};
|
|
|
|
const getCurrentSessionType = () => {
|
|
if (!activeSessionId) return "";
|
|
const sidebarSessionType = findSessionTypeById(activeSessionId);
|
|
if (sidebarSessionType) return sidebarSessionType;
|
|
if (activeTaskId && activeSessionId === activeTaskId) return "task";
|
|
return "conversation";
|
|
};
|
|
|
|
const ensureActiveSession = async (options = {}) => {
|
|
const shouldCreateTaskSession =
|
|
options.isTaskSession === undefined ? isTaskRoute : options.isTaskSession;
|
|
const expectedSessionType = shouldCreateTaskSession ? "task" : "conversation";
|
|
const currentSessionType = getCurrentSessionType();
|
|
|
|
if (activeSessionId && currentSessionType === expectedSessionType) {
|
|
return activeSessionId;
|
|
}
|
|
|
|
return createAndActivateSession({ isTaskSession: shouldCreateTaskSession });
|
|
};
|
|
|
|
const appendStreamingText = (messageKey, text) => {
|
|
if (!text) return;
|
|
updateConversationMessage(messageKey, (item) => ({
|
|
title: item.title || "飞小尚",
|
|
content: `${item.content || ""}${text}`,
|
|
}));
|
|
};
|
|
|
|
//[合并实时日志事件,使用事件 id 去重并累计询问、命令次数]
|
|
const updateRunLogFromEvent = (messageKey, eventName, eventData = {}) => {
|
|
updateConversationMessage(messageKey, (item) => {
|
|
const currentRunLog = item.runLog || {};
|
|
const startedAt = currentRunLog.startedAt || Date.now();
|
|
const toolName = getSuperAgentToolName(eventData) || "工具";
|
|
const eventId = getSuperAgentToolCallId(eventData);
|
|
const handledEventIds = Array.isArray(currentRunLog.handledEventIds)
|
|
? currentRunLog.handledEventIds
|
|
: [];
|
|
const currentEntries = Array.isArray(currentRunLog.entries)
|
|
? currentRunLog.entries
|
|
: [];
|
|
const shouldCountTool = eventName === "tool_start" && isSuperAgentRunLogTool(toolName);
|
|
const fallbackRunningEntry = currentEntries.find(
|
|
(entry) => entry.name === toolName && entry.status === "running"
|
|
);
|
|
const countKey =
|
|
eventId ||
|
|
fallbackRunningEntry?.id ||
|
|
`${toolName}_${handledEventIds.length + 1}`;
|
|
const matchedIndex = currentEntries.findIndex((entry) => {
|
|
if (eventId) return entry.id === eventId;
|
|
return entry.id === countKey || (entry.name === toolName && entry.status === "running");
|
|
});
|
|
const isResultEvent = eventName === "tool_result" || eventName === "tool_end";
|
|
const isNewTool =
|
|
shouldCountTool &&
|
|
matchedIndex < 0 &&
|
|
!handledEventIds.includes(countKey);
|
|
const isQuestion = isNewTool && isSuperAgentQuestionTool(toolName);
|
|
let nextEntries = currentEntries;
|
|
|
|
if (isNewTool) {
|
|
nextEntries = [
|
|
...currentEntries,
|
|
{
|
|
id: countKey,
|
|
name: toolName,
|
|
kind: isQuestion ? "question" : "command",
|
|
status: "running",
|
|
arguments: getSuperAgentToolArguments(eventData),
|
|
result: "",
|
|
},
|
|
];
|
|
}
|
|
|
|
if (isResultEvent) {
|
|
if (matchedIndex >= 0) {
|
|
nextEntries = nextEntries.map((entry, index) =>
|
|
index === matchedIndex
|
|
? {
|
|
...entry,
|
|
status: "done",
|
|
result: getSuperAgentToolResult(eventData),
|
|
}
|
|
: entry
|
|
);
|
|
} else if (isSuperAgentRunLogTool(toolName)) {
|
|
nextEntries = [
|
|
...nextEntries,
|
|
{
|
|
id: countKey,
|
|
name: toolName,
|
|
kind: isSuperAgentQuestionTool(toolName) ? "question" : "command",
|
|
status: "done",
|
|
arguments: getSuperAgentToolArguments(eventData),
|
|
result: getSuperAgentToolResult(eventData),
|
|
},
|
|
];
|
|
}
|
|
}
|
|
|
|
return {
|
|
runLog: {
|
|
...currentRunLog,
|
|
startedAt,
|
|
status: "running",
|
|
questionCount: Number(currentRunLog.questionCount || 0) + (isQuestion ? 1 : 0),
|
|
commandCount:
|
|
Number(currentRunLog.commandCount || 0) + (isNewTool && !isQuestion ? 1 : 0),
|
|
elapsedMs: Date.now() - startedAt,
|
|
handledEventIds: isNewTool
|
|
? [...handledEventIds, countKey]
|
|
: handledEventIds,
|
|
entries: nextEntries,
|
|
},
|
|
};
|
|
});
|
|
};
|
|
|
|
//[结束当前运行日志并记录本轮处理耗时]
|
|
const finishRunLog = (runLog = null) => {
|
|
if (!runLog) return null;
|
|
const startedAt = Number(runLog.startedAt || 0);
|
|
return {
|
|
...runLog,
|
|
status: "done",
|
|
elapsedMs: startedAt ? Date.now() - startedAt : Number(runLog.elapsedMs || 0),
|
|
entries: (runLog.entries || []).map((entry) => ({
|
|
...entry,
|
|
status: "done",
|
|
})),
|
|
};
|
|
};
|
|
|
|
const clearWaitingNoticeMessages = () => {
|
|
setConversationMessages((prev) =>
|
|
prev.filter((item) => !item.isWaitingNotice)
|
|
);
|
|
};
|
|
|
|
const normalizeProgressEventData = (eventData = {}) => {
|
|
const task = eventData.task || eventData.data?.task || eventData;
|
|
return {
|
|
task_type: task?.task_type || task?.taskType || "",
|
|
status: task?.status || "",
|
|
stage: task?.stage || eventData.stage || "",
|
|
progress: Number(task?.progress ?? eventData.progress ?? 0),
|
|
section_id: task?.section_id || task?.sectionId || eventData.section_id || eventData.sectionId || "",
|
|
section_title: task?.section_title || task?.sectionTitle || eventData.section_title || eventData.sectionTitle || "",
|
|
done: Number(task?.done ?? eventData.done ?? 0),
|
|
total: Number(task?.total ?? eventData.total ?? 0),
|
|
};
|
|
};
|
|
|
|
const scheduleSidebarRefresh = useCallback(() => {
|
|
if (sidebarRefreshTimerRef.current) {
|
|
window.clearTimeout(sidebarRefreshTimerRef.current);
|
|
}
|
|
//[done 后延迟刷新侧栏,避免与对话收尾同一帧抢布局]
|
|
sidebarRefreshTimerRef.current = window.setTimeout(() => {
|
|
sidebarRefreshTimerRef.current = null;
|
|
loadSidebar({ silent: true });
|
|
}, 1800);
|
|
}, [loadSidebar]);
|
|
|
|
const upsertTaskProgressMessage = (sessionId, progressData, options = {}) => {
|
|
if (!sessionId) return;
|
|
const progress = Math.max(0, Math.min(100, Number(progressData.progress || 0)));
|
|
const stageText = progressData.stage || "任务处理中";
|
|
const statusText = progressData.status || "running";
|
|
const normalizedTask = {
|
|
...progressData,
|
|
stage: stageText,
|
|
progress,
|
|
status: statusText,
|
|
};
|
|
|
|
updateTaskById(sessionId, {
|
|
stage: stageText,
|
|
progress,
|
|
status: statusText,
|
|
taskType: progressData.task_type || "",
|
|
});
|
|
|
|
if (options.suppressMessage) return;
|
|
|
|
const flushProgressMessage = (taskPayload) => {
|
|
upsertAssistantMessage({
|
|
key: `session_progress_${sessionId}`,
|
|
role: "assistant",
|
|
type: "taskProgress",
|
|
title: "正文生成",
|
|
task: taskPayload,
|
|
sessionId,
|
|
});
|
|
};
|
|
|
|
const isTerminal =
|
|
progress >= 100 ||
|
|
["reviewing", "writed", "completed", "failed", "cancelled", "error"].includes(
|
|
String(statusText || "").toLowerCase()
|
|
);
|
|
const now = Date.now();
|
|
if (isTerminal || now - taskProgressUpsertAtRef.current >= 800) {
|
|
taskProgressUpsertAtRef.current = now;
|
|
taskProgressPendingRef.current = null;
|
|
flushProgressMessage(normalizedTask);
|
|
return;
|
|
}
|
|
|
|
//[进度卡节流:合并短时间内的多次 progress 事件]
|
|
taskProgressPendingRef.current = normalizedTask;
|
|
if (!taskProgressTimerRef.current) {
|
|
taskProgressTimerRef.current = window.setTimeout(() => {
|
|
taskProgressTimerRef.current = null;
|
|
const pending = taskProgressPendingRef.current;
|
|
if (!pending) return;
|
|
taskProgressPendingRef.current = null;
|
|
taskProgressUpsertAtRef.current = Date.now();
|
|
flushProgressMessage(pending);
|
|
}, 800);
|
|
}
|
|
};
|
|
|
|
const extractEventText = (data) => {
|
|
if (!data || typeof data !== "object") return "";
|
|
return (
|
|
data.content ||
|
|
data.text ||
|
|
data.message ||
|
|
data.delta ||
|
|
data.chunk ||
|
|
data.token ||
|
|
data?.choices?.[0]?.delta?.content ||
|
|
data?.choices?.[0]?.text ||
|
|
data?.data?.content ||
|
|
data?.data?.text ||
|
|
""
|
|
);
|
|
};
|
|
|
|
const handleSuperAgentEvent = (eventPayload, assistantMessageKey, options = {}) => {
|
|
const eventName = eventPayload?.event || "message";
|
|
const eventData = eventPayload?.data || {};
|
|
const eventText = extractEventText(eventData);
|
|
superAgentLog("chat.event", "handle", {
|
|
sessionId: options.sessionId,
|
|
assistantMessageKey,
|
|
eventName,
|
|
checkpointId:
|
|
eventData?.checkpoint_id ||
|
|
eventData?.checkpointId ||
|
|
eventData?.checkpoint?.checkpoint_id ||
|
|
eventData?.checkpoint?.checkpointId,
|
|
checkpointType:
|
|
eventData?.checkpoint_type ||
|
|
eventData?.checkpoint?.checkpoint_type ||
|
|
eventData?.checkpoint?.type,
|
|
progress: eventData?.progress ?? eventData?.task?.progress,
|
|
status: eventData?.status ?? eventData?.task?.status,
|
|
textLen: eventText?.length || 0,
|
|
data: summarizeSuperAgentPayload(eventData),
|
|
});
|
|
if (["text", "token", "content", "delta", "chunk"].includes(eventName)) {
|
|
if (eventText) clearWaitingNoticeMessages();
|
|
appendStreamingText(assistantMessageKey, eventText);
|
|
return;
|
|
}
|
|
|
|
if (["thinking", "reasoning", "thought", "tool_start", "tool_result", "tool_end"].includes(eventName)) {
|
|
updateRunLogFromEvent(assistantMessageKey, eventName, eventData);
|
|
return;
|
|
}
|
|
|
|
if (eventName === "progress") {
|
|
const progressData = normalizeProgressEventData(eventData);
|
|
const sectionTitle = progressData.section_title || progressData.stage || "正文";
|
|
const doneText =
|
|
progressData.done && progressData.total
|
|
? `(${progressData.done}/${progressData.total})`
|
|
: "";
|
|
clearWaitingNoticeMessages();
|
|
const progress = Math.max(0, Math.min(100, Number(progressData.progress || 0)));
|
|
const statusText = progressData.status || "running";
|
|
const normalizedStatus = String(statusText).toLowerCase();
|
|
const isArtifactProgress = progressData.task_type === "artifact_build";
|
|
const isArtifactCompleted =
|
|
isArtifactProgress &&
|
|
!["failed", "cancelled", "error"].includes(normalizedStatus) &&
|
|
(progress >= 100 ||
|
|
["reviewing", "writed", "completed", "done", "success"].includes(
|
|
normalizedStatus
|
|
));
|
|
if (isArtifactProgress) {
|
|
artifactProgressSessionRef.current = options.sessionId;
|
|
}
|
|
if (isArtifactCompleted) {
|
|
}
|
|
updateConversationMessage(assistantMessageKey, (item) => ({
|
|
title: progress >= 100 ? "正文生成完成" : `正在生成:${sectionTitle}`,
|
|
loadingText:
|
|
progress >= 100
|
|
? `正文已生成完成${doneText},可点击按钮打开正文工作区`
|
|
: `正文生成中${doneText},可点击按钮打开正文工作区`,
|
|
task: {
|
|
...progressData,
|
|
progress,
|
|
status: statusText,
|
|
},
|
|
}));
|
|
//[正文进度使用独立进度卡session_progress_渲染,流式消息不转为taskProgress避免重复卡片]
|
|
upsertTaskProgressMessage(options.sessionId, progressData);
|
|
|
|
return;
|
|
}
|
|
|
|
if (eventName === "checkpoint") {
|
|
const checkpointData =
|
|
eventData.checkpoint || eventData.data?.checkpoint || eventData;
|
|
const taskData =
|
|
eventData.task || eventData.data?.task || checkpointData.task || null;
|
|
clearWaitingNoticeMessages();
|
|
const checkpointId =
|
|
checkpointData.checkpoint_id || checkpointData.checkpointId || checkpointData.id || "";
|
|
setConversationMessages((prev) => {
|
|
const existingCheckpoint = prev.find((item) => {
|
|
const itemCheckpointId =
|
|
item?.checkpoint?.checkpoint_id ||
|
|
item?.checkpoint?.checkpointId ||
|
|
item?.checkpoint?.id ||
|
|
"";
|
|
return checkpointId && itemCheckpointId === checkpointId;
|
|
});
|
|
const buildCheckpointCard = (item, mergedCheckpointData) => {
|
|
const normalizedCheckpoint = normalizeCheckpoint(mergedCheckpointData);
|
|
const localStatus = String(item?.checkpointStatus || "").toLowerCase();
|
|
const keepsLocalStatus =
|
|
localStatus === "running" ||
|
|
localStatus === "submitting" ||
|
|
terminalCheckpointStatuses.has(localStatus);
|
|
return {
|
|
type: "checkpoint",
|
|
title: normalizedCheckpoint.title,
|
|
content: normalizedCheckpoint.description,
|
|
checkpoint: normalizedCheckpoint,
|
|
task: taskData,
|
|
sessionId: options.sessionId,
|
|
checkpointStatus: keepsLocalStatus
|
|
? localStatus
|
|
: normalizedCheckpoint.checkpoint_status,
|
|
loadingText: "",
|
|
};
|
|
};
|
|
|
|
const canRemoveStreamingMessage = (item = {}) => {
|
|
if (!item || item.key !== assistantMessageKey) return false;
|
|
if (String(item.content || "").trim()) return false;
|
|
if (item.runLog) return false;
|
|
return true;
|
|
};
|
|
const buildCheckpointKey = (fallbackItem = {}) => {
|
|
if (fallbackItem.key && fallbackItem.type === "checkpoint") return fallbackItem.key;
|
|
if (checkpointId && options.sessionId) {
|
|
return `state_checkpoint_${options.sessionId}_${checkpointId}`;
|
|
}
|
|
return `checkpoint_${assistantMessageKey}`;
|
|
};
|
|
|
|
if (existingCheckpoint && existingCheckpoint.key !== assistantMessageKey) {
|
|
const mergedCheckpointData = mergeCheckpointEventData(
|
|
existingCheckpoint,
|
|
checkpointData
|
|
);
|
|
const checkpointCard = buildCheckpointCard(
|
|
existingCheckpoint,
|
|
mergedCheckpointData
|
|
);
|
|
return prev
|
|
.filter((item) => !canRemoveStreamingMessage(item))
|
|
.map((item) =>
|
|
item.key === existingCheckpoint.key
|
|
? { ...item, ...checkpointCard }
|
|
: item
|
|
);
|
|
}
|
|
|
|
const currentItem = prev.find((item) => item.key === assistantMessageKey) || {};
|
|
const mergedCheckpointData = mergeCheckpointEventData(currentItem, checkpointData);
|
|
const checkpointCard = buildCheckpointCard(currentItem, mergedCheckpointData);
|
|
const shouldReplaceCurrentItem = canRemoveStreamingMessage(currentItem);
|
|
if (shouldReplaceCurrentItem) {
|
|
return prev.map((item) =>
|
|
item.key === assistantMessageKey ? { ...item, ...checkpointCard } : item
|
|
);
|
|
}
|
|
|
|
const nextCheckpointCard = {
|
|
...checkpointCard,
|
|
key: buildCheckpointKey(currentItem),
|
|
role: "assistant",
|
|
};
|
|
const hasCheckpointCard = prev.some((item) => item.key === nextCheckpointCard.key);
|
|
if (hasCheckpointCard) {
|
|
return prev.map((item) =>
|
|
item.key === nextCheckpointCard.key
|
|
? { ...item, ...nextCheckpointCard }
|
|
: item
|
|
);
|
|
}
|
|
|
|
return [...prev, nextCheckpointCard];
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (eventName === "checkpoint_done") {
|
|
const checkpointId = eventData.checkpoint_id || eventData.checkpointId || eventData.id;
|
|
setConversationMessages((prev) =>
|
|
prev.map((item) => {
|
|
const itemCheckpointId =
|
|
item?.checkpoint?.checkpoint_id ||
|
|
item?.checkpoint?.checkpointId ||
|
|
item?.checkpoint?.id;
|
|
if (!checkpointId || itemCheckpointId !== checkpointId) return item;
|
|
const localStatus = String(item.checkpointStatus || "").toLowerCase();
|
|
if (terminalCheckpointStatuses.has(localStatus)) return item;
|
|
return {
|
|
...item,
|
|
checkpointStatus: "running",
|
|
};
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (eventName === "done") {
|
|
if (artifactProgressSessionRef.current === options.sessionId) {
|
|
artifactProgressSessionRef.current = "";
|
|
}
|
|
clearWaitingNoticeMessages();
|
|
updateConversationMessage(assistantMessageKey, (item) => {
|
|
//[旁路答疑或正常流式:已有正文则保留;checkpoint 卡片本身不算失败]
|
|
if (item.type === "checkpoint") {
|
|
return {
|
|
title: item.title || "需要确认",
|
|
loadingText: "",
|
|
runLog: finishRunLog(item.runLog),
|
|
};
|
|
}
|
|
const hasAnswerContent = Boolean(item.content || eventText);
|
|
if (!hasAnswerContent) {
|
|
if (options.allowEmptyDone) {
|
|
return {
|
|
title: "飞小尚",
|
|
content: "已确认,正在继续推进任务。",
|
|
runLog: finishRunLog(item.runLog),
|
|
};
|
|
}
|
|
return {
|
|
title: "处理失败",
|
|
content: "超级智能体未返回有效内容,请检查服务连接或稍后重试。",
|
|
runLog: finishRunLog(item.runLog),
|
|
};
|
|
}
|
|
const answerContent = item.content || eventText;
|
|
//[旁路答疑:若模型误吐标书流程文案,替换为简短提示,避免弹出流程卡]
|
|
if (
|
|
(options.sideQa || item.sideQa) &&
|
|
isWorkflowBodyText(answerContent)
|
|
) {
|
|
return {
|
|
title: "飞小尚",
|
|
content:
|
|
"当前仍在确认卡片旁路答疑阶段,我只能回答与当前任务/卡片相关的简单问题,不会重新展示标书生成流程。请继续提问,或点击上方确认卡片按钮推进任务。",
|
|
loadingText: "",
|
|
sideQa: true,
|
|
runLog: finishRunLog(item.runLog),
|
|
};
|
|
}
|
|
return {
|
|
title: item.title || "飞小尚",
|
|
content: answerContent,
|
|
loadingText: "",
|
|
sideQa: Boolean(options.sideQa || item.sideQa),
|
|
runLog: finishRunLog(item.runLog),
|
|
};
|
|
});
|
|
scheduleSidebarRefresh();
|
|
return;
|
|
}
|
|
|
|
if (eventName === "error") {
|
|
if (artifactProgressSessionRef.current === options.sessionId) {
|
|
artifactProgressSessionRef.current = "";
|
|
}
|
|
clearWaitingNoticeMessages();
|
|
updateConversationMessage(assistantMessageKey, (item) => ({
|
|
title: "处理失败",
|
|
content: eventText || eventData.error || "超级智能体处理失败,请稍后重试。",
|
|
runLog: finishRunLog(item.runLog),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (eventText) clearWaitingNoticeMessages();
|
|
if (eventText) appendStreamingText(assistantMessageKey, eventText);
|
|
};
|
|
|
|
const executeMessageToAgent = async (messageText, options = {}) => {
|
|
const content = String(messageText || "").trim();
|
|
if (!content) return;
|
|
const traceId = createSuperAgentTraceId("send");
|
|
|
|
let nextAbortController = null;
|
|
try {
|
|
const sessionId =
|
|
options.sessionId ||
|
|
(await ensureActiveSession({ isTaskSession: options.isTaskSession }));
|
|
superAgentLog("chat.send", "start", {
|
|
traceId,
|
|
sessionId,
|
|
currentUserId,
|
|
isStreamingAnswer,
|
|
content,
|
|
options,
|
|
});
|
|
if (!options.skipUserMessage) {
|
|
clearPendingEmptySession(sessionId);
|
|
const hasUserDisplayText = Object.prototype.hasOwnProperty.call(
|
|
options,
|
|
"userDisplayText"
|
|
);
|
|
const userDisplayText = hasUserDisplayText ? options.userDisplayText : content;
|
|
addUserMessage(userDisplayText, options.userFiles || [], sessionId);
|
|
patchSessionTitleAfterUserMessage({
|
|
sessionId,
|
|
isTaskSession: Boolean(options.isTaskSession),
|
|
text: userDisplayText || content,
|
|
});
|
|
}
|
|
|
|
if (chatAbortControllerRef.current) chatAbortControllerRef.current.abort();
|
|
nextAbortController = new AbortController();
|
|
chatAbortControllerRef.current = nextAbortController;
|
|
setIsStreamingAnswer(true);
|
|
|
|
const runChatOnce = async ({
|
|
chatMessage,
|
|
assistantTitle,
|
|
loadingText,
|
|
allowEmptyDone = false,
|
|
}) => {
|
|
const assistantMessageKey = `assistant_stream_${Date.now()}_${Math.random()}`;
|
|
const runState = {
|
|
hasCheckpoint: false,
|
|
hasError: false,
|
|
};
|
|
const sideQa = Boolean(options.sideQa);
|
|
streamingMessageKeyRef.current = assistantMessageKey;
|
|
setConversationMessages((prev) => [
|
|
...prev,
|
|
{
|
|
key: assistantMessageKey,
|
|
role: "assistant",
|
|
type: "plain",
|
|
title: assistantTitle,
|
|
loadingText,
|
|
content: "",
|
|
sessionId,
|
|
sideQa,
|
|
},
|
|
]);
|
|
|
|
superAgentLog("chat.send", "run.start", {
|
|
traceId,
|
|
sessionId,
|
|
assistantMessageKey,
|
|
assistantTitle,
|
|
allowEmptyDone,
|
|
sideQa,
|
|
chatMessage,
|
|
});
|
|
await sendSuperAgentChat({
|
|
sessionId,
|
|
userId: currentUserId,
|
|
message: chatMessage,
|
|
displayContent: options.displayContent || chatMessage,
|
|
attachmentIds: options.attachmentIds || [],
|
|
signal: nextAbortController.signal,
|
|
onEvent: (eventPayload) => {
|
|
const eventName = eventPayload?.event || "message";
|
|
if (eventName === "checkpoint") {
|
|
runState.hasCheckpoint = true;
|
|
}
|
|
if (eventName === "error") {
|
|
runState.hasError = true;
|
|
}
|
|
handleSuperAgentEvent(eventPayload, assistantMessageKey, {
|
|
allowEmptyDone,
|
|
sideQa,
|
|
sessionId,
|
|
isTaskSession: options.isTaskSession,
|
|
restoreContentOnDone: options.restoreContentOnDone,
|
|
});
|
|
},
|
|
});
|
|
|
|
//[部分服务不会发送 done 事件;本轮进度仍由 useStream 业务事件维护]
|
|
if (artifactProgressSessionRef.current === sessionId) {
|
|
artifactProgressSessionRef.current = "";
|
|
}
|
|
|
|
superAgentLog("chat.send", "run.done", {
|
|
traceId,
|
|
sessionId,
|
|
assistantMessageKey,
|
|
runState,
|
|
});
|
|
return runState;
|
|
};
|
|
|
|
const firstRunState = await runChatOnce({
|
|
chatMessage: content,
|
|
assistantTitle: options.waitingTitle || "飞小尚",
|
|
loadingText: options.waitingText || "正在输出",
|
|
allowEmptyDone: options.autoContinueOnce,
|
|
});
|
|
|
|
const shouldAutoContinue =
|
|
options.autoContinueOnce &&
|
|
!firstRunState.hasCheckpoint &&
|
|
!firstRunState.hasError;
|
|
|
|
if (shouldAutoContinue) {
|
|
superAgentWarn("chat.send", "auto_continue", {
|
|
traceId,
|
|
sessionId,
|
|
autoContinueMessage: options.autoContinueMessage || "继续",
|
|
firstRunState,
|
|
});
|
|
await runChatOnce({
|
|
chatMessage: options.autoContinueMessage || "继续",
|
|
assistantTitle: "飞小尚",
|
|
loadingText: "正在输出",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
superAgentError("chat.send", "error", {
|
|
traceId,
|
|
error,
|
|
messageText: content,
|
|
options,
|
|
});
|
|
if (error?.name !== "AbortError") {
|
|
message.error(error?.message || "超级智能体请求失败");
|
|
}
|
|
if (options.throwOnError) {
|
|
throw error;
|
|
}
|
|
} finally {
|
|
if (chatAbortControllerRef.current !== nextAbortController) return;
|
|
superAgentLog("chat.send", "finish", {
|
|
traceId,
|
|
messageText: content,
|
|
options,
|
|
});
|
|
setIsStreamingAnswer(false);
|
|
chatAbortControllerRef.current = null;
|
|
streamingMessageKeyRef.current = "";
|
|
}
|
|
};
|
|
|
|
const sendMessageToAgent = (messageText, options = {}) => {
|
|
const content = String(messageText || "").trim();
|
|
if (!content) return Promise.resolve();
|
|
if (chatRunActiveRef.current && !options.allowWhileStreaming) {
|
|
superAgentWarn("chat.send", "skip.streaming", {
|
|
messageText: content,
|
|
options,
|
|
});
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const previousRun = chatRunPromiseRef.current.catch(() => undefined);
|
|
const nextRun = chatRunActiveRef.current
|
|
? previousRun.then(() => executeMessageToAgent(content, options))
|
|
: executeMessageToAgent(content, options);
|
|
chatRunActiveRef.current = true;
|
|
let trackedRun;
|
|
trackedRun = Promise.resolve(nextRun).finally(() => {
|
|
if (chatRunPromiseRef.current === trackedRun) {
|
|
chatRunActiveRef.current = false;
|
|
}
|
|
});
|
|
chatRunPromiseRef.current = trackedRun;
|
|
return trackedRun;
|
|
};
|
|
|
|
/**
|
|
* 功能:提交 checkpoint action 并直接消费 respond SSE
|
|
* 使用场景:确认、修改、重新生成等 checkpoint 操作
|
|
*
|
|
* 入参:
|
|
* - sessionId 当前任务会话 ID
|
|
* - respondBody checkpoint/respond 请求体
|
|
* - actionText 前端展示文案
|
|
*
|
|
* 返回:本次 SSE 是否收到事件、是否收到错误事件
|
|
*
|
|
* 注意:respond 接口本身会恢复 LangGraph 中断,不再额外调用 /api/chat。
|
|
*/
|
|
const respondCheckpointToAgent = async ({
|
|
sessionId,
|
|
respondBody,
|
|
actionText = {},
|
|
onFirstEvent,
|
|
}) => {
|
|
if (!sessionId) {
|
|
throw new Error("当前任务会话不存在,请重新选择任务后再操作");
|
|
}
|
|
|
|
const traceId = createSuperAgentTraceId("checkpoint_respond");
|
|
const assistantMessageKey = `assistant_checkpoint_${Date.now()}_${Math.random()}`;
|
|
const runState = {
|
|
hasEvent: false,
|
|
hasError: false,
|
|
errorMessage: "",
|
|
};
|
|
let nextAbortController = null;
|
|
|
|
try {
|
|
if (chatAbortControllerRef.current) chatAbortControllerRef.current.abort();
|
|
nextAbortController = new AbortController();
|
|
chatAbortControllerRef.current = nextAbortController;
|
|
streamingMessageKeyRef.current = assistantMessageKey;
|
|
setIsStreamingAnswer(true);
|
|
setConversationMessages((prev) => [
|
|
...prev,
|
|
{
|
|
key: assistantMessageKey,
|
|
role: "assistant",
|
|
type: "plain",
|
|
title: actionText.title || "飞小尚正在继续处理",
|
|
loadingText: actionText.loadingText || "正在请求 AI 继续推进任务,请稍候",
|
|
content: "",
|
|
sessionId,
|
|
},
|
|
]);
|
|
|
|
superAgentLog("chat.checkpoint", "respond.start", {
|
|
traceId,
|
|
sessionId,
|
|
respondBody,
|
|
});
|
|
|
|
await respondSuperAgentCheckpointStream({
|
|
body: respondBody,
|
|
userId: currentUserId,
|
|
sessionId,
|
|
signal: nextAbortController.signal,
|
|
onEvent: (eventPayload) => {
|
|
const eventName = eventPayload?.event || "message";
|
|
if (eventName === "error") {
|
|
runState.hasError = true;
|
|
runState.errorMessage =
|
|
eventPayload?.data?.message ||
|
|
eventPayload?.data?.error ||
|
|
eventPayload?.data?.content ||
|
|
"提交确认结果失败";
|
|
} else {
|
|
runState.hasEvent = true;
|
|
if (!runState.firstEventFired && onFirstEvent) {
|
|
runState.firstEventFired = true;
|
|
onFirstEvent();
|
|
}
|
|
}
|
|
handleSuperAgentEvent(eventPayload, assistantMessageKey, {
|
|
allowEmptyDone: true,
|
|
sessionId,
|
|
isTaskSession: true,
|
|
});
|
|
},
|
|
});
|
|
|
|
if (artifactProgressSessionRef.current === sessionId) {
|
|
artifactProgressSessionRef.current = "";
|
|
}
|
|
|
|
superAgentLog("chat.checkpoint", "respond.done", {
|
|
traceId,
|
|
sessionId,
|
|
runState,
|
|
});
|
|
return runState;
|
|
} catch (error) {
|
|
superAgentError("chat.checkpoint", "respond.error", {
|
|
traceId,
|
|
sessionId,
|
|
respondBody,
|
|
runState,
|
|
error,
|
|
});
|
|
if (error?.name !== "AbortError") {
|
|
updateConversationMessage(assistantMessageKey, (item) => ({
|
|
title: "处理失败",
|
|
content: error?.message || "提交确认后继续推进失败",
|
|
runLog: finishRunLog(item.runLog),
|
|
}));
|
|
}
|
|
throw error;
|
|
} finally {
|
|
if (chatAbortControllerRef.current !== nextAbortController) return;
|
|
setIsStreamingAnswer(false);
|
|
chatAbortControllerRef.current = null;
|
|
streamingMessageKeyRef.current = "";
|
|
}
|
|
};
|
|
|
|
const handleStopAnswer = () => {
|
|
const currentAbortController = chatAbortControllerRef.current;
|
|
const streamingMessageKey = streamingMessageKeyRef.current;
|
|
|
|
if (!currentAbortController) return;
|
|
superAgentWarn("chat.send", "abort", {
|
|
streamingMessageKey,
|
|
});
|
|
|
|
currentAbortController.abort();
|
|
chatAbortControllerRef.current = null;
|
|
streamingMessageKeyRef.current = "";
|
|
setIsStreamingAnswer(false);
|
|
|
|
if (!streamingMessageKey) return;
|
|
|
|
updateConversationMessage(streamingMessageKey, (item) => {
|
|
const hasContent = Boolean(item.content);
|
|
return {
|
|
title: hasContent ? item.title || "飞小尚" : "已停止生成",
|
|
content: hasContent ? item.content : "已停止生成。",
|
|
loadingText: "",
|
|
runLog: { ...(item.runLog || {}), status: "stopped" },
|
|
};
|
|
});
|
|
};
|
|
|
|
//[显式停止按钮:中止SSE + 通知后端取消任务]
|
|
const handleStopAndCancel = () => {
|
|
handleStopAnswer();
|
|
const sessionId = activeSessionId || activeTaskId;
|
|
if (sessionId && currentUserId) {
|
|
cancelSuperAgentSession({ sessionId, userId: currentUserId }).catch((error) => {
|
|
superAgentWarn("chat.stop", "cancel_failed", {
|
|
sessionId,
|
|
error: error?.message,
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
const startTaskConversation = () => {
|
|
setRouteType("task");
|
|
setTaskStage(activeTask?.stage || TASK_STAGE.IDLE);
|
|
setWorkspaceOpen(false);
|
|
if (!conversationMessages.length) {
|
|
addAssistantMessage({
|
|
type: "taskStart",
|
|
title: TASK_WELCOME_TITLE,
|
|
content: TASK_WELCOME_CONTENT,
|
|
});
|
|
}
|
|
return activeSessionId || activeTaskId;
|
|
};
|
|
|
|
const resetRouteView = (nextRouteType) => {
|
|
setRouteType(nextRouteType);
|
|
setWorkspaceOpen(false);
|
|
setActiveSessionId("");
|
|
setActiveTaskId("");
|
|
clearActiveSessionId();
|
|
setConversationMessages([]);
|
|
setTaskStage(TASK_STAGE.IDLE);
|
|
setUploadedFileList([]);
|
|
setSelectedTaskType(null);
|
|
};
|
|
|
|
const buildEmptyTaskStartMessage = () => ({
|
|
key: `assistant_task_start_${Date.now()}`,
|
|
role: "assistant",
|
|
type: "taskStart",
|
|
title: TASK_WELCOME_TITLE,
|
|
content: TASK_WELCOME_CONTENT,
|
|
});
|
|
|
|
const enterTenderTaskGuide = () => {
|
|
setSelectedTaskType("tender");
|
|
setConversationMessages((prev) => {
|
|
const hasTaskStart = prev.some((item) => item?.type === "taskStart");
|
|
if (!hasTaskStart) {
|
|
return [
|
|
{
|
|
key: `assistant_task_start_${Date.now()}`,
|
|
role: "assistant",
|
|
type: "taskStart",
|
|
title: TENDER_TASK_WELCOME_TITLE,
|
|
content: TENDER_TASK_WELCOME_CONTENT,
|
|
taskType: "tender",
|
|
},
|
|
...prev,
|
|
];
|
|
}
|
|
return prev.map((item) =>
|
|
item?.type === "taskStart"
|
|
? {
|
|
...item,
|
|
title: TENDER_TASK_WELCOME_TITLE,
|
|
content: TENDER_TASK_WELCOME_CONTENT,
|
|
taskType: "tender",
|
|
}
|
|
: item
|
|
);
|
|
});
|
|
};
|
|
|
|
const handleSelectTaskType = (taskKey) => {
|
|
if (taskKey === "tender") {
|
|
//[投标文件:与「新对话」快捷入口一致,走对话引导,不直接出静态流程卡]
|
|
// handleTenderQuickStart 定义在后方,此处仅作类型兜底;UI 点击应直接调 handleTenderQuickStart
|
|
setSelectedTaskType("tender");
|
|
return;
|
|
}
|
|
message.info("该任务类型即将上线,敬请期待");
|
|
};
|
|
|
|
const handleCreateNewChat = async () => {
|
|
if (creatingSessionRef.current || isCreatingChat || isCreatingTask) return;
|
|
|
|
try {
|
|
creatingSessionRef.current = true;
|
|
setIsCreatingChat(true);
|
|
|
|
resetRouteView("chat");
|
|
await createAndActivateSession({ isTaskSession: false, shouldResetMessages: true });
|
|
} catch (error) {
|
|
message.error(error?.message || "创建新对话失败");
|
|
} finally {
|
|
setIsCreatingChat(false);
|
|
creatingSessionRef.current = false;
|
|
}
|
|
};
|
|
|
|
const handleCreateNewTask = async () => {
|
|
if (creatingSessionRef.current || isCreatingChat || isCreatingTask) return;
|
|
|
|
try {
|
|
creatingSessionRef.current = true;
|
|
setIsCreatingTask(true);
|
|
|
|
resetRouteView("task");
|
|
await createAndActivateSession({ isTaskSession: true, shouldResetMessages: true });
|
|
setRouteType("task");
|
|
setTaskStage(TASK_STAGE.IDLE);
|
|
addAssistantMessage({
|
|
type: "taskStart",
|
|
title: TASK_WELCOME_TITLE,
|
|
content: TASK_WELCOME_CONTENT,
|
|
});
|
|
} catch (error) {
|
|
message.error(error?.message || "创建新任务失败");
|
|
} finally {
|
|
setIsCreatingTask(false);
|
|
creatingSessionRef.current = false;
|
|
}
|
|
};
|
|
|
|
const handleCreateSessionInFolder = async (realm, folderId) => {
|
|
if (creatingSessionRef.current || isCreatingChat || isCreatingTask) return;
|
|
|
|
const isTaskRealm = realm === "task";
|
|
// 普通对话不挂文件夹;folderId 仅对任务生效;虚拟默认 → null
|
|
const targetFolderId = isTaskRealm ? resolveApiFolderId(folderId) : null;
|
|
try {
|
|
creatingSessionRef.current = true;
|
|
if (isTaskRealm) setIsCreatingTask(true);
|
|
else setIsCreatingChat(true);
|
|
|
|
if (isTaskRealm) {
|
|
resetRouteView("task");
|
|
await createAndActivateSession({
|
|
isTaskSession: true,
|
|
folderId: targetFolderId,
|
|
shouldResetMessages: true,
|
|
});
|
|
setRouteType("task");
|
|
setTaskStage(TASK_STAGE.IDLE);
|
|
addAssistantMessage({
|
|
type: "taskStart",
|
|
title: TASK_WELCOME_TITLE,
|
|
content: TASK_WELCOME_CONTENT,
|
|
});
|
|
} else {
|
|
resetRouteView("chat");
|
|
await createAndActivateSession({
|
|
isTaskSession: false,
|
|
folderId: null,
|
|
shouldResetMessages: true,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
message.error(error?.message || "创建会话失败");
|
|
} finally {
|
|
setIsCreatingChat(false);
|
|
setIsCreatingTask(false);
|
|
creatingSessionRef.current = false;
|
|
}
|
|
};
|
|
|
|
const openAgentSquare = () => {
|
|
setRouteType("agents");
|
|
setWorkspaceOpen(false);
|
|
};
|
|
|
|
const openResourceLibrary = () => {
|
|
setRouteType("assets");
|
|
setWorkspaceOpen(false);
|
|
};
|
|
|
|
const handleTenderQuickStart = () => {
|
|
//[新任务 / 新对话入口统一:发起对话引导,由 AI 回复后再渲染流程/上传卡]
|
|
setSelectedTaskType("tender");
|
|
if (!isTaskRoute) {
|
|
startTaskConversation();
|
|
} else {
|
|
setRouteType("task");
|
|
}
|
|
setTaskStage(TASK_STAGE.BASIC_INFO);
|
|
sendMessageToAgent("我要生成投标文件,请引导我上传招标文件并开始标书生成流程。", {
|
|
isTaskSession: true,
|
|
});
|
|
};
|
|
|
|
const handleUploadFile = (uploadPayload) => {
|
|
const file = uploadPayload?.file || uploadPayload;
|
|
const fileName = file?.name || "";
|
|
|
|
if (isUploadingFile) {
|
|
message.warning("文件正在上传,请稍候");
|
|
return false;
|
|
}
|
|
|
|
if (!file || !fileName) {
|
|
message.warning("请选择要上传的文件");
|
|
return false;
|
|
}
|
|
|
|
const currentSessionType = getCurrentSessionType();
|
|
|
|
//[选中文件后直接上传到当前会话;无会话时沿用当前页面类型创建会话]
|
|
performUpload(file, fileName, currentSessionType);
|
|
return false;
|
|
};
|
|
|
|
//[执行上传文件的实际逻辑]
|
|
const performUpload = (file, fileName, currentSessionType) => {
|
|
const currentSessionId = activeSessionId || activeTaskId;
|
|
const shouldCreateSession = !currentSessionId;
|
|
const shouldUseTaskSession =
|
|
currentSessionType === "task" || (shouldCreateSession && isTaskRoute);
|
|
const shouldCreateNewTask = shouldCreateSession && shouldUseTaskSession;
|
|
superAgentLog("chat.upload", "perform.start", {
|
|
fileName,
|
|
currentSessionId,
|
|
currentSessionType,
|
|
shouldCreateSession,
|
|
shouldUseTaskSession,
|
|
shouldCreateNewTask,
|
|
file,
|
|
});
|
|
|
|
if (shouldCreateNewTask) {
|
|
startTaskConversation();
|
|
setSelectedTaskType("tender");
|
|
updateActiveTask({
|
|
title: fileName.replace(/\.[^.]+$/, "") || "AI 标书任务",
|
|
fileName,
|
|
stage: TASK_STAGE.BASIC_INFO,
|
|
});
|
|
setTaskStage(TASK_STAGE.BASIC_INFO);
|
|
} else if (shouldUseTaskSession) {
|
|
setSelectedTaskType("tender");
|
|
}
|
|
|
|
const uploadAndStart = async () => {
|
|
setIsUploadingFile(true);
|
|
setUploadProgress({
|
|
fileName,
|
|
loaded: 0,
|
|
total: Number(file?.size || 0),
|
|
percent: 0,
|
|
status: "preparing",
|
|
});
|
|
try {
|
|
const sessionId =
|
|
currentSessionId ||
|
|
(await createAndActivateSession({
|
|
isTaskSession: shouldUseTaskSession,
|
|
title: shouldUseTaskSession
|
|
? fileName.replace(/\.[^.]+$/, "") || "AI 标书任务"
|
|
: "",
|
|
}));
|
|
superAgentLog("chat.upload", "session.ready", {
|
|
sessionId,
|
|
fileName,
|
|
shouldUseTaskSession,
|
|
});
|
|
const uploadResult = await uploadSuperAgentFile({
|
|
sessionId,
|
|
userId: currentUserId,
|
|
file,
|
|
onProgress: (progress) => {
|
|
setUploadProgress({
|
|
fileName,
|
|
...progress,
|
|
status: "uploading",
|
|
});
|
|
},
|
|
});
|
|
setUploadProgress((previous) => ({
|
|
...previous,
|
|
percent: 100,
|
|
status: "processing",
|
|
}));
|
|
const uploadData = uploadResult?.data || uploadResult || {};
|
|
try {
|
|
await createResourceFileFromUpload(uploadData);
|
|
} catch (resourceError) {
|
|
superAgentWarn("chat.upload", "resource_library_save_failed", {
|
|
fileName,
|
|
error: resourceError?.message,
|
|
});
|
|
message.warning("文件已上传,但保存到资源库失败");
|
|
}
|
|
const uploadedFileName =
|
|
uploadData.filename ||
|
|
uploadData.original_filename ||
|
|
uploadData.originalFilename ||
|
|
fileName;
|
|
const rawUploadedFilePath =
|
|
uploadData.path ||
|
|
uploadData.url ||
|
|
uploadData.object_key ||
|
|
uploadData.objectKey ||
|
|
"";
|
|
const uploadedFilePath = buildQiniuFilePath(rawUploadedFilePath);
|
|
const uploadedLocalPath = uploadData.local_path || uploadData.localPath || "";
|
|
const uploadedFileStorage = uploadData.storage || "";
|
|
const normalizedUploadedFileStorage =
|
|
uploadedFileStorage || uploadData.storage_provider || uploadData.storageProvider || "";
|
|
superAgentLog("chat.upload", "upload.done", {
|
|
sessionId,
|
|
uploadedFileName,
|
|
uploadedFilePath,
|
|
uploadedLocalPath,
|
|
uploadedFileStorage: normalizedUploadedFileStorage,
|
|
uploadResult,
|
|
});
|
|
const uploadedFile = {
|
|
id: `${sessionId}_${uploadedFileName}_${Date.now()}`,
|
|
attachmentId: uploadData.id || "",
|
|
name: uploadedFileName,
|
|
url: uploadedFilePath,
|
|
objectKey: uploadData.object_key || uploadData.objectKey || "",
|
|
localPath: uploadedLocalPath,
|
|
storage: normalizedUploadedFileStorage,
|
|
};
|
|
|
|
if (shouldCreateNewTask) {
|
|
updateActiveTask({
|
|
title: uploadedFileName.replace(/\.[^.]+$/, "") || "AI 标书任务",
|
|
fileName: uploadedFileName,
|
|
filePath: uploadedFilePath,
|
|
localPath: uploadedLocalPath,
|
|
fileStorage: normalizedUploadedFileStorage,
|
|
});
|
|
}
|
|
|
|
setUploadedFileList((prev) => [...prev, uploadedFile]);
|
|
message.success("文件上传成功,请输入指令后发送");
|
|
} catch (error) {
|
|
superAgentError("chat.upload", "error", {
|
|
fileName,
|
|
currentSessionType,
|
|
error,
|
|
});
|
|
message.error(error?.message || "上传文件失败");
|
|
} finally {
|
|
setIsUploadingFile(false);
|
|
setUploadProgress(null);
|
|
}
|
|
};
|
|
|
|
uploadAndStart();
|
|
};
|
|
|
|
const handleRemoveUploadedFile = (fileId) => {
|
|
setUploadedFileList((prev) => prev.filter((file) => file.id !== fileId));
|
|
};
|
|
|
|
const confirmBasicInfo = () => {
|
|
superAgentLog("chat.confirm", "basic_info", { activeSessionId, activeTaskId });
|
|
setTaskStage(TASK_STAGE.SCORE);
|
|
sendMessageToAgent("我确认基本信息无误,请继续解析评分点。", {
|
|
isTaskSession: true,
|
|
allowWhileStreaming: true,
|
|
waitingTitle: "飞小尚正在解析评分点",
|
|
waitingText: "已收到基本信息确认,正在分析评分点,请稍候",
|
|
});
|
|
};
|
|
|
|
const confirmScoreItems = () => {
|
|
superAgentLog("chat.confirm", "score_items", { activeSessionId, activeTaskId });
|
|
setTaskStage(TASK_STAGE.CATALOG);
|
|
sendMessageToAgent("我确认评分点,请继续生成投标文件大纲。", {
|
|
isTaskSession: true,
|
|
allowWhileStreaming: true,
|
|
waitingTitle: "飞小尚正在生成大纲",
|
|
waitingText: "已收到评分点确认,正在生成投标文件大纲,请稍候",
|
|
});
|
|
};
|
|
|
|
const confirmCatalog = () => {
|
|
superAgentLog("chat.confirm", "catalog", { activeSessionId, activeTaskId });
|
|
setTaskStage(TASK_STAGE.CONTENT);
|
|
sendMessageToAgent("我确认大纲,请继续生成投标文件正文。", {
|
|
isTaskSession: true,
|
|
allowWhileStreaming: true,
|
|
taskAction: "continue_generation",
|
|
restoreContentOnDone: true,
|
|
waitingTitle: "飞小尚正在生成正文",
|
|
waitingText: "已收到大纲确认,正在生成正文内容,请稍候",
|
|
});
|
|
};
|
|
|
|
const handleSend = () => {
|
|
const userInput = inputValue.trim();
|
|
const currentFileList = uploadedFileList;
|
|
const fileReferenceText = buildUploadedFileReference(currentFileList);
|
|
const finalMessage = [fileReferenceText, userInput].filter(Boolean).join("\n");
|
|
|
|
if (!finalMessage || isStreamingAnswer) return;
|
|
|
|
superAgentLog("chat.input", "send", {
|
|
currentSessionType: getCurrentSessionType(),
|
|
uploadedFileCount: currentFileList.length,
|
|
hasTaskKeyword:
|
|
userInput.includes("标书") ||
|
|
userInput.includes("投标") ||
|
|
userInput.includes("招标"),
|
|
finalMessage,
|
|
});
|
|
setInputValue("");
|
|
setUploadedFileList([]);
|
|
|
|
if (!currentFileList.length && userInput.includes("发现智能体")) {
|
|
addUserMessage(userInput);
|
|
openAgentSquare();
|
|
return;
|
|
}
|
|
|
|
const currentSessionType = getCurrentSessionType();
|
|
const hasTaskKeyword =
|
|
userInput.includes("标书") ||
|
|
userInput.includes("投标") ||
|
|
userInput.includes("招标");
|
|
const shouldUseTaskSession = currentSessionType === "task" || isTaskRoute;
|
|
|
|
if (shouldUseTaskSession) {
|
|
startTaskConversation();
|
|
if (hasTaskKeyword || currentFileList.length > 0) {
|
|
setSelectedTaskType("tender");
|
|
}
|
|
}
|
|
sendMessageToAgent(finalMessage, {
|
|
isTaskSession: shouldUseTaskSession,
|
|
userDisplayText: userInput,
|
|
userFiles: currentFileList,
|
|
//[文件引用写入历史展示字段,刷新或切换会话后才能还原文件标签]
|
|
displayContent: finalMessage,
|
|
attachmentIds: Array.from(
|
|
new Set(currentFileList.map((file) => file.attachmentId).filter(Boolean))
|
|
),
|
|
});
|
|
};
|
|
|
|
const handleSelectTask = async (task) => {
|
|
activeSessionIdRef.current = task.id;
|
|
setActiveTaskId(task.id);
|
|
setActiveSessionId(task.id);
|
|
saveActiveSessionId(task.id);
|
|
setRouteType("task");
|
|
setTaskStage(task.stage || TASK_STAGE.IDLE);
|
|
setWorkspaceOpen(false);
|
|
setUploadedFileList([]);
|
|
setSelectedTaskType(null);
|
|
setConversationMessages([]);
|
|
//[useStream 是唯一会话恢复来源,stream.messages 负责在当前 thread 完成快照加载后还原消息]
|
|
setConversationMessages([buildEmptyTaskStartMessage()]);
|
|
return { success: true };
|
|
};
|
|
|
|
const handleSelectChat = async (chat) => {
|
|
const sessionId = chat.id || chat.chatId;
|
|
activeSessionIdRef.current = sessionId;
|
|
setActiveSessionId(sessionId);
|
|
saveActiveSessionId(sessionId);
|
|
setActiveTaskId("");
|
|
setRouteType("chat");
|
|
setTaskStage(TASK_STAGE.IDLE);
|
|
setWorkspaceOpen(false);
|
|
setUploadedFileList([]);
|
|
setSelectedTaskType(null);
|
|
setConversationMessages([]);
|
|
//[useStream 是唯一会话恢复来源,stream.messages 负责在当前 thread 完成快照加载后还原消息]
|
|
return { success: true };
|
|
};
|
|
|
|
const handleDeleteSidebarSession = async (item, itemType) => {
|
|
const sessionId = item?.id || item?.chatId;
|
|
if (!sessionId) {
|
|
message.error("删除失败,缺少会话 ID");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await dispatch(deleteSuperAgentSession({
|
|
params: buildSessionParams(sessionId),
|
|
query: { userId: currentUserId },
|
|
})).then((res) => ensureActionSuccess(res, "删除超级智能体会话失败"));
|
|
removeSidebarSessionLocally(sessionId, itemType);
|
|
clearActiveSidebarSession(sessionId);
|
|
await loadSidebar({ silent: true });
|
|
message.success("删除成功");
|
|
} catch (error) {
|
|
message.error(error?.message || "删除超级智能体会话失败");
|
|
}
|
|
};
|
|
|
|
const handleDeleteSidebarSessionItem = (session) => {
|
|
const sessionKind = session?.sessionKind || session?.type || "conversation";
|
|
return handleDeleteSidebarSession(session, sessionKind === "task" ? "task" : "chat");
|
|
};
|
|
|
|
return {
|
|
routeType,
|
|
inputValue,
|
|
setInputValue,
|
|
taskList,
|
|
activeTaskId,
|
|
activeTask,
|
|
taskStage,
|
|
setTaskStage,
|
|
conversationMessages,
|
|
uploadedFileList,
|
|
uploadProgress,
|
|
ordinaryChatList,
|
|
sidebarTree,
|
|
collapsedFolderMap,
|
|
activeSessionId,
|
|
isLoadingSessions,
|
|
isUploadingFile,
|
|
isStreamingAnswer,
|
|
isCreatingChat,
|
|
isCreatingTask,
|
|
agentCategory,
|
|
setAgentCategory,
|
|
agentKeyword,
|
|
setAgentKeyword,
|
|
isTaskRoute,
|
|
isAgentRoute,
|
|
isResourceLibraryRoute,
|
|
breadcrumbTitle,
|
|
selectedTaskType,
|
|
addUserMessage,
|
|
upsertAssistantMessage,
|
|
updateTaskById,
|
|
updateActiveTask,
|
|
updateConversationMessage,
|
|
removeConversationMessage,
|
|
createAndActivateSession,
|
|
sendMessageToAgent,
|
|
respondCheckpointToAgent,
|
|
handleStopAnswer,
|
|
handleStopAndCancel,
|
|
handleCreateNewChat,
|
|
handleCreateNewTask,
|
|
openAgentSquare,
|
|
openResourceLibrary,
|
|
handleSelectTaskType,
|
|
handleTenderQuickStart,
|
|
handleUploadFile,
|
|
handleRemoveUploadedFile,
|
|
confirmBasicInfo,
|
|
confirmScoreItems,
|
|
confirmCatalog,
|
|
handleSend,
|
|
handleSelectTask,
|
|
handleSelectChat,
|
|
handleDeleteSidebarSession,
|
|
handleCreateFolder,
|
|
handleRenameFolder,
|
|
handleDeleteFolder,
|
|
handleCreateSessionInFolder,
|
|
handleDeleteSidebarSessionItem,
|
|
handleRenameSession,
|
|
handleMoveSession,
|
|
toggleFolderCollapsed,
|
|
loadSidebar,
|
|
};
|
|
};
|
|
|
|
export default useSuperAgentChat;
|
|
|