Merge remote-tracking branch 'origin/master' into codex/tiaoyitiao
# Conflicts: # server-rs/crates/api-server/src/jump_hop.rs # server-rs/crates/api-server/src/modules/jump_hop.rs
This commit is contained in:
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
createBarkBattleDraft,
|
||||
deleteBarkBattleWork,
|
||||
generateAllBarkBattleImageAssets,
|
||||
publishBarkBattleWork,
|
||||
regenerateBarkBattleImageAsset,
|
||||
@@ -73,6 +74,21 @@ describe('barkBattleCreationClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes a draft or published work through runtime works API', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({ items: [] });
|
||||
|
||||
await deleteBarkBattleWork('bark-battle-work-1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/bark-battle/works/bark-battle-work-1',
|
||||
{ method: 'DELETE' },
|
||||
'删除汪汪声浪作品失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({ retryUnsafeMethods: true }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('persists generated image slots into an existing draft config', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({ draftId: 'draft-1' });
|
||||
|
||||
|
||||
@@ -290,6 +290,17 @@ export function listBarkBattleWorks(
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteBarkBattleWork(workId: string) {
|
||||
return requestJson<BarkBattleWorksResponse>(
|
||||
`${BARK_BATTLE_RUNTIME_API_BASE}/works/${encodeURIComponent(workId)}`,
|
||||
{ method: 'DELETE' },
|
||||
'删除汪汪声浪作品失败',
|
||||
{
|
||||
retry: BARK_BATTLE_CREATION_WRITE_RETRY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function listBarkBattleGallery() {
|
||||
return requestJson<BarkBattleWorksResponse>(
|
||||
`${BARK_BATTLE_RUNTIME_API_BASE}/gallery`,
|
||||
@@ -441,6 +452,7 @@ export async function generateAllBarkBattleImageAssets(payload: {
|
||||
|
||||
export const barkBattleCreationClient = {
|
||||
createDraft: createBarkBattleDraft,
|
||||
deleteWork: deleteBarkBattleWork,
|
||||
generateAllImageAssets: generateAllBarkBattleImageAssets,
|
||||
listGallery: listBarkBattleGallery,
|
||||
listWorks: listBarkBattleWorks,
|
||||
|
||||
@@ -7,6 +7,7 @@ export {
|
||||
type BarkBattleImageGenerationFailures,
|
||||
type BarkBattleUploadedAsset,
|
||||
createBarkBattleDraft,
|
||||
deleteBarkBattleWork,
|
||||
generateAllBarkBattleImageAssets,
|
||||
listBarkBattleGallery,
|
||||
listBarkBattleWorks,
|
||||
|
||||
@@ -27,6 +27,19 @@ beforeEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
});
|
||||
|
||||
test('jump hop delete work uses creation works endpoint', async () => {
|
||||
const { jumpHopClient } = await import('./jumpHopClient');
|
||||
requestJsonMock.mockResolvedValueOnce({ items: [] });
|
||||
|
||||
await jumpHopClient.deleteWork('jump-hop-profile-1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/creation/jump-hop/works/jump-hop-profile-1',
|
||||
{ method: 'DELETE' },
|
||||
'删除跳一跳作品失败',
|
||||
);
|
||||
});
|
||||
|
||||
test('jump hop creation keeps image2 generation requests alive long enough', async () => {
|
||||
await import('./jumpHopClient');
|
||||
|
||||
@@ -37,3 +50,93 @@ test('jump hop creation keeps image2 generation requests alive long enough', asy
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('jump hop work detail preserves flattened back button asset', async () => {
|
||||
const backButtonAsset = {
|
||||
assetId: 'back-button-1',
|
||||
imageSrc: '/generated-jump-hop-assets/back-button-1.png',
|
||||
imageObjectKey: 'jump-hop/back-button-1.png',
|
||||
assetObjectId: 'asset-object-back-button-1',
|
||||
generationProvider: 'image2',
|
||||
prompt: '主题返回按钮',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
const characterAsset = {
|
||||
assetId: 'character-1',
|
||||
imageSrc: 'builtin://jump-hop/default-character',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'character-object-1',
|
||||
generationProvider: 'builtin-three',
|
||||
prompt: '内置默认角色',
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
const draft = {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: 'profile-1',
|
||||
themeText: '森林茶馆',
|
||||
workTitle: '森林茶馆跳一跳',
|
||||
workDescription: '森林茶馆主题',
|
||||
themeTags: ['森林茶馆', '跳一跳'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
defaultCharacter: null,
|
||||
characterPrompt: '内置默认角色',
|
||||
tilePrompt: '森林茶馆主题地块',
|
||||
endMoodPrompt: null,
|
||||
characterAsset,
|
||||
tileAtlasAsset: characterAsset,
|
||||
tileAssets: [],
|
||||
path: {
|
||||
seed: 'profile-1',
|
||||
difficulty: 'standard',
|
||||
platforms: [],
|
||||
scoring: {
|
||||
perfectRadiusRatio: 0.24,
|
||||
hitRadiusRatio: 0.52,
|
||||
maxChargeMs: 1200,
|
||||
minChargeMs: 80,
|
||||
maxJumpDistance: 5,
|
||||
},
|
||||
},
|
||||
coverComposite: null,
|
||||
backButtonAsset: null,
|
||||
generationStatus: 'ready',
|
||||
};
|
||||
requestJsonMock.mockResolvedValue({
|
||||
item: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'work-1',
|
||||
profileId: 'profile-1',
|
||||
ownerUserId: 'owner-1',
|
||||
sourceSessionId: 'session-1',
|
||||
themeText: '森林茶馆',
|
||||
workTitle: '森林茶馆跳一跳',
|
||||
workDescription: '森林茶馆主题',
|
||||
themeTags: ['森林茶馆', '跳一跳'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'published',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-06-05T00:00:00Z',
|
||||
publishedAt: '2026-06-05T00:00:00Z',
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
draft,
|
||||
path: draft.path,
|
||||
defaultCharacter: null,
|
||||
characterAsset,
|
||||
tileAtlasAsset: characterAsset,
|
||||
tileAssets: [],
|
||||
backButtonAsset,
|
||||
},
|
||||
});
|
||||
|
||||
const { jumpHopClient } = await import('./jumpHopClient');
|
||||
const response = await jumpHopClient.getWorkDetail('profile-1');
|
||||
|
||||
expect(response.item.backButtonAsset).toEqual(backButtonAsset);
|
||||
});
|
||||
|
||||
@@ -136,6 +136,8 @@ function normalizeJumpHopWorkProfile(
|
||||
characterAsset: flattened.characterAsset,
|
||||
tileAtlasAsset: flattened.tileAtlasAsset,
|
||||
tileAssets: flattened.tileAssets,
|
||||
backButtonAsset:
|
||||
flattened.backButtonAsset ?? flattened.draft?.backButtonAsset ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -248,6 +250,14 @@ export async function publishJumpHopWork(profileId: string) {
|
||||
return normalizeJumpHopWorkMutationResponse(response);
|
||||
}
|
||||
|
||||
export async function deleteJumpHopWork(profileId: string) {
|
||||
return requestJson<JumpHopWorksResponse>(
|
||||
`${JUMP_HOP_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'DELETE' },
|
||||
'删除跳一跳作品失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function startJumpHopRuntimeRun(
|
||||
profileId: string,
|
||||
options: JumpHopStartRunOptions = {},
|
||||
@@ -339,6 +349,7 @@ export async function restartJumpHopRuntimeRun(
|
||||
|
||||
export const jumpHopClient = {
|
||||
createSession: createJumpHopCreationSession,
|
||||
deleteWork: deleteJumpHopWork,
|
||||
getSession: getJumpHopCreationSession,
|
||||
executeAction: executeJumpHopCreationAction,
|
||||
getGalleryDetail: getJumpHopGalleryDetail,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildJumpHopGenerationAnchorEntries,
|
||||
buildMatch3DGenerationAnchorEntries,
|
||||
buildMiniGameDraftGenerationProgress,
|
||||
buildPuzzleClearGenerationAnchorEntries,
|
||||
buildPuzzleGenerationAnchorEntries,
|
||||
buildWoodenFishGenerationAnchorEntries,
|
||||
createMiniGameDraftGenerationState,
|
||||
@@ -653,6 +654,56 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('puzzle clear draft generation exposes atlas and slice pipeline', () => {
|
||||
const state = createMiniGameDraftGenerationState('puzzle-clear');
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 190_000,
|
||||
);
|
||||
|
||||
expect(progress?.steps.map((step) => step.id)).toEqual([
|
||||
'puzzle-clear-draft',
|
||||
'puzzle-clear-background',
|
||||
'puzzle-clear-atlas',
|
||||
'puzzle-clear-slices',
|
||||
'puzzle-clear-write-draft',
|
||||
]);
|
||||
expect(progress?.phaseId).toBe('puzzle-clear-atlas');
|
||||
expect(progress?.phaseLabel).toBe('生成复合图集');
|
||||
expect(progress?.estimatedRemainingMs).toBe(430_000);
|
||||
});
|
||||
|
||||
test('puzzle clear generation anchors expose title, theme and background mode', () => {
|
||||
const entries = buildPuzzleClearGenerationAnchorEntries(null, {
|
||||
templateId: 'puzzle-clear',
|
||||
workTitle: '星港拼消消',
|
||||
workDescription: '星港主题连续消除。',
|
||||
themePrompt: '霓虹星港',
|
||||
boardBackgroundPrompt: '美少女',
|
||||
generateBoardBackground: true,
|
||||
boardBackgroundAsset: null,
|
||||
});
|
||||
|
||||
expect(entries).toEqual([
|
||||
{
|
||||
id: 'puzzle-clear-title',
|
||||
label: '作品',
|
||||
value: '星港拼消消',
|
||||
},
|
||||
{
|
||||
id: 'puzzle-clear-theme',
|
||||
label: '主题',
|
||||
value: '霓虹星港',
|
||||
},
|
||||
{
|
||||
id: 'puzzle-clear-background',
|
||||
label: '底图',
|
||||
value: '美少女',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('puzzle generation anchors expose form payload as the display source', () => {
|
||||
const entries = buildPuzzleGenerationAnchorEntries(
|
||||
{
|
||||
|
||||
@@ -11,6 +11,10 @@ import type {
|
||||
CreatePuzzleAgentSessionRequest,
|
||||
PuzzleAgentSessionSnapshot,
|
||||
} from '../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type {
|
||||
PuzzleClearSessionSnapshotResponse,
|
||||
PuzzleClearWorkspaceCreateRequest,
|
||||
} from '../../packages/shared/src/contracts/puzzleClear';
|
||||
import type {
|
||||
CustomWorldGenerationProgress,
|
||||
CustomWorldGenerationStep,
|
||||
@@ -33,6 +37,7 @@ export type MiniGameDraftGenerationKind =
|
||||
| 'match3d'
|
||||
| 'baby-object-match'
|
||||
| 'jump-hop'
|
||||
| 'puzzle-clear'
|
||||
| 'wooden-fish';
|
||||
|
||||
export type MiniGameDraftGenerationPhase =
|
||||
@@ -66,6 +71,11 @@ export type MiniGameDraftGenerationPhase =
|
||||
| 'jump-hop-tile-atlas'
|
||||
| 'jump-hop-slice-tiles'
|
||||
| 'jump-hop-write-draft'
|
||||
| 'puzzle-clear-draft'
|
||||
| 'puzzle-clear-background'
|
||||
| 'puzzle-clear-atlas'
|
||||
| 'puzzle-clear-slices'
|
||||
| 'puzzle-clear-write-draft'
|
||||
| 'wooden-fish-draft'
|
||||
| 'wooden-fish-hit-object'
|
||||
| 'wooden-fish-background'
|
||||
@@ -418,6 +428,41 @@ const JUMP_HOP_STEPS = [
|
||||
|
||||
const JUMP_HOP_ESTIMATED_WAIT_MS = 5 * 60_000;
|
||||
|
||||
const PUZZLE_CLEAR_STEPS = [
|
||||
{
|
||||
id: 'puzzle-clear-draft',
|
||||
label: '整理玩法草稿',
|
||||
detail: '保存作品信息、主题词与底图策略。',
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-clear-background',
|
||||
label: '准备场地底图',
|
||||
detail: '处理上传底图或生成中央场地底图。',
|
||||
weight: 22,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-clear-atlas',
|
||||
label: '生成复合图集',
|
||||
detail: '生成 135 组复合图案 atlas。',
|
||||
weight: 42,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-clear-slices',
|
||||
label: '切分卡牌碎片',
|
||||
detail: '按预排坐标切成 1x1 卡牌碎片并校验。',
|
||||
weight: 20,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-clear-write-draft',
|
||||
label: '写入正式草稿',
|
||||
detail: '保存底图、图集、碎片和作品摘要。',
|
||||
weight: 8,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
const PUZZLE_CLEAR_ESTIMATED_WAIT_MS = 620_000;
|
||||
|
||||
const WOODEN_FISH_STEPS = [
|
||||
{
|
||||
id: 'wooden-fish-draft',
|
||||
@@ -497,6 +542,9 @@ function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
||||
if (kind === 'jump-hop') {
|
||||
return JUMP_HOP_STEPS;
|
||||
}
|
||||
if (kind === 'puzzle-clear') {
|
||||
return PUZZLE_CLEAR_STEPS;
|
||||
}
|
||||
if (kind === 'wooden-fish') {
|
||||
return WOODEN_FISH_STEPS;
|
||||
}
|
||||
@@ -571,9 +619,11 @@ export function createMiniGameDraftGenerationState(
|
||||
? 'baby-object-draft'
|
||||
: kind === 'jump-hop'
|
||||
? 'jump-hop-draft'
|
||||
: kind === 'wooden-fish'
|
||||
? 'wooden-fish-draft'
|
||||
: 'compile',
|
||||
: kind === 'puzzle-clear'
|
||||
? 'puzzle-clear-draft'
|
||||
: kind === 'wooden-fish'
|
||||
? 'wooden-fish-draft'
|
||||
: 'compile',
|
||||
startedAtMs,
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
@@ -657,6 +707,24 @@ function resolveJumpHopPhaseByElapsedMs(
|
||||
return 'jump-hop-draft';
|
||||
}
|
||||
|
||||
function resolvePuzzleClearPhaseByElapsedMs(
|
||||
elapsedMs: number,
|
||||
): MiniGameDraftGenerationPhase {
|
||||
if (elapsedMs >= 590_000) {
|
||||
return 'puzzle-clear-write-draft';
|
||||
}
|
||||
if (elapsedMs >= 470_000) {
|
||||
return 'puzzle-clear-slices';
|
||||
}
|
||||
if (elapsedMs >= 120_000) {
|
||||
return 'puzzle-clear-atlas';
|
||||
}
|
||||
if (elapsedMs >= 8_000) {
|
||||
return 'puzzle-clear-background';
|
||||
}
|
||||
return 'puzzle-clear-draft';
|
||||
}
|
||||
|
||||
function buildWoodenFishPhaseTimeline(): Array<{
|
||||
phase: Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
@@ -798,6 +866,8 @@ function resolveElapsedActiveStepProgressRatio(
|
||||
? BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS
|
||||
: kind === 'jump-hop'
|
||||
? JUMP_HOP_ESTIMATED_WAIT_MS
|
||||
: kind === 'puzzle-clear'
|
||||
? PUZZLE_CLEAR_ESTIMATED_WAIT_MS
|
||||
: kind === 'wooden-fish'
|
||||
? WOODEN_FISH_ESTIMATED_WAIT_MS
|
||||
: 1;
|
||||
@@ -924,7 +994,14 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
...state,
|
||||
phase: resolveJumpHopPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
: state.kind === 'puzzle-clear' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolvePuzzleClearPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
|
||||
const puzzleTimedSteps =
|
||||
normalizedState.kind === 'puzzle'
|
||||
@@ -984,6 +1061,11 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'puzzle-clear'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? (woodenFishTimeline?.activeStepProgressRatio ?? 0)
|
||||
: 0;
|
||||
@@ -1023,6 +1105,8 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? '宝贝识物草稿已准备完成,可进入结果页继续发布。'
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? '跳一跳草稿已准备完成,可进入结果页试玩或发布。'
|
||||
: normalizedState.kind === 'puzzle-clear'
|
||||
? '拼消消草稿已准备完成,可进入结果页试玩或发布。'
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? '敲木鱼草稿已准备完成,可进入结果页试玩或发布。'
|
||||
: '首关草稿与正式图已准备完成,可进入结果页补作品信息。'
|
||||
@@ -1050,6 +1134,11 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? Math.max(0, BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? Math.max(0, JUMP_HOP_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: normalizedState.kind === 'puzzle-clear'
|
||||
? Math.max(
|
||||
0,
|
||||
PUZZLE_CLEAR_ESTIMATED_WAIT_MS - elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? Math.max(0, WOODEN_FISH_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: null,
|
||||
@@ -1163,6 +1252,49 @@ export function buildWoodenFishGenerationAnchorEntries(
|
||||
.filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
export function buildPuzzleClearGenerationAnchorEntries(
|
||||
session: PuzzleClearSessionSnapshotResponse | null | undefined,
|
||||
formPayload: PuzzleClearWorkspaceCreateRequest | null | undefined = null,
|
||||
): CustomWorldStructuredAnchorEntry[] {
|
||||
const draft = session?.draft;
|
||||
const entries: Array<MiniGameAnchorSource | null> = [
|
||||
{
|
||||
key: 'puzzle-clear-title',
|
||||
label: '作品',
|
||||
value:
|
||||
formPayload?.workTitle?.trim() || draft?.workTitle?.trim() || '拼消消',
|
||||
},
|
||||
{
|
||||
key: 'puzzle-clear-theme',
|
||||
label: '主题',
|
||||
value:
|
||||
formPayload?.themePrompt?.trim() || draft?.themePrompt?.trim() || '',
|
||||
},
|
||||
{
|
||||
key: 'puzzle-clear-background',
|
||||
label: '底图',
|
||||
value:
|
||||
formPayload?.boardBackgroundPrompt?.trim() ||
|
||||
draft?.boardBackgroundPrompt?.trim() ||
|
||||
(formPayload?.boardBackgroundAsset ?? draft?.boardBackgroundAsset)
|
||||
?.prompt?.trim() ||
|
||||
(formPayload?.generateBoardBackground ??
|
||||
draft?.generateBoardBackground
|
||||
? 'AI生成'
|
||||
: '上传底图'),
|
||||
},
|
||||
];
|
||||
|
||||
return entries
|
||||
.filter((entry): entry is MiniGameAnchorSource => Boolean(entry))
|
||||
.map((entry) => ({
|
||||
id: entry.key,
|
||||
label: entry.label,
|
||||
value: entry.value,
|
||||
}))
|
||||
.filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
export function buildPuzzleGenerationAnchorEntries(
|
||||
session: PuzzleAgentSessionSnapshot | null | undefined,
|
||||
formPayload: CreatePuzzleAgentSessionRequest | null | undefined = null,
|
||||
|
||||
@@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildCustomWorldPublicWorkCode,
|
||||
buildJumpHopPublicWorkCode,
|
||||
buildPuzzleClearPublicWorkCode,
|
||||
buildWoodenFishPublicWorkCode,
|
||||
isSameCustomWorldPublicWorkCode,
|
||||
isSameJumpHopPublicWorkCode,
|
||||
isSamePuzzleClearPublicWorkCode,
|
||||
isSameWoodenFishPublicWorkCode,
|
||||
} from './publicWorkCode';
|
||||
|
||||
@@ -28,6 +30,24 @@ describe('publicWorkCode', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('builds and matches puzzle-clear public work codes from profile ids', () => {
|
||||
expect(buildPuzzleClearPublicWorkCode('puzzle-clear-profile-12345678')).toBe(
|
||||
'PC-12345678',
|
||||
);
|
||||
expect(
|
||||
isSamePuzzleClearPublicWorkCode(
|
||||
'pc-12345678',
|
||||
'puzzle-clear-profile-12345678',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isSamePuzzleClearPublicWorkCode(
|
||||
'puzzle clear profile 12345678',
|
||||
'puzzle-clear-profile-12345678',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('builds wooden fish public work codes with WF prefix', () => {
|
||||
expect(buildWoodenFishPublicWorkCode('wooden-fish-profile-1234abcd')).toBe(
|
||||
'WF-1234ABCD',
|
||||
|
||||
@@ -13,6 +13,14 @@ export function buildPuzzlePublicWorkCode(profileId: string) {
|
||||
return `PZ-${suffix}`;
|
||||
}
|
||||
|
||||
export function buildPuzzleClearPublicWorkCode(profileId: string) {
|
||||
const normalized = normalizePublicCodeText(profileId);
|
||||
const fallback = normalized || '00000000';
|
||||
const suffix = fallback.slice(-8).padStart(8, '0');
|
||||
|
||||
return `PC-${suffix}`;
|
||||
}
|
||||
|
||||
export function buildBigFishPublicWorkCode(sessionId: string) {
|
||||
const normalized = normalizePublicCodeText(sessionId);
|
||||
const fallback = normalized || '00000000';
|
||||
@@ -115,6 +123,19 @@ export function isSamePuzzlePublicWorkCode(keyword: string, profileId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function isSamePuzzleClearPublicWorkCode(
|
||||
keyword: string,
|
||||
profileId: string,
|
||||
) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
return (
|
||||
normalizedKeyword ===
|
||||
normalizePublicCodeText(buildPuzzleClearPublicWorkCode(profileId)) ||
|
||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameBigFishPublicWorkCode(
|
||||
keyword: string,
|
||||
sessionId: string,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
const requestJsonMock = vi.hoisted(() => vi.fn());
|
||||
const { createCreationAgentClientMock } = vi.hoisted(() => ({
|
||||
createCreationAgentClientMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: requestJsonMock,
|
||||
}));
|
||||
|
||||
vi.mock('../creation-agent', () => ({
|
||||
createCreationAgentClient: createCreationAgentClientMock,
|
||||
}));
|
||||
|
||||
vi.mock('../runtimeGuestAuth', () => ({
|
||||
buildRuntimeGuestAuthOptions: vi.fn(() => ({})),
|
||||
buildRuntimeGuestHeaders: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
createCreationAgentClientMock.mockReset();
|
||||
createCreationAgentClientMock.mockReturnValue({
|
||||
createSession: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
streamMessage: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
});
|
||||
requestJsonMock.mockReset();
|
||||
});
|
||||
|
||||
test('拼消消创作保留足够长的 image2 生成等待窗口', async () => {
|
||||
await import('./puzzleClearClient');
|
||||
|
||||
expect(createCreationAgentClientMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
createSessionTimeoutMs: 40 * 60 * 1000,
|
||||
executeActionTimeoutMs: 40 * 60 * 1000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('拼消消客户端区分创作详情和公开运行态详情', async () => {
|
||||
const { puzzleClearClient } = await import('./puzzleClearClient');
|
||||
requestJsonMock.mockResolvedValue({
|
||||
item: {
|
||||
summary: {
|
||||
runtimeKind: 'puzzle-clear',
|
||||
workId: 'work-1',
|
||||
profileId: 'profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'session-1',
|
||||
workTitle: '拼消消',
|
||||
workDescription: '',
|
||||
themePrompt: '星港',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
draft: {
|
||||
templateId: 'puzzle-clear',
|
||||
templateName: '拼消消',
|
||||
profileId: 'profile-1',
|
||||
workTitle: '拼消消',
|
||||
workDescription: '',
|
||||
themePrompt: '星港',
|
||||
generateBoardBackground: true,
|
||||
boardBackgroundAsset: null,
|
||||
cardBackImageSrc: null,
|
||||
atlasAsset: {
|
||||
assetId: 'atlas-1',
|
||||
imageSrc: '/generated-puzzle-clear-assets/atlas.png',
|
||||
imageObjectKey: 'generated-puzzle-clear-assets/atlas.png',
|
||||
assetObjectId: 'assetobj_atlas',
|
||||
generationProvider: 'gpt-image-2',
|
||||
prompt: '星港',
|
||||
width: 1024,
|
||||
height: 1536,
|
||||
},
|
||||
patternGroups: [],
|
||||
cardAssets: [],
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
boardBackgroundAsset: null,
|
||||
atlasAsset: {
|
||||
assetId: 'atlas-1',
|
||||
imageSrc: '/generated-puzzle-clear-assets/atlas.png',
|
||||
imageObjectKey: 'generated-puzzle-clear-assets/atlas.png',
|
||||
assetObjectId: 'assetobj_atlas',
|
||||
generationProvider: 'gpt-image-2',
|
||||
prompt: '星港',
|
||||
width: 1024,
|
||||
height: 1536,
|
||||
},
|
||||
patternGroups: [],
|
||||
cardAssets: [],
|
||||
},
|
||||
});
|
||||
|
||||
await puzzleClearClient.getWorkDetail('profile-1');
|
||||
await puzzleClearClient.getRuntimeWorkDetail('profile-1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/creation/puzzle-clear/works/profile-1',
|
||||
{ method: 'GET' },
|
||||
'读取拼消消作品详情失败',
|
||||
);
|
||||
expect(requestJsonMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/runtime/puzzle-clear/works/profile-1',
|
||||
{ method: 'GET' },
|
||||
'读取拼消消公开作品详情失败',
|
||||
expect.objectContaining({
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,417 @@
|
||||
import type {
|
||||
PuzzleClearActionRequest,
|
||||
PuzzleClearActionResponse,
|
||||
PuzzleClearGalleryCardResponse,
|
||||
PuzzleClearGalleryDetailResponse,
|
||||
PuzzleClearNextLevelRequest,
|
||||
PuzzleClearRetryLevelRequest,
|
||||
PuzzleClearRunResponse,
|
||||
PuzzleClearSessionResponse,
|
||||
PuzzleClearSessionSnapshotResponse,
|
||||
PuzzleClearSwapRequest,
|
||||
PuzzleClearTimeUpRequest,
|
||||
PuzzleClearWorkDetailResponse,
|
||||
PuzzleClearWorkMutationResponse,
|
||||
PuzzleClearWorkProfileResponse,
|
||||
PuzzleClearWorksResponse,
|
||||
PuzzleClearWorkspaceCreateRequest,
|
||||
PuzzleClearWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/puzzleClear';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
import { createCreationAgentClient } from '../creation-agent';
|
||||
import {
|
||||
buildRuntimeGuestAuthOptions,
|
||||
buildRuntimeGuestHeaders,
|
||||
type RuntimeGuestRequestOptions,
|
||||
} from '../runtimeGuestAuth';
|
||||
|
||||
const PUZZLE_CLEAR_API_BASE = '/api/creation/puzzle-clear/sessions';
|
||||
const PUZZLE_CLEAR_WORKS_API_BASE = '/api/creation/puzzle-clear/works';
|
||||
const PUZZLE_CLEAR_RUNTIME_API_BASE = '/api/runtime/puzzle-clear';
|
||||
// 中文注释:拼消消编译要串行等待底图、4 张素材工作表、切片、最终 atlas 合成和 OSS 写入;VectorEngine 偶发单张图可超过 10 分钟。
|
||||
const PUZZLE_CLEAR_GENERATION_TIMEOUT_MS = 40 * 60 * 1000;
|
||||
const PUZZLE_CLEAR_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
};
|
||||
const PUZZLE_CLEAR_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
export type {
|
||||
PuzzleClearActionRequest,
|
||||
PuzzleClearActionResponse,
|
||||
PuzzleClearGalleryCardResponse,
|
||||
PuzzleClearGalleryDetailResponse,
|
||||
PuzzleClearNextLevelRequest,
|
||||
PuzzleClearRetryLevelRequest,
|
||||
PuzzleClearRunResponse,
|
||||
PuzzleClearSessionResponse,
|
||||
PuzzleClearSessionSnapshotResponse,
|
||||
PuzzleClearSwapRequest,
|
||||
PuzzleClearTimeUpRequest,
|
||||
PuzzleClearWorkDetailResponse,
|
||||
PuzzleClearWorkMutationResponse,
|
||||
PuzzleClearWorkProfileResponse,
|
||||
PuzzleClearWorksResponse,
|
||||
PuzzleClearWorkspaceCreateRequest,
|
||||
PuzzleClearWorkSummaryResponse,
|
||||
};
|
||||
export type CreatePuzzleClearSessionRequest =
|
||||
PuzzleClearWorkspaceCreateRequest;
|
||||
export type PuzzleClearSessionSnapshot = PuzzleClearSessionSnapshotResponse;
|
||||
type PuzzleClearRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||
|
||||
const puzzleClearCreationClient = createCreationAgentClient<
|
||||
PuzzleClearWorkspaceCreateRequest,
|
||||
PuzzleClearSessionResponse,
|
||||
PuzzleClearSessionResponse,
|
||||
PuzzleClearSessionSnapshotResponse,
|
||||
never,
|
||||
never,
|
||||
PuzzleClearActionRequest,
|
||||
PuzzleClearActionResponse
|
||||
>({
|
||||
apiBase: PUZZLE_CLEAR_API_BASE,
|
||||
messages: {
|
||||
createSession: '创建拼消消共创会话失败',
|
||||
getSession: '读取拼消消共创会话失败',
|
||||
sendMessage: '发送拼消消共创消息失败',
|
||||
streamIncomplete: '拼消消共创消息流式结果不完整',
|
||||
executeAction: '执行拼消消共创操作失败',
|
||||
},
|
||||
createSessionTimeoutMs: PUZZLE_CLEAR_GENERATION_TIMEOUT_MS,
|
||||
executeActionTimeoutMs: PUZZLE_CLEAR_GENERATION_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
type FlattenedPuzzleClearWorkProfileResponse = Omit<
|
||||
PuzzleClearWorkProfileResponse,
|
||||
'summary'
|
||||
> &
|
||||
PuzzleClearWorkSummaryResponse;
|
||||
|
||||
export type PuzzleClearGalleryResponse = {
|
||||
items: PuzzleClearGalleryCardResponse[];
|
||||
hasMore: boolean;
|
||||
nextCursor: string | null;
|
||||
};
|
||||
|
||||
function normalizePuzzleClearWorkProfile(
|
||||
work:
|
||||
| PuzzleClearWorkProfileResponse
|
||||
| FlattenedPuzzleClearWorkProfileResponse,
|
||||
): PuzzleClearWorkProfileResponse {
|
||||
if ('summary' in work && work.summary) {
|
||||
return work;
|
||||
}
|
||||
|
||||
const flattened = work as FlattenedPuzzleClearWorkProfileResponse;
|
||||
const summary: PuzzleClearWorkProfileResponse['summary'] = {
|
||||
runtimeKind: flattened.runtimeKind,
|
||||
workId: flattened.workId,
|
||||
profileId: flattened.profileId,
|
||||
ownerUserId: flattened.ownerUserId,
|
||||
sourceSessionId: flattened.sourceSessionId ?? null,
|
||||
workTitle: flattened.workTitle,
|
||||
workDescription: flattened.workDescription,
|
||||
themePrompt: flattened.themePrompt,
|
||||
coverImageSrc: flattened.coverImageSrc ?? null,
|
||||
publicationStatus: flattened.publicationStatus,
|
||||
playCount: flattened.playCount,
|
||||
updatedAt: flattened.updatedAt,
|
||||
publishedAt: flattened.publishedAt ?? null,
|
||||
publishReady: flattened.publishReady,
|
||||
generationStatus: flattened.generationStatus,
|
||||
};
|
||||
|
||||
return {
|
||||
summary,
|
||||
draft: flattened.draft,
|
||||
boardBackgroundAsset:
|
||||
flattened.boardBackgroundAsset ?? flattened.draft?.boardBackgroundAsset ?? null,
|
||||
atlasAsset: flattened.atlasAsset,
|
||||
patternGroups: flattened.patternGroups,
|
||||
cardAssets: flattened.cardAssets,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePuzzleClearActionResponse(
|
||||
response: PuzzleClearActionResponse,
|
||||
): PuzzleClearActionResponse {
|
||||
return {
|
||||
...response,
|
||||
work: response.work ? normalizePuzzleClearWorkProfile(response.work) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePuzzleClearWorkDetailResponse(
|
||||
response: PuzzleClearWorkDetailResponse,
|
||||
): PuzzleClearWorkDetailResponse {
|
||||
return {
|
||||
...response,
|
||||
item: normalizePuzzleClearWorkProfile(response.item),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePuzzleClearWorkMutationResponse(
|
||||
response: PuzzleClearWorkMutationResponse,
|
||||
): PuzzleClearWorkMutationResponse {
|
||||
return {
|
||||
...response,
|
||||
item: normalizePuzzleClearWorkProfile(response.item),
|
||||
};
|
||||
}
|
||||
|
||||
export function createPuzzleClearCreationSession(
|
||||
payload: PuzzleClearWorkspaceCreateRequest,
|
||||
) {
|
||||
return puzzleClearCreationClient.createSession(payload);
|
||||
}
|
||||
|
||||
export function getPuzzleClearCreationSession(sessionId: string) {
|
||||
return puzzleClearCreationClient.getSession(sessionId);
|
||||
}
|
||||
|
||||
export function executePuzzleClearCreationAction(
|
||||
sessionId: string,
|
||||
payload: PuzzleClearActionRequest,
|
||||
) {
|
||||
return puzzleClearCreationClient
|
||||
.executeAction(sessionId, payload)
|
||||
.then(normalizePuzzleClearActionResponse);
|
||||
}
|
||||
|
||||
export async function getPuzzleClearWorkDetail(profileId: string) {
|
||||
const response = await requestJson<PuzzleClearWorkDetailResponse>(
|
||||
`${PUZZLE_CLEAR_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取拼消消作品详情失败',
|
||||
);
|
||||
return normalizePuzzleClearWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
export async function getPuzzleClearRuntimeWorkDetail(profileId: string) {
|
||||
const response = await requestJson<PuzzleClearWorkDetailResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取拼消消公开作品详情失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_READ_RETRY,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
return normalizePuzzleClearWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
export async function listPuzzleClearWorks() {
|
||||
return requestJson<PuzzleClearWorksResponse>(
|
||||
PUZZLE_CLEAR_WORKS_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取拼消消作品列表失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_READ_RETRY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function listPuzzleClearGallery() {
|
||||
return requestJson<PuzzleClearGalleryResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/gallery`,
|
||||
{ method: 'GET' },
|
||||
'读取拼消消广场失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_READ_RETRY,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPuzzleClearGalleryDetail(publicWorkCode: string) {
|
||||
const response = await requestJson<PuzzleClearGalleryDetailResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/gallery/${encodeURIComponent(publicWorkCode)}`,
|
||||
{ method: 'GET' },
|
||||
'读取拼消消广场详情失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_READ_RETRY,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
return normalizePuzzleClearWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
export async function publishPuzzleClearWork(profileId: string) {
|
||||
const response = await requestJson<PuzzleClearWorkMutationResponse>(
|
||||
`${PUZZLE_CLEAR_WORKS_API_BASE}/${encodeURIComponent(profileId)}/publish`,
|
||||
{ method: 'POST' },
|
||||
'发布拼消消作品失败',
|
||||
);
|
||||
return normalizePuzzleClearWorkMutationResponse(response);
|
||||
}
|
||||
|
||||
export async function startPuzzleClearRuntimeRun(
|
||||
profileId: string,
|
||||
options: PuzzleClearRuntimeRequestOptions = {},
|
||||
) {
|
||||
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||
return requestJson<PuzzleClearRunResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/runs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...buildRuntimeGuestHeaders(options),
|
||||
},
|
||||
body: JSON.stringify({ profileId }),
|
||||
},
|
||||
'启动拼消消运行态失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_WRITE_RETRY,
|
||||
...requestOptions,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPuzzleClearRuntimeRun(
|
||||
runId: string,
|
||||
options: PuzzleClearRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<PuzzleClearRunResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: buildRuntimeGuestHeaders(options),
|
||||
},
|
||||
'读取拼消消运行态失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_READ_RETRY,
|
||||
...buildRuntimeGuestAuthOptions(options),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function swapPuzzleClearCards(
|
||||
runId: string,
|
||||
payload: Omit<PuzzleClearSwapRequest, 'clientActionId'>,
|
||||
options: PuzzleClearRuntimeRequestOptions = {},
|
||||
) {
|
||||
const requestPayload: PuzzleClearSwapRequest = {
|
||||
...payload,
|
||||
clientActionId: `swap-${runId}-${Date.now()}`,
|
||||
};
|
||||
return requestJson<PuzzleClearRunResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/swap`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...buildRuntimeGuestHeaders(options),
|
||||
},
|
||||
body: JSON.stringify(requestPayload),
|
||||
},
|
||||
'交换拼消消卡片失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_WRITE_RETRY,
|
||||
...buildRuntimeGuestAuthOptions(options),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function retryPuzzleClearLevel(
|
||||
runId: string,
|
||||
options: PuzzleClearRuntimeRequestOptions = {},
|
||||
) {
|
||||
const requestPayload: PuzzleClearRetryLevelRequest = {
|
||||
clientActionId: `retry-${runId}-${Date.now()}`,
|
||||
};
|
||||
return requestJson<PuzzleClearRunResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/retry-level`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...buildRuntimeGuestHeaders(options),
|
||||
},
|
||||
body: JSON.stringify(requestPayload),
|
||||
},
|
||||
'重试拼消消关卡失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_WRITE_RETRY,
|
||||
...buildRuntimeGuestAuthOptions(options),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function advancePuzzleClearNextLevel(
|
||||
runId: string,
|
||||
options: PuzzleClearRuntimeRequestOptions = {},
|
||||
) {
|
||||
const requestPayload: PuzzleClearNextLevelRequest = {
|
||||
clientActionId: `next-${runId}-${Date.now()}`,
|
||||
};
|
||||
return requestJson<PuzzleClearRunResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/next-level`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...buildRuntimeGuestHeaders(options),
|
||||
},
|
||||
body: JSON.stringify(requestPayload),
|
||||
},
|
||||
'推进拼消消关卡失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_WRITE_RETRY,
|
||||
...buildRuntimeGuestAuthOptions(options),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function markPuzzleClearTimeUp(
|
||||
runId: string,
|
||||
options: PuzzleClearRuntimeRequestOptions = {},
|
||||
) {
|
||||
const requestPayload: PuzzleClearTimeUpRequest = {
|
||||
clientActionId: `time-up-${runId}-${Date.now()}`,
|
||||
};
|
||||
return requestJson<PuzzleClearRunResponse>(
|
||||
`${PUZZLE_CLEAR_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/time-up`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...buildRuntimeGuestHeaders(options),
|
||||
},
|
||||
body: JSON.stringify(requestPayload),
|
||||
},
|
||||
'同步拼消消倒计时失败',
|
||||
{
|
||||
retry: PUZZLE_CLEAR_RUNTIME_WRITE_RETRY,
|
||||
...buildRuntimeGuestAuthOptions(options),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const puzzleClearClient = {
|
||||
advanceNextLevel: advancePuzzleClearNextLevel,
|
||||
createSession: createPuzzleClearCreationSession,
|
||||
executeAction: executePuzzleClearCreationAction,
|
||||
getGalleryDetail: getPuzzleClearGalleryDetail,
|
||||
getRun: getPuzzleClearRuntimeRun,
|
||||
getSession: getPuzzleClearCreationSession,
|
||||
getRuntimeWorkDetail: getPuzzleClearRuntimeWorkDetail,
|
||||
getWorkDetail: getPuzzleClearWorkDetail,
|
||||
listGallery: listPuzzleClearGallery,
|
||||
listWorks: listPuzzleClearWorks,
|
||||
markTimeUp: markPuzzleClearTimeUp,
|
||||
publishWork: publishPuzzleClearWork,
|
||||
retryLevel: retryPuzzleClearLevel,
|
||||
startRun: startPuzzleClearRuntimeRun,
|
||||
swapCards: swapPuzzleClearCards,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -50,3 +50,16 @@ test('wooden fish list works uses creation works endpoint', async () => {
|
||||
'读取敲木鱼作品列表失败',
|
||||
);
|
||||
});
|
||||
|
||||
test('wooden fish delete work uses creation works endpoint', async () => {
|
||||
const { woodenFishClient } = await import('./woodenFishClient');
|
||||
requestJsonMock.mockResolvedValueOnce({ items: [] });
|
||||
|
||||
await woodenFishClient.deleteWork('wooden-fish-profile-1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/creation/wooden-fish/works/wooden-fish-profile-1',
|
||||
{ method: 'DELETE' },
|
||||
'删除敲木鱼作品失败',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -233,6 +233,14 @@ export async function publishWoodenFishWork(profileId: string) {
|
||||
return normalizeWoodenFishWorkMutationResponse(response);
|
||||
}
|
||||
|
||||
export async function deleteWoodenFishWork(profileId: string) {
|
||||
return requestJson<WoodenFishWorksResponse>(
|
||||
`${WOODEN_FISH_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'DELETE' },
|
||||
'删除敲木鱼作品失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function startWoodenFishRuntimeRun(
|
||||
profileId: string,
|
||||
options: WoodenFishRuntimeRequestOptions = {},
|
||||
@@ -317,6 +325,7 @@ export async function finishWoodenFishRun(
|
||||
export const woodenFishClient = {
|
||||
checkpointRun: checkpointWoodenFishRun,
|
||||
createSession: createWoodenFishCreationSession,
|
||||
deleteWork: deleteWoodenFishWork,
|
||||
executeAction: executeWoodenFishCreationAction,
|
||||
finishRun: finishWoodenFishRun,
|
||||
getGalleryDetail: getWoodenFishGalleryDetail,
|
||||
|
||||
Reference in New Issue
Block a user