前端按业务ID提交图片编辑
优先使用图层 resourceId 并回退正式 sourceAssetId 未登记来源在上传标注图前失败关闭 保持红框标注图为辅助 referenceImageSrcs 而非主来源 移除图片编辑请求中的旧来源和类型字段并补齐工作流测试
This commit is contained in:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user