refactor: 收口创作恢复URL模型
This commit is contained in:
@@ -1281,6 +1281,14 @@
|
||||
- 验证方式:`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 Creation URL State Model 收口
|
||||
|
||||
- 背景:平台壳内散落各玩法创作恢复 URL 的 `sessionId` / `profileId` / `draftId` / `workId` 组装、空值归一化、拼图 runtime query key 与拼图稳定身份互推,导致刷新恢复规则缺少稳定测试面。
|
||||
- 决策:新增 `src/components/platform-entry/platformCreationUrlStateModel.ts` 作为 Creation URL State Module,Interface 收口各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、URL state 非空判断和 runtime state key;新增 `src/components/platform-entry/platformPuzzleIdentityModel.ts` 作为拼图稳定身份 Module,`platformDraftGenerationShelfModel.ts` 仅 re-export 旧入口以保持兼容。`PlatformEntryFlowShellImpl.tsx` 只保留路由、URL 写入和网络副作用 Adapter。
|
||||
- 影响范围:创作流程刷新恢复、拼图草稿 / 发布 runtime 深链、作品架打开试玩、跳一跳 / 敲木鱼 work-backed 恢复、Bark Battle / 宝贝识物本地草稿恢复。
|
||||
- 验证方式:`npm run test -- src/components/platform-entry/platformCreationUrlStateModel.test.ts src/components/platform-entry/platformPuzzleIdentityModel.test.ts`、`npm run test -- src/services/creationUrlState.test.ts`、`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||
- 关联文档:`docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md`。
|
||||
|
||||
## 2026-06-03 Public Work Presentation 收口
|
||||
|
||||
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
|
||||
|
||||
@@ -45,6 +45,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
||||
|
||||
平台入口创作生成通知、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)。
|
||||
|
||||
平台入口创作恢复 URL 私有 query、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-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)。
|
||||
|
||||
36
docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md
Normal file
36
docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# CreationUrlStateModel 收口计划
|
||||
|
||||
## 背景
|
||||
|
||||
`PlatformEntryFlowShellImpl.tsx` 曾直接承载多玩法创作恢复 URL 的拼装规则:`sessionId`、`profileId`、`draftId`、`workId` 的优先级、拼图草稿 runtime query、以及空值归一化散在壳层 Implementation 内。平台壳因此需要理解各玩法快照结构,新增玩法或修复刷新恢复时缺少稳定测试面。
|
||||
|
||||
## 决策
|
||||
|
||||
- 新增 `src/components/platform-entry/platformCreationUrlStateModel.ts` 作为 Creation URL State Module。
|
||||
- 该 Module 的 Interface 收口为各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、`normalizeCreationUrlValue`、`hasCreationUrlStateValue`、`hasPuzzleRuntimeUrlStateValue` 与 `buildPuzzleRuntimeUrlStateKey`。
|
||||
- 新增 `src/components/platform-entry/platformPuzzleIdentityModel.ts` 作为拼图稳定身份 Module,统一 `puzzle-session-*`、`puzzle-profile-*`、`puzzle-work-*` 的互推规则。
|
||||
- `PlatformEntryFlowShellImpl.tsx` 保留 React state、路由、登录门禁、网络请求和 URL 写入副作用 Adapter;不再在壳层内定义各玩法 URL 状态构造函数。
|
||||
|
||||
## Interface 约束
|
||||
|
||||
- 创作恢复私有 query 只使用 `sessionId`、`profileId`、`draftId`、`workId`;不得新增说明性 query 字段。
|
||||
- 空字符串、全空白字符串统一视为 `null`,避免刷新恢复时写入无效私有参数。
|
||||
- work-backed 玩法优先使用后端 work summary 的公开 `workId` / `profileId`;仅缺失时才回退 session draft。
|
||||
- 拼图 runtime query 独立使用 `mode`、`runtimeSessionId`、`runtimeProfileId`、`runtimeLevelId`、`publicWorkCode`,不与创作恢复 query 混写。
|
||||
- 拼图 draft runtime 若没有 `sourceSessionId`,只允许从 `puzzle-profile-*` 反推出 `puzzle-session-*`。
|
||||
|
||||
## Depth / Leverage / Locality
|
||||
|
||||
- **Depth**:调用方只传玩法快照或作品摘要,即可得到规范化 URL state;各玩法字段优先级藏在 Module Implementation 内。
|
||||
- **Leverage**:新增或调整玩法恢复规则时,优先补 Module Interface 测试,再接壳层 Adapter。
|
||||
- **Locality**:恢复 query、拼图 runtime query 和拼图稳定身份规则集中在两个小 Module,避免散落在页面壳、作品架和 runtime 打开逻辑中。
|
||||
|
||||
## 验收
|
||||
|
||||
- `npm run test -- src/components/platform-entry/platformCreationUrlStateModel.test.ts src/components/platform-entry/platformPuzzleIdentityModel.test.ts`
|
||||
- `npm run test -- src/services/creationUrlState.test.ts`
|
||||
- `npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`
|
||||
- `npx eslint src/components/platform-entry/platformCreationUrlStateModel.ts src/components/platform-entry/platformCreationUrlStateModel.test.ts src/components/platform-entry/platformPuzzleIdentityModel.ts src/components/platform-entry/platformPuzzleIdentityModel.test.ts --max-warnings 0`
|
||||
- `npx eslint src/components/platform-entry/PlatformEntryFlowShellImpl.tsx src/components/platform-entry/platformDraftGenerationShelfModel.ts --quiet`
|
||||
- `npm run typecheck`
|
||||
- `npm run check:encoding`
|
||||
@@ -158,7 +158,6 @@ import {
|
||||
} from '../../services/creationEntryConfigService';
|
||||
import {
|
||||
clearCreationUrlState,
|
||||
type CreationUrlState,
|
||||
isCreationRestorePath,
|
||||
readCreationUrlState,
|
||||
writeCreationUrlState,
|
||||
@@ -405,6 +404,23 @@ import {
|
||||
mergeBarkBattleWorkSummary,
|
||||
shouldPreserveLocalBarkBattleWorkOnRefresh,
|
||||
} from './barkBattleWorkCache';
|
||||
import {
|
||||
buildBabyObjectMatchCreationUrlState,
|
||||
buildBarkBattleCreationUrlState,
|
||||
buildBigFishCreationUrlState,
|
||||
buildJumpHopCreationUrlState,
|
||||
buildMatch3DCreationUrlState,
|
||||
buildPuzzleCreationUrlState,
|
||||
buildPuzzleDraftRuntimeUrlState,
|
||||
buildPuzzlePublishedRuntimeUrlState,
|
||||
buildPuzzleRuntimeUrlStateKey,
|
||||
buildSquareHoleCreationUrlState,
|
||||
buildVisualNovelCreationUrlState,
|
||||
buildWoodenFishCreationUrlState,
|
||||
hasCreationUrlStateValue,
|
||||
hasPuzzleRuntimeUrlStateValue,
|
||||
normalizeCreationUrlValue,
|
||||
} from './platformCreationUrlStateModel';
|
||||
import {
|
||||
buildCreationWorkShelfRuntimeState,
|
||||
buildDraftCompletionDialogSource,
|
||||
@@ -417,8 +433,6 @@ import {
|
||||
buildPendingSquareHoleWorks,
|
||||
buildPendingVisualNovelWorks,
|
||||
buildPendingWoodenFishWorks,
|
||||
buildPuzzleResultProfileId,
|
||||
buildPuzzleResultWorkId,
|
||||
collectDraftNoticeKeys,
|
||||
collectVisibleDraftNoticeKeys,
|
||||
createPendingDraftShelfState,
|
||||
@@ -491,6 +505,10 @@ import {
|
||||
mergePlatformPublicGalleryEntries,
|
||||
type RecommendRuntimeKind,
|
||||
} from './platformPublicGalleryFlow';
|
||||
import {
|
||||
buildPuzzleResultProfileId,
|
||||
buildPuzzleResultWorkId,
|
||||
} from './platformPuzzleIdentityModel';
|
||||
import {
|
||||
PlatformTaskCompletionDialog,
|
||||
type PlatformTaskCompletionDialogPayload,
|
||||
@@ -1362,131 +1380,6 @@ function buildAgentResultPublishGateView(
|
||||
};
|
||||
}
|
||||
|
||||
function buildPuzzleSessionIdFromProfileId(
|
||||
profileId: string | null | undefined,
|
||||
) {
|
||||
const normalizedProfileId = profileId?.trim();
|
||||
if (!normalizedProfileId?.startsWith('puzzle-profile-')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stableSuffix = normalizedProfileId.slice('puzzle-profile-'.length);
|
||||
return stableSuffix ? `puzzle-session-${stableSuffix}` : null;
|
||||
}
|
||||
|
||||
function normalizeCreationUrlValue(value: string | null | undefined) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function hasCreationUrlStateValue(state: CreationUrlState) {
|
||||
return Boolean(
|
||||
normalizeCreationUrlValue(state.sessionId) ||
|
||||
normalizeCreationUrlValue(state.profileId) ||
|
||||
normalizeCreationUrlValue(state.draftId) ||
|
||||
normalizeCreationUrlValue(state.workId),
|
||||
);
|
||||
}
|
||||
|
||||
function hasPuzzleRuntimeUrlStateValue(state: PuzzleRuntimeUrlState) {
|
||||
return Boolean(
|
||||
normalizeCreationUrlValue(state.runtimeSessionId) ||
|
||||
normalizeCreationUrlValue(state.runtimeProfileId) ||
|
||||
normalizeCreationUrlValue(state.runtimeLevelId) ||
|
||||
normalizeCreationUrlValue(state.publicWorkCode) ||
|
||||
normalizeCreationUrlValue(state.mode),
|
||||
);
|
||||
}
|
||||
|
||||
function buildPuzzleRuntimeUrlStateKey(state: PuzzleRuntimeUrlState) {
|
||||
return [
|
||||
normalizeCreationUrlValue(state.mode),
|
||||
normalizeCreationUrlValue(state.runtimeSessionId),
|
||||
normalizeCreationUrlValue(state.runtimeProfileId),
|
||||
normalizeCreationUrlValue(state.runtimeLevelId),
|
||||
normalizeCreationUrlValue(state.publicWorkCode),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function buildBigFishCreationUrlState(
|
||||
session: BigFishSessionSnapshotResponse | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
return {
|
||||
sessionId,
|
||||
workId: sessionId ? `big-fish-work-${sessionId}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMatch3DCreationUrlState(
|
||||
session: Match3DAgentSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
session?.draft?.profileId ?? session?.publishedProfileId,
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: profileId,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSquareHoleCreationUrlState(
|
||||
session: SquareHoleSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
session?.draft?.profileId ?? session?.publishedProfileId,
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: profileId,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPuzzleCreationUrlState(
|
||||
session: PuzzleAgentSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
session?.publishedProfileId ?? buildPuzzleResultProfileId(sessionId),
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: sessionId ? buildPuzzleResultWorkId(sessionId) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPuzzleDraftRuntimeUrlState(
|
||||
item: PuzzleWorkSummary,
|
||||
levelId?: string | null,
|
||||
): PuzzleRuntimeUrlState {
|
||||
const runtimeSessionId =
|
||||
normalizeCreationUrlValue(item.sourceSessionId) ??
|
||||
buildPuzzleSessionIdFromProfileId(item.profileId);
|
||||
|
||||
return {
|
||||
mode: 'draft',
|
||||
runtimeSessionId,
|
||||
runtimeProfileId: normalizeCreationUrlValue(item.profileId),
|
||||
runtimeLevelId: normalizeCreationUrlValue(levelId),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPuzzlePublishedRuntimeUrlState(
|
||||
item: PuzzleWorkSummary,
|
||||
levelId?: string | null,
|
||||
): PuzzleRuntimeUrlState {
|
||||
return {
|
||||
mode: 'published',
|
||||
runtimeProfileId: normalizeCreationUrlValue(item.profileId),
|
||||
runtimeLevelId: normalizeCreationUrlValue(levelId),
|
||||
publicWorkCode: buildPuzzlePublicWorkCode(item.profileId),
|
||||
};
|
||||
}
|
||||
|
||||
function openPuzzleRuntimeStage(
|
||||
setSelectionStage: (stage: SelectionStage) => void,
|
||||
state: PuzzleRuntimeUrlState,
|
||||
@@ -1531,33 +1424,6 @@ function buildPuzzleRuntimeWorkFromSession(
|
||||
};
|
||||
}
|
||||
|
||||
function buildVisualNovelCreationUrlState(
|
||||
session: VisualNovelAgentSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(session?.draft?.profileId);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: profileId ?? sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
function buildJumpHopCreationUrlState(params: {
|
||||
session?: JumpHopSessionSnapshotResponse | null;
|
||||
work?: JumpHopWorkProfileResponse | null;
|
||||
}): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(params.session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
params.work?.summary.profileId ?? params.session?.draft?.profileId,
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: normalizeCreationUrlValue(params.work?.summary.workId ?? profileId),
|
||||
};
|
||||
}
|
||||
|
||||
function buildJumpHopPendingSession(
|
||||
item: JumpHopWorkSummaryResponse,
|
||||
): JumpHopSessionSnapshotResponse {
|
||||
@@ -1591,23 +1457,6 @@ function buildJumpHopPendingSession(
|
||||
};
|
||||
}
|
||||
|
||||
function buildWoodenFishCreationUrlState(params: {
|
||||
session?: WoodenFishSessionSnapshotResponse | null;
|
||||
work?: WoodenFishWorkProfileResponse | null;
|
||||
}): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(params.session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
params.work?.summary.profileId ?? params.session?.draft?.profileId,
|
||||
);
|
||||
const draftId = profileId ?? sessionId;
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
draftId,
|
||||
workId: normalizeCreationUrlValue(params.work?.summary.workId ?? profileId),
|
||||
};
|
||||
}
|
||||
|
||||
function buildWoodenFishSessionFromWorkDetail(
|
||||
work: WoodenFishWorkProfileResponse,
|
||||
fallbackItem?: WoodenFishWorkSummaryResponse | null,
|
||||
@@ -1658,26 +1507,6 @@ function buildWoodenFishPendingSession(
|
||||
};
|
||||
}
|
||||
|
||||
function buildBarkBattleCreationUrlState(
|
||||
draft: BarkBattleDraftConfig | null,
|
||||
): CreationUrlState {
|
||||
return {
|
||||
draftId: normalizeCreationUrlValue(draft?.draftId),
|
||||
workId: normalizeCreationUrlValue(draft?.workId ?? draft?.draftId),
|
||||
};
|
||||
}
|
||||
|
||||
function buildBabyObjectMatchCreationUrlState(
|
||||
draft: BabyObjectMatchDraft | null,
|
||||
): CreationUrlState {
|
||||
const profileId = normalizeCreationUrlValue(draft?.profileId);
|
||||
return {
|
||||
profileId,
|
||||
draftId: normalizeCreationUrlValue(draft?.draftId),
|
||||
workId: profileId,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePlatformErrorMessage(message: string | null | undefined) {
|
||||
const normalized = message?.trim();
|
||||
return normalized ? normalized : null;
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { BarkBattleDraftConfig } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { SquareHoleSessionSnapshot } from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type { VisualNovelAgentSessionSnapshot } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type {
|
||||
JumpHopSessionSnapshotResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
} from '../../services/jump-hop/jumpHopClient';
|
||||
import type {
|
||||
WoodenFishSessionSnapshotResponse,
|
||||
WoodenFishWorkProfileResponse,
|
||||
} from '../../services/wooden-fish/woodenFishClient';
|
||||
import {
|
||||
buildBabyObjectMatchCreationUrlState,
|
||||
buildBarkBattleCreationUrlState,
|
||||
buildBigFishCreationUrlState,
|
||||
buildJumpHopCreationUrlState,
|
||||
buildMatch3DCreationUrlState,
|
||||
buildPuzzleCreationUrlState,
|
||||
buildPuzzleDraftRuntimeUrlState,
|
||||
buildPuzzlePublishedRuntimeUrlState,
|
||||
buildPuzzleRuntimeUrlStateKey,
|
||||
buildSquareHoleCreationUrlState,
|
||||
buildVisualNovelCreationUrlState,
|
||||
buildWoodenFishCreationUrlState,
|
||||
hasCreationUrlStateValue,
|
||||
hasPuzzleRuntimeUrlStateValue,
|
||||
normalizeCreationUrlValue,
|
||||
} from './platformCreationUrlStateModel';
|
||||
|
||||
describe('platformCreationUrlStateModel', () => {
|
||||
test('normalizes private creation url state values', () => {
|
||||
expect(normalizeCreationUrlValue(' session-1 ')).toBe('session-1');
|
||||
expect(normalizeCreationUrlValue(' ')).toBeNull();
|
||||
expect(
|
||||
hasCreationUrlStateValue({
|
||||
sessionId: ' ',
|
||||
profileId: null,
|
||||
draftId: undefined,
|
||||
workId: 'work-1',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(hasCreationUrlStateValue({})).toBe(false);
|
||||
});
|
||||
|
||||
test('builds creation restore state for core session based plays', () => {
|
||||
expect(
|
||||
buildBigFishCreationUrlState({
|
||||
sessionId: ' big-fish-session-1 ',
|
||||
} as BigFishSessionSnapshotResponse),
|
||||
).toEqual({
|
||||
sessionId: 'big-fish-session-1',
|
||||
workId: 'big-fish-work-big-fish-session-1',
|
||||
});
|
||||
|
||||
expect(
|
||||
buildMatch3DCreationUrlState({
|
||||
sessionId: 'match3d-session-1',
|
||||
draft: { profileId: 'match3d-profile-draft' },
|
||||
} as Match3DAgentSessionSnapshot),
|
||||
).toEqual({
|
||||
sessionId: 'match3d-session-1',
|
||||
profileId: 'match3d-profile-draft',
|
||||
workId: 'match3d-profile-draft',
|
||||
});
|
||||
|
||||
expect(
|
||||
buildSquareHoleCreationUrlState({
|
||||
sessionId: 'square-session-1',
|
||||
publishedProfileId: 'square-profile-published',
|
||||
} as SquareHoleSessionSnapshot),
|
||||
).toEqual({
|
||||
sessionId: 'square-session-1',
|
||||
profileId: 'square-profile-published',
|
||||
workId: 'square-profile-published',
|
||||
});
|
||||
|
||||
expect(
|
||||
buildVisualNovelCreationUrlState({
|
||||
sessionId: 'visual-session-1',
|
||||
draft: { profileId: 'visual-profile-1' },
|
||||
} as VisualNovelAgentSessionSnapshot),
|
||||
).toEqual({
|
||||
sessionId: 'visual-session-1',
|
||||
profileId: 'visual-profile-1',
|
||||
workId: 'visual-profile-1',
|
||||
});
|
||||
});
|
||||
|
||||
test('builds puzzle creation and runtime query state', () => {
|
||||
expect(
|
||||
buildPuzzleCreationUrlState({
|
||||
sessionId: 'puzzle-session-ocean',
|
||||
} as PuzzleAgentSessionSnapshot),
|
||||
).toEqual({
|
||||
sessionId: 'puzzle-session-ocean',
|
||||
profileId: 'puzzle-profile-ocean',
|
||||
workId: 'puzzle-work-ocean',
|
||||
});
|
||||
|
||||
const draftRuntime = buildPuzzleDraftRuntimeUrlState(
|
||||
buildPuzzleWork({
|
||||
profileId: 'puzzle-profile-ocean',
|
||||
sourceSessionId: null,
|
||||
}),
|
||||
'level-2',
|
||||
);
|
||||
expect(draftRuntime).toEqual({
|
||||
mode: 'draft',
|
||||
runtimeSessionId: 'puzzle-session-ocean',
|
||||
runtimeProfileId: 'puzzle-profile-ocean',
|
||||
runtimeLevelId: 'level-2',
|
||||
});
|
||||
expect(hasPuzzleRuntimeUrlStateValue(draftRuntime)).toBe(true);
|
||||
expect(buildPuzzleRuntimeUrlStateKey(draftRuntime)).toBe(
|
||||
'draft|puzzle-session-ocean|puzzle-profile-ocean|level-2|',
|
||||
);
|
||||
|
||||
const publishedRuntime = buildPuzzlePublishedRuntimeUrlState(
|
||||
buildPuzzleWork({ profileId: 'puzzle-profile-ocean' }),
|
||||
);
|
||||
expect(publishedRuntime.mode).toBe('published');
|
||||
expect(publishedRuntime.runtimeProfileId).toBe('puzzle-profile-ocean');
|
||||
expect(publishedRuntime.publicWorkCode).toMatch(/^PZ-/u);
|
||||
});
|
||||
|
||||
test('builds creation state for work backed plays with work id priority', () => {
|
||||
expect(
|
||||
buildJumpHopCreationUrlState({
|
||||
session: {
|
||||
sessionId: 'jump-session-1',
|
||||
draft: { profileId: 'jump-profile-draft' },
|
||||
} as JumpHopSessionSnapshotResponse,
|
||||
work: {
|
||||
summary: {
|
||||
profileId: 'jump-profile-work',
|
||||
workId: 'jump-work-1',
|
||||
},
|
||||
} as JumpHopWorkProfileResponse,
|
||||
}),
|
||||
).toEqual({
|
||||
sessionId: 'jump-session-1',
|
||||
profileId: 'jump-profile-work',
|
||||
workId: 'jump-work-1',
|
||||
});
|
||||
|
||||
expect(
|
||||
buildWoodenFishCreationUrlState({
|
||||
session: {
|
||||
sessionId: 'wood-session-1',
|
||||
draft: { profileId: 'wood-profile-draft' },
|
||||
} as WoodenFishSessionSnapshotResponse,
|
||||
work: {
|
||||
summary: {
|
||||
profileId: 'wood-profile-work',
|
||||
workId: 'wood-work-1',
|
||||
},
|
||||
} as WoodenFishWorkProfileResponse,
|
||||
}),
|
||||
).toEqual({
|
||||
sessionId: 'wood-session-1',
|
||||
profileId: 'wood-profile-work',
|
||||
draftId: 'wood-profile-work',
|
||||
workId: 'wood-work-1',
|
||||
});
|
||||
});
|
||||
|
||||
test('builds creation state for draft backed local plays', () => {
|
||||
expect(
|
||||
buildBarkBattleCreationUrlState({
|
||||
draftId: 'bark-draft-1',
|
||||
workId: 'bark-work-1',
|
||||
} as BarkBattleDraftConfig),
|
||||
).toEqual({
|
||||
draftId: 'bark-draft-1',
|
||||
workId: 'bark-work-1',
|
||||
});
|
||||
|
||||
expect(
|
||||
buildBabyObjectMatchCreationUrlState({
|
||||
draftId: 'baby-draft-1',
|
||||
profileId: 'baby-profile-1',
|
||||
} as BabyObjectMatchDraft),
|
||||
).toEqual({
|
||||
profileId: 'baby-profile-1',
|
||||
draftId: 'baby-draft-1',
|
||||
workId: 'baby-profile-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
202
src/components/platform-entry/platformCreationUrlStateModel.ts
Normal file
202
src/components/platform-entry/platformCreationUrlStateModel.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { BarkBattleDraftConfig } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { SquareHoleSessionSnapshot } from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type { VisualNovelAgentSessionSnapshot } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { CreationUrlState } from '../../services/creationUrlState';
|
||||
import type {
|
||||
JumpHopSessionSnapshotResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
} from '../../services/jump-hop/jumpHopClient';
|
||||
import { buildPuzzlePublicWorkCode } from '../../services/publicWorkCode';
|
||||
import type { PuzzleRuntimeUrlState } from '../../services/puzzleRuntimeUrlState';
|
||||
import type {
|
||||
WoodenFishSessionSnapshotResponse,
|
||||
WoodenFishWorkProfileResponse,
|
||||
} from '../../services/wooden-fish/woodenFishClient';
|
||||
import {
|
||||
buildPuzzleResultProfileId,
|
||||
buildPuzzleResultWorkId,
|
||||
buildPuzzleSessionIdFromProfileId,
|
||||
} from './platformPuzzleIdentityModel';
|
||||
|
||||
/** 平台创作恢复 URL 私有 query 的纯模型,调用方只需传入玩法快照。 */
|
||||
export function normalizeCreationUrlValue(value: string | null | undefined) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
export function hasCreationUrlStateValue(state: CreationUrlState) {
|
||||
return Boolean(
|
||||
normalizeCreationUrlValue(state.sessionId) ||
|
||||
normalizeCreationUrlValue(state.profileId) ||
|
||||
normalizeCreationUrlValue(state.draftId) ||
|
||||
normalizeCreationUrlValue(state.workId),
|
||||
);
|
||||
}
|
||||
|
||||
export function hasPuzzleRuntimeUrlStateValue(state: PuzzleRuntimeUrlState) {
|
||||
return Boolean(
|
||||
normalizeCreationUrlValue(state.runtimeSessionId) ||
|
||||
normalizeCreationUrlValue(state.runtimeProfileId) ||
|
||||
normalizeCreationUrlValue(state.runtimeLevelId) ||
|
||||
normalizeCreationUrlValue(state.publicWorkCode) ||
|
||||
normalizeCreationUrlValue(state.mode),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildPuzzleRuntimeUrlStateKey(state: PuzzleRuntimeUrlState) {
|
||||
return [
|
||||
normalizeCreationUrlValue(state.mode),
|
||||
normalizeCreationUrlValue(state.runtimeSessionId),
|
||||
normalizeCreationUrlValue(state.runtimeProfileId),
|
||||
normalizeCreationUrlValue(state.runtimeLevelId),
|
||||
normalizeCreationUrlValue(state.publicWorkCode),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
export function buildBigFishCreationUrlState(
|
||||
session: BigFishSessionSnapshotResponse | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
return {
|
||||
sessionId,
|
||||
workId: sessionId ? `big-fish-work-${sessionId}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMatch3DCreationUrlState(
|
||||
session: Match3DAgentSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
session?.draft?.profileId ?? session?.publishedProfileId,
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: profileId,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSquareHoleCreationUrlState(
|
||||
session: SquareHoleSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
session?.draft?.profileId ?? session?.publishedProfileId,
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: profileId,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPuzzleCreationUrlState(
|
||||
session: PuzzleAgentSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
session?.publishedProfileId ?? buildPuzzleResultProfileId(sessionId),
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: sessionId ? buildPuzzleResultWorkId(sessionId) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPuzzleDraftRuntimeUrlState(
|
||||
item: PuzzleWorkSummary,
|
||||
levelId?: string | null,
|
||||
): PuzzleRuntimeUrlState {
|
||||
const runtimeSessionId =
|
||||
normalizeCreationUrlValue(item.sourceSessionId) ??
|
||||
buildPuzzleSessionIdFromProfileId(item.profileId);
|
||||
|
||||
return {
|
||||
mode: 'draft',
|
||||
runtimeSessionId,
|
||||
runtimeProfileId: normalizeCreationUrlValue(item.profileId),
|
||||
runtimeLevelId: normalizeCreationUrlValue(levelId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPuzzlePublishedRuntimeUrlState(
|
||||
item: PuzzleWorkSummary,
|
||||
levelId?: string | null,
|
||||
): PuzzleRuntimeUrlState {
|
||||
return {
|
||||
mode: 'published',
|
||||
runtimeProfileId: normalizeCreationUrlValue(item.profileId),
|
||||
runtimeLevelId: normalizeCreationUrlValue(levelId),
|
||||
publicWorkCode: buildPuzzlePublicWorkCode(item.profileId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildVisualNovelCreationUrlState(
|
||||
session: VisualNovelAgentSessionSnapshot | null,
|
||||
): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(session?.draft?.profileId);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: profileId ?? sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildJumpHopCreationUrlState(params: {
|
||||
session?: JumpHopSessionSnapshotResponse | null;
|
||||
work?: JumpHopWorkProfileResponse | null;
|
||||
}): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(params.session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
params.work?.summary.profileId ?? params.session?.draft?.profileId,
|
||||
);
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
workId: normalizeCreationUrlValue(params.work?.summary.workId ?? profileId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWoodenFishCreationUrlState(params: {
|
||||
session?: WoodenFishSessionSnapshotResponse | null;
|
||||
work?: WoodenFishWorkProfileResponse | null;
|
||||
}): CreationUrlState {
|
||||
const sessionId = normalizeCreationUrlValue(params.session?.sessionId);
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
params.work?.summary.profileId ?? params.session?.draft?.profileId,
|
||||
);
|
||||
const draftId = profileId ?? sessionId;
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
draftId,
|
||||
workId: normalizeCreationUrlValue(params.work?.summary.workId ?? profileId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBarkBattleCreationUrlState(
|
||||
draft: BarkBattleDraftConfig | null,
|
||||
): CreationUrlState {
|
||||
return {
|
||||
draftId: normalizeCreationUrlValue(draft?.draftId),
|
||||
workId: normalizeCreationUrlValue(draft?.workId ?? draft?.draftId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBabyObjectMatchCreationUrlState(
|
||||
draft: BabyObjectMatchDraft | null,
|
||||
): CreationUrlState {
|
||||
const profileId = normalizeCreationUrlValue(draft?.profileId);
|
||||
return {
|
||||
profileId,
|
||||
draftId: normalizeCreationUrlValue(draft?.draftId),
|
||||
workId: profileId,
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,15 @@ import {
|
||||
type CreationWorkShelfRuntimeState,
|
||||
resolvePuzzleWorkCoverImageSrc,
|
||||
} from '../custom-world-home/creationWorkShelf';
|
||||
import {
|
||||
buildPuzzleResultProfileId,
|
||||
buildPuzzleResultWorkId,
|
||||
} from './platformPuzzleIdentityModel';
|
||||
|
||||
export {
|
||||
buildPuzzleResultProfileId,
|
||||
buildPuzzleResultWorkId,
|
||||
} from './platformPuzzleIdentityModel';
|
||||
|
||||
export type DraftGenerationNoticeStatus = 'generating' | 'ready' | 'failed';
|
||||
|
||||
@@ -58,20 +67,6 @@ export type PlatformDraftGenerationVisibleShelfSources = {
|
||||
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,
|
||||
@@ -825,18 +820,6 @@ export function buildPendingBarkBattleWorks(
|
||||
}));
|
||||
}
|
||||
|
||||
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>,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
buildPuzzleResultProfileId,
|
||||
buildPuzzleResultWorkId,
|
||||
buildPuzzleSessionIdFromProfileId,
|
||||
} from './platformPuzzleIdentityModel';
|
||||
|
||||
describe('platformPuzzleIdentityModel', () => {
|
||||
test('builds stable puzzle result identities from a session id', () => {
|
||||
expect(buildPuzzleResultProfileId(' puzzle-session-ocean ')).toBe(
|
||||
'puzzle-profile-ocean',
|
||||
);
|
||||
expect(buildPuzzleResultWorkId('puzzle-session-ocean')).toBe(
|
||||
'puzzle-work-ocean',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps legacy suffix inputs usable', () => {
|
||||
expect(buildPuzzleResultProfileId('ocean')).toBe('puzzle-profile-ocean');
|
||||
expect(buildPuzzleResultWorkId('ocean')).toBe('puzzle-work-ocean');
|
||||
});
|
||||
|
||||
test('builds draft runtime session ids from profile ids', () => {
|
||||
expect(buildPuzzleSessionIdFromProfileId(' puzzle-profile-ocean ')).toBe(
|
||||
'puzzle-session-ocean',
|
||||
);
|
||||
expect(buildPuzzleSessionIdFromProfileId('puzzle-work-ocean')).toBeNull();
|
||||
expect(buildPuzzleSessionIdFromProfileId('puzzle-profile-')).toBeNull();
|
||||
});
|
||||
});
|
||||
36
src/components/platform-entry/platformPuzzleIdentityModel.ts
Normal file
36
src/components/platform-entry/platformPuzzleIdentityModel.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/** 收口拼图草稿在 session/profile/work 之间的稳定身份互推规则。 */
|
||||
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 buildPuzzleSessionIdFromProfileId(
|
||||
profileId: string | null | undefined,
|
||||
) {
|
||||
const normalizedProfileId = profileId?.trim();
|
||||
if (!normalizedProfileId?.startsWith('puzzle-profile-')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stableSuffix = normalizedProfileId.slice('puzzle-profile-'.length);
|
||||
return stableSuffix ? `puzzle-session-${stableSuffix}` : 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;
|
||||
}
|
||||
Reference in New Issue
Block a user