refactor: 收口推荐 runtime 自动启动

This commit is contained in:
2026-06-04 04:44:22 +08:00
parent 4e23995347
commit 05713e1d3b
7 changed files with 209 additions and 36 deletions

View File

@@ -32,6 +32,14 @@
- 验证方式:`npm run test -- src/components/platform-entry/platformRecommendRuntimeAuthModel.test.ts`、针对新 Module 与平台壳执行 ESLint、`npm run typecheck``npm run check:encoding` - 验证方式:`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` - 关联文档:`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 收口 ## 2026-06-04 Platform Creation Launch Model 收口
- 背景:平台创作入口点击回调曾在 `PlatformEntryFlowShellImpl.tsx` 内联判断 `airp` 占位、隐藏的 `baby-object-match`、未知入口和各玩法工作台启动目标,壳层同时承接入口 ID 规则、启动前准备顺序和副作用。 - 背景:平台创作入口点击回调曾在 `PlatformEntryFlowShellImpl.tsx` 内联判断 `airp` 占位、隐藏的 `baby-object-match`、未知入口和各玩法工作台启动目标,壳层同时承接入口 ID 规则、启动前准备顺序和副作用。

View File

@@ -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 的匿名 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)。 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)。 平台入口创作生成通知、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)。

View File

@@ -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`

View File

@@ -171,7 +171,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
删除等破坏性动作当前未接入 jump-hop 删除 API如果后续要在作品架提供删除入口必须先补齐后端/SpacetimeDB/前端整条删除链路,再开放按钮。 删除等破坏性动作当前未接入 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 或启动指定作品。
## 敲木鱼 ## 敲木鱼

View File

@@ -553,9 +553,9 @@ import {
buildPlatformPublicGalleryFeeds, buildPlatformPublicGalleryFeeds,
getPlatformPublicGalleryEntryKey, getPlatformPublicGalleryEntryKey,
getPlatformRecommendRuntimeKind, getPlatformRecommendRuntimeKind,
isPlatformRecommendRuntimeReadyForEntry,
isSamePlatformPublicGalleryEntry, isSamePlatformPublicGalleryEntry,
type RecommendRuntimeKind, type RecommendRuntimeKind,
resolvePlatformRecommendRuntimeAutoStartDecision,
resolvePlatformRecommendRuntimeStartIntent, resolvePlatformRecommendRuntimeStartIntent,
} from './platformPublicGalleryFlow'; } from './platformPublicGalleryFlow';
import { import {
@@ -12309,31 +12309,15 @@ export function PlatformEntryFlowShellImpl({
]); ]);
useEffect(() => { useEffect(() => {
if ( const decision = resolvePlatformRecommendRuntimeAutoStartDecision({
isDesktopLayout || isDesktopLayout,
selectionStage !== 'platform' || selectionStage,
platformBootstrap.platformTab !== 'home' || platformTab: platformBootstrap.platformTab,
platformBootstrap.isLoadingPlatform isLoadingPlatform: platformBootstrap.isLoadingPlatform,
) { entries: recommendRuntimeEntries,
return; activeEntryKey: activeRecommendEntryKey,
} isStarting: isStartingRecommendEntry,
readyState: {
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, {
activeKind: activeRecommendRuntimeKind, activeKind: activeRecommendRuntimeKind,
hasBabyObjectMatchDraft: Boolean(babyObjectMatchDraft), hasBabyObjectMatchDraft: Boolean(babyObjectMatchDraft),
hasBigFishRun: Boolean(bigFishRun), hasBigFishRun: Boolean(bigFishRun),
@@ -12345,19 +12329,21 @@ export function PlatformEntryFlowShellImpl({
puzzleRunEntryProfileId: puzzleRun?.entryProfileId ?? null, puzzleRunEntryProfileId: puzzleRun?.entryProfileId ?? null,
puzzleRunCurrentLevelProfileId: puzzleRunCurrentLevelProfileId:
puzzleRun?.currentLevel?.profileId ?? null, puzzleRun?.currentLevel?.profileId ?? null,
}); },
if ( });
(activeRecommendEntry !== null && isActiveRecommendRuntimeReady) ||
isStartingRecommendEntry if (decision.type === 'noop') {
) {
return; return;
} }
const nextRecommendEntry = if (decision.type === 'clear') {
activeRecommendEntry ?? recommendRuntimeEntries[0]; setActiveRecommendEntryKey(null);
if (nextRecommendEntry) { setActiveRecommendRuntimeKind(null);
void selectRecommendRuntimeEntry(nextRecommendEntry); setActiveRecommendRuntimeError(null);
return;
} }
void selectRecommendRuntimeEntry(decision.entry);
}, [ }, [
activeRecommendEntryKey, activeRecommendEntryKey,
activeRecommendRuntimeKind, activeRecommendRuntimeKind,

View File

@@ -22,6 +22,7 @@ import {
mergePlatformPublicGalleryEntries, mergePlatformPublicGalleryEntries,
type PlatformRecommendRuntimeStartIntentDeps, type PlatformRecommendRuntimeStartIntentDeps,
type RecommendRuntimeKind, type RecommendRuntimeKind,
resolvePlatformRecommendRuntimeAutoStartDecision,
resolvePlatformRecommendRuntimeStartIntent, resolvePlatformRecommendRuntimeStartIntent,
} from './platformPublicGalleryFlow'; } from './platformPublicGalleryFlow';
import { import {
@@ -611,6 +612,92 @@ test('platform public gallery flow resolves puzzle and edutainment readiness det
).toBe(false); ).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', () => { test('platform public gallery flow merges duplicate identities and sorts newest first', () => {
const staleRpgEntry = buildRpgEntry({ const staleRpgEntry = buildRpgEntry({
profileId: 'shared-rpg', profileId: 'shared-rpg',

View File

@@ -140,6 +140,22 @@ export type PlatformRecommendRuntimeReadyState = {
puzzleRunCurrentLevelProfileId?: string | null; 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 = { export type PlatformPublicGalleryFeedsInput = {
rpgEntries: readonly CustomWorldGalleryCard[]; rpgEntries: readonly CustomWorldGalleryCard[];
bigFishEntries: readonly BigFishWorkSummary[]; bigFishEntries: readonly BigFishWorkSummary[];
@@ -423,6 +439,40 @@ export function isPlatformRecommendRuntimeReadyForEntry(
return true; 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( export function isSamePlatformPublicGalleryEntry(
left: PlatformPublicGalleryCard, left: PlatformPublicGalleryCard,
right: PlatformPublicGalleryCard, right: PlatformPublicGalleryCard,