From c193a352df4509d02b0231663e2115b5dcd3959d Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 31 May 2026 05:57:34 +0000 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E5=88=9B=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E7=BB=9F=E4=B8=80=E6=80=BB=E8=AE=A1=E5=88=92=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AD=89=E5=BE=85=E9=A1=B5=E7=AA=84=E5=B1=8F?= =?UTF-8?q?=E8=A3=81=E5=88=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 33 +- .hermes/shared-memory/development-workflow.md | 2 +- .hermes/shared-memory/document-map.md | 6 +- .hermes/shared-memory/pitfalls.md | 15 + apps/admin-web/src/api/adminApiTypes.ts | 18 + .../AdminCreationEntrySwitchPage.test.tsx | 112 ++++++ .../pages/AdminCreationEntrySwitchPage.tsx | 198 +++++++++- docs/README.md | 4 +- docs/planning/README.md | 13 + ...玩法创作】创作æµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md | 371 ++++++++++++++++++ ...„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md | 4 +- ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 4 +- ...玩法创作】生æˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md | 8 +- quality-gates/README.md | 20 +- ...Ž©æ³•åˆ›ä½œã€‘è·¨çŽ©æ³•å›žå½’ä¸Žå†’çƒŸé—¨ç¦-2026-05-30.md | 94 +++++ scripts/dev.mjs | 6 +- server-rs/Cargo.lock | 1 + server-rs/crates/api-server/src/admin.rs | 22 ++ .../crates/module-runtime/src/application.rs | 14 +- server-rs/crates/module-runtime/src/domain.rs | 2 + .../crates/shared-contracts/src/admin.rs | 6 + .../src/creation_entry_config.rs | 96 ++++- .../spacetime-client/src/mapper/runtime.rs | 3 + ...tion_entry_type_admin_upsert_input_type.rs | 1 + .../creation_entry_type_config_type.rs | 7 + .../creation_entry_type_snapshot_type.rs | 1 + server-rs/crates/spacetime-module/Cargo.toml | 1 + .../crates/spacetime-module/src/migration.rs | 5 +- .../src/runtime/creation_entry_config.rs | 24 ++ .../spacetime-module/src/visual_novel.rs | 6 +- .../CustomWorldGenerationView.test.tsx | 9 +- src/components/GenerationProgressHero.tsx | 68 ++-- .../BarkBattleGeneratingView.test.tsx | 9 +- .../JumpHopWorkspace.test.tsx | 92 +++++ .../JumpHopResultView.test.tsx | 144 +++++++ .../JumpHopRuntimeShell.test.tsx | 212 ++++++++++ ...Match3DAgentWorkspace.interaction.test.tsx | 22 ++ .../Match3DAgentWorkspace.tsx | 35 +- .../PlatformEntryFlowShellImpl.tsx | 144 +++++-- .../PuzzleAgentWorkspace.interaction.test.tsx | 23 ++ .../puzzle-agent/PuzzleAgentWorkspace.tsx | 14 +- ...gEntryFlowShell.agent.interaction.test.tsx | 218 ++++++++++ src/components/rpg-entry/RpgEntryHomeView.tsx | 16 +- .../rpg-entry/rpgEntryWorldPresentation.ts | 6 +- .../SquareHoleResultView.test.tsx | 102 +++++ .../UnifiedCreationPage.test.tsx | 12 + .../unified-creation/UnifiedCreationPage.tsx | 19 +- .../UnifiedGenerationPage.tsx | 34 +- .../unified-creation/unifiedGenerationCopy.ts | 34 ++ .../VisualNovelAgentWorkspace.test.tsx | 8 +- .../WoodenFishWorkspace.test.tsx | 21 + .../WoodenFishWorkspace.tsx | 8 +- src/index.css | 6 +- 53 files changed, 2192 insertions(+), 161 deletions(-) create mode 100644 apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx create mode 100644 docs/planning/README.md create mode 100644 docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md create mode 100644 quality-gates/ã€çŽ©æ³•åˆ›ä½œã€‘è·¨çŽ©æ³•å›žå½’ä¸Žå†’çƒŸé—¨ç¦-2026-05-30.md create mode 100644 src/components/jump-hop-creation/JumpHopWorkspace.test.tsx create mode 100644 src/components/jump-hop-result/JumpHopResultView.test.tsx create mode 100644 src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx create mode 100644 src/components/square-hole-result/SquareHoleResultView.test.tsx create mode 100644 src/components/unified-creation/unifiedGenerationCopy.ts diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 93ddf685..d5a01ba1 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,12 +16,37 @@ --- -## 2026-05-27 生æˆé¡µæ€»è¿›åº¦åœ†å¼§é”定固定画布 +## 2026-05-30 创作æµç¨‹ç»Ÿä¸€åŒ–é—¨ç¦æ‰©å±•为跨玩法矩阵 -- 背景:多轮圆环角度微调åŽï¼Œ`GenerationProgressHero` çš„ SVG 圆弧ä»ä¼šå‡ºçŽ°åº•éƒ¨å¼€å£å斜的问题,且圆环还会éšç€å®¹å™¨å®½åº¦ä¼¸ç¼©ï¼Œå¯¼è‡´ UI çœ‹èµ·æ¥æ—¶å¤§æ—¶å°ã€ä½ç½®æ¼‚移。 -- 决策:共用 `GenerationProgressHero` çš„ SVG 圆弧起始角固定为 `135deg`,轨é“和橘黄色填充都从åŒä¸€ä¸ªå¯¹ç§°èµ·ç‚¹ `rotate(135 200 200)` 出å‘ï¼›`270deg` 扫æè§’é…åˆæ­£ä¸‹æ–¹ `90deg` 留空,圆环本体改为固定 `400x400` 画布,ä¸å†è·Ÿéšé¡µé¢å®½åº¦ç¼©æ”¾ï¼Œå¤–层布局åªè´Ÿè´£å®šä½ï¼Œä¸è´Ÿè´£æ”¹åŠ¨åœ†çŽ¯æ ·å¼ã€‚ +- 背景:统一创作 / 统一生æˆé—¨ç¦å·²ç»è¶³å¤Ÿè¦†ç›– Phase 2 的入å£ä¸Žå£³å±‚ï¼Œä½†å½“å‰æ€»è®¡åˆ’å·²ç»æŽ¨è¿›åˆ° Phase 3-6,继续åªä¿ç•™å•页门ç¦ä¼šè®© Phase 4 的特殊工作å°ã€Phase 5 的结果页 / ä½œå“æž¶ / 公开详情和 Phase 6 的冻结验收没有统一入å£ã€‚ +- 决策:`quality-gates/README.md` ç»§ç»­ä¿ç•™å•页门ç¦ä¸Ž `dev-stack` é—¨ç¦ï¼ŒåŒæ—¶æ–°å¢žè·¨çŽ©æ³•å›žå½’ / 冒烟门ç¦ï¼ŒæŒ‰ Phase 2 到 Phase 5 的最å°éªŒè¯é›†åˆåˆ†å±‚执行;Phase 6 冻结å‰ä»¥è¿™ä»½çŸ©é˜µä¸ºä¸»ï¼Œä¸å†å¦å¤–拆新波次。涉åŠå…¥å£é…ç½®ã€ç»Ÿä¸€å­—段 specã€æ™®é€šå·¥ä½œå°ã€RPG / Bark Battle / 视觉å°è¯´ç‰¹æ®Šè¾¹ç•Œã€å‘布 / 公开 / runtime 或本地 smoke çš„å˜æ›´ï¼Œä¼˜å…ˆå¯¹ç…§è¿™ä»½çŸ©é˜µè¡¥é½éªŒæ”¶å‘½ä»¤ã€‚ +- å½±å“范围:`quality-gates/README.md`ã€`quality-gates/ã€çŽ©æ³•åˆ›ä½œã€‘è·¨çŽ©æ³•å›žå½’ä¸Žå†’çƒŸé—¨ç¦-2026-05-30.md`ã€`docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md`ã€åŽç»­ Phase 2-6 玩法接入与冻结æµç¨‹ã€‚ +- éªŒè¯æ–¹å¼ï¼šæŒ‰çŸ©é˜µæ‰§è¡Œ `npm run check:encoding`ã€`npm run typecheck`ã€`npm run admin-web:typecheck`ã€å¯¹åº”分期 `npm run test`ã€`npm run check:visual-novel-vn11`,以åŠéœ€è¦æ—¶çš„ `npm run dev:api-server` + `/healthz` smoke。 +- å…³è”æ–‡æ¡£ï¼š`quality-gates/README.md`ã€`quality-gates/ã€çŽ©æ³•åˆ›ä½œã€‘è·¨çŽ©æ³•å›žå½’ä¸Žå†’çƒŸé—¨ç¦-2026-05-30.md`ã€`docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md`。 + +## 2026-05-30 跳一跳结果页直达必须优先æ¢å¤ä½œå“è€Œä¸æ˜¯ç™½å± + +- èƒŒæ™¯ï¼šè·³ä¸€è·³ç»“æžœé¡µå·²ç»æŽ¥å…¥ç»Ÿä¸€å£³ï¼Œä½†å¦‚æžœç”¨æˆ·ç›´æŽ¥æ‰“å¼€ `/creation/jump-hop/result`,旧路径容易因为缺少 `draft` æ¢å¤ä¿¡æ¯è€Œçœ‹èµ·æ¥åƒç™½å±ï¼Œè¯¯å¯¼æˆç»“果页å了。 +- 决策:`PlatformEntryFlowShellImpl` 的跳一跳æ¢å¤é¡ºåºå›ºå®šä¸º `profileId -> getWorkDetail`ï¼Œå† `sessionId -> getSession`;两者都拿ä¸åˆ°æ—¶å¿…须展示 `跳一跳è‰ç¨¿æœªæ¢å¤` æ¢å¤é¢æ¿å’Œ `返回创作`,ä¸èƒ½ç»§ç»­ç•™ç©ºç™½ç»“果页。进入结果页的 smoke å…许æ¢å¤é¢æ¿ï¼Œä½†ä¸å…许纯空白。 +- å½±å“范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`ã€`quality-gates/ã€çŽ©æ³•åˆ›ä½œã€‘è·¨çŽ©æ³•å›žå½’ä¸Žå†’çƒŸé—¨ç¦-2026-05-30.md`ã€`docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md`。 +- éªŒè¯æ–¹å¼ï¼š`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"`;手测 `/creation/jump-hop/result` å’Œ `/creation/jump-hop/result?profileId=`。 +- å…³è”æ–‡æ¡£ï¼š`docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md`ã€`quality-gates/ã€çŽ©æ³•åˆ›ä½œã€‘è·¨çŽ©æ³•å›žå½’ä¸Žå†’çƒŸé—¨ç¦-2026-05-30.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## 2026-05-29 一期统一创作页必须æä¾›å¯è§ç»Ÿä¸€å¤–壳 + +- 背景:`UnifiedCreationPage` é¦–ç‰ˆåªæš´éœ²éšè— spec 元数æ®å¹¶åŒ…裹旧玩法工作å°ï¼Œç”¨æˆ·æ‰“开拼图创作页时ä»åªèƒ½çœ‹åˆ°æ—§å·¥ä½œå°å¤–观,无法验收“统一创作页â€ã€‚ +- å†³ç­–ï¼šä¸€æœŸç»Ÿä¸€åˆ›ä½œé¡µï¼ˆæ‹¼å›¾ã€æŠ“å¤§é¹…ã€æ•²æœ¨é±¼ï¼‰å¿…须由 `UnifiedCreationPage` æä¾›ç»Ÿä¸€æ ‡é¢˜æ ã€å†…容区和éšè—字段契约;字段元信æ¯åªç•™ç»™æµ‹è¯•和代ç ï¼Œä¸å†é¢å¤–作为å¯è§ chip å ç”¨é¦–å±ã€‚玩法工作å°åªæ‰¿è½½å…·ä½“输入控件ã€ä¸Šä¼ ã€åކå²ç´ æã€æ ¡éªŒå’Œæäº¤ï¼Œä¸å†å„è‡ªæ¸²æŸ“å·¨å¤§å…¥å£æ ‡é¢˜ã€‚拼图继续å¤ç”¨ `PuzzleAgentWorkspace` 的上传ã€è£å‰ªã€åކå²å›¾ã€AI é‡ç»˜å’Œæäº¤é€»è¾‘,抓大鹅继续å¤ç”¨ `Match3DAgentWorkspace` 的题æä¸Žéš¾åº¦è¡¨å•逻辑;二者在统一壳内å¯ç”¨ `unifiedChrome`,收起旧标题与外层壳。敲木鱼å³ä¾§éŸ³æ•ˆå’ŒåŠŸå¾·é¢æ¿ä¸å¾—å†å¥—内部滚动容器,移动端应自然跟éšé¡µé¢æ»šåŠ¨ã€‚ +- 追加决策:`UnifiedCreationPage` ä¸åˆ›å»ºè‡ªå·±çš„çºµå‘æ»šåŠ¨çª—ï¼›æ‹¼å›¾ã€æŠ“å¤§é¹…å’Œæ•²æœ¨é±¼ä¸‰ä¸ªç»Ÿä¸€åˆ›ä½œå…¥å£ç”±å¹³å° stage 承担整页滚动,竖å±ç§»åŠ¨ç«¯å¿…é¡»èƒ½ä»Žç»Ÿä¸€æ ‡é¢˜ã€è¡¨å•控件一路滑到æäº¤æŒ‰é’®ï¼Œé¿å…工作å°å†…部或å³ä¾§é¢æ¿å½¢æˆå¥—滚动。 +- å½±å“范围:`src/components/unified-creation/UnifiedCreationPage.tsx`ã€`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`ã€`src/components/wooden-fish-creation/WoodenFishWorkspace.tsx`ã€`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€çŽ©æ³•é“¾è·¯æ–‡æ¡£ã€‚ +- éªŒè¯æ–¹å¼ï¼š`UnifiedCreationPage` 测试应断言éšè—契约ä»åœ¨ä½† UI ä¸å†å‡ºçŽ°å­—æ®µ chipï¼›æ‹¼å›¾å’ŒæŠ“å¤§é¹…å·¥ä½œå°æµ‹è¯•应断言 `unifiedChrome=true` æ—¶ä¸å†æ¸²æŸ“旧巨大标题且ä»ä¿ç•™è¡¨å•è¾“å…¥ï¼›æœ¨é±¼å·¥ä½œå°æµ‹è¯•æˆ–æ‰‹æµ‹åº”ç¡®è®¤æ•²å‡»éŸ³æ•ˆå’ŒåŠŸå¾·è¯æ¡ä¸å†åœç•™åœ¨ç‹¬ç«‹æ»šåŠ¨çª—å†…ã€‚ +- å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## 2026-05-27 生æˆé¡µæ€»è¿›åº¦åœ†å¼§é”定固定 SVG åæ ‡ç³» + +- 背景:多轮圆环角度微调åŽï¼Œ`GenerationProgressHero` çš„ SVG 圆弧ä»ä¼šå‡ºçŽ°åº•éƒ¨å¼€å£åæ–œçš„é—®é¢˜ï¼›åŽæ¥çª„å±éªŒæ”¶åˆå‘现固定 `400px` 外层宽度会让等待页å³ä¾§è¢«è£åˆ‡ã€‚ +- 决策:共用 `GenerationProgressHero` çš„ SVG 圆弧起始角固定为 `135deg`,轨é“和橘黄色填充都从åŒä¸€ä¸ªå¯¹ç§°èµ·ç‚¹ `rotate(135 200 200)` 出å‘ï¼›`270deg` 扫æè§’é…åˆæ­£ä¸‹æ–¹ `90deg` 留空。SVG å†…éƒ¨åæ ‡ç³»å›ºå®šä¸º `400x400`,圆弧使用 `r=166` å’Œ `strokeWidth=18`;外层显示宽度以 `400px` 为上é™ï¼Œçª„屿Œ‰ `min(400px, calc(100vw - 2.5rem))` 等比收缩。预计等待 / 已耗时信æ¯å¡åœ¨çª„å±ä¸‹è½åˆ°åœ†çŽ¯ä¸‹æ–¹ä¸¤åˆ—ï¼Œ`sm` åŠä»¥ä¸Šå†å›žåˆ°å·¦å³æ‚¬æµ®ã€‚ - å½±å“范围:`src/components/GenerationProgressHero.tsx`ã€å…±ç”¨ `CustomWorldGenerationView`ã€æ±ªæ±ªå£°æµª `BarkBattleGeneratingView` 以åŠç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€æ–‡æ¡£ã€‚ -- éªŒè¯æ–¹å¼ï¼š`CustomWorldGenerationView` å’Œ `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`ã€`data-ring-fill-start-degrees=135`,且圆环容器固定为 `h-[400px] w-[400px]`,track / fill transform 都是 `rotate(135 200 200)`。 +- éªŒè¯æ–¹å¼ï¼š`CustomWorldGenerationView` å’Œ `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`ã€`data-ring-fill-start-degrees=135`ï¼Œä¸”åœ†çŽ¯å®¹å™¨åŒ…å« `w-[min(400px,calc(100vw-2.5rem))]`ã€`max-w-full` 与 `aspect-square`,track / fill transform 都是 `rotate(135 200 200)`ï¼›ç«–å± smoke 至少覆盖 `280px / 320px / 360px / 390px` 宽度。 - å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘ç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md`。 ## 2026-05-26 å¹³å°è·¨æµç¨‹é”™è¯¯ç»Ÿä¸€ç”¨å¯å¤åˆ¶æ¥æºå¼¹çª—展示 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index 23e61f20..6bafa510 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -256,7 +256,7 @@ npm run check:server-rs-ddd ## æäº¤å‰å»ºè®®è®© Hermes 执行 -æ¶‰åŠæ‹¼å›¾ã€æŠ“å¤§é¹…ã€æ•²æœ¨é±¼ç»Ÿä¸€åˆ›ä½œ / 生æˆé“¾è·¯æˆ–本地 dev 栈时,先按 `quality-gates/README.md` å’Œå¯¹åº”é—¨ç¦æ–‡æ¡£æ‰§è¡Œè‡ªåŠ¨è„šæœ¬ä¸Žä½“éªŒæ£€æŸ¥ã€‚ +æ¶‰åŠæ‹¼å›¾ã€æŠ“å¤§é¹…ã€æ•²æœ¨é±¼ç»Ÿä¸€åˆ›ä½œ / 生æˆé“¾è·¯ã€Phase 2 之åŽçš„跨玩法回归或本地 dev 栈时,先按 `quality-gates/README.md`ã€`quality-gates/ã€çŽ©æ³•åˆ›ä½œã€‘è·¨çŽ©æ³•å›žå½’ä¸Žå†’çƒŸé—¨ç¦-2026-05-30.md` 和对应å•é¡¹é—¨ç¦æ–‡æ¡£æ‰§è¡Œè‡ªåŠ¨è„šæœ¬ä¸Žä½“éªŒæ£€æŸ¥ã€‚ ```text è¯·æ£€æŸ¥å½“å‰ git diff,指出: diff --git a/.hermes/shared-memory/document-map.md b/.hermes/shared-memory/document-map.md index 512c5949..62a5538f 100644 --- a/.hermes/shared-memory/document-map.md +++ b/.hermes/shared-memory/document-map.md @@ -11,6 +11,7 @@ | 产å“ã€å‘½åã€UIã€å作和废弃路线 | `docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md` | | åŽç«¯ã€DDDã€APIã€SpacetimeDB schema 和表目录 | `docs/ã€åŽç«¯æž¶æž„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md` | | 创作入å£ã€è‰ç¨¿æž¶å’ŒçŽ©æ³•é“¾è·¯ | `docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md` | +| 创作æµç¨‹ç»Ÿä¸€é˜¶æ®µè®¡åˆ’ | `docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md` | | 本地å¯åЍã€éªŒè¯ã€éƒ¨ç½²ã€åŸ‹ç‚¹å’Œè¿è¥æŸ¥è¯¢ | `docs/ã€å¼€å‘è¿ç»´ã€‘本地开å‘验è¯ä¸Žç”Ÿäº§è¿ç»´-2026-05-15.md` | | UI åƒç´ èµ„产与 9-slice 规范 | `UI_CODING_STANDARD.md` | @@ -32,8 +33,9 @@ 玩法 / åˆ›ä½œå…¥å£ / è¿è¡Œæ€ï¼š 1. `docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md` -2. `docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md` -3. 相关å‰ç«¯ç»„ä»¶ã€serviceã€shared contract å’ŒåŽç«¯ module +2. 若任务涉åŠè·¨çŽ©æ³•åˆ›ä½œæµç¨‹ç»Ÿä¸€ï¼Œè¯»å– `docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md` +3. `docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md` +4. 相关å‰ç«¯ç»„ä»¶ã€serviceã€shared contract å’ŒåŽç«¯ module 生产部署 / æœåС噍 / Jenkins: diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 3dea8cd4..caca24d6 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -1537,6 +1537,13 @@ - 现象:移动端创作 Tab 里进入汪汪声浪表å•åŽï¼Œé¡µé¢å³ä¾§å‡ºçްä¸è‡ªç„¶çš„内层滚动æ¡ï¼Œæœ€åŽçš„形象æè¿°è¾“入框容易被“生æˆè‰ç¨¿â€æŒ‰é’®ã€é”®ç›˜æˆ–底部 TabBar 挤压 / 鮿Œ¡ï¼›é¡¶éƒ¨çŽ©æ³•å¡é¦–尾也å¯èƒ½è´´è¾¹æ˜¾å¾—被è£ã€‚ - 原因:外层 `.platform-tab-panel` å·²ç»æ˜¯çºµå‘æ»šåŠ¨å®¹å™¨ï¼Œåˆ›ä½œé¡µä¸­é—´åˆæœ‰å¤šå±‚ `overflow-hidden`,旧的 `BarkBattleConfigEditor` 根节点å†åŠ  `overflow-y-auto`,形æˆå¤–层 Tab 颿¿ + 内层表å•的套滚动;底部按钮åªé¢„ç•™ safe-area,ä¸é¢„留真实æ“作区è·ç¦»ï¼›é¡¶éƒ¨çŽ©æ³•å¡æ¨ªå‘滚动æ¡éšè—且首尾没有 scroll padding。 - 处ç†ï¼šç§»åŠ¨ç«¯è®© Bark Battle 表å•è·Ÿéšçˆ¶çº§æ»šåŠ¨ï¼Œ`lg` ä»¥ä¸Šæ‰æ¢å¤è¡¨å•内滚动;创作页容器移动端使用 `overflow-visible` å’Œ safe-area 底部 paddingï¼›é¡¶éƒ¨æ¨¡æ¿ tablist 加 `scroll-px-3` / æ¨ªå‘ padding,移动端å¡ç‰‡å®½åº¦æ”¶çª„,é¿å…首尾 ring 和圆角贴边è£åˆ‡ã€‚ + +## 统一创作页ä¸è¦æŠŠç«–屿»šåЍé”进内部内容区 + +- çŽ°è±¡ï¼šç«–å±æ‰“å¼€æ‹¼å›¾ã€æŠ“å¤§é¹…æˆ–æ•²æœ¨é±¼åˆ›ä½œé¡µæ—¶ï¼Œæµè§ˆå™¨é¡µé¢æœ¬èº«æ— æ³•æ»šåŠ¨ï¼Œç”ŸæˆæŒ‰é’®æˆ–å³ä¾§è¡¨å•颿¿è½åˆ°è§†å£å¤–ï¼›æœ¨é±¼çš„æ•²å‡»éŸ³æ•ˆå’ŒåŠŸå¾·è¯æ¡çœ‹èµ·æ¥åƒè¢«å¡žè¿›å•独滑动窗å£ã€‚ +- åŽŸå› ï¼šå¹³å°æ ¹å£³å›ºå®šä¸€å±å¹¶éšè—溢出,`UnifiedCreationPage` åˆä½¿ç”¨ `h-full min-h-0 overflow-hidden` 和内容区 `overflow-y-auto`,导致滚动责任è½åˆ°å†…éƒ¨å†…å®¹çª—ï¼Œè€Œä¸æ˜¯æ•´ä¸ªåˆ›ä½œ stage。 +- 处ç†ï¼š`UnifiedCreationPage` åªä¿ç•™ç»Ÿä¸€æ ‡é¢˜ã€éšè—字段契约和内容包装,ä¸å†è®¾ç½®å†…éƒ¨çºµå‘æ»šåŠ¨ï¼›æ‹¼å›¾ã€æŠ“å¤§é¹…å’Œæ•²æœ¨é±¼ä¸‰ä¸ªç»Ÿä¸€åˆ›ä½œå…¥å£çš„ `motion.div` stage è´Ÿè´£ `overflow-y-auto overflow-x-hidden`。拼图和抓大鹅在 `unifiedChrome` 下收起旧 `h-full overflow-hidden` 外壳,让表å•ä¸»ä½“è·Ÿéš stage 滚动。 +- 验è¯ï¼šç”¨ç«–屿µè§ˆå™¨è§†å£æ‰“å¼€ `/creation/wooden-fish`ã€`/creation/puzzle` å’Œ `/creation/match3d`,页é¢çº§ stage åº”å¯æ»šåŠ¨åˆ°ç”ŸæˆæŒ‰é’®ï¼›`.unified-creation-page__content` ä¸åº”åŒ…å« `overflow-y-auto`,木鱼工作å°å†…部也ä¸åº”å‡ºçŽ°ç‹¬ç«‹çºµå‘æ»šåŠ¨å®¹å™¨ï¼Œæ‹¼å›¾ / 抓大鹅å¯è§æ ‡é¢˜ä¸åº”é‡å¤ã€‚ - 验è¯ï¼š`npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx`ã€`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab shows template tabs"`ã€ç§»åŠ¨ç«¯è§†å£æ£€æŸ¥æœ€åŽä¸€ä¸ªè¾“入框与“生æˆè‰ç¨¿â€æŒ‰é’®ä¸é‡å ã€‚ - å…³è”:`src/components/bark-battle-creation/BarkBattleConfigEditor.tsx`ã€`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 @@ -1596,6 +1603,14 @@ - 验è¯ï¼šç§»åŠ¨ç«¯è§†å£æ£€æŸ¥è§†é¢‘ `rect` 应覆盖整个视å£ï¼Œ`paused` 应最终å˜ä¸º `false`,`currentTime` 应æŒç»­å‰è¿›ã€‚ - å…³è”:`src/components/GenerationProgressHero.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘ç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md`。 +## 跳一跳结果页直达时ä¸è¦æŠŠæ¢å¤é¢æ¿å½“æˆç©ºç™½é¡µ + +- 现象:æµè§ˆå™¨ç›´æŽ¥æ‰“å¼€ `/creation/jump-hop/result`,如果没有 `sessionId`ã€`profileId`ã€`draftId` 或 `workId`,页é¢ä»¥å‰ä¼šçœ‹èµ·æ¥åƒç©ºç™½ï¼Œå®¹æ˜“误判æˆç»“果页å了。 +- 原因:跳一跳结果页æ¢å¤åŽŸå…ˆåªç›¯ `jumpHopSession.draft`,没有把“缺æ¢å¤ä¿¡æ¯â€æ˜Žç¡®å…œæˆå¯è§æ¢å¤é¢æ¿ï¼›ç›´è¾¾ç»“果页时也没有优先用 `profileId -> getWorkDetail` 补回完整作å“。 +- 处ç†ï¼š`PlatformEntryFlowShellImpl` 的跳一跳æ¢å¤é€»è¾‘改æˆå…ˆå°è¯• `profileId -> getWorkDetail`,å†å°è¯• `sessionId -> getSession`;两者都没有时显示 `跳一跳è‰ç¨¿æœªæ¢å¤` å’Œ `返回创作`,ä¸å†ç•™ç©ºç™½é¡µã€‚ +- 验è¯ï¼š`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"`,并手测 `/creation/jump-hop/result` 与 `/creation/jump-hop/result?profileId=` ä¸¤ç§æƒ…况。 +- å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`ã€`docs/planning/ã€çŽ©æ³•åˆ›ä½œã€‘åˆ›ä½œæµç¨‹ç»Ÿä¸€æ€»è®¡åˆ’-2026-05-30.md`。 + 2026-05-24 补充:`GenerationPageBackdrop` ä¸è¦é€šè¿‡ portal 挂到 `document.body`。body 级 fixed 背景会逃离生æˆé¡µè‡ªå·±çš„ stacking context,å³ä½¿ä¸šåŠ¡å†…å®¹æœ‰å±€éƒ¨ `z-10`,真实æµè§ˆå™¨é‡Œä¹Ÿå¯èƒ½æŠŠæ•´é¡µ UI 压ä½ã€‚背景视频应作为生æˆé¡µæ ¹å®¹å™¨å­èŠ‚ç‚¹ä¿ç•™ `fixed inset-0 z-0`,生æˆé¡µå†…å®¹ä¿æŒ `relative z-10`ï¼›ç›¸å…³æµ‹è¯•åº”åŒæ—¶æ–­è¨€èƒŒæ™¯å®¹å™¨ä½Žå±‚级ã€ç”Ÿæˆé¡µæ ¹å®¹å™¨é«˜å±‚级,以åŠè§†é¢‘节点ä»åœ¨ç”Ÿæˆé¡µ DOM 内部。视觉调整时还è¦è®°ä½ï¼šç©ºå¿ƒåœ†çŽ¯çš„ä¸­å¿ƒå—è¦æŠ½æŽ‰ï¼Œæ—¶é—´å¡ä¸Žæ€»è¿›åº¦æ ‡é¢˜éƒ½åº”缩å°ï¼Œä¸è¦è®©ç”Ÿæˆé¡µå†å›žåˆ°â€œçº¯è‰²åº• + 大字å·è¯´æ˜Žå¡â€çš„状æ€ã€‚顶部返回和å³ä¸Šçжæ€ä¹Ÿä¸èƒ½æ²¿ç”¨ `text-lg` / `sm:text-2xl` 这类展示级字å·ï¼›å½“剿­¥éª¤åã€æ­¥éª¤çжæ€å’Œåº•éƒ¨çŽ©æ³•ä¿¡æ¯æ ‡é¢˜è¦ç»´æŒæ™®é€š UI å­—å·æ¡£ä½ï¼Œä¼˜å…ˆä¿æŒ `text-xs` 到 `text-sm` 区间。 2026-05-24 补充:生æˆé¡µâ€œé¢„计等待 / 已耗时â€å¡ç‰‡æœ¬èº«å·²ç»æœ‰æ ‡ç­¾ï¼Œä¼ ç»™ `GenerationProgressHero` 的值åªèƒ½æ˜¯çº¯æ—¶é—´ï¼Œä¾‹å¦‚ `4 分钟`ã€`1 分 15 ç§’`,ä¸è¦å†æ‹¼æŽ¥â€œé¢„è®¡è¿˜éœ€â€æˆ–“已耗时â€ï¼›ä¸¤å¼ æ—¶é—´å¡ä¹Ÿè¦å’Œå½“剿­¥éª¤å¡ä¸€æ ·ä¿æŒåŠé€æ˜Žã€‚拼图总进度åˆå§‹å¸§å¿…é¡»å…许显示 `0%`,ä¸è¦å†ç”¨ `Math.max(1, nextProgress)` ä¹‹ç±»çš„ä¿æŠ¤æŠŠå¯åŠ¨æ€æŠ¬åˆ° `1%`。 diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 6bcb7c11..3ba26abc 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -161,6 +161,7 @@ export interface AdminCreationEntryTypeConfigPayload { categoryLabel: string; categorySortOrder: number; updatedAtMicros: number; + unifiedCreationSpec?: UnifiedCreationSpecPayload | null; } export interface AdminUpsertCreationEntryTypeConfigRequest { @@ -175,6 +176,23 @@ export interface AdminUpsertCreationEntryTypeConfigRequest { categoryId: string; categoryLabel: string; categorySortOrder: number; + unifiedCreationSpec?: UnifiedCreationSpecPayload | null; +} + +export interface UnifiedCreationSpecPayload { + playId: string; + title: string; + workspaceStage: string; + generationStage: string; + resultStage: string; + fields: UnifiedCreationFieldPayload[]; +} + +export interface UnifiedCreationFieldPayload { + id: string; + kind: 'text' | 'select' | 'image' | 'audio'; + label: string; + required: boolean; } export interface AdminWorkVisibilityEntryPayload { diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx new file mode 100644 index 00000000..75c84504 --- /dev/null +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx @@ -0,0 +1,112 @@ +/* @vitest-environment jsdom */ + +import {fireEvent, render, screen, waitFor} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {beforeEach, expect, test, vi} from 'vitest'; + +import { + getAdminCreationEntryConfig, + upsertAdminCreationEntryConfig, +} from '../api/adminApiClient'; +import type { + AdminCreationEntryConfigResponse, + UnifiedCreationSpecPayload, +} from '../api/adminApiTypes'; +import {AdminCreationEntrySwitchPage} from './AdminCreationEntrySwitchPage'; + +vi.mock('../api/adminApiClient', () => ({ + formatAdminApiError: vi.fn((error: unknown) => + error instanceof Error ? error.message : '请求失败', + ), + getAdminCreationEntryConfig: vi.fn(), + isAdminApiError: vi.fn(() => false), + upsertAdminCreationEntryConfig: vi.fn(), +})); + +const puzzleSpec: UnifiedCreationSpecPayload = { + playId: 'puzzle', + title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + workspaceStage: 'puzzle-agent-workspace', + generationStage: 'puzzle-generating', + resultStage: 'puzzle-result', + fields: [ + { + id: 'pictureDescription', + kind: 'text', + label: 'ç”»é¢æè¿°', + required: true, + }, + ], +}; + +const configResponse: AdminCreationEntryConfigResponse = { + entries: [ + { + id: 'puzzle', + title: '拼图', + subtitle: '拼图关å¡åˆ›ä½œ', + badge: 'å¯åˆ›å»º', + imageSrc: '/creation-type-references/puzzle.webp', + visible: true, + open: true, + sortOrder: 30, + categoryId: 'recent', + categoryLabel: '最近创作', + categorySortOrder: 10, + updatedAtMicros: 1, + unifiedCreationSpec: puzzleSpec, + }, + ], +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getAdminCreationEntryConfig).mockResolvedValue(configResponse); + vi.mocked(upsertAdminCreationEntryConfig).mockResolvedValue(configResponse); +}); + +test('创作入å£åŽå°å±•示并ä¿å­˜ç»Ÿä¸€åˆ›ä½œå¥‘约', async () => { + const user = userEvent.setup(); + const {container} = render( + , + ); + + await screen.findByText('pictureDescription'); + expect(container.querySelector('.admin-subsection .admin-info-list')).not.toBeNull(); + expect(container.querySelector('.admin-panel .admin-panel')).toBeNull(); + expect(container.querySelector('.admin-muted')).toBeNull(); + + await user.click(screen.getByRole('button', {name: 'ä¿å­˜å…¥åº“'})); + await user.click(screen.getByRole('button', {name: '确认'})); + + await waitFor(() => { + expect(upsertAdminCreationEntryConfig).toHaveBeenCalledWith( + 'admin-token', + expect.objectContaining({ + id: 'puzzle', + unifiedCreationSpec: puzzleSpec, + }), + ); + }); +}); + +test('创作入å£åŽå°æ‹’ç» playId ä¸ä¸€è‡´çš„统一创作契约', async () => { + const user = userEvent.setup(); + render( + , + ); + + const textarea = await screen.findByLabelText('契约 JSON'); + fireEvent.change(textarea, { + target: { + value: JSON.stringify({ + ...puzzleSpec, + playId: 'match3d', + }), + }, + }); + await user.click(screen.getByRole('button', {name: 'ä¿å­˜å…¥åº“'})); + + expect(await screen.findByText('统一创作契约 playId å¿…é¡»ä¸Žå…¥å£ ID 一致')).toBeTruthy(); + expect(upsertAdminCreationEntryConfig).not.toHaveBeenCalled(); +}); diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx index fb817c65..4a06ddd9 100644 --- a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx @@ -5,7 +5,11 @@ import { getAdminCreationEntryConfig, upsertAdminCreationEntryConfig, } from '../api/adminApiClient'; -import type {AdminCreationEntryTypeConfigPayload} from '../api/adminApiTypes'; +import type { + AdminCreationEntryTypeConfigPayload, + UnifiedCreationFieldPayload, + UnifiedCreationSpecPayload, +} from '../api/adminApiTypes'; import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm'; import {handlePageError} from './pageUtils'; @@ -30,6 +34,7 @@ export function AdminCreationEntrySwitchPage({ const [categoryId, setCategoryId] = useState('recent'); const [categoryLabel, setCategoryLabel] = useState('最近创作'); const [categorySortOrder, setCategorySortOrder] = useState('10'); + const [unifiedCreationSpecJson, setUnifiedCreationSpecJson] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [listErrorMessage, setListErrorMessage] = useState(''); @@ -66,6 +71,14 @@ export function AdminCreationEntrySwitchPage({ const targetId = selectedId.trim(); setErrorMessage(''); + const unifiedCreationSpecResult = parseUnifiedCreationSpecJson( + targetId, + unifiedCreationSpecJson, + ); + if (!unifiedCreationSpecResult.ok) { + setErrorMessage(unifiedCreationSpecResult.message); + return; + } const confirmed = await confirmWrite({ action: 'ä¿å­˜åˆ›ä½œå…¥å£å¼€å…³', target: targetId, @@ -88,6 +101,7 @@ export function AdminCreationEntrySwitchPage({ categoryId: categoryId.trim(), categoryLabel: categoryLabel.trim(), categorySortOrder: parseInteger(categorySortOrder), + unifiedCreationSpec: unifiedCreationSpecResult.spec, }); const nextEntries = sortEntries(response.entries); setEntries(nextEntries); @@ -114,6 +128,7 @@ export function AdminCreationEntrySwitchPage({ setCategoryId(entry.categoryId); setCategoryLabel(entry.categoryLabel); setCategorySortOrder(String(entry.categorySortOrder)); + setUnifiedCreationSpecJson(formatUnifiedCreationSpecJson(entry.unifiedCreationSpec)); } return ( @@ -224,6 +239,26 @@ export function AdminCreationEntrySwitchPage({ /> +
+
+ 统一创作契约 + {unifiedCreationSpecJson.trim() ? 'å·²é…ç½®' : '未é…ç½®'} +
+ {unifiedCreationSpecJson.trim() ? ( + + ) : ( +
未é…置统一创作页契约
+ )} +