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.
992 lines
35 KiB
992 lines
35 KiB
/**
|
|
* 文件作用:superAgent 会话数据工具
|
|
* 职责范围:
|
|
* 1. 读取和维护本地任务会话标记
|
|
* 2. 归一化后端 session 与历史消息
|
|
* 3. 格式化左侧会话时间
|
|
*
|
|
* 不负责:
|
|
* - 调用后端接口
|
|
* - 修改 React 状态
|
|
* - 渲染会话列表
|
|
*
|
|
* 维护说明:
|
|
* - 当前任务合集由后端 session 加本地 tender session 标记共同识别
|
|
* - 任务侧栏始终展示固定「默认任务集」;后端第一条同名任务集会绑定到该槽位
|
|
*/
|
|
"use strict";
|
|
|
|
import { normalizeCheckpoint } from "./checkpoint";
|
|
import {
|
|
FOLDER_COLLAPSED_STORAGE_KEY,
|
|
PENDING_EMPTY_SESSION_STORAGE_KEY,
|
|
ACTIVE_SESSION_STORAGE_KEY,
|
|
DEFAULT_TASK_FOLDER_ID,
|
|
DEFAULT_TASK_FOLDER_NAME,
|
|
TASK_SESSION_STORAGE_KEY,
|
|
TASK_STORAGE_KEY,
|
|
} from "../constants";
|
|
|
|
export const normalizeSessionTitle = (title, fallbackTitle) => {
|
|
const sessionTitle = String(title || "").trim();
|
|
if (sessionTitle.toLowerCase() === "new session") return "新对话";
|
|
if (!sessionTitle) return fallbackTitle;
|
|
return sessionTitle;
|
|
};
|
|
|
|
export const readTaskListFromStorage = () => {
|
|
try {
|
|
const rawValue = window.localStorage.getItem(TASK_STORAGE_KEY);
|
|
const parsedValue = JSON.parse(rawValue || "[]");
|
|
if (!Array.isArray(parsedValue)) return [];
|
|
return parsedValue.map((item) => ({
|
|
...item,
|
|
title: normalizeSessionTitle(item?.title || item?.name || item?.summary, "新对话"),
|
|
}));
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
export const saveTaskListToStorage = (taskList) => {
|
|
window.localStorage.setItem(TASK_STORAGE_KEY, JSON.stringify(taskList));
|
|
};
|
|
|
|
export const readTenderSessionIds = () => {
|
|
try {
|
|
const rawValue = window.localStorage.getItem(TASK_SESSION_STORAGE_KEY);
|
|
const parsedValue = JSON.parse(rawValue || "[]");
|
|
return Array.isArray(parsedValue)
|
|
? parsedValue.map((item) => String(item)).filter(Boolean)
|
|
: [];
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
export const markTenderSessionId = (sessionId) => {
|
|
if (!sessionId) return;
|
|
const nextIds = Array.from(new Set([String(sessionId), ...readTenderSessionIds()]));
|
|
window.localStorage.setItem(TASK_SESSION_STORAGE_KEY, JSON.stringify(nextIds));
|
|
};
|
|
|
|
export const removeTenderSessionId = (sessionId) => {
|
|
if (!sessionId) return;
|
|
const nextIds = readTenderSessionIds().filter((item) => item !== String(sessionId));
|
|
window.localStorage.setItem(TASK_SESSION_STORAGE_KEY, JSON.stringify(nextIds));
|
|
};
|
|
|
|
const normalizePendingEmptySessionMap = (parsedValue = {}) => {
|
|
if (parsedValue?.sessionId && parsedValue?.type) {
|
|
return {
|
|
[String(parsedValue.type)]: {
|
|
sessionId: String(parsedValue.sessionId),
|
|
type: String(parsedValue.type),
|
|
folderId: parsedValue.folderId || parsedValue.folder_id || null,
|
|
},
|
|
};
|
|
}
|
|
|
|
return ["conversation", "task"].reduce((sessionMap, type) => {
|
|
const item = parsedValue?.[type];
|
|
if (item?.sessionId) {
|
|
sessionMap[type] = {
|
|
sessionId: String(item.sessionId),
|
|
type,
|
|
folderId: item.folderId || item.folder_id || null,
|
|
};
|
|
}
|
|
return sessionMap;
|
|
}, {});
|
|
};
|
|
|
|
export const readPendingEmptySessionMap = () => {
|
|
try {
|
|
const rawValue = window.localStorage.getItem(PENDING_EMPTY_SESSION_STORAGE_KEY);
|
|
const parsedValue = JSON.parse(rawValue || "{}");
|
|
return normalizePendingEmptySessionMap(parsedValue);
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
export const readPendingEmptySession = (type = "") => {
|
|
const sessionMap = readPendingEmptySessionMap();
|
|
if (type) return sessionMap[type] || null;
|
|
return sessionMap.conversation || sessionMap.task || null;
|
|
};
|
|
|
|
export const markPendingEmptySession = (sessionId, type, folderId = null) => {
|
|
if (!sessionId || !type) return;
|
|
const sessionMap = readPendingEmptySessionMap();
|
|
const nextSessionMap = {
|
|
...sessionMap,
|
|
[type]: {
|
|
sessionId: String(sessionId),
|
|
type: String(type),
|
|
folderId: folderId || null,
|
|
},
|
|
};
|
|
window.localStorage.setItem(
|
|
PENDING_EMPTY_SESSION_STORAGE_KEY,
|
|
JSON.stringify(nextSessionMap)
|
|
);
|
|
};
|
|
|
|
export const clearPendingEmptySession = (sessionId = "") => {
|
|
if (!sessionId) {
|
|
window.localStorage.removeItem(PENDING_EMPTY_SESSION_STORAGE_KEY);
|
|
return;
|
|
}
|
|
|
|
const sessionMap = readPendingEmptySessionMap();
|
|
const nextSessionMap = Object.entries(sessionMap).reduce((result, [type, item]) => {
|
|
if (item?.sessionId !== String(sessionId)) {
|
|
result[type] = item;
|
|
}
|
|
return result;
|
|
}, {});
|
|
|
|
if (Object.keys(nextSessionMap).length) {
|
|
window.localStorage.setItem(
|
|
PENDING_EMPTY_SESSION_STORAGE_KEY,
|
|
JSON.stringify(nextSessionMap)
|
|
);
|
|
return;
|
|
}
|
|
|
|
window.localStorage.removeItem(PENDING_EMPTY_SESSION_STORAGE_KEY);
|
|
};
|
|
|
|
export const readActiveSessionId = () => {
|
|
try {
|
|
return String(window.localStorage.getItem(ACTIVE_SESSION_STORAGE_KEY) || "").trim();
|
|
} catch (error) {
|
|
return "";
|
|
}
|
|
};
|
|
|
|
export const saveActiveSessionId = (sessionId) => {
|
|
if (!sessionId) return;
|
|
window.localStorage.setItem(ACTIVE_SESSION_STORAGE_KEY, String(sessionId));
|
|
};
|
|
|
|
export const clearActiveSessionId = (sessionId = "") => {
|
|
if (!sessionId) {
|
|
window.localStorage.removeItem(ACTIVE_SESSION_STORAGE_KEY);
|
|
return;
|
|
}
|
|
|
|
if (readActiveSessionId() === String(sessionId)) {
|
|
window.localStorage.removeItem(ACTIVE_SESSION_STORAGE_KEY);
|
|
}
|
|
};
|
|
|
|
export const readCollapsedFolderMap = () => {
|
|
try {
|
|
const rawValue = window.localStorage.getItem(FOLDER_COLLAPSED_STORAGE_KEY);
|
|
const parsedValue = JSON.parse(rawValue || "{}");
|
|
return parsedValue && typeof parsedValue === "object" ? parsedValue : {};
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
export const saveCollapsedFolderMap = (folderMap) => {
|
|
window.localStorage.setItem(FOLDER_COLLAPSED_STORAGE_KEY, JSON.stringify(folderMap || {}));
|
|
};
|
|
|
|
export const getSessionKind = (item = {}) => {
|
|
const taskType = item.task_type || item.taskType;
|
|
if (taskType === "conversation") return "conversation";
|
|
if (taskType) return "task";
|
|
const kind = item.session_kind || item.sessionKind || item.type;
|
|
if (kind === "task" || kind === "conversation") return kind;
|
|
return "conversation";
|
|
};
|
|
|
|
export const isTenderSession = (item = {}) => {
|
|
const sessionId = item.session_id || item.id || item.chatId || item.chat_id || "";
|
|
if (!sessionId) return false;
|
|
const kind = getSessionKind(item);
|
|
if (kind === "conversation") return false;
|
|
if (kind === "task" || item.isTask || item.scene === "tender") return true;
|
|
return readTenderSessionIds().includes(String(sessionId));
|
|
};
|
|
|
|
export const normalizeSidebarSession = (item = {}, realm = "conversation") => {
|
|
const conversationId = item.id || item.conversation_id || item.conversationId || "";
|
|
const threadId = item.thread_id || item.threadId || "";
|
|
const sessionKind = getSessionKind({ ...item, type: item.type || realm });
|
|
const isTask = sessionKind === "task";
|
|
const tasksetId = item.taskset_id || item.tasksetId || item.folder_id || item.folderId || null;
|
|
return {
|
|
id: conversationId,
|
|
chatId: conversationId,
|
|
conversationId,
|
|
threadId,
|
|
title: normalizeSessionTitle(item.title || item.name || item.summary, isTask ? "未命名任务" : "新对话"),
|
|
time: item.updated_at || item.updateTime || item.updatedAt || item.created_at || item.createdAt || Date.now(),
|
|
sessionKind,
|
|
type: sessionKind,
|
|
taskType: item.task_type || item.taskType || "",
|
|
status: item.status || "",
|
|
isTask,
|
|
folderId: tasksetId,
|
|
tasksetId,
|
|
sortOrder: Number(item.sort_order ?? item.sortOrder ?? 0),
|
|
hasActiveTask: Boolean(item.has_active_task ?? item.hasActiveTask),
|
|
raw: item,
|
|
};
|
|
};
|
|
|
|
|
|
export const buildSidebarTreeFromAgentResources = (tasksets = [], conversations = []) => {
|
|
const sessions = (conversations || []).map((item) => normalizeSidebarSession(item, item.type));
|
|
const tasks = sessions.filter((session) => session.sessionKind === "task");
|
|
const chats = sessions.filter((session) => session.sessionKind !== "task");
|
|
const folders = (tasksets || []).map((taskset) => ({
|
|
id: taskset.id,
|
|
name: taskset.name || "未命名任务集",
|
|
sortOrder: 0,
|
|
updatedAt: taskset.updated_at || taskset.updatedAt || "",
|
|
realm: "task",
|
|
isVirtualDefault: false,
|
|
sessions: tasks.filter((session) => session.tasksetId === taskset.id),
|
|
}));
|
|
return {
|
|
realms: [
|
|
{
|
|
realm: "task",
|
|
label: "任务合集",
|
|
folders,
|
|
sessions: [],
|
|
},
|
|
{
|
|
realm: "conversation",
|
|
label: "普通对话",
|
|
folders: [],
|
|
sessions: chats,
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|
|
export const normalizeSidebarFolder = (folder = {}, realm = "conversation") => ({
|
|
id: folder.id || folder.folder_id || "",
|
|
name: folder.name || "未命名文件夹",
|
|
sortOrder: Number(folder.sort_order ?? folder.sortOrder ?? 0),
|
|
updatedAt: folder.updated_at || folder.updatedAt || "",
|
|
realm,
|
|
isVirtualDefault: Boolean(folder.isVirtualDefault),
|
|
sessions: (folder.sessions || []).map((session) => normalizeSidebarSession(session, realm)),
|
|
});
|
|
|
|
export const isVirtualDefaultFolder = (folderOrId) => {
|
|
if (!folderOrId) return false;
|
|
if (typeof folderOrId === "string") {
|
|
return folderOrId === DEFAULT_TASK_FOLDER_ID;
|
|
}
|
|
return Boolean(
|
|
folderOrId.isVirtualDefault || folderOrId.id === DEFAULT_TASK_FOLDER_ID
|
|
);
|
|
};
|
|
|
|
const isPlaceholderDefaultFolderId = (folderId) =>
|
|
!folderId || folderId === DEFAULT_TASK_FOLDER_ID;
|
|
|
|
/** 占位默认任务集(__default__)→ null;已绑定真实任务集则返回真实 id */
|
|
export const resolveApiFolderId = (folderId) => {
|
|
if (!folderId) return null;
|
|
if (typeof folderId === "string") {
|
|
return isPlaceholderDefaultFolderId(folderId) ? null : folderId;
|
|
}
|
|
if (isPlaceholderDefaultFolderId(folderId.id)) return null;
|
|
return folderId.id;
|
|
};
|
|
|
|
/**
|
|
* 任务合集展示归一化:页面始终保留一个固定「默认任务集」。
|
|
* 后端第一条同名真实任务集会绑定到该固定槽位,避免空库新建后出现两个默认任务集。
|
|
*/
|
|
export const normalizeTaskRealmForDisplay = (realmItem = {}) => {
|
|
const rootSessions = Array.isArray(realmItem.sessions) ? realmItem.sessions : [];
|
|
const incomingFolders = Array.isArray(realmItem.folders) ? realmItem.folders : [];
|
|
const placeholderFolders = incomingFolders.filter((folder) =>
|
|
isPlaceholderDefaultFolderId(folder?.id)
|
|
);
|
|
const realFolders = incomingFolders.filter((folder) =>
|
|
!isPlaceholderDefaultFolderId(folder?.id)
|
|
);
|
|
const boundDefaultFolders = realFolders.filter((folder) =>
|
|
folder?.isVirtualDefault || String(folder.name || "").trim() === DEFAULT_TASK_FOLDER_NAME
|
|
);
|
|
const otherFolders = realFolders.filter((folder) =>
|
|
!folder?.isVirtualDefault && String(folder.name || "").trim() !== DEFAULT_TASK_FOLDER_NAME
|
|
);
|
|
const boundDefaultFolder = boundDefaultFolders[0] || null;
|
|
const virtualDefaultFolder = {
|
|
id: boundDefaultFolder?.id || DEFAULT_TASK_FOLDER_ID,
|
|
name: DEFAULT_TASK_FOLDER_NAME,
|
|
sortOrder: -1000,
|
|
updatedAt: boundDefaultFolder?.updatedAt || "",
|
|
realm: "task",
|
|
isVirtualDefault: true,
|
|
sessions: [
|
|
...placeholderFolders.flatMap((folder) => folder.sessions || []),
|
|
...boundDefaultFolders.flatMap((folder) => folder.sessions || []),
|
|
...rootSessions,
|
|
],
|
|
};
|
|
|
|
return {
|
|
...realmItem,
|
|
realm: "task",
|
|
folders: [virtualDefaultFolder, ...otherFolders],
|
|
sessions: [],
|
|
};
|
|
};
|
|
|
|
export const normalizeSidebarTree = (payload = {}) => {
|
|
if (Array.isArray(payload)) {
|
|
const sessions = payload.map((session) => normalizeSidebarSession(session));
|
|
return {
|
|
realms: [
|
|
{
|
|
realm: "task",
|
|
label: "任务合集",
|
|
folders: [],
|
|
sessions: sessions.filter((session) => session.sessionKind === "task"),
|
|
},
|
|
{
|
|
realm: "conversation",
|
|
label: "普通对话",
|
|
folders: [],
|
|
sessions: sessions.filter((session) => session.sessionKind === "conversation"),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const realms = Array.isArray(payload.realms) ? payload.realms : [];
|
|
return {
|
|
realms: realms.map((realmItem) => {
|
|
const realm = realmItem.realm === "task" ? "task" : "conversation";
|
|
return {
|
|
realm,
|
|
label: realmItem.label || (realm === "task" ? "任务合集" : "普通对话"),
|
|
folders: (realmItem.folders || []).map((folder) => normalizeSidebarFolder(folder, realm)),
|
|
sessions: (realmItem.sessions || []).map((session) => normalizeSidebarSession(session, realm)),
|
|
};
|
|
}),
|
|
};
|
|
};
|
|
|
|
export const getRealmFromSidebarTree = (tree, realm) =>
|
|
(tree?.realms || []).find((item) => item.realm === realm) || null;
|
|
|
|
export const flattenRealmSessions = (realmItem = {}) => {
|
|
const rootSessions = realmItem.sessions || [];
|
|
const folderSessions = (realmItem.folders || []).flatMap((folder) => folder.sessions || []);
|
|
return [...folderSessions, ...rootSessions];
|
|
};
|
|
|
|
export const findSessionInSidebarTree = (tree, sessionId) => {
|
|
if (!sessionId) return null;
|
|
for (const realmItem of tree?.realms || []) {
|
|
for (const session of realmItem.sessions || []) {
|
|
if (session.id === sessionId) {
|
|
return { session, realm: realmItem.realm, folderId: null };
|
|
}
|
|
}
|
|
for (const folder of realmItem.folders || []) {
|
|
for (const session of folder.sessions || []) {
|
|
if (session.id === sessionId) {
|
|
return { session, realm: realmItem.realm, folderId: folder.id };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export const updateSessionInSidebarTree = (tree, sessionId, patch = {}) => {
|
|
if (!tree || !sessionId) return tree;
|
|
const nextTree = {
|
|
realms: (tree.realms || []).map((realmItem) => ({
|
|
...realmItem,
|
|
sessions: (realmItem.sessions || []).map((session) =>
|
|
session.id === sessionId ? { ...session, ...patch } : session
|
|
),
|
|
folders: (realmItem.folders || []).map((folder) => ({
|
|
...folder,
|
|
sessions: (folder.sessions || []).map((session) =>
|
|
session.id === sessionId ? { ...session, ...patch } : session
|
|
),
|
|
})),
|
|
})),
|
|
};
|
|
return nextTree;
|
|
};
|
|
|
|
export const removeSessionFromSidebarTree = (tree, sessionId) => {
|
|
if (!tree || !sessionId) return tree;
|
|
return {
|
|
realms: (tree.realms || []).map((realmItem) => ({
|
|
...realmItem,
|
|
sessions: (realmItem.sessions || []).filter((session) => session.id !== sessionId),
|
|
folders: (realmItem.folders || []).map((folder) => ({
|
|
...folder,
|
|
sessions: (folder.sessions || []).filter((session) => session.id !== sessionId),
|
|
})),
|
|
})),
|
|
};
|
|
};
|
|
|
|
export const normalizeSuperAgentSession = (item = {}) => {
|
|
const sessionId = item.session_id || item.id || item.chatId || item.chat_id || "";
|
|
const sessionKind = getSessionKind(item);
|
|
const isTask = sessionKind === "task";
|
|
const rawTitle = item.title || item.name || item.summary;
|
|
return {
|
|
id: sessionId,
|
|
chatId: sessionId,
|
|
title: normalizeSessionTitle(rawTitle, isTask ? "未命名任务" : "新对话"),
|
|
time: item.updated_at || item.updateTime || item.updatedAt || item.created_at || item.createdAt || Date.now(),
|
|
sessionKind,
|
|
type: sessionKind,
|
|
status: item.status || "",
|
|
isTask,
|
|
folderId: item.folder_id || item.folderId || null,
|
|
sortOrder: Number(item.sort_order ?? item.sortOrder ?? 0),
|
|
hasActiveTask: Boolean(item.has_active_task ?? item.hasActiveTask),
|
|
raw: item,
|
|
};
|
|
};
|
|
|
|
const getFileNameFromUrl = (url, fallbackName) => {
|
|
const fileName = String(url || "").split("/").pop() || fallbackName || "已上传文件";
|
|
try {
|
|
return decodeURIComponent(fileName);
|
|
} catch (error) {
|
|
return fileName;
|
|
}
|
|
};
|
|
|
|
const normalizeFileNameKey = (fileName = "") => {
|
|
const rawFileName = String(fileName || "").trim();
|
|
if (!rawFileName) return "";
|
|
const cleanFileName = rawFileName.split("?")[0].split("#")[0];
|
|
try {
|
|
return decodeURIComponent(cleanFileName).trim().toLowerCase();
|
|
} catch (error) {
|
|
return cleanFileName.trim().toLowerCase();
|
|
}
|
|
};
|
|
|
|
const getHistoryMessageContent = (item = {}, checkpoint = null) => {
|
|
const contentCandidates = [
|
|
item.display_content,
|
|
item.displayContent,
|
|
item.data?.display_content,
|
|
item.data?.displayContent,
|
|
item.content,
|
|
item.text,
|
|
item.message,
|
|
checkpoint?.description,
|
|
].filter((content) => content !== undefined && content !== null);
|
|
const fileReferenceContent = contentCandidates.find((content) =>
|
|
/文件名|招标文件|文件路径|路径为/.test(String(content))
|
|
);
|
|
return fileReferenceContent ?? contentCandidates[0] ?? "";
|
|
};
|
|
|
|
const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0) => {
|
|
const rawFileList = [
|
|
item.files,
|
|
item.file_list,
|
|
item.attachments,
|
|
item.data?.files,
|
|
item.data?.file_list,
|
|
item.data?.attachments,
|
|
item.metadata?.files,
|
|
item.metadata?.attachments,
|
|
item.additional_kwargs?.files,
|
|
item.additional_kwargs?.attachments,
|
|
].find(Array.isArray) || [];
|
|
const fileList = Array.isArray(rawFileList)
|
|
? rawFileList.map((rawFile, index) => {
|
|
const file = typeof rawFile === "string" ? { url: rawFile } : rawFile || {};
|
|
const url =
|
|
file.url ||
|
|
file.path ||
|
|
file.file_path ||
|
|
file.filePath ||
|
|
file.download_url ||
|
|
file.downloadUrl ||
|
|
file.object_key ||
|
|
file.objectKey ||
|
|
"";
|
|
return {
|
|
...file,
|
|
id: file.id || `history_file_${messageIndex}_${index + 1}`,
|
|
attachmentId: file.attachmentId || file.attachment_id || file.id || "",
|
|
name:
|
|
file.name ||
|
|
file.filename ||
|
|
file.original_filename ||
|
|
file.originalFilename ||
|
|
file.display_name ||
|
|
file.displayName ||
|
|
getFileNameFromUrl(url),
|
|
url,
|
|
objectKey: file.object_key || file.objectKey || "",
|
|
localPath: file.local_path || file.localPath || "",
|
|
storage:
|
|
file.storage ||
|
|
file.storage_provider ||
|
|
file.storageProvider ||
|
|
"",
|
|
};
|
|
})
|
|
: [];
|
|
const fileMap = new Map(fileList.filter((file) => file.url).map((file) => [file.url, file]));
|
|
const fileNameSet = new Set(
|
|
fileList.map((file) => normalizeFileNameKey(file.name)).filter(Boolean)
|
|
);
|
|
const contentWithoutReferences = String(content || "").replace(
|
|
/(\s*(?:文件名|招标文件)[::]\s*([\s\S]*?)[,,]\s*(?:文件路径|路径为)[::]\s*([\s\S]*?))/g,
|
|
(referenceText, matchedFileName, url) => {
|
|
const fileName = matchedFileName?.trim() || getFileNameFromUrl(url);
|
|
const normalizedUrl = String(url || "").trim();
|
|
const fileNameKey = normalizeFileNameKey(fileName);
|
|
if (!fileMap.has(normalizedUrl) && !fileNameSet.has(fileNameKey)) {
|
|
fileMap.set(normalizedUrl, {
|
|
id: `history_file_${messageIndex}_${fileMap.size + 1}`,
|
|
name: fileName,
|
|
url: normalizedUrl,
|
|
});
|
|
if (fileNameKey) fileNameSet.add(fileNameKey);
|
|
}
|
|
return "";
|
|
}
|
|
);
|
|
|
|
return {
|
|
files: Array.from(fileMap.values()),
|
|
content: contentWithoutReferences.replace(/^\s*\n?\s*|\s*\n?\s*$/g, ""),
|
|
};
|
|
};
|
|
|
|
const isMergeableAssistantHistoryMessage = (item = {}) => {
|
|
if (item.role !== "assistant") return false;
|
|
const type = String(item.type || "plain");
|
|
return !["checkpoint", "taskProgress", "content"].includes(type);
|
|
};
|
|
|
|
const mergeHistoryRunLog = (prevRunLog, nextRunLog) => {
|
|
if (!prevRunLog) return nextRunLog || null;
|
|
if (!nextRunLog) return prevRunLog;
|
|
return {
|
|
...prevRunLog,
|
|
...nextRunLog,
|
|
questionCount: Number(prevRunLog.questionCount || 0) + Number(nextRunLog.questionCount || 0),
|
|
commandCount: Number(prevRunLog.commandCount || 0) + Number(nextRunLog.commandCount || 0),
|
|
elapsedMs: Number(prevRunLog.elapsedMs || 0) + Number(nextRunLog.elapsedMs || 0),
|
|
entries: [
|
|
...(Array.isArray(prevRunLog.entries) ? prevRunLog.entries : []),
|
|
...(Array.isArray(nextRunLog.entries) ? nextRunLog.entries : []),
|
|
],
|
|
};
|
|
};
|
|
|
|
const parseHistoryToolArguments = (toolCall = {}) => {
|
|
const rawArguments = getSuperAgentToolArguments(toolCall);
|
|
if (!rawArguments) return {};
|
|
if (typeof rawArguments === "object") return rawArguments;
|
|
try {
|
|
return JSON.parse(String(rawArguments));
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
const getHistoryCheckpointFromToolCall = (toolCall = {}) => {
|
|
const toolName = getSuperAgentToolName(toolCall);
|
|
if (toolName !== "emit_checkpoint") return null;
|
|
|
|
const toolArguments = parseHistoryToolArguments(toolCall);
|
|
return (
|
|
toolArguments.checkpoint ||
|
|
toolArguments.data?.checkpoint ||
|
|
toolArguments
|
|
);
|
|
};
|
|
|
|
const hasHistoryCheckpointToolCall = (item = {}) => {
|
|
const toolCalls = item.tool_calls || item.toolCalls || item.data?.tool_calls || [];
|
|
if (!Array.isArray(toolCalls)) return false;
|
|
return toolCalls.some((toolCall) => Boolean(getHistoryCheckpointFromToolCall(toolCall)));
|
|
};
|
|
|
|
const buildHistoryCheckpointMessage = ({
|
|
checkpoint,
|
|
sourceItem,
|
|
sourceIndex,
|
|
checkpointIndex,
|
|
sessionId,
|
|
}) => {
|
|
const normalizedCheckpoint = normalizeCheckpoint(checkpoint);
|
|
const checkpointId = normalizedCheckpoint.checkpoint_id;
|
|
const checkpointMessageKey =
|
|
checkpointId && sessionId
|
|
? `state_checkpoint_${sessionId}_${checkpointId}`
|
|
: "";
|
|
const checkpointStatus =
|
|
normalizedCheckpoint.checkpoint_status || sourceItem.checkpointStatus || "pending";
|
|
const isCollapsedAfterRespond = [
|
|
"confirmed",
|
|
"completed",
|
|
"done",
|
|
"success",
|
|
"regenerate",
|
|
].includes(String(checkpointStatus || "").toLowerCase());
|
|
|
|
return {
|
|
key:
|
|
checkpointMessageKey ||
|
|
`${sourceItem.id || sourceItem.message_id || `history_${sourceIndex}`}_checkpoint_${checkpointIndex}`,
|
|
role: "assistant",
|
|
type: "checkpoint",
|
|
title: normalizedCheckpoint.title,
|
|
content: normalizedCheckpoint.description,
|
|
checkpoint: normalizedCheckpoint,
|
|
task: sourceItem.task || sourceItem.data?.task || checkpoint?.task || null,
|
|
checkpointStatus,
|
|
isCollapsedAfterRespond,
|
|
isRestoredFromHistory: true,
|
|
};
|
|
};
|
|
|
|
const mergeAssistantHistoryMessages = (messageList = []) => {
|
|
return messageList.reduce((result, item) => {
|
|
const prevItem = result[result.length - 1];
|
|
if (
|
|
isMergeableAssistantHistoryMessage(prevItem) &&
|
|
isMergeableAssistantHistoryMessage(item)
|
|
) {
|
|
const prevContent = String(prevItem.content || "").trim();
|
|
const nextContent = String(item.content || "").trim();
|
|
const mergedContent = [prevContent, nextContent].filter(Boolean).join("\n\n");
|
|
result[result.length - 1] = {
|
|
...prevItem,
|
|
content: mergedContent,
|
|
runLog: mergeHistoryRunLog(prevItem.runLog, item.runLog),
|
|
loadingText: prevItem.loadingText || item.loadingText || "",
|
|
};
|
|
return result;
|
|
}
|
|
|
|
result.push(item);
|
|
return result;
|
|
}, []);
|
|
};
|
|
|
|
//[不计入运行日志的流程控制工具:对应内容已由 checkpoint 卡片单独展示]
|
|
const RUN_LOG_IGNORED_TOOL_NAMES = new Set(["emit_checkpoint"]);
|
|
|
|
//[读取工具调用名称,兼容历史消息和实时事件的不同字段]
|
|
export const getSuperAgentToolName = (toolCall = {}) => {
|
|
if (typeof toolCall === "string") return toolCall;
|
|
return String(
|
|
toolCall.tool ||
|
|
toolCall.tool_name ||
|
|
toolCall.name ||
|
|
toolCall.function?.name ||
|
|
toolCall.data?.tool ||
|
|
toolCall.data?.tool_name ||
|
|
toolCall.data?.name ||
|
|
""
|
|
).trim();
|
|
};
|
|
|
|
//[读取工具调用标识,用于实时开始/结束事件合并]
|
|
export const getSuperAgentToolCallId = (toolCall = {}) =>
|
|
String(
|
|
toolCall.call_id ||
|
|
toolCall.callId ||
|
|
toolCall.tool_call_id ||
|
|
toolCall.toolCallId ||
|
|
toolCall.id ||
|
|
toolCall.data?.call_id ||
|
|
toolCall.data?.tool_call_id ||
|
|
toolCall.data?.id ||
|
|
""
|
|
).trim();
|
|
|
|
//[读取工具参数,保留原始结构供展开区格式化]
|
|
export const getSuperAgentToolArguments = (toolCall = {}) =>
|
|
toolCall.args ||
|
|
toolCall.arguments ||
|
|
toolCall.input ||
|
|
toolCall.parameters ||
|
|
toolCall.function?.arguments ||
|
|
toolCall.data?.args ||
|
|
toolCall.data?.arguments ||
|
|
toolCall.data?.input ||
|
|
"";
|
|
|
|
//[读取工具结果,兼容实时事件常见字段]
|
|
export const getSuperAgentToolResult = (toolCall = {}) =>
|
|
toolCall.result ||
|
|
toolCall.output ||
|
|
toolCall.response ||
|
|
toolCall.data?.result ||
|
|
toolCall.data?.output ||
|
|
toolCall.data ||
|
|
"";
|
|
|
|
//[需要展示的运行工具:checkpoint 等流程控制事件不重复计数]
|
|
export const isSuperAgentRunLogTool = (toolName = "") => {
|
|
const normalizedName = String(toolName || "").trim();
|
|
return Boolean(normalizedName) && !RUN_LOG_IGNORED_TOOL_NAMES.has(normalizedName);
|
|
};
|
|
|
|
//[询问类工具单独计数,其他工具统一归入命令执行]
|
|
export const isSuperAgentQuestionTool = (toolName = "") =>
|
|
/(?:ask|question|request_user_input)/i.test(String(toolName || ""));
|
|
|
|
//[构建历史会话运行摘要,避免切换会话后工具轨迹消失]
|
|
export const buildSuperAgentRunLog = (toolCalls = [], sourceItem = {}) => {
|
|
if (!Array.isArray(toolCalls) || !toolCalls.length) return null;
|
|
|
|
let questionCount = 0;
|
|
let commandCount = 0;
|
|
const entries = [];
|
|
toolCalls.forEach((toolCall, index) => {
|
|
const toolName = getSuperAgentToolName(toolCall);
|
|
if (!isSuperAgentRunLogTool(toolName)) return;
|
|
const isQuestion = isSuperAgentQuestionTool(toolName);
|
|
if (isQuestion) {
|
|
questionCount += 1;
|
|
} else {
|
|
commandCount += 1;
|
|
}
|
|
entries.push({
|
|
id: getSuperAgentToolCallId(toolCall) || `history_tool_${index + 1}`,
|
|
name: toolName,
|
|
kind: isQuestion ? "question" : "command",
|
|
status: "done",
|
|
arguments: getSuperAgentToolArguments(toolCall),
|
|
result: getSuperAgentToolResult(toolCall),
|
|
});
|
|
});
|
|
|
|
if (!questionCount && !commandCount) return null;
|
|
|
|
const elapsedMs = Number(
|
|
sourceItem.elapsed_ms || sourceItem.elapsedMs || sourceItem.duration_ms || sourceItem.durationMs || 0
|
|
);
|
|
return {
|
|
questionCount,
|
|
commandCount,
|
|
elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : 0,
|
|
status: "done",
|
|
entries,
|
|
};
|
|
};
|
|
|
|
export const normalizeHistoryMessages = (messages = [], sessionId = "") => {
|
|
//[历史 tool 结果按 call id 回填到对应调用,缺少结果时仍保留调用参数]
|
|
const toolResultMap = new Map();
|
|
messages.forEach((item) => {
|
|
const role = item?.role || item?.sender || item?.type || "";
|
|
if (role !== "tool") return;
|
|
const callId = getSuperAgentToolCallId(item);
|
|
const result = item.content || item.text || item.message || getSuperAgentToolResult(item);
|
|
if (!callId || !result || String(result).trim() === "[工具执行完成]") return;
|
|
toolResultMap.set(callId, result);
|
|
});
|
|
const buildHistoryRunLog = (item = {}) => {
|
|
const toolCalls = item.tool_calls || item.toolCalls || item.data?.tool_calls || [];
|
|
const runLog = buildSuperAgentRunLog(toolCalls, item);
|
|
if (!runLog) return null;
|
|
return {
|
|
...runLog,
|
|
entries: runLog.entries.map((entry) => ({
|
|
...entry,
|
|
result: entry.result || toolResultMap.get(entry.id) || "",
|
|
})),
|
|
};
|
|
};
|
|
|
|
const normalizedMessages = messages
|
|
.filter((item) => {
|
|
const role = item?.role || item?.sender || item?.type || "";
|
|
const checkpoint = item?.checkpoint || item?.data?.checkpoint || null;
|
|
const content = getHistoryMessageContent(item, checkpoint);
|
|
const isUser = role === "user" || role === "human";
|
|
const isAssistant = role === "assistant";
|
|
const isCheckpoint = Boolean(checkpoint);
|
|
const hasDisplayContent = Boolean(
|
|
String(content || "").trim() ||
|
|
checkpoint ||
|
|
buildHistoryRunLog(item) ||
|
|
hasHistoryCheckpointToolCall(item)
|
|
);
|
|
|
|
return (isUser || isAssistant || isCheckpoint) && hasDisplayContent;
|
|
})
|
|
.flatMap((item, index) => {
|
|
const role = item.role || item.sender || item.type || "";
|
|
const isUser = role === "user" || role === "human";
|
|
const rawCheckpoint = item.checkpoint || item.data?.checkpoint || null;
|
|
const checkpoint = rawCheckpoint ? normalizeCheckpoint(rawCheckpoint) : null;
|
|
const task = item.task || item.data?.task || rawCheckpoint?.task || null;
|
|
const rawContent = getHistoryMessageContent(item, checkpoint);
|
|
const checkpointStatus = checkpoint
|
|
? checkpoint.checkpoint_status || item.checkpointStatus || "pending"
|
|
: undefined;
|
|
const checkpointId = checkpoint?.checkpoint_id || checkpoint?.checkpointId || checkpoint?.id || "";
|
|
const checkpointMessageKey =
|
|
checkpoint && sessionId && checkpointId
|
|
? `state_checkpoint_${sessionId}_${checkpointId}`
|
|
: "";
|
|
const historyFileData = isUser
|
|
? restoreHistoryFileReferences(rawContent, item, index)
|
|
: { files: [], content: rawContent };
|
|
const isCollapsedAfterRespond = [
|
|
"confirmed",
|
|
"completed",
|
|
"done",
|
|
"success",
|
|
"regenerate",
|
|
].includes(String(checkpointStatus || "").toLowerCase());
|
|
const historyMessage = {
|
|
key: checkpointMessageKey || item.id || item.message_id || `history_${index}`,
|
|
role: isUser ? "user" : "assistant",
|
|
type: checkpoint ? "checkpoint" : item.type || "plain",
|
|
title: item.title || checkpoint?.title || "",
|
|
content: checkpoint ? checkpoint.description || rawContent : historyFileData.content,
|
|
reasoning: item.reasoning || item.thinking || "",
|
|
runLog: buildHistoryRunLog(item),
|
|
checkpoint,
|
|
task,
|
|
checkpointStatus,
|
|
isCollapsedAfterRespond,
|
|
isRestoredFromHistory: Boolean(checkpoint),
|
|
fileName: item.fileName || item.file_name || "",
|
|
files: historyFileData.files,
|
|
};
|
|
if (isUser || checkpoint) return [historyMessage];
|
|
|
|
const toolCalls = item.tool_calls || item.toolCalls || item.data?.tool_calls || [];
|
|
const checkpointMessages = Array.isArray(toolCalls)
|
|
? toolCalls
|
|
.map((toolCall, checkpointIndex) => {
|
|
const toolCheckpoint = getHistoryCheckpointFromToolCall(toolCall);
|
|
if (!toolCheckpoint) return null;
|
|
return buildHistoryCheckpointMessage({
|
|
checkpoint: toolCheckpoint,
|
|
sourceItem: item,
|
|
sourceIndex: index,
|
|
checkpointIndex,
|
|
sessionId,
|
|
});
|
|
})
|
|
.filter(Boolean)
|
|
: [];
|
|
const hasHistoryContent = Boolean(
|
|
String(historyMessage.content || "").trim() || historyMessage.runLog
|
|
);
|
|
if (checkpointMessages.length && !hasHistoryContent) {
|
|
return checkpointMessages;
|
|
}
|
|
return checkpointMessages.length
|
|
? [historyMessage, ...checkpointMessages]
|
|
: [historyMessage];
|
|
});
|
|
|
|
return mergeAssistantHistoryMessages(normalizedMessages).reduce((messageList, item) => {
|
|
const checkpointId = item.checkpoint?.checkpoint_id;
|
|
const existingCheckpointIndex = checkpointId
|
|
? messageList.findIndex(
|
|
(messageItem) => messageItem.checkpoint?.checkpoint_id === checkpointId
|
|
)
|
|
: -1;
|
|
if (existingCheckpointIndex >= 0) {
|
|
const existingCheckpoint = messageList[existingCheckpointIndex];
|
|
messageList[existingCheckpointIndex] = {
|
|
...existingCheckpoint,
|
|
...item,
|
|
key: existingCheckpoint.key,
|
|
};
|
|
return messageList;
|
|
}
|
|
|
|
const previousItem = messageList[messageList.length - 1];
|
|
const isRunLogOnlyMessage = Boolean(
|
|
item.role === "assistant" &&
|
|
item.type === "plain" &&
|
|
item.runLog &&
|
|
!String(item.title || "").trim() &&
|
|
!String(item.content || "").trim() &&
|
|
!item.checkpoint &&
|
|
!item.task
|
|
);
|
|
const canMergeIntoPrevious = Boolean(
|
|
isRunLogOnlyMessage &&
|
|
previousItem?.role === "assistant" &&
|
|
previousItem?.type === "plain" &&
|
|
previousItem?.runLog
|
|
);
|
|
if (!canMergeIntoPrevious) {
|
|
messageList.push(item);
|
|
return messageList;
|
|
}
|
|
|
|
//[连续纯命令消息并入上一条回复,避免历史 tool_calls 形成多个独立日志块]
|
|
const mergedEntries = [
|
|
...(previousItem.runLog.entries || []),
|
|
...(item.runLog.entries || []),
|
|
].map((entry, index) => ({
|
|
...entry,
|
|
id: `${entry.id || entry.name || "tool"}_${index + 1}`,
|
|
}));
|
|
const mergedRunLog = {
|
|
...previousItem.runLog,
|
|
status:
|
|
previousItem.runLog.status === "running" || item.runLog.status === "running"
|
|
? "running"
|
|
: "done",
|
|
questionCount: mergedEntries.length
|
|
? mergedEntries.filter((entry) => entry.kind === "question").length
|
|
: Number(previousItem.runLog.questionCount || 0) +
|
|
Number(item.runLog.questionCount || 0),
|
|
commandCount: mergedEntries.length
|
|
? mergedEntries.filter((entry) => entry.kind !== "question").length
|
|
: Number(previousItem.runLog.commandCount || 0) +
|
|
Number(item.runLog.commandCount || 0),
|
|
elapsedMs:
|
|
Number(previousItem.runLog.elapsedMs || 0) + Number(item.runLog.elapsedMs || 0),
|
|
entries: mergedEntries,
|
|
};
|
|
messageList[messageList.length - 1] = {
|
|
...previousItem,
|
|
runLog: mergedRunLog,
|
|
};
|
|
return messageList;
|
|
}, []);
|
|
};
|
|
|
|
export const formatChatTime = (rawTime) => {
|
|
if (!rawTime) return "";
|
|
const timeValue = Number(rawTime);
|
|
const dateValue = Number.isFinite(timeValue)
|
|
? new Date(timeValue < 10000000000 ? timeValue * 1000 : timeValue)
|
|
: new Date(String(rawTime).replace(/-/g, "/"));
|
|
if (!Number.isFinite(dateValue.getTime())) return "";
|
|
const today = new Date();
|
|
const isToday = dateValue.toDateString() === today.toDateString();
|
|
return isToday ? "今天" : `${dateValue.getMonth() + 1}-${dateValue.getDate()}`;
|
|
};
|
|
|