From 50e335ba479c3ad16f5a1787fb49a048db09a126 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 6 Jun 2026 21:36:38 +0800 Subject: [PATCH] fix: polish platform creation flow interactions --- .hermes/shared-memory/pitfalls.md | 16 ++ ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 11 +- .../common/CreativeImageInputPanel.test.tsx | 112 +++++++++++++ .../common/CreativeImageInputPanel.tsx | 148 ++++++++++++++---- .../PlatformEntryFlowShellImpl.tsx | 55 ++++++- .../puzzle-result/PuzzleResultView.test.tsx | 13 ++ .../puzzle-result/PuzzleResultView.tsx | 2 + ...gEntryFlowShell.agent.interaction.test.tsx | 50 ++++++ .../RpgEntryHomeView.recharge.test.tsx | 12 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 60 +------ src/index.css | 29 +++- src/index.test.ts | 28 ++++ 12 files changed, 434 insertions(+), 102 deletions(-) diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 811f055b..f38cafb3 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -1876,3 +1876,19 @@ - 处ç†ï¼šå«ä¸­æ–‡æç¤ºè¯çš„ live 验è¯ä¼˜å…ˆå†™æˆ UTF-8 `.mjs` æ–‡ä»¶å†æ‰§è¡Œï¼Œæˆ–使用能确认 UTF-8 çš„è¿è¡Œå…¥å£ï¼›æ‰§è¡ŒåŽå…ˆæ£€æŸ¥æœ¬æ¬¡ `request.json` 是å¦ä¿ç•™çœŸå®žä¸­æ–‡ï¼Œå†åˆ¤æ–­ç”Ÿå›¾è´¨é‡ã€‚ä¸è¦åŸºäºŽ `????` prompt 生æˆçš„图片调整项目æç¤ºè¯ã€‚ - 验è¯ï¼šç”Ÿæˆå‰åŽæ£€æŸ¥ `request.json`,其中 `prompt` å­—æ®µåº”æ˜¾ç¤ºä¸­æ–‡è€Œä¸æ˜¯é—®å·ï¼›åŒä¸€æç¤ºè¯åœ¨ UTF-8 文件脚本下应能得到符åˆä¸»é¢˜çš„图。 - å…³è”:`.codex/skills/gpt-image-2-apimart/SKILL.md`ã€`server-rs/crates/api-server/src/jump_hop.rs`。 + +## 自动试玩退出ä¸è¦å›žåˆ°ç”Ÿæˆé¡µ + +- 现象:拼图è‰ç¨¿ç”Ÿæˆå®ŒæˆåŽè‡ªåŠ¨è¿›å…¥è¯•çŽ©ï¼Œç”¨æˆ·ä»Žè¯•çŽ©é€€å‡ºæˆ–ä½¿ç”¨ç³»ç»Ÿè¿”å›žæ—¶è½å›žç”Ÿæˆè¿›åº¦é¡µï¼Œé¡µé¢è¿˜æš´éœ²â€œé‡æ–°ç”Ÿæˆâ€æŒ‰é’®ã€‚ +- 原因:自动试玩å‰å¦‚果没有先把 `/creation/puzzle/result` å†™æˆ `/runtime/puzzle` çš„æµè§ˆå™¨åކå²å‰ä¸€ç«™ï¼Œç³»ç»Ÿè¿”回会命中旧的生æˆé¡µåކå²é¡¹ï¼›ä»…é è¿è¡Œæ€å†…部 `returnStage='puzzle-result'` åªèƒ½è¦†ç›–è¿è¡Œæ€æŒ‰é’®è¿”回,ä¸èƒ½è¦†ç›–æµè§ˆå™¨ / WebView 系统返回。 +- 处ç†ï¼šæ‰€æœ‰â€œç”Ÿæˆå®ŒæˆåŽè‡ªåŠ¨è¿›å…¥è‰ç¨¿è¯•玩â€çš„分支在 `openPuzzleRuntimeStage(...)` å‰éƒ½å¿…须调用结果页历å²å†™å…¥ helper,把 `/creation/puzzle/result` ä¸Žå½“å‰ `sessionId/profileId/workId` 写入历å²ï¼›è¿è¡Œæ€æŒ‰é’®è¿”回到 `puzzle-result` æ—¶ä¹ŸåŒæ­¥å†™å›žåˆ›ä½œæ¢å¤ query。 +- 验è¯ï¼š`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial and runtime back opens draft result"`。 +- å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## CreativeImageInputPanel 主图点击默认预览 + +- 现象:å¤ç”¨ `CreativeImageInputPanel` 的结果页 / 编辑页已有主图时,用户点击图片å´è§¦å‘上传,无法直接查看大图;ä¸åŒçŽ©æ³•è‹¥å„自手写上传按钮会让主图ã€åކå²å›¾ã€AI é‡ç»˜å’Œå‚è€ƒå›¾è¡Œä¸ºå†æ¬¡åˆ†å‰ã€‚ +- åŽŸå› ï¼šæ—§ä¸»å›¾å¡æ•´å¡æ˜¯ä¸Šä¼  label,缺少主图预览模å¼å’Œä¸Šä¼  / 历å²å…¥å£çš„æ˜¾å¼æŽ§åˆ¶å‚数。 +- 处ç†ï¼šé€šç”¨é¢æ¿å·²æœ‰ä¸»å›¾æ—¶é»˜è®¤ç‚¹å‡»ä¸»å›¾æ‰“开全å±é¢„览,上传 / æ›´æ¢æ”¶å£åˆ°å³ä¸‹è§’ `ImagePlus` 图标按钮;无图时ä»å…许点击空图å¡ä¸Šä¼ ã€‚调用方用 `canUploadMainImage` å’Œ `canUseImageHistory` åˆ†åˆ«æŽ§åˆ¶ä¸Šä¼ ä¸ŽåŽ†å²æŒ‰é’®ï¼Œä¸è¦å¤åˆ¶é¢æ¿æˆ–用样å¼é®æŒ¡æŒ‰é’®ã€‚ +- 验è¯ï¼š`npm run test -- src/components/common/CreativeImageInputPanel.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`。 +- å…³è”:`src/components/common/CreativeImageInputPanel.tsx`ã€`src/components/puzzle-result/PuzzleResultView.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index 630ff6e4..4b2381bd 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -1,6 +1,6 @@ # å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯ -更新时间:`2026-06-03` +更新时间:`2026-06-06` ## å¹³å°åˆ›ä½œå…¥å£ @@ -40,7 +40,7 @@ 默认工作å°åªæäº¤ç»“构化表å•ã€å›¾ç‰‡æ§½ä½å’Œé…ç½® payload,ä¸é»˜è®¤å¢žåŠ èŠå¤©è¾“å…¥åŒºã€æµå¼æ¶ˆæ¯åŒºæˆ–轻输入 Agent。确需åç¦»è¯¥æ¨¡å¼æ—¶ï¼Œå¿…须先在 PRD 和本文档写明例外原因ã€å½±å“范围和回退方å¼ï¼Œå†è¿›å…¥ç¼–ç ã€‚ -å•图资产编辑统一通过 `CreativeImageInputPanel` 承载上传ã€AI é‡ç»˜ã€å‚考图ã€åކå²å›¾å’Œåˆ é™¤ç¡®è®¤ï¼›æ–°çŽ©æ³•é¡µé¢ä¸å¾—é‡å¤æ‰‹å†™è¿™äº›äº¤äº’。系列素æå›¾é›†ç”Ÿæˆç»Ÿä¸€èµ°â€œæ‰¹é‡è§„划 -> sheet 生图 -> åŽç«¯åˆ‡å›¾ -> 逿˜ŽåŒ– -> OSS æŒä¹…化 -> 状æ€å›žå†™ -> 局部é‡ç”Ÿæˆâ€æµç¨‹ï¼ŒçŽ©æ³•åªæä¾› `sheetSpec`ã€`slotSpecs`ã€æç¤ºè¯å’Œå­—æ®µæ˜ å°„ï¼Œä¸æŠŠä»»ä¸€çŽ©æ³•ä¸“å±žç´ æ DTO 当作平å°é€šç”¨æ¨¡åž‹ã€‚ +å•图资产编辑统一通过 `CreativeImageInputPanel` 承载上传ã€AI é‡ç»˜ã€å‚考图ã€åކå²å›¾ã€ä¸»å›¾é¢„览和删除确认;新玩法页é¢ä¸å¾—é‡å¤æ‰‹å†™è¿™äº›äº¤äº’。主图已有图片时,默认点击图片打开全å±é¢„览,上传 / æ›´æ¢æ”¶å£åˆ°å³ä¸‹è§’ `ImagePlus` 图标按钮;无图时ä»å…许点击空图å¡ä¸Šä¼ ã€‚调用方åªèƒ½é€šè¿‡ `canUploadMainImage`ã€`canUseImageHistory` ç­‰å—æŽ§å‚æ•°å¼€å…³ä¸Šä¼ å’Œåކå²å…¥å£ï¼Œä¸å¾—用å¤åˆ¶ç»„件或样å¼é®æŒ¡æ”¹è¡Œä¸ºã€‚系列素æå›¾é›†ç”Ÿæˆç»Ÿä¸€èµ°â€œæ‰¹é‡è§„划 -> sheet 生图 -> åŽç«¯åˆ‡å›¾ -> 逿˜ŽåŒ– -> OSS æŒä¹…化 -> 状æ€å›žå†™ -> 局部é‡ç”Ÿæˆâ€æµç¨‹ï¼ŒçŽ©æ³•åªæä¾› `sheetSpec`ã€`slotSpecs`ã€æç¤ºè¯å’Œå­—æ®µæ˜ å°„ï¼Œä¸æŠŠä»»ä¸€çŽ©æ³•ä¸“å±žç´ æ DTO 当作平å°é€šç”¨æ¨¡åž‹ã€‚ 通用系列素æå›¾é›†èƒ½åŠ›çš„å®žçŽ°çœŸç›¸æºåœ¨ `platform-image::generated_asset_sheets`:`n` æ˜¯å¿…é€‰å‚æ•°ï¼Œæ¨¡å—负责组装 `n*n` sheet promptã€æŒ‰ `n*n` 切片ã€é»˜è®¤ç»¿å¹• / è¿‘ç™½åº•é€æ˜ŽåŒ–ã€å¯¼å‡º PNG å’Œ OSS æŒä¹…åŒ–è¯·æ±‚ï¼›é«˜é£Žé™©æ’žè‰²çŽ©æ³•å¯æ˜¾å¼ä½¿ç”¨ä¸“用 key 色ã€å…³é—­è¿‘白扣除并é™åˆ¶ä¸ºè¾¹ç¼˜è¿žé€šèƒŒæ™¯æ‰£é™¤ã€‚`api-server::generated_asset_sheets` åªä¿ç•™ `AppError` / `AppState` 适é…,ä¸å†æ‰¿è½½å›¾åƒå¤„ç†å’Œ OSS 请求构造细节。物å“åç§° prompt 和特殊设定 prompt 是å¯é€‰è¾“入;调用方å¯ä¼ å…¥ç±»ä¼¼â€œæ¯ä¸ªç‰©å“生æˆäº”个ä¸åŒè§†å›¾â€çš„视角约æŸï¼Œé€šç”¨æ¨¡å—会把 sheet promptã€ç‰©å“行 promptã€ç‰¹æ®Šè®¾å®š prompt ç¼–ç å†™å…¥ OSS 元数æ®ã€‚玩法ä»è´Ÿè´£è®¡è´¹ã€ç‰©å“规划ã€slot 映射ã€å¤±è´¥å›žå†™å’ŒæŠŠé€šç”¨åˆ‡ç‰‡ç»“果映射回自己的è‰ç¨¿ / profile / runtime 字段。 @@ -62,8 +62,12 @@ å‘现页 / 推è页公开作å“å¡çš„ä½œè€…è¡Œåªæ˜¾ç¤ºå…¬å¼€æ˜µç§°æˆ–è´¦å·ç”Ÿæˆçš„è„±æ•æ‰‹æœºå·ï¼›ä¸å¾—把纯 `SY-*` é™¶æ³¥å·æˆ–作å“å·å½“作å¡ç‰‡ä½œè€…åã€‚é™¶æ³¥å·æœç´¢ã€ä½œå“å·å¤åˆ¶å’Œå®Œæ•´ä½œå“身份åªåœ¨æœç´¢ã€è¯¦æƒ…页或明确的å¤åˆ¶å…¥å£å±•示,é¿å…å¡ç‰‡åˆ—表暴露é¢å¤–è´¦å·æ ‡è¯†ã€‚ +移动端底部导航的创作按钮在登录å‰åŽå¿…é¡»ä¿æŒåŒä¸€ä¸ªå›¾ç‰‡åŒ–创作图标,ä¸å› ç™»å½•æ€åˆ‡æ¢æˆåŠ å·ã€‚ + å‘现 Tabã€åˆ›ä½œ Tab 与è‰ç¨¿ Tab çš„é¡µé¢æ ¹å†…容区ä¸å†å¥— `platform-page-stage` 外层全局å¡ç‰‡å£³ï¼Œè®©åˆ—表ã€ç­›é€‰å’ŒçŽ©æ³•å¡èŽ·å¾—æ›´å®½çš„æ¨ªå‘空间;推èé¡µå’Œæˆ‘çš„é¡µä»æŒ‰å„自页é¢è®¾è®¡ä¿ç•™åŽŸæœ‰å…¨å±€å¡ç‰‡å£å¾„。移动端“我的â€é¡µä»æŒ‰é¡¶éƒ¨å¤´åƒ / 昵称 / é™¶æ³¥å·ã€ä¼šå‘˜æ¨ªå¹…ã€ä¸‰å¼ ç»Ÿè®¡å¡ã€æ¯æ—¥ä»»åŠ¡ã€äº”项常用功能宫格ã€è®¾ç½®å…¥å£å’Œæ³•律信æ¯ç»„织,ä¸ä¿ç•™æ—§çš„底部“填邀请ç â€æ¬¡çº§å…¥å£ï¼›å¸¸ç”¨åŠŸèƒ½å½“å‰åªå±•ç¤ºå››é¡¹å¸¸é©»å…¥å£æ—¶å¿…须按四列铺满整行,ä¸ä¿ç•™äº”列网格导致左对é½ç©ºä½ï¼›æ¯æ—¥ä»»åŠ¡å¡å¿…é¡»è¯»å– `/api/profile/tasks` 的当å‰ä»»åŠ¡æ‘˜è¦å¹¶åœ¨é¢†å–åŽåŒæ­¥åˆ·æ–°å¡ç‰‡è¿›åº¦ã€‚å­—å·å¿…须维æŒå¹³å°æ™®é€š UI æ¡£ä½ï¼Œä¸èƒ½å› ä¸ºçª„å±æŠŠå¡ç‰‡æ ‡é¢˜ã€åŠŸèƒ½ label æˆ–æ³•å¾‹ä¿¡æ¯æ’‘æˆå±•示级字å·ï¼›æœ€åŽä¸€å±å†…容必须能在底部 dock 上方完整滚动露出,ä¸å¾—è¢«å›ºå®šåº•éƒ¨å¯¼èˆªé®æŒ¡ã€‚ +å¹³å°åº”用éšè—æµè§ˆå™¨æ ¹èŠ‚ç‚¹ `html` / `body` / `#root` 和平å°é¡µé¢çº§æ»šåŠ¨å®¹å™¨çš„æœ€å¤–å±‚æ»šåŠ¨æ¡å¯è§è½¨é“;弹窗ã€åˆ—表ã€è¿è¡Œæ€ä¾§æ ç­‰å†…éƒ¨æ»šåŠ¨å®¹å™¨ç»§ç»­ä½¿ç”¨åŽŸæœ‰æ»šåŠ¨æ¡æ ·å¼æˆ–æ˜¾å¼ `.scrollbar-hide` 控制。 + ## RPG / 自定义世界 å½“å‰ RPG 创作入å£ä½¿ç”¨ `playId = rpg`,工程域和è¿è¡Œæ€æºç±»åž‹æ²¿ç”¨åŽ†å² `custom-world`。默认入å£çжæ€ä¸º `visible=true`ã€`open=true`,对外展示为“文字冒险â€ï¼›`airp` 仿˜¯ç‹¬ç«‹çš„“AI RPGâ€å ä½å…¥å£ï¼Œä¿æŒ `open=false`,ä¸è¦æŠŠå®ƒå½“ä½œå½“å‰ RPG 创作链路开放。 @@ -107,7 +111,7 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— - 支æŒç”»é¢æè¿°ç”Ÿå›¾ã€å¤šå‚考图生图ã€ä¸Šä¼ æˆ–历å²ç”Ÿæˆä¸»å›¾åŽ AI é‡ç»˜ã€ä¸Šä¼ æˆ–历å²ç”Ÿæˆä¸»å›¾åŽä¸é‡ç»˜ï¼›ä¸»é“¾è¦æ±‚æµè§ˆå™¨å…ˆç» `/api/assets/direct-upload-tickets` ç›´ä¼  OSS 并确认 `asset_object`,创作 action åªæäº¤ `referenceImageAssetObjectId(s)`,由åŽç«¯æ ¡éªŒ owner / bucket / kind / MIME / size åŽç­¾å‘ OSS åªè¯» URL 并下载为 VectorEngine `/v1/images/edits` çš„ multipart `image` part。本地上传 Data URL ä¸ŽåŽ†å² `/generated-*` 图片路径仅ä¿ç•™ä¸ºæ—§è‰ç¨¿ã€æ—§å…¥å£æˆ–未è¿ç§»å®¢æˆ·ç«¯çš„兼容输入;关闭 AI é‡ç»˜æ—¶ï¼ŒåŽç«¯ç»Ÿä¸€è§£æžä¸ºé¦–关或当å‰å…³å¡æ­£å¼å›¾åŽå†æŒä¹…化,ä¸è°ƒç”¨ç¬¬ä¸€æ®µæ‹¼å›¾é¦–图生æˆã€‚ - è‰ç¨¿ç”Ÿæˆä¼šå…ˆæŒä¹…化 `generationStatus=generating` çš„ä½œå“æ‘˜è¦ï¼Œç”Ÿæˆå®Œæˆå¹¶å›žå†™å…³å¡æ‹¼å›¾ç”»é¢ã€å…³å¡ç”»é¢å‚考图ã€UI spritesheet 和关å¡èƒŒæ™¯å›¾åŽå†å˜ä¸º `ready`;当å‰ä¸è‡ªåŠ¨ç”ŸæˆèƒŒæ™¯éŸ³ä¹ã€‚生æˆé¡µæ­¥éª¤æŽ¨è¿›å¿…须跟éšåŽç«¯ session `progressPercent` 的真实里程碑:`88` 表示è‰ç¨¿ç¼–译完æˆå¹¶è¿›å…¥å‡ºå›¾æ­¥éª¤ï¼Œ`94` 表示生æˆå›¾å·²ä¿å­˜å¹¶è¿›å…¥ UI / 背景步骤,`96` 表示正å¼å›¾ä¸Ž UI 背景已确认并进入写入步骤,最终 action æˆåŠŸæˆ–å‘布æ‰è¿›å…¥å®Œæˆæ€ï¼›æ¯ä¸ªæ­¥éª¤å†…部å¯ä»¥æŒ‰å®žé™…等待时间使用å‡è¿›åº¦å¹³æ»‘推进。`88/94/96` åªè´Ÿè´£åˆ‡æ¢å½“剿­¥éª¤ï¼Œä¸ä½œä¸ºæ€»è¿›åº¦åœ°æ¿ï¼›æ€»è¿›åº¦æŒ‰å·²å®Œæˆæ­¥éª¤æƒé‡åР当剿­¥éª¤å†…å‡è¿›åº¦æŽ¨å¯¼ï¼Œéžå®Œæˆæ€æœ€å¤šåœåœ¨ `98%`ã€‚ä»»ä¸€åŒæ­¥ action 回包到达时立å³ä»¥çœŸå®žå®Œæˆ/失败结果冻结进度。 - ä½œå“æž¶æ‹¼å›¾è‰ç¨¿çš„“生æˆä¸­â€é®ç½©åªè¡¨ç¤ºåˆå§‹è‰ç¨¿è¿˜æ²¡æœ‰å¯æŸ¥çœ‹ç»“果;åªè¦ä½œå“摘è¦ã€é¦–å…³å°é¢æˆ–任一关å¡å€™é€‰å›¾å·²ç»å¯ç”¨ï¼ŒåŽç»­ UI 背景é‡ç”Ÿæˆå’Œè¿½åŠ å…³å¡ç”Ÿå›¾éƒ½å¿…é¡»ä½œä¸ºç»“æžœé¡µå±€éƒ¨ç”Ÿæˆæ€å¤„ç†ï¼Œä¸èƒ½é˜»æ­¢æ‰“å¼€è‰ç¨¿ç»“果页。生æˆå¤±è´¥åŽï¼ŒåŒä¸€æµè§ˆå™¨ä¼šè¯å†…的失败 notice 必须覆盖åŽç«¯å¯èƒ½ä»çŸ­æš‚返回的 `generationStatus=generating` 摘è¦ï¼Œä½œå“æž¶ä¿ç•™å¯¹åº”è‰ç¨¿å¡ä½†ä¸å†æ˜¾ç¤ºâ€œç”Ÿæˆä¸­â€ï¼Œç‚¹å‡»åŽå›žåˆ°å¤±è´¥ / é‡è¯•状æ€ã€‚ -- 拼图è‰ç¨¿ç¼–译是长耗时 action,å‰ç«¯ action 请求默认等待 `1_800_000ms`(30 分钟)且ä¸è‡ªåЍé‡è¯•ã€‚æ¯æ¬¡å›¾ç‰‡ç”Ÿæˆè°ƒç”¨çš„预期用时按 90 秒计算,但 `ç”Ÿæˆæ‹¼å›¾é¦–图` å•独按 4 分钟展示;完整 AI é‡ç»˜è·¯å¾„为 `编译首关è‰ç¨¿` 8 ç§’ã€`生æˆå…³å¡åç§°` 10 ç§’ã€`ç”Ÿæˆæ‹¼å›¾é¦–图` 4 分钟ã€`生æˆå…³å¡ç”»é¢` 90 ç§’ã€`生æˆUI与背景` 90 ç§’ã€`写入正å¼è‰ç¨¿` 10 秒,åˆè®¡çº¦ 448 秒。上传图且关闭 AI é‡ç»˜æ—¶å¿…须跳过 `ç”Ÿæˆæ‹¼å›¾é¦–图`,直接进入 `生æˆå…³å¡ç”»é¢` å’Œ `生æˆUI与背景`,åˆè®¡çº¦ 208 秒。生æˆé¡µæ¢å¤æ—¶å¿…须使用åŽç«¯ session `updatedAt` æˆ–ä½œå“æ‘˜è¦ `updatedAt` 作为原始 `startedAtMs`;失败/å®Œæˆæ€ç”¨ `finishedAtMs` 冻结耗时。未收到对应åŽç«¯é‡Œç¨‹ç¢‘å‰ï¼ŒåŽç»­æ­¥éª¤ä¿æŒå¾…处ç†ï¼›å³ä½¿å½“剿­¥éª¤é¢„计时长耗尽,也åªèƒ½è®©å½“剿­¥éª¤å†…部进度åœåœ¨ `98%` 内,ä¸èƒ½è‡ªåŠ¨å®Œæˆå½“剿­¥éª¤æˆ–跳到åŽç»­æ­¥éª¤ã€‚生æˆé¡µæ¯ä¸ªæ­¥éª¤åªå±•示标题和进度,ä¸å±•示步骤详细æè¿°ã€‚ +- 拼图è‰ç¨¿ç¼–译是长耗时 action,å‰ç«¯ action 请求默认等待 `1_800_000ms`(30 分钟)且ä¸è‡ªåЍé‡è¯•ã€‚æ¯æ¬¡å›¾ç‰‡ç”Ÿæˆè°ƒç”¨çš„预期用时按 90 秒计算,但 `ç”Ÿæˆæ‹¼å›¾é¦–图` å•独按 4 分钟展示;完整 AI é‡ç»˜è·¯å¾„为 `编译首关è‰ç¨¿` 8 ç§’ã€`生æˆå…³å¡åç§°` 10 ç§’ã€`ç”Ÿæˆæ‹¼å›¾é¦–图` 4 分钟ã€`生æˆå…³å¡ç”»é¢` 90 ç§’ã€`生æˆUI与背景` 90 ç§’ã€`写入正å¼è‰ç¨¿` 10 秒,åˆè®¡çº¦ 448 秒。上传图且关闭 AI é‡ç»˜æ—¶å¿…须跳过 `ç”Ÿæˆæ‹¼å›¾é¦–图`,直接进入 `生æˆå…³å¡ç”»é¢` å’Œ `生æˆUI与背景`,åˆè®¡çº¦ 208 秒。生æˆé¡µæ¢å¤æ—¶å¿…须使用åŽç«¯ session `updatedAt` æˆ–ä½œå“æ‘˜è¦ `updatedAt` 作为原始 `startedAtMs`;失败/å®Œæˆæ€ç”¨ `finishedAtMs` 冻结耗时。生æˆå®ŒæˆåŽè‹¥è‡ªåŠ¨è¿›å…¥è‰ç¨¿è¯•玩,进入 `/runtime/puzzle` å‰å¿…须先把 `/creation/puzzle/result` å’Œå½“å‰ `sessionId/profileId/workId` å†™æˆæµè§ˆå™¨åކå²å‰ä¸€ç«™ï¼›è¿è¡Œæ€è¿”回按钮和系统返回都应回到结果页,ä¸å¾—退回生æˆè¿›åº¦é¡µæˆ–æš´éœ²é‡æ–°ç”Ÿæˆå…¥å£ã€‚未收到对应åŽç«¯é‡Œç¨‹ç¢‘å‰ï¼ŒåŽç»­æ­¥éª¤ä¿æŒå¾…处ç†ï¼›å³ä½¿å½“剿­¥éª¤é¢„计时长耗尽,也åªèƒ½è®©å½“剿­¥éª¤å†…部进度åœåœ¨ `98%` 内,ä¸èƒ½è‡ªåŠ¨å®Œæˆå½“剿­¥éª¤æˆ–跳到åŽç»­æ­¥éª¤ã€‚生æˆé¡µæ¯ä¸ªæ­¥éª¤åªå±•示标题和进度,ä¸å±•示步骤详细æè¿°ã€‚ - å‰ç«¯åˆ›ä½œã€ç»“果页ã€ç”Ÿæˆé¡µå’Œé”™è¯¯æç¤ºä¸å±•示 GPT / Gemini 等具体模型å称;如需在内部ä¿ç•™æ¨¡åž‹è·¯ç”±ï¼ŒUI åªä½¿ç”¨â€œæ ‡å‡†æ¨¡å¼â€â€œåˆ›æ„模å¼â€ç­‰äº§å“化å称。 - è‹¥æµè§ˆå™¨é”å±ã€æ¯å±æˆ–网络切æ¢å¯¼è‡´ compile 请求失败,å‰ç«¯åœ¨æ ‡è®°å¤±è´¥å‰å¿…须先å¤è¯» `getPuzzleAgentSession(sessionId)`ï¼›åªæœ‰æœ€æ–° session ä»ç¼º `draft.coverImageSrc`ã€é¦–å…³ `coverImageSrc` 或候选图时æ‰å±•示失败,å¤è¯»åˆ°å·²ç”Ÿæˆè‰ç¨¿æ—¶æŒ‰æˆåŠŸæ”¶å°¾ã€åˆ·æ–°ä½œå“架并继续自动试玩/结果页链路。 - 拼图å‚考图 AI é‡ç»˜èµ° VectorEngine `/v1/images/edits`;无å‚考图时走 `/v1/images/generations`。两者模型都使用 `gpt-image-2`,å‚考图由åŽç«¯ä½œä¸º multipart `image` part 传入编辑接å£ã€‚ @@ -127,6 +131,7 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— - 拼图è¿è¡Œæ€è¿›è¡Œä¸­å…³å¡çš„ `elapsedMs` 仿˜¯ç»“ç®—å­—æ®µï¼Œè®¾ç½®é¢æ¿çš„“当å‰ç”¨æ—¶â€å¿…须按 `startedAtMs`ã€æš‚åœç´¯è®¡å’Œå†»ç»“累计实时派生;ä¸è¦ç›´æŽ¥æŠŠè¿›è¡Œä¸­çš„ `currentLevel.elapsedMs` 当作展示值。 - 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œé€šå…³ç»“算弹层必须挂到页é¢çº§ fixed 浮层,ä¸èƒ½ç•™åœ¨æŽ¨èå¡ç‰‡è§†è§‰åŒºå†…çš„ absolute 覆盖层;推è页滑动å¡ç‰‡å’Œè¿è¡Œæ€è§†å£éƒ½ä½¿ç”¨ `overflow: hidden`,åŠå±å†…容区会è£å‰ªæŽ’行榜ã€ä¸‹ä¸€å…³æŒ‰é’®å’Œç›¸ä¼¼ä½œå“å¡ã€‚ - 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œâ€œä¸‹ä¸€å…³â€åº”优先切到相似作å“ï¼›å¦‚æžœå½“å‰æŽ¨è候选为空,æ‰å›žé€€åˆ°åŒä½œå“下一关,é¿å…åŒ¿åæŽ¨èæµåœ¨å¤šå…³å¡ä½œå“上æŒç»­åœç•™åœ¨åŒä¸€ä½œå“内。下一关请求 pending 期间必须ä¿ç•™å½“å‰ `PuzzleRuntimeShell` 和棋盘,ä¸å¾—把推è塿•´ä½“切回 `加载中...` å ä½æ€ï¼›å±€éƒ¨åŒæ­¥çжæ€ç”±æ‹¼å›¾è¿è¡Œæ€è‡ªå·±çš„ busy 表现承接。åŽç«¯è¿”回的新关å¡å±žäºŽå…¶å®ƒä½œå“时,å‰ç«¯å¿…é¡»åŒæ­¥ `selectedPuzzleDetail`ã€æŽ¨è页 `puzzleGalleryEntries` 缓存和 `activeRecommendEntryKey`,让底部作å“ä¿¡æ¯ã€åˆ†äº« / 点赞 / 改造和下一次“下一个â€åŸºå‡†éƒ½æŒ‡å‘新作å“;但这ä»å±žäºŽåŒä¸€ä¸ª runtime run 内部推进,ä¸èƒ½è§¦å‘推è rail 切å¡åŠ¨ç”»ã€çºµå‘ä½ç§»æˆ–å¯åЍå°é¢é‡ç½®ï¼Œå·²æŒ‚载且 ready çš„è¿è¡Œæ€ç”»é¢åº”ä¿æŒç¨³å®šï¼Œåªé™é»˜æ›´æ–°ä½œå“ä¿¡æ¯å’Œæ“作基准。 +- 推è页作å“ä¿¡æ¯åŒºçš„分享按钮统一唤起å‘布分享弹窗 `PublishShareModal`,ä¸åœ¨æŽ¨èå¡å†…部å•独拼接分享文案或åªåšå‰ªè´´æ¿å¤åˆ¶å馈;拼图推è作å“的分享链接继续沿用 `/gallery/puzzle/detail?work=...`,其它统一公开作å“默认走 `/works/detail?work=...`。 - 推è页里的拼图作å“如果从è¿è¡Œæ€è¿›å…¥â€œæ”¹é€ â€ç»“果页,返回平å°åŽè¦æ¸…掉推è嵌入æ€çš„ `activeRecommendEntryKey` / `activeRecommendRuntimeKind` / `isStartingRecommendEntry`,å†é‡æ–°æŒ‰æŽ¨è页自动å¯åŠ¨é€»è¾‘è¿›å…¥ä½œå“,ä¸èƒ½å¤ç”¨å·²ç»è¢«æ¸…空的旧 `puzzleRun`。 - 拼图è¿è¡Œæ€å…许å‰ç«¯ä½Žå»¶è¿Ÿäº¤äº’è¡¨çŽ°ï¼Œä½†é€šå…³ã€æŽ’è¡Œæ¦œã€å¥–励和作å“状æ€ä»ä»¥åŽç«¯ç¡®è®¤ä¸ºå‡†ã€‚ diff --git a/src/components/common/CreativeImageInputPanel.test.tsx b/src/components/common/CreativeImageInputPanel.test.tsx index c5d8dacf..e21ef9bd 100644 --- a/src/components/common/CreativeImageInputPanel.test.tsx +++ b/src/components/common/CreativeImageInputPanel.test.tsx @@ -287,6 +287,118 @@ test('creative image input panel supports a preview-only main image mode', () => expect(onSubmit).toHaveBeenCalledTimes(1); }); +test('creative image input panel can preview the main image and keep upload on a corner button', () => { + const onMainImageFileSelect = vi.fn(); + const inputClickSpy = vi + .spyOn(HTMLInputElement.prototype, 'click') + .mockImplementation(() => undefined); + + try { + render( + {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onSubmit={() => {}} + />, + ); + + fireEvent.click(screen.getByRole('button', { name: '查看关å¡å›¾ç‰‡' })); + expect(screen.getByRole('dialog', { name: '查看关å¡å›¾ç‰‡' })).toBeTruthy(); + expect(screen.getAllByAltText('拼图关å¡å›¾').length).toBeGreaterThanOrEqual(2); + fireEvent.click( + screen.getByRole('button', { name: '关闭关å¡å›¾ç‰‡é¢„览' }), + ); + + fireEvent.click(screen.getByRole('button', { name: 'æ›´æ¢å‚考图' })); + expect(inputClickSpy).toHaveBeenCalledTimes(1); + + fireEvent.change(screen.getByLabelText('上传å‚考图', { selector: 'input' }), { + target: { + files: [new File(['a'], 'level-reference.png', { type: 'image/png' })], + }, + }); + expect(onMainImageFileSelect).toHaveBeenCalledWith(expect.any(File)); + } finally { + inputClickSpy.mockRestore(); + } +}); + +test('creative image input panel can hide upload and history controls independently', () => { + render( + {}} + onMainImageRemove={() => {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onHistoryClick={() => {}} + onSubmit={() => {}} + />, + ); + + expect(screen.getByRole('button', { name: '查看关å¡å›¾ç‰‡' })).toBeTruthy(); + expect(screen.queryByRole('button', { name: 'æ›´æ¢å‚考图' })).toBeNull(); + expect( + screen.queryByLabelText('上传å‚考图', { selector: 'input' }), + ).toBeNull(); + expect(screen.queryByRole('button', { name: '选择历å²å›¾ç‰‡' })).toBeNull(); +}); + test('creative image input panel does not show empty upload hint over a non-removable image', () => { render( (null); const [previewReferenceImage, setPreviewReferenceImage] = useState(null); + const [isMainImagePreviewOpen, setIsMainImagePreviewOpen] = useState(false); const [isRemoveImageConfirmOpen, setIsRemoveImageConfirmOpen] = useState(false); const showPrompt = mainImageMode === 'preview' || !uploadedImageSrc || aiRedraw; @@ -127,10 +137,19 @@ export function CreativeImageInputPanel({ const promptReferenceUploadDisabled = disabled || promptReferenceImages.length >= promptReferenceLimit; const canEditMainImage = mainImageMode === 'edit'; + const isMainImageUploadEnabled = canEditMainImage && canUploadMainImage; + const shouldShowHistoryButton = + canEditMainImage && canUseImageHistory && Boolean(onHistoryClick); + const shouldPreviewMainImage = + mainImageClickMode === 'preview' && Boolean(uploadedImageSrc); + const shouldShowMainImageUploadButton = + isMainImageUploadEnabled && shouldPreviewMainImage; useEffect(() => { if (uploadedImageSrc) { setPreviewReferenceImage(null); + } else { + setIsMainImagePreviewOpen(false); } }, [uploadedImageSrc]); @@ -187,35 +206,48 @@ export function CreativeImageInputPanel({
- {canEditMainImage ? ( - <> - { - const file = event.currentTarget.files?.[0] ?? null; - event.currentTarget.value = ''; - if (file) { - onMainImageFileSelect(file); - } - }} - className="sr-only" - /> - - + }} + className="sr-only" + /> + ) : null} + {shouldPreviewMainImage ? ( + + ) : null} + {shouldShowHistoryButton ? ( - ) : canEditMainImage && !uploadedImageSrc ? ( + ) : isMainImageUploadEnabled && !uploadedImageSrc ? (