From 4e23995347f0e49da834591d23932674e27392c4 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 04:38:01 +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=E9=89=B4=E6=9D=83=E8=AE=A1=E5=88=92?= 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 + ...mRecommendRuntimeAuthModel收口计划-2026-06-04.md | 36 +++++++ ...玩法创作】平台入口与玩法链路-2026-05-15.md | 2 +- .../PlatformEntryFlowShellImpl.tsx | 74 +++++++++----- .../platformRecommendRuntimeAuthModel.test.ts | 96 +++++++++++++++++++ .../platformRecommendRuntimeAuthModel.ts | 58 +++++++++++ 7 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 docs/technical/【前端架构】PlatformRecommendRuntimeAuthModel收口计划-2026-06-04.md create mode 100644 src/components/platform-entry/platformRecommendRuntimeAuthModel.test.ts create mode 100644 src/components/platform-entry/platformRecommendRuntimeAuthModel.ts diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 00b7f77e..53fb6e1a 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -24,6 +24,14 @@ - 验证方式:`npm run test -- src/components/platform-entry/barkBattleWorkCache.test.ts`、针对 Bark Battle Work Cache Module 与平台壳执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md`。 +## 2026-06-04 Platform Recommend Runtime Auth Model 收口 + +- 背景:平台推荐 runtime 的 embedded 启动需要在匿名 Runtime Guest Token、已登录 background auth 和非 embedded 默认鉴权之间分流,拼图还额外维护 `isolated` / `default` runtime auth mode;旧规则散在顶层 helper 与多个启动 callback。 +- 决策:新增 `src/components/platform-entry/platformRecommendRuntimeAuthModel.ts`,以 `resolvePlatformRecommendRuntimeAuthPlan(input)` 和 `shouldUsePlatformRecommendRuntimeGuestAuth(input)` 收口纯鉴权计划。壳层仍负责读取 `getStoredAccessToken()`、申请 `ensureRuntimeGuestToken()`、拼装 request options 和写入拼图 runtime auth mode。 +- 影响范围:推荐 Tab 内嵌 runtime 启动、拼图公开详情 isolated 入口、推荐运行态后续 action 的局部鉴权口径,以及后续新增可嵌入推荐 runtime 的玩法。 +- 验证方式:`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 Creation Launch Model 收口 - 背景:平台创作入口点击回调曾在 `PlatformEntryFlowShellImpl.tsx` 内联判断 `airp` 占位、隐藏的 `baby-object-match`、未知入口和各玩法工作台启动目标,壳层同时承接入口 ID 规则、启动前准备顺序和副作用。 diff --git a/docs/README.md b/docs/README.md index cadcc080..32dacdc3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,6 +69,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ Bark Battle 草稿三图完整性、生成状态归一、发布快照 / 发布回包资产兜底和草稿 / 已发布作品进入 runtime 前的 `BarkBattlePublishedConfig` 映射收口到 `src/components/platform-entry/barkBattleWorkCache.ts`,规则见 [【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md](./technical/【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md)。 +平台首页推荐 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)。 + 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/【前端架构】PlatformRecommendRuntimeAuthModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformRecommendRuntimeAuthModel收口计划-2026-06-04.md new file mode 100644 index 00000000..c2c8e32d --- /dev/null +++ b/docs/technical/【前端架构】PlatformRecommendRuntimeAuthModel收口计划-2026-06-04.md @@ -0,0 +1,36 @@ +# 【前端架构】Platform Recommend Runtime Auth Model 收口计划 + +## 背景 + +平台首页推荐流会以 embedded runtime 方式启动跳一跳、抓大鹅、方洞挑战、拼图、敲木鱼、视觉小说、大鱼吃小鱼和汪汪声浪等玩法。旧规则散在 `PlatformEntryFlowShellImpl.tsx` 顶层 helper 与多个启动 callback:匿名访客应申请 Runtime Guest Token,已登录或已有 access token 时应走 background auth,非 embedded 正常启动则不改普通鉴权。拼图还额外维护 `isolated` / `default` runtime auth mode,容易与通用推荐流口径漂移。 + +## 决策 + +新增 `src/components/platform-entry/platformRecommendRuntimeAuthModel.ts`,以纯 **Module** 收口推荐 runtime 鉴权计划: + +- `resolvePlatformRecommendRuntimeAuthPlan(input)`:返回 `requestKind` 为 `none`、`background` 或 `runtime-guest`,并给出拼图 runtime 应落到 `default` 还是 `isolated`。 +- `shouldUsePlatformRecommendRuntimeGuestAuth(input)`:只判断当前用户状态和是否允许 guest auth,不读取本地 token。 + +`PlatformEntryFlowShellImpl.tsx` 继续作为 **Adapter**:它读取 `getStoredAccessToken()`、调用 `ensureRuntimeGuestToken()`、拼装具体 request options,并在启动拼图时写入 `setPuzzleRuntimeAuthMode(...)`。 + +## Interface 约束 + +- 非 embedded 且未显式允许 runtime guest auth 时,计划为 `none`。 +- embedded 推荐 runtime 若无登录用户且无本地 access token,计划为 `runtime-guest`。 +- embedded 推荐 runtime 若已有登录用户或本地 access token,计划为 `background`。 +- 拼图公开详情要求 `authMode='isolated'` 时,匿名状态应返回 `runtime-guest` 且 `puzzleRuntimeAuthMode='isolated'`。 +- 拼图公开详情要求 `authMode='isolated'` 但已登录或已有 access token 时,应回到 `default`,避免把账号态伪装成匿名 isolated guest。 + +## Depth / Leverage / Locality + +- **Depth**:壳层传入 embedded、是否允许 guest、用户 ID 与本地 token 布尔值,即得 request 计划和拼图 runtime auth mode。 +- **Leverage**:所有推荐 runtime 启动复用同一鉴权矩阵;新增玩法只需消费计划,不再重写匿名 / 已登录分支。 +- **Locality**:guest token 选择规则集中在纯测试面,具体 token 获取和 request options 仍留在壳层副作用 Adapter。 + +## 验收 + +- `npm run test -- src/components/platform-entry/platformRecommendRuntimeAuthModel.test.ts` +- `npx eslint --max-warnings 0 src/components/platform-entry/platformRecommendRuntimeAuthModel.ts src/components/platform-entry/platformRecommendRuntimeAuthModel.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 1bcbf2b8..cd1a00cd 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 等账号/所有权动作仍保持普通用户鉴权。 +推荐页匿名游玩不再限定为跳一跳。移动端一级 `推荐` 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。 ## 敲木鱼 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index b9d7807c..23174933 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -585,6 +585,10 @@ import { buildPuzzleResultProfileId, buildPuzzleResultWorkId, } from './platformPuzzleIdentityModel'; +import { + type PlatformPuzzleRuntimeAuthMode, + resolvePlatformRecommendRuntimeAuthPlan, +} from './platformRecommendRuntimeAuthModel'; import { buildPlatformRpgAgentResultPublishGateView, resolvePlatformRpgAgentResultPreviewSourceLabel, @@ -624,7 +628,7 @@ type PuzzleRuntimeReturnStage = | 'puzzle-gallery-detail' | 'work-detail' | 'platform'; -type PuzzleRuntimeAuthMode = 'default' | 'isolated'; +type PuzzleRuntimeAuthMode = PlatformPuzzleRuntimeAuthMode; type PuzzleOnboardingDraft = { promptText: string; @@ -686,6 +690,10 @@ const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS = BACKGROUND_AUTH_REQUEST_OPTIONS; const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS = RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS; +type RecommendRuntimeAuthUi = + | { user?: { id?: string } | null } + | null + | undefined; async function buildRecommendRuntimeGuestOptions() { const { token } = await ensureRuntimeGuestToken(); return { @@ -693,24 +701,35 @@ async function buildRecommendRuntimeGuestOptions() { runtimeGuestToken: token, }; } -function shouldUseRecommendRuntimeGuestAuth( - authUi: { user?: { id?: string } | null } | null | undefined, +function resolveCurrentRecommendRuntimeAuthPlan( + authUi: RecommendRuntimeAuthUi, + input: { embedded?: boolean; allowRuntimeGuestAuth?: boolean } = {}, ) { - return !authUi?.user?.id?.trim() && !getStoredAccessToken(); + return resolvePlatformRecommendRuntimeAuthPlan({ + embedded: input.embedded, + allowRuntimeGuestAuth: input.allowRuntimeGuestAuth, + authUserId: authUi?.user?.id ?? null, + hasStoredAccessToken: Boolean(getStoredAccessToken()), + }); } -async function buildRecommendRuntimeAuthOptions( - authUi: { user?: { id?: string } | null } | null | undefined, - embedded?: boolean, +async function buildRecommendRuntimeOptionsFromAuthPlan( + plan: ReturnType, ) { - if (!embedded) { - return {}; - } - - if (shouldUseRecommendRuntimeGuestAuth(authUi)) { + if (plan.requestKind === 'runtime-guest') { return buildRecommendRuntimeGuestOptions(); } - - return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS; + if (plan.requestKind === 'background') { + return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS; + } + return {}; +} +async function buildRecommendRuntimeAuthOptions( + authUi: RecommendRuntimeAuthUi, + embedded?: boolean, +) { + return buildRecommendRuntimeOptionsFromAuthPlan( + resolveCurrentRecommendRuntimeAuthPlan(authUi, { embedded }), + ); } const PUZZLE_DRAFT_GENERATION_POINT_COST = 2; const MATCH3D_DRAFT_GENERATION_POINT_COST = 10; @@ -8106,19 +8125,22 @@ export function PlatformEntryFlowShellImpl({ profileId: item.profileId, levelId: normalizedLevelId || null, }; - const canUseRuntimeGuestAuth = - options.embedded || options.authMode === 'isolated'; - const useRuntimeGuestAuth = - canUseRuntimeGuestAuth && shouldUseRecommendRuntimeGuestAuth(authUi); - const runtimeGuestOptions = useRuntimeGuestAuth - ? await buildRecommendRuntimeGuestOptions() - : {}; - const authMode = useRuntimeGuestAuth ? 'isolated' : 'default'; - const runtimeAuthOptions = useRuntimeGuestAuth - ? runtimeGuestOptions - : canUseRuntimeGuestAuth - ? RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS + const authPlan = resolveCurrentRecommendRuntimeAuthPlan(authUi, { + embedded: options.embedded, + allowRuntimeGuestAuth: + options.embedded || options.authMode === 'isolated', + }); + const runtimeGuestOptions = + authPlan.requestKind === 'runtime-guest' + ? await buildRecommendRuntimeGuestOptions() : {}; + const authMode = authPlan.puzzleRuntimeAuthMode; + const runtimeAuthOptions = + authPlan.requestKind === 'runtime-guest' + ? runtimeGuestOptions + : authPlan.requestKind === 'background' + ? RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS + : {}; const { run } = authMode === 'isolated' ? await startPuzzleRun(startRunPayload, runtimeGuestOptions) diff --git a/src/components/platform-entry/platformRecommendRuntimeAuthModel.test.ts b/src/components/platform-entry/platformRecommendRuntimeAuthModel.test.ts new file mode 100644 index 00000000..82a05576 --- /dev/null +++ b/src/components/platform-entry/platformRecommendRuntimeAuthModel.test.ts @@ -0,0 +1,96 @@ +import { expect, test } from 'vitest'; + +import { + resolvePlatformRecommendRuntimeAuthPlan, + shouldUsePlatformRecommendRuntimeGuestAuth, +} from './platformRecommendRuntimeAuthModel'; + +test('uses runtime guest auth for anonymous embedded recommendation runtime', () => { + expect( + resolvePlatformRecommendRuntimeAuthPlan({ + embedded: true, + authUserId: null, + hasStoredAccessToken: false, + }), + ).toEqual({ + requestKind: 'runtime-guest', + puzzleRuntimeAuthMode: 'isolated', + }); +}); + +test('uses background auth for signed-in embedded recommendation runtime', () => { + expect( + resolvePlatformRecommendRuntimeAuthPlan({ + embedded: true, + authUserId: 'user-1', + hasStoredAccessToken: false, + }), + ).toEqual({ + requestKind: 'background', + puzzleRuntimeAuthMode: 'default', + }); +}); + +test('uses background auth when embedded runtime has only a stored access token', () => { + expect( + resolvePlatformRecommendRuntimeAuthPlan({ + embedded: true, + authUserId: null, + hasStoredAccessToken: true, + }), + ).toEqual({ + requestKind: 'background', + puzzleRuntimeAuthMode: 'default', + }); +}); + +test('does not alter auth for non-embedded runtime launches by default', () => { + expect( + resolvePlatformRecommendRuntimeAuthPlan({ + embedded: false, + authUserId: null, + hasStoredAccessToken: false, + }), + ).toEqual({ + requestKind: 'none', + puzzleRuntimeAuthMode: 'default', + }); +}); + +test('uses isolated guest auth for anonymous puzzle isolated launch', () => { + expect( + resolvePlatformRecommendRuntimeAuthPlan({ + embedded: false, + allowRuntimeGuestAuth: true, + authUserId: null, + hasStoredAccessToken: false, + }), + ).toEqual({ + requestKind: 'runtime-guest', + puzzleRuntimeAuthMode: 'isolated', + }); +}); + +test('falls back to default puzzle auth when isolated launch has account auth', () => { + expect( + resolvePlatformRecommendRuntimeAuthPlan({ + embedded: false, + allowRuntimeGuestAuth: true, + authUserId: 'user-1', + hasStoredAccessToken: false, + }), + ).toEqual({ + requestKind: 'none', + puzzleRuntimeAuthMode: 'default', + }); +}); + +test('guest auth decision trims user id before treating account as signed in', () => { + expect( + shouldUsePlatformRecommendRuntimeGuestAuth({ + allowRuntimeGuestAuth: true, + authUserId: ' ', + hasStoredAccessToken: false, + }), + ).toBe(true); +}); diff --git a/src/components/platform-entry/platformRecommendRuntimeAuthModel.ts b/src/components/platform-entry/platformRecommendRuntimeAuthModel.ts new file mode 100644 index 00000000..6db1007a --- /dev/null +++ b/src/components/platform-entry/platformRecommendRuntimeAuthModel.ts @@ -0,0 +1,58 @@ +export type PlatformRecommendRuntimeRequestKind = + | 'none' + | 'background' + | 'runtime-guest'; + +export type PlatformPuzzleRuntimeAuthMode = 'default' | 'isolated'; + +export type PlatformRecommendRuntimeAuthPlan = { + requestKind: PlatformRecommendRuntimeRequestKind; + puzzleRuntimeAuthMode: PlatformPuzzleRuntimeAuthMode; +}; + +export type PlatformRecommendRuntimeAuthInput = { + embedded?: boolean; + allowRuntimeGuestAuth?: boolean; + authUserId?: string | null; + hasStoredAccessToken?: boolean; +}; + +function hasAccountAuth(input: { + authUserId?: string | null; + hasStoredAccessToken?: boolean; +}) { + return Boolean(input.authUserId?.trim() || input.hasStoredAccessToken); +} + +export function shouldUsePlatformRecommendRuntimeGuestAuth( + input: Pick< + PlatformRecommendRuntimeAuthInput, + 'allowRuntimeGuestAuth' | 'authUserId' | 'hasStoredAccessToken' + >, +) { + return Boolean(input.allowRuntimeGuestAuth) && !hasAccountAuth(input); +} + +export function resolvePlatformRecommendRuntimeAuthPlan( + input: PlatformRecommendRuntimeAuthInput, +): PlatformRecommendRuntimeAuthPlan { + const embedded = Boolean(input.embedded); + const allowRuntimeGuestAuth = input.allowRuntimeGuestAuth ?? embedded; + const useRuntimeGuestAuth = shouldUsePlatformRecommendRuntimeGuestAuth({ + allowRuntimeGuestAuth, + authUserId: input.authUserId, + hasStoredAccessToken: input.hasStoredAccessToken, + }); + + if (useRuntimeGuestAuth) { + return { + requestKind: 'runtime-guest', + puzzleRuntimeAuthMode: 'isolated', + }; + } + + return { + requestKind: embedded ? 'background' : 'none', + puzzleRuntimeAuthMode: 'default', + }; +}