前端按业务ID提交图片编辑

优先使用图层 resourceId 并回退正式 sourceAssetId
未登记来源在上传标注图前失败关闭
保持红框标注图为辅助 referenceImageSrcs 而非主来源
移除图片编辑请求中的旧来源和类型字段并补齐工作流测试
This commit is contained in:
2026-08-07 14:04:26 +08:00
parent 0cca0adcb1
commit 609b036b04
6 changed files with 150 additions and 56 deletions
@@ -9,6 +9,7 @@ import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
resolveRegisteredEditorReferenceId,
} from './ImageCanvasGenerationSubmissionModel';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
@@ -30,6 +31,33 @@ function createLayer(overrides: Partial<CanvasLayer> = {}): 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: {
@@ -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<GenerateDialogState['generationReferences']>,
) {
@@ -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(
<SubmissionWorkflowHarness
initialLayers={[
createLayer({
resourceId: 'local-resource-source',
sourceAssetId: 'upload-source',
}),
]}
initialQuickEditPanel={{
mode: 'quick-edit',
sourceLayerId: 'layer-source',
prompt: '修改未登记图片',
model: 'gpt-image-2',
aspectRatio: '1:1',
imageSize: '1K',
size: '1024x1024',
status: 'idle',
}}
initialQuickEditSelectionState={{
sourceLayerId: 'layer-source',
tool: 'rect',
model: 'gpt-image-2',
marks: [
{
id: 'quick-edit-selection-mark-1',
tool: 'rect',
x: 24,
y: 36,
width: 160,
height: 120,
},
],
references: [],
draftMark: null,
status: 'idle',
}}
/>,
);
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,
@@ -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,
@@ -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',
}),
}),
@@ -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 }