修复外部生成阶段上报与后处理降级
为 phase procedure 增加结构化 lease/fencing 拒绝结果并同步 typed bindings 仅对 Build、ConnectDropped、Timeout 重试一次,最终失败不回 pending 角色、图标和 UI 后处理最终失败时保留原图并以 completed warning 收口 同步 inline、队列轮询和刷新恢复 warning,保留 sliceWarning 契约 同步 external v1 OpenAPI、权威文档和 external editor skill 更新 BgFilter 服务地址
This commit is contained in:
@@ -2938,7 +2938,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
phaseDetail: '生成已完成。',
|
||||
warning: '连通域数量不足',
|
||||
warning: '图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
completedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAtMicros: 2,
|
||||
|
||||
@@ -1251,9 +1251,7 @@ export function ImageCanvasEditorView({
|
||||
}
|
||||
const warning = tasks.find((task) => task.warning?.trim())?.warning?.trim();
|
||||
if (warning) {
|
||||
showGenerationWarning(
|
||||
`图集已生成,但自动拆分未完成:${warning}`,
|
||||
);
|
||||
showGenerationWarning(warning);
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
void loadEditorProject(projectId)
|
||||
|
||||
@@ -1029,7 +1029,14 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
|
||||
it('submits character redraws through the character image prompt contract', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '角色换成蓝色披风' }),
|
||||
createGenerated({
|
||||
prompt: '角色换成蓝色披风',
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -1080,6 +1087,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
||||
'layer-source:idle:角色换成蓝色披风:-',
|
||||
);
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits video quick edits through the video API with the source video', async () => {
|
||||
@@ -1653,7 +1663,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(uploadEditorMediaAssetFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes the wallet balance after a queued generation reaches a terminal state', async () => {
|
||||
it('refreshes the wallet and shows the warning after a queued character generation completes', async () => {
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
const refreshTaskList = vi.fn();
|
||||
const refreshWalletBalance = vi.fn();
|
||||
@@ -1671,9 +1681,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
imageSrc: '/generated-editor-images/project/image.png',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
prompt: '队列生成',
|
||||
prompt: '队列角色生成',
|
||||
}),
|
||||
queueState: createQueueState({
|
||||
warning:
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
}),
|
||||
queueState: createQueueState(),
|
||||
});
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -1682,9 +1695,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
onQueuedGenerationTask={refreshTaskList}
|
||||
onWalletBalanceMayHaveChanged={refreshWalletBalance}
|
||||
initialDialog={{
|
||||
id: 'dialog-generate',
|
||||
mode: 'generate',
|
||||
prompt: '队列生成',
|
||||
id: 'dialog-character',
|
||||
mode: 'character',
|
||||
prompt: '队列角色生成',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
imageModel: 'gpt-image-2',
|
||||
@@ -1712,6 +1725,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(refreshWalletBalance).toHaveBeenCalledTimes(1);
|
||||
expect(applyProjectSnapshot).toHaveBeenCalledWith(backendProject);
|
||||
expect(getExternalGenerationJobStatusMock).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).not.toContain(
|
||||
'layer-generated-1',
|
||||
);
|
||||
@@ -1805,7 +1821,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await applyQueuedEditorGenerationProject(
|
||||
{
|
||||
queueState: createQueueState({
|
||||
warning: '连通域数量不足',
|
||||
warning:
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
}),
|
||||
},
|
||||
'editor-project-1',
|
||||
@@ -1816,7 +1833,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
|
||||
expect(onGenerationWarning).toHaveBeenCalledWith(
|
||||
'图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2388,6 +2405,11 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-ui-assets',
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'UI 素材图集已生成,但透明背景处理失败,已将原图放入画布;未生成透明图集和拆分素材。',
|
||||
},
|
||||
});
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -2414,6 +2436,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'layer-icon-spritesheet-1',
|
||||
);
|
||||
expect(screen.getByTestId('fit-count').textContent).toBe('1');
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'UI 素材图集已生成,但透明背景处理失败,已将原图放入画布;未生成透明图集和拆分素材。',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders selected UI marks into the extraction reference image and sends derived extraction pricing', async () => {
|
||||
@@ -2545,7 +2570,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
|
||||
it('keeps character generation dialog data after the result is created', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ width: 768, height: 768 }),
|
||||
createGenerated({
|
||||
width: 768,
|
||||
height: 768,
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -2584,6 +2617,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'character:idle:open:layer-generated-1:placeholder:-',
|
||||
);
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
);
|
||||
});
|
||||
|
||||
it('moves character animation panels from generating to completed', async () => {
|
||||
|
||||
@@ -88,6 +88,9 @@ import { renderUiDesignAssetExtractionMarkedImage } from './ImageCanvasUiAssetEx
|
||||
|
||||
type CanvasSize = { width: number; height: number };
|
||||
|
||||
const EDITOR_SPRITESHEET_SLICE_WARNING_PREFIX =
|
||||
'图集已生成,但自动拆分未完成:';
|
||||
|
||||
type CanvasGenerationDialogUpdater = (
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => CanvasGenerationDialogState | null;
|
||||
@@ -438,7 +441,21 @@ function notifyEditorGenerationWarning(
|
||||
if (!reason) {
|
||||
return;
|
||||
}
|
||||
callback?.(`图集已生成,但自动拆分未完成:${reason}`);
|
||||
callback?.(reason);
|
||||
}
|
||||
|
||||
function resolveEditorGenerationWarningMessage(
|
||||
warning: string | null | undefined,
|
||||
sliceWarning: string | null | undefined,
|
||||
) {
|
||||
const commonReason = warning?.trim();
|
||||
if (commonReason) {
|
||||
return commonReason;
|
||||
}
|
||||
const sliceReason = sliceWarning?.trim();
|
||||
return sliceReason
|
||||
? `${EDITOR_SPRITESHEET_SLICE_WARNING_PREFIX}${sliceReason}`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function runEditorGenerationWithWalletRefresh<T>(
|
||||
@@ -1097,7 +1114,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
notifyEditorGenerationWarning(
|
||||
generated.sliceWarning?.reason,
|
||||
resolveEditorGenerationWarningMessage(
|
||||
generated.warning?.reason,
|
||||
generated.sliceWarning?.reason,
|
||||
),
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
@@ -1234,7 +1254,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
);
|
||||
rememberImageModel(submissionPlan.rememberImageModel);
|
||||
notifyEditorGenerationWarning(
|
||||
generated.sliceWarning?.reason,
|
||||
resolveEditorGenerationWarningMessage(
|
||||
generated.warning?.reason,
|
||||
generated.sliceWarning?.reason,
|
||||
),
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
@@ -1590,6 +1613,15 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const characterWarningCallback = isCharacterRedraw
|
||||
? onGenerationWarning
|
||||
: undefined;
|
||||
if (isCharacterRedraw) {
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
characterWarningCallback,
|
||||
);
|
||||
}
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -1597,6 +1629,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
characterWarningCallback,
|
||||
)
|
||||
) {
|
||||
if (panelMode === 'quick-edit') {
|
||||
@@ -1681,6 +1714,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setQuickEditPanel,
|
||||
updateCanvasGenerationDialogById,
|
||||
viewport,
|
||||
onGenerationWarning,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
]);
|
||||
|
||||
@@ -1999,6 +2033,16 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const characterWarningCallback =
|
||||
imageGenerationInput.kind === 'character'
|
||||
? onGenerationWarning
|
||||
: undefined;
|
||||
if (imageGenerationInput.kind === 'character') {
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
characterWarningCallback,
|
||||
);
|
||||
}
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -2006,6 +2050,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
characterWarningCallback,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -2078,6 +2123,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setGenerateDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
upsertGeneratedAsset,
|
||||
onGenerationWarning,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -743,6 +743,11 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'vector-task-1',
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await generateEditorImage({
|
||||
@@ -750,6 +755,11 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('vector-task-1');
|
||||
expect(result.warning).toEqual({
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'图片已生成,但透明背景处理失败,已将原图放入画布;未生成透明处理图。',
|
||||
});
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/editor/images/generations',
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -273,6 +273,7 @@ export type EditorImageGenerationResult = {
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
project?: EditorProjectSnapshot | null;
|
||||
warning?: EditorGenerationWarning | null;
|
||||
queueState?: ExternalGenerationJobStatusRecord | null;
|
||||
};
|
||||
|
||||
@@ -289,11 +290,13 @@ export type EditorIconSpritesheetIconResult = {
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetSliceWarning = {
|
||||
export type EditorGenerationWarning = {
|
||||
code: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetSliceWarning = EditorGenerationWarning;
|
||||
|
||||
export type EditorIconSpritesheetGenerationResult = {
|
||||
spritesheetImageSrc: string;
|
||||
spritesheetWidth: number;
|
||||
@@ -309,6 +312,7 @@ export type EditorIconSpritesheetGenerationResult = {
|
||||
spritesheetResource?: EditorProjectResourceSnapshot | null;
|
||||
spritesheetAsset?: EditorAssetSnapshot | null;
|
||||
project?: EditorProjectSnapshot | null;
|
||||
warning?: EditorGenerationWarning | null;
|
||||
queueState?: ExternalGenerationJobStatusRecord | null;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user