From f9f22e56630108e49c293777dc587a20995cbf4c Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 04:30:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=20Bark=20Battle?= =?UTF-8?q?=20work=20cache=20=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 8 ++ docs/README.md | 2 + ...arkBattleWorkCache草稿状态收口计划-2026-06-04.md | 44 ++++++ ...玩法创作】平台入口与玩法链路-2026-05-15.md | 4 +- .../PlatformEntryFlowShellImpl.tsx | 115 +++------------- .../barkBattleWorkCache.test.ts | 126 +++++++++++++++++- .../platform-entry/barkBattleWorkCache.ts | 112 +++++++++++++++- 7 files changed, 305 insertions(+), 106 deletions(-) create mode 100644 docs/technical/【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 2a6e4dd8..00b7f77e 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-04 Bark Battle Work Cache 草稿状态收口 + +- 背景:`PlatformEntryFlowShellImpl.tsx` 仍内联维护 Bark Battle 草稿三图完整性、生成状态归一,以及草稿 / 已发布作品进入 runtime 前的 `BarkBattlePublishedConfig` 字段映射,导致结果页试玩、作品架启动和公开详情启动都要理解同一份资产字段清单。 +- 决策:扩展 `src/components/platform-entry/barkBattleWorkCache.ts`,以 `hasBarkBattleDraftRequiredImages`、`resolveBarkBattleDraftGenerationStatus`、`buildBarkBattlePublishedConfigFromDraft`、`buildBarkBattlePublishedConfigFromWork`、`buildBarkBattlePublishSnapshot` 和 `mergeBarkBattlePublishedConfigAssets` 收口 Bark Battle 纯规则。平台壳只保留 API、缓存刷新、React state、URL 和 stage 副作用。 +- 影响范围:Bark Battle 草稿生成完成、结果页保存、草稿试玩、作品架 / 公开详情启动正式 runtime,以及后续 Bark Battle 资产字段或 ruleset 默认值调整。 +- 验证方式:`npm run test -- src/components/platform-entry/barkBattleWorkCache.test.ts`、针对 Bark Battle Work Cache Module 与平台壳执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md`。 + ## 2026-06-04 Platform Creation Launch Model 收口 - 背景:平台创作入口点击回调曾在 `PlatformEntryFlowShellImpl.tsx` 内联判断 `airp` 占位、隐藏的 `baby-object-match`、未知入口和各玩法工作台启动目标,壳层同时承接入口 ID 规则、启动前准备顺序和副作用。 diff --git a/docs/README.md b/docs/README.md index 728645d3..cadcc080 100644 --- a/docs/README.md +++ b/docs/README.md @@ -67,6 +67,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 平台入口个人钱包本地 delta、dashboard 乐观更新与服务端快照对账规则收口到 `src/components/platform-entry/platformProfileWalletDeltaModel.ts`,平台壳只保留 API、ref 与 state 副作用,规则见 [【前端架构】PlatformProfileWalletDeltaModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformProfileWalletDeltaModel收口计划-2026-06-04.md)。 +Bark Battle 草稿三图完整性、生成状态归一、发布快照 / 发布回包资产兜底和草稿 / 已发布作品进入 runtime 前的 `BarkBattlePublishedConfig` 映射收口到 `src/components/platform-entry/barkBattleWorkCache.ts`,规则见 [【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md](./technical/【前端架构】BarkBattleWorkCache草稿状态收口计划-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/【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md b/docs/technical/【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md new file mode 100644 index 00000000..14936e1e --- /dev/null +++ b/docs/technical/【前端架构】BarkBattleWorkCache草稿状态收口计划-2026-06-04.md @@ -0,0 +1,44 @@ +# 【前端架构】Bark Battle Work Cache 草稿状态收口计划 + +## 背景 + +`PlatformEntryFlowShellImpl.tsx` 仍内联维护 Bark Battle 草稿三图完整性、生成状态归一,以及草稿 / 已发布作品进入 runtime 前的 `BarkBattlePublishedConfig` 映射。壳层因此需要同时理解三图资产字段、`partial_failed` 与 `pending_assets` 的差异、`publishedAt` 兜底和草稿试玩配置默认值。 + +这些规则属于 Bark Battle 作品摘要与草稿缓存的纯模型。若留在平台壳层,后续发布、作品架刷新、公开详情启动或草稿试玩都容易重复一份字段清单。 + +## 决策 + +扩展 `src/components/platform-entry/barkBattleWorkCache.ts`,作为 Bark Battle Work Cache **Module** 继续承接作品摘要缓存和草稿 runtime 配置规则。新增公开 **Interface**: + +- `hasBarkBattleDraftRequiredImages(draft)`:判断草稿是否已具备玩家形象、对手形象和竞技背景三图。 +- `resolveBarkBattleDraftGenerationStatus(draft, partialFailed)`:三图齐备返回 `ready`,否则按是否部分失败返回 `partial_failed` 或 `pending_assets`。 +- `buildBarkBattlePublishedConfigFromDraft(draft)`:把草稿结果页试玩所需配置映射为 `BarkBattlePublishedConfig`。 +- `buildBarkBattlePublishedConfigFromWork(work)`:把作品架 / 公开详情启动正式 runtime 所需配置映射为 `BarkBattlePublishedConfig`。 +- `buildBarkBattlePublishSnapshot(draft)`:拼装发布接口所需的最终草稿快照。 +- `mergeBarkBattlePublishedConfigAssets(published, draft)`:发布回包缺少三图字段时沿用结果页草稿图。 + +`PlatformEntryFlowShellImpl.tsx` 继续作为 **Adapter**:它只负责 API 请求、React state、URL、运行态 stage 切换和错误提示,不再持有 Bark Battle 三图完整性与 runtime config 字段清单。 + +## Interface 约束 + +- 草稿三图必须同时具备 `playerCharacterImageSrc`、`opponentCharacterImageSrc` 和 `uiBackgroundImageSrc` 的非空值,才视为 `ready`。 +- 未齐三图且 `partialFailed=true` 时返回 `partial_failed`,否则返回 `pending_assets`。 +- 草稿试玩配置的 `workId` 优先使用草稿稳定 `workId`,缺失时回退 `draftId`。 +- 草稿试玩配置的 `configVersion` 与 `rulesetVersion` 使用草稿值,缺失时回退 `1` 与 `bark-battle-ruleset-v1`。 +- 已发布作品配置的 `publishedAt` 缺失时回退 `updatedAt`,保持旧 runtime 启动语义。 +- 发布快照只携带草稿已有的三图字段,不凭空补空字符串。 +- 发布接口回包缺少三图字段时,结果页草稿图继续作为 runtime 和作品摘要的兜底。 + +## Depth / Leverage / Locality + +- **Depth**:壳层传入草稿或作品摘要,即可得到生成状态或 runtime 配置;字段归一、默认值和三图完整性藏入 Module Implementation。 +- **Leverage**:结果页试玩、作品架启动、公开详情启动和缓存刷新可复用同一组 Bark Battle 规则。 +- **Locality**:Bark Battle 资产完整性与配置映射集中到纯测试面,后续变更三图字段或规则集默认值时无需搜索巨型平台壳。 + +## 验收 + +- `npm run test -- src/components/platform-entry/barkBattleWorkCache.test.ts` +- `npx eslint --max-warnings 0 src/components/platform-entry/barkBattleWorkCache.ts src/components/platform-entry/barkBattleWorkCache.test.ts` +- `npx eslint 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 a46a9786..1bcbf2b8 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -306,8 +306,8 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列 - 结果页:围绕三图槽位展示错误态与已生成结果,只保留单槽重试、重新生成和上传,不再提供一次生成按钮、音频配置入口或排名配置;生成回写 `partial_failed` 时作品架不再显示整卡“生成中”遮罩,由结果页槽位错误承接失败。 - 手动上传:结果页通过平台资产直传 `/api/assets/direct-upload-tickets` 与 `/api/assets/objects/confirm` 写入私有资产,再把返回的历史 generated 路径写回草稿配置。 - 发布:结果页确认后必须携带草稿返回的同一个 `workId` 和结果页最终 `publishedSnapshot` 调用 `POST /api/creation/bark-battle/works/publish`;SpacetimeDB 发布态的 `config_json` 必须使用该最终快照,works summary 若拿到 `publishedSnapshotJson` 也优先使用最终快照映射封面三图。发布成功后先进入统一作品详情页,再由详情页进入正式 runtime;缺少 `workId` 的旧草稿状态需要重新生成草稿。 -- 作品架:Bark Battle 草稿 / 已发布列表优先读取后端 `/works`,但创建、生成完成、保存或发布后的本地摘要必须在后端 read model 尚未回读到同 `workId` 前继续保留;创作中心作品架同时接入 pending shelf 兜底,避免 ready 且三图齐全的草稿在刷新窗口期从“我的草稿 / 已发布”中消失。 -- 试玩与正式 runtime:草稿试玩使用 `runtimeMode=draft` 和 mock 输入,不写正式 run;正式 runtime 使用 `runtimeMode=published`,进入运行态后直接申请真实麦克风权限,授权成功后立刻进入倒计时,启动对局时调用 `POST /api/runtime/bark-battle/works/{workId}/runs` 登记 start run,并以返回的 `runtimeConfig` 作为本局前端规则参数;结算时调用 `POST /api/runtime/bark-battle/runs/{runId}/finish` 写入基础统计派生指标;对局会在能量条推到任一侧边界时提前结算并弹出独立结算弹窗,运行态内固定提供返回按钮。 +- 作品架:Bark Battle 草稿 / 已发布列表优先读取后端 `/works`,但创建、生成完成、保存或发布后的本地摘要必须在后端 read model 尚未回读到同 `workId` 前继续保留;创作中心作品架同时接入 pending shelf 兜底,避免 ready 且三图齐全的草稿在刷新窗口期从“我的草稿 / 已发布”中消失。草稿三图完整性、`pending_assets` / `partial_failed` / `ready` 生成状态归一和作品摘要合并规则统一由 `barkBattleWorkCache.ts` 承接,平台壳只执行读取、刷新与 React state 副作用。 +- 试玩与正式 runtime:草稿试玩使用 `runtimeMode=draft` 和 mock 输入,不写正式 run;正式 runtime 使用 `runtimeMode=published`,进入运行态后直接申请真实麦克风权限,授权成功后立刻进入倒计时,启动对局时调用 `POST /api/runtime/bark-battle/works/{workId}/runs` 登记 start run,并以返回的 `runtimeConfig` 作为本局前端规则参数;结算时调用 `POST /api/runtime/bark-battle/runs/{runId}/finish` 写入基础统计派生指标;对局会在能量条推到任一侧边界时提前结算并弹出独立结算弹窗,运行态内固定提供返回按钮。发布快照拼装、发布回包缺图时沿用草稿图,以及草稿 / 已发布作品进入前端 runtime 前的 `BarkBattlePublishedConfig` 映射也统一由 `barkBattleWorkCache.ts` 提供,缺失 `publishedAt` 时仍按 `updatedAt` 兜底。 支持的创作者可替换内容: diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 89f42158..b9d7807c 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -381,10 +381,14 @@ import { } from '../visual-novel-creation/visualNovelEntryGeneration'; import { createMockVisualNovelRunFromDraft } from '../visual-novel-runtime/visualNovelMockData'; import { - type BarkBattleGenerationStatus, + buildBarkBattlePublishedConfigFromDraft, + buildBarkBattlePublishedConfigFromWork, + buildBarkBattlePublishSnapshot, buildBarkBattleWorkSummaryFromDraft, + mergeBarkBattlePublishedConfigAssets, mergeBarkBattleWorksByWorkId, mergeBarkBattleWorkSummary, + resolveBarkBattleDraftGenerationStatus, shouldPreserveLocalBarkBattleWorkOnRefresh, } from './barkBattleWorkCache'; import { @@ -712,30 +716,6 @@ const PUZZLE_DRAFT_GENERATION_POINT_COST = 2; const MATCH3D_DRAFT_GENERATION_POINT_COST = 10; const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3; -function mapBarkBattleWorkToPublishedConfig( - work: BarkBattleWorkSummary, -): BarkBattlePublishedConfig { - return { - workId: work.workId, - draftId: work.draftId ?? null, - configVersion: 1, - rulesetVersion: 'bark-battle-ruleset-v1', - playTypeId: 'bark-battle', - title: work.title, - description: work.summary, - themeDescription: work.themeDescription, - playerImageDescription: work.playerImageDescription, - opponentImageDescription: work.opponentImageDescription, - onomatopoeia: work.onomatopoeia, - playerCharacterImageSrc: work.playerCharacterImageSrc ?? undefined, - opponentCharacterImageSrc: work.opponentCharacterImageSrc ?? undefined, - uiBackgroundImageSrc: work.uiBackgroundImageSrc ?? undefined, - difficultyPreset: work.difficultyPreset, - updatedAt: work.updatedAt, - publishedAt: work.publishedAt ?? work.updatedAt, - }; -} - function mapVisualNovelWorkDetailToSession( work: VisualNovelWorkDetail, ): VisualNovelAgentSessionSnapshot { @@ -1004,24 +984,6 @@ function mergeBigFishWorkSummary( : current; } -function hasBarkBattleDraftRequiredImages(draft: BarkBattleDraftConfig) { - return Boolean( - draft.playerCharacterImageSrc?.trim() && - draft.opponentCharacterImageSrc?.trim() && - draft.uiBackgroundImageSrc?.trim(), - ); -} - -function resolveBarkBattleDraftGenerationStatus( - draft: BarkBattleDraftConfig, - partialFailed: boolean, -): BarkBattleGenerationStatus { - if (hasBarkBattleDraftRequiredImages(draft)) { - return 'ready'; - } - return partialFailed ? 'partial_failed' : 'pending_assets'; -} - async function resolvePublicWorkAuthorSummary( entry: PlatformPublicGalleryCard, ): Promise { @@ -6190,39 +6152,18 @@ export function PlatformEntryFlowShellImpl({ ], ); - const buildBarkBattleDraftRuntimeConfig = useCallback( - (draft: BarkBattleDraftConfig): BarkBattlePublishedConfig => ({ - workId: draft.workId ?? draft.draftId, - draftId: draft.draftId, - configVersion: draft.configVersion ?? 1, - rulesetVersion: draft.rulesetVersion ?? 'bark-battle-ruleset-v1', - playTypeId: 'bark-battle', - title: draft.title, - description: draft.description, - themeDescription: draft.themeDescription, - playerImageDescription: draft.playerImageDescription, - opponentImageDescription: draft.opponentImageDescription, - onomatopoeia: draft.onomatopoeia, - playerCharacterImageSrc: draft.playerCharacterImageSrc, - opponentCharacterImageSrc: draft.opponentCharacterImageSrc, - uiBackgroundImageSrc: draft.uiBackgroundImageSrc, - difficultyPreset: draft.difficultyPreset, - updatedAt: draft.updatedAt, - publishedAt: draft.updatedAt, - }), - [], - ); - const testBarkBattleDraft = useCallback( (draft: BarkBattleDraftConfig) => { setBarkBattleError(null); setBarkBattleRuntimeMode('draft'); setBarkBattleRuntimeReturnStage('bark-battle-result'); - setBarkBattlePublishedConfig(buildBarkBattleDraftRuntimeConfig(draft)); + setBarkBattlePublishedConfig( + buildBarkBattlePublishedConfigFromDraft(draft), + ); selectionStageRef.current = 'bark-battle-runtime'; setSelectionStage('bark-battle-runtime'); }, - [buildBarkBattleDraftRuntimeConfig, setSelectionStage], + [setSelectionStage], ); const publishBarkBattleDraft = useCallback( @@ -6237,39 +6178,15 @@ export function PlatformEntryFlowShellImpl({ } setIsBarkBattleBusy(true); try { - const publishedSnapshot: BarkBattleConfigEditorPayload = { - title: draft.title, - description: draft.description, - themeDescription: draft.themeDescription, - playerImageDescription: draft.playerImageDescription, - opponentImageDescription: draft.opponentImageDescription, - onomatopoeia: draft.onomatopoeia, - ...(draft.playerCharacterImageSrc - ? { playerCharacterImageSrc: draft.playerCharacterImageSrc } - : {}), - ...(draft.opponentCharacterImageSrc - ? { opponentCharacterImageSrc: draft.opponentCharacterImageSrc } - : {}), - ...(draft.uiBackgroundImageSrc - ? { uiBackgroundImageSrc: draft.uiBackgroundImageSrc } - : {}), - difficultyPreset: draft.difficultyPreset, - }; const published = await publishBarkBattleWork({ draftId: draft.draftId, workId, - publishedSnapshot, + publishedSnapshot: buildBarkBattlePublishSnapshot(draft), }); - const publishedWithAssets: BarkBattlePublishedConfig = { - ...published, - playerCharacterImageSrc: - published.playerCharacterImageSrc ?? draft.playerCharacterImageSrc, - opponentCharacterImageSrc: - published.opponentCharacterImageSrc ?? - draft.opponentCharacterImageSrc, - uiBackgroundImageSrc: - published.uiBackgroundImageSrc ?? draft.uiBackgroundImageSrc, - }; + const publishedWithAssets = mergeBarkBattlePublishedConfigAssets( + published, + draft, + ); const publicWorkCode = buildBarkBattlePublicWorkCode( publishedWithAssets.workId, ); @@ -11597,7 +11514,9 @@ export function PlatformEntryFlowShellImpl({ setBarkBattleError(null); setBarkBattleGenerationPartialFailed(false); setBarkBattleRuntimeMode('published'); - setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item)); + setBarkBattlePublishedConfig( + buildBarkBattlePublishedConfigFromWork(item), + ); setBarkBattleRuntimeReturnStage(returnStage); try { const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions( diff --git a/src/components/platform-entry/barkBattleWorkCache.test.ts b/src/components/platform-entry/barkBattleWorkCache.test.ts index 9d84f972..8115dde9 100644 --- a/src/components/platform-entry/barkBattleWorkCache.test.ts +++ b/src/components/platform-entry/barkBattleWorkCache.test.ts @@ -1,9 +1,18 @@ import { expect, test } from 'vitest'; -import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; +import type { + BarkBattleDraftConfig, + BarkBattlePublishedConfig, + BarkBattleWorkSummary, +} from '../../../packages/shared/src/contracts/barkBattle'; import { + buildBarkBattlePublishedConfigFromDraft, + buildBarkBattlePublishedConfigFromWork, + buildBarkBattlePublishSnapshot, + mergeBarkBattlePublishedConfigAssets, mergeBarkBattleWorksByWorkId, mergeBarkBattleWorkSummary, + resolveBarkBattleDraftGenerationStatus, shouldPreserveLocalBarkBattleWorkOnRefresh, } from './barkBattleWorkCache'; @@ -34,6 +43,29 @@ function buildBarkBattleWork( }; } +function buildBarkBattleDraft( + overrides: Partial = {}, +): BarkBattleDraftConfig { + return { + draftId: 'bark-battle-draft-1', + workId: 'BB-cache-race-12345678', + configVersion: 2, + rulesetVersion: 'bark-battle-ruleset-v2', + title: '汪汪测试杯', + description: '测试声浪赛', + themeDescription: '阳光草坪声浪竞技场', + playerImageDescription: '戴红色围巾的柯基选手', + opponentImageDescription: '蓝色护目镜哈士奇对手', + onomatopoeia: ['汪', '破阵'], + playerCharacterImageSrc: '/generated-bark-battle/player.png', + opponentCharacterImageSrc: '/generated-bark-battle/opponent.png', + uiBackgroundImageSrc: '/generated-bark-battle/background.png', + difficultyPreset: 'normal', + updatedAt: '2026-05-21T10:00:00.000Z', + ...overrides, + }; +} + test('preserves local published bark battle when refresh only returns same work draft', () => { const published = buildBarkBattleWork({ status: 'published', @@ -106,3 +138,95 @@ test('preserves local ready bark battle draft when refresh has not returned it y expect(merged[0]?.generationStatus).toBe('ready'); }); +test('resolves bark battle draft generation status from required images', () => { + expect( + resolveBarkBattleDraftGenerationStatus( + buildBarkBattleDraft({ uiBackgroundImageSrc: undefined }), + false, + ), + ).toBe('pending_assets'); + expect( + resolveBarkBattleDraftGenerationStatus( + buildBarkBattleDraft({ opponentCharacterImageSrc: '' }), + true, + ), + ).toBe('partial_failed'); + expect(resolveBarkBattleDraftGenerationStatus(buildBarkBattleDraft(), true)).toBe( + 'ready', + ); +}); + +test('builds draft runtime config with stable defaults', () => { + const config = buildBarkBattlePublishedConfigFromDraft( + buildBarkBattleDraft({ + workId: undefined, + configVersion: undefined, + rulesetVersion: undefined, + }), + ); + + expect(config.workId).toBe('bark-battle-draft-1'); + expect(config.draftId).toBe('bark-battle-draft-1'); + expect(config.configVersion).toBe(1); + expect(config.rulesetVersion).toBe('bark-battle-ruleset-v1'); + expect(config.playTypeId).toBe('bark-battle'); + expect(config.publishedAt).toBe('2026-05-21T10:00:00.000Z'); +}); + +test('builds work runtime config with publishedAt fallback', () => { + const config = buildBarkBattlePublishedConfigFromWork( + buildBarkBattleWork({ publishedAt: null }), + ); + + expect(config.workId).toBe('BB-cache-race-12345678'); + expect(config.description).toBe('测试声浪赛'); + expect(config.publishedAt).toBe('2026-05-21T10:00:00.000Z'); + expect(config.playerCharacterImageSrc).toBe('/generated-bark-battle/player.png'); +}); + +test('builds publish snapshot without empty asset fields', () => { + const snapshot = buildBarkBattlePublishSnapshot( + buildBarkBattleDraft({ + playerCharacterImageSrc: '', + opponentCharacterImageSrc: undefined, + }), + ); + + expect(snapshot).not.toHaveProperty('playerCharacterImageSrc'); + expect(snapshot).not.toHaveProperty('opponentCharacterImageSrc'); + expect(snapshot.uiBackgroundImageSrc).toBe( + '/generated-bark-battle/background.png', + ); +}); + +test('merges draft assets into published config when publish response omits them', () => { + const draft = buildBarkBattleDraft(); + const published: BarkBattlePublishedConfig = { + workId: 'BB-cache-race-12345678', + draftId: 'bark-battle-draft-1', + configVersion: 2, + rulesetVersion: 'bark-battle-ruleset-v2', + playTypeId: 'bark-battle', + title: '汪汪测试杯', + description: '测试声浪赛', + themeDescription: '阳光草坪声浪竞技场', + playerImageDescription: '戴红色围巾的柯基选手', + opponentImageDescription: '蓝色护目镜哈士奇对手', + onomatopoeia: ['汪', '破阵'], + difficultyPreset: 'normal', + updatedAt: '2026-05-21T10:01:00.000Z', + publishedAt: '2026-05-21T10:01:00.000Z', + }; + + const merged = mergeBarkBattlePublishedConfigAssets(published, draft); + + expect(merged.playerCharacterImageSrc).toBe( + '/generated-bark-battle/player.png', + ); + expect(merged.opponentCharacterImageSrc).toBe( + '/generated-bark-battle/opponent.png', + ); + expect(merged.uiBackgroundImageSrc).toBe( + '/generated-bark-battle/background.png', + ); +}); diff --git a/src/components/platform-entry/barkBattleWorkCache.ts b/src/components/platform-entry/barkBattleWorkCache.ts index c6ae45cf..b56520db 100644 --- a/src/components/platform-entry/barkBattleWorkCache.ts +++ b/src/components/platform-entry/barkBattleWorkCache.ts @@ -1,6 +1,8 @@ import type { + BarkBattleConfigEditorPayload, BarkBattleDraftConfig, BarkBattleGenerationStatus as SharedBarkBattleGenerationStatus, + BarkBattlePublishedConfig, BarkBattleWorkSummary, } from '../../../packages/shared/src/contracts/barkBattle'; @@ -36,6 +38,110 @@ export function hasBarkBattleSummaryRequiredImages(item: BarkBattleWorkSummary) ); } +export function hasBarkBattleDraftRequiredImages(draft: BarkBattleDraftConfig) { + return Boolean( + draft.playerCharacterImageSrc?.trim() && + draft.opponentCharacterImageSrc?.trim() && + draft.uiBackgroundImageSrc?.trim(), + ); +} + +export function resolveBarkBattleDraftGenerationStatus( + draft: BarkBattleDraftConfig, + partialFailed: boolean, +): BarkBattleGenerationStatus { + if (hasBarkBattleDraftRequiredImages(draft)) { + return 'ready'; + } + return partialFailed ? 'partial_failed' : 'pending_assets'; +} + +export function buildBarkBattlePublishedConfigFromDraft( + draft: BarkBattleDraftConfig, +): BarkBattlePublishedConfig { + return { + workId: draft.workId ?? draft.draftId, + draftId: draft.draftId, + configVersion: draft.configVersion ?? 1, + rulesetVersion: draft.rulesetVersion ?? 'bark-battle-ruleset-v1', + playTypeId: 'bark-battle', + title: draft.title, + description: draft.description, + themeDescription: draft.themeDescription, + playerImageDescription: draft.playerImageDescription, + opponentImageDescription: draft.opponentImageDescription, + onomatopoeia: draft.onomatopoeia, + playerCharacterImageSrc: draft.playerCharacterImageSrc, + opponentCharacterImageSrc: draft.opponentCharacterImageSrc, + uiBackgroundImageSrc: draft.uiBackgroundImageSrc, + difficultyPreset: draft.difficultyPreset, + updatedAt: draft.updatedAt, + publishedAt: draft.updatedAt, + }; +} + +export function buildBarkBattlePublishSnapshot( + draft: BarkBattleDraftConfig, +): BarkBattleConfigEditorPayload { + return { + title: draft.title, + description: draft.description, + themeDescription: draft.themeDescription, + playerImageDescription: draft.playerImageDescription, + opponentImageDescription: draft.opponentImageDescription, + onomatopoeia: draft.onomatopoeia, + ...(draft.playerCharacterImageSrc + ? { playerCharacterImageSrc: draft.playerCharacterImageSrc } + : {}), + ...(draft.opponentCharacterImageSrc + ? { opponentCharacterImageSrc: draft.opponentCharacterImageSrc } + : {}), + ...(draft.uiBackgroundImageSrc + ? { uiBackgroundImageSrc: draft.uiBackgroundImageSrc } + : {}), + difficultyPreset: draft.difficultyPreset, + }; +} + +export function mergeBarkBattlePublishedConfigAssets( + published: BarkBattlePublishedConfig, + draft: BarkBattleDraftConfig, +): BarkBattlePublishedConfig { + return { + ...published, + playerCharacterImageSrc: + published.playerCharacterImageSrc ?? draft.playerCharacterImageSrc, + opponentCharacterImageSrc: + published.opponentCharacterImageSrc ?? draft.opponentCharacterImageSrc, + uiBackgroundImageSrc: + published.uiBackgroundImageSrc ?? draft.uiBackgroundImageSrc, + }; +} + +export function buildBarkBattlePublishedConfigFromWork( + work: BarkBattleWorkSummary, +): BarkBattlePublishedConfig { + return { + workId: work.workId, + draftId: work.draftId ?? null, + configVersion: 1, + rulesetVersion: 'bark-battle-ruleset-v1', + playTypeId: 'bark-battle', + title: work.title, + description: work.summary, + themeDescription: work.themeDescription, + playerImageDescription: work.playerImageDescription, + opponentImageDescription: work.opponentImageDescription, + onomatopoeia: work.onomatopoeia, + playerCharacterImageSrc: work.playerCharacterImageSrc ?? undefined, + opponentCharacterImageSrc: work.opponentCharacterImageSrc ?? undefined, + uiBackgroundImageSrc: work.uiBackgroundImageSrc ?? undefined, + difficultyPreset: work.difficultyPreset, + updatedAt: work.updatedAt, + publishedAt: work.publishedAt ?? work.updatedAt, + }; +} + export function shouldPreserveLocalBarkBattleWorkOnRefresh( item: BarkBattleWorkSummary, refreshed: readonly BarkBattleWorkSummary[], @@ -85,11 +191,7 @@ export function buildBarkBattleWorkSummaryFromDraft( difficultyPreset: draft.difficultyPreset, status: 'draft', generationStatus, - publishReady: Boolean( - draft.playerCharacterImageSrc?.trim() && - draft.opponentCharacterImageSrc?.trim() && - draft.uiBackgroundImageSrc?.trim(), - ), + publishReady: hasBarkBattleDraftRequiredImages(draft), playCount: 0, updatedAt: draft.updatedAt, publishedAt: null,