修复图标规范评审问题
迁移旧版 ui 规范快照并隔离跨对话框优化结果。 关闭非幂等图标规范生成重试并统一参考图解析逻辑。 限制 LLM 重试范围并严格校验优化文本与补全参数。 修正图集键色、数量与素材间距提示约束并补充行为测试。 同步图片画布与生成面板技术文档。
This commit is contained in:
@@ -785,6 +785,18 @@ describe('ImageCanvasEditorModel', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('migrates restored legacy ui specs to the icon spec flow', () => {
|
||||
expect(
|
||||
hydrateCanvasGenerationDialog({
|
||||
id: 'generation-dialog-legacy-ui-spec',
|
||||
mode: 'spec',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
specType: 'ui',
|
||||
})?.specType,
|
||||
).toBe('icon');
|
||||
});
|
||||
|
||||
it('drops restored generator references owned by another user', () => {
|
||||
const dialog: CanvasGenerationDialogState = {
|
||||
id: 'generation-dialog-owner',
|
||||
|
||||
@@ -575,9 +575,7 @@ export function hydrateCanvasGenerationDialog(
|
||||
: true,
|
||||
sourceLayerId: stringOrUndefined(snapshot.sourceLayerId),
|
||||
generatedLayerId: stringOrUndefined(snapshot.generatedLayerId),
|
||||
specType: isSpecGenerationType(snapshot.specType)
|
||||
? snapshot.specType
|
||||
: undefined,
|
||||
specType: normalizeSpecGenerationType(snapshot.specType),
|
||||
specValues: hydrateSpecFormValues(snapshot.specValues),
|
||||
specReference: hydrateCharacterReference(
|
||||
snapshot.specReference,
|
||||
@@ -1250,12 +1248,16 @@ function isGenerationStatus(
|
||||
function isSpecGenerationType(
|
||||
value: unknown,
|
||||
): value is NonNullable<CanvasGenerationDialogState['specType']> {
|
||||
return (
|
||||
value === 'character' ||
|
||||
value === 'ui' ||
|
||||
value === 'icon' ||
|
||||
value === 'custom'
|
||||
);
|
||||
return value === 'character' || value === 'icon' || value === 'custom';
|
||||
}
|
||||
|
||||
function normalizeSpecGenerationType(
|
||||
value: unknown,
|
||||
): CanvasGenerationDialogState['specType'] {
|
||||
if (value === 'ui') {
|
||||
return 'icon';
|
||||
}
|
||||
return isSpecGenerationType(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function hydratePublicationWorkflowId(
|
||||
|
||||
@@ -353,11 +353,13 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
'玩法设定',
|
||||
) as HTMLTextAreaElement;
|
||||
const artStyle = screen.getByLabelText('美术风格') as HTMLTextAreaElement;
|
||||
expect(playSetting.maxLength).toBe(200);
|
||||
expect(artStyle.maxLength).toBe(200);
|
||||
expect(playSetting.hasAttribute('maxlength')).toBe(false);
|
||||
expect(artStyle.hasAttribute('maxlength')).toBe(false);
|
||||
|
||||
fireEvent.change(playSetting, { target: { value: overLimit } });
|
||||
expect(Array.from(playSetting.value)).toHaveLength(200);
|
||||
fireEvent.change(playSetting, { target: { value: '🎮'.repeat(201) } });
|
||||
expect(Array.from(playSetting.value)).toHaveLength(200);
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交生成规范' }));
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -451,6 +453,36 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores an optimization result after the active dialog changes', async () => {
|
||||
const deferred = createDeferred<string>();
|
||||
const updateSpecFormValue = vi.fn();
|
||||
iconSpecClientMocks.refineGamePlay.mockReturnValueOnce(deferred.promise);
|
||||
const { rerender } = render(
|
||||
<ImageCanvasSpecGenerationPanelView
|
||||
dialog={createSpecDialog({ id: 'dialog-a', specType: 'icon' })}
|
||||
style={{ left: 10, top: 20 }}
|
||||
onUpdateSpecFormValue={updateSpecFormValue}
|
||||
onRequestUpload={vi.fn()}
|
||||
onSubmit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
|
||||
rerender(
|
||||
<ImageCanvasSpecGenerationPanelView
|
||||
dialog={createSpecDialog({ id: 'dialog-b', specType: 'icon' })}
|
||||
style={{ left: 10, top: 20 }}
|
||||
onUpdateSpecFormValue={updateSpecFormValue}
|
||||
onRequestUpload={vi.fn()}
|
||||
onSubmit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await act(async () => deferred.resolve('不应写入新对话框'));
|
||||
|
||||
expect(updateSpecFormValue).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole('alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps one undo snapshot per icon spec field and replaces it after a later successful optimization', async () => {
|
||||
iconSpecClientMocks.refineGamePlay
|
||||
.mockResolvedValueOnce('第一次优化')
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
@@ -143,7 +144,16 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
const [optimizationError, setOptimizationError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const optimizationDialogKey = `${dialog.id ?? 'active'}:${dialog.mode}:${dialog.specType ?? ''}`;
|
||||
const activeOptimizationDialogKeyRef = useRef(optimizationDialogKey);
|
||||
const optimizationRequestVersionsRef = useRef({
|
||||
playSetting: 0,
|
||||
artStyle: 0,
|
||||
});
|
||||
activeOptimizationDialogKeyRef.current = optimizationDialogKey;
|
||||
useEffect(() => {
|
||||
optimizationRequestVersionsRef.current.playSetting += 1;
|
||||
optimizationRequestVersionsRef.current.artStyle += 1;
|
||||
setPlaySettingOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
|
||||
setArtStyleOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
|
||||
setOptimizationError(null);
|
||||
@@ -185,23 +195,39 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
const setOptimization = isPlaySetting
|
||||
? setPlaySettingOptimization
|
||||
: setArtStyleOptimization;
|
||||
const requestVersion = optimizationRequestVersionsRef.current[field] + 1;
|
||||
optimizationRequestVersionsRef.current[field] = requestVersion;
|
||||
const requestDialogKey = optimizationDialogKey;
|
||||
setOptimization((current) => ({ ...current, optimizing: true }));
|
||||
setOptimizationError(null);
|
||||
try {
|
||||
const refined = isPlaySetting
|
||||
? await refineEditorIconSpecPlaySetting(value)
|
||||
: await refineEditorIconSpecArtStyle(value);
|
||||
if (
|
||||
activeOptimizationDialogKeyRef.current !== requestDialogKey ||
|
||||
optimizationRequestVersionsRef.current[field] !== requestVersion
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onUpdateSpecFormValue(key, refined);
|
||||
setOptimization({ optimizing: false, undoValue: value });
|
||||
} catch (error) {
|
||||
if (
|
||||
activeOptimizationDialogKeyRef.current !== requestDialogKey ||
|
||||
optimizationRequestVersionsRef.current[field] !== requestVersion
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setOptimization((current) => ({ ...current, optimizing: false }));
|
||||
setOptimizationError(
|
||||
const fallbackMessage = isPlaySetting
|
||||
? '优化玩法设定失败'
|
||||
: '优化美术风格失败';
|
||||
const errorMessage =
|
||||
error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: isPlaySetting
|
||||
? '优化玩法设定失败'
|
||||
: '优化美术风格失败',
|
||||
);
|
||||
: fallbackMessage;
|
||||
setOptimizationError(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -389,7 +415,8 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : dialog.specType === 'custom' ? (
|
||||
) : null}
|
||||
{!isUiDesignDialog && dialog.specType === 'custom' ? (
|
||||
<label className="image-canvas-editor__field-block">
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
@@ -411,7 +438,8 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : isIconSpec ? (
|
||||
) : null}
|
||||
{!isUiDesignDialog && isIconSpec ? (
|
||||
<>
|
||||
{(
|
||||
[
|
||||
@@ -448,7 +476,6 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
variant="textarea"
|
||||
aria-label={item.title}
|
||||
value={value}
|
||||
maxLength={EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH}
|
||||
placeholder={item.placeholder}
|
||||
disabled={isGenerating || item.optimization.optimizing}
|
||||
size="sm"
|
||||
@@ -504,7 +531,8 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
) : null}
|
||||
{!isUiDesignDialog && dialog.specType !== 'custom' && !isIconSpec ? (
|
||||
<>
|
||||
<label className="image-canvas-editor__field-block">
|
||||
<PlatformFieldLabel
|
||||
@@ -612,7 +640,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
|
||||
@@ -310,11 +310,13 @@ type GenerationSubmissionWorkflowOptions = {
|
||||
onGenerationWarning?: (message: string) => void;
|
||||
};
|
||||
|
||||
async function normalizeImageGenerationReferenceImages(
|
||||
input: Parameters<typeof generateEditorImage>[0],
|
||||
async function normalizeGenerationReferenceImages<
|
||||
T extends { referenceImageSrcs?: string[] },
|
||||
>(
|
||||
input: T,
|
||||
references: CharacterReferenceImage[],
|
||||
projectId?: string | null,
|
||||
) {
|
||||
): Promise<T> {
|
||||
if (!input.referenceImageSrcs?.length) {
|
||||
return input;
|
||||
}
|
||||
@@ -335,30 +337,6 @@ async function normalizeImageGenerationReferenceImages(
|
||||
};
|
||||
}
|
||||
|
||||
async function normalizeIconSpecGenerationReferenceImages(
|
||||
input: Parameters<typeof generateEditorIconSpec>[0],
|
||||
references: CharacterReferenceImage[],
|
||||
projectId?: string | null,
|
||||
) {
|
||||
if (!input.referenceImageSrcs?.length) {
|
||||
return input;
|
||||
}
|
||||
return {
|
||||
...input,
|
||||
referenceImageSrcs: await Promise.all(
|
||||
input.referenceImageSrcs.map((referenceImageSrc, index) =>
|
||||
resolveEditorGenerationMediaReference(
|
||||
references[index]
|
||||
? { ...references[index], src: referenceImageSrc }
|
||||
: { src: referenceImageSrc },
|
||||
'image',
|
||||
projectId,
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveImageGenerationDialogReferences(
|
||||
dialog: GenerateDialogState,
|
||||
): CharacterReferenceImage[] {
|
||||
@@ -1941,12 +1919,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
submissionPlan.result.title,
|
||||
);
|
||||
} else if (submissionPlan.kind === 'icon-spec') {
|
||||
const iconSpecInput =
|
||||
await normalizeIconSpecGenerationReferenceImages(
|
||||
submissionPlan.input,
|
||||
resolveImageGenerationDialogReferences(dialog),
|
||||
projectId,
|
||||
);
|
||||
const iconSpecInput = await normalizeGenerationReferenceImages(
|
||||
submissionPlan.input,
|
||||
resolveImageGenerationDialogReferences(dialog),
|
||||
projectId,
|
||||
);
|
||||
const canvasCompletionPlaceholder =
|
||||
getGeneratingDialogPlaceholder(dialog);
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
@@ -2118,12 +2095,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
canvasDialog?.id,
|
||||
);
|
||||
} else {
|
||||
const imageGenerationInput =
|
||||
await normalizeImageGenerationReferenceImages(
|
||||
submissionPlan.input,
|
||||
resolveImageGenerationDialogReferences(dialog),
|
||||
projectId,
|
||||
);
|
||||
const imageGenerationInput = await normalizeGenerationReferenceImages(
|
||||
submissionPlan.input,
|
||||
resolveImageGenerationDialogReferences(dialog),
|
||||
projectId,
|
||||
);
|
||||
const resultTitle =
|
||||
submissionPlan.result.title ??
|
||||
`生成图片 ${layerCounterRef.current + 1}`;
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
loadEditorGenerationPricing,
|
||||
loadEditorProject,
|
||||
loadOrCreateRecentEditorProject,
|
||||
removeEditorImageBackground,
|
||||
refineEditorIconSpecArtStyle,
|
||||
refineEditorIconSpecPlaySetting,
|
||||
removeEditorImageBackground,
|
||||
renameEditorProject,
|
||||
saveEditorProjectLayout,
|
||||
splitEditorIconSpritesheet,
|
||||
@@ -1356,6 +1356,9 @@ describe('editorProjectClient', () => {
|
||||
expect(body).not.toHaveProperty('prompt');
|
||||
expect(body).not.toHaveProperty('kind');
|
||||
expect(body).not.toHaveProperty('assetKind');
|
||||
expect(requestJsonMock.mock.calls[0]?.[3]).toEqual({
|
||||
timeoutMs: 1_200_000,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes publication material generation kind and size to the backend BFF', async () => {
|
||||
|
||||
@@ -1020,7 +1020,6 @@ export async function generateEditorIconSpec(
|
||||
'生成图标规范失败',
|
||||
{
|
||||
timeoutMs: 1_200_000,
|
||||
retry: EDITOR_REQUEST_RETRY_OPTIONS,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user