补齐生成中间产物持久化与拆图提示
图标和UI图集拆分失败时保留主图集并在内联、队列及刷新路径提示 角色、图片修改、图标UI和角色动作链路把可恢复中间产物写入项目与素材库 外部生成摘要新增独立告警字段并清理成功任务的历史错误 同步共享契约、SpacetimeDB迁移与绑定、OpenAPI、设计文档和回归测试
This commit is contained in:
@@ -262,6 +262,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
phaseDetail: overrides.phaseDetail ?? '正在生成角色形象。',
|
||||
progress: overrides.progress ?? 40,
|
||||
error: overrides.error ?? null,
|
||||
warning: overrides.warning ?? null,
|
||||
priceMudPoints: overrides.priceMudPoints ?? 3,
|
||||
refundLedgerId: overrides.refundLedgerId ?? null,
|
||||
notificationAcknowledgedAt: overrides.notificationAcknowledgedAt ?? null,
|
||||
@@ -2895,6 +2896,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
phaseDetail: '生成已完成。',
|
||||
warning: '连通域数量不足',
|
||||
completedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAtMicros: 2,
|
||||
@@ -2934,6 +2936,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(loadEditorProjectMock).toHaveBeenCalledWith(projectId);
|
||||
});
|
||||
expect(await screen.findByAltText('画布图片:刷新后生成结果')).toBeTruthy();
|
||||
expect((await screen.findByRole('alert')).textContent).toBe(
|
||||
'图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(loadEditorAssetLibraryMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -1098,18 +1098,6 @@ export function ImageCanvasEditorView({
|
||||
},
|
||||
[applyProjectSnapshot, refreshAssetLibrary],
|
||||
);
|
||||
const handleExternalGenerationTasksCompleted = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) => {
|
||||
if (!projectId || tasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
void loadEditorProject(projectId)
|
||||
.then(applyGeneratedProjectSnapshot)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
[applyGeneratedProjectSnapshot, projectId, refreshEditorWalletBalance],
|
||||
);
|
||||
const handleEditorAgentGenerationResult = useCallback(
|
||||
(event: EditorAgentGenerationResultEvent) => {
|
||||
const hasGeneratedResource = event.images.some((image) =>
|
||||
@@ -1255,6 +1243,30 @@ export function ImageCanvasEditorView({
|
||||
applyProjectSnapshot: applyGeneratedProjectSnapshot,
|
||||
onWalletBalanceMayHaveChanged: refreshEditorWalletBalance,
|
||||
});
|
||||
const showGenerationWarning = generationSurface.showGenerationWarning;
|
||||
const handleExternalGenerationTasksCompleted = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) => {
|
||||
if (!projectId || tasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
const warning = tasks.find((task) => task.warning?.trim())?.warning?.trim();
|
||||
if (warning) {
|
||||
showGenerationWarning(
|
||||
`图集已生成,但自动拆分未完成:${warning}`,
|
||||
);
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
void loadEditorProject(projectId)
|
||||
.then(applyGeneratedProjectSnapshot)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
[
|
||||
applyGeneratedProjectSnapshot,
|
||||
projectId,
|
||||
refreshEditorWalletBalance,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
const effectiveIsAgentConversationOpen =
|
||||
isAgentConversationEnabled && isAgentConversationOpen;
|
||||
const toggleAgentConversation = useCallback(() => {
|
||||
|
||||
@@ -58,6 +58,7 @@ function createExternalTask(
|
||||
phaseDetail: overrides.phaseDetail ?? '正在生成第 2/4 段。',
|
||||
progress: overrides.progress ?? 35,
|
||||
error: overrides.error ?? null,
|
||||
warning: overrides.warning ?? null,
|
||||
priceMudPoints: overrides.priceMudPoints ?? 1,
|
||||
refundLedgerId: overrides.refundLedgerId ?? null,
|
||||
notificationAcknowledgedAt: overrides.notificationAcknowledgedAt ?? null,
|
||||
@@ -236,6 +237,7 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
updatedAt: completedAt,
|
||||
progress: 100,
|
||||
phaseDetail: '生成已完成。',
|
||||
warning: '连通域数量不足',
|
||||
}),
|
||||
createExternalTask({
|
||||
jobId: 'done-other-project',
|
||||
@@ -273,6 +275,7 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: /已完成/u }));
|
||||
expect(await screen.findByText(/已完成提示词/u)).toBeTruthy();
|
||||
expect(screen.getByText('连通域数量不足')).toBeTruthy();
|
||||
expect(screen.getByText(/完成于/u)).toBeTruthy();
|
||||
expect(screen.queryByText(/另一个项目的已完成任务/u)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -129,6 +129,9 @@ function externalTaskProgressDetail(
|
||||
task: ExternalGenerationTaskRecord,
|
||||
status: CanvasTaskStatus,
|
||||
) {
|
||||
if (status === 'done') {
|
||||
return task.warning?.trim() || null;
|
||||
}
|
||||
if (!isActiveTaskStatus(status)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ function createQueueState(
|
||||
phaseDetail: string;
|
||||
progress: number;
|
||||
error: string | null;
|
||||
warning: string | null;
|
||||
updatedAtMicros: number;
|
||||
}> = {},
|
||||
) {
|
||||
@@ -223,6 +224,9 @@ function SubmissionWorkflowHarness({
|
||||
useState<CharacterAnimationPanelState | null>(
|
||||
initialCharacterAnimationPanel,
|
||||
);
|
||||
const [generationWarning, setGenerationWarning] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const rememberedImageModelRef = useRef<string | null>(null);
|
||||
const fitLayersMockRef = useRef(vi.fn());
|
||||
const layerCounterRef = useRef(0);
|
||||
@@ -284,6 +288,7 @@ function SubmissionWorkflowHarness({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
onGenerationWarning: setGenerationWarning,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -328,6 +333,7 @@ function SubmissionWorkflowHarness({
|
||||
<span data-testid="remembered-model">
|
||||
{rememberedImageModelRef.current ?? '-'}
|
||||
</span>
|
||||
<span data-testid="generation-warning">{generationWarning ?? '-'}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1694,6 +1700,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
applyProjectSnapshot,
|
||||
refreshTaskList,
|
||||
refreshWalletBalance,
|
||||
undefined,
|
||||
'dialog-background-removal',
|
||||
);
|
||||
await Promise.resolve();
|
||||
@@ -1717,6 +1724,35 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('reports a non-blocking warning after a queued generation completes', async () => {
|
||||
const onGenerationWarning = vi.fn();
|
||||
loadEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'editor-project-1',
|
||||
title: '队列图集项目',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [],
|
||||
resources: [],
|
||||
updatedAt: '2026-06-23T08:00:00.000Z',
|
||||
});
|
||||
|
||||
await applyQueuedEditorGenerationProject(
|
||||
{
|
||||
queueState: createQueueState({
|
||||
warning: '连通域数量不足',
|
||||
}),
|
||||
},
|
||||
'editor-project-1',
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
onGenerationWarning,
|
||||
);
|
||||
|
||||
expect(onGenerationWarning).toHaveBeenCalledWith(
|
||||
'图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps a queued generation pending when the frontend queue wait expires', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
@@ -2144,6 +2180,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(screen.getByTestId('remembered-model').textContent).toBe(
|
||||
'gpt-image-2',
|
||||
);
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits uploaded objectKey icon references before submitting spritesheets', async () => {
|
||||
@@ -2482,6 +2521,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
projectId="editor-project-1"
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
id: 'character-layer',
|
||||
@@ -2514,6 +2554,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
promptText: '跑步',
|
||||
model: 'seedance2.0-fast',
|
||||
assetFolderId: 'project',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -293,6 +293,7 @@ type GenerationSubmissionWorkflowOptions = {
|
||||
applyProjectSnapshot?: (project: EditorProjectSnapshot) => void;
|
||||
onQueuedGenerationTask?: () => void;
|
||||
onWalletBalanceMayHaveChanged?: () => void;
|
||||
onGenerationWarning?: (message: string) => void;
|
||||
};
|
||||
|
||||
async function normalizeImageGenerationReferenceImages(
|
||||
@@ -428,6 +429,17 @@ function notifyWalletBalanceMayHaveChanged(callback?: () => void) {
|
||||
callback?.();
|
||||
}
|
||||
|
||||
function notifyEditorGenerationWarning(
|
||||
warning: string | null | undefined,
|
||||
callback?: (message: string) => void,
|
||||
) {
|
||||
const reason = warning?.trim();
|
||||
if (!reason) {
|
||||
return;
|
||||
}
|
||||
callback?.(`图集已生成,但自动拆分未完成:${reason}`);
|
||||
}
|
||||
|
||||
async function runEditorGenerationWithWalletRefresh<T>(
|
||||
operation: Promise<T>,
|
||||
onWalletBalanceMayHaveChanged?: () => void,
|
||||
@@ -472,6 +484,7 @@ export async function applyQueuedEditorGenerationProject(
|
||||
applyProjectSnapshot: ((project: EditorProjectSnapshot) => void) | undefined,
|
||||
onQueuedGenerationTask?: () => void,
|
||||
onWalletBalanceMayHaveChanged?: () => void,
|
||||
onGenerationWarning?: (message: string) => void,
|
||||
completionDialogId?: string | null,
|
||||
transformProjectSnapshot?: (
|
||||
project: EditorProjectSnapshot,
|
||||
@@ -491,6 +504,7 @@ export async function applyQueuedEditorGenerationProject(
|
||||
if (!terminalState) {
|
||||
return true;
|
||||
}
|
||||
notifyEditorGenerationWarning(terminalState.warning, onGenerationWarning);
|
||||
if (projectId && applyProjectSnapshot) {
|
||||
const applyLoadedProject = (project: EditorProjectSnapshot) => {
|
||||
applyProjectSnapshot(transformProjectSnapshot?.(project) ?? project);
|
||||
@@ -576,6 +590,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
onGenerationWarning,
|
||||
}: GenerationSubmissionWorkflowOptions) {
|
||||
const addGeneratedLayersToCanvas = useCallback(
|
||||
(nextLayers: CanvasLayer[]) => {
|
||||
@@ -1075,6 +1090,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
notifyEditorGenerationWarning(
|
||||
generated.sliceWarning?.reason,
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -1082,6 +1101,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
onGenerationWarning,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -1120,6 +1140,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
fitLayers,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
onGenerationWarning,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
],
|
||||
);
|
||||
@@ -1203,6 +1224,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
rememberImageModel(submissionPlan.rememberImageModel);
|
||||
notifyEditorGenerationWarning(
|
||||
generated.sliceWarning?.reason,
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -1210,6 +1235,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
onGenerationWarning,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -1243,6 +1269,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
projectId,
|
||||
assetFolderId,
|
||||
updateCanvasGenerationDialogById,
|
||||
onGenerationWarning,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
],
|
||||
);
|
||||
@@ -2101,6 +2128,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
projectId,
|
||||
generationInputs,
|
||||
sourceResourceId: characterAnimationSourceLayer.resourceId,
|
||||
assetFolderId,
|
||||
...(canvasDialog && projectId && canvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
|
||||
@@ -343,7 +343,7 @@ export function useImageCanvasGenerationSurface({
|
||||
|
||||
const generationComposerNode = (
|
||||
<>
|
||||
{generationWorkflow.referencePickWarning ? (
|
||||
{generationWorkflow.generationWarning ? (
|
||||
<PlatformRuntimeStatusToast
|
||||
tone="warning"
|
||||
surface="solid"
|
||||
@@ -351,12 +351,12 @@ export function useImageCanvasGenerationSurface({
|
||||
className="image-canvas-editor__reference-pick-toast"
|
||||
role="alert"
|
||||
>
|
||||
<span>{generationWorkflow.referencePickWarning}</span>
|
||||
<span>{generationWorkflow.generationWarning}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="image-canvas-editor__reference-pick-toast-close"
|
||||
aria-label="关闭提示"
|
||||
onClick={generationWorkflow.clearReferencePickWarning}
|
||||
onClick={generationWorkflow.clearGenerationWarning}
|
||||
>
|
||||
<X aria-hidden="true" size={14} strokeWidth={2.4} />
|
||||
</button>
|
||||
|
||||
@@ -247,7 +247,7 @@ function GenerationWorkflowHarness({
|
||||
{activeDialog?.uiDesignSpecReference?.label ?? '-'}
|
||||
</span>
|
||||
<span data-testid="reference-pick-warning">
|
||||
{workflow.referencePickWarning ?? '-'}
|
||||
{workflow.generationWarning ?? '-'}
|
||||
</span>
|
||||
<span data-testid="quick-edit">
|
||||
{workflow.quickEditPanel
|
||||
|
||||
@@ -654,9 +654,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
] = useState(false);
|
||||
const [quickEditPanel, setQuickEditPanel] =
|
||||
useState<QuickEditPanelState | null>(null);
|
||||
const [referencePickWarning, setReferencePickWarning] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [generationWarning, setGenerationWarning] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [cropExpandPanel, setCropExpandPanel] =
|
||||
useState<CropExpandPanelState | null>(null);
|
||||
const [uiAssetExtractionState, setUiAssetExtractionState] =
|
||||
@@ -1618,6 +1618,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
applyProjectSnapshot,
|
||||
refreshTaskListForQueuedGeneration,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
setGenerationWarning,
|
||||
backgroundRemovalDialogId,
|
||||
applyBackgroundRemovalProjectSnapshot
|
||||
? (project) =>
|
||||
@@ -1757,10 +1758,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const pickCharacterSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
if (!isCharacterSpecReferenceLayer(layer)) {
|
||||
setReferencePickWarning(INVALID_CHARACTER_SPEC_WARNING);
|
||||
setGenerationWarning(INVALID_CHARACTER_SPEC_WARNING);
|
||||
return;
|
||||
}
|
||||
setReferencePickWarning(null);
|
||||
setGenerationWarning(null);
|
||||
setGenerateDialog((currentDialog) => {
|
||||
const nextDialog = assignCharacterSpecReference(currentDialog, layer);
|
||||
if (
|
||||
@@ -1817,10 +1818,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const pickIconSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
if (!isIconSpecReferenceLayer(layer)) {
|
||||
setReferencePickWarning(INVALID_ICON_SPEC_WARNING);
|
||||
setGenerationWarning(INVALID_ICON_SPEC_WARNING);
|
||||
return;
|
||||
}
|
||||
setReferencePickWarning(null);
|
||||
setGenerationWarning(null);
|
||||
const nextDialog = assignIconSpecReference(generateDialog, layer);
|
||||
if (nextDialog?.mode === 'icon' && nextDialog.iconSpecReference) {
|
||||
lastIconSpecReferenceRef.current = nextDialog.iconSpecReference;
|
||||
@@ -1839,10 +1840,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const pickUiDesignSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
if (!isIconSpecReferenceLayer(layer)) {
|
||||
setReferencePickWarning(INVALID_ICON_SPEC_WARNING);
|
||||
setGenerationWarning(INVALID_ICON_SPEC_WARNING);
|
||||
return;
|
||||
}
|
||||
setReferencePickWarning(null);
|
||||
setGenerationWarning(null);
|
||||
const nextDialog = assignUiDesignSpecReference(generateDialog, layer);
|
||||
if (
|
||||
nextDialog?.mode === 'ui-design' &&
|
||||
@@ -1923,6 +1924,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask: refreshTaskListForQueuedGeneration,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
onGenerationWarning: setGenerationWarning,
|
||||
});
|
||||
|
||||
const setEffectiveCharacterAnimationPanel: Dispatch<
|
||||
@@ -2488,8 +2490,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setIsPublicationReferenceMenuOpen,
|
||||
isPickingPublicationReferenceFromCanvas,
|
||||
setIsPickingPublicationReferenceFromCanvas,
|
||||
referencePickWarning,
|
||||
clearReferencePickWarning: () => setReferencePickWarning(null),
|
||||
generationWarning,
|
||||
showGenerationWarning: setGenerationWarning,
|
||||
clearGenerationWarning: () => setGenerationWarning(null),
|
||||
openGenerateDialog,
|
||||
openSpecDialog,
|
||||
openCharacterAnimationPanel,
|
||||
@@ -2579,7 +2582,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
isPublicationReferenceMenuOpen,
|
||||
isSpecMenuOpen,
|
||||
isPickingPublicationReferenceFromCanvas,
|
||||
referencePickWarning,
|
||||
generationWarning,
|
||||
openBackgroundMusicGenerationDialog,
|
||||
openCharacterAnimationPanel,
|
||||
openCharacterGenerationDialog,
|
||||
|
||||
@@ -359,6 +359,7 @@ export type EditorCharacterAnimationGenerationInput = {
|
||||
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
sourceResourceId?: string | null;
|
||||
assetFolderId?: string | null;
|
||||
};
|
||||
|
||||
export type EditorCharacterAnimationFrameResult = {
|
||||
|
||||
Reference in New Issue
Block a user