fix: tighten public work type routing

This commit is contained in:
2026-06-07 14:49:36 +08:00
parent 8f460feb41
commit 8131894bb5
21 changed files with 611 additions and 228 deletions

View File

@@ -395,6 +395,7 @@ import {
buildPlatformPublicGalleryCardKey,
isBarkBattleGalleryEntry,
isBigFishGalleryEntry,
isCustomWorldGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isMatch3DGalleryEntry,
@@ -701,6 +702,10 @@ function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
function getPlatformRecommendRuntimeKind(
entry: PlatformPublicGalleryCard,
): RecommendRuntimeKind {
if (isCustomWorldGalleryEntry(entry)) {
return 'rpg';
}
if (isBigFishGalleryEntry(entry)) {
return 'big-fish';
}
@@ -741,7 +746,7 @@ function getPlatformRecommendRuntimeKind(
return 'edutainment';
}
return 'rpg';
throw new Error('未知公开作品类型,无法启动推荐玩法。');
}
function resolveRecommendEntryShareStage(
@@ -758,6 +763,14 @@ function resolveRecommendEntryShareStage(
return 'work-detail';
}
function resolveUnsupportedPublicWorkActionMessage(
entry: PlatformPublicGalleryCard,
actionLabel: string,
) {
const sourceType = 'sourceType' in entry ? entry.sourceType : 'custom-world';
return `作品类型 ${sourceType} 暂不支持${actionLabel}`;
}
function isRecommendRuntimeReadyForEntry(
entry: PlatformPublicGalleryCard,
state: RecommendRuntimeState,
@@ -800,8 +813,11 @@ function isRecommendRuntimeReadyForEntry(
if (expectedKind === 'edutainment') {
return Boolean(state.babyObjectMatchDraft);
}
if (expectedKind === 'rpg') {
return true;
}
return true;
throw new Error('未知推荐玩法类型。');
}
function isSamePlatformPublicGalleryEntry(
@@ -834,7 +850,10 @@ function mergePlatformPublicGalleryEntries(
function mapRpgGalleryCardToPublicWorkDetail(
entry: CustomWorldGalleryCard,
): PlatformPublicGalleryCard {
return entry;
return {
...entry,
sourceType: 'custom-world',
};
}
function mapPuzzleWorkToPublicWorkDetail(
@@ -13521,54 +13540,55 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isEdutainmentGalleryEntry(entry)) {
setPublicWorkDetailError('宝贝识物点赞将在后续版本开放。');
if (isCustomWorldGalleryEntry(entry)) {
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((updatedEntry) => {
setSelectedDetailEntry((current) =>
current?.profileId === updatedEntry.profileId
? updatedEntry
: current,
);
platformBootstrap.setPublishedGalleryEntries((current) =>
current.map((item) =>
item.profileId === updatedEntry.profileId
? updatedEntry
: item,
),
);
syncUpdatedPublicWorkDetail(
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '点赞 RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (
isPuzzleClearGalleryEntry(entry) ||
isJumpHopGalleryEntry(entry) ||
isWoodenFishGalleryEntry(entry) ||
isMatch3DGalleryEntry(entry) ||
isEdutainmentGalleryEntry(entry) ||
isBarkBattleGalleryEntry(entry) ||
isSquareHoleGalleryEntry(entry) ||
isVisualNovelGalleryEntry(entry)
) {
setPublicWorkDetailError(
resolveUnsupportedPublicWorkActionMessage(entry, '点赞'),
);
setIsPublicWorkDetailBusy(false);
return;
}
if (isBarkBattleGalleryEntry(entry)) {
setPublicWorkDetailError('汪汪声浪点赞将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isSquareHoleGalleryEntry(entry)) {
setPublicWorkDetailError('方洞挑战点赞将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isVisualNovelGalleryEntry(entry)) {
setPublicWorkDetailError('视觉小说点赞将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((updatedEntry) => {
setSelectedDetailEntry((current) =>
current?.profileId === updatedEntry.profileId
? updatedEntry
: current,
);
platformBootstrap.setPublishedGalleryEntries((current) =>
current.map((item) =>
item.profileId === updatedEntry.profileId ? updatedEntry : item,
),
);
syncUpdatedPublicWorkDetail(
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '点赞 RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
setPublicWorkDetailError('未知公开作品类型,无法点赞。');
setIsPublicWorkDetailBusy(false);
});
},
[
@@ -14189,7 +14209,12 @@ export function PlatformEntryFlowShellImpl({
return;
}
void openRpgPublicWorkDetail(entry);
if (isCustomWorldGalleryEntry(entry)) {
void openRpgPublicWorkDetail(entry);
return;
}
setPublicWorkDetailError('未知公开作品类型,无法打开作品详情。');
},
[
openPuzzlePublicWorkDetail,
@@ -15506,14 +15531,6 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isJumpHopGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startJumpHopRunFromProfile(selectedPublicWorkDetail.profileId, {
returnStage: 'work-detail',
});
return;
}
if (isWoodenFishGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startWoodenFishRunFromProfile(selectedPublicWorkDetail.profileId, {
@@ -15588,6 +15605,11 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (!isCustomWorldGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError('未知公开作品类型,无法进入玩法。');
return;
}
const launchEntry =
selectedDetailEntry?.profileId === selectedPublicWorkDetail.profileId
? selectedDetailEntry
@@ -15759,8 +15781,10 @@ export function PlatformEntryFlowShellImpl({
embedded: true,
},
);
} else {
} else if (isCustomWorldGalleryEntry(entry)) {
started = true;
} else {
throw new Error('未知公开作品类型,无法启动推荐玩法。');
}
if (!isCurrentStartRequest()) {
@@ -16403,74 +16427,49 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isPuzzleClearGalleryEntry(entry)) {
setPublicWorkDetailError('拼消消作品改造将在后续版本开放。');
if (
isPuzzleClearGalleryEntry(entry) ||
isMatch3DGalleryEntry(entry) ||
isSquareHoleGalleryEntry(entry) ||
isJumpHopGalleryEntry(entry) ||
isWoodenFishGalleryEntry(entry) ||
isVisualNovelGalleryEntry(entry) ||
isEdutainmentGalleryEntry(entry) ||
isBarkBattleGalleryEntry(entry)
) {
setPublicWorkDetailError(
resolveUnsupportedPublicWorkActionMessage(entry, '改造'),
);
setIsPublicWorkDetailBusy(false);
return;
}
if (isMatch3DGalleryEntry(entry)) {
setPublicWorkDetailError('抓大鹅作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
if (isCustomWorldGalleryEntry(entry)) {
void remixRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((response) => {
const nextEntry = response.entry;
setSelectedDetailEntry(nextEntry);
platformBootstrap.setSavedCustomWorldEntries([
nextEntry,
...platformBootstrap.savedCustomWorldEntries.filter(
(entry) => entry.profileId !== nextEntry.profileId,
),
]);
void detailNavigation.openSavedCustomWorldEditor(nextEntry);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, 'Remix RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (isSquareHoleGalleryEntry(entry)) {
setPublicWorkDetailError('方洞挑战作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isJumpHopGalleryEntry(entry)) {
setPublicWorkDetailError('跳一跳作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isWoodenFishGalleryEntry(entry)) {
setPublicWorkDetailError('敲木鱼作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isVisualNovelGalleryEntry(entry)) {
setPublicWorkDetailError('视觉小说作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isEdutainmentGalleryEntry(entry)) {
setPublicWorkDetailError('宝贝识物作品改造将在创作链路接入后开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isBarkBattleGalleryEntry(entry)) {
setPublicWorkDetailError('汪汪声浪作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
void remixRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((response) => {
const nextEntry = response.entry;
setSelectedDetailEntry(nextEntry);
platformBootstrap.setSavedCustomWorldEntries([
nextEntry,
...platformBootstrap.savedCustomWorldEntries.filter(
(entry) => entry.profileId !== nextEntry.profileId,
),
]);
void detailNavigation.openSavedCustomWorldEditor(nextEntry);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, 'Remix RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
setPublicWorkDetailError('未知公开作品类型,无法改造。');
setIsPublicWorkDetailBusy(false);
});
},
[
@@ -16630,6 +16629,11 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (!isCustomWorldGalleryEntry(entry)) {
setPublicWorkDetailError('未知公开作品类型,无法编辑。');
return;
}
const editEntry =
selectedDetailEntry?.profileId === entry.profileId
? selectedDetailEntry
@@ -16737,6 +16741,7 @@ export function PlatformEntryFlowShellImpl({
const entry =
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
const card = {
sourceType: 'custom-world',
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
publicWorkCode: entry.publicWorkCode,
@@ -16755,7 +16760,7 @@ export function PlatformEntryFlowShellImpl({
playCount: entry.playCount ?? 0,
remixCount: entry.remixCount ?? 0,
likeCount: entry.likeCount ?? 0,
} satisfies CustomWorldGalleryCard;
} satisfies PlatformPublicGalleryCard;
if (!canExposePublicWork(card)) {
throw new Error(EDUTAINMENT_HIDDEN_MESSAGE);
}

View File

@@ -24,7 +24,11 @@ import {
formatPlatformWorkDisplayTags,
formatPlatformWorldTime,
isBarkBattleGalleryEntry,
isCustomWorldGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isPuzzleClearGalleryEntry,
isWoodenFishGalleryEntry,
type PlatformPublicGalleryCard,
resolvePlatformWorkAuthorDisplayName,
resolvePlatformPublicWorkCode,
@@ -57,9 +61,18 @@ function getSourceLabel(entry: PlatformPublicGalleryCard) {
if ('sourceType' in entry && entry.sourceType === 'puzzle') {
return '拼图';
}
if (isPuzzleClearGalleryEntry(entry)) {
return '拼消消';
}
if ('sourceType' in entry && entry.sourceType === 'big-fish') {
return '大鱼吃小鱼';
}
if (isJumpHopGalleryEntry(entry)) {
return '跳一跳';
}
if (isWoodenFishGalleryEntry(entry)) {
return '敲木鱼';
}
if ('sourceType' in entry && entry.sourceType === 'match3d') {
return '抓大鹅';
}
@@ -75,7 +88,11 @@ function getSourceLabel(entry: PlatformPublicGalleryCard) {
if (isEdutainmentGalleryEntry(entry)) {
return entry.templateName;
}
return 'RPG';
if (isCustomWorldGalleryEntry(entry)) {
return 'RPG';
}
throw new Error('未知公开作品类型。');
}
function getAuthorAvatarLabel(authorDisplayName: string) {

View File

@@ -1,5 +1,6 @@
import { afterEach, expect, test, vi } from 'vitest';
import type { CreationEntryTypeConfig } from '../../services/creationEntryConfigService';
import {
derivePlatformCreationTypes,
groupVisiblePlatformCreationTypes,
@@ -81,7 +82,7 @@ test('database entry config controls visibility open state and display order', (
test('visible platform creation types hide invisible cards and put locked cards last', () => {
const cards = derivePlatformCreationTypes([
{
id: 'hidden',
id: 'airp',
title: '隐藏',
subtitle: '隐藏',
badge: '隐藏',
@@ -95,7 +96,7 @@ test('visible platform creation types hide invisible cards and put locked cards
updatedAtMicros: 1,
},
{
id: 'locked',
id: 'visual-novel',
title: '锁定',
subtitle: '锁定',
badge: '即将开放',
@@ -109,7 +110,7 @@ test('visible platform creation types hide invisible cards and put locked cards
updatedAtMicros: 1,
},
{
id: 'open',
id: 'rpg',
title: '开放',
subtitle: '开放',
badge: '可创建',
@@ -125,13 +126,13 @@ test('visible platform creation types hide invisible cards and put locked cards
]);
expect(getVisiblePlatformCreationTypes(cards).map((item) => item.id)).toEqual(
['open', 'locked'],
['rpg', 'visual-novel'],
);
expect(isPlatformCreationTypeVisible(cards, 'hidden')).toBe(false);
expect(isPlatformCreationTypeVisible(cards, 'open')).toBe(true);
expect(isPlatformCreationTypeOpen(cards, 'hidden')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'locked')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'open')).toBe(true);
expect(isPlatformCreationTypeVisible(cards, 'airp')).toBe(false);
expect(isPlatformCreationTypeVisible(cards, 'rpg')).toBe(true);
expect(isPlatformCreationTypeOpen(cards, 'airp')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'visual-novel')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'rpg')).toBe(true);
expect(
cards.every((item) =>
item.imageSrc.startsWith('/creation-type-references/'),
@@ -288,7 +289,7 @@ test('groups visible platform creation types by backend category metadata', () =
updatedAtMicros: 1,
},
{
id: 'hidden',
id: 'airp',
title: '隐藏入口',
subtitle: '隐藏',
badge: '隐藏',
@@ -319,7 +320,7 @@ test('groups visible platform creation types by backend category metadata', () =
test('falls back when backend creation type category metadata is missing', () => {
const cards = derivePlatformCreationTypes([
{
id: 'legacy-entry',
id: 'creative-agent',
title: '历史入口',
subtitle: '旧数据缺少分类字段',
badge: '可创建',
@@ -336,7 +337,7 @@ test('falls back when backend creation type category metadata is missing', () =>
expect(cards[0]).toEqual(
expect.objectContaining({
id: 'legacy-entry',
id: 'creative-agent',
categoryId: 'recommended',
categoryLabel: '热门推荐',
}),
@@ -348,3 +349,24 @@ test('falls back when backend creation type category metadata is missing', () =>
}),
]);
});
test('throws when backend sends an unknown creation type id', () => {
const unknownEntry = {
id: 'unknown-play',
title: '未知玩法',
subtitle: '未知',
badge: '未知',
imageSrc: '/creation-type-references/puzzle.webp',
visible: true,
open: true,
sortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
} as unknown as CreationEntryTypeConfig;
expect(() => derivePlatformCreationTypes([unknownEntry])).toThrow(
'未知创作类型unknown-play',
);
});

View File

@@ -1,7 +1,11 @@
import {
assertPlatformCreationTypeId,
type PlatformCreationTypeId,
} from '../../../packages/shared/src/contracts/playTypes';
import type { CreationEntryTypeConfig } from '../../services/creationEntryConfigService';
import { isEdutainmentEntryEnabled } from './platformEdutainmentVisibility';
export type PlatformCreationTypeId = string;
export type { PlatformCreationTypeId };
export type PlatformCreationTypeCard = {
id: PlatformCreationTypeId;
@@ -117,21 +121,25 @@ export function derivePlatformCreationTypes(
): PlatformCreationTypeCard[] {
const orderedCards = [...creationTypes]
.sort((left, right) => left.sortOrder - right.sortOrder)
.map((item) => ({
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
imageSrc: item.imageSrc,
locked: !item.open,
categoryId: normalizeCategoryId(item.categoryId),
categoryLabel: normalizeCategoryLabel(item.categoryLabel),
categorySortOrder: item.categorySortOrder,
sortOrder: item.sortOrder,
hidden:
!item.visible ||
(item.id === 'baby-object-match' && !isEdutainmentEntryEnabled()),
}));
.map((item) => {
const id = assertPlatformCreationTypeId(item.id);
return {
id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
imageSrc: item.imageSrc,
locked: !item.open,
categoryId: normalizeCategoryId(item.categoryId),
categoryLabel: normalizeCategoryLabel(item.categoryLabel),
categorySortOrder: item.categorySortOrder,
sortOrder: item.sortOrder,
hidden:
!item.visible ||
(id === 'baby-object-match' && !isEdutainmentEntryEnabled()),
};
});
return [
...orderedCards.filter((item) => !item.hidden && !item.locked),

View File

@@ -1,6 +1,8 @@
import {
buildPlatformPublicGalleryCardKey,
isEdutainmentGalleryEntry,
type PlatformPublicGalleryCard,
resolvePlatformPublicWorkSourceType,
} from '../rpg-entry/rpgEntryWorldPresentation';
const MS_PER_DAY = 86_400_000;
@@ -70,19 +72,11 @@ function getRecommendationMetric(
}
function getRecommendationSourceType(entry: PlatformPublicGalleryCard) {
if ('sourceType' in entry) {
if (
entry.sourceType === 'edutainment' &&
'templateId' in entry &&
entry.templateId
) {
return `edutainment:${entry.templateId}`;
}
return entry.sourceType;
if (isEdutainmentGalleryEntry(entry)) {
return `edutainment:${entry.templateId}`;
}
return 'rpg';
return resolvePlatformPublicWorkSourceType(entry);
}
function getRecommendationThemeTags(entry: PlatformPublicGalleryCard) {