refactor: 收口推荐 runtime 鉴权计划
This commit is contained in:
@@ -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 规则、启动前准备顺序和副作用。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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`
|
||||
@@ -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。
|
||||
|
||||
## 敲木鱼
|
||||
|
||||
|
||||
@@ -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<typeof resolvePlatformRecommendRuntimeAuthPlan>,
|
||||
) {
|
||||
if (!embedded) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (shouldUseRecommendRuntimeGuestAuth(authUi)) {
|
||||
if (plan.requestKind === 'runtime-guest') {
|
||||
return buildRecommendRuntimeGuestOptions();
|
||||
}
|
||||
|
||||
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,17 +8125,20 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: item.profileId,
|
||||
levelId: normalizedLevelId || null,
|
||||
};
|
||||
const canUseRuntimeGuestAuth =
|
||||
options.embedded || options.authMode === 'isolated';
|
||||
const useRuntimeGuestAuth =
|
||||
canUseRuntimeGuestAuth && shouldUseRecommendRuntimeGuestAuth(authUi);
|
||||
const runtimeGuestOptions = useRuntimeGuestAuth
|
||||
const authPlan = resolveCurrentRecommendRuntimeAuthPlan(authUi, {
|
||||
embedded: options.embedded,
|
||||
allowRuntimeGuestAuth:
|
||||
options.embedded || options.authMode === 'isolated',
|
||||
});
|
||||
const runtimeGuestOptions =
|
||||
authPlan.requestKind === 'runtime-guest'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const authMode = useRuntimeGuestAuth ? 'isolated' : 'default';
|
||||
const runtimeAuthOptions = useRuntimeGuestAuth
|
||||
const authMode = authPlan.puzzleRuntimeAuthMode;
|
||||
const runtimeAuthOptions =
|
||||
authPlan.requestKind === 'runtime-guest'
|
||||
? runtimeGuestOptions
|
||||
: canUseRuntimeGuestAuth
|
||||
: authPlan.requestKind === 'background'
|
||||
? RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS
|
||||
: {};
|
||||
const { run } =
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user