Switch to VectorEngine gpt-image-2 and edits

Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
This commit is contained in:
2026-05-22 03:06:41 +08:00
parent 321e1ea33a
commit ae014ac881
90 changed files with 7078 additions and 3389 deletions

View File

@@ -14,6 +14,10 @@ import * as match3dWorksService from '../../services/match3d-works';
import { clearMatch3DGeneratedModelBytesCache } from '../../services/match3dGeneratedModelCache';
import { Match3DResultView } from './Match3DResultView';
const match3dSpritesheetParser = vi.hoisted(() => ({
loadMatch3DSpritesheetAssetRegions: vi.fn(),
}));
vi.mock('../ResolvedAssetImage', () => ({
ResolvedAssetImage: ({
src,
@@ -45,9 +49,20 @@ vi.mock('../../services/match3d-works', () => ({
updateMatch3DWork: vi.fn(),
}));
vi.mock('../../services/match3dSpritesheetParser', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../services/match3dSpritesheetParser')>();
return {
...actual,
loadMatch3DSpritesheetAssetRegions:
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
};
});
afterEach(() => {
clearMatch3DGeneratedModelBytesCache();
vi.clearAllMocks();
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockReset();
vi.unstubAllGlobals();
});
@@ -687,8 +702,10 @@ describe('Match3DResultView', () => {
expect(screen.queryByRole('button', { name: '音乐' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
expect(screen.getByRole('button', { name: '物品' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'UI' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'UI素材' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
expect(screen.getAllByRole('button', { name: /打开.+物品素材/u }))
.toHaveLength(20);
fireEvent.click(
screen.getByRole('button', { name: '打开水果核心物件物品素材' }),
@@ -1075,7 +1092,7 @@ describe('Match3DResultView', () => {
);
});
expect(screen.getByText('63 件')).toBeTruthy();
expect(screen.getAllByText('21 种').length).toBeGreaterThan(0);
expect(screen.getAllByText('20 种').length).toBeGreaterThan(0);
expect(onSaved).toHaveBeenCalledWith(
expect.objectContaining({
clearCount: 21,
@@ -1323,13 +1340,32 @@ describe('Match3DResultView', () => {
).toBeNull();
});
test('素材配置 UI 子 Tab 展示默认提示词并支持预览UI页面', () => {
test('素材配置 UI素材子 Tab 仅预览背景图和UI spritesheet', () => {
render(
<Match3DResultView
profile={createProfile({
backgroundPrompt: '果园主题抓大鹅竖屏背景',
backgroundImageSrc:
'/generated-match3d-assets/session/profile/background/background.png',
generatedBackgroundAsset: {
prompt: '果园主题抓大鹅竖屏背景',
levelScenePrompt: '果园完整关卡画面',
levelSceneImageSrc:
'/generated-match3d-assets/session/profile/level-scene/scene.png',
levelSceneImageObjectKey: null,
imageSrc:
'/generated-match3d-assets/session/profile/background/background.png',
imageObjectKey: null,
uiSpritesheetPrompt: 'UI spritesheet',
uiSpritesheetImageSrc:
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
uiSpritesheetImageObjectKey: null,
containerPrompt: null,
containerImageSrc: null,
containerImageObjectKey: null,
status: 'image_ready',
error: null,
},
})}
onBack={() => {}}
onStartTestRun={() => {}}
@@ -1337,15 +1373,18 @@ describe('Match3DResultView', () => {
);
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
'/generated-match3d-assets/session/profile/background/background.png',
);
expect(screen.getByLabelText('UI背景图画面描述提示词')).toHaveProperty(
'value',
'果园主题抓大鹅竖屏背景',
expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
);
expect(
screen.queryByLabelText('UI背景图画面描述提示词'),
).toBeNull();
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
expect(screen.getByRole('dialog', { name: 'UI预览' })).toBeTruthy();
@@ -1359,7 +1398,7 @@ describe('Match3DResultView', () => {
'img[src="/match3d-background-references/pot-fused-reference.png"]',
);
expect(containerImage).toBeTruthy();
expect(containerImage?.className).toContain('w-[min(108vw,38rem)]');
expect(containerImage?.className).toContain('w-[min(116vw,42rem)]');
expect(containerImage?.className).toContain('-translate-x-1/2');
expect(
document.querySelector('.animate-spin, [class*="border-l-transparent"]'),
@@ -1371,7 +1410,7 @@ describe('Match3DResultView', () => {
).toBeTruthy();
});
test('素材配置 UI 子 Tab 从物品挂载资产展示生成背景和容器', async () => {
test('素材配置 UI素材子 Tab 从物品挂载资产展示生成背景和UI spritesheet', async () => {
const onStartTestRun = vi.fn();
const profile = createProfile({
backgroundPrompt: null,
@@ -1388,11 +1427,14 @@ describe('Match3DResultView', () => {
'/generated-match3d-assets/session/profile/background/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/background.png',
containerPrompt: '果园容器',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/container.png',
uiSpritesheetPrompt: '果园UI素材',
uiSpritesheetImageSrc:
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
uiSpritesheetImageObjectKey:
'generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
containerPrompt: null,
containerImageSrc: null,
containerImageObjectKey: null,
status: 'image_ready',
error: null,
},
@@ -1417,15 +1459,16 @@ describe('Match3DResultView', () => {
);
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
'/generated-match3d-assets/session/profile/background/background.png',
);
expect(screen.getByLabelText('UI背景图画面描述提示词')).toHaveProperty(
'value',
'果园背景',
expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
);
expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
expect(
@@ -1435,14 +1478,14 @@ describe('Match3DResultView', () => {
).toBeTruthy();
expect(
document.querySelector(
'img[src="/generated-match3d-assets/session/profile/ui-container/container.png"]',
'img[src="/generated-match3d-assets/session/profile/ui-spritesheet/ui.png"]',
),
).toBeTruthy();
expect(
document.querySelector(
'img[src="/match3d-background-references/pot-fused-reference.png"]',
),
).toBeNull();
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
@@ -1455,8 +1498,8 @@ describe('Match3DResultView', () => {
generatedBackgroundAsset: expect.objectContaining({
imageSrc:
'/generated-match3d-assets/session/profile/background/background.png',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/container.png',
uiSpritesheetImageSrc:
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
}),
}),
{
@@ -1466,7 +1509,81 @@ describe('Match3DResultView', () => {
});
});
test('素材配置 UI 子 Tab 修改提示词后调用背景图生成接口并刷新素材', async () => {
test('素材配置 UI素材子 Tab 预览物品spritesheet解析结果', async () => {
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockResolvedValue(
Array.from({ length: 100 }, (_, index) => ({
label: `${index + 1}`,
x: index * 2,
y: 0,
width: 2,
height: 2,
sheetWidth: 200,
sheetHeight: 2,
imageSrc: `data:image/png;base64,item-${index + 1}`,
})),
);
render(
<Match3DResultView
profile={createProfile({
generatedItemAssets: [
{ ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
{ ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
],
generatedBackgroundAsset: {
prompt: '果园主题抓大鹅竖屏背景',
imageSrc:
'/generated-match3d-assets/session/profile/background/background.png',
imageObjectKey: null,
uiSpritesheetPrompt: 'UI spritesheet',
uiSpritesheetImageSrc:
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
uiSpritesheetImageObjectKey: null,
itemSpritesheetPrompt: '物品 spritesheet',
itemSpritesheetImageSrc:
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
itemSpritesheetImageObjectKey: null,
status: 'image_ready',
error: null,
},
})}
onBack={() => {}}
onStartTestRun={() => {}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
expect(screen.getByAltText('物品素材图').getAttribute('src')).toBe(
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
);
await waitFor(() => {
expect(
screen
.getByTestId('match3d-item-spritesheet-preview-0-0')
.getAttribute('src'),
).toBe('data:image/png;base64,item-1');
expect(
screen
.getByTestId('match3d-item-spritesheet-preview-1-4')
.getAttribute('src'),
).toBe('data:image/png;base64,item-10');
});
expect(screen.getByText('草莓')).toBeTruthy();
expect(screen.getByText('苹果')).toBeTruthy();
expect(
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
).toHaveBeenCalledWith(
expect.objectContaining({
maxRegions: 100,
source:
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
}),
);
});
test('素材配置 UI素材子 Tab 不提供背景或容器重新生成入口', () => {
const profile = createProfile({
generatedItemAssets: [
{
@@ -1500,267 +1617,24 @@ describe('Match3DResultView', () => {
error: null,
},
});
const nextProfile = createProfile({
...profile,
backgroundPrompt: '新背景提示词',
backgroundImageSrc:
'/generated-match3d-assets/session/profile/background/new/background.png',
generatedItemAssets: [
{
...profile.generatedItemAssets![0]!,
backgroundAsset: {
prompt: '新背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/new/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/new/background.png',
containerPrompt: '新容器提示词',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/new/container.png',
status: 'image_ready',
error: null,
},
},
],
generatedBackgroundAsset: {
prompt: '新背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/new/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/new/background.png',
containerPrompt: '新容器提示词',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/new/container.png',
status: 'image_ready',
error: null,
},
});
const onSaved = vi.fn();
vi.mocked(
match3dWorksService.generateMatch3DBackgroundImage,
).mockResolvedValue({
item: nextProfile,
backgroundImageSrc:
'/generated-match3d-assets/session/profile/background/new/background.png',
backgroundImageObjectKey:
'generated-match3d-assets/session/profile/background/new/background.png',
generatedBackgroundAsset: nextProfile.generatedBackgroundAsset!,
prompt: '新背景提示词',
});
render(
<Match3DResultView
profile={profile}
onBack={() => {}}
onSaved={onSaved}
onStartTestRun={() => {}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
fireEvent.change(screen.getByLabelText('UI背景图画面描述提示词'), {
target: { value: '新背景提示词' },
});
expect(
screen.getByRole('button', { name: /重新生成 · 2泥点/u }),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /重新生成/u }));
expect(screen.getByRole('dialog', { name: '确认消耗泥点' })).toBeTruthy();
confirmPointCost();
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
await waitFor(() => {
expect(
match3dWorksService.generateMatch3DBackgroundImage,
).toHaveBeenCalledWith(profile.profileId, {
prompt: '新背景提示词',
});
expect(onSaved).toHaveBeenCalledWith(
expect.objectContaining({
generatedItemAssets: [
expect.objectContaining({
itemId: 'match3d-item-1',
backgroundAsset: expect.objectContaining({
prompt: '新背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/new/background.png',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
}),
}),
],
}),
);
});
});
test('素材配置 UI 子 Tab 重新生成后显示90秒倒计时进度', async () => {
const deferred =
createDeferred<
Awaited<
ReturnType<typeof match3dWorksService.generateMatch3DBackgroundImage>
>
>();
vi.mocked(
match3dWorksService.generateMatch3DBackgroundImage,
).mockReturnValue(deferred.promise);
render(
<Match3DResultView
profile={createProfile({
generatedBackgroundAsset: {
prompt: '旧背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/old/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/old/background.png',
containerPrompt: '旧容器提示词',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/old/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/old/container.png',
status: 'image_ready',
error: null,
},
})}
onBack={() => {}}
onStartTestRun={() => {}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: 'UI' }));
fireEvent.click(screen.getByRole('button', { name: /重新生成/u }));
confirmPointCost();
await waitFor(() => {
expect(
screen.getByRole('progressbar', { name: 'UI背景图生成进度' }),
).toBeTruthy();
expect(screen.getByText('预计剩余 90 秒')).toBeTruthy();
});
});
test('素材配置容器形象子 Tab 单独调用容器图生成接口并刷新素材', async () => {
const profile = createProfile({
generatedBackgroundAsset: {
prompt: '旧背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/old/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/old/background.png',
containerPrompt: '旧容器提示词',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/old/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/old/container.png',
status: 'image_ready',
error: null,
},
generatedItemAssets: [
{
...createReadyGeneratedItemAsset(1),
backgroundAsset: {
prompt: '旧背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/old/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/old/background.png',
containerPrompt: '旧容器提示词',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/old/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/old/container.png',
status: 'image_ready',
error: null,
},
},
],
});
const nextBackgroundAsset = {
prompt: '旧背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/old/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/old/background.png',
containerPrompt: '新容器提示词',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/new/container.png',
status: 'image_ready',
error: null,
};
const nextProfile = createProfile({
...profile,
generatedBackgroundAsset: nextBackgroundAsset,
generatedItemAssets: [
{
...profile.generatedItemAssets![0]!,
backgroundAsset: nextBackgroundAsset,
},
],
});
const onSaved = vi.fn();
vi.mocked(
match3dWorksService.generateMatch3DContainerImage,
).mockResolvedValue({
item: nextProfile,
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/new/container.png',
generatedBackgroundAsset: nextBackgroundAsset,
prompt: '新容器提示词',
});
render(
<Match3DResultView
profile={profile}
onBack={() => {}}
onSaved={onSaved}
onStartTestRun={() => {}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: '容器形象' }));
fireEvent.change(screen.getByLabelText('容器形象画面描述提示词'), {
target: { value: '新容器提示词' },
});
fireEvent.click(screen.getByRole('button', { name: /重新生成/u }));
confirmPointCost();
await waitFor(() => {
expect(
match3dWorksService.generateMatch3DContainerImage,
).toHaveBeenCalledWith(profile.profileId, {
prompt: '新容器提示词',
});
expect(
match3dWorksService.generateMatch3DBackgroundImage,
).not.toHaveBeenCalled();
expect(onSaved).toHaveBeenCalledWith(
expect.objectContaining({
generatedItemAssets: [
expect.objectContaining({
itemId: 'match3d-item-1',
backgroundAsset: expect.objectContaining({
prompt: '旧背景提示词',
imageSrc:
'/generated-match3d-assets/session/profile/background/old/background.png',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/new/container.png',
}),
}),
],
}),
);
});
expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
expect(screen.queryByLabelText('容器形象画面描述提示词')).toBeNull();
expect(screen.queryByRole('button', { name: '容器形象' })).toBeNull();
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
expect(match3dWorksService.generateMatch3DBackgroundImage).not.toHaveBeenCalled();
expect(match3dWorksService.generateMatch3DContainerImage).not.toHaveBeenCalled();
});
test('历史草稿同时带旧 draft 和 profile 素材时以 profile 多视角素材补齐试玩资产', async () => {