refactor: 收口小游戏草稿 payload

This commit is contained in:
2026-06-04 03:25:28 +08:00
parent 5114a230ae
commit cd959b4095
7 changed files with 671 additions and 200 deletions

View File

@@ -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` - 验证方式:`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` - 关联文档:`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 收口 ## 2026-06-03 Public Work Presentation 收口
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。 - 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。

View File

@@ -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)。 平台小游戏生成状态的恢复、失败 / 完成收尾、展示 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)。 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,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`

View File

@@ -16,6 +16,8 @@
平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定统一由 `platformMiniGameDraftGenerationStateModel.ts` 处理。平台壳只决定何时调用并写入对应 React state不得在壳层重新维护 `MiniGameDraftGenerationState` 的 phase 阈值、`finishedAtMs` 清理或拼图进度 metadata 合并规则。 平台小游戏生成状态的恢复、失败 / 完成收尾、展示 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 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*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。 统一创作入口覆盖当前可进入创作链路的已有模板:`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*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。

View File

@@ -522,6 +522,16 @@ import {
rebaseMiniGameDraftGenerationStateForDisplay, rebaseMiniGameDraftGenerationStateForDisplay,
resolveFinishedMiniGameDraftGenerationState, resolveFinishedMiniGameDraftGenerationState,
} from './platformMiniGameDraftGenerationStateModel'; } from './platformMiniGameDraftGenerationStateModel';
import {
buildMatch3DFormPayloadFromSession,
buildMatch3DFormPayloadFromWork,
buildPendingMatch3DDraftMetadata,
buildPendingPuzzleDraftMetadata,
buildPuzzleCompileActionFromFormPayload,
buildPuzzleFormPayloadFromAction,
buildPuzzleFormPayloadFromSession,
buildPuzzleFormPayloadFromWork,
} from './platformMiniGameDraftPayloadModel';
import { import {
buildJumpHopPendingSession, buildJumpHopPendingSession,
buildPuzzleRuntimeWorkFromSession, buildPuzzleRuntimeWorkFromSession,
@@ -1035,89 +1045,6 @@ function openPuzzleRuntimeStage(
writePuzzleRuntimeUrlState(state); 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( function normalizeRecoveredPuzzleDraftSession(
session: PuzzleAgentSessionSnapshot, session: PuzzleAgentSessionSnapshot,
): PuzzleAgentSessionSnapshot { ): PuzzleAgentSessionSnapshot {
@@ -1243,123 +1170,6 @@ function reconcileProfileWalletLocalDeltaWithServerDashboard(
return Math.max(0, normalizedDelta - reflectedCredit); 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) { function isPuzzleFormOnlyDraft(session: PuzzleAgentSessionSnapshot | null) {
return Boolean( return Boolean(
session?.stage === 'collecting_anchors' && session.draft?.formDraft, session?.stage === 'collecting_anchors' && session.draft?.formDraft,

View File

@@ -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> = {},
): PuzzleDraftLevel {
return {
levelId: 'level-1',
levelName: '星桥机关',
pictureDescription: '关卡画面描述',
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'idle',
...overrides,
};
}
function buildPuzzleWork(
overrides: Partial<PuzzleWorkSummary> = {},
): 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> = {},
): 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> = {},
): 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> = {},
): 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> = {},
): 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: '海岛抓大鹅',
});
});
});

View File

@@ -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),
};
}