修复外部生成任务大载荷导致的内存膨胀

阻止图片编辑器将 Data URL 和超限 JSON 写入生成任务,复用 objectKey 等轻量引用
新增外部生成任务摘要投影与分批 payload 压缩回填流程,避免正式列表读取大字段
收紧 SpacetimeDB 发布为永不删数据并移除 Jenkins 普通清库入口,补齐维护窗口门禁
更新生成绑定、运维文档和回归测试,并完成生产数据副本迁移与压缩验证
This commit is contained in:
2026-07-11 13:55:05 +08:00
parent f848cbf78b
commit 324f99efdf
42 changed files with 3528 additions and 317 deletions
@@ -25,6 +25,7 @@ import type { UiAssetExtractionState } from './ImageCanvasUiAssetExtractionModel
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
import {
applyQueuedEditorGenerationProject,
resolveEditorGenerationMediaReference,
useImageCanvasGenerationSubmissionWorkflow,
} from './useImageCanvasGenerationSubmissionWorkflow';
@@ -32,6 +33,7 @@ const resolveEditorImageReferenceDataUrlMock = vi.hoisted(() => vi.fn());
const resolveEditorImageReferenceDataUrlForGenerationMock = vi.hoisted(() =>
vi.fn(),
);
const uploadEditorMediaAssetFileMock = vi.hoisted(() => vi.fn());
const editEditorImageMock = vi.hoisted(() => vi.fn());
const extractEditorUiDesignAssetsMock = vi.hoisted(() => vi.fn());
const renderUiDesignAssetExtractionMarkedImageMock = vi.hoisted(() => vi.fn());
@@ -68,6 +70,10 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
};
});
vi.mock('../../services/image-editor/editorMediaAssetUploadClient', () => ({
uploadEditorMediaAssetFile: uploadEditorMediaAssetFileMock,
}));
vi.mock('../../services/external-generation', () => ({
getExternalGenerationJobStatus: getExternalGenerationJobStatusMock,
}));
@@ -466,6 +472,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
beforeEach(() => {
resolveEditorImageReferenceDataUrlMock.mockReset();
resolveEditorImageReferenceDataUrlForGenerationMock.mockReset();
uploadEditorMediaAssetFileMock.mockReset();
editEditorImageMock.mockReset();
extractEditorUiDesignAssetsMock.mockReset();
renderUiDesignAssetExtractionMarkedImageMock.mockReset();
@@ -483,11 +490,47 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
resolveEditorImageReferenceDataUrlForGenerationMock.mockImplementation(
async (src: string) => src,
);
uploadEditorMediaAssetFileMock.mockResolvedValue({
src: 'https://signed.example.test/generation-reference.png',
objectKey:
'generated-character-drafts/editor/generation-references/reference.png',
assetObjectId: 'asset-object-generation-reference',
legacyPublicPath:
'/generated-character-drafts/editor/generation-references/reference.png',
});
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValue(
'data:image/png;base64,marked-ui-design',
'data:image/png;base64,bWFya2VkLXVpLWRlc2lnbg==',
);
});
it('uses distinct upload paths for concurrent inline references in the same millisecond', async () => {
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(1_752_144_000_000);
try {
await Promise.all([
resolveEditorGenerationMediaReference(
{ src: 'data:image/png;base64,YQ==' },
'image',
'project-1',
),
resolveEditorGenerationMediaReference(
{ src: 'data:image/png;base64,Yg==' },
'image',
'project-1',
),
]);
} finally {
dateNowSpy.mockRestore();
}
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledTimes(2);
const [firstFile, , firstOptions] =
uploadEditorMediaAssetFileMock.mock.calls[0] ?? [];
const [secondFile, , secondOptions] =
uploadEditorMediaAssetFileMock.mock.calls[1] ?? [];
expect((firstFile as File).name).not.toBe((secondFile as File).name);
expect(firstOptions.pathSegments).not.toEqual(secondOptions.pathSegments);
});
it('submits quick edits and updates the source layer directly', async () => {
editEditorImageMock.mockResolvedValueOnce(
createGenerated({ prompt: '快速修图' }),
@@ -506,7 +549,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '快速修图',
sourceImageSrc: 'data:image/png;base64,source.png',
sourceImageSrc: 'resource-source',
size: '1024x768',
model: 'gpt-image-2',
}),
@@ -516,6 +559,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
'canvasCompletion',
);
expect(generateEditorImageMock).not.toHaveBeenCalled();
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByTestId('layers').textContent).toContain(
'layer-source:源图:resource-source:-:-:data:image/png;base64,generated',
@@ -606,7 +650,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '把当前图改成雨天',
sourceImageSrc: 'data:image/png;base64,source.png',
sourceImageSrc: 'resource-source',
size: '1024x768',
model: 'gpt-image-2',
sourceResourceId: 'resource-source',
@@ -727,7 +771,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
/>,
);
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValueOnce(
'data:image/png;base64,marked-source',
'data:image/png;base64,bWFya2VkLXNvdXJjZQ==',
);
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
@@ -751,13 +795,24 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
);
});
await waitFor(() => {
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledWith(
expect.any(File),
'image',
expect.objectContaining({
assetKind: 'editor_generation_reference_image',
}),
);
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceImageSrc: 'data:image/png;base64,marked-source',
sourceImageSrc:
'generated-character-drafts/editor/generation-references/reference.png',
targetLayerId: 'layer-source',
}),
);
});
expect(
uploadEditorMediaAssetFileMock.mock.invocationCallOrder[0],
).toBeLessThan(editEditorImageMock.mock.invocationCallOrder[0] ?? 0);
expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty(
'referenceImageSrcs',
);
@@ -812,7 +867,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
/>,
);
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValueOnce(
'data:image/png;base64,marked-source',
'data:image/png;base64,bWFya2VkLXNvdXJjZQ==',
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
@@ -839,7 +894,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
await waitFor(() => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceImageSrc: 'data:image/png;base64,marked-source',
sourceImageSrc:
'generated-character-drafts/editor/generation-references/reference.png',
model: 'gpt-image-2',
targetLayerId: 'layer-source',
}),
@@ -907,7 +963,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
imageSize: '1K',
kind: 'character',
model: 'gpt-image-2',
referenceImageSrcs: ['data:image/png;base64,source.png'],
referenceImageSrcs: ['resource-source'],
assetKind: 'character',
}),
);
@@ -959,8 +1015,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
initialLayers={[
createLayer({
title: '源视频',
src: 'https://assets.example.test/source.mp4',
objectKey: 'generated-character-drafts/editor/videos/source.mp4',
src: 'data:video/mp4;base64,c291cmNlLXZpZGVv',
mediaType: 'video',
assetKind: 'video',
model: 'gpt-image-2',
@@ -1001,7 +1056,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
mode: 'std',
sound: 'off',
referenceVideoSrcs: [
'generated-character-drafts/editor/videos/source.mp4',
'generated-character-drafts/editor/generation-references/reference.png',
],
assetFolderId: 'project',
assetLabel: '源视频 快速编辑',
@@ -1026,6 +1081,66 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
});
});
it('uploads inline video references and keeps image objectKey and asset URLs', async () => {
generateEditorVideoMock.mockResolvedValueOnce(createVideoGenerated());
render(
<SubmissionWorkflowHarness
initialDialog={{
id: 'dialog-video-references',
mode: 'video',
prompt: '让画面动起来',
status: 'idle',
composerOpen: true,
videoModel: 'seedance2.0-fast',
generationReferences: [
{
id: 'image-reference',
label: '图片参考',
src: 'data:image/png;base64,aW1hZ2U=',
objectKey: 'generated-character-drafts/editor/refs/image.png',
},
{
id: 'video-reference',
label: '视频参考',
src: 'data:video/mp4;base64,dmlkZW8=',
mediaType: 'video',
},
{
id: 'audio-reference',
label: '音频参考',
src: 'asset://asset-audio-reference',
mediaType: 'audio',
},
],
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await waitFor(() => {
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledWith(
expect.any(File),
'video',
expect.objectContaining({
assetKind: 'editor_generation_reference_video',
}),
);
expect(generateEditorVideoMock).toHaveBeenCalledWith(
expect.objectContaining({
referenceImageSrcs: [
'generated-character-drafts/editor/refs/image.png',
],
referenceVideoSrcs: [
'generated-character-drafts/editor/generation-references/reference.png',
],
referenceAudioSrcs: ['asset://asset-audio-reference'],
}),
);
});
});
it('submits character action quick edits through image edits', async () => {
editEditorImageMock.mockResolvedValueOnce(
createGenerated({
@@ -1075,7 +1190,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '让当前图跳得更高',
sourceImageSrc: 'data:image/png;base64,frame.png',
sourceImageSrc: 'resource-source',
model: 'gpt-image-2',
assetLabel: '角色动作 快速编辑',
targetLayerId: 'layer-source',
@@ -1117,7 +1232,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '修改当前图片',
sourceImageSrc: 'data:image/png;base64,source.png',
sourceImageSrc: 'resource-source',
}),
);
});
@@ -1341,10 +1456,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
});
});
it('resolves uploaded objectKey references before submitting image generation', async () => {
resolveEditorImageReferenceDataUrlForGenerationMock.mockResolvedValueOnce(
'data:image/jpeg;base64,normalized-ref',
);
it('submits uploaded objectKey references without downloading or inlining them', async () => {
generateEditorImageMock.mockResolvedValueOnce(
createGenerated({ prompt: '参考图生成' }),
);
@@ -1382,16 +1494,17 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await waitFor(() => {
expect(
resolveEditorImageReferenceDataUrlForGenerationMock,
).toHaveBeenCalledWith('generated-character-drafts/editor/refs/ref.png');
expect(generateEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '参考图生成',
referenceImageSrcs: ['data:image/jpeg;base64,normalized-ref'],
referenceImageSrcs: [
'generated-character-drafts/editor/refs/ref.png',
],
}),
);
});
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
expect(uploadEditorMediaAssetFileMock).not.toHaveBeenCalled();
});
it('refreshes the wallet balance after a queued generation reaches a terminal state', async () => {
@@ -1525,10 +1638,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
1,
unresolvedProject,
);
expect(applyProjectSnapshot).toHaveBeenNthCalledWith(
2,
completedProject,
);
expect(applyProjectSnapshot).toHaveBeenNthCalledWith(2, completedProject);
} finally {
vi.useRealTimers();
}
@@ -1691,7 +1801,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
{
id: 'ref-1',
label: '主视觉参考',
src: 'data:image/png;base64,large-ref',
src: 'DaTa:image/png;base64,bGFyZ2UtcmVm',
},
],
placeholder: {
@@ -1721,7 +1831,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
model: 'gpt-image-2',
aspectRatio: '9:16',
imageSize: '1K',
referenceImageSrcs: ['data:image/png;base64,large-ref'],
referenceImageSrcs: [
'generated-character-drafts/editor/generation-references/reference.png',
],
}),
);
});
@@ -1742,10 +1854,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
);
});
it('compresses publication references before submitting generation', async () => {
resolveEditorImageReferenceDataUrlForGenerationMock.mockResolvedValueOnce(
'data:image/jpeg;base64,compressed-ref',
);
it('submits publication resource ids without downloading or inlining them', async () => {
generateEditorImageMock.mockResolvedValueOnce(
createGenerated({
imageSrc: 'data:image/png;base64,publication',
@@ -1773,6 +1882,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
id: 'ref-1',
label: '主视觉参考',
src: 'data:image/png;base64,large-original-ref',
resourceId: 'resource-publication-ref',
},
],
}}
@@ -1783,18 +1893,17 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await waitFor(() => {
expect(
resolveEditorImageReferenceDataUrlForGenerationMock,
).toHaveBeenCalledWith('data:image/png;base64,large-original-ref');
expect(generateEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'publication-material',
aspectRatio: '4:3',
imageSize: '1K',
referenceImageSrcs: ['data:image/jpeg;base64,compressed-ref'],
referenceImageSrcs: ['resource-publication-ref'],
}),
);
});
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
expect(uploadEditorMediaAssetFileMock).not.toHaveBeenCalled();
});
it('submits icon spec objects without requiring an icon spec reference', async () => {
@@ -1913,7 +2022,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
iconSpecReference: {
id: 'spec',
label: '图标规范',
src: 'data:image/png;base64,spec',
src: 'data:image/png;base64,c3BlYw==',
},
iconDescriptions: ['旧返回', '旧设置'],
placeholder: {
@@ -1934,7 +2043,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
await waitFor(() => {
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
expect.objectContaining({
referenceImageSrc: 'data:image/png;base64,spec',
referenceImageSrc:
'generated-character-drafts/editor/generation-references/reference.png',
iconDescriptions: ['返回按钮', '设置按钮'],
model: 'gpt-image-2',
}),
@@ -1992,7 +2102,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
id: 'ref-style',
label: '风格参考',
src: 'https://signed.example.test/icon-style.png?expires=1',
objectKey: 'generated-character-drafts/editor/refs/icon-style.png',
objectKey:
'generated-character-drafts/editor/refs/icon-style.png',
assetObjectId: 'assetobj-icon-style',
},
],
@@ -2004,7 +2115,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '提交图标' }));
await waitFor(() => {
expect(resolveEditorImageReferenceDataUrlForGenerationMock).not.toHaveBeenCalled();
expect(
resolveEditorImageReferenceDataUrlForGenerationMock,
).not.toHaveBeenCalled();
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
expect.objectContaining({
referenceImageSrc:
@@ -2115,6 +2228,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
});
it('renders selected UI marks into the extraction reference image and sends derived extraction pricing', async () => {
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValueOnce(
'data:image/png;base64,bWFya2VkLXVpLWRlc2lnbg==',
);
extractEditorUiDesignAssetsMock.mockResolvedValueOnce({
spritesheetImageSrc: 'data:image/png;base64,ui-sheet',
spritesheetWidth: 512,
@@ -2166,7 +2282,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
await waitFor(() => {
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceImageSrc: 'data:image/png;base64,marked-ui-design',
sourceImageSrc:
'generated-character-drafts/editor/generation-references/reference.png',
model: 'gemini-3.1-flash-image-preview',
screenColor: 'auto',
segModel: 'birefnet',
@@ -2213,7 +2330,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '提取带参考图UI素材' }));
await waitFor(() => {
expect(resolveEditorImageReferenceDataUrlForGenerationMock).not.toHaveBeenCalled();
expect(
resolveEditorImageReferenceDataUrlForGenerationMock,
).not.toHaveBeenCalled();
});
await waitFor(() => {
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
@@ -2290,7 +2409,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
createLayer({
id: 'character-layer',
assetKind: 'character',
objectKey: 'generated/character.png',
src: 'data:image/png;base64,Y2hhcmFjdGVy',
}),
]}
initialCharacterAnimationPanel={{
@@ -2314,7 +2433,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(generateEditorCharacterAnimationMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceLayerId: 'character-layer',
sourceImageSrc: 'generated/character.png',
sourceImageSrc:
'generated-character-drafts/editor/generation-references/reference.png',
promptText: '跑步',
model: 'seedance2.0-fast',
}),
@@ -2450,7 +2570,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
);
render(
<SubmissionWorkflowHarness
initialLayers={[createLayer({ id: 'character-layer' })]}
initialLayers={[
createLayer({
id: 'character-layer',
objectKey: 'generated/character.png',
}),
]}
initialCharacterAnimationPanel={{
sourceLayerId: 'character-layer',
promptText: '跑步',
@@ -2478,7 +2603,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
generateEditorCharacterAnimationMock.mockRejectedValueOnce(new Error(''));
render(
<SubmissionWorkflowHarness
initialLayers={[createLayer({ id: 'character-layer' })]}
initialLayers={[
createLayer({
id: 'character-layer',
objectKey: 'generated/character.png',
}),
]}
initialCharacterAnimationPanel={{
sourceLayerId: 'character-layer',
promptText: '',
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,12 @@
/* @vitest-environment jsdom */
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { useRef, useState } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -12,9 +18,9 @@ import type {
ImageContextMenuState,
SidebarPanel,
} from './ImageCanvasEditorTypes';
import { IMAGE_MODEL_GPT_IMAGE_2 } from './ImageCanvasGenerationModel';
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
import { IMAGE_MODEL_GPT_IMAGE_2 } from './ImageCanvasGenerationModel';
const generateEditorImageMock = vi.hoisted(() => vi.fn());
const generateEditorCharacterAnimationMock = vi.hoisted(() => vi.fn());
@@ -1268,7 +1274,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
assetFolderId: undefined,
assetKind: undefined,
assetLabel: '生成图片 1',
referenceImageSrcs: ['data:image/png;base64,source'],
referenceImageSrcs: ['resource-source'],
generationInputs: {
fields: [{ title: '生成提示词', value: '一张生成图' }],
references: [
@@ -1514,6 +1520,14 @@ describe('useImageCanvasGenerationWorkflow', () => {
});
it('submits spec generation with reference image and reference prompt semantics', async () => {
uploadEditorMediaAssetFileMock.mockResolvedValueOnce({
src: 'https://signed.example.test/generation-reference.png',
objectKey:
'generated-character-drafts/editor/generation-references/reference.png',
assetObjectId: 'asset-object-generation-reference',
legacyPublicPath:
'/generated-character-drafts/editor/generation-references/reference.png',
});
generateEditorImageMock.mockResolvedValueOnce(
createGenerated({ prompt: 'UI规范图' }),
);
@@ -1531,7 +1545,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
model: 'gpt-image-2',
aspectRatio: '16:9',
imageSize: '2K',
referenceImageSrcs: ['data:image/png;base64,ref'],
referenceImageSrcs: [
'generated-character-drafts/editor/generation-references/reference.png',
],
prompt: expect.stringContaining('参考图生成规范'),
}),
);
@@ -1630,7 +1646,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '快速修图',
sourceImageSrc: 'data:image/png;base64,source',
sourceImageSrc: 'resource-source',
assetLabel: '源图 快速编辑',
sourceResourceId: 'resource-source',
generationInputs: {
@@ -1789,7 +1805,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
await waitFor(() => {
expect(removeImageBackgroundMock).toHaveBeenCalledWith({
sourceImageSrc: '/generated-character-drafts/editor/private.png',
sourceImageSrc: 'generated-character-drafts/editor/private.png',
projectId: undefined,
targetLayerId: 'layer-source',
assetKind: 'character',
@@ -1832,6 +1848,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
expect(removeImageBackgroundMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceImageSrc: 'resource-source',
projectId: 'project-1',
targetLayerId: 'layer-source',
assetLabel: '源图 去背景',
@@ -1845,6 +1862,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
}),
}),
);
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
await act(async () => {
resolveBackgroundRemoval?.({
imageSrc:
@@ -19,6 +19,7 @@ import {
type EditorProjectResourceSnapshot,
type EditorProjectSnapshot,
} from '../../services/image-editor/editorProjectClient';
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
import type {
CanvasGenerationDialogState,
CanvasLayer,
@@ -36,7 +37,6 @@ import type {
SpecFormValues,
SpecGenerationType,
} from './ImageCanvasEditorTypes';
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
import {
appendCharacterReference,
appendGenerationReference,
@@ -103,6 +103,8 @@ import {
} from './ImageCanvasUiAssetExtractionModel';
import {
applyQueuedEditorGenerationProject,
createEditorGenerationMediaUploadId,
resolveEditorGenerationMediaReference,
useImageCanvasGenerationSubmissionWorkflow,
} from './useImageCanvasGenerationSubmissionWorkflow';
@@ -222,7 +224,10 @@ function preserveSourceLayerInProjectSnapshot(
resources: sourceResource
? [...project.resources, sourceResource]
: project.resources,
layers: [createProjectLayerSnapshotFromLayer(sourceLayer), ...project.layers],
layers: [
createProjectLayerSnapshotFromLayer(sourceLayer),
...project.layers,
],
};
}
@@ -1434,9 +1439,10 @@ export function useImageCanvasGenerationWorkflow({
cropExpandSourceLayer.resourceId;
let cropExpandAssetKind = cropExpandSourceLayer.assetKind;
if (cropExpandProjectId) {
const cropExpandUploadId = createEditorGenerationMediaUploadId();
const cropExpandFile = dataUrlToImageFile(
result.imageSrc,
`crop-expand-${cropExpandIndex}.png`,
`crop-expand-${cropExpandIndex}-${cropExpandUploadId}.png`,
);
const uploadedCropExpand = await uploadEditorMediaAssetFile(
cropExpandFile,
@@ -1447,7 +1453,7 @@ export function useImageCanvasGenerationWorkflow({
'editor',
'crop-expand',
cropExpandProjectId,
`${Date.now()}`,
cropExpandUploadId,
],
entityId: cropExpandProjectId,
metadata: {
@@ -1480,8 +1486,7 @@ export function useImageCanvasGenerationWorkflow({
cropExpandObjectKey =
cropExpandResource.objectKey ?? uploadedCropExpand.objectKey;
cropExpandAssetObjectId =
cropExpandResource.assetObjectId ??
uploadedCropExpand.assetObjectId;
cropExpandResource.assetObjectId ?? uploadedCropExpand.assetObjectId;
cropExpandSourceResourceId =
cropExpandResource.sourceResourceId ??
cropExpandSourceLayer.resourceId;
@@ -1574,10 +1579,11 @@ export function useImageCanvasGenerationWorkflow({
}
: undefined;
try {
const sourceObjectKey = sourceLayer.objectKey?.trim();
const sourceImageSrc = sourceObjectKey
? `/${sourceObjectKey.replace(/^\/+/u, '')}`
: await resolveEditorImageReferenceDataUrl(sourceLayer.src);
const sourceImageSrc = await resolveEditorGenerationMediaReference(
sourceLayer,
'image',
projectId,
);
const result = await removeImageBackground({
sourceImageSrc,
projectId,
@@ -1672,7 +1678,6 @@ export function useImageCanvasGenerationWorkflow({
openPlacedCanvasGenerationDialog,
projectId,
refreshTaskListForQueuedGeneration,
resolveEditorImageReferenceDataUrl,
setActiveSidebarPanel,
setImageContextMenu,
setMetadataLayer,