Merge remote-tracking branch 'origin/master' into hermes/hermes-1e775b03
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
# Conflicts: # docs/technical/README.md # src/components/custom-world-home/CustomWorldCreationHub.tsx # src/components/custom-world-home/creationWorkShelf.ts # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx
This commit is contained in:
@@ -19,6 +19,7 @@ type CreationAgentClientOptions = {
|
||||
apiBase: string;
|
||||
messages: CreationAgentClientMessages;
|
||||
createSessionTimeoutMs?: number;
|
||||
executeActionTimeoutMs?: number;
|
||||
readRetry?: ApiRetryOptions;
|
||||
writeRetry?: ApiRetryOptions;
|
||||
};
|
||||
@@ -84,6 +85,7 @@ export function createCreationAgentClient<
|
||||
apiBase,
|
||||
messages,
|
||||
createSessionTimeoutMs = 15000,
|
||||
executeActionTimeoutMs,
|
||||
readRetry = DEFAULT_CREATION_AGENT_READ_RETRY,
|
||||
writeRetry = DEFAULT_CREATION_AGENT_WRITE_RETRY,
|
||||
}: CreationAgentClientOptions) {
|
||||
@@ -152,6 +154,7 @@ export function createCreationAgentClient<
|
||||
messages.executeAction,
|
||||
{
|
||||
retry: writeRetry,
|
||||
timeoutMs: executeActionTimeoutMs,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
90
src/services/creation-audio/creationAudioGenerationClient.ts
Normal file
90
src/services/creation-audio/creationAudioGenerationClient.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type {
|
||||
AudioGenerationTaskResponse,
|
||||
CreateBackgroundMusicRequest,
|
||||
CreateSoundEffectRequest,
|
||||
GeneratedAudioAssetResponse,
|
||||
PublishGeneratedAudioAssetRequest,
|
||||
} from '../../../packages/shared/src/contracts/creationAudio';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
|
||||
const CREATION_AUDIO_API_BASE = '/api/creation/audio';
|
||||
|
||||
const CREATION_AUDIO_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 500,
|
||||
maxDelayMs: 1200,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
export function createBackgroundMusicTask(payload: CreateBackgroundMusicRequest) {
|
||||
return requestJson<AudioGenerationTaskResponse>(
|
||||
`${CREATION_AUDIO_API_BASE}/background-music`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'提交背景音乐生成失败',
|
||||
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 20000 },
|
||||
);
|
||||
}
|
||||
|
||||
export function publishBackgroundMusicAsset(
|
||||
taskId: string,
|
||||
payload: PublishGeneratedAudioAssetRequest,
|
||||
) {
|
||||
return requestJson<GeneratedAudioAssetResponse>(
|
||||
`${CREATION_AUDIO_API_BASE}/background-music/${encodeURIComponent(taskId)}/asset`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'生成背景音乐素材失败',
|
||||
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 30000 },
|
||||
);
|
||||
}
|
||||
|
||||
export function createSoundEffectTask(payload: CreateSoundEffectRequest) {
|
||||
return requestJson<AudioGenerationTaskResponse>(
|
||||
`${CREATION_AUDIO_API_BASE}/sound-effect`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'提交音效生成失败',
|
||||
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 20000 },
|
||||
);
|
||||
}
|
||||
|
||||
export function publishSoundEffectAsset(
|
||||
taskId: string,
|
||||
payload: PublishGeneratedAudioAssetRequest,
|
||||
) {
|
||||
return requestJson<GeneratedAudioAssetResponse>(
|
||||
`${CREATION_AUDIO_API_BASE}/sound-effect/${encodeURIComponent(taskId)}/asset`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'生成音效素材失败',
|
||||
{ retry: CREATION_AUDIO_RETRY, timeoutMs: 30000 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForGeneratedAudioAsset(
|
||||
taskId: string,
|
||||
publish: () => Promise<GeneratedAudioAssetResponse>,
|
||||
) {
|
||||
let latestAsset: GeneratedAudioAssetResponse | null = null;
|
||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||
latestAsset = await publish();
|
||||
if (latestAsset.audioSrc?.trim()) {
|
||||
return latestAsset;
|
||||
}
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 3000));
|
||||
}
|
||||
throw new Error(latestAsset?.status || `音频生成超时:${taskId}`);
|
||||
}
|
||||
1
src/services/creation-audio/index.ts
Normal file
1
src/services/creation-audio/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './creationAudioGenerationClient';
|
||||
@@ -10,6 +10,7 @@ import type { TextStreamOptions } from '../aiTypes';
|
||||
import { createCreationAgentClient } from '../creation-agent';
|
||||
|
||||
const MATCH3D_AGENT_API_BASE = '/api/creation/match3d/sessions';
|
||||
const MATCH3D_EXECUTE_ACTION_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
|
||||
const match3dAgentHttpClient = createCreationAgentClient<
|
||||
CreateMatch3DSessionRequest,
|
||||
@@ -29,6 +30,7 @@ const match3dAgentHttpClient = createCreationAgentClient<
|
||||
streamIncomplete: '抓大鹅共创消息流式结果不完整',
|
||||
executeAction: '执行抓大鹅共创操作失败',
|
||||
},
|
||||
executeActionTimeoutMs: MATCH3D_EXECUTE_ACTION_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export {
|
||||
deleteMatch3DWork,
|
||||
generateMatch3DWorkTags,
|
||||
getMatch3DWorkDetail,
|
||||
listMatch3DGallery,
|
||||
listMatch3DWorks,
|
||||
match3dWorksClient,
|
||||
publishMatch3DWork,
|
||||
updateMatch3DAudioAssets,
|
||||
updateMatch3DGeneratedItemAssets,
|
||||
updateMatch3DWork,
|
||||
} from './match3dWorksClient';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type {
|
||||
GenerateMatch3DWorkTagsRequest,
|
||||
GenerateMatch3DWorkTagsResponse,
|
||||
Match3DWorkDetailResponse,
|
||||
Match3DWorkMutationResponse,
|
||||
Match3DWorksResponse,
|
||||
PutMatch3DAudioAssetsRequest,
|
||||
PutMatch3DWorkRequest,
|
||||
} from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
@@ -79,6 +82,43 @@ export function updateMatch3DWork(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存抓大鹅结果页生成的素材快照。
|
||||
*/
|
||||
export function updateMatch3DGeneratedItemAssets(
|
||||
profileId: string,
|
||||
payload: PutMatch3DAudioAssetsRequest,
|
||||
) {
|
||||
return requestJson<Match3DWorkMutationResponse>(
|
||||
`${MATCH3D_WORKS_API_BASE}/${encodeURIComponent(profileId)}/audio-assets`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'更新抓大鹅生成素材失败',
|
||||
{ retry: MATCH3D_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
export const updateMatch3DAudioAssets = updateMatch3DGeneratedItemAssets;
|
||||
|
||||
/**
|
||||
* 根据当前作品名称与题材生成发布标签。
|
||||
*/
|
||||
export function generateMatch3DWorkTags(payload: GenerateMatch3DWorkTagsRequest) {
|
||||
return requestJson<GenerateMatch3DWorkTagsResponse>(
|
||||
`${MATCH3D_WORKS_API_BASE}/tags`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'生成抓大鹅作品标签失败',
|
||||
{ retry: MATCH3D_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布抓大鹅作品。发布门槛由后端最终确认。
|
||||
*/
|
||||
@@ -105,9 +145,12 @@ export function deleteMatch3DWork(profileId: string) {
|
||||
|
||||
export const match3dWorksClient = {
|
||||
delete: deleteMatch3DWork,
|
||||
generateTags: generateMatch3DWorkTags,
|
||||
getDetail: getMatch3DWorkDetail,
|
||||
listGallery: listMatch3DGallery,
|
||||
list: listMatch3DWorks,
|
||||
publish: publishMatch3DWork,
|
||||
updateAudioAssets: updateMatch3DAudioAssets,
|
||||
updateGeneratedItemAssets: updateMatch3DGeneratedItemAssets,
|
||||
update: updateMatch3DWork,
|
||||
};
|
||||
|
||||
@@ -161,14 +161,47 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
);
|
||||
|
||||
expect(progress?.steps.map((step) => step.id)).toEqual([
|
||||
'match3d-work-title',
|
||||
'match3d-item-names',
|
||||
'match3d-material-sheet',
|
||||
'match3d-slice-images',
|
||||
'match3d-upload-images',
|
||||
'match3d-generate-models',
|
||||
]);
|
||||
expect(progress?.phaseId).toBe('match3d-material-sheet');
|
||||
expect(progress?.phaseLabel).toBe('生成素材图');
|
||||
expect(progress?.estimatedRemainingMs).toBe(103_000);
|
||||
expect(progress?.estimatedRemainingMs).toBe(583_000);
|
||||
});
|
||||
|
||||
test('match3d draft generation starts from title generation', () => {
|
||||
const state = createMiniGameDraftGenerationState('match3d');
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 1_000,
|
||||
);
|
||||
|
||||
expect(progress?.phaseId).toBe('match3d-work-title');
|
||||
expect(progress?.phaseLabel).toBe('生成游戏名称');
|
||||
expect(progress?.steps[0]?.detail).toBe('根据题材设定生成作品名称与标签。');
|
||||
});
|
||||
|
||||
test('match3d draft generation keeps backend observed model phase', () => {
|
||||
const state = {
|
||||
...createMiniGameDraftGenerationState('match3d'),
|
||||
phase: 'match3d-generate-models' as const,
|
||||
completedAssetCount: 1,
|
||||
totalAssetCount: 3,
|
||||
};
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 20_000,
|
||||
);
|
||||
|
||||
expect(progress?.phaseId).toBe('match3d-generate-models');
|
||||
expect(progress?.steps.at(-1)?.completed).toBe(1);
|
||||
expect(progress?.steps.at(-1)?.total).toBe(3);
|
||||
});
|
||||
|
||||
test('match3d generation anchors show theme and fixed three items', () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { BigFishSessionSnapshotResponse } from '../../packages/shared/src/contracts/bigFish';
|
||||
import type {
|
||||
CreatePuzzleAgentSessionRequest,
|
||||
PuzzleAgentSessionSnapshot,
|
||||
} from '../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type {
|
||||
CreateMatch3DSessionRequest,
|
||||
Match3DAgentSessionSnapshot,
|
||||
} from '../../packages/shared/src/contracts/match3dAgent';
|
||||
import type {
|
||||
CreatePuzzleAgentSessionRequest,
|
||||
PuzzleAgentSessionSnapshot,
|
||||
} from '../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type {
|
||||
CustomWorldGenerationProgress,
|
||||
CustomWorldGenerationStep,
|
||||
@@ -30,10 +30,12 @@ export type MiniGameDraftGenerationPhase =
|
||||
| 'square-hole-cover'
|
||||
| 'square-hole-shapes'
|
||||
| 'square-hole-ready'
|
||||
| 'match3d-work-title'
|
||||
| 'match3d-item-names'
|
||||
| 'match3d-material-sheet'
|
||||
| 'match3d-slice-images'
|
||||
| 'match3d-upload-images'
|
||||
| 'match3d-generate-models'
|
||||
| 'match3d-ready'
|
||||
| 'puzzle-images'
|
||||
| 'puzzle-select-image'
|
||||
@@ -140,32 +142,55 @@ const SQUARE_HOLE_STEPS = [
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
const MATCH3D_STEPS = [
|
||||
{
|
||||
id: 'match3d-work-title',
|
||||
label: '生成游戏名称',
|
||||
detail: '根据题材设定生成作品名称与标签。',
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
id: 'match3d-item-names',
|
||||
label: '生成物品名称',
|
||||
detail: '根据题材生成本局的 3 个物品名称。',
|
||||
weight: 16,
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
id: 'match3d-material-sheet',
|
||||
label: '生成素材图',
|
||||
detail: '生成一张 1:1 的网格素材图。',
|
||||
weight: 30,
|
||||
weight: 18,
|
||||
},
|
||||
{
|
||||
id: 'match3d-slice-images',
|
||||
label: '切割独立图片',
|
||||
detail: '把素材图切成独立物品参考图。',
|
||||
weight: 14,
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
id: 'match3d-upload-images',
|
||||
label: '上传图片资产',
|
||||
detail: '写入切割图片并准备进入草稿页。',
|
||||
weight: 40,
|
||||
detail: '写入素材图和独立物品参考图。',
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
id: 'match3d-generate-models',
|
||||
label: '生成3D模型',
|
||||
detail: '调用 Hyper3D Rodin 生成 GLB 模型并转存。',
|
||||
weight: 50,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
const MATCH3D_PHASE_ORDER: Partial<
|
||||
Record<MiniGameDraftGenerationPhase, number>
|
||||
> = {
|
||||
'match3d-work-title': 0,
|
||||
'match3d-item-names': 1,
|
||||
'match3d-material-sheet': 2,
|
||||
'match3d-slice-images': 3,
|
||||
'match3d-upload-images': 4,
|
||||
'match3d-generate-models': 5,
|
||||
};
|
||||
|
||||
function clampProgress(value: number) {
|
||||
return Math.max(0, Math.min(100, Math.round(value)));
|
||||
}
|
||||
@@ -234,7 +259,7 @@ export function createMiniGameDraftGenerationState(
|
||||
: kind === 'square-hole'
|
||||
? 'square-hole-draft'
|
||||
: kind === 'match3d'
|
||||
? 'match3d-item-names'
|
||||
? 'match3d-work-title'
|
||||
: 'compile',
|
||||
startedAtMs: Date.now(),
|
||||
completedAssetCount: 0,
|
||||
@@ -269,17 +294,23 @@ function resolveSquareHolePhaseByElapsedMs(
|
||||
|
||||
function resolveMatch3DPhaseByElapsedMs(
|
||||
elapsedMs: number,
|
||||
currentPhase: MiniGameDraftGenerationPhase,
|
||||
): MiniGameDraftGenerationPhase {
|
||||
if (elapsedMs >= 72_000) {
|
||||
return 'match3d-upload-images';
|
||||
}
|
||||
if (elapsedMs >= 58_000) {
|
||||
return 'match3d-slice-images';
|
||||
}
|
||||
if (elapsedMs >= 16_000) {
|
||||
return 'match3d-material-sheet';
|
||||
}
|
||||
return 'match3d-item-names';
|
||||
const elapsedPhase =
|
||||
elapsedMs >= 92_000
|
||||
? 'match3d-generate-models'
|
||||
: elapsedMs >= 72_000
|
||||
? 'match3d-upload-images'
|
||||
: elapsedMs >= 58_000
|
||||
? 'match3d-slice-images'
|
||||
: elapsedMs >= 16_000
|
||||
? 'match3d-material-sheet'
|
||||
: elapsedMs >= 4_000
|
||||
? 'match3d-item-names'
|
||||
: 'match3d-work-title';
|
||||
const elapsedOrder = MATCH3D_PHASE_ORDER[elapsedPhase] ?? 0;
|
||||
const currentOrder = MATCH3D_PHASE_ORDER[currentPhase] ?? -1;
|
||||
return currentOrder > elapsedOrder ? currentPhase : elapsedPhase;
|
||||
}
|
||||
|
||||
function resolvePuzzleTimelineByElapsedMs(elapsedMs: number) {
|
||||
@@ -347,7 +378,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs),
|
||||
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs, state.phase),
|
||||
}
|
||||
: state;
|
||||
|
||||
@@ -422,7 +453,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? Math.max(0, 12_000 - elapsedMs)
|
||||
: normalizedState.kind === 'match3d'
|
||||
? Math.max(0, 120_000 - elapsedMs)
|
||||
? Math.max(0, 10 * 60_000 - elapsedMs)
|
||||
: null,
|
||||
activeStepIndex,
|
||||
steps: buildMiniGameProgressSteps(
|
||||
|
||||
@@ -395,6 +395,7 @@ describe('puzzleLocalRuntime', () => {
|
||||
rank: 1,
|
||||
nickname: '本地玩家',
|
||||
elapsedMs: clearedRun.currentLevel?.elapsedMs ?? 0,
|
||||
visibleTags: [],
|
||||
isCurrentPlayer: true,
|
||||
},
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user