Merge branch 'master' into refactor/agc-resource-menu
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m43s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 5m58s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m29s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 4m40s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m19s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m43s
Project CI / Native shell tests (pull_request) Failing after 2m15s
Project CI / Repository checks (pull_request) Failing after 1m55s
Project CI / Frontend tests (pull_request) Failing after 3m30s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m34s
Project CI / Backend tests (pull_request) Successful in 8m43s

This commit is contained in:
2026-09-17 19:30:00 +08:00
176 changed files with 11430 additions and 882 deletions
@@ -2749,6 +2749,7 @@ describe('ImageCanvasEditorView generation integration', () => {
expect.objectContaining({
referenceId: 'resource-icon-spec',
iconDescriptions: ['返回按钮\n设置按钮'],
sliceMode: 'connected-components',
model: 'gemini-3.1-flash-image-preview',
aspectRatio: '1:1',
imageSize: '1K',
@@ -1080,6 +1080,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
referenceId: 'resource-icon-spec',
referenceImageSrcs: ['data:image/png;base64,ref'],
iconDescriptions: ['返回按钮\n\n设置按钮'],
sliceMode: 'connected-components',
model: 'gpt-image-2',
screenColor: 'auto',
segModel: 'birefnet',
@@ -1136,6 +1136,9 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
}
: {}),
iconDescriptions,
// 切分模式没有默认值:画板链路按自由排布生成图标表,因此显式声明连通域切分;
// 等分网格或固定槽位需求必须由调用方显式传 grid + gridX/gridY。
sliceMode: 'connected-components',
model: rememberImageModel,
screenColor,
segModel,
@@ -4249,6 +4249,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
'generated-character-drafts/editor/refs/icon-style.png',
],
iconDescriptions: ['返回按钮'],
sliceMode: 'connected-components',
}),
);
});
@@ -1112,6 +1112,7 @@ describe('editorProjectClient', () => {
referenceId: 'editor-resource-spec',
referenceImageSrcs: references.slice(0, 5),
iconDescriptions: ['返回按钮'],
sliceMode: 'connected-components',
model: 'gpt-image-2',
}),
).rejects.toThrow('图标素材参考图最多允许 4 张');
@@ -1162,6 +1163,29 @@ describe('editorProjectClient', () => {
);
});
it('requires an explicit slice declaration and rejects contradictory grid dimensions', async () => {
requestJsonMock.mockResolvedValue({ queueState: { status: 'queued' } });
await expect(
generateEditorIconSpritesheet({
referenceId: 'editor-resource-icon-spec',
iconDescriptions: ['返回按钮'],
sliceMode: 'grid',
}),
).rejects.toThrow('sliceMode=grid 必须同时提供 gridX 与 gridY');
await expect(
generateEditorIconSpritesheet({
referenceId: 'editor-resource-icon-spec',
iconDescriptions: ['返回按钮'],
sliceMode: 'connected-components',
gridX: 2,
gridY: 2,
}),
).rejects.toThrow('sliceMode=connected-components 不接受 gridX/gridY');
expect(requestJsonMock).not.toHaveBeenCalled();
});
it('rejects oversized stable video reference fields before submission', async () => {
await expect(
generateEditorVideo({
@@ -1331,6 +1355,9 @@ describe('editorProjectClient', () => {
referenceId: 'editor-resource-spec',
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
iconDescriptions: ['返回按钮', '设置按钮'],
sliceMode: 'grid',
gridX: 3,
gridY: 2,
assetLabel: '冒险游戏图标',
});
@@ -1348,6 +1375,9 @@ describe('editorProjectClient', () => {
referenceId: 'editor-resource-spec',
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
iconDescriptions: ['返回按钮', '设置按钮'],
sliceMode: 'grid',
gridX: 3,
gridY: 2,
model: 'gemini-3.1-flash-image-preview',
assetLabel: '冒险游戏图标',
}),
@@ -1365,6 +1395,7 @@ describe('editorProjectClient', () => {
generateEditorIconSpritesheet({
referenceId: 'editor-resource-spec',
iconDescriptions: ['图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS + 1)],
sliceMode: 'connected-components',
}),
).rejects.toThrow(`不能超过 ${EDITOR_ICON_DESCRIPTION_MAX_CHARS} 个字符`);
@@ -1377,6 +1408,7 @@ describe('editorProjectClient', () => {
'图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
),
],
sliceMode: 'connected-components',
}),
).rejects.toThrow(
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS} 个字符`,
@@ -1388,6 +1420,7 @@ describe('editorProjectClient', () => {
iconDescriptions: Array.from({ length: 8 }, () =>
'😀'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
),
sliceMode: 'connected-components',
}),
).rejects.toThrow(
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES} 个 UTF-8 字节`,
@@ -1412,6 +1445,7 @@ describe('editorProjectClient', () => {
await generateEditorIconSpritesheet({
referenceId: 'editor-resource-spec',
iconDescriptions: ['返回按钮'],
sliceMode: 'connected-components',
model: 'gpt-image-2',
screenColor: '#E6D8FF',
segModel: 'anime-seg',
@@ -1434,6 +1468,7 @@ describe('editorProjectClient', () => {
body: JSON.stringify({
referenceId: 'editor-resource-spec',
iconDescriptions: ['返回按钮'],
sliceMode: 'connected-components',
model: 'gpt-image-2',
screenColor: '#E6D8FF',
segModel: 'anime-seg',
@@ -380,7 +380,8 @@ export type EditorIconSpritesheetGenerationInput = {
referenceId: string;
referenceImageSrcs?: string[];
iconDescriptions: string[];
sliceMode?: 'connected-components' | 'grid';
/** 必填且没有默认值:必须显式声明连通域或网格切分。 */
sliceMode: 'connected-components' | 'grid';
gridX?: number;
gridY?: number;
model?: string;
@@ -1300,6 +1301,16 @@ export async function generateEditorIconSpritesheet(
input.iconDescriptions,
);
const model = input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2;
// 切分模式没有默认值:声明必须自洽,网格尺寸只能与 grid 同时提交。
if (input.sliceMode === 'grid') {
if (input.gridX === undefined || input.gridY === undefined) {
throw new Error('sliceMode=grid 必须同时提供 gridX 与 gridY');
}
} else if (input.gridX !== undefined || input.gridY !== undefined) {
throw new Error(
'sliceMode=connected-components 不接受 gridX/gridY:网格尺寸只能与 grid 同时提交',
);
}
assertStableEditorMediaReferences(input.referenceImageSrcs, '图标素材参考图');
assertEditorReferenceLimit(
input.referenceImageSrcs,
@@ -1317,9 +1328,13 @@ export async function generateEditorIconSpritesheet(
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
iconDescriptions,
...(input.sliceMode ? { sliceMode: input.sliceMode } : {}),
...(input.gridX !== undefined ? { gridX: input.gridX } : {}),
...(input.gridY !== undefined ? { gridY: input.gridY } : {}),
sliceMode: input.sliceMode,
...(input.sliceMode === 'grid' && input.gridX !== undefined
? { gridX: input.gridX }
: {}),
...(input.sliceMode === 'grid' && input.gridY !== undefined
? { gridY: input.gridY }
: {}),
model,
...(input.screenColor ? { screenColor: input.screenColor } : {}),
...(input.segModel ? { segModel: input.segModel } : {}),