重构图标规范与图集生成链路
新增图标规范独立队列任务并将参数补全纳入统一计费操作 将图标图集生成和手工拆分迁入独立图标模块并复用公共生图能力 将图标规范引用收紧为项目资源或素材 ID 同步 SpacetimeDB 引用解析契约、External OpenAPI、前端与 Agent 提交链路 补充相关回归测试和长期架构约定
This commit is contained in:
@@ -2701,7 +2701,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
await waitFor(() => {
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrc: 'resource-icon-spec',
|
||||
referenceId: 'resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮\n设置按钮'],
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
@@ -3171,7 +3171,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
await waitFor(() => {
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrc: 'resource-editor-icon-spec-renamed-1',
|
||||
referenceId: 'resource-editor-icon-spec-renamed-1',
|
||||
iconDescriptions: ['背包按钮'],
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
|
||||
@@ -243,7 +243,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
input: {
|
||||
playSetting: '休闲消除',
|
||||
artStyle: '清爽卡通',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
referenceId: 'resource-spec-ref',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '玩法设定', value: '休闲消除' },
|
||||
@@ -262,6 +262,20 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
result: {
|
||||
assetKind: 'icon-spec',
|
||||
title: '图标规范 8',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '玩法设定', value: '休闲消除' },
|
||||
{ title: '美术风格', value: '清爽卡通' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '参考.png',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-spec-ref',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(plan.kind === 'icon-spec' ? plan.result.generationInputs : null).toEqual(
|
||||
@@ -733,7 +747,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
ok: true,
|
||||
iconDescriptions: ['返回按钮\n\n设置按钮'],
|
||||
input: {
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
referenceId: 'resource-icon-spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
iconDescriptions: ['返回按钮\n\n设置按钮'],
|
||||
model: 'gpt-image-2',
|
||||
|
||||
@@ -91,6 +91,25 @@ export function resolveImageReferenceSubmissionSource(reference: {
|
||||
return reference.objectKey?.trim() || reference.src;
|
||||
}
|
||||
|
||||
export function resolveRegisteredEditorReferenceId(reference: {
|
||||
resourceId?: string | null;
|
||||
sourceAssetId?: string | null;
|
||||
}) {
|
||||
const resourceId = reference.resourceId?.trim();
|
||||
if (
|
||||
resourceId &&
|
||||
!resourceId.startsWith('local-resource-') &&
|
||||
!resourceId.startsWith('generation-dialog:')
|
||||
) {
|
||||
return resourceId;
|
||||
}
|
||||
const assetId = reference.sourceAssetId?.trim();
|
||||
if (assetId) {
|
||||
return assetId;
|
||||
}
|
||||
throw new Error('参考图尚未登记为项目资源或素材,请重新选择或上传后再试');
|
||||
}
|
||||
|
||||
function buildSeedanceVideoReferenceInput(
|
||||
references: NonNullable<GenerateDialogState['generationReferences']>,
|
||||
) {
|
||||
@@ -318,9 +337,9 @@ export function buildImageGenerationSubmissionPlan({
|
||||
artStyle: specValues.artStyle.trim(),
|
||||
...(dialog.specReference?.src
|
||||
? {
|
||||
referenceImageSrcs: [
|
||||
resolveImageReferenceSubmissionSource(dialog.specReference),
|
||||
],
|
||||
referenceId: resolveRegisteredEditorReferenceId(
|
||||
dialog.specReference,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
generationInputs,
|
||||
@@ -664,9 +683,7 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
ok: true,
|
||||
iconDescriptions,
|
||||
input: {
|
||||
referenceImageSrc: resolveImageReferenceSubmissionSource(
|
||||
dialog.iconSpecReference,
|
||||
),
|
||||
referenceId: resolveRegisteredEditorReferenceId(dialog.iconSpecReference),
|
||||
...(dialog.generationReferences?.length
|
||||
? {
|
||||
referenceImageSrcs: dialog.generationReferences.map((reference) =>
|
||||
|
||||
@@ -2667,6 +2667,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
id: 'spec',
|
||||
label: '图标规范',
|
||||
src: 'data:image/png;base64,c3BlYw==',
|
||||
resourceId: 'resource-icon-spec',
|
||||
},
|
||||
iconDescriptions: ['旧返回', '旧设置'],
|
||||
placeholder: {
|
||||
@@ -2687,8 +2688,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrc:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
referenceId: 'resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮\n设置按钮'],
|
||||
model: 'gpt-image-2',
|
||||
assetLabel: '复古操作图标',
|
||||
@@ -2754,6 +2754,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
id: 'spec',
|
||||
label: '图标规范',
|
||||
src: 'data:image/png;base64,c3BlYw==',
|
||||
resourceId: 'resource-icon-spec',
|
||||
},
|
||||
iconDescriptions: [],
|
||||
placeholder: {
|
||||
@@ -2779,7 +2780,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('submits uploaded objectKey icon references before submitting spritesheets', async () => {
|
||||
it('submits registered icon spec IDs and keeps objectKey extra references', async () => {
|
||||
generateEditorIconSpritesheetMock.mockResolvedValueOnce({
|
||||
spritesheetImageSrc: 'data:image/png;base64,sheet',
|
||||
spritesheetWidth: 512,
|
||||
@@ -2806,6 +2807,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
src: 'https://signed.example.test/icon-spec.png?expires=1',
|
||||
objectKey: 'generated-character-drafts/editor/specs/icon-spec.png',
|
||||
assetObjectId: 'assetobj-icon-spec',
|
||||
resourceId: 'resource-icon-spec',
|
||||
},
|
||||
generationReferences: [
|
||||
{
|
||||
@@ -2830,8 +2832,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
).not.toHaveBeenCalled();
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrc:
|
||||
'generated-character-drafts/editor/specs/icon-spec.png',
|
||||
referenceId: 'resource-icon-spec',
|
||||
referenceImageSrcs: [
|
||||
'generated-character-drafts/editor/refs/icon-style.png',
|
||||
],
|
||||
|
||||
@@ -81,6 +81,7 @@ import {
|
||||
buildIconSpritesheetGenerationSubmissionPlan,
|
||||
buildImageGenerationSubmissionPlan,
|
||||
resolveGenerationAssetLabel,
|
||||
resolveRegisteredEditorReferenceId,
|
||||
} from './ImageCanvasGenerationSubmissionModel';
|
||||
import type {
|
||||
UiAssetExtractionMark,
|
||||
@@ -1273,11 +1274,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
try {
|
||||
const canvasCompletionPlaceholder =
|
||||
getGeneratingDialogPlaceholder(dialog);
|
||||
const referenceImageSrc = await resolveEditorGenerationMediaReference(
|
||||
iconSpecReference,
|
||||
'image',
|
||||
projectId,
|
||||
);
|
||||
const referenceId = resolveRegisteredEditorReferenceId(iconSpecReference);
|
||||
const referenceImageSrcs = await Promise.all(
|
||||
(dialog.generationReferences ?? []).map((reference) =>
|
||||
resolveEditorGenerationMediaReference(
|
||||
@@ -1290,7 +1287,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
generateEditorIconSpritesheet({
|
||||
...submissionPlan.input,
|
||||
referenceImageSrc,
|
||||
referenceId,
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
projectId,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
@@ -1943,11 +1940,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
submissionPlan.result.title,
|
||||
);
|
||||
} else if (submissionPlan.kind === 'icon-spec') {
|
||||
const iconSpecInput = await normalizeGenerationReferenceImages(
|
||||
submissionPlan.input,
|
||||
resolveImageGenerationDialogReferences(dialog),
|
||||
projectId,
|
||||
);
|
||||
const iconSpecInput = submissionPlan.input;
|
||||
const canvasCompletionPlaceholder =
|
||||
getGeneratingDialogPlaceholder(dialog);
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
|
||||
@@ -900,7 +900,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',
|
||||
@@ -935,7 +935,7 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
|
||||
await generateEditorIconSpritesheet({
|
||||
referenceImageSrc: '/generated-images/editor/icon-spec.png',
|
||||
referenceId: 'editor-resource-icon-spec',
|
||||
referenceImageSrcs: references,
|
||||
iconDescriptions: ['返回按钮'],
|
||||
});
|
||||
@@ -1118,7 +1118,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: '冒险游戏图标',
|
||||
@@ -1135,7 +1135,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',
|
||||
@@ -1164,7 +1164,7 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
await generateEditorIconSpritesheet({
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
@@ -1186,7 +1186,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',
|
||||
@@ -1452,20 +1452,6 @@ describe('editorProjectClient', () => {
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects excess icon spec references before dispatch', async () => {
|
||||
await expect(
|
||||
generateEditorIconSpec({
|
||||
playSetting: '回合制占点',
|
||||
artStyle: '低多边形',
|
||||
referenceImageSrcs: Array.from(
|
||||
{ length: 6 },
|
||||
(_, index) => `resource-reference-${index + 1}`,
|
||||
),
|
||||
}),
|
||||
).rejects.toThrow('图标规范参考图最多允许 5 张,当前选择 6 张');
|
||||
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',
|
||||
@@ -1476,7 +1462,7 @@ describe('editorProjectClient', () => {
|
||||
await generateEditorIconSpec({
|
||||
playSetting: '回合制占点',
|
||||
artStyle: '低多边形',
|
||||
referenceImageSrcs: ['resource-reference-1'],
|
||||
referenceId: 'resource-reference-1',
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [],
|
||||
@@ -1498,7 +1484,7 @@ describe('editorProjectClient', () => {
|
||||
expect(body).toEqual({
|
||||
playSetting: '回合制占点',
|
||||
artStyle: '低多边形',
|
||||
referenceImageSrcs: ['resource-reference-1'],
|
||||
referenceId: 'resource-reference-1',
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [],
|
||||
|
||||
@@ -258,7 +258,7 @@ export type EditorImageGenerationInput = {
|
||||
export type EditorIconSpecGenerationInput = {
|
||||
playSetting: string;
|
||||
artStyle: string;
|
||||
referenceImageSrcs?: string[];
|
||||
referenceId?: string;
|
||||
projectId?: string | null;
|
||||
generationInputs?: EditorAssetGenerationInputs | null;
|
||||
assetFolderId?: string | null;
|
||||
@@ -281,7 +281,7 @@ export type EditorCanvasGenerationCompletionInput = {
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetGenerationInput = {
|
||||
referenceImageSrc: string;
|
||||
referenceId: string;
|
||||
referenceImageSrcs?: string[];
|
||||
iconDescriptions: string[];
|
||||
model?: string;
|
||||
@@ -1105,20 +1105,12 @@ export async function generateEditorIconSpec(
|
||||
input.artStyle,
|
||||
'美术风格',
|
||||
);
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, '图标规范参考图');
|
||||
assertEditorReferenceLimit(
|
||||
input.referenceImageSrcs,
|
||||
EDITOR_IMAGE_REFERENCE_LIMIT,
|
||||
'图标规范参考图',
|
||||
);
|
||||
return requestJson<EditorImageGenerationResponse>(
|
||||
EDITOR_ICON_SPEC_GENERATION_API,
|
||||
jsonRequest('POST', {
|
||||
playSetting,
|
||||
artStyle,
|
||||
...(input.referenceImageSrcs?.length
|
||||
? { referenceImageSrcs: input.referenceImageSrcs }
|
||||
: {}),
|
||||
...(input.referenceId ? { referenceId: input.referenceId } : {}),
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
...(input.generationInputs
|
||||
? { generationInputs: input.generationInputs }
|
||||
@@ -1143,7 +1135,6 @@ export async function generateEditorIconSpritesheet(
|
||||
input: EditorIconSpritesheetGenerationInput,
|
||||
) {
|
||||
const model = input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2;
|
||||
assertStableEditorMediaReference(input.referenceImageSrc, '图标素材规范');
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, '图标素材参考图');
|
||||
assertEditorReferenceLimit(
|
||||
input.referenceImageSrcs,
|
||||
@@ -1156,7 +1147,7 @@ export async function generateEditorIconSpritesheet(
|
||||
return requestJson<EditorIconSpritesheetGenerationResponse>(
|
||||
EDITOR_ICON_SPRITESHEET_GENERATION_API,
|
||||
jsonRequest('POST', {
|
||||
referenceImageSrc: input.referenceImageSrc,
|
||||
referenceId: input.referenceId,
|
||||
...(input.referenceImageSrcs?.length
|
||||
? { referenceImageSrcs: input.referenceImageSrcs }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user