合并图标规范与图集生成优化
合入 PR 137 的图标规范生成、稳定引用与图集提示词链路 保留主分支现行音效提交、Prompt 模块与结果压缩逻辑 解决 External Worker、生成提交和客户端测试冲突
This commit is contained in:
@@ -12,10 +12,15 @@ import {
|
||||
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_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS,
|
||||
extractEditorUiDesignAssets,
|
||||
generateEditorBackgroundMusic,
|
||||
generateEditorCharacterAnimation,
|
||||
generateEditorIconSpec,
|
||||
generateEditorIconSpritesheet,
|
||||
generateEditorImage,
|
||||
generateEditorScene,
|
||||
@@ -28,6 +33,8 @@ import {
|
||||
loadEditorProject,
|
||||
loadOrCreateRecentEditorProject,
|
||||
optimizeEditorSoundEffectPrompt,
|
||||
refineEditorIconSpecArtStyle,
|
||||
refineEditorIconSpecPlaySetting,
|
||||
removeEditorImageBackground,
|
||||
renameEditorProject,
|
||||
saveEditorProjectLayout,
|
||||
@@ -1070,7 +1077,7 @@ describe('editorProjectClient', () => {
|
||||
).rejects.toThrow('生成参考图最多允许 5 张');
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: references.slice(0, 5),
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
@@ -1105,7 +1112,7 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
|
||||
await generateEditorIconSpritesheet({
|
||||
referenceImageSrc: '/generated-images/editor/icon-spec.png',
|
||||
referenceId: 'editor-resource-icon-spec',
|
||||
referenceImageSrcs: references,
|
||||
iconDescriptions: ['返回按钮'],
|
||||
});
|
||||
@@ -1288,7 +1295,7 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
const result = await generateEditorIconSpritesheet({
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
assetLabel: '冒险游戏图标',
|
||||
@@ -1305,7 +1312,7 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
@@ -1320,6 +1327,42 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects icon descriptions outside the item and aggregate contracts before requesting', async () => {
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS + 1)],
|
||||
}),
|
||||
).rejects.toThrow(`不能超过 ${EDITOR_ICON_DESCRIPTION_MAX_CHARS} 个字符`);
|
||||
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: [
|
||||
'图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
|
||||
...Array.from({ length: 10 }, () =>
|
||||
'图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
|
||||
),
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS} 个字符`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: Array.from({ length: 8 }, () =>
|
||||
'😀'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
|
||||
),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES} 个 UTF-8 字节`,
|
||||
);
|
||||
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes image model options to icon spritesheet generation', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
spritesheetImageSrc: 'data:image/png;base64,sheet',
|
||||
@@ -1334,7 +1377,7 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
await generateEditorIconSpritesheet({
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
@@ -1356,7 +1399,7 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
@@ -1566,6 +1609,144 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses dedicated icon spec refine endpoints', async () => {
|
||||
requestJsonMock
|
||||
.mockResolvedValueOnce({ playSetting: '优化玩法' })
|
||||
.mockResolvedValueOnce({ artStyle: '优化画风' });
|
||||
|
||||
await expect(refineEditorIconSpecPlaySetting('原玩法')).resolves.toBe(
|
||||
'优化玩法',
|
||||
);
|
||||
await expect(refineEditorIconSpecArtStyle('原画风')).resolves.toBe(
|
||||
'优化画风',
|
||||
);
|
||||
expect(requestJsonMock.mock.calls[0]).toEqual([
|
||||
'/api/editor/llm/icon-specs/refine-game-play',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ playSetting: '原玩法' }),
|
||||
}),
|
||||
'优化玩法设定失败',
|
||||
]);
|
||||
expect(requestJsonMock.mock.calls[1]).toEqual([
|
||||
'/api/editor/llm/icon-specs/refine-art-style',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ artStyle: '原画风' }),
|
||||
}),
|
||||
'优化美术风格失败',
|
||||
]);
|
||||
});
|
||||
|
||||
it('enforces icon spec prompt length before and after refine requests', async () => {
|
||||
const overLimit = '玩'.repeat(EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH + 1);
|
||||
|
||||
await expect(refineEditorIconSpecPlaySetting(overLimit)).rejects.toThrow(
|
||||
'玩法设定不能超过 200 个字符',
|
||||
);
|
||||
await expect(refineEditorIconSpecArtStyle(overLimit)).rejects.toThrow(
|
||||
'美术风格不能超过 200 个字符',
|
||||
);
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
|
||||
requestJsonMock.mockResolvedValueOnce({ playSetting: overLimit });
|
||||
await expect(refineEditorIconSpecPlaySetting('原玩法')).rejects.toThrow(
|
||||
'玩法设定不能超过 200 个字符',
|
||||
);
|
||||
});
|
||||
|
||||
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({
|
||||
playSetting: '玩'.repeat(EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH + 1),
|
||||
artStyle: '低多边形',
|
||||
}),
|
||||
).rejects.toThrow('玩法设定不能超过 200 个字符');
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits icon spec business fields without a client prompt or image kind', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
imageSrc: 'data:image/png;base64,spec',
|
||||
width: 2048,
|
||||
height: 1152,
|
||||
});
|
||||
|
||||
await generateEditorIconSpec({
|
||||
playSetting: '回合制占点',
|
||||
artStyle: '低多边形',
|
||||
referenceId: 'resource-reference-1',
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '参考图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-reference-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '图标规范 1',
|
||||
sourceResourceId: 'resource-source-1',
|
||||
});
|
||||
|
||||
const body = JSON.parse(requestJsonMock.mock.calls[0]?.[1]?.body as string);
|
||||
expect(body).toEqual({
|
||||
playSetting: '回合制占点',
|
||||
artStyle: '低多边形',
|
||||
referenceId: 'resource-reference-1',
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '参考图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-reference-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '图标规范 1',
|
||||
sourceResourceId: 'resource-source-1',
|
||||
});
|
||||
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 () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
imageSrc: 'data:image/png;base64,publication',
|
||||
|
||||
Reference in New Issue
Block a user