diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index bab8893e..78854d31 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -49,7 +49,7 @@ ## 2026-06-03 平台入口公开作品流身份规则收口 - 背景:平台入口公开作品推荐流需要同时处理 RPG、拼图、抓大鹅、跳一跳、敲木鱼、视觉小说、Bark Battle、宝贝识物等卡片,公开作品身份、跨玩法去重、排序和推荐运行态 kind 判定曾放在 `PlatformEntryFlowShellImpl.tsx` 巨型实现里。 -- 决策:公开作品身份、排序规则和推荐 runtime 启动意图统一收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`;入口壳层只调用该 Module 的 `getPlatformPublicGalleryEntryKey`、`getPlatformRecommendRuntimeKind`、`resolvePlatformRecommendRuntimeStartIntent`、`isSamePlatformPublicGalleryEntry` 和 `mergePlatformPublicGalleryEntries`。`edutainment` key 必须带 `templateId`,RPG 卡片回退为 `rpg`。推荐 runtime 启动 intent 只返回启动目标、`embedded` / `returnStage` 参数、阻断文案和错误落点;壳层仍执行 request key、运行态 API、错误 setter 与 UI 状态。 +- 决策:公开作品身份、排序规则、推荐 runtime 启动意图和 ready 判定统一收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`;入口壳层只调用该 Module 的 `getPlatformPublicGalleryEntryKey`、`getPlatformRecommendRuntimeKind`、`resolvePlatformRecommendRuntimeStartIntent`、`isPlatformRecommendRuntimeReadyForEntry`、`isSamePlatformPublicGalleryEntry` 和 `mergePlatformPublicGalleryEntries`。`edutainment` key 必须带 `templateId`,RPG 卡片回退为 `rpg`。推荐 runtime 启动 intent 只返回启动目标、`embedded` / `returnStage` 参数、阻断文案和错误落点;ready 判定只接布尔值与拼图 profile id,避免把各玩法 run snapshot 类型拖入 Module。壳层仍执行 request key、运行态 API、错误 setter 与 UI 状态。 - 影响范围:平台入口推荐流、公开作品详情、推荐 runtime 启动、跨玩法公开作品合并,以及后续新增玩法的入口接入。 - 验证方式:`npm run test -- src/components/platform-entry/platformPublicGalleryFlow.test.ts`、`npm run typecheck`、`npm run check:encoding`、相关文件 ESLint 通过。 - 关联文档:`docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md`。 diff --git a/docs/README.md b/docs/README.md index 4dd4b85b..466e891a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,7 +39,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 前端 Server-Sent Events 客户端传输层收口到 `src/services/sseStream.ts`,事件边界、UTF-8 flush、JSON 解析跳过和提前取消约定见 [【前端架构】SSE客户端传输层收口约定-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91SSE%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BC%A0%E8%BE%93%E5%B1%82%E6%94%B6%E5%8F%A3%E7%BA%A6%E5%AE%9A-2026-06-03.md)。 -平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定、推荐 runtime 启动意图和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 +平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定、推荐 runtime 启动意图、ready 判定和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 统一作品详情页的玩法 kind、详情打开策略、自有作品动作模式、编辑 / 点赞 / 改造 / 启动意图和公开详情映射收口到 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`;抓大鹅公开详情映射与启动 / 编辑 Adapter 的素材归一仍归 `platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md](./technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md b/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md index 6b09dd89..4fdebc80 100644 --- a/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md +++ b/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md @@ -11,10 +11,11 @@ - `getPlatformPublicGalleryEntryKey(entry)`:按玩法类型、作者和 `profileId` 生成公开作品身份。 - `getPlatformRecommendRuntimeKind(entry)`:把公开作品卡映射为推荐运行态 kind。 - `resolvePlatformRecommendRuntimeStartIntent(entry, deps)`:把公开作品卡映射为推荐 runtime 启动意图、错误落点和 embedded / returnStage 参数。 +- `isPlatformRecommendRuntimeReadyForEntry(entry, state)`:用标量 ready state 判定当前推荐 runtime 是否已能承接该公开作品。 - `isSamePlatformPublicGalleryEntry(left, right)`:按公开作品身份比较。 - `mergePlatformPublicGalleryEntries(rpgEntries, puzzleEntries)`:统一完成 RPG 与各玩法公开作品去重、覆盖和倒序排序。 -入口壳层只调用这些函数,不再在 `PlatformEntryFlowShellImpl.tsx` 内手写公开作品身份、排序规则和推荐 runtime 启动能力矩阵。`isRecommendRuntimeReadyForEntry` 暂留入口壳层,因为它依赖各运行态 run 状态,直接抽出会把更多 runtime 状态类型拖入这个 Module,降低本次改造的 locality。 +入口壳层只调用这些函数,不再在 `PlatformEntryFlowShellImpl.tsx` 内手写公开作品身份、排序规则、推荐 runtime 启动能力矩阵和 ready 判定。ready 判定只接布尔值与拼图 profile id,不把各玩法 run snapshot 类型拖入 Module。 ## 玩法身份规则 @@ -33,9 +34,17 @@ - 抓大鹅 public detail -> work mapper 必须作为 Adapter 注入,继续由 Match3D Runtime Profile Module 维护 `generatedItemAssets` 归一化与背景资产提升。推荐 runtime 固定沿用旧参数 `returnStage = 'work-detail'` 与 `embedded = true`。 - 汪汪声浪优先使用推荐流已持有的 `barkBattleGalleryEntries`,再回退公开卡映射;不额外读取作品架列表。 +## 推荐 runtime ready 判定 + +- `isPlatformRecommendRuntimeReadyForEntry` 先要求 `state.activeKind` 与当前公开作品的 `getPlatformRecommendRuntimeKind(entry)` 相同,否则返回 `false`。 +- 大鱼吃小鱼、跳一跳、敲木鱼、抓大鹅、方洞挑战和视觉小说只看对应 `has*Run` 布尔值,保持旧行为,不在本 Module 内解析 run snapshot。 +- 拼图只看 `puzzleRunEntryProfileId` 或 `puzzleRunCurrentLevelProfileId` 是否等于当前公开作品 `profileId`。 +- 汪汪声浪和 RPG 在 kind 匹配时沿用旧 `ready = true` 行为;宝贝识物只看 `hasBabyObjectMatchDraft`。 +- 若未来要修正同玩法旧 run 误判或 RPG 无嵌入 runtime 的旧行为,应另立行为变更任务;本 Module 先只收口现有规则。 + ## 后续深化 -下一步可继续把平台入口的作品架刷新、删除确认和直达恢复逻辑收口成更深的 Work Shelf **Module**。当前 `platformPublicGalleryFlow` 先提供一个稳定 seam,使公开作品 identity、runtime kind 与推荐 runtime 启动意图的修改集中在一处。 +下一步可继续把平台入口的作品架刷新、删除确认和直达恢复逻辑收口成更深的 Work Shelf **Module**。当前 `platformPublicGalleryFlow` 先提供一个稳定 seam,使公开作品 identity、runtime kind、推荐 runtime 启动意图与 ready 判定的修改集中在一处。 ## 验证 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 3b9c34f6..c43ea772 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -503,6 +503,7 @@ import { import { getPlatformPublicGalleryEntryKey, getPlatformRecommendRuntimeKind, + isPlatformRecommendRuntimeReadyForEntry, isSamePlatformPublicGalleryEntry, mergePlatformPublicGalleryEntries, type RecommendRuntimeKind, @@ -600,18 +601,6 @@ type WoodenFishRuntimeReturnStage = type VisualNovelEntryGenerationPhase = 'generating' | 'ready' | 'failed'; type BabyObjectMatchGenerationPhase = 'generating' | 'ready' | 'failed'; -type RecommendRuntimeState = { - activeKind: RecommendRuntimeKind | null; - babyObjectMatchDraft: BabyObjectMatchDraft | null; - bigFishRun: BigFishRuntimeSnapshotResponse | null; - jumpHopRun: JumpHopRunResponse['run'] | null; - match3dRun: Match3DRunSnapshot | null; - puzzleRun: PuzzleRunSnapshot | null; - squareHoleRun: SquareHoleRunSnapshot | null; - visualNovelRun: VisualNovelRunSnapshot | null; - woodenFishRun: WoodenFishRunResponse['run'] | null; -}; - type PuzzleSaveArchiveState = { runtimeKind?: unknown; entryProfileId?: unknown; @@ -682,49 +671,6 @@ const PUZZLE_DRAFT_GENERATION_POINT_COST = 2; const MATCH3D_DRAFT_GENERATION_POINT_COST = 10; const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3; -function isRecommendRuntimeReadyForEntry( - entry: PlatformPublicGalleryCard, - state: RecommendRuntimeState, -) { - const expectedKind = getPlatformRecommendRuntimeKind(entry); - if (state.activeKind !== expectedKind) { - return false; - } - - if (expectedKind === 'big-fish') { - return Boolean(state.bigFishRun); - } - if (expectedKind === 'jump-hop') { - return Boolean(state.jumpHopRun); - } - if (expectedKind === 'wooden-fish') { - return Boolean(state.woodenFishRun); - } - if (expectedKind === 'match3d') { - return Boolean(state.match3dRun); - } - if (expectedKind === 'puzzle') { - return ( - state.puzzleRun?.entryProfileId === entry.profileId || - state.puzzleRun?.currentLevel?.profileId === entry.profileId - ); - } - if (expectedKind === 'square-hole') { - return Boolean(state.squareHoleRun); - } - if (expectedKind === 'visual-novel') { - return Boolean(state.visualNovelRun); - } - if (expectedKind === 'bark-battle') { - return true; - } - if (expectedKind === 'edutainment') { - return Boolean(state.babyObjectMatchDraft); - } - - return true; -} - function mapBarkBattleWorkToPublishedConfig( work: BarkBattleWorkSummary, ): BarkBattlePublishedConfig { @@ -13359,16 +13305,18 @@ export function PlatformEntryFlowShellImpl({ : null; const isActiveRecommendRuntimeReady = activeRecommendEntry !== null && - isRecommendRuntimeReadyForEntry(activeRecommendEntry, { + isPlatformRecommendRuntimeReadyForEntry(activeRecommendEntry, { activeKind: activeRecommendRuntimeKind, - babyObjectMatchDraft, - bigFishRun, - jumpHopRun, - match3dRun, - puzzleRun, - squareHoleRun, - visualNovelRun, - woodenFishRun, + hasBabyObjectMatchDraft: Boolean(babyObjectMatchDraft), + hasBigFishRun: Boolean(bigFishRun), + hasJumpHopRun: Boolean(jumpHopRun), + hasMatch3DRun: Boolean(match3dRun), + hasSquareHoleRun: Boolean(squareHoleRun), + hasVisualNovelRun: Boolean(visualNovelRun), + hasWoodenFishRun: Boolean(woodenFishRun), + puzzleRunEntryProfileId: puzzleRun?.entryProfileId ?? null, + puzzleRunCurrentLevelProfileId: + puzzleRun?.currentLevel?.profileId ?? null, }); if ( (activeRecommendEntry !== null && isActiveRecommendRuntimeReady) || diff --git a/src/components/platform-entry/platformPublicGalleryFlow.test.ts b/src/components/platform-entry/platformPublicGalleryFlow.test.ts index 410c601c..5c53d3e6 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.test.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.test.ts @@ -13,6 +13,7 @@ import { getPlatformPublicGalleryEntryKey, getPlatformPublicGalleryEntryTime, getPlatformRecommendRuntimeKind, + isPlatformRecommendRuntimeReadyForEntry, isSamePlatformPublicGalleryEntry, mergePlatformPublicGalleryEntries, type PlatformRecommendRuntimeStartIntentDeps, @@ -439,6 +440,94 @@ test('platform public gallery flow resolves recommend runtime bark battle priori }); }); +test('platform public gallery flow resolves recommend runtime readiness', () => { + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('big-fish'), { + activeKind: 'puzzle', + hasBigFishRun: true, + }), + ).toBe(false); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('big-fish'), { + activeKind: 'big-fish', + hasBigFishRun: true, + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('jump-hop'), { + activeKind: 'jump-hop', + hasJumpHopRun: true, + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('wooden-fish'), { + activeKind: 'wooden-fish', + hasWoodenFishRun: true, + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('match3d'), { + activeKind: 'match3d', + hasMatch3DRun: true, + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('square-hole'), { + activeKind: 'square-hole', + hasSquareHoleRun: true, + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('visual-novel'), { + activeKind: 'visual-novel', + hasVisualNovelRun: true, + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('bark-battle'), { + activeKind: 'bark-battle', + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildRpgEntry(), { + activeKind: 'rpg', + }), + ).toBe(true); +}); + +test('platform public gallery flow resolves puzzle and edutainment readiness details', () => { + const puzzleEntry = buildTypedEntry('puzzle', { + profileId: 'puzzle-profile', + }); + + expect( + isPlatformRecommendRuntimeReadyForEntry(puzzleEntry, { + activeKind: 'puzzle', + puzzleRunEntryProfileId: 'other-profile', + puzzleRunCurrentLevelProfileId: 'puzzle-profile', + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(puzzleEntry, { + activeKind: 'puzzle', + puzzleRunEntryProfileId: 'other-profile', + puzzleRunCurrentLevelProfileId: 'another-profile', + }), + ).toBe(false); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('edutainment'), { + activeKind: 'edutainment', + hasBabyObjectMatchDraft: true, + }), + ).toBe(true); + expect( + isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('edutainment'), { + activeKind: 'edutainment', + hasBabyObjectMatchDraft: false, + }), + ).toBe(false); +}); + test('platform public gallery flow merges duplicate identities and sorts newest first', () => { const staleRpgEntry = buildRpgEntry({ profileId: 'shared-rpg', diff --git a/src/components/platform-entry/platformPublicGalleryFlow.ts b/src/components/platform-entry/platformPublicGalleryFlow.ts index c8dd70e7..914d64bb 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.ts @@ -114,6 +114,19 @@ export type PlatformRecommendRuntimeStartIntentDeps = { ) => Match3DWorkSummary | null; }; +export type PlatformRecommendRuntimeReadyState = { + activeKind: RecommendRuntimeKind | null; + hasBabyObjectMatchDraft?: boolean; + hasBigFishRun?: boolean; + hasJumpHopRun?: boolean; + hasMatch3DRun?: boolean; + hasSquareHoleRun?: boolean; + hasVisualNovelRun?: boolean; + hasWoodenFishRun?: boolean; + puzzleRunEntryProfileId?: string | null; + puzzleRunCurrentLevelProfileId?: string | null; +}; + export function getPlatformPublicGalleryEntryTime( entry: PlatformPublicGalleryCard, ) { @@ -332,6 +345,49 @@ export function resolvePlatformRecommendRuntimeStartIntent( }; } +export function isPlatformRecommendRuntimeReadyForEntry( + entry: PlatformPublicGalleryCard, + state: PlatformRecommendRuntimeReadyState, +) { + const expectedKind = getPlatformRecommendRuntimeKind(entry); + if (state.activeKind !== expectedKind) { + return false; + } + + if (expectedKind === 'big-fish') { + return Boolean(state.hasBigFishRun); + } + if (expectedKind === 'jump-hop') { + return Boolean(state.hasJumpHopRun); + } + if (expectedKind === 'wooden-fish') { + return Boolean(state.hasWoodenFishRun); + } + if (expectedKind === 'match3d') { + return Boolean(state.hasMatch3DRun); + } + if (expectedKind === 'puzzle') { + return ( + state.puzzleRunEntryProfileId === entry.profileId || + state.puzzleRunCurrentLevelProfileId === entry.profileId + ); + } + if (expectedKind === 'square-hole') { + return Boolean(state.hasSquareHoleRun); + } + if (expectedKind === 'visual-novel') { + return Boolean(state.hasVisualNovelRun); + } + if (expectedKind === 'bark-battle') { + return true; + } + if (expectedKind === 'edutainment') { + return Boolean(state.hasBabyObjectMatchDraft); + } + + return true; +} + export function isSamePlatformPublicGalleryEntry( left: PlatformPublicGalleryCard, right: PlatformPublicGalleryCard,