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`。
|
- 验证方式:`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`。
|
- 关联文档:`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 收口
|
## 2026-06-03 Public Work Presentation 收口
|
||||||
|
|
||||||
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
|
- 背景:作品卡、推荐 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)。
|
平台入口创作生成通知、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 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)。
|
||||||
|
|||||||
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';
|
} from '../../services/creationEntryConfigService';
|
||||||
import {
|
import {
|
||||||
clearCreationUrlState,
|
clearCreationUrlState,
|
||||||
type CreationUrlState,
|
|
||||||
isCreationRestorePath,
|
isCreationRestorePath,
|
||||||
readCreationUrlState,
|
readCreationUrlState,
|
||||||
writeCreationUrlState,
|
writeCreationUrlState,
|
||||||
@@ -405,6 +404,23 @@ import {
|
|||||||
mergeBarkBattleWorkSummary,
|
mergeBarkBattleWorkSummary,
|
||||||
shouldPreserveLocalBarkBattleWorkOnRefresh,
|
shouldPreserveLocalBarkBattleWorkOnRefresh,
|
||||||
} from './barkBattleWorkCache';
|
} from './barkBattleWorkCache';
|
||||||
|
import {
|
||||||
|
buildBabyObjectMatchCreationUrlState,
|
||||||
|
buildBarkBattleCreationUrlState,
|
||||||
|
buildBigFishCreationUrlState,
|
||||||
|
buildJumpHopCreationUrlState,
|
||||||
|
buildMatch3DCreationUrlState,
|
||||||
|
buildPuzzleCreationUrlState,
|
||||||
|
buildPuzzleDraftRuntimeUrlState,
|
||||||
|
buildPuzzlePublishedRuntimeUrlState,
|
||||||
|
buildPuzzleRuntimeUrlStateKey,
|
||||||
|
buildSquareHoleCreationUrlState,
|
||||||
|
buildVisualNovelCreationUrlState,
|
||||||
|
buildWoodenFishCreationUrlState,
|
||||||
|
hasCreationUrlStateValue,
|
||||||
|
hasPuzzleRuntimeUrlStateValue,
|
||||||
|
normalizeCreationUrlValue,
|
||||||
|
} from './platformCreationUrlStateModel';
|
||||||
import {
|
import {
|
||||||
buildCreationWorkShelfRuntimeState,
|
buildCreationWorkShelfRuntimeState,
|
||||||
buildDraftCompletionDialogSource,
|
buildDraftCompletionDialogSource,
|
||||||
@@ -417,8 +433,6 @@ import {
|
|||||||
buildPendingSquareHoleWorks,
|
buildPendingSquareHoleWorks,
|
||||||
buildPendingVisualNovelWorks,
|
buildPendingVisualNovelWorks,
|
||||||
buildPendingWoodenFishWorks,
|
buildPendingWoodenFishWorks,
|
||||||
buildPuzzleResultProfileId,
|
|
||||||
buildPuzzleResultWorkId,
|
|
||||||
collectDraftNoticeKeys,
|
collectDraftNoticeKeys,
|
||||||
collectVisibleDraftNoticeKeys,
|
collectVisibleDraftNoticeKeys,
|
||||||
createPendingDraftShelfState,
|
createPendingDraftShelfState,
|
||||||
@@ -491,6 +505,10 @@ import {
|
|||||||
mergePlatformPublicGalleryEntries,
|
mergePlatformPublicGalleryEntries,
|
||||||
type RecommendRuntimeKind,
|
type RecommendRuntimeKind,
|
||||||
} from './platformPublicGalleryFlow';
|
} from './platformPublicGalleryFlow';
|
||||||
|
import {
|
||||||
|
buildPuzzleResultProfileId,
|
||||||
|
buildPuzzleResultWorkId,
|
||||||
|
} from './platformPuzzleIdentityModel';
|
||||||
import {
|
import {
|
||||||
PlatformTaskCompletionDialog,
|
PlatformTaskCompletionDialog,
|
||||||
type PlatformTaskCompletionDialogPayload,
|
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(
|
function openPuzzleRuntimeStage(
|
||||||
setSelectionStage: (stage: SelectionStage) => void,
|
setSelectionStage: (stage: SelectionStage) => void,
|
||||||
state: PuzzleRuntimeUrlState,
|
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(
|
function buildJumpHopPendingSession(
|
||||||
item: JumpHopWorkSummaryResponse,
|
item: JumpHopWorkSummaryResponse,
|
||||||
): JumpHopSessionSnapshotResponse {
|
): 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(
|
function buildWoodenFishSessionFromWorkDetail(
|
||||||
work: WoodenFishWorkProfileResponse,
|
work: WoodenFishWorkProfileResponse,
|
||||||
fallbackItem?: WoodenFishWorkSummaryResponse | null,
|
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) {
|
function normalizePlatformErrorMessage(message: string | null | undefined) {
|
||||||
const normalized = message?.trim();
|
const normalized = message?.trim();
|
||||||
return normalized ? normalized : null;
|
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,
|
type CreationWorkShelfRuntimeState,
|
||||||
resolvePuzzleWorkCoverImageSrc,
|
resolvePuzzleWorkCoverImageSrc,
|
||||||
} from '../custom-world-home/creationWorkShelf';
|
} from '../custom-world-home/creationWorkShelf';
|
||||||
|
import {
|
||||||
|
buildPuzzleResultProfileId,
|
||||||
|
buildPuzzleResultWorkId,
|
||||||
|
} from './platformPuzzleIdentityModel';
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildPuzzleResultProfileId,
|
||||||
|
buildPuzzleResultWorkId,
|
||||||
|
} from './platformPuzzleIdentityModel';
|
||||||
|
|
||||||
export type DraftGenerationNoticeStatus = 'generating' | 'ready' | 'failed';
|
export type DraftGenerationNoticeStatus = 'generating' | 'ready' | 'failed';
|
||||||
|
|
||||||
@@ -58,20 +67,6 @@ export type PlatformDraftGenerationVisibleShelfSources = {
|
|||||||
babyObjectMatchItems: readonly BabyObjectMatchDraft[];
|
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(
|
export function buildDraftNoticeKey(
|
||||||
kind: CreationWorkShelfKind,
|
kind: CreationWorkShelfKind,
|
||||||
id: string,
|
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(
|
function pickDraftCompletionDialogSourceId(
|
||||||
ids: Array<string | null | undefined>,
|
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