合入最新 master

合并 origin/master 并保留平台入口运行态与推荐链路语义
修正合并后基于 tab 语义变化的前端断言
This commit is contained in:
2026-06-11 22:59:35 +08:00
120 changed files with 9591 additions and 1099 deletions

View File

@@ -11,7 +11,7 @@ import {
useState,
} from 'react';
import jumpHopRuntimeLevelLogo from '../../../media/logo.png';
import jumpHopRuntimeLevelLogo from '../../../media/logo-runtime-hud.webp';
import type {
JumpHopRunStatus,
JumpHopRuntimeRunSnapshotResponse,

View File

@@ -16,7 +16,7 @@ import {
useState,
} from 'react';
import match3DRuntimeLevelLogo from '../../../media/logo.png';
import match3DRuntimeLevelLogo from '../../../media/logo-runtime-hud.webp';
import type {
Match3DClickItemRequest,
Match3DClickItemResult,

View File

@@ -317,6 +317,7 @@ import {
submitRpgProfileFeedback,
} from '../../services/rpg-entry/rpgProfileClient';
import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest';
import { type RuntimeGuestRequestOptions } from '../../services/runtimeGuestAuth';
import { squareHoleCreationClient } from '../../services/square-hole-creation';
import {
dropSquareHoleShape,
@@ -377,11 +378,15 @@ import {
type CreationWorkShelfItem,
isPersistedBarkBattleDraftGenerating,
} from '../custom-world-home/creationWorkShelf';
import { selectAdjacentPlatformRecommendEntry } from '../rpg-entry/rpgEntryPublicGalleryViewModel';
import {
buildPlatformRecommendFeedEntries,
selectAdjacentPlatformRecommendEntry,
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
import {
isBigFishGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isPuzzleClearGalleryEntry,
isPuzzleGalleryEntry,
mapPuzzleClearWorkToPlatformGalleryCard,
type PlatformPublicGalleryCard,
@@ -495,7 +500,6 @@ import {
import {
canExposePublicWork,
EDUTAINMENT_HIDDEN_MESSAGE,
filterGeneralPublicWorks,
} from './platformEdutainmentVisibility';
import {
PlatformDraftGenerationPointNoticeDialog,
@@ -1496,6 +1500,8 @@ export function PlatformEntryFlowShellImpl({
useState<MiniGameDraftGenerationState | null>(null);
const [jumpHopError, setJumpHopError] = useState<string | null>(null);
const [isJumpHopBusy, setIsJumpHopBusy] = useState(false);
const [barkBattleRuntimeRequestOptions, setBarkBattleRuntimeRequestOptions] =
useState<RuntimeGuestRequestOptions | null>(null);
const [puzzleClearSession, setPuzzleClearSession] =
useState<PuzzleClearSessionSnapshotResponse | null>(null);
const [puzzleClearRun, setPuzzleClearRun] = useState<
@@ -1599,6 +1605,34 @@ export function PlatformEntryFlowShellImpl({
useState<PuzzleRuntimeReturnStage>('puzzle-gallery-detail');
const [puzzleRuntimeAuthMode, setPuzzleRuntimeAuthMode] =
useState<PuzzleRuntimeAuthMode>('default');
const buildRecommendRuntimeRequestOptions = useCallback(
async (
input: {
kind?: RecommendRuntimeKind;
embedded?: boolean;
forcePublicRuntime?: boolean;
} = {},
) => {
const shouldUseRuntimeOptions = Boolean(
input.forcePublicRuntime ||
input.embedded ||
(input.kind && activeRecommendRuntimeKind === input.kind),
);
return shouldUseRuntimeOptions
? buildRecommendRuntimeAuthOptions(authUi, true)
: {};
},
[activeRecommendRuntimeKind, authUi],
);
const buildPuzzleRuntimeRequestOptions = useCallback(
() =>
buildRecommendRuntimeRequestOptions({
kind: 'puzzle',
forcePublicRuntime: puzzleRuntimeAuthMode === 'isolated',
}),
[buildRecommendRuntimeRequestOptions, puzzleRuntimeAuthMode],
);
const [isPuzzleLeaderboardBusy, setIsPuzzleLeaderboardBusy] = useState(false);
const submittedPuzzleLeaderboardKeysRef = useRef(new Set<string>());
const puzzleStartInFlightKeyRef = useRef<string | null>(null);
@@ -1734,6 +1768,10 @@ export function PlatformEntryFlowShellImpl({
const entries = creationEntryConfig?.creationTypes ?? [];
return new Map(entries.map((entry) => [entry.id, entry]));
}, [creationEntryConfig]);
const publicWorkInteractions = useMemo(
() => creationEntryConfig?.publicWorkInteractions ?? [],
[creationEntryConfig],
);
const getUnifiedSpec = useCallback(
(playId: UnifiedCreationPlayId) =>
getUnifiedCreationSpec(playId, unifiedCreationConfigById.get(playId)),
@@ -2938,10 +2976,10 @@ export function PlatformEntryFlowShellImpl({
} = publicGalleryFeeds;
const recommendRuntimeEntries = useMemo(
() =>
buildPlatformRecommendedEntries({
featuredEntries: filterGeneralPublicWorks(featuredGalleryEntries),
latestEntries: filterGeneralPublicWorks(latestGalleryEntries),
}),
buildPlatformRecommendFeedEntries(
featuredGalleryEntries,
latestGalleryEntries,
),
[featuredGalleryEntries, latestGalleryEntries],
);
@@ -6994,15 +7032,17 @@ export function PlatformEntryFlowShellImpl({
profileId: targetProfileId,
mode: 'play' as const,
};
const runtimeGuestOptions =
options.embedded || workDetail.summary.publishStatus === 'draft'
? await buildRecommendRuntimeAuthOptions(authUi, true)
: {};
const runtimeRequestOptions = await buildRecommendRuntimeRequestOptions(
{
kind: 'visual-novel',
embedded: options.embedded,
},
);
const { run } = options.embedded
? await startVisualNovelRun(
targetProfileId,
startRunPayload,
runtimeGuestOptions,
runtimeRequestOptions,
)
: await startVisualNovelRun(targetProfileId, startRunPayload);
setVisualNovelWork(workDetail);
@@ -7028,7 +7068,7 @@ export function PlatformEntryFlowShellImpl({
}
},
[
authUi,
buildRecommendRuntimeRequestOptions,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
@@ -7050,14 +7090,14 @@ export function PlatformEntryFlowShellImpl({
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const runtimeGuestOptions =
activeRecommendRuntimeKind === 'visual-novel'
? await buildRecommendRuntimeAuthOptions(authUi, true)
: {};
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'visual-novel',
});
const nextRun = await streamVisualNovelRuntimeAction(
visualNovelRun.runId,
payload,
runtimeGuestOptions,
runtimeRequestOptions,
);
setVisualNovelRun(nextRun);
} catch (error) {
@@ -7069,8 +7109,7 @@ export function PlatformEntryFlowShellImpl({
}
},
[
activeRecommendRuntimeKind,
authUi,
buildRecommendRuntimeRequestOptions,
isVisualNovelBusy,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
@@ -7519,22 +7558,15 @@ export function PlatformEntryFlowShellImpl({
setJumpHopError(null);
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions =
options.embedded || shouldUseRecommendRuntimeGuestAuth(authUi)
? await buildRecommendRuntimeAuthOptions(authUi, true)
: RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'jump-hop',
embedded: options.embedded,
forcePublicRuntime: shouldUseRecommendRuntimeGuestAuth(authUi),
});
setJumpHopRuntimeRequestOptions(
runtimeGuestOptions.runtimeGuestToken?.trim()
? {
runtimeGuestToken: runtimeGuestOptions.runtimeGuestToken,
authImpact: runtimeGuestOptions.authImpact,
skipAuth: runtimeGuestOptions.skipAuth,
skipRefresh: runtimeGuestOptions.skipRefresh,
notifyAuthStateChange:
runtimeGuestOptions.notifyAuthStateChange,
clearAuthOnUnauthorized:
runtimeGuestOptions.clearAuthOnUnauthorized,
}
Object.keys(runtimeRequestOptions).length > 0
? runtimeRequestOptions
: null,
);
const [detail, runResponse] = await Promise.all([
@@ -7544,7 +7576,7 @@ export function PlatformEntryFlowShellImpl({
.getWorkDetail(normalizedProfileId)
.catch(() => null),
jumpHopClient.startRun(normalizedProfileId, {
...runtimeGuestOptions,
...runtimeRequestOptions,
runtimeMode: 'published',
}),
]);
@@ -7571,7 +7603,7 @@ export function PlatformEntryFlowShellImpl({
setIsJumpHopBusy(false);
}
},
[authUi, setSelectionStage],
[authUi, buildRecommendRuntimeRequestOptions, setSelectionStage],
);
useEffect(() => {
@@ -8012,15 +8044,16 @@ export function PlatformEntryFlowShellImpl({
setPuzzleClearError(null);
setPuzzleClearRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
embedded: options.embedded,
});
const [detail, runResponse] = await Promise.all([
puzzleClearClient
.getRuntimeWorkDetail(normalizedProfileId)
.catch(() => null),
puzzleClearClient.startRun(normalizedProfileId, runtimeGuestOptions),
puzzleClearClient.startRun(normalizedProfileId, runtimeRequestOptions),
]);
if (detail?.item) {
setPuzzleClearWork(detail.item);
@@ -8045,7 +8078,7 @@ export function PlatformEntryFlowShellImpl({
setIsPuzzleClearBusy(false);
}
},
[authUi, setSelectionStage],
[buildRecommendRuntimeRequestOptions, setSelectionStage],
);
const retryPuzzleClearLevelRun = useCallback(async () => {
@@ -8070,7 +8103,14 @@ export function PlatformEntryFlowShellImpl({
setIsPuzzleClearBusy(true);
setPuzzleClearError(null);
try {
const response = await puzzleClearClient.retryLevel(runId);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.retryLevel(
runId,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
@@ -8082,6 +8122,7 @@ export function PlatformEntryFlowShellImpl({
}, [
puzzleClearRun,
puzzleClearWork,
buildRecommendRuntimeRequestOptions,
setSelectionStage,
startPuzzleClearTestRunFromProfile,
]);
@@ -8107,7 +8148,14 @@ export function PlatformEntryFlowShellImpl({
setIsPuzzleClearBusy(true);
setPuzzleClearError(null);
try {
const response = await puzzleClearClient.advanceNextLevel(runId);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.advanceNextLevel(
runId,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
@@ -8116,7 +8164,12 @@ export function PlatformEntryFlowShellImpl({
} finally {
setIsPuzzleClearBusy(false);
}
}, [puzzleClearRun, puzzleClearWork, setSelectionStage]);
}, [
puzzleClearRun,
puzzleClearWork,
buildRecommendRuntimeRequestOptions,
setSelectionStage,
]);
const markPuzzleClearLevelTimeUp = useCallback(async () => {
const runId = puzzleClearRun?.runId;
@@ -8130,14 +8183,21 @@ export function PlatformEntryFlowShellImpl({
}
try {
const response = await puzzleClearClient.markTimeUp(runId);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.markTimeUp(
runId,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '同步拼消消倒计时失败。'),
);
}
}, [puzzleClearRun]);
}, [puzzleClearRun, buildRecommendRuntimeRequestOptions]);
const swapPuzzleClearCardsInRun = useCallback(
async (payload: {
@@ -8156,7 +8216,15 @@ export function PlatformEntryFlowShellImpl({
return;
}
try {
const response = await puzzleClearClient.swapCards(runId, payload);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.swapCards(
runId,
payload,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
@@ -8164,7 +8232,7 @@ export function PlatformEntryFlowShellImpl({
);
}
},
[puzzleClearRun],
[puzzleClearRun, buildRecommendRuntimeRequestOptions],
);
const compileWoodenFishSession = useCallback(
@@ -8516,16 +8584,17 @@ export function PlatformEntryFlowShellImpl({
setWoodenFishError(null);
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'wooden-fish',
embedded: options.embedded,
});
const [detail, runResponse] = await Promise.all([
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
options.embedded
? woodenFishClient.startRun(
normalizedProfileId,
runtimeGuestOptions,
runtimeRequestOptions,
)
: woodenFishClient.startRun(normalizedProfileId),
]);
@@ -8552,7 +8621,7 @@ export function PlatformEntryFlowShellImpl({
setIsWoodenFishBusy(false);
}
},
[authUi, setSelectionStage],
[buildRecommendRuntimeRequestOptions, setSelectionStage],
);
const checkpointWoodenFishRuntimeRun = useCallback(
@@ -8564,10 +8633,18 @@ export function PlatformEntryFlowShellImpl({
if (!runId) {
return;
}
const response = await woodenFishClient.checkpointRun(runId, payload);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'wooden-fish',
});
const response = await woodenFishClient.checkpointRun(
runId,
payload,
runtimeRequestOptions,
);
setWoodenFishRun(response.run);
},
[woodenFishRun?.runId],
[buildRecommendRuntimeRequestOptions, woodenFishRun?.runId],
);
const executePuzzleAction = puzzleFlow.executeAction;
@@ -9038,9 +9115,12 @@ export function PlatformEntryFlowShellImpl({
try {
let runtimeProfile: Match3DWorkProfile | Match3DWorkSummary = profile;
const canReadProtectedMatch3DDetail =
!options.embedded || !shouldUseRecommendRuntimeGuestAuth(authUi);
if (
!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
!hasMatch3DRuntimeBackgroundAsset(profile)
canReadProtectedMatch3DDetail &&
(!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
!hasMatch3DRuntimeBackgroundAsset(profile))
) {
try {
const { item } = await getMatch3DWorkDetail(profile.profileId);
@@ -9066,12 +9146,14 @@ export function PlatformEntryFlowShellImpl({
runtimeProfile.generatedBackgroundAsset,
{ expireSeconds: 300 },
);
const runtimeGuestOptions =
options.authMode === 'isolated'
? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS
: await buildRecommendRuntimeAuthOptions(authUi, options.embedded);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'match3d',
embedded: options.embedded,
forcePublicRuntime: options.authMode === 'isolated',
});
const runtimeOptions = {
...runtimeGuestOptions,
...runtimeRequestOptions,
...(typeof options.itemTypeCountOverride === 'number'
? { itemTypeCountOverride: options.itemTypeCountOverride }
: {}),
@@ -9120,6 +9202,7 @@ export function PlatformEntryFlowShellImpl({
[
isMatch3DBusy,
authUi,
buildRecommendRuntimeRequestOptions,
match3dFlow,
resolveMatch3DErrorMessage,
resolveMatch3DRuntimeAdapter,
@@ -9143,12 +9226,13 @@ export function PlatformEntryFlowShellImpl({
setSquareHoleError(null);
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
embedded: options.embedded,
});
const { run } = options.embedded
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
? await startSquareHoleRun(profile.profileId, runtimeRequestOptions)
: await startSquareHoleRun(profile.profileId);
setSquareHoleRun(run);
setSquareHoleRuntimeReturnStage(returnStage);
@@ -9180,7 +9264,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isSquareHoleBusy,
authUi,
buildRecommendRuntimeRequestOptions,
resolveSquareHoleErrorMessage,
setSelectionStage,
setSquareHoleError,
@@ -9298,14 +9382,14 @@ export function PlatformEntryFlowShellImpl({
bigFishInputInFlightRef.current = true;
try {
const runtimeGuestOptions =
activeRecommendRuntimeKind === 'big-fish'
? await buildRecommendRuntimeAuthOptions(authUi, true)
: {};
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'big-fish',
});
const { run } = await submitBigFishRuntimeInput(
bigFishRun.runId,
payload,
runtimeGuestOptions,
runtimeRequestOptions,
);
setBigFishRun(run);
} catch (error) {
@@ -9317,8 +9401,7 @@ export function PlatformEntryFlowShellImpl({
}
},
[
activeRecommendRuntimeKind,
authUi,
buildRecommendRuntimeRequestOptions,
bigFishRun,
resolveBigFishErrorMessage,
setBigFishError,
@@ -9333,21 +9416,18 @@ export function PlatformEntryFlowShellImpl({
const elapsedMs = Math.max(1_000, Date.now() - bigFishRuntimeStartedAt);
setBigFishRuntimeStartedAt(null);
const reportPromise =
activeRecommendRuntimeKind === 'big-fish'
? buildRecommendRuntimeAuthOptions(authUi, true).then(
(runtimeAuthOptions) =>
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
)
: recordBigFishPlay(sessionId, { elapsedMs });
const reportPromise = buildRecommendRuntimeRequestOptions({
kind: 'big-fish',
}).then((runtimeRequestOptions) =>
recordBigFishPlay(sessionId, { elapsedMs }, runtimeRequestOptions),
);
void reportPromise.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩时长失败。'),
);
});
}, [
activeRecommendRuntimeKind,
authUi,
buildRecommendRuntimeRequestOptions,
bigFishRun?.sessionId,
bigFishRuntimeStartedAt,
resolveBigFishErrorMessage,
@@ -9665,14 +9745,11 @@ export function PlatformEntryFlowShellImpl({
profileId: currentLevel.profileId,
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
};
const runtimeGuestOptions =
puzzleRuntimeAuthMode === 'isolated'
? await buildRecommendRuntimeGuestOptions()
: {};
const { run } =
puzzleRuntimeAuthMode === 'isolated'
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
: await startPuzzleRun(startRunPayload);
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
const { run } = await startPuzzleRun(
startRunPayload,
runtimeRequestOptions,
);
setSelectedPuzzleDetail(detailItem);
puzzleRunRef.current = run;
setPuzzleRun(run);
@@ -9686,7 +9763,7 @@ export function PlatformEntryFlowShellImpl({
}, [
isPuzzleBusy,
puzzleRun,
puzzleRuntimeAuthMode,
buildPuzzleRuntimeRequestOptions,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
@@ -9798,16 +9875,10 @@ export function PlatformEntryFlowShellImpl({
return;
}
const submitLeaderboardPromise =
puzzleRuntimeAuthMode === 'isolated'
? buildRecommendRuntimeGuestOptions().then((runtimeGuestOptions) =>
submitPuzzleLeaderboard(
puzzleRun.runId,
payload,
runtimeGuestOptions,
),
)
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
const submitLeaderboardPromise = buildPuzzleRuntimeRequestOptions().then(
(runtimeRequestOptions) =>
submitPuzzleLeaderboard(puzzleRun.runId, payload, runtimeRequestOptions),
);
void submitLeaderboardPromise
.then(({ run }) => {
@@ -9831,7 +9902,7 @@ export function PlatformEntryFlowShellImpl({
authUi?.user?.displayName,
platformBootstrap,
puzzleRun,
puzzleRuntimeAuthMode,
buildPuzzleRuntimeRequestOptions,
resolvePuzzleErrorMessage,
setPuzzleError,
]);
@@ -9861,10 +9932,7 @@ export function PlatformEntryFlowShellImpl({
return;
}
const runtimeGuestOptions =
puzzleRuntimeAuthMode === 'isolated'
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
const targetProfileId = _target?.profileId?.trim() ?? '';
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
const itemPromise =
@@ -9873,18 +9941,13 @@ export function PlatformEntryFlowShellImpl({
: getPuzzleGalleryDetail(targetProfileId).then(
(response) => response.item,
);
const advancePromise =
puzzleRuntimeAuthMode === 'isolated'
? advancePuzzleNextLevel(
puzzleRun.runId,
{
targetProfileId,
},
runtimeGuestOptions,
)
: advancePuzzleNextLevel(puzzleRun.runId, {
targetProfileId,
});
const advancePromise = advancePuzzleNextLevel(
puzzleRun.runId,
{
targetProfileId,
},
runtimeRequestOptions,
);
const [{ run }, item] = await Promise.all([
advancePromise,
itemPromise,
@@ -9901,14 +9964,11 @@ export function PlatformEntryFlowShellImpl({
return;
}
const { run } =
puzzleRuntimeAuthMode === 'isolated'
? await advancePuzzleNextLevel(
puzzleRun.runId,
{},
runtimeGuestOptions,
)
: await advancePuzzleNextLevel(puzzleRun.runId, {});
const { run } = await advancePuzzleNextLevel(
puzzleRun.runId,
{},
runtimeRequestOptions,
);
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
@@ -9921,7 +9981,7 @@ export function PlatformEntryFlowShellImpl({
isPuzzleBusy,
isPuzzleLeaderboardBusy,
puzzleRun,
puzzleRuntimeAuthMode,
buildPuzzleRuntimeRequestOptions,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
@@ -10696,7 +10756,10 @@ export function PlatformEntryFlowShellImpl({
setIsPublicWorkDetailBusy(true);
setPublicWorkDetailError(null);
const intent = resolvePlatformPublicWorkLikeIntent(entry);
const intent = resolvePlatformPublicWorkLikeIntent(
entry,
publicWorkInteractions,
);
if (intent.type === 'like-big-fish') {
void likeBigFishGalleryWork(intent.profileId)
@@ -10806,6 +10869,7 @@ export function PlatformEntryFlowShellImpl({
[
isPublicWorkDetailBusy,
platformBootstrap,
publicWorkInteractions,
resolveBigFishErrorMessage,
resolvePuzzleErrorMessage,
runProtectedAction,
@@ -12452,12 +12516,13 @@ export function PlatformEntryFlowShellImpl({
setBigFishRuntimeReturnStage(returnStage);
setBigFishRun(null);
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'big-fish',
embedded: options.embedded,
});
const { run } = options.embedded
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
? await startBigFishRuntimeRun(sessionId, runtimeRequestOptions)
: await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRun(run);
@@ -12468,7 +12533,7 @@ export function PlatformEntryFlowShellImpl({
);
}
const recordPlayPromise = options.embedded
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeGuestOptions)
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeRequestOptions)
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
void recordPlayPromise.catch((error) => {
setBigFishError(
@@ -12484,7 +12549,7 @@ export function PlatformEntryFlowShellImpl({
}
},
[
authUi,
buildRecommendRuntimeRequestOptions,
bigFishFlow,
resolveBigFishErrorMessage,
setBigFishError,
@@ -12511,12 +12576,18 @@ export function PlatformEntryFlowShellImpl({
);
setBarkBattleRuntimeReturnStage(returnStage);
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'bark-battle',
embedded: options.embedded,
});
setBarkBattleRuntimeRequestOptions(
Object.keys(runtimeRequestOptions).length > 0
? runtimeRequestOptions
: null,
);
const runResponse = options.embedded
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
? await startBarkBattleRun(item.workId, {}, runtimeRequestOptions)
: await startBarkBattleRun(item.workId);
void runResponse;
selectionStageRef.current = 'bark-battle-runtime';
@@ -12537,7 +12608,11 @@ export function PlatformEntryFlowShellImpl({
return false;
}
},
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
[
buildRecommendRuntimeRequestOptions,
resolveBarkBattleErrorMessage,
setSelectionStage,
],
);
const startSelectedPublicWork = useCallback(() => {
@@ -12991,10 +13066,12 @@ export function PlatformEntryFlowShellImpl({
match3dFlow.setIsBusy(true);
setMatch3DError(null);
void resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
)
.restartRun(match3dRun.runId)
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
.then((runtimeRequestOptions) =>
resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
).restartRun(match3dRun.runId, runtimeRequestOptions),
)
.then(({ run }) => {
setMatch3DRun(run);
})
@@ -13008,24 +13085,28 @@ export function PlatformEntryFlowShellImpl({
});
}}
onOptimisticRunChange={setMatch3DRun}
onClickItem={(payload) => {
onClickItem={async (payload) => {
const runId = payload.runId ?? match3dRun?.runId;
if (!runId) {
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
}
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({ kind: 'match3d' });
return resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
).clickItem(runId, payload);
).clickItem(runId, payload, runtimeRequestOptions);
}}
onTimeExpired={() => {
if (!match3dRun?.runId) {
return;
}
void resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
)
.finishTimeUp(match3dRun.runId)
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
.then((runtimeRequestOptions) =>
resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
).finishTimeUp(match3dRun.runId, runtimeRequestOptions),
)
.then(({ run }) => {
setMatch3DRun(run);
})
@@ -13159,9 +13240,17 @@ export function PlatformEntryFlowShellImpl({
squareHoleRun?.runId &&
squareHoleRun.status.toLowerCase() === 'running'
) {
void stopSquareHoleRun(squareHoleRun.runId).catch(
() => undefined,
);
void buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
})
.then((runtimeRequestOptions) =>
stopSquareHoleRun(
squareHoleRun.runId,
undefined,
runtimeRequestOptions,
),
)
.catch(() => undefined);
}
setActiveRecommendRuntimeKind(null);
}}
@@ -13172,7 +13261,12 @@ export function PlatformEntryFlowShellImpl({
squareHoleFlow.setIsBusy(true);
setSquareHoleError(null);
void restartSquareHoleRun(squareHoleRun.runId)
void buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
})
.then((runtimeRequestOptions) =>
restartSquareHoleRun(squareHoleRun.runId, runtimeRequestOptions),
)
.then(({ run }) => {
setSquareHoleRun(run);
})
@@ -13194,14 +13288,26 @@ export function PlatformEntryFlowShellImpl({
if (!runId) {
return Promise.reject(new Error('方洞挑战运行态缺少 runId。'));
}
return dropSquareHoleShape(runId, payload);
return buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
}).then((runtimeRequestOptions) =>
dropSquareHoleShape(runId, payload, runtimeRequestOptions),
);
}}
onTimeExpired={() => {
if (!squareHoleRun?.runId) {
return;
}
void finishSquareHoleTimeUp(squareHoleRun.runId)
void buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
})
.then((runtimeRequestOptions) =>
finishSquareHoleTimeUp(
squareHoleRun.runId,
runtimeRequestOptions,
),
)
.then(({ run }) => {
setSquareHoleRun(run);
})
@@ -13246,6 +13352,7 @@ export function PlatformEntryFlowShellImpl({
workId={barkBattlePublishedConfig.workId}
publishedConfig={barkBattlePublishedConfig}
runtimeMode="published"
runtimeRequestOptions={barkBattleRuntimeRequestOptions ?? undefined}
onExit={() => {
setActiveRecommendRuntimeKind(null);
}}
@@ -13276,7 +13383,9 @@ export function PlatformEntryFlowShellImpl({
activeRecommendEntryKey,
activeRecommendRuntimeKind,
barkBattlePublishedConfig,
barkBattleRuntimeRequestOptions,
babyObjectMatchDraft,
buildRecommendRuntimeRequestOptions,
bigFishError,
bigFishRun,
bigFishRuntimeShare,
@@ -13313,6 +13422,7 @@ export function PlatformEntryFlowShellImpl({
recommendRuntimeEntries,
remodelCurrentPuzzleRuntimeWork,
resolveMatch3DErrorMessage,
resolveMatch3DRuntimeAdapter,
resolveSquareHoleErrorMessage,
reportBigFishObservedPlayTime,
restartBigFishRun,
@@ -13442,7 +13552,10 @@ export function PlatformEntryFlowShellImpl({
setIsPublicWorkDetailBusy(true);
setPublicWorkDetailError(null);
const intent = resolvePlatformPublicWorkRemixIntent(entry);
const intent = resolvePlatformPublicWorkRemixIntent(
entry,
publicWorkInteractions,
);
if (intent.type === 'remix-big-fish') {
void remixBigFishGalleryWork(intent.profileId)
@@ -13517,6 +13630,7 @@ export function PlatformEntryFlowShellImpl({
isPublicWorkDetailBusy,
platformBootstrap,
puzzleFlow,
publicWorkInteractions,
resetRecommendRuntimeSelection,
resolveBigFishErrorMessage,
resolvePuzzleErrorMessage,
@@ -16702,6 +16816,9 @@ export function PlatformEntryFlowShellImpl({
workId={barkBattlePublishedConfig.workId}
publishedConfig={barkBattlePublishedConfig}
runtimeMode={barkBattleRuntimeMode}
runtimeRequestOptions={
barkBattleRuntimeRequestOptions ?? undefined
}
onExit={() => {
if (
barkBattleRuntimeReturnStage === 'bark-battle-result' &&

View File

@@ -899,6 +899,23 @@ test('platform public work detail flow resolves like intent', () => {
});
});
test('platform public work detail flow respects configured like disable', () => {
expect(
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle'), [
{
sourceType: 'puzzle',
likeEnabled: false,
remixEnabled: true,
likeDisabledMessage: '拼图点赞维护中。',
remixDisabledMessage: '拼图改造维护中。',
},
]),
).toEqual({
type: 'unsupported',
errorMessage: '拼图点赞维护中。',
});
});
test('platform public work detail flow resolves remix intent', () => {
expect(
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('big-fish')),
@@ -969,13 +986,31 @@ test('platform public work detail flow resolves remix intent', () => {
});
});
test('platform public work detail flow respects configured remix disable', () => {
expect(
resolvePlatformPublicWorkRemixIntent(buildRpgEntry(), [
{
sourceType: 'custom-world',
likeEnabled: true,
remixEnabled: false,
likeDisabledMessage: 'RPG 点赞维护中。',
remixDisabledMessage: 'RPG 改造维护中。',
},
]),
).toEqual({
type: 'unsupported',
errorMessage: 'RPG 改造维护中。',
});
});
test('platform public work detail flow resolves edit intent for draft-backed works', () => {
const bigFishEntry = buildTypedEntry('big-fish');
expect(resolvePlatformPublicWorkEditIntent(bigFishEntry, buildEditIntentDeps()))
.toEqual({
type: 'edit-big-fish',
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
});
expect(
resolvePlatformPublicWorkEditIntent(bigFishEntry, buildEditIntentDeps()),
).toEqual({
type: 'edit-big-fish',
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
});
const selectedPuzzleDetail = buildPuzzleWork({
profileId: 'puzzle-profile',
@@ -1153,7 +1188,10 @@ test('platform public work detail flow resolves edit intent for unsupported and
const edutainmentEntry = buildTypedEntry('edutainment');
expect(
resolvePlatformPublicWorkEditIntent(edutainmentEntry, buildEditIntentDeps()),
resolvePlatformPublicWorkEditIntent(
edutainmentEntry,
buildEditIntentDeps(),
),
).toEqual({
type: 'resolve-edutainment-draft',
entry: edutainmentEntry,

View File

@@ -97,6 +97,14 @@ export type PlatformPublicWorkDetailOpenStrategy =
export type PlatformPublicWorkActionMode = 'edit' | 'remix';
export type PlatformPublicWorkInteractionConfig = {
sourceType: string;
likeEnabled: boolean;
remixEnabled: boolean;
likeDisabledMessage: string;
remixDisabledMessage: string;
};
export type PlatformPublicWorkLikeIntent =
| {
type: 'like-big-fish';
@@ -678,9 +686,55 @@ export function resolvePlatformPublicWorkActionMode(
: 'remix';
}
export function getPlatformPublicWorkInteractionSourceType(
entry: PlatformPublicGalleryCard,
) {
return 'sourceType' in entry ? entry.sourceType : 'custom-world';
}
function resolveConfiguredPublicWorkInteractionBlock(
entry: PlatformPublicGalleryCard,
configs: readonly PlatformPublicWorkInteractionConfig[] | null | undefined,
action: 'like' | 'remix',
): PlatformPublicWorkLikeIntent | PlatformPublicWorkRemixIntent | null {
const sourceType = getPlatformPublicWorkInteractionSourceType(entry);
const config = configs?.find((item) => item.sourceType === sourceType);
if (!config) {
return null;
}
if (action === 'like' && !config.likeEnabled) {
return {
type: 'unsupported',
errorMessage:
config.likeDisabledMessage.trim() || '该作品类型暂不支持点赞。',
};
}
if (action === 'remix' && !config.remixEnabled) {
return {
type: 'unsupported',
errorMessage:
config.remixDisabledMessage.trim() || '该作品类型暂不支持改造。',
};
}
return null;
}
export function resolvePlatformPublicWorkLikeIntent(
entry: PlatformPublicGalleryCard,
configs?: readonly PlatformPublicWorkInteractionConfig[] | null,
): PlatformPublicWorkLikeIntent {
const configuredBlock = resolveConfiguredPublicWorkInteractionBlock(
entry,
configs,
'like',
);
if (configuredBlock) {
return configuredBlock as PlatformPublicWorkLikeIntent;
}
if (isBigFishGalleryEntry(entry)) {
return {
type: 'like-big-fish',
@@ -760,7 +814,17 @@ export function resolvePlatformPublicWorkLikeIntent(
export function resolvePlatformPublicWorkRemixIntent(
entry: PlatformPublicGalleryCard,
configs?: readonly PlatformPublicWorkInteractionConfig[] | null,
): PlatformPublicWorkRemixIntent {
const configuredBlock = resolveConfiguredPublicWorkInteractionBlock(
entry,
configs,
'remix',
);
if (configuredBlock) {
return configuredBlock as PlatformPublicWorkRemixIntent;
}
if (isBigFishGalleryEntry(entry)) {
return {
type: 'remix-big-fish',
@@ -933,8 +997,9 @@ export function resolvePlatformPublicWorkEditIntent(
if (isVisualNovelGalleryEntry(entry)) {
const work =
deps.visualNovelWorks?.find((item) => item.profileId === entry.profileId) ??
null;
deps.visualNovelWorks?.find(
(item) => item.profileId === entry.profileId,
) ?? null;
if (!work) {
return {
type: 'blocked',

View File

@@ -748,7 +748,7 @@ test('顶部不显示作者,关卡标题和倒计时使用游戏铭牌结构',
const levelLogo = screen.getByTestId(
'puzzle-runtime-level-logo',
) as HTMLImageElement;
expect(levelLogo.getAttribute('src')).toContain('logo.png');
expect(levelLogo.getAttribute('src')).toContain('logo-runtime-hud.webp');
expect(levelLogo.closest('.puzzle-runtime-level-logo')).toBeTruthy();
expect(document.querySelector('.puzzle-runtime-level-mascot')).toBeNull();
expect(timer.closest('.puzzle-runtime-timer-card')).toBeTruthy();

View File

@@ -18,7 +18,7 @@ import {
} from 'react';
import { createPortal } from 'react-dom';
import puzzleLevelLogo from '../../../media/logo.png';
import puzzleLevelLogo from '../../../media/logo-runtime-hud.webp';
import type {
DragPuzzlePieceRequest,
PuzzleBoardSnapshot,

View File

@@ -3801,11 +3801,6 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
expect(await findCreationTypeButton('汪汪声浪')).toBeTruthy();
expect(await findCreationTypeButton('宝贝识物')).toBeTruthy();
expect(queryCreationTypeButton('智能创作')).toBeNull();
expect(
screen
.getByRole('tab', { name: '热门推荐' })
.querySelector('[class*="bg-[#d9793f]"]'),
).toBeTruthy();
expect(screen.queryByRole('button', { name: /智能创作/u })).toBeNull();
expect(screen.queryByPlaceholderText('问一问陶泥儿')).toBeNull();
expect(screen.queryByRole('button', { name: /角色扮演/u })).toBeNull();
@@ -7855,6 +7850,113 @@ test('logged out home recommendation next starts the next puzzle work', async ()
});
});
test('home recommendation next follows the same scored queue shown in preview', async () => {
const user = userEvent.setup();
const quietWork = {
workId: 'puzzle-work-public-quiet',
profileId: 'puzzle-profile-public-quiet',
ownerUserId: 'user-2',
sourceSessionId: 'puzzle-session-public-quiet',
authorDisplayName: '拼图作者',
levelName: '安静拼图',
summary: '列表里排在前面但热度较低。',
themeTags: ['安静', '拼图'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-04-25T10:00:00.000Z',
publishedAt: '2026-04-25T10:00:00.000Z',
playCount: 40,
likeCount: 0,
publishReady: true,
} satisfies PuzzleWorkSummary;
const hotWork = {
...quietWork,
workId: 'puzzle-work-public-hot',
profileId: 'puzzle-profile-public-hot',
sourceSessionId: 'puzzle-session-public-hot',
levelName: '热门拼图',
summary: '推荐评分更高,应该先展示。',
playCount: 120,
updatedAt: '2026-04-25T09:00:00.000Z',
publishedAt: '2026-04-25T09:00:00.000Z',
} satisfies PuzzleWorkSummary;
const middleWork = {
...quietWork,
workId: 'puzzle-work-public-middle',
profileId: 'puzzle-profile-public-middle',
sourceSessionId: 'puzzle-session-public-middle',
levelName: '中间拼图',
summary: '推荐评分排在后面。',
playCount: 0,
updatedAt: '2026-04-25T08:00:00.000Z',
publishedAt: '2026-04-25T08:00:00.000Z',
} satisfies PuzzleWorkSummary;
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [quietWork, hotWork, middleWork],
});
vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
item:
profileId === hotWork.profileId
? hotWork
: profileId === middleWork.profileId
? middleWork
: quietWork,
}));
vi.mocked(startPuzzleRun).mockImplementation(async (payload) => ({
run: buildMockPuzzleRun(
payload.profileId,
payload.profileId === hotWork.profileId
? hotWork.levelName
: payload.profileId === middleWork.profileId
? middleWork.levelName
: quietWork.levelName,
),
}));
render(<TestWrapper withAuth />);
await waitFor(() => {
expect(startPuzzleRun).toHaveBeenCalledWith(
{
profileId: hotWork.profileId,
levelId: null,
},
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
);
});
expect(
await screen.findByLabelText('热门拼图 作品信息', undefined, {
timeout: 3000,
}),
).toBeTruthy();
const nextPreview = document.querySelector(
'.platform-recommend-swipe-page--next',
);
expect(nextPreview).toBeTruthy();
expect(
within(nextPreview as HTMLElement).getByLabelText('安静拼图 作品信息'),
).toBeTruthy();
await user.click(await screen.findByRole('button', { name: '下一个' }));
await waitFor(() => {
expect(startPuzzleRun).toHaveBeenCalledWith(
{
profileId: quietWork.profileId,
levelId: null,
},
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
);
});
expect(
await screen.findByLabelText('安静拼图 作品信息', undefined, {
timeout: 3000,
}),
).toBeTruthy();
});
test('home recommendation keeps cover while switching during a pending puzzle start', async () => {
const user = userEvent.setup();
const firstWork = {
@@ -8184,6 +8286,54 @@ test('home recommendation Match3D runtime keeps profile generated models when ca
});
});
test('logged out home recommendation Match3D runtime skips protected detail and starts with guest auth', async () => {
const match3dCard: Match3DWorkSummary = {
workId: 'match3d-work-card-guest',
profileId: 'match3d-profile-card-guest',
ownerUserId: 'user-2',
sourceSessionId: 'match3d-session-card-guest',
gameName: '游客抓大鹅',
themeText: '游客果园',
summary: '游客可直接游玩。',
tags: ['果园', '抓大鹅'],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 3,
difficulty: 5,
publicationStatus: 'published',
playCount: 3,
updatedAt: '2026-04-25T10:30:00.000Z',
publishedAt: '2026-04-25T10:30:00.000Z',
publishReady: true,
generatedItemAssets: [],
};
vi.mocked(listMatch3DGallery).mockResolvedValue({
items: [match3dCard],
});
vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
item: match3dCard,
});
match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
run: buildMockMatch3DRun(match3dCard.profileId),
});
render(<TestWrapper />);
await waitFor(() => {
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
'match3d-profile-card-guest',
expect.objectContaining({
runtimeGuestToken: 'runtime-guest-token',
skipRefresh: true,
}),
);
});
expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
'match3d-profile-card-guest',
);
});
test('home recommendation Match3D runtime keeps image, music and UI assets without requiring models', async () => {
const match3dCard: Match3DWorkSummary = {
workId: 'match3d-work-card-image-only',
@@ -9400,6 +9550,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
elapsedMs: 18_000,
nickname: '测试玩家',
},
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
);
});
@@ -9420,6 +9571,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
clearedFirstLevel.runId,
{},
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
);
});
expect(
@@ -9582,6 +9734,7 @@ test('formal puzzle similar work keeps current run level progression', async ()
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
clearedThirdLevel.runId,
{ targetProfileId: 'puzzle-profile-similar-2' },
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
);
});
expect(startPuzzleRun).not.toHaveBeenCalled();
@@ -10326,7 +10479,7 @@ test('existing draft sessions open result page refinement instead of agent dialo
expect(screen.getByText(/基本设定/u)).toBeTruthy();
expect(screen.queryByRole('button', { name: /新增场景角色/u })).toBeNull();
await user.click(screen.getByRole('button', { name: /场景角色/u }));
await user.click(screen.getByRole('tab', { name: /场景角色/u }));
expect(screen.getByRole('button', { name: /顾潮音/u })).toBeTruthy();
await user.click(screen.getByRole('button', { name: /顾潮音/u }));
expect(await screen.findByText(/编辑场景角色:顾潮音/u)).toBeTruthy();
@@ -12256,7 +12409,7 @@ test('creation hub published work edit keeps loaded detail profile assets instea
document.querySelector('video[src="/assets/custom-world/opening.mp4"]'),
).toBeTruthy();
await user.click(screen.getByRole('button', { name: /场景\s+2/u }));
await user.click(screen.getByRole('tab', { name: /场景\s+2/u }));
expect((await screen.findByAltText('废都营地')).getAttribute('src')).toBe(
'/assets/custom-world/star-waste-camp.png',
);
@@ -12264,12 +12417,12 @@ test('creation hub published work edit keeps loaded detail profile assets instea
'/assets/custom-world/act-stardust-opening-1.png',
);
await user.click(screen.getByRole('button', { name: /可扮演角色\s+1/u }));
await user.click(screen.getByRole('tab', { name: /可扮演角色\s+1/u }));
expect((await screen.findByAltText('砂眠')).getAttribute('src')).toBe(
'/assets/custom-world/playable-stardust-1.png',
);
await user.click(screen.getByRole('button', { name: /场景角色\s+1/u }));
await user.click(screen.getByRole('tab', { name: /场景角色\s+1/u }));
expect((await screen.findByAltText('钟守')).getAttribute('src')).toBe(
'/assets/custom-world/story-clock-keeper-1.png',
);

View File

@@ -189,6 +189,27 @@ test('public gallery ViewModel builds recommend feed from general public entries
).toEqual([latestPuzzle]);
});
test('public gallery ViewModel keeps recommend feed in scored runtime order', () => {
const quietPuzzle = buildPuzzleEntry({
profileId: 'quiet',
worldName: '安静拼图',
playCount: 0,
updatedAt: '2026-05-03T00:00:00.000Z',
publishedAt: '2026-05-03T00:00:00.000Z',
});
const hotPuzzle = buildPuzzleEntry({
profileId: 'hot',
worldName: '热门拼图',
playCount: 120,
updatedAt: '2026-05-02T00:00:00.000Z',
publishedAt: '2026-05-02T00:00:00.000Z',
});
expect(buildPlatformRecommendFeedEntries([], [quietPuzzle, hotPuzzle])).toEqual(
[hotPuzzle, quietPuzzle],
);
});
test('public gallery ViewModel selects recommend feed window with wraparound neighbors', () => {
const firstEntry = buildPuzzleEntry({ profileId: 'first' });
const secondEntry = buildJumpHopEntry({ profileId: 'second' });

View File

@@ -1,5 +1,6 @@
import { filterGeneralPublicWorks } from '../platform-entry/platformEdutainmentVisibility';
import { getPlatformPublicGalleryEntryKey } from '../platform-entry/platformPublicGalleryFlow';
import { buildPlatformRecommendedEntries } from '../platform-entry/platformRecommendation';
import {
buildPlatformWorldDisplayTags,
isBarkBattleGalleryEntry,
@@ -146,9 +147,10 @@ export function buildPlatformRecommendFeedEntries(
featuredEntries: PlatformPublicGalleryCard[],
latestEntries: PlatformPublicGalleryCard[],
) {
return dedupePlatformPublicGalleryEntries(
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]),
);
return buildPlatformRecommendedEntries({
featuredEntries: filterGeneralPublicWorks(featuredEntries),
latestEntries: filterGeneralPublicWorks(latestEntries),
});
}
export function selectAdjacentPlatformRecommendEntry(

View File

@@ -8,7 +8,7 @@ import {
useState,
} from 'react';
import woodenFishRuntimeLogo from '../../../media/logo.png';
import woodenFishRuntimeLogo from '../../../media/logo-runtime-hud.webp';
import type {
WoodenFishRuntimeRunSnapshotResponse,
WoodenFishWordCounter,