Merge remote-tracking branch 'origin/master' into feat/recommend-runtime-guest
# Conflicts: # docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
This commit is contained in:
@@ -9,6 +9,9 @@ export type CreationEntryTypeConfig = {
|
||||
visible: boolean;
|
||||
open: boolean;
|
||||
sortOrder: number;
|
||||
categoryId: string;
|
||||
categoryLabel: string;
|
||||
categorySortOrder: number;
|
||||
updatedAtMicros: number;
|
||||
};
|
||||
|
||||
@@ -23,6 +26,14 @@ export type CreationEntryConfig = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
eventBanner: {
|
||||
title: string;
|
||||
description: string;
|
||||
coverImageSrc: string;
|
||||
prizePoolMudPoints: number;
|
||||
startsAtText: string;
|
||||
endsAtText: string;
|
||||
};
|
||||
creationTypes: CreationEntryTypeConfig[];
|
||||
};
|
||||
|
||||
|
||||
@@ -36,12 +36,15 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
expect(progress?.steps[0]?.detail).toBe(
|
||||
'建立可恢复草稿,整理首关描述与关卡结构,约 8 秒。',
|
||||
);
|
||||
expect(progress?.estimatedRemainingMs).toBe(296_500);
|
||||
expect(progress?.overallProgress).toBeGreaterThan(0);
|
||||
expect(progress?.steps[2]?.detail).toBe(
|
||||
'调用 gpt-image-2 生成 1:1 拼图首图,预计 4 分钟。',
|
||||
);
|
||||
expect(progress?.estimatedRemainingMs).toBe(446_500);
|
||||
expect(progress?.overallProgress).toBe(0);
|
||||
expect(progress?.steps[0]?.completed).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('puzzle draft generation advances steps across the current asset pipeline', () => {
|
||||
test('puzzle draft generation starts total progress from zero', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
phase: 'compile',
|
||||
@@ -51,42 +54,81 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
error: null,
|
||||
};
|
||||
|
||||
const imageProgress = buildMiniGameDraftGenerationProgress(state, 26_000);
|
||||
const uiProgress = buildMiniGameDraftGenerationProgress(state, 206_000);
|
||||
const writeBackProgress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
296_000,
|
||||
);
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 1000);
|
||||
|
||||
expect(imageProgress?.phaseId).toBe('puzzle-cover-image');
|
||||
expect(imageProgress?.estimatedRemainingMs).toBe(273_000);
|
||||
expect(imageProgress?.steps[1]?.status).toBe('completed');
|
||||
expect(imageProgress?.steps[2]?.status).toBe('active');
|
||||
expect(imageProgress?.steps[2]?.completed).toBeGreaterThan(0);
|
||||
expect(uiProgress?.phaseId).toBe('puzzle-ui-assets');
|
||||
expect(writeBackProgress?.phaseId).toBe('puzzle-select-image');
|
||||
expect(writeBackProgress?.estimatedRemainingMs).toBe(3_000);
|
||||
expect(writeBackProgress?.steps[4]?.status).toBe('completed');
|
||||
expect(writeBackProgress?.steps[5]?.status).toBe('active');
|
||||
expect(progress?.overallProgress).toBe(0);
|
||||
expect(progress?.completedWeight).toBe(0);
|
||||
expect(progress?.estimatedRemainingMs).toBe(448_000);
|
||||
expect(progress?.steps[0]?.completed).toBe(0);
|
||||
});
|
||||
|
||||
test('puzzle write-back step turns completed once rounded progress reaches 100%', () => {
|
||||
test('puzzle draft generation total progress advances after startup', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
phase: 'compile',
|
||||
startedAtMs: 1000,
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 7000);
|
||||
|
||||
expect(progress?.overallProgress).toBeGreaterThan(0);
|
||||
expect(progress?.overallProgress).toBeLessThan(88);
|
||||
expect(progress?.phaseId).toBe('compile');
|
||||
});
|
||||
|
||||
test('puzzle draft generation keeps current step until real progress advances it', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
phase: 'compile',
|
||||
startedAtMs: 1000,
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const longRunningProgress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
296_000,
|
||||
);
|
||||
const progressedState: MiniGameDraftGenerationState = {
|
||||
...state,
|
||||
phase: 'puzzle-cover-image',
|
||||
};
|
||||
const realProgress = buildMiniGameDraftGenerationProgress(
|
||||
progressedState,
|
||||
296_000,
|
||||
);
|
||||
|
||||
expect(longRunningProgress?.phaseId).toBe('compile');
|
||||
expect(longRunningProgress?.steps[0]?.status).toBe('active');
|
||||
expect(longRunningProgress?.steps[1]?.status).toBe('pending');
|
||||
expect(longRunningProgress?.overallProgress).toBeLessThanOrEqual(98);
|
||||
expect(longRunningProgress?.overallProgress).toBeGreaterThan(40);
|
||||
expect(realProgress?.phaseId).toBe('puzzle-cover-image');
|
||||
expect(realProgress?.steps[1]?.status).toBe('completed');
|
||||
expect(realProgress?.steps[2]?.status).toBe('active');
|
||||
});
|
||||
|
||||
test('puzzle write-back step stays active until the generation action finishes', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
phase: 'puzzle-select-image',
|
||||
startedAtMs: 1_000,
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 298_950);
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 448_950);
|
||||
|
||||
expect(progress?.phaseId).toBe('puzzle-select-image');
|
||||
expect(progress?.overallProgress).toBe(98);
|
||||
expect(progress?.estimatedRemainingMs).toBe(50);
|
||||
expect(progress?.steps[5]?.completed).toBe(1);
|
||||
expect(progress?.steps[5]?.status).toBe('completed');
|
||||
expect(progress?.steps[5]?.completed).toBe(0.98);
|
||||
expect(progress?.steps[5]?.status).toBe('active');
|
||||
});
|
||||
|
||||
test('puzzle direct upload generation skips the first image generation step', () => {
|
||||
@@ -112,14 +154,14 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
'生成UI与背景',
|
||||
'写入正式草稿',
|
||||
]);
|
||||
expect(progress?.phaseId).toBe('puzzle-level-scene');
|
||||
expect(progress?.phaseId).toBe('compile');
|
||||
expect(progress?.steps[2]?.detail).toContain('直接使用上传图作为参考');
|
||||
expect(progress?.estimatedRemainingMs).toBe(189_000);
|
||||
expect(writeBackProgress?.phaseId).toBe('puzzle-select-image');
|
||||
expect(writeBackProgress?.phaseId).toBe('compile');
|
||||
expect(writeBackProgress?.estimatedRemainingMs).toBe(3_000);
|
||||
});
|
||||
|
||||
test('puzzle draft generation keeps moving without claiming completion before response', () => {
|
||||
test('puzzle draft generation does not advance or claim completion before response', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
phase: 'compile',
|
||||
@@ -129,18 +171,88 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
error: null,
|
||||
};
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 480_000);
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 630_000);
|
||||
|
||||
expect(progress?.phaseId).toBe('puzzle-select-image');
|
||||
expect(progress?.overallProgress).toBe(98);
|
||||
expect(progress?.phaseId).toBe('compile');
|
||||
expect(progress?.overallProgress).toBeLessThan(88);
|
||||
expect(progress?.overallProgress).toBeGreaterThan(80);
|
||||
expect(progress?.estimatedRemainingMs).toBe(0);
|
||||
expect(progress?.steps[5]?.completed).toBe(1);
|
||||
expect(progress?.steps[5]?.status).toBe('completed');
|
||||
expect(progress?.steps.every((step) => step.status === 'completed')).toBe(
|
||||
expect(progress?.steps[0]?.status).toBe('active');
|
||||
expect(progress?.steps[0]?.completed).toBe(0.98);
|
||||
expect(progress?.steps.slice(1).every((step) => step.status === 'pending')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('puzzle draft generation advances steps from backend progress percent only', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
phase: 'compile',
|
||||
startedAtMs: 1_000,
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
error: null,
|
||||
metadata: {
|
||||
puzzleAiRedraw: true,
|
||||
puzzleProgressPercent: 88,
|
||||
} as MiniGameDraftGenerationState['metadata'],
|
||||
};
|
||||
|
||||
const imageProgress = buildMiniGameDraftGenerationProgress(state, 120_000);
|
||||
const uiProgress = buildMiniGameDraftGenerationProgress(
|
||||
{
|
||||
...state,
|
||||
metadata: {
|
||||
puzzleAiRedraw: true,
|
||||
puzzleProgressPercent: 94,
|
||||
} as MiniGameDraftGenerationState['metadata'],
|
||||
},
|
||||
230_000,
|
||||
);
|
||||
const writeProgress = buildMiniGameDraftGenerationProgress(
|
||||
{
|
||||
...state,
|
||||
metadata: {
|
||||
puzzleAiRedraw: true,
|
||||
puzzleProgressPercent: 96,
|
||||
} as MiniGameDraftGenerationState['metadata'],
|
||||
},
|
||||
260_000,
|
||||
);
|
||||
|
||||
expect(imageProgress?.phaseId).toBe('puzzle-cover-image');
|
||||
expect(imageProgress?.steps[2]?.status).toBe('active');
|
||||
expect(imageProgress?.steps[3]?.status).toBe('pending');
|
||||
expect(uiProgress?.phaseId).toBe('puzzle-ui-assets');
|
||||
expect(uiProgress?.steps[4]?.status).toBe('active');
|
||||
expect(uiProgress?.steps[5]?.status).toBe('pending');
|
||||
expect(writeProgress?.phaseId).toBe('puzzle-select-image');
|
||||
expect(writeProgress?.steps[5]?.status).toBe('active');
|
||||
});
|
||||
|
||||
test('puzzle backend milestone starts fake progress from the current step entry time', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
phase: 'compile',
|
||||
startedAtMs: 1_000,
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
error: null,
|
||||
metadata: {
|
||||
puzzleAiRedraw: true,
|
||||
puzzleProgressPercent: 88,
|
||||
puzzleActivePhaseId: 'puzzle-cover-image',
|
||||
puzzleActiveStepStartedAtMs: 120_000,
|
||||
} as MiniGameDraftGenerationState['metadata'],
|
||||
};
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 121_000);
|
||||
|
||||
expect(progress?.phaseId).toBe('puzzle-cover-image');
|
||||
expect(progress?.steps[2]?.status).toBe('active');
|
||||
expect(progress?.steps[2]?.completed).toBeLessThan(0.02);
|
||||
});
|
||||
|
||||
test('puzzle ready copy points to result page work info completion', () => {
|
||||
const state: MiniGameDraftGenerationState = {
|
||||
kind: 'puzzle',
|
||||
@@ -268,6 +380,19 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('match3d draft generation starts total progress from zero', () => {
|
||||
const state = createMiniGameDraftGenerationState('match3d');
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs,
|
||||
);
|
||||
|
||||
expect(progress?.overallProgress).toBe(0);
|
||||
expect(progress?.completedWeight).toBe(0);
|
||||
expect(progress?.steps[0]?.completed).toBe(0);
|
||||
});
|
||||
|
||||
test('match3d draft generation keeps backend observed asset phase', () => {
|
||||
const state = {
|
||||
...createMiniGameDraftGenerationState('match3d'),
|
||||
@@ -407,7 +532,7 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('wooden fish draft generation exposes hit object, background and sound pipeline', () => {
|
||||
test('wooden fish draft generation exposes hit object, background and back button pipeline', () => {
|
||||
const state = createMiniGameDraftGenerationState('wooden-fish');
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(
|
||||
@@ -419,12 +544,40 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
'wooden-fish-draft',
|
||||
'wooden-fish-hit-object',
|
||||
'wooden-fish-background',
|
||||
'wooden-fish-hit-sound',
|
||||
'wooden-fish-back-button',
|
||||
'wooden-fish-write-draft',
|
||||
]);
|
||||
expect(progress?.phaseId).toBe('wooden-fish-hit-object');
|
||||
expect(progress?.phaseLabel).toBe('生成敲击物图案');
|
||||
expect(progress?.estimatedRemainingMs).toBe(272_000);
|
||||
expect(progress?.estimatedRemainingMs).toBe(530_000);
|
||||
});
|
||||
|
||||
test('wooden fish draft generation follows hit object, background, back button and writeback', () => {
|
||||
const state = createMiniGameDraftGenerationState('wooden-fish');
|
||||
|
||||
const hitObjectProgress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 20_000,
|
||||
);
|
||||
const backgroundProgress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 200_000,
|
||||
);
|
||||
const backButtonProgress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 390_000,
|
||||
);
|
||||
const writeBackProgress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 575_000,
|
||||
);
|
||||
|
||||
expect(hitObjectProgress?.phaseId).toBe('wooden-fish-hit-object');
|
||||
expect(backgroundProgress?.phaseId).toBe('wooden-fish-background');
|
||||
expect(backButtonProgress?.phaseId).toBe('wooden-fish-back-button');
|
||||
expect(writeBackProgress?.phaseId).toBe('wooden-fish-write-draft');
|
||||
expect(writeBackProgress?.estimatedRemainingMs).toBe(0);
|
||||
expect(writeBackProgress?.steps[4]?.status).toBe('completed');
|
||||
});
|
||||
|
||||
test('wooden fish generation anchors expose hit object, sound and words', () => {
|
||||
@@ -434,7 +587,15 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
workDescription: '敲一下,好事发生。',
|
||||
themeTags: ['解压'],
|
||||
hitObjectPrompt: '金色小木鱼',
|
||||
hitSoundPrompt: '清脆木鱼声',
|
||||
hitSoundAsset: {
|
||||
assetId: 'wooden-fish-default-hit-sound',
|
||||
audioSrc: '/wooden-fish/default-hit-sound.mp3',
|
||||
audioObjectKey: 'public/wooden-fish/default-hit-sound.mp3',
|
||||
assetObjectId: 'wooden-fish-default-hit-sound',
|
||||
source: 'bundled-default',
|
||||
prompt: '默认木鱼音',
|
||||
durationMs: 3000,
|
||||
},
|
||||
floatingWords: ['幸运+1', '功德+1'],
|
||||
});
|
||||
|
||||
@@ -447,7 +608,7 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
{
|
||||
id: 'wooden-fish-hit-sound',
|
||||
label: '音效',
|
||||
value: '清脆木鱼声',
|
||||
value: '默认木鱼音',
|
||||
},
|
||||
{
|
||||
id: 'wooden-fish-words',
|
||||
|
||||
@@ -70,7 +70,7 @@ export type MiniGameDraftGenerationPhase =
|
||||
| 'wooden-fish-draft'
|
||||
| 'wooden-fish-hit-object'
|
||||
| 'wooden-fish-background'
|
||||
| 'wooden-fish-hit-sound'
|
||||
| 'wooden-fish-back-button'
|
||||
| 'wooden-fish-write-draft'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
@@ -91,6 +91,9 @@ export type MiniGameDraftGenerationState = {
|
||||
error: string | null;
|
||||
metadata?: {
|
||||
puzzleAiRedraw?: boolean;
|
||||
puzzleActivePhaseId?: MiniGameDraftGenerationPhase;
|
||||
puzzleActiveStepStartedAtMs?: number;
|
||||
puzzleProgressPercent?: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,10 +114,14 @@ type MiniGameAnchorSource = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const PUZZLE_COVER_IMAGE_GENERATION_EXPECTED_MS = 240_000;
|
||||
const PUZZLE_IMAGE_GENERATION_EXPECTED_MS = 90_000;
|
||||
const PUZZLE_COMPILE_EXPECTED_MS = 8_000;
|
||||
const PUZZLE_LEVEL_NAME_EXPECTED_MS = 10_000;
|
||||
const PUZZLE_WRITE_DRAFT_EXPECTED_MS = 10_000;
|
||||
const PUZZLE_COMPILE_MILESTONE_PROGRESS = 88;
|
||||
const PUZZLE_IMAGE_MILESTONE_PROGRESS = 94;
|
||||
const PUZZLE_UI_MILESTONE_PROGRESS = 96;
|
||||
|
||||
function shouldSkipPuzzleCoverGeneration(state: MiniGameDraftGenerationState) {
|
||||
return state.metadata?.puzzleAiRedraw === false;
|
||||
@@ -160,8 +167,8 @@ function buildPuzzleTimedSteps(state: MiniGameDraftGenerationState) {
|
||||
steps.push({
|
||||
id: 'puzzle-cover-image',
|
||||
label: '生成拼图首图',
|
||||
detail: '调用 gpt-image-2 生成 1:1 拼图首图,预计 90 秒。',
|
||||
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
|
||||
detail: '调用 gpt-image-2 生成 1:1 拼图首图,预计 4 分钟。',
|
||||
durationMs: PUZZLE_COVER_IMAGE_GENERATION_EXPECTED_MS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,30 +211,40 @@ function resolvePuzzleEstimatedWaitMs(state: MiniGameDraftGenerationState) {
|
||||
|
||||
const PUZZLE_NON_READY_MAX_PROGRESS = 98;
|
||||
const BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS = 6 * 60_000;
|
||||
function buildPuzzlePhaseTimeline(state: MiniGameDraftGenerationState): Array<{
|
||||
phase: Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
| 'compile'
|
||||
| 'puzzle-level-name'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
| 'puzzle-ui-assets'
|
||||
| 'puzzle-select-image'
|
||||
>;
|
||||
durationMs: number;
|
||||
}> {
|
||||
return buildPuzzleTimedSteps(state).map((step) => ({
|
||||
phase: step.id as Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
| 'compile'
|
||||
| 'puzzle-level-name'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
| 'puzzle-ui-assets'
|
||||
| 'puzzle-select-image'
|
||||
>,
|
||||
durationMs: step.durationMs,
|
||||
}));
|
||||
|
||||
function resolvePuzzleBackendProgressPercent(
|
||||
state: MiniGameDraftGenerationState,
|
||||
) {
|
||||
const progressPercent = state.metadata?.puzzleProgressPercent;
|
||||
if (typeof progressPercent !== 'number' || !Number.isFinite(progressPercent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, Math.round(progressPercent)));
|
||||
}
|
||||
|
||||
function resolvePuzzlePhaseByBackendProgress(
|
||||
state: MiniGameDraftGenerationState,
|
||||
): MiniGameDraftGenerationPhase | null {
|
||||
const progressPercent = resolvePuzzleBackendProgressPercent(state);
|
||||
if (progressPercent == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 中文注释:拼图生成页的跨步骤只跟随后端会话真实里程碑;
|
||||
// 每步内部的等待反馈仍由本地假进度补足。
|
||||
if (progressPercent >= 96) {
|
||||
return 'puzzle-select-image';
|
||||
}
|
||||
if (progressPercent >= 94) {
|
||||
return 'puzzle-ui-assets';
|
||||
}
|
||||
if (progressPercent >= 88) {
|
||||
return shouldSkipPuzzleCoverGeneration(state)
|
||||
? 'puzzle-level-scene'
|
||||
: 'puzzle-cover-image';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const BIG_FISH_STEPS = [
|
||||
@@ -415,30 +432,36 @@ const WOODEN_FISH_STEPS = [
|
||||
{
|
||||
id: 'wooden-fish-hit-object',
|
||||
label: '生成敲击物图案',
|
||||
detail: '使用 image2 生成最终运行态敲击物图案。',
|
||||
weight: 34,
|
||||
detail: '调用 image2 生成绿幕敲击物并去绿透明化,预计约 3 分钟。',
|
||||
weight: 32,
|
||||
},
|
||||
{
|
||||
id: 'wooden-fish-background',
|
||||
label: '生成背景环境图',
|
||||
detail: '使用 image2 生成敲击背景环境图。',
|
||||
weight: 34,
|
||||
detail: '使用透明敲击物作参考生成 9:16 背景环境图,预计约 3 分钟。',
|
||||
weight: 32,
|
||||
},
|
||||
{
|
||||
id: 'wooden-fish-hit-sound',
|
||||
label: '准备敲击音效',
|
||||
detail: '生成或写回短促敲击音效资产。',
|
||||
weight: 16,
|
||||
id: 'wooden-fish-back-button',
|
||||
label: '生成返回按钮图',
|
||||
detail: '使用敲击物和背景作参考生成主题圆形返回按钮,预计约 3 分钟。',
|
||||
weight: 20,
|
||||
},
|
||||
{
|
||||
id: 'wooden-fish-write-draft',
|
||||
label: '写入正式草稿',
|
||||
detail: '保存图案、背景、音效、飘字和封面摘要。',
|
||||
detail: '保存图案、背景、返回按钮、音效、飘字和封面摘要。',
|
||||
weight: 8,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
const WOODEN_FISH_ESTIMATED_WAIT_MS = 5 * 60_000;
|
||||
const WOODEN_FISH_COMPILE_EXPECTED_MS = 8_000;
|
||||
const WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS = 180_000;
|
||||
const WOODEN_FISH_WRITE_DRAFT_EXPECTED_MS = 10_000;
|
||||
const WOODEN_FISH_ESTIMATED_WAIT_MS =
|
||||
WOODEN_FISH_COMPILE_EXPECTED_MS +
|
||||
WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS * 3 +
|
||||
WOODEN_FISH_WRITE_DRAFT_EXPECTED_MS;
|
||||
|
||||
function clampProgress(value: number) {
|
||||
return Math.max(0, Math.min(100, Math.round(value)));
|
||||
@@ -486,15 +509,16 @@ function buildMiniGameProgressSteps(
|
||||
return steps.map((step, index) => {
|
||||
// 中文注释:拼图草稿编译的 action 回包才代表可进入结果页;
|
||||
// 但预计写入时长已耗尽时,最后一步自身应呈现已完成,避免出现“进行中 100%”。
|
||||
const isPuzzleWriteStepCompleted =
|
||||
state.kind === 'puzzle' &&
|
||||
const isTimedWriteStepCompleted =
|
||||
(state.kind === 'puzzle' || state.kind === 'wooden-fish') &&
|
||||
state.phase !== 'failed' &&
|
||||
step.id === 'puzzle-select-image' &&
|
||||
(step.id === 'puzzle-select-image' ||
|
||||
step.id === 'wooden-fish-write-draft') &&
|
||||
clampProgress(activeStepProgressRatio * 100) >= 100;
|
||||
const isCompleted =
|
||||
state.phase === 'ready' ||
|
||||
index < activeStepIndex ||
|
||||
isPuzzleWriteStepCompleted;
|
||||
isTimedWriteStepCompleted;
|
||||
const isActive =
|
||||
state.phase !== 'failed' && !isCompleted && index === activeStepIndex;
|
||||
const isAssetStep = step.id === state.phase && state.totalAssetCount > 0;
|
||||
@@ -618,31 +642,45 @@ function resolveJumpHopPhaseByElapsedMs(
|
||||
return 'jump-hop-draft';
|
||||
}
|
||||
|
||||
function resolveWoodenFishPhaseByElapsedMs(
|
||||
elapsedMs: number,
|
||||
): MiniGameDraftGenerationPhase {
|
||||
if (elapsedMs >= 270_000) {
|
||||
return 'wooden-fish-write-draft';
|
||||
}
|
||||
if (elapsedMs >= 240_000) {
|
||||
return 'wooden-fish-hit-sound';
|
||||
}
|
||||
if (elapsedMs >= 120_000) {
|
||||
return 'wooden-fish-background';
|
||||
}
|
||||
if (elapsedMs >= 12_000) {
|
||||
return 'wooden-fish-hit-object';
|
||||
}
|
||||
return 'wooden-fish-draft';
|
||||
function buildWoodenFishPhaseTimeline(): Array<{
|
||||
phase: Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
| 'wooden-fish-draft'
|
||||
| 'wooden-fish-hit-object'
|
||||
| 'wooden-fish-background'
|
||||
| 'wooden-fish-back-button'
|
||||
| 'wooden-fish-write-draft'
|
||||
>;
|
||||
durationMs: number;
|
||||
}> {
|
||||
return [
|
||||
{
|
||||
phase: 'wooden-fish-draft',
|
||||
durationMs: WOODEN_FISH_COMPILE_EXPECTED_MS,
|
||||
},
|
||||
{
|
||||
phase: 'wooden-fish-hit-object',
|
||||
durationMs: WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS,
|
||||
},
|
||||
{
|
||||
phase: 'wooden-fish-background',
|
||||
durationMs: WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS,
|
||||
},
|
||||
{
|
||||
phase: 'wooden-fish-back-button',
|
||||
durationMs: WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS,
|
||||
},
|
||||
{
|
||||
phase: 'wooden-fish-write-draft',
|
||||
durationMs: WOODEN_FISH_WRITE_DRAFT_EXPECTED_MS,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function resolvePuzzleTimelineByElapsedMs(
|
||||
elapsedMs: number,
|
||||
state: MiniGameDraftGenerationState,
|
||||
) {
|
||||
function resolveWoodenFishTimelineByElapsedMs(elapsedMs: number) {
|
||||
let elapsedBeforePhase = 0;
|
||||
|
||||
for (const item of buildPuzzlePhaseTimeline(state)) {
|
||||
for (const item of buildWoodenFishPhaseTimeline()) {
|
||||
const elapsedInPhase = elapsedMs - elapsedBeforePhase;
|
||||
|
||||
if (elapsedInPhase < item.durationMs) {
|
||||
@@ -659,11 +697,148 @@ function resolvePuzzleTimelineByElapsedMs(
|
||||
}
|
||||
|
||||
return {
|
||||
phase: 'puzzle-select-image' as const,
|
||||
phase: 'wooden-fish-write-draft' as const,
|
||||
activeStepProgressRatio: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePuzzleActiveStepProgressRatio(
|
||||
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
|
||||
activeStepIndex: number,
|
||||
elapsedMs: number,
|
||||
) {
|
||||
const activeStep = steps[activeStepIndex];
|
||||
if (!activeStep) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const elapsedBeforeActiveStep = steps
|
||||
.slice(0, activeStepIndex)
|
||||
.reduce((sum, step) => sum + step.durationMs, 0);
|
||||
const elapsedInActiveStep = Math.max(0, elapsedMs - elapsedBeforeActiveStep);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedInActiveStep / Math.max(1, activeStep.durationMs)),
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePuzzleActiveStepElapsedProgressRatio(
|
||||
state: MiniGameDraftGenerationState,
|
||||
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
|
||||
activeStepIndex: number,
|
||||
elapsedMs: number,
|
||||
effectiveNowMs: number,
|
||||
) {
|
||||
if (resolvePuzzleBackendProgressPercent(state) != null) {
|
||||
const stepStartedAtMs = state.metadata?.puzzleActiveStepStartedAtMs;
|
||||
if (
|
||||
state.metadata?.puzzleActivePhaseId === state.phase &&
|
||||
typeof stepStartedAtMs === 'number' &&
|
||||
Number.isFinite(stepStartedAtMs)
|
||||
) {
|
||||
const activeStep = steps[activeStepIndex];
|
||||
if (!activeStep) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
0.98,
|
||||
(effectiveNowMs - stepStartedAtMs) /
|
||||
Math.max(1, activeStep.durationMs),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return resolvePuzzleActiveStepProgressRatio(
|
||||
steps,
|
||||
activeStepIndex,
|
||||
elapsedMs,
|
||||
);
|
||||
}
|
||||
|
||||
const activeStep = steps[activeStepIndex];
|
||||
if (!activeStep) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 中文注释:未收到后端真实里程碑时,跨步骤必须卡住;
|
||||
// 但当前步骤内的假进度要按整段等待时间继续向前走,避免短步骤几秒后停死。
|
||||
const fallbackDurationMs = Math.max(1, resolvePuzzleEstimatedWaitMs(state));
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedMs / fallbackDurationMs),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveElapsedActiveStepProgressRatio(
|
||||
kind: MiniGameDraftGenerationKind,
|
||||
elapsedMs: number,
|
||||
) {
|
||||
const estimatedWaitMs =
|
||||
kind === 'big-fish'
|
||||
? 7_000
|
||||
: kind === 'square-hole'
|
||||
? 12_000
|
||||
: kind === 'match3d'
|
||||
? MATCH3D_ESTIMATED_WAIT_MS
|
||||
: kind === 'baby-object-match'
|
||||
? BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS
|
||||
: kind === 'jump-hop'
|
||||
? JUMP_HOP_ESTIMATED_WAIT_MS
|
||||
: kind === 'wooden-fish'
|
||||
? WOODEN_FISH_ESTIMATED_WAIT_MS
|
||||
: 1;
|
||||
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedMs / Math.max(1, estimatedWaitMs)),
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePuzzleOverallProgress(
|
||||
state: MiniGameDraftGenerationState,
|
||||
activeStepProgressRatio: number,
|
||||
) {
|
||||
const backendProgressPercent = resolvePuzzleBackendProgressPercent(state);
|
||||
// 中文注释:88 以下的后端进度只保留为会话事实,不参与首帧总进度抬升。
|
||||
// 生成页恢复时必须先从 0% 起步,再由当前步骤内的假进度平滑推进。
|
||||
const backendProgressFloor =
|
||||
backendProgressPercent != null &&
|
||||
backendProgressPercent >= PUZZLE_COMPILE_MILESTONE_PROGRESS
|
||||
? backendProgressPercent
|
||||
: 0;
|
||||
const range =
|
||||
state.phase === 'puzzle-select-image'
|
||||
? {
|
||||
start: PUZZLE_UI_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_NON_READY_MAX_PROGRESS,
|
||||
}
|
||||
: state.phase === 'puzzle-ui-assets'
|
||||
? {
|
||||
start: PUZZLE_IMAGE_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_UI_MILESTONE_PROGRESS,
|
||||
}
|
||||
: state.phase === 'puzzle-cover-image' ||
|
||||
state.phase === 'puzzle-level-scene'
|
||||
? {
|
||||
start: PUZZLE_COMPILE_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_IMAGE_MILESTONE_PROGRESS,
|
||||
}
|
||||
: {
|
||||
start: 0,
|
||||
end: PUZZLE_COMPILE_MILESTONE_PROGRESS,
|
||||
};
|
||||
const fakeProgress =
|
||||
range.start + (range.end - range.start) * activeStepProgressRatio;
|
||||
const nextProgress = Math.min(
|
||||
PUZZLE_NON_READY_MAX_PROGRESS,
|
||||
Math.max(range.start, backendProgressFloor, fakeProgress),
|
||||
);
|
||||
|
||||
return nextProgress;
|
||||
}
|
||||
|
||||
export function buildMiniGameDraftGenerationProgress(
|
||||
state: MiniGameDraftGenerationState | null,
|
||||
nowMs = Date.now(),
|
||||
@@ -677,18 +852,29 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? state.finishedAtMs
|
||||
: nowMs;
|
||||
const elapsedMs = Math.max(0, effectiveNowMs - state.startedAtMs);
|
||||
const puzzleTimeline =
|
||||
const puzzleBackendPhase =
|
||||
state.kind === 'puzzle' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? resolvePuzzleTimelineByElapsedMs(elapsedMs, state)
|
||||
? resolvePuzzlePhaseByBackendProgress(state)
|
||||
: null;
|
||||
const woodenFishTimeline =
|
||||
state.kind === 'wooden-fish' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? resolveWoodenFishTimelineByElapsedMs(elapsedMs)
|
||||
: null;
|
||||
const normalizedState =
|
||||
puzzleTimeline != null
|
||||
puzzleBackendPhase != null
|
||||
? {
|
||||
...state,
|
||||
phase: puzzleTimeline.phase,
|
||||
phase: puzzleBackendPhase,
|
||||
}
|
||||
: woodenFishTimeline != null
|
||||
? {
|
||||
...state,
|
||||
phase: woodenFishTimeline.phase,
|
||||
}
|
||||
: state.kind === 'big-fish' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
@@ -724,61 +910,92 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
...state,
|
||||
phase: resolveJumpHopPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state.kind === 'wooden-fish' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveWoodenFishPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
|
||||
const puzzleTimedSteps =
|
||||
normalizedState.kind === 'puzzle'
|
||||
? buildPuzzleTimedSteps(normalizedState)
|
||||
: null;
|
||||
const steps =
|
||||
normalizedState.kind === 'puzzle'
|
||||
? buildPuzzleSteps(normalizedState)
|
||||
? buildWeightedPuzzleSteps(
|
||||
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
|
||||
)
|
||||
: getStepDefinitions(normalizedState.kind);
|
||||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||||
const activeStep = steps[activeStepIndex] ?? steps[0];
|
||||
const activeStepProgressRatio =
|
||||
normalizedState.kind === 'puzzle'
|
||||
? normalizedState.phase === 'ready'
|
||||
? 1
|
||||
: normalizedState.phase === 'failed'
|
||||
? 0
|
||||
: resolvePuzzleActiveStepElapsedProgressRatio(
|
||||
normalizedState,
|
||||
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
|
||||
activeStepIndex,
|
||||
elapsedMs,
|
||||
effectiveNowMs,
|
||||
)
|
||||
: normalizedState.totalAssetCount > 0
|
||||
? Math.min(
|
||||
1,
|
||||
normalizedState.completedAssetCount / normalizedState.totalAssetCount,
|
||||
)
|
||||
: normalizedState.phase === 'ready'
|
||||
? 1
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'match3d'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? (woodenFishTimeline?.activeStepProgressRatio ?? 0)
|
||||
: 0;
|
||||
const completedWeight = steps
|
||||
.slice(
|
||||
0,
|
||||
normalizedState.phase === 'ready' ? steps.length : activeStepIndex,
|
||||
)
|
||||
.reduce((sum, step) => sum + step.weight, 0);
|
||||
const activeStep = steps[activeStepIndex] ?? steps[0];
|
||||
const assetRatio =
|
||||
normalizedState.totalAssetCount > 0
|
||||
? Math.min(
|
||||
1,
|
||||
normalizedState.completedAssetCount / normalizedState.totalAssetCount,
|
||||
)
|
||||
: normalizedState.phase === 'ready'
|
||||
? 1
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? (puzzleTimeline?.activeStepProgressRatio ?? 0)
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? 0.55
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? 0.42
|
||||
: normalizedState.kind === 'match3d'
|
||||
? 0.5
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? 0.52
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? 0.5
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? 0.5
|
||||
: 0;
|
||||
const overallProgress =
|
||||
normalizedState.phase === 'failed'
|
||||
? Math.max(1, completedWeight)
|
||||
: normalizedState.phase === 'ready'
|
||||
? 100
|
||||
: completedWeight + activeStep.weight * assetRatio;
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? resolvePuzzleOverallProgress(
|
||||
normalizedState,
|
||||
activeStepProgressRatio,
|
||||
)
|
||||
: completedWeight + activeStep.weight * activeStepProgressRatio;
|
||||
const cappedOverallProgress =
|
||||
normalizedState.phase === 'ready' || normalizedState.phase === 'failed'
|
||||
? overallProgress
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? Math.min(PUZZLE_NON_READY_MAX_PROGRESS, overallProgress)
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? Math.min(PUZZLE_NON_READY_MAX_PROGRESS, overallProgress)
|
||||
: overallProgress;
|
||||
|
||||
return {
|
||||
@@ -835,7 +1052,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
steps,
|
||||
activeStepIndex,
|
||||
normalizedState,
|
||||
assetRatio,
|
||||
activeStepProgressRatio,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -917,8 +1134,7 @@ export function buildWoodenFishGenerationAnchorEntries(
|
||||
key: 'wooden-fish-hit-sound',
|
||||
label: '音效',
|
||||
value:
|
||||
formPayload?.hitSoundPrompt?.trim() ||
|
||||
draft?.hitSoundPrompt?.trim() ||
|
||||
formPayload?.hitSoundAsset?.prompt?.trim() ||
|
||||
draft?.hitSoundAsset?.prompt?.trim() ||
|
||||
'',
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildCustomWorldPublicWorkCode,
|
||||
buildJumpHopPublicWorkCode,
|
||||
buildWoodenFishPublicWorkCode,
|
||||
isSameCustomWorldPublicWorkCode,
|
||||
isSameJumpHopPublicWorkCode,
|
||||
isSameWoodenFishPublicWorkCode,
|
||||
} from './publicWorkCode';
|
||||
@@ -32,6 +34,16 @@ describe('publicWorkCode', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('builds and matches custom world public work codes from profile ids', () => {
|
||||
expect(buildCustomWorldPublicWorkCode('world-public-1')).toBe('CW-00000001');
|
||||
expect(isSameCustomWorldPublicWorkCode('cw-00000001', 'world-public-1')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isSameCustomWorldPublicWorkCode('world-public-1', 'world-public-1'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('matches wooden fish public work codes and raw profile ids', () => {
|
||||
expect(
|
||||
isSameWoodenFishPublicWorkCode(
|
||||
|
||||
@@ -53,6 +53,28 @@ export function buildBabyObjectMatchPublicWorkCode(profileId: string) {
|
||||
return `BO-${suffix}`;
|
||||
}
|
||||
|
||||
function normalizeCustomWorldPublicWorkCodeSuffix(profileId: string) {
|
||||
const digits = profileId
|
||||
.split('')
|
||||
.filter((character) => character >= '0' && character <= '9')
|
||||
.join('');
|
||||
|
||||
if (digits.length === 0) {
|
||||
const bytes = new TextEncoder().encode(profileId);
|
||||
const checksum = bytes.reduce((accumulator, value) => {
|
||||
return (accumulator * 131 + value) >>> 0;
|
||||
}, 0);
|
||||
|
||||
return String(checksum % 100_000_000).padStart(8, '0');
|
||||
}
|
||||
|
||||
return digits.slice(-8).padStart(8, '0');
|
||||
}
|
||||
|
||||
export function buildCustomWorldPublicWorkCode(profileId: string) {
|
||||
return `CW-${normalizeCustomWorldPublicWorkCodeSuffix(profileId)}`;
|
||||
}
|
||||
|
||||
function normalizeBarkBattlePublicWorkCodeSuffix(workId: string) {
|
||||
const normalized = normalizePublicCodeText(workId);
|
||||
const withoutPrefix = normalized.startsWith('BB')
|
||||
@@ -155,6 +177,19 @@ export function isSameBabyObjectMatchPublicWorkCode(
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameCustomWorldPublicWorkCode(
|
||||
keyword: string,
|
||||
profileId: string,
|
||||
) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
return (
|
||||
normalizedKeyword ===
|
||||
normalizePublicCodeText(buildCustomWorldPublicWorkCode(profileId)) ||
|
||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameBarkBattlePublicWorkCode(keyword: string, workId: string) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
|
||||
36
src/services/wooden-fish/woodenFishClient.test.ts
Normal file
36
src/services/wooden-fish/woodenFishClient.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
const { createCreationAgentClientMock } = vi.hoisted(() => ({
|
||||
createCreationAgentClientMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../creation-agent', () => ({
|
||||
createCreationAgentClient: createCreationAgentClientMock,
|
||||
}));
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
createCreationAgentClientMock.mockReset();
|
||||
createCreationAgentClientMock.mockReturnValue({
|
||||
createSession: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
streamMessage: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
test('wooden fish creation keeps image2 generation requests alive long enough', async () => {
|
||||
await import('./woodenFishClient');
|
||||
|
||||
expect(createCreationAgentClientMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
createSessionTimeoutMs: 20 * 60 * 1000,
|
||||
executeActionTimeoutMs: 20 * 60 * 1000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
const WOODEN_FISH_API_BASE = '/api/creation/wooden-fish/sessions';
|
||||
const WOODEN_FISH_WORKS_API_BASE = '/api/creation/wooden-fish/works';
|
||||
const WOODEN_FISH_RUNTIME_API_BASE = '/api/runtime/wooden-fish';
|
||||
// 中文注释:敲木鱼创作会串行等待多次 image2 与 OSS 写入,前端请求窗口需要覆盖完整生成链路。
|
||||
const WOODEN_FISH_GENERATION_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
const WOODEN_FISH_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
@@ -78,6 +80,8 @@ const woodenFishCreationClient = createCreationAgentClient<
|
||||
streamIncomplete: '敲木鱼共创消息流式结果不完整',
|
||||
executeAction: '执行敲木鱼共创操作失败',
|
||||
},
|
||||
createSessionTimeoutMs: WOODEN_FISH_GENERATION_TIMEOUT_MS,
|
||||
executeActionTimeoutMs: WOODEN_FISH_GENERATION_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
type FlattenedWoodenFishWorkProfileResponse = Omit<
|
||||
@@ -120,6 +124,8 @@ function normalizeWoodenFishWorkProfile(
|
||||
hitObjectAsset: flattened.hitObjectAsset,
|
||||
backgroundAsset:
|
||||
flattened.backgroundAsset ?? flattened.draft?.backgroundAsset ?? null,
|
||||
backButtonAsset:
|
||||
flattened.backButtonAsset ?? flattened.draft?.backButtonAsset ?? null,
|
||||
hitSoundAsset: flattened.hitSoundAsset,
|
||||
floatingWords: flattened.floatingWords,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import type { WoodenFishAudioAsset } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
|
||||
export const WOODEN_FISH_DEFAULT_HIT_OBJECT_SRC =
|
||||
'/wooden-fish/default-hit-object.png';
|
||||
|
||||
export const WOODEN_FISH_DEFAULT_HIT_SOUND_SRC =
|
||||
'/wooden-fish/default-hit-sound.mp3';
|
||||
|
||||
export const WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT =
|
||||
'默认敲击物图案,圆润木质质感,透明背景';
|
||||
|
||||
export const WOODEN_FISH_DEFAULT_HIT_SOUND_ASSET: WoodenFishAudioAsset = {
|
||||
assetId: 'wooden-fish-default-hit-sound',
|
||||
audioSrc: WOODEN_FISH_DEFAULT_HIT_SOUND_SRC,
|
||||
audioObjectKey: 'public/wooden-fish/default-hit-sound.mp3',
|
||||
assetObjectId: 'wooden-fish-default-hit-sound',
|
||||
source: 'bundled-default',
|
||||
prompt: '默认木鱼音',
|
||||
durationMs: 3000,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user