From 838c74d8feaa72b3d21d350f59df251c56be5148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=94=E9=A6=99=E4=B8=B8=E5=AD=90?= <15518898337@163.com> Date: Sun, 24 May 2026 20:34:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=95=B2=E6=9C=A8?= =?UTF-8?q?=E9=B1=BC=E7=BB=93=E6=9E=9C=E9=A1=B5=E5=85=83=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E8=A1=A5=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 10 +- .hermes/shared-memory/pitfalls.md | 10 +- ...€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md | 38 +-- ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 7 +- .../crates/api-server/src/match3d/tests.rs | 9 +- .../crates/api-server/src/wooden_fish.rs | 6 +- .../common/CreativeImageInputPanel.tsx | 48 +-- .../PlatformEntryFlowShellImpl.tsx | 49 +++ .../WoodenFishWorkspace.test.tsx | 85 ++++- .../WoodenFishWorkspace.tsx | 187 +++++------ .../WoodenFishResultView.test.tsx | 41 ++- .../WoodenFishResultView.tsx | 317 ++++++++++++++++-- .../miniGameDraftGenerationProgress.test.ts | 34 +- .../miniGameDraftGenerationProgress.ts | 131 +++++--- 14 files changed, 757 insertions(+), 215 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index d4ca26f6..04d08a92 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -27,7 +27,7 @@ ## 2026-05-22 敲木鱼图片创作采用三图 image2 链路 - 背景:敲木鱼自定义题æåªç”Ÿæˆä¸­å¤®æ•²å‡»ç‰©æ—¶ï¼Œè¿è¡Œæ€ç¼ºå°‘与新主题匹é…的竖å±èƒŒæ™¯å’Œä¸»é¢˜åŒ–返回按钮;若直接让背景 prompt è‡ªç”±å‘æŒ¥ï¼Œåˆå®¹æ˜“把敲击物或木槌画进背景里。 -- 决策:敲木鱼 `compile-draft` / `regenerate-hit-object` 图片链路固定为三步 image2 edits。第一步调用 VectorEngine `/v1/images/edits` + `gpt-image-2`,以默认木鱼图作为结构和画风å‚考,用户上传å‚考图åªä½œä¸ºåŒæ¬¡è¯·æ±‚的新主题å‚考,结åˆç”¨æˆ·é¢˜æå…³é”®è¯æˆ–å‚è€ƒå›¾ä¸»é¢˜ç”Ÿæˆ `1:1` 绿色背景主体图;`api-server` 先对这张绿幕图执行去绿背景处ç†å¹¶å†™å›ž `hitObjectAsset`。第二步必须以第一步抠图完æˆåŽçš„逿˜Žæ•²å‡»ç‰©å›¾ä½œä¸ºå‚考,结åˆç”¨æˆ·åŽŸå§‹é¢˜æç”Ÿæˆ `9:16` 背景环境图并写回 `backgroundAsset`,é¿å…背景图继承绿幕或纯绿色画布。第三步必须以去绿åŽçš„æ•²å‡»ç‰©ä¸»ä½“图和背景环境图为å‚è€ƒï¼Œç”Ÿæˆ `1:1` 绿色背景返回按钮图,æœåŠ¡ç«¯åŽ»ç»¿åŽå†™å›ž `backButtonAsset`。三步 prompt 使用 PRD 中固定éšè—关键è¯ï¼Œä¸è¿½åŠ é¢å¤– negative prompt;返回按钮åªå…许å‚考图约æŸåœ†å½¢åº•色和箭头é…色,ä¸å…è®¸ç»§æ‰¿å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框或装饰图案;背景图ä¸å¾—åŒ…å«æ•²å‡»ç‰©æœ¬ä½“或木槌互动物å“,返回按钮图ä¸å¾—åŒ…å«æ–‡å­—ã€æ•°å­—ã€æ°´å°æˆ–é¢å¤– UI 颿¿ã€‚ +- 决策:敲木鱼 `compile-draft` / `regenerate-hit-object` 图片链路固定为三步 image2 edits。第一步调用 VectorEngine `/v1/images/edits` + `gpt-image-2`,以默认木鱼图作为结构和画风å‚考,用户上传å‚考图åªä½œä¸ºåŒæ¬¡è¯·æ±‚的新主题å‚考,结åˆç”¨æˆ·é¢˜æå…³é”®è¯æˆ–å‚è€ƒå›¾ä¸»é¢˜ç”Ÿæˆ `1:1` 绿色背景主体图;`api-server` 先对这张绿幕图执行去绿背景处ç†å¹¶å†™å›ž `hitObjectAsset`。第二步必须以第一步抠图完æˆåŽçš„逿˜Žæ•²å‡»ç‰©å›¾ä½œä¸ºå‚考,结åˆç”¨æˆ·åŽŸå§‹é¢˜æç”Ÿæˆ `9:16` 背景环境图并写回 `backgroundAsset`,é¿å…背景图继承绿幕或纯绿色画布。第三步必须以去绿åŽçš„æ•²å‡»ç‰©ä¸»ä½“图和背景环境图为å‚è€ƒï¼Œç”Ÿæˆ `1:1` 绿色背景返回按钮图,æœåŠ¡ç«¯åŽ»ç»¿åŽå†™å›ž `backButtonAsset`。三步 prompt 使用 PRD 中固定éšè—关键è¯ï¼Œä¸è¿½åŠ é¢å¤– negative prompt;返回按钮åªå…许å‚考图约æŸåœ†å½¢åº•色和箭头é…色,ä¸å…è®¸ç»§æ‰¿å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚å½¢å¤–æ¡†æˆ–è£…é¥°å›¾æ¡ˆï¼Œä¸»ä½“è§†è§‰å°ºå¯¸æ¯”å½“å‰æ¨¡æ¿å†æ”¾å¤§çº¦ 50%,并带主题色外æè¾¹ï¼›èƒŒæ™¯å›¾ä¸å¾—åŒ…å«æ•²å‡»ç‰©æœ¬ä½“或木槌互动物å“,返回按钮图ä¸å¾—åŒ…å«æ–‡å­—ã€æ•°å­—ã€æ°´å°æˆ–é¢å¤– UI 颿¿ã€‚ - å½±å“范围:`api-server` 木鱼图片生æˆç¼–排ã€`wooden_fish_work_profile.background_asset_json`ã€`wooden_fish_work_profile.back_button_asset_json`ã€shared contractsã€å‰ç«¯ç»“果页 / è¿è¡Œæ€èƒŒæ™¯ä¸Žè¿”å›žæŒ‰é’®å±•ç¤ºã€æ•²æœ¨é±¼ PRD 和平å°é“¾è·¯æ–‡æ¡£ã€‚ - éªŒè¯æ–¹å¼ï¼šæ‰§è¡Œ `cargo test -p api-server wooden_fish --manifest-path server-rs/Cargo.toml`ã€`cargo test -p spacetime-client wooden_fish --manifest-path server-rs/Cargo.toml`ã€`npm run spacetime:generate`ã€`npm run check:spacetime-schema`ã€`npm run typecheck`。 - å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 @@ -855,3 +855,11 @@ - 背景:结果页承载预览ã€ä¿®è¡¥å’Œå‘布,若继续放“一次生æˆâ€æŒ‰é’®ä¼šæŠŠåˆå§‹ç”Ÿæˆå’Œç»“果修补èŒè´£æ··åœ¨ä¸€èµ·ã€‚ - 决策:åˆå§‹ä¸‰å›¾ç”Ÿæˆæ”¹ç”± `bark-battle-generating` 独立生æˆé¡µè‡ªåŠ¨æ‰§è¡Œï¼Œç›®æ ‡æ§½ä½åªæœ‰çŽ©å®¶å½¢è±¡ã€å¯¹æ‰‹å½¢è±¡å’Œç«žæŠ€èƒŒæ™¯ï¼›è¡¨å•术语统一为 `themeDescription`ã€çŽ©å®¶å½¢è±¡æè¿°å’Œå¯¹æ‰‹å½¢è±¡æè¿°ï¼Œä¸å†å›žé€€ `themePreset`ã€ç‹—狗皮肤预设或“角色设定â€ã€‚部分失败也进入结果页。结果页ä¸å†æä¾›ä¸€æ¬¡ç”ŸæˆæŒ‰é’®ï¼ŒéŸ³é¢‘é…置和排åé…ç½®ä¸è¿›å…¥ v1 公开闭环;结果页åªä¿ç•™å•æ§½é‡è¯•ã€é‡æ–°ç”Ÿæˆå’Œä¸Šä¼ ã€‚å‘布时 SpacetimeDB `bark_battle_published_config.config_json` 使用规范化åŽçš„æœ€ç»ˆ `publishedSnapshot`,`published_snapshot_json` åŒæ­¥ä¿å­˜åŒä¸€ä»½å¿«ç…§ã€‚ - éªŒè¯æ–¹å¼ï¼šè¡¨å•æäº¤åŽè¿›å…¥ `bark-battle-generating`;结果页ä¸ä¼šå‡ºçŽ°ä¸€æ¬¡ç”ŸæˆæŒ‰é’®ã€éŸ³é¢‘æ§½ã€çš®è‚¤é¢„è®¾å…¥å£æˆ–排åé…置;Bark Battle å‘å¸ƒåŽæ­£å¼ runtime 应读å–结果页最终图片素æè€Œä¸æ˜¯åˆå§‹è‰ç¨¿ç´ æã€‚ + +## 2026-05-24 敲木鱼结果页先补录作å“ä¿¡æ¯å†è¯•玩 / å‘布 + +- 背景:敲木鱼工作å°åªåº”ä¿ç•™ç”Ÿæˆæ‰€éœ€è¾“å…¥ï¼Œä½œå“æ ‡é¢˜ã€ç®€ä»‹å’Œä¸»é¢˜æ ‡ç­¾é€‚åˆæ”¾åœ¨ç”Ÿæˆè‰ç¨¿åŽçš„补录阶段。 +- 决策:敲木鱼的 `workTitle`ã€`workDescription` å’Œ `themeTags` 从工作å°é¦–å±ç§»åˆ°ç»“果页;结果页编辑åŽåœ¨è¯•玩或å‘布å‰å…ˆè°ƒç”¨ `update-work-meta` 写回当å‰ä½œå“ä¿¡æ¯ã€‚主题标签编辑样å¼å¯¹é½æ‹¼å›¾ç»“果页的胶囊标签编辑器。 +- å½±å“范围:`WoodenFishWorkspace`ã€`WoodenFishResultView`ã€`PlatformEntryFlowShellImpl`ã€æ•²æœ¨é±¼ PRD 和平å°å…¥å£é“¾è·¯æ–‡æ¡£ã€‚ +- éªŒè¯æ–¹å¼ï¼šå·¥ä½œå°é¦–å±ä¸å†å‡ºçŽ°æ ‡é¢˜ / 简介 / 标签输入;结果页修改åŽç‚¹è¯•玩或å‘布会先写回当å‰ä½œå“ä¿¡æ¯ã€‚ +- å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index abd649a6..ca486930 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -51,7 +51,7 @@ - 现象:返回按钮试玩图有时会被画æˆå¾½ç« ã€èŠ±ç›˜ã€æµ®é›•åœ†ç‰Œï¼Œç”šè‡³å‡ºçŽ°å¤æ‚外圈和装饰花纹,左箭头å而ä¸å¤Ÿçªå‡ºã€‚ - 原因:prompt åªè¯´â€œä¸»é¢˜åŒ–è¿”å›žæŒ‰é’®â€æ—¶ï¼Œimage2 会把å‚考图里的装饰语言一起学进去;如果没有把形状收æŸåˆ°â€œæ ‡å‡†åœ†å½¢ + å•个居中左箭头â€ï¼Œæ¨¡åž‹ä¼šä¼˜å…ˆè¡¥é€ åž‹è€Œä¸æ˜¯è¡¥å›¾æ ‡ã€‚ -- 处ç†ï¼šè¿”å›žæŒ‰é’®ç”Ÿæˆ prompt å¿…é¡»åªå…许å‚考图约æŸåœ†å½¢åº•色与箭头é…è‰²ï¼Œæ˜Žç¡®ç¦æ­¢å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框和装饰图案,按钮本体固定为标准圆形。 +- 处ç†ï¼šè¿”å›žæŒ‰é’®ç”Ÿæˆ prompt å¿…é¡»åªå…许å‚考图约æŸåœ†å½¢åº•色与箭头é…è‰²ï¼Œæ˜Žç¡®ç¦æ­¢å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚å½¢å¤–æ¡†å’Œè£…é¥°å›¾æ¡ˆï¼ŒæŒ‰é’®æœ¬ä½“å›ºå®šä¸ºæ ‡å‡†åœ†å½¢ï¼Œè§†è§‰å°ºå¯¸æ¯”å½“å‰æ¨¡æ¿å†æ”¾å¤§çº¦ 50%,圆形外沿需è¦ä¸€åœˆä¸Žä¸»é¢˜è‰²æ­é…的干净外æè¾¹ã€‚ - 验è¯ï¼š`cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml`ï¼Œå¹¶é‡æ–°è¯•玩确认返回按钮åªå‰©åœ†å½¢åº•色和中央左箭头。 - å…³è”:`server-rs/crates/api-server/src/wooden_fish.rs`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`. @@ -63,6 +63,14 @@ - 验è¯ï¼š`npm run test -- src/services/wooden-fish/woodenFishClient.test.ts`,并在本地触å‘一次木鱼创作确认ä¸å†å‡ºçް 15 ç§’å‰ç«¯è¶…时。 - å…³è”:`src/services/wooden-fish/woodenFishClient.ts`ã€`src/services/creation-agent/creationAgentClientFactory.ts`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`。 +## 敲木鱼创作“å¡ä½â€å…ˆæŸ¥ 2xx 慢请求 + +- 现象:敲木鱼工作å°ç‚¹å‡»ç”ŸæˆåŽé•¿æ—¶é—´åœç•™åœ¨ç”Ÿæˆé¡µï¼Œçœ‹èµ·æ¥åƒå¡ä½ï¼›`api-server` 日志å¯èƒ½å‡ºçް `/api/creation/wooden-fish/sessions/{sessionId}/actions` çš„ `2xx` 慢请求,耗时å¯è¾¾æ•°åˆ†é’Ÿï¼Œä¾‹å¦‚ `latency_ms=525473`。 +- åŽŸå› ï¼šå½“å‰ `compile-draft` æ˜¯åŒæ­¥ action,会串行等待敲击物ã€èƒŒæ™¯çŽ¯å¢ƒå›¾ã€è¿”回按钮图三次 image2 editsã€åŽ»ç»¿å¤„ç†ã€OSS 写入和 SpacetimeDB è‰ç¨¿å†™å›žï¼›æç¤ºè¯ç”ŸæˆéŸ³æ•ˆå·²å…³é—­ï¼Œä¸åº”作为生æˆé˜¶æ®µã€‚ +- 处ç†ï¼šå…ˆç¡®è®¤æ—¥å¿—中该 action æ˜¯ä¸æ˜¯æœ€ç»ˆ 200;若是 200 慢请求,ä¸è¦ä¼˜å…ˆæŽ’查 WebSocket 或 SpacetimeDB procedure。å‰ç«¯ç”Ÿæˆé¡µè¿›åº¦å¿…须按“整ç†è‰ç¨¿ -> ç”Ÿæˆæ•²å‡»ç‰© -> 生æˆèƒŒæ™¯çŽ¯å¢ƒå›¾ -> 生æˆè¿”回按钮图 -> 写入正å¼è‰ç¨¿â€å±•示,并在未收到 action 回包å‰ä¿æŒç­‰å¾…æ€ï¼Œä¸å®£ç§°å®Œæˆã€‚ +- 验è¯ï¼š`npm run test -- src/services/miniGameDraftGenerationProgress.test.ts -t "wooden fish"`,并观察木鱼生æˆé¡µåœ¨ 5 分钟以上等待时ä»åœç•™åœ¨åˆç†é˜¶æ®µã€‚ +- å…³è”:`src/services/miniGameDraftGenerationProgress.ts`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + ## 敲木鱼点击生æˆå‡ºçް SpacetimeDB procedure è¶…æ—¶å…ˆæŸ¥ç‰ˆæœ¬é”™é… - 现象:敲木鱼创作时点击“生æˆâ€ï¼Œå‰ç«¯æç¤º `SpacetimeDB procedure 调用超时`,但æœåŠ¡ç«¯æ—¥å¿—æ›´æ—©å‡ºçŽ° `Failed to BSATN deserialize procedure return value` 或类似ååºåˆ—化错误。 diff --git a/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md b/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md index 21eba04e..238a200b 100644 --- a/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md +++ b/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md @@ -113,31 +113,29 @@ WF-* 必填字段: 1. `templateId = "wooden-fish"`ï¼› -2. `workTitle`ï¼šä½œå“æ ‡é¢˜ï¼› -3. `hitObjectPrompt`ï¼šç”¨æˆ·æƒ³æ•²çš„å¯¹è±¡å…³é”®è¯æˆ–æè¿°ï¼Œé»˜è®¤â€œé»˜è®¤æ•²å‡»ç‰©å›¾æ¡ˆï¼Œåœ†æ¶¦æœ¨è´¨è´¨æ„Ÿï¼Œé€æ˜ŽèƒŒæ™¯â€ï¼› -4. `floatingWords[]`:ç¥ç¦è¯ï¼Œæœ€å¤š 8 æ¡ï¼Œä¸å¡«æˆ–清空时使用默认ç¥ç¦è¯ã€‚ +2. `hitObjectPrompt`ï¼šç”¨æˆ·æƒ³æ•²çš„å¯¹è±¡å…³é”®è¯æˆ–æè¿°ï¼Œé»˜è®¤â€œé»˜è®¤æ•²å‡»ç‰©å›¾æ¡ˆï¼Œåœ†æ¶¦æœ¨è´¨è´¨æ„Ÿï¼Œé€æ˜ŽèƒŒæ™¯â€ï¼› +3. `floatingWords[]`:ç¥ç¦è¯ï¼Œæœ€å¤š 8 æ¡ï¼Œä¸å¡«æˆ–清空时使用默认ç¥ç¦è¯ã€‚ å¯é€‰å­—段: -1. `workDescription`:作å“简介; -2. `themeTags[]`:最多 6 个标签; -3. `hitObjectReferenceImageSrc`:上传或历å²å›¾å¼•用,åªèƒ½ä½œä¸º image2 å‚考,ä¸å¯ç›´æŽ¥è¿›å…¥è¿è¡Œæ€ï¼› -4. `hitSoundPrompt`:历å²å…¼å®¹å­—段,当å‰åˆ›ä½œæµç¨‹ä¸å†ä½¿ç”¨ï¼› -5. `hitSoundAsset`:用户上传ã€å½•音或默认音频资产。 +1. `hitObjectReferenceImageSrc`:上传或历å²å›¾å¼•用,åªèƒ½ä½œä¸º image2 å‚考,ä¸å¯ç›´æŽ¥è¿›å…¥è¿è¡Œæ€ï¼› +2. `hitSoundPrompt`:历å²å…¼å®¹å­—段,当å‰åˆ›ä½œæµç¨‹ä¸å†ä½¿ç”¨ï¼› +3. `hitSoundAsset`:用户上传ã€å½•音或默认音频资产。 -默认ç¥ç¦è¯ï¼š +结果页补录字段: + +1. `workTitle`ï¼šä½œå“æ ‡é¢˜ï¼Œé»˜è®¤å€¼åœ¨ç»“果页å¯ç¼–辑; +2. `workDescription`:作å“简介; +3. `themeTags[]`:最多 6 个标签,样å¼å¯¹é½æ‹¼å›¾ç»“果页标签编辑器。 + +创作界é¢é»˜è®¤ç¥ç¦è¯ï¼š ```text å¹¸è¿ -å¥åº· -财富 -姻缘 -å¹¸ç¦ -事业 -æˆåŠŸ -功德 ``` +用户å¯é€šè¿‡åŠ å·ç»§ç»­æ–°å¢ž 7 ä¸ªè¯æ¡ï¼Œæ€»æ•°æœ€å¤š 8 æ¡ã€‚æ–°å¢žè¯æ¡å³ä¾§æä¾›å‡å· / åˆ é™¤å°æŒ‰é’®ï¼›é»˜è®¤çš„ç¬¬ä¸€ä¸ªè¯æ¡ä¿ç•™ä¸ºæ™®é€šè¾“入格。 + `floatingWords[]` ä¿å­˜è¯æ¡å本身,ä¸ä¿å­˜ `+1` åŽç¼€ï¼›è¿è¡Œæ€æ¯æ¬¡æ•²å‡»æ—¶å†æŠŠé£˜å­—å±•ç¤ºä¸ºâ€œè¯æ¡+1â€ã€‚ ## 6. 生æˆè§„则 @@ -181,11 +179,11 @@ WF-* 1. 调用 VectorEngine `/v1/images/edits`,模型固定为 `gpt-image-2`ï¼› 2. multipart å‚考图固定包å«ç¬¬ä¸€æ­¥åŽ»é™¤ç»¿è‰²èƒŒæ™¯åŽçš„æ•²å‡»ç‰©ä¸»ä½“图,以åŠç¬¬äºŒæ­¥ç”Ÿæˆçš„背景环境图; 3. 尺寸固定 `1:1`,必须输出绿色背景主体图(纯绿色绿幕),åŽç«¯è½åº“剿‰§è¡ŒåŒä¸€å¥—去绿背景处ç†ï¼› -4. 按主题ã€ç”»é£Žã€æè´¨å’Œé…色生æˆå·¦ä¸Šè§’返回按钮图,但å‚考图åªç”¨äºŽçº¦æŸåœ†å½¢åº•色和中央左箭头的颜色æ­é…,ä¸å¾—å€Ÿé‰´å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框或装饰图案;按钮必须始终是标准圆形,中央åªä¿ç•™å•个清晰左箭头或返回箭头,ä¸å¾—åŒ…å«æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€é¢å¤– UI 颿¿ã€æœ¨æ§Œæˆ–敲击é“å…·ï¼› +4. 按主题ã€ç”»é£Žã€æè´¨å’Œé…色生æˆå·¦ä¸Šè§’返回按钮图,但å‚考图åªç”¨äºŽçº¦æŸåœ†å½¢åº•色和中央左箭头的颜色æ­é…,ä¸å¾—å€Ÿé‰´å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚å½¢å¤–æ¡†æˆ–è£…é¥°å›¾æ¡ˆï¼›æŒ‰é’®å¿…é¡»å§‹ç»ˆæ˜¯æ ‡å‡†åœ†å½¢ï¼Œä¸»ä½“è§†è§‰å°ºå¯¸æ¯”å½“å‰æ¨¡æ¿å†æ”¾å¤§çº¦ 50%,圆形外沿必须有与主题色æ­é…的干净外æè¾¹ï¼Œä¸­å¤®åªä¿ç•™å•个清晰左箭头或返回箭头,ä¸å¾—åŒ…å«æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€é¢å¤– UI 颿¿ã€æœ¨æ§Œæˆ–敲击é“å…·ï¼› 5. æç¤ºè¯ä¸¥æ ¼ä½¿ç”¨ï¼š ```text -ç”Ÿæˆæ•²æœ¨é±¼å·¦ä¸Šè§’è¿”å›žæŒ‰é’®å›¾ã€‚è¦æ±‚以å‚考图-去除绿色背景åŽçš„æ•²å‡»ç‰©ä¸»ä½“和背景环境图为主题ã€ç”»é£Žã€æè´¨å’Œé…色å‚考,但å‚考图åªç”¨æ¥çº¦æŸåœ†å½¢åº•色和中央左箭头的颜色æ­é…,ä¸è¦ç»§æ‰¿å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框或装饰图案。按钮必须始终是标准圆形,整体åƒå•个圆形图标,圆心居中,圆形内部åªä¿ç•™ä¸€ä¸ªæ¸…æ™°ã€ç®€æ´ã€å±…中的å‘左返回箭头,ä¸è¦å‡ºçŽ°æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€æŒ‰é’®å¤–标签ã€é¢å¤– UI 颿¿ã€æœ¨æ§Œæˆ–敲击é“具。尺寸1:1,输出绿色背景主体图(纯绿色绿幕),背景必须是å•一纯绿色 #00FF00 且平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€‚按钮主体边缘干净,åŽç»­ç”±æœåŠ¡ç«¯æ‰£é™¤ç»¿è‰²èƒŒæ™¯ï¼›æŒ‰é’®åº•è‰²ä¸è¦ä½¿ç”¨ä¸Žç»¿å¹•接近的纯绿色,若主题天然包å«ç»¿è‰²ï¼Œè¯·ä»…åœ¨åœ†å½¢åº•è‰²ä¸Šä½¿ç”¨åæ·±ã€å黄或åè“的主题绿色,并用更高对比的箭头颜色区分。 +ç”Ÿæˆæ•²æœ¨é±¼å·¦ä¸Šè§’è¿”å›žæŒ‰é’®å›¾ã€‚è¦æ±‚以å‚考图-去除绿色背景åŽçš„æ•²å‡»ç‰©ä¸»ä½“和背景环境图为主题ã€ç”»é£Žã€æè´¨å’Œé…色å‚考,但å‚考图åªç”¨æ¥çº¦æŸåœ†å½¢åº•色和中央左箭头的颜色æ­é…,ä¸è¦ç»§æ‰¿å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框或装饰图案。按钮必须始终是标准圆形,整体åƒå•ä¸ªåœ†å½¢å›¾æ ‡ï¼ŒæŒ‰é’®ä¸»ä½“åœ¨ç”»å¸ƒä¸­çš„è§†è§‰å°ºå¯¸æ¯”å½“å‰æ¨¡æ¿å†æ”¾å¤§çº¦ 50%,圆心居中,圆形外沿加一圈和主题色æ­é…的干净外æè¾¹ï¼Œè®©å®ƒæ›´åƒä¸€ä¸ªæŒ‰é’®ï¼Œä½†ä»ç„¶åªä¿ç•™ä¸€ä¸ªæ¸…æ™°ã€ç®€æ´ã€å±…中的å‘左返回箭头,ä¸è¦å‡ºçŽ°æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€æŒ‰é’®å¤–标签ã€é¢å¤– UI 颿¿ã€æœ¨æ§Œæˆ–敲击é“具。尺寸1:1,输出绿色背景主体图(纯绿色绿幕),背景必须是å•一纯绿色 #00FF00 且平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€‚按钮主体边缘干净,åŽç»­ç”±æœåŠ¡ç«¯æ‰£é™¤ç»¿è‰²èƒŒæ™¯ï¼›æŒ‰é’®åº•è‰²ä¸è¦ä½¿ç”¨ä¸Žç»¿å¹•接近的纯绿色,若主题天然包å«ç»¿è‰²ï¼Œè¯·ä»…åœ¨åœ†å½¢åº•è‰²ä¸Šä½¿ç”¨åæ·±ã€å黄或åè“的主题绿色,并用更高对比的箭头颜色区分。 主题为:(用户æä¾›å‚考图或用户输入关键è¯ï¼‰ ``` @@ -304,7 +302,7 @@ finish `compile-draft` 是长耗时动作。å‰ç«¯è¿›å…¥ç”Ÿæˆé¡µåŽåº”å±•ç¤ºå¯æ¢å¤è¿›åº¦ï¼›å¦‚果请求失败,标记失败å‰å¿…é¡»å¤è¯» session,确认åŽç«¯æ˜¯å¦å·²ç»ç”Ÿæˆå¹¶å†™å›žè‰ç¨¿ã€‚ -敲木鱼创作请求在å‰ç«¯å¿…须使用长等待窗å£ï¼Œé¿å… `createSession` 或 `executeAction` 仿²¿ç”¨å…±äº«åˆ›ä½œå·¥åŽ‚é»˜è®¤çš„ 15 秒超时。因为 `compile-draft` 会串行等待敲击物ã€èƒŒæ™¯ã€è¿”回按钮和 OSS è½åº“,木鱼 client 需è¦å•独é…ç½®ä¸Žæ•´æ¡ image2 链路匹é…的超时。 +敲木鱼创作请求在å‰ç«¯å¿…须使用长等待窗å£ï¼Œé¿å… `createSession` 或 `executeAction` 仿²¿ç”¨å…±äº«åˆ›ä½œå·¥åŽ‚é»˜è®¤çš„ 15 秒超时。因为 `compile-draft` 会串行等待敲击物ã€èƒŒæ™¯ã€è¿”回按钮三次 image2 å’Œ OSS è½åº“,木鱼 client 需è¦å•独é…ç½®ä¸Žæ•´æ¡ image2 链路匹é…的超时。本地测试中该 action å¯èƒ½è¾¾åˆ°æ•°åˆ†é’Ÿçº§ï¼›ç”Ÿæˆé¡µè¿›åº¦å¿…须按“整ç†è‰ç¨¿ -> ç”Ÿæˆæ•²å‡»ç‰© -> 生æˆèƒŒæ™¯çŽ¯å¢ƒå›¾ -> 生æˆè¿”回按钮图 -> 写入正å¼è‰ç¨¿â€å±•示,ä¸å±•示“æç¤ºè¯ç”ŸæˆéŸ³æ•ˆâ€é˜¶æ®µï¼Œå› ä¸ºå½“剿œ¨é±¼éŸ³æ•ˆåªæ”¯æŒä¸Šä¼ ã€å½•音或默认音。 ## 9. SpacetimeDB 表和 view @@ -340,7 +338,7 @@ finish 1. é‡ç”Ÿæˆæ•²å‡»ç‰©å›¾æ¡ˆï¼› 2. 上传ã€å½•åˆ¶æˆ–æ›¿æ¢æ•²å‡»éŸ³æ•ˆï¼›æœªæä¾›æ—¶ä½¿ç”¨é»˜è®¤æœ¨é±¼éŸ³ï¼› -3. 修改标题ã€ç®€ä»‹å’Œæ ‡ç­¾ï¼› +3. 修改标题ã€ç®€ä»‹å’Œæ ‡ç­¾ï¼Œå¹¶åœ¨è¯•玩或å‘布å‰å†™å›žå½“å‰ä½œå“ä¿¡æ¯ï¼› 4. 修改ç¥ç¦è¯ï¼Œæœ€å¤š 8 æ¡ã€‚ 图案é‡ç”Ÿæˆæ˜¯ç‹¬ç«‹å±€éƒ¨ç”Ÿæˆæ€ï¼Œä¸å¾—æŠŠå·²æœ‰å¯æŸ¥çœ‹ç»“æžœé‡æ–°å˜æˆä¸å¯æ‰“开的全局生æˆä¸­ã€‚音效替æ¢åªæŽ¥å—上传或录音资产,ä¸è§¦å‘æç¤ºè¯éŸ³æ•ˆç”Ÿæˆã€‚ diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index 8e0ef674..0afe6e42 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -134,9 +134,12 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£é€‰æ‹©å…¥å£ç»Ÿä¸€åœ¨ä¸ªäººä¸­å¿ƒ `常用功能 > 1. `敲什么`:敲击物å•图资产槽ä½ã€‚默认模æ¿ä½¿ç”¨å†…ç½®é€æ˜Ž PNG `/wooden-fish/default-hit-object.png` 作为 `bundled-default` 敲击物资产,é¿å…默认关键è¯è¢«é‡æ–°è¯­ä¹‰åŒ–æ”¹å½¢ï¼›ç”¨æˆ·è¾“å…¥è‡ªå®šä¹‰å…³é”®è¯æˆ–上传å‚考图时,åŽç«¯å¿…须以默认木鱼图作为基础结构和画风å‚考,使用 image2 ç”Ÿæˆæœ€ç»ˆæ•²å‡»ç‰©å›¾æ¡ˆï¼Œä¸Šä¼ å›¾åªä½œä¸ºæ–°ä¸»é¢˜å‚考,ä¸ç›´æŽ¥è¿›å…¥è¿è¡Œæ€ã€‚自定义 `compile-draft` / `regenerate-hit-object` å¿…é¡»å®Œæˆ image2 -> OSS ç§æœ‰å¯¹è±¡ -> asset object 登记和绑定åŽï¼Œå†ç”± `api-server` 注入真实 `hitObjectAsset.imageSrc`,ä¸èƒ½åªå†™ `/generated-wooden-fish-assets/...` å ä½è·¯å¾„,也ä¸èƒ½æŽ¥å—å‰ç«¯è¯·æ±‚自带的 `hitObjectAsset` 短路生æˆã€‚ 2. `敲击音效`:音频资产槽ä½ï¼Œå½“å‰åˆ›ä½œé˜¶æ®µåªæ”¯æŒç”¨æˆ·ä¸Šä¼ æˆ–麦克风录制;未æä¾›éŸ³é¢‘时统一写回内置默认木鱼音 `/wooden-fish/default-hit-sound.mp3`。æç¤ºè¯ç”ŸæˆéŸ³æ•ˆå…¥å£ä¸´æ—¶å…³é—­ï¼Œé€šç”¨ `/api/creation/audio/sound-effect` 对木鱼 `hit_sound` 目标也返回 `410 Gone`ï¼›`hitSoundPrompt` åªä½œä¸ºåކå²å…¼å®¹å­—段ä¿ç•™ï¼Œä¸å‚与当å‰åˆ›ä½œæµç¨‹ï¼Œä¹Ÿä¸å¾—ç”± `spacetime-client` åˆæˆå‡éŸ³é¢‘路径。 -3. `功德有什么`:最多 8 æ¡é£˜å­—,默认 `幸è¿ã€å¥åº·ã€è´¢å¯Œã€å§»ç¼˜ã€å¹¸ç¦ã€äº‹ä¸šã€æˆåŠŸã€åŠŸå¾·`;创作æ€åªä¿å­˜è¯æ¡å,è¿è¡Œæ€é£˜å­—展示时å†è¿½åŠ  `+1`。è¿è¡Œæ€é¡¶éƒ¨æ€»æ•°å¡é‡‡ç”¨å“牌化徽标样å¼ï¼Œå­é¡¹è®¡æ•°å™¨é¢„置展示在å¯å±•开颿¿ä¸­ï¼Œæœªå‡ºçŽ°è¯æ¡åˆå§‹å€¼ä¸º 0。 +3. `功德有什么`:最多 8 æ¡é£˜å­—,创作æ€é¦–å±åªä¿ç•™ä¸€ä¸ªé»˜è®¤è¯æ¡ `幸è¿`,其下æä¾›åŠ å·æ ¼ç»§ç»­è¿½åŠ è¯æ¡ï¼›åˆ›ä½œæ€åªä¿å­˜è¯æ¡å,è¿è¡Œæ€é£˜å­—展示时å†è¿½åŠ  `+1`。è¿è¡Œæ€é¡¶éƒ¨æ€»æ•°å¡é‡‡ç”¨å“牌化徽标样å¼ï¼Œå­é¡¹è®¡æ•°å™¨é¢„置展示在å¯å±•开颿¿ä¸­ï¼Œæœªå‡ºçŽ°è¯æ¡åˆå§‹å€¼ä¸º 0。 +4. `ä½œå“æ ‡é¢˜ / 作å“简介 / 主题标签`:ä¸å†æ”¾åœ¨åˆ›ä½œå·¥ä½œå°é¦–å±ï¼Œæ”¹ä¸ºç”Ÿæˆè‰ç¨¿åŽçš„结果页补录区,æäº¤è¯•玩或å‘布å‰å¿…须先写回当å‰ä½œå“ä¿¡æ¯ã€‚主题标签编辑样å¼å¯¹é½æ‹¼å›¾ç»“果页的胶囊标签编辑器。 -图片生æˆé“¾è·¯å›ºå®šä¸ºä¸‰å›¾ image2 æµç¨‹ï¼šç¬¬ä¸€æ­¥ç”¨é»˜è®¤æœ¨é±¼å›¾ä½œä¸ºç»“构和画风å‚考,按用户题æå…³é”®è¯æˆ–å‚è€ƒå›¾ä¸»é¢˜ç”Ÿæˆ `1:1` 绿色背景主体图(纯绿色绿幕),prompt 必须显å¼è¦æ±‚背景为å•一纯绿色 `#00FF00` 且平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€æ— é“å…·ï¼Œä¸»ä½“å®Œæ•´å±…ä¸­ï¼Œä¸”ç¦æ­¢é»‘底ã€ç™½åº•ã€æ£‹ç›˜æ ¼å’Œä»»ä½•实底背景;åŽç«¯åœ¨è½åº“å‰åªå¯¹è¿™å¼ ç»¿å¹•主体图执行去绿背景处ç†ï¼Œä¸åšæ³›æŠ å›¾ï¼Œé¿å…误伤玉米等主体åƒç´ ã€‚第二步必须使用第一步抠图完æˆåŽçš„逿˜Žå›¾ä½œä¸ºå‚考图,å†ç”¨æ–°æ•²å‡»ç‰©ä½œä¸ºä¸»é¢˜å’Œç”»é£Žå‚è€ƒç”Ÿæˆ `9:16` 背景环境图,背景图åªé€‚é…主题和画风,ä¸èƒ½åŒ…嫿–°æ•²å‡»ç‰©æœ¬ä½“,也ä¸èƒ½å¢žåŠ æœ¨æ§Œäº’åŠ¨ç‰©å“;画é¢ä¸­å¤®ä¸»ä½“预留区必须干净,中央 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€è½®å»“影孿ˆ–é‡å¤å…ƒç´ ï¼Œä¸»é¢˜å…ƒç´ åªèƒ½ä½œä¸ºå¤–围氛围。第三步必须使用去绿åŽçš„æ•²å‡»ç‰©ä¸»ä½“图和背景环境图作为å‚è€ƒå›¾ç”Ÿæˆ `1:1` 返回按钮图,返回按钮必须始终是标准圆形,中央åªä¿ç•™å•个左箭头,å‚考图åªçº¦æŸåœ†å½¢åº•色和箭头é…色,ä¸å¾—å»¶ä¼¸åˆ°å¤æ‚造型和花纹;按钮ä¸å¾—å‡ºçŽ°æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€é¢å¤– UI 颿¿æˆ–木槌物å“。三个资产分别写回 `hitObjectAsset`ã€`backgroundAsset` 与 `backButtonAsset`,并绑定到 `wooden_fish_work` çš„ `hit_object` / `background` / `back_button` æ§½ä½ã€‚è¿è¡Œæ€å’Œç»“果页消费 `backgroundAsset` åšç«–å±èƒŒæ™¯ï¼Œä¸­å¤®å†å åŠ  `hitObjectAsset`,左上角返回按钮消费 `backButtonAsset`。 +图片生æˆé“¾è·¯å›ºå®šä¸ºä¸‰å›¾ image2 æµç¨‹ï¼šç¬¬ä¸€æ­¥ç”¨é»˜è®¤æœ¨é±¼å›¾ä½œä¸ºç»“构和画风å‚考,按用户题æå…³é”®è¯æˆ–å‚è€ƒå›¾ä¸»é¢˜ç”Ÿæˆ `1:1` 绿色背景主体图(纯绿色绿幕),prompt 必须显å¼è¦æ±‚背景为å•一纯绿色 `#00FF00` 且平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€æ— é“å…·ï¼Œä¸»ä½“å®Œæ•´å±…ä¸­ï¼Œä¸”ç¦æ­¢é»‘底ã€ç™½åº•ã€æ£‹ç›˜æ ¼å’Œä»»ä½•实底背景;åŽç«¯åœ¨è½åº“å‰åªå¯¹è¿™å¼ ç»¿å¹•主体图执行去绿背景处ç†ï¼Œä¸åšæ³›æŠ å›¾ï¼Œé¿å…误伤玉米等主体åƒç´ ã€‚第二步必须使用第一步抠图完æˆåŽçš„逿˜Žå›¾ä½œä¸ºå‚考图,å†ç”¨æ–°æ•²å‡»ç‰©ä½œä¸ºä¸»é¢˜å’Œç”»é£Žå‚è€ƒç”Ÿæˆ `9:16` 背景环境图,背景图åªé€‚é…主题和画风,ä¸èƒ½åŒ…嫿–°æ•²å‡»ç‰©æœ¬ä½“,也ä¸èƒ½å¢žåŠ æœ¨æ§Œäº’åŠ¨ç‰©å“;画é¢ä¸­å¤®ä¸»ä½“预留区必须干净,中央 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€è½®å»“影孿ˆ–é‡å¤å…ƒç´ ï¼Œä¸»é¢˜å…ƒç´ åªèƒ½ä½œä¸ºå¤–围氛围。第三步必须使用去绿åŽçš„æ•²å‡»ç‰©ä¸»ä½“图和背景环境图作为å‚è€ƒå›¾ç”Ÿæˆ `1:1` è¿”å›žæŒ‰é’®å›¾ï¼Œè¿”å›žæŒ‰é’®å¿…é¡»å§‹ç»ˆæ˜¯æ ‡å‡†åœ†å½¢ï¼Œä¸»ä½“è§†è§‰å°ºå¯¸æ¯”å½“å‰æ¨¡æ¿å†æ”¾å¤§çº¦ 50%,圆形外沿必须有与主题色æ­é…的干净外æè¾¹ï¼Œä¸­å¤®åªä¿ç•™å•个左箭头,å‚考图åªçº¦æŸåœ†å½¢åº•色和箭头é…色,ä¸å¾—å»¶ä¼¸åˆ°å¤æ‚造型和花纹;按钮ä¸å¾—å‡ºçŽ°æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€é¢å¤– UI 颿¿æˆ–木槌物å“。三个资产分别写回 `hitObjectAsset`ã€`backgroundAsset` 与 `backButtonAsset`,并绑定到 `wooden_fish_work` çš„ `hit_object` / `background` / `back_button` æ§½ä½ã€‚è¿è¡Œæ€å’Œç»“果页消费 `backgroundAsset` åšç«–å±èƒŒæ™¯ï¼Œä¸­å¤®å†å åŠ  `hitObjectAsset`,左上角返回按钮消费 `backButtonAsset`。 + +木鱼åˆå§‹ `compile-draft` æ˜¯é•¿è€—æ—¶åŒæ­¥ action,生æˆé¡µå¿…须按上述三图 image2 链路展示进度:整ç†è‰ç¨¿ã€ç”Ÿæˆæ•²å‡»ç‰©ã€ç”ŸæˆèƒŒæ™¯çŽ¯å¢ƒå›¾ã€ç”Ÿæˆè¿”回按钮图ã€å†™å…¥æ­£å¼è‰ç¨¿ã€‚本地或供应商慢时一次 action å¯èƒ½æŒç»­æ•°åˆ†é’Ÿï¼›å‰ç«¯ä¸å¾—把已关闭的æç¤ºè¯ç”ŸæˆéŸ³æ•ˆå½“æˆè¿›åº¦é˜¶æ®µï¼Œä¹Ÿä¸å¾—在未收到 action 回包å‰å®£ç§°ç”Ÿæˆå®Œæˆã€‚ è¿è¡Œæ€è§„则真相以åŽç«¯ run 摘è¦ä¸ºå‡†ï¼Œå‰ç«¯åªåšç‚¹å‡»ä½Žå»¶è¿Ÿè¡¨çŽ°ã€æ•²å‡»åŠ¨ç”»ã€éŸ³é¢‘æ’­æ”¾å’Œé£˜å­—æ¸²æŸ“ã€‚æ¯æ¬¡éžåŠŸèƒ½åŒºç‚¹å‡»åœ¨å½“å‰ run 内累计 `totalTapCount` å’Œ `wordCounters`;计数ä¸è¿›å…¥è´¦å·é•¿æœŸè´¦æœ¬ï¼Œä¸åšæŽ’è¡Œæ¦œã€‚é¡¶éƒ¨æ€»æ•°å¡ç‚¹å‡»åŽå±•å¼€å­é¡¹è®¡æ•°å™¨é¢æ¿ï¼Œå­é¡¹è®¡æ•°åœ¨é¢æ¿ä¸­æŒ‰è¯æ¡çºµåˆ—é¢„ç½®å±•ç¤ºï¼Œæœªå‡ºçŽ°è¯æ¡åˆå§‹å€¼ä¸º 0,åŽç»­åŒè¯æ¡ç»§ç»­ç´¯åŠ ï¼›è¿è¡Œæ€å·¦ä¸Šè§’ä½¿ç”¨ä¸»é¢˜åŒ–è¿”å›žæŒ‰é’®å›¾ï¼Œä¸æä¾›å³ä¸Šè§’é‡å¼€æŒ‰é’®ã€‚ diff --git a/server-rs/crates/api-server/src/match3d/tests.rs b/server-rs/crates/api-server/src/match3d/tests.rs index 905a1697..425fcb69 100644 --- a/server-rs/crates/api-server/src/match3d/tests.rs +++ b/server-rs/crates/api-server/src/match3d/tests.rs @@ -874,6 +874,7 @@ fn match3d_regenerated_asset_keeps_stable_identity_and_side_assets() { container_image_object_key: None, status: "image_ready".to_string(), error: None, + ..Default::default() }); let mut generated_asset = test_match3d_generated_item_asset(99, "æ–°è‰èŽ“"); generated_asset.image_src = @@ -1062,6 +1063,7 @@ fn match3d_background_asset_requires_background_and_container_images() { container_image_object_key: None, status: "image_ready".to_string(), error: None, + ..Default::default() }; let with_container = Match3DGeneratedBackgroundAsset { container_prompt: Some("果园容器".to_string()), @@ -1108,6 +1110,7 @@ fn match3d_default_cover_prefers_generated_container_ui_image() { container_image_object_key: None, status: "image_ready".to_string(), error: None, + ..Default::default() }), status: "image_ready".to_string(), error: None, @@ -1169,7 +1172,7 @@ fn match3d_cover_reference_prompt_marks_reference_images() { #[test] fn match3d_cover_edit_prompt_preserves_uploaded_image() { - let prompt = build_match3d_cover_edit_prompt("æ°´æžœå°é¢"); + let prompt = build_match3d_cover_uploaded_reference_prompt("æ°´æžœå°é¢"); assert!(prompt.contains("上传的å°é¢å›¾ä½œä¸ºç¬¬ä¸€ä¼˜å…ˆçº§")); assert!(prompt.contains("ä¿ç•™ä¸»å›¾çš„ä¸»ä½“ã€æž„图ã€è§†è§’和主è¦é…色")); @@ -1212,6 +1215,7 @@ fn match3d_fallback_work_profile_keeps_generated_background_asset() { ), status: "image_ready".to_string(), error: None, + ..Default::default() }), status: "image_ready".to_string(), error: None, @@ -1349,6 +1353,7 @@ fn match3d_agent_session_response_hydrates_persisted_ui_assets() { ), status: "image_ready".to_string(), error: None, + ..Default::default() }), status: "image_ready".to_string(), error: None, @@ -1424,6 +1429,7 @@ fn match3d_agent_session_response_keeps_draft_ui_assets_without_work_detail_hydr ), status: "image_ready".to_string(), error: None, + ..Default::default() }), status: "image_ready".to_string(), error: None, @@ -1807,6 +1813,7 @@ fn match3d_work_summary_marks_complete_generated_assets_ready() { ), status: "image_ready".to_string(), error: None, + ..Default::default() }), ..test_match3d_generated_item_asset(1, "è‰èŽ“") }]; diff --git a/server-rs/crates/api-server/src/wooden_fish.rs b/server-rs/crates/api-server/src/wooden_fish.rs index b5e94452..e254f3aa 100644 --- a/server-rs/crates/api-server/src/wooden_fish.rs +++ b/server-rs/crates/api-server/src/wooden_fish.rs @@ -747,7 +747,7 @@ fn build_wooden_fish_background_prompt(prompt: &str) -> String { fn build_wooden_fish_back_button_prompt(prompt: &str) -> String { format!( - "ç”Ÿæˆæ•²æœ¨é±¼å·¦ä¸Šè§’è¿”å›žæŒ‰é’®å›¾ã€‚è¦æ±‚以å‚考图-去除绿色背景åŽçš„æ•²å‡»ç‰©ä¸»ä½“和背景环境图为主题ã€ç”»é£Žã€æè´¨å’Œé…色å‚考,但å‚考图åªç”¨æ¥çº¦æŸåœ†å½¢åº•色和中央左箭头的颜色æ­é…,ä¸è¦ç»§æ‰¿å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框或装饰图案。按钮必须始终是标准圆形,整体åƒå•个圆形图标,圆心居中,圆形内部åªä¿ç•™ä¸€ä¸ªæ¸…æ™°ã€ç®€æ´ã€å±…中的å‘左返回箭头,ä¸è¦å‡ºçŽ°æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€æŒ‰é’®å¤–标签ã€é¢å¤– UI 颿¿ã€æœ¨æ§Œæˆ–敲击é“具。尺寸1:1,输出绿色背景主体图(纯绿色绿幕),背景必须是å•一纯绿色 #00FF00 且平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€‚按钮主体边缘干净,åŽç»­ç”±æœåŠ¡ç«¯æ‰£é™¤ç»¿è‰²èƒŒæ™¯ï¼›æŒ‰é’®åº•è‰²ä¸è¦ä½¿ç”¨ä¸Žç»¿å¹•接近的纯绿色,若主题天然包å«ç»¿è‰²ï¼Œè¯·ä»…åœ¨åœ†å½¢åº•è‰²ä¸Šä½¿ç”¨åæ·±ã€å黄或åè“的主题绿色,并用更高对比的箭头颜色区分。\n主题为:{}", + "ç”Ÿæˆæ•²æœ¨é±¼å·¦ä¸Šè§’è¿”å›žæŒ‰é’®å›¾ã€‚è¦æ±‚以å‚考图-去除绿色背景åŽçš„æ•²å‡»ç‰©ä¸»ä½“和背景环境图为主题ã€ç”»é£Žã€æè´¨å’Œé…色å‚考,但å‚考图åªç”¨æ¥çº¦æŸåœ†å½¢åº•色和中央左箭头的颜色æ­é…,ä¸è¦ç»§æ‰¿å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框或装饰图案。按钮必须始终是标准圆形,整体åƒå•ä¸ªåœ†å½¢å›¾æ ‡ï¼ŒæŒ‰é’®ä¸»ä½“åœ¨ç”»å¸ƒä¸­çš„è§†è§‰å°ºå¯¸æ¯”å½“å‰æ¨¡æ¿å†æ”¾å¤§çº¦ 50%,圆心居中,圆形外沿加一圈和主题色æ­é…的干净外æè¾¹ï¼Œè®©å®ƒæ›´åƒä¸€ä¸ªæŒ‰é’®ï¼Œä½†ä»ç„¶åªä¿ç•™ä¸€ä¸ªæ¸…æ™°ã€ç®€æ´ã€å±…中的å‘左返回箭头,ä¸è¦å‡ºçŽ°æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€æŒ‰é’®å¤–标签ã€é¢å¤– UI 颿¿ã€æœ¨æ§Œæˆ–敲击é“具。尺寸1:1,输出绿色背景主体图(纯绿色绿幕),背景必须是å•一纯绿色 #00FF00 且平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€‚按钮主体边缘干净,åŽç»­ç”±æœåŠ¡ç«¯æ‰£é™¤ç»¿è‰²èƒŒæ™¯ï¼›æŒ‰é’®åº•è‰²ä¸è¦ä½¿ç”¨ä¸Žç»¿å¹•接近的纯绿色,若主题天然包å«ç»¿è‰²ï¼Œè¯·ä»…åœ¨åœ†å½¢åº•è‰²ä¸Šä½¿ç”¨åæ·±ã€å黄或åè“的主题绿色,并用更高对比的箭头颜色区分。\n主题为:{}", clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT) ) } @@ -1230,7 +1230,9 @@ mod tests { assert!(prompt.contains("å‚考图åªç”¨æ¥çº¦æŸåœ†å½¢åº•色和中央左箭头的颜色æ­é…")); assert!(prompt.contains("按钮必须始终是标准圆形")); - assert!(prompt.contains("圆形内部åªä¿ç•™ä¸€ä¸ªæ¸…æ™°ã€ç®€æ´ã€å±…中的å‘左返回箭头")); + assert!(prompt.contains("æŒ‰é’®ä¸»ä½“åœ¨ç”»å¸ƒä¸­çš„è§†è§‰å°ºå¯¸æ¯”å½“å‰æ¨¡æ¿å†æ”¾å¤§çº¦ 50%")); + assert!(prompt.contains("圆形外沿加一圈和主题色æ­é…的干净外æè¾¹")); + assert!(prompt.contains("åªä¿ç•™ä¸€ä¸ªæ¸…æ™°ã€ç®€æ´ã€å±…中的å‘左返回箭头")); assert!(prompt.contains("ä¸è¦ç»§æ‰¿å¤æ‚造型ã€èŠ±çº¹ã€æµ®é›•è¾¹ã€å¼‚形外框或装饰图案")); assert!(prompt.contains("ä¸è¦å‡ºçŽ°æ–‡å­—ã€æ•°å­—ã€æ°´å°ã€æŒ‰é’®å¤–标签ã€é¢å¤– UI 颿¿ã€æœ¨æ§Œæˆ–敲击é“å…·")); assert!(prompt.contains("按钮底色ä¸è¦ä½¿ç”¨ä¸Žç»¿å¹•接近的纯绿色")); diff --git a/src/components/common/CreativeImageInputPanel.tsx b/src/components/common/CreativeImageInputPanel.tsx index 78448e9d..1c23c838 100644 --- a/src/components/common/CreativeImageInputPanel.tsx +++ b/src/components/common/CreativeImageInputPanel.tsx @@ -56,6 +56,7 @@ export type CreativeImageInputPanelProps = { imageModelPicker?: ReactNode; error?: string | null; inputError?: string | null; + showSubmitButton?: boolean; submitLabel: string; submitCostLabel?: string | null; submitDisabled: boolean; @@ -98,6 +99,7 @@ export function CreativeImageInputPanel({ imageModelPicker = null, error = null, inputError = null, + showSubmitButton = true, submitLabel, submitCostLabel = null, submitDisabled, @@ -382,27 +384,31 @@ export function CreativeImageInputPanel({ -
- -
+ {showSubmitButton ? ( +
+ +
+ ) : null} {previewReferenceImage ? (
{ + const sessionId = woodenFishSession?.sessionId?.trim(); + const profileId = + woodenFishWork?.summary.profileId?.trim() || + woodenFishSession?.draft?.profileId?.trim() || + ''; + if (!sessionId || !profileId) { + setWoodenFishError('敲木鱼è‰ç¨¿å°šæœªç”Ÿæˆå¯ä¿å­˜ä½œå“ä¿¡æ¯ã€‚'); + setSelectionStage('wooden-fish-result'); + return false; + } + + setIsWoodenFishBusy(true); + setWoodenFishError(null); + try { + const response = await woodenFishClient.executeAction(sessionId, { + actionType: 'update-work-meta', + profileId, + workTitle: payload.workTitle, + workDescription: payload.workDescription, + themeTags: payload.themeTags, + }); + setWoodenFishSession(response.session); + setWoodenFishWork(response.work ?? woodenFishWork); + return true; + } catch (error) { + setWoodenFishError( + resolveRpgCreationErrorMessage(error, 'ä¿å­˜æ•²æœ¨é±¼ä½œå“ä¿¡æ¯å¤±è´¥ã€‚'), + ); + setSelectionStage('wooden-fish-result'); + return false; + } finally { + setIsWoodenFishBusy(false); + } + }, + [ + setSelectionStage, + woodenFishSession?.draft?.profileId, + woodenFishSession?.sessionId, + woodenFishWork, + ], + ); + const publishWoodenFishDraft = useCallback(async () => { const profileId = woodenFishWork?.summary.profileId?.trim(); if (!profileId) { @@ -14386,6 +14434,7 @@ export function PlatformEntryFlowShellImpl({ onRegenerateHitObject={() => { void regenerateWoodenFishAsset('regenerate-hit-object'); }} + onUpdateWorkMeta={updateWoodenFishWorkMeta} /> diff --git a/src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx b/src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx index d7ce7650..9a9643fe 100644 --- a/src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx +++ b/src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx @@ -1,10 +1,51 @@ /* @vitest-environment jsdom */ -import { render, screen, within } from '@testing-library/react'; -import { expect, test } from 'vitest'; +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { beforeEach, expect, test, vi } from 'vitest'; +import { woodenFishClient } from '../../services/wooden-fish/woodenFishClient'; +import { WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT } from '../../services/wooden-fish/woodenFishDefaults'; import { WoodenFishWorkspace } from './WoodenFishWorkspace'; +vi.mock('../../services/wooden-fish/woodenFishClient', () => ({ + woodenFishClient: { + createSession: vi.fn(), + }, +})); + +beforeEach(() => { + vi.mocked(woodenFishClient.createSession).mockReset(); + vi.mocked(woodenFishClient.createSession).mockResolvedValue({ + session: { + sessionId: 'wooden-fish-session-test', + ownerUserId: 'user-test', + status: 'draft', + draft: null, + createdAt: '2026-05-24T00:00:00Z', + updatedAt: '2026-05-24T00:00:00Z', + }, + }); +}); + +test('敲什么输入æ åˆå§‹ç½®ç©ºä½†æäº¤æ—¶ä»ä½¿ç”¨é»˜è®¤ç”Ÿæˆæç¤ºè¯', async () => { + const onSubmitted = vi.fn(); + + render( + {}} + onSubmitted={onSubmitted} + />, + ); + + expect(screen.getByLabelText('敲什么')).toHaveProperty('value', ''); + fireEvent.click(screen.getByRole('button', { name: '生æˆ' })); + + await waitFor(() => expect(onSubmitted).toHaveBeenCalledTimes(1)); + expect(onSubmitted.mock.calls[0]?.[1]).toMatchObject({ + hitObjectPrompt: WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT, + }); +}); + test('åŠŸå¾·æœ‰ä»€ä¹ˆé»˜è®¤åªæ˜¾ç¤ºåŸºç¡€è¯æ¡ï¼Œä¸æ˜¾ç¤ºè¿è¡Œæ€ +1 åŽç¼€', () => { render( { + render( + {}} + onSubmitted={() => {}} + />, + ); + + fireEvent.click(screen.getByRole('button', { name: 'æ–°å¢žåŠŸå¾·è¯æ¡' })); + const secondInput = screen.getByLabelText('åŠŸå¾·è¯æ¡ 2'); + fireEvent.change(secondInput, { target: { value: 'å¥åº·' } }); + + expect(screen.getByDisplayValue('幸è¿')).toBeTruthy(); + expect(screen.getByDisplayValue('å¥åº·')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: 'åˆ é™¤åŠŸå¾·è¯æ¡ 2' })); + expect(screen.queryByDisplayValue('å¥åº·')).toBeNull(); + expect(screen.getByDisplayValue('幸è¿')).toBeTruthy(); }); test('敲击音效临时关闭æç¤ºè¯ç”Ÿæˆå…¥å£ï¼Œä»…ä¿ç•™ä¸Šä¼ å’Œå½•音', () => { @@ -40,3 +106,14 @@ test('敲击音效临时关闭æç¤ºè¯ç”Ÿæˆå…¥å£ï¼Œä»…ä¿ç•™ä¸Šä¼ å’Œå½•音', expect(within(section as HTMLElement).getByText('上传')).toBeTruthy(); expect(within(section as HTMLElement).getByText('录音')).toBeTruthy(); }); + +test('工作å°åªä¿ç•™ä¸€ä¸ªç”ŸæˆæŒ‰é’®', () => { + render( + {}} + onSubmitted={() => {}} + />, + ); + + expect(screen.getAllByRole('button', { name: '生æˆ' })).toHaveLength(1); +}); diff --git a/src/components/wooden-fish-creation/WoodenFishWorkspace.tsx b/src/components/wooden-fish-creation/WoodenFishWorkspace.tsx index 37ec8184..9b6e6445 100644 --- a/src/components/wooden-fish-creation/WoodenFishWorkspace.tsx +++ b/src/components/wooden-fish-creation/WoodenFishWorkspace.tsx @@ -3,7 +3,9 @@ import { Loader2, Mic, Pause, + Plus, Send, + X, Upload, } from 'lucide-react'; import { useMemo, useRef, useState } from 'react'; @@ -32,44 +34,24 @@ type WoodenFishWorkspaceProps = { }; type WoodenFishWorkspaceFormState = { - workTitle: string; - workDescription: string; - themeTags: string; hitObjectPrompt: string; hitObjectReferenceImageSrc: string; hitSoundAsset: WoodenFishAudioAsset | null; floatingWords: string[]; }; -const DEFAULT_FLOATING_WORDS = [ - '幸è¿', - 'å¥åº·', - '财富', - '姻缘', - '幸ç¦', - '事业', - 'æˆåŠŸ', - '功德', -]; +const DEFAULT_WORK_TITLE = '今日敲木鱼'; +const DEFAULT_THEME_TAGS = ['敲木鱼', '解压']; +const DEFAULT_FLOATING_WORDS = ['幸è¿']; +const MAX_FLOATING_WORD_COUNT = 8; const DEFAULT_FORM_STATE: WoodenFishWorkspaceFormState = { - workTitle: '今日敲木鱼', - workDescription: '', - themeTags: '敲木鱼 解压', - hitObjectPrompt: WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT, + hitObjectPrompt: '', hitObjectReferenceImageSrc: '', hitSoundAsset: null, floatingWords: DEFAULT_FLOATING_WORDS, }; -function splitTags(value: string) { - return value - .split(/[,,ã€\s]+/u) - .map((item) => item.trim()) - .filter(Boolean) - .slice(0, 6); -} - function normalizeFloatingWords(words: string[]) { const seen = new Set(); const normalized: string[] = []; @@ -84,7 +66,7 @@ function normalizeFloatingWords(words: string[]) { break; } } - return normalized.length > 0 ? normalized : DEFAULT_FLOATING_WORDS; + return normalized.length > 0 ? normalized : [...DEFAULT_FLOATING_WORDS]; } function readAudioFileAsAsset(file: File, source: 'uploaded' | 'recorded') { @@ -278,11 +260,42 @@ export function WoodenFishWorkspace({ () => normalizeFloatingWords(formState.floatingWords), [formState.floatingWords], ); - const canSubmit = Boolean( - formState.workTitle.trim() && - formState.hitObjectPrompt.trim() && - normalizedFloatingWords.length > 0, - ); + const canSubmit = normalizedFloatingWords.length > 0; + + const updateFloatingWord = (index: number, value: string) => { + setFormState((current) => { + const nextWords = [...current.floatingWords]; + nextWords[index] = value; + return { + ...current, + floatingWords: nextWords.slice(0, MAX_FLOATING_WORD_COUNT), + }; + }); + }; + + const addFloatingWord = () => { + setFormState((current) => { + if (current.floatingWords.length >= MAX_FLOATING_WORD_COUNT) { + return current; + } + return { + ...current, + floatingWords: [...current.floatingWords, ''], + }; + }); + }; + + const removeFloatingWord = (index: number) => { + if (index <= 0) { + return; + } + setFormState((current) => ({ + ...current, + floatingWords: current.floatingWords.filter( + (_word, currentIndex) => currentIndex !== index, + ), + })); + }; const handleSubmit = async () => { if (!canSubmit || isSubmitting || isBusy) { @@ -296,10 +309,11 @@ export function WoodenFishWorkspace({ try { const payload: WoodenFishWorkspaceCreateRequest = { templateId: 'wooden-fish', - workTitle: formState.workTitle.trim(), - workDescription: formState.workDescription.trim(), - themeTags: splitTags(formState.themeTags), - hitObjectPrompt: formState.hitObjectPrompt.trim(), + workTitle: DEFAULT_WORK_TITLE, + workDescription: '', + themeTags: DEFAULT_THEME_TAGS, + hitObjectPrompt: + formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT, hitObjectReferenceImageSrc: formState.hitObjectReferenceImageSrc.trim() || null, hitSoundPrompt: null, @@ -345,6 +359,7 @@ export function WoodenFishWorkspace({ promptRows={4} aiRedraw={aiRedraw} promptReferenceImages={[]} + showSubmitButton={false} submitLabel="生æˆ" submitDisabled={!canSubmit || isSubmitting || isBusy} labels={{ @@ -395,55 +410,6 @@ export function WoodenFishWorkspace({
-
- -