refactor: 收口草稿生成作品架模型
This commit is contained in:
@@ -1273,6 +1273,14 @@
|
|||||||
- 验证方式:`npm run test -- src/components/platform-entry/platformMatch3DRuntimeProfile.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "match3d|抓大鹅"`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
- 验证方式:`npm run test -- src/components/platform-entry/platformMatch3DRuntimeProfile.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "match3d|抓大鹅"`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
- 关联文档:`docs/technical/【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md`。
|
- 关联文档:`docs/technical/【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
|
## 2026-06-03 Draft Generation Shelf Model 收口
|
||||||
|
|
||||||
|
- 背景:平台壳内散落创作生成 notice key、pending 作品架占位、失败文案覆盖、拼图稳定 ID、持久化 generating/failed 判断与草稿 Tab 未读点,新增或调整玩法时需要在多处理解 `workId` / `profileId` / `sourceSessionId` / `draftId` 形状。
|
||||||
|
- 决策:新增 `src/components/platform-entry/platformDraftGenerationShelfModel.ts` 作为 Draft Generation Shelf Module,Interface 收口 `collectDraftNoticeKeys`、`getGenerationNoticeShelfKeys`、`createPendingDraftShelfState`、各玩法 `buildPending*Works`、`buildCreationWorkShelfRuntimeState`、`collectVisibleDraftNoticeKeys`、`hasUnreadDraftGenerationUpdates`、拼图稳定 ID 与持久化状态判断;`PlatformEntryFlowShellImpl.tsx` 仅作为 React state、网络刷新、路由和弹窗副作用 Adapter。
|
||||||
|
- 影响范围:创作中心草稿 Tab 未读点、作品架生成中遮罩、失败草稿摘要、pending 草稿占位、拼图 / 抓大鹅生成恢复和各玩法生成完成通知。
|
||||||
|
- 验证方式:`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`、`npm run test -- src/components/custom-world-home/creationWorkShelf.test.ts -t "generation state|failure notice|failed puzzle"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating puzzle draft|persisted generating match3d draft|completed baby object match draft"`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.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 内。
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,Hub 不再按玩法 `kind` 分发,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,Hub 不再按玩法 `kind` 分发,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
|
平台入口创作生成通知、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)。
|
||||||
|
|
||||||
小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish、Bark Battle、Puzzle 公开 / 推荐运行态请求、Jump Hop / Wooden Fish 正式 run 请求和 Visual Novel 局部 JSON runtime 请求已先迁移,规则见 [【前端架构】RuntimeClientFamily收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RuntimeClientFamily%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish、Bark Battle、Puzzle 公开 / 推荐运行态请求、Jump Hop / Wooden Fish 正式 run 请求和 Visual Novel 局部 JSON runtime 请求已先迁移,规则见 [【前端架构】RuntimeClientFamily收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RuntimeClientFamily%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
抓大鹅 runtime profile 的公开详情转 work、session draft 转 profile、生成背景资产提升和 run/profile/public detail 素材优先级收口到 `src/components/platform-entry/platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91Match3DRuntimeProfile%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
抓大鹅 runtime profile 的公开详情转 work、session draft 转 profile、生成背景资产提升和 run/profile/public detail 素材优先级收口到 `src/components/platform-entry/platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91Match3DRuntimeProfile%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 【前端架构】Draft Generation Shelf Model 收口计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 同时承载创作生成状态、草稿 Tab 未读点、pending 作品架占位、失败文案覆盖和跨玩法 notice key。拼图、抓大鹅、方洞、跳一跳、敲木鱼、视觉小说、汪汪声浪、大鱼吃小鱼和宝贝识物各有不同的 `workId` / `profileId` / `sourceSessionId` / `draftId`,这些规则散在平台壳 **Implementation** 内,导致调用方必须理解每种玩法的草稿身份形状。
|
||||||
|
|
||||||
|
该 **Interface** 过浅:页面看似只关心“生成中 / 已完成未读 / 失败”,却要知道多 ID 去重、pending 草稿去重、失败摘要、拼图空标题兜底和持久化 generating 覆盖规则。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
新增 `src/components/platform-entry/platformDraftGenerationShelfModel.ts` 作为 Draft Generation Shelf **Module**。其 **Interface** 收口为:
|
||||||
|
|
||||||
|
- `collectDraftNoticeKeys(kind, ids)` / `getGenerationNoticeShelfKeys(item)`:统一把玩法草稿身份映射为 notice key。
|
||||||
|
- `createPendingDraftShelfState(...)` 与 `buildPending*Works(...)`:统一把本地 pending 生成状态映射成作品架占位,并避免与后端已有草稿重复。
|
||||||
|
- `buildCreationWorkShelfRuntimeState({ item, notices, pendingShelfItems })`:统一输出 `CreationWorkShelfRuntimeState`,处理失败覆盖、拼图空标题 `拼图草稿` 兜底、summary 占位覆盖、生成中遮罩和 ready 未读点。
|
||||||
|
- `collectVisibleDraftNoticeKeys(...)` / `hasUnreadDraftGenerationUpdates(...)`:统一草稿 Tab 顶部未读点规则。
|
||||||
|
- `buildPuzzleResultWorkId(...)` / `buildPuzzleResultProfileId(...)`、`isPersistedDraftGenerating(...)` / `isPersistedDraftFailed(...)`:把拼图稳定 ID 与持久化状态判断收在同一 **Seam**。
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 仍作为 React state 与副作用 **Adapter**:负责写入 `draftGenerationNotices` / `pendingDraftShelfItems`、启动生成、刷新后端列表、打开结果页和弹窗;它不再内联 pending shelf row shape、notice key 汇总和作品架 runtime state 规则。
|
||||||
|
|
||||||
|
## 约定
|
||||||
|
|
||||||
|
- 新玩法若需进入草稿生成通知,必须在此 **Module** 补 notice key、pending 占位和 visible key 映射,避免在平台壳里新增散落 switch。
|
||||||
|
- pending 作品只用于本地生成任务尚未被后端作品架返回时的临时展示;一旦后端已有同一 `sourceSessionId` / `profileId` / `workId`,pending 占位必须让位。
|
||||||
|
- 失败 notice 优先级高于持久化 generating,且可通过 pending metadata 提供更具体 summary;否则回退玩法默认失败摘要。
|
||||||
|
- 已有封面的拼图草稿即使局部关卡仍在后台生成,也不得被整卡遮罩为不可打开的生成中状态。
|
||||||
|
- 本 **Module** 不做网络请求、路由切换、弹窗副作用或 React state 写入,只保留纯 **Implementation**,以提高 **Depth**、**Leverage** 与 **Locality**。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- `npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`
|
||||||
|
- `npm run test -- src/components/custom-world-home/creationWorkShelf.test.ts -t "generation state|failure notice|failed puzzle"`
|
||||||
|
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating puzzle draft|persisted generating match3d draft|completed baby object match draft"`
|
||||||
|
- 针对新 **Module** 与测试执行 ESLint;`PlatformEntryFlowShellImpl.tsx` 保留既有 hook dependency warnings,不在本切片扩大处理。
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:encoding`
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
|
import { buildCreationWorkShelfItems } from '../custom-world-home/creationWorkShelf';
|
||||||
|
import {
|
||||||
|
buildCreationWorkShelfRuntimeState,
|
||||||
|
buildPendingPuzzleWorks,
|
||||||
|
buildPuzzleResultProfileId,
|
||||||
|
buildPuzzleResultWorkId,
|
||||||
|
collectVisibleDraftNoticeKeys,
|
||||||
|
createPendingDraftShelfState,
|
||||||
|
type DraftGenerationNoticeMap,
|
||||||
|
getGenerationNoticeShelfKeys,
|
||||||
|
hasUnreadDraftGenerationUpdates,
|
||||||
|
} from './platformDraftGenerationShelfModel';
|
||||||
|
|
||||||
|
describe('platformDraftGenerationShelfModel', () => {
|
||||||
|
test('buildPendingPuzzleWorks creates failed puzzle placeholder with stable ids and fallback title', () => {
|
||||||
|
const pending = buildPendingPuzzleWorks(
|
||||||
|
{
|
||||||
|
'puzzle-session-ocean': createPendingDraftShelfState(
|
||||||
|
'failed',
|
||||||
|
false,
|
||||||
|
'2026-06-03T08:00:00.000Z',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pending).toHaveLength(1);
|
||||||
|
expect(pending[0]).toMatchObject({
|
||||||
|
workId: 'puzzle-work-ocean',
|
||||||
|
profileId: 'puzzle-profile-ocean',
|
||||||
|
sourceSessionId: 'puzzle-session-ocean',
|
||||||
|
workTitle: '拼图草稿',
|
||||||
|
summary: '拼图草稿生成失败,可重新打开处理。',
|
||||||
|
generationStatus: 'failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildPendingPuzzleWorks skips pending item when backend shelf already has the session', () => {
|
||||||
|
const pending = buildPendingPuzzleWorks(
|
||||||
|
{
|
||||||
|
'puzzle-session-ocean': createPendingDraftShelfState(
|
||||||
|
'generating',
|
||||||
|
false,
|
||||||
|
'2026-06-03T08:00:00.000Z',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[buildPuzzleWork({ sourceSessionId: 'puzzle-session-ocean' })],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pending).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildCreationWorkShelfRuntimeState lets failure notice override persisted generating puzzle copy', () => {
|
||||||
|
const [item] = buildCreationWorkShelfItems({
|
||||||
|
rpgItems: [],
|
||||||
|
bigFishItems: [],
|
||||||
|
puzzleItems: [
|
||||||
|
buildPuzzleWork({
|
||||||
|
workId: 'puzzle-work-empty',
|
||||||
|
profileId: 'puzzle-profile-empty',
|
||||||
|
sourceSessionId: 'puzzle-session-empty',
|
||||||
|
workTitle: '',
|
||||||
|
workDescription: '',
|
||||||
|
levelName: '',
|
||||||
|
summary: '正在生成拼图草稿。',
|
||||||
|
generationStatus: 'generating',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(item).toBeTruthy();
|
||||||
|
|
||||||
|
const noticeKeys = getGenerationNoticeShelfKeys(item!);
|
||||||
|
const notices = Object.fromEntries(
|
||||||
|
noticeKeys.map((key) => [
|
||||||
|
key,
|
||||||
|
{ status: 'failed', seen: false },
|
||||||
|
]),
|
||||||
|
) as DraftGenerationNoticeMap;
|
||||||
|
|
||||||
|
const state = buildCreationWorkShelfRuntimeState({
|
||||||
|
item: item!,
|
||||||
|
notices,
|
||||||
|
pendingShelfItems: {
|
||||||
|
puzzle: {
|
||||||
|
'puzzle-session-empty': createPendingDraftShelfState(
|
||||||
|
'failed',
|
||||||
|
false,
|
||||||
|
'2026-06-03T08:00:00.000Z',
|
||||||
|
{ summary: '图片生成超时,可重新打开处理。' },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state).toMatchObject({
|
||||||
|
isGenerating: false,
|
||||||
|
hasGenerationFailure: true,
|
||||||
|
generationFailureSummary: '拼图草稿生成失败,可重新打开处理。',
|
||||||
|
hasUnreadUpdate: false,
|
||||||
|
suppressPersistedGenerating: true,
|
||||||
|
titleOverride: '拼图草稿',
|
||||||
|
summaryOverride: '图片生成超时,可重新打开处理。',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collectVisibleDraftNoticeKeys and hasUnreadDraftGenerationUpdates share unread dot rule', () => {
|
||||||
|
const puzzle = buildPuzzleWork({
|
||||||
|
workId: 'puzzle-work-ocean',
|
||||||
|
profileId: 'puzzle-profile-ocean',
|
||||||
|
sourceSessionId: 'puzzle-session-ocean',
|
||||||
|
});
|
||||||
|
const visibleKeys = collectVisibleDraftNoticeKeys({
|
||||||
|
rpgItems: [],
|
||||||
|
bigFishItems: [],
|
||||||
|
jumpHopItems: [],
|
||||||
|
woodenFishItems: [],
|
||||||
|
match3dItems: [],
|
||||||
|
squareHoleItems: [],
|
||||||
|
puzzleItems: [puzzle],
|
||||||
|
visualNovelItems: [],
|
||||||
|
barkBattleItems: [],
|
||||||
|
babyObjectMatchItems: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(visibleKeys).toContain('puzzle:puzzle-work-ocean');
|
||||||
|
expect(visibleKeys).toContain('puzzle:puzzle-profile-ocean');
|
||||||
|
expect(visibleKeys).toContain('puzzle:puzzle-session-ocean');
|
||||||
|
expect(buildPuzzleResultWorkId('puzzle-session-ocean')).toBe(
|
||||||
|
'puzzle-work-ocean',
|
||||||
|
);
|
||||||
|
expect(buildPuzzleResultProfileId('puzzle-session-ocean')).toBe(
|
||||||
|
'puzzle-profile-ocean',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasUnreadDraftGenerationUpdates(
|
||||||
|
{
|
||||||
|
'puzzle:puzzle-profile-ocean': {
|
||||||
|
status: 'ready',
|
||||||
|
seen: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visibleKeys,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
hasUnreadDraftGenerationUpdates(
|
||||||
|
{
|
||||||
|
'puzzle:puzzle-profile-ocean': {
|
||||||
|
status: 'ready',
|
||||||
|
seen: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visibleKeys,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildPuzzleWork(
|
||||||
|
overrides: Partial<PuzzleWorkSummary> = {},
|
||||||
|
): PuzzleWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'puzzle-work-base',
|
||||||
|
profileId: 'puzzle-profile-base',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
sourceSessionId: 'puzzle-session-base',
|
||||||
|
authorDisplayName: '测试作者',
|
||||||
|
workTitle: '潮雾拼图',
|
||||||
|
workDescription: '潮雾港口拼图。',
|
||||||
|
levelName: '潮雾拼图',
|
||||||
|
summary: '潮雾港口拼图。',
|
||||||
|
themeTags: [],
|
||||||
|
coverImageSrc: null,
|
||||||
|
coverAssetId: null,
|
||||||
|
publicationStatus: 'draft',
|
||||||
|
updatedAt: '2026-06-03T08:00:00.000Z',
|
||||||
|
publishedAt: null,
|
||||||
|
playCount: 0,
|
||||||
|
remixCount: 0,
|
||||||
|
likeCount: 0,
|
||||||
|
publishReady: false,
|
||||||
|
levels: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,860 @@
|
|||||||
|
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||||
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
|
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
|
||||||
|
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||||
|
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||||
|
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||||
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
|
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
|
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||||
|
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||||
|
import {
|
||||||
|
type CreationWorkShelfItem,
|
||||||
|
type CreationWorkShelfKind,
|
||||||
|
type CreationWorkShelfRuntimeState,
|
||||||
|
resolvePuzzleWorkCoverImageSrc,
|
||||||
|
} from '../custom-world-home/creationWorkShelf';
|
||||||
|
|
||||||
|
export type DraftGenerationNoticeStatus = 'generating' | 'ready' | 'failed';
|
||||||
|
|
||||||
|
export type DraftGenerationNotice = {
|
||||||
|
status: DraftGenerationNoticeStatus;
|
||||||
|
seen: boolean;
|
||||||
|
completedAtMs?: number;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DraftGenerationNoticeMap = Record<string, DraftGenerationNotice>;
|
||||||
|
|
||||||
|
export type PendingDraftShelfState = {
|
||||||
|
status: DraftGenerationNoticeStatus;
|
||||||
|
seen: boolean;
|
||||||
|
updatedAt: string;
|
||||||
|
title?: string;
|
||||||
|
summary?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PendingDraftShelfKind = Exclude<CreationWorkShelfKind, 'rpg'>;
|
||||||
|
|
||||||
|
export type PendingDraftShelfMap = Partial<
|
||||||
|
Record<PendingDraftShelfKind, Record<string, PendingDraftShelfState>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type PendingDraftShelfMetadata = {
|
||||||
|
title?: string | null;
|
||||||
|
summary?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlatformDraftGenerationVisibleShelfSources = {
|
||||||
|
rpgItems: readonly CustomWorldWorkSummary[];
|
||||||
|
bigFishItems: readonly BigFishWorkSummary[];
|
||||||
|
jumpHopItems: readonly JumpHopWorkSummaryResponse[];
|
||||||
|
woodenFishItems: readonly WoodenFishWorkSummaryResponse[];
|
||||||
|
match3dItems: readonly Match3DWorkSummary[];
|
||||||
|
squareHoleItems: readonly SquareHoleWorkSummary[];
|
||||||
|
puzzleItems: readonly PuzzleWorkSummary[];
|
||||||
|
visualNovelItems: readonly VisualNovelWorkSummary[];
|
||||||
|
barkBattleItems: readonly BarkBattleWorkSummary[];
|
||||||
|
babyObjectMatchItems: readonly BabyObjectMatchDraft[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildPuzzleResultProfileId(
|
||||||
|
sessionId: string | null | undefined,
|
||||||
|
) {
|
||||||
|
const stableSuffix = resolvePuzzleSessionStableSuffix(sessionId);
|
||||||
|
return stableSuffix ? `puzzle-profile-${stableSuffix}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPuzzleResultWorkId(
|
||||||
|
sessionId: string | null | undefined,
|
||||||
|
) {
|
||||||
|
const stableSuffix = resolvePuzzleSessionStableSuffix(sessionId);
|
||||||
|
return stableSuffix ? `puzzle-work-${stableSuffix}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDraftNoticeKey(
|
||||||
|
kind: CreationWorkShelfKind,
|
||||||
|
id: string,
|
||||||
|
) {
|
||||||
|
return `${kind}:${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectDraftNoticeKeys(
|
||||||
|
kind: CreationWorkShelfKind,
|
||||||
|
ids: Array<string | null | undefined>,
|
||||||
|
) {
|
||||||
|
const keys = new Set<string>();
|
||||||
|
for (const id of ids) {
|
||||||
|
const normalizedId = id?.trim();
|
||||||
|
if (normalizedId) {
|
||||||
|
keys.add(buildDraftNoticeKey(kind, normalizedId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDraftNoticeId(id: string | null | undefined) {
|
||||||
|
return id?.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePendingDraftShelfLookupId(
|
||||||
|
kind: PendingDraftShelfKind,
|
||||||
|
id: string | null | undefined,
|
||||||
|
) {
|
||||||
|
const normalizedId = normalizeDraftNoticeId(id);
|
||||||
|
if (!normalizedId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noticePrefix = `${kind}:`;
|
||||||
|
if (!normalizedId.startsWith(noticePrefix)) {
|
||||||
|
return normalizedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeDraftNoticeId(normalizedId.slice(noticePrefix.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPendingDraftShelfState(
|
||||||
|
status: DraftGenerationNoticeStatus,
|
||||||
|
seen = false,
|
||||||
|
updatedAt = new Date().toISOString(),
|
||||||
|
metadata?: PendingDraftShelfMetadata,
|
||||||
|
): PendingDraftShelfState {
|
||||||
|
const title = metadata?.title?.trim();
|
||||||
|
const summary = metadata?.summary?.trim();
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
seen,
|
||||||
|
updatedAt,
|
||||||
|
...(title ? { title } : {}),
|
||||||
|
...(summary ? { summary } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDraftFailedShelfSummary(kind: CreationWorkShelfKind) {
|
||||||
|
switch (kind) {
|
||||||
|
case 'puzzle':
|
||||||
|
return '拼图草稿生成失败,可重新打开处理。';
|
||||||
|
case 'match3d':
|
||||||
|
return '玩法素材生成失败,可重新打开处理。';
|
||||||
|
case 'big-fish':
|
||||||
|
return '草稿生成失败,可重新打开处理。';
|
||||||
|
case 'square-hole':
|
||||||
|
return '挑战素材生成失败,可重新打开处理。';
|
||||||
|
case 'jump-hop':
|
||||||
|
return '跳一跳玩法草稿生成失败,可重新打开处理。';
|
||||||
|
case 'wooden-fish':
|
||||||
|
return '敲木鱼草稿生成失败,可重新打开处理。';
|
||||||
|
case 'visual-novel':
|
||||||
|
return '视觉小说草稿生成失败,可重新打开处理。';
|
||||||
|
case 'bark-battle':
|
||||||
|
return '声浪竞技素材生成失败,可重新打开处理。';
|
||||||
|
case 'baby-object-match':
|
||||||
|
return '宝贝识物草稿生成失败,可重新打开处理。';
|
||||||
|
default:
|
||||||
|
return '草稿生成失败,可重新打开处理。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDraftCompletionDialogSource(
|
||||||
|
kind: CreationWorkShelfKind,
|
||||||
|
ids: Array<string | null | undefined>,
|
||||||
|
): string {
|
||||||
|
const sourceId = pickDraftCompletionDialogSourceId(ids);
|
||||||
|
switch (kind) {
|
||||||
|
case 'rpg':
|
||||||
|
return formatDraftTaskCompletionSource('RPG 草稿', sourceId);
|
||||||
|
case 'big-fish':
|
||||||
|
return formatDraftTaskCompletionSource('大鱼吃小鱼草稿', sourceId);
|
||||||
|
case 'match3d':
|
||||||
|
return formatDraftTaskCompletionSource('抓大鹅草稿', sourceId);
|
||||||
|
case 'square-hole':
|
||||||
|
return formatDraftTaskCompletionSource('方洞挑战草稿', sourceId);
|
||||||
|
case 'jump-hop':
|
||||||
|
return formatDraftTaskCompletionSource('跳一跳草稿', sourceId);
|
||||||
|
case 'wooden-fish':
|
||||||
|
return formatDraftTaskCompletionSource('敲木鱼草稿', sourceId);
|
||||||
|
case 'puzzle':
|
||||||
|
return formatDraftTaskCompletionSource('拼图草稿', sourceId);
|
||||||
|
case 'visual-novel':
|
||||||
|
return formatDraftTaskCompletionSource('视觉小说草稿', sourceId);
|
||||||
|
case 'bark-battle':
|
||||||
|
return formatDraftTaskCompletionSource('汪汪声浪草稿', sourceId);
|
||||||
|
case 'baby-object-match':
|
||||||
|
return formatDraftTaskCompletionSource('宝贝识物草稿', sourceId);
|
||||||
|
}
|
||||||
|
return formatDraftTaskCompletionSource('创作草稿', sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDraftShelfSummaryPlaceholder(
|
||||||
|
value: string | null | undefined,
|
||||||
|
) {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /^(正在生成|.*生成失败,可重新打开处理。$|未填写作品描述$)/u.test(
|
||||||
|
normalized,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPersistedDraftGenerating(value: string | null | undefined) {
|
||||||
|
return value?.trim() === 'generating';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPersistedDraftFailed(value: string | null | undefined) {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
return normalized === 'failed' || normalized === 'partial_failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGenerationNoticeShelfKeys(
|
||||||
|
item: CreationWorkShelfItem,
|
||||||
|
): string[] {
|
||||||
|
switch (item.source.kind) {
|
||||||
|
case 'rpg':
|
||||||
|
return collectDraftNoticeKeys('rpg', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.sessionId,
|
||||||
|
item.source.item.profileId,
|
||||||
|
]);
|
||||||
|
case 'big-fish':
|
||||||
|
return collectDraftNoticeKeys('big-fish', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.sourceSessionId,
|
||||||
|
]);
|
||||||
|
case 'match3d':
|
||||||
|
return collectDraftNoticeKeys('match3d', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.profileId,
|
||||||
|
item.source.item.sourceSessionId,
|
||||||
|
]);
|
||||||
|
case 'square-hole':
|
||||||
|
return collectDraftNoticeKeys('square-hole', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.profileId,
|
||||||
|
item.source.item.sourceSessionId,
|
||||||
|
]);
|
||||||
|
case 'jump-hop':
|
||||||
|
return collectDraftNoticeKeys('jump-hop', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.profileId,
|
||||||
|
item.source.item.sourceSessionId,
|
||||||
|
]);
|
||||||
|
case 'puzzle':
|
||||||
|
return collectDraftNoticeKeys('puzzle', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.profileId,
|
||||||
|
item.source.item.sourceSessionId,
|
||||||
|
buildPuzzleResultWorkId(item.source.item.sourceSessionId),
|
||||||
|
buildPuzzleResultProfileId(item.source.item.sourceSessionId),
|
||||||
|
]);
|
||||||
|
case 'visual-novel':
|
||||||
|
return collectDraftNoticeKeys('visual-novel', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.profileId,
|
||||||
|
]);
|
||||||
|
case 'baby-object-match':
|
||||||
|
return collectDraftNoticeKeys('baby-object-match', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.profileId,
|
||||||
|
item.source.item.draftId,
|
||||||
|
]);
|
||||||
|
case 'bark-battle':
|
||||||
|
return collectDraftNoticeKeys('bark-battle', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.draftId,
|
||||||
|
]);
|
||||||
|
case 'wooden-fish':
|
||||||
|
return collectDraftNoticeKeys('wooden-fish', [
|
||||||
|
item.id,
|
||||||
|
item.source.item.workId,
|
||||||
|
item.source.item.profileId,
|
||||||
|
item.source.item.sourceSessionId,
|
||||||
|
]);
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDraftGenerationNotice(
|
||||||
|
notices: DraftGenerationNoticeMap,
|
||||||
|
keys: readonly string[],
|
||||||
|
) {
|
||||||
|
for (const key of keys) {
|
||||||
|
const notice = notices[key];
|
||||||
|
if (notice) {
|
||||||
|
return notice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPendingDraftShelfState(
|
||||||
|
pendingShelfItems: PendingDraftShelfMap,
|
||||||
|
kind: PendingDraftShelfKind,
|
||||||
|
keys: readonly string[],
|
||||||
|
) {
|
||||||
|
const entries = pendingShelfItems[kind];
|
||||||
|
if (!entries) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const normalizedKey = normalizePendingDraftShelfLookupId(kind, key);
|
||||||
|
const pending = normalizedKey ? entries[normalizedKey] : null;
|
||||||
|
if (pending) {
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasDraftGenerationNoticeStatus(
|
||||||
|
notices: DraftGenerationNoticeMap,
|
||||||
|
kind: CreationWorkShelfKind,
|
||||||
|
ids: Array<string | null | undefined>,
|
||||||
|
status: DraftGenerationNoticeStatus,
|
||||||
|
) {
|
||||||
|
return collectDraftNoticeKeys(kind, ids).some(
|
||||||
|
(key) => notices[key]?.status === status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasUnreadReadyDraftGenerationNotice(
|
||||||
|
notices: DraftGenerationNoticeMap,
|
||||||
|
kind: CreationWorkShelfKind,
|
||||||
|
ids: Array<string | null | undefined>,
|
||||||
|
) {
|
||||||
|
return collectDraftNoticeKeys(kind, ids).some((key) => {
|
||||||
|
const notice = notices[key];
|
||||||
|
return notice?.status === 'ready' && !notice.seen;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCreationWorkShelfRuntimeState(params: {
|
||||||
|
item: CreationWorkShelfItem;
|
||||||
|
notices: DraftGenerationNoticeMap;
|
||||||
|
pendingShelfItems: PendingDraftShelfMap;
|
||||||
|
}): CreationWorkShelfRuntimeState {
|
||||||
|
const { item, notices, pendingShelfItems } = params;
|
||||||
|
const noticeKeys = getGenerationNoticeShelfKeys(item);
|
||||||
|
const notice = getDraftGenerationNotice(notices, noticeKeys);
|
||||||
|
|
||||||
|
if (notice?.status === 'failed') {
|
||||||
|
const failedSummary = buildDraftFailedShelfSummary(item.source.kind);
|
||||||
|
const pending =
|
||||||
|
item.source.kind === 'rpg'
|
||||||
|
? null
|
||||||
|
: getPendingDraftShelfState(
|
||||||
|
pendingShelfItems,
|
||||||
|
item.source.kind,
|
||||||
|
noticeKeys,
|
||||||
|
);
|
||||||
|
const pendingSummary = pending?.summary?.trim();
|
||||||
|
return {
|
||||||
|
isGenerating: false,
|
||||||
|
hasGenerationFailure: true,
|
||||||
|
generationFailureSummary: failedSummary,
|
||||||
|
hasUnreadUpdate: false,
|
||||||
|
suppressPersistedGenerating: true,
|
||||||
|
titleOverride:
|
||||||
|
item.source.kind === 'puzzle' &&
|
||||||
|
item.status === 'draft' &&
|
||||||
|
!item.source.item.workTitle?.trim()
|
||||||
|
? '拼图草稿'
|
||||||
|
: undefined,
|
||||||
|
summaryOverride: isDraftShelfSummaryPlaceholder(item.summary)
|
||||||
|
? (pendingSummary ?? failedSummary)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
item.source.kind === 'puzzle' &&
|
||||||
|
isPersistedDraftFailed(item.source.item.generationStatus)
|
||||||
|
) {
|
||||||
|
const failedSummary = buildDraftFailedShelfSummary('puzzle');
|
||||||
|
return {
|
||||||
|
isGenerating: false,
|
||||||
|
hasGenerationFailure: true,
|
||||||
|
generationFailureSummary: failedSummary,
|
||||||
|
hasUnreadUpdate: false,
|
||||||
|
suppressPersistedGenerating: true,
|
||||||
|
titleOverride:
|
||||||
|
item.status === 'draft' && !item.source.item.workTitle?.trim()
|
||||||
|
? '拼图草稿'
|
||||||
|
: undefined,
|
||||||
|
summaryOverride: isDraftShelfSummaryPlaceholder(item.summary)
|
||||||
|
? failedSummary
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNoticeGenerating =
|
||||||
|
notice?.status === 'generating' &&
|
||||||
|
(item.source.kind !== 'puzzle' ||
|
||||||
|
!resolvePuzzleWorkCoverImageSrc(item.source.item));
|
||||||
|
return {
|
||||||
|
isGenerating: isNoticeGenerating || item.isGenerating === true,
|
||||||
|
hasUnreadUpdate: notice?.status === 'ready' && !notice.seen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectVisibleDraftNoticeKeys(
|
||||||
|
sources: PlatformDraftGenerationVisibleShelfSources,
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
...sources.rpgItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('rpg', [
|
||||||
|
item.workId,
|
||||||
|
item.sessionId,
|
||||||
|
item.profileId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...sources.bigFishItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('big-fish', [item.workId, item.sourceSessionId]),
|
||||||
|
),
|
||||||
|
...sources.jumpHopItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('jump-hop', [
|
||||||
|
item.workId,
|
||||||
|
item.profileId,
|
||||||
|
item.sourceSessionId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...sources.woodenFishItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('wooden-fish', [
|
||||||
|
item.workId,
|
||||||
|
item.profileId,
|
||||||
|
item.sourceSessionId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...sources.match3dItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('match3d', [
|
||||||
|
item.workId,
|
||||||
|
item.profileId,
|
||||||
|
item.sourceSessionId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...sources.squareHoleItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('square-hole', [
|
||||||
|
item.workId,
|
||||||
|
item.profileId,
|
||||||
|
item.sourceSessionId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...sources.puzzleItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('puzzle', [
|
||||||
|
item.workId,
|
||||||
|
item.profileId,
|
||||||
|
item.sourceSessionId,
|
||||||
|
buildPuzzleResultWorkId(item.sourceSessionId),
|
||||||
|
buildPuzzleResultProfileId(item.sourceSessionId),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
...sources.visualNovelItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('visual-novel', [item.profileId]),
|
||||||
|
),
|
||||||
|
...sources.barkBattleItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('bark-battle', [item.workId, item.draftId]),
|
||||||
|
),
|
||||||
|
...sources.babyObjectMatchItems.flatMap((item) =>
|
||||||
|
collectDraftNoticeKeys('baby-object-match', [
|
||||||
|
item.profileId,
|
||||||
|
item.draftId,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasUnreadDraftGenerationUpdates(
|
||||||
|
notices: DraftGenerationNoticeMap,
|
||||||
|
visibleKeys: readonly string[],
|
||||||
|
) {
|
||||||
|
return visibleKeys.some((key) => {
|
||||||
|
const notice = notices[key];
|
||||||
|
return notice?.status === 'ready' && !notice.seen;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingBigFishWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly BigFishWorkSummary[],
|
||||||
|
): BigFishWorkSummary[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([sessionId]) =>
|
||||||
|
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||||
|
)
|
||||||
|
.map(([sessionId, state]) => {
|
||||||
|
const isFailed = state.status === 'failed';
|
||||||
|
return {
|
||||||
|
workId: `big-fish-work-${sessionId}`,
|
||||||
|
sourceSessionId: sessionId,
|
||||||
|
ownerUserId: '',
|
||||||
|
authorDisplayName: '',
|
||||||
|
title: '大鱼吃小鱼草稿',
|
||||||
|
subtitle: isFailed ? '生成失败待重试' : '草稿生成中',
|
||||||
|
summary: isFailed
|
||||||
|
? '草稿生成失败,可重新打开处理。'
|
||||||
|
: '正在生成玩法草稿。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
status: 'draft',
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
publishReady: false,
|
||||||
|
levelCount: 0,
|
||||||
|
levelMainImageReadyCount: 0,
|
||||||
|
levelMotionReadyCount: 0,
|
||||||
|
backgroundReady: false,
|
||||||
|
playCount: 0,
|
||||||
|
remixCount: 0,
|
||||||
|
likeCount: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingJumpHopWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly JumpHopWorkSummaryResponse[],
|
||||||
|
): JumpHopWorkSummaryResponse[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([sessionId]) =>
|
||||||
|
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||||
|
)
|
||||||
|
.map(([sessionId, state]) => {
|
||||||
|
const generationStatus =
|
||||||
|
state.status === 'failed'
|
||||||
|
? 'failed'
|
||||||
|
: state.status === 'generating'
|
||||||
|
? 'generating'
|
||||||
|
: 'ready';
|
||||||
|
return {
|
||||||
|
runtimeKind: 'jump-hop',
|
||||||
|
workId: `jump-hop-work-${sessionId}`,
|
||||||
|
profileId: `jump-hop-profile-${sessionId}`,
|
||||||
|
ownerUserId: '',
|
||||||
|
sourceSessionId: sessionId,
|
||||||
|
workTitle: '跳一跳草稿',
|
||||||
|
workDescription:
|
||||||
|
state.status === 'failed'
|
||||||
|
? '跳一跳玩法草稿生成失败,可重新打开处理。'
|
||||||
|
: '正在生成跳一跳玩法草稿。',
|
||||||
|
themeTags: [],
|
||||||
|
difficulty: 'standard',
|
||||||
|
stylePreset: 'minimal-blocks',
|
||||||
|
coverImageSrc: null,
|
||||||
|
publicationStatus: 'draft',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
publishReady: false,
|
||||||
|
generationStatus,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingWoodenFishWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly WoodenFishWorkSummaryResponse[],
|
||||||
|
): WoodenFishWorkSummaryResponse[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([sessionId]) =>
|
||||||
|
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||||
|
)
|
||||||
|
.map(([sessionId, state]) => {
|
||||||
|
const generationStatus =
|
||||||
|
state.status === 'failed'
|
||||||
|
? 'failed'
|
||||||
|
: state.status === 'generating'
|
||||||
|
? 'generating'
|
||||||
|
: 'ready';
|
||||||
|
return {
|
||||||
|
runtimeKind: 'wooden-fish',
|
||||||
|
workId: `wooden-fish-work-${sessionId}`,
|
||||||
|
profileId: sessionId,
|
||||||
|
ownerUserId: '',
|
||||||
|
sourceSessionId: sessionId,
|
||||||
|
workTitle: '敲木鱼草稿',
|
||||||
|
workDescription:
|
||||||
|
state.status === 'failed'
|
||||||
|
? '敲木鱼草稿生成失败,可重新打开处理。'
|
||||||
|
: '正在生成敲木鱼草稿。',
|
||||||
|
themeTags: ['敲木鱼'],
|
||||||
|
coverImageSrc: null,
|
||||||
|
publicationStatus: 'draft',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
publishReady: false,
|
||||||
|
generationStatus,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingMatch3DWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly Match3DWorkSummary[],
|
||||||
|
): Match3DWorkSummary[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([sessionId]) =>
|
||||||
|
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||||
|
)
|
||||||
|
.map(([sessionId, state]) => {
|
||||||
|
const themeText = state.summary?.trim() || state.title?.trim() || '';
|
||||||
|
const fallbackSummary =
|
||||||
|
state.status === 'failed'
|
||||||
|
? '玩法素材生成失败,可重新打开处理。'
|
||||||
|
: '正在生成玩法素材。';
|
||||||
|
return {
|
||||||
|
workId: `match3d-work-${sessionId}`,
|
||||||
|
profileId: sessionId,
|
||||||
|
ownerUserId: '',
|
||||||
|
sourceSessionId: sessionId,
|
||||||
|
gameName: '抓大鹅草稿',
|
||||||
|
themeText,
|
||||||
|
summary: themeText || fallbackSummary,
|
||||||
|
tags: [],
|
||||||
|
coverImageSrc: null,
|
||||||
|
referenceImageSrc: null,
|
||||||
|
clearCount: 0,
|
||||||
|
difficulty: 0,
|
||||||
|
publicationStatus: 'draft',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
publishReady: false,
|
||||||
|
generationStatus:
|
||||||
|
state.status === 'failed'
|
||||||
|
? 'failed'
|
||||||
|
: state.status === 'generating'
|
||||||
|
? 'generating'
|
||||||
|
: 'ready',
|
||||||
|
generatedItemAssets: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingSquareHoleWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly SquareHoleWorkSummary[],
|
||||||
|
): SquareHoleWorkSummary[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([sessionId]) =>
|
||||||
|
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||||
|
)
|
||||||
|
.map(([sessionId, state]) => ({
|
||||||
|
workId: `square-hole-work-${sessionId}`,
|
||||||
|
profileId: sessionId,
|
||||||
|
ownerUserId: '',
|
||||||
|
sourceSessionId: sessionId,
|
||||||
|
gameName: '方洞挑战草稿',
|
||||||
|
themeText: '',
|
||||||
|
twistRule: '',
|
||||||
|
summary:
|
||||||
|
state.status === 'failed'
|
||||||
|
? '挑战素材生成失败,可重新打开处理。'
|
||||||
|
: '正在生成挑战素材。',
|
||||||
|
tags: [],
|
||||||
|
coverImageSrc: null,
|
||||||
|
backgroundPrompt: '',
|
||||||
|
backgroundImageSrc: null,
|
||||||
|
shapeOptions: [],
|
||||||
|
holeOptions: [],
|
||||||
|
shapeCount: 0,
|
||||||
|
difficulty: 0,
|
||||||
|
publicationStatus: 'draft',
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
publishReady: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingPuzzleWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly PuzzleWorkSummary[],
|
||||||
|
): PuzzleWorkSummary[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([sessionId]) =>
|
||||||
|
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||||
|
)
|
||||||
|
.map(([sessionId, state]) => {
|
||||||
|
const profileId =
|
||||||
|
buildPuzzleResultProfileId(sessionId) ?? `puzzle-profile-${sessionId}`;
|
||||||
|
const title = state.title?.trim() || '拼图草稿';
|
||||||
|
const summary =
|
||||||
|
state.summary?.trim() ||
|
||||||
|
(state.status === 'failed'
|
||||||
|
? '拼图草稿生成失败,可重新打开处理。'
|
||||||
|
: '正在生成拼图草稿。');
|
||||||
|
return {
|
||||||
|
workId:
|
||||||
|
buildPuzzleResultWorkId(sessionId) ?? `puzzle-work-${sessionId}`,
|
||||||
|
profileId,
|
||||||
|
ownerUserId: '',
|
||||||
|
sourceSessionId: sessionId,
|
||||||
|
authorDisplayName: '',
|
||||||
|
workTitle: title,
|
||||||
|
workDescription: summary,
|
||||||
|
levelName: title,
|
||||||
|
summary,
|
||||||
|
themeTags: [],
|
||||||
|
coverImageSrc: null,
|
||||||
|
coverAssetId: null,
|
||||||
|
publicationStatus: 'draft',
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
playCount: 0,
|
||||||
|
remixCount: 0,
|
||||||
|
likeCount: 0,
|
||||||
|
publishReady: false,
|
||||||
|
generationStatus:
|
||||||
|
state.status === 'generating'
|
||||||
|
? 'generating'
|
||||||
|
: state.status === 'failed'
|
||||||
|
? 'failed'
|
||||||
|
: 'ready',
|
||||||
|
levels: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingVisualNovelWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly VisualNovelWorkSummary[],
|
||||||
|
): VisualNovelWorkSummary[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([profileId]) =>
|
||||||
|
existingItems.every((item) => item.profileId !== profileId),
|
||||||
|
)
|
||||||
|
.map(([profileId, state]) => ({
|
||||||
|
runtimeKind: 'visual-novel',
|
||||||
|
profileId,
|
||||||
|
ownerUserId: '',
|
||||||
|
title: '视觉小说草稿',
|
||||||
|
description:
|
||||||
|
state.status === 'failed'
|
||||||
|
? '视觉小说草稿生成失败,可重新打开处理。'
|
||||||
|
: '正在生成视觉小说草稿。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
tags: [],
|
||||||
|
publishStatus: 'draft',
|
||||||
|
publishReady: false,
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPendingBarkBattleWorks(
|
||||||
|
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||||
|
existingItems: readonly BarkBattleWorkSummary[],
|
||||||
|
): BarkBattleWorkSummary[] {
|
||||||
|
if (!pending) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(pending)
|
||||||
|
.filter(([id]) =>
|
||||||
|
existingItems.every((item) => item.workId !== id && item.draftId !== id),
|
||||||
|
)
|
||||||
|
.map(([id, state]) => ({
|
||||||
|
workId: id,
|
||||||
|
draftId: id,
|
||||||
|
ownerUserId: '',
|
||||||
|
authorDisplayName: '',
|
||||||
|
title: '汪汪声浪草稿',
|
||||||
|
summary:
|
||||||
|
state.status === 'failed'
|
||||||
|
? '声浪竞技素材生成失败,可重新打开处理。'
|
||||||
|
: '正在生成声浪竞技素材。',
|
||||||
|
themeDescription: '',
|
||||||
|
playerImageDescription: '',
|
||||||
|
opponentImageDescription: '',
|
||||||
|
onomatopoeia: [],
|
||||||
|
playerCharacterImageSrc: null,
|
||||||
|
opponentCharacterImageSrc: null,
|
||||||
|
uiBackgroundImageSrc: null,
|
||||||
|
difficultyPreset: 'normal',
|
||||||
|
status: 'draft',
|
||||||
|
generationStatus:
|
||||||
|
state.status === 'generating'
|
||||||
|
? 'pending_assets'
|
||||||
|
: state.status === 'failed'
|
||||||
|
? 'partial_failed'
|
||||||
|
: 'ready',
|
||||||
|
publishReady: false,
|
||||||
|
playCount: 0,
|
||||||
|
updatedAt: state.updatedAt,
|
||||||
|
publishedAt: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePuzzleSessionStableSuffix(
|
||||||
|
sessionId: string | null | undefined,
|
||||||
|
) {
|
||||||
|
const normalizedSessionId = sessionId?.trim();
|
||||||
|
if (!normalizedSessionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizedSessionId.startsWith('puzzle-session-')
|
||||||
|
? normalizedSessionId.slice('puzzle-session-'.length)
|
||||||
|
: normalizedSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDraftCompletionDialogSourceId(
|
||||||
|
ids: Array<string | null | undefined>,
|
||||||
|
) {
|
||||||
|
const normalizedIds = ids
|
||||||
|
.map((id) => id?.trim() ?? '')
|
||||||
|
.filter((id) => Boolean(id));
|
||||||
|
return (
|
||||||
|
normalizedIds.find((id) => /session/i.test(id)) ??
|
||||||
|
normalizedIds.find((id) => /work/i.test(id)) ??
|
||||||
|
normalizedIds.find((id) => /draft/i.test(id)) ??
|
||||||
|
normalizedIds.find((id) => /run/i.test(id)) ??
|
||||||
|
normalizedIds.find((id) => /profile/i.test(id)) ??
|
||||||
|
normalizedIds[0] ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDraftTaskCompletionSource(label: string, id?: string | null) {
|
||||||
|
const normalizedId = id?.trim();
|
||||||
|
return normalizedId ? `${label} ${normalizedId}` : label;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user