refactor: 收口公开码搜索映射

This commit is contained in:
2026-06-04 06:04:55 +08:00
parent 217cc881e6
commit e570d50e9f
9 changed files with 749 additions and 123 deletions

View File

@@ -226,15 +226,7 @@ import {
buildSquareHolePublicWorkCode,
buildVisualNovelPublicWorkCode,
buildWoodenFishPublicWorkCode,
isSameBabyObjectMatchPublicWorkCode,
isSameBarkBattlePublicWorkCode,
isSameBigFishPublicWorkCode,
isSameJumpHopPublicWorkCode,
isSameMatch3DPublicWorkCode,
isSamePuzzlePublicWorkCode,
isSameSquareHolePublicWorkCode,
isSameVisualNovelPublicWorkCode,
isSameWoodenFishPublicWorkCode,
} from '../../services/publicWorkCode';
import {
createPuzzleAgentSession,
@@ -356,8 +348,6 @@ import {
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
import {
isEdutainmentGalleryEntry,
mapBabyObjectMatchDraftToPlatformGalleryCard,
mapBarkBattleWorkToPlatformGalleryCard,
mapPuzzleWorkToPlatformGalleryCard,
type PlatformPublicGalleryCard,
resolvePlatformPublicWorkCode,
@@ -550,8 +540,18 @@ import {
resolveProfileWalletBalance,
} from './platformProfileWalletDeltaModel';
import {
mapRpgPublicCodeSearchDetailToGalleryCard,
type PlatformPublicCodeSearchStep,
resolveBabyObjectMatchPublicCodeSearchMatch,
resolveBarkBattlePublicCodeSearchMatch,
resolveBigFishPublicCodeSearchMatch,
resolveJumpHopPublicCodeSearchMatch,
resolveMatch3DPublicCodeSearchMatch,
resolvePlatformPublicCodeSearchPlan,
resolvePuzzlePublicCodeSearchMatch,
resolveSquareHolePublicCodeSearchMatch,
resolveVisualNovelPublicCodeSearchMatch,
resolveWoodenFishPublicCodeSearchMatch,
} from './platformPublicCodeSearchModel';
import {
buildPlatformPublicGalleryFeeds,
@@ -12281,26 +12281,7 @@ export function PlatformEntryFlowShellImpl({
const tryOpenGalleryEntry = async () => {
const entry =
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
const card = {
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
publicWorkCode: entry.publicWorkCode,
authorPublicUserCode: entry.authorPublicUserCode,
visibility: 'published',
publishedAt: entry.publishedAt,
updatedAt: entry.updatedAt,
authorDisplayName: entry.authorDisplayName,
worldName: entry.worldName,
subtitle: entry.subtitle,
summaryText: entry.summaryText,
coverImageSrc: entry.coverImageSrc,
themeMode: entry.themeMode,
playableNpcCount: entry.playableNpcCount,
landmarkCount: entry.landmarkCount,
playCount: entry.playCount ?? 0,
remixCount: entry.remixCount ?? 0,
likeCount: entry.likeCount ?? 0,
} satisfies CustomWorldGalleryCard;
const card = mapRpgPublicCodeSearchDetailToGalleryCard(entry);
if (!canExposePublicWork(card)) {
throw new Error(EDUTAINMENT_HIDDEN_MESSAGE);
}
@@ -12313,18 +12294,16 @@ export function PlatformEntryFlowShellImpl({
puzzleGalleryEntries.length > 0
? puzzleGalleryEntries
: await refreshPuzzleGallery();
const matchedEntry = entries
.map(mapPuzzleWorkToPublicWorkDetail)
.filter(canExposePublicWork)
.find((entry) =>
isSamePuzzlePublicWorkCode(normalizedKeyword, entry.profileId),
);
const matchedEntry = resolvePuzzlePublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到拼图作品。');
}
await openPuzzlePublicWorkDetail(matchedEntry.profileId, {
await openPuzzlePublicWorkDetail(matchedEntry.detail.profileId, {
tab: platformBootstrap.platformTab,
});
};
@@ -12333,170 +12312,133 @@ export function PlatformEntryFlowShellImpl({
bigFishGalleryEntries.length > 0
? bigFishGalleryEntries
: await refreshBigFishGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapBigFishWorkToPublicWorkDetail(entry);
return (
canExposePublicWork(detailEntry) &&
isSameBigFishPublicWorkCode(
normalizedKeyword,
entry.sourceSessionId,
)
);
});
const matchedEntry = resolveBigFishPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到大鱼吃小鱼作品。');
}
openPublicWorkDetail(mapBigFishWorkToPublicWorkDetail(matchedEntry));
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenJumpHopGalleryEntry = async () => {
const entries =
jumpHopGalleryEntries.length > 0
? jumpHopGalleryEntries
: await refreshJumpHopGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapJumpHopWorkToPublicWorkDetail(entry);
return (
canExposePublicWork(detailEntry) &&
isSameJumpHopPublicWorkCode(normalizedKeyword, entry.profileId)
);
});
const matchedEntry = resolveJumpHopPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到跳一跳作品。');
}
openPublicWorkDetail(mapJumpHopWorkToPublicWorkDetail(matchedEntry));
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenWoodenFishGalleryEntry = async () => {
const entries =
woodenFishGalleryEntries.length > 0
? woodenFishGalleryEntries
: await refreshWoodenFishGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapWoodenFishWorkToPublicWorkDetail(entry);
return (
canExposePublicWork(detailEntry) &&
isSameWoodenFishPublicWorkCode(normalizedKeyword, entry.profileId)
);
});
const matchedEntry = resolveWoodenFishPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到敲木鱼作品。');
}
openPublicWorkDetail(mapWoodenFishWorkToPublicWorkDetail(matchedEntry));
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenMatch3DGalleryEntry = async () => {
const entries =
match3dGalleryEntries.length > 0
? match3dGalleryEntries
: await refreshMatch3DGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapMatch3DWorkToPublicWorkDetail(entry);
return (
canExposePublicWork(detailEntry) &&
isSameMatch3DPublicWorkCode(normalizedKeyword, entry.profileId)
);
});
const matchedEntry = resolveMatch3DPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到抓大鹅作品。');
}
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(matchedEntry));
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenSquareHoleGalleryEntry = async () => {
const entries =
squareHoleGalleryEntries.length > 0
? squareHoleGalleryEntries
: await refreshSquareHoleGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapSquareHoleWorkToPublicWorkDetail(entry);
return (
canExposePublicWork(detailEntry) &&
isSameSquareHolePublicWorkCode(normalizedKeyword, entry.profileId)
);
});
const matchedEntry = resolveSquareHolePublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到方洞挑战作品。');
}
openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(matchedEntry));
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenVisualNovelGalleryEntry = async () => {
const entries =
visualNovelGalleryEntries.length > 0
? visualNovelGalleryEntries
: await refreshVisualNovelGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapVisualNovelWorkToPublicWorkDetail(entry);
return (
canExposePublicWork(detailEntry) &&
isSameVisualNovelPublicWorkCode(normalizedKeyword, entry.profileId)
);
});
const matchedEntry = resolveVisualNovelPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到视觉小说作品。');
}
openPublicWorkDetail(
mapVisualNovelWorkToPublicWorkDetail(matchedEntry),
);
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenBabyObjectMatchGalleryEntry = async () => {
const entries = (await listLocalBabyObjectMatchDrafts()).filter(
(draft) => draft.publicationStatus === 'published',
);
const matchedDraft = entries.find((draft) => {
const detailEntry =
mapBabyObjectMatchDraftToPlatformGalleryCard(draft);
return (
canExposePublicWork(detailEntry) &&
isSameBabyObjectMatchPublicWorkCode(
normalizedKeyword,
draft.profileId,
)
);
});
const matchedDraft = resolveBabyObjectMatchPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedDraft) {
throw new Error('未找到宝贝识物作品。');
}
const detailEntry =
mapBabyObjectMatchDraftToPlatformGalleryCard(matchedDraft);
setBabyObjectMatchDraft(matchedDraft);
openPublicWorkDetail(detailEntry);
setBabyObjectMatchDraft(matchedDraft.item);
openPublicWorkDetail(matchedDraft.detail);
};
const tryOpenBarkBattleGalleryEntry = async () => {
const entries =
barkBattleGalleryEntries.length > 0
? barkBattleGalleryEntries
: await refreshBarkBattleGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapBarkBattleWorkToPlatformGalleryCard(entry);
return (
canExposePublicWork(detailEntry) &&
isSameBarkBattlePublicWorkCode(normalizedKeyword, entry.workId)
);
});
const matchedEntry = resolveBarkBattlePublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到汪汪声浪作品。');
}
if (selectionStage === 'bark-battle-runtime') {
startBarkBattleRunFromWork(matchedEntry, 'platform');
startBarkBattleRunFromWork(matchedEntry.item, 'platform');
return;
}
openPublicWorkDetail(
mapBarkBattleWorkToPlatformGalleryCard(matchedEntry),
);
openPublicWorkDetail(matchedEntry.detail);
};
const runSearchStep = async (step: PlatformPublicCodeSearchStep) => {

View File

@@ -1,8 +1,39 @@
import { describe, expect, test } from 'vitest';
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
import {
buildBarkBattlePublicWorkCode,
buildBigFishPublicWorkCode,
buildJumpHopPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzlePublicWorkCode,
buildSquareHolePublicWorkCode,
buildVisualNovelPublicWorkCode,
buildWoodenFishPublicWorkCode,
} from '../../services/publicWorkCode';
import type { CustomWorldProfile } from '../../types';
import {
mapRpgPublicCodeSearchDetailToGalleryCard,
type PlatformPublicCodeSearchStep,
resolveBabyObjectMatchPublicCodeSearchMatch,
resolveBarkBattlePublicCodeSearchMatch,
resolveBigFishPublicCodeSearchMatch,
resolveJumpHopPublicCodeSearchMatch,
resolveMatch3DPublicCodeSearchMatch,
resolvePlatformPublicCodeSearchPlan,
resolvePuzzlePublicCodeSearchMatch,
resolveSquareHolePublicCodeSearchMatch,
resolveVisualNovelPublicCodeSearchMatch,
resolveWoodenFishPublicCodeSearchMatch,
} from './platformPublicCodeSearchModel';
function expectSearchSteps(
@@ -66,4 +97,386 @@ describe('platformPublicCodeSearchModel', () => {
expectSearchSteps('SY-00000001', legacyFallbackSteps);
expectSearchSteps('月井守望', legacyFallbackSteps);
});
test('maps RPG detail responses to gallery cards with count defaults', () => {
expect(
mapRpgPublicCodeSearchDetailToGalleryCard(
buildRpgDetailEntry({
playCount: undefined,
remixCount: undefined,
likeCount: undefined,
}),
),
).toMatchObject({
profileId: 'rpg-profile-1',
visibility: 'published',
worldName: '潮雾世界',
playCount: 0,
remixCount: 0,
likeCount: 0,
});
});
test('resolves public code matches for every play-specific gallery type', () => {
const puzzle = buildPuzzleWork({ profileId: 'puzzle-profile-12345678' });
const bigFish = buildBigFishWork({
sourceSessionId: 'big-fish-session-12345678',
});
const jumpHop = buildJumpHopCard({ profileId: 'jump-hop-profile-12345678' });
const woodenFish = buildWoodenFishCard({
profileId: 'wooden-fish-profile-12345678',
});
const babyObjectMatch = buildBabyObjectMatchDraft({
profileId: 'baby-object-profile-12345678',
});
const match3d = buildMatch3DWork({ profileId: 'match3d-profile-12345678' });
const squareHole = buildSquareHoleWork({
profileId: 'square-hole-profile-12345678',
});
const visualNovel = buildVisualNovelWork({
profileId: 'visual-novel-profile-12345678',
});
const barkBattle = buildBarkBattleWork({
workId: 'bark-battle-work-12345678',
});
expect(
resolvePuzzlePublicCodeSearchMatch(
[puzzle],
buildPuzzlePublicWorkCode(puzzle.profileId),
)?.detail,
).toMatchObject({ sourceType: 'puzzle' });
expect(
resolveBigFishPublicCodeSearchMatch(
[bigFish],
buildBigFishPublicWorkCode(bigFish.sourceSessionId),
)?.detail,
).toMatchObject({ sourceType: 'big-fish' });
expect(
resolveJumpHopPublicCodeSearchMatch(
[jumpHop],
buildJumpHopPublicWorkCode(jumpHop.profileId),
)?.detail,
).toMatchObject({ sourceType: 'jump-hop' });
expect(
resolveWoodenFishPublicCodeSearchMatch(
[woodenFish],
buildWoodenFishPublicWorkCode(woodenFish.profileId),
)?.detail,
).toMatchObject({ sourceType: 'wooden-fish' });
expect(
resolveBabyObjectMatchPublicCodeSearchMatch(
[babyObjectMatch],
`BO-${babyObjectMatch.profileId.slice(-8)}`,
)?.detail,
).toMatchObject({ sourceType: 'edutainment' });
expect(
resolveMatch3DPublicCodeSearchMatch(
[match3d],
buildMatch3DPublicWorkCode(match3d.profileId),
)?.detail,
).toMatchObject({ sourceType: 'match3d' });
expect(
resolveSquareHolePublicCodeSearchMatch(
[squareHole],
buildSquareHolePublicWorkCode(squareHole.profileId),
)?.detail,
).toMatchObject({ sourceType: 'square-hole' });
expect(
resolveVisualNovelPublicCodeSearchMatch(
[visualNovel],
buildVisualNovelPublicWorkCode(visualNovel.profileId),
)?.detail,
).toMatchObject({ sourceType: 'visual-novel' });
expect(
resolveBarkBattlePublicCodeSearchMatch(
[barkBattle],
buildBarkBattlePublicWorkCode(barkBattle.workId),
)?.detail,
).toMatchObject({ sourceType: 'bark-battle' });
});
test('public code search matchers skip entries hidden by visibility policy', () => {
const hiddenPuzzle = buildPuzzleWork({
profileId: 'hidden-profile-12345678',
});
expect(
resolvePuzzlePublicCodeSearchMatch(
[hiddenPuzzle],
buildPuzzlePublicWorkCode(hiddenPuzzle.profileId),
() => false,
),
).toBeNull();
});
});
function buildRpgDetailEntry(
overrides: Partial<CustomWorldLibraryEntry<CustomWorldProfile>> = {},
): CustomWorldLibraryEntry<CustomWorldProfile> {
return {
ownerUserId: 'rpg-owner-1',
profileId: 'rpg-profile-1',
publicWorkCode: 'CW-00000001',
authorPublicUserCode: 'SY-00000001',
profile: {} as CustomWorldProfile,
visibility: 'published',
publishedAt: '2026-06-04T00:00:00.000Z',
updatedAt: '2026-06-04T00:00:00.000Z',
authorDisplayName: '测试作者',
worldName: '潮雾世界',
subtitle: '潮雾港',
summaryText: '潮雾世界说明。',
coverImageSrc: null,
themeMode: 'tide',
playableNpcCount: 1,
landmarkCount: 1,
playCount: 1,
remixCount: 1,
likeCount: 1,
...overrides,
};
}
function buildPuzzleWork(
overrides: Partial<PuzzleWorkSummary> = {},
): PuzzleWorkSummary {
return {
workId: 'puzzle-work-1',
profileId: 'puzzle-profile-1',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-1',
authorDisplayName: '测试作者',
workTitle: '潮雾拼图',
workDescription: '潮雾拼图说明。',
levelName: '潮雾拼图',
summary: '潮雾拼图说明。',
themeTags: [],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: true,
levels: [],
...overrides,
};
}
function buildBigFishWork(
overrides: Partial<BigFishWorkSummary> = {},
): BigFishWorkSummary {
return {
workId: 'big-fish-work-1',
sourceSessionId: 'big-fish-session-1',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
title: '潮雾大鱼',
subtitle: '潮雾港',
summary: '潮雾大鱼说明。',
coverImageSrc: null,
status: 'published',
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: true,
levelCount: 1,
levelMainImageReadyCount: 1,
levelMotionReadyCount: 1,
backgroundReady: true,
...overrides,
};
}
function buildJumpHopCard(
overrides: Partial<JumpHopGalleryCardResponse> = {},
): JumpHopGalleryCardResponse {
const profileId = overrides.profileId ?? 'jump-hop-profile-1';
return {
publicWorkCode: buildJumpHopPublicWorkCode(profileId),
workId: 'jump-hop-work-1',
profileId,
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
workTitle: '潮雾跳一跳',
workDescription: '潮雾跳一跳说明。',
coverImageSrc: null,
themeTags: [],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
publicationStatus: 'published',
playCount: 0,
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
generationStatus: 'ready',
...overrides,
};
}
function buildWoodenFishCard(
overrides: Partial<WoodenFishGalleryCardResponse> = {},
): WoodenFishGalleryCardResponse {
const profileId = overrides.profileId ?? 'wooden-fish-profile-1';
return {
publicWorkCode: buildWoodenFishPublicWorkCode(profileId),
workId: 'wooden-fish-work-1',
profileId,
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
workTitle: '潮雾木鱼',
workDescription: '潮雾木鱼说明。',
coverImageSrc: null,
themeTags: ['敲木鱼'],
publicationStatus: 'published',
playCount: 0,
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
generationStatus: 'ready',
...overrides,
};
}
function buildBabyObjectMatchDraft(
overrides: Partial<BabyObjectMatchDraft> = {},
): BabyObjectMatchDraft {
return {
draftId: 'baby-draft-1',
profileId: 'baby-object-profile-1',
templateId: 'baby-object-match',
templateName: '宝贝识物',
workTitle: '潮雾识物',
workDescription: '潮雾识物说明。',
itemNames: ['苹果', '香蕉'],
itemAssets: [
buildBabyObjectMatchItemAsset('item-a', '苹果'),
buildBabyObjectMatchItemAsset('item-b', '香蕉'),
],
visualPackage: null,
themeTags: ['寓教于乐'],
publicationStatus: 'published',
createdAt: '2026-06-04T00:00:00.000Z',
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
...overrides,
};
}
function buildBabyObjectMatchItemAsset(itemId: string, itemName: string) {
return {
itemId,
itemName,
imageSrc: `/media/${itemId}.png`,
assetObjectId: null,
generationProvider: 'placeholder' as const,
prompt: itemName,
};
}
function buildMatch3DWork(
overrides: Partial<Match3DWorkSummary> = {},
): Match3DWorkSummary {
return {
workId: 'match3d-work-1',
profileId: 'match3d-profile-1',
ownerUserId: 'user-1',
sourceSessionId: 'match3d-session-1',
gameName: '潮雾抓大鹅',
themeText: '潮雾港',
summary: '潮雾抓大鹅说明。',
tags: [],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 0,
difficulty: 1,
publicationStatus: 'published',
playCount: 0,
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
publishReady: true,
generationStatus: 'ready',
generatedItemAssets: [],
...overrides,
};
}
function buildSquareHoleWork(
overrides: Partial<SquareHoleWorkSummary> = {},
): SquareHoleWorkSummary {
return {
workId: 'square-hole-work-1',
profileId: 'square-hole-profile-1',
ownerUserId: 'user-1',
sourceSessionId: 'square-hole-session-1',
gameName: '潮雾方洞',
themeText: '潮雾港',
twistRule: '避开雾门',
summary: '潮雾方洞说明。',
tags: [],
coverImageSrc: null,
backgroundPrompt: '潮雾港',
backgroundImageSrc: null,
shapeOptions: [],
holeOptions: [],
shapeCount: 1,
difficulty: 1,
publicationStatus: 'published',
playCount: 0,
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
publishReady: true,
...overrides,
};
}
function buildVisualNovelWork(
overrides: Partial<VisualNovelWorkSummary> = {},
): VisualNovelWorkSummary {
return {
runtimeKind: 'visual-novel',
profileId: 'visual-novel-profile-1',
ownerUserId: 'user-1',
title: '潮雾视觉小说',
description: '潮雾视觉小说说明。',
coverImageSrc: null,
tags: [],
publishStatus: 'published',
publishReady: true,
playCount: 0,
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
...overrides,
};
}
function buildBarkBattleWork(
overrides: Partial<BarkBattleWorkSummary> = {},
): BarkBattleWorkSummary {
return {
workId: 'bark-battle-work-1',
draftId: 'bark-battle-draft-1',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
title: '潮雾声浪',
summary: '潮雾声浪说明。',
themeDescription: '潮雾港',
playerImageDescription: '小狗',
opponentImageDescription: '对手',
onomatopoeia: ['汪'],
playerCharacterImageSrc: null,
opponentCharacterImageSrc: null,
uiBackgroundImageSrc: null,
difficultyPreset: 'normal',
status: 'published',
generationStatus: 'ready',
publishReady: true,
playCount: 0,
updatedAt: '2026-06-04T00:00:00.000Z',
publishedAt: '2026-06-04T00:00:00.000Z',
...overrides,
};
}

View File

@@ -1,3 +1,42 @@
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
import {
isSameBabyObjectMatchPublicWorkCode,
isSameBarkBattlePublicWorkCode,
isSameBigFishPublicWorkCode,
isSameJumpHopPublicWorkCode,
isSameMatch3DPublicWorkCode,
isSamePuzzlePublicWorkCode,
isSameSquareHolePublicWorkCode,
isSameVisualNovelPublicWorkCode,
isSameWoodenFishPublicWorkCode,
} from '../../services/publicWorkCode';
import type { CustomWorldProfile } from '../../types';
import type { PlatformPublicGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
import { mapBabyObjectMatchDraftToPlatformGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
import { canExposePublicWork } from './platformEdutainmentVisibility';
import { mapMatch3DWorkToPublicWorkDetail } from './platformMatch3DRuntimeProfile';
import {
mapBarkBattleWorkToPublicWorkDetail,
mapBigFishWorkToPublicWorkDetail,
mapJumpHopWorkToPublicWorkDetail,
mapPuzzleWorkToPublicWorkDetail,
mapSquareHoleWorkToPublicWorkDetail,
mapVisualNovelWorkToPublicWorkDetail,
mapWoodenFishWorkToPublicWorkDetail,
} from './platformPublicWorkDetailFlow';
export type PlatformPublicCodeSearchStep =
| 'user-id'
| 'public-user-code'
@@ -17,6 +56,19 @@ export type PlatformPublicCodeSearchPlan = {
steps: readonly PlatformPublicCodeSearchStep[];
};
export type PlatformPublicCodeSearchMatch<TItem> = {
item: TItem;
detail: PlatformPublicGalleryCard;
};
type PlatformPublicCodeSearchMatcherInput<TItem> = {
keyword: string;
entries: readonly TItem[];
mapEntry: (item: TItem) => PlatformPublicGalleryCard;
matchesEntry: (keyword: string, item: TItem) => boolean;
canExposeEntry?: (entry: PlatformPublicGalleryCard) => boolean;
};
const PLATFORM_PUBLIC_USER_ID_PATTERN = /^user[_-][a-z0-9_-]+$/iu;
const PLATFORM_RPG_WORK_NUMERIC_CODE_PATTERN = /^\d{1,8}$/u;
@@ -81,3 +133,181 @@ export function resolvePlatformPublicCodeSearchPlan(
],
};
}
export function mapRpgPublicCodeSearchDetailToGalleryCard(
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
): CustomWorldGalleryCard {
return {
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
publicWorkCode: entry.publicWorkCode,
authorPublicUserCode: entry.authorPublicUserCode,
visibility: 'published',
publishedAt: entry.publishedAt,
updatedAt: entry.updatedAt,
authorDisplayName: entry.authorDisplayName,
worldName: entry.worldName,
subtitle: entry.subtitle,
summaryText: entry.summaryText,
coverImageSrc: entry.coverImageSrc,
themeMode: entry.themeMode,
playableNpcCount: entry.playableNpcCount,
landmarkCount: entry.landmarkCount,
playCount: entry.playCount ?? 0,
remixCount: entry.remixCount ?? 0,
likeCount: entry.likeCount ?? 0,
};
}
export function resolvePuzzlePublicCodeSearchMatch(
entries: readonly PuzzleWorkSummary[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapPuzzleWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSamePuzzlePublicWorkCode(searchKeyword, item.profileId),
canExposeEntry,
});
}
export function resolveBigFishPublicCodeSearchMatch(
entries: readonly BigFishWorkSummary[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapBigFishWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSameBigFishPublicWorkCode(searchKeyword, item.sourceSessionId),
canExposeEntry,
});
}
export function resolveJumpHopPublicCodeSearchMatch(
entries: readonly JumpHopGalleryCardResponse[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapJumpHopWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSameJumpHopPublicWorkCode(searchKeyword, item.profileId),
canExposeEntry,
});
}
export function resolveWoodenFishPublicCodeSearchMatch(
entries: readonly WoodenFishGalleryCardResponse[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapWoodenFishWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSameWoodenFishPublicWorkCode(searchKeyword, item.profileId),
canExposeEntry,
});
}
export function resolveBabyObjectMatchPublicCodeSearchMatch(
entries: readonly BabyObjectMatchDraft[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapBabyObjectMatchDraftToPlatformGalleryCard,
matchesEntry: (searchKeyword, item) =>
isSameBabyObjectMatchPublicWorkCode(searchKeyword, item.profileId),
canExposeEntry,
});
}
export function resolveMatch3DPublicCodeSearchMatch(
entries: readonly Match3DWorkSummary[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapMatch3DWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSameMatch3DPublicWorkCode(searchKeyword, item.profileId),
canExposeEntry,
});
}
export function resolveSquareHolePublicCodeSearchMatch(
entries: readonly SquareHoleWorkSummary[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapSquareHoleWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSameSquareHolePublicWorkCode(searchKeyword, item.profileId),
canExposeEntry,
});
}
export function resolveVisualNovelPublicCodeSearchMatch(
entries: readonly VisualNovelWorkSummary[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapVisualNovelWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSameVisualNovelPublicWorkCode(searchKeyword, item.profileId),
canExposeEntry,
});
}
export function resolveBarkBattlePublicCodeSearchMatch(
entries: readonly BarkBattleWorkSummary[],
keyword: string,
canExposeEntry = canExposePublicWork,
) {
return resolveMappedPublicCodeSearchMatch({
keyword,
entries,
mapEntry: mapBarkBattleWorkToPublicWorkDetail,
matchesEntry: (searchKeyword, item) =>
isSameBarkBattlePublicWorkCode(searchKeyword, item.workId),
canExposeEntry,
});
}
function resolveMappedPublicCodeSearchMatch<TItem>({
keyword,
entries,
mapEntry,
matchesEntry,
canExposeEntry = canExposePublicWork,
}: PlatformPublicCodeSearchMatcherInput<TItem>):
| PlatformPublicCodeSearchMatch<TItem>
| null {
for (const item of entries) {
const detail = mapEntry(item);
if (canExposeEntry(detail) && matchesEntry(keyword, item)) {
return { item, detail };
}
}
return null;
}

View File

@@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
import {
buildCustomWorldPublicWorkCode,
buildJumpHopPublicWorkCode,
buildMatch3DPublicWorkCode,
buildWoodenFishPublicWorkCode,
isSameCustomWorldPublicWorkCode,
isSameJumpHopPublicWorkCode,
isSameMatch3DPublicWorkCode,
isSameWoodenFishPublicWorkCode,
} from './publicWorkCode';
@@ -34,6 +36,24 @@ describe('publicWorkCode', () => {
);
});
it('matches current and legacy match3d public work prefixes', () => {
expect(buildMatch3DPublicWorkCode('match3d-profile-12345678')).toBe(
'M3-12345678',
);
expect(
isSameMatch3DPublicWorkCode(
'M3-12345678',
'match3d-profile-12345678',
),
).toBe(true);
expect(
isSameMatch3DPublicWorkCode(
'M3D-12345678',
'match3d-profile-12345678',
),
).toBe(true);
});
it('builds and matches custom world public work codes from profile ids', () => {
expect(buildCustomWorldPublicWorkCode('world-public-1')).toBe('CW-00000001');
expect(isSameCustomWorldPublicWorkCode('cw-00000001', 'world-public-1')).toBe(

View File

@@ -29,6 +29,14 @@ export function buildMatch3DPublicWorkCode(profileId: string) {
return `M3-${suffix}`;
}
function buildLegacyMatch3DPublicWorkCode(profileId: string) {
const normalized = normalizePublicCodeText(profileId);
const fallback = normalized || '00000000';
const suffix = fallback.slice(-8).padStart(8, '0');
return `M3D-${suffix}`;
}
export function buildSquareHolePublicWorkCode(profileId: string) {
const normalized = normalizePublicCodeText(profileId);
const fallback = normalized || '00000000';
@@ -134,6 +142,8 @@ export function isSameMatch3DPublicWorkCode(keyword: string, profileId: string)
return (
normalizedKeyword ===
normalizePublicCodeText(buildMatch3DPublicWorkCode(profileId)) ||
normalizedKeyword ===
normalizePublicCodeText(buildLegacyMatch3DPublicWorkCode(profileId)) ||
normalizedKeyword === normalizePublicCodeText(profileId)
);
}