From 4033b23149733480469af072b44713202bddda4e Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 15 Jul 2026 09:29:40 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=89=8B=E5=8A=A8=E5=8E=BB?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E5=85=A5=E9=98=9F=E5=93=8D=E5=BA=94=E5=A5=91?= =?UTF-8?q?=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一手动去背景客户端类型为 queueState 响应 移除前端同步完成分支并更新队列测试 修正文档中的接口响应说明 --- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 4 +- .../useImageCanvasGenerationWorkflow.test.tsx | 165 ++---------------- .../useImageCanvasGenerationWorkflow.ts | 67 ++----- .../image-editor/editorProjectClient.test.ts | 21 +-- .../image-editor/editorProjectClient.ts | 14 +- 5 files changed, 44 insertions(+), 227 deletions(-) diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index af3cec512..275449f3b 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -21,7 +21,7 @@ - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片,并提供“复制信息”复制当前可见字段。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 - 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。重绘 / 改造输入框只允许从 `generationInputs.fields` 中恢复用户可见输入快照,例如普通生成提示词、视频描述、音效 `prompt`、背景音乐 `gpt_description_prompt`、角色设定、UI 用户输入、图标素材描述、规范表单和宣发素材字段;禁止回退展示资源 `prompt` / `actualPrompt` 中的后端拼接 Prompt、固定生成模板或模型默认提示词。没有用户输入快照的旧图层打开改造时保持空输入,等待用户重新填写。 - 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑统一打开框选区域 + 单提示词 + 模型选择面板,默认沿用原图模型,不展示参考图或比例 / 尺寸控件;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型;两类面板不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`,由后端自动决策具体抠图背景色,`anime-seg` 作为内部保留能力不在用户界面暴露。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后走同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片源读取为图片 Data URL;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作的 `生成动画` 仍固定使用 `seedance2.0-fast` 动作 / 视频模型,角色动作素材的 `快速编辑` 按当前帧图片走图片编辑。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 -- 图片画布抠图统一使用 BgFilter 服务 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL/remove-background`,默认 `http://58.87.105.82/bgfilter/remove-background`,默认请求超时 `180000ms`(BgFilter CPU 推理)。手动去除背景面向用户任意图片,仍走登录态同源 BFF `POST /api/editor/images/background-removals` 和外部生成队列;worker 固定提交 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不提交 `screen_color`。首次请求失败后立即重试 `1` 次,两次都失败则返回最终错误;manual complex 不接入依赖纯色键值的阿里云 / 本地键色降级链,也不改变 flat 链路的熔断状态。手动与标准纯色背景两类模式共用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`、`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 和共享 HTTP client;BgFilter token 未配置时只兼容回退读取旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。所有令牌都只在服务端注入,前端不持有令牌。api-server 对上游结果做响应字节和图片尺寸上限保护,并先落 OSS / asset object,再返回 `imageSrc/objectKey/assetObjectId/taskId`;有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端,完成后由后端写入结果图层和最新项目快照。 +- 图片画布抠图统一使用 BgFilter 服务 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL/remove-background`,默认 `http://58.87.105.82/bgfilter/remove-background`,默认请求超时 `180000ms`(BgFilter CPU 推理)。手动去除背景面向用户任意图片,仍走登录态同源 BFF `POST /api/editor/images/background-removals` 和外部生成队列;worker 固定提交 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不提交 `screen_color`。首次请求失败后立即重试 `1` 次,两次都失败则返回最终错误;manual complex 不接入依赖纯色键值的阿里云 / 本地键色降级链,也不改变 flat 链路的熔断状态。手动与标准纯色背景两类模式共用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN`、`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 和共享 HTTP client;BgFilter token 未配置时只兼容回退读取旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。所有令牌都只在服务端注入,前端不持有令牌。worker 对上游结果做响应字节和图片尺寸上限保护,并先落 OSS / asset object;接口只返回 `queueState`,有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端,完成后由后端写入结果图层和最新项目快照。 - 编辑器自己生成的标准纯色背景抠图资产在保存源图后统一调用 BgFilter `background_mode=flat`。角色形象生成、图标 spritesheet 生成、UI 设计图素材提取和角色动作的前端用户路径都固定把 `screenColor=auto` 注入请求体,但用户可见 `generationInputs.fields` 不再记录 `抠图背景色` 或 `抠图模型`;api-server 在组装 prompt 前调用背景决策模块,从 12 个候选色中选择具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`。后端仍保留手动 hex 解析能力供内部兼容。最终生图 prompt、动作视频实色背景和 BgFilter `screen_color` multipart 字段只接收解析后的具体 hex,不透传 `auto`。四条 flat 路径同时把默认 `segModel=birefnet` 传为 `seg_model`,并显式传 `cross_check`:角色形象生成和角色动作逐帧去背传 `on`,图标 spritesheet 和 UI 设计图素材提取传 `off`,不依赖 BgFilter 服务端默认值;后端仍保留识别 `anime-seg` 的内部兼容能力,但前端用户入口不展示也不提交 `seg_model`、`background_mode` 或 `cross_check`。flat 请求首次失败后立即重试 `1` 次;第二次仍失败、返回非成功状态、空图片或非法图片时,以及连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 后的 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=300` 秒熔断期,api-server 都先调用阿里云通用抠图,只有阿里云失败才用本地 `editor_green_screen` 按同一 `screenColor` 兜底去背。角色动作生成的序列帧背景色已与生图统一:后端把源角色图合成到视觉决策出的具体 hex 后再图生视频;抽帧后逐帧进入同一条 `BgFilter(background_mode=flat,cross_check=on)→ 阿里云 → 本地键色` 链路。 - 角色动作逐帧抠图在 api-server 内复用共享 BgFilter HTTP Client;单帧首次失败立即重试 `1` 次,第二次仍失败才进入阿里云/本地降级链。全部 `32 / 40 / 48` 帧按“对应绿幕源图落 OSS → BgFilter/降级 → 透明帧落 OSS”连续加入无序在途流水线,允许响应乱序完成并在最终返回前按 `frameIndex` 恢复顺序;任一帧最终失败时仍排空全部已启动请求,整个动作任务失败退款,不发布缺帧动画。 - 多产物生成以后端项目快照为唯一画布真相:同一任务的原始产物、抠图 / 透明化结果和拆分结果都要先登记为 `editor_project_resource`,再通过一次 `canvasCompletion` 原子写入画布。角色形象、图标 spritesheet 和 UI 素材提取的纯色背景原图不能只留在 OSS;透明后处理结果保持主图层和 `generatedLayerId` 锚点,原图及其它附属产物从主结果右侧开始错开放置。无项目上下文时不创建项目资源或画布图层。 @@ -88,7 +88,7 @@ - `PATCH /api/editor/assets/{assetId}`:重命名素材或移动素材到文件夹。 - `DELETE /api/editor/assets/{assetId}`:删除素材。已放入画布的 project resource 不被级联删除,避免旧画布丢图。 - `POST /api/editor/images/generations`:按提示词调用 VectorEngine 生成图片;普通图片的 provider 回图先留在内存,尺寸变换成功后只上传变换结果,变换失败则只上传 provider 原图,主结果只写一次 OSS 且不额外创建“原始输出”。角色生成可携带 `model`、`screenColor`、`segModel`、`aspectRatio`、`imageSize` 和 `referenceImageSrcs`,生成成功后 api-server 先保存带纯色背景源图,再调用 BgFilter 并传入 `screen_color=`、`seg_model=` 生成透明 PNG。宣发素材携带 `kind: "publication-material"` 时固定归一为 `gpt-image-2`,不支持 `nanobanana2`。`nanobanana2` 参考图作为 `inline_data` 进入 `generateContent`,`gpt-image-2` 参考图进入 edits;`nanobanana2` 的 `512 / 1024 / 2K` 是标量清晰度档位,后端保留 provider 输出几何尺寸,不按 `宽x高` 解析。普通重绘继续走该接口并把当前图层图片作为参考图;图片快速编辑不走该接口。请求可携带 `projectId`、`assetFolderId`、`assetKind`、`generationInputs` 和 `sourceResourceId`,后端生成成功后创建 project resource / 账号素材并在响应中返回 resource / asset 快照。 -- `POST /api/editor/images/background-removals`:接收当前图片源,校验登录态后由 api-server 解析为图片文件,并通过共享 BgFilter HTTP client 调用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL/remove-background`;multipart 固定为 `file + background_mode=complex + seg_model=birefnet + cross_check=off`,不包含 `screen_color`,首次失败立即重试 `1` 次,两次都失败返回最终错误。请求可携带 `projectId`、`targetLayerId`、`assetFolderId`、`assetLabel`、`sourceResourceId` 和 `canvasCompletion`,有 `canvasCompletion` 时完成后按生成占位写入结果图层,否则沿用旧的目标图层替换路径;响应返回 `imageSrc`、`objectKey`、`assetObjectId`、`width`、`height`、`taskId`、`elapsedMs`、`provider: "BgFilter"` 和可选 `project` 快照。令牌只在服务端通过 `GENARRATIVE_EDITOR_BGFILTER_TOKEN` 注入,未配置时兼容回退旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。 +- `POST /api/editor/images/background-removals`:接收当前图片源,校验登录态后无条件创建外部生成任务,响应只返回 `queueState`。worker 由 api-server 解析图片文件,并通过共享 BgFilter HTTP client 调用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL/remove-background`;multipart 固定为 `file + background_mode=complex + seg_model=birefnet + cross_check=off`,不包含 `screen_color`,首次失败立即重试 `1` 次,两次都失败返回最终错误。请求可携带 `projectId`、`targetLayerId`、`assetFolderId`、`assetLabel`、`sourceResourceId` 和 `canvasCompletion`,有 `canvasCompletion` 时完成后按生成占位写入结果图层,否则沿用旧的目标图层替换路径。令牌只在服务端通过 `GENARRATIVE_EDITOR_BGFILTER_TOKEN` 注入,未配置时兼容回退旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN`。 - `POST /api/editor/icon-spritesheets/generations`:按图标规范图和素材描述数组生成 spritesheet,生成成功后 api-server 先保存带纯色背景 spritesheet 源图,再调用 BgFilter 生成透明 spritesheet。请求支持 `model`、`screenColor`、`segModel`、`aspectRatio`、`imageSize`、`priceMudPoints`、`projectId`、`assetFolderId` 和 `generationInputs`;`priceMudPoints` 必须来自编辑器生成计费配置中对应生图模型的尺寸档位(如 `nanobanana2` 的 `0.5K / 1K / 2K` 或 `gpt-image-2` 的 `1K / 2K`),后端用 `editor_generation_config` 校验后才调用上游;`nanobanana2` 走原生 `generateContent` 并写入 `generationConfig.imageConfig.aspectRatio/imageSize`,`0.5K` 传 `"512"`;`gpt-image-2` 走 `/v1/images/edits`。后端保存透明 spritesheet project resource / 账号素材,并随响应返回对应快照。 - `POST /api/editor/ui-designs/assets/extractions`:前端把红色框选轮廓绘入本地临时图后,先将该图上传 OSS 并确认 asset object,再以返回的 `objectKey` 作为参考图入队;Data URL / Blob URL 只允许停留在上传前的浏览器临时态。接口固定 `gpt-image-2` 和自动决策纯色背景素材提取提示词生成素材 spritesheet,生成成功后 api-server 先保存带纯色背景 spritesheet 源图,再调用 BgFilter 生成透明 spritesheet,并按连通域自动拆分为 `素材 1..N`,返回结构复用图标 spritesheet 响应。请求必须携带 `screenColor`、`segModel`、`aspectRatio: "1:1"`、`imageSize: "1K" | "2K"` 和 `priceMudPoints`;框选数量不超过 6 个时前端按 `1:1·1K` 与 gpt-image-2 1K 价格提交,超过 6 个时按 `1:1·2K` 与 2K 价格提交。后端必须在调用上游前校验比例、尺寸和泥点价格,只允许 `1:1 / 1K / 2K`。请求可携带 `projectId`、`assetFolderId`、`generationInputs` 和 `spritesheetLabel`,后端保存 spritesheet / 拆分素材并返回对应 resource / asset 快照;前端必须把 spritesheet 原图与拆分素材都加入画布。 - `POST /api/editor/images/edits`:按提示词和当前图片的已登记 `objectKey` / `resourceId` 调用 VectorEngine edits,返回新的生成图片元数据;图片快速编辑当前只提交 `sourceImageSrc`,不提交隐藏的 `referenceImageSrcs`。画布快速编辑必须把源图精确 `originalWidth x originalHeight` 作为业务目标 `size` 提交,不能重新映射为近似比例或 1K / 2K 预设;api-server 在 VectorEngine provider 边界把目标尺寸和所有 multipart 参考图临时补齐到 16 的倍数,回图后在内存恢复业务目标尺寸,成功时只上传恢复结果,失败时只上传 provider 原图。无论是否发生尺寸恢复都只创建一个 project resource / 账号素材,不显示重复“原始输出”。16 对齐尺寸不得泄漏到正常完成的最终响应、资源或图层 Resolution;变换失败降级时以实际 provider 原图尺寸为准。本地红框标记图必须先上传再提交 objectKey;请求携带 project / asset 上下文时由后端创建新 resource / asset,前端只消费响应快照。 diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx index f35c01921..8ca83fe19 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx @@ -1868,21 +1868,19 @@ describe('useImageCanvasGenerationWorkflow', () => { expect(screen.getByTestId('sidebar').textContent).toBe('layers'); }); - it('resolves private character image before removing background', async () => { + it('queues background removal for private character images', async () => { resolveEditorImageReferenceDataUrlMock.mockResolvedValueOnce( 'data:image/png;base64,resolved-character', ); removeImageBackgroundMock.mockResolvedValueOnce({ - imageSrc: - '/generated-character-drafts/editor/background-removal/result.png', - objectKey: - 'generated-character-drafts/editor/background-removal/result.png', - assetObjectId: 'asset-object-background-removal', - width: 512, - height: 768, - taskId: 'background-removal-task', - elapsedMs: 1234, - provider: 'BgFilter', + queueState: { + operationId: 'background-removal-task', + status: 'completed', + phaseLabel: '已完成', + phaseDetail: '已完成', + progress: 100, + updatedAtMicros: 1, + }, }); render( { sourceResourceId: 'resource-source', }); }); + await waitFor(() => { + expect(screen.getByTestId('task-refresh-key').textContent).toBe('1'); + }); expect(screen.getByTestId('layers').textContent).toContain( - 'layer-source:源图:resource-source:character:/generated-character-drafts/editor/background-removal/result.png:background-removal-task', + '/api/assets/read-url?objectKey=generated-character-drafts/editor/private.png', + ); + expect(screen.getByTestId('layers').textContent).not.toContain( + 'background-removal-task', ); - expect(screen.getByTestId('task-refresh-key').textContent).toBe('0'); }); it('opens a generating canvas placeholder when removing background in a project', async () => { - let resolveBackgroundRemoval: ((value: unknown) => void) | null = null; removeImageBackgroundMock.mockImplementationOnce( - () => - new Promise((resolve) => { - resolveBackgroundRemoval = resolve; - }), + () => new Promise(() => {}), ); render( @@ -1960,136 +1959,6 @@ describe('useImageCanvasGenerationWorkflow', () => { }), ); expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled(); - await act(async () => { - resolveBackgroundRemoval?.({ - imageSrc: - '/generated-character-drafts/editor/background-removal/project-result.png', - objectKey: - 'generated-character-drafts/editor/background-removal/project-result.png', - assetObjectId: 'asset-object-background-removal', - width: 512, - height: 768, - taskId: 'background-removal-project-task', - elapsedMs: 1234, - provider: 'BgFilter', - project: null, - }); - }); - expect(screen.getByTestId('layers').textContent).not.toContain( - 'project-result.png', - ); - expect(screen.getByTestId('layers').textContent).not.toContain( - 'background-removal-project-task', - ); - }); - - it('preserves a just-created crop layer when applying a background removal project snapshot', async () => { - const applyProjectSnapshot = vi.fn(); - let resolveBackgroundRemoval: ((value: unknown) => void) | null = null; - removeImageBackgroundMock.mockImplementationOnce( - () => - new Promise((resolve) => { - resolveBackgroundRemoval = resolve; - }), - ); - render( - , - ); - - fireEvent.click(screen.getByRole('button', { name: '去除背景' })); - - await waitFor(() => expect(removeImageBackgroundMock).toHaveBeenCalled()); - await act(async () => { - resolveBackgroundRemoval?.({ - imageSrc: - '/generated-character-drafts/editor/background-removal/project-result.png', - objectKey: - 'generated-character-drafts/editor/background-removal/project-result.png', - assetObjectId: 'asset-object-background-removal', - width: 512, - height: 768, - taskId: 'background-removal-project-task', - elapsedMs: 1234, - provider: 'BgFilter', - project: { - projectId: 'project-1', - title: '队列项目', - viewport: { x: 0, y: 0, scale: 1 }, - layers: [ - { - layerId: 'layer-background-removal-result', - resourceId: 'resource-background-removal', - title: '源图 裁扩 去背景', - x: 480, - y: 140, - width: 512, - height: 768, - originalWidth: 512, - originalHeight: 768, - zIndex: 9, - sourceType: 'generated', - }, - ], - resources: [ - { - resourceId: 'resource-background-removal', - projectId: 'project-1', - imageSrc: - '/generated-character-drafts/editor/background-removal/project-result.png', - objectKey: - 'generated-character-drafts/editor/background-removal/project-result.png', - assetObjectId: 'asset-object-background-removal', - width: 512, - height: 768, - sourceType: 'generated', - sourceResourceId: 'resource-crop-expand', - }, - ], - updatedAt: '2026-06-23T08:00:01.000Z', - }, - }); - }); - - const appliedProject = applyProjectSnapshot.mock.calls[0]?.[0]; - expect(appliedProject?.layers).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - layerId: 'layer-crop-expand-1', - resourceId: 'resource-crop-expand', - title: '源图 裁扩', - }), - expect.objectContaining({ - layerId: 'layer-background-removal-result', - resourceId: 'resource-background-removal', - }), - ]), - ); - expect(appliedProject?.resources).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - resourceId: 'resource-crop-expand', - objectKey: - 'generated-character-drafts/editor/crop-expand/project-1/result.png', - assetObjectId: 'asset-object-crop-expand', - }), - ]), - ); }); it('opens UI design extraction as a mark selection state before submitting', () => { diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index 5e9c3af9c..e0c37d02e 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -1613,58 +1613,19 @@ export function useImageCanvasGenerationWorkflow({ } : {}), }); - if ( - await applyQueuedEditorGenerationProject( - result, - projectId, - applyProjectSnapshot, - refreshTaskListForQueuedGeneration, - onWalletBalanceMayHaveChanged, - setGenerationWarning, - backgroundRemovalDialogId, - applyBackgroundRemovalProjectSnapshot - ? (project) => - preserveSourceLayerInProjectSnapshot(project, sourceLayer) - : undefined, - ) - ) { - return; - } - if (result.project && applyBackgroundRemovalProjectSnapshot) { - applyBackgroundRemovalProjectSnapshot(result.project); - return; - } - if (backgroundRemovalPlacement?.placeholder && applyProjectSnapshot) { - return; - } - updateSourceLayer(sourceLayer.id, (layer) => ({ - ...layer, - resourceId: result.resource?.resourceId ?? layer.resourceId, - src: result.imageSrc, - width: result.width, - height: result.height, - originalWidth: result.width, - originalHeight: result.height, - sourceType: result.sourceType ?? 'generated', - provider: result.provider ?? 'BgFilter', - taskId: result.taskId ?? null, - objectKey: result.resource?.objectKey ?? result.objectKey ?? null, - assetObjectId: - result.resource?.assetObjectId ?? result.assetObjectId ?? null, - sourceResourceId: - result.resource?.sourceResourceId ?? sourceLayer.resourceId, - sourceAssetId: null, - prompt: result.resource?.prompt ?? layer.prompt, - actualPrompt: result.resource?.actualPrompt ?? layer.actualPrompt, - model: result.resource?.model ?? layer.model, - assetKind: - (result.resource?.assetKind as CanvasLayer['assetKind']) ?? - layer.assetKind, - generationInputs: - result.resource?.generationInputs ?? layer.generationInputs, - generatedAssetSnapshot: result.asset ?? layer.generatedAssetSnapshot, - })); - setActiveSidebarPanel('layers'); + await applyQueuedEditorGenerationProject( + result, + projectId, + applyProjectSnapshot, + refreshTaskListForQueuedGeneration, + onWalletBalanceMayHaveChanged, + setGenerationWarning, + backgroundRemovalDialogId, + applyBackgroundRemovalProjectSnapshot + ? (project) => + preserveSourceLayerInProjectSnapshot(project, sourceLayer) + : undefined, + ); } catch (error) { if (backgroundRemovalDialogId) { updateCanvasGenerationDialogById( @@ -1689,11 +1650,9 @@ export function useImageCanvasGenerationWorkflow({ openPlacedCanvasGenerationDialog, projectId, refreshTaskListForQueuedGeneration, - setActiveSidebarPanel, setImageContextMenu, setMetadataLayer, updateCanvasGenerationDialogById, - updateSourceLayer, ], ); diff --git a/src/services/image-editor/editorProjectClient.test.ts b/src/services/image-editor/editorProjectClient.test.ts index 0edc3658b..2e8da9ddb 100644 --- a/src/services/image-editor/editorProjectClient.test.ts +++ b/src/services/image-editor/editorProjectClient.test.ts @@ -1618,18 +1618,17 @@ describe('editorProjectClient', () => { it('passes canvas completion context to background removal', async () => { requestJsonMock.mockResolvedValueOnce({ - imageSrc: 'data:image/png;base64,cutout', - width: 512, - height: 512, - objectKey: 'generated-character-drafts/editor-cutouts/cutout.png', - assetObjectId: 'asset-object-cutout', - taskId: 'background-removal-1', - elapsedMs: 1200, - provider: 'BgFilter', - project: null, + queueState: { + operationId: 'background-removal-1', + status: 'queued', + phaseLabel: '排队中', + phaseDetail: '排队中', + progress: 0, + updatedAtMicros: 1, + }, }); - await removeEditorImageBackground({ + const response = await removeEditorImageBackground({ sourceImageSrc: 'data:image/png;base64,source', projectId: 'editor-project-1', targetLayerId: 'layer-source', @@ -1647,6 +1646,8 @@ describe('editorProjectClient', () => { }, }); + expect(response.queueState.operationId).toBe('background-removal-1'); + expect(requestJsonMock).toHaveBeenCalledWith( '/api/editor/images/background-removals', expect.objectContaining({ diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index d4d1e8360..15c72b3ad 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -277,19 +277,7 @@ export type EditorImageGenerationResult = { }; export type EditorBackgroundRemovalResult = { - imageSrc: string; - objectKey?: string | null; - assetObjectId?: string | null; - width: number; - height: number; - sourceType?: 'generated'; - taskId?: string | null; - elapsedMs?: number; - provider?: string; - resource?: EditorProjectResourceSnapshot | null; - asset?: EditorAssetSnapshot | null; - project?: EditorProjectSnapshot | null; - queueState?: ExternalGenerationJobStatusRecord | null; + queueState: ExternalGenerationJobStatusRecord; }; export type EditorIconSpritesheetIconResult = {