修复图标图集审查中的边界问题
校验 LLM 完成原因并规范化 Agent 图标描述 统一前后端图标规范输入、错误返回和精确撤销行为 收紧图集拆分授权并拒绝引用 ID 歧义 修正快照映射诊断和表单标签可访问性 补充定向测试与编辑器设计文档
This commit is contained in:
@@ -697,6 +697,25 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an icon spritesheet error when the spec reference is not registered', () => {
|
||||
const plan = buildIconSpritesheetGenerationSubmissionPlan({
|
||||
mode: 'icon',
|
||||
prompt: '返回按钮',
|
||||
status: 'idle',
|
||||
iconSpecReference: {
|
||||
id: 'local-icon-spec',
|
||||
label: '本地图标规范',
|
||||
src: 'data:image/png;base64,spec',
|
||||
resourceId: 'local-resource-icon-spec',
|
||||
},
|
||||
});
|
||||
|
||||
expect(plan).toEqual({
|
||||
ok: false,
|
||||
errorMessage: '参考图尚未登记为项目资源或素材,请重新选择或上传后再试',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an icon spritesheet error when descriptions are empty', () => {
|
||||
const plan = buildIconSpritesheetGenerationSubmissionPlan({
|
||||
mode: 'icon',
|
||||
|
||||
@@ -686,6 +686,18 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
errorMessage: `素材描述不能超过 ${EDITOR_ICON_DESCRIPTION_MAX_CHARS} 个字符`,
|
||||
};
|
||||
}
|
||||
let referenceId: string;
|
||||
try {
|
||||
referenceId = resolveRegisteredEditorReferenceId(dialog.iconSpecReference);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: '图标规范尚未完成资源登记,请重新选择或上传后再试',
|
||||
};
|
||||
}
|
||||
|
||||
const rememberImageModel = normalizeEditorImageModel(dialog.imageModel);
|
||||
const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR;
|
||||
@@ -694,7 +706,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
ok: true,
|
||||
iconDescriptions,
|
||||
input: {
|
||||
referenceId: resolveRegisteredEditorReferenceId(dialog.iconSpecReference),
|
||||
referenceId,
|
||||
...(dialog.generationReferences?.length
|
||||
? {
|
||||
referenceImageSrcs: dialog.generationReferences.map((reference) =>
|
||||
|
||||
@@ -532,6 +532,26 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
).toBe('手动修改玩法');
|
||||
});
|
||||
|
||||
it('restores the exact pre-optimization value including surrounding whitespace', async () => {
|
||||
iconSpecClientMocks.refineGamePlay.mockResolvedValueOnce('优化后的玩法');
|
||||
render(<IconSpecHarness playSetting=" 回合制占点 " />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
(screen.getByLabelText('玩法设定') as HTMLTextAreaElement).value,
|
||||
).toBe('优化后的玩法'),
|
||||
);
|
||||
expect(iconSpecClientMocks.refineGamePlay).toHaveBeenCalledWith(
|
||||
'回合制占点',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '撤销玩法设定优化' }));
|
||||
expect(
|
||||
(screen.getByLabelText('玩法设定') as HTMLTextAreaElement).value,
|
||||
).toBe(' 回合制占点 ');
|
||||
});
|
||||
|
||||
it('clears the field undo snapshot after a manual edit', async () => {
|
||||
iconSpecClientMocks.refineGamePlay.mockResolvedValueOnce('成功优化');
|
||||
render(<IconSpecHarness />);
|
||||
|
||||
@@ -206,7 +206,8 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
const key: keyof SpecFormValues = isPlaySetting
|
||||
? 'playSetting'
|
||||
: 'artStyle';
|
||||
const value = dialog.specValues?.[key]?.trim() ?? '';
|
||||
const undoValue = dialog.specValues?.[key] ?? '';
|
||||
const value = undoValue.trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
@@ -229,7 +230,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
return;
|
||||
}
|
||||
onUpdateSpecFormValue(key, refined);
|
||||
setOptimization({ optimizing: false, undoValue: value });
|
||||
setOptimization({ optimizing: false, undoValue });
|
||||
} catch (error) {
|
||||
if (
|
||||
activeOptimizationDialogKeyRef.current !== requestDialogKey ||
|
||||
@@ -472,18 +473,22 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
] as const
|
||||
).map((item) => {
|
||||
const value = dialog.specValues?.[item.key] ?? '';
|
||||
const fieldId = `image-canvas-icon-spec-${dialog.id ?? 'active'}-${item.field}`;
|
||||
return (
|
||||
<label
|
||||
<div
|
||||
key={item.field}
|
||||
className="image-canvas-editor__field-block"
|
||||
>
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
{item.title}
|
||||
</PlatformFieldLabel>
|
||||
<label htmlFor={fieldId}>
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
{item.title}
|
||||
</PlatformFieldLabel>
|
||||
</label>
|
||||
<AutoGrowTextArea
|
||||
id={fieldId}
|
||||
aria-label={item.title}
|
||||
value={value}
|
||||
placeholder={item.placeholder}
|
||||
@@ -562,7 +567,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
{EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -83,7 +83,6 @@ import {
|
||||
buildIconSpritesheetGenerationSubmissionPlan,
|
||||
buildImageGenerationSubmissionPlan,
|
||||
resolveGenerationAssetLabel,
|
||||
resolveRegisteredEditorReferenceId,
|
||||
} from './ImageCanvasGenerationSubmissionModel';
|
||||
import type {
|
||||
UiAssetExtractionMark,
|
||||
@@ -1341,11 +1340,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
if (!canvasDialog) {
|
||||
return;
|
||||
}
|
||||
const iconSpecReference = dialog.iconSpecReference;
|
||||
if (!iconSpecReference) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmittingIconDialog({
|
||||
...canvasDialog,
|
||||
iconDescriptions: submissionPlan.iconDescriptions,
|
||||
@@ -1357,7 +1351,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
try {
|
||||
const canvasCompletionPlaceholder =
|
||||
getGeneratingDialogPlaceholder(dialog);
|
||||
const referenceId = resolveRegisteredEditorReferenceId(iconSpecReference);
|
||||
const referenceImageSrcs = await Promise.all(
|
||||
(dialog.generationReferences ?? []).map((reference) =>
|
||||
resolveEditorGenerationMediaReference(
|
||||
@@ -1370,7 +1363,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
generateEditorIconSpritesheet({
|
||||
...submissionPlan.input,
|
||||
referenceId,
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
projectId,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
deleteEditorAssetFolder,
|
||||
deleteEditorProject,
|
||||
editEditorImage,
|
||||
EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS,
|
||||
EDITOR_ICON_DESCRIPTION_MAX_CHARS,
|
||||
EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS,
|
||||
EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES,
|
||||
EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH,
|
||||
EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS,
|
||||
extractEditorUiDesignAssets,
|
||||
generateEditorBackgroundMusic,
|
||||
generateEditorCharacterAnimation,
|
||||
@@ -1596,6 +1596,31 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes icon spec fields and rejects blank values before requesting', async () => {
|
||||
await expect(refineEditorIconSpecPlaySetting(' \n ')).rejects.toThrow(
|
||||
'玩法设定不能为空',
|
||||
);
|
||||
await expect(
|
||||
generateEditorIconSpec({
|
||||
playSetting: '回合制',
|
||||
artStyle: ' ',
|
||||
}),
|
||||
).rejects.toThrow('美术风格不能为空');
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
|
||||
requestJsonMock.mockResolvedValueOnce({ playSetting: ' 优化玩法 ' });
|
||||
await expect(refineEditorIconSpecPlaySetting(' 原玩法 ')).resolves.toBe(
|
||||
'优化玩法',
|
||||
);
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/editor/llm/icon-specs/refine-game-play',
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({ playSetting: '原玩法' }),
|
||||
}),
|
||||
'优化玩法设定失败',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects overlong icon spec generation fields before dispatch', async () => {
|
||||
await expect(
|
||||
generateEditorIconSpec({
|
||||
|
||||
@@ -76,12 +76,16 @@ function assertStableEditorMediaReferences(
|
||||
}
|
||||
|
||||
function requireEditorIconSpecPromptLength(value: string, fieldLabel: string) {
|
||||
if (Array.from(value).length > EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH) {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) {
|
||||
throw new Error(`${fieldLabel}不能为空`);
|
||||
}
|
||||
if (Array.from(normalized).length > EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH) {
|
||||
throw new Error(
|
||||
`${fieldLabel}不能超过 ${EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH} 个字符`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolveEditorProviderReferenceLimit(model: string | null | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user