diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 028c7a07..24fa12c2 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1340,6 +1340,7 @@ - 背景:平台壳内散落各玩法创作恢复 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。 - 追加决策:初始创作 URL 恢复的已处理、非创作路径、无私有 query、平台配置加载中、受保护数据暂不可读与可恢复判定也收口到 `resolveInitialCreationUrlRestoreDecision`;壳层只按 `skip`、`mark-handled`、`wait`、`restore` 执行 ref 标记或进入原恢复副作用。 +- 追加决策:创作直达恢复目标解析收口到 `resolveCreationUrlRestoreTarget(pathname, state)`;Module 统一识别 big-fish、match3d、square-hole、puzzle、visual-novel、bark-battle、baby-object-match、jump-hop、wooden-fish 的 path、私有 query 归一化、生成路径标记和 big-fish workId 到 sessionId 兜底。壳层仍执行作品列表读取、草稿恢复、错误处理、stage 切换和 URL 写回;`/creation/rpg` 继续保持无具体恢复目标,后续要接入需先补规则与测试。 - 影响范围:创作流程刷新恢复、拼图草稿 / 发布 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`。 diff --git a/docs/README.md b/docs/README.md index 275a97b6..53de69ab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,7 +49,7 @@ 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)。 +平台入口创作恢复 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)。 平台入口错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key 与任务完成文案收口到 `src/components/platform-entry/platformDialogStateModel.ts`,规则见 [【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md](./technical/【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md b/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md index d50cd8e5..340f4220 100644 --- a/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md +++ b/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md @@ -7,7 +7,7 @@ ## 决策 - 新增 `src/components/platform-entry/platformCreationUrlStateModel.ts` 作为 Creation URL State Module。 -- 该 Module 的 Interface 收口为各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、`normalizeCreationUrlValue`、`hasCreationUrlStateValue`、`hasPuzzleRuntimeUrlStateValue`、`buildPuzzleRuntimeUrlStateKey` 与初始创作 URL 恢复判定 `resolveInitialCreationUrlRestoreDecision`。 +- 该 Module 的 Interface 收口为各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、`normalizeCreationUrlValue`、`hasCreationUrlStateValue`、`hasPuzzleRuntimeUrlStateValue`、`buildPuzzleRuntimeUrlStateKey`、初始创作 URL 恢复判定 `resolveInitialCreationUrlRestoreDecision`,以及创作直达恢复目标解析 `resolveCreationUrlRestoreTarget`。 - 新增 `src/components/platform-entry/platformPuzzleIdentityModel.ts` 作为拼图稳定身份 Module,统一 `puzzle-session-*`、`puzzle-profile-*`、`puzzle-work-*` 的互推规则。 - `PlatformEntryFlowShellImpl.tsx` 保留 React state、路由、登录门禁、网络请求和 URL 写入副作用 Adapter;不再在壳层内定义各玩法 URL 状态构造函数,也不直接内联初始恢复的已处理 / 等待 / 可恢复判定。 @@ -19,11 +19,13 @@ - 拼图 runtime query 独立使用 `mode`、`runtimeSessionId`、`runtimeProfileId`、`runtimeLevelId`、`publicWorkCode`,不与创作恢复 query 混写。 - 拼图 draft runtime 若没有 `sourceSessionId`,只允许从 `puzzle-profile-*` 反推出 `puzzle-session-*`。 - 初始创作 URL 恢复只在未处理、当前路径属于创作恢复路径、私有 query 有值、平台配置加载完成且受保护数据可读时执行;非创作路径或无私有 query 时标记已处理,加载中或暂不可读时等待。 +- 创作直达恢复目标由 `resolveCreationUrlRestoreTarget(pathname, state)` 统一识别;它只返回玩法 kind、归一化后的四个私有 query、生成路径标记和大鱼吃小鱼 session 兜底,不执行网络请求、草稿打开、stage 切换或 URL 写回。 +- `/creation/rpg` 当前仍不归入具体恢复目标;若后续要恢复 RPG 直达,需要先补明确恢复规则和测试,不得让壳层重新内联路径判定。 ## Depth / Leverage / Locality - **Depth**:调用方只传玩法快照或作品摘要,即可得到规范化 URL state;各玩法字段优先级藏在 Module Implementation 内。 -- **Leverage**:新增或调整玩法恢复规则、恢复等待条件时,优先补 Module Interface 测试,再接壳层 Adapter。 +- **Leverage**:新增或调整玩法恢复规则、恢复目标或恢复等待条件时,优先补 Module Interface 测试,再接壳层 Adapter。 - **Locality**:恢复 query、拼图 runtime query 和拼图稳定身份规则集中在两个小 Module,避免散落在页面壳、作品架和 runtime 打开逻辑中。 ## 验收 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 323d3d2e..97d37305 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -8,7 +8,7 @@ 当前点击底部加号进入的创作入口页承载后台公告位、创作入口页签和两列模板卡;页签中只有真实后端作品架摘要存在时才展示“最近创作”,其余为玩法模板分类。点击模板卡后直接进入对应玩法已有的入口创作表单 stage,不再经过空白占位页,也不把旧表单嵌进创作入口页。移动端创作入口页顶栏在 `陶泥儿` 品牌同一行显示真实账户泥点数,数据来自 `profileDashboard.walletBalance`,不得再把公告内容或活动奖池当作账号余额展示。创作入口页公告位数据优先读取 `GET /api/creation-entry/config` 的 `eventBanners` 数组,多条配置时前端自动轮播,旧 `eventBanner` 仅作为单条兼容兜底。后台公告配置面向表单:每条公告包含标题和 HTML 内容,后台保存时序列化为后端 `eventBannersJson` 传输字段,由前端空权限沙箱 iframe 展示;旧结构化 banner 字段仅保留回显兼容,不再作为后台公告配置主格式;不得执行 JSX 或把后台代码直接注入 DOM。玩法列表不再套外部边框卡片,移动端需要压缩横向边距和两列间距;玩法卡统一按“上图、左上状态标签(仅非开放态显示)、封面右下 `10-20泥点数`、下方白底标题/描述”结构展示,卡片高度保持紧凑但标题、描述和预估消耗点数都必须可见。创作入口页根容器不再使用 `platform-page-stage` 这类全局内容卡片壳,但继续保留 `platform-remap-surface` 作为主题和输入框样式命中钩子。创作入口页字号需要对齐平台普通 UI 档位:顶栏泥点组件、公告正文、分类 Tab 和玩法卡标题 / 副标题 / 消耗说明优先使用 `11px` 到 `14px`,不使用 `text-lg`、`text-xl` 或更大的展示级字号。草稿 Tab 继续承接作品架;底部加号入口页的“最近创作”只用 7 天内的真实后端作品架摘要判断是否展示,并从摘要里推导最近使用过的模板 ID,页面必须展示“仅显示最近7天内使用过的模板”提示,列表内容必须复用其它页签里的模板卡样式、文案和点击行为,不展示具体作品名称、摘要或生成状态,也不新增独立最近创作卡组件。RPG、RPG 之外的各玩法入口分别落到既有的 `agent-workspace`、`big-fish-agent-workspace`、`match3d-agent-workspace`、`square-hole-agent-workspace`、`jump-hop-workspace`、`wooden-fish-workspace`、`puzzle-agent-workspace`、`bark-battle-workspace`、`visual-novel-agent-workspace`、`baby-object-match-workspace`,这些入口继续承接各玩法自己的表单、草稿恢复和后续编排,不作为创作入口页内容。 -创作恢复参数只保留 `sessionId`、`profileId`、`draftId`、`workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留;切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。生成页等待时间统一以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 仍只用于排序与摘要展示,不作为前端自行推导业务状态的真相。 +创作恢复参数只保留 `sessionId`、`profileId`、`draftId`、`workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留;切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。平台入口刷新直达时,路径到玩法恢复目标、四个 query 归一化、生成页标记和大鱼吃小鱼 workId 兜底统一由 `platformCreationUrlStateModel.ts` 解析,壳层只执行读取作品、恢复草稿和切换阶段等副作用。生成页等待时间统一以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 仍只用于排序与摘要展示,不作为前端自行推导业务状态的真相。 统一创作入口覆盖当前可进入创作链路的已有模板:`rpg`、`big-fish`、`puzzle`、`match3d`、`jump-hop`、`wooden-fish`、`square-hole`、`bark-battle`、`visual-novel`、`baby-object-match` 和 `creative-agent`;`airp` 仍是未开放占位,不作为当前统一创作链路目标。拼图、抓大鹅、跳一跳和敲木鱼在前端继续经过 `UnifiedCreationWorkspace` 和 `UnifiedGenerationPage`:`UnifiedCreationWorkspace` 作为平台壳依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace`、`Match3DCreationWorkspace`、`JumpHopCreationWorkspace` 和 `WoodenFishCreationWorkspace`。其它已有模板由平台壳用 `UnifiedCreationPage` 包住既有工作台,复用统一标题栏、返回入口、页面级纵向滚动和隐藏字段契约,同时保留各玩法自己的表单、草稿恢复和后续编排。创作页字段清单由后端在 `GET /api/creation-entry/config` 的 `creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec;字段类型只保留 `text`、`select`、`image`、`audio`。`UnifiedCreationPage` 不在 UI 中额外展示字段说明 chip,也不在右上角显示内部 `playId`、模板 ID 或工作台阶段名;竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交,但返回按钮只保留在统一页头,工作台内部不再重复渲染。暗色创作进度卡片位于 `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap,确保白字、浅色边框和进度条底色不会被全局规则改成深色;不要只依赖通用 `text-white*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 8872eea5..0d0f5b4c 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -409,6 +409,7 @@ import { buildWoodenFishCreationUrlState, hasPuzzleRuntimeUrlStateValue, normalizeCreationUrlValue, + resolveCreationUrlRestoreTarget, resolveInitialCreationUrlRestoreDecision, } from './platformCreationUrlStateModel'; import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow'; @@ -12142,21 +12143,17 @@ export function PlatformEntryFlowShellImpl({ handledInitialCreationUrlStateRef.current = true; const restoreCreationUrlState = async () => { - const path = window.location.pathname; - const sessionId = normalizeCreationUrlValue( - initialCreationUrlState.sessionId, + const target = resolveCreationUrlRestoreTarget( + window.location.pathname, + initialCreationUrlState, ); - const profileId = normalizeCreationUrlValue( - initialCreationUrlState.profileId, - ); - const draftId = normalizeCreationUrlValue( - initialCreationUrlState.draftId, - ); - const workId = normalizeCreationUrlValue(initialCreationUrlState.workId); + if (!target) { + return; + } + const { sessionId, profileId, draftId, workId } = target; - if (path.startsWith('/creation/big-fish')) { - const targetSessionId = - sessionId ?? workId?.replace(/^big-fish-work-/u, ''); + if (target.kind === 'big-fish') { + const targetSessionId = target.bigFishSessionId; if (targetSessionId) { const matchedWork = (bigFishWorks.length > 0 @@ -12176,7 +12173,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/match3d')) { + if (target.kind === 'match3d') { const matchedWork = (match3dWorks.length > 0 ? match3dWorks @@ -12199,7 +12196,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/square-hole')) { + if (target.kind === 'square-hole') { const matchedWork = (squareHoleWorks.length > 0 ? squareHoleWorks @@ -12220,7 +12217,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/puzzle')) { + if (target.kind === 'puzzle') { const matchedWork = (puzzleWorks.length > 0 ? puzzleWorks @@ -12241,7 +12238,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/visual-novel')) { + if (target.kind === 'visual-novel') { const matchedWork = (visualNovelWorks.length > 0 ? visualNovelWorks @@ -12257,7 +12254,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/bark-battle')) { + if (target.kind === 'bark-battle') { const matchedWork = (barkBattleWorks.length > 0 ? barkBattleWorks @@ -12271,7 +12268,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/baby-object-match')) { + if (target.kind === 'baby-object-match') { const matchedDraft = (babyObjectMatchDrafts.length > 0 ? babyObjectMatchDrafts @@ -12288,7 +12285,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/jump-hop')) { + if (target.kind === 'jump-hop') { let session: JumpHopSessionSnapshotResponse | null = null; let work: JumpHopWorkProfileResponse | null = null; try { @@ -12316,7 +12313,7 @@ export function PlatformEntryFlowShellImpl({ ); enterCreateTab(); setSelectionStage( - path.includes('/generating') + target.isGeneratingPath ? 'jump-hop-generating' : session?.draft || work ? 'jump-hop-result' @@ -12330,7 +12327,7 @@ export function PlatformEntryFlowShellImpl({ return; } - if (path.startsWith('/creation/wooden-fish')) { + if (target.kind === 'wooden-fish') { if (!sessionId) { return; } @@ -12347,7 +12344,7 @@ export function PlatformEntryFlowShellImpl({ ); enterCreateTab(); setSelectionStage( - path.includes('/generating') + target.isGeneratingPath ? 'wooden-fish-generating' : session.draft ? 'wooden-fish-result' diff --git a/src/components/platform-entry/platformCreationUrlStateModel.test.ts b/src/components/platform-entry/platformCreationUrlStateModel.test.ts index 90ca06cc..b7f7cbf4 100644 --- a/src/components/platform-entry/platformCreationUrlStateModel.test.ts +++ b/src/components/platform-entry/platformCreationUrlStateModel.test.ts @@ -32,6 +32,7 @@ import { hasCreationUrlStateValue, hasPuzzleRuntimeUrlStateValue, normalizeCreationUrlValue, + resolveCreationUrlRestoreTarget, resolveInitialCreationUrlRestoreDecision, } from './platformCreationUrlStateModel'; @@ -94,6 +95,104 @@ describe('platformCreationUrlStateModel', () => { }); }); + test('resolves supported creation url restore targets from paths', () => { + const state = { + sessionId: ' session-1 ', + profileId: ' profile-1 ', + draftId: ' draft-1 ', + workId: ' work-1 ', + }; + const cases = [ + ['/creation/big-fish/result', 'big-fish'], + ['/creation/match3d/result', 'match3d'], + ['/creation/square-hole/result', 'square-hole'], + ['/creation/puzzle/result', 'puzzle'], + ['/creation/visual-novel/result', 'visual-novel'], + ['/creation/bark-battle/result', 'bark-battle'], + ['/creation/baby-object-match/result', 'baby-object-match'], + ['/creation/jump-hop/result', 'jump-hop'], + ['/creation/wooden-fish/result', 'wooden-fish'], + ] as const; + + cases.forEach(([pathname, kind]) => { + expect(resolveCreationUrlRestoreTarget(pathname, state)).toMatchObject({ + kind, + sessionId: 'session-1', + profileId: 'profile-1', + draftId: 'draft-1', + workId: 'work-1', + isGeneratingPath: false, + }); + }); + }); + + test('normalizes creation url restore target values and generating paths', () => { + expect( + resolveCreationUrlRestoreTarget('/creation/jump-hop/generating', { + sessionId: ' ', + profileId: ' jump-profile-1 ', + draftId: undefined, + workId: null, + }), + ).toEqual({ + kind: 'jump-hop', + sessionId: null, + profileId: 'jump-profile-1', + draftId: null, + workId: null, + isGeneratingPath: true, + }); + }); + + test('derives big fish restore session from work id when needed', () => { + expect( + resolveCreationUrlRestoreTarget('/creation/big-fish/result', { + workId: 'big-fish-work-river', + }), + ).toEqual({ + kind: 'big-fish', + sessionId: null, + profileId: null, + draftId: null, + workId: 'big-fish-work-river', + isGeneratingPath: false, + bigFishSessionId: 'river', + }); + + expect( + resolveCreationUrlRestoreTarget('/creation/big-fish/result', { + sessionId: 'big-fish-session-carp', + workId: 'big-fish-work-river', + }), + ).toMatchObject({ + kind: 'big-fish', + bigFishSessionId: 'big-fish-session-carp', + }); + }); + + test('keeps unsupported creation paths without a concrete restore target', () => { + expect( + resolveCreationUrlRestoreTarget('/creation/rpg/result', { + sessionId: 'rpg-session-1', + }), + ).toBeNull(); + expect( + resolveCreationUrlRestoreTarget('/creation/unknown/result', { + sessionId: 'unknown-session-1', + }), + ).toBeNull(); + expect( + resolveCreationUrlRestoreTarget('/creation/big-fishery/result', { + sessionId: 'big-fish-session-1', + }), + ).toBeNull(); + expect( + resolveCreationUrlRestoreTarget('/works/detail', { + workId: 'work-1', + }), + ).toBeNull(); + }); + test('builds creation restore state for core session based plays', () => { expect( buildBigFishCreationUrlState({ diff --git a/src/components/platform-entry/platformCreationUrlStateModel.ts b/src/components/platform-entry/platformCreationUrlStateModel.ts index 0961b1fb..fbf366fb 100644 --- a/src/components/platform-entry/platformCreationUrlStateModel.ts +++ b/src/components/platform-entry/platformCreationUrlStateModel.ts @@ -60,6 +60,93 @@ export function buildPuzzleRuntimeUrlStateKey(state: PuzzleRuntimeUrlState) { ].join('|'); } +export type CreationUrlRestoreTargetKind = + | 'big-fish' + | 'match3d' + | 'square-hole' + | 'puzzle' + | 'visual-novel' + | 'bark-battle' + | 'baby-object-match' + | 'jump-hop' + | 'wooden-fish'; + +type CreationUrlRestoreTargetBase = { + kind: CreationUrlRestoreTargetKind; + sessionId: string | null; + profileId: string | null; + draftId: string | null; + workId: string | null; + isGeneratingPath: boolean; +}; + +export type CreationUrlRestoreTarget = + | (CreationUrlRestoreTargetBase & { + kind: 'big-fish'; + bigFishSessionId: string | null; + }) + | (CreationUrlRestoreTargetBase & { + kind: Exclude; + }); + +type NonBigFishCreationUrlRestoreTarget = Extract< + CreationUrlRestoreTarget, + { kind: Exclude } +>; + +const CREATION_URL_RESTORE_TARGET_ROUTES = [ + ['/creation/big-fish', 'big-fish'], + ['/creation/match3d', 'match3d'], + ['/creation/square-hole', 'square-hole'], + ['/creation/puzzle', 'puzzle'], + ['/creation/visual-novel', 'visual-novel'], + ['/creation/bark-battle', 'bark-battle'], + ['/creation/baby-object-match', 'baby-object-match'], + ['/creation/jump-hop', 'jump-hop'], + ['/creation/wooden-fish', 'wooden-fish'], +] as const satisfies readonly (readonly [ + string, + CreationUrlRestoreTargetKind, +])[]; + +export function resolveCreationUrlRestoreTarget( + pathname: string | undefined, + state: CreationUrlState, +): CreationUrlRestoreTarget | null { + const path = pathname?.trim() ?? ''; + const route = CREATION_URL_RESTORE_TARGET_ROUTES.find(([prefix]) => + path === prefix || path.startsWith(`${prefix}/`), + ); + if (!route) { + return null; + } + + const kind = route[1]; + const sessionId = normalizeCreationUrlValue(state.sessionId); + const profileId = normalizeCreationUrlValue(state.profileId); + const draftId = normalizeCreationUrlValue(state.draftId); + const workId = normalizeCreationUrlValue(state.workId); + const base = { + kind, + sessionId, + profileId, + draftId, + workId, + isGeneratingPath: path.includes('/generating'), + }; + + if (kind === 'big-fish') { + return { + ...base, + kind, + bigFishSessionId: + sessionId ?? workId?.replace(/^big-fish-work-/u, '') ?? null, + }; + } + + return base as NonBigFishCreationUrlRestoreTarget; +} + export type InitialCreationUrlRestoreDecision = | { type: 'skip' } | { type: 'mark-handled' }