diff --git a/web/client/src/sections/superAgent/actions/agentResources.js b/web/client/src/sections/superAgent/actions/agentResources.js index dd21a32..a8c1491 100644 --- a/web/client/src/sections/superAgent/actions/agentResources.js +++ b/web/client/src/sections/superAgent/actions/agentResources.js @@ -140,6 +140,9 @@ export const listAttachments = (conversationId, query = {}) => fallbackMessage: "获取附件失败", }).then(parseAgentPage); +export const listAllAttachments = (conversationId, query = {}) => + listAllPages((pageQuery) => listAttachments(conversationId, pageQuery), query); + export const listArtifacts = (conversationId, query = {}) => jsonAction({ method: "GET", diff --git a/web/client/src/sections/superAgent/container/index.jsx b/web/client/src/sections/superAgent/container/index.jsx index 1ff21a3..5b186a0 100644 --- a/web/client/src/sections/superAgent/container/index.jsx +++ b/web/client/src/sections/superAgent/container/index.jsx @@ -451,7 +451,9 @@ const SuperAgent = () => { activeTask, taskStage, conversationMessages: localConversationMessages, + conversationAttachments, uploadedFileList, + setUploadedFileList, uploadProgress, sidebarTree, collapsedFolderMap, @@ -1031,8 +1033,8 @@ const SuperAgent = () => { ? buildStreamConversationMessages( projectedStreamMessages, stream.toolCalls, - stream.interrupts, - { isRunActive: isStreamRunning } + stream.interrupts, + { isRunActive: isStreamRunning, attachments: conversationAttachments } ) : []; const visibleStreamConversationMessages = streamConversationMessages.map((item) => { @@ -1501,13 +1503,19 @@ const SuperAgent = () => { threadId: activeSessionId, allowDuringInterruptResponse: true, multitaskStrategy: "enqueue", - }); - setInputValue(""); - await followUpPromise; - currentFileList.forEach((file) => handleRemoveUploadedFile(file.id)); + }); + setInputValue(""); + setUploadedFileList([]); + await followUpPromise; } } catch (error) { setInputValue((currentValue) => currentValue || userInput); + setUploadedFileList((previous) => [ + ...currentFileList, + ...previous.filter( + (file) => !currentFileList.some((sentFile) => sentFile.id === file.id) + ), + ]); message.error(error?.message || "提交中断后的消息失败"); } return; @@ -1535,13 +1543,19 @@ const SuperAgent = () => { displayContent: content, attachmentIds, threadId: targetSessionId, - }); - setInputValue(""); - await submitPromise; - currentFileList.forEach((file) => handleRemoveUploadedFile(file.id)); - } catch (error) { - setInputValue((currentValue) => currentValue || userInput); - message.error(error?.message || "智能体服务暂不可用"); + }); + setInputValue(""); + setUploadedFileList([]); + await submitPromise; + } catch (error) { + setInputValue((currentValue) => currentValue || userInput); + setUploadedFileList((previous) => [ + ...currentFileList, + ...previous.filter( + (file) => !currentFileList.some((sentFile) => sentFile.id === file.id) + ), + ]); + message.error(error?.message || "智能体服务暂不可用"); } }; diff --git a/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js b/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js index 6d798c1..504a1ce 100644 --- a/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js +++ b/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js @@ -28,7 +28,7 @@ import { import { completeAttachmentUpload, createAttachmentUploadIntent, - listAttachments, + listAllAttachments, } from "../actions/agentResources"; import useSuperAgentSidebar from "./useSuperAgentSidebar"; import { @@ -89,6 +89,9 @@ const toReadyUploadedFile = (attachment) => ({ attachmentId: String(attachment.id || attachment.attachment_id || attachment.attachmentId), name: attachment.file_name || attachment.filename || "未命名文件", status: attachment.status, + messageId: attachment.message_id || attachment.messageId || "", + mimeType: attachment.mime_type || attachment.mimeType || "", + size: attachment.file_size || attachment.fileSize || 0, }); const buildSessionParams = (sessionId) => ({ @@ -135,6 +138,7 @@ const useSuperAgentChat = ({ const [isCreatingChat, setIsCreatingChat] = useState(false); const [isCreatingTask, setIsCreatingTask] = useState(false); const [uploadedFileList, setUploadedFileList] = useState([]); + const [conversationAttachments, setConversationAttachments] = useState([]); const [agentCategory, setAgentCategory] = useState("全部应用"); const [agentKeyword, setAgentKeyword] = useState(""); /** 任务大厅已选类型:null 表示尚未选择;tender 表示已进入标书引导 */ @@ -160,17 +164,18 @@ const useSuperAgentChat = ({ const conversationId = String(activeConversationId || ""); if (!conversationId) { setUploadedFileList([]); + setConversationAttachments([]); return undefined; } let active = true; - listAttachments(conversationId) - .then((page) => { - if (!active) return; - setUploadedFileList( - page.items - .filter((attachment) => attachment.status === "ready" && !attachment.message_id) - .map(toReadyUploadedFile) - ); + listAllAttachments(conversationId) + .then((attachments) => { + if (!active) return; + const readyFiles = attachments + .filter((attachment) => attachment.status === "ready") + .map(toReadyUploadedFile); + setConversationAttachments(readyFiles); + setUploadedFileList(readyFiles.filter((attachment) => !attachment.messageId)); }) .catch((error) => { superAgentWarn("chat.upload", "attachment_list_failed", { @@ -1642,8 +1647,8 @@ const useSuperAgentChat = ({ status: "processing", })); const uploadData = await completeAttachmentUpload(uploadIntent.attachmentId); - const attachmentPage = await listAttachments(conversationId); - const readyAttachment = attachmentPage.items.find((attachment) => + const attachmentList = await listAllAttachments(conversationId); + const readyAttachment = attachmentList.find((attachment) => String(attachment.id || attachment.attachment_id || attachment.attachmentId) === uploadIntent.attachmentId ) || uploadData?.data || uploadData || {}; if (readyAttachment.status !== "ready") { @@ -1674,6 +1679,10 @@ const useSuperAgentChat = ({ }); } + setConversationAttachments((prev) => [ + ...prev.filter((item) => item.id !== uploadedFile.id), + uploadedFile, + ]); setUploadedFileList((prev) => [...prev, uploadedFile]); message.success("文件上传成功,请输入指令后发送"); } catch (error) { @@ -1864,6 +1873,8 @@ const useSuperAgentChat = ({ setTaskStage, conversationMessages, uploadedFileList, + setUploadedFileList, + conversationAttachments, uploadProgress, ordinaryChatList, sidebarError, diff --git a/web/client/src/sections/superAgent/utils/session.js b/web/client/src/sections/superAgent/utils/session.js index 174dae8..f2fdb85 100644 --- a/web/client/src/sections/superAgent/utils/session.js +++ b/web/client/src/sections/superAgent/utils/session.js @@ -523,7 +523,7 @@ const getHistoryMessageContent = (item = {}, checkpoint = null) => { return fileReferenceContent ?? contentCandidates[0] ?? ""; }; -const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0) => { +const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0, attachmentCatalog = []) => { const rawFileList = [ item.files, item.file_list, @@ -536,8 +536,23 @@ const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0) => { item.additional_kwargs?.files, item.additional_kwargs?.attachments, ].find(Array.isArray) || []; - const fileList = Array.isArray(rawFileList) - ? rawFileList.map((rawFile, index) => { + const messageAttachmentIds = [ + item.attachment_ids, + item.attachmentIds, + item.additional_kwargs?.attachment_ids, + item.additional_kwargs?.attachmentIds, + ].find(Array.isArray) || []; + const catalog = new Map( + (Array.isArray(attachmentCatalog) ? attachmentCatalog : []) + .map((file) => [String(file?.id || file?.attachmentId || file?.attachment_id || ""), file]) + .filter(([id]) => id) + ); + const completeFileList = [ + ...rawFileList, + ...messageAttachmentIds.map((id) => catalog.get(String(id)) || { id }), + ]; + const fileList = Array.isArray(completeFileList) + ? completeFileList.map((rawFile, index) => { const file = typeof rawFile === "string" ? { url: rawFile } : rawFile || {}; const url = file.url || @@ -572,7 +587,11 @@ const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0) => { }; }) : []; - const fileMap = new Map(fileList.filter((file) => file.url).map((file) => [file.url, file])); + const fileMap = new Map( + fileList + .map((file) => [file.attachmentId || file.id || file.url || file.name, file]) + .filter(([key]) => key) + ); const fileNameSet = new Set( fileList.map((file) => normalizeFileNameKey(file.name)).filter(Boolean) ); @@ -818,7 +837,7 @@ export const buildSuperAgentRunLog = (toolCalls = [], sourceItem = {}) => { }; }; -export const normalizeHistoryMessages = (messages = [], sessionId = "") => { +export const normalizeHistoryMessages = (messages = [], sessionId = "", attachmentCatalog = []) => { //[历史 tool 结果按 call id 回填到对应调用,缺少结果时仍保留调用参数] const toolResultMap = new Map(); messages.forEach((item) => { @@ -874,8 +893,8 @@ export const normalizeHistoryMessages = (messages = [], sessionId = "") => { checkpoint && sessionId && checkpointId ? `state_checkpoint_${sessionId}_${checkpointId}` : ""; - const historyFileData = isUser - ? restoreHistoryFileReferences(rawContent, item, index) + const historyFileData = isUser + ? restoreHistoryFileReferences(rawContent, item, index, attachmentCatalog) : { files: [], content: rawContent }; const isCollapsedAfterRespond = [ "confirmed", diff --git a/web/client/src/sections/superAgent/utils/streamMessage.js b/web/client/src/sections/superAgent/utils/streamMessage.js index a27bf16..c3affcc 100644 --- a/web/client/src/sections/superAgent/utils/streamMessage.js +++ b/web/client/src/sections/superAgent/utils/streamMessage.js @@ -61,6 +61,69 @@ const normalizeMessageContent = (content) => { }; }; +const getMessageAttachmentSources = (message = {}) => { + const additional = message.additional_kwargs || message.additionalKwargs || {}; + const kwargs = message.kwargs || {}; + const attachments = [ + message.attachments, + message.files, + additional.attachments, + additional.files, + kwargs.attachments, + kwargs.files, + ].find(Array.isArray) || []; + const attachmentIds = [ + message.attachment_ids, + message.attachmentIds, + additional.attachment_ids, + additional.attachmentIds, + kwargs.attachment_ids, + kwargs.attachmentIds, + ].find(Array.isArray) || []; + return [...attachments, ...attachmentIds]; +}; + +const normalizeMessageAttachments = (message = {}, attachmentCatalog = []) => { + const catalog = new Map( + (Array.isArray(attachmentCatalog) ? attachmentCatalog : []) + .map((item) => [String(item?.id || item?.attachmentId || item?.attachment_id || ""), item]) + .filter(([id]) => id) + ); + const files = new Map(); + getMessageAttachmentSources(message).forEach((source) => { + const raw = typeof source === "string" ? { id: source } : source || {}; + const id = String(raw.id || raw.attachmentId || raw.attachment_id || "").trim(); + const catalogItem = id ? catalog.get(id) || {} : {}; + const file = { + ...catalogItem, + ...raw, + id: id || catalogItem.id || catalogItem.attachmentId, + attachmentId: id || catalogItem.attachmentId || catalogItem.attachment_id || catalogItem.id, + name: + raw.name || + raw.file_name || + raw.filename || + catalogItem.name || + catalogItem.file_name || + catalogItem.filename || + "未命名文件", + url: raw.url || catalogItem.url || "", + }; + const key = String(file.attachmentId || file.id || file.name); + if (key) files.set(key, file); + }); + return [...files.values()]; +}; + +const mergeMessageFiles = (contentFiles, attachmentFiles) => { + const files = new Map(); + [...contentFiles, ...attachmentFiles].forEach((file) => { + const key = String(file.attachmentId || file.id || file.url || file.name || ""); + if (key) files.set(key, file); + }); + return [...files.values()]; +}; + const ACTIVE_TOOL_STATUSES = new Set([ "running", "pending", @@ -233,6 +296,7 @@ export const buildStreamConversationMessages = ( const normalizedStreamTools = (Array.isArray(toolCalls) ? toolCalls : []).map(normalizeToolCall); const normalizedMessageTools = getMessageToolCalls(messages).map(normalizeToolCall); const normalizedTools = mergeToolCalls(normalizedStreamTools, normalizedMessageTools); + const attachmentCatalog = options.attachments || options.attachmentCatalog || []; const displayMessages = (Array.isArray(messages) ? messages : []).reduce((result, message, index) => { const type = readMessageType(message); if (type === "tool") return result; @@ -253,12 +317,13 @@ export const buildStreamConversationMessages = ( (tool) => messageToolIds.has(tool.id) || (tool.messageId && tool.messageId === id) ); const normalizedContent = normalizeMessageContent(message?.content); + const attachmentFiles = normalizeMessageAttachments(message, attachmentCatalog); result.push({ key: id, id, role: type === "human" || type === "user" ? "user" : "assistant", content: normalizedContent.content, - files: normalizedContent.files, + files: mergeMessageFiles(normalizedContent.files, attachmentFiles), toolCalls: attachedTools, runLog: attachedTools.length ? buildToolRunLog(attachedTools, options) diff --git a/web/tests/superAgentFlowContract.test.mjs b/web/tests/superAgentFlowContract.test.mjs index fdeb77c..83885cc 100644 --- a/web/tests/superAgentFlowContract.test.mjs +++ b/web/tests/superAgentFlowContract.test.mjs @@ -34,6 +34,7 @@ assert.match(streamHook, /role:\s*"user"/); assert.doesNotMatch(streamHook, /type:\s*"human"/); assert.match(streamHook, /const state = await super\.getState\(\)/); assert.match(streamHook, /hydratedState\?\.values\?\.messages/); + assert.match(container, /attachments: conversationAttachments/); assert.match(streamHook, /const stopRun =/); assert.match(streamHook, /await stream\.stop\(\)/); assert.match(streamHook, /disconnect:\s*stream\.disconnect/);