重构:统一编辑器工作流中的 "icon-spec" 处理

- 将所有相关工作流从 "ui-spec" 迁移至 "icon-spec"。
- 优化 `playSetting` 和 `artStyle` 的输入处理逻辑。
- 更新编辑器提示词、提交模型及测试用例以适配 "icon-spec"。
- 在 UI 及后端调用中集成 `EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH` 及相关校验。
- 新增 "icon-spec" 生成与微调的专用 API 路径。
- 移除旧版 "ui-spec" 相关定义及提示词构建器。
This commit is contained in:
2026-08-04 20:04:07 +08:00
parent cc86d29b83
commit 3485bad2d3
19 changed files with 980 additions and 187 deletions
@@ -9,9 +9,11 @@ import {
deleteEditorAssetFolder,
deleteEditorProject,
editEditorImage,
EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH,
extractEditorUiDesignAssets,
generateEditorBackgroundMusic,
generateEditorCharacterAnimation,
generateEditorIconSpec,
generateEditorIconSpritesheet,
generateEditorImage,
generateEditorSoundEffect,
@@ -23,6 +25,8 @@ import {
loadEditorProject,
loadOrCreateRecentEditorProject,
removeEditorImageBackground,
refineEditorIconSpecArtStyle,
refineEditorIconSpecPlaySetting,
renameEditorProject,
saveEditorProjectLayout,
splitEditorIconSpritesheet,
@@ -282,7 +286,9 @@ describe('editorProjectClient', () => {
nextCursor: null,
});
const page = await listPublicEditorProjectResources({ cursor: ' cursor-1 ' });
const page = await listPublicEditorProjectResources({
cursor: ' cursor-1 ',
});
expect(page.resources).toEqual([]);
expect(page.nextCursor).toBeNull();
@@ -749,8 +755,7 @@ describe('editorProjectClient', () => {
taskId: 'vector-task-1',
warning: {
code: 'postprocess-failed-source-preserved',
reason:
'生成任务成功,后处理失败。',
reason: '生成任务成功,后处理失败。',
},
});
@@ -761,8 +766,7 @@ describe('editorProjectClient', () => {
expect(result.taskId).toBe('vector-task-1');
expect(result.warning).toEqual({
code: 'postprocess-failed-source-preserved',
reason:
'生成任务成功,后处理失败。',
reason: '生成任务成功,后处理失败。',
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/images/generations',
@@ -812,7 +816,9 @@ describe('editorProjectClient', () => {
resolution: '720p',
mode: 'std',
sound: 'off',
referenceVideoSrcs: [`https://assets.example.test/${'a'.repeat(256 * 1024)}`],
referenceVideoSrcs: [
`https://assets.example.test/${'a'.repeat(256 * 1024)}`,
],
}),
).rejects.toThrow('稳定引用字段总长度不能超过 256KB');
@@ -1242,6 +1248,116 @@ 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('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: '低多边形',
referenceImageSrcs: ['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: '低多边形',
referenceImageSrcs: ['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');
});
it('passes publication material generation kind and size to the backend BFF', async () => {
requestJsonMock.mockResolvedValueOnce({
imageSrc: 'data:image/png;base64,publication',