diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 46ec8486..2c7eea2f 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1430,9 +1430,9 @@ ## 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`、`isPuzzleFormOnlyDraft`、`isEmptyPuzzleFormOnlyDraft`、`buildMatch3DFormPayloadFromSession`、`buildMatch3DFormPayloadFromWork` 与 `buildPendingMatch3DDraftMetadata`;`parseOptionalFiniteNumber` 留在 Module 内部。 -- 影响范围:拼图 action 完成 / 执行前 / 失败恢复、拼图表单直生草稿、拼图 form-only 草稿恢复 / 分流 / 结果页渲染、拼图草稿架恢复、抓大鹅表单直生草稿与失败恢复。 +- 背景:`PlatformEntryFlowShellImpl.tsx` 内联维护拼图 / 抓大鹅表单 payload、拼图作品更新 payload、拼图编译 action、作品摘要回填 payload 和 pending 草稿 metadata,壳层需要理解描述字段优先级、formDraft 回退、结果页 draft 到作品更新字段的映射、Match3D config / draft / anchorPack 优先级和数字解析。 +- 决策:新增 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,收口 `buildPuzzleFormPayloadFromWork`、`buildPuzzleFormPayloadFromSession`、`buildPuzzleFormPayloadFromAction`、`buildPuzzleCompileActionFromFormPayload`、`buildPuzzleWorkUpdatePayloadFromDraft`、`buildPendingPuzzleDraftMetadata`、`isPuzzleFormOnlyDraft`、`isEmptyPuzzleFormOnlyDraft`、`buildMatch3DFormPayloadFromSession`、`buildMatch3DFormPayloadFromWork` 与 `buildPendingMatch3DDraftMetadata`;`parseOptionalFiniteNumber` 留在 Module 内部。 +- 影响范围:拼图 action 完成 / 执行前 / 失败恢复、拼图结果页试玩前作品更新、拼图表单直生草稿、拼图 form-only 草稿恢复 / 分流 / 结果页渲染、拼图草稿架恢复、抓大鹅表单直生草稿与失败恢复。 - 验证方式:`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`。 diff --git a/docs/README.md b/docs/README.md index 8b402e03..b6d29a6a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,7 +59,7 @@ 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 与拼图 form-only 草稿判定收口到 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,壳层只保留 API、Action 执行、background task 与状态副作用,规则见 [【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md)。 +平台小游戏草稿恢复和提交所需的拼图 / 抓大鹅表单 payload、拼图作品更新 payload、拼图编译 action、pending metadata 与拼图 form-only 草稿判定收口到 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,壳层只保留 API、Action 执行、background task 与状态副作用,规则见 [【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md)。 平台拼图生成完成后刷新恢复的草稿归一化与可恢复完成态判定收口到 `src/components/platform-entry/platformPuzzleDraftRecoveryModel.ts`,恢复链路只有在首图、关卡画面、UI spritesheet 与关卡背景资产包完整时才抬为 ready,规则见 [【前端架构】PlatformPuzzleDraftRecoveryModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPuzzleDraftRecoveryModel收口计划-2026-06-04.md)。 diff --git a/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md index f294bbd3..34a7d312 100644 --- a/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md +++ b/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md @@ -2,7 +2,7 @@ ## 背景 -`PlatformEntryFlowShellImpl.tsx` 曾内联维护拼图和抓大鹅草稿恢复所需的表单 payload、拼图编译 action payload、作品摘要回填 payload 和 pending 草稿 metadata。壳层因此需要理解拼图描述字段优先级、formDraft 回退、Match3D config / draft / anchorPack 优先级,以及 pending 作品架标题摘要如何从 payload 派生。后续还残留拼图 form-only 草稿判定,影响 action 分流、草稿恢复阶段和结果页渲染。 +`PlatformEntryFlowShellImpl.tsx` 曾内联维护拼图和抓大鹅草稿恢复所需的表单 payload、拼图编译 action payload、拼图作品更新 payload、作品摘要回填 payload 和 pending 草稿 metadata。壳层因此需要理解拼图描述字段优先级、formDraft 回退、结果页 draft 到作品更新字段的映射、Match3D config / draft / anchorPack 优先级,以及 pending 作品架标题摘要如何从 payload 派生。后续还残留拼图 form-only 草稿判定,影响 action 分流、草稿恢复阶段和结果页渲染。 这些逻辑都是 DTO 变换;不读取 React state,不请求网络,也不写 URL。壳层只应决定何时恢复、何时提交 action、何时写入生成状态。 @@ -14,6 +14,7 @@ - `buildPuzzleFormPayloadFromSession(session)`:从拼图 session 恢复创作表单 payload。 - `buildPuzzleFormPayloadFromAction(payload)`:从拼图 action 还原表单 payload,仅接受 `compile_puzzle_draft` 与 `save_puzzle_form_draft`。 - `buildPuzzleCompileActionFromFormPayload(payload)`:从表单 payload 构造拼图编译 action。 +- `buildPuzzleWorkUpdatePayloadFromDraft(draft)`:从拼图结果 draft 构造 `updatePuzzleWork(...)` 所需 payload。 - `buildPendingPuzzleDraftMetadata(payload)`:从拼图 payload 派生 pending 作品架 metadata。 - `isPuzzleFormOnlyDraft(session)` 与 `isEmptyPuzzleFormOnlyDraft(session)`:判断拼图 session 是否仍只是表单草稿,以及表单草稿是否没有任何可提交内容。 - `buildMatch3DFormPayloadFromSession(session)` 与 `buildMatch3DFormPayloadFromWork(item)`:从抓大鹅 session / work 恢复表单 payload。 @@ -27,6 +28,7 @@ - 拼图 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`。 +- 拼图作品更新 payload 必须直接映射 `workTitle`、`workDescription`、`levelName`、`summary`、`themeTags`、`coverImageSrc`、`coverAssetId`,`levels` 缺失时回退空数组。 - 拼图 form-only 草稿只在 `session.stage === 'collecting_anchors'` 且存在 `draft.formDraft` 时成立。 - 空 form-only 草稿必须同时缺少 `seedText`、`formDraft.workTitle`、`formDraft.workDescription` 与 `formDraft.pictureDescription`。 - 抓大鹅 session payload 优先读取 `config`,其次 `draft`,最后 `anchorPack`;`anchorPack.clearCount` 与 `anchorPack.difficulty` 只接受有限数字字符串或数字。 @@ -35,9 +37,9 @@ ## Depth / Leverage / Locality -- **Depth**:壳层以一组表意函数取得 payload / metadata;字段优先级、默认空资产和数字解析藏入 Module Implementation。 -- **Leverage**:后续调整拼图或抓大鹅草稿恢复表单时,先改 Module 与单测,再保持壳层 API / state 副作用不变。 -- **Locality**:表单恢复与 action payload 规则集中到一个纯测试面,避免在大型平台壳的生成、重试和恢复流程里重复散落 DTO 拼装。 +- **Depth**:壳层以一组表意函数取得 payload / metadata;字段优先级、结果页 draft 更新字段、默认空资产和数字解析藏入 Module Implementation。 +- **Leverage**:后续调整拼图或抓大鹅草稿恢复表单、拼图作品更新字段时,先改 Module 与单测,再保持壳层 API / state 副作用不变。 +- **Locality**:表单恢复、作品更新与 action payload 规则集中到一个纯测试面,避免在大型平台壳的生成、重试和恢复流程里重复散落 DTO 拼装。 ## 验收 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index d6b7b155..4c2ab0b0 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -531,6 +531,7 @@ import { buildPuzzleFormPayloadFromAction, buildPuzzleFormPayloadFromSession, buildPuzzleFormPayloadFromWork, + buildPuzzleWorkUpdatePayloadFromDraft, isEmptyPuzzleFormOnlyDraft, isPuzzleFormOnlyDraft, } from './platformMiniGameDraftPayloadModel'; @@ -3818,16 +3819,10 @@ export function PlatformEntryFlowShellImpl({ } try { - const { item } = await updatePuzzleWork(draftProfileId, { - workTitle: draft.workTitle, - workDescription: draft.workDescription, - levelName: draft.levelName, - summary: draft.summary, - themeTags: draft.themeTags, - coverImageSrc: draft.coverImageSrc, - coverAssetId: draft.coverAssetId, - levels: draft.levels ?? [], - }); + const { item } = await updatePuzzleWork( + draftProfileId, + buildPuzzleWorkUpdatePayloadFromDraft(draft), + ); const run = startLocalPuzzleRun(item); setSelectedPuzzleDetail(item); setPuzzleRun(run); @@ -4183,16 +4178,10 @@ export function PlatformEntryFlowShellImpl({ } try { - const { item } = await updatePuzzleWork(profileId, { - workTitle: draft.workTitle, - workDescription: draft.workDescription, - levelName: draft.levelName, - summary: draft.summary, - themeTags: draft.themeTags, - coverImageSrc: draft.coverImageSrc, - coverAssetId: draft.coverAssetId, - levels: draft.levels ?? [], - }); + const { item } = await updatePuzzleWork( + profileId, + buildPuzzleWorkUpdatePayloadFromDraft(draft), + ); const run = startLocalPuzzleRun(item); setSelectedPuzzleDetail(item); setPuzzleRun(run); @@ -5090,16 +5079,10 @@ export function PlatformEntryFlowShellImpl({ } try { - const { item } = await updatePuzzleWork(draftProfileId, { - workTitle: draft.workTitle, - workDescription: draft.workDescription, - levelName: draft.levelName, - summary: draft.summary, - themeTags: draft.themeTags, - coverImageSrc: draft.coverImageSrc, - coverAssetId: draft.coverAssetId, - levels: draft.levels ?? [], - }); + const { item } = await updatePuzzleWork( + draftProfileId, + buildPuzzleWorkUpdatePayloadFromDraft(draft), + ); const run = startLocalPuzzleRun(item); setSelectedPuzzleDetail(item); setPuzzleRun(run); @@ -8353,16 +8336,10 @@ export function PlatformEntryFlowShellImpl({ setIsPuzzleBusy(true); setPuzzleError(null); try { - const { item } = await updatePuzzleWork(profileId, { - workTitle: draft.workTitle, - workDescription: draft.workDescription, - levelName: draft.levelName, - summary: draft.summary, - themeTags: draft.themeTags, - coverImageSrc: draft.coverImageSrc, - coverAssetId: draft.coverAssetId, - levels: draft.levels ?? [], - }); + const { item } = await updatePuzzleWork( + profileId, + buildPuzzleWorkUpdatePayloadFromDraft(draft), + ); const run = startLocalPuzzleRun(item, options.levelId ?? null); setSelectedPuzzleDetail(item); setPuzzleRun(run); diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts index 9fdeb7a0..084fb40f 100644 --- a/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts @@ -24,6 +24,7 @@ import { buildPuzzleFormPayloadFromAction, buildPuzzleFormPayloadFromSession, buildPuzzleFormPayloadFromWork, + buildPuzzleWorkUpdatePayloadFromDraft, isEmptyPuzzleFormOnlyDraft, isPuzzleFormOnlyDraft, } from './platformMiniGameDraftPayloadModel'; @@ -219,6 +220,28 @@ describe('platformMiniGameDraftPayloadModel', () => { }); }); + test('builds puzzle work update payload from result draft', () => { + const draft = buildPuzzleSession().draft!; + + expect(buildPuzzleWorkUpdatePayloadFromDraft(draft)).toEqual({ + workTitle: '会话标题', + workDescription: '会话描述', + levelName: '星桥机关', + summary: '会话摘要', + themeTags: ['星桥'], + coverImageSrc: null, + coverAssetId: null, + levels: [buildPuzzleLevel()], + }); + + expect( + buildPuzzleWorkUpdatePayloadFromDraft({ + ...draft, + levels: undefined, + }).levels, + ).toEqual([]); + }); + test('builds puzzle form payload from session form draft and fallbacks', () => { expect(buildPuzzleFormPayloadFromSession(buildPuzzleSession())).toEqual({ seedText: '表单画面', diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts index f5181e61..bc9f3f60 100644 --- a/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts @@ -4,12 +4,27 @@ import type { } 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 { + PuzzleDraftLevel, + PuzzleResultDraft, +} 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'; +export type PuzzleWorkUpdatePayload = { + workTitle?: string; + workDescription?: string; + levelName: string; + summary: string; + themeTags: string[]; + coverImageSrc?: string | null; + coverAssetId?: string | null; + levels: PuzzleDraftLevel[]; +}; + export function buildPuzzleFormPayloadFromWork( item: PuzzleWorkSummary, ): CreatePuzzleAgentSessionRequest { @@ -35,6 +50,21 @@ export function buildPuzzleFormPayloadFromWork( }; } +export function buildPuzzleWorkUpdatePayloadFromDraft( + draft: PuzzleResultDraft, +): PuzzleWorkUpdatePayload { + return { + workTitle: draft.workTitle, + workDescription: draft.workDescription, + levelName: draft.levelName, + summary: draft.summary, + themeTags: draft.themeTags, + coverImageSrc: draft.coverImageSrc, + coverAssetId: draft.coverAssetId, + levels: draft.levels ?? [], + }; +} + function parseOptionalFiniteNumber(value: string | number | null | undefined) { if (typeof value === 'number') { return Number.isFinite(value) ? value : undefined;