From 8f460feb41edc95d339529d9d3397631b989e1df Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 7 Jun 2026 14:14:16 +0800 Subject: [PATCH] fix: route recommend puzzle next through feed --- .hermes/shared-memory/decision-log.md | 4 +- .hermes/shared-memory/pitfalls.md | 2 +- ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 8 +- .../src/contracts/puzzleRuntimeSession.ts | 1 - .../PlatformEntryFlowShellImpl.tsx | 21 ++-- ...gEntryFlowShell.agent.interaction.test.tsx | 97 ++++++++----------- .../RpgEntryHomeView.recharge.test.tsx | 46 ++++----- src/components/rpg-entry/RpgEntryHomeView.tsx | 8 +- .../rpg-entry/useRpgEntryBootstrap.ts | 8 +- .../puzzle-runtime/puzzleRuntimeClient.ts | 6 +- .../recommendedRuntimeGuestLaunch.test.ts | 9 +- 11 files changed, 84 insertions(+), 126 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 83bd37c1..4fb35137 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -240,9 +240,9 @@ ## 2026-05-26 推è页拼图下一关 pending æ—¶ä¿ç•™å½“å‰è¿è¡Œæ€ - 背景:推èé¡µåµŒå…¥æ‹¼å›¾åœ¨ç‚¹å‡»â€œä¸‹ä¸€å…³â€æ—¶ï¼Œ`advancePuzzleNextLevel` çš„æœåŠ¡ç«¯è¯·æ±‚ä¼šçŸ­æš‚å¤„äºŽ pending。旧逻辑把推èå¡çš„ `isStartingRecommendEntry` 和拼图局部 busy 混在一起,导致外层直接切回“加载中...â€ï¼ŒæŠŠå½“å‰ `PuzzleRuntimeShell` 一起å¸è½½ï¼Œè§†è§‰ä¸Šåƒæ˜¯åˆ‡å…³é—ªå›žã€‚ -- 决策:推è页嵌入拼图切关 pending 期间必须ä¿ç•™å½“å‰è¿è¡Œæ€ä¸Žæ£‹ç›˜ï¼Œåªè®©æ‹¼å›¾å£³å†…部 busy è¡¨çŽ°æ‰¿æŽ¥åŒæ­¥ï¼›`isStartingRecommendEntry` åªè¡¨ç¤ºæŽ¨è作å“尚未真正å¯åŠ¨å‡ºæ¥ï¼Œä¸å†æŠŠå·²æœ‰åµŒå…¥æ‹¼å›¾ run 的局部 busy ä¸€å¹¶å½“æˆæ•´å¡åŠ è½½æ€ã€‚若下一关è½åˆ°ç›¸ä¼¼ä½œå“,å‰ç«¯è¿˜å¿…须把新作å“写回推èç¼“å­˜å¹¶åŒæ­¥ `activeRecommendEntryKey`,é¿å…è¿è¡Œæ€è¿›å…¥æ–°ä½œå“但推èå¡å…ƒä¿¡æ¯ã€åˆ†äº« / 点赞 / 改造和åŽç»­â€œä¸‹ä¸€ä¸ªâ€ä»é”šå®šæ—§ä½œå“ï¼›ä½†è¿™ä¸ªåŒæ­¥ä»å±žäºŽåŒä¸€ä¸ª run 内部推进,ä¸å¾—è§¦å‘æŽ¨è rail 切å¡åŠ¨ç”»ã€çºµå‘ä½ç§»æˆ–å¯åЍå°é¢é‡ç½®ã€‚ +- 决策:推è页嵌入拼图切关 pending 期间必须ä¿ç•™å½“å‰è¿è¡Œæ€ä¸Žæ£‹ç›˜ï¼Œåªè®©æ‹¼å›¾å£³å†…部 busy è¡¨çŽ°æ‰¿æŽ¥åŒæ­¥ï¼›`isStartingRecommendEntry` åªè¡¨ç¤ºæŽ¨è作å“尚未真正å¯åŠ¨å‡ºæ¥ï¼Œä¸å†æŠŠå·²æœ‰åµŒå…¥æ‹¼å›¾ run 的局部 busy ä¸€å¹¶å½“æˆæ•´å¡åŠ è½½æ€ã€‚推è页拼图“下一关â€å¿…须走推è页统一相邻作å“åˆ‡æ¢æµç¨‹ï¼Œå‰ç«¯ä¸å¾—传递 `preferSimilarWork`,也ä¸å¾—让拼图 runtime è‡ªå·±æŠŠå½“å‰ run handoff 到其它作å“。 - å½±å“范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€æŽ¨è页拼图切关测试与平å°é“¾è·¯æ–‡æ¡£ã€‚ -- éªŒè¯æ–¹å¼ï¼šç‚¹å‡»æŽ¨è页拼图“下一关â€åŽï¼Œåœ¨ `advancePuzzleNextLevel` 未返回å‰ï¼Œé¡µé¢ä»åº”ä¿ç•™ `puzzle-board`,且ä¸å‡ºçް `加载中...` å ä½ï¼›è¿”回相似作å“åŽï¼Œå½“剿ލèå¡çš„ `作å“ä¿¡æ¯` åº”æ˜¾ç¤ºæ–°ä½œå“æ ‡é¢˜ã€‚ +- éªŒè¯æ–¹å¼ï¼šç‚¹å‡»æŽ¨è页拼图“下一关â€åŽï¼Œé¡µé¢å…ˆä¿ç•™ `puzzle-board`,且ä¸å‡ºçް `加载中...` å ä½ï¼›éšåŽåº”调用推è页统一下一作å“å¯åŠ¨é€»è¾‘ï¼Œè€Œä¸æ˜¯è°ƒç”¨ `advancePuzzleNextLevel(...)`。 - å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 ## 2026-05-24 创作入å£é¡µ banner 曾固定主题赛 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index a4c67529..c3e5ad97 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -1532,7 +1532,7 @@ ## 推è页嵌入拼图通关结算ä¸è¦æ”¾åœ¨è¿è¡Œæ€å†…部 absolute 层 -- 现象:推è页里玩拼图通关åŽï¼Œç»“ç®—é¢æ¿åªæ˜¾ç¤ºä¸ŠåŠéƒ¨åˆ†ï¼ŒæŽ’行榜ã€ä¸‹ä¸€å…³æŒ‰é’®æˆ–相似作å“å¡è¢«æˆªæ–­ã€‚ +- 现象:推è页里玩拼图通关åŽï¼Œç»“ç®—é¢æ¿åªæ˜¾ç¤ºä¸ŠåŠéƒ¨åˆ†ï¼ŒæŽ’行榜或下一关按钮被截断。 - 原因:推è页把è¿è¡Œæ€æ”¾åœ¨æ»‘动作å“å¡çš„视觉区内,`platform-recommend-swipe-page`ã€`platform-recommend-swipe-card__visual` å’Œ `platform-recommend-runtime-viewport` 都是 `overflow: hidden`ï¼›æ‹¼å›¾é€šå…³ç»“ç®—å¦‚æžœä»æ˜¯è¿è¡Œæ€å†…部 `absolute inset-0` 弹层,就åªèƒ½åœ¨åŠå±å¡ç‰‡åŒºåŸŸé‡Œæ˜¾ç¤ºã€‚ - 处ç†ï¼š`PuzzleRuntimeShell` 在 `embedded` 模å¼ä¸‹æŠŠé€šå…³ç»“算层通过 portal 挂到 `document.body`,使用 `puzzle-runtime-modal-overlay--fixed` 页é¢çº§ fixed 浮层;éžåµŒå…¥æ€ç»§ç»­ä½¿ç”¨è¿è¡Œæ€å†…部覆盖层。 - 验è¯ï¼šè¿è¡Œ `npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "推è页嵌入拼图通关结算使用页é¢çº§æµ®å±‚é¿å…å¡ç‰‡è£å‰ª"`,确认弹层ä¸å†ä½äºŽ `.platform-recommend-runtime-viewport` 内。 diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index a9da1df2..561a690c 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -124,15 +124,15 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— - 拼图试玩和正å¼è¿è¡Œæ€åˆ·æ–°æ¢å¤ä¸å¤ç”¨åˆ›ä½œç§æœ‰ 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 时,返回ã€è®¾ç½®å’Œä¸‹ä¸€å…³çš„ç‚¹å‡»å®¹å™¨åªæä¾›é€æ˜Žç‚¹å‡»åŒºï¼Œä¸å†å åŠ é»˜è®¤ç™½è‰²åœ†å½¢åº•ã€èƒ¶å›Šä¸»æŒ‰é’®åº•或é¢å¤–文字;下一关按钮在通关弹窗和底部入å£ä¸­éƒ½ç›´æŽ¥ä½¿ç”¨ spritesheet è£åˆ‡å‡ºçš„ next ç´ æä½œä¸ºæŒ‰é’®æœ¬ä½“。底部æç¤ºã€åŽŸå›¾ã€å†»ç»“ä¸‰æžšç´ ææŒ‰æ£€æµ‹çŸ©å½¢çš„原始宽高比显示,ä¸èƒ½å¼ºè¡Œæ‹‰ä¼¸æˆæ­£åœ†æˆ–铺满整列。底部é“具区ä¸å†ä½¿ç”¨è¿žç‰‡èƒ¶å›ŠèƒŒæ™¯ï¼Œæç¤ºã€åŽŸå›¾ã€å†»ç»“三个按钮å‡åŒ€åˆ†å¸ƒï¼›è¿è¡Œæ€åªå±•ç¤ºæŒ‰é’®ç´ ææœ¬èº«ï¼Œä¸é¢å¤–å åŠ â€œæç¤º / 原图 / å†»ç»“â€æ–‡å­—。 -- 推èé¡µæœ¬èº«ä¸æ˜¯ç™»å½•é—¨ç¦å…¥å£ï¼Œæœªç™»å½•用户点击底部或侧边æ çš„æŽ¨è Tab 应直接进入嵌入è¿è¡Œæ€ï¼Œä¸ä¸»åŠ¨æ‰“å¼€ç™»å½•å¼¹çª—ã€‚æŽ¨è页嵌入è¿è¡Œæ€å¿…须按真实身份分æµï¼šå·²ç™»å½•用户或本地已有 access token 时,å¯åŠ¨æ‹¼å›¾å’ŒåŽç»­æŽ’行榜 / 下一关等正å¼è¯·æ±‚ç»§ç»­èµ°è´¦å· Bearerï¼›åªæœ‰ç¡®è®¤ä¸ºåŒ¿å访客时æ‰ç”³è¯·å¹¶é€ä¼  runtime guest token。`/api/runtime/puzzle/runs*` åŽç«¯ç»Ÿä¸€æŽ¥å— `RuntimePrincipal`,å¯è¯†åˆ«è´¦å·ç”¨æˆ·å’ŒåŒ¿å runtime guest;推èå¡ç‰‡çš„åŽå°è¯»å†™è¯·æ±‚ä»ä½¿ç”¨ local auth impact,é¿å…å•å¡ 401 清空整站登录æ€ã€‚创作ã€ä¸ªäººä½œå“ã€åˆ é™¤ã€å‘布ã€Remix ç­‰è´¦å·æˆ–所有æƒåŠ¨ä½œä»ä¿æŒæ™®é€šç”¨æˆ·é‰´æƒã€‚ +- 推èé¡µæœ¬èº«ä¸æ˜¯ç™»å½•é—¨ç¦å…¥å£ï¼Œå¹³å°é¦–页默认è½ç‚¹ä¹Ÿæ˜¯æŽ¨è页;未登录用户点击底部或侧边æ çš„æŽ¨è Tab 应直接进入嵌入è¿è¡Œæ€ï¼Œä¸ä¸»åŠ¨æ‰“å¼€ç™»å½•å¼¹çª—ã€‚æŽ¨è页嵌入è¿è¡Œæ€å¿…须按真实身份分æµï¼šå·²ç™»å½•用户或本地已有 access token 时,å¯åŠ¨æ‹¼å›¾å’ŒåŽç»­æŽ’行榜 / 下一关等正å¼è¯·æ±‚ç»§ç»­èµ°è´¦å· Bearerï¼›åªæœ‰ç¡®è®¤ä¸ºåŒ¿å访客时æ‰ç”³è¯·å¹¶é€ä¼  runtime guest token。`/api/runtime/puzzle/runs*` åŽç«¯ç»Ÿä¸€æŽ¥å— `RuntimePrincipal`,å¯è¯†åˆ«è´¦å·ç”¨æˆ·å’ŒåŒ¿å runtime guest;推èå¡ç‰‡çš„åŽå°è¯»å†™è¯·æ±‚ä»ä½¿ç”¨ local auth impact,é¿å…å•å¡ 401 清空整站登录æ€ã€‚创作ã€ä¸ªäººä½œå“ã€åˆ é™¤ã€å‘布ã€Remix ç­‰è´¦å·æˆ–所有æƒåŠ¨ä½œä»ä¿æŒæ™®é€šç”¨æˆ·é‰´æƒã€‚ - 拼图è¿è¡Œæ€æ£‹ç›˜ä¸å åŠ åˆ†å—è’™ç‰ˆã€æè¾¹ã€é˜´å½±ã€é€‰ä¸­åº•色或åˆå¹¶å— SVG 轮廓;拼图片本体需è¦è£åˆ‡ä¸ºåœ†è§’形状,å•å—使用独立圆角è£åˆ‡ï¼Œåˆå¹¶å—使用 SVG 原生 `clipPath` è£åˆ‡æ•´ä½“外轮廓,外凸角和内凹角分别计算åŠå¾„,内凹角åŠå¾„è¦æ¯”外凸角更明显以é¿å…手机 WebView 中看起æ¥ä»æ˜¯ç›´è§’。原图é“å…·åªåœ¨ç”¨æˆ·ä¸»åŠ¨ç¡®è®¤åŽæ‰“开独立原图查看层,ä¸åœ¨å½“剿‹¼å›¾æ£‹ç›˜ä¸Šå åŠ åŽŸå›¾ã€‚ - 拼图è¿è¡Œæ€æ‹–æ‹½å¿…é¡»å®Œå…¨è·Ÿéšæ‰‹æŒ‡æˆ–é¼ æ ‡ä½ç½®ï¼Œ`pointermove` æœŸé—´å³æ—¶å†™å…¥å¯è§æ‹¼å—çš„ transform,ä¸ä¾èµ–等待åŽç«¯å›žåŒ…ã€React 釿¸²æŸ“或下一帧动画队列;进入拖动åŽä¸å±•示拼å—é€‰ä¸­æ€æˆ–â€œå·²é€‰æ‹©â€æç¤ºï¼Œæ¾æ‰‹åŽå†æäº¤ç›®æ ‡æ ¼åŒæ­¥è§„则真相。 - 拼图è¿è¡Œæ€çš„æç¤ºã€è®¾ç½®ç­‰ç‚¹å‡»å¼¹å±‚è·Ÿéšå½“å‰è¿è¡Œæ€ä¸»è‰²ä¸»é¢˜ï¼Œä½¿ç”¨æ™®é€šåœ†è§’ä¸»é¢˜é¢æ¿ï¼Œä¸å¤ç”¨åƒç´ ä¹å®«æ ¼ç´ ææ¡†ã€‚ - 拼图è¿è¡Œæ€å£³å±‚自身è¦è¡¥é½ `platform-ui-shell` / `platform-theme` / `platform-theme--light|dark`,ä¸èƒ½ä¾èµ–外层平å°å£³æ¥æä¾›ä¸»é¢˜å˜é‡ï¼›`/puzzle` 直达页和平å°å†…嵌页都必须渲染åŒä¸€å¥—主题语义类。 - 拼图è¿è¡Œæ€é¡¶éƒ¨å…³å¡ä¿¡æ¯é‡‡ç”¨æ¸¸æˆåŒ–铭牌样å¼ï¼šæ©˜æ£•横å‘å…³å¡å牌承载 `第 N å…³` 和关å¡å,左侧固定使用 `media/logo.png` å¡é€šå½¢è±¡ï¼›å€’计时作为下挂米白å°ç‰Œç‹¬ç«‹æ˜¾ç¤ºï¼Œç´§è´´é“­ç‰Œä½†ä¸é®æŒ¡æ£‹ç›˜ã€‚该样å¼åªæ”¹å˜è¿è¡Œæ€ HUD è§†è§‰ï¼Œä¸æ”¹å˜è®¡æ—¶ã€æš‚åœã€å¤±è´¥åŒæ­¥æˆ–å…³å¡æŽ¨è¿›è§„åˆ™ã€‚ - 拼图è¿è¡Œæ€è¿›è¡Œä¸­å…³å¡çš„ `elapsedMs` 仿˜¯ç»“ç®—å­—æ®µï¼Œè®¾ç½®é¢æ¿çš„“当å‰ç”¨æ—¶â€å¿…须按 `startedAtMs`ã€æš‚åœç´¯è®¡å’Œå†»ç»“累计实时派生;ä¸è¦ç›´æŽ¥æŠŠè¿›è¡Œä¸­çš„ `currentLevel.elapsedMs` 当作展示值。 -- 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œé€šå…³ç»“算弹层必须挂到页é¢çº§ fixed 浮层,ä¸èƒ½ç•™åœ¨æŽ¨èå¡ç‰‡è§†è§‰åŒºå†…çš„ absolute 覆盖层;推è页滑动å¡ç‰‡å’Œè¿è¡Œæ€è§†å£éƒ½ä½¿ç”¨ `overflow: hidden`,åŠå±å†…容区会è£å‰ªæŽ’行榜ã€ä¸‹ä¸€å…³æŒ‰é’®å’Œç›¸ä¼¼ä½œå“å¡ã€‚ -- 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œâ€œä¸‹ä¸€å…³â€åº”优先切到相似作å“ï¼›å¦‚æžœå½“å‰æŽ¨è候选为空,æ‰å›žé€€åˆ°åŒä½œå“下一关,é¿å…åŒ¿åæŽ¨èæµåœ¨å¤šå…³å¡ä½œå“上æŒç»­åœç•™åœ¨åŒä¸€ä½œå“内。下一关请求 pending 期间必须ä¿ç•™å½“å‰ `PuzzleRuntimeShell` 和棋盘,ä¸å¾—把推è塿•´ä½“切回 `加载中...` å ä½æ€ï¼›å±€éƒ¨åŒæ­¥çжæ€ç”±æ‹¼å›¾è¿è¡Œæ€è‡ªå·±çš„ busy 表现承接。åŽç«¯è¿”回的新关å¡å±žäºŽå…¶å®ƒä½œå“时,å‰ç«¯å¿…é¡»åŒæ­¥ `selectedPuzzleDetail`ã€æŽ¨è页 `puzzleGalleryEntries` 缓存和 `activeRecommendEntryKey`,让底部作å“ä¿¡æ¯ã€åˆ†äº« / 点赞 / 改造和下一次“下一个â€åŸºå‡†éƒ½æŒ‡å‘新作å“;但这ä»å±žäºŽåŒä¸€ä¸ª runtime run 内部推进,ä¸èƒ½è§¦å‘推è rail 切å¡åŠ¨ç”»ã€çºµå‘ä½ç§»æˆ–å¯åЍå°é¢é‡ç½®ï¼Œå·²æŒ‚载且 ready çš„è¿è¡Œæ€ç”»é¢åº”ä¿æŒç¨³å®šï¼Œåªé™é»˜æ›´æ–°ä½œå“ä¿¡æ¯å’Œæ“作基准。 +- 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œé€šå…³ç»“算弹层必须挂到页é¢çº§ fixed 浮层,ä¸èƒ½ç•™åœ¨æŽ¨èå¡ç‰‡è§†è§‰åŒºå†…çš„ absolute 覆盖层;推è页滑动å¡ç‰‡å’Œè¿è¡Œæ€è§†å£éƒ½ä½¿ç”¨ `overflow: hidden`,åŠå±å†…容区会è£å‰ªæŽ’行榜和下一关按钮。 +- 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œâ€œä¸‹ä¸€å…³â€å¿…须走推è页统一相邻作å“åˆ‡æ¢æµç¨‹ï¼Œä¸å¾—由拼图 runtime 自己传递 `preferSimilarWork` 或ç§è‡ªæŠŠå½“å‰ run handoff 到其它拼图作å“。点击åŽåº”与推è页底部“下一个â€ä½¿ç”¨åŒä¸€å¥— `activeRecommendEntryKey` / 推è队列切æ¢å’Œæ–°ä½œå“å¯åŠ¨è¯­ä¹‰ï¼ŒæŽ¨è塿 ‡é¢˜ã€åˆ†äº« / 点赞 / 改造基准都由统一推è切æ¢ç»“果决定。切æ¢å‘èµ·å‰ä»å¿…é¡»ä¿ç•™å½“å‰ `PuzzleRuntimeShell` 和棋盘,ä¸å¾—把推è塿•´ä½“切回 `加载中...` å ä½æ€ï¼›åŽç»­å±€éƒ¨åŒæ­¥çжæ€ç”±æŽ¨è页å¯åŠ¨æ–°ä½œå“的统一 busy 表现承接。 - 推è页作å“ä¿¡æ¯åŒºçš„分享按钮统一唤起å‘布分享弹窗 `PublishShareModal`,ä¸åœ¨æŽ¨èå¡å†…部å•独拼接分享文案或åªåšå‰ªè´´æ¿å¤åˆ¶å馈;拼图推è作å“的分享链接继续沿用 `/gallery/puzzle/detail?work=...`,其它统一公开作å“默认走 `/works/detail?work=...`。 - 推è页里的拼图作å“如果从è¿è¡Œæ€è¿›å…¥â€œæ”¹é€ â€ç»“果页,返回平å°åŽè¦æ¸…掉推è嵌入æ€çš„ `activeRecommendEntryKey` / `activeRecommendRuntimeKind` / `isStartingRecommendEntry`,å†é‡æ–°æŒ‰æŽ¨è页自动å¯åŠ¨é€»è¾‘è¿›å…¥ä½œå“,ä¸èƒ½å¤ç”¨å·²ç»è¢«æ¸…空的旧 `puzzleRun`。 - 拼图è¿è¡Œæ€å…许å‰ç«¯ä½Žå»¶è¿Ÿäº¤äº’è¡¨çŽ°ï¼Œä½†é€šå…³ã€æŽ’è¡Œæ¦œã€å¥–励和作å“状æ€ä»ä»¥åŽç«¯ç¡®è®¤ä¸ºå‡†ã€‚ @@ -175,7 +175,7 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— è·³ä¸€è·³ä½œå“æž¶åˆ é™¤å…¥å£å¿…须走 `/api/creation/jump-hop/works/{profile_id}`,并通过 SpacetimeDB åŒæ­¥åˆ é™¤ work profileã€æº sessionã€è¿è¡Œæ€ run 与事件,å†åˆ·æ–°ä½œå“架和公开广场;ä¸å¾—åªåšå‰ç«¯æœ¬åœ°éšè—。 -推èé¡µåŒ¿åæ¸¸çŽ©ä¸å†é™å®šä¸ºè·³ä¸€è·³ã€‚移动端一级 `推è` Tab 是内嵌è¿è¡Œæ€åˆ·å¡æµï¼Œä¼šè‡ªåŠ¨é€‰æ‹©æŽ¨è作å“å¹¶å¯åŠ¨å¯¹åº”çŽ©æ³•ï¼›æ¡Œé¢ç«¯é¦–页ä¸å¯åŠ¨è¿™å¥—ç§»åŠ¨æŽ¨èè¿è¡Œæ€ï¼Œè€Œæ˜¯æ¸²æŸ“桌é¢å‘现壳,展示 `今日游æˆ`ã€`推è`ã€`作å“分类` 等桌é¢å†…容。推è页候选顺åºç”±å‰ç«¯è½»é‡æŽ¨è算法 `platformRecommendation.ts` 统一生æˆï¼šå…ˆæŒ‰å…¬å¼€ä½œå“ key 去é‡ï¼Œå†ä½¿ç”¨å…¬å¼€è¯»æ¨¡åž‹å·²æœ‰çš„ç²¾é€‰æ¥æºã€è¿‘ 7 日游玩ã€ç‚¹èµžã€æ”¹é€ ã€æ€»æ¸¸çŽ©ã€å‘布时间新鲜度ã€å°é¢å’Œæ ‡ç­¾å®Œæ•´åº¦åšç¡®å®šæ€§è¯„分,最åŽä¼˜å…ˆäº¤é”™ä¸åŒçŽ©æ³•ç±»åž‹ï¼›åªè¦è¿˜æœ‰å…¶å®ƒçŽ©æ³•å€™é€‰ï¼Œå°±ä¸è¦è¿žç»­æŽ¨èåŒä¸€çŽ©æ³•ï¼Œåªæœ‰å€™é€‰æ± å·²æ²¡æœ‰å…¶å®ƒçŽ©æ³•æ—¶æ‰å…许åŒçŽ©æ³•ç›¸é‚»ã€‚è¯¥ç®—æ³•ä¸å¾—新增å‰ç«¯ä¸šåŠ¡çœŸç›¸æˆ–ç»•è¿‡å…¬å¼€ä½œå“ read model。断点事实统一走 `platformEntryResponsive.ts` çš„ `usePlatformDesktopLayout()`,平å°å£³å’Œé¦–页视图必须共用åŒä¸€ä¸ªåˆ¤æ–­ï¼Œé¿å…桌é¢å‘现页与移动推èé¡µåŒæ—¶æŒ‚è½½ã€é‡å¤è§¦å‘请求或å¯åЍè¿è¡Œæ€ã€‚移动端推è页å¯åŠ¨æˆ–åˆ‡æ¢ä½œå“时先展示当å‰ä½œå“å°é¢ï¼ŒåµŒå…¥ runtime 在å°é¢ä¸‹å±‚åŠ è½½ï¼›åªæœ‰å¯¹åº”è¿è¡Œæ€ run / profile 已准备且 lazy runtime ç»„ä»¶å®ŒæˆæŒ‚è½½åŽï¼Œå°é¢æ‰æ¸éšï¼Œä¸åœ¨ä¸­é€”å±•ç¤ºâ€œåŠ è½½ä¸­â€æ–‡æ¡ˆã€‚拼图下一关在åŒä¸€ä¸ª run å†…æŽ¨è¿›åˆ°ç›¸ä¼¼ä½œå“æ—¶ä¸è§†ä¸ºæŽ¨è作å“切æ¢ï¼Œä¸èƒ½é‡æ–°æ˜¾ç¤ºå¯åЍå°é¢ã€‚推è页嵌入è¿è¡Œæ€å¯åŠ¨æ—¶æŒ‰çœŸå®žèº«ä»½åˆ†æµï¼šå·²ç™»å½•用户或本地已有 access token æ—¶ç»§ç»­ä½¿ç”¨è´¦å· Bearer,但请求选项必须是 local auth impact,é¿å…å•å¡ 401 清空整站登录æ€ï¼›åªæœ‰ç¡®è®¤ä¸ºåŒ¿å访客时æ‰ç”³è¯·çŸ­æœŸ Runtime Guest Tokenï¼Œå¹¶åªæŠŠå®ƒä½œä¸ºå±€éƒ¨è¯·æ±‚å¤´ä¼ ç»™è¿è¡Œæ€å®¢æˆ·ç«¯ï¼Œä¸å†™å…¥å…¨å±€ç™»å½•æ€ã€ä¸è§¦å‘ refreshï¼Œä¹Ÿä¸æŠŠåŒ¿åæµé‡ä¼ªè£…æˆæ™®é€šç”¨æˆ·ã€‚当å‰è¦†ç›–矩阵为:跳一跳ã€è§†è§‰å°è¯´ã€æŠ“大鹅 Match3Dã€æ–¹æ´žæŒ‘æˆ˜ã€æ‹¼å›¾ã€æ•²æœ¨é±¼ã€å¤§é±¼åƒå°é±¼ã€æ±ªæ±ªå£°æµªã€‚æ¯ä¸ªæ¨¡æ¿çš„å¯åŠ¨è¯·æ±‚ã€æŽ¨è页内åŽç»­è¿è¡Œæ€åŠ¨ä½œä»¥åŠéœ€è¦ä¸ŠæŠ¥çš„ play/finish/leaderboard/next-level 类请求,都必须继续按该身份分æµï¼›å…¬å¼€è¯»å–å…¥å£ä»å¯åŒ¿å读å–,创作ã€ä¸ªäººä½œå“ã€åˆ é™¤ã€å‘布ã€Remix 等账å·/所有æƒåŠ¨ä½œä»ä¿æŒæ™®é€šç”¨æˆ·é‰´æƒã€‚ +推èé¡µåŒ¿åæ¸¸çŽ©ä¸å†é™å®šä¸ºè·³ä¸€è·³ã€‚移动端一级 `推è` Tab 是内嵌è¿è¡Œæ€åˆ·å¡æµï¼Œä¼šè‡ªåŠ¨é€‰æ‹©æŽ¨è作å“å¹¶å¯åŠ¨å¯¹åº”çŽ©æ³•ï¼›æ¡Œé¢ç«¯é¦–页ä¸å¯åŠ¨è¿™å¥—ç§»åŠ¨æŽ¨èè¿è¡Œæ€ï¼Œè€Œæ˜¯æ¸²æŸ“桌é¢å‘现壳,展示 `今日游æˆ`ã€`推è`ã€`作å“分类` 等桌é¢å†…容。推è页候选顺åºç”±å‰ç«¯è½»é‡æŽ¨è算法 `platformRecommendation.ts` 统一生æˆï¼šå…ˆæŒ‰å…¬å¼€ä½œå“ key 去é‡ï¼Œå†ä½¿ç”¨å…¬å¼€è¯»æ¨¡åž‹å·²æœ‰çš„ç²¾é€‰æ¥æºã€è¿‘ 7 日游玩ã€ç‚¹èµžã€æ”¹é€ ã€æ€»æ¸¸çŽ©ã€å‘布时间新鲜度ã€å°é¢å’Œæ ‡ç­¾å®Œæ•´åº¦åšç¡®å®šæ€§è¯„分,最åŽä¼˜å…ˆäº¤é”™ä¸åŒçŽ©æ³•ç±»åž‹ï¼›åªè¦è¿˜æœ‰å…¶å®ƒçŽ©æ³•å€™é€‰ï¼Œå°±ä¸è¦è¿žç»­æŽ¨èåŒä¸€çŽ©æ³•ï¼Œåªæœ‰å€™é€‰æ± å·²æ²¡æœ‰å…¶å®ƒçŽ©æ³•æ—¶æ‰å…许åŒçŽ©æ³•ç›¸é‚»ã€‚è¯¥ç®—æ³•ä¸å¾—新增å‰ç«¯ä¸šåŠ¡çœŸç›¸æˆ–ç»•è¿‡å…¬å¼€ä½œå“ read model。断点事实统一走 `platformEntryResponsive.ts` çš„ `usePlatformDesktopLayout()`,平å°å£³å’Œé¦–页视图必须共用åŒä¸€ä¸ªåˆ¤æ–­ï¼Œé¿å…桌é¢å‘现页与移动推èé¡µåŒæ—¶æŒ‚è½½ã€é‡å¤è§¦å‘请求或å¯åЍè¿è¡Œæ€ã€‚移动端推è页å¯åŠ¨æˆ–åˆ‡æ¢ä½œå“时先展示当å‰ä½œå“å°é¢ï¼ŒåµŒå…¥ runtime 在å°é¢ä¸‹å±‚åŠ è½½ï¼›åªæœ‰å¯¹åº”è¿è¡Œæ€ run / profile 已准备且 lazy runtime ç»„ä»¶å®ŒæˆæŒ‚è½½åŽï¼Œå°é¢æ‰æ¸éšï¼Œä¸åœ¨ä¸­é€”å±•ç¤ºâ€œåŠ è½½ä¸­â€æ–‡æ¡ˆã€‚推è页内拼图通关åŽçš„“下一关â€å±žäºŽæŽ¨è页统一切å¡å…¥å£ï¼Œä¸èƒ½å¤ç”¨æ‹¼å›¾ runtime çš„è·¨ä½œå“ handoff,也ä¸èƒ½ç›´æŽ¥æŠŠå½“å‰ run 改写到å¦ä¸€ä¸ªä½œå“ï¼›`activeRecommendEntryKey` åªèƒ½ç”±æŽ¨è页统一选择下一作å“åŽæ›´æ–°ã€‚推è页嵌入è¿è¡Œæ€å¯åŠ¨æ—¶æŒ‰çœŸå®žèº«ä»½åˆ†æµï¼šå·²ç™»å½•用户或本地已有 access token æ—¶ç»§ç»­ä½¿ç”¨è´¦å· Bearer,但请求选项必须是 local auth impact,é¿å…å•å¡ 401 清空整站登录æ€ï¼›åªæœ‰ç¡®è®¤ä¸ºåŒ¿å访客时æ‰ç”³è¯·çŸ­æœŸ Runtime Guest Tokenï¼Œå¹¶åªæŠŠå®ƒä½œä¸ºå±€éƒ¨è¯·æ±‚å¤´ä¼ ç»™è¿è¡Œæ€å®¢æˆ·ç«¯ï¼Œä¸å†™å…¥å…¨å±€ç™»å½•æ€ã€ä¸è§¦å‘ refreshï¼Œä¹Ÿä¸æŠŠåŒ¿åæµé‡ä¼ªè£…æˆæ™®é€šç”¨æˆ·ã€‚当å‰è¦†ç›–矩阵为:跳一跳ã€è§†è§‰å°è¯´ã€æŠ“大鹅 Match3Dã€æ–¹æ´žæŒ‘æˆ˜ã€æ‹¼å›¾ã€æ•²æœ¨é±¼ã€å¤§é±¼åƒå°é±¼ã€æ±ªæ±ªå£°æµªã€‚æ¯ä¸ªæ¨¡æ¿çš„å¯åŠ¨è¯·æ±‚ã€æŽ¨è页内åŽç»­è¿è¡Œæ€åŠ¨ä½œä»¥åŠéœ€è¦ä¸ŠæŠ¥çš„ play/finish/leaderboard/next-level 类请求,都必须继续按该身份分æµï¼›å…¬å¼€è¯»å–å…¥å£ä»å¯åŒ¿å读å–,创作ã€ä¸ªäººä½œå“ã€åˆ é™¤ã€å‘布ã€Remix 等账å·/所有æƒåŠ¨ä½œä»ä¿æŒæ™®é€šç”¨æˆ·é‰´æƒã€‚ ## 敲木鱼 diff --git a/packages/shared/src/contracts/puzzleRuntimeSession.ts b/packages/shared/src/contracts/puzzleRuntimeSession.ts index 11d91c27..5e705782 100644 --- a/packages/shared/src/contracts/puzzleRuntimeSession.ts +++ b/packages/shared/src/contracts/puzzleRuntimeSession.ts @@ -136,7 +136,6 @@ export interface DragPuzzlePieceRequest { export interface AdvancePuzzleNextLevelRequest { targetProfileId?: string | null; - preferSimilarWork?: boolean; } export interface UsePuzzleRuntimePropRequest { diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 6f206be9..812e70ed 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -5808,10 +5808,10 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRun(null); setPuzzleRuntimeAuthMode('default'); setSelectedPuzzleDetail(null); - platformBootstrap.setPlatformTab(authUi?.user ? 'home' : 'category'); + platformBootstrap.setPlatformTab('home'); setSelectionStage('platform'); clearPuzzleRuntimeUrlState(); - }, [authUi?.user, platformBootstrap, setSelectionStage]); + }, [platformBootstrap, setSelectionStage]); useEffect(() => { if ( @@ -12566,10 +12566,6 @@ export function PlatformEntryFlowShellImpl({ ? await buildRecommendRuntimeGuestOptions() : {}; const targetProfileId = _target?.profileId?.trim() ?? ''; - const preferSimilarWork = - activeRecommendRuntimeKind === 'puzzle' && - puzzleRuntimeReturnStage === 'platform' && - puzzleRun.nextLevelMode === 'sameWork'; if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) { const itemPromise = selectedPuzzleDetail?.profileId === targetProfileId @@ -12609,13 +12605,10 @@ export function PlatformEntryFlowShellImpl({ puzzleRuntimeAuthMode === 'isolated' ? await advancePuzzleNextLevel( puzzleRun.runId, - preferSimilarWork ? { preferSimilarWork: true } : {}, + {}, runtimeGuestOptions, ) - : await advancePuzzleNextLevel( - puzzleRun.runId, - preferSimilarWork ? { preferSimilarWork: true } : {}, - ); + : await advancePuzzleNextLevel(puzzleRun.runId, {}); const nextProfileId = run.currentLevel?.profileId?.trim() ?? ''; if ( nextProfileId && @@ -16011,8 +16004,8 @@ export function PlatformEntryFlowShellImpl({ onDragPiece={(payload) => { void dragPuzzlePiece(payload); }} - onAdvanceNextLevel={(target) => { - void advancePuzzleLevel(target); + onAdvanceNextLevel={() => { + selectAdjacentRecommendRuntimeEntry(1, activeRecommendEntryKey); }} onRestartLevel={() => { void restartPuzzleCurrentLevel(); @@ -16266,9 +16259,9 @@ export function PlatformEntryFlowShellImpl({ squareHoleRun, submitBigFishInput, submitVisualNovelRuntimeAction, - advancePuzzleLevel, dragPuzzlePiece, restartPuzzleCurrentLevel, + selectAdjacentRecommendRuntimeEntry, setSquareHoleError, swapPuzzlePiecesInRun, syncPuzzleRuntimeTimeout, diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index a5e77bc8..cc7efee6 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -13,16 +13,16 @@ import type { CustomWorldAgentSessionSnapshot, CustomWorldWorkSummary, } from '../../../packages/shared/src/contracts/customWorldAgent'; +import type { + BabyObjectMatchDraft, + CreateBabyObjectMatchDraftRequest, +} from '../../../packages/shared/src/contracts/edutainmentBabyObject'; import type { JumpHopRuntimeRunSnapshotResponse, JumpHopWorkDetailResponse, JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, } from '../../../packages/shared/src/contracts/jumpHop'; -import type { - BabyObjectMatchDraft, - CreateBabyObjectMatchDraftRequest, -} from '../../../packages/shared/src/contracts/edutainmentBabyObject'; import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent'; import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime'; import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; @@ -71,7 +71,6 @@ import { submitBigFishInput, } from '../../services/big-fish-runtime'; import { listBigFishWorks } from '../../services/big-fish-works'; -import { jumpHopClient } from '../../services/jump-hop/jumpHopClient'; import { type CreationEntryConfig, fetchCreationEntryConfig, @@ -91,6 +90,7 @@ import { regenerateBabyObjectMatchDraftAssets, saveBabyObjectMatchDraft, } from '../../services/edutainment-baby-object'; +import { jumpHopClient } from '../../services/jump-hop/jumpHopClient'; import { match3dCreationClient } from '../../services/match3d-creation'; import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime'; import { @@ -334,10 +334,6 @@ const ISOLATED_RUNTIME_AUTH_OPTIONS = { notifyAuthStateChange: false, clearAuthOnUnauthorized: false, }; -const RECOMMEND_RUNTIME_AUTH_OPTIONS = { - ...ISOLATED_RUNTIME_AUTH_OPTIONS, - runtimeGuestToken: 'runtime-guest-token', -}; const LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS = ISOLATED_RUNTIME_AUTH_OPTIONS; function getPlatformTabPanel(tab: string) { @@ -7505,7 +7501,7 @@ test('logged out home recommendation next starts the next puzzle work', async () }); }); -test('home recommendation puzzle next level switches to similar work detail', async () => { +test('home recommendation puzzle next level uses unified recommend switching', async () => { const user = userEvent.setup(); const entryWork = { workId: 'puzzle-work-public-guest-1', @@ -7547,17 +7543,17 @@ test('home recommendation puzzle next level switches to similar work detail', as }, ], } satisfies PuzzleWorkSummary; - const similarWork = { + const nextRecommendWork = { ...entryWork, - workId: 'puzzle-work-similar-guest-1', - profileId: 'puzzle-profile-similar-guest-1', + workId: 'puzzle-work-public-guest-2', + profileId: 'puzzle-profile-public-guest-2', levelName: '风塔试炼', - summary: 'å¦ä¸€å¥—奇幻机关拼图。', + summary: 'å¦ä¸€å¥—æŽ¨èæ‹¼å›¾ã€‚', levels: [ { - levelId: 'similar-level-1', + levelId: 'next-recommend-level-1', levelName: '风塔试炼', - pictureDescription: '相似作å“首关。', + pictureDescription: '推è队列下一张拼图。', candidates: [], selectedCandidateId: null, coverImageSrc: null, @@ -7586,47 +7582,35 @@ test('home recommendation puzzle next level switches to similar work detail', as entryWork.profileId, entryWork.levelName, ); - const similarRun = { - ...buildMockPuzzleRun(similarWork.profileId, similarWork.levelName), - runId: clearedRun.runId, - entryProfileId: entryWork.profileId, - currentLevelIndex: 2, - currentLevel: { - ...buildMockPuzzleRun(similarWork.profileId, similarWork.levelName) - .currentLevel!, - runId: clearedRun.runId, - levelIndex: 2, - levelId: 'similar-level-1', - startedAtMs: Date.now(), - }, - }; + const nextRecommendRun = buildMockPuzzleRun( + nextRecommendWork.profileId, + nextRecommendWork.levelName, + ); vi.mocked(listPuzzleGallery).mockResolvedValue({ - items: [entryWork], + items: [entryWork, nextRecommendWork], }); vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({ - item: profileId === similarWork.profileId ? similarWork : entryWork, + item: profileId === nextRecommendWork.profileId ? nextRecommendWork : entryWork, })); - vi.mocked(startPuzzleRun).mockResolvedValue({ - run: { - ...startedRun, - currentLevel: { - ...startedRun.currentLevel!, - startedAtMs: Date.now(), + vi.mocked(startPuzzleRun).mockImplementation(async (payload) => { + const run = + payload.profileId === nextRecommendWork.profileId + ? nextRecommendRun + : startedRun; + return { + run: { + ...run, + currentLevel: { + ...run.currentLevel!, + startedAtMs: Date.now(), + }, }, - }, + }; }); vi.mocked(submitPuzzleLeaderboard).mockResolvedValue({ run: clearedRunWithSameWorkNext, }); - let resolveAdvancePuzzleNextLevel!: (value: { - run: PuzzleRunSnapshot; - }) => void; - vi.mocked(advancePuzzleNextLevel).mockReturnValue( - new Promise((resolve) => { - resolveAdvancePuzzleNextLevel = resolve; - }), - ); vi.mocked(swapLocalPuzzlePieces).mockReturnValue(clearedRun); render(); @@ -7655,24 +7639,23 @@ test('home recommendation puzzle next level switches to similar work detail', as await user.click(within(dialog).getByRole('button', { name: '下一关' })); await waitFor(() => { - expect(advancePuzzleNextLevel).toHaveBeenCalledWith(clearedRun.runId, { - preferSimilarWork: true, - }); + expect(startPuzzleRun).toHaveBeenCalledWith( + { + profileId: nextRecommendWork.profileId, + levelId: null, + }, + LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS, + ); }); + expect(advancePuzzleNextLevel).not.toHaveBeenCalled(); expect(screen.getByTestId('puzzle-board')).toBeTruthy(); expect(screen.queryByText('加载中...')).toBeNull(); - - resolveAdvancePuzzleNextLevel({ run: similarRun }); - await waitFor(() => { - expect(getPuzzleGalleryDetail).toHaveBeenCalledWith(similarWork.profileId); - }); expect( await screen.findByLabelText('风塔试炼 作å“ä¿¡æ¯', undefined, { timeout: 3000, }), ).toBeTruthy(); - expect(screen.getAllByText('风塔试炼').length).toBeGreaterThan(0); - expect(startPuzzleRun).toHaveBeenCalledTimes(1); + expect(startPuzzleRun).toHaveBeenCalledTimes(2); }); test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => { diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 4c467e98..684729f4 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -1039,7 +1039,7 @@ function renderStatefulLoggedOutHomeView( function StatefulLoggedOutHomeView() { const [activeTab, setActiveTab] = - useState('category'); + useState('home'); return ( { +test('logged out mobile shell defaults to recommend tab', () => { const { container } = renderStatefulLoggedOutHomeView({ latestEntries: [puzzlePublicEntry], }); const activePanel = container.querySelector('.platform-tab-panel--active'); - expect(activePanel?.id).toBe('platform-tab-panel-category'); - expect( - screen.getByPlaceholderText('æœç´¢ä½œå“å·ã€åç§°ã€ä½œè€…ã€æè¿°'), - ).toBeTruthy(); - expect(container.querySelector('.platform-mobile-topbar')).toBeTruthy(); + expect(activePanel?.id).toBe('platform-tab-panel-home'); expect( container.querySelector('.platform-mobile-entry-shell--recommend'), - ).toBeNull(); + ).toBeTruthy(); }); test('logged out recommend tab opens embedded runtime without login modal', async () => { - const user = userEvent.setup(); const { container, openLoginModal } = renderStatefulLoggedOutHomeView({ latestEntries: [puzzlePublicEntry], activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1', @@ -3667,10 +3662,6 @@ test('logged out recommend tab opens embedded runtime without login modal', asyn throw new Error('缺少底部导航'); } - await user.click( - within(bottomNav as HTMLElement).getByRole('button', { name: '推è' }), - ); - expect(openLoginModal).not.toHaveBeenCalled(); expect(container.querySelector('.platform-recommend-cover-only')).toBeNull(); expect(container.querySelector('.platform-mobile-topbar')).toBeNull(); @@ -3683,7 +3674,6 @@ test('logged out recommend tab opens embedded runtime without login modal', asyn }); test('logged out recommend runtime keeps detail callback idle', async () => { - const user = userEvent.setup(); const onOpenGalleryDetail = vi.fn(); const { openLoginModal } = renderStatefulLoggedOutHomeView({ latestEntries: [puzzlePublicEntry], @@ -3695,10 +3685,6 @@ test('logged out recommend runtime keeps detail callback idle', async () => { throw new Error('缺少底部导航'); } - await user.click( - within(bottomNav as HTMLElement).getByRole('button', { name: '推è' }), - ); - expect(openLoginModal).not.toHaveBeenCalled(); expect(screen.getByTestId('recommend-runtime')).toBeTruthy(); expect(onOpenGalleryDetail).not.toHaveBeenCalled(); @@ -3920,7 +3906,7 @@ test('mobile recommend startup keeps cover visible without loading copy', () => expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0); }); -test('mobile recommend next level keeps runtime visual stable when active work changes', async () => { +test('mobile recommend keeps runtime visual stable when active entry changes', async () => { const animationCallbacks: FrameRequestCallback[] = []; Object.defineProperty(window, 'requestAnimationFrame', { configurable: true, @@ -3944,18 +3930,18 @@ test('mobile recommend next level keeps runtime visual stable when active work c worldName: '当剿‹¼å›¾', coverImageSrc: 'current-cover.png', } satisfies PlatformPublicGalleryCard; - const similarEntry = { + const nextEntry = { ...puzzlePublicEntry, - workId: 'puzzle-work-similar-1', - profileId: 'puzzle-profile-similar-1', + workId: 'puzzle-work-next-1', + profileId: 'puzzle-profile-next-1', ownerUserId: 'user-feed-2', - publicWorkCode: 'PZ-SIMILAR1', - worldName: '相似拼图', - coverImageSrc: 'similar-cover.png', + publicWorkCode: 'PZ-NEXT1', + worldName: '下一张拼图', + coverImageSrc: 'next-cover.png', } satisfies PlatformPublicGalleryCard; const { rerender } = renderLoggedOutHomeView(vi.fn(), { - latestEntries: [firstEntry, similarEntry], + latestEntries: [firstEntry, nextEntry], activeRecommendEntryKey: 'puzzle:user-feed-1:puzzle-profile-feed-1', isRecommendRuntimeReady: true, }); @@ -3998,7 +3984,7 @@ test('mobile recommend next level keeps runtime visual stable when active work c saveEntries={[]} saveError={null} featuredEntries={[]} - latestEntries={[firstEntry, similarEntry]} + latestEntries={[firstEntry, nextEntry]} myEntries={[]} historyEntries={[]} profileDashboard={null} @@ -4013,7 +3999,7 @@ test('mobile recommend next level keeps runtime visual stable when active work c onOpenCreateTypePicker={vi.fn()} onOpenGalleryDetail={vi.fn()} recommendRuntimeContent={
} - activeRecommendEntryKey="puzzle:user-feed-2:puzzle-profile-similar-1" + activeRecommendEntryKey="puzzle:user-feed-2:puzzle-profile-next-1" isRecommendRuntimeReady onOpenLibraryDetail={vi.fn()} onSearchPublicCode={vi.fn()} @@ -4026,7 +4012,7 @@ test('mobile recommend next level keeps runtime visual stable when active work c ) as HTMLElement | null; expect(rail?.className).toContain('platform-recommend-swipe-rail--settled'); expect(rail?.style.transform).toBe('translate3d(0, 0px, 0)'); - expect(screen.getByLabelText('相似拼图 作å“ä¿¡æ¯')).toBeTruthy(); + expect(screen.getByLabelText('下一张拼图 作å“ä¿¡æ¯')).toBeTruthy(); expect( document.querySelector('.platform-recommend-runtime-cover')?.className, ).toContain('platform-recommend-runtime-cover--hidden'); @@ -4394,6 +4380,7 @@ test('mobile discover recommend feed only rotates the card closest to screen cen }); test('mobile discover recommend feed renders cover fallback for legacy browsers', async () => { + const user = userEvent.setup(); renderStatefulLoggedOutHomeView({ latestEntries: [ { @@ -4403,6 +4390,7 @@ test('mobile discover recommend feed renders cover fallback for legacy browsers' }, ], }); + await user.click(screen.getByRole('button', { name: 'å‘现' })); const discoverPanel = document.getElementById('platform-tab-panel-category'); if (!discoverPanel) { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index e5f62d21..525437ff 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -1019,8 +1019,8 @@ function RecommendRuntimeVisual({ } previousEntryKeyRef.current = activeEntryKey; setIsRuntimeMounted((currentValue) => { - // ä¸­æ–‡æ³¨é‡Šï¼šæ‹¼å›¾æŽ¨èæµâ€œä¸‹ä¸€å…³â€ä¼šåœ¨åŒä¸€ä¸ª run 内切到相似作å“ï¼› - // æ­¤æ—¶åªæ›´æ–°ä½œå“ä¿¡æ¯å’Œåˆ†äº«åŸºå‡†ï¼Œä¸åº”釿˜¾å°é¢é€ æˆè¿è¡Œæ€é—ªè·³ã€‚ + // 中文注释:推èè¿è¡Œæ€å·²æŒ‚è½½åŽï¼Œç”¨æˆ·åˆ‡æ¢æŽ¨è作å“åªæ›´æ–°ä½œå“ä¿¡æ¯ï¼› + // ä¸é‡æ˜¾å°é¢ï¼Œé¿å…å·² ready çš„è¿è¡Œæ€è§†è§‰é—ªè·³ã€‚ if (currentValue && !isStarting && isRuntimeReady) { return currentValue; } @@ -4425,9 +4425,9 @@ export function RpgEntryHomeView({ useEffect(() => { if (!visibleTabs.includes(activeTab)) { - onTabChange(isAuthenticated ? 'home' : 'category'); + onTabChange('home'); } - }, [activeTab, isAuthenticated, onTabChange, visibleTabs]); + }, [activeTab, onTabChange, visibleTabs]); useEffect(() => { if ( diff --git a/src/components/rpg-entry/useRpgEntryBootstrap.ts b/src/components/rpg-entry/useRpgEntryBootstrap.ts index ded70c37..2e149512 100644 --- a/src/components/rpg-entry/useRpgEntryBootstrap.ts +++ b/src/components/rpg-entry/useRpgEntryBootstrap.ts @@ -74,8 +74,7 @@ export function useRpgEntryBootstrap( PlatformBrowseHistoryEntry[] >([]); const [saveEntries, setSaveEntries] = useState([]); - const [platformTab, setPlatformTabState] = - useState('category'); + const [platformTab, setPlatformTabState] = useState('home'); const [platformError, setPlatformError] = useState(null); const [dashboardError, setDashboardError] = useState(null); const [historyError, setHistoryError] = useState(null); @@ -351,8 +350,8 @@ export function useRpgEntryBootstrap( !hasInitialAgentSession && !hasExplicitPlatformTabSelectionRef.current ) { - // 中文注释:新用户先进入å‘现页;推è页å¯ç›´æŽ¥è¿›å…¥ï¼ŒçœŸæ­£å—ä¿æŠ¤çš„åŠ¨ä½œå†å•独åšç™»å½•é—¨ç¦ã€‚ - setPlatformTabState(isAuthenticated ? 'home' : 'category'); + // 中文注释:新用户也先进入推è页;真正å—ä¿æŠ¤çš„åŠ¨ä½œå†å•独åšç™»å½•é—¨ç¦ã€‚ + setPlatformTabState('home'); } } finally { if (isActive) { @@ -369,7 +368,6 @@ export function useRpgEntryBootstrap( canReadProtectedData, getProfileDashboard, hasInitialAgentSession, - isAuthenticated, user, ]); diff --git a/src/services/puzzle-runtime/puzzleRuntimeClient.ts b/src/services/puzzle-runtime/puzzleRuntimeClient.ts index be9393ef..8c72b0e1 100644 --- a/src/services/puzzle-runtime/puzzleRuntimeClient.ts +++ b/src/services/puzzle-runtime/puzzleRuntimeClient.ts @@ -1,6 +1,6 @@ import type { - DragPuzzlePieceRequest, AdvancePuzzleNextLevelRequest, + DragPuzzlePieceRequest, PuzzleRunResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest, @@ -119,7 +119,7 @@ export async function dragPuzzlePieceOrGroup( } /** - * 进入推è出的下一关。 + * è¿›å…¥å½“å‰ run 的下一关。 */ export async function advancePuzzleNextLevel( runId: string, @@ -128,10 +128,8 @@ export async function advancePuzzleNextLevel( ) { const requestOptions = buildRuntimeGuestAuthOptions(options); const targetProfileId = payload.targetProfileId?.trim() ?? ''; - const preferSimilarWork = payload.preferSimilarWork === true; const requestPayload = { ...(targetProfileId ? { targetProfileId } : {}), - ...(preferSimilarWork ? { preferSimilarWork: true } : {}), }; const hasRequestPayload = Object.keys(requestPayload).length > 0; return requestJson( diff --git a/src/services/recommendedRuntimeGuestLaunch.test.ts b/src/services/recommendedRuntimeGuestLaunch.test.ts index eb5307c0..e7cd8345 100644 --- a/src/services/recommendedRuntimeGuestLaunch.test.ts +++ b/src/services/recommendedRuntimeGuestLaunch.test.ts @@ -13,8 +13,8 @@ vi.mock('./apiClient', async () => { }; }); -import { startBigFishRun } from './big-fish-runtime/bigFishRuntimeClient'; import { startBarkBattleRun } from './bark-battle-runtime/barkBattleRuntimeClient'; +import { startBigFishRun } from './big-fish-runtime/bigFishRuntimeClient'; import { startJumpHopRuntimeRun } from './jump-hop/jumpHopClient'; import { startMatch3DRun } from './match3d-runtime/match3dRuntimeClient'; import { @@ -130,10 +130,10 @@ describe('recommended runtime guest launch clients', () => { }, ); - it('puzzle next level can carry preferSimilarWork through the runtime guest request', async () => { + it('puzzle next level keeps the default current-run handoff without a request body', async () => { await advancePuzzleNextLevel( 'run-puzzle-1', - { preferSimilarWork: true }, + {}, { runtimeGuestToken: 'runtime-guest-token' }, ); @@ -144,11 +144,10 @@ describe('recommended runtime guest launch clients', () => { method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer runtime-guest-token', - 'Content-Type': 'application/json', }), - body: JSON.stringify({ preferSimilarWork: true }), }), ); + expect(init.body).toBeUndefined(); expect(options).toEqual( expect.objectContaining({ skipAuth: true,