refactor: 收口推荐 runtime 自动启动
This commit is contained in:
@@ -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 规则、启动前准备顺序和副作用。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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`
|
||||
@@ -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 或启动指定作品。
|
||||
|
||||
## 敲木鱼
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user