From 609b036b048ae6cdfb35bac679d275d7bbd76a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 7 Aug 2026 14:04:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=8C=89=E4=B8=9A=E5=8A=A1ID?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=9B=BE=E7=89=87=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优先使用图层 resourceId 并回退正式 sourceAssetId 未登记来源在上传标注图前失败关闭 保持红框标注图为辅助 referenceImageSrcs 而非主来源 移除图片编辑请求中的旧来源和类型字段并补齐工作流测试 --- ...ageCanvasGenerationSubmissionModel.test.ts | 28 ++++++++ .../ImageCanvasGenerationSubmissionModel.ts | 19 +++++ ...anvasGenerationSubmissionWorkflow.test.tsx | 70 ++++++++++++++++--- ...ImageCanvasGenerationSubmissionWorkflow.ts | 68 ++++++++++-------- .../image-editor/editorProjectClient.test.ts | 10 +-- .../image-editor/editorProjectClient.ts | 11 +-- 6 files changed, 150 insertions(+), 56 deletions(-) diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts index 55e55d23e..68d99f557 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts @@ -9,6 +9,7 @@ import { buildCharacterAnimationSubmissionPlan, buildIconSpritesheetGenerationSubmissionPlan, buildImageGenerationSubmissionPlan, + resolveRegisteredEditorReferenceId, } from './ImageCanvasGenerationSubmissionModel'; function createLayer(overrides: Partial = {}): CanvasLayer { @@ -30,6 +31,33 @@ function createLayer(overrides: Partial = {}): CanvasLayer { } describe('ImageCanvasGenerationSubmissionModel', () => { + it('resolves registered image-edit sources by resource ID first', () => { + expect( + resolveRegisteredEditorReferenceId({ + resourceId: ' resource-source ', + sourceAssetId: 'asset-source', + }), + ).toBe('resource-source'); + }); + + it('falls back to a registered asset ID for image edits', () => { + expect( + resolveRegisteredEditorReferenceId({ + resourceId: 'local-resource-source', + sourceAssetId: ' asset-source ', + }), + ).toBe('asset-source'); + }); + + it('rejects unregistered image-edit sources', () => { + expect(() => + resolveRegisteredEditorReferenceId({ + resourceId: 'generation-dialog:source', + sourceAssetId: 'upload-source', + }), + ).toThrow('原图尚未登记为项目资源或素材'); + }); + it('builds normal image generation submission plans', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts index e4168b4e7..28cbfaad5 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts @@ -91,6 +91,25 @@ export function resolveImageReferenceSubmissionSource(reference: { return reference.objectKey?.trim() || reference.src; } +export function resolveRegisteredEditorReferenceId(reference: { + resourceId?: string | null; + sourceAssetId?: string | null; +}) { + const resourceId = reference.resourceId?.trim(); + if ( + resourceId && + !resourceId.startsWith('local-resource-') && + !resourceId.startsWith('generation-dialog:') + ) { + return resourceId; + } + const assetId = reference.sourceAssetId?.trim(); + if (assetId && !assetId.startsWith('upload-')) { + return assetId; + } + throw new Error('原图尚未登记为项目资源或素材,请重新选择或上传后再试'); +} + function buildSeedanceVideoReferenceInput( references: NonNullable, ) { diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx index 08b9a96a9..2d0d97b9f 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -824,7 +824,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { expect(editEditorImageMock).toHaveBeenCalledWith( expect.objectContaining({ prompt: '快速修图', - sourceImageSrc: 'resource-source', + sourceReferenceId: 'resource-source', size: '2048x1365', model: 'gemini-3.1-flash-image-preview', aspectRatio: '3:2', @@ -940,6 +940,58 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { ); }); + it('rejects unregistered quick-edit sources before uploading marked references', async () => { + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' })); + + await waitFor(() => { + expect(screen.getByTestId('quick-edit').textContent).toContain( + 'failed:修改未登记图片:原图尚未登记为项目资源或素材', + ); + }); + expect(renderUiDesignAssetExtractionMarkedImageMock).not.toHaveBeenCalled(); + expect(uploadEditorMediaAssetObjectFileMock).not.toHaveBeenCalled(); + expect(editEditorImageMock).not.toHaveBeenCalled(); + }); + it('allows direct quick edit submission for icon spritesheets', async () => { editEditorImageMock.mockResolvedValueOnce( createGenerated({ prompt: '调整图集布局' }), @@ -1047,10 +1099,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { expect(editEditorImageMock).toHaveBeenCalledWith( expect.objectContaining({ prompt: '把当前图改成雨天', - sourceImageSrc: 'resource-source', + sourceReferenceId: 'resource-source', size: '1024x768', model: 'gpt-image-2', - sourceResourceId: 'resource-source', targetLayerId: 'layer-source', assetLabel: '源图 快速编辑', }), @@ -1201,8 +1252,10 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { ); expect(editEditorImageMock).toHaveBeenCalledWith( expect.objectContaining({ - sourceImageSrc: + sourceReferenceId: 'resource-source', + referenceImageSrcs: [ 'generated-character-drafts/editor/generation-references/reference.png', + ], targetLayerId: 'layer-source', }), ); @@ -1210,9 +1263,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { expect( uploadEditorMediaAssetObjectFileMock.mock.invocationCallOrder[0], ).toBeLessThan(editEditorImageMock.mock.invocationCallOrder[0] ?? 0); - expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty( - 'referenceImageSrcs', - ); expect(generateEditorImageMock).not.toHaveBeenCalled(); }); @@ -1291,8 +1341,10 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { await waitFor(() => { expect(editEditorImageMock).toHaveBeenCalledWith( expect.objectContaining({ - sourceImageSrc: + sourceReferenceId: 'resource-source', + referenceImageSrcs: [ 'generated-character-drafts/editor/generation-references/reference.png', + ], model: 'gpt-image-2', targetLayerId: 'layer-source', }), @@ -1632,7 +1684,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { expect(editEditorImageMock).toHaveBeenCalledWith( expect.objectContaining({ prompt: '修改当前图片', - sourceImageSrc: 'resource-source', + sourceReferenceId: 'resource-source', canvasCompletion: expect.objectContaining({ placeholder: { x: 472, diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts index a138e107c..dea85a697 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts @@ -82,6 +82,7 @@ import { buildIconSpritesheetGenerationSubmissionPlan, buildImageGenerationSubmissionPlan, resolveGenerationAssetLabel, + resolveRegisteredEditorReferenceId, } from './ImageCanvasGenerationSubmissionModel'; import type { UiAssetExtractionMark, @@ -1662,7 +1663,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({ quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id ? quickEditSelectionState.marks : []; - let sourceReferenceImageSrc: string; + const sourceReferenceId = + panelMode === 'quick-edit' + ? resolveRegisteredEditorReferenceId(quickEditSourceLayer) + : ''; + let markedReferenceImageSrc: string | null = null; if (quickEditSelectionMarks.length > 0) { const renderSourceImageSrc = await resolveEditorImageReferenceDataUrl( quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src, @@ -1673,17 +1678,23 @@ export function useImageCanvasGenerationSubmissionWorkflow({ marks: quickEditSelectionMarks, showIndexLabels: true, }); - sourceReferenceImageSrc = await uploadEditorGenerationImageDataUrl( + markedReferenceImageSrc = await uploadEditorGenerationImageDataUrl( markedImageDataUrl, projectId, ); - } else { - sourceReferenceImageSrc = await resolveEditorGenerationMediaReference( - quickEditSourceLayer, - 'image', - projectId, - ); } + const imageEditReferenceSrcs = [ + ...extraReferenceImageSrcs, + ...(markedReferenceImageSrc ? [markedReferenceImageSrc] : []), + ]; + const redrawSourceReferenceImageSrc = + panelMode === 'redraw' + ? await resolveEditorGenerationMediaReference( + quickEditSourceLayer, + 'image', + projectId, + ) + : null; const generationInputs = panelMode === 'redraw' ? buildEditGenerationInputs( @@ -1705,20 +1716,18 @@ export function useImageCanvasGenerationSubmissionWorkflow({ ? await runEditorGenerationWithWalletRefresh( editEditorImage({ prompt: normalizedPrompt, - sourceImageSrc: sourceReferenceImageSrc, + sourceReferenceId, size: quickEditSize, model: normalizedQuickEditModel, aspectRatio: quickEditAspectRatio, imageSize: quickEditImageSize, - ...(extraReferenceImageSrcs.length - ? { referenceImageSrcs: extraReferenceImageSrcs } + ...(imageEditReferenceSrcs.length + ? { referenceImageSrcs: imageEditReferenceSrcs } : {}), projectId, - assetKind: quickEditSourceLayer.assetKind, generationInputs, assetFolderId, assetLabel: quickEditResultTitle, - sourceResourceId: quickEditSourceLayer.resourceId, targetLayerId: quickEditSourceLayer.id, }), onWalletBalanceMayHaveChanged, @@ -1733,7 +1742,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({ model: normalizedQuickEditModel, referenceImageSrcs: [ ...extraReferenceImageSrcs, - sourceReferenceImageSrc, + ...(redrawSourceReferenceImageSrc + ? [redrawSourceReferenceImageSrc] + : []), ], projectId, assetKind: quickEditSourceLayer.assetKind, @@ -1917,10 +1928,8 @@ export function useImageCanvasGenerationSubmissionWorkflow({ canonicalBackgroundMusicPrompt: backgroundMusicSubmission?.prompt, }); if (submissionPlan.kind === 'edit') { - const referenceImageSrc = await resolveEditorGenerationMediaReference( + const sourceReferenceId = resolveRegisteredEditorReferenceId( submissionPlan.sourceLayer, - 'image', - projectId, ); const canvasCompletionPlaceholder = getGeneratingDialogPlaceholder(dialog); @@ -1938,14 +1947,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({ const generated = await runEditorGenerationWithWalletRefresh( editEditorImage({ prompt: submissionPlan.normalizedPrompt, - sourceImageSrc: referenceImageSrc, + sourceReferenceId, size: `${submissionPlan.sourceLayer.originalWidth}x${submissionPlan.sourceLayer.originalHeight}`, projectId, - assetKind: submissionPlan.sourceLayer.assetKind, generationInputs: submissionPlan.generationInputs, assetFolderId, assetLabel: submissionPlan.resultTitle, - sourceResourceId: submissionPlan.sourceLayer.resourceId, ...(projectId && editCanvasCompletionPlaceholder ? { canvasCompletion: { @@ -1979,12 +1986,15 @@ export function useImageCanvasGenerationSubmissionWorkflow({ generationInputs: submissionPlan.generationInputs, }); } else if (submissionPlan.kind === 'quick-edit') { + const sourceReferenceId = resolveRegisteredEditorReferenceId( + submissionPlan.sourceLayer, + ); const quickEditSelectionMarks = quickEditSelectionState?.sourceLayerId === submissionPlan.sourceLayer.id ? quickEditSelectionState.marks : []; - let referenceImageSrc: string; + let markedReferenceImageSrc: string | null = null; if (quickEditSelectionMarks.length > 0) { const renderSourceImageSrc = await resolveEditorImageReferenceDataUrl( @@ -1997,28 +2007,24 @@ export function useImageCanvasGenerationSubmissionWorkflow({ marks: quickEditSelectionMarks, showIndexLabels: true, }); - referenceImageSrc = await uploadEditorGenerationImageDataUrl( + markedReferenceImageSrc = await uploadEditorGenerationImageDataUrl( markedImageDataUrl, projectId, ); - } else { - referenceImageSrc = await resolveEditorGenerationMediaReference( - submissionPlan.sourceLayer, - 'image', - projectId, - ); } + const referenceImageSrcs = markedReferenceImageSrc + ? [markedReferenceImageSrc] + : undefined; const generated = await runEditorGenerationWithWalletRefresh( editEditorImage({ prompt: submissionPlan.normalizedPrompt, - sourceImageSrc: referenceImageSrc, + sourceReferenceId, ...submissionPlan.editInput, + ...(referenceImageSrcs?.length ? { referenceImageSrcs } : {}), projectId, - assetKind: submissionPlan.result.assetKind, generationInputs: submissionPlan.result.generationInputs, assetFolderId, assetLabel: submissionPlan.result.title, - sourceResourceId: submissionPlan.sourceLayer.resourceId, targetLayerId: submissionPlan.sourceLayer.id, }), onWalletBalanceMayHaveChanged, diff --git a/src/services/image-editor/editorProjectClient.test.ts b/src/services/image-editor/editorProjectClient.test.ts index da38e4d8e..b5d337ce5 100644 --- a/src/services/image-editor/editorProjectClient.test.ts +++ b/src/services/image-editor/editorProjectClient.test.ts @@ -1020,7 +1020,7 @@ describe('editorProjectClient', () => { await expect( editEditorImage({ prompt: '修改图片', - sourceImageSrc: '/generated-images/editor/source.png', + sourceReferenceId: 'resource-source', referenceImageSrcs: references.slice(0, 5), model: 'gpt-image-2', }), @@ -2075,17 +2075,15 @@ describe('editorProjectClient', () => { const result = await editEditorImage({ prompt: '把画面改成黄昏光线', - sourceImageSrc: '/generated-images/editor/source.png', + sourceReferenceId: 'resource-character-1', size: '1024x1024', model: 'gpt-image-2', aspectRatio: '1:1', imageSize: '1K', referenceImageSrcs: ['/generated-images/editor/style.png'], projectId: 'editor-project-1', - assetKind: 'character', assetFolderId: 'project', assetLabel: '角色形象 修改结果', - sourceResourceId: 'resource-character-1', targetLayerId: 'layer-character-1', generationInputs: { fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }], @@ -2101,21 +2099,19 @@ describe('editorProjectClient', () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: '把画面改成黄昏光线', - sourceImageSrc: '/generated-images/editor/source.png', + sourceReferenceId: 'resource-character-1', size: '1024x1024', model: 'gpt-image-2', aspectRatio: '1:1', imageSize: '1K', referenceImageSrcs: ['/generated-images/editor/style.png'], projectId: 'editor-project-1', - assetKind: 'character', generationInputs: { fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }], references: [], }, assetFolderId: 'project', assetLabel: '角色形象 修改结果', - sourceResourceId: 'resource-character-1', targetLayerId: 'layer-character-1', }), }), diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index fe34c74b0..0eef6f1a8 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -311,18 +311,16 @@ export type EditorUiDesignAssetExtractionInput = { export type EditorImageEditInput = { prompt: string; - sourceImageSrc: string; + sourceReferenceId: string; size?: string; model?: string; aspectRatio?: string; imageSize?: string; referenceImageSrcs?: string[]; projectId?: string | null; - assetKind?: string | null; generationInputs?: EditorAssetGenerationInputs | null; assetFolderId?: string | null; assetLabel?: string | null; - sourceResourceId?: string | null; targetLayerId?: string | null; canvasCompletion?: EditorCanvasGenerationCompletionInput | null; }; @@ -1175,7 +1173,6 @@ export async function extractEditorUiDesignAssets( } export async function editEditorImage(input: EditorImageEditInput) { - assertStableEditorMediaReference(input.sourceImageSrc, '待修改图片'); assertStableEditorMediaReferences(input.referenceImageSrcs, '修改参考图'); assertEditorReferenceLimit( input.referenceImageSrcs, @@ -1189,7 +1186,7 @@ export async function editEditorImage(input: EditorImageEditInput) { EDITOR_IMAGE_EDIT_API, jsonRequest('POST', { prompt: input.prompt, - sourceImageSrc: input.sourceImageSrc, + sourceReferenceId: input.sourceReferenceId, ...(input.size ? { size: input.size } : {}), ...(input.model ? { model: input.model } : {}), ...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}), @@ -1198,15 +1195,11 @@ export async function editEditorImage(input: EditorImageEditInput) { ? { referenceImageSrcs: input.referenceImageSrcs } : {}), ...(input.projectId ? { projectId: input.projectId } : {}), - ...(input.assetKind ? { assetKind: input.assetKind } : {}), ...(input.generationInputs ? { generationInputs: input.generationInputs } : {}), ...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}), ...(input.assetLabel ? { assetLabel: input.assetLabel } : {}), - ...(input.sourceResourceId - ? { sourceResourceId: input.sourceResourceId } - : {}), ...(input.targetLayerId ? { targetLayerId: input.targetLayerId } : {}), ...(input.canvasCompletion ? { canvasCompletion: input.canvasCompletion }