diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 51eb670df..34779120b 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -117,7 +117,7 @@ 5. 生成中的进行中动画; 6. 错误气泡(失败/余额不足,带原因); 7. 普通消息请求等待期间禁用发送按钮,不提供客户端停止操作;前端持续等待后端响应,避免后端已持久化消息但前端中断请求后产生会话状态错位。 -8. 桌面端右键消息正文可复制该条可见文本;右键消息附件或生成结果可下载素材,图片额外支持复制图片本体和“引用”到当前输入区。引用复用附件去重、9 张上限和发送链路;右键菜单只保留已有的 `objectKey` 与素材 `source`,引用时由输入区优先按 `objectKey`、缺失时按 `source` 从当前画布和素材库选项重新查询并构造 `EditorAgentAttachmentRef`,同时命中时画布优先。 +8. 桌面端右键消息正文可复制该条可见文本;右键消息附件或生成结果可下载素材,图片额外支持复制图片本体和“引用”到当前输入区。引用复用附件去重、9 张上限和发送链路; 9. 消息右键菜单遵循 Canva 式单实例交互:任一菜单已打开时,下一次右键必须先关闭旧菜单;新落点是消息正文或素材时再在新位置打开对应菜单,新落点没有右键动作时仅收起旧菜单,不允许多个消息菜单并存。复制、引用或下载成功后自动关闭菜单;失败时保留菜单和失败状态,避免错误无提示消失。 不做(明确排除,防止后人补齐): diff --git a/src/components/image-editor/EditorAgentConversation/AttachmentChip.tsx b/src/components/image-editor/EditorAgentConversation/AttachmentChip.tsx index b5596b7a5..b4e9cfaeb 100644 --- a/src/components/image-editor/EditorAgentConversation/AttachmentChip.tsx +++ b/src/components/image-editor/EditorAgentConversation/AttachmentChip.tsx @@ -22,9 +22,9 @@ function AttachmentChip({ onRightClickMenu ? (event) => onRightClickMenu(event, { + ...attachment, + kind: 'attachment', mediaType: 'image', - source: attachment.imageSrc, - objectKey: attachment.objectKey, suggestedFileName: label, }) : undefined diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx index 26e249099..ef29dcfb7 100644 --- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx @@ -531,6 +531,154 @@ describe('EditorAgentConversationPanelView', () => { expect(screen.queryByRole('option', { name: 'conversation-1' })).toBeNull(); }); + it('references a historical pasted attachment outside the current canvas', async () => { + const client = createClient(); + vi.mocked(client.getConversation).mockResolvedValue({ + conversationId: 'conversation-1', + projectId: 'project-1', + title: '历史附件', + messages: [ + { + id: 8, + role: 'user', + text: '参考这张粘贴图', + attachments: [ + { + source: 'canvas_resource', + referenceId: 'resource-historical-paste', + objectKey: 'editor/agent-paste/historical.png', + imageSrc: '/generated/historical-paste.png', + thumbnailSrc: null, + label: '历史粘贴图', + width: 320, + height: 240, + }, + ], + toolCall: null, + createdAt: '2026-07-20T00:00:00.000Z', + }, + ], + createdAt: '2026-07-20T00:00:00.000Z', + updatedAt: '2026-07-20T00:00:10.000Z', + }); + + render( + , + ); + + const historicalAttachment = await screen.findByText('历史粘贴图'); + fireEvent.contextMenu(historicalAttachment.closest('.group')!); + fireEvent.click(screen.getByRole('menuitem', { name: '引用' })); + await waitFor(() => { + expect(screen.getAllByText('历史粘贴图')).toHaveLength(2); + }); + + fireEvent.click(screen.getByRole('button', { name: '发送' })); + await waitFor(() => { + expect(client.sendMessage).toHaveBeenCalledWith( + 'conversation-1', + expect.objectContaining({ + attachments: [ + { + source: 'canvas_resource', + referenceId: 'resource-historical-paste', + objectKey: 'editor/agent-paste/historical.png', + imageSrc: '/generated/historical-paste.png', + thumbnailSrc: null, + label: '历史粘贴图', + width: 320, + height: 240, + }, + ], + }), + expect.any(Object), + ); + }); + }); + + it('prefers the current attachment matched by source and reference id', async () => { + const client = createClient(); + vi.mocked(client.getConversation).mockResolvedValue({ + conversationId: 'conversation-1', + projectId: 'project-1', + title: '附件更新', + messages: [ + { + id: 8, + role: 'user', + text: '参考旧版图片', + attachments: [ + { + source: 'canvas_resource', + referenceId: 'resource-updated', + objectKey: 'editor/old.png', + imageSrc: '/generated/old.png', + label: '旧版附件', + }, + ], + toolCall: null, + createdAt: '2026-07-20T00:00:00.000Z', + }, + ], + createdAt: '2026-07-20T00:00:00.000Z', + updatedAt: '2026-07-20T00:00:10.000Z', + }); + + render( + , + ); + + const historicalAttachment = await screen.findByText('旧版附件'); + fireEvent.contextMenu(historicalAttachment.closest('.group')!); + fireEvent.click(screen.getByRole('menuitem', { name: '引用' })); + expect(await screen.findByText('最新附件')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: '发送' })); + await waitFor(() => { + expect(client.sendMessage).toHaveBeenCalledWith( + 'conversation-1', + expect.objectContaining({ + attachments: [ + expect.objectContaining({ + source: 'canvas_resource', + referenceId: 'resource-updated', + objectKey: 'editor/current.png', + imageSrc: '/generated/current.png', + }), + ], + }), + expect.any(Object), + ); + }); + }); + it('keeps a referenced image when a pending paste upload finishes', async () => { const client = createClient(); vi.mocked(client.getConversation).mockResolvedValue({ diff --git a/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx b/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx index 6aec36f33..2d9866437 100644 --- a/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx @@ -284,7 +284,10 @@ describe('MessageBubble', () => { expect(onReferenceImage).toHaveBeenCalledWith( expect.objectContaining({ - source: '/generated-editor-images/reference.png', + kind: 'attachment', + source: 'canvas_resource', + referenceId: 'resource-reference-1', + imageSrc: '/generated-editor-images/reference.png', objectKey: 'editor/reference.png', }), ); @@ -339,7 +342,8 @@ describe('MessageBubble', () => { fireEvent.click(screen.getByRole('menuitem', { name: '引用' })); expect(onReferenceImage).toHaveBeenCalledWith( expect.objectContaining({ - source: '/generated-editor-images/generated.png', + kind: 'generated_media', + mediaSrc: '/generated-editor-images/generated.png', objectKey: 'editor/generated.png', }), ); diff --git a/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx b/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx index 0bbb3c367..743813b4f 100644 --- a/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx +++ b/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { + contextAssetMediaSrc, type EditorAgentContextAsset, EditorAgentRightClickAction, type RightClickMenuTarget, @@ -176,7 +177,7 @@ export function MessageBubbleRightClickMenu({ <> {target.asset.mediaType === 'image' ? ( <> - {target.asset.source.trim() ? ( + {contextAssetMediaSrc(target.asset).trim() ? (