diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index fa2c5a69..b437fecd 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1403,6 +1403,14 @@ - 验证方式:`npm run test -- src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 +## 2026-06-04 Platform Mini Game Draft Payload Model 收口 + +- 背景:`PlatformEntryFlowShellImpl.tsx` 内联维护拼图 / 抓大鹅表单 payload、拼图编译 action、作品摘要回填 payload 和 pending 草稿 metadata,壳层需要理解描述字段优先级、formDraft 回退、Match3D config / draft / anchorPack 优先级和数字解析。 +- 决策:新增 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,收口 `buildPuzzleFormPayloadFromWork`、`buildPuzzleFormPayloadFromSession`、`buildPuzzleFormPayloadFromAction`、`buildPuzzleCompileActionFromFormPayload`、`buildPendingPuzzleDraftMetadata`、`buildMatch3DFormPayloadFromSession`、`buildMatch3DFormPayloadFromWork` 与 `buildPendingMatch3DDraftMetadata`;`parseOptionalFiniteNumber` 留在 Module 内部。 +- 影响范围:拼图 action 完成 / 执行前 / 失败恢复、拼图表单直生草稿、拼图草稿架恢复、抓大鹅表单直生草稿与失败恢复。 +- 验证方式:`npm run test -- src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## 2026-06-03 Public Work Presentation 收口 - 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。 diff --git a/docs/README.md b/docs/README.md index 82fbcb7f..008afbd3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,6 +59,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定收口到 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts`,壳层只保留 API、后台任务、React state、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md)。 +平台小游戏草稿恢复和提交所需的拼图 / 抓大鹅表单 payload、拼图编译 action 与 pending metadata 收口到 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,壳层只保留 API、Action 执行、background task 与状态副作用,规则见 [【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-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)。 diff --git a/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md new file mode 100644 index 00000000..b8ac88c5 --- /dev/null +++ b/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md @@ -0,0 +1,44 @@ +# 【前端架构】Platform Mini Game Draft Payload Model 收口计划 + +## 背景 + +`PlatformEntryFlowShellImpl.tsx` 曾内联维护拼图和抓大鹅草稿恢复所需的表单 payload、拼图编译 action payload、作品摘要回填 payload 和 pending 草稿 metadata。壳层因此需要理解拼图描述字段优先级、formDraft 回退、Match3D config / draft / anchorPack 优先级,以及 pending 作品架标题摘要如何从 payload 派生。 + +这些逻辑都是 DTO 变换;不读取 React state,不请求网络,也不写 URL。壳层只应决定何时恢复、何时提交 action、何时写入生成状态。 + +## 决策 + +新增 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts` 作为 Platform Mini Game Draft Payload **Module**。其公开 **Interface** 为: + +- `buildPuzzleFormPayloadFromWork(item)`:从拼图作品摘要恢复创作表单 payload。 +- `buildPuzzleFormPayloadFromSession(session)`:从拼图 session 恢复创作表单 payload。 +- `buildPuzzleFormPayloadFromAction(payload)`:从拼图 action 还原表单 payload,仅接受 `compile_puzzle_draft` 与 `save_puzzle_form_draft`。 +- `buildPuzzleCompileActionFromFormPayload(payload)`:从表单 payload 构造拼图编译 action。 +- `buildPendingPuzzleDraftMetadata(payload)`:从拼图 payload 派生 pending 作品架 metadata。 +- `buildMatch3DFormPayloadFromSession(session)` 与 `buildMatch3DFormPayloadFromWork(item)`:从抓大鹅 session / work 恢复表单 payload。 +- `buildPendingMatch3DDraftMetadata(payload)`:从抓大鹅 payload 派生 pending metadata。 + +`PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:它继续负责 API、Action 执行、background task、生成状态、错误提示、作品架和阶段切换。 + +## Interface 约束 + +- 拼图 work payload 的 `pictureDescription` 优先级固定为 `workDescription > summary > first level pictureDescription > levelName > workTitle > ''`。 +- 拼图 session payload 的 `pictureDescription` 优先级固定为 `formDraft.pictureDescription > first level pictureDescription > anchorPack.visualSubject.value > seedText > ''`。 +- 拼图编译 action 的 `promptText` 来自 `pictureDescription || seedText`;`workDescription` 缺省回退到图片描述;`candidateCount` 固定为 `1`。 +- 拼图 action 还原只接受 `compile_puzzle_draft` 与 `save_puzzle_form_draft`;其它 action 返回 `null`。 +- 抓大鹅 session payload 优先读取 `config`,其次 `draft`,最后 `anchorPack`;`anchorPack.clearCount` 与 `anchorPack.difficulty` 只接受有限数字字符串或数字。 +- 抓大鹅 work payload 的 `themeText` 优先 `themeText`,缺失回退 `gameName`。 +- pending metadata 只收非空 trim 后标题和摘要;抓大鹅 metadata 用 `themeText || seedText` 同时作为 title 和 summary。 + +## Depth / Leverage / Locality + +- **Depth**:壳层以一组表意函数取得 payload / metadata;字段优先级、默认空资产和数字解析藏入 Module Implementation。 +- **Leverage**:后续调整拼图或抓大鹅草稿恢复表单时,先改 Module 与单测,再保持壳层 API / state 副作用不变。 +- **Locality**:表单恢复与 action payload 规则集中到一个纯测试面,避免在大型平台壳的生成、重试和恢复流程里重复散落 DTO 拼装。 + +## 验收 + +- `npm run test -- src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts` +- `npx eslint src/components/platform-entry/platformMiniGameDraftPayloadModel.ts src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet` +- `npm run typecheck` +- `npm run check:encoding` diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 964d9423..2f5f5cdf 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -16,6 +16,8 @@ 平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定统一由 `platformMiniGameDraftGenerationStateModel.ts` 处理。平台壳只决定何时调用并写入对应 React state,不得在壳层重新维护 `MiniGameDraftGenerationState` 的 phase 阈值、`finishedAtMs` 清理或拼图进度 metadata 合并规则。 +拼图 / 抓大鹅草稿恢复和提交所需的表单 payload、拼图编译 action 与 pending metadata 统一由 `platformMiniGameDraftPayloadModel.ts` 构造。平台壳不得重新手写拼图描述字段优先级、formDraft 回退、Match3D config / draft / anchorPack 优先级、数字解析或 pending 标题摘要派生规则。 + RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts` 判定:平台壳不得重新手写 `CustomWorldProfile` 顶层、`creatorIntent`、`anchorContent`、章节蓝图与首幕 acts 的结构探测,也不得在壳层内联 result preview source label 映射;壳层只负责 session/profile 编排和结果页 props 传递。 统一创作入口覆盖当前可进入创作链路的已有模板:`rpg`、`big-fish`、`puzzle`、`match3d`、`jump-hop`、`wooden-fish`、`square-hole`、`bark-battle`、`visual-novel`、`baby-object-match` 和 `creative-agent`;`airp` 仍是未开放占位,不作为当前统一创作链路目标。拼图、抓大鹅、跳一跳和敲木鱼在前端继续经过 `UnifiedCreationWorkspace` 和 `UnifiedGenerationPage`:`UnifiedCreationWorkspace` 作为平台壳依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace`、`Match3DCreationWorkspace`、`JumpHopCreationWorkspace` 和 `WoodenFishCreationWorkspace`。其它已有模板由平台壳用 `UnifiedCreationPage` 包住既有工作台,复用统一标题栏、返回入口、页面级纵向滚动和隐藏字段契约,同时保留各玩法自己的表单、草稿恢复和后续编排。创作页字段清单由后端在 `GET /api/creation-entry/config` 的 `creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec;字段类型只保留 `text`、`select`、`image`、`audio`。`UnifiedCreationPage` 不在 UI 中额外展示字段说明 chip,也不在右上角显示内部 `playId`、模板 ID 或工作台阶段名;竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交,但返回按钮只保留在统一页头,工作台内部不再重复渲染。暗色创作进度卡片位于 `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap,确保白字、浅色边框和进度条底色不会被全局规则改成深色;不要只依赖通用 `text-white*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index ace14e6e..19788c5d 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -522,6 +522,16 @@ import { rebaseMiniGameDraftGenerationStateForDisplay, resolveFinishedMiniGameDraftGenerationState, } from './platformMiniGameDraftGenerationStateModel'; +import { + buildMatch3DFormPayloadFromSession, + buildMatch3DFormPayloadFromWork, + buildPendingMatch3DDraftMetadata, + buildPendingPuzzleDraftMetadata, + buildPuzzleCompileActionFromFormPayload, + buildPuzzleFormPayloadFromAction, + buildPuzzleFormPayloadFromSession, + buildPuzzleFormPayloadFromWork, +} from './platformMiniGameDraftPayloadModel'; import { buildJumpHopPendingSession, buildPuzzleRuntimeWorkFromSession, @@ -1035,89 +1045,6 @@ function openPuzzleRuntimeStage( writePuzzleRuntimeUrlState(state); } -function buildPuzzleFormPayloadFromWork( - item: PuzzleWorkSummary, -): CreatePuzzleAgentSessionRequest { - const pictureDescription = - item.workDescription?.trim() || - item.summary?.trim() || - item.levels?.[0]?.pictureDescription?.trim() || - item.levelName?.trim() || - item.workTitle?.trim() || - ''; - - return { - seedText: pictureDescription, - workTitle: item.workTitle?.trim() || item.levelName?.trim() || undefined, - workDescription: item.workDescription?.trim() || item.summary?.trim(), - pictureDescription, - referenceImageSrc: null, - referenceImageSrcs: [], - referenceImageAssetObjectId: null, - referenceImageAssetObjectIds: [], - imageModel: null, - aiRedraw: true, - }; -} - -function parseOptionalFiniteNumber(value: string | number | null | undefined) { - if (typeof value === 'number') { - return Number.isFinite(value) ? value : undefined; - } - - const normalizedValue = value?.trim(); - if (!normalizedValue) { - return undefined; - } - - const parsedValue = Number(normalizedValue); - return Number.isFinite(parsedValue) ? parsedValue : undefined; -} - -function buildMatch3DFormPayloadFromSession( - session: Match3DAgentSessionSnapshot, -): CreateMatch3DSessionRequest { - const themeText = - session.config?.themeText?.trim() || - session.draft?.themeText?.trim() || - session.anchorPack.theme.value.trim() || - ''; - - return { - seedText: themeText, - themeText, - referenceImageSrc: - session.config?.referenceImageSrc ?? session.draft?.referenceImageSrc ?? null, - clearCount: - session.config?.clearCount ?? - session.draft?.clearCount ?? - parseOptionalFiniteNumber(session.anchorPack.clearCount.value) ?? - undefined, - difficulty: - session.config?.difficulty ?? - session.draft?.difficulty ?? - parseOptionalFiniteNumber(session.anchorPack.difficulty.value) ?? - undefined, - assetStyleId: session.config?.assetStyleId ?? null, - assetStyleLabel: session.config?.assetStyleLabel ?? null, - assetStylePrompt: session.config?.assetStylePrompt ?? null, - generateClickSound: session.config?.generateClickSound, - }; -} - -function buildMatch3DFormPayloadFromWork( - item: Match3DWorkSummary, -): CreateMatch3DSessionRequest { - const themeText = item.themeText?.trim() || item.gameName?.trim() || ''; - return { - seedText: themeText, - themeText, - referenceImageSrc: item.referenceImageSrc ?? null, - clearCount: item.clearCount, - difficulty: item.difficulty, - }; -} - function normalizeRecoveredPuzzleDraftSession( session: PuzzleAgentSessionSnapshot, ): PuzzleAgentSessionSnapshot { @@ -1243,123 +1170,6 @@ function reconcileProfileWalletLocalDeltaWithServerDashboard( return Math.max(0, normalizedDelta - reflectedCredit); } -function buildPuzzleCompileActionFromFormPayload( - payload: CreatePuzzleAgentSessionRequest | null, -): PuzzleAgentActionRequest { - const pictureDescription = - payload?.pictureDescription?.trim() || payload?.seedText?.trim(); - const workTitle = payload?.workTitle?.trim(); - const workDescription = payload?.workDescription?.trim() || pictureDescription; - - return { - action: 'compile_puzzle_draft', - promptText: pictureDescription, - ...(workTitle ? { workTitle } : {}), - ...(workDescription ? { workDescription } : {}), - ...(pictureDescription ? { pictureDescription } : {}), - referenceImageSrc: payload?.referenceImageSrc || null, - referenceImageSrcs: payload?.referenceImageSrcs ?? [], - referenceImageAssetObjectId: payload?.referenceImageAssetObjectId ?? null, - referenceImageAssetObjectIds: payload?.referenceImageAssetObjectIds ?? [], - imageModel: payload?.imageModel ?? null, - aiRedraw: payload?.aiRedraw ?? true, - candidateCount: 1, - }; -} - -function buildPuzzleFormPayloadFromSession( - session: PuzzleAgentSessionSnapshot, -): CreatePuzzleAgentSessionRequest { - const formDraft = session.draft?.formDraft; - const pictureDescription = - formDraft?.pictureDescription?.trim() || - session.draft?.levels?.[0]?.pictureDescription?.trim() || - session.anchorPack.visualSubject.value.trim() || - session.seedText?.trim() || - ''; - const workTitle = - formDraft?.workTitle?.trim() || session.draft?.workTitle?.trim(); - const workDescription = - formDraft?.workDescription?.trim() || - session.draft?.workDescription?.trim() || - session.draft?.summary?.trim() || - pictureDescription; - - return { - seedText: pictureDescription, - ...(workTitle ? { workTitle } : {}), - ...(workDescription ? { workDescription } : {}), - pictureDescription, - referenceImageSrc: null, - referenceImageSrcs: [], - referenceImageAssetObjectId: null, - referenceImageAssetObjectIds: [], - imageModel: null, - aiRedraw: true, - }; -} - -function buildPendingPuzzleDraftMetadata( - payload: CreatePuzzleAgentSessionRequest | null | undefined, -) { - const title = payload?.workTitle?.trim(); - const summary = - payload?.workDescription?.trim() || - payload?.pictureDescription?.trim() || - payload?.seedText?.trim(); - return { - ...(title ? { title } : {}), - ...(summary ? { summary } : {}), - }; -} - -function buildPendingMatch3DDraftMetadata( - payload: CreateMatch3DSessionRequest | null | undefined, -) { - const themeText = payload?.themeText?.trim() || payload?.seedText?.trim(); - return { - ...(themeText ? { title: themeText, summary: themeText } : {}), - }; -} - -function buildPuzzleFormPayloadFromAction( - payload: PuzzleAgentActionRequest, -): CreatePuzzleAgentSessionRequest | null { - if ( - payload.action !== 'compile_puzzle_draft' && - payload.action !== 'save_puzzle_form_draft' - ) { - return null; - } - - const workTitle = payload.workTitle?.trim() ?? ''; - const workDescription = payload.workDescription?.trim() ?? ''; - const pictureDescription = - payload.pictureDescription?.trim() || payload.promptText?.trim() || ''; - - return { - seedText: pictureDescription, - ...(workTitle ? { workTitle } : {}), - ...(workDescription ? { workDescription } : {}), - pictureDescription, - referenceImageSrc: - payload.action === 'compile_puzzle_draft' - ? (payload.referenceImageSrc ?? null) - : (payload.referenceImageSrc ?? null), - referenceImageSrcs: payload.referenceImageSrcs ?? [], - referenceImageAssetObjectId: payload.referenceImageAssetObjectId ?? null, - referenceImageAssetObjectIds: payload.referenceImageAssetObjectIds ?? [], - imageModel: - payload.action === 'compile_puzzle_draft' - ? (payload.imageModel ?? null) - : (payload.imageModel ?? null), - aiRedraw: - payload.action === 'compile_puzzle_draft' - ? (payload.aiRedraw ?? true) - : (payload.aiRedraw ?? true), - }; -} - function isPuzzleFormOnlyDraft(session: PuzzleAgentSessionSnapshot | null) { return Boolean( session?.stage === 'collecting_anchors' && session.draft?.formDraft, diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts new file mode 100644 index 00000000..056404c6 --- /dev/null +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts @@ -0,0 +1,392 @@ +import { describe, expect, test } from 'vitest'; + +import type { + Match3DAgentSessionSnapshot, + Match3DAnchorPackResponse, +} from '../../../packages/shared/src/contracts/match3dAgent'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions'; +import type { + PuzzleAnchorPack, + PuzzleDraftLevel, +} from '../../../packages/shared/src/contracts/puzzleAgentDraft'; +import type { + CreatePuzzleAgentSessionRequest, + PuzzleAgentSessionSnapshot, +} from '../../../packages/shared/src/contracts/puzzleAgentSession'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import { + buildMatch3DFormPayloadFromSession, + buildMatch3DFormPayloadFromWork, + buildPendingMatch3DDraftMetadata, + buildPendingPuzzleDraftMetadata, + buildPuzzleCompileActionFromFormPayload, + buildPuzzleFormPayloadFromAction, + buildPuzzleFormPayloadFromSession, + buildPuzzleFormPayloadFromWork, +} from './platformMiniGameDraftPayloadModel'; + +function buildPuzzleAnchorPack(): PuzzleAnchorPack { + const item = { + key: 'theme', + label: '主题', + value: '星桥机关', + status: 'confirmed' as const, + }; + return { + themePromise: item, + visualSubject: item, + visualMood: item, + compositionHooks: item, + tagsAndForbidden: item, + }; +} + +function buildPuzzleLevel( + overrides: Partial = {}, +): PuzzleDraftLevel { + return { + levelId: 'level-1', + levelName: '星桥机关', + pictureDescription: '关卡画面描述', + candidates: [], + selectedCandidateId: null, + coverImageSrc: null, + coverAssetId: null, + generationStatus: 'idle', + ...overrides, + }; +} + +function buildPuzzleWork( + overrides: Partial = {}, +): PuzzleWorkSummary { + return { + workId: 'puzzle-work-1', + profileId: 'puzzle-profile-1', + ownerUserId: 'user-1', + authorDisplayName: '玩家', + workTitle: ' 星桥拼图 ', + workDescription: ' 修复星桥机关。 ', + levelName: '星桥机关', + summary: '把碎片拼回原位。', + themeTags: ['星桥'], + coverImageSrc: '/cover.png', + coverAssetId: null, + publicationStatus: 'draft', + updatedAt: '2026-06-01T10:00:00.000Z', + publishedAt: null, + publishReady: false, + levels: [buildPuzzleLevel()], + ...overrides, + }; +} + +function buildPuzzleSession( + overrides: Partial = {}, +): PuzzleAgentSessionSnapshot { + const anchorPack = buildPuzzleAnchorPack(); + return { + sessionId: 'puzzle-session-1', + seedText: '种子描述', + currentTurn: 1, + progressPercent: 20, + stage: 'collecting_anchors', + anchorPack, + draft: { + workTitle: '会话标题', + workDescription: '会话描述', + levelName: '星桥机关', + summary: '会话摘要', + themeTags: ['星桥'], + forbiddenDirectives: [], + creatorIntent: null, + anchorPack, + candidates: [], + selectedCandidateId: null, + coverImageSrc: null, + coverAssetId: null, + generationStatus: 'idle', + levels: [buildPuzzleLevel()], + formDraft: { + workTitle: '表单标题', + workDescription: '表单描述', + pictureDescription: '表单画面', + }, + }, + messages: [], + lastAssistantReply: null, + publishedProfileId: null, + suggestedActions: [], + resultPreview: null, + updatedAt: '2026-06-01T10:00:00.000Z', + ...overrides, + }; +} + +function buildMatch3DAnchorPack( + overrides: Partial = {}, +): Match3DAnchorPackResponse { + return { + theme: { + key: 'theme', + label: '主题', + value: '海岛玩具', + status: 'confirmed', + }, + clearCount: { + key: 'clearCount', + label: '消除次数', + value: '12', + status: 'confirmed', + }, + difficulty: { + key: 'difficulty', + label: '难度', + value: '3', + status: 'confirmed', + }, + ...overrides, + }; +} + +function buildMatch3DSession( + overrides: Partial = {}, +): Match3DAgentSessionSnapshot { + return { + sessionId: 'match3d-session-1', + currentTurn: 1, + progressPercent: 20, + stage: 'collecting', + anchorPack: buildMatch3DAnchorPack(), + config: null, + draft: null, + messages: [], + lastAssistantReply: null, + publishedProfileId: null, + updatedAt: '2026-06-01T11:00:00.000Z', + ...overrides, + }; +} + +function buildMatch3DWork( + overrides: Partial = {}, +): Match3DWorkSummary { + return { + workId: 'match3d-work-1', + profileId: 'match3d-profile-1', + ownerUserId: 'user-1', + gameName: '海岛抓大鹅', + themeText: ' 海岛玩具 ', + summary: '收集海岛玩具。', + tags: ['海岛'], + coverImageSrc: '/match3d-cover.png', + referenceImageSrc: '/match3d-reference.png', + clearCount: 12, + difficulty: 3, + publicationStatus: 'draft', + playCount: 0, + updatedAt: '2026-06-01T11:00:00.000Z', + publishedAt: null, + publishReady: false, + ...overrides, + }; +} + +describe('platformMiniGameDraftPayloadModel', () => { + test('builds puzzle form payload from work with fallback description priority', () => { + expect( + buildPuzzleFormPayloadFromWork( + buildPuzzleWork({ + workDescription: ' ', + summary: ' 摘要描述 ', + levelName: ' 关卡标题 ', + }), + ), + ).toEqual({ + seedText: '摘要描述', + workTitle: '星桥拼图', + workDescription: '摘要描述', + pictureDescription: '摘要描述', + referenceImageSrc: null, + referenceImageSrcs: [], + referenceImageAssetObjectId: null, + referenceImageAssetObjectIds: [], + imageModel: null, + aiRedraw: true, + }); + }); + + test('builds puzzle form payload from session form draft and fallbacks', () => { + expect(buildPuzzleFormPayloadFromSession(buildPuzzleSession())).toEqual({ + seedText: '表单画面', + workTitle: '表单标题', + workDescription: '表单描述', + pictureDescription: '表单画面', + referenceImageSrc: null, + referenceImageSrcs: [], + referenceImageAssetObjectId: null, + referenceImageAssetObjectIds: [], + imageModel: null, + aiRedraw: true, + }); + + expect( + buildPuzzleFormPayloadFromSession( + buildPuzzleSession({ + draft: { + ...buildPuzzleSession().draft!, + formDraft: null, + levels: [buildPuzzleLevel({ pictureDescription: '关卡优先' })], + }, + }), + ).pictureDescription, + ).toBe('关卡优先'); + }); + + test('builds puzzle compile action and restores form payload from action', () => { + const payload: CreatePuzzleAgentSessionRequest = { + seedText: '种子', + workTitle: ' 标题 ', + workDescription: '', + pictureDescription: ' 画面 ', + referenceImageSrc: '/ref.png', + referenceImageSrcs: ['/ref-a.png'], + referenceImageAssetObjectId: 'asset-ref', + referenceImageAssetObjectIds: ['asset-ref-a'], + imageModel: 'image-model', + aiRedraw: false, + }; + const action = buildPuzzleCompileActionFromFormPayload(payload); + + expect(action).toEqual({ + action: 'compile_puzzle_draft', + promptText: '画面', + workTitle: '标题', + workDescription: '画面', + pictureDescription: '画面', + referenceImageSrc: '/ref.png', + referenceImageSrcs: ['/ref-a.png'], + referenceImageAssetObjectId: 'asset-ref', + referenceImageAssetObjectIds: ['asset-ref-a'], + imageModel: 'image-model', + aiRedraw: false, + candidateCount: 1, + }); + expect(buildPuzzleFormPayloadFromAction(action)).toEqual({ + seedText: '画面', + workTitle: '标题', + workDescription: '画面', + pictureDescription: '画面', + referenceImageSrc: '/ref.png', + referenceImageSrcs: ['/ref-a.png'], + referenceImageAssetObjectId: 'asset-ref', + referenceImageAssetObjectIds: ['asset-ref-a'], + imageModel: 'image-model', + aiRedraw: false, + }); + expect( + buildPuzzleFormPayloadFromAction({ + action: 'publish_puzzle_work', + } as PuzzleAgentActionRequest), + ).toBeNull(); + }); + + test('builds pending puzzle metadata from non-empty payload fields', () => { + expect( + buildPendingPuzzleDraftMetadata({ + workTitle: ' 标题 ', + workDescription: ' ', + pictureDescription: ' 画面 ', + seedText: '种子', + }), + ).toEqual({ + title: '标题', + summary: '画面', + }); + expect(buildPendingPuzzleDraftMetadata(null)).toEqual({}); + }); + + test('builds match3d form payload from session config, draft and anchors', () => { + expect( + buildMatch3DFormPayloadFromSession( + buildMatch3DSession({ + config: { + themeText: ' 配置主题 ', + referenceImageSrc: '/config-ref.png', + clearCount: 9, + difficulty: 4, + assetStyleId: 'style-1', + assetStyleLabel: '手办', + assetStylePrompt: '软陶手办', + generateClickSound: true, + }, + draft: { + profileId: 'profile-1', + gameName: '草稿标题', + themeText: '草稿主题', + tags: [], + referenceImageSrc: '/draft-ref.png', + clearCount: 6, + difficulty: 2, + }, + }), + ), + ).toEqual({ + seedText: '配置主题', + themeText: '配置主题', + referenceImageSrc: '/config-ref.png', + clearCount: 9, + difficulty: 4, + assetStyleId: 'style-1', + assetStyleLabel: '手办', + assetStylePrompt: '软陶手办', + generateClickSound: true, + }); + + expect( + buildMatch3DFormPayloadFromSession( + buildMatch3DSession({ + anchorPack: buildMatch3DAnchorPack({ + clearCount: { + key: 'clearCount', + label: '消除次数', + value: 'not-number', + status: 'confirmed', + }, + }), + }), + ), + ).toMatchObject({ + seedText: '海岛玩具', + clearCount: undefined, + difficulty: 3, + }); + }); + + test('builds match3d form payload from work and pending metadata', () => { + expect( + buildMatch3DFormPayloadFromWork( + buildMatch3DWork({ + themeText: ' ', + }), + ), + ).toEqual({ + seedText: '海岛抓大鹅', + themeText: '海岛抓大鹅', + referenceImageSrc: '/match3d-reference.png', + clearCount: 12, + difficulty: 3, + }); + + expect( + buildPendingMatch3DDraftMetadata({ + themeText: ' ', + seedText: ' 海岛抓大鹅 ', + }), + ).toEqual({ + title: '海岛抓大鹅', + summary: '海岛抓大鹅', + }); + }); +}); diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts new file mode 100644 index 00000000..f889b3f0 --- /dev/null +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts @@ -0,0 +1,213 @@ +import type { + CreateMatch3DSessionRequest, + Match3DAgentSessionSnapshot, +} from '../../../packages/shared/src/contracts/match3dAgent'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions'; +import type { + CreatePuzzleAgentSessionRequest, + PuzzleAgentSessionSnapshot, +} from '../../../packages/shared/src/contracts/puzzleAgentSession'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; + +export function buildPuzzleFormPayloadFromWork( + item: PuzzleWorkSummary, +): CreatePuzzleAgentSessionRequest { + const pictureDescription = + item.workDescription?.trim() || + item.summary?.trim() || + item.levels?.[0]?.pictureDescription?.trim() || + item.levelName?.trim() || + item.workTitle?.trim() || + ''; + + return { + seedText: pictureDescription, + workTitle: item.workTitle?.trim() || item.levelName?.trim() || undefined, + workDescription: item.workDescription?.trim() || item.summary?.trim(), + pictureDescription, + referenceImageSrc: null, + referenceImageSrcs: [], + referenceImageAssetObjectId: null, + referenceImageAssetObjectIds: [], + imageModel: null, + aiRedraw: true, + }; +} + +function parseOptionalFiniteNumber(value: string | number | null | undefined) { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : undefined; + } + + const normalizedValue = value?.trim(); + if (!normalizedValue) { + return undefined; + } + + const parsedValue = Number(normalizedValue); + return Number.isFinite(parsedValue) ? parsedValue : undefined; +} + +export function buildMatch3DFormPayloadFromSession( + session: Match3DAgentSessionSnapshot, +): CreateMatch3DSessionRequest { + const themeText = + session.config?.themeText?.trim() || + session.draft?.themeText?.trim() || + session.anchorPack.theme.value.trim() || + ''; + + return { + seedText: themeText, + themeText, + referenceImageSrc: + session.config?.referenceImageSrc ?? + session.draft?.referenceImageSrc ?? + null, + clearCount: + session.config?.clearCount ?? + session.draft?.clearCount ?? + parseOptionalFiniteNumber(session.anchorPack.clearCount.value) ?? + undefined, + difficulty: + session.config?.difficulty ?? + session.draft?.difficulty ?? + parseOptionalFiniteNumber(session.anchorPack.difficulty.value) ?? + undefined, + assetStyleId: session.config?.assetStyleId ?? null, + assetStyleLabel: session.config?.assetStyleLabel ?? null, + assetStylePrompt: session.config?.assetStylePrompt ?? null, + generateClickSound: session.config?.generateClickSound, + }; +} + +export function buildMatch3DFormPayloadFromWork( + item: Match3DWorkSummary, +): CreateMatch3DSessionRequest { + const themeText = item.themeText?.trim() || item.gameName?.trim() || ''; + return { + seedText: themeText, + themeText, + referenceImageSrc: item.referenceImageSrc ?? null, + clearCount: item.clearCount, + difficulty: item.difficulty, + }; +} + +export function buildPuzzleCompileActionFromFormPayload( + payload: CreatePuzzleAgentSessionRequest | null, +): PuzzleAgentActionRequest { + const pictureDescription = + payload?.pictureDescription?.trim() || payload?.seedText?.trim(); + const workTitle = payload?.workTitle?.trim(); + const workDescription = payload?.workDescription?.trim() || pictureDescription; + + return { + action: 'compile_puzzle_draft', + promptText: pictureDescription, + ...(workTitle ? { workTitle } : {}), + ...(workDescription ? { workDescription } : {}), + ...(pictureDescription ? { pictureDescription } : {}), + referenceImageSrc: payload?.referenceImageSrc || null, + referenceImageSrcs: payload?.referenceImageSrcs ?? [], + referenceImageAssetObjectId: payload?.referenceImageAssetObjectId ?? null, + referenceImageAssetObjectIds: payload?.referenceImageAssetObjectIds ?? [], + imageModel: payload?.imageModel ?? null, + aiRedraw: payload?.aiRedraw ?? true, + candidateCount: 1, + }; +} + +export function buildPuzzleFormPayloadFromSession( + session: PuzzleAgentSessionSnapshot, +): CreatePuzzleAgentSessionRequest { + const formDraft = session.draft?.formDraft; + const pictureDescription = + formDraft?.pictureDescription?.trim() || + session.draft?.levels?.[0]?.pictureDescription?.trim() || + session.anchorPack.visualSubject.value.trim() || + session.seedText?.trim() || + ''; + const workTitle = + formDraft?.workTitle?.trim() || session.draft?.workTitle?.trim(); + const workDescription = + formDraft?.workDescription?.trim() || + session.draft?.workDescription?.trim() || + session.draft?.summary?.trim() || + pictureDescription; + + return { + seedText: pictureDescription, + ...(workTitle ? { workTitle } : {}), + ...(workDescription ? { workDescription } : {}), + pictureDescription, + referenceImageSrc: null, + referenceImageSrcs: [], + referenceImageAssetObjectId: null, + referenceImageAssetObjectIds: [], + imageModel: null, + aiRedraw: true, + }; +} + +export function buildPendingPuzzleDraftMetadata( + payload: CreatePuzzleAgentSessionRequest | null | undefined, +) { + const title = payload?.workTitle?.trim(); + const summary = + payload?.workDescription?.trim() || + payload?.pictureDescription?.trim() || + payload?.seedText?.trim(); + return { + ...(title ? { title } : {}), + ...(summary ? { summary } : {}), + }; +} + +export function buildPendingMatch3DDraftMetadata( + payload: CreateMatch3DSessionRequest | null | undefined, +) { + const themeText = payload?.themeText?.trim() || payload?.seedText?.trim(); + return { + ...(themeText ? { title: themeText, summary: themeText } : {}), + }; +} + +export function buildPuzzleFormPayloadFromAction( + payload: PuzzleAgentActionRequest, +): CreatePuzzleAgentSessionRequest | null { + if ( + payload.action !== 'compile_puzzle_draft' && + payload.action !== 'save_puzzle_form_draft' + ) { + return null; + } + + const workTitle = payload.workTitle?.trim() ?? ''; + const workDescription = payload.workDescription?.trim() ?? ''; + const pictureDescription = + payload.pictureDescription?.trim() || payload.promptText?.trim() || ''; + + return { + seedText: pictureDescription, + ...(workTitle ? { workTitle } : {}), + ...(workDescription ? { workDescription } : {}), + pictureDescription, + referenceImageSrc: + payload.action === 'compile_puzzle_draft' + ? (payload.referenceImageSrc ?? null) + : (payload.referenceImageSrc ?? null), + referenceImageSrcs: payload.referenceImageSrcs ?? [], + referenceImageAssetObjectId: payload.referenceImageAssetObjectId ?? null, + referenceImageAssetObjectIds: payload.referenceImageAssetObjectIds ?? [], + imageModel: + payload.action === 'compile_puzzle_draft' + ? (payload.imageModel ?? null) + : (payload.imageModel ?? null), + aiRedraw: + payload.action === 'compile_puzzle_draft' + ? (payload.aiRedraw ?? true) + : (payload.aiRedraw ?? true), + }; +}