From dbc00be2cc5936210859733ac3efd3d53958103a Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 01:49:12 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E9=98=B6=E6=AE=B5=E5=A4=B1=E6=9D=83=E5=88=A4=E5=AE=9A?= 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 + ...latformSelectionStageModel收口计划-2026-06-04.md | 28 +++++++ .../PlatformEntryFlowShellImpl.tsx | 23 ++---- .../platformSelectionStageModel.test.ts | 75 +++++++++++++++++++ .../platformSelectionStageModel.ts | 59 +++++++++++++++ 6 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 docs/technical/【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md create mode 100644 src/components/platform-entry/platformSelectionStageModel.test.ts create mode 100644 src/components/platform-entry/platformSelectionStageModel.ts diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 919b3850..028c7a07 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-04 Platform Selection Stage Model 收口 + +- 背景:平台入口在受保护数据失效后会清空当前用户私有作品、草稿、运行态和生成状态,但哪些 `SelectionStage` 可保留、哪些必须回首页曾以内联长否定串散在 `PlatformEntryFlowShellImpl.tsx`。 +- 决策:新增 `src/components/platform-entry/platformSelectionStageModel.ts`,以 `resolveSelectionStageAfterProtectedDataLoss(stage)` 收口受保护数据失效后的 stage 去留判定。模型内部使用 `satisfies Record` 全量分类,新增 stage 时必须明确保留或回首页。壳层仍负责检测权限变化、清 state 和调用 `setSelectionStage`。 +- 影响范围:退出登录、鉴权上下文收回、平台入口公开页 / 工作台 / 结果页 / 生成页 / 运行态的阶段恢复规则,以及后续新增 `SelectionStage`。 +- 验证方式:`npm run test -- src/components/platform-entry/platformSelectionStageModel.test.ts`、针对新 Module 与壳层执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md`。 + ## 2026-06-04 Creation Work Delete Flow 收口 - 背景:平台入口作品架删除入口在 RPG、拼图、抓大鹅、方洞挑战、大鱼吃小鱼、视觉小说和宝贝识物 handler 内重复计算确认标题、删除说明、草稿 notice key 与拼图派生稳定 ID,导致删除确认规则散在巨型壳层。 diff --git a/docs/README.md b/docs/README.md index b52ab02d..275a97b6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -53,6 +53,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 平台入口错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key 与任务完成文案收口到 `src/components/platform-entry/platformDialogStateModel.ts`,规则见 [【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md](./technical/【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md)。 +平台入口受保护数据失效后的 stage 去留判定收口到 `src/components/platform-entry/platformSelectionStageModel.ts`,壳层只执行缓存清空和必要跳转,规则见 [【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md)。 + 小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish、Bark Battle、Puzzle 公开 / 推荐运行态请求、Jump Hop / Wooden Fish 正式 run 请求和 Visual Novel 局部 JSON runtime 请求已先迁移,规则见 [【前端架构】RuntimeClientFamily收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RuntimeClientFamily%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 抓大鹅 runtime profile 的公开详情转 work、session draft 转 profile、生成背景资产提升和 run/profile/public detail 素材优先级收口到 `src/components/platform-entry/platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91Match3DRuntimeProfile%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md new file mode 100644 index 00000000..4662012a --- /dev/null +++ b/docs/technical/【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md @@ -0,0 +1,28 @@ +# 【前端架构】Platform Selection Stage Model 收口计划 + +## 背景 + +`PlatformEntryFlowShellImpl.tsx` 在受保护数据失效后会清空当前用户的私有作品、运行态、草稿 notice 和生成状态。清理完成后,壳层还要判断当前 `SelectionStage` 是否还能继续展示:公开首页、公开详情、工作台入口等阶段可保留;结果页、生成页、运行态、个人反馈等依赖私有数据或运行态快照的阶段必须回到首页。 + +此前该规则以内联长否定串维护在壳层 **Implementation** 内。新增玩法 stage 或调整登录态行为时,维护者必须在巨型壳层中查找白名单,缺少独立测试面。 + +## 决策 + +新增 `src/components/platform-entry/platformSelectionStageModel.ts` 作为 Platform Selection Stage **Module**。其公开 **Interface** 为: + +- `resolveSelectionStageAfterProtectedDataLoss(stage)`:输入当前 `SelectionStage`,输出受保护数据失效后应停留的 stage;可保留则原样返回,否则返回 `platform`。 + +`PlatformEntryFlowShellImpl.tsx` 仍作为副作用 **Adapter**:负责检测受保护数据从可读变为不可读、清空各玩法缓存、重置生成和错误状态,并只在模型输出与当前 stage 不一致时调用 `setSelectionStage(nextStage)`。 + +## 约定 + +- 新增 `SelectionStage` 时,必须判断它在退出登录或鉴权上下文收回后是否仍可展示,并在本 **Module** 的全量 `Record` 与测试中列明。 +- 公开列表、公开详情和创作工作台入口可保留;依赖当前用户私有数据、生成 session、运行态 run 或个人资料的 stage 默认回 `platform`。 +- 此 **Module** 不清理 state、不调用路由、不触发登录弹窗,只表达纯 stage 决策。 + +## 验收 + +- `npm run test -- src/components/platform-entry/platformSelectionStageModel.test.ts` +- `npx eslint src/components/platform-entry/platformSelectionStageModel.ts src/components/platform-entry/platformSelectionStageModel.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet` +- `npm run typecheck` +- `npm run check:encoding` diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 665af49c..8872eea5 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -532,6 +532,7 @@ import { buildPuzzleResultProfileId, buildPuzzleResultWorkId, } from './platformPuzzleIdentityModel'; +import { resolveSelectionStageAfterProtectedDataLoss } from './platformSelectionStageModel'; import { PlatformTaskCompletionDialog } from './PlatformTaskCompletionDialog'; import { PlatformWorkDetailView } from './PlatformWorkDetailView'; import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController'; @@ -6657,24 +6658,10 @@ export function PlatformEntryFlowShellImpl({ persistRpgAgentUiState(null, null); resetAutoSaveTrackingToIdle(); - if ( - selectionStage !== 'platform' && - selectionStage !== 'work-detail' && - selectionStage !== 'detail' && - selectionStage !== 'agent-workspace' && - selectionStage !== 'big-fish-agent-workspace' && - selectionStage !== 'match3d-agent-workspace' && - selectionStage !== 'square-hole-agent-workspace' && - selectionStage !== 'jump-hop-workspace' && - selectionStage !== 'wooden-fish-workspace' && - selectionStage !== 'puzzle-agent-workspace' && - selectionStage !== 'bark-battle-workspace' && - selectionStage !== 'visual-novel-agent-workspace' && - selectionStage !== 'baby-object-match-workspace' && - selectionStage !== 'creative-agent-workspace' && - selectionStage !== 'puzzle-gallery-detail' - ) { - setSelectionStage('platform'); + const nextSelectionStage = + resolveSelectionStageAfterProtectedDataLoss(selectionStage); + if (nextSelectionStage !== selectionStage) { + setSelectionStage(nextSelectionStage); } }, [ authUi?.user, diff --git a/src/components/platform-entry/platformSelectionStageModel.test.ts b/src/components/platform-entry/platformSelectionStageModel.test.ts new file mode 100644 index 00000000..56a7acde --- /dev/null +++ b/src/components/platform-entry/platformSelectionStageModel.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test } from 'vitest'; + +import type { SelectionStage } from './platformEntryTypes'; +import { resolveSelectionStageAfterProtectedDataLoss } from './platformSelectionStageModel'; + +describe('platformSelectionStageModel', () => { + test('keeps public and workspace stages after protected data loss', () => { + const stableStages: SelectionStage[] = [ + 'platform', + 'work-detail', + 'detail', + 'agent-workspace', + 'big-fish-agent-workspace', + 'match3d-agent-workspace', + 'square-hole-agent-workspace', + 'jump-hop-workspace', + 'wooden-fish-workspace', + 'puzzle-agent-workspace', + 'bark-battle-workspace', + 'visual-novel-agent-workspace', + 'baby-object-match-workspace', + 'creative-agent-workspace', + 'puzzle-gallery-detail', + ]; + + stableStages.forEach((stage) => { + expect(resolveSelectionStageAfterProtectedDataLoss(stage)).toBe(stage); + }); + }); + + test('resets private result, generating, runtime and profile stages to platform', () => { + const resetStages: SelectionStage[] = [ + 'profile-feedback', + 'big-fish-generating', + 'big-fish-result', + 'big-fish-runtime', + 'match3d-generating', + 'match3d-result', + 'match3d-runtime', + 'square-hole-generating', + 'square-hole-result', + 'square-hole-runtime', + 'jump-hop-generating', + 'jump-hop-result', + 'jump-hop-runtime', + 'jump-hop-gallery-detail', + 'wooden-fish-generating', + 'wooden-fish-result', + 'wooden-fish-runtime', + 'visual-novel-generating', + 'visual-novel-result', + 'visual-novel-gallery-detail', + 'visual-novel-runtime', + 'baby-object-match-generating', + 'baby-object-match-result', + 'baby-object-match-runtime', + 'baby-love-drawing-runtime', + 'puzzle-generating', + 'puzzle-onboarding', + 'puzzle-result', + 'puzzle-runtime', + 'custom-world-generating', + 'custom-world-result', + 'bark-battle-generating', + 'bark-battle-result', + 'bark-battle-runtime', + ]; + + resetStages.forEach((stage) => { + expect(resolveSelectionStageAfterProtectedDataLoss(stage)).toBe( + 'platform', + ); + }); + }); +}); diff --git a/src/components/platform-entry/platformSelectionStageModel.ts b/src/components/platform-entry/platformSelectionStageModel.ts new file mode 100644 index 00000000..33dca39c --- /dev/null +++ b/src/components/platform-entry/platformSelectionStageModel.ts @@ -0,0 +1,59 @@ +import type { SelectionStage } from './platformEntryTypes'; + +const PROTECTED_DATA_LOSS_STABLE_STAGE_BY_STAGE = { + platform: true, + 'profile-feedback': false, + 'work-detail': true, + detail: true, + 'agent-workspace': true, + 'big-fish-agent-workspace': true, + 'big-fish-generating': false, + 'big-fish-result': false, + 'big-fish-runtime': false, + 'match3d-agent-workspace': true, + 'match3d-generating': false, + 'match3d-result': false, + 'match3d-runtime': false, + 'square-hole-agent-workspace': true, + 'square-hole-generating': false, + 'square-hole-result': false, + 'square-hole-runtime': false, + 'jump-hop-workspace': true, + 'jump-hop-generating': false, + 'jump-hop-result': false, + 'jump-hop-runtime': false, + 'jump-hop-gallery-detail': false, + 'bark-battle-workspace': true, + 'bark-battle-generating': false, + 'bark-battle-result': false, + 'bark-battle-runtime': false, + 'wooden-fish-workspace': true, + 'wooden-fish-generating': false, + 'wooden-fish-result': false, + 'wooden-fish-runtime': false, + 'creative-agent-workspace': true, + 'visual-novel-agent-workspace': true, + 'visual-novel-generating': false, + 'visual-novel-result': false, + 'visual-novel-gallery-detail': false, + 'visual-novel-runtime': false, + 'baby-object-match-workspace': true, + 'baby-object-match-generating': false, + 'baby-object-match-result': false, + 'baby-object-match-runtime': false, + 'baby-love-drawing-runtime': false, + 'puzzle-agent-workspace': true, + 'puzzle-generating': false, + 'puzzle-onboarding': false, + 'puzzle-result': false, + 'puzzle-gallery-detail': true, + 'puzzle-runtime': false, + 'custom-world-generating': false, + 'custom-world-result': false, +} as const satisfies Record; + +export function resolveSelectionStageAfterProtectedDataLoss( + stage: SelectionStage, +): SelectionStage { + return PROTECTED_DATA_LOSS_STABLE_STAGE_BY_STAGE[stage] ? stage : 'platform'; +}