From eb6ab404e2b805276705fe9b8c0688de6d723c60 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 25 May 2026 22:52:38 +0800 Subject: [PATCH] fix: restore puzzle runtime url state --- .hermes/shared-memory/pitfalls.md | 24 + ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 3 + .../PlatformEntryFlowShellImpl.tsx | 822 +++++++++++++++++- ...atformCreationAgentFlowController.test.tsx | 200 +++++ .../usePlatformCreationAgentFlowController.ts | 29 +- ...gEntryFlowShell.agent.interaction.test.tsx | 371 ++++---- src/routing/appPageRoutes.test.ts | 42 + src/routing/appPageRoutes.ts | 29 +- src/services/creationUrlState.test.ts | 85 ++ src/services/creationUrlState.ts | 220 +++++ src/services/puzzleRuntimeUrlState.test.ts | 114 +++ src/services/puzzleRuntimeUrlState.ts | 155 ++++ 12 files changed, 1917 insertions(+), 177 deletions(-) create mode 100644 src/services/creationUrlState.test.ts create mode 100644 src/services/creationUrlState.ts create mode 100644 src/services/puzzleRuntimeUrlState.test.ts create mode 100644 src/services/puzzleRuntimeUrlState.ts diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 682af630..5dc32812 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -23,6 +23,30 @@ - 验è¯ï¼šç‚¹æ‹¼å›¾ / 抓大鹅 / 汪汪声浪å¡ç‰‡åŽï¼Œåº”看到å„自既有工作å°å†…容,例如测试中的 `拼图工作区:missing-session`ã€`抓大鹅工作区:missing-session` 或 `汪汪声浪é…置表å•`,并且ä¸å†å‡ºçŽ°â€œX 创作入å£â€ç©ºç™½é¡µã€‚ - å…³è”:`src/components/platform-entry/platformEntryTypes.ts`ã€`src/routing/appPageRoutes.ts`ã€`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`。 +## 创作æµç¨‹åˆ·æ–°æ¢å¤å¿…é¡»å†™ç§æœ‰ query + +- 现象:创作生æˆé¡µæˆ–结果页刷新åŽå›žåˆ°ç©ºç™½å·¥ä½œåŒºã€å¹³å°é¦–页,或者从作å“详情返回时错误å¤ç”¨äº†åˆ«çš„玩法è‰ç¨¿ã€‚ +- 原因:部分创作æµç¨‹åªæŠŠ `sessionId` / `profileId` / `draftId` / `workId` 放在å‰ç«¯å†…存里,没有写进 URL;也曾把写 URL 放在 stage 切æ¢å‰ï¼Œ`writeCreationUrlState` 因为还åœåœ¨éžåˆ›ä½œè·¯å¾„而直接跳过。若跨玩法或公开详情继续ä¿ç•™ç§æœ‰ query,还会污染 `/works/detail?work=...`。 +- 处ç†ï¼šåˆ›ä½œé¡µåªä½¿ç”¨ç§æœ‰ query `sessionId`ã€`profileId`ã€`draftId`ã€`workId` åšåˆ·æ–°æ¢å¤ï¼Œä¸å¤ç”¨å…¬å¼€ `work` 傿•°ï¼›`pushAppHistoryPath` åªåœ¨åŒä¸€åˆ›ä½œæµå†…ä¿ç•™è¿™äº› queryï¼Œç¦»å¼€åˆ›ä½œæµæˆ–切到å¦ä¸€ä¸ªçŽ©æ³•å¿…é¡»æ¸…æŽ‰ï¼›æ‰‹åŠ¨ draft 打开ã€ç”Ÿæˆå®Œæˆå’Œä¿å­˜å›žè°ƒè¦åœ¨è·¯ç”±å·²ç»åˆ‡åˆ° `/creation/` åŽå†è°ƒç”¨ `writeCreationUrlState`。 +- 验è¯ï¼š`npm run test -- src/services/creationUrlState.test.ts src/routing/appPageRoutes.test.ts src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx`;手测生æˆé¡µ / ç»“æžœé¡µåˆ·æ–°ä»æ¢å¤åŒä¸€è‰ç¨¿ï¼Œæ‰“开公开作å“详情 URL ä¸å¸¦ç§æœ‰æ¢å¤å‚数。 +- å…³è”:`src/services/creationUrlState.ts`ã€`src/routing/appPageRoutes.ts`ã€`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## 拼图生æˆé¡µè½®è¯¢ä¸è¦ç»‘展示 phase 或ä¸ç¨³å®š setter + +- 现象:拼图创作进入生æˆä¸­é¡µåŽï¼Œ`/api/runtime/puzzle/agent/sessions/{sessionId}` 会在 0.3 到 0.5 秒内被åå¤ GET,看起æ¥åƒè½®è¯¢é£Žæš´ï¼Œè€Œä¸æ˜¯ 3 秒一次的正常刷新。 +- 原因:轮询 `useEffect` åŒæ—¶ä¾èµ–了拼图展示 phase 和会éšçˆ¶ç»„件渲染å˜åŒ–çš„ `setSession` 函数,导致 `puzzleGenerationState` 的进度åˆå¹¶æˆ–页é¢é‡æ¸²æŸ“å°±ä¼šé‡æŒ‚ effectï¼›effect 里åˆä¼šç«‹å³å…ˆè¯·æ±‚一次 session,于是请求被放大æˆå¯†é›†å¾ªçŽ¯ã€‚ +- 处ç†ï¼šæ‹¼å›¾è½®è¯¢åªç»‘定 `selectionStage`ã€`activePuzzleGenerationSessionId` 和“是å¦ä»åœ¨ç”Ÿæˆä¸­â€è¿™ä¸ªå¸ƒå°”æ¡ä»¶ï¼›`setSession` 通过 ref ä¿æŒç¨³å®šï¼Œä¸è®©çˆ¶ç»„件釿–°æ¸²æŸ“改å˜è½®è¯¢å™¨èº«ä»½ã€‚进度 phase å˜åŒ–åªæ›´æ–°å±•示,ä¸é‡å»ºè½®è¯¢ã€‚ +- 验è¯ï¼š`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "persisted generating puzzle draft"`,并确认æ¢å¤ç”Ÿæˆä¸­è‰ç¨¿åŽ `getPuzzleAgentSession` ä¸ä¼šå› ä¸ºè¿›åº¦åˆ·æ–°ç»§ç»­è¿žå‘。 +- å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/platform-entry/usePlatformCreationAgentFlowController.ts`ã€`src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx`。 + +## 拼图试玩æ¢å¤ query 必须先切到è¿è¡Œæ€è·¯å¾„å†å†™ + +- 现象:拼图试玩或正å¼è¿è¡Œæ€æ‰“å¼€åŽï¼Œåˆ·æ–°ä¼šåœåœ¨â€œæ­£åœ¨è¿›å…¥æ‹¼å›¾å…³å¡â€ï¼Œæˆ–åœ°å€æ åªæœ‰ `runtimeProfileId`,缺少è‰ç¨¿ `runtimeSessionId`。 +- 原因:`writePuzzleRuntimeUrlState` åªä¼šåœ¨å½“å‰è·¯å¾„å·²ç»æ˜¯ `/runtime/puzzle` 时写入;如果先触å‘阶段切æ¢å†å†™ query,或者è‰ç¨¿ä½œå“摘è¦ç¼ºå°‘ `sourceSessionId`,就会把æ¢å¤å‚数写丢。`App.tsx` çš„ stage åŒæ­¥ä¹Ÿä¼šæ”¹ pathname,所以顺åºä¸å¯¹æ—¶å®¹æ˜“åªç•™ä¸‹éƒ¨åˆ† query。 +- 处ç†ï¼šè¿›å…¥æ‹¼å›¾ runtime æ—¶å…ˆ `pushAppHistoryPath('/runtime/puzzle')`ï¼Œå† `setSelectionStage('puzzle-runtime')`,最åŽå†™ `runtimeProfileId`ã€`runtimeSessionId`ã€`runtimeLevelId`ã€`work`ã€`mode`ï¼›è‰ç¨¿ runtime URL state å…许从 `profileId` åæŽ¨ `puzzle-session-*`,作为 `sourceSessionId` 的兜底。 +- 验è¯ï¼š`npm test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t \"puzzle draft generation auto starts trial and runtime back opens draft result\"`,确认 `window.location.pathname === '/runtime/puzzle'` 且 `window.location.search` åŒæ—¶åŒ…å« `runtimeProfileId` å’Œ `runtimeSessionId`。 +- å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/services/puzzleRuntimeUrlState.ts`ã€`src/routing/appPageRoutes.ts`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + ## è‰ç¨¿é¡µæœªè¯»ç‚¹ä¸è¦ç»§ç»­ç”¨çº¢è‰² literal - 现象:è‰ç¨¿é¡µåº•部 Tab å’Œä½œå“æž¶çš„æœªè¯»ç‚¹è§†è§‰ä¸Šä»åƒçº¢ç‚¹ï¼Œæˆ– glow ä»å¸¦çº¢è‰²é˜´å½±ï¼Œå’Œå¹³å°æš–棕体系ä¸ä¸€è‡´ã€‚ diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index a538870c..f534dd58 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -8,6 +8,8 @@ 当å‰åˆ›ä½œ Tab åªæ‰¿è½½èµ›äº‹ bannerã€çŽ©æ³•æ¨¡æ¿åˆ†ç±»å’Œä¸¤åˆ—模æ¿å¡ï¼›ç‚¹å‡»æ¨¡æ¿å¡åŽç›´æŽ¥è¿›å…¥å¯¹åº”玩法已有的入å£åˆ›ä½œè¡¨å• stage,ä¸å†ç»è¿‡ç©ºç™½å ä½é¡µï¼Œä¹Ÿä¸æŠŠæ—§è¡¨å•嵌进创作 Tab 首å±ã€‚移动端创作 Tab é¡¶æ åœ¨ `陶泥儿` å“牌åŒä¸€è¡Œæ˜¾ç¤ºçœŸå®žè´¦æˆ·æ³¥ç‚¹æ•°ï¼Œæ•°æ®æ¥è‡ª `profileDashboard.walletBalance`,ä¸å¾—å†æŠŠæ´»åŠ¨å¥–æ± å½“ä½œè´¦å·ä½™é¢å±•ç¤ºã€‚é¦–å± banner 结构按å‚è€ƒå›¾æ‹†æˆæ¨ªå‘坿»‘动赛事å¡ã€ä¸»ä½“宣传图文区ã€å¥–池胶囊ã€å¼€å§‹ / ç»“æŸæ—¶é—´æ¡å’Œå¡ç‰‡å†…分页点;轮播åªä¿ç•™ `拼图主题创作赛` å’Œ `抓大鹅主题创作赛`,两个主题赛事奖池å‡ä¸º `1000` 泥点数。玩法列表ä¸å†å¥—外部边框å¡ç‰‡ï¼Œç§»åŠ¨ç«¯éœ€è¦åŽ‹ç¼©æ¨ªå‘è¾¹è·å’Œä¸¤åˆ—é—´è·ï¼›çŽ©æ³•å¡ç»Ÿä¸€æŒ‰â€œä¸Šå›¾ã€å·¦ä¸ŠçŠ¶æ€æ ‡ç­¾ï¼ˆä»…éžå¼€æ”¾æ€æ˜¾ç¤ºï¼‰ã€å°é¢å³ä¸‹ `10-20泥点数`ã€ä¸‹æ–¹ç™½åº•标题/æè¿°â€ç»“构展示,å¡ç‰‡é«˜åº¦ä¿æŒç´§å‡‘ä½†æ ‡é¢˜ã€æè¿°å’Œé¢„ä¼°æ¶ˆè€—ç‚¹æ•°éƒ½å¿…é¡»å¯è§ã€‚创作 Tab 根容器ä¸å†ä½¿ç”¨ `platform-page-stage` 这类全局内容å¡ç‰‡å£³ï¼Œä½†ç»§ç»­ä¿ç•™ `platform-remap-surface` 作为主题和输入框样å¼å‘½ä¸­é’©å­ã€‚创作首å±å­—å·éœ€è¦å¯¹é½å¹³å°æ™®é€š UI æ¡£ä½ï¼šé¡¶æ æ³¥ç‚¹ç»„ä»¶ã€banner 正文ã€åˆ†ç±» Tab å’ŒçŽ©æ³•å¡æ ‡é¢˜ / 副标题 / 消耗说明优先使用 `11px` 到 `14px`,ä¸ä½¿ç”¨ `text-lg`ã€`text-xl` 或更大的展示级字å·ã€‚è‰ç¨¿ Tab ç»§ç»­æ‰¿æŽ¥ä½œå“æž¶ã€‚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`,这些入å£ç»§ç»­æ‰¿æŽ¥å„玩法自己的表å•ã€è‰ç¨¿æ¢å¤å’ŒåŽç»­ç¼–排,ä¸ä½œä¸ºåˆ›ä½œ Tab 首å±å†…容。 +创作æ¢å¤å‚æ•°åªä¿ç•™ `sessionId`ã€`profileId`ã€`draftId`ã€`workId` è¿™å››ä¸ªç§æœ‰ query。它们åªå…许在åŒä¸€æ¡åˆ›ä½œé“¾è·¯çš„结果页ã€ç”Ÿæˆé¡µã€å·¥ä½œå°ä¹‹é—´ä¿ç•™ï¼›åˆ‡åˆ°é¦–页ã€å…¬å¼€ä½œå“详情ã€runtime 或å¦ä¸€æ¡çŽ©æ³•é“¾è·¯æ—¶å¿…é¡»æ¸…æŽ‰ã€‚ç”Ÿæˆé¡µæ¢å¤æ—¶åªè®¤å½“å‰è¿›å…¥é¡µçš„æ—¶é—´ä½œä¸ºæ–°çš„ `startedAtMs`ï¼Œä½œå“æ‘˜è¦é‡Œçš„ `updatedAt` åªç”¨äºŽæŽ’åºä¸Žæ‘˜è¦å±•示,ä¸å†ä½œä¸ºç”Ÿæˆè¿›åº¦èµ·ç‚¹ã€‚ + `PlatformEntryFlowShellImpl.tsx` 仿˜¯å¹³å°å…¥å£ç¼–排壳,åŽç»­ç»´æŠ¤æ—¶åº”优先把独立 UI 片段ã€å…¬å¼€ä½œå“映射ã€è‰ç¨¿ç”Ÿæˆ notice å’Œè¿è¡Œæ€çŠ¶æ€ helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或åŒç›®å½•ç´§é‚» helper 文件。拆分åªå…è®¸æ”¹å˜æ–‡ä»¶ç»„ç»‡ï¼Œä¸æ”¹å˜å…¥å£é…置事实æºã€é»˜è®¤å¯¼å‡ºã€propsã€é¡µé¢é˜¶æ®µã€UI 文案或现有交互;其中拼图首访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`。 ç§»åŠ¨ç«¯åº•éƒ¨ä¸€çº§å¯¼èˆªæ˜¯å…¨å±€å¹³å°æ ·å¼ï¼Œä¸æŒ‰å•一玩法分å‰ã€‚当å‰è§†è§‰ç»Ÿä¸€ä¸ºç±³ç™½æµ®åŠ¨èƒ¶å›Šåº•åº§ã€æµ…æ£•åˆ†éš”çº¿ã€æ£•è‰²çº¿æ€§å›¾æ ‡ã€æ©˜è‰²é€‰ä¸­æ€å’Œåº•部短下划线;中间 `创作` å…¥å£ä¿æŒå‡¸èµ·åœ†å½¢ä¸»æŒ‰é’®ï¼Œä½†å‡¸èµ·ä½ç§»åªèƒ½ä½œç”¨åœ¨æŒ‰é’®å†…容层,ä¸èƒ½ç§»åŠ¨æ‰¿è½½åˆ†éš”çº¿çš„ Tab 按钮容器,确ä¿åˆ›ä½œå·¦å³åˆ†éš”线与其他分隔线垂直ä½ç½®ä¸€è‡´ã€‚Tab åç§°å’Œå¯è§æ€§ä»ç”±çŽ°æœ‰ `PlatformHomeTab` / 登录æ€è§„则决定,样å¼è°ƒæ•´ä¸å¾—改写 Tab 文案或导航状æ€ã€‚ @@ -90,6 +92,7 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£é€‰æ‹©å…¥å£ç»Ÿä¸€åœ¨ä¸ªäººä¸­å¿ƒ `æ¬¡çº§å…¥å£ > - 结果页å…许多关å¡å¹¶è¡Œç¼–辑和生æˆï¼›æŸä¸€å…³å¡å›¾ç‰‡ç”Ÿæˆå®Œæˆå›žåŒ…åªé™é»˜æ›´æ–°è¯¥å…³å¡ç´ æä¸Žç”Ÿæˆæ€ï¼Œä¸å¾—自动打开或切æ¢å…³å¡è¯¦æƒ…颿¿ï¼Œé¿å…打断用户正在编辑的其它关å¡ã€‚ - 结果页关å¡å›¾ç‰‡ç”Ÿæˆåªæ ‡è®°å¯¹åº”å…³å¡çš„局部生æˆè¿›åº¦ï¼Œä¸ç¦ç”¨â€œæ–°å¢žå…³å¡â€ã€å…¶å®ƒå…³å¡è¯¦æƒ…编辑和结果页导航。 - 结果页å•关测试åªèƒ½æŠŠå®Œæ•´è‰ç¨¿æŒä¹…化,并通过 `levelId` 指定è¿è¡Œæ€èµ·å§‹å…³å¡ï¼›ä¸å¾—把å•关快照作为整份è‰ç¨¿è°ƒç”¨ `updatePuzzleWork`,å¦åˆ™ source session å’Œä½œå“ profile çš„ `levels` 会被覆盖æˆå•关,退出é‡è¿›åŽå…¶å®ƒå…³å¡ä¼šä¸¢å¤±ã€‚ +- 拼图试玩和正å¼è¿è¡Œæ€åˆ·æ–°æ¢å¤ä¸å¤ç”¨åˆ›ä½œç§æœ‰ query。进入 `/runtime/puzzle` 时必须写入 `runtimeProfileId`ã€è‰ç¨¿ `runtimeSessionId`ã€å¯é€‰ `runtimeLevelId`ã€å…¬å¼€ä½œå“ `work` å’Œ `mode=draft|published`;进入è¿è¡Œæ€çš„导航顺åºå¿…须先切到 `/runtime/puzzle`,å†å†™è¿™äº› runtime query,é¿å…被阶段导航清掉åŽåˆ·æ–°åœåœ¨â€œæ­£åœ¨è¿›å…¥æ‹¼å›¾å…³å¡â€ã€‚ - 结果页生æˆå…³å¡å›¾æ—¶è‹¥å…³å¡å为空,å‰ç«¯å¿…须传 `shouldAutoNameLevel=true`,åŽç«¯å¤ç”¨é¦–关命åå¥‘çº¦å…ˆæŒ‰ç”»é¢æè¿°ç”Ÿæˆå…³å¡å,å†åœ¨å›¾ç‰‡ç”ŸæˆåŽç”¨è§†è§‰å‘½å结果精修,并把生æˆåå’Œ UI 背景æç¤ºè¯éšæœ¬æ¬¡å…³å¡å¿«ç…§å†™å›žã€‚ - 拼图è¿è¡Œæ€èƒŒæ™¯ä¼˜å…ˆè¯»å–当å‰å…³å¡ `levelBackgroundImageSrc/levelBackgroundImageObjectKey`ï¼Œæ—§æ•°æ®æ‰å…¼å®¹ `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩ã€ç›´è¾¾æŒ‡å®šå…³å¡å’Œæ­£å¼ `next-level` 推进时,目标关å¡ç¼ºå…³å¡èƒŒæ™¯æ—¶å¿…须继承åŒä½œå“首个å¯ç”¨å…³å¡èƒŒæ™¯ï¼Œä»ç¼ºå¤±æ—¶æ‰æ²¿ç”¨å½“å‰è¿è¡Œæ€å¿«ç…§èƒŒæ™¯æˆ–默认 UI。è¿è¡Œæ€æŒ‰é’®è§†è§‰ä¼˜å…ˆè¯»å–当å‰å…³å¡ `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`ï¼Œå…ˆæŒ‰é€æ˜Ž alpha 自动边界检测识别 spritesheet ä¸­çš„ç‹¬ç«‹æŒ‰é’®å±•ç¤ºçŸ©å½¢ï¼Œå†æŒ‰åŽŸå›¾ä½ç½®ä»Žå·¦åˆ°å³ã€ä»Žä¸Šåˆ°ä¸‹æ˜ å°„到返回ã€è®¾ç½®ã€ä¸‹ä¸€å…³ã€æç¤ºã€åŽŸå›¾ã€å†»ç»“ï¼›åŒä¸€ç»„ä»¶è¿˜è¦æŒ‰è¾ƒé«˜ alpha é˜ˆå€¼æ´¾ç”Ÿç´§è‡´ç‚¹å‡»çƒ­åŒºï¼Œé€æ˜Žç•™ç™½å’ŒæŸ”边低 alpha 区域尽é‡ä¸å“应点击。检测失败时回退旧固定六格è£åˆ‡ï¼Œç¼ºå¤±æ—¶æ‰ç”¨çŽ°æœ‰å›¾æ ‡æŒ‰é’®å…œåº•ã€‚æœ‰ spritesheet æ—¶ï¼Œè¿”å›žå’Œè®¾ç½®æŒ‰é’®çš„ç‚¹å‡»å®¹å™¨åªæä¾›é€æ˜Žç‚¹å‡»åŒºï¼Œä¸å†å åŠ é»˜è®¤ç™½è‰²åœ†å½¢åº•ï¼›åº•éƒ¨æç¤ºã€åŽŸå›¾ã€å†»ç»“ä¸‰æžšç´ ææŒ‰æ£€æµ‹çŸ©å½¢çš„原始宽高比显示,ä¸èƒ½å¼ºè¡Œæ‹‰ä¼¸æˆæ­£åœ†æˆ–铺满整列。底部é“具区ä¸å†ä½¿ç”¨è¿žç‰‡èƒ¶å›ŠèƒŒæ™¯ï¼Œæç¤ºã€åŽŸå›¾ã€å†»ç»“三个按钮å‡åŒ€åˆ†å¸ƒï¼›è¿è¡Œæ€åªå±•ç¤ºæŒ‰é’®ç´ ææœ¬èº«ï¼Œä¸é¢å¤–å åŠ â€œæç¤º / 原图 / å†»ç»“â€æ–‡å­—。 - 拼图è¿è¡Œæ€æ£‹ç›˜ä¸å åŠ åˆ†å—è’™ç‰ˆã€æè¾¹ã€é˜´å½±ã€é€‰ä¸­åº•色或åˆå¹¶å— SVG 轮廓;拼图片本体需è¦è£åˆ‡ä¸ºåœ†è§’形状,å•å—使用独立圆角è£åˆ‡ï¼Œåˆå¹¶å—使用 SVG 原生 `clipPath` è£åˆ‡æ•´ä½“外轮廓,外凸角和内凹角分别计算åŠå¾„,内凹角åŠå¾„è¦æ¯”外凸角更明显以é¿å…手机 WebView 中看起æ¥ä»æ˜¯ç›´è§’。原图é“å…·åªåœ¨ç”¨æˆ·ä¸»åŠ¨ç¡®è®¤åŽæ‰“开独立原图查看层,ä¸åœ¨å½“剿‹¼å›¾æ£‹ç›˜ä¸Šå åŠ åŽŸå›¾ã€‚ diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 0ccbafd7..773e7cde 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -158,6 +158,19 @@ import { streamCreativeAgentMessage, streamCreativeDraftEdit, } from '../../services/creative-agent'; +import { + clearCreationUrlState, + type CreationUrlState, + isCreationRestorePath, + readCreationUrlState, + writeCreationUrlState, +} from '../../services/creationUrlState'; +import { + clearPuzzleRuntimeUrlState, + readPuzzleRuntimeUrlState, + writePuzzleRuntimeUrlState, + type PuzzleRuntimeUrlState, +} from '../../services/puzzleRuntimeUrlState'; import { readCustomWorldAgentUiState, shouldRestoreCustomWorldAgentUiState, @@ -401,7 +414,10 @@ import { buildCreationHubFallbackItems, resolveRpgCreationErrorMessage, } from './platformEntryShared'; -import type { PlatformEntryFlowShellProps } from './platformEntryTypes'; +import type { + PlatformEntryFlowShellProps, + SelectionStage, +} from './platformEntryTypes'; import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView'; import { PlatformFeedbackView } from './PlatformFeedbackView'; import { PlatformWorkDetailView } from './PlatformWorkDetailView'; @@ -1700,6 +1716,237 @@ function buildPuzzleResultWorkId(sessionId: string | null | undefined) { return `puzzle-work-${stableSuffix}`; } +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, +) { + pushAppHistoryPath('/runtime/puzzle'); + setSelectionStage('puzzle-runtime'); + writePuzzleRuntimeUrlState(state); +} + +function buildPuzzleRuntimeWorkFromSession( + session: PuzzleAgentSessionSnapshot, + owner: { userId?: string | null; displayName?: string | null }, +): PuzzleWorkSummary | null { + const draft = session.draft; + const profileId = + session.publishedProfileId ?? buildPuzzleResultProfileId(session.sessionId); + if (!draft || !profileId || !draft.coverImageSrc?.trim()) { + return null; + } + + return { + workId: buildPuzzleResultWorkId(session.sessionId) ?? profileId, + profileId, + ownerUserId: owner.userId ?? 'current-user', + sourceSessionId: session.sessionId, + authorDisplayName: owner.displayName ?? '玩家', + workTitle: draft.workTitle, + workDescription: draft.workDescription, + levelName: draft.levelName, + summary: draft.summary, + themeTags: draft.themeTags, + coverImageSrc: draft.coverImageSrc, + coverAssetId: draft.coverAssetId, + publicationStatus: 'draft', + updatedAt: session.updatedAt, + publishedAt: null, + playCount: 0, + remixCount: 0, + likeCount: 0, + publishReady: Boolean(session.resultPreview?.publishReady), + levels: draft.levels, + }; +} + +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 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, + ); + return { + sessionId, + profileId, + workId: normalizeCreationUrlValue(params.work?.summary.workId ?? profileId), + }; +} + +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 buildDraftNoticeKey(kind: CreationWorkShelfKind, id: string) { return `${kind}:${id}`; } @@ -2976,6 +3223,12 @@ export function PlatformEntryFlowShellImpl({ useState({}); const [pendingDraftShelfItems, setPendingDraftShelfItems] = useState({}); + const [initialCreationUrlState] = useState(() => readCreationUrlState()); + const handledInitialCreationUrlStateRef = useRef(false); + const [initialPuzzleRuntimeUrlState] = useState(() => + readPuzzleRuntimeUrlState(), + ); + const handledPuzzleRuntimeUrlStateKeyRef = useRef(null); useEffect(() => { selectionStageRef.current = selectionStage; @@ -4176,6 +4429,7 @@ export function PlatformEntryFlowShellImpl({ setPuzzleOnboardingPhase('input'); platformBootstrap.setPlatformTab('home'); setSelectionStage('platform'); + clearPuzzleRuntimeUrlState(); void refreshPuzzleShelf(); } catch (error) { setPuzzleOnboardingError( @@ -4213,6 +4467,7 @@ export function PlatformEntryFlowShellImpl({ setSelectedPuzzleDetail(null); platformBootstrap.setPlatformTab(authUi?.user ? 'home' : 'category'); setSelectionStage('platform'); + clearPuzzleRuntimeUrlState(); }, [authUi?.user, platformBootstrap, setSelectionStage]); useEffect(() => { @@ -4265,7 +4520,10 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRun(startLocalPuzzleRun(item)); setPuzzleRuntimeAuthMode('default'); setPuzzleRuntimeReturnStage('platform'); - setSelectionStage('puzzle-runtime'); + openPuzzleRuntimeStage( + setSelectionStage, + buildPuzzleDraftRuntimeUrlState(item, null), + ); }, PUZZLE_ONBOARDING_GENERATED_DELAY_MS); } catch (error) { setPuzzleOnboardingPhase('input'); @@ -4365,6 +4623,9 @@ export function PlatformEntryFlowShellImpl({ onSessionOpened: () => { setShowCreationTypeModal(false); }, + onSessionChanged: (session) => { + writeCreationUrlState(buildBigFishCreationUrlState(session)); + }, onActionComplete: ({ payload, response, setSession }) => { setSession(response.session); if (payload.action === 'big_fish_publish_game') { @@ -4465,6 +4726,9 @@ export function PlatformEntryFlowShellImpl({ onSessionOpened: () => { setShowCreationTypeModal(false); }, + onSessionChanged: (session) => { + writeCreationUrlState(buildMatch3DCreationUrlState(session)); + }, onActionComplete: async ({ payload, response, setSession }) => { setSession(response.session); if (payload.action !== 'match3d_compile_draft') { @@ -4611,6 +4875,9 @@ export function PlatformEntryFlowShellImpl({ onSessionOpened: () => { setShowCreationTypeModal(false); }, + onSessionChanged: (session) => { + writeCreationUrlState(buildSquareHoleCreationUrlState(session)); + }, beforeExecuteAction: ({ payload, session }) => { if (payload.action === 'square_hole_compile_draft') { markDraftGenerating('square-hole', [ @@ -4865,6 +5132,9 @@ export function PlatformEntryFlowShellImpl({ setPuzzleCreationError(null); setShowCreationTypeModal(false); }, + onSessionChanged: (session) => { + writeCreationUrlState(buildPuzzleCreationUrlState(session)); + }, onOpenError: ({ errorMessage }) => { sessionController.setCreationTypeError(errorMessage); setPuzzleCreationError(errorMessage); @@ -4942,7 +5212,10 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRun(run); setPuzzleRuntimeAuthMode('default'); setPuzzleRuntimeReturnStage('puzzle-result'); - setSelectionStage('puzzle-runtime'); + openPuzzleRuntimeStage( + setSelectionStage, + buildPuzzleDraftRuntimeUrlState(item, null), + ); } catch (error) { setPuzzleError( resolvePuzzleErrorMessage(error, 'å¯åŠ¨æ‹¼å›¾è¯•çŽ©å¤±è´¥ã€‚'), @@ -5093,6 +5366,9 @@ export function PlatformEntryFlowShellImpl({ onSessionOpened: () => { setShowCreationTypeModal(false); }, + onSessionChanged: (session) => { + writeCreationUrlState(buildVisualNovelCreationUrlState(session)); + }, onActionComplete: ({ response, setSession }) => { setSession(response.session); const openResult = @@ -5287,7 +5563,10 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRun(run); setPuzzleRuntimeAuthMode('default'); setPuzzleRuntimeReturnStage('puzzle-result'); - setSelectionStage('puzzle-runtime'); + openPuzzleRuntimeStage( + setSelectionStage, + buildPuzzleDraftRuntimeUrlState(item, null), + ); } catch (error) { puzzleErrorSetterRef.current( resolvePuzzleErrorMessage(error, 'å¯åŠ¨æ‹¼å›¾è¯•çŽ©å¤±è´¥ã€‚'), @@ -5352,7 +5631,6 @@ export function PlatformEntryFlowShellImpl({ isMiniGameDraftGenerating( activePuzzleBackgroundCompileTask?.generationState ?? null, ); - const puzzleGenerationViewPhase = puzzleGenerationViewState?.phase ?? null; const shouldPollPuzzleGenerationSession = selectionStage === 'puzzle-generating' && activePuzzleGenerationSessionId != null && @@ -5417,7 +5695,6 @@ export function PlatformEntryFlowShellImpl({ }; }, [ activePuzzleGenerationSessionId, - puzzleGenerationViewPhase, shouldPollPuzzleGenerationSession, setPuzzleSession, ]); @@ -5798,7 +6075,10 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRun(run); setPuzzleRuntimeAuthMode('default'); setPuzzleRuntimeReturnStage('puzzle-result'); - setSelectionStage('puzzle-runtime'); + openPuzzleRuntimeStage( + setSelectionStage, + buildPuzzleDraftRuntimeUrlState(item, null), + ); } catch (error) { setPuzzleError( resolvePuzzleErrorMessage(error, 'å¯åŠ¨æ‹¼å›¾è¯•çŽ©å¤±è´¥ã€‚'), @@ -6167,6 +6447,9 @@ export function PlatformEntryFlowShellImpl({ try { const response = await createBabyObjectMatchDraft(payload); setBabyObjectMatchDraft(response.draft); + writeCreationUrlState( + buildBabyObjectMatchCreationUrlState(response.draft), + ); void refreshBabyObjectMatchShelf(); setBabyObjectMatchGenerationPhase('ready'); setBabyObjectMatchGenerationState((current) => @@ -6503,6 +6786,7 @@ export function PlatformEntryFlowShellImpl({ setBigFishRuntimeStartedAt(null); setBigFishRuntimeReturnStage('platform'); setBigFishGenerationState(null); + clearCreationUrlState(); bigFishFlow.leaveFlow(); }, [bigFishFlow]); @@ -6512,6 +6796,7 @@ export function PlatformEntryFlowShellImpl({ setMatch3DFormDraftPayload(null); setMatch3DGenerationState(null); setMatch3DRuntimeReturnStage('match3d-result'); + clearCreationUrlState(); match3dFlow.leaveFlow(); }, [match3dFlow, setMatch3DFormDraftPayload]); @@ -6519,6 +6804,7 @@ export function PlatformEntryFlowShellImpl({ setSquareHoleRun(null); setSquareHoleRuntimeReturnStage('square-hole-result'); setSquareHoleGenerationState(null); + clearCreationUrlState(); squareHoleFlow.leaveFlow(); }, [squareHoleFlow]); @@ -6529,6 +6815,7 @@ export function PlatformEntryFlowShellImpl({ setJumpHopGenerationState(null); setJumpHopSession(null); setJumpHopError(null); + clearCreationUrlState(); setSelectionStage('platform'); }, [setSelectionStage]); @@ -6539,6 +6826,7 @@ export function PlatformEntryFlowShellImpl({ setWoodenFishGenerationState(null); setWoodenFishSession(null); setWoodenFishError(null); + clearCreationUrlState(); setSelectionStage('platform'); }, [setSelectionStage]); @@ -6568,6 +6856,7 @@ export function PlatformEntryFlowShellImpl({ setBarkBattleError(null); setBarkBattleGenerationPartialFailed(false); setIsBarkBattleBusy(false); + clearCreationUrlState(); selectionStageRef.current = 'platform'; setSelectionStage('platform'); }, [setSelectionStage]); @@ -6584,6 +6873,7 @@ export function PlatformEntryFlowShellImpl({ try { const draft = await createBarkBattleDraft(payload); setBarkBattleDraftConfig(draft); + writeCreationUrlState(buildBarkBattleCreationUrlState(draft)); setBarkBattlePublishedConfig(null); markDraftGenerating('bark-battle', [draft.workId, draft.draftId]); markPendingDraftGenerating('bark-battle', draft.workId ?? draft.draftId); @@ -6620,6 +6910,7 @@ export function PlatformEntryFlowShellImpl({ const handleBarkBattleGenerationComplete = useCallback( (draft: BarkBattleDraftConfig, partialFailed: boolean) => { setBarkBattleDraftConfig(draft); + writeCreationUrlState(buildBarkBattleCreationUrlState(draft)); setBarkBattlePublishedConfig(null); setBarkBattleGenerationPartialFailed(partialFailed); const generationStatus = resolveBarkBattleDraftGenerationStatus( @@ -6685,6 +6976,7 @@ export function PlatformEntryFlowShellImpl({ draft.uiBackgroundImageSrc ?? persistedDraft.uiBackgroundImageSrc, }; setBarkBattleDraftConfig(nextDraft); + writeCreationUrlState(buildBarkBattleCreationUrlState(nextDraft)); updateBarkBattleWorkCaches( buildBarkBattleWorkSummaryFromDraft( nextDraft, @@ -6848,6 +7140,8 @@ export function PlatformEntryFlowShellImpl({ setActiveCreativeAgentSessionId(null); setCreativeDraftEditError(null); resetRecommendRuntimeSelection(); + clearCreationUrlState(); + clearPuzzleRuntimeUrlState(); puzzleFlow.leaveFlow(); }, [puzzleFlow, resetRecommendRuntimeSelection]); @@ -6858,6 +7152,7 @@ export function PlatformEntryFlowShellImpl({ setVisualNovelFormDraftPayload(null); setVisualNovelGenerationStartedAtMs(null); setVisualNovelGenerationPhase('generating'); + clearCreationUrlState(); visualNovelFlow.leaveFlow(); }, [visualNovelFlow]); @@ -6867,6 +7162,7 @@ export function PlatformEntryFlowShellImpl({ setBabyObjectMatchGenerationState(null); setBabyObjectMatchGenerationPhase('generating'); setBabyObjectMatchError(null); + clearCreationUrlState(); enterCreateTab(); selectionStageRef.current = 'platform'; setSelectionStage('platform'); @@ -6879,6 +7175,9 @@ export function PlatformEntryFlowShellImpl({ try { const response = await saveBabyObjectMatchDraft({ draft }); setBabyObjectMatchDraft(response.draft); + writeCreationUrlState( + buildBabyObjectMatchCreationUrlState(response.draft), + ); void refreshBabyObjectMatchShelf(); } catch (error) { setBabyObjectMatchError( @@ -7518,6 +7817,9 @@ export function PlatformEntryFlowShellImpl({ const generationState = createMiniGameDraftGenerationState('jump-hop'); setJumpHopError(null); setJumpHopSession(created.session); + writeCreationUrlState( + buildJumpHopCreationUrlState({ session: created.session }), + ); setJumpHopWork(null); setJumpHopRun(null); setJumpHopGenerationState(generationState); @@ -7550,6 +7852,12 @@ export function PlatformEntryFlowShellImpl({ const readyState = createReadyJumpHopGenerationState(generationState); setJumpHopSession(response.session); setJumpHopWork(response.work ?? null); + writeCreationUrlState( + buildJumpHopCreationUrlState({ + session: response.session, + work: response.work, + }), + ); setJumpHopGenerationState(readyState); setSelectionStage('jump-hop-result'); } catch (error) { @@ -7571,9 +7879,15 @@ export function PlatformEntryFlowShellImpl({ ); setJumpHopSession(latest.session); setJumpHopWork(null); + writeCreationUrlState( + buildJumpHopCreationUrlState({ session: latest.session }), + ); } catch { setJumpHopSession(created.session); setJumpHopWork(null); + writeCreationUrlState( + buildJumpHopCreationUrlState({ session: created.session }), + ); } } finally { setIsJumpHopBusy(false); @@ -7620,6 +7934,12 @@ export function PlatformEntryFlowShellImpl({ ); setJumpHopSession(response.session); setJumpHopWork(response.work ?? jumpHopWork); + writeCreationUrlState( + buildJumpHopCreationUrlState({ + session: response.session, + work: response.work ?? jumpHopWork, + }), + ); setJumpHopGenerationState( createReadyJumpHopGenerationState(generationState), ); @@ -7809,6 +8129,9 @@ export function PlatformEntryFlowShellImpl({ const generationState = createMiniGameDraftGenerationState('wooden-fish'); setWoodenFishError(null); setWoodenFishSession(created.session); + writeCreationUrlState( + buildWoodenFishCreationUrlState({ session: created.session }), + ); setWoodenFishWork(null); setWoodenFishRun(null); setWoodenFishGenerationState(generationState); @@ -7839,6 +8162,12 @@ export function PlatformEntryFlowShellImpl({ ); setWoodenFishSession(response.session); setWoodenFishWork(response.work ?? null); + writeCreationUrlState( + buildWoodenFishCreationUrlState({ + session: response.session, + work: response.work, + }), + ); setWoodenFishGenerationState( createReadyWoodenFishGenerationState(generationState), ); @@ -7862,9 +8191,15 @@ export function PlatformEntryFlowShellImpl({ ); setWoodenFishSession(latest.session); setWoodenFishWork(null); + writeCreationUrlState( + buildWoodenFishCreationUrlState({ session: latest.session }), + ); } catch { setWoodenFishSession(created.session); setWoodenFishWork(null); + writeCreationUrlState( + buildWoodenFishCreationUrlState({ session: created.session }), + ); } } finally { setIsWoodenFishBusy(false); @@ -7911,6 +8246,12 @@ export function PlatformEntryFlowShellImpl({ ); setWoodenFishSession(response.session); setWoodenFishWork(response.work ?? woodenFishWork); + writeCreationUrlState( + buildWoodenFishCreationUrlState({ + session: response.session, + work: response.work ?? woodenFishWork, + }), + ); setWoodenFishGenerationState( createReadyWoodenFishGenerationState(generationState), ); @@ -7970,6 +8311,12 @@ export function PlatformEntryFlowShellImpl({ }); setWoodenFishSession(response.session); setWoodenFishWork(response.work ?? woodenFishWork); + writeCreationUrlState( + buildWoodenFishCreationUrlState({ + session: response.session, + work: response.work ?? woodenFishWork, + }), + ); return true; } catch (error) { setWoodenFishError( @@ -8315,7 +8662,12 @@ export function PlatformEntryFlowShellImpl({ if (selectionStage === 'big-fish-runtime' && !bigFishRun) { setSelectionStage(bigFishSession?.draft ? 'big-fish-result' : 'platform'); } - }, [bigFishRun, bigFishSession, selectionStage, setSelectionStage]); + }, [ + bigFishRun, + bigFishSession, + selectionStage, + setSelectionStage, + ]); useEffect(() => { if (selectionStage === 'match3d-result' && !match3dSession?.draft) { @@ -8326,7 +8678,12 @@ export function PlatformEntryFlowShellImpl({ if (selectionStage === 'match3d-runtime' && !match3dRun) { setSelectionStage(match3dSession?.draft ? 'match3d-result' : 'platform'); } - }, [match3dRun, match3dSession, selectionStage, setSelectionStage]); + }, [ + match3dRun, + match3dSession, + selectionStage, + setSelectionStage, + ]); useEffect(() => { if (selectionStage === 'square-hole-result' && !squareHoleSession?.draft) { @@ -8339,7 +8696,12 @@ export function PlatformEntryFlowShellImpl({ squareHoleSession?.draft ? 'square-hole-result' : 'platform', ); } - }, [selectionStage, setSelectionStage, squareHoleRun, squareHoleSession]); + }, [ + selectionStage, + setSelectionStage, + squareHoleRun, + squareHoleSession, + ]); useEffect(() => { if ( @@ -8511,12 +8873,11 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRuntimeAuthMode(authMode); setPuzzleRuntimeReturnStage(returnStage); if (!options.embedded) { - setSelectionStage('puzzle-runtime'); - pushAppHistoryPath( - buildPublicWorkStagePath( - 'puzzle-runtime', - buildPuzzlePublicWorkCode(item.profileId), - ), + openPuzzleRuntimeStage( + setSelectionStage, + authMode === 'isolated' + ? buildPuzzlePublishedRuntimeUrlState(item, levelId) + : buildPuzzleDraftRuntimeUrlState(item, levelId), ); } return true; @@ -8795,7 +9156,10 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRun(run); setPuzzleRuntimeAuthMode('default'); setPuzzleRuntimeReturnStage('puzzle-result'); - setSelectionStage('puzzle-runtime'); + openPuzzleRuntimeStage( + setSelectionStage, + buildPuzzleDraftRuntimeUrlState(item, options.levelId ?? null), + ); return true; } catch (error) { setPuzzleError(resolvePuzzleErrorMessage(error, 'å¯åŠ¨æ‹¼å›¾è¯•çŽ©å¤±è´¥ã€‚')); @@ -8958,6 +9322,152 @@ export function PlatformEntryFlowShellImpl({ return () => window.clearInterval(timerId); }, [puzzleRun, selectionStage]); + useEffect(() => { + if (selectionStage !== 'puzzle-runtime' || puzzleRun) { + return; + } + if (!hasPuzzleRuntimeUrlStateValue(initialPuzzleRuntimeUrlState)) { + return; + } + const runtimeUrlStateKey = buildPuzzleRuntimeUrlStateKey( + initialPuzzleRuntimeUrlState, + ); + if ( + handledPuzzleRuntimeUrlStateKeyRef.current === runtimeUrlStateKey || + isPuzzleBusy + ) { + return; + } + + handledPuzzleRuntimeUrlStateKeyRef.current = runtimeUrlStateKey; + let cancelled = false; + + const restorePuzzleRuntime = async () => { + const runtimeProfileId = normalizeCreationUrlValue( + initialPuzzleRuntimeUrlState.runtimeProfileId, + ); + const runtimeSessionId = normalizeCreationUrlValue( + initialPuzzleRuntimeUrlState.runtimeSessionId, + ); + const runtimeLevelId = normalizeCreationUrlValue( + initialPuzzleRuntimeUrlState.runtimeLevelId, + ); + const publicWorkCode = normalizeCreationUrlValue( + initialPuzzleRuntimeUrlState.publicWorkCode, + ); + const runtimeMode = initialPuzzleRuntimeUrlState.mode ?? null; + const isPublishedRuntime = runtimeMode === 'published'; + if ( + !isPublishedRuntime && + (platformBootstrap.isLoadingPlatform || + !platformBootstrap.canReadProtectedData) + ) { + handledPuzzleRuntimeUrlStateKeyRef.current = null; + return; + } + const fallbackStage: PuzzleRuntimeReturnStage = + runtimeMode === 'published' ? 'work-detail' : 'puzzle-result'; + + const candidateItems = isPublishedRuntime + ? puzzleGalleryEntries.length > 0 + ? puzzleGalleryEntries + : await refreshPuzzleGallery() + : ( + puzzleWorks.length > 0 + ? puzzleWorks + : (await listPuzzleWorks().catch(() => ({ items: [] }))).items + ); + const targetItem = + runtimeProfileId || runtimeSessionId || publicWorkCode + ? candidateItems.find( + (item) => + item.profileId === runtimeProfileId || + item.sourceSessionId === runtimeSessionId || + (publicWorkCode + ? isSamePuzzlePublicWorkCode(publicWorkCode, item.profileId) + : false), + ) ?? null + : null; + + if (!targetItem) { + if (runtimeSessionId) { + try { + const { session } = await getPuzzleAgentSession(runtimeSessionId); + const restoredWork = buildPuzzleRuntimeWorkFromSession(session, { + userId: authUi?.user?.id ?? 'current-user', + displayName: authUi?.user?.displayName ?? '玩家', + }); + if (restoredWork && !cancelled) { + setSelectedPuzzleDetail(restoredWork); + setPuzzleRun( + startLocalPuzzleRun(restoredWork, runtimeLevelId ?? null), + ); + setPuzzleRuntimeAuthMode('default'); + setPuzzleRuntimeReturnStage(fallbackStage); + openPuzzleRuntimeStage( + setSelectionStage, + buildPuzzleDraftRuntimeUrlState(restoredWork, runtimeLevelId), + ); + return; + } + } catch { + // 读ä¸åˆ°è‰ç¨¿æ—¶ç»§ç»­å›žé€€åˆ°å¹³å°é¡µï¼Œé¿å…å¡åœ¨ç©ºè¿è¡Œæ€ã€‚ + } + } + + if (!cancelled) { + setPuzzleError( + runtimeMode === 'published' + ? '这份拼图è¿è¡Œæ€ç¼ºå°‘坿¢å¤ä½œå“,请返回作å“è¯¦æƒ…é‡æ–°è¿›å…¥ã€‚' + : 'è¿™ä»½æ‹¼å›¾è¯•çŽ©ç¼ºå°‘å¯æ¢å¤è‰ç¨¿ï¼Œè¯·è¿”å›žç»“æžœé¡µé‡æ–°è¿›å…¥ã€‚', + ); + setPuzzleRuntimeAuthMode('default'); + setPuzzleRun(null); + setSelectionStage('platform'); + pushAppHistoryPath('/'); + } + return; + } + + if (cancelled) { + return; + } + + setSelectedPuzzleDetail(targetItem); + setPuzzleRun( + startLocalPuzzleRun(targetItem, runtimeLevelId ?? null), + ); + setPuzzleRuntimeAuthMode(isPublishedRuntime ? 'isolated' : 'default'); + setPuzzleRuntimeReturnStage(fallbackStage); + openPuzzleRuntimeStage( + setSelectionStage, + isPublishedRuntime + ? buildPuzzlePublishedRuntimeUrlState(targetItem, runtimeLevelId) + : buildPuzzleDraftRuntimeUrlState(targetItem, runtimeLevelId), + ); + }; + + void restorePuzzleRuntime(); + + return () => { + cancelled = true; + }; + }, [ + authUi?.user?.displayName, + authUi?.user?.id, + initialPuzzleRuntimeUrlState, + isPuzzleBusy, + platformBootstrap.canReadProtectedData, + platformBootstrap.isLoadingPlatform, + puzzleGalleryEntries, + puzzleRun, + puzzleRuntimeReturnStage, + puzzleWorks, + refreshPuzzleGallery, + selectionStage, + setSelectionStage, + ]); + const setPuzzleRuntimePaused = useCallback( async (paused: boolean) => { if (!puzzleRun?.currentLevel) { @@ -9255,10 +9765,11 @@ export function PlatformEntryFlowShellImpl({ ]); setSelectedPuzzleDetail(item); setPuzzleRun(run); - pushAppHistoryPath( - buildPublicWorkStagePath( - 'puzzle-runtime', - buildPuzzlePublicWorkCode(item.profileId), + openPuzzleRuntimeStage( + setSelectionStage, + buildPuzzlePublishedRuntimeUrlState( + item, + run.currentLevel?.levelId ?? null, ), ); return; @@ -10981,7 +11492,7 @@ export function PlatformEntryFlowShellImpl({ return; } - setBarkBattleDraftConfig({ + const nextDraft: BarkBattleDraftConfig = { draftId: item.draftId ?? item.workId, workId: item.workId, title: item.title, @@ -10997,13 +11508,15 @@ export function PlatformEntryFlowShellImpl({ configVersion: 1, rulesetVersion: 'bark-battle-ruleset-v1', updatedAt: item.updatedAt, - }); + }; + setBarkBattleDraftConfig(nextDraft); enterCreateTab(); selectionStageRef.current = isPersistedBarkBattleDraftGenerating(item) ? 'bark-battle-generating' : 'bark-battle-result'; setSelectionStage(selectionStageRef.current); + writeCreationUrlState(buildBarkBattleCreationUrlState(nextDraft)); }, [enterCreateTab, markDraftNoticeSeen, openPublicWorkDetail, setSelectionStage], ); @@ -11091,10 +11604,273 @@ export function PlatformEntryFlowShellImpl({ setBabyObjectMatchError(null); enterCreateTab(); setSelectionStage('baby-object-match-result'); + writeCreationUrlState(buildBabyObjectMatchCreationUrlState(draft)); }, [enterCreateTab, markDraftNoticeSeen, setSelectionStage], ); + useEffect(() => { + if (handledInitialCreationUrlStateRef.current) { + return; + } + if (!isCreationRestorePath(window.location.pathname)) { + handledInitialCreationUrlStateRef.current = true; + return; + } + if (!hasCreationUrlStateValue(initialCreationUrlState)) { + handledInitialCreationUrlStateRef.current = true; + return; + } + if (platformBootstrap.isLoadingPlatform) { + return; + } + if (!platformBootstrap.canReadProtectedData) { + return; + } + + handledInitialCreationUrlStateRef.current = true; + + const restoreCreationUrlState = async () => { + const path = window.location.pathname; + const sessionId = normalizeCreationUrlValue( + initialCreationUrlState.sessionId, + ); + const profileId = normalizeCreationUrlValue( + initialCreationUrlState.profileId, + ); + const draftId = normalizeCreationUrlValue(initialCreationUrlState.draftId); + const workId = normalizeCreationUrlValue(initialCreationUrlState.workId); + + if (path.startsWith('/creation/big-fish')) { + const targetSessionId = sessionId ?? workId?.replace(/^big-fish-work-/u, ''); + if (targetSessionId) { + const matchedWork = + ( + bigFishWorks.length > 0 + ? bigFishWorks + : (await listBigFishWorks().catch(() => ({ items: [] }))).items + ).find( + (item) => + item.sourceSessionId === targetSessionId || + item.workId === workId, + ) ?? null; + if (matchedWork) { + await openBigFishDraft(matchedWork); + return; + } + await bigFishFlow.restoreDraft(targetSessionId); + } + return; + } + + if (path.startsWith('/creation/match3d')) { + const matchedWork = + ( + match3dWorks.length > 0 + ? match3dWorks + : mapMatch3DWorksForRuntimeUi( + (await listMatch3DWorks().catch(() => ({ items: [] }))).items, + ) + ).find( + (item) => + item.sourceSessionId === sessionId || + item.profileId === profileId || + item.workId === workId, + ) ?? null; + if (matchedWork) { + await openMatch3DDraft(matchedWork, { forceDraft: true }); + return; + } + if (sessionId) { + await match3dFlow.restoreDraft(sessionId); + } + return; + } + + if (path.startsWith('/creation/square-hole')) { + const matchedWork = + ( + squareHoleWorks.length > 0 + ? squareHoleWorks + : (await listSquareHoleWorks().catch(() => ({ items: [] }))).items + ).find( + (item) => + item.sourceSessionId === sessionId || + item.profileId === profileId || + item.workId === workId, + ) ?? null; + if (matchedWork) { + await openSquareHoleDraft(matchedWork, { forceDraft: true }); + return; + } + if (sessionId) { + await squareHoleFlow.restoreDraft(sessionId); + } + return; + } + + if (path.startsWith('/creation/puzzle')) { + const matchedWork = + ( + puzzleWorks.length > 0 + ? puzzleWorks + : (await listPuzzleWorks().catch(() => ({ items: [] }))).items + ).find( + (item) => + item.sourceSessionId === sessionId || + item.profileId === profileId || + item.workId === workId, + ) ?? null; + if (matchedWork) { + await openPuzzleDraft(matchedWork); + return; + } + if (sessionId) { + await puzzleFlow.restoreDraft(sessionId); + } + return; + } + + if (path.startsWith('/creation/visual-novel')) { + const matchedWork = + ( + visualNovelWorks.length > 0 + ? visualNovelWorks + : (await listVisualNovelWorks().catch(() => ({ works: [] }))).works + ).find((item) => item.profileId === profileId) ?? null; + if (matchedWork) { + await openVisualNovelDraft(matchedWork, { forceDraft: true }); + return; + } + if (sessionId) { + await visualNovelFlow.restoreDraft(sessionId); + } + return; + } + + if (path.startsWith('/creation/bark-battle')) { + const matchedWork = + ( + barkBattleWorks.length > 0 + ? barkBattleWorks + : (await listBarkBattleWorks().catch(() => ({ items: [] }))).items + ).find( + (item) => item.workId === workId || item.draftId === draftId, + ) ?? null; + if (matchedWork) { + openBarkBattleDraft(matchedWork, { forceDraft: true }); + } + return; + } + + if (path.startsWith('/creation/baby-object-match')) { + const matchedDraft = + ( + babyObjectMatchDrafts.length > 0 + ? babyObjectMatchDrafts + : await listLocalBabyObjectMatchDrafts().catch(() => []) + ).find( + (item) => + item.profileId === profileId || + item.draftId === draftId || + item.profileId === workId, + ) ?? null; + if (matchedDraft) { + openBabyObjectMatchDraft(matchedDraft); + } + return; + } + + if (path.startsWith('/creation/jump-hop')) { + if (!sessionId) { + return; + } + try { + const { session } = await jumpHopClient.getSession(sessionId); + let work: JumpHopWorkProfileResponse | null = null; + if (profileId) { + work = (await jumpHopClient.getWorkDetail(profileId)).item; + } + setJumpHopSession(session); + setJumpHopWork(work); + writeCreationUrlState(buildJumpHopCreationUrlState({ session, work })); + enterCreateTab(); + setSelectionStage( + path.includes('/generating') + ? 'jump-hop-generating' + : session.draft + ? 'jump-hop-result' + : 'jump-hop-workspace', + ); + } catch (error) { + setJumpHopError( + resolveRpgCreationErrorMessage(error, '读å–跳一跳创作è‰ç¨¿å¤±è´¥ã€‚'), + ); + } + return; + } + + if (path.startsWith('/creation/wooden-fish')) { + if (!sessionId) { + return; + } + try { + const { session } = await woodenFishClient.getSession(sessionId); + let work: WoodenFishWorkProfileResponse | null = null; + if (profileId) { + work = (await woodenFishClient.getWorkDetail(profileId)).item; + } + setWoodenFishSession(session); + setWoodenFishWork(work); + writeCreationUrlState( + buildWoodenFishCreationUrlState({ session, work }), + ); + enterCreateTab(); + setSelectionStage( + path.includes('/generating') + ? 'wooden-fish-generating' + : session.draft + ? 'wooden-fish-result' + : 'wooden-fish-workspace', + ); + } catch (error) { + setWoodenFishError( + resolveRpgCreationErrorMessage(error, 'è¯»å–æ•²æœ¨é±¼åˆ›ä½œè‰ç¨¿å¤±è´¥ã€‚'), + ); + } + } + }; + + void restoreCreationUrlState(); + }, [ + babyObjectMatchDrafts, + barkBattleWorks, + bigFishFlow, + bigFishWorks, + enterCreateTab, + initialCreationUrlState, + jumpHopClient, + match3dFlow, + match3dWorks, + openBabyObjectMatchDraft, + openBarkBattleDraft, + openBigFishDraft, + openMatch3DDraft, + openPuzzleDraft, + openSquareHoleDraft, + openVisualNovelDraft, + platformBootstrap.canReadProtectedData, + platformBootstrap.isLoadingPlatform, + puzzleFlow, + puzzleWorks, + setSelectionStage, + squareHoleFlow, + squareHoleWorks, + visualNovelFlow, + visualNovelWorks, + woodenFishClient, + ]); + const startBigFishRunFromWork = useCallback( async ( item: BigFishWorkSummary, diff --git a/src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx b/src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx index 68f9039b..ded82926 100644 --- a/src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx +++ b/src/components/platform-entry/usePlatformCreationAgentFlowController.test.tsx @@ -300,6 +300,161 @@ function ActionCompleteHarness({ ); } +function SessionChangeHarness({ + onSessionChanged, +}: { + onSessionChanged: (session: TestSession | null) => void; +}) { + const flow = usePlatformCreationAgentFlowController< + TestSession, + Record, + { session: TestSession }, + TestMessagePayload, + { action: string }, + { session: TestSession } + >({ + client: { + createSession: async () => ({ + session: { + sessionId: 'session-open', + messages: [], + }, + }), + getSession: async () => ({ + session: { + sessionId: 'session-restore', + messages: [], + }, + }), + streamMessage: async () => ({ + sessionId: 'session-open', + messages: [], + }), + executeAction: async () => ({ + session: { + sessionId: 'session-compile', + messages: [], + }, + }), + selectSession: (response) => response.session, + }, + createPayload: {}, + workspaceStage: 'match3d-agent-workspace', + resultStage: 'match3d-result', + platformStage: 'platform', + isCompileAction: (payload) => payload.action === 'match3d_compile_draft', + resolveErrorMessage: (error, fallback) => + error instanceof Error ? error.message : fallback, + errorMessages: { + open: '打开失败', + restoreMissingSession: '缺少会è¯', + restore: 'æ¢å¤å¤±è´¥', + submit: 'å‘é€å¤±è´¥', + execute: '执行失败', + }, + enterCreateTab: () => {}, + setSelectionStage: () => {}, + onSessionChanged, + onActionComplete: ({ response, setSession }) => { + setSession(response.session); + }, + }); + + return ( +
+ + + +
+ ); +} + +function SessionSetterIdentityHarness({ + onSetterIdentity, +}: { + onSetterIdentity: (setter: unknown) => void; +}) { + const [renderCount, setRenderCount] = useState(0); + const flow = usePlatformCreationAgentFlowController< + TestSession, + Record, + { session: TestSession }, + TestMessagePayload, + { action: string }, + { session: TestSession } + >({ + client: { + createSession: async () => ({ + session: { + sessionId: 'session-open', + messages: [], + }, + }), + getSession: async () => ({ + session: { + sessionId: 'session-restore', + messages: [], + }, + }), + streamMessage: async () => ({ + sessionId: 'session-open', + messages: [], + }), + executeAction: async () => ({ + session: { + sessionId: 'session-compile', + messages: [], + }, + }), + selectSession: (response) => response.session, + }, + createPayload: {}, + workspaceStage: 'match3d-agent-workspace', + resultStage: 'match3d-result', + platformStage: 'platform', + isCompileAction: () => false, + resolveErrorMessage: (error, fallback) => + error instanceof Error ? error.message : fallback, + errorMessages: { + open: '打开失败', + restoreMissingSession: '缺少会è¯', + restore: 'æ¢å¤å¤±è´¥', + submit: 'å‘é€å¤±è´¥', + execute: '执行失败', + }, + enterCreateTab: () => {}, + setSelectionStage: () => {}, + onSessionChanged: () => {}, + }); + + useEffect(() => { + onSetterIdentity(flow.setSession); + }); + + return ( + + ); +} + test('creation agent flow preserves streamed assistant text when stream fails', async () => { const streamMessage = vi.fn(async (_sessionId, _payload, options) => { options?.onUpdate?.('先把方洞万能的å差定ä½ã€‚'); @@ -391,3 +546,48 @@ test('creation agent flow suppresses compile result stage for background complet 'match3d-agent-workspace', ); }); + +test('creation agent flow notifies session changes after open restore and compile', async () => { + const onSessionChanged = vi.fn(); + + render(); + + await act(async () => { + screen.getByRole('button', { name: '打开' }).click(); + }); + await act(async () => { + screen.getByRole('button', { name: 'æ¢å¤' }).click(); + }); + await act(async () => { + screen.getByRole('button', { name: '编译' }).click(); + }); + + await waitFor(() => { + expect(onSessionChanged).toHaveBeenCalledTimes(3); + }); + + expect( + onSessionChanged.mock.calls.map(([session]) => session?.sessionId), + ).toEqual(['session-open', 'session-restore', 'session-compile']); +}); + +test('creation agent flow keeps session setter stable across parent rerenders', async () => { + const onSetterIdentity = vi.fn(); + + render(); + + await waitFor(() => { + expect(onSetterIdentity).toHaveBeenCalledTimes(1); + }); + const initialSetter = onSetterIdentity.mock.calls[0]?.[0]; + + await act(async () => { + screen.getByRole('button', { name: /釿¸²æŸ“/u }).click(); + }); + + await waitFor(() => { + expect(onSetterIdentity).toHaveBeenCalledTimes(2); + }); + + expect(onSetterIdentity.mock.calls[1]?.[0]).toBe(initialSetter); +}); diff --git a/src/components/platform-entry/usePlatformCreationAgentFlowController.ts b/src/components/platform-entry/usePlatformCreationAgentFlowController.ts index ba7ab67b..f89b778e 100644 --- a/src/components/platform-entry/usePlatformCreationAgentFlowController.ts +++ b/src/components/platform-entry/usePlatformCreationAgentFlowController.ts @@ -1,4 +1,5 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; import type { TextStreamOptions } from '../../services/aiTypes'; import type { SelectionStage } from './platformEntryTypes'; @@ -75,12 +76,13 @@ type PlatformCreationAgentFlowControllerOptions< enterCreateTab: () => void; setSelectionStage: (stage: SelectionStage) => void; onSessionOpened?: () => void; + onSessionChanged?: (session: TSession | null) => void; onOpenError?: (params: { error: unknown; errorMessage: string }) => void; onActionComplete?: (params: { payload: TActionPayload; response: TActionResponse; session: TSession; - setSession: (session: TSession) => void; + setSession: Dispatch>; }) => | Promise<{ openResult?: boolean } | void> | { openResult?: boolean } @@ -94,7 +96,7 @@ type PlatformCreationAgentFlowControllerOptions< error: unknown; errorMessage: string; session: TSession; - setSession: (session: TSession) => void; + setSession: Dispatch>; }) => void | Promise; }; @@ -141,12 +143,27 @@ export function usePlatformCreationAgentFlowController< TActionResponse >, ) { - const [session, setSession] = useState(null); + const [session, rawSetSession] = useState(null); const [error, setError] = useState(null); const [isBusy, setIsBusy] = useState(false); const [streamingReplyText, setStreamingReplyText] = useState(''); const [isStreamingReply, setIsStreamingReply] = useState(false); const latestStreamingReplyTextRef = useRef(''); + const onSessionChangedRef = useRef(options.onSessionChanged); + + useEffect(() => { + onSessionChangedRef.current = options.onSessionChanged; + }, [options.onSessionChanged]); + + const setSession = useCallback( + (nextSessionOrUpdater: SetStateAction) => { + rawSetSession(nextSessionOrUpdater); + if (typeof nextSessionOrUpdater !== 'function') { + onSessionChangedRef.current?.(nextSessionOrUpdater); + } + }, + [], + ); const updateStreamingReplyText = useCallback((text: string) => { latestStreamingReplyTextRef.current = text; @@ -174,10 +191,10 @@ export function usePlatformCreationAgentFlowController< createPayload ?? options.createPayload, ); const nextSession = options.client.selectSession(response); - setSession(nextSession); options.enterCreateTab(); options.onSessionOpened?.(); options.setSelectionStage(options.workspaceStage); + setSession(nextSession); return nextSession; } catch (caughtError) { const errorMessage = options.resolveErrorMessage( @@ -212,11 +229,11 @@ export function usePlatformCreationAgentFlowController< try { const response = await options.client.getSession(normalizedSessionId); const nextSession = options.client.selectSession(response); - setSession(nextSession); options.enterCreateTab(); options.setSelectionStage( nextSession.draft ? options.resultStage : options.workspaceStage, ); + setSession(nextSession); return nextSession; } catch (caughtError) { setError( diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index 036b09fe..f533f741 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -119,10 +119,7 @@ import { startLocalPuzzleRun, swapLocalPuzzlePieces, } from '../../services/puzzle-runtime/puzzleLocalRuntime'; -import { - listPuzzleWorks, - updatePuzzleWork, -} from '../../services/puzzle-works'; +import { listPuzzleWorks, updatePuzzleWork } from '../../services/puzzle-works'; import { createRpgCreationSession, executeRpgCreationAction, @@ -212,14 +209,22 @@ async function openCreateTemplateHub(user: ReturnType) { async function findCreationTypeButton(name: string | RegExp) { const matcher = - typeof name === 'string' ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u') : name; - return within(getPlatformTabPanel('create')).findByRole('button', { name: matcher }); + typeof name === 'string' + ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u') + : name; + return within(getPlatformTabPanel('create')).findByRole('button', { + name: matcher, + }); } function queryCreationTypeButton(name: string | RegExp) { const matcher = - typeof name === 'string' ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u') : name; - return within(getPlatformTabPanel('create')).queryByRole('button', { name: matcher }); + typeof name === 'string' + ? new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'u') + : name; + return within(getPlatformTabPanel('create')).queryByRole('button', { + name: matcher, + }); } async function openDraftHub(user: ReturnType) { @@ -228,9 +233,7 @@ async function openDraftHub(user: ReturnType) { await waitFor(() => { expect(panel.getAttribute('aria-hidden')).toBe('false'); }); - expect( - await within(panel).findByRole('tab', { name: /全部/u }), - ).toBeTruthy(); + expect(await within(panel).findByRole('tab', { name: /全部/u })).toBeTruthy(); } async function expectDraftHubGeneratingBadgeCountAtLeast(count: number) { @@ -606,7 +609,12 @@ vi.mock('../../services/match3dGeneratedModelCache', () => ({ ( primaryAssets: Match3DWorkSummary['generatedItemAssets'], fallbackAssets: Match3DWorkSummary['generatedItemAssets'], - ) => (primaryAssets ? [...primaryAssets] : fallbackAssets ? [...fallbackAssets] : []), + ) => + primaryAssets + ? [...primaryAssets] + : fallbackAssets + ? [...fallbackAssets] + : [], ), preloadMatch3DGeneratedRuntimeAssets: vi.fn(() => Promise.resolve()), })); @@ -1040,20 +1048,16 @@ vi.mock('../match3d-runtime/Match3DRuntimeShell', () => ({ }
- { - generatedBackgroundAsset?.imageSrc?.trim() || - generatedBackgroundAsset?.imageObjectKey?.trim() - ? 1 - : 0 - } + {generatedBackgroundAsset?.imageSrc?.trim() || + generatedBackgroundAsset?.imageObjectKey?.trim() + ? 1 + : 0}
- { - generatedBackgroundAsset?.containerImageSrc?.trim() || - generatedBackgroundAsset?.containerImageObjectKey?.trim() - ? 1 - : 0 - } + {generatedBackgroundAsset?.containerImageSrc?.trim() || + generatedBackgroundAsset?.containerImageObjectKey?.trim() + ? 1 + : 0}