From 8706e6d9af12ee10de820602368c0250b7b62e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 31 Jul 2026 15:26:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E7=9B=B4=E6=8E=A5=E8=81=9A=E7=84=A6=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84toolcall=20status=E7=AD=89,=20=E7=94=A8switch?= =?UTF-8?q?=E4=BB=A3=E6=9B=BF=E5=B5=8C=E5=A5=97=E7=9A=84=E4=B8=89=E5=85=83?= =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared-memory/decision-log.md | 10 ++ ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 2 +- .../【编辑器】画布Agent对话面板-2026-07-03.md | 6 +- .../MessageBubble.test.tsx | 7 +- .../ToolCallView.test.tsx | 6 +- .../EditorAgentConversation/ToolCallView.tsx | 101 ++++++++++++------ 6 files changed, 89 insertions(+), 43 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3492a8bda..3c2fc3002 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -38,6 +38,16 @@ --- +## 2026-07-31 画布 Agent 图片结果单击直接定位 + +- 背景:生成图片在对话中是静态缩略图,用户需要用更直接的方式回到对应画布图层;视频和音频仍有播放、拖动和音量等原生点击交互,不能共用该行为。 +- 决策:携带有效 `resourceId` 的 Agent 生成图片在普通单击时直接调用实例级 `ImageCanvasActionsContext.focusResource(resourceId)`;无 `resourceId` 的旧图片保持无动作。视频和音频的普通点击仍只操作播放器,三类媒体均保留右键菜单的“在画布中定位”。图片卡片本次不新增按钮语义或键盘 Tab 停靠,Enter / Space 不触发定位。 +- 影响范围:`ToolCallView`、消息气泡交互测试和画布 Agent 前端专题文档;不修改共享 DTO、后端 API、SpacetimeDB 或 viewport 动画语义。 +- 验证方式:覆盖有效图片单击、旧图片无动作、视频 / 音频单击无定位及三类媒体右键定位;运行前端定向测试、`npm run typecheck`、`npm run lint:eslint`、`npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/【编辑器】画布Agent对话面板-2026-07-03.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 + +--- + ## 2026-07-30 画布 Agent 结果通过实例级 Action Context 刷新并从右键菜单定位资源 - 背景:画布 Agent 图片、视频和音频结果需要提供画布定位和结果完成后的工程刷新;若继续从舞台向面板、消息和工具结果逐层传 callback,会扩大现有 prop drilling,而把 callback 或瞬时命令放入全局 Zustand 又会引入多实例和卸载残留问题。直接把点击和定位语义附到生成媒体上还会让视频 / 音频的播放、暂停、拖动与音量操作误触发画布聚焦,并给原生媒体控件附加错误的定位标签。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 5c4456959..bcb5dad85 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -153,7 +153,7 @@ - Agent 面板能读取当前工程会话列表;无历史会话时发送第一条消息会先创建“新对话”。支持新建会话、切换会话和删除当前会话;删除必须通过独立确认弹窗完成,不能在面板下方追加确认内容。 - Agent 输入支持文本消息和带附件的文本消息;不支持纯附件消息。附件选择弹窗可在“画布 / 素材库”之间切换,只展示图片类资源,最多选择 9 张。 - 发送消息后,面板先展示本地用户消息和请求等待态,再应用普通 JSON 响应中的 `deltaMessages`;客户端取消等待只终止本次 transport 等待,不把已经确认入队的外部生成任务改成停止态。 -- Agent 工具任务完成并懒回填后,消息内缩略图不显示名称;前端通过编辑器作用域 Action Context 的 `refreshCanvas()` 直接重新读取工程快照和素材库,不从 Editor 经 Stage、Panel 和 MessageBubble 透传刷新 callback。图片、视频和音频结果携带有效 `resourceId` 时,在素材右键菜单显示“在画布中定位”,并通过同一 Context 的 `focusResource(resourceId)` 请求画布在 `420ms` 内平滑 fit 到对应图层;普通媒体卡片不声明按钮语义或 `tabIndex`,点击、Enter 和 Space 均不得触发定位,视频和音频的原生播放器交互保持独立。定位只改变 viewport,不选择图层、不切换工具或侧栏、不收起 Agent 面板,也不避让面板覆盖区。缺少 `resourceId` 时不显示定位菜单项,目标图层已删除时保持无动作。对话入口触发生成时不创建“即将生成”画布占位,生成完成后由后端 `canvasCompletion` 落新图层。规划或工具失败时消息内必须保留可回读的失败状态和错误气泡,不能只弹一次性 toast 或返回瞬时 `errorMessage`。 +- Agent 工具任务完成并懒回填后,消息内缩略图不显示名称;前端通过编辑器作用域 Action Context 的 `refreshCanvas()` 直接重新读取工程快照和素材库,不从 Editor 经 Stage、Panel 和 MessageBubble 透传刷新 callback。图片、视频和音频结果携带有效 `resourceId` 时,在素材右键菜单显示“在画布中定位”;有效图片结果的普通单击也直接通过同一 Context 的 `focusResource(resourceId)` 请求画布在 `420ms` 内平滑 fit 到对应图层。结果卡片不声明按钮语义或 `tabIndex`,Enter 和 Space 不得触发定位;视频和音频的普通点击及原生播放器交互保持独立。定位只改变 viewport,不选择图层、不切换工具或侧栏、不收起 Agent 面板,也不避让面板覆盖区。缺少 `resourceId` 时单击无动作且不显示定位菜单项,目标图层已删除时保持无动作。对话入口触发生成时不创建“即将生成”画布占位,生成完成后由后端 `canvasCompletion` 落新图层。规划或工具失败时消息内必须保留可回读的失败状态和错误气泡,不能只弹一次性 toast 或返回瞬时 `errorMessage`。 - 画布 Agent 会话刷新后能从后端恢复会话标题、消息、附件和生成记录;前端不得根据本地临时状态伪造会话持久化结果。 - 图片选中后的浮动工具栏按钮顺序固定为:快速编辑、分割线、裁扩按钮、去除背景按钮、UI设计图专属提取素材、角色图专属生成动画、分割线、重绘、下载按钮。裁扩通过画布边界拖拉完成,不再展示四边数值输入;默认自由比例,选择固定比例后拖拉边界保持对应比例,完成后在原素材旁边新增裁扩结果图层,扩展区域透明填充。去除背景调用同源 BFF `POST /api/editor/images/background-removals`;父流程解析并校验私有 OSS object key 后只调用一次唯一内部 `bgfilter-worker` 的 complex 链路,子 worker 负责签发 600 秒 URL、`N / Q` 限流和最多两次顺序 provider attempt,complex 失败不接入 fallback,成功二进制返回后仍由父流程完成最终持久化。有项目上下文时先在画布创建关闭面板的去背景生成占位,完成后由后端通过 `canvasCompletion` 把新 project resource 写入该占位并返回快照,无占位上下文时才用新的 project resource 引用替换当前图层。画布任务侧栏按“排队/生成中”和“已完成”分页,生成中排在排队前,生成中耗时从任务开始时间戳实时计算,排队中不计时;进行中任务只显示阶段文本和已用时,不显示百分比;完成态生成任务副标题显示用户提示词并单行截断;点击任务只聚焦对应画布内容,不激活生成面板或改变任务顺序,聚焦时必须预留图片上方工具栏、底部工具栏和可见生成对话框空间。UI设计图的提取素材必须先进入红框素材框选状态,默认启用矩形框选,右侧框选工具与快速编辑统一且可再次点击取消启用态,当前启用工具按钮必须保持高亮。素材提取面板必须在素材下方,使用与生成新素材一致的面板宽度和底部模型 / 按钮样式,提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选区域截图预览、固定模型 `gpt-image-2`、左下角计划规格 `1:1·1K/2K` 和 `提取 · N泥点` 按钮,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。至少框选一个区域后才可提交,前端把红色轮廓绘入原图后固定走 `gpt-image-2` 和自动决策纯色背景素材提取提示词。透明处理及拆分正常完成时,透明 spritesheet 和拆分素材都按后端快照保留为画布图层;透明处理失败时仅原图作为主结果,既不要求透明图也不要求切片;透明图成功但拆分失败时保留整张透明图并展示拆分告警。三种完成结果都以后端项目快照为准。 - 重绘生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图,且重绘面板保持打开。 diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 84a5f4f89..0ea41ceb5 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -50,7 +50,7 @@ ## 生成结果落画板(对现有占位规则的例外) - 对话入口触发的生成**不创建"即将生成"画布占位**(区别于其余生成面板);生成中状态由工具消息和外部任务状态承载。 -- 生成完成后:结果图按统一 placement 避让模型(视口中心就近、避开现有图层、32px 间距)落画板为新图层,同时登记到默认项目素材库;前端轮询到任务终态或收到已经包含结果媒体的会话增量时,直接通过编辑器作用域 `ImageCanvasActionsContext.refreshCanvas()` 重新读取工程快照并刷新素材库,不经过 Stage、Agent 面板或消息组件逐层透传 callback。重新读取会话后,以回填的轻量媒体引用显示缩略图。图片、视频或音频结果携带有效 `resourceId` 时,右键素材后通过“在画布中定位”调用同一 Context 的 `focusResource(resourceId)`,请求画布视口在 `420ms` 内平滑 fit 到对应图层;普通媒体卡片不承载按钮语义、不进入键盘 Tab 顺序,普通点击以及 Enter / Space 均不触发画布聚焦,视频和音频的播放、暂停、拖动、音量等原生控制只操作播放器。定位不选中图层、不切换工具或侧栏、不收起 Agent 面板,也不为 Agent 面板预留可见区域。 +- 生成完成后:结果图按统一 placement 避让模型(视口中心就近、避开现有图层、32px 间距)落画板为新图层,同时登记到默认项目素材库;前端轮询到任务终态或收到已经包含结果媒体的会话增量时,直接通过编辑器作用域 `ImageCanvasActionsContext.refreshCanvas()` 重新读取工程快照并刷新素材库,不经过 Stage、Agent 面板或消息组件逐层透传 callback。重新读取会话后,以回填的轻量媒体引用显示缩略图。携带有效 `resourceId` 的图片结果在普通单击时直接调用同一 Context 的 `focusResource(resourceId)`;图片、视频或音频结果仍可从右键素材菜单选择“在画布中定位”。两种入口都请求画布视口在 `420ms` 内平滑 fit 到对应图层;结果卡片不进入键盘 Tab 顺序,Enter / Space 不触发画布聚焦,视频和音频的点击、播放、暂停、拖动、音量等原生交互只操作播放器。定位不选中图层、不切换工具或侧栏、不收起 Agent 面板,也不为 Agent 面板预留可见区域。 - 消息内生成结果缩略图必须携带并优先使用 `objectKey` / `assetObjectId`,前端通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签后渲染,不能把裸 `/generated-*` 私有路径直接交给 ``。 - 既有编辑器 worker 通过 `canvasCompletion` 写回工程快照;刷新后由 external generation task 状态和会话懒回填恢复结果。 - 该例外已同步登记在《生成类面板Lovart统一改造方案-2026-06-17》「画布占位落点」节。 @@ -126,7 +126,7 @@ 1. 助手文本随普通 JSON 消息响应一次性返回; 2. 消息请求等待态,以及工具任务的待确认、生成中、完成 / 失败状态; 3. 工具/模型标注行(生成时显示模型名 + 图标); -4. 消息内生成结果缩略图不显示名称;携带有效 `resourceId` 时可从素材右键菜单选择“在画布中定位”,以平滑视口动画定位对应画布图层;普通点击只保留媒体自身交互; +4. 消息内生成结果缩略图不显示名称;携带有效 `resourceId` 的图片可单击定位,图片、视频和音频都可从素材右键菜单选择“在画布中定位”,以平滑视口动画定位对应画布图层;视频和音频的普通点击只保留媒体自身交互; 5. 生成中的进行中动画; 6. 错误气泡(失败/余额不足,带原因); 7. 普通消息请求等待期间禁用发送按钮,不提供客户端停止操作;前端持续等待后端响应,超过 120 秒但 POST 仍 pending 时在思考气泡中显示“仍在处理中,请耐心等待”,最终成功或失败后自动移除,避免后端已持久化消息但前端中断请求后产生会话状态错位。 @@ -172,6 +172,6 @@ - 打开画布 Agent 后任务侧栏应关闭,再次打开任务侧栏时 Agent 面板应关闭;素材 / 图层面板与 Agent 可同时展开,互不改写开关状态。 - 发送消息时先本地追加用户消息,再应用 JSON 响应中的 `deltaMessages`;请求等待期间发送按钮保持禁用,前端不主动中断当前回合。 -- Agent 消息内生成结果缩略图不显示名称;轮询到任务终态并完成会话懒回填后,通过实例级 `refreshCanvas()` 统一刷新工程快照和素材库,禁止重新引入中间刷新 props。只允许从有效结果的素材右键菜单触发画布定位,普通点击以及视频 / 音频原生播放器操作不得触发;定位时必须保持 Agent 面板、当前图层选择、工具和侧栏不变,只平滑调整 viewport 到对应图层;旧消息缺少 `resourceId` 或目标图层已删除时不显示定位菜单项。 +- Agent 消息内生成结果缩略图不显示名称;轮询到任务终态并完成会话懒回填后,通过实例级 `refreshCanvas()` 统一刷新工程快照和素材库,禁止重新引入中间刷新 props。有效图片结果的普通单击和三类媒体的“在画布中定位”右键菜单项都可触发画布定位;视频 / 音频普通点击与原生播放器操作不得触发。定位时必须保持 Agent 面板、当前图层选择、工具和侧栏不变,只平滑调整 viewport 到对应图层;旧消息缺少 `resourceId` 时单击无动作且不显示定位菜单项,目标图层已删除时保持无动作。 - 对话内容可被用户选中复制;用户从输入框或对话内容点击回画布图层 / 生成器时,焦点应回到画布对象,Backspace / Delete 等画布快捷键继续生效。 - 对话正文右键菜单只复制当前气泡展示的完整文本,隐藏的内部 system 文本不得进入菜单;素材右键菜单优先于正文菜单,私有素材继续通过既有读取链路换签或代理下载,不复制会过期的临时链接。 diff --git a/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx b/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx index 9c5371d5d..3608746d6 100644 --- a/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx @@ -387,7 +387,7 @@ describe('MessageBubble', () => { expect(screen.getByRole('menuitem', { name: '下载图片' })).toBeTruthy(); }); - it('focuses generated image, video and audio only from their right-click menus', async () => { + it('focuses generated images on click and all generated media from their right-click menus', async () => { const { container } = renderMessage({ id: 9, role: 'system', @@ -431,7 +431,8 @@ describe('MessageBubble', () => { fireEvent.click(imageCard!); fireEvent.click(video!); fireEvent.click(audio!); - expect(focusResourceMock).not.toHaveBeenCalled(); + expect(focusResourceMock).toHaveBeenCalledTimes(1); + expect(focusResourceMock).toHaveBeenCalledWith('resource-image'); const targets = [ [imageCard, 'resource-image'], @@ -448,7 +449,7 @@ describe('MessageBubble', () => { ).toBeNull(), ); } - expect(focusResourceMock).toHaveBeenCalledTimes(3); + expect(focusResourceMock).toHaveBeenCalledTimes(4); focusResourceMock.mockReturnValueOnce({ found: false }); fireEvent.contextMenu(imageCard!, { clientX: 30, clientY: 40 }); diff --git a/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx b/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx index 778e8a7e1..616924aaa 100644 --- a/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx @@ -176,7 +176,7 @@ describe('ToolCallView', () => { expect(refreshCanvasMock).toHaveBeenCalledTimes(1); }); - it('keeps media clicks passive and exposes canvas resources to the right-click menu', () => { + it('focuses image clicks only and exposes canvas resources to the right-click menu', () => { const onRightClickMenu = vi.fn(); const { container } = renderWithCanvasActions( { fireEvent.click(imageCards[0]!); fireEvent.keyDown(imageCards[0]!, { key: 'Enter' }); fireEvent.keyDown(imageCards[0]!, { key: ' ', code: 'Space' }); + fireEvent.click(imageCards[1]!); fireEvent.click(video!); fireEvent.click(audio!); - expect(focusResourceMock).not.toHaveBeenCalled(); + expect(focusResourceMock).toHaveBeenCalledTimes(1); + expect(focusResourceMock).toHaveBeenCalledWith('resource-image'); expect( screen.queryByRole('button', { name: '在画布中定位Agent生成图片-1', diff --git a/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx b/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx index 63462e073..fc86e0f9d 100644 --- a/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx +++ b/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx @@ -11,6 +11,57 @@ import { getExternalGenerationJobStatus } from '@/src/services/external-generati import type { RightClickMenuHandler } from './common.ts'; +type ToolCallDisplayStatus = 'pending' | 'completed' | 'failed' | 'cancelled'; + +function getInitialDisplayStatus( + status: EditorAgentToolCall['status'], +): ToolCallDisplayStatus { + switch (status) { + case 'completed': + case 'failed': + case 'cancelled': + return status; + case 'not_completed': + return 'pending'; + } +} + +function getStatusPresentation( + displayStatus: ToolCallDisplayStatus, + hasJobId: boolean, +) { + switch (displayStatus) { + case 'completed': + return { + label: '已完成', + icon: