重构图标规范与图集生成链路

新增图标规范独立队列任务并将参数补全纳入统一计费操作
将图标图集生成和手工拆分迁入独立图标模块并复用公共生图能力
将图标规范引用收紧为项目资源或素材 ID
同步 SpacetimeDB 引用解析契约、External OpenAPI、前端与 Agent 提交链路
补充相关回归测试和长期架构约定
This commit is contained in:
2026-08-06 15:43:10 +08:00
parent e6f0e6248e
commit 3fdbe81bfe
30 changed files with 3042 additions and 2691 deletions
@@ -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(