修复外部生成阶段上报与后处理降级

为 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:
2026-07-16 03:49:48 +00:00
parent fddf30d7c3
commit d6ff106a0b
32 changed files with 1311 additions and 131 deletions
@@ -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;
};