fix: move puzzle next level source selection server side

This commit is contained in:
2026-04-25 15:55:05 +08:00
parent 9cb3c6a27e
commit 4e04679ba4
9 changed files with 418 additions and 168 deletions

View File

@@ -23,7 +23,6 @@ import type {
PuzzleAgentActionRequest,
PuzzleAgentOperationRecord,
} from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type { PuzzleGeneratedImageCandidate } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type {
PuzzleAgentSessionSnapshot,
SendPuzzleAgentMessageRequest,
@@ -68,13 +67,9 @@ import {
getPuzzleAgentSession,
streamPuzzleAgentMessage,
} from '../../services/puzzle-agent';
import { getPuzzleGalleryDetail } from '../../services/puzzle-gallery';
import { advanceLocalPuzzleNextLevel } from '../../services/puzzle-runtime';
import {
getPuzzleGalleryDetail,
listPuzzleGallery,
} from '../../services/puzzle-gallery';
import {
advanceLocalPuzzleLevel,
advanceLocalPuzzleLevelWithWork,
dragLocalPuzzlePiece,
startLocalPuzzleRun,
swapLocalPuzzlePieces,
@@ -114,53 +109,6 @@ type AgentResultPublishGateView = {
publishReady: boolean;
};
function buildPuzzleCandidateWorkSummary(
candidate: PuzzleGeneratedImageCandidate,
session: PuzzleAgentSessionSnapshot,
levelIndex: number,
): PuzzleWorkSummary {
const draft = session.draft;
const nowIso = new Date().toISOString();
return {
workId: `${session.sessionId}-${candidate.candidateId}-level-${levelIndex}-runtime-work`,
profileId: `${session.sessionId}-${candidate.candidateId}-level-${levelIndex}-runtime-profile`,
ownerUserId: 'local-runtime',
sourceSessionId: session.sessionId,
authorDisplayName: '当前草稿',
levelName: draft?.levelName
? `${draft.levelName} · 候选 ${levelIndex}`
: `候选拼图 ${levelIndex}`,
summary: draft?.summary ?? candidate.prompt,
themeTags: draft?.themeTags ?? [],
coverImageSrc: candidate.imageSrc,
coverAssetId: candidate.assetId,
publicationStatus: 'published',
updatedAt: nowIso,
publishedAt: nowIso,
playCount: 0,
publishReady: true,
};
}
function pickPuzzleCandidateForLevel(
candidates: PuzzleGeneratedImageCandidate[],
playedProfileIds: string[],
) {
return candidates.find(
(candidate) =>
candidate.imageSrc &&
!playedProfileIds.some((profileId) =>
profileId.includes(candidate.candidateId),
),
);
}
function pickFreshGeneratedPuzzleCandidate(
candidates: PuzzleGeneratedImageCandidate[],
) {
return candidates.find((candidate) => candidate.imageSrc);
}
type AgentResultBlockerView = {
code?: string;
message: string;
@@ -1226,104 +1174,16 @@ export function PlatformEntryFlowShellImpl({
}
setIsPuzzleBusy(true);
setIsPuzzleNextLevelGenerating(true);
setPuzzleError(null);
try {
const galleryResponse = await listPuzzleGallery();
setPuzzleWorks(galleryResponse.items);
const galleryNext = galleryResponse.items.find(
(item) =>
item.publicationStatus === 'published' &&
item.coverImageSrc &&
!puzzleRun.playedProfileIds.includes(item.profileId),
);
if (galleryNext) {
const { item } = await getPuzzleGalleryDetail(galleryNext.profileId);
setSelectedPuzzleDetail(item);
setPuzzleRun(advanceLocalPuzzleLevelWithWork(puzzleRun, item));
return;
}
const existingCandidate = pickPuzzleCandidateForLevel(
puzzleSession?.draft?.candidates ?? [],
puzzleRun.playedProfileIds,
);
if (existingCandidate && puzzleSession) {
setPuzzleRun(
advanceLocalPuzzleLevelWithWork(
puzzleRun,
buildPuzzleCandidateWorkSummary(
existingCandidate,
puzzleSession,
puzzleRun.currentLevelIndex + 1,
),
),
);
return;
}
if (!puzzleSession?.draft) {
const sourceSessionId = selectedPuzzleDetail?.sourceSessionId?.trim();
if (sourceSessionId) {
const { session } = await getPuzzleAgentSession(sourceSessionId);
setPuzzleSession(session);
if (session.draft) {
setIsPuzzleNextLevelGenerating(true);
const response = await executePuzzleAgentAction(session.sessionId, {
action: 'generate_puzzle_images',
candidateCount: 2,
});
setPuzzleOperation(response.operation);
setPuzzleSession(response.session);
const sourceSessionCandidate = pickPuzzleCandidateForLevel(
response.session.draft?.candidates ?? [],
puzzleRun.playedProfileIds,
);
if (sourceSessionCandidate) {
setPuzzleRun(
advanceLocalPuzzleLevelWithWork(
puzzleRun,
buildPuzzleCandidateWorkSummary(
sourceSessionCandidate,
response.session,
puzzleRun.currentLevelIndex + 1,
),
),
);
return;
}
}
}
throw new Error('当前拼图缺少可用于生成下一关的草稿会话。');
}
setIsPuzzleNextLevelGenerating(true);
const response = await executePuzzleAgentAction(puzzleSession.sessionId, {
action: 'generate_puzzle_images',
candidateCount: 2,
const { run } = await advanceLocalPuzzleNextLevel({
run: puzzleRun,
sourceSessionId:
selectedPuzzleDetail?.sourceSessionId ?? puzzleSession?.sessionId ?? null,
});
setPuzzleOperation(response.operation);
setPuzzleSession(response.session);
const generatedCandidate = pickPuzzleCandidateForLevel(
response.session.draft?.candidates ?? [],
puzzleRun.playedProfileIds,
);
if (generatedCandidate) {
setPuzzleRun(
advanceLocalPuzzleLevelWithWork(
puzzleRun,
buildPuzzleCandidateWorkSummary(
generatedCandidate,
response.session,
puzzleRun.currentLevelIndex + 1,
),
),
);
return;
}
setPuzzleRun(advanceLocalPuzzleLevel(puzzleRun));
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
} finally {

View File

@@ -1,4 +1,5 @@
export {
advanceLocalPuzzleNextLevel,
advancePuzzleNextLevel,
dragPuzzlePieceOrGroup,
getPuzzleRun,

View File

@@ -112,13 +112,13 @@ function buildLocalNextProfileId(entryProfileId: string, levelIndex: number) {
return `${entryProfileId}::local-level-${levelIndex}`;
}
// 第一版单机玩法没有后端推荐池,本地沿用当前作品图片生成可推进的临时关卡名。
// 第一版单机兜底没有后端推荐池时,才沿用当前作品图片生成可推进的临时关卡名。
function buildLocalLevelName(previousLevelName: string, levelIndex: number) {
return `${previousLevelName.replace(/ · 第 \d+ 关$/, '')} · 第 ${levelIndex}`;
}
// 本地运行态只保证单次游玩闭环:通关后立即重建下一关棋盘,不写回后端。
function buildNextLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
// 本地兜底只保证单次游玩闭环:通关后立即重建下一关棋盘,不写回后端。
function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
const currentLevel = run.currentLevel;
if (!currentLevel || currentLevel.status !== 'cleared') {
return run;
@@ -240,5 +240,5 @@ export function dragLocalPuzzlePiece(
}
export function advanceLocalPuzzleLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
return buildNextLocalLevel(run);
return buildFallbackLocalLevel(run);
}

View File

@@ -1,4 +1,5 @@
import type {
AdvanceLocalPuzzleNextLevelRequest,
DragPuzzlePieceRequest,
PuzzleRunResponse,
StartPuzzleRunRequest,
@@ -111,7 +112,28 @@ export async function advancePuzzleNextLevel(runId: string) {
);
}
/**
* 单机运行态进入下一关,图片来源选择全部由后端裁决。
*/
export async function advanceLocalPuzzleNextLevel(
payload: AdvanceLocalPuzzleNextLevelRequest,
) {
return requestJson<PuzzleRunResponse>(
`${PUZZLE_RUNTIME_API_BASE}/local-next-level`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'进入下一关失败',
{
retry: PUZZLE_RUNTIME_WRITE_RETRY,
},
);
}
export const puzzleRuntimeClient = {
advanceLocalNextLevel: advanceLocalPuzzleNextLevel,
advanceNextLevel: advancePuzzleNextLevel,
drag: dragPuzzlePieceOrGroup,
getRun: getPuzzleRun,