This commit is contained in:
2026-05-01 00:33:39 +08:00
parent 61969c5116
commit fe02603ba1
68 changed files with 4586 additions and 748 deletions

View File

@@ -31,6 +31,7 @@ import type {
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type {
PuzzleRunSnapshot,
PuzzleRuntimePropKind,
SubmitPuzzleLeaderboardRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
@@ -39,6 +40,7 @@ import type {
CustomWorldLibraryEntry,
ProfilePlayedWorkSummary,
ProfilePlayStatsResponse,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import {
@@ -111,8 +113,10 @@ import {
import {
applyLocalPuzzleFreezeTime,
dragLocalPuzzlePiece,
extendLocalPuzzleTime,
isLocalPuzzleRun,
refreshLocalPuzzleTimer,
restartLocalPuzzleLevel,
setLocalPuzzlePaused,
startLocalPuzzleRun,
submitLocalPuzzleLeaderboard,
@@ -128,7 +132,10 @@ import {
recordRpgEntryWorldGalleryPlay,
remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import { getRpgProfilePlayStats } from '../../services/rpg-entry/rpgProfileClient';
import {
getRpgProfilePlayStats,
resumeRpgProfileSaveArchive,
} from '../../services/rpg-entry/rpgProfileClient';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import {
@@ -178,6 +185,13 @@ type PuzzleRuntimeReturnStage =
type BigFishRuntimeReturnStage = 'big-fish-result' | 'work-detail' | 'platform';
type PuzzleSaveArchiveState = {
runtimeKind?: unknown;
entryProfileId?: unknown;
currentProfileId?: unknown;
currentLevelId?: unknown;
};
type AgentResultBlockerView = {
code?: string;
message: string;
@@ -210,7 +224,10 @@ function isSamePlatformPublicGalleryEntry(
left: PlatformPublicGalleryCard,
right: PlatformPublicGalleryCard,
) {
return getPlatformPublicGalleryEntryKey(left) === getPlatformPublicGalleryEntryKey(right);
return (
getPlatformPublicGalleryEntryKey(left) ===
getPlatformPublicGalleryEntryKey(right)
);
}
function mergePlatformPublicGalleryEntries(
@@ -272,6 +289,17 @@ function mapPublicWorkDetailToPuzzleWork(
remixCount: entry.remixCount ?? 0,
likeCount: entry.likeCount ?? 0,
publishReady: true,
levels:
entry.coverSlides?.map((slide, index) => ({
levelId: slide.id || `puzzle-level-${index + 1}`,
levelName: slide.label,
pictureDescription: entry.summaryText,
candidates: [],
selectedCandidateId: null,
coverImageSrc: slide.imageSrc,
coverAssetId: null,
generationStatus: 'ready' as const,
})) ?? [],
};
}
@@ -322,7 +350,9 @@ function mergeBigFishWorkSummary(
current: BigFishWorkSummary,
updated: BigFishWorkSummary,
): BigFishWorkSummary {
return current.sourceSessionId === updated.sourceSessionId ? updated : current;
return current.sourceSessionId === updated.sourceSessionId
? updated
: current;
}
async function resolvePublicWorkAuthorSummary(
@@ -562,6 +592,22 @@ function isPuzzleFormOnlyDraft(session: PuzzleAgentSessionSnapshot | null) {
);
}
function isEmptyPuzzleFormOnlyDraft(
session: PuzzleAgentSessionSnapshot | null,
) {
if (!isPuzzleFormOnlyDraft(session)) {
return false;
}
const formDraft = session?.draft?.formDraft;
return !(
session?.seedText?.trim() ||
formDraft?.workTitle?.trim() ||
formDraft?.workDescription?.trim() ||
formDraft?.pictureDescription?.trim()
);
}
const CustomWorldGenerationView = lazy(async () => {
const module = await import('../CustomWorldGenerationView');
return {
@@ -665,11 +711,20 @@ function mergePuzzleServiceRuntimeState(
? serviceLevel.leaderboardEntries
: serviceRun.leaderboardEntries;
return {
...currentRun,
leaderboardEntries,
currentLevel: {
return {
...currentRun,
recommendedNextProfileId: serviceRun.recommendedNextProfileId,
nextLevelMode: serviceRun.nextLevelMode,
nextLevelProfileId: serviceRun.nextLevelProfileId,
nextLevelId: serviceRun.nextLevelId,
recommendedNextWorks: serviceRun.recommendedNextWorks,
leaderboardEntries,
currentLevel: {
...currentRun.currentLevel,
status: serviceLevel.status,
startedAtMs: serviceLevel.startedAtMs,
clearedAtMs: serviceLevel.clearedAtMs,
elapsedMs: serviceLevel.elapsedMs,
timeLimitMs: serviceLevel.timeLimitMs,
remainingMs: serviceLevel.remainingMs,
pausedAccumulatedMs: serviceLevel.pausedAccumulatedMs,
@@ -1338,7 +1393,10 @@ export function PlatformEntryFlowShellImpl({
const createPuzzleDraftFromForm = useCallback(
async (payload: CreatePuzzleAgentSessionRequest) => {
setPuzzleFormDraftPayload(payload);
const nextSession = puzzleFlow.session ?? (await puzzleFlow.openWorkspace(payload));
const nextSession =
puzzleFlow.session && !isEmptyPuzzleFormOnlyDraft(puzzleFlow.session)
? puzzleFlow.session
: await puzzleFlow.openWorkspace(payload);
if (!nextSession) {
return;
}
@@ -1504,6 +1562,35 @@ export function PlatformEntryFlowShellImpl({
const executePuzzleAction = puzzleFlow.executeAction;
const retryPuzzleDraftGeneration = useCallback(() => {
if (puzzleFormDraftPayload) {
void createPuzzleDraftFromForm(puzzleFormDraftPayload);
return;
}
void executePuzzleAction(
buildPuzzleCompileActionFromFormPayload(puzzleFormDraftPayload),
);
}, [createPuzzleDraftFromForm, executePuzzleAction, puzzleFormDraftPayload]);
const executePuzzleWorkspaceAction = useCallback(
(payload: PuzzleAgentActionRequest) => {
if (
payload.action === 'compile_puzzle_draft' &&
isEmptyPuzzleFormOnlyDraft(puzzleFlow.session)
) {
const formPayload = buildPuzzleFormPayloadFromAction(payload);
if (formPayload) {
void createPuzzleDraftFromForm(formPayload);
return;
}
}
void executePuzzleAction(payload);
},
[createPuzzleDraftFromForm, executePuzzleAction, puzzleFlow.session],
);
useEffect(() => {
if (selectionStage === 'big-fish-result' && !bigFishSession?.draft) {
setSelectionStage(
@@ -1655,6 +1742,7 @@ export function PlatformEntryFlowShellImpl({
remixCount: 0,
likeCount: 0,
publishReady: Boolean(puzzleSession?.resultPreview?.publishReady),
levels: draft.levels,
} satisfies PuzzleWorkSummary;
},
[
@@ -1782,7 +1870,9 @@ export function PlatformEntryFlowShellImpl({
paused,
});
setPuzzleRun((currentRun) =>
currentRun ? mergePuzzleServiceRuntimeState(currentRun, run) : currentRun,
currentRun
? mergePuzzleServiceRuntimeState(currentRun, run)
: currentRun,
);
void platformBootstrap.refreshProfileDashboard();
} catch (error) {
@@ -1812,7 +1902,9 @@ export function PlatformEntryFlowShellImpl({
try {
const { run } = await getPuzzleRun(puzzleRun.runId);
setPuzzleRun((currentRun) =>
currentRun ? mergePuzzleServiceRuntimeState(currentRun, run) : currentRun,
currentRun
? mergePuzzleServiceRuntimeState(currentRun, run)
: currentRun,
);
} catch (error) {
setPuzzleError(
@@ -1822,11 +1914,13 @@ export function PlatformEntryFlowShellImpl({
}, [puzzleRun, resolvePuzzleErrorMessage, setPuzzleError]);
const usePuzzleProp = useCallback(
async (propKind: 'hint' | 'reference' | 'freezeTime') => {
if (
!puzzleRun?.currentLevel ||
puzzleRun.currentLevel.status !== 'playing'
) {
async (propKind: PuzzleRuntimePropKind) => {
if (!puzzleRun?.currentLevel) {
return null;
}
const expectedStatus =
propKind === 'extendTime' ? 'failed' : 'playing';
if (puzzleRun.currentLevel.status !== expectedStatus) {
return null;
}
@@ -1836,7 +1930,9 @@ export function PlatformEntryFlowShellImpl({
return null;
}
const nextRun =
propKind === 'freezeTime'
propKind === 'extendTime'
? extendLocalPuzzleTime(currentRun)
: propKind === 'freezeTime'
? applyLocalPuzzleFreezeTime(currentRun)
: setLocalPuzzlePaused(currentRun, propKind === 'reference');
puzzleRunRef.current = nextRun;
@@ -1859,6 +1955,92 @@ export function PlatformEntryFlowShellImpl({
[platformBootstrap, puzzleRun],
);
const restartPuzzleCurrentLevel = useCallback(async () => {
const currentLevel = puzzleRun?.currentLevel ?? null;
if (!puzzleRun || !currentLevel || isPuzzleBusy) {
return;
}
setPuzzleError(null);
if (isLocalPuzzleRun(puzzleRun)) {
const nextRun = restartLocalPuzzleLevel(puzzleRunRef.current ?? puzzleRun);
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
return;
}
await startPuzzleRunFromProfile(
currentLevel.profileId,
puzzleRuntimeReturnStage,
selectedPuzzleDetail?.profileId === currentLevel.profileId
? selectedPuzzleDetail
: undefined,
false,
currentLevel.levelId ?? null,
);
}, [
isPuzzleBusy,
puzzleRun,
puzzleRuntimeReturnStage,
selectedPuzzleDetail,
setPuzzleError,
startPuzzleRunFromProfile,
]);
const resumePuzzleSaveArchive = useCallback(
async (entry: ProfileSaveArchiveSummary) => {
if (isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
platformBootstrap.setSaveError(null);
try {
const resumedArchive = await resumeRpgProfileSaveArchive(
entry.worldKey,
);
platformBootstrap.setSaveEntries((currentEntries) =>
currentEntries.map((currentEntry) =>
currentEntry.worldKey === resumedArchive.entry.worldKey
? resumedArchive.entry
: currentEntry,
),
);
const gameState = resumedArchive.snapshot
.gameState as PuzzleSaveArchiveState;
const profileId =
typeof gameState.currentProfileId === 'string' &&
gameState.currentProfileId.trim()
? gameState.currentProfileId
: typeof gameState.entryProfileId === 'string' &&
gameState.entryProfileId.trim()
? gameState.entryProfileId
: (entry.profileId ?? entry.worldKey.replace(/^puzzle:/u, ''));
const levelId =
typeof gameState.currentLevelId === 'string' &&
gameState.currentLevelId.trim()
? gameState.currentLevelId
: null;
await startPuzzleRunFromProfile(profileId, 'platform', undefined, false, levelId);
} catch (error) {
platformBootstrap.setSaveError(
resolvePuzzleErrorMessage(error, '恢复拼图存档失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
platformBootstrap,
resolvePuzzleErrorMessage,
setPuzzleError,
startPuzzleRunFromProfile,
],
);
useEffect(() => {
const currentLevel = puzzleRun?.currentLevel ?? null;
if (!puzzleRun || !currentLevel || currentLevel.status !== 'cleared') {
@@ -1916,7 +2098,10 @@ export function PlatformEntryFlowShellImpl({
setPuzzleError,
]);
const advancePuzzleLevel = useCallback(async () => {
const advancePuzzleLevel = useCallback(async (target?: {
profileId?: string;
levelId?: string | null;
}) => {
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
return;
}
@@ -1931,6 +2116,21 @@ export function PlatformEntryFlowShellImpl({
setPuzzleError(null);
try {
const targetProfileId = target?.profileId?.trim();
if (
targetProfileId &&
targetProfileId !== currentLevel.profileId &&
puzzleRun.nextLevelMode === 'similarWorks'
) {
await startPuzzleRunFromProfile(
targetProfileId,
'puzzle-gallery-detail',
undefined,
false,
null,
);
return;
}
const { run } = isLocalPuzzleRun(puzzleRun)
? await advanceLocalPuzzleNextLevel({
run: puzzleRun,
@@ -1954,6 +2154,7 @@ export function PlatformEntryFlowShellImpl({
puzzleSession,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
startPuzzleRunFromProfile,
]);
const leaveAgentWorkspace = useCallback(() => {
@@ -2262,24 +2463,27 @@ export function PlatformEntryFlowShellImpl({
return;
}
setBigFishGalleryEntries((current) =>
current.map((item) => mergeBigFishWorkSummary(item, updatedWork)),
current.map((item) =>
mergeBigFishWorkSummary(item, updatedWork),
),
);
setBigFishWorks((current) =>
current.map((item) => mergeBigFishWorkSummary(item, updatedWork)),
current.map((item) =>
mergeBigFishWorkSummary(item, updatedWork),
),
);
syncUpdatedPublicWorkDetail(
mapBigFishWorkToPublicWorkDetail(updatedWork),
);
setBigFishRuntimeWork((current) =>
current ? mergeBigFishWorkSummary(current, updatedWork) : current,
current
? mergeBigFishWorkSummary(current, updatedWork)
: current,
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveBigFishErrorMessage(
error,
'点赞大鱼吃小鱼作品失败。',
),
resolveBigFishErrorMessage(error, '点赞大鱼吃小鱼作品失败。'),
);
})
.finally(() => {
@@ -2293,13 +2497,19 @@ export function PlatformEntryFlowShellImpl({
.then((response) => {
const updatedWork = response.item;
setPuzzleGalleryEntries((current) =>
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
);
setPuzzleWorks((current) =>
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
);
setSelectedPuzzleDetail((current) =>
current ? mergePuzzleWorkSummary(current, updatedWork) : current,
current
? mergePuzzleWorkSummary(current, updatedWork)
: current,
);
syncUpdatedPublicWorkDetail(
mapPuzzleWorkToPublicWorkDetail(updatedWork),
@@ -2319,13 +2529,13 @@ export function PlatformEntryFlowShellImpl({
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((updatedEntry) => {
setSelectedDetailEntry((current) =>
current?.profileId === updatedEntry.profileId ? updatedEntry : current,
current?.profileId === updatedEntry.profileId
? updatedEntry
: current,
);
platformBootstrap.setPublishedGalleryEntries((current) =>
current.map((item) =>
item.profileId === updatedEntry.profileId
? mapRpgGalleryCardToPublicWorkDetail(updatedEntry)
: item,
item.profileId === updatedEntry.profileId ? updatedEntry : item,
),
);
syncUpdatedPublicWorkDetail(
@@ -3186,6 +3396,13 @@ export function PlatformEntryFlowShellImpl({
createTabContent={creationHubContent}
onContinueGame={handleContinueGame}
onResumeSave={(entry) => {
if (
(entry.worldType ?? '').toLowerCase() === 'puzzle' ||
entry.worldKey.startsWith('puzzle:')
) {
void resumePuzzleSaveArchive(entry);
return;
}
void platformBootstrap.handleResumeSaveEntry(entry);
}}
onOpenCreateWorld={openCreationTypePicker}
@@ -3286,7 +3503,9 @@ export function PlatformEntryFlowShellImpl({
<PlatformWorkDetailView
entry={mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)}
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
authorDisplayName={selectedPublicWorkAuthor?.displayName ?? null}
authorDisplayName={
selectedPublicWorkAuthor?.displayName ?? null
}
isBusy={detailNavigation.isMutatingDetail}
error={detailNavigation.detailError}
onBack={() => {
@@ -3568,7 +3787,7 @@ export function PlatformEntryFlowShellImpl({
void submitPuzzleMessage(payload);
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
executePuzzleWorkspaceAction(payload);
}}
initialFormPayload={puzzleFormDraftPayload}
onCreateFromForm={(payload) => {
@@ -3610,13 +3829,7 @@ export function PlatformEntryFlowShellImpl({
onEditSetting={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onRetry={() => {
void executePuzzleAction(
buildPuzzleCompileActionFromFormPayload(
puzzleFormDraftPayload,
),
);
}}
onRetry={retryPuzzleDraftGeneration}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
@@ -3635,35 +3848,35 @@ export function PlatformEntryFlowShellImpl({
{selectionStage === 'puzzle-result' &&
puzzleSession?.draft &&
!isPuzzleFormOnlyDraft(puzzleSession) && (
<motion.div
key="puzzle-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图结果..." />}
<motion.div
key="puzzle-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<PuzzleResultView
session={puzzleSession}
profileId={
puzzleSession.publishedProfileId ??
buildPuzzleResultProfileId(puzzleSession.sessionId)
}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
onStartTestRun={startPuzzleTestRunFromDraft}
/>
</Suspense>
</motion.div>
)}
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图结果..." />}
>
<PuzzleResultView
session={puzzleSession}
profileId={
puzzleSession.publishedProfileId ??
buildPuzzleResultProfileId(puzzleSession.sessionId)
}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
onStartTestRun={startPuzzleTestRunFromDraft}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-gallery-detail' && selectedPuzzleDetail && (
<motion.div
@@ -3737,8 +3950,11 @@ export function PlatformEntryFlowShellImpl({
onDragPiece={(payload) => {
void dragPuzzlePiece(payload);
}}
onAdvanceNextLevel={() => {
void advancePuzzleLevel();
onAdvanceNextLevel={(target) => {
void advancePuzzleLevel(target);
}}
onRestartLevel={() => {
void restartPuzzleCurrentLevel();
}}
onPauseChange={setPuzzleRuntimePaused}
onUseProp={usePuzzleProp}