merge: sync origin master into puzzle runtime restore

This commit is contained in:
2026-05-25 22:57:07 +08:00
70 changed files with 4096 additions and 2121 deletions

View File

@@ -115,8 +115,10 @@ import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFound
import {
ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS,
getStoredAccessToken,
} from '../../services/apiClient';
import {
ensureRuntimeGuestToken,
getPublicAuthUserByCode,
getPublicAuthUserById,
} from '../../services/authService';
@@ -127,6 +129,7 @@ import {
publishBarkBattleWork,
updateBarkBattleDraftConfig,
} from '../../services/bark-battle-creation';
import { startBarkBattleRun } from '../../services/bark-battle-runtime';
import {
createBigFishCreationSession,
executeBigFishCreationAction,
@@ -190,9 +193,10 @@ import {
type JumpHopRunResponse,
type JumpHopSessionResponse,
type JumpHopSessionSnapshotResponse,
type JumpHopWorkProfileResponse,
type JumpHopWorkspaceCreateRequest,
JumpHopWorkProfileResponse,
JumpHopWorkspaceCreateRequest,
} from '../../services/jump-hop/jumpHopClient';
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import { match3dCreationClient } from '../../services/match3d-creation';
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
import {
@@ -423,6 +427,7 @@ import { PlatformFeedbackView } from './PlatformFeedbackView';
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
import { usePlatformDesktopLayout } from './platformEntryResponsive';
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
@@ -565,8 +570,34 @@ const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
]);
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
BACKGROUND_AUTH_REQUEST_OPTIONS;
const PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS =
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS =
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
async function buildRecommendRuntimeGuestOptions() {
const { token } = await ensureRuntimeGuestToken();
return {
...RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
runtimeGuestToken: token,
};
}
function shouldUseRecommendRuntimeGuestAuth(
authUi: { user?: { id?: string } | null } | null | undefined,
) {
return !authUi?.user?.id?.trim() && !getStoredAccessToken();
}
async function buildRecommendRuntimeAuthOptions(
authUi: { user?: { id?: string } | null } | null | undefined,
embedded?: boolean,
) {
if (!embedded) {
return {};
}
if (shouldUseRecommendRuntimeGuestAuth(authUi)) {
return buildRecommendRuntimeGuestOptions();
}
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
}
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
@@ -2190,7 +2221,7 @@ function hasRecoverableGeneratedPuzzleDraft(
);
}
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem) {
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem): string[] {
switch (item.source.kind) {
case 'rpg':
return collectDraftNoticeKeys('rpg', [
@@ -2219,6 +2250,13 @@ function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem) {
item.source.item.profileId,
item.source.item.sourceSessionId,
]);
case 'jump-hop':
return collectDraftNoticeKeys('jump-hop', [
item.id,
item.source.item.workId,
item.source.item.profileId,
item.source.item.sourceSessionId,
]);
case 'puzzle':
return collectDraftNoticeKeys('puzzle', [
item.id,
@@ -2304,6 +2342,39 @@ function buildPendingBigFishWorks(
}));
}
function buildPendingJumpHopWorks(
pending: Record<string, PendingDraftShelfState> | undefined,
existingItems: readonly JumpHopWorkSummaryResponse[],
): JumpHopWorkSummaryResponse[] {
if (!pending) {
return [];
}
return Object.entries(pending)
.filter(([sessionId]) =>
existingItems.every((item) => item.sourceSessionId !== sessionId),
)
.map(([sessionId, state]) => ({
runtimeKind: 'jump-hop',
workId: `jump-hop-work-${sessionId}`,
profileId: `jump-hop-profile-${sessionId}`,
ownerUserId: '',
sourceSessionId: sessionId,
workTitle: '跳一跳草稿',
workDescription: '正在生成跳一跳玩法草稿。',
themeTags: [],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: state.updatedAt,
publishedAt: null,
publishReady: false,
generationStatus: state.status === 'generating' ? 'generating' : 'ready',
}));
}
function buildPendingMatch3DWorks(
pending: Record<string, PendingDraftShelfState> | undefined,
existingItems: readonly Match3DWorkSummary[],
@@ -2913,7 +2984,12 @@ export function PlatformEntryFlowShellImpl({
authUi?.platformTheme === 'dark'
? 'platform-theme--dark'
: 'platform-theme--light';
const isDesktopLayout = usePlatformDesktopLayout();
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
const [draftGenerationPointNotice, setDraftGenerationPointNotice] = useState<{
title: string;
message: string;
} | null>(null);
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
const [selectedPublicWorkDetail, setSelectedPublicWorkDetail] =
@@ -2974,6 +3050,9 @@ export function PlatformEntryFlowShellImpl({
const [jumpHopGalleryEntries, setJumpHopGalleryEntries] = useState<
JumpHopGalleryCardResponse[]
>([]);
const [jumpHopWorks, setJumpHopWorks] = useState<
JumpHopWorkSummaryResponse[]
>([]);
const [jumpHopRuntimeReturnStage, setJumpHopRuntimeReturnStage] =
useState<JumpHopRuntimeReturnStage>('jump-hop-result');
const [jumpHopGenerationState, setJumpHopGenerationState] =
@@ -3190,6 +3269,10 @@ export function PlatformEntryFlowShellImpl({
creationEntryTypes,
'big-fish',
);
const isJumpHopCreationVisible = isPlatformCreationTypeVisible(
creationEntryTypes,
'jump-hop',
);
const isSquareHoleCreationVisible = isPlatformCreationTypeVisible(
creationEntryTypes,
'square-hole',
@@ -3497,7 +3580,7 @@ export function PlatformEntryFlowShellImpl({
[draftGenerationNotices],
);
const ensureEnoughDraftGenerationPointsFromServer = useCallback(
async (pointsCost: number, setError: (message: string | null) => void) => {
async (pointsCost: number) => {
try {
const latestDashboard = await getPlatformProfileDashboard(
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
@@ -3505,25 +3588,26 @@ export function PlatformEntryFlowShellImpl({
platformBootstrap.setProfileDashboard(latestDashboard);
const walletBalance = resolveProfileWalletBalance(latestDashboard);
if (walletBalance >= pointsCost) {
setDraftGenerationPointNotice(null);
return true;
}
setError(
`泥点不足,本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
setDraftGenerationPointNotice(
{
title: '泥点不足',
message: `本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
},
);
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
return false;
} catch {
setError('读取泥点余额失败,请稍后重试。');
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
setDraftGenerationPointNotice({
title: '读取泥点余额失败',
message: '请稍后重试。',
});
return false;
}
},
[enterCreateTab, platformBootstrap, setSelectionStage],
[platformBootstrap],
);
const resolveBigFishErrorMessage = useCallback(
@@ -3546,6 +3630,11 @@ export function PlatformEntryFlowShellImpl({
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolveBarkBattleErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const refreshBigFishShelf = useCallback(async () => {
setIsBigFishLoadingLibrary(true);
@@ -3645,6 +3734,22 @@ export function PlatformEntryFlowShellImpl({
}
}, []);
const refreshJumpHopShelf = useCallback(async () => {
if (!isJumpHopCreationVisible) {
setJumpHopWorks([]);
return [];
}
try {
const worksResponse = await jumpHopClient.listWorks();
setJumpHopWorks(worksResponse.items);
return worksResponse.items;
} catch {
setJumpHopWorks([]);
return [];
}
}, [isJumpHopCreationVisible]);
const refreshWoodenFishGallery = useCallback(async () => {
try {
const galleryResponse = await woodenFishClient.listGallery();
@@ -3854,6 +3959,22 @@ export function PlatformEntryFlowShellImpl({
selectionStage,
]);
useEffect(() => {
if (!platformBootstrap.canReadProtectedData) {
setJumpHopWorks([]);
return;
}
if (platformBootstrap.platformTab === 'create' || selectionStage === 'platform') {
void refreshJumpHopShelf();
}
}, [
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshJumpHopShelf,
selectionStage,
]);
const sessionController = useRpgCreationSessionController({
userId: authUi?.user?.id,
openLoginModal: authUi?.openLoginModal,
@@ -4201,6 +4322,16 @@ export function PlatformEntryFlowShellImpl({
],
[bigFishWorks, pendingDraftShelfItems],
);
const jumpHopShelfItems = useMemo(
() => [
...buildPendingJumpHopWorks(
pendingDraftShelfItems['jump-hop'],
jumpHopWorks,
),
...jumpHopWorks,
],
[jumpHopWorks, pendingDraftShelfItems],
);
const match3dShelfItems = useMemo(
() => [
...buildPendingMatch3DWorks(pendingDraftShelfItems.match3d, match3dWorks),
@@ -4276,6 +4407,13 @@ export function PlatformEntryFlowShellImpl({
...bigFishShelfItems.flatMap((item) =>
collectDraftNoticeKeys('big-fish', [item.workId, item.sourceSessionId]),
),
...jumpHopShelfItems.flatMap((item) =>
collectDraftNoticeKeys('jump-hop', [
item.workId,
item.profileId,
item.sourceSessionId,
]),
),
...match3dShelfItems.flatMap((item) =>
collectDraftNoticeKeys('match3d', [
item.workId,
@@ -4318,6 +4456,7 @@ export function PlatformEntryFlowShellImpl({
babyObjectMatchDrafts,
barkBattleShelfItems,
bigFishShelfItems,
jumpHopShelfItems,
creationHubItems,
isSquareHoleCreationVisible,
match3dShelfItems,
@@ -5440,30 +5579,27 @@ export function PlatformEntryFlowShellImpl({
setPuzzleError(null);
return ensureEnoughDraftGenerationPointsFromServer(
PUZZLE_DRAFT_GENERATION_POINT_COST,
(message) => {
setPuzzleCreationError(message);
setPuzzleError(message);
},
);
}, [
ensureEnoughDraftGenerationPointsFromServer,
setPuzzleCreationError,
setPuzzleError,
]);
const preflightMatch3DDraftGeneration = useCallback(async () => {
setMatch3DError(null);
return ensureEnoughDraftGenerationPointsFromServer(
MATCH3D_DRAFT_GENERATION_POINT_COST,
setMatch3DError,
);
}, [ensureEnoughDraftGenerationPointsFromServer, setMatch3DError]);
}, [ensureEnoughDraftGenerationPointsFromServer]);
const preflightBarkBattleDraftGeneration = useCallback(async () => {
setBarkBattleError(null);
return ensureEnoughDraftGenerationPointsFromServer(
BARK_BATTLE_DRAFT_GENERATION_POINT_COST,
setBarkBattleError,
);
}, [ensureEnoughDraftGenerationPointsFromServer]);
const draftGenerationPointNoticeDescription = draftGenerationPointNotice
? draftGenerationPointNotice.title === '读取泥点余额失败'
? '当前表单不会丢失,关闭后可继续编辑,稍后再试。'
: '当前表单不会丢失,关闭后可继续编辑或补足泥点再继续。'
: undefined;
const recoverCompletedPuzzleDraftGeneration = useCallback(
async ({
sessionId,
@@ -7575,11 +7711,15 @@ export function PlatformEntryFlowShellImpl({
profileId: targetProfileId,
mode: 'play' as const,
};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const { run } = options.embedded
? await startVisualNovelRun(
targetProfileId,
startRunPayload,
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
runtimeGuestOptions,
)
: await startVisualNovelRun(targetProfileId, startRunPayload);
setVisualNovelWork(workDetail);
@@ -7605,6 +7745,7 @@ export function PlatformEntryFlowShellImpl({
}
},
[
authUi,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
@@ -7626,9 +7767,14 @@ export function PlatformEntryFlowShellImpl({
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const runtimeGuestOptions =
activeRecommendRuntimeKind === 'visual-novel'
? await buildRecommendRuntimeAuthOptions(authUi, true)
: {};
const nextRun = await streamVisualNovelRuntimeAction(
visualNovelRun.runId,
payload,
runtimeGuestOptions,
);
setVisualNovelRun(nextRun);
} catch (error) {
@@ -7640,6 +7786,8 @@ export function PlatformEntryFlowShellImpl({
}
},
[
activeRecommendRuntimeKind,
authUi,
isVisualNovelBusy,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
@@ -7859,6 +8007,22 @@ export function PlatformEntryFlowShellImpl({
}),
);
setJumpHopGenerationState(readyState);
if (response.work) {
setJumpHopWorks((current) =>
[response.work!.summary, ...current.filter((item) => item.workId !== response.work!.summary.workId)],
);
markPendingDraftReady('jump-hop', created.session.sessionId, false);
markDraftReady(
'jump-hop',
[
created.session.sessionId,
response.work.summary.workId,
response.work.summary.profileId,
],
false,
);
void refreshJumpHopShelf().catch(() => undefined);
}
setSelectionStage('jump-hop-result');
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
@@ -7985,6 +8149,10 @@ export function PlatformEntryFlowShellImpl({
try {
const response = await jumpHopClient.publishWork(profileId);
setJumpHopWork(response.item);
setJumpHopWorks((current) =>
[response.item.summary, ...current.filter((item) => item.workId !== response.item.summary.workId)],
);
void refreshJumpHopShelf().catch(() => undefined);
openPublishShareModal({
title: response.item.summary.workTitle || '跳一跳',
publicWorkCode: buildJumpHopPublicWorkCode(
@@ -8049,12 +8217,13 @@ export function PlatformEntryFlowShellImpl({
setJumpHopError(null);
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const [detail, runResponse] = await Promise.all([
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
jumpHopClient.startRun(
normalizedProfileId,
options.embedded ? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS : {},
),
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
]);
if (detail?.item) {
setJumpHopWork(detail.item);
@@ -8079,7 +8248,7 @@ export function PlatformEntryFlowShellImpl({
setIsJumpHopBusy(false);
}
},
[setSelectionStage],
[authUi, setSelectionStage],
);
const restartJumpHopRuntimeRun = useCallback(async () => {
@@ -8413,9 +8582,15 @@ export function PlatformEntryFlowShellImpl({
setWoodenFishError(null);
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const [detail, runResponse] = await Promise.all([
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
woodenFishClient.startRun(normalizedProfileId),
options.embedded
? woodenFishClient.startRun(normalizedProfileId, runtimeGuestOptions)
: woodenFishClient.startRun(normalizedProfileId),
]);
if (detail?.item) {
setWoodenFishWork(detail.item);
@@ -8440,7 +8615,7 @@ export function PlatformEntryFlowShellImpl({
setIsWoodenFishBusy(false);
}
},
[setSelectionStage],
[authUi, setSelectionStage],
);
const checkpointWoodenFishRuntimeRun = useCallback(
@@ -8858,16 +9033,23 @@ export function PlatformEntryFlowShellImpl({
profileId: item.profileId,
levelId: levelId ?? null,
};
const authMode = options.embedded
? 'isolated'
: (options.authMode ?? 'default');
const canUseRuntimeGuestAuth =
options.embedded || options.authMode === 'isolated';
const useRuntimeGuestAuth =
canUseRuntimeGuestAuth && shouldUseRecommendRuntimeGuestAuth(authUi);
const runtimeGuestOptions = useRuntimeGuestAuth
? await buildRecommendRuntimeGuestOptions()
: {};
const authMode = useRuntimeGuestAuth ? 'isolated' : 'default';
const runtimeAuthOptions = useRuntimeGuestAuth
? runtimeGuestOptions
: canUseRuntimeGuestAuth
? RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS
: {};
const { run } =
authMode === 'isolated'
? await startPuzzleRun(
startRunPayload,
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
)
: await startPuzzleRun(startRunPayload);
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
: await startPuzzleRun(startRunPayload, runtimeAuthOptions);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode(authMode);
@@ -8909,6 +9091,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isPuzzleBusy,
authUi,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
@@ -8961,10 +9144,12 @@ export function PlatformEntryFlowShellImpl({
runtimeProfile.generatedBackgroundAsset,
{ expireSeconds: 300 },
);
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runtimeOptions = {
...(options.embedded
? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS
: {}),
...runtimeGuestOptions,
...(typeof options.itemTypeCountOverride === 'number'
? { itemTypeCountOverride: options.itemTypeCountOverride }
: {}),
@@ -9009,6 +9194,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isMatch3DBusy,
authUi,
match3dFlow,
match3dRuntimeAdapter,
resolveMatch3DErrorMessage,
@@ -9032,11 +9218,12 @@ export function PlatformEntryFlowShellImpl({
setSquareHoleError(null);
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const { run } = options.embedded
? await startSquareHoleRun(
profile.profileId,
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
: await startSquareHoleRun(profile.profileId);
setSquareHoleRun(run);
setSquareHoleRuntimeReturnStage(returnStage);
@@ -9068,6 +9255,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isSquareHoleBusy,
authUi,
resolveSquareHoleErrorMessage,
setSelectionStage,
setSquareHoleError,
@@ -9191,9 +9379,14 @@ export function PlatformEntryFlowShellImpl({
bigFishInputInFlightRef.current = true;
try {
const runtimeGuestOptions =
activeRecommendRuntimeKind === 'big-fish'
? await buildRecommendRuntimeAuthOptions(authUi, true)
: {};
const { run } = await submitBigFishRuntimeInput(
bigFishRun.runId,
payload,
runtimeGuestOptions,
);
setBigFishRun(run);
} catch (error) {
@@ -9204,7 +9397,13 @@ export function PlatformEntryFlowShellImpl({
bigFishInputInFlightRef.current = false;
}
},
[bigFishRun, resolveBigFishErrorMessage, setBigFishError],
[
activeRecommendRuntimeKind,
authUi,
bigFishRun,
resolveBigFishErrorMessage,
setBigFishError,
],
);
const reportBigFishObservedPlayTime = useCallback(() => {
@@ -9217,10 +9416,9 @@ export function PlatformEntryFlowShellImpl({
setBigFishRuntimeStartedAt(null);
const reportPromise =
activeRecommendRuntimeKind === 'big-fish'
? recordBigFishPlay(
sessionId,
{ elapsedMs },
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
? buildRecommendRuntimeAuthOptions(authUi, true).then(
(runtimeAuthOptions) =>
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
)
: recordBigFishPlay(sessionId, { elapsedMs });
void reportPromise.catch((error) => {
@@ -9230,6 +9428,7 @@ export function PlatformEntryFlowShellImpl({
});
}, [
activeRecommendRuntimeKind,
authUi,
bigFishRun?.sessionId,
bigFishRuntimeStartedAt,
resolveBigFishErrorMessage,
@@ -9551,12 +9750,13 @@ export function PlatformEntryFlowShellImpl({
profileId: currentLevel.profileId,
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
};
const runtimeGuestOptions =
puzzleRuntimeAuthMode === 'isolated'
? await buildRecommendRuntimeGuestOptions()
: {};
const { run } =
puzzleRuntimeAuthMode === 'isolated'
? await startPuzzleRun(
startRunPayload,
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
)
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
: await startPuzzleRun(startRunPayload);
setSelectedPuzzleDetail(detailItem);
puzzleRunRef.current = run;
@@ -9679,10 +9879,8 @@ export function PlatformEntryFlowShellImpl({
const submitLeaderboardPromise =
puzzleRuntimeAuthMode === 'isolated'
? submitPuzzleLeaderboard(
puzzleRun.runId,
payload,
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
? buildRecommendRuntimeGuestOptions().then((runtimeGuestOptions) =>
submitPuzzleLeaderboard(puzzleRun.runId, payload, runtimeGuestOptions),
)
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
@@ -9739,6 +9937,10 @@ export function PlatformEntryFlowShellImpl({
return;
}
const runtimeGuestOptions =
puzzleRuntimeAuthMode === 'isolated'
? await buildRecommendRuntimeGuestOptions()
: {};
const targetProfileId = _target?.profileId?.trim() ?? '';
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
const itemPromise =
@@ -9754,7 +9956,7 @@ export function PlatformEntryFlowShellImpl({
{
targetProfileId,
},
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
runtimeGuestOptions,
)
: advancePuzzleNextLevel(puzzleRun.runId, {
targetProfileId,
@@ -9780,7 +9982,7 @@ export function PlatformEntryFlowShellImpl({
? await advancePuzzleNextLevel(
puzzleRun.runId,
{},
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
runtimeGuestOptions,
)
: await advancePuzzleNextLevel(puzzleRun.runId);
setPuzzleRun(run);
@@ -10865,6 +11067,43 @@ export function PlatformEntryFlowShellImpl({
[openPublicWorkDetail, setJumpHopError, setSelectionStage],
);
const openJumpHopDraft = useCallback(
async (item: JumpHopWorkSummaryResponse) => {
markDraftNoticeSeen(
collectDraftNoticeKeys('jump-hop', [
item.workId,
item.profileId,
item.sourceSessionId,
]),
);
if (item.publicationStatus === 'published') {
void openJumpHopPublicWorkDetail(item.profileId);
return;
}
setJumpHopError(null);
setPublicWorkDetailError(null);
setIsJumpHopBusy(true);
try {
const detail = await jumpHopClient.getWorkDetail(item.profileId);
setJumpHopSession(null);
setJumpHopRun(null);
setJumpHopWork(detail.item);
setJumpHopRuntimeReturnStage('jump-hop-result');
enterCreateTab();
setSelectionStage('jump-hop-result');
} catch (error) {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '读取跳一跳草稿失败。'),
);
} finally {
setIsJumpHopBusy(false);
}
},
[enterCreateTab, markDraftNoticeSeen, openPublicWorkDetail, setSelectionStage],
);
const openWoodenFishPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
@@ -11896,11 +12135,12 @@ export function PlatformEntryFlowShellImpl({
setBigFishRuntimeReturnStage(returnStage);
setBigFishRun(null);
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const { run } = options.embedded
? await startBigFishRuntimeRun(
sessionId,
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
: await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRun(run);
@@ -11911,11 +12151,7 @@ export function PlatformEntryFlowShellImpl({
);
}
const recordPlayPromise = options.embedded
? recordBigFishPlay(
sessionId,
{ elapsedMs: 0 },
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeGuestOptions)
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
void recordPlayPromise.catch((error) => {
setBigFishError(
@@ -11930,13 +12166,14 @@ export function PlatformEntryFlowShellImpl({
return false;
}
},
[bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
[authUi, bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
);
const startBarkBattleRunFromWork = useCallback(
(
async (
item: BarkBattleWorkSummary,
returnStage: BarkBattleRuntimeReturnStage = 'work-detail',
options: { embedded?: boolean } = {},
) => {
if (item.status !== 'published') {
setBarkBattleError('汪汪声浪作品发布后才能进入正式玩法。');
@@ -11948,17 +12185,34 @@ export function PlatformEntryFlowShellImpl({
setBarkBattleRuntimeMode('published');
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
setBarkBattleRuntimeReturnStage(returnStage);
selectionStageRef.current = 'bark-battle-runtime';
setSelectionStage('bark-battle-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'bark-battle-runtime',
buildBarkBattlePublicWorkCode(item.workId),
),
);
return true;
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runResponse = options.embedded
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
: await startBarkBattleRun(item.workId);
void runResponse;
selectionStageRef.current = 'bark-battle-runtime';
if (!options.embedded) {
setSelectionStage('bark-battle-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'bark-battle-runtime',
buildBarkBattlePublicWorkCode(item.workId),
),
);
}
return true;
} catch (error) {
setBarkBattleError(
resolveBarkBattleErrorMessage(error, '启动汪汪声浪玩法失败。'),
);
return false;
}
},
[setSelectionStage],
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
);
const startSelectedPublicWork = useCallback(() => {
@@ -12230,7 +12484,9 @@ export function PlatformEntryFlowShellImpl({
'当前汪汪声浪作品信息不完整,暂时无法进入玩法。',
);
} else {
started = startBarkBattleRunFromWork(work, 'platform');
started = await startBarkBattleRunFromWork(work, 'platform', {
embedded: true,
});
}
} else if (isEdutainmentGalleryEntry(entry)) {
started = await startBabyObjectMatchRuntimeFromEntry(
@@ -12324,6 +12580,7 @@ export function PlatformEntryFlowShellImpl({
const recommendRuntimeContent = useMemo(() => {
if (
isDesktopLayout ||
selectionStage !== 'platform' ||
platformBootstrap.platformTab !== 'home' ||
!activeRecommendRuntimeKind
@@ -12730,10 +12987,12 @@ export function PlatformEntryFlowShellImpl({
visualNovelSession,
visualNovelWork,
checkpointWoodenFishRuntimeRun,
isDesktopLayout,
]);
useEffect(() => {
if (
isDesktopLayout ||
selectionStage !== 'platform' ||
platformBootstrap.platformTab !== 'home' ||
platformBootstrap.isLoadingPlatform
@@ -12789,6 +13048,7 @@ export function PlatformEntryFlowShellImpl({
match3dRun,
platformBootstrap.isLoadingPlatform,
platformBootstrap.platformTab,
isDesktopLayout,
puzzleRun,
recommendRuntimeEntries,
selectRecommendRuntimeEntry,
@@ -13895,6 +14155,7 @@ export function PlatformEntryFlowShellImpl({
deletingWorkId={deletingCreationWorkId}
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
bigFishItems={isBigFishCreationVisible ? bigFishShelfItems : []}
jumpHopItems={isJumpHopCreationVisible ? jumpHopShelfItems : []}
onOpenBigFishDetail={
isBigFishCreationVisible
? (item) => {
@@ -13904,6 +14165,15 @@ export function PlatformEntryFlowShellImpl({
}
: undefined
}
onOpenJumpHopDetail={
isJumpHopCreationVisible
? (item) => {
runProtectedAction(() => {
void openJumpHopDraft(item);
});
}
: undefined
}
onDeleteBigFish={
isBigFishCreationVisible
? (item) => {
@@ -13911,6 +14181,7 @@ export function PlatformEntryFlowShellImpl({
}
: null
}
onDeleteJumpHop={null}
match3dItems={match3dShelfItems}
onOpenMatch3DDetail={(item) => {
runProtectedAction(() => {
@@ -14017,6 +14288,7 @@ export function PlatformEntryFlowShellImpl({
isLoadingPlatform={platformBootstrap.isLoadingPlatform}
isLoadingDashboard={platformBootstrap.isLoadingDashboard}
hasUnreadDraftUpdate={hasUnreadDraftUpdates}
isDesktopLayout={isDesktopLayout}
isResumingSaveWorldKey={platformBootstrap.isResumingSaveWorldKey}
platformError={
platformBootstrap.isLoadingPlatform
@@ -16242,6 +16514,29 @@ export function PlatformEntryFlowShellImpl({
}}
/>
) : null}
<UnifiedModal
open={Boolean(draftGenerationPointNotice)}
title={draftGenerationPointNotice?.title ?? '泥点提示'}
description={draftGenerationPointNoticeDescription}
onClose={() => setDraftGenerationPointNotice(null)}
closeOnBackdrop
size="sm"
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
panelClassName="platform-remap-surface rounded-[1.75rem]"
footer={
<button
type="button"
onClick={() => setDraftGenerationPointNotice(null)}
className="platform-button platform-button--primary min-h-0 rounded-full px-4 py-2 text-sm"
>
</button>
}
>
<div className="text-sm leading-6 text-[var(--platform-text-base)]">
{draftGenerationPointNotice?.message}
</div>
</UnifiedModal>
<PublishShareModal
open={Boolean(publishSharePayload)}
payload={publishSharePayload}

View File

@@ -315,3 +315,36 @@ test('groups visible platform creation types by backend category metadata', () =
]);
expect(groups[1]?.items.map((item) => item.id)).toEqual(['visual-novel']);
});
test('falls back when backend creation type category metadata is missing', () => {
const cards = derivePlatformCreationTypes([
{
id: 'legacy-entry',
title: '历史入口',
subtitle: '旧数据缺少分类字段',
badge: '可创建',
imageSrc: '/creation-type-references/puzzle.webp',
visible: true,
open: true,
sortOrder: 10,
categoryId: undefined as unknown as string,
categoryLabel: undefined as unknown as string,
categorySortOrder: 0,
updatedAtMicros: 1,
},
]);
expect(cards[0]).toEqual(
expect.objectContaining({
id: 'legacy-entry',
categoryId: 'recent',
categoryLabel: '最近创作',
}),
);
expect(groupVisiblePlatformCreationTypes(cards)).toEqual([
expect.objectContaining({
id: 'recent',
label: '最近创作',
}),
]);
});

View File

@@ -55,13 +55,13 @@ export function isPlatformCreationTypeOpen(
);
}
function normalizeCategoryId(value: string) {
const normalized = value.trim();
function normalizeCategoryId(value: string | null | undefined) {
const normalized = typeof value === 'string' ? value.trim() : '';
return normalized || FALLBACK_CREATION_CATEGORY_ID;
}
function normalizeCategoryLabel(value: string) {
const normalized = value.trim();
function normalizeCategoryLabel(value: string | null | undefined) {
const normalized = typeof value === 'string' ? value.trim() : '';
return normalized || FALLBACK_CREATION_CATEGORY_LABEL;
}

View File

@@ -0,0 +1,47 @@
import { useEffect, useState } from 'react';
export const PLATFORM_DESKTOP_LAYOUT_QUERY = '(min-width: 1024px)';
export function getInitialPlatformDesktopLayout() {
if (
typeof window === 'undefined' ||
typeof window.matchMedia !== 'function'
) {
return false;
}
return window.matchMedia(PLATFORM_DESKTOP_LAYOUT_QUERY).matches;
}
export function usePlatformDesktopLayout() {
const [isDesktopLayout, setIsDesktopLayout] = useState(
getInitialPlatformDesktopLayout,
);
useEffect(() => {
if (
typeof window === 'undefined' ||
typeof window.matchMedia !== 'function'
) {
return;
}
const mediaQuery = window.matchMedia(PLATFORM_DESKTOP_LAYOUT_QUERY);
const updateLayout = (event?: MediaQueryListEvent) => {
setIsDesktopLayout(event?.matches ?? mediaQuery.matches);
};
updateLayout();
// 平台页只挂载当前断点外壳,避免隐藏的移动端/桌面端内容重复抢占查询。
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateLayout);
return () => mediaQuery.removeEventListener('change', updateLayout);
}
mediaQuery.addListener(updateLayout);
return () => mediaQuery.removeListener(updateLayout);
}, []);
return isDesktopLayout;
}