Browse Source

fix: 文件显示问题

master
Senor-Liu 1 month ago
parent
commit
6789455b14
  1. 3
      web/client/src/sections/superAgent/actions/agentResources.js
  2. 40
      web/client/src/sections/superAgent/container/index.jsx
  3. 33
      web/client/src/sections/superAgent/hooks/useSuperAgentChat.js
  4. 33
      web/client/src/sections/superAgent/utils/session.js
  5. 67
      web/client/src/sections/superAgent/utils/streamMessage.js
  6. 1
      web/tests/superAgentFlowContract.test.mjs

3
web/client/src/sections/superAgent/actions/agentResources.js

@ -140,6 +140,9 @@ export const listAttachments = (conversationId, query = {}) =>
fallbackMessage: "获取附件失败", fallbackMessage: "获取附件失败",
}).then(parseAgentPage); }).then(parseAgentPage);
export const listAllAttachments = (conversationId, query = {}) =>
listAllPages((pageQuery) => listAttachments(conversationId, pageQuery), query);
export const listArtifacts = (conversationId, query = {}) => export const listArtifacts = (conversationId, query = {}) =>
jsonAction({ jsonAction({
method: "GET", method: "GET",

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

@ -451,7 +451,9 @@ const SuperAgent = () => {
activeTask, activeTask,
taskStage, taskStage,
conversationMessages: localConversationMessages, conversationMessages: localConversationMessages,
conversationAttachments,
uploadedFileList, uploadedFileList,
setUploadedFileList,
uploadProgress, uploadProgress,
sidebarTree, sidebarTree,
collapsedFolderMap, collapsedFolderMap,
@ -1031,8 +1033,8 @@ const SuperAgent = () => {
? buildStreamConversationMessages( ? buildStreamConversationMessages(
projectedStreamMessages, projectedStreamMessages,
stream.toolCalls, stream.toolCalls,
stream.interrupts, stream.interrupts,
{ isRunActive: isStreamRunning } { isRunActive: isStreamRunning, attachments: conversationAttachments }
) )
: []; : [];
const visibleStreamConversationMessages = streamConversationMessages.map((item) => { const visibleStreamConversationMessages = streamConversationMessages.map((item) => {
@ -1501,13 +1503,19 @@ const SuperAgent = () => {
threadId: activeSessionId, threadId: activeSessionId,
allowDuringInterruptResponse: true, allowDuringInterruptResponse: true,
multitaskStrategy: "enqueue", multitaskStrategy: "enqueue",
}); });
setInputValue(""); setInputValue("");
await followUpPromise; setUploadedFileList([]);
currentFileList.forEach((file) => handleRemoveUploadedFile(file.id)); await followUpPromise;
} }
} catch (error) { } catch (error) {
setInputValue((currentValue) => currentValue || userInput); setInputValue((currentValue) => currentValue || userInput);
setUploadedFileList((previous) => [
...currentFileList,
...previous.filter(
(file) => !currentFileList.some((sentFile) => sentFile.id === file.id)
),
]);
message.error(error?.message || "提交中断后的消息失败"); message.error(error?.message || "提交中断后的消息失败");
} }
return; return;
@ -1535,13 +1543,19 @@ const SuperAgent = () => {
displayContent: content, displayContent: content,
attachmentIds, attachmentIds,
threadId: targetSessionId, threadId: targetSessionId,
}); });
setInputValue(""); setInputValue("");
await submitPromise; setUploadedFileList([]);
currentFileList.forEach((file) => handleRemoveUploadedFile(file.id)); await submitPromise;
} catch (error) { } catch (error) {
setInputValue((currentValue) => currentValue || userInput); setInputValue((currentValue) => currentValue || userInput);
message.error(error?.message || "智能体服务暂不可用"); setUploadedFileList((previous) => [
...currentFileList,
...previous.filter(
(file) => !currentFileList.some((sentFile) => sentFile.id === file.id)
),
]);
message.error(error?.message || "智能体服务暂不可用");
} }
}; };

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

@ -28,7 +28,7 @@ import {
import { import {
completeAttachmentUpload, completeAttachmentUpload,
createAttachmentUploadIntent, createAttachmentUploadIntent,
listAttachments, listAllAttachments,
} from "../actions/agentResources"; } from "../actions/agentResources";
import useSuperAgentSidebar from "./useSuperAgentSidebar"; import useSuperAgentSidebar from "./useSuperAgentSidebar";
import { import {
@ -89,6 +89,9 @@ const toReadyUploadedFile = (attachment) => ({
attachmentId: String(attachment.id || attachment.attachment_id || attachment.attachmentId), attachmentId: String(attachment.id || attachment.attachment_id || attachment.attachmentId),
name: attachment.file_name || attachment.filename || "未命名文件", name: attachment.file_name || attachment.filename || "未命名文件",
status: attachment.status, 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) => ({ const buildSessionParams = (sessionId) => ({
@ -135,6 +138,7 @@ const useSuperAgentChat = ({
const [isCreatingChat, setIsCreatingChat] = useState(false); const [isCreatingChat, setIsCreatingChat] = useState(false);
const [isCreatingTask, setIsCreatingTask] = useState(false); const [isCreatingTask, setIsCreatingTask] = useState(false);
const [uploadedFileList, setUploadedFileList] = useState([]); const [uploadedFileList, setUploadedFileList] = useState([]);
const [conversationAttachments, setConversationAttachments] = useState([]);
const [agentCategory, setAgentCategory] = useState("全部应用"); const [agentCategory, setAgentCategory] = useState("全部应用");
const [agentKeyword, setAgentKeyword] = useState(""); const [agentKeyword, setAgentKeyword] = useState("");
/** 任务大厅已选类型:null 表示尚未选择;tender 表示已进入标书引导 */ /** 任务大厅已选类型:null 表示尚未选择;tender 表示已进入标书引导 */
@ -160,17 +164,18 @@ const useSuperAgentChat = ({
const conversationId = String(activeConversationId || ""); const conversationId = String(activeConversationId || "");
if (!conversationId) { if (!conversationId) {
setUploadedFileList([]); setUploadedFileList([]);
setConversationAttachments([]);
return undefined; return undefined;
} }
let active = true; let active = true;
listAttachments(conversationId) listAllAttachments(conversationId)
.then((page) => { .then((attachments) => {
if (!active) return; if (!active) return;
setUploadedFileList( const readyFiles = attachments
page.items .filter((attachment) => attachment.status === "ready")
.filter((attachment) => attachment.status === "ready" && !attachment.message_id) .map(toReadyUploadedFile);
.map(toReadyUploadedFile) setConversationAttachments(readyFiles);
); setUploadedFileList(readyFiles.filter((attachment) => !attachment.messageId));
}) })
.catch((error) => { .catch((error) => {
superAgentWarn("chat.upload", "attachment_list_failed", { superAgentWarn("chat.upload", "attachment_list_failed", {
@ -1642,8 +1647,8 @@ const useSuperAgentChat = ({
status: "processing", status: "processing",
})); }));
const uploadData = await completeAttachmentUpload(uploadIntent.attachmentId); const uploadData = await completeAttachmentUpload(uploadIntent.attachmentId);
const attachmentPage = await listAttachments(conversationId); const attachmentList = await listAllAttachments(conversationId);
const readyAttachment = attachmentPage.items.find((attachment) => const readyAttachment = attachmentList.find((attachment) =>
String(attachment.id || attachment.attachment_id || attachment.attachmentId) === uploadIntent.attachmentId String(attachment.id || attachment.attachment_id || attachment.attachmentId) === uploadIntent.attachmentId
) || uploadData?.data || uploadData || {}; ) || uploadData?.data || uploadData || {};
if (readyAttachment.status !== "ready") { if (readyAttachment.status !== "ready") {
@ -1674,6 +1679,10 @@ const useSuperAgentChat = ({
}); });
} }
setConversationAttachments((prev) => [
...prev.filter((item) => item.id !== uploadedFile.id),
uploadedFile,
]);
setUploadedFileList((prev) => [...prev, uploadedFile]); setUploadedFileList((prev) => [...prev, uploadedFile]);
message.success("文件上传成功,请输入指令后发送"); message.success("文件上传成功,请输入指令后发送");
} catch (error) { } catch (error) {
@ -1864,6 +1873,8 @@ const useSuperAgentChat = ({
setTaskStage, setTaskStage,
conversationMessages, conversationMessages,
uploadedFileList, uploadedFileList,
setUploadedFileList,
conversationAttachments,
uploadProgress, uploadProgress,
ordinaryChatList, ordinaryChatList,
sidebarError, sidebarError,

33
web/client/src/sections/superAgent/utils/session.js

@ -523,7 +523,7 @@ const getHistoryMessageContent = (item = {}, checkpoint = null) => {
return fileReferenceContent ?? contentCandidates[0] ?? ""; return fileReferenceContent ?? contentCandidates[0] ?? "";
}; };
const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0) => { const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0, attachmentCatalog = []) => {
const rawFileList = [ const rawFileList = [
item.files, item.files,
item.file_list, item.file_list,
@ -536,8 +536,23 @@ const restoreHistoryFileReferences = (content, item = {}, messageIndex = 0) => {
item.additional_kwargs?.files, item.additional_kwargs?.files,
item.additional_kwargs?.attachments, item.additional_kwargs?.attachments,
].find(Array.isArray) || []; ].find(Array.isArray) || [];
const fileList = Array.isArray(rawFileList) const messageAttachmentIds = [
? rawFileList.map((rawFile, index) => { 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 file = typeof rawFile === "string" ? { url: rawFile } : rawFile || {};
const url = const url =
file.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( const fileNameSet = new Set(
fileList.map((file) => normalizeFileNameKey(file.name)).filter(Boolean) 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 回填到对应调用,缺少结果时仍保留调用参数] //[历史 tool 结果按 call id 回填到对应调用,缺少结果时仍保留调用参数]
const toolResultMap = new Map(); const toolResultMap = new Map();
messages.forEach((item) => { messages.forEach((item) => {
@ -874,8 +893,8 @@ export const normalizeHistoryMessages = (messages = [], sessionId = "") => {
checkpoint && sessionId && checkpointId checkpoint && sessionId && checkpointId
? `state_checkpoint_${sessionId}_${checkpointId}` ? `state_checkpoint_${sessionId}_${checkpointId}`
: ""; : "";
const historyFileData = isUser const historyFileData = isUser
? restoreHistoryFileReferences(rawContent, item, index) ? restoreHistoryFileReferences(rawContent, item, index, attachmentCatalog)
: { files: [], content: rawContent }; : { files: [], content: rawContent };
const isCollapsedAfterRespond = [ const isCollapsedAfterRespond = [
"confirmed", "confirmed",

67
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([ const ACTIVE_TOOL_STATUSES = new Set([
"running", "running",
"pending", "pending",
@ -233,6 +296,7 @@ export const buildStreamConversationMessages = (
const normalizedStreamTools = (Array.isArray(toolCalls) ? toolCalls : []).map(normalizeToolCall); const normalizedStreamTools = (Array.isArray(toolCalls) ? toolCalls : []).map(normalizeToolCall);
const normalizedMessageTools = getMessageToolCalls(messages).map(normalizeToolCall); const normalizedMessageTools = getMessageToolCalls(messages).map(normalizeToolCall);
const normalizedTools = mergeToolCalls(normalizedStreamTools, normalizedMessageTools); const normalizedTools = mergeToolCalls(normalizedStreamTools, normalizedMessageTools);
const attachmentCatalog = options.attachments || options.attachmentCatalog || [];
const displayMessages = (Array.isArray(messages) ? messages : []).reduce((result, message, index) => { const displayMessages = (Array.isArray(messages) ? messages : []).reduce((result, message, index) => {
const type = readMessageType(message); const type = readMessageType(message);
if (type === "tool") return result; if (type === "tool") return result;
@ -253,12 +317,13 @@ export const buildStreamConversationMessages = (
(tool) => messageToolIds.has(tool.id) || (tool.messageId && tool.messageId === id) (tool) => messageToolIds.has(tool.id) || (tool.messageId && tool.messageId === id)
); );
const normalizedContent = normalizeMessageContent(message?.content); const normalizedContent = normalizeMessageContent(message?.content);
const attachmentFiles = normalizeMessageAttachments(message, attachmentCatalog);
result.push({ result.push({
key: id, key: id,
id, id,
role: type === "human" || type === "user" ? "user" : "assistant", role: type === "human" || type === "user" ? "user" : "assistant",
content: normalizedContent.content, content: normalizedContent.content,
files: normalizedContent.files, files: mergeMessageFiles(normalizedContent.files, attachmentFiles),
toolCalls: attachedTools, toolCalls: attachedTools,
runLog: attachedTools.length runLog: attachedTools.length
? buildToolRunLog(attachedTools, options) ? buildToolRunLog(attachedTools, options)

1
web/tests/superAgentFlowContract.test.mjs

@ -34,6 +34,7 @@ assert.match(streamHook, /role:\s*"user"/);
assert.doesNotMatch(streamHook, /type:\s*"human"/); assert.doesNotMatch(streamHook, /type:\s*"human"/);
assert.match(streamHook, /const state = await super\.getState\(\)/); assert.match(streamHook, /const state = await super\.getState\(\)/);
assert.match(streamHook, /hydratedState\?\.values\?\.messages/); assert.match(streamHook, /hydratedState\?\.values\?\.messages/);
assert.match(container, /attachments: conversationAttachments/);
assert.match(streamHook, /const stopRun =/); assert.match(streamHook, /const stopRun =/);
assert.match(streamHook, /await stream\.stop\(\)/); assert.match(streamHook, /await stream\.stop\(\)/);
assert.match(streamHook, /disconnect:\s*stream\.disconnect/); assert.match(streamHook, /disconnect:\s*stream\.disconnect/);

Loading…
Cancel
Save