refactor: 收口草稿生成作品架模型

This commit is contained in:
2026-06-03 20:11:25 +08:00
parent 69167da8d0
commit fe2f8a66e6
6 changed files with 1180 additions and 798 deletions
+8
View File
@@ -1273,6 +1273,14 @@
- 验证方式:`npm run test -- src/components/platform-entry/platformMatch3DRuntimeProfile.test.ts``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "match3d|抓大鹅"`、针对新 Module 执行 ESLint、`npm run typecheck``npm run check:encoding`
- 关联文档:`docs/technical/【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md`
## 2026-06-03 Draft Generation Shelf Model 收口
- 背景:平台壳内散落创作生成 notice key、pending 作品架占位、失败文案覆盖、拼图稳定 ID、持久化 generating/failed 判断与草稿 Tab 未读点,新增或调整玩法时需要在多处理解 `workId` / `profileId` / `sourceSessionId` / `draftId` 形状。
- 决策:新增 `src/components/platform-entry/platformDraftGenerationShelfModel.ts` 作为 Draft Generation Shelf ModuleInterface 收口 `collectDraftNoticeKeys``getGenerationNoticeShelfKeys``createPendingDraftShelfState`、各玩法 `buildPending*Works``buildCreationWorkShelfRuntimeState``collectVisibleDraftNoticeKeys``hasUnreadDraftGenerationUpdates`、拼图稳定 ID 与持久化状态判断;`PlatformEntryFlowShellImpl.tsx` 仅作为 React state、网络刷新、路由和弹窗副作用 Adapter。
- 影响范围:创作中心草稿 Tab 未读点、作品架生成中遮罩、失败草稿摘要、pending 草稿占位、拼图 / 抓大鹅生成恢复和各玩法生成完成通知。
- 验证方式:`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts``npm run test -- src/components/custom-world-home/creationWorkShelf.test.ts -t "generation state|failure notice|failed puzzle"``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating puzzle draft|persisted generating match3d draft|completed baby object match draft"`、针对新 Module 执行 ESLint、`npm run typecheck``npm run check:encoding`
- 关联文档:`docs/technical/【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md`
## 2026-06-03 Public Work Presentation 收口
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
+2
View File
@@ -43,6 +43,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,Hub 不再按玩法 `kind` 分发,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%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)。
小游戏 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)。
@@ -0,0 +1,36 @@
# 【前端架构】Draft Generation Shelf Model 收口计划
## 背景
`PlatformEntryFlowShellImpl.tsx` 同时承载创作生成状态、草稿 Tab 未读点、pending 作品架占位、失败文案覆盖和跨玩法 notice key。拼图、抓大鹅、方洞、跳一跳、敲木鱼、视觉小说、汪汪声浪、大鱼吃小鱼和宝贝识物各有不同的 `workId` / `profileId` / `sourceSessionId` / `draftId`,这些规则散在平台壳 **Implementation** 内,导致调用方必须理解每种玩法的草稿身份形状。
**Interface** 过浅:页面看似只关心“生成中 / 已完成未读 / 失败”,却要知道多 ID 去重、pending 草稿去重、失败摘要、拼图空标题兜底和持久化 generating 覆盖规则。
## 决策
新增 `src/components/platform-entry/platformDraftGenerationShelfModel.ts` 作为 Draft Generation Shelf **Module**。其 **Interface** 收口为:
- `collectDraftNoticeKeys(kind, ids)` / `getGenerationNoticeShelfKeys(item)`:统一把玩法草稿身份映射为 notice key。
- `createPendingDraftShelfState(...)``buildPending*Works(...)`:统一把本地 pending 生成状态映射成作品架占位,并避免与后端已有草稿重复。
- `buildCreationWorkShelfRuntimeState({ item, notices, pendingShelfItems })`:统一输出 `CreationWorkShelfRuntimeState`,处理失败覆盖、拼图空标题 `拼图草稿` 兜底、summary 占位覆盖、生成中遮罩和 ready 未读点。
- `collectVisibleDraftNoticeKeys(...)` / `hasUnreadDraftGenerationUpdates(...)`:统一草稿 Tab 顶部未读点规则。
- `buildPuzzleResultWorkId(...)` / `buildPuzzleResultProfileId(...)``isPersistedDraftGenerating(...)` / `isPersistedDraftFailed(...)`:把拼图稳定 ID 与持久化状态判断收在同一 **Seam**
`PlatformEntryFlowShellImpl.tsx` 仍作为 React state 与副作用 **Adapter**:负责写入 `draftGenerationNotices` / `pendingDraftShelfItems`、启动生成、刷新后端列表、打开结果页和弹窗;它不再内联 pending shelf row shape、notice key 汇总和作品架 runtime state 规则。
## 约定
- 新玩法若需进入草稿生成通知,必须在此 **Module** 补 notice key、pending 占位和 visible key 映射,避免在平台壳里新增散落 switch。
- pending 作品只用于本地生成任务尚未被后端作品架返回时的临时展示;一旦后端已有同一 `sourceSessionId` / `profileId` / `workId`pending 占位必须让位。
- 失败 notice 优先级高于持久化 generating,且可通过 pending metadata 提供更具体 summary;否则回退玩法默认失败摘要。
- 已有封面的拼图草稿即使局部关卡仍在后台生成,也不得被整卡遮罩为不可打开的生成中状态。
-**Module** 不做网络请求、路由切换、弹窗副作用或 React state 写入,只保留纯 **Implementation**,以提高 **Depth**、**Leverage** 与 **Locality**
## 验证
- `npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`
- `npm run test -- src/components/custom-world-home/creationWorkShelf.test.ts -t "generation state|failure notice|failed puzzle"`
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating puzzle draft|persisted generating match3d draft|completed baby object match draft"`
- 针对新 **Module** 与测试执行 ESLint`PlatformEntryFlowShellImpl.tsx` 保留既有 hook dependency warnings,不在本切片扩大处理。
- `npm run typecheck`
- `npm run check:encoding`
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,189 @@
import { describe, expect, test } from 'vitest';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { buildCreationWorkShelfItems } from '../custom-world-home/creationWorkShelf';
import {
buildCreationWorkShelfRuntimeState,
buildPendingPuzzleWorks,
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
collectVisibleDraftNoticeKeys,
createPendingDraftShelfState,
type DraftGenerationNoticeMap,
getGenerationNoticeShelfKeys,
hasUnreadDraftGenerationUpdates,
} from './platformDraftGenerationShelfModel';
describe('platformDraftGenerationShelfModel', () => {
test('buildPendingPuzzleWorks creates failed puzzle placeholder with stable ids and fallback title', () => {
const pending = buildPendingPuzzleWorks(
{
'puzzle-session-ocean': createPendingDraftShelfState(
'failed',
false,
'2026-06-03T08:00:00.000Z',
),
},
[],
);
expect(pending).toHaveLength(1);
expect(pending[0]).toMatchObject({
workId: 'puzzle-work-ocean',
profileId: 'puzzle-profile-ocean',
sourceSessionId: 'puzzle-session-ocean',
workTitle: '拼图草稿',
summary: '拼图草稿生成失败,可重新打开处理。',
generationStatus: 'failed',
});
});
test('buildPendingPuzzleWorks skips pending item when backend shelf already has the session', () => {
const pending = buildPendingPuzzleWorks(
{
'puzzle-session-ocean': createPendingDraftShelfState(
'generating',
false,
'2026-06-03T08:00:00.000Z',
),
},
[buildPuzzleWork({ sourceSessionId: 'puzzle-session-ocean' })],
);
expect(pending).toEqual([]);
});
test('buildCreationWorkShelfRuntimeState lets failure notice override persisted generating puzzle copy', () => {
const [item] = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
buildPuzzleWork({
workId: 'puzzle-work-empty',
profileId: 'puzzle-profile-empty',
sourceSessionId: 'puzzle-session-empty',
workTitle: '',
workDescription: '',
levelName: '',
summary: '正在生成拼图草稿。',
generationStatus: 'generating',
}),
],
});
expect(item).toBeTruthy();
const noticeKeys = getGenerationNoticeShelfKeys(item!);
const notices = Object.fromEntries(
noticeKeys.map((key) => [
key,
{ status: 'failed', seen: false },
]),
) as DraftGenerationNoticeMap;
const state = buildCreationWorkShelfRuntimeState({
item: item!,
notices,
pendingShelfItems: {
puzzle: {
'puzzle-session-empty': createPendingDraftShelfState(
'failed',
false,
'2026-06-03T08:00:00.000Z',
{ summary: '图片生成超时,可重新打开处理。' },
),
},
},
});
expect(state).toMatchObject({
isGenerating: false,
hasGenerationFailure: true,
generationFailureSummary: '拼图草稿生成失败,可重新打开处理。',
hasUnreadUpdate: false,
suppressPersistedGenerating: true,
titleOverride: '拼图草稿',
summaryOverride: '图片生成超时,可重新打开处理。',
});
});
test('collectVisibleDraftNoticeKeys and hasUnreadDraftGenerationUpdates share unread dot rule', () => {
const puzzle = buildPuzzleWork({
workId: 'puzzle-work-ocean',
profileId: 'puzzle-profile-ocean',
sourceSessionId: 'puzzle-session-ocean',
});
const visibleKeys = collectVisibleDraftNoticeKeys({
rpgItems: [],
bigFishItems: [],
jumpHopItems: [],
woodenFishItems: [],
match3dItems: [],
squareHoleItems: [],
puzzleItems: [puzzle],
visualNovelItems: [],
barkBattleItems: [],
babyObjectMatchItems: [],
});
expect(visibleKeys).toContain('puzzle:puzzle-work-ocean');
expect(visibleKeys).toContain('puzzle:puzzle-profile-ocean');
expect(visibleKeys).toContain('puzzle:puzzle-session-ocean');
expect(buildPuzzleResultWorkId('puzzle-session-ocean')).toBe(
'puzzle-work-ocean',
);
expect(buildPuzzleResultProfileId('puzzle-session-ocean')).toBe(
'puzzle-profile-ocean',
);
expect(
hasUnreadDraftGenerationUpdates(
{
'puzzle:puzzle-profile-ocean': {
status: 'ready',
seen: false,
},
},
visibleKeys,
),
).toBe(true);
expect(
hasUnreadDraftGenerationUpdates(
{
'puzzle:puzzle-profile-ocean': {
status: 'ready',
seen: true,
},
},
visibleKeys,
),
).toBe(false);
});
});
function buildPuzzleWork(
overrides: Partial<PuzzleWorkSummary> = {},
): PuzzleWorkSummary {
return {
workId: 'puzzle-work-base',
profileId: 'puzzle-profile-base',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-base',
authorDisplayName: '测试作者',
workTitle: '潮雾拼图',
workDescription: '潮雾港口拼图。',
levelName: '潮雾拼图',
summary: '潮雾港口拼图。',
themeTags: [],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'draft',
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: false,
levels: [],
...overrides,
};
}
File diff suppressed because it is too large Load Diff