diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 4f60749bb..6329638a0 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -74,7 +74,7 @@ - 附件选择弹窗使用 `PlatformToolModalShell` 承接 portal 主题变量和不透明 panel 背景;不能直接把未注入 `platform-theme` 的 `UnifiedModal` portal 到 `document.body`,否则 `--platform-modal-fill` 失效后面板会变透明。 - 应用后附件以胶囊 chip 挂在输入框上方;发出的消息内附件渲染为纯文本胶囊 chip(名称 + 小图标),**默认无缩略图,鼠标悬浮才浮出缩略图预览**。 - 附件领域形状:统一为画布资源 / 素材库对象引用(`resourceId` / `assetId` + 可选 `objectKey`),不存在只属于对话的第三种图;单条消息上限 9 张(前后端共同校验)。前端可携带展示用 `imageSrc` / `thumbnailSrc`,后端必须按当前工程和当前账号重新归一、校验归属与 `objectKey`。 -- 输入区附件临时状态统一收口到 `useConversationAttachments`,选择弹窗由独立的 `AttachmentPicker` 负责纯展示;选择、引用、粘贴上传完成、移除、发送清空和失败恢复都必须经同一最新状态更新入口。发送失败时,已发送附件必须与等待期间新增的附件去重合并,不得因输入区已非空而丢弃;合并后超过 9 张时优先保留等待期间的最新附件,不恢复失败请求的附件,并立即显示上限错误。异步粘贴完成时基于当时的最新附件去重并重新校验 9 张上限,不能用上传开始时捕获的旧列表覆盖期间新增的引用。 +- 输入区附件临时状态统一收口到 `useConversationAttachments`,选择弹窗由独立的 `AttachmentPicker` 负责纯展示;选择、引用、粘贴上传完成、移除、发送清空和失败恢复都必须经同一最新状态更新入口。引用历史消息附件时先保留消息中的展示快照,最终发送前再按 `source + referenceId` 从当前画布和素材库选项刷新,避免提前刷新后又被失败恢复的旧快照覆盖。发送失败时,已发送附件必须与等待期间新增的附件去重合并,不得因输入区已非空而丢弃;合并后超过 9 张时优先保留等待期间的最新附件,不恢复失败请求的附件,并立即显示上限错误。异步粘贴完成时基于当时的最新附件去重并重新校验 9 张上限,不能用上传开始时捕获的旧列表覆盖期间新增的引用。 ## 工具调用确认展示契约 diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx index a275dbb4b..4f49fd310 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -617,7 +617,7 @@ describe('EditorAgentConversationPanelView', () => { }); }); - it('prefers the current attachment matched by source and reference id', async () => { + it('refreshes an attachment matched by source and reference id when sending', async () => { const client = createClient(); vi.mocked(client.getConversation).mockResolvedValue({ conversationId: 'conversation-1', @@ -673,7 +673,10 @@ describe('EditorAgentConversationPanelView', () => { const historicalAttachment = await screen.findByText('旧版附件'); fireEvent.contextMenu(historicalAttachment.closest('.group')!); fireEvent.click(screen.getByRole('menuitem', { name: '引用' })); - expect(await screen.findByText('最新附件')).toBeTruthy(); + await waitFor(() => { + expect(screen.getAllByText('旧版附件')).toHaveLength(2); + }); + expect(screen.queryByText('最新附件')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '发送' })); await waitFor(() => { diff --git a/src/components/image-editor/EditorAgentConversation/useConversationAttachments.ts b/src/components/image-editor/EditorAgentConversation/useConversationAttachments.ts index 5254cc524..130b095e0 100644 --- a/src/components/image-editor/EditorAgentConversation/useConversationAttachments.ts +++ b/src/components/image-editor/EditorAgentConversation/useConversationAttachments.ts @@ -259,22 +259,22 @@ export function useConversationAttachments({ const referenceContextAsset = useCallback( (asset: EditorAgentContextAsset) => { - const directAttachment: EditorAgentAttachmentRef | undefined = - asset.kind === 'attachment' - ? { - source: asset.source, - referenceId: asset.referenceId, - objectKey: asset.objectKey, - imageSrc: asset.imageSrc, - thumbnailSrc: asset.thumbnailSrc, - label: asset.label, - width: asset.width, - height: asset.height, - } - : undefined; - const referenceOption = directAttachment - ? attachmentOptionsByKey.get(attachmentKey(directAttachment)) - : undefined; + if (asset.kind === 'attachment') { + const directAttachment: EditorAgentAttachmentRef = { + source: asset.source, + referenceId: asset.referenceId, + objectKey: asset.objectKey, + imageSrc: asset.imageSrc, + thumbnailSrc: asset.thumbnailSrc, + label: asset.label, + width: asset.width, + height: asset.height, + }; + return directAttachment.referenceId.trim() && + directAttachment.imageSrc.trim() + ? appendAttachments([directAttachment]) + : false; + } const objectKey = asset.objectKey?.trim(); const mediaSrc = contextAssetMediaSrc(asset).trim(); const objectKeyOption = objectKey @@ -290,14 +290,13 @@ export function useConversationAttachments({ attachment.thumbnailSrc?.trim() === mediaSrc ); }); - const matchedAttachment = - referenceOption?.attachment || option?.attachment || directAttachment; + const matchedAttachment = option?.attachment; return matchedAttachment?.referenceId.trim() && matchedAttachment.imageSrc.trim() ? appendAttachments([matchedAttachment]) : false; }, - [appendAttachments, attachmentOptions, attachmentOptionsByKey], + [appendAttachments, attachmentOptions], ); const handleInputPaste = useCallback( @@ -361,9 +360,14 @@ export function useConversationAttachments({ const consumeAttachments = useCallback(() => { const currentAttachments = attachmentsRef.current; + const refreshedAttachments = currentAttachments.map( + (attachment) => + attachmentOptionsByKey.get(attachmentKey(attachment))?.attachment ?? + attachment, + ); updateAttachments([]); - return currentAttachments; - }, [updateAttachments]); + return refreshedAttachments; + }, [attachmentOptionsByKey, updateAttachments]); const restoreAttachments = useCallback( (failedAttachments: EditorAgentAttachmentRef[]) => {