diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 243a3e96..4f7e0899 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -25,6 +25,7 @@ - 追加决策:公开详情点赞能力矩阵由 `resolvePlatformPublicWorkLikeIntent(entry)` 收口;Module 只返回大鱼吃小鱼、拼图、旧 RPG gallery fallback 或不可用文案,壳层仍执行鉴权、API 调用、缓存同步、错误展示和 busy 状态。 - 追加决策:公开详情改造能力矩阵由 `resolvePlatformPublicWorkRemixIntent(entry)` 收口;Module 只返回大鱼吃小鱼、拼图、旧 RPG gallery fallback 或不可用文案,壳层仍执行鉴权、remix API、session / 缓存写入、stage 切换、错误展示和 busy 状态。 - 追加决策:公开详情启动分流由 `resolvePlatformPublicWorkStartIntent(entry, deps)` 收口;Module 只返回大鱼吃小鱼、拼图、跳一跳、敲木鱼、抓大鹅、方洞挑战、视觉小说、汪汪声浪、宝贝识物或旧 RPG gallery 记录游玩的 intent。壳层仍执行登录保护、运行态启动、RPG 游玩记录、详情更新、busy 状态和错误展示;抓大鹅 public detail -> work mapper 作为 Adapter 注入,继续由 Match3D Runtime Profile Module 维护素材归一与背景资产提升。 +- 追加决策:自有公开作品编辑分流由 `resolvePlatformPublicWorkEditIntent(entry, deps)` 收口;Module 只返回可编辑草稿目标、需解析宝贝识物本地草稿 intent、旧 RPG gallery 编辑 intent 或原阻断文案。壳层仍执行登录保护、草稿恢复、宝贝识物异步草稿解析、RPG 编辑导航和错误展示;抓大鹅 public detail -> work mapper 仍作为 Adapter 注入,不复制 Match3D 素材归一规则。 - 影响范围:统一作品详情入口、公开详情打开策略、自有公开作品编辑 / 改造动作模式,以及后续新增玩法公开详情接入。 - 验证方式:`npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`、`npm run test -- src/components/platform-entry/platformMatch3DRuntimeProfile.test.ts`、公开详情壳层交互回归、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md`。 diff --git a/docs/README.md b/docs/README.md index bf0f499e..abdad1fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,7 +41,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 -统一作品详情页的玩法 kind、详情打开策略、自有作品动作模式、点赞 / 改造 / 启动意图和公开详情映射收口到 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`;抓大鹅公开详情映射与启动 Adapter 的素材归一仍归 `platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md](./technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md)。 +统一作品详情页的玩法 kind、详情打开策略、自有作品动作模式、编辑 / 点赞 / 改造 / 启动意图和公开详情映射收口到 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`;抓大鹅公开详情映射与启动 / 编辑 Adapter 的素材归一仍归 `platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md](./technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md)。 创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,生产 Hub 只接收 `CreationWorkShelfItem[]` 与 UI 状态,不再接收各玩法 raw items 和回调列阵,规则见 [【前端架构】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)。 diff --git a/docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md b/docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md index 3160a28d..37571dab 100644 --- a/docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md +++ b/docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md @@ -11,6 +11,7 @@ - `getPlatformPublicWorkDetailKind(entry)` - `resolvePlatformPublicWorkDetailOpenStrategy(entry)` - `resolvePlatformPublicWorkActionMode(entry, viewerUserId)` + - `resolvePlatformPublicWorkEditIntent(entry, deps)` - `resolvePlatformPublicWorkLikeIntent(entry)` - `resolvePlatformPublicWorkRemixIntent(entry)` - `resolvePlatformPublicWorkStartIntent(entry, deps)` @@ -25,7 +26,7 @@ - `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter:根据 open strategy 调用 `openPublicWorkDetail`、`openPuzzlePublicWorkDetail`、`openJumpHopPublicWorkDetail`、`openWoodenFishPublicWorkDetail`、`openVisualNovelPublicWorkDetail` 或 `openRpgPublicWorkDetail`。 - 公开详情 entry 映射与公开详情反推玩法 work 摘要也收口到 Module。壳层只在运行态启动、编辑、改造、推荐缓存和详情展示时调用映射 Interface,不再在壳层顶部持有每个玩法的 DTO 拼装 Implementation。 - `mapMatch3DWorkToPublicWorkDetail` 归入 `platformMatch3DRuntimeProfile.ts`,继续委托 `normalizeMatch3DWorkForRuntimeUi` 处理素材归一和背景资产提升;`platformPublicWorkDetailFlow.ts` 不复制 Match3D 运行态素材规则。 -- 公开详情启动、点赞和改造只抽“意图” Interface,不把整个 callback 搬进 Module。壳层继续作为 Adapter 执行鉴权、API 调用、运行态启动、busy 状态、缓存同步、stage 切换和错误 setter,避免形成伪 Seam。 +- 公开详情启动、编辑、点赞和改造只抽“意图” Interface,不把整个 callback 搬进 Module。壳层继续作为 Adapter 执行鉴权、API 调用、运行态启动、草稿恢复、busy 状态、缓存同步、stage 切换和错误 setter,避免形成伪 Seam。 ## Interface 约束 @@ -35,6 +36,8 @@ - 大鱼吃小鱼、抓大鹅、方洞挑战、汪汪声浪、宝贝识物和其它可直接展示的公开 entry 返回 `use-entry` strategy。 - RPG 返回 `load-rpg-detail` strategy,由壳层 Adapter 继续调用 RPG 详情读取流程。 - `resolvePlatformPublicWorkActionMode` 只比较 `entry.ownerUserId` 与当前 viewer user id;当前用户拥有该公开作品时返回 `edit`,否则返回 `remix`。 +- `resolvePlatformPublicWorkEditIntent` 只表达自有公开作品编辑意图:大鱼吃小鱼、拼图、抓大鹅、方洞挑战、视觉小说和汪汪声浪在能定位原草稿时返回对应 draft open 目标;跳一跳、敲木鱼和缺草稿作品返回原阻断文案;宝贝识物只返回需解析本地草稿的 intent;旧 RPG gallery fallback 只在完整 RPG 详情已补读且 profile 匹配时返回编辑 intent。壳层仍执行草稿恢复、宝贝识物异步草稿解析、RPG 编辑导航和错误展示。 +- `resolvePlatformPublicWorkEditIntent` 的 `deps` 只接编辑决策所需的当前拼图详情、当前 RPG 详情、视觉小说作品缓存、汪汪声浪作品缓存,以及抓大鹅 public detail -> work 的 Adapter。抓大鹅 Adapter 必须来自 Match3D Runtime Profile Module,以保留 `generatedItemAssets` 归一化与背景资产提升的 Locality。 - `resolvePlatformPublicWorkLikeIntent` 只表达公开作品点赞意图:大鱼吃小鱼、拼图和旧 RPG gallery fallback 返回可执行 intent;宝贝识物、汪汪声浪、方洞挑战和视觉小说返回不可用文案。壳层只按 intent 调用 API、写缓存和展示错误,不再持有这组能力矩阵。 - `resolvePlatformPublicWorkRemixIntent` 只表达公开作品改造意图:大鱼吃小鱼和拼图返回可执行 intent 与成功后目标 stage,旧 RPG gallery fallback 返回可执行 intent,其它玩法返回原未开放文案。壳层只按 intent 调用 remix API、写 session / 缓存、切 stage 和展示错误。 - `resolvePlatformPublicWorkStartIntent` 只表达公开作品“开始游玩”意图:大鱼吃小鱼、拼图、跳一跳、敲木鱼、抓大鹅、方洞挑战、视觉小说、汪汪声浪和宝贝识物返回对应启动目标;旧 RPG gallery fallback 只在完整 RPG 详情已补读且 profile 匹配时返回记录游玩 intent,否则返回原阻断文案。壳层仍执行登录保护、运行态启动、RPG 游玩记录、详情更新、busy 状态和错误展示。 @@ -48,7 +51,7 @@ ## Depth / Leverage / Locality -- **Depth**:壳层传入公开作品 entry、玩法 work summary、当前用户 id、当前拼图 run 或少量启动 deps,即可得到详情打开策略、动作模式、点赞 / 改造 / 启动意图、统一详情映射和封面可见数;玩法判定、能力矩阵与 DTO 默认值藏在 Module Implementation 内。 +- **Depth**:壳层传入公开作品 entry、玩法 work summary、当前用户 id、当前拼图 run 或少量启动 / 编辑 deps,即可得到详情打开策略、动作模式、编辑 / 点赞 / 改造 / 启动意图、统一详情映射和封面可见数;玩法判定、能力矩阵与 DTO 默认值藏在 Module Implementation 内。 - **Leverage**:新增玩法公开详情时先补 Strategy / Mapping 单测,再接壳层 Adapter,不必在多个 JSX / callback 位置重复 sourceType 判断或 DTO 回填。 - **Locality**:公开作品详情入口的纯策略与通用映射集中到一个小 Module;Match3D 素材归一仍在 Match3D Module;启动运行态、点赞、改造、编辑等副作用仍留在壳层,避免伪 Seam。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index b3474814..a828c87d 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -532,6 +532,7 @@ import { resolvePlatformPublicWorkActionMode, resolvePlatformPublicWorkDetailOpenDecision, resolvePlatformPublicWorkDetailOpenStrategy, + resolvePlatformPublicWorkEditIntent, resolvePlatformPublicWorkLikeIntent, resolvePlatformPublicWorkRemixIntent, resolvePlatformPublicWorkStartIntent, @@ -13513,117 +13514,65 @@ export function PlatformEntryFlowShellImpl({ runProtectedAction(async () => { setPublicWorkDetailError(null); - // 中文注释:自有公开作品必须恢复原草稿,不能复用 remix 复制链路。 - if (isBigFishGalleryEntry(entry)) { - const work = mapPublicWorkDetailToBigFishWork(entry); - if (!work?.sourceSessionId?.trim()) { - setPublicWorkDetailError( - '这份大鱼吃小鱼作品缺少原草稿会话,暂时无法编辑。', + const intent = resolvePlatformPublicWorkEditIntent(entry, { + selectedPuzzleDetail, + selectedRpgDetailEntry: selectedDetailEntry, + visualNovelWorks, + barkBattleGalleryEntries, + barkBattleWorks, + mapMatch3DWork: mapPublicWorkDetailToMatch3DWork, + }); + + switch (intent.type) { + case 'blocked': + setPublicWorkDetailError(intent.errorMessage); + return; + case 'edit-big-fish': + void openBigFishDraft(intent.work); + return; + case 'edit-puzzle': + void openPuzzleDraft(intent.work); + return; + case 'edit-match3d': + void openMatch3DDraft(intent.work, { + forceDraft: intent.forceDraft, + }); + return; + case 'edit-square-hole': + void openSquareHoleDraft(intent.work, { + forceDraft: intent.forceDraft, + }); + return; + case 'edit-visual-novel': + void openVisualNovelDraft(intent.work, { + forceDraft: intent.forceDraft, + }); + return; + case 'resolve-edutainment-draft': { + const matchedDraft = await resolveBabyObjectMatchRuntimeDraft( + intent.entry, ); + if (!matchedDraft) { + setPublicWorkDetailError('这份宝贝识物缺少可编辑草稿。'); + return; + } + + openBabyObjectMatchDraft(matchedDraft); return; } - void openBigFishDraft(work); - return; - } - - if (isPuzzleGalleryEntry(entry)) { - const work = - selectedPuzzleDetail?.profileId === entry.profileId - ? selectedPuzzleDetail - : mapPublicWorkDetailToPuzzleWork(entry); - if (!work?.sourceSessionId?.trim()) { - setPublicWorkDetailError( - '这份拼图作品缺少原草稿会话,暂时无法编辑。', - ); + case 'edit-bark-battle': + openBarkBattleDraft(intent.work, { + forceDraft: intent.forceDraft, + }); return; - } - void openPuzzleDraft(work); - return; - } - - if (isMatch3DGalleryEntry(entry)) { - const work = mapPublicWorkDetailToMatch3DWork(entry); - if (!work?.sourceSessionId?.trim()) { - setPublicWorkDetailError( - '这份抓大鹅作品缺少原草稿会话,暂时无法编辑。', - ); + case 'edit-rpg-gallery': + void detailNavigation.openSavedCustomWorldEditor(intent.entry); return; + default: { + const exhaustive: never = intent; + return exhaustive; } - void openMatch3DDraft(work, { forceDraft: true }); - return; } - - if (isSquareHoleGalleryEntry(entry)) { - const work = mapPublicWorkDetailToSquareHoleWork(entry); - if (!work?.sourceSessionId?.trim()) { - setPublicWorkDetailError( - '这份方洞挑战作品缺少原草稿会话,暂时无法编辑。', - ); - return; - } - void openSquareHoleDraft(work, { forceDraft: true }); - return; - } - - if (isJumpHopGalleryEntry(entry)) { - setPublicWorkDetailError('这份跳一跳作品暂时请从作品架编辑。'); - return; - } - - if (isWoodenFishGalleryEntry(entry)) { - setPublicWorkDetailError('这份敲木鱼作品暂时请从作品架编辑。'); - return; - } - - if (isVisualNovelGalleryEntry(entry)) { - const matchedWork = visualNovelWorks.find( - (work) => work.profileId === entry.profileId, - ); - if (!matchedWork) { - setPublicWorkDetailError('这份视觉小说缺少可编辑草稿。'); - return; - } - void openVisualNovelDraft(matchedWork, { forceDraft: true }); - return; - } - - if (isEdutainmentGalleryEntry(entry)) { - const matchedDraft = await resolveBabyObjectMatchRuntimeDraft(entry); - if (!matchedDraft) { - setPublicWorkDetailError('这份宝贝识物缺少可编辑草稿。'); - return; - } - - openBabyObjectMatchDraft(matchedDraft); - return; - } - - if (isBarkBattleGalleryEntry(entry)) { - const matchedWork = - barkBattleWorks.find((work) => work.workId === entry.workId) ?? - barkBattleGalleryEntries.find( - (work) => work.workId === entry.workId, - ) ?? - mapBarkBattlePublicDetailToWorkSummary(entry); - if (!matchedWork?.draftId?.trim()) { - setPublicWorkDetailError('这份汪汪声浪缺少可编辑草稿。'); - return; - } - - openBarkBattleDraft(matchedWork, { forceDraft: true }); - return; - } - - const editEntry = - selectedDetailEntry?.profileId === entry.profileId - ? selectedDetailEntry - : null; - if (!editEntry) { - setPublicWorkDetailError('作品详情尚未读取完成。'); - return; - } - - void detailNavigation.openSavedCustomWorldEditor(editEntry); }); }, [ diff --git a/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts b/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts index 50886cd2..d8d40e0c 100644 --- a/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts +++ b/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts @@ -6,10 +6,14 @@ import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/co import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; -import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime'; +import type { + CustomWorldGalleryCard, + CustomWorldLibraryEntry, +} from '../../../packages/shared/src/contracts/runtime'; import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish'; +import type { CustomWorldProfile } from '../../types'; import { EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID, EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME, @@ -31,11 +35,13 @@ import { mapWoodenFishWorkToPublicWorkDetail, type PlatformPublicWorkDetailKind, type PlatformPublicWorkDetailOpenStrategy, + type PlatformPublicWorkEditIntentDeps, type PlatformPublicWorkStartIntentDeps, resolveActivePlatformPublicWorkAuthorEntry, resolvePlatformPublicWorkActionMode, resolvePlatformPublicWorkDetailOpenDecision, resolvePlatformPublicWorkDetailOpenStrategy, + resolvePlatformPublicWorkEditIntent, resolvePlatformPublicWorkLikeIntent, resolvePlatformPublicWorkRemixIntent, resolvePlatformPublicWorkStartIntent, @@ -88,6 +94,18 @@ function buildRpgEntry( }; } +function buildRpgLibraryEntry( + overrides: Partial> = {}, +): CustomWorldLibraryEntry { + return { + ...buildRpgEntry(overrides), + profile: { + id: overrides.profileId ?? 'rpg-profile', + } as unknown as CustomWorldProfile, + ...overrides, + }; +} + function buildTypedEntry( sourceType: TSourceType, overrides: TypedPlatformPublicGalleryCardOverrides = {}, @@ -410,6 +428,20 @@ function buildStartIntentDeps( }; } +function buildEditIntentDeps( + overrides: Partial = {}, +): PlatformPublicWorkEditIntentDeps { + return { + selectedPuzzleDetail: null, + selectedRpgDetailEntry: null, + visualNovelWorks: [], + barkBattleGalleryEntries: [], + barkBattleWorks: [], + mapMatch3DWork: () => buildMatch3DWork(), + ...overrides, + }; +} + test('platform public work detail flow resolves detail kind for every play kind', () => { const cases: Array< [sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind] @@ -435,7 +467,7 @@ test('platform public work detail flow resolves detail kind for every play kind' }); test('platform public work detail flow resolves open strategy', () => { - const rpgEntry = buildRpgEntry(); + const rpgEntry = buildRpgLibraryEntry(); const cases: Array< [ entry: PlatformPublicGalleryCard, @@ -522,7 +554,7 @@ test('platform public work detail flow resolves open strategy', () => { }); test('platform public work detail flow maps work summaries to detail entries', () => { - const rpgEntry = buildRpgEntry(); + const rpgEntry = buildRpgLibraryEntry(); expect(mapRpgGalleryCardToPublicWorkDetail(rpgEntry)).toBe(rpgEntry); expect(mapPuzzleWorkToPublicWorkDetail(buildPuzzleWork())).toMatchObject({ @@ -843,6 +875,205 @@ test('platform public work detail flow resolves remix intent', () => { }); }); +test('platform public work detail flow resolves edit intent for draft-backed works', () => { + const bigFishEntry = buildTypedEntry('big-fish'); + expect(resolvePlatformPublicWorkEditIntent(bigFishEntry, buildEditIntentDeps())) + .toEqual({ + type: 'edit-big-fish', + work: mapPublicWorkDetailToBigFishWork(bigFishEntry), + }); + + const selectedPuzzleDetail = buildPuzzleWork({ + profileId: 'puzzle-profile', + sourceSessionId: 'selected-puzzle-session', + }); + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('puzzle'), + buildEditIntentDeps({ selectedPuzzleDetail }), + ), + ).toEqual({ + type: 'edit-puzzle', + work: selectedPuzzleDetail, + }); + + const puzzleEntry = buildTypedEntry('puzzle', { + profileId: 'fallback-puzzle-profile', + sourceSessionId: 'fallback-puzzle-session', + }); + expect( + resolvePlatformPublicWorkEditIntent( + puzzleEntry, + buildEditIntentDeps({ + selectedPuzzleDetail: buildPuzzleWork({ profileId: 'stale-profile' }), + }), + ), + ).toEqual({ + type: 'edit-puzzle', + work: mapPublicWorkDetailToPuzzleWork(puzzleEntry), + }); + + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('puzzle', { sourceSessionId: null }), + buildEditIntentDeps(), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份拼图作品缺少原草稿会话,暂时无法编辑。', + }); +}); + +test('platform public work detail flow resolves edit intent for mapper-backed works', () => { + const match3DEntry = buildTypedEntry('match3d'); + const match3DWork = buildMatch3DWork({ workId: 'editable-match3d-work' }); + expect( + resolvePlatformPublicWorkEditIntent( + match3DEntry, + buildEditIntentDeps({ + mapMatch3DWork: (entry) => + entry === match3DEntry ? match3DWork : null, + }), + ), + ).toEqual({ + type: 'edit-match3d', + work: match3DWork, + forceDraft: true, + }); + expect( + resolvePlatformPublicWorkEditIntent( + match3DEntry, + buildEditIntentDeps({ + mapMatch3DWork: () => buildMatch3DWork({ sourceSessionId: ' ' }), + }), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份抓大鹅作品缺少原草稿会话,暂时无法编辑。', + }); + + const squareHoleEntry = buildTypedEntry('square-hole', { + sourceSessionId: 'square-hole-session', + }); + expect( + resolvePlatformPublicWorkEditIntent(squareHoleEntry, buildEditIntentDeps()), + ).toEqual({ + type: 'edit-square-hole', + work: mapPublicWorkDetailToSquareHoleWork(squareHoleEntry), + forceDraft: true, + }); + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('square-hole', { sourceSessionId: null }), + buildEditIntentDeps(), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份方洞挑战作品缺少原草稿会话,暂时无法编辑。', + }); +}); + +test('platform public work detail flow resolves edit intent for cached work lookups', () => { + const visualNovelWork = buildVisualNovelWork(); + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('visual-novel'), + buildEditIntentDeps({ visualNovelWorks: [visualNovelWork] }), + ), + ).toEqual({ + type: 'edit-visual-novel', + work: visualNovelWork, + forceDraft: true, + }); + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('visual-novel'), + buildEditIntentDeps(), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份视觉小说缺少可编辑草稿。', + }); + + const entry = buildTypedEntry('bark-battle'); + const galleryWork = buildBarkBattleWork({ + workId: 'bark-battle-work', + draftId: 'gallery-draft', + }); + const loadedWork = buildBarkBattleWork({ + workId: 'bark-battle-work', + draftId: 'loaded-draft', + }); + expect( + resolvePlatformPublicWorkEditIntent( + entry, + buildEditIntentDeps({ + barkBattleGalleryEntries: [galleryWork], + barkBattleWorks: [loadedWork], + }), + ), + ).toEqual({ + type: 'edit-bark-battle', + work: loadedWork, + forceDraft: true, + }); + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('bark-battle', { sourceSessionId: null }), + buildEditIntentDeps(), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份汪汪声浪缺少可编辑草稿。', + }); +}); + +test('platform public work detail flow resolves edit intent for unsupported and deferred works', () => { + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('jump-hop'), + buildEditIntentDeps(), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份跳一跳作品暂时请从作品架编辑。', + }); + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('wooden-fish'), + buildEditIntentDeps(), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份敲木鱼作品暂时请从作品架编辑。', + }); + + const edutainmentEntry = buildTypedEntry('edutainment'); + expect( + resolvePlatformPublicWorkEditIntent(edutainmentEntry, buildEditIntentDeps()), + ).toEqual({ + type: 'resolve-edutainment-draft', + entry: edutainmentEntry, + }); + + const rpgEntry = buildRpgLibraryEntry(); + expect( + resolvePlatformPublicWorkEditIntent( + rpgEntry, + buildEditIntentDeps({ selectedRpgDetailEntry: rpgEntry }), + ), + ).toEqual({ + type: 'edit-rpg-gallery', + entry: rpgEntry, + }); + expect( + resolvePlatformPublicWorkEditIntent(rpgEntry, buildEditIntentDeps()), + ).toEqual({ + type: 'blocked', + errorMessage: '作品详情尚未读取完成。', + }); +}); + test('platform public work detail flow resolves start intent for direct launches', () => { const bigFishEntry = buildTypedEntry('big-fish'); expect( diff --git a/src/components/platform-entry/platformPublicWorkDetailFlow.ts b/src/components/platform-entry/platformPublicWorkDetailFlow.ts index 2dc79d32..9bcb4c0f 100644 --- a/src/components/platform-entry/platformPublicWorkDetailFlow.ts +++ b/src/components/platform-entry/platformPublicWorkDetailFlow.ts @@ -7,7 +7,10 @@ import type { import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; -import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime'; +import type { + CustomWorldGalleryCard, + CustomWorldLibraryEntry, +} from '../../../packages/shared/src/contracts/runtime'; import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; import type { @@ -15,6 +18,7 @@ import type { WoodenFishWorkProfileResponse, } from '../../../packages/shared/src/contracts/woodenFish'; import { buildPublicWorkStagePath } from '../../routing/appPageRoutes'; +import type { CustomWorldProfile } from '../../types'; import { isBarkBattleGalleryEntry, isBigFishGalleryEntry, @@ -51,6 +55,10 @@ export type PlatformPublicWorkDetailKind = | 'visual-novel' | 'wooden-fish'; +export type PlatformRpgPublicWorkDetailEntry = + | CustomWorldGalleryCard + | CustomWorldLibraryEntry; + export type PlatformPublicWorkDetailOpenStrategy = | { type: 'use-entry'; @@ -77,7 +85,7 @@ export type PlatformPublicWorkDetailOpenStrategy = } | { type: 'load-rpg-detail'; - entry: CustomWorldGalleryCard; + entry: PlatformRpgPublicWorkDetailEntry; }; export type PlatformPublicWorkActionMode = 'edit' | 'remix'; @@ -122,6 +130,59 @@ export type PlatformPublicWorkRemixIntent = errorMessage: string; }; +export type PlatformPublicWorkEditIntent = + | { + type: 'blocked'; + errorMessage: string; + } + | { + type: 'edit-big-fish'; + work: BigFishWorkSummary; + } + | { + type: 'edit-puzzle'; + work: PuzzleWorkSummary; + } + | { + type: 'edit-match3d'; + work: Match3DWorkSummary; + forceDraft: true; + } + | { + type: 'edit-square-hole'; + work: SquareHoleWorkSummary; + forceDraft: true; + } + | { + type: 'edit-visual-novel'; + work: VisualNovelWorkSummary; + forceDraft: true; + } + | { + type: 'resolve-edutainment-draft'; + entry: PlatformPublicGalleryCard; + } + | { + type: 'edit-bark-battle'; + work: BarkBattleWorkSummary; + forceDraft: true; + } + | { + type: 'edit-rpg-gallery'; + entry: CustomWorldLibraryEntry; + }; + +export type PlatformPublicWorkEditIntentDeps = { + selectedPuzzleDetail?: PuzzleWorkSummary | null; + selectedRpgDetailEntry?: PlatformRpgPublicWorkDetailEntry | null; + visualNovelWorks?: readonly VisualNovelWorkSummary[]; + barkBattleGalleryEntries?: readonly BarkBattleWorkSummary[]; + barkBattleWorks?: readonly BarkBattleWorkSummary[]; + mapMatch3DWork: ( + entry: PlatformPublicGalleryCard, + ) => Match3DWorkSummary | null; +}; + export type PlatformPublicWorkStartIntent = | { type: 'blocked'; @@ -175,12 +236,12 @@ export type PlatformPublicWorkStartIntent = } | { type: 'record-rpg-gallery-play'; - entry: CustomWorldGalleryCard; + entry: PlatformRpgPublicWorkDetailEntry; }; export type PlatformPublicWorkStartIntentDeps = { selectedPuzzleDetail?: PuzzleWorkSummary | null; - selectedRpgDetailEntry?: CustomWorldGalleryCard | null; + selectedRpgDetailEntry?: PlatformRpgPublicWorkDetailEntry | null; barkBattleGalleryEntries?: readonly BarkBattleWorkSummary[]; barkBattleWorks?: readonly BarkBattleWorkSummary[]; mapMatch3DWork: ( @@ -213,21 +274,27 @@ export type PlatformPublicWorkDetailOpenDecisionDeps = { export type ActivePlatformPublicWorkAuthorEntryInput = { selectionStage: string; selectedPublicWorkDetail: PlatformPublicGalleryCard | null; - selectedRpgDetailEntry: CustomWorldGalleryCard | null; + selectedRpgDetailEntry: PlatformRpgPublicWorkDetailEntry | null; }; export function isRpgPublicWorkDetailEntry( entry: PlatformPublicGalleryCard, -): entry is CustomWorldGalleryCard { +): entry is PlatformRpgPublicWorkDetailEntry { return !('sourceType' in entry); } export function mapRpgGalleryCardToPublicWorkDetail( - entry: CustomWorldGalleryCard, + entry: PlatformRpgPublicWorkDetailEntry, ): PlatformPublicGalleryCard { return entry; } +function isRpgPublicWorkLibraryEntry( + entry: PlatformRpgPublicWorkDetailEntry | null | undefined, +): entry is CustomWorldLibraryEntry { + return Boolean(entry && 'profile' in entry); +} + export function mapPuzzleWorkToPublicWorkDetail( item: PuzzleWorkSummary, ): PlatformPublicGalleryCard { @@ -689,6 +756,154 @@ export function resolvePlatformPublicWorkRemixIntent( }; } +export function resolvePlatformPublicWorkEditIntent( + entry: PlatformPublicGalleryCard, + deps: PlatformPublicWorkEditIntentDeps, +): PlatformPublicWorkEditIntent { + if (isBigFishGalleryEntry(entry)) { + const work = mapPublicWorkDetailToBigFishWork(entry); + if (!work?.sourceSessionId?.trim()) { + return { + type: 'blocked', + errorMessage: '这份大鱼吃小鱼作品缺少原草稿会话,暂时无法编辑。', + }; + } + + return { + type: 'edit-big-fish', + work, + }; + } + + if (isPuzzleGalleryEntry(entry)) { + const work = + deps.selectedPuzzleDetail?.profileId === entry.profileId + ? deps.selectedPuzzleDetail + : mapPublicWorkDetailToPuzzleWork(entry); + if (!work?.sourceSessionId?.trim()) { + return { + type: 'blocked', + errorMessage: '这份拼图作品缺少原草稿会话,暂时无法编辑。', + }; + } + + return { + type: 'edit-puzzle', + work, + }; + } + + if (isMatch3DGalleryEntry(entry)) { + // 中文注释:抓大鹅草稿恢复仍复用 Match3D Module 的 public detail -> work Adapter。 + const work = deps.mapMatch3DWork(entry); + if (!work?.sourceSessionId?.trim()) { + return { + type: 'blocked', + errorMessage: '这份抓大鹅作品缺少原草稿会话,暂时无法编辑。', + }; + } + + return { + type: 'edit-match3d', + work, + forceDraft: true, + }; + } + + if (isSquareHoleGalleryEntry(entry)) { + const work = mapPublicWorkDetailToSquareHoleWork(entry); + if (!work?.sourceSessionId?.trim()) { + return { + type: 'blocked', + errorMessage: '这份方洞挑战作品缺少原草稿会话,暂时无法编辑。', + }; + } + + return { + type: 'edit-square-hole', + work, + forceDraft: true, + }; + } + + if (isJumpHopGalleryEntry(entry)) { + return { + type: 'blocked', + errorMessage: '这份跳一跳作品暂时请从作品架编辑。', + }; + } + + if (isWoodenFishGalleryEntry(entry)) { + return { + type: 'blocked', + errorMessage: '这份敲木鱼作品暂时请从作品架编辑。', + }; + } + + if (isVisualNovelGalleryEntry(entry)) { + const work = + deps.visualNovelWorks?.find((item) => item.profileId === entry.profileId) ?? + null; + if (!work) { + return { + type: 'blocked', + errorMessage: '这份视觉小说缺少可编辑草稿。', + }; + } + + return { + type: 'edit-visual-novel', + work, + forceDraft: true, + }; + } + + if (isEdutainmentGalleryEntry(entry)) { + return { + type: 'resolve-edutainment-draft', + entry, + }; + } + + if (isBarkBattleGalleryEntry(entry)) { + const work = + deps.barkBattleWorks?.find((item) => item.workId === entry.workId) ?? + deps.barkBattleGalleryEntries?.find( + (item) => item.workId === entry.workId, + ) ?? + mapBarkBattlePublicDetailToWorkSummary(entry); + if (!work?.draftId?.trim()) { + return { + type: 'blocked', + errorMessage: '这份汪汪声浪缺少可编辑草稿。', + }; + } + + return { + type: 'edit-bark-battle', + work, + forceDraft: true, + }; + } + + const editEntry = + deps.selectedRpgDetailEntry?.profileId === entry.profileId && + isRpgPublicWorkLibraryEntry(deps.selectedRpgDetailEntry) + ? deps.selectedRpgDetailEntry + : null; + if (!editEntry) { + return { + type: 'blocked', + errorMessage: '作品详情尚未读取完成。', + }; + } + + return { + type: 'edit-rpg-gallery', + entry: editEntry, + }; +} + export function resolvePlatformPublicWorkStartIntent( entry: PlatformPublicGalleryCard, deps: PlatformPublicWorkStartIntentDeps,