From 05713e1d3bb9b23f2027ae1ff3596dd14cf0d9e1 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 04:44:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=20runtime=20=E8=87=AA=E5=8A=A8=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 8 ++ docs/README.md | 2 + ...mRecommendRuntimeAutoStart收口计划-2026-06-04.md | 40 +++++++++ ...玩法创作】平台入口与玩法链路-2026-05-15.md | 2 +- .../PlatformEntryFlowShellImpl.tsx | 56 +++++------- .../platformPublicGalleryFlow.test.ts | 87 +++++++++++++++++++ .../platformPublicGalleryFlow.ts | 50 +++++++++++ 7 files changed, 209 insertions(+), 36 deletions(-) create mode 100644 docs/technical/【前端架构】PlatformRecommendRuntimeAutoStart收口计划-2026-06-04.md diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 53fb6e1a..52dbd032 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -32,6 +32,14 @@ - 验证方式:`npm run test -- src/components/platform-entry/platformRecommendRuntimeAuthModel.test.ts`、针对新 Module 与平台壳执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】PlatformRecommendRuntimeAuthModel收口计划-2026-06-04.md`。 +## 2026-06-04 Platform Recommend Runtime Auto Start 收口 + +- 背景:推荐 runtime 自动启动 effect 同时判断桌面断点、stage、Tab、loading、推荐列表、active entry、ready 状态和启动中状态,导致壳层 effect 依赖过长且混合推荐流状态机知识。 +- 决策:扩展 `src/components/platform-entry/platformPublicGalleryFlow.ts`,新增 `resolvePlatformRecommendRuntimeAutoStartDecision(input)`,只返回 `noop`、`clear` 或 `start(entry)`。平台壳只执行清空 active runtime state 或调用 `selectRecommendRuntimeEntry(entry)`。 +- 影响范围:移动端首页推荐 runtime 自动启动、推荐列表为空时清空状态、active entry ready 判定,以及后续新增推荐 runtime 玩法的启动时机。 +- 验证方式:`npm run test -- src/components/platform-entry/platformPublicGalleryFlow.test.ts`、针对 Flow Module 与平台壳执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】PlatformRecommendRuntimeAutoStart收口计划-2026-06-04.md`。 + ## 2026-06-04 Platform Creation Launch Model 收口 - 背景:平台创作入口点击回调曾在 `PlatformEntryFlowShellImpl.tsx` 内联判断 `airp` 占位、隐藏的 `baby-object-match`、未知入口和各玩法工作台启动目标,壳层同时承接入口 ID 规则、启动前准备顺序和副作用。 diff --git a/docs/README.md b/docs/README.md index 32dacdc3..94a90752 100644 --- a/docs/README.md +++ b/docs/README.md @@ -71,6 +71,8 @@ Bark Battle 草稿三图完整性、生成状态归一、发布快照 / 发布 平台首页推荐 runtime 的匿名 Runtime Guest Token、已登录 background auth、非 embedded no-op 和拼图 isolated/default auth mode 计划收口到 `src/components/platform-entry/platformRecommendRuntimeAuthModel.ts`,规则见 [【前端架构】PlatformRecommendRuntimeAuthModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformRecommendRuntimeAuthModel收口计划-2026-06-04.md)。 +平台首页推荐 runtime 自动启动的桌面 / Tab / stage / loading gate、active entry 查找、ready 判定和 clear/start/noop 决策收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】PlatformRecommendRuntimeAutoStart收口计划-2026-06-04.md](./technical/【前端架构】PlatformRecommendRuntimeAutoStart收口计划-2026-06-04.md)。 + RPG Agent 结果页发布门禁展示和预览来源 label 收口到 `src/components/platform-entry/platformRpgAgentResultPreviewModel.ts`,壳层只保留 session/profile 编排和结果页 props 传递,规则见 [【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md)。 平台入口创作生成通知、pending 作品架占位、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】PlatformRecommendRuntimeAutoStart收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformRecommendRuntimeAutoStart收口计划-2026-06-04.md new file mode 100644 index 00000000..efb106f9 --- /dev/null +++ b/docs/technical/【前端架构】PlatformRecommendRuntimeAutoStart收口计划-2026-06-04.md @@ -0,0 +1,40 @@ +# 【前端架构】Platform Recommend Runtime Auto Start 收口计划 + +## 背景 + +平台推荐页的 embedded runtime 会在移动端首页自动选择当前推荐作品并启动对应玩法。旧 `useEffect` 同时判断桌面断点、当前 stage、当前 Tab、平台 loading、推荐列表是否为空、active entry 是否仍存在、对应 runtime 是否 ready、是否已有启动请求,以及下一条 entry 应选谁。 + +这组判断是纯推荐流自动启动决策,但留在 `PlatformEntryFlowShellImpl.tsx` 会让 effect 依赖很长,也让后续新增玩法时容易把 ready 判定和启动时机混在副作用里。 + +## 决策 + +扩展 `src/components/platform-entry/platformPublicGalleryFlow.ts`,新增 `resolvePlatformRecommendRuntimeAutoStartDecision(input)`: + +- `noop`:当前不需要改变推荐 runtime。 +- `clear`:推荐列表为空,壳层应清空 active entry、runtime kind 和错误。 +- `start`:壳层应调用既有 `selectRecommendRuntimeEntry(entry)` 启动指定作品。 + +`PlatformEntryFlowShellImpl.tsx` 继续作为 **Adapter**:它负责收集 React state、清空 state、调用 `selectRecommendRuntimeEntry(...)` 和执行各玩法 runtime 副作用。 + +## Interface 约束 + +- 桌面端、非 `platform` stage、非 `home` Tab 或平台仍在 loading 时返回 `noop`。 +- 推荐列表为空时返回 `clear`。 +- active entry 存在且对应 runtime 已 ready 时返回 `noop`。 +- 当前已有启动请求时返回 `noop`。 +- active entry 存在但未 ready 时返回 `start(activeEntry)`。 +- active key 缺失或已不在列表中时返回 `start(firstEntry)`。 + +## Depth / Leverage / Locality + +- **Depth**:壳层只消费三态决策;列表查找、ready 判定和自动启动门禁藏入 Flow Module Implementation。 +- **Leverage**:后续推荐流新增玩法或改 ready 判定,只需补 `platformPublicGalleryFlow.ts` 的模型测试。 +- **Locality**:effect 只保留副作用动作,不再承载推荐流状态机知识。 + +## 验收 + +- `npm run test -- src/components/platform-entry/platformPublicGalleryFlow.test.ts` +- `npx eslint --max-warnings 0 src/components/platform-entry/platformPublicGalleryFlow.ts src/components/platform-entry/platformPublicGalleryFlow.test.ts` +- `npx eslint src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet` +- `npm run typecheck` +- `npm run check:encoding` diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index cd1a00cd..a7d03114 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -171,7 +171,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列 删除等破坏性动作当前未接入 jump-hop 删除 API;如果后续要在作品架提供删除入口,必须先补齐后端/SpacetimeDB/前端整条删除链路,再开放按钮。 -推荐页匿名游玩不再限定为跳一跳。移动端一级 `推荐` Tab 是内嵌运行态刷卡流,会自动选择推荐作品并启动对应玩法;桌面端首页不启动这套移动推荐运行态,而是渲染桌面发现壳,展示 `今日游戏`、`推荐`、`作品分类` 等桌面内容。断点事实统一走 `platformEntryResponsive.ts` 的 `usePlatformDesktopLayout()`,平台壳和首页视图必须共用同一个判断,避免桌面发现页与移动推荐页同时挂载、重复触发请求或启动运行态。推荐页嵌入运行态启动时按真实身份分流:已登录用户或本地已有 access token 时继续使用账号 Bearer,但请求选项必须是 local auth impact,避免单卡 401 清空整站登录态;只有确认为匿名访客时才申请短期 Runtime Guest Token,并只把它作为局部请求头传给运行态客户端,不写入全局登录态、不触发 refresh,也不把匿名流量伪装成普通用户。当前覆盖矩阵为:跳一跳、视觉小说、抓大鹅 Match3D、方洞挑战、拼图、敲木鱼、大鱼吃小鱼、汪汪声浪。每个模板的启动请求、推荐页内后续运行态动作以及需要上报的 play/finish/leaderboard/next-level 类请求,都必须继续按该身份分流;公开读取入口仍可匿名读取,创作、个人作品、删除、发布、Remix 等账号/所有权动作仍保持普通用户鉴权。推荐 runtime 的 `none` / `background` / `runtime-guest` 请求计划和拼图 `default` / `isolated` runtime auth mode 由 `platformRecommendRuntimeAuthModel.ts` 统一判定,平台壳只负责读取 token、申请 Runtime Guest Token 和传递 request options。 +推荐页匿名游玩不再限定为跳一跳。移动端一级 `推荐` Tab 是内嵌运行态刷卡流,会自动选择推荐作品并启动对应玩法;桌面端首页不启动这套移动推荐运行态,而是渲染桌面发现壳,展示 `今日游戏`、`推荐`、`作品分类` 等桌面内容。断点事实统一走 `platformEntryResponsive.ts` 的 `usePlatformDesktopLayout()`,平台壳和首页视图必须共用同一个判断,避免桌面发现页与移动推荐页同时挂载、重复触发请求或启动运行态。推荐页嵌入运行态启动时按真实身份分流:已登录用户或本地已有 access token 时继续使用账号 Bearer,但请求选项必须是 local auth impact,避免单卡 401 清空整站登录态;只有确认为匿名访客时才申请短期 Runtime Guest Token,并只把它作为局部请求头传给运行态客户端,不写入全局登录态、不触发 refresh,也不把匿名流量伪装成普通用户。当前覆盖矩阵为:跳一跳、视觉小说、抓大鹅 Match3D、方洞挑战、拼图、敲木鱼、大鱼吃小鱼、汪汪声浪。每个模板的启动请求、推荐页内后续运行态动作以及需要上报的 play/finish/leaderboard/next-level 类请求,都必须继续按该身份分流;公开读取入口仍可匿名读取,创作、个人作品、删除、发布、Remix 等账号/所有权动作仍保持普通用户鉴权。推荐 runtime 的 `none` / `background` / `runtime-guest` 请求计划和拼图 `default` / `isolated` runtime auth mode 由 `platformRecommendRuntimeAuthModel.ts` 统一判定,平台壳只负责读取 token、申请 Runtime Guest Token 和传递 request options。推荐 runtime 自动启动只由 `platformPublicGalleryFlow.ts` 输出 `noop` / `clear` / `start(entry)` 决策,平台壳只执行清空 state 或启动指定作品。 ## 敲木鱼 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 23174933..c55f45cd 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -553,9 +553,9 @@ import { buildPlatformPublicGalleryFeeds, getPlatformPublicGalleryEntryKey, getPlatformRecommendRuntimeKind, - isPlatformRecommendRuntimeReadyForEntry, isSamePlatformPublicGalleryEntry, type RecommendRuntimeKind, + resolvePlatformRecommendRuntimeAutoStartDecision, resolvePlatformRecommendRuntimeStartIntent, } from './platformPublicGalleryFlow'; import { @@ -12309,31 +12309,15 @@ export function PlatformEntryFlowShellImpl({ ]); useEffect(() => { - if ( - isDesktopLayout || - selectionStage !== 'platform' || - platformBootstrap.platformTab !== 'home' || - platformBootstrap.isLoadingPlatform - ) { - return; - } - - if (recommendRuntimeEntries.length === 0) { - setActiveRecommendEntryKey(null); - setActiveRecommendRuntimeKind(null); - setActiveRecommendRuntimeError(null); - return; - } - - const activeRecommendEntry = activeRecommendEntryKey - ? (recommendRuntimeEntries.find( - (entry) => - getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey, - ) ?? null) - : null; - const isActiveRecommendRuntimeReady = - activeRecommendEntry !== null && - isPlatformRecommendRuntimeReadyForEntry(activeRecommendEntry, { + const decision = resolvePlatformRecommendRuntimeAutoStartDecision({ + isDesktopLayout, + selectionStage, + platformTab: platformBootstrap.platformTab, + isLoadingPlatform: platformBootstrap.isLoadingPlatform, + entries: recommendRuntimeEntries, + activeEntryKey: activeRecommendEntryKey, + isStarting: isStartingRecommendEntry, + readyState: { activeKind: activeRecommendRuntimeKind, hasBabyObjectMatchDraft: Boolean(babyObjectMatchDraft), hasBigFishRun: Boolean(bigFishRun), @@ -12345,19 +12329,21 @@ export function PlatformEntryFlowShellImpl({ puzzleRunEntryProfileId: puzzleRun?.entryProfileId ?? null, puzzleRunCurrentLevelProfileId: puzzleRun?.currentLevel?.profileId ?? null, - }); - if ( - (activeRecommendEntry !== null && isActiveRecommendRuntimeReady) || - isStartingRecommendEntry - ) { + }, + }); + + if (decision.type === 'noop') { return; } - const nextRecommendEntry = - activeRecommendEntry ?? recommendRuntimeEntries[0]; - if (nextRecommendEntry) { - void selectRecommendRuntimeEntry(nextRecommendEntry); + if (decision.type === 'clear') { + setActiveRecommendEntryKey(null); + setActiveRecommendRuntimeKind(null); + setActiveRecommendRuntimeError(null); + return; } + + void selectRecommendRuntimeEntry(decision.entry); }, [ activeRecommendEntryKey, activeRecommendRuntimeKind, diff --git a/src/components/platform-entry/platformPublicGalleryFlow.test.ts b/src/components/platform-entry/platformPublicGalleryFlow.test.ts index c3c1f3bc..c5df79fc 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.test.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.test.ts @@ -22,6 +22,7 @@ import { mergePlatformPublicGalleryEntries, type PlatformRecommendRuntimeStartIntentDeps, type RecommendRuntimeKind, + resolvePlatformRecommendRuntimeAutoStartDecision, resolvePlatformRecommendRuntimeStartIntent, } from './platformPublicGalleryFlow'; import { @@ -611,6 +612,92 @@ test('platform public gallery flow resolves puzzle and edutainment readiness det ).toBe(false); }); +test('platform public gallery flow resolves recommend runtime auto-start gates', () => { + const entry = buildTypedEntry('big-fish'); + const baseInput: Parameters< + typeof resolvePlatformRecommendRuntimeAutoStartDecision + >[0] = { + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'home', + isLoadingPlatform: false, + entries: [entry], + activeEntryKey: null, + isStarting: false, + readyState: { activeKind: null }, + }; + + expect( + resolvePlatformRecommendRuntimeAutoStartDecision({ + ...baseInput, + isDesktopLayout: true, + }), + ).toEqual({ type: 'noop' }); + expect( + resolvePlatformRecommendRuntimeAutoStartDecision({ + ...baseInput, + platformTab: 'discover', + }), + ).toEqual({ type: 'noop' }); + expect( + resolvePlatformRecommendRuntimeAutoStartDecision({ + ...baseInput, + entries: [], + }), + ).toEqual({ type: 'clear' }); + expect( + resolvePlatformRecommendRuntimeAutoStartDecision({ + ...baseInput, + isStarting: true, + }), + ).toEqual({ type: 'noop' }); +}); + +test('platform public gallery flow resolves recommend runtime auto-start target', () => { + const firstEntry = buildTypedEntry('big-fish', { + profileId: 'big-fish-first', + }); + const activeEntry = buildTypedEntry('puzzle', { + profileId: 'puzzle-active', + }); + const activeEntryKey = getPlatformPublicGalleryEntryKey(activeEntry); + const baseInput: Parameters< + typeof resolvePlatformRecommendRuntimeAutoStartDecision + >[0] = { + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'home', + isLoadingPlatform: false, + entries: [firstEntry, activeEntry], + activeEntryKey, + isStarting: false, + readyState: { activeKind: 'puzzle' }, + }; + + expect( + resolvePlatformRecommendRuntimeAutoStartDecision({ + ...baseInput, + readyState: { + activeKind: 'puzzle', + puzzleRunEntryProfileId: 'puzzle-active', + }, + }), + ).toEqual({ type: 'noop' }); + expect(resolvePlatformRecommendRuntimeAutoStartDecision(baseInput)).toEqual({ + type: 'start', + entry: activeEntry, + }); + expect( + resolvePlatformRecommendRuntimeAutoStartDecision({ + ...baseInput, + activeEntryKey: 'missing-entry', + }), + ).toEqual({ + type: 'start', + entry: firstEntry, + }); +}); + 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 c1475d5a..b4cdc1df 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.ts @@ -140,6 +140,22 @@ export type PlatformRecommendRuntimeReadyState = { puzzleRunCurrentLevelProfileId?: string | null; }; +export type PlatformRecommendRuntimeAutoStartDecision = + | { type: 'noop' } + | { type: 'clear' } + | { type: 'start'; entry: PlatformPublicGalleryCard }; + +export type PlatformRecommendRuntimeAutoStartInput = { + isDesktopLayout: boolean; + selectionStage: string; + platformTab: string; + isLoadingPlatform: boolean; + entries: readonly PlatformPublicGalleryCard[]; + activeEntryKey: string | null; + isStarting: boolean; + readyState: PlatformRecommendRuntimeReadyState; +}; + export type PlatformPublicGalleryFeedsInput = { rpgEntries: readonly CustomWorldGalleryCard[]; bigFishEntries: readonly BigFishWorkSummary[]; @@ -423,6 +439,40 @@ export function isPlatformRecommendRuntimeReadyForEntry( return true; } +export function resolvePlatformRecommendRuntimeAutoStartDecision( + input: PlatformRecommendRuntimeAutoStartInput, +): PlatformRecommendRuntimeAutoStartDecision { + if ( + input.isDesktopLayout || + input.selectionStage !== 'platform' || + input.platformTab !== 'home' || + input.isLoadingPlatform + ) { + return { type: 'noop' }; + } + + if (input.entries.length === 0) { + return { type: 'clear' }; + } + + const activeEntry = input.activeEntryKey + ? (input.entries.find( + (entry) => + getPlatformPublicGalleryEntryKey(entry) === input.activeEntryKey, + ) ?? null) + : null; + const isActiveRuntimeReady = + activeEntry !== null && + isPlatformRecommendRuntimeReadyForEntry(activeEntry, input.readyState); + + if ((activeEntry !== null && isActiveRuntimeReady) || input.isStarting) { + return { type: 'noop' }; + } + + const nextEntry = activeEntry ?? input.entries[0]; + return nextEntry ? { type: 'start', entry: nextEntry } : { type: 'clear' }; +} + export function isSamePlatformPublicGalleryEntry( left: PlatformPublicGalleryCard, right: PlatformPublicGalleryCard,