This commit is contained in:
2026-05-05 14:40:41 +08:00
parent e847fcea6f
commit 07e777fef8
76 changed files with 4246 additions and 444 deletions

View File

@@ -7,6 +7,46 @@ import {
} from './miniGameDraftGenerationProgress';
describe('miniGameDraftGenerationProgress', () => {
test('puzzle draft generation follows picture-only creation steps', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
const progress = buildMiniGameDraftGenerationProgress(state, 1500);
expect(progress?.steps.map((step) => step.label)).toEqual([
'编译首关草稿',
'生成首关画面',
'写入正式草稿',
]);
expect(progress?.phaseLabel).toBe('编译首关草稿');
expect(progress?.steps[0]?.detail).toBe(
'根据画面描述生成首关名称和结果页草稿。',
);
});
test('puzzle ready copy points to result page work info completion', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'ready',
startedAtMs: 1000,
completedAssetCount: 1,
totalAssetCount: 1,
error: null,
};
const progress = buildMiniGameDraftGenerationProgress(state, 2000);
expect(progress?.phaseDetail).toBe(
'首关草稿与正式图已准备完成,可进入结果页补作品信息。',
);
});
test('big fish draft generation exposes multiple draft steps', () => {
const state: MiniGameDraftGenerationState = {
kind: 'big-fish',
@@ -111,24 +151,12 @@ describe('miniGameDraftGenerationProgress', () => {
resultPreview: null,
updatedAt: '2026-04-29T00:00:00.000Z',
}, {
seedText: '表单作品名',
workTitle: '暖灯猫街',
workDescription: '一套雨夜猫街主题拼图。',
seedText: '一只猫在雨夜灯牌下回头。',
pictureDescription: '一只猫在雨夜灯牌下回头。',
referenceImageSrc: null,
});
expect(entries).toEqual([
{
id: 'puzzle-title',
label: '作品名称',
value: '暖灯猫街',
},
{
id: 'work-description',
label: '作品描述',
value: '一套雨夜猫街主题拼图。',
},
{
id: 'picture-description',
label: '画面描述',

View File

@@ -47,20 +47,20 @@ type MiniGameAnchorSource = {
const PUZZLE_STEPS = [
{
id: 'compile',
label: '编译拼图草稿',
detail: '整理主题、主体、构图与标签。',
label: '编译首关草稿',
detail: '根据画面描述生成首关名称和结果页草稿。',
weight: 34,
},
{
id: 'puzzle-images',
label: '生成拼图图片',
detail: '根据草稿生成候选图。',
label: '生成首关画面',
detail: '按画面描述和参考图生成第一张拼图图。',
weight: 33,
},
{
id: 'puzzle-select-image',
label: '确认正式图片',
detail: '选择候选图写入结果页。',
label: '写入正式草稿',
detail: '把首图设为正式图并同步到结果页。',
weight: 33,
},
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
@@ -211,7 +211,7 @@ export function buildMiniGameDraftGenerationProgress(
(normalizedState.phase === 'ready'
? normalizedState.kind === 'big-fish'
? '玩法草稿已准备完成,可进入结果页继续生成主图、动作和背景。'
: '完整草稿与资产已准备完成。'
: '首关草稿与正式图已准备完成,可进入结果页补作品信息。'
: activeStep.detail),
batchLabel: activeStep.label,
overallProgress: clampProgress(overallProgress),
@@ -238,28 +238,12 @@ export function buildPuzzleGenerationAnchorEntries(
}
const entries: Array<MiniGameAnchorSource | null> = [
{
key: 'puzzle-title',
label: '作品名称',
value:
formPayload?.workTitle?.trim() ||
formPayload?.seedText?.trim() ||
session.draft?.workTitle ||
session.anchorPack.themePromise.value,
},
{
key: 'work-description',
label: '作品描述',
value:
formPayload?.workDescription?.trim() ||
session.draft?.workDescription ||
'',
},
{
key: 'picture-description',
label: '画面描述',
value:
formPayload?.pictureDescription?.trim() ||
formPayload?.seedText?.trim() ||
session.draft?.levels?.[0]?.pictureDescription ||
session.anchorPack.visualSubject.value,
},

View File

@@ -7,6 +7,7 @@ const { requestJsonMock } = vi.hoisted(() => ({
}));
import {
generateRpgWorldOpeningCg,
generateRpgWorldLandmark,
generateRpgWorldSceneImage,
generateRpgWorldSceneNpc,
@@ -23,6 +24,11 @@ describe('rpgCreationAssetClient', () => {
entity: { id: 'landmark-1', name: '雾港' },
imageSrc: '/generated-custom-world-scenes/profile/scene/image.webp',
npc: { id: 'npc-1', name: '守灯人' },
openingCg: {
id: 'opening-cg-1',
status: 'ready',
videoSrc: '/generated-custom-world-scenes/profile/opening.mp4',
},
});
});
@@ -89,4 +95,24 @@ describe('rpgCreationAssetClient', () => {
'生成场景 NPC 失败',
);
});
it('posts opening cg generation to the runtime custom world asset route', async () => {
const openingCg = await generateRpgWorldOpeningCg({
profile: {
id: 'profile-1',
name: '雾海群岛',
} as never,
});
expect(openingCg.videoSrc).toBe(
'/generated-custom-world-scenes/profile/opening.mp4',
);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world/opening-cg',
expect.objectContaining({
method: 'POST',
}),
'生成开局 CG 失败',
);
});
});

View File

@@ -2,6 +2,7 @@ import { ASSET_API_PATHS } from '../../editor/shared/editorApiClient';
import type {
CustomWorldLandmark,
CustomWorldNpc,
CustomWorldOpeningCgProfile,
CustomWorldPlayableNpc,
CustomWorldProfile,
} from '../../types';
@@ -132,6 +133,20 @@ export async function generateRpgWorldLandmark(payload: {
return response.entity;
}
export async function generateRpgWorldOpeningCg(payload: {
profile: CustomWorldProfile;
}) {
const response = await requestRpgCreationPostJson<{
openingCg: CustomWorldOpeningCgProfile;
}>(
`${RPG_CREATION_ASSET_API_BASE}/opening-cg`,
payload,
'生成开局 CG 失败',
);
return response.openingCg;
}
/**
* 工作包 D 把结果页与编辑器依赖的资产请求迁入 RPG 创作域 client
* 保留封面资产服务的既有边界,不把逻辑重新塞回 `aiService.ts`。
@@ -143,6 +158,7 @@ export const rpgCreationAssetClient = {
generatePlayableNpc: generateRpgWorldPlayableNpc,
generateStoryNpc: generateRpgWorldStoryNpc,
generateLandmark: generateRpgWorldLandmark,
generateOpeningCg: generateRpgWorldOpeningCg,
generateCoverImage: generateCustomWorldCoverImage,
uploadCoverImage: uploadCustomWorldCoverImage,
};

View File

@@ -65,7 +65,8 @@ function createRuntimeProjection(
overrides: RuntimeProjectionOverrides = {},
): StoryRuntimeProjectionResponse {
const storySession = createStorySession(overrides.storySession);
const serverVersion = overrides.serverVersion ?? storySession.version;
const serverVersion =
overrides.serverVersion ?? storySession.version ?? 1;
return {
storySession,