editor agent: add attachment count limit for recovery
This commit is contained in:
@@ -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 张上限,不能用上传开始时捕获的旧列表覆盖期间新增的引用。
|
||||
- 输入区附件临时状态统一收口到 `useConversationAttachments`,选择弹窗由独立的 `AttachmentPicker` 负责纯展示;选择、引用、粘贴上传完成、移除、发送清空和失败恢复都必须经同一最新状态更新入口。发送失败时,已发送附件必须与等待期间新增的附件去重合并,不得因输入区已非空而丢弃;合并后超过 9 张时优先保留等待期间的最新附件,不恢复失败请求的附件,并立即显示上限错误。异步粘贴完成时基于当时的最新附件去重并重新校验 9 张上限,不能用上传开始时捕获的旧列表覆盖期间新增的引用。
|
||||
|
||||
## 工具调用确认展示契约
|
||||
|
||||
|
||||
+94
@@ -1204,6 +1204,100 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
expect(screen.getByText('附件 B')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps the latest nine attachments when failed attachment recovery would exceed the limit', async () => {
|
||||
const client = createClient();
|
||||
let rejectSend!: (reason?: unknown) => void;
|
||||
vi.mocked(client.sendMessage).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<EditorAgentMessageResponse>((_resolve, reject) => {
|
||||
rejectSend = reject;
|
||||
}),
|
||||
);
|
||||
const layers = Array.from({ length: 18 }, (_, index) => ({
|
||||
id: `layer-${index + 1}`,
|
||||
resourceId: `resource-${index + 1}`,
|
||||
title: `附件-${index + 1}`,
|
||||
src: `/generated-editor-images/attachment-${index + 1}.png`,
|
||||
objectKey: `editor/attachment-${index + 1}.png`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
originalWidth: 512,
|
||||
originalHeight: 512,
|
||||
zIndex: index + 1,
|
||||
sourceType: 'generated' as const,
|
||||
}));
|
||||
|
||||
render(
|
||||
<EditorAgentConversationPanelView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
client={client}
|
||||
layers={layers}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加附件' }));
|
||||
let attachmentDialog = screen.getByRole('dialog', {
|
||||
name: '选择图片附件',
|
||||
});
|
||||
for (let index = 1; index <= 9; index += 1) {
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('checkbox', {
|
||||
name: `选择画布图片 附件-${index}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('button', { name: '应用' }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(client.sendMessage).toHaveBeenCalledWith(
|
||||
'conversation-1',
|
||||
expect.objectContaining({ attachments: expect.any(Array) }),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(
|
||||
vi.mocked(client.sendMessage).mock.calls[0]?.[1].attachments,
|
||||
).toHaveLength(9);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加附件' }));
|
||||
attachmentDialog = screen.getByRole('dialog', {
|
||||
name: '选择图片附件',
|
||||
});
|
||||
for (let index = 10; index <= 18; index += 1) {
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('checkbox', {
|
||||
name: `选择画布图片 附件-${index}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('button', { name: '应用' }),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
rejectSend(new Error('Network error'));
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText('最多 9 张,发送失败的附件未恢复'),
|
||||
).toBeTruthy();
|
||||
for (let index = 1; index <= 9; index += 1) {
|
||||
expect(screen.queryByText(`附件-${index}`)).toBeNull();
|
||||
}
|
||||
for (let index = 10; index <= 18; index += 1) {
|
||||
expect(screen.getByText(`附件-${index}`)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves messages when the panel is collapsed and reopened', async () => {
|
||||
const client = createClient();
|
||||
vi.mocked(client.sendMessage).mockResolvedValue({
|
||||
|
||||
@@ -367,9 +367,20 @@ export function useConversationAttachments({
|
||||
|
||||
const restoreAttachments = useCallback(
|
||||
(failedAttachments: EditorAgentAttachmentRef[]) => {
|
||||
updateAttachments((currentAttachments) =>
|
||||
mergeAttachments(failedAttachments, currentAttachments),
|
||||
);
|
||||
updateAttachments((currentAttachments) => {
|
||||
const restoredAttachments = mergeAttachments(
|
||||
failedAttachments,
|
||||
currentAttachments,
|
||||
);
|
||||
if (restoredAttachments.length > EDITOR_AGENT_MAX_ATTACHMENTS) {
|
||||
setAttachmentError(
|
||||
`最多 ${EDITOR_AGENT_MAX_ATTACHMENTS} 张,发送失败的附件未恢复`,
|
||||
);
|
||||
return currentAttachments;
|
||||
}
|
||||
setAttachmentError(null);
|
||||
return restoredAttachments;
|
||||
});
|
||||
},
|
||||
[updateAttachments],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user