From 74fd9a33ac92f9fd5ebd54a39192b3361b608e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Fri, 15 May 2026 02:40:59 +0800 Subject: [PATCH] Increase VectorEngine timeouts and add image UI Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps. --- .codex/logs/run-dev-web-final.ps1 | 4 + .../generate-anthro-cat-illustrations.mjs | 2 +- .../scripts/generate-template-samples.mjs | 2 +- .env.example | 5 + .hermes/shared-memory/decision-log.md | 42 + .hermes/shared-memory/pitfalls.md | 77 +- deploy/env/api-server.env.example | 2 +- ..._WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md | 22 +- ...EATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md | 2 + ..._DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md | 1 + docs/experience/MOBILE_UI_DEV_EXPERIENCE.md | 15 +- ...ATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md | 1 + .../MY_TAB_FEATURE_PRD_INDEX_2026-04-16.md | 1 + ...MY_TAB_MEMBERSHIP_CENTER_PRD_2026-04-16.md | 2 + ..._EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md | 6 +- ...ENERATION_POINTS_CONSUMPTION_2026-04-27.md | 2 + ...ISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md | 7 + ...UTH_NEW_ACCOUNT_INVITE_MODAL_2026-05-01.md | 11 +- ...E_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md | 10 +- ...ATION_WORK_SHELF_UNIFICATION_2026-04-25.md | 1 + ...FT_ASSET_GENERATION_PIPELINE_2026-05-10.md | 14 +- .../NEW_WORK_ENTRY_CONFIG_2026-05-01.md | 7 +- ...MMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md | 2 + ..._APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md | 2 +- .../PUZZLE_FORM_CREATION_FLOW_2026-04-29.md | 27 +- ...PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md | 3 +- docs/technical/README.md | 2 + ...RATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md | 2 +- ...NGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md | 2 +- ...½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md | 32 + ...Œã€‘移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md | 22 + .../src/contracts/puzzleAgentActions.ts | 3 + .../src/contracts/puzzleAgentSession.ts | 1 + packages/shared/src/http.ts | 8 +- scripts/generate-bark-battle-assets.mjs | 2 +- scripts/generate-child-motion-demo-assets.mjs | 2 +- scripts/generate-match3d-style-references.mjs | 2 +- scripts/generate-taonier-logo-concepts.mjs | 2 +- server-rs/crates/api-server/src/config.rs | 14 +- .../api-server/src/edutainment_baby_object.rs | 6 +- server-rs/crates/api-server/src/match3d.rs | 416 ++++++++- .../api-server/src/openai_image_generation.rs | 4 +- .../src/prompt/puzzle/level_name.rs | 24 +- server-rs/crates/api-server/src/puzzle.rs | 556 +++++++++++- server-rs/crates/module-runtime/src/lib.rs | 17 + .../shared-contracts/src/puzzle_agent.rs | 4 + .../spacetime-module/src/match3d/mod.rs | 75 +- spacetime.local.json | 2 +- .../CustomWorldGenerationView.test.tsx | 129 +++ src/components/CustomWorldGenerationView.tsx | 111 ++- .../BarkBattleConfigEditor.test.tsx | 20 +- .../BarkBattleConfigEditor.tsx | 281 +++++-- .../BarkBattlePreviewCard.tsx | 59 +- .../common/CreativeImageInputPanel.test.tsx | 145 ++++ .../common/CreativeImageInputPanel.tsx | 463 ++++++++++ ...ustomWorldCreationHub.interaction.test.tsx | 84 +- .../custom-world-home/CustomWorldWorkCard.tsx | 388 ++++++--- .../creationWorkShelf.test.ts | 178 +++- .../custom-world-home/creationWorkShelf.ts | 74 +- .../match3d-result/Match3DResultView.test.tsx | 183 +++- .../match3d-result/Match3DResultView.tsx | 792 +++++++++++------- .../Match3DRuntimeShell.test.tsx | 36 +- .../match3d-runtime/Match3DRuntimeShell.tsx | 30 +- .../PlatformEntryFlowShellImpl.tsx | 215 ++++- .../platform-entry/platformEntryTypes.ts | 1 - .../PuzzleAgentWorkspace.interaction.test.tsx | 99 ++- .../puzzle-agent/PuzzleAgentWorkspace.tsx | 476 +++++------ .../PuzzleHistoryAssetPickerDialog.tsx | 71 +- .../puzzle-result/PuzzleResultView.test.tsx | 48 +- .../puzzle-result/PuzzleResultView.tsx | 7 +- .../PuzzleRuntimeShell.test.tsx | 111 +++ .../puzzle-runtime/PuzzleRuntimeShell.tsx | 3 +- ...gEntryFlowShell.agent.interaction.test.tsx | 542 +++++++++++- .../RpgEntryHomeView.recharge.test.tsx | 13 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 6 - src/index.css | 239 +++--- src/main.tsx | 2 + src/mobileViewportKeyboardFocus.test.ts | 77 ++ src/mobileViewportKeyboardFocus.ts | 261 ++++++ src/services/apiClient.test.ts | 43 + .../babyDrawingClient.test.ts | 2 +- .../babyDrawingClient.ts | 3 +- .../babyObjectMatchClient.test.ts | 2 +- .../babyObjectMatchClient.ts | 2 +- .../match3d-works/match3dWorksClient.ts | 7 +- .../platform-entry/platformProfileClient.ts | 4 +- .../puzzle-works/puzzleHistoryAsset.ts | 94 +++ 87 files changed, 5508 insertions(+), 1261 deletions(-) create mode 100644 .codex/logs/run-dev-web-final.ps1 create mode 100644 docs/technical/ã€å‰ç«¯ä½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md create mode 100644 docs/technical/ã€å‰ç«¯ä½“验】移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md create mode 100644 src/components/CustomWorldGenerationView.test.tsx create mode 100644 src/components/common/CreativeImageInputPanel.test.tsx create mode 100644 src/components/common/CreativeImageInputPanel.tsx create mode 100644 src/mobileViewportKeyboardFocus.test.ts create mode 100644 src/mobileViewportKeyboardFocus.ts create mode 100644 src/services/puzzle-works/puzzleHistoryAsset.ts diff --git a/.codex/logs/run-dev-web-final.ps1 b/.codex/logs/run-dev-web-final.ps1 new file mode 100644 index 00000000..196bc9f0 --- /dev/null +++ b/.codex/logs/run-dev-web-final.ps1 @@ -0,0 +1,4 @@ +Set-Location 'C:\Genarrative' +$env:RUST_SERVER_TARGET = 'http://127.0.0.1:8082' +$env:GENARRATIVE_RUNTIME_SERVER_TARGET = 'http://127.0.0.1:8082' +npm.cmd run dev:web *> 'C:\Genarrative\.codex\logs\dev-web-final.out.log' diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs index 63e35176..1ba9eb69 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs @@ -8,7 +8,7 @@ const __dirname = path.dirname(__filename); const skillRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(skillRoot, '..', '..', '..'); const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations'); -const defaultTimeoutMs = 180000; +const defaultTimeoutMs = 1000000; const prompts = [ { diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs index f3a69aed..72e05646 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs @@ -13,7 +13,7 @@ const promptsPath = path.join( 'puzzle-template-prompts.json', ); const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates'); -const defaultTimeoutMs = 180000; +const defaultTimeoutMs = 1000000; const args = new Map(); for (let index = 2; index < process.argv.length; index += 1) { diff --git a/.env.example b/.env.example index d2cfd6e6..03d8c2c1 100644 --- a/.env.example +++ b/.env.example @@ -127,6 +127,11 @@ APIMART_BASE_URL="https://api.apimart.ai/v1" APIMART_API_KEY="YOUR_APIMART_API_KEY" APIMART_IMAGE_REQUEST_TIMEOUT_MS="180000" +# VectorEngine GPT-image-2 / Gemini image generation config. +VECTOR_ENGINE_BASE_URL="https://api.vectorengine.ai" +VECTOR_ENGINE_API_KEY="" +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000" + # 阿里云 OSS é…置。 # Rust `server-rs` çš„ `api-server` 会优先从 `.env` / `.env.local` 读å–这些å˜é‡ï¼Œ # ç”¨äºŽç­¾å‘æµè§ˆå™¨ PostObject 直传票æ®ï¼Œå¹¶ä¿æŒ `/generated-*` 旧路径习惯。 diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 3d5493df..dde079e5 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,38 @@ --- +## 2026-05-14 创作页图åƒè¾“入统一å°è£…为图åƒç»„ä»¶ + +- 背景:拼图创作页已ç»å…·å¤‡â€œç”»é¢æè¿°ç”Ÿå›¾ / 多å‚考图生图 / ä¸Šä¼ ä¸»å›¾åŽ AI é‡ç»˜ / 上传主图åŽä¸é‡ç»˜â€å››æ¡è·¯å¾„,抓大鹅å°é¢å’ŒåŽç»­åˆ›ä½œé¡µä¹Ÿä¼šå¤ç”¨åŒä¸€å¥—交互;继续在页é¢å†…å¤åˆ¶ä¼šå¯¼è‡´å‚考图ã€é¢„览ã€åˆ é™¤ç¡®è®¤å’Œé‡ç»˜å¼€å…³æ¼‚移。 +- 决策:通用图åƒè¾“å…¥ UI 统一使用 `src/components/common/CreativeImageInputPanel.tsx`ã€‚ç»„ä»¶é‡‡ç”¨å—æŽ§æ¨¡å¼ï¼Œåªè´Ÿè´£ä¸»å›¾ä¸Šä¼ å¡ã€ç”»é¢æè¿°è¾“å…¥ã€å‚考图缩略图与预览ã€AI é‡ç»˜å¼€å…³ã€é”™è¯¯å±•示和æäº¤æŒ‰é’®ï¼›å¤–层页é¢è´Ÿè´£æ–‡ä»¶è¯»å–/è£å‰ªã€åކå²ç´ æå¼¹å±‚ã€è®¡è´¹ç¡®è®¤ã€è‡ªåЍä¿å­˜å’Œå…·ä½“åŽç«¯è¯·æ±‚。 +- å½±å“范围:拼图创作入å£ã€åŽç»­æŠ“大鹅å°é¢ç”Ÿæˆå…¥å£ã€å…¶å®ƒéœ€è¦å¤ç”¨å›¾åƒè¾“入链路的创作页。 +- éªŒè¯æ–¹å¼ï¼šæ‹¼å›¾å…¥å£äº¤äº’测试继续覆盖四ç§è·¯å¾„ï¼›åŽç»­é¡µé¢æŽ¥å…¥æ—¶åªä¼ å…¥ä¸šåŠ¡å›žè°ƒä¸Žæ–‡æ¡ˆï¼Œä¸å¤åˆ¶ä¸Šä¼ å¡å’Œå‚考图缩略图实现。 +- å…³è”æ–‡æ¡£ï¼š`docs/technical/ã€å‰ç«¯ä½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md`。 + +## 2026-05-14 æ±ªæ±ªå£°æµªåˆ›ä½œå…¥å£æ”¹ä¸ºåˆ›ä½œ Tab 内嵌轻é…ç½® + +- èƒŒæ™¯ï¼šæ±ªæ±ªå£°æµªå…¥å£æœ€åˆèµ°ç‹¬ç«‹é…ç½®é˜¶æ®µï¼Œå’Œæ‹¼å›¾ã€æŠ“å¤§é¹…çš„åˆ›ä½œé¡µå†…åµŒç»“æž„ä¸ä¸€è‡´ï¼Œç”¨æˆ·åœ¨å…¥å£åˆ‡æ¢æ—¶ä¼šæ„Ÿè§‰åƒè·³åˆ°äº†å¦ä¸€å¼ é¡µé¢ã€‚ +- 决策:`bark-battle` 的创作入å£åªåœ¨åˆ›ä½œ Tab 内嵌渲染轻é…置表å•,入å£ç‚¹å‡»åªåˆ‡åˆ°åˆ›ä½œé¡µå¹¶é€‰ä¸­è¯¥æ¨¡æ¿ï¼Œä¸å†ä½¿ç”¨ `bark-battle-config` 独立阶段;runtime 退出时回到创作页并æ¢å¤æ±ªæ±ªå£°æµªæ¨¡æ¿é€‰ä¸­æ€ã€‚ +- å½±å“范围:`PlatformEntryFlowShellImpl`ã€`BarkBattleConfigEditor`ã€`BarkBattleRuntimeShell`ã€å…¥å£é…置说明和相关交互测试。 +- éªŒè¯æ–¹å¼ï¼šåˆ›ä½œ Tab 中点击汪汪声浪åŽç›´æŽ¥çœ‹åˆ°å†…嵌表å•,ä¸åº”å†å‡ºçްå•独é…置页;å‘布进入 runtime åŽé€€å‡ºåº”回到创作页的汪汪声浪模æ¿ã€‚ +- å…³è”æ–‡æ¡£ï¼š`docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md`。 + +## 2026-05-14 拼图与抓大鹅生æˆé¡µç§»åŠ¨ç«¯æ”¶å£ä¸ºç­‰å¾…ä¸Žè®¡æ—¶åŒæ  + +- 背景:拼图与抓大鹅的è‰ç¨¿ç”Ÿæˆé¡µåœ¨ç§»åŠ¨ç«¯åŒæ—¶å±•ç¤ºâ€œå½“å‰æ‰¹æ¬¡â€â€œé¢„计等待â€â€œè®¡æ—¶â€æ—¶ï¼Œæ¨¡åž‹æ‰§è¡Œè§†è§’过é‡ï¼Œä¿¡æ¯ä¹Ÿæ˜¾å¾—散。 +- 决策:这两类轻é‡çŽ©æ³•çš„ç”Ÿæˆé¡µéšè—â€œå½“å‰æ‰¹æ¬¡â€æ¨¡å—,åªä¿ç•™â€œé¢„计等待â€å’Œâ€œè®¡æ—¶â€å¹¶æŽ’å±•ç¤ºï¼›ç”Ÿæˆæ­¥éª¤è¿›å…¥é¡µé¢æ—¶æŒ‰é¡ºåºä»Žå·¦ä¾§æ»‘入,强化推进感。 +- å½±å“范围:`CustomWorldGenerationView`ã€æ‹¼å›¾ä¸ŽæŠ“大鹅创作入å£è°ƒç”¨å¤„ã€ç§»åŠ¨ç«¯ç”Ÿæˆé¡µä½“验文档。 +- éªŒè¯æ–¹å¼ï¼šæ‹¼å›¾ä¸ŽæŠ“大鹅生æˆé¡µåœ¨æ‰‹æœºç«–å±ä¸‹åªæ˜¾ç¤ºç­‰å¾…ä¸Žè®¡æ—¶åŒæ ï¼Œæ­¥éª¤å¡æŒ‰é¡ºåºæ»‘入;其它未传入éšè—傿•°çš„生æˆé¡µç»§ç»­ä¿ç•™åŽŸæ‰¹æ¬¡æ¨¡å—。 +- å…³è”æ–‡æ¡£ï¼š`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。 + +## 2026-05-14 移动端输入法弹出时平å°ç”»å¸ƒä¸åŽ‹ç¼© + +- èƒŒæ™¯ï¼šå¹³å°æ ¹å£³ä½¿ç”¨ `100dvh` åŽï¼Œæ‰‹æœºæµè§ˆå™¨è¾“入法弹出会让å¯è§è§†å£å˜å°ï¼Œå¯¼è‡´åˆ›ä½œé¦–é¡µã€æŽ¨è页等固定游æˆå¼ç”»å¸ƒè¢«é‡æ–°åŽ‹ç¼©ã€‚ +- 决策:主站入å£ç»Ÿä¸€æ³¨å†Œç§»åŠ¨ç«¯è¾“å…¥æ³•èšç„¦é€‚é…;输入法未打开时记录稳定布局高度,输入法打开期间 `.platform-viewport-shell` ä¸è·Ÿéš `visualViewport.height` 缩å°ï¼Œåªé€šè¿‡ `--platform-keyboard-focus-offset` 上移画é¢èšç„¦å½“å‰è¾“入框,并临时éšè—移动端底部 dock。 +- å½±å“范围:主站平å°å£³ã€ç§»åŠ¨ç«¯åˆ›ä½œé¦–é¡µåº•éƒ¨è¾“å…¥æ¡†ã€åŽç»­æ‰€æœ‰å¤ç”¨ `.platform-viewport-shell` 的输入表å•;业务组件ä¸é‡å¤æ³¨å†Œé”®ç›˜é€‚é…。 +- éªŒè¯æ–¹å¼ï¼šæ‰‹æœºç«–å±ç‚¹å‡»è¾“入框,画布ä¸åŽ‹ç¼©ï¼Œè¾“å…¥æ¡†ç§»åŠ¨åˆ°è¾“å…¥æ³•ä¸Šæ–¹ï¼›è¾“å…¥æ³•å…³é—­åŽç”»å¸ƒå›žä½ï¼Œåº•部 dock æ¢å¤ã€‚ +- å…³è”æ–‡æ¡£ï¼š`docs/technical/ã€å‰ç«¯ä½“验】移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md`ã€`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。 + ## 2026-05-14 抓大鹅物å“ç´ ææ‰¹é‡é‡æ–°ç”Ÿæˆå¤ç”¨ item-assets æ›¿æ¢æ¨¡å¼ - 背景:抓大鹅结果页 `ç´ æé…ç½® > 物å“` 需è¦åœ¨ä¸æ”¹å˜çŽ©æ³•ç‰©å“æ˜ å°„çš„å‰æä¸‹ï¼Œæ‰¹é‡é‡æ–°ç”Ÿæˆå·²å­˜åœ¨ç‰©å“çš„ 2D 五视角图片。 @@ -48,6 +80,8 @@ - éªŒè¯æ–¹å¼ï¼šè‰ç¨¿é¡µä½œå“å¡ä¸Žåˆ†ç±»é¡µåˆ—表视觉å£å¾„ä¿æŒä¸€è‡´ï¼›`npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`ã€`npm run typecheck`ã€`npm run check:encoding`。 - å…³è”æ–‡æ¡£ï¼š`docs/design/MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md`ã€`docs/experience/MOBILE_UI_DEV_EXPERIENCE.md`。 +2026-05-14 补充:è‰ç¨¿é¡µä½œå“å¡ä¸å†ç”¨â€œè‰ç¨¿ / å·²å‘å¸ƒâ€æ–‡å­—标识状æ€ï¼Œæ”¹ä¸ºå›¾æ ‡åŒ– UI 状æ€ç‚¹ï¼›ä½œå“å°é¢ç›´æŽ¥é“ºåˆ°å¡ç‰‡å³åŠåŒºå¹¶ä»Žå³å‘å·¦æ¸éšï¼›å·²å‘布作å“å³ä¸Šè§’常驻分享图标;è‰ç¨¿é•¿æŒ‰å¼¹å‡ºåˆ é™¤é¢æ¿ï¼Œå·²å‘å¸ƒé•¿æŒ‰å¼¹å‡ºåˆ†äº«å’Œåˆ é™¤é¢æ¿ã€‚ + ## 2026-05-13 认è¯è¿è¡ŒæœŸåŒæ­¥ç›´æŽ¥å¯¼å…¥æ­£å¼è®¤è¯è¡¨ - 背景:`auth_store_snapshot` 是 Stage 1 整包快照过渡表,主键固定 `default`,会让所有用户状æ€é›†ä¸­åœ¨ä¸€æ¡ `snapshot_json` 中;Stage 2/3 已有 `user_account/auth_identity/refresh_session` æ­£å¼è®¤è¯è¡¨ï¼Œç»§ç»­åˆ·æ–° `default` 容易让è¿è¡Œæ—¶çœŸç›¸å’Œè¡¨æ‹†åˆ†ç›®æ ‡æ··åœ¨ä¸€èµ·ã€‚ @@ -346,6 +380,14 @@ - éªŒè¯æ–¹å¼ï¼šæ£€æŸ¥ç§»åŠ¨ç«¯åº•éƒ¨å¯¼èˆªæ–‡æ¡ˆå’Œé¡ºåºï¼Œç¡®è®¤ç™»å½•æ€ä¸ºâ€œæŽ¨è/å‘现/创作/è‰ç¨¿/我的â€ï¼Œæœªç™»å½•æ€ä¸ºâ€œæŽ¨è/创作/å‘现â€ä¸”创作居中;“推èâ€æ— æœç´¢/频铿 ç›´å‡ºä½œå“æµï¼Œâ€œå‘现â€åŒ…嫿œç´¢/推è/今日/分类/排行,“创作â€åªæ˜¾ç¤ºæ–°å»ºå…¥å£ï¼Œâ€œè‰ç¨¿â€æ˜¾ç¤ºä½œå“架,“我的-玩过â€å¯æ¢å¤å­˜æ¡£ã€‚ - å…³è”æ–‡æ¡£ï¼š`docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md`。 +## 2026-05-14 推è页å¡ç‰‡ä¸»è§†è§‰ä¼˜å…ˆäºŽåº•部作者热区 + +- 背景:移动端推è页的å¡ç‰‡åº•部作者与æ“作区如果过高,会压缩作å“è¿è¡Œæ€å¯è§†é«˜åº¦ï¼Œå½±å“首屿²‰æµ¸æ„Ÿã€‚ +- 决策:推è页å¡ç‰‡åº•部信æ¯åŒºä¿æŒç´§å‡‘å›ºå®šé«˜åº¦ï¼Œåˆ‡æ¢æ‰‹åŠ¿ä»åªç»‘定在该区域;视觉主体高度优先扩展,ä¸å†è®©ä½œè€…ä¿¡æ¯åŒºå ç”¨è¿‡å¤šé¦–å±ç©ºé—´ã€‚ +- å½±å“范围:`src/components/rpg-entry/RpgEntryHomeView.tsx` 的推è页å¡ç‰‡å¸ƒå±€ï¼Œä»¥åŠ `src/index.css` 中的推è页å¡ç‰‡çƒ­åŒºæ ·å¼ã€‚ +- éªŒè¯æ–¹å¼ï¼šç§»åŠ¨ç«¯æŽ¨è页首å±åº”明显看到更大的作å“内容区,底部作者信æ¯åŒºåªä¿ç•™ç´§å‡‘一æ¡ï¼Œä¸å†æ˜Žæ˜¾æŒ¤åŽ‹è¿è¡Œæ€ã€‚ +- å…³è”æ–‡æ¡£ï¼š`docs/technical/PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md`。 + ## 2026-05-05 创作 Tab 固定为智能创作首页,è‰ç¨¿ Tab æ‰¿æŽ¥æ—§ä½œå“æž¶ - 背景:创作首页需è¦å˜æˆé¢å‘对è¯å¼ç”Ÿæˆçš„æ™ºèƒ½åˆ›ä½œé¡µï¼Œæ—§æ¨¡æ¿å¡å’Œä½œå“æž¶ç»§ç»­ä¿ç•™ä½†ä¸åº”å†å æ®åˆ›ä½œé¦–å±ã€‚ diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 17303bc9..e4a67fee 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -14,6 +14,22 @@ - å…³è”ï¼šç›¸å…³æ–‡ä»¶ã€æ–‡æ¡£ã€æäº¤æˆ– Issue ``` +## 图åƒè¾“入组件ä¸è¦æŠŠä¸šåŠ¡çŠ¶æ€è—在页é¢å†…è”实现里 + +- 现象:拼图页把å‚考图上传ã€ç¼©ç•¥å›¾ã€ä¸»å›¾åˆ é™¤ç¡®è®¤å’Œ AI é‡ç»˜å¼€å…³å†…è”实现åŽï¼ŒåŽç»­æƒ³å¤ç”¨åˆ°å…¶å®ƒåˆ›ä½œé¡µæ—¶ï¼Œé¡µé¢çº§çжæ€å’Œé€šç”¨ UI çŠ¶æ€æ··åœ¨ä¸€èµ·ï¼Œå®¹æ˜“出现多套上传å¡å’Œå‚考图展示å£å¾„。 +- 原因:通用图åƒè¾“å…¥æ˜¯å—æŽ§è¾“å…¥é¢æ¿ï¼Œä¸æ˜¯åªæœåŠ¡å•é¡µçš„ä¸´æ—¶å®žçŽ°ï¼›å›¾ç‰‡ã€æç¤ºè¯ã€å‚考图数组ã€é‡ç»˜å¼€å…³ç­‰ä¸šåŠ¡çœŸç›¸åº”ç”±å¤–å±‚é¡µé¢æŒæœ‰ï¼Œç»„ä»¶æœ€å¤šæŒæœ‰å‚考图预览ã€åˆ é™¤ç¡®è®¤è¿™ç±»çŸ­ç”Ÿå‘½å‘¨æœŸ UI 状æ€ã€‚ +- 处ç†ï¼šæŠ½ `CreativeImageInputPanel` 时,ä¿ç•™ä¸Šä¼ å¡ã€å‚考图入å£ã€ç¼©ç•¥å›¾ã€é¢„览弹层ã€åˆ é™¤ç¡®è®¤å’Œæäº¤æŒ‰é’®çš„统一壳,但把主图文件读å–ã€è£å‰ªã€åކå²ç´ æã€è®¡è´¹ç¡®è®¤å’Œå…·ä½“æäº¤åŠ¨ä½œç•™ç»™å¤–å±‚é¡µé¢ï¼›åŽç»­é¡µé¢æŽ¥å…¥æ—¶åªä¼ ä¸šåŠ¡å›žè°ƒå’Œæ–‡æ¡ˆã€‚ +- 验è¯ï¼šæ‹¼å›¾å…¥å£æµ‹è¯•ä»å¯é€šè¿‡ï¼Œä¸”新组件å¯é€šè¿‡ä¸åŒé¡µé¢å¤ç”¨è€Œä¸éœ€è¦å¤åˆ¶ä¸Šä¼ å¡å®žçŽ°ã€‚ +- å…³è”:`src/components/common/CreativeImageInputPanel.tsx`ã€`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`。 + +## 汪汪声浪入å£ä¸è¦å†å›žåˆ°ç‹¬ç«‹é…置阶段 + +- 现象:汪汪声浪入å£å¦‚果继续切æ¢åˆ°ç‹¬ç«‹é…ç½®é˜¶æ®µï¼Œä¼šå’Œæ‹¼å›¾ã€æŠ“å¤§é¹…çš„åˆ›ä½œé¡µå†…åµŒç»“æž„ä¸ä¸€è‡´ï¼Œç”¨æˆ·ä¼šæ„Ÿè§‰å…¥å£è·³é¡µã€‚ +- 原因:旧实现把 `bark-battle` å•独挂到 `bark-battle-config` selectionStageï¼Œè€Œä¸æ˜¯å¤ç”¨åˆ›ä½œ Tab 里的模æ¿åŒºã€‚ +- 处ç†ï¼šå…¥å£ç‚¹å‡»åªè®¾ç½® `activeCreationFormType = 'bark-battle'` 并回到创作 Tabï¼›`BarkBattleConfigEditor` 作为内嵌表å•使用,默认éšè—è¿”å›žæŒ‰é’®å’Œé¡µé¢æ ‡é¢˜ï¼›runtime `onExit` 釿–°å›žåˆ°åˆ›ä½œ Tab 的汪汪声浪模æ¿ã€‚ +- 验è¯ï¼šç‚¹å‡»æ±ªæ±ªå£°æµªåŽç›´æŽ¥çœ‹åˆ°åˆ›ä½œé¡µå†…嵌表å•,ä¸å†å‡ºçŽ°ç‹¬ç«‹é…置页;测试应覆盖内嵌表å•与 runtime 返回路径。 +- å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/bark-battle-creation/BarkBattleConfigEditor.tsx`ã€`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`。 + ## 抓大鹅批é‡é‡æ–°ç”Ÿæˆç‰©å“ä¸è¦æ–°å¢ž itemId - 现象:结果页批é‡é‡æ–°ç”Ÿæˆç‰©å“åŽï¼Œè¯•玩或正å¼è¿è¡Œæ€çš„物å“类型和图片对应关系漂移,或者用户输入一个ä¸å­˜åœ¨åç§°åŽè¢«å½“作新物å“追加。 @@ -22,6 +38,14 @@ - 验è¯ï¼š`npm run test -- src\components\match3d-result\Match3DResultView.test.tsx` 覆盖å‰ç«¯æäº¤å£å¾„,`cargo test -p api-server match3d_item_asset --manifest-path server-rs\Cargo.toml` å’Œ `cargo test -p api-server match3d_regenerated_asset --manifest-path server-rs\Cargo.toml` 覆盖åŽç«¯æ›¿æ¢è®¡åˆ’与身份ä¿ç•™ã€‚ - å…³è”:`src/components/match3d-result/Match3DResultView.tsx`ã€`server-rs/crates/api-server/src/match3d.rs`ã€`packages/shared/src/contracts/match3dWorks.ts`ã€`server-rs/crates/shared-contracts/src/match3d_works.rs`ã€`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 +## 抓大鹅生æˆå°é¢å›¾ä¸è¦è¦†ç›–物å“ç´ ææˆ–é…ç½® + +- 现象:结果页生æˆå°é¢å›¾åŽï¼Œ`ç´ æé…ç½® > 物å“` 中已有物å“ç´ æè¢«æ¸…空ã€å›žé€€æ—§å¿«ç…§ï¼Œæˆ–难度 / 消除次数被改回旧值。 +- 原因:å°é¢ç”Ÿæˆå±žäºŽå®šå‘å›¾ç‰‡æ§½ä½æ›´æ–°ï¼›è‹¥åŽç«¯å¤ç”¨è‰ç¨¿ç¼–译写回,å¯èƒ½æŒ‰ session config é‡ç®—作å“行。å³ä½¿åŽç«¯å·²ä¿®æ­£ï¼Œå‰ç«¯è‹¥ç›´æŽ¥æŠŠå°é¢æŽ¥å£è¿”回的整份 `item` å½“æˆæœ€æ–° profile,也å¯èƒ½ç”¨æ—§å›žåŒ…里的空 `generatedItemAssets` 覆盖当å‰é¡µé¢ç´ æã€‚ +- 处ç†ï¼š`POST /api/creation/match3d/works/{profileId}/cover-image` åªä¿å­˜ `coverImageSrc` / `coverAssetId` ç­‰å°é¢å­—段,ä¿ç•™å½“å‰ `generated_item_assets_json`ã€éš¾åº¦ã€æ¶ˆé™¤æ¬¡æ•°ã€é¢˜æå’Œæè¿°ï¼›å‰ç«¯æ”¶åˆ°å›žåŒ…åŽåªåˆå¹¶ `coverImageSrc`,继续ä¿ç•™å½“å‰å¯è§ `generatedItemAssets`ã€`clearCount` å’Œ `difficulty`。 +- 验è¯ï¼š`npm run test -- src\components\match3d-result\Match3DResultView.test.tsx` 覆盖旧回包ä¸è¦†ç›–物å“ç´ æå’Œé…置;`cargo test -p api-server match3d_cover --manifest-path server-rs\Cargo.toml` 覆盖å°é¢æç¤ºè¯ä¸Žå‚考图链路。 +- å…³è”:`src/components/match3d-result/Match3DResultView.tsx`ã€`server-rs/crates/api-server/src/match3d.rs`ã€`server-rs/crates/spacetime-module/src/match3d/mod.rs`ã€`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 + ## OSS V4 ç­¾åæ—¶é—´å’Œ bucket/object_key 兼容 - 现象:OSS V4 ç§æœ‰è¯»ç­¾å在部分时间点失败,å¯èƒ½å‡ºçް `OSS V4 ç­¾åæ—¶é—´æ ¼å¼åŒ–失败` 或æœåŠ¡ç«¯åˆ¤å®šç­¾åæ ¼å¼é”™è¯¯ï¼›æŽ’查用例中 bucket 为 `xushi-dev`,object_key 为 `generated-square-hole-assets/.../image.png`。 @@ -71,12 +95,20 @@ - çŽ°è±¡ï¼šç‚¹å‡»ç”ŸæˆæŠ“å¤§é¹…è‰ç¨¿åŽï¼Œé¡µé¢åªæç¤ºâ€œæœåŠ¡æš‚ä¸å¯ç”¨â€ï¼Œæˆ–者本地 `npm run api-server` 看似å¯åŠ¨ä½†ç”ŸæˆæŽ¥å£ä¸å¯ç”¨ã€‚ - 原因:é…置缺失类错误通常在åŽç«¯ `error.details.reason` 中给出具体缺项,å‰ç«¯å¦‚æžœåªè¯» `details.message` ä¼šåžæŽ‰åŽŸå› ï¼›æœ¬åœ°åªé…ç½® `ALIYUN_OSS_BUCKET` / `ALIYUN_OSS_ENDPOINT` 时,旧逻辑还会在å¯åŠ¨æœŸæž„é€ ç©º AccessKey çš„ OSS å®¢æˆ·ç«¯å¹¶å¤±è´¥ã€‚æŠ“å¤§é¹…æ–°é“¾è·¯ä»æ˜¯ 2D 生图切割,ä¸éœ€è¦ä¹Ÿä¸åº”回退 Rodin/GLB。 -- 处ç†ï¼šå‰ç«¯ API é”™è¯¯å±•ç¤ºè¯»å– `details.message` åŽç»§ç»­è¯»å– `details.reason`ï¼›`api-server` åªæœ‰åœ¨ OSS 四件套é½å…¨æ—¶åˆå§‹åŒ– OSS 客户端,部分缺失åªè®° warning 并让具体 generated 上传/æ¢ç­¾æŽ¥å£è¿”回 `OSS 未完æˆçŽ¯å¢ƒå˜é‡é…ç½®`。抓大鹅素æã€å°é¢å’ŒèƒŒæ™¯ç”Ÿæˆåœ¨è°ƒç”¨ VectorEngine å‰å…ˆé¢„检 OSS,并通过 `details.missingEnv` 列出缺项;真实生æˆéœ€è¡¥é½ `VECTOR_ENGINE_BASE_URL`ã€`VECTOR_ENGINE_API_KEY` 和完整 `ALIYUN_OSS_*` 四件套。抓大鹅 `5*5` ç´ æå›¾æç¤ºè¯è¿˜å¿…é¡»è¦æ±‚相邻物体主体至少ä¿ç•™ `1/4` 啿 ¼å®½åº¦ç©ºç™½é—´è·ï¼Œé¿å…切割åŽç›¸é‚»æ ¼å†…容污染。 +- 处ç†ï¼šå‰ç«¯ API é”™è¯¯å±•ç¤ºä¼˜å…ˆè¯»å– `details.reason`,å†è¯»å– `details.message`,é¿å…底层 `error sending request` è¦†ç›–çœŸæ­£å¯æ“作的é…置或网络原因;`api-server` åªæœ‰åœ¨ OSS 四件套é½å…¨æ—¶åˆå§‹åŒ– OSS 客户端,部分缺失åªè®° warning 并让具体 generated 上传/æ¢ç­¾æŽ¥å£è¿”回 `OSS 未完æˆçŽ¯å¢ƒå˜é‡é…ç½®`。抓大鹅素æã€å°é¢å’ŒèƒŒæ™¯ç”Ÿæˆåœ¨è°ƒç”¨ VectorEngine å‰å…ˆé¢„检 OSS,并通过 `details.missingEnv` 列出缺项;真实生æˆéœ€è¡¥é½ `VECTOR_ENGINE_BASE_URL`ã€`VECTOR_ENGINE_API_KEY` 和完整 `ALIYUN_OSS_*` 四件套。抓大鹅 `5*5` ç´ æå›¾æç¤ºè¯è¿˜å¿…é¡»è¦æ±‚相邻物体主体至少ä¿ç•™ `1/4` 啿 ¼å®½åº¦ç©ºç™½é—´è·ï¼Œé¿å…切割åŽç›¸é‚»æ ¼å†…容污染。 - 验è¯ï¼š`npm run test -- src/services/apiClient.test.ts` 覆盖 `details.reason`ï¼›`cargo test -p api-server state --manifest-path server-rs/Cargo.toml` 覆盖åŠé…ç½® OSS ä¸é˜»æ–­å¯åŠ¨ï¼›`npm run api-server` åŽæŒ‰å®žé™… `GENARRATIVE_API_PORT` 请求 `/healthz`,ä¸è¦é»˜è®¤æ‰“ `3100`。 - å…³è”:`packages/shared/src/http.ts`ã€`server-rs/crates/api-server/src/state.rs`ã€`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`ã€`docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md`。 2026-05-14 补充:抓大鹅“物å“ç´ æ sheetâ€å·²æ”¹ç”¨ VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`,真实生æˆè¯»å– `VECTOR_ENGINE_BASE_URL`ã€`VECTOR_ENGINE_API_KEY` å’Œ `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`ï¼›å°é¢å’Œ `9:16` 背景图走 VectorEngine `/v1/images/generations`,`1:1` 容器 UI èµ° VectorEngine `/v1/images/edits` multipart å‚考图链路。排查素æ sheet 时看请求路径是å¦ä¸º `/v1beta/models/gemini-3-pro-image-preview:generateContent?key=...`,å“应图片在 `candidates[].content.parts[].inlineData.data` / `inline_data.data`,ä¸è¦å†æŒ‰ APIMart `/images/generations` 或 `/tasks/{task_id}` 排查。 +## 抓大鹅å‘布按钮è¦å…ˆå¼€å‘å¸ƒé¢æ¿ï¼Œå°é¢ç¼–辑收å£åˆ°å‘å¸ƒé¢æ¿å†… + +- 现象:抓大鹅结果页å‘布按钮看起æ¥ç‚¹ä¸äº†ï¼Œæˆ–者å°é¢ç¼–辑ä»ç„¶åˆ†æ•£åœ¨ä½œå“ä¿¡æ¯ Tab 里,和拼图å‘布体验ä¸ä¸€è‡´ã€‚ +- 原因:å‘布按钮被 `publishReady` 直接ç¦ç”¨ï¼Œå¯¼è‡´æœªæ»¡è¶³é—¨æ§›æ—¶æ— æ³•进入å‘å¸ƒæ£€æŸ¥é¢æ¿ï¼›å°é¢ç¼–è¾‘ä»æŒ‚在作å“ä¿¡æ¯ Tab,ä¸èƒ½å’Œå‘布检查一起收å£ã€‚ +- 处ç†ï¼šå‘布按钮åªå—å¿™ç¢Œæ€æŽ§åˆ¶ï¼Œç‚¹å‡»åŽå§‹ç»ˆæ‰“开独立å‘å¸ƒé¢æ¿ï¼›å‘å¸ƒé¢æ¿å†…å…ˆå±•ç¤ºé˜»æ–­é¡¹ï¼Œå†æ‰¿è½½å°é¢å›¾ä¸Šä¼  / AI é‡ç»˜ / å‚考图编辑,满足æ¡ä»¶åŽå†ç‚¹å‡» `å‘布到广场`。 +- 验è¯ï¼š`npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`ï¼›`npm run typecheck`。 +- å…³è”:`src/components/match3d-result/Match3DResultView.tsx`ã€`src/components/match3d-result/Match3DResultView.test.tsx`ã€`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 + ## `.hermes` åªæ”¾å…±äº«å†…å®¹ï¼Œä¸æ”¾ä¸ªäºº Hermes é…ç½® - 现象:团队æˆå‘˜è¯¯æŠŠä¸ªäºº Hermes é…ç½®ã€ä¼šè¯æˆ–密钥å¤åˆ¶è¿›ä»“库。 @@ -195,7 +227,7 @@ - 现象:拼图有å‚考图时返回 `拼图图片生æˆå¤±è´¥ï¼šåˆ›å»ºæ‹¼å›¾ VectorEngine 图片编辑任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/edits)`,åŽç«¯æ²¡æœ‰ `拼图 VectorEngine 图片编辑 HTTP 返回` 日志。 - 原因:这是 `reqwest` 在 `send()` 阶段失败,尚未收到 VectorEngine HTTP å“应;常è§åŽŸå› æ˜¯æœåŠ¡å™¨ç½‘ç»œ / DNS / 防ç«å¢™ / 代ç†é—®é¢˜ï¼Œæˆ–上游网关中断 multipart 连接。 - 处ç†ï¼šæŸ¥çœ‹é”™è¯¯å“应 `details.reason/source/connect/body/timeout/endpoint` å’Œ `拼图 VectorEngine 请求å‘é€å¤±è´¥` 日志。拼图图片客户端已强制 HTTP/1.1,é™ä½Ž multipart HTTP/2 兼容风险;若 `connect=true` 先查网络出å£ï¼Œè‹¥ `body=true` 先查å‚考图大å°å’Œ multipart å‘é€ã€‚ -- 验è¯ï¼š`curl -i -X POST https://api.vectorengine.ai/v1/images/edits -H "Authorization: Bearer invalid" -F "model=gpt-image-2-all" -F "prompt=test" -F "n=1" -F "size=1024x1024"` 至少应返回 HTTP `401`,说明域åã€TLS 和路径å¯è¾¾ï¼›æ‰§è¡Œ `cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`。 +- 验è¯ï¼š`curl --http1.1 -i -X POST https://api.vectorengine.ai/v1/images/edits -H "Authorization: Bearer invalid" -F "model=gpt-image-2" -F "prompt=test" -F "n=1" -F "size=1024x1024" -F "image=@public/match3d-background-references/pot-fused-reference.png;type=image/png"` 至少应返回 HTTP `401`,说明域åã€TLSã€è·¯å¾„å’Œ multipart 上传å¯è¾¾ï¼›æ‰§è¡Œ `cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`。 - å…³è”:`server-rs/crates/api-server/src/puzzle.rs`ã€`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`ã€`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。 ## 拼图 UI 背景缺失先区分生æˆå¤±è´¥å’Œæ¶ˆè´¹é“¾è·¯ä¸¢å­—段 @@ -225,7 +257,7 @@ ## 拼图è‰ç¨¿ç”Ÿæˆ 180 ç§’åŽ 502/504 先查 VectorEngine 超时与å‰ç«¯é‡è¯• - çŽ°è±¡ï¼šç‚¹å‡»â€œç”Ÿæˆæ‹¼å›¾æ¸¸æˆè‰ç¨¿â€åŽï¼Œ`POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions` 等待约 180 秒返回 `502 Bad Gateway` 或 `504 Gateway Timeout`ï¼›é’±åŒ…æµæ°´é‡ŒåŒä¸€ session å¯èƒ½å‡ºçŽ°è¿žç»­ä¸¤ç»„ `puzzle_initial_image` 扣费åŽé€€æ¬¾ã€‚ -- 原因:首图生æˆèµ° VectorEngine `gpt-image-2-all`,默认 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000`;若上游在该窗å£å†…未返回,åŽç«¯é€€æ¬¾å¹¶è¿”回超时错误。旧å‰ç«¯ action 写请求会对 502/503/504 自动é‡è¯•一次,导致åŒä¸€æ¬¡ç‚¹å‡»é‡å¤è§¦å‘生图与扣退费。 +- 原因:首图生æˆèµ° VectorEngine `gpt-image-2-all`,默认 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000`;若上游在该窗å£å†…未返回,åŽç«¯é€€æ¬¾å¹¶è¿”回超时错误。旧å‰ç«¯ action 写请求会对 502/503/504 自动é‡è¯•一次,导致åŒä¸€æ¬¡ç‚¹å‡»é‡å¤è§¦å‘生图与扣退费。 - 处ç†ï¼šæ‹¼å›¾/创作 Agent çš„ `executeAction` 默认ä¸åšå‰ç«¯è‡ªåЍé‡è¯•ï¼›åŽç«¯å°† VectorEngine / 图片请求超时映射为 `504 Gateway Timeout`,`error.details.provider=vector-engine` 且 `timeout=true`。真实排障按日志åŒä¸€ `session_id` 查 `拼图 VectorEngine å›¾ç‰‡ç”Ÿæˆ HTTP 返回` 是å¦ç¼ºå¤±ï¼Œä»¥åŠé’±åŒ…æµæ°´æ‰£è´¹åˆ°é€€æ¬¾çš„æ—¶é—´å·®æ˜¯å¦æŽ¥è¿‘ `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`。 - 验è¯ï¼šè¿è¡Œ `npm run test -- src/services/creation-agent/creationAgentClientFactory.test.ts src/services/apiClient.test.ts`ã€`cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`,真实è”è°ƒé‡å¯ `npm run api-server` åŽæ£€æŸ¥ `/healthz`。 - å…³è”:`src/services/creation-agent/creationAgentClientFactory.ts`ã€`server-rs/crates/api-server/src/puzzle.rs`ã€`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。 @@ -372,6 +404,7 @@ - 本地å¯åŠ¨è„šæœ¬æ²¡æœ‰è®© `.env.local` 覆盖 `.env`,`SMS_AUTH_ENABLED=true` ä¸ç”Ÿæ•ˆï¼ŒåŽç«¯åªè¿”回 `["password"]`。 - Rust API 直连已返回 `["phone","password"]`,但 Vite 代ç†ç›®æ ‡æŒ‡å‘未监å¬ç«¯å£ï¼Œå¯¼è‡´ 3000 域å下的 `login-options` 返回 `500`,`AuthGate` é™çº§æˆ `["password"]`。 - 3000 端å£è¢«æ—§ `dev:web` å ç”¨åŽï¼Œæ–°çš„完整栈 Vite 自动漂移到 3001/3002ï¼›æµè§ˆå™¨ä»æ‰“开旧 3000 页é¢ï¼Œæ—§é¡µé¢ç»§ç»­ä»£ç†åˆ°å·²ç»ä¸‹çº¿çš„端å£ã€‚ + - 生æˆé¡µ UI 改动看起æ¥â€œå®Œå…¨æ²¡å˜åŒ–â€æ—¶ï¼Œä¹Ÿè¦å…ˆç¡®è®¤å½“剿µè§ˆå™¨æ‰“开的 Vite 进程正在返回最新æºç ï¼›ä¾‹å¦‚直接请求 `http://127.0.0.1:3000/src/components/CustomWorldGenerationView.tsx` 检查是å¦åŒ…嫿œ¬æ¬¡æ–°å¢žç±»å或关键字。 - å•独 `npm run dev:web` å¯åŠ¨çž¬é—´å¦ä¸€ä¸ªä¸´æ—¶ API 端å£å¯ç”¨ï¼Œè„šæœ¬è‹¥è‡ªåŠ¨åˆ‡è¿‡åŽ»ï¼Œä¹‹åŽä¸´æ—¶ API åœæŽ‰ä¹Ÿä¼šè®© 3000 继续代ç†åˆ°ç©ºç«¯å£ã€‚ - 处ç†ï¼šä¼˜å…ˆç”¨ `npm run api-server`ã€`npm run dev:rust` 或 `npm run dev` å¯åŠ¨ï¼Œè¿™äº›å…¥å£åº”ä¿æŒ shell 环境å˜é‡æœ€é«˜ä¼˜å…ˆçº§ï¼Œå¹¶å…许 `.env.local` 覆盖 `.env`;完整栈å¯åŠ¨æ—¶è¿˜è¦ç¡®ä¿è„šæœ¬è®¡ç®—å‡ºçš„ `RUST_SERVER_TARGET` ä¸è¢« `.env.local` 里的旧值覆盖。排查时先请求 3000 域å下的 `/api/auth/login-options`,å†ç›´è¿ž Rust API 目标,并核对 `.env.local` çš„ `SMS_AUTH_ENABLED` 与代ç†ç«¯å£ï¼›è‹¥ 3001/3002 æ‰è¿”å›žæ­£ç¡®ç»“æžœï¼Œè¯´æ˜Žå½“å‰ 3000 是旧å‰ç«¯è¿›ç¨‹ï¼Œåº”æ¸…ç†æ—§è¿›ç¨‹åŽé‡å¯ã€‚ - 验è¯ï¼š`http://127.0.0.1:3000/api/auth/login-options` 返回至少 `{"availableLoginMethods":["phone","password"]}` åŽï¼Œç™»å½•弹窗会æ¢å¤çŸ­ä¿¡ç™»å½•页签和“获å–验è¯ç â€æŒ‰é’®ã€‚ @@ -729,11 +762,19 @@ ## 抓大鹅物å“切图白边或绿幕残留先查åŽç«¯é€æ˜ŽåŒ– - 现象:抓大鹅生æˆçš„物å“视角图è£å‰ªåŽä»å¸¦ç™½è¾¹ï¼Œæˆ–者整å—çº¯ç»¿è‰²ç»¿å¹•èƒŒæ™¯æ²¡æœ‰è¢«é€æ˜ŽåŒ–,è¿è¡Œæ€çœ‹åˆ°ç»¿è‰²æ–¹å—。 -- 原因:素æ sheet å¯èƒ½æ˜¯â€œæ¯æ ¼å†…éƒ¨ç»¿å¹•ã€æ•´å¼ å›¾å¤–圈近白底â€ï¼Œå†…部绿幕ä¸ä¸€å®šè¿žé€šåˆ° sheet 外边缘;旧 flood fill åªä»Žå¤–è¾¹ç¼˜æ‰¾èƒŒæ™¯ä¼šæ¼æŽ‰è¿™ç§ç»¿å¹•å—。白底抗锯齿如果ä¸çº³å…¥æŠ åƒå’Œè¾¹ç¼˜åŽ»æ±¡æŸ“ï¼Œä¹Ÿä¼šéšè£å‰ªè¾“出æˆä¸€åœˆç™½è¾¹ã€‚ -- 处ç†ï¼š`api-server` çš„ `slice_match3d_material_sheet` 必须先在整张 sheet 上åšé€æ˜ŽèƒŒæ™¯åŽå¤„ç†ï¼šå¤–边缘连通绿幕/近白底清 alpha,éžè¿žé€šä½†é«˜ç½®ä¿¡çº¯ç»¿å—也清 alpha,边缘近白和绿幕抗锯齿åšé€æ˜Žæˆ–åŽ»æ±¡æŸ“ï¼›åŒæ—¶ä¿æŠ¤ä¸å¤Ÿçº¯çš„绿色主体åƒç´ ã€‚ +- 原因:素æ sheet å¯èƒ½æ˜¯â€œæ¯æ ¼å†…éƒ¨ç»¿å¹•ã€æ•´å¼ å›¾å¤–圈近白底â€ï¼Œå†…部绿幕ä¸ä¸€å®šè¿žé€šåˆ° sheet 外边缘;旧 flood fill åªä»Žå¤–è¾¹ç¼˜æ‰¾èƒŒæ™¯ä¼šæ¼æŽ‰è¿™ç§ç»¿å¹•å—。白底抗锯齿如果ä¸çº³å…¥æŠ åƒå’Œè¾¹ç¼˜åŽ»æ±¡æŸ“ï¼Œä¹Ÿä¼šéšè£å‰ªè¾“出æˆä¸€åœˆç™½è¾¹ã€‚å³ä½¿é¡ºåºå·²æ˜¯å…ˆæ•´å¼  sheet 去绿å†è£å‰ªï¼Œè¾ƒåŽšçš„åŠé€æ˜Žæˆ–混色软绿边ä»å¯èƒ½ä½ŽäºŽé«˜ç½®ä¿¡ç»¿å¹•é˜ˆå€¼ï¼Œè¢«å½“ä½œå‰æ™¯å¸¦è¿›ç‹¬ç«‹ PNG。 +- 处ç†ï¼š`api-server` çš„ `slice_match3d_material_sheet` 必须先在整张 sheet 上åšé€æ˜ŽèƒŒæ™¯åŽå¤„ç†ï¼šå¤–边缘连通绿幕/近白底清 alpha,éžè¿žé€šä½†é«˜ç½®ä¿¡çº¯ç»¿å—也清 alpha,沿整张 sheet 逿˜ŽèƒŒæ™¯ç»§ç»­åƒæŽ‰è½¯ç»¿è¾¹ï¼Œè¾¹ç¼˜è¿‘白和绿幕抗锯齿åšé€æ˜Žæˆ–åŽ»æ±¡æŸ“ï¼›åŒæ—¶ä¿æŠ¤ä¸å¤Ÿçº¯çš„绿色主体åƒç´ ã€‚ä¸è¦æ”¹æˆå…ˆè£å‰ªå•æ ¼å†åŽ»ç»¿ã€‚ - 验è¯ï¼š`cargo test -p api-server match3d_material_sheet_slicing --manifest-path server-rs\Cargo.toml` 覆盖éžè¿žé€šç»¿å¹•ã€ç™½è¾¹ã€è´´è¾¹ä¸»ä½“ä¿ç•™å’Œå›ºå®š 5x5 切图。 - å…³è”:`server-rs/crates/api-server/src/match3d.rs`ã€`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 +## 抓大鹅物å“详情大方格åªåšå•张大图查看 + +- 现象:结果页 `ç´ æé…ç½® > 物å“` 打开详情åŽï¼Œä¸Šæ–¹å¤§æ–¹æ ¼ä»æ˜¾ç¤ºæ¨ªå‘五图带ã€ç„¦ç‚¹å†…框或å°ç¼©ç•¥å›¾è¾¹æ¡†ï¼Œç‰©å“本体看起æ¥åå°ä¸”åƒå¸¦ç€ç´ æè‡ªå¸¦è¾¹æ¡†ã€‚ +- 原因:旧预览把上方区域当作横å‘视角带,当å‰ç„¦ç‚¹åªæ˜¯å¸¦å†…ç¼©ç•¥å›¾çš„ä¸€å¼ ï¼Œè§†è§‰ä¸Šä¸æ˜¯â€œè¯¦ç»†æŸ¥çœ‹ç‰©å“形象â€çš„大图。 +- 处ç†ï¼šä¸Šæ–¹æ–¹æ ¼åªæ¸²æŸ“当å‰é€‰ä¸­çš„å•张大图,使用 `object-contain` 和少é‡å†…è¾¹è·æ”¾å¤§æŸ¥çœ‹ï¼›åº•部缩略图æ è´Ÿè´£åˆ‡æ¢è§†è§’,缩略图å¯ä»¥ä¿ç•™é€‰ä¸­æ€è¾¹æ¡†ï¼Œä½†ä¸Šæ–¹å¤§å›¾ä¸æ¸²æŸ“焦点内框或缩略图容器边框。 +- 验è¯ï¼š`npm run test -- src/components/match3d-result/Match3DResultView.test.tsx` 覆盖上方大图ã€åº•部缩略图和视角切æ¢ã€‚ +- å…³è”:`src/components/match3d-result/Match3DResultView.tsx`ã€`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 + ## è‰ç¨¿é¡µå¡ç‰‡æœ‰çœŸå®žç´ æä½†ä»æ˜¾ç¤ºé»‘å¡å…ˆæŸ¥æ‘˜è¦å­—段 - 现象:è‰ç¨¿é¡µæ‹¼å›¾å¡ç‰‡æ²¡æœ‰å…³å¡å›¾èƒŒæ™¯ï¼ŒæŠ“大鹅å¡ç‰‡æ²¡æœ‰èƒŒæ™¯å›¾æˆ–物å“å›¾èƒŒæ™¯ï¼Œç”šè‡³å…œåº•è§†è§‰ä¹Ÿé€€å›žé»‘è‰²é¢æ¿ã€‚ @@ -766,12 +807,12 @@ - 验è¯ï¼šç»“果页 UI Tabã€`startLocalPuzzleRun` å’Œ `PuzzleRuntimeShell` 都应在仅有 `objectKey` 时显示生æˆèƒŒæ™¯ï¼Œä¸å†å›žè½é»˜è®¤ UI。 - å…³è”:`src/services/puzzle-runtime/puzzleUiBackgroundSource.ts`ã€`src/components/puzzle-result/PuzzleResultView.tsx`ã€`src/services/puzzle-runtime/puzzleLocalRuntime.ts`ã€`src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`ã€`server-rs/crates/module-puzzle/src/application.rs`。 -## 拼图 UI 背景æç¤ºè¯ä¸åƒ AI 生æˆå…ˆæŸ¥é¦–关命å契约 +## 拼图 UI 背景æç¤ºè¯æˆ–作å“元信æ¯å¼‚常先查首关命å契约 -- 现象:拼图è‰ç¨¿ç”Ÿæˆå®ŒæˆåŽï¼Œ`ç´ æé…ç½® > UI` 里显示的 `UI背景æç¤ºè¯` åƒå‰ç«¯æˆ–åŽç«¯æ¨¡æ¿æ‹¼æŽ¥ï¼Œè€Œä¸æ˜¯ AI 生æˆçš„视觉æç¤ºè¯ã€‚ -- 原因:首关命å LLM 旧契约åªè¿”回 `levelName`,自动 UI 背景阶段åªèƒ½ç”¨ä½œå“åã€ä½œå“æè¿°ã€å…³å¡æè¿°å’Œæ ‡ç­¾æ‹¼æŽ¥ç¡®å®šæ€§å…œåº•æç¤ºè¯ï¼›å‰ç«¯æ—§å®žçްåˆä¼šåœ¨ `uiBackgroundPrompt` 为空时把本地默认模æ¿ç›´æŽ¥å¡«è¿›æ–‡æœ¬æ¡†ï¼Œé€ æˆâ€œçœ‹èµ·æ¥å·²æœ‰ AI æç¤ºè¯â€çš„å‡è±¡ã€‚ -- 处ç†ï¼šé¦–关命å LLM å¥‘çº¦å¿…é¡»åŒæ—¶è¿”回 `{"levelName":"...","uiBackgroundPrompt":"..."}`ï¼›è‰ç¨¿è‡ªåЍ UI 背景生æˆä¼˜å…ˆä½¿ç”¨è¯¥ AI æç¤ºè¯ï¼Œè§†è§‰ç²¾ä¿®è¯·æ±‚若返回新æç¤ºè¯åˆ™è¦†ç›–文本请求æç¤ºè¯ï¼Œå¦åˆ™ä¿ç•™æ–‡æœ¬è¯·æ±‚æç¤ºè¯ã€‚å‰ç«¯æ–‡æœ¬æ¡†åªå±•示已ä¿å­˜çš„ `uiBackgroundPrompt` 或用户编辑值,字段为空时ä¸å±•示本地兜底模æ¿ã€‚ -- 验è¯ï¼šæ‰§è¡Œ `cargo test -p api-server puzzle_level_naming --manifest-path server-rs\Cargo.toml`ã€`cargo test -p api-server puzzle_initial_ui_background_prompt --manifest-path server-rs\Cargo.toml`ã€`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx`。 +- 现象:拼图è‰ç¨¿ç”Ÿæˆå®ŒæˆåŽï¼Œç¬¬ä¸€å…³å称或作å“åç§°å˜æˆ `levelNam` / `levelName` 这类字段å片段,或 `ç´ æé…ç½® > UI` 里显示的 `UI背景æç¤ºè¯` åƒå‰ç«¯æˆ–åŽç«¯æ¨¡æ¿æ‹¼æŽ¥ï¼Œè€Œä¸æ˜¯ AI 生æˆçš„视觉æç¤ºè¯ã€‚ +- 原因:首关命å LLM 旧契约åªè¿”回 `levelName`,自动 UI 背景阶段åªèƒ½ç”¨ä½œå“åã€ä½œå“æè¿°ã€å…³å¡æè¿°å’Œæ ‡ç­¾æ‹¼æŽ¥ç¡®å®šæ€§å…œåº•æç¤ºè¯ï¼›å¦‚果模型返回截断 JSON,解æžå±‚还å¯èƒ½æŠŠ `levelNam` 这类字段å片段当作普通英文关å¡å归一化通过。 +- 处ç†ï¼šé¦–关命å LLM å¥‘çº¦å¿…é¡»åŒæ—¶è¿”回 `{"levelName":"...","workDescription":"...","workTags":["..."],"uiBackgroundPrompt":"..."}`;解æžå±‚å¿…é¡»æ‹’ç» `levelNam`ã€`levelName`ã€`workDescription`ã€`workTags`ã€`uiBackgroundPrompt` 等字段å片段作为关å¡å。è‰ç¨¿è‡ªåЍ UI 背景生æˆä¼˜å…ˆä½¿ç”¨è¯¥ AI æç¤ºè¯ï¼Œä½œå“æè¿°å’Œ 6 ä¸ªä½œå“æ ‡ç­¾é»˜è®¤å¡«å…¥è‰ç¨¿ï¼›è§†è§‰ç²¾ä¿®è¯·æ±‚若返回新æç¤ºè¯æˆ–作å“元信æ¯åˆ™è¦†ç›–文本请求结果,å¦åˆ™ä¿ç•™æ–‡æœ¬è¯·æ±‚结果。å‰ç«¯æ–‡æœ¬æ¡†åªå±•示已ä¿å­˜çš„ `uiBackgroundPrompt` 或用户编辑值,字段为空时ä¸å±•示本地兜底模æ¿ã€‚ +- 验è¯ï¼šæ‰§è¡Œ `cargo test -p api-server puzzle_level_naming_parser --manifest-path server-rs\Cargo.toml`ã€`cargo test -p api-server puzzle_first_level_name --manifest-path server-rs\Cargo.toml`ã€`cargo test -p api-server puzzle_initial --manifest-path server-rs\Cargo.toml`ã€`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx`。 - å…³è”:`server-rs/crates/api-server/src/prompt/puzzle/level_name.rs`ã€`server-rs/crates/api-server/src/puzzle.rs`ã€`src/components/puzzle-result/PuzzleResultView.tsx`ã€`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`。 ## 拼图 / 抓大鹅 UI 背景é‡ç”ŸæˆæŠ¥ No such procedure 先查 SpacetimeDB 版本漂移 @@ -781,3 +822,19 @@ - 处ç†ï¼šä¸´æ—¶å®¹é”™æ˜¯æŠŠè¿™ç±» `No such procedure` 当作åŽç«¯ç‰ˆæœ¬æ¼‚移:泥点预扣阶段跳过扣费,图片已ç»ç”Ÿæˆä½†ä¿å­˜å¤±è´¥æ—¶è¿”回本次内存快照 / 内存 profile,é¿å…è‰ç¨¿é¡µç›´æŽ¥æŠ¥é”™ã€‚长期修å¤ä»æ˜¯å‘布最新 `spacetime-module`ã€é‡æ–°ç”Ÿæˆ bindings,并用 `spacetime describe` æˆ–å®šå‘ smoke 确认 procedure 已导出。 - 验è¯ï¼š`cargo test -p api-server asset_operation_billing_skips_spacetime_connectivity_errors --manifest-path server-rs\Cargo.toml`ã€`cargo test -p api-server match3d_fallback_work_profile_keeps_generated_background_asset --manifest-path server-rs\Cargo.toml`ã€`npm run api-server` åŽæ£€æŸ¥ `/healthz`。 - å…³è”:`server-rs/crates/api-server/src/asset_billing.rs`ã€`server-rs/crates/api-server/src/match3d.rs`ã€`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`ã€`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 + +## 拼图åˆå¹¶å—æ‹–èµ·åŽåŽŸä½ç½®å‡ºçŽ°çº¢è‰²å—å…ˆæŸ¥é€‰ä¸­æ€æ³„æ¼ + +- 现象:拼图è¿è¡Œæ€ä¸­ï¼Œå¤šä¸ªæ‹¼å›¾ç‰‡åˆå¹¶åŽæ‹–起整体å—,原ä½ç½®ä¼šéœ²å‡ºä¸€å—粉红 / 红色底色。 +- 原因:åˆå¹¶å—拖拽的å¯è§å±‚æ¥è‡ª `mergedGroups` ç»å¯¹å®šä½æ•´ä½“层,但 `pointerdown` ä¼šåŒæ­¥å†™å…¥ `selectedPieceId`;若棋盘格里的底层å•å— DOM 先匹é…选中æ€ï¼Œå†åŒ¹é…åˆå¹¶æ€ï¼Œæ•´ä½“层移开åŽå°±ä¼šéœ²å‡ºå•å—选中填充色。 +- 处ç†ï¼šåˆå¹¶æ ¼åº•层 DOM åªä½œä¸ºé€æ˜Žå®šä½å ä½ï¼Œ`isSelected` 必须排除 `isMerged`ï¼›åˆå¹¶æ ¼æ ·å¼ä¼˜å…ˆçº§é«˜äºŽå•å—选中æ€ã€‚ +- 验è¯ï¼šè¿è¡Œ `npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "拖拽åˆå¹¶å¤§å—æ—¶åº•å±‚å•æ ¼ä¸æ˜¾ç¤ºé€‰ä¸­è‰²å—"`,并确认åˆå¹¶å—拖拽时底层 `[data-piece-id]` ä»ä¸º `puzzle-runtime-piece--merged`。 +- å…³è”:`src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`ã€`src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx`ã€`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`。 + +## 拼图历å²å›¾ç‰‡åˆ—表ä¸è¦æŠŠè´¦å·å½’属当图片å + +- 现象:拼图创作页或结果页打开“选择历å²å›¾ç‰‡â€åŽï¼Œåކå²åˆ—表显示 `è´¦å· user-1` ä¹‹ç±»å½’å±žæ–‡æ¡ˆè€Œä¸æ˜¯å›¾ç‰‡åï¼›`1713686400.000000Z` 这类时间显示为未知;选中åŽé¢„览或生æˆå‚考图å¯èƒ½è¢«æ€€ç–‘ä¸å¯ç”¨ã€‚ +- 原因:`/api/assets/history?kind=puzzle_cover_image` 返回的 `ownerLabel` 是资产归属账å·ï¼Œä¸æ˜¯å›¾ç‰‡æ ‡é¢˜ï¼›`createdAt` å¯èƒ½æ˜¯ SpacetimeDB / shared-kernel 秒级时间字符串,ä¸èƒ½åªç”¨æµè§ˆå™¨ `new Date(value)` è§£æžã€‚历å²å›¾çš„ `imageSrc` 是 `/generated-*` ç§æœ‰å…¼å®¹è·¯å¾„,æµè§ˆå™¨é¢„览必须æ¢ç­¾ã€‚ +- 处ç†ï¼šå‰ç«¯æ ‡é¢˜å’Œé€‰ä¸­æ ‡ç­¾ä»Ž `imageSrc` 路径末尾推导,例如 `image.png`;时间解æžå…¼å®¹ ISO 与 `1713686400.000000Z`;创作页主图ã€åކå²åˆ—表图和结果页å‚考图继续用 `ResolvedAssetImage`,æäº¤ç»™åŽç«¯æ—¶ä»ä¿ç•™åŽŸå§‹ `imageSrc`。 +- 验è¯ï¼š`npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`,并执行 `npm run check:encoding`。 +- å…³è”:`src/services/puzzle-works/puzzleHistoryAsset.ts`ã€`src/components/puzzle-agent/PuzzleHistoryAssetPickerDialog.tsx`ã€`docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md`。 diff --git a/deploy/env/api-server.env.example b/deploy/env/api-server.env.example index 4637302f..7420d6c9 100644 --- a/deploy/env/api-server.env.example +++ b/deploy/env/api-server.env.example @@ -41,7 +41,7 @@ APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000 VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn VECTOR_ENGINE_API_KEY= -VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000 +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000 HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2 diff --git a/docs/design/MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md b/docs/design/MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md index 4c6f33fd..0b79c134 100644 --- a/docs/design/MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md +++ b/docs/design/MOBILE_CREATION_WORK_LIST_TWO_COLUMN_LAYOUT_2026-04-29.md @@ -4,24 +4,24 @@ è‰ç¨¿é¡µçš„ä½œå“æ¨¡å—需è¦åŒæ—¶æ‰¿è½½ RPGã€æ‹¼å›¾å’Œå¤§é±¼åƒå°é±¼ç­‰çŽ©æ³•ã€‚ä¸åŒçŽ©æ³•å¡ç‰‡ä¸èƒ½å„自展示阶段ã€ç´ æã€ä¸»é¢˜ç­‰ç»†èŠ‚æ ‡ç­¾ï¼Œå¦åˆ™ä½œå“列表会在移动端显得拥挤,并且è‰ç¨¿ä½œå“会暴露过多编辑æ€ä¿¡æ¯ã€‚ -本次将作å“列表å¡ç‰‡æ”¶å£æˆç»Ÿä¸€ä¿¡æ¯ç»“构:è‰ç¨¿åªç”¨äºŽå¿«é€Ÿè¯†åˆ«å’Œç»§ç»­åˆ›ä½œï¼Œå·²å‘å¸ƒä½œå“æ‰å±•示公开数æ®ï¼›åˆ é™¤ä¸Žåˆ†äº«ç­‰ä½Žé¢‘æ“作收进左滑æ“作层,é¿å…列表常æ€è¢«æŒ‰é’®æŒ¤å ã€‚ +本次将作å“列表å¡ç‰‡æ”¶å£æˆç»Ÿä¸€ä¿¡æ¯ç»“构:è‰ç¨¿åªç”¨äºŽå¿«é€Ÿè¯†åˆ«å’Œç»§ç»­åˆ›ä½œï¼Œå·²å‘å¸ƒä½œå“æ‰å±•示公开数æ®ï¼›åˆ é™¤ä¸Žåˆ†äº«ç­‰ä½Žé¢‘æ“ä½œæ”¶è¿›é•¿æŒ‰åŠ¨ä½œé¢æ¿ï¼Œé¿å…列表常æ€è¢«æŒ‰é’®æŒ¤å ã€‚ ## è½åœ°èŒƒå›´ - 列表容器:`src/components/custom-world-home/CustomWorldCreationHub.tsx` - 作å“å¡ç‰‡ï¼š`src/components/custom-world-home/CustomWorldWorkCard.tsx` - 䏿”¹åŠ¨ä½œå“æ•°æ®èšåˆã€ç­›é€‰ã€æ‰“开和体验逻辑。 -- å·²å‘布作å“ä¿ç•™åˆ†äº«èƒ½åŠ›ï¼›å¯åˆ é™¤ä½œå“ä¿ç•™åˆ é™¤èƒ½åŠ›ï¼Œä½†å¸¸æ€ä¸æ˜¾ç¤ºä¸ºå³ä¾§æŒ‰é’®ã€‚ +- å·²å‘布作å“ä¿ç•™åˆ†äº«èƒ½åŠ›ï¼Œå¡ç‰‡å³ä¸Šè§’常驻分享图标;å¯åˆ é™¤ä½œå“ä¿ç•™åˆ é™¤èƒ½åŠ›ï¼Œä½†å¸¸æ€ä¸æ˜¾ç¤ºåˆ é™¤æŒ‰é’®ã€‚ ## å¡ç‰‡ç»“构规则 -1. å¡ç‰‡æ•´ä½“对é½å‘现 / 分类页的横å‘作å“列表结构:左侧为标题ã€çжæ€ã€ç±»åž‹ã€æ‘˜è¦ä¸Žå¿…è¦æ•°æ®ï¼Œå³ä¾§ä¸ºå¸¦é€æ˜Žåº¦çš„å°é¢å›¾ã€‚ +1. å¡ç‰‡æ•´ä½“对é½å‘现 / 分类页的横å‘作å“列表结构:内容层承载标题ã€çжæ€ã€ç±»åž‹ã€æ‘˜è¦ä¸Žå¿…è¦æ•°æ®ï¼Œå°é¢ä½œä¸ºä¸å å†…容布局的å³åŠåŒºé€æ˜ŽèƒŒæ™¯å±‚。 2. ä¸å†æ˜¾ç¤ºé˜¶æ®µã€ä¸»é¢˜ã€ç´ æå®Œæˆåº¦ã€ä½œè€…ã€ä½œå“å·ç­‰é¢å¤–标签。 -3. 标题区域ä¿ç•™ä½œå“状æ€ä¸Žæ¸¸æˆç±»åž‹ï¼›ç”Ÿæˆä¸­çš„è‰ç¨¿çŠ¶æ€æ˜¾ç¤ºä¸ºâ€œç”Ÿæˆä¸­â€ã€‚ +3. 标题区域ä¿ç•™ä½œå“状æ€ä¸Žæ¸¸æˆç±»åž‹ï¼›è‰ç¨¿å’Œå·²å‘布状æ€åªç”¨å›¾æ ‡åŒ– UI 标识,ä¸å†åœ¨å¡ç‰‡ä¸Šæ˜¾ç¤ºâ€œè‰ç¨¿ / å·²å‘å¸ƒâ€æ–‡å­—。 4. è‰ç¨¿å¡ç‰‡åˆ°ä½œå“æè¿°ä¸ºæ­¢ï¼Œä¸æ˜¾ç¤ºå³ä¾§â€œç»§ç»­åˆ›ä½œâ€ç­‰å›ºå®šåŠ¨ä½œã€ç»Ÿè®¡ã€ä½œå“å·æˆ–公开指标。 5. å·²å‘布å¡ç‰‡åœ¨æè¿°ä¸‹æ–¹æ˜¾ç¤ºä¸‰é¡¹å…¬å¼€æŒ‡æ ‡ï¼šæ¸¸çŽ©æ•°ã€æ”¹é€ æ•°ã€ç‚¹èµžæ•°ã€‚ -6. å·²å‘布å¡ç‰‡çš„åˆ†äº«å…¥å£æ”¶è¿›å·¦æ»‘æ“作层,点击åŽå¤åˆ¶ä½œå“分享文案,ä¸è§¦å‘å¡ç‰‡æ‰“开。 -7. å¯åˆ é™¤å¡ç‰‡çš„åˆ é™¤å…¥å£æ”¶è¿›å·¦æ»‘æ“作层,常æ€ä¸æ˜¾ç¤ºåˆ é™¤æŒ‰é’®ï¼›å·¦æ»‘露出åŽç‚¹å‡»åˆ é™¤ä¸è§¦å‘å¡ç‰‡æ‰“开。 +6. å·²å‘布å¡ç‰‡å³ä¸Šè§’显示分享图标,点击åŽå¤åˆ¶ä½œå“分享文案,ä¸è§¦å‘å¡ç‰‡æ‰“开。 +7. 长按è‰ç¨¿ä½œå“å¼¹å‡ºç‹¬ç«‹åŠ¨ä½œé¢æ¿ï¼Œåªå±•示删除作å“;长按已å‘布作å“å¼¹å‡ºç‹¬ç«‹åŠ¨ä½œé¢æ¿ï¼Œå±•示分享和删除。动作按钮点击åŽä¸å¾—触å‘å¡ç‰‡æ‰“开。 8. å¡ç‰‡ä¸æ˜¾ç¤ºæœ€åŽä¿®æ”¹æ—¶é—´ï¼›`updatedAt` åªç”¨äºŽä½œå“列表排åºã€‚ 9. 生æˆä¸­çš„å¡ç‰‡åœ¨æ•´å¡ä¸Šå åŠ åŠé€æ˜Žè’™ç‰ˆã€æ—‹è½¬ç­‰å¾…符å·å’Œâ€œç”Ÿæˆä¸­...â€æ ‡è¯†ï¼›è’™ç‰ˆä¸èƒ½ç§»é™¤æ ‡é¢˜ã€çжæ€ã€ç±»åž‹ã€æ‘˜è¦ã€å³ä¾§å°é¢ç­‰åŽŸæœ‰ä¿¡æ¯ã€‚ @@ -32,7 +32,7 @@ 3. ç”¨æˆ·æ¯æ¬¡è¿›å…¥åˆ›ä½œé¡µæ—¶ï¼Œå‰ç«¯è¯»å–上一次进入该页é¢ç¼“存的公开指标快照;当已å‘布作å“å¡ç‰‡æ»‘动进入视å£åŽï¼Œæ•°å­—从缓存值增长到本次接å£è¿”回的最新值。 4. 若最新值高于缓存值,动画完æˆåŽåœ¨å¯¹åº”指标å³ä¸‹è§’展示红色å‘上箭头和本次上涨的具体数值,字å·ä½ŽäºŽä¸»æ•°å­—,é¿å…抢å ä¸»ä¿¡æ¯å±‚级。 5. 若没有缓存值ã€ç¼“存值ä¸ä½ŽäºŽæœ€æ–°å€¼æˆ–作å“仿˜¯è‰ç¨¿ï¼Œåˆ™ç›´æŽ¥æ˜¾ç¤ºæœ€æ–°å€¼ï¼Œä¸å±•示上涨标记。 -6. æ¯å¼ ä½œå“å¡ç‰‡ç»§ç»­ä½¿ç”¨ä½œå“å°é¢ä½œä¸ºå³ä¾§æ–¹å½¢åŠé€æ˜Žä¸»è§†è§‰ï¼›å°é¢ä¸èƒ½å› ä¸ºåˆ—表收缩被拉伸å˜å½¢ã€‚ +6. æ¯å¼ ä½œå“å¡ç‰‡ç»§ç»­ä½¿ç”¨ä½œå“å°é¢ä½œä¸ºå³ä¾§åŠåŒºé€æ˜ŽèƒŒæ™¯ä¸»è§†è§‰ï¼›å°é¢ä»Žå³å‘å·¦æ¸éšï¼Œä¸èƒ½å‡ºçŽ°ç‹¬ç«‹æ–¹å½¢è¾¹ç•Œï¼Œä¹Ÿä¸èƒ½å› ä¸ºåˆ—è¡¨æ”¶ç¼©æŒ¤å æ­£æ–‡å¸ƒå±€æˆ–被拉伸å˜å½¢ã€‚ 7. 作å“列表按 `updatedAt` å€’åºæŽ’åˆ—ï¼›å‰ç«¯æŽ’åºéœ€è¦å…¼å®¹ ISO æ—¶é—´å’Œ Rust åŽç«¯å¸¸ç”¨çš„ `seconds.microsZ` 时间文本。 8. 若玩法摘è¦ç¼ºå°‘ `coverImageSrc`,å…许从åŒä¸€ä½œå“的正å¼å…³å¡å›¾ã€èƒŒæ™¯å›¾æˆ–ç´ æå›¾é‡Œå–第一张å¯ç”¨å›¾ç‰‡ä½œä¸ºå¡ç‰‡èƒŒæ™¯å…œåº•。 9. 若作å“真实图片为空ã€ç§æœ‰èµ„æºæ¢ç­¾å¤±è´¥æˆ–æµè§ˆå™¨å›¾ç‰‡åŠ è½½å¤±è´¥ï¼Œå¡ç‰‡å¿…须切到对应玩法的 `public/creation-type-references/` å‚è€ƒå›¾ï¼›æœ€ç»ˆå…œåº•åº•è‰²ä½¿ç”¨å¹³å°æµ…粉暖白色系,ä¸å…è®¸é€€å›žé»‘è‰²æ™®é€šé¢æ¿ã€‚ @@ -40,16 +40,16 @@ ## 移动端布局规则 1. 作å“列表默认使用å•列纵å‘列表,视觉上与å‘çŽ°é¡µåˆ†ç±»åˆ—è¡¨ä¿æŒä¸€è‡´ã€‚ -2. 移动端æ¯å¼ å¡ç‰‡ä½¿ç”¨å›ºå®šå³åˆ—方形åŠé€æ˜Žå°é¢ï¼Œå³åˆ—建议 `5.1rem` å·¦å³ï¼›è‰ç¨¿å¡å³ä½¿å¤ç”¨åˆ†ç±»é¡µåŸºç¡€ç±»å,也必须用自身选择器覆盖分类页的 `4.3rem + 正文 + action` 三列规则,é¿å…正文被压进窄列。 -3. å·²å‘布作å“的公开指标在å¡ç‰‡æ­£æ–‡å†…ä¿ç•™ï¼Œä½†éœ€è¦åŽ‹ç¼©å­—å·å’Œé—´è·ï¼Œä¸èƒ½è®©å³ä¾§å°é¢åˆ—é”™ä½ã€‚ +2. 移动端æ¯å¼ å¡ç‰‡ä½¿ç”¨ç»å¯¹å®šä½å³åŠåŒºå°é¢èƒŒæ™¯å±‚,å°é¢åœ¨å³è¾¹ç¼˜æœ€æ¸…æ™°ã€å‘å·¦æ¸éšï¼›è‰ç¨¿å¡å³ä½¿å¤ç”¨åˆ†ç±»é¡µåŸºç¡€ç±»å,也必须用自身选择器覆盖分类页的 `4.3rem + 正文 + action` 三列规则,é¿å…正文被压进窄列。 +3. å·²å‘布作å“的公开指标在å¡ç‰‡æ­£æ–‡å†…ä¿ç•™ï¼Œä½†éœ€è¦åŽ‹ç¼©å­—å·å’Œé—´è·ï¼Œä¸èƒ½ä¾èµ–å³ä¾§å°é¢åˆ—å‚与排版。 4. å°å±å¡ç‰‡é™ä½Žé«˜åº¦ã€å†…è¾¹è·ã€æ ‡é¢˜å­—å·å’Œå¾½æ ‡å°ºå¯¸ï¼Œé¿å…长标题或中文æè¿°æ’‘破容器。 -5. å³ä¾§å°é¢å®¹å™¨æœ¬èº«å¿…须带玩法å‚考图 CSS 背景兜底;`img` 的真实å°é¢æˆ– `ResolvedAssetImage` fallback 加载失败时,也ä¸èƒ½å‡ºçŽ°ç©ºç™½æˆ–é»‘å¡ã€‚ +5. å³ä¾§å°é¢å±‚本身必须带玩法å‚考图 CSS 背景兜底;`img` 的真实å°é¢æˆ– `ResolvedAssetImage` fallback 加载失败时,也ä¸èƒ½å‡ºçŽ°ç©ºç™½æˆ–é»‘å¡ã€‚ ## 网页端布局规则 1. ç½‘é¡µç«¯ä½œå“æž¶ä¸èƒ½ç»§ç»­æ‹‰æˆæ•´è¡Œè¶…宽列表;从 `768px` 起使用多列å¡ç‰‡å¼ç½‘æ ¼ï¼Œé»˜è®¤ä¸¤åˆ—ï¼Œå®½å±æå‡åˆ°ä¸‰åˆ—。 2. 网页端å¡ç‰‡ä¿ç•™ç§»åŠ¨ç«¯åŒä¸€ä¿¡æ¯ç»“构,但å¡ç‰‡é«˜åº¦å¢žåŠ ï¼Œæ­£æ–‡åŒºå¯æ˜¾ç¤ºæ›´å¤šæ‘˜è¦ä¸Žå…¬å¼€æŒ‡æ ‡ï¼Œå³ä¾§å°é¢æ”¹ä¸ºæ›´é«˜çš„åŠé€æ˜Žè§†è§‰åŒºã€‚ -3. 删除与分享ä»ç„¶åªåœ¨å·¦æ»‘或键盘æ­ç¤ºæ€æ˜¾ç¤ºï¼›é»˜è®¤æ€ä¸å¾—é€å‡ºçº¢è‰²åˆ é™¤åº•层或分享底层。 +3. 删除与分享ä»ç„¶åªåœ¨é•¿æŒ‰åŠ¨ä½œé¢æ¿æˆ–键盘æ­ç¤ºæ€æ˜¾ç¤ºï¼›é»˜è®¤æ€ä¸å¾—é€å‡ºçº¢è‰²åˆ é™¤åº•层。 ## æ–‡æ¡ˆçº¦æŸ diff --git a/docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md b/docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md index 94007d11..e6b59adc 100644 --- a/docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md +++ b/docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md @@ -60,3 +60,5 @@ 3. é£Žæ ¼å¡æ ‡ç­¾ä½¿ç”¨æµ…底胶囊,ä¿è¯å›¾ç‰‡ä»æ˜¯ä¸»ä½“。 4. 难度等分段选项å¯ä»¥ä½¿ç”¨ä¸»å“牌色,但选中æ€éœ€è¦é™ä½Žé˜´å½±å’Œé¥±å’Œåº¦ã€‚ 5. UI 中ä¸è¡¥å……çŽ©æ³•è§„åˆ™è¯´æ˜Žæ–‡æ¡ˆï¼Œä¿æŒåˆ›ä½œå…¥å£æ¸…爽。 +6. 拼图创作表å•åœ¨æœªä¸Šä¼ ä¸»å›¾æ—¶ï¼Œç”»é¢æè¿°è¾“å…¥æ¡†å³ä¸‹è§’ä¿ç•™ä¸€ä¸ªå‚考图上传入å£ï¼›æ”¯æŒå¤šé€‰ï¼Œæœ€å¤š 5 张,上传åŽä»¥ä¸‹æ–¹å°ç¼©ç•¥å›¾å±•ç¤ºï¼Œç‚¹å‡»ç¼©ç•¥å›¾å¯æ”¾å¤§é¢„览。 +7. 当剿‹¼å›¾åŽç«¯åªæ¶ˆè´¹ç¬¬ä¸€å¼ æœ‰æ•ˆå‚考图åšç”Ÿæˆï¼Œå‰ç«¯ä»éœ€ä¿ç•™æ•°ç»„输入,方便åŽç»­æ‰©å±•多å‚考图能力。 diff --git a/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md b/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md index 96cf7523..58215816 100644 --- a/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md +++ b/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md @@ -126,6 +126,7 @@ - 用户å†è¿›å…¥è‰ç¨¿ Tab 并点击åŒä¸€è‰ç¨¿æ—¶ï¼Œè‹¥ç”Ÿæˆä»æœªå®Œæˆï¼Œè¿›å…¥å¯¹åº”生æˆè¿‡ç¨‹é¡µæŸ¥çœ‹æœ€æ–°è¿›åº¦ï¼›è‹¥å·²å®Œæˆï¼Œç›´æŽ¥è¿›å…¥å¯¹åº”结果页。 - è‰ç¨¿ä½œå“å¡åœ¨ç”Ÿæˆä¸­å±•示“生æˆä¸­â€çŠ¶æ€æ ‡è®°ï¼›æ–°ç”Ÿæˆå®Œæˆä¸”用户尚未查看的è‰ç¨¿åœ¨å¡ç‰‡å³ä¸Šè§’展示红点。 - 底部一级“è‰ç¨¿â€Tab 在存在未查看新完æˆè‰ç¨¿æ—¶å±•示红点;用户点击查看带红点的作å“åŽï¼Œè¯¥ä½œå“红点消失。若è‰ç¨¿é¡µå·²æ— ä»»ä½•带红点作å“,底部“è‰ç¨¿â€Tab çº¢ç‚¹åŒæ­¥æ¶ˆå¤±ã€‚ +- 红点通知链路覆盖所有进入è‰ç¨¿ä½œå“架的生æˆåž‹çŽ©æ³•ï¼›å®è´è¯†ç‰©ç­‰ä»…有 `profileId` å’Œ `draftId` 的轻é‡è‰ç¨¿ï¼Œä¹Ÿå¿…须把两个 ID 都纳入åŒä¸€ç»„通知 key,é¿å…å¡ç‰‡çº¢ç‚¹å’Œåº•部è‰ç¨¿ Tab 红点漂移。 - 生æˆå®Œæˆæ—¶å¦‚果用户ä»åœç•™åœ¨å¯¹åº”生æˆè¿‡ç¨‹é¡µï¼Œå¯è‡ªåŠ¨è¿›å…¥ç»“æžœé¡µï¼›å¦‚æžœç”¨æˆ·å·²ç»å›žåˆ°åˆ›ä½œä¸­å¿ƒæˆ–å…¶å®ƒåŠŸèƒ½é¡µï¼Œä¸æ‰“æ–­å½“å‰æ“作。 - 创作 Tab 的模æ¿å…¥å£åªå…许被模æ¿è‡ªèº«çš„开放状æ€ç¦ç”¨ï¼›æŸä¸ªè‰ç¨¿åŽå°ç”Ÿæˆä¸­æ—¶ï¼Œä¸å¾—用该玩法的 busy 状æ€ç¦ç”¨å…¶å®ƒæ¨¡æ¿å…¥å£ã€åŒæ¨¡æ¿å†æ¬¡åˆ›å»ºå…¥å£æˆ–阻止用户继续创建新作å“。 - åŒæ¨¡æ¿å†æ¬¡ç‚¹å‡»ç”Ÿæˆæ—¶å¿…须创建新的è‰ç¨¿ç”Ÿæˆä»»åŠ¡ï¼Œä¸å¾—因为当å‰çŽ©æ³•å·²æœ‰åŽå°ç”Ÿæˆ session 就跳回上一æ¡è‰ç¨¿çš„生æˆè¿‡ç¨‹é¡µï¼›æŸ¥çœ‹ä¸Šä¸€æ¡ç”Ÿæˆè¿›åº¦åªèƒ½ä»Žè‰ç¨¿ Tab 的对应作å“å¡è¿›å…¥ã€‚ diff --git a/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md b/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md index 870941e4..691c2e47 100644 --- a/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md +++ b/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md @@ -143,13 +143,13 @@ 5. 标签 ## 6.3 è‰ç¨¿é¡µä½œå“å¡å¯¹é½åˆ†ç±»åˆ—表 -- è‰ç¨¿ Tab çš„ä½œå“æž¶è¦ä¼˜å…ˆå¯¹é½å‘现页分类列表的横å‘å¡ç‰‡ï¼šå·¦ä¾§æ‰¿è½½æ ‡é¢˜/状æ€/类型/摘è¦ï¼Œå³ä¾§æ˜¾ç¤ºå¸¦é€æ˜Žåº¦çš„æ–¹å½¢å°é¢å›¾ã€‚ -- è‰ç¨¿å¡ä¸èƒ½ä¸ºäº†è§†è§‰å¯¹é½ä¸¢æŽ‰åŽŸæœ‰ä¿¡æ¯ï¼šåˆ é™¤ã€åˆ†äº«ã€å·²å‘å¸ƒç»Ÿè®¡ã€æ‹¼å›¾ç§¯åˆ†æ¿€åŠ±ã€æœªè¯»çº¢ç‚¹éƒ½è¦ä¿ç•™ï¼›å…¶ä¸­åˆ é™¤å’Œåˆ†äº«å±žäºŽä½Žé¢‘动作,常æ€ä¸æ˜¾ç¤ºæŒ‰é’®ï¼Œå‘左划动列表项åŽéœ²å‡ºæ“作。 +- è‰ç¨¿ Tab çš„ä½œå“æž¶è¦ä¼˜å…ˆå¯¹é½å‘现页分类列表的横å‘å¡ç‰‡ï¼šå†…容层承载标题/状æ€/类型/摘è¦ï¼Œå°é¢ä½œä¸ºå³åŠåŒºåŠé€æ˜ŽèƒŒæ™¯å±‚。 +- è‰ç¨¿å¡ä¸èƒ½ä¸ºäº†è§†è§‰å¯¹é½ä¸¢æŽ‰åŽŸæœ‰ä¿¡æ¯ï¼šåˆ é™¤ã€åˆ†äº«ã€å·²å‘å¸ƒç»Ÿè®¡ã€æ‹¼å›¾ç§¯åˆ†æ¿€åŠ±ã€æœªè¯»çº¢ç‚¹éƒ½è¦ä¿ç•™ï¼›å…¶ä¸­åˆ é™¤å±žäºŽä½Žé¢‘动作,常æ€ä¸æ˜¾ç¤ºæŒ‰é’®ï¼Œé•¿æŒ‰åˆ—表项åŽè¿›å…¥ç‹¬ç«‹åŠ¨ä½œé¢æ¿ï¼›å·²å‘布作å“å³ä¸Šè§’å¯ä»¥å¸¸é©»åˆ†äº«å›¾æ ‡ã€‚ - å¡ç‰‡å³ä¾§ä¸å†å¸¸é©»â€œç»§ç»­åˆ›ä½œâ€â€œæŸ¥çœ‹è¯¦æƒ…â€â€œæŸ¥çœ‹è¿›åº¦â€ç­‰åŠ¨ä½œæŒ‰é’®ï¼Œæ‰“å¼€ä½œå“由整张å¡ç‰‡æ‰¿æ‹…。 - ç§»åŠ¨ç«¯ä¿æŒå•列列表;网页端使用多列å¡ç‰‡å¼ç½‘格,é¿å…在宽å±ä¸ŠæŠŠä½œå“塿‹‰æˆä¸€æ•´è¡Œé•¿æ¡ã€‚ - 生æˆä¸­çš„状æ€ä½¿ç”¨æ•´å¡è’™ç‰ˆã€æ—‹è½¬ç­‰å¾…符å·å’Œâ€œç”Ÿæˆä¸­...â€æ ‡è¯†ï¼›è’™ç‰ˆåªèƒ½ä½œä¸ºçжæ€å±‚,ä¸èƒ½æ›¿æ¢æˆ–移除å¡ç‰‡æœ¬èº«çš„ä¿¡æ¯ã€‚ - è‰ç¨¿å¡å¤ç”¨åˆ†ç±»é¡µåŸºç¡€ç±»å时,è¦ç”¨ `.creation-work-card.platform-category-game-item` 覆盖分类页移动端三列规则;å¦åˆ™æ­£æ–‡ä¼šè¢«å½“作å°é¢åˆ—压缩,中文标题会断æˆä¸€ä¸¤ä¸ªå­—一行。 -- å³ä¾§å°é¢ä¸è¦åªä¾èµ– `img` fallbackï¼Œå®¹å™¨å±‚ä¹Ÿè¦æœ‰çŽ©æ³•å‚考图 CSS èƒŒæ™¯å…œåº•ï¼Œç§æœ‰èµ„æºæ¢ç­¾å¤±è´¥æˆ–图片 onerror æ—¶ä»èƒ½çœ‹åˆ°å°é¢è§†è§‰ã€‚ +- å³ä¾§å°é¢ä¸è¦åªä¾èµ– `img` fallbackï¼Œå®¹å™¨å±‚ä¹Ÿè¦æœ‰çŽ©æ³•å‚考图 CSS èƒŒæ™¯å…œåº•ï¼Œç§æœ‰èµ„æºæ¢ç­¾å¤±è´¥æˆ–图片 onerror æ—¶ä»èƒ½çœ‹åˆ°å°é¢è§†è§‰ï¼›å°é¢å±‚适åˆç»å¯¹å®šä½é“ºåˆ°å¡ç‰‡å³åŠåŒºï¼Œä½œä¸ºåŠé€æ˜ŽèƒŒæ™¯ä»Žå³åˆ°å·¦æ¸éšï¼Œä¸åº”出现独立方形边界或å‚与内容排版。 ## 7. æ ·å¼ä¸ŽåŠ¨ç”»ç»éªŒ @@ -235,3 +235,12 @@ - 主站移动端以固定游æˆç”»å¸ƒä½“éªŒä¸ºå‡†ï¼Œå…¥å£ `viewport` 需è¦é”定 `minimum-scale=1.0`ã€`maximum-scale=1.0` å’Œ `user-scalable=no`ï¼ŒåŒæ—¶ä¿ç•™ `viewport-fit=cover` 适é…安全区。 - æµè§ˆå™¨ä»å¯èƒ½é€šè¿‡ iOS `gesture*` 或多指 `touchmove` è§¦å‘æ•´é¡µç¼©æ”¾ï¼Œå› æ­¤ä¸»ç«™å¯åЍ入å£åº”统一调用 `lockMobileViewportZoom()` 拦截页é¢çº§æåˆä¸Žå¿«é€ŸåŒå‡»ç¼©æ”¾ã€‚ - ä¸è¦åœ¨æ¯ä¸ªç”»å¸ƒç»„件里é‡å¤æ³¨å†Œç¼©æ”¾æ‹¦æˆªï¼›å•指滚动ã€ç‚¹å‡»ã€æ‹–拽应继续留给具体页é¢å’ŒçŽ©æ³•å¤„ç†ã€‚ + +### 10.4 移动端输入法ä¸è¦åŽ‹ç¼©ç”»å¸ƒ +- å¹³å°ä¸»ç«™ç‚¹å‡»è¾“入框弹出输入法时,ä¸èƒ½è®© `100dvh` è·Ÿéšé”®ç›˜ç¼©å°åŽé‡æ–°åŽ‹ç¼©æ•´é¡µå¸ƒå±€ã€‚ +- æ­£ç¡®åšæ³•æ˜¯åœ¨è¾“å…¥æ³•æœªæ‰“å¼€æ—¶è®°å½•ç¨³å®šå¸ƒå±€é«˜åº¦ï¼Œè¾“å…¥æ³•æ‰“å¼€æœŸé—´ä¿æŒç”»å¸ƒé«˜åº¦ä¸å˜ï¼Œåªæ ¹æ®å½“å‰è¾“入框ä½ç½®è®¡ç®—ç”»é¢ä¸Šç§»é‡ã€‚ +- 该行为应在主站入å£ç»Ÿä¸€æ³¨å†Œï¼Œä¸šåŠ¡ç»„ä»¶åªä¿ç•™æ™®é€š `input` / `textarea`,ä¸è¦åœ¨æ¯ä¸ªè¾“入框里é‡å¤å†™é”®ç›˜é€‚é…逻辑。 + +### 10.5 移动端创作生æˆé¡µä¸è¦æš´éœ²æ‰¹æ¬¡è§†è§’ +- æ‹¼å›¾ã€æŠ“å¤§é¹…è¿™ç±»è½»é‡çŽ©æ³•çš„è‰ç¨¿ç”Ÿæˆé¡µåªä¿ç•™â€œé¢„计等待â€å’Œâ€œè®¡æ—¶â€ä¸¤ä¸ªç”¨æˆ·å…³å¿ƒçš„状æ€ï¼Œç§»åŠ¨ç«¯æ”¾åœ¨åŒä¸€è¡Œï¼›ä¸è¦é»˜è®¤å±•ç¤ºâ€œå½“å‰æ‰¹æ¬¡â€è¿™ç±»æ¨¡åž‹æ‰§è¡Œè§†è§’。 +- ç”Ÿæˆæ­¥éª¤åœ¨ç§»åŠ¨ç«¯è¿›å…¥é¡µé¢æ—¶æŒ‰é¡ºåºä»Žå·¦ä¾§æ»‘入,强化“正在推进â€çš„节å¥ï¼›åŠ¨ç”»åªç»‘定步骤å¡ï¼Œä¸å½±å“桌é¢ç«¯å¯†é›†å¸ƒå±€å’Œå…¶å®ƒä¿¡æ¯å¡ã€‚ diff --git a/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md b/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md index 25ce63db..bdeb828d 100644 --- a/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md +++ b/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md @@ -348,6 +348,7 @@ interface PuzzleAnchorPack { 7. åŽ†å²æ‹¼å›¾ç´ æåº“è¯»å– `asset_kind = puzzle_cover_image` 的资产记录,åªç”¨äºŽé€‰æ‹©å‚考图,ä¸ç›´æŽ¥æ›¿æ¢æ­£å¼å›¾ã€‚ 8. 从历å²ç´ æåº“选择素æåŽï¼Œå‰ç«¯æŠŠè¯¥ç´ æçš„ `imageSrc` 作为 `referenceImageSrc` 传入下一次生æˆè¯·æ±‚。 9. 本地上传å‚考图与历å²ç´ æå‚考图互斥;åŽé€‰æ‹©è€…覆盖先选择者。 +10. 历å²ç´ æåˆ—表的图片å称必须从 `imageSrc` 的路径末尾推导,ä¸èƒ½æŠŠ `ownerLabel` è´¦å·å½’属文案当æˆå›¾ç‰‡åç§°ï¼›ç”Ÿæˆæ—¶é—´å¿…须兼容 SpacetimeDB 秒级时间字符串。 å‰ç«¯ UI 规则: diff --git a/docs/prd/MY_TAB_FEATURE_PRD_INDEX_2026-04-16.md b/docs/prd/MY_TAB_FEATURE_PRD_INDEX_2026-04-16.md index a2420fe8..1cbc5e03 100644 --- a/docs/prd/MY_TAB_FEATURE_PRD_INDEX_2026-04-16.md +++ b/docs/prd/MY_TAB_FEATURE_PRD_INDEX_2026-04-16.md @@ -80,6 +80,7 @@ - `PlatformHomeView` 继续作为“我的â€Tab 首屿‰¿è½½å±‚ - ä¼˜å…ˆé‡‡ç”¨çŽ°æœ‰é¢æ¿ã€æŠ½å±‰ã€å¼¹çª—ï¼Œä¸æ–°å»ºç‹¬ç«‹å¤§ç³»ç»Ÿ - 页é¢åªå±•示åŽç«¯è¿”回的状æ€ï¼Œä¸è‡ªè¡Œè®¡ç®—ç»“è®ºåž‹ä¸šåŠ¡çŠ¶æ€ +- 会员中心与充值入å£åªä¿ç•™åœ¨é¡¶éƒ¨èº«ä»½å¡å³ä¾§æŒ‰é’®ï¼Œä¸åœ¨â€œå¸¸ç”¨åŠŸèƒ½â€åŒºé‡å¤å±•示 - æ¯æ—¥ä»»åС入壿”¾åœ¨â€œå¸¸ç”¨åŠŸèƒ½â€ï¼Œç‚¹å‡»åŽå¼¹å‡ºç‹¬ç«‹ä»»åС颿¿ ### 4.2 åŽç«¯è¾¹ç•Œ diff --git a/docs/prd/MY_TAB_MEMBERSHIP_CENTER_PRD_2026-04-16.md b/docs/prd/MY_TAB_MEMBERSHIP_CENTER_PRD_2026-04-16.md index f72f5706..dffed120 100644 --- a/docs/prd/MY_TAB_MEMBERSHIP_CENTER_PRD_2026-04-16.md +++ b/docs/prd/MY_TAB_MEMBERSHIP_CENTER_PRD_2026-04-16.md @@ -75,6 +75,8 @@ 3. 中部显示æƒç›Šå¡ç‰‡ 4. 底部显示套é¤ä¸Žè´­ä¹°æŒ‰é’® +2026-05-14 补充:充值入å£åªä¿ç•™åœ¨â€œæˆ‘çš„â€é¡µé¡¶éƒ¨èº«ä»½å¡å³ä¾§æŒ‰é’®ï¼Œå¸¸ç”¨åŠŸèƒ½åŒºä¸å†é‡å¤å±•示充值å¡ç‰‡ï¼Œé¿å…åŒå±å‡ºçŽ°ä¸¤ä¸ªç›¸åŒä¸šåС入å£ã€‚ + ## 4.2 页é¢å†…容 页é¢å±•示模å—: diff --git a/docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md b/docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md index 87b2b47a..793aa4ed 100644 --- a/docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md +++ b/docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md @@ -42,7 +42,7 @@ APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000 # VectorEngine / Gemini 原生图片 / GPT-image-2 / Suno / Vidu 生æˆç½‘å…³ VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn VECTOR_ENGINE_API_KEY= -VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000 +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000 # Hyper3D Rodin Gen-2 3D æ¨¡åž‹ç”Ÿæˆ @@ -102,14 +102,14 @@ HYPER3D_MODEL_REQUEST_TIMEOUT_MS / RODIN_MODEL_REQUEST_TIMEOUT_MS 3. 文本 LLM provider 为 `ark` 且未é…ç½® `GENARRATIVE_LLM_BASE_URL` 时,ä»å›žé€€åˆ° Ark 公开基础 URL。 4. 角色视频 provider å¤ç”¨ Ark 且未é…ç½® `ARK_CHARACTER_VIDEO_BASE_URL` 时,ä»å›žé€€åˆ° Ark 公开基础 URL。 5. 具体模型å缺失时ä¸åœ¨é…置层伪造默认模型,调用到对应能力时由下游é…置校验返回缺é…置错误。 -6. VectorEngine 图片与音频生æˆåªè¯»å– `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,其中 GPT-image-2 与抓大鹅 Gemini ç´ æ sheet 图片生æˆé¢å¤–è¯»å– `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`ï¼›ä¸å¤ç”¨ `APIMART_*`ã€`GENARRATIVE_LLM_*` 或å‰ç«¯å˜é‡ã€‚拼图 Agent çš„ç”Ÿæˆ action ä¸åšå‰ç«¯è‡ªåЍé‡è¯•,é¿å…一次点击在上游超时åŽé‡å¤è§¦å‘外部生图与钱包扣退费;若 VectorEngine 请求达到该超时窗å£ï¼Œapi-server 返回 `504 Gateway Timeout`,`error.details.provider` 为 `vector-engine`,并ä¿ç•™å…·ä½“è¶…æ—¶ message。 +6. VectorEngine 图片与音频生æˆåªè¯»å– `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,其中 GPT-image-2 与抓大鹅 Gemini ç´ æ sheet 图片生æˆé¢å¤–è¯»å– `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`ï¼›ä¸å¤ç”¨ `APIMART_*`ã€`GENARRATIVE_LLM_*` 或å‰ç«¯å˜é‡ã€‚图片请求默认超时窗å£ä¸º `1000000ms`,且 api-server 会把旧环境中较å°çš„ `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` æå‡åˆ°è¯¥ä¸‹é™ï¼Œé¿å… 500 秒级生图被æå‰æˆªæ–­ã€‚拼图 Agent çš„ç”Ÿæˆ action ä¸åšå‰ç«¯è‡ªåЍé‡è¯•,é¿å…一次点击在上游超时åŽé‡å¤è§¦å‘外部生图与钱包扣退费;若 VectorEngine 请求达到该超时窗å£ï¼Œapi-server 返回 `504 Gateway Timeout`,`error.details.provider` 为 `vector-engine`,并ä¿ç•™å…·ä½“è¶…æ—¶ message。 7. ç«å±±å¼•擎语音能力由 `platform-speech` æ”¶å£å议帧与上游鉴æƒï¼Œ`api-server` åªæš´éœ²å¹³å°é‰´æƒåŽçš„代ç†è·¯ç”±ï¼Œä¸å‘å‰ç«¯è¿”回任何密钥字段。 8. Hyper3D Rodin Gen-2 使用公开默认 `https://api.hyper3d.com/api/v2`,API Key åªè¯»å– `HYPER3D_API_KEY` / `RODIN_API_KEY`,ä¸å¤ç”¨æ–‡æœ¬ LLMã€å›¾ç‰‡æˆ–音频网关密钥。 9. APIMart 当å‰åªä¿ç•™ç»™åˆ›æ„ Agent çš„ `gpt-5` Responses 文本/多模æ€ç†è§£é“¾è·¯ï¼›æŠ“大鹅物å“ç´ æ sheetã€GPT-image-2 图片生æˆå’ŒéŸ³é¢‘生æˆéƒ½ä¸å¾—è¯»å– APIMart é…置。 10. 本地 `npm run api-server`ã€`npm run dev:rust`ã€`npm run dev` 与 `npm run dev:web` 的环境文件优先级固定为éžç©ºå¤–层 shell å˜é‡æœ€é«˜ï¼Œå…¶åŽ `.env`ã€`.env.local`ã€`.env.secrets.local` é€å±‚覆盖;真实密钥建议放在 `.env.secrets.local`,防止 `.env` 中的空示例值覆盖ç§å¯†é…置。外层 shell å˜é‡å¦‚果是空字符串或全空白,ä¸å†é®è”½æœ¬åœ° env 文件中的真实值。 11. OSS 客户端åªåœ¨ `ALIYUN_OSS_BUCKET`ã€`ALIYUN_OSS_ENDPOINT`ã€`ALIYUN_OSS_ACCESS_KEY_ID`ã€`ALIYUN_OSS_ACCESS_KEY_SECRET` 四项é½å…¨æ—¶åˆå§‹åŒ–。四项全部缺失表示未å¯ç”¨ OSS;部分缺失时 `api-server` 记录 warning å¹¶ç»§ç»­å¯åŠ¨ï¼Œå…·ä½“ä¸Šä¼ ã€æ¢ç­¾æˆ–è¯»å– generated ç§æœ‰èµ„产的接å£è¿”回 `OSS 未完æˆçŽ¯å¢ƒå˜é‡é…ç½®`,并在 `error.details.missingEnv` 中列出缺失å˜é‡ã€‚ 12. 抓大鹅 2D è‰ç¨¿ç´ æç”Ÿæˆéœ€è¦åŒæ—¶å…·å¤‡ VectorEngine 与 OSS é…置:VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent` è´Ÿè´£ç”Ÿæˆ 5x5 物å“ç´ æ sheetï¼›å°é¢å’Œ `9:16` 背景图走 VectorEngine `/v1/images/generations` çš„ `gpt-image-2-all` JSON 链路;`1:1` 容器 UI 图走 VectorEngine `/v1/images/edits` multipart 链路,并把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传,ä¸èƒ½å†ç”¨ generations `image` 数组弱å‚考。OSS è´Ÿè´£ä¿å­˜åˆ‡å‰²åŽçš„五视角图片åŠå…¶å®ƒç”Ÿæˆå›¾ã€‚缺少 VectorEngine 或 OSS 时应通过 `error.details.reason` å‘å‰ç«¯æš´éœ²å…·ä½“缺项,ä¸èƒ½åªæ˜¾ç¤ºæ³›åŒ–“æœåŠ¡æš‚ä¸å¯ç”¨â€ã€‚ç´ æå›¾ã€å°é¢å›¾å’ŒèƒŒæ™¯å›¾ç”Ÿæˆåœ¨è°ƒç”¨å¤–部生图å‰å¿…须先预检 OSS,é¿å…å·²æ¶ˆè€—å¤–éƒ¨ç”Ÿå›¾åŽæ‰å‘现无法è½åº“。 -13. 拼图有å‚è€ƒå›¾ä¸”å¼€å¯ AI é‡ç»˜æ—¶ä½¿ç”¨ VectorEngine `POST /v1/images/edits` multipart 接å£ã€‚若返回 `error sending request for url`,代表åŽç«¯æœªæ”¶åˆ° HTTP å“应;å“应 `details` 会带 `reason`ã€`source`ã€`connect`ã€`body`ã€`timeout` å’Œ `endpoint`,排查时优先检查æœåŠ¡å™¨ç½‘ç»œã€DNSã€é˜²ç«å¢™ã€ä»£ç†å’Œå‚考图大å°ã€‚拼图图片客户端强制 HTTP/1.1,以é™ä½Žä¸Šæ¸¸ multipart HTTP/2 连接中断风险。 +13. 拼图有å‚è€ƒå›¾ä¸”å¼€å¯ AI é‡ç»˜æ—¶ä½¿ç”¨ VectorEngine `POST /v1/images/edits` multipart 接å£ã€‚若返回 `error sending request for url`,代表åŽç«¯æœªæ”¶åˆ° HTTP å“应;å“应 `details` 会带 `reason`ã€`source`ã€`connect`ã€`body`ã€`timeout` å’Œ `endpoint`,å‰ç«¯å±•示优先使用 `details.reason`,排查时优先检查æœåŠ¡å™¨ç½‘ç»œã€DNSã€é˜²ç«å¢™ã€ä»£ç†å’Œå‚考图大å°ã€‚拼图图片客户端强制 HTTP/1.1,以é™ä½Žä¸Šæ¸¸ multipart HTTP/2 连接中断风险。 14. 本地排查 `OSS 未完æˆçŽ¯å¢ƒå˜é‡é…ç½®` æ—¶å¿…é¡»æ ¸å¯¹é”®åæ˜¯å¦ç²¾ç¡®ä¸º `ALIYUN_OSS_ACCESS_KEY_SECRET`。常è§è¯¯å†™æ˜¯æŠŠ `OSS` çš„é¦–å­—æ¯ `O` å†™æˆæ•°å­— `0`,例如 `ALIYUN_0SS_ACCESS_KEY_SECRET`;该键ä¸ä¼šè¢« `api-server` 读å–。 ## 本地é…置检查 diff --git a/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md b/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md index dd5b0236..75d990fb 100644 --- a/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md +++ b/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md @@ -45,6 +45,8 @@ 所有å‰ç«¯å¯è§ä¸”会消耗泥点的按钮,点击åŽå¿…é¡»å…ˆå¼¹å‡ºç‹¬ç«‹ç¡®è®¤é¢æ¿ï¼Œé¢æ¿æ ‡é¢˜ä½¿ç”¨ `确认消耗泥点`,正文åªå±•示本次消耗数é‡ï¼Œä¾‹å¦‚ `消耗 2 泥点`。用户点击 `确定` åŽæ‰å…许调用åŽç«¯æ‰£è´¹åŠ¨ä½œï¼›ç‚¹å‡» `å–æ¶ˆ` æˆ–å…³é—­é¢æ¿ä¸å¾—è§¦å‘æŽ¥å£ã€‚ +2026-05-14 补充:创作页入å£ç‚¹å‡»ç¡®è®¤åŽï¼Œå‰ç«¯å¿…须先刷新 `/api/profile/dashboard` 钱包余é¢ï¼›ä½™é¢å¤§äºŽç­‰äºŽæœ¬æ¬¡æ¶ˆè€—æ—¶æ‰å…许创建玩法 session / è‰ç¨¿ï¼Œä½™é¢ä¸è¶³æ—¶åœç•™åœ¨åˆ›ä½œé¡µå¹¶å±•示ä¸è¶³æç¤ºã€‚è¯¥é¢„æ£€ä¸æ›¿ä»£åŽç«¯æ‰£è´¹åŽŸå­æ ¡éªŒï¼Œåªç”¨äºŽé¿å…余颿˜Žæ˜¾ä¸è¶³æ—¶å…ˆç”ŸæˆåŠæˆå“è‰ç¨¿ã€‚ + 2026-05-14 当å‰å·²è¦†ç›–çš„è‰ç¨¿é¡µå…¥å£åŒ…括: - æ‹¼å›¾å…¥å£ `AIé‡ç»˜=true` çš„ `ç”Ÿæˆæ‹¼å›¾æ¸¸æˆè‰ç¨¿`:`2` 泥点;`AIé‡ç»˜=false` ç›´æŽ¥ä½¿ç”¨ä¸Šä¼ å›¾ï¼Œä¸æ˜¾ç¤ºæ³¥ç‚¹ç¡®è®¤ã€‚ diff --git a/docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md b/docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md index abe8fd50..6c288a46 100644 --- a/docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md +++ b/docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md @@ -13,6 +13,9 @@ 1. `server-rs/crates/api-server/src/assets.rs` 中的历å²ç´ æç±»åž‹ç™½åå•统一收å£ä¸ºå•䏀叏釿ºã€‚ 2. HTTP 层错误文案与实际支æŒåˆ—表由åŒä¸€å‡½æ•°ç”Ÿæˆï¼Œé¿å…åŽç»­å†å‡ºçŽ°â€œæ ¡éªŒæ”¹äº†ä½†æç¤ºæ–‡æ¡ˆè¿˜æ˜¯æ—§å£å¾„â€çš„æ¼‚移。 3. 增加 `puzzle_cover_image` çš„å›žå½’æµ‹è¯•ï¼Œç¡®ä¿æ‹¼å›¾å°é¢ç´ æä¸ä¼šå†æ¬¡è¢«åކ岿ޥå£é—æ¼ã€‚ +4. `ownerLabel` åªè¡¨ç¤ºèµ„产归属账å·ï¼Œä¸æ˜¯åކå²å›¾ç‰‡æ ‡é¢˜ï¼›å‰ç«¯åކå²ç´ æå¡ç‰‡æ ‡é¢˜å¿…须从 `imageSrc` 的路径末尾推导,例如 `/generated-puzzle-assets/history/image.png` 展示为 `image.png`。 +5. `createdAt / updatedAt` å¯èƒ½æ¥è‡ª SpacetimeDB / shared-kernel 的秒级字符串,例如 `1713686400.000000Z`,å‰ç«¯ä¸å¾—åªç”¨ `new Date(value)` è§£æžåŽæŠŠå®ƒæ˜¾ç¤ºæˆæœªçŸ¥æ—¶é—´ã€‚ +6. 历å²ç´ æé€‰ä¸­åŽä»æŠŠ `imageSrc` 作为 `referenceImageSrc` 传给生æˆé“¾è·¯ï¼›åˆ›ä½œé¡µå’Œå…³å¡è¯¦æƒ…页的预览必须通过 `ResolvedAssetImage` æ¢ç­¾å±•示,ä¸ç›´æŽ¥è¯·æ±‚裸 `/generated-*` 路径。 ## åŽç»­çº¦æŸ @@ -21,3 +24,7 @@ - `spacetime-module` 的历å²ç´ æç™½åå• - 对应å‰ç«¯è°ƒç”¨å¸¸é‡ä¸Žæµ‹è¯• 2. 如果è¿è¡Œæ€ä»è¿”回旧白åå•错误,优先检查本地 `api-server.exe` 是å¦å·²æŒ‰æœ€æ–°æºç é‡æ–°ç¼–译并é‡å¯ï¼Œè€Œä¸æ˜¯å…ˆå›žé€€å‰ç«¯ç±»åž‹å‚数。 +3. 历å²ç´ æåˆ—表的 UI 回归测试应覆盖: + - å¡ç‰‡æ ‡é¢˜ä¸ä½¿ç”¨ `è´¦å· user-1` 这类归属文案。 + - `1713686400.000000Z` 能显示为å¯è¯»ç”Ÿæˆæ—¶é—´ã€‚ + - 选中素æåŽå·¥ä½œå° / å…³å¡è¯¦æƒ…展示 `历å²ç´ æ · image.png`,并继续æäº¤åŽŸå§‹ `imageSrc`。 diff --git a/docs/technical/AUTH_NEW_ACCOUNT_INVITE_MODAL_2026-05-01.md b/docs/technical/AUTH_NEW_ACCOUNT_INVITE_MODAL_2026-05-01.md index cd6e06f5..5544a246 100644 --- a/docs/technical/AUTH_NEW_ACCOUNT_INVITE_MODAL_2026-05-01.md +++ b/docs/technical/AUTH_NEW_ACCOUNT_INVITE_MODAL_2026-05-01.md @@ -53,11 +53,18 @@ POST /api/profile/referrals/redeem-code 若用户登录的是已有账å·ï¼Œåˆ™ä¸ä¼šå¼¹å‡ºæ–°è´¦å·é‚€è¯·ç é¢æ¿ã€‚ -## 5. 完æˆå®šä¹‰ +## 5. æ–°è´¦å·æ³¥ç‚¹åˆå§‹åŒ– + +当短信登录ã€å¼€å‘密ç å…¥å£æˆ–微信激活æµç¨‹åˆ›å»ºæ–°è´¦å·æ—¶ï¼ŒåŽç«¯æ³¨å†Œé“¾è·¯å¿…须调用 `grant_new_user_registration_wallet_reward`,为该用户写入 `10` 个åˆå§‹æ³¥ç‚¹ã€‚ + +该赠é€è½åœ¨ `profile_dashboard_state.wallet_balance` 与 `profile_wallet_ledger` ä¸­ï¼Œæµæ°´æ¥æºä¸º `new_user_registration_reward`ï¼Œæµæ°´ ID 固定为 `new-user-registration:{user_id}`,用于ä¿è¯é‡å¤è°ƒç”¨ä¸é‡å¤å‘放。已有账å·ç™»å½•ä¸å¾—冿¬¡å‘放。 + +## 6. 完æˆå®šä¹‰ 1. 登录弹窗内ä¸å¯è§æ³¨å†Œå…¥å£ã€‚ 2. 短信登录创建新账å·åŽå¼¹å‡ºé‚€è¯·ç é¢æ¿ã€‚ 3. 邀请ç ä¸ºç©ºæ—¶æŒ‰é’®ä¸º `跳过`,éžç©ºæ—¶æŒ‰é’®ä¸º `æäº¤`。 4. å–æ¶ˆæŒ‰é’®å¯å…³é—­é¢æ¿ã€‚ 5. å·²ç™»å½•é‚€è¯·ç æŽ¥å£å…许æäº¤ï¼Œå¹¶ç»§ç»­ç”± SpacetimeDB procedure 兜底业务校验。 -6. å‰ç«¯æµ‹è¯•覆盖注册入å£åˆ é™¤ã€æ–°è´¦å·å¼¹çª—ã€URL 邀请ç é¢„填与æäº¤ã€‚ +6. æ–°è´¦å·åˆ›å»ºæˆåŠŸåŽé»˜è®¤èŽ·å¾— `10` 个泥点,且é‡å¤ç™»å½•或é‡è¯•ä¸å¾—é‡å¤å‘放。 +7. å‰ç«¯æµ‹è¯•覆盖注册入å£åˆ é™¤ã€æ–°è´¦å·å¼¹çª—ã€URL 邀请ç é¢„填与æäº¤ã€‚ diff --git a/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md b/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md index 77202297..25fb8340 100644 --- a/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md +++ b/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md @@ -52,7 +52,7 @@ Phase 2 的作å“é…置边界是“轻创作é…置作å“â€ï¼šåˆ›ä½œè€…å¯ä»¥é… 建议先支æŒâ€œæœ¬åœ° runtime + å¯å‘布é…ç½®åŒ–ä½œå“ + å•局结果记录 + ä¸ªäººåŽ†å²æˆç»© / 作å“统计 / æœ€å°æŽ’è¡Œæ¦œâ€çš„闭环: -1. 创作者从玩法选择进入 bark-battle åŽåˆ›å»ºè‰ç¨¿ï¼Œé€šè¿‡å•页轻é…ç½®è¡¨å• + 预览å¡ç‰‡é…ç½®æ ‡é¢˜ã€æè¿°ã€ä¸»é¢˜/背景预设ã€ç‹—狗皮肤预设ã€éš¾åº¦é¢„设和排行榜开关。 +1. 创作者从玩法选择进入 bark-battle åŽåˆ›å»ºè‰ç¨¿ï¼Œé€šè¿‡åˆ›ä½œ Tab 内嵌轻é…ç½®è¡¨å• + 预览å¡ç‰‡é…ç½®æ ‡é¢˜ã€æè¿°ã€ä¸»é¢˜/背景预设ã€ç‹—狗皮肤预设ã€éš¾åº¦é¢„设和排行榜开关。 2. å‘å¸ƒä¸ºç¨³å®šä½œå“ ID,`playTypeId = "bark-battle"`。 3. 玩家å¯ä»Žä½œå“详情页 CTAã€å¹¿åœº/作å“å¡ç‰‡ã€æˆ‘的作å“/ä¸ªäººä½œå“æž¶è¿›å…¥æ­£å¼ runtime,å‰ç«¯ä½¿ç”¨ç¨³å®šä½œå“ ID 获å–å‘å¸ƒæ€ runtime config。 4. 玩家授æƒéº¦å…‹é£ŽåŽåœ¨æœ¬åœ°å®Œæˆ 30 秒声控对战。 @@ -91,15 +91,15 @@ Phase 2 çš„ä¸ªäººåŽ†å²æˆç»©ç”±â€œæœ€è¿‘记录列表 + 个人最佳摘è¦â€ç»„ ### 2.2.6 æ­£å¼ä½œå“å…¥å£é—­çޝ -Phase 2 必须接入 Bark Battle æ­£å¼ä½œå“å…¥å£é—­çŽ¯ï¼Œä½†ä¸æ–°å¢žç‹¬ç«‹ä¸“åŒºã€æ´»åŠ¨é¡µã€æŒ‘战分享页ã€å¥½å‹é‚€è¯·æˆ–多人房间入å£ã€‚å…¥å£èŒƒå›´åŒ…括:创作入å£/玩法选择中出现 `bark-battle`,进入å•页轻é…ç½®è¡¨å• + 预览å¡ç‰‡ï¼›ä½œå“详情页 CTA 点击“开始游玩â€è¿›å…¥æ­£å¼ runtime;广场/作å“å¡ç‰‡å¯ä»¥å±•ç¤ºã€æ‰“开详情并开始游玩;我的作å“/ä¸ªäººä½œå“æž¶èƒ½çœ‹åˆ°ä½œè€…å‘布的 Bark Battle 作å“ï¼›runtime è·¯ç”±ä½¿ç”¨ç¨³å®šä½œå“ ID 并从åŽç«¯å‘å¸ƒæ€ config 拉å–é…置。 +Phase 2 必须接入 Bark Battle æ­£å¼ä½œå“å…¥å£é—­çŽ¯ï¼Œä½†ä¸æ–°å¢žç‹¬ç«‹ä¸“åŒºã€æ´»åŠ¨é¡µã€æŒ‘战分享页ã€å¥½å‹é‚€è¯·æˆ–多人房间入å£ã€‚å…¥å£èŒƒå›´åŒ…括:创作入å£/玩法选择中出现 `bark-battle`,在创作 Tab æ¨¡æ¿æ¡ä¸‹æ–¹ç›´æŽ¥å†…嵌轻é…ç½®è¡¨å• + 预览å¡ç‰‡ï¼›ä½œå“详情页 CTA 点击“开始游玩â€è¿›å…¥æ­£å¼ runtime;广场/作å“å¡ç‰‡å¯ä»¥å±•ç¤ºã€æ‰“开详情并开始游玩;我的作å“/ä¸ªäººä½œå“æž¶èƒ½çœ‹åˆ°ä½œè€…å‘布的 Bark Battle 作å“ï¼›runtime è·¯ç”±ä½¿ç”¨ç¨³å®šä½œå“ ID 并从åŽç«¯å‘å¸ƒæ€ config 拉å–é…置。 æ­£å¼ run start æˆåŠŸåŽå¿…须写 `work_play_start`,其中 `scope_kind=work`ã€`scope_id=ç¨³å®šä½œå“ ID`,metadata è‡³å°‘åŒ…å« `playType=bark-battle`ã€`workId`ã€`sourceRoute`ã€`userId`。内部试玩入å£å¯ä»¥ä½œä¸ºå¼€å‘调试ä¿ç•™ï¼Œä½†ä¸å¾—作为 Phase 2 æ­£å¼å…¥å£ã€‚ ### 2.2.7 è½»é…置编辑æµç¨‹ -Phase 2 çš„åˆ›ä½œç¼–è¾‘å½¢æ€æ˜¯â€œå•页轻é…ç½®è¡¨å• + 预览å¡ç‰‡â€ï¼Œä¸æ˜¯å¤šæ­¥éª¤å‘å¯¼ã€æ‹–拽编辑器或完整规则编辑器。表å•字段包å«ï¼šæ ‡é¢˜ï¼ˆå¿…填)ã€ç®€ä»‹ï¼ˆé€‰å¡«ï¼‰ã€ä¸»é¢˜/背景预设(必填枚举)ã€ç‹—狗皮肤预设(必填枚举)ã€éš¾åº¦é¢„设(必填,默认 `normal`ï¼‰ã€æŽ’è¡Œæ¦œå¼€å…³ï¼ˆé»˜è®¤å¼€å¯ï¼‰ã€‚ +Phase 2 çš„åˆ›ä½œç¼–è¾‘å½¢æ€æ˜¯â€œåˆ›ä½œ Tab 内嵌轻é…ç½®è¡¨å• + 预览å¡ç‰‡â€ï¼Œä¸æ˜¯ç‹¬ç«‹é…置页é¢ã€å¤šæ­¥éª¤å‘å¯¼ã€æ‹–拽编辑器或完整规则编辑器。表å•字段包å«ï¼šæ ‡é¢˜ï¼ˆå¿…填)ã€ç®€ä»‹ï¼ˆé€‰å¡«ï¼‰ã€ä¸»é¢˜/背景预设(必填枚举)ã€ç‹—狗皮肤预设(必填枚举)ã€éš¾åº¦é¢„设(必填,默认 `normal`ï¼‰ã€æŽ’è¡Œæ¦œå¼€å…³ï¼ˆé»˜è®¤å¼€å¯ï¼‰ã€‚ -交互æµç¨‹ï¼šåˆ›ä½œè€…从玩法选择进入åŽç”Ÿæˆè‰ç¨¿ï¼›åœ¨åŒä¸€é¡µç¼–辑轻é…置并查看预览å¡ç‰‡ï¼›æ”¯æŒä¿å­˜è‰ç¨¿å’Œå‘布;å‘布æˆåŠŸåŽè·³è½¬ä½œå“详情;å¯ä»Žæˆ‘的作å“冿¬¡ç¼–辑è‰ç¨¿æˆ–基于已å‘布作å“创建新版本。Phase 2 ä¸åš AI 生æˆé…ç½®ã€å¤šæ­¥éª¤ wizardã€è§„åˆ™å‚æ•°ç¼–辑ã€å¤æ‚å°é¢ç¼–辑ã€runtime 内嵌预览或大段玩法说明文案。 +交互æµç¨‹ï¼šåˆ›ä½œè€…从玩法选择进入åŽä¿æŒåœ¨åˆ›ä½œ Tabï¼›åœ¨æ¨¡æ¿æ¡ä¸‹æ–¹ç¼–辑轻é…置并查看预览å¡ç‰‡ï¼›æ”¯æŒä¿å­˜è‰ç¨¿å’Œå‘布;å‘布æˆåŠŸåŽè·³è½¬ä½œå“详情或进入试玩 runtime,runtime 退出时回到创作 Tab 的汪汪声浪模æ¿ã€‚å¯ä»Žæˆ‘的作å“冿¬¡ç¼–辑è‰ç¨¿æˆ–基于已å‘布作å“创建新版本。Phase 2 ä¸åš AI 生æˆé…ç½®ã€å¤šæ­¥éª¤ wizardã€è§„åˆ™å‚æ•°ç¼–辑ã€å¤æ‚å°é¢ç¼–辑ã€runtime 内嵌预览或大段玩法说明文案。 ### 2.2 åŽç»­å¢žå¼ºè·¯å¾„ @@ -115,7 +115,7 @@ Phase 2 æŒ‰â€œå¥‘çº¦å’Œé¢†åŸŸè§„åˆ™å…ˆè¡Œï¼Œç„¶åŽæœ€å°çºµåˆ‡ï¼Œå†æ‰©å±•投影 1. 契约与领域规则:补 `shared-contracts` DTOã€`module-bark-battle` 纯领域规则ã€`rulesetVersion` / `difficultyPreset` / score adjudicationï¼Œå¹¶å…ˆå†™å•æµ‹ã€‚ 2. SpacetimeDB 表与 reducer + api-server BFF:è½è‰ç¨¿/config/å‘å¸ƒæ€ configã€runtime run start / finishã€score recordã€leaderboard entryã€work stats projectionã€personal summary projectionã€`migration.rs` 与绑定生æˆã€‚ -3. 最å°å‰ç«¯çºµåˆ‡ï¼šæŽ¥åˆ›ä½œå…¥å£ã€å•页轻é…置表å•ã€å‘布到稳定 workIdã€ä½œå“详情 CTAã€runtime 拉 configã€start / finish 串通ã€ç»“算展示 `serverResult`。 +3. 最å°å‰ç«¯çºµåˆ‡ï¼šæŽ¥åˆ›ä½œå…¥å£ã€åˆ›ä½œ Tab 内嵌轻é…置表å•ã€å‘布到稳定 workIdã€ä½œå“详情 CTAã€runtime 拉 configã€start / finish 串通ã€ç»“算展示 `serverResult`。 4. 投影与列表体验:接排行榜ã€ä¸ªäººåކ岿œ€è¿‘记录 + 最佳摘è¦ã€ä½œå“ç»Ÿè®¡ã€æˆ‘的作å“/广场å¡ç‰‡é€‚é…。 5. æ”¶å£éªŒè¯ï¼šæŠŠ BDD 场景è½åˆ°æµ‹è¯•ï¼Œæ‰§è¡Œç¼–ç æ£€æŸ¥ã€åŽç«¯ `/healthz` + API smokeã€å‰ç«¯äººå·¥éªŒæ”¶è·¯å¾„,并更新 README/文档。 diff --git a/docs/technical/CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md b/docs/technical/CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md index 0676ecc6..0900004b 100644 --- a/docs/technical/CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md +++ b/docs/technical/CREATION_WORK_SHELF_UNIFICATION_2026-04-25.md @@ -55,3 +55,4 @@ 3. 兜底背景底色跟éšç™¾æ¢¦æµ…ç²‰ã€æš–白和çŠç‘šè‰²è°ƒï¼Œä¸èƒ½ç»§ç»­ä½¿ç”¨æ·±é»‘æˆ–æš—è“æ¸å˜ä½œä¸ºè‰ç¨¿å¡é»˜è®¤è§†è§‰ã€‚ 4. 拼图作å“列表摘è¦å¿…é¡»ä¸‹å‘ `levels`,è‰ç¨¿é¡µä¼˜å…ˆç”¨å…³å¡ `coverImageSrc`,å†ç”¨é€‰ä¸­å€™é€‰å›¾æˆ–最åŽä¸€å¼ å€™é€‰å›¾ä½œä¸ºçœŸå®žä½œå“å°é¢å…œåº•。 5. 抓大鹅作å“列表摘è¦å¿…é¡»ä¿ç•™ `generatedBackgroundAsset` 与 `generatedItemAssets` 中的 `imageObjectKey`ã€`containerImageObjectKey` å’Œ `imageViews[].imageObjectKey`ï¼›å‰ç«¯æ‹¿åˆ° object key åŽç»Ÿä¸€äº¤ç»™ `ResolvedAssetImage` æ¢ç­¾ï¼Œä¸èƒ½å› ä¸ºç¼ºå°‘公开 URL 而退回黑å¡ã€‚ +6. `coverImageSrc` è‹¥æŒ‡å‘ `/creation-type-references/*`,åªèƒ½è§†ä¸ºçŽ©æ³•å‚考图兜底,ä¸èƒ½å½“作作å“真实å°é¢ã€‚è‰ç¨¿é¡µé‡åˆ°è¿™ç±»å€¼æ—¶å¿…须继续å‘下解æžåŒä½œå“真实素æï¼šæ‹¼å›¾ä¼˜å…ˆç¬¬ä¸€å…³æ­£å¼å›¾ï¼Œå†å–选中候选图或最åŽä¸€å¼ å€™é€‰å›¾ï¼›æŠ“大鹅优先 UI 背景图 `backgroundImageSrc/backgroundImageObjectKey/generatedBackgroundAsset.image*`,å†å–å®¹å™¨å›¾ï¼Œæœ€åŽæ‰å–物å“视角图或物å“主图。 diff --git a/docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md b/docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md index 9d9d7f70..fb3d9dcd 100644 --- a/docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md +++ b/docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md @@ -27,6 +27,8 @@ 5. ç”ŸæˆæˆåŠŸåŽè‡ªåŠ¨è¿›å…¥ `match3d-result`。 6. 生æˆå¤±è´¥æ—¶åœç•™åœ¨ç”Ÿæˆè¿‡ç¨‹é¡µï¼Œå…è®¸é‡æ–°ç”Ÿæˆæˆ–è¿”å›žåˆ›ä½œä¸­å¿ƒï¼›é‡æ–°ç”Ÿæˆå¿…é¡»å¤ç”¨åŒä¸€ä¸ª session / profile,并从缺失的素æé˜¶æ®µç»§ç»­ï¼Œä¸æ–°å»ºç¬¬äºŒä»½è‰ç¨¿ã€‚ +抓大鹅生æˆè¿‡ç¨‹é¡µä¸å±•ç¤ºâ€œå½“å‰æ‰¹æ¬¡â€æ¨¡å—;移动端åªä¿ç•™â€œé¢„计等待â€å’Œâ€œè®¡æ—¶â€ä¸¤å¼ çжæ€å¡å¹¶æŽ’展示,步骤å¡è¿›å…¥é¡µé¢æ—¶æŒ‰é¡ºåºä»Žå±å¹•左侧滑入。 + 生æˆé¡µæ­¥éª¤å›ºå®šä¸ºï¼š ```text @@ -52,9 +54,9 @@ 7. åŽç«¯ä»ŽåŒä¸€ä»½ä½œå“生æˆè®¡åˆ’读å–当å‰éš¾åº¦æ‰€éœ€æ•°é‡çš„短物å“å称,并兼容ä¿å­˜åŽ†å² `soundPrompt` 字段;当å‰ä¸ç”Ÿæˆç‚¹å‡»éŸ³æ•ˆã€‚ 8. 调用 VectorEngine Gemini 原生图片接å£ç”Ÿæˆ `1:1` ç´ æå›¾ï¼Œè¯·æ±‚模型固定为 `gemini-3-pro-image-preview`,走 `POST {VECTOR_ENGINE_BASE_URL}/v1beta/models/gemini-3-pro-image-preview:generateContent?key={VECTOR_ENGINE_API_KEY}`。请求体使用 `contents[].parts[].text` å’Œ `generationConfig.responseModalities = ["TEXT", "IMAGE"]`ã€`generationConfig.imageConfig.aspectRatio = "1:1"`,å“应从 `candidates[].content.parts[].inlineData.data` / `inline_data.data` è¯»å– base64 图片。æç¤ºè¯å¿…é¡»åˆå…¥å…¥å£é¡µé€‰æ‹©çš„ `assetStylePrompt`ï¼Œå¹¶å¼ºåˆ¶æ¯æ ¼ä½¿ç”¨ç»Ÿä¸€çº¯ç»¿è‰²ç»¿å¹•背景,é¿å…白底或纹ç†èƒŒæ™¯è¿›å…¥è¿è¡Œæ€ç´ æã€‚该调整åªä½œç”¨äºŽæŠ“大鹅物å“ç´ æ sheetï¼›å°é¢å’Œ `9:16` 纯背景图继续使用 VectorEngine `/v1/images/generations` çš„ `gpt-image-2-all` JSON 链路,`1:1` 容器 UI 图必须使用 VectorEngine `/v1/images/edits` multipart 图生图链路,ä¸èƒ½å†æŠŠå‚考图作为 generations çš„ `image` 数组弱å‚考。 9. æ¯ä¸ªç‰©å“å›ºå®šéœ€è¦ `5` 个ä¸åŒè§†è§’。å•å¼ ç´ æå›¾å›ºå®šä¸º `5*5 = 25` 格,因此å•张图承载 `5` 个物å“ã€‚è‹¥ç”¨æˆ·è¦æ±‚或难度派生的物å“ç§ç±»ä¸æ˜¯ `5` çš„å€æ•°ï¼ŒåŽç«¯å¿…é¡»å‘上补é½ç‰©å“å称和对应图片到最近的 `5` çš„å€æ•°ï¼›ä¾‹å¦‚æ ‡å‡†éš¾åº¦éœ€è¦ `9` ç§çŽ©æ³•ç‰©å“ï¼Œå®žé™…ç”Ÿæˆ `10` 个物å“å称和对应五视角图片。若è‰ç¨¿ç‰©å“数超过 `5`,åŽç«¯æŒ‰æ¯æ‰¹ `5` 个物å“自动分批,多张素æå›¾å¹¶è¡Œç”Ÿæˆã€‚ -10. å°†æ¯å¼ ç´ æå›¾æŒ‰å›ºå®š `5 行 * 5 列` 切割æˆç‹¬ç«‹å›¾ç‰‡ï¼Œå¹¶æŒ‰ç‰©å“顺åºè¿žç»­åˆ†é… `5` 张视角图。素æå›¾æç¤ºè¯å¿…é¡»è¦æ±‚ `5*5` 严格å‡åŒ€æŽ’å¸ƒã€æ¯æ ¼ä¸»ä½“完整居中ã€ç»Ÿä¸€çº¯ç»¿è‰²ç»¿å¹•背景ã€ç›¸é‚»ç‰©ä½“主体至少ä¿ç•™ `1/4` 啿 ¼å®½åº¦ç©ºç™½é—´è·ã€ä¸å¾—跨格ã€è´´è¾¹æˆ–越界,é¿å…è£å‰ªåŽç›¸é‚»æ ¼å†…容污染。切割å‰å¿…须先在整张素æå›¾ä¸Šåšé€æ˜ŽèƒŒæ™¯åŽå¤„ç†ï¼šè¿žé€šåˆ° sheet 外边缘的绿幕/è¿‘ç™½åº•è¦æ¸…æˆ alphaï¼›æ¯æ ¼å†…部未连到外边缘但高置信的纯绿绿幕å—ä¹Ÿå¿…é¡»æ¸…æˆ alpha;物å“边缘的绿幕抗锯齿和近白白边è¦åšé€æ˜Žæˆ–去污染处ç†ï¼›ä¸å¤Ÿçº¯çš„绿色主体åƒç´ ä¸å¾—被当作绿幕误删。éšåŽå†åœ¨æ¯ä¸ªç†è®ºæ ¼å­å†…æŒ‰é€æ˜ŽèƒŒæ™¯/剿™¯åƒç´ åšå†…容边界校准,并带少é‡å®‰å…¨ç•™ç™½å¯¼å‡ºï¼›ä¸èƒ½åšå›ºå®šå†…缩è£å‰ªï¼Œé¿å…贴近格线但未跨格的樱桃ã€å¶ç‰‡ã€æŠŠæ‰‹ç­‰ä¸»ä½“边缘被切掉。æ¯ä¸ªç‰©å“ JSON 写入 `imageViews[]`ï¼ŒåŒæ—¶æŠŠç¬¬ä¸€ä¸ªè§†è§’兼容写入 `imageSrc/imageObjectKey`。 +10. å°†æ¯å¼ ç´ æå›¾æŒ‰å›ºå®š `5 行 * 5 列` 切割æˆç‹¬ç«‹å›¾ç‰‡ï¼Œå¹¶æŒ‰ç‰©å“顺åºè¿žç»­åˆ†é… `5` 张视角图。素æå›¾æç¤ºè¯å¿…é¡»è¦æ±‚ `5*5` 严格å‡åŒ€æŽ’å¸ƒã€æ¯æ ¼ä¸»ä½“完整居中ã€ç»Ÿä¸€çº¯ç»¿è‰²ç»¿å¹•背景ã€ç›¸é‚»ç‰©ä½“主体至少ä¿ç•™ `1/4` 啿 ¼å®½åº¦ç©ºç™½é—´è·ã€ä¸å¾—跨格ã€è´´è¾¹æˆ–越界,é¿å…è£å‰ªåŽç›¸é‚»æ ¼å†…容污染。切割å‰å¿…须先在整张素æå›¾ä¸Šåšé€æ˜ŽèƒŒæ™¯åŽå¤„ç†ï¼šè¿žé€šåˆ° sheet 外边缘的绿幕/è¿‘ç™½åº•è¦æ¸…æˆ alphaï¼›æ¯æ ¼å†…部未连到外边缘但高置信的纯绿绿幕å—ä¹Ÿå¿…é¡»æ¸…æˆ alpha;物å“边缘的绿幕抗锯齿和近白白边è¦åšé€æ˜Žæˆ–去污染处ç†ï¼›è¾ƒåŽšçš„åŠé€æ˜Žæˆ–混色软绿边必须沿整张 sheet 逿˜ŽèƒŒæ™¯ç»§ç»­æ¸…ç†ï¼Œä¸èƒ½å…ˆè£å‰ªå•æ ¼å†å„自去绿,å¦åˆ™è£å‰ªå›¾ä¼šæ®‹ç•™ç»¿è‰²æè¾¹ï¼›ä¸å¤Ÿçº¯çš„绿色主体åƒç´ ä¸å¾—被当作绿幕误删。éšåŽå†åœ¨æ¯ä¸ªç†è®ºæ ¼å­å†…æŒ‰é€æ˜ŽèƒŒæ™¯/剿™¯åƒç´ åšå†…容边界校准,并带少é‡å®‰å…¨ç•™ç™½å¯¼å‡ºï¼›ä¸èƒ½åšå›ºå®šå†…缩è£å‰ªï¼Œé¿å…贴近格线但未跨格的樱桃ã€å¶ç‰‡ã€æŠŠæ‰‹ç­‰ä¸»ä½“边缘被切掉。æ¯ä¸ªç‰©å“ JSON 写入 `imageViews[]`ï¼ŒåŒæ—¶æŠŠç¬¬ä¸€ä¸ªè§†è§’兼容写入 `imageSrc/imageObjectKey`。 11. 将素æå›¾å’Œæ¯å¼ ç‹¬ç«‹è§†è§’图片上传到 OSSã€‚æ¯æ¬¡è޷得坿¢å¤çš„图片资产åŽï¼Œéƒ½è¦å›žå†™ `match3d_work_profile.generated_item_assets_json`。æˆåŠŸç´ æçжæ€ä¸º `image_ready`;失败素æä¿ç•™å·²æˆåŠŸå›¾ç‰‡å¼•ç”¨å¹¶è®°å½• `error`。æ¯ä¸ªç´ æ JSON å¯ç»§ç»­ä¿å­˜åŽ†å² `soundPrompt`ï¼›ä¸å†å†™å…¥æ–°çš„ `backgroundMusicTitle/backgroundMusicStyle/backgroundMusicPrompt`。 -12. UI 背景生æˆç”± `api-server` 分æˆä¸¤å¼ èµ„产:第一张是 `9:16` 纯背景图,走 VectorEngine `/v1/images/generations` çš„ `gpt-image-2-all` JSON 请求,ä¸ä¼ é”…å‚è€ƒå›¾ï¼Œä¸”å¿…é¡»ç¦æ­¢é”…ã€åœ†ç›˜ã€æ‰˜ç›˜ã€æ‹¼å›¾æ§½ã€ç‰©å“æ§½ã€HUDã€æ–‡å­—ã€æŒ‰é’®ã€å€’计时ã€åˆ†æ•°å’Œç‰©å“;第二张是 `1:1` 题æå®¹å™¨ UI 图,走 VectorEngine `/v1/images/edits` multipart 请求,把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传,åªç”Ÿæˆä¸€ä¸ªè´´åˆé¢˜æè®¾å®šçš„圆形或浅盘状竞技容器,ä¸ç”Ÿæˆæ•´é¡µèƒŒæ™¯ã€æ–‡å­—ã€æŒ‰é’®æˆ–物å“。容器图必须沿用å‚è€ƒå›¾çš„å¤§å°ºå¯¸è½»ä¿¯è§†æž„å›¾ï¼šå¤–è½®å»“æŽ¥è¿‘ç”»å¸ƒå››è¾¹ï¼Œå®½åº¦çº¦å  `86%-92%`ã€é«˜åº¦çº¦å  `82%-90%`,内å£ä¸ºæ¨ªå‘æ¤­åœ†ï¼Œç¦æ­¢ç”Ÿæˆå°å®¹å™¨ã€æ­£ä¿¯è§†åœ†ç›˜ã€ä¾§è§†ç¢—ã€é¤ç›˜æˆ–å°æ‰˜ç›˜ã€‚纯背景上传到 `generated-match3d-assets/{sessionId}/{profileId}/background/{taskId}/background.png`,容器 UI 图上传到 `generated-match3d-assets/{sessionId}/{profileId}/ui-container/{taskId}/container.png`,两者都作为 `backgroundAsset` 挂在首个 `generatedItemAssets[]` JSON 上;HTTP DTO åŒæ—¶é¡¶å±‚输出兼容的 `backgroundPrompt`ã€`backgroundImageSrc`ã€`backgroundImageObjectKey` 与 `generatedBackgroundAsset`,容器图通过 `generatedBackgroundAsset.containerImageSrc/containerImageObjectKey` 返回。若作å“尚无用户自定义å°é¢ï¼Œè‰ç¨¿ç”Ÿæˆå®ŒæˆåŽé»˜è®¤æŠŠå®¹å™¨ UI 图写入 `coverImageSrc`,作为è‰ç¨¿æž¶å’Œä½œå“ä¿¡æ¯çš„默认å°é¢ã€‚ +12. UI 背景生æˆç”± `api-server` 分æˆä¸¤å¼ èµ„产:第一张是 `9:16` 纯背景图,走 VectorEngine `/v1/images/generations` çš„ `gpt-image-2-all` JSON 请求,ä¸ä¼ é”…å‚è€ƒå›¾ï¼Œä¸”å¿…é¡»ç¦æ­¢é”…ã€åœ†ç›˜ã€æ‰˜ç›˜ã€æ‹¼å›¾æ§½ã€ç‰©å“æ§½ã€HUDã€æ–‡å­—ã€æŒ‰é’®ã€å€’计时ã€åˆ†æ•°å’Œç‰©å“;第二张是 `1:1` 题æå®¹å™¨ UI 图,走 VectorEngine `/v1/images/edits` multipart 请求,把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传,åªç”Ÿæˆä¸€ä¸ªè´´åˆé¢˜æè®¾å®šçš„圆形或浅盘状竞技容器,ä¸ç”Ÿæˆæ•´é¡µèƒŒæ™¯ã€æ–‡å­—ã€æŒ‰é’®æˆ–物å“。容器图必须沿用å‚è€ƒå›¾çš„å¤§å°ºå¯¸è½»ä¿¯è§†æž„å›¾ï¼šå¤–è½®å»“æŽ¥è¿‘ç”»å¸ƒå››è¾¹ï¼Œå®½åº¦çº¦å  `86%-92%`ã€é«˜åº¦çº¦å  `82%-90%`,内å£ä¸ºæ¨ªå‘æ¤­åœ†ï¼Œç¦æ­¢ç”Ÿæˆå°å®¹å™¨ã€æ­£ä¿¯è§†åœ†ç›˜ã€ä¾§è§†ç¢—ã€é¤ç›˜æˆ–å°æ‰˜ç›˜ã€‚容器图入库å‰å¿…须统一转æˆå¸¦é€æ˜Ž alpha çš„ PNGï¼›è‹¥ä¸Šæ¸¸è¿”å›žç™½åº•ã€æµ…色底或抗锯齿底色,`api-server` 在上传 OSS 剿¸…æˆé€æ˜ŽèƒŒæ™¯ã€‚纯背景上传到 `generated-match3d-assets/{sessionId}/{profileId}/background/{taskId}/background.png`,容器 UI 图上传到 `generated-match3d-assets/{sessionId}/{profileId}/ui-container/{taskId}/container.png`,两者都作为 `backgroundAsset` 挂在首个 `generatedItemAssets[]` JSON 上;HTTP DTO åŒæ—¶é¡¶å±‚输出兼容的 `backgroundPrompt`ã€`backgroundImageSrc`ã€`backgroundImageObjectKey` 与 `generatedBackgroundAsset`,容器图通过 `generatedBackgroundAsset.containerImageSrc/containerImageObjectKey` 返回。若作å“尚无用户自定义å°é¢ï¼Œè‰ç¨¿ç”Ÿæˆå®ŒæˆåŽé»˜è®¤æŠŠå®¹å™¨ UI 图写入 `coverImageSrc`,作为è‰ç¨¿æž¶å’Œä½œå“ä¿¡æ¯çš„默认å°é¢ï¼›è‰ç¨¿æž¶å°é¢è§£æžä¹Ÿåº”ä¼˜å…ˆä½¿ç”¨å®¹å™¨å›¾ï¼Œå…¶æ¬¡æ‰æ˜¯çº¯èƒŒæ™¯å›¾å’Œç‰©å“图,完全缺失生æˆå›¾æ—¶ä½¿ç”¨é€æ˜Žå‚考容器图兜底。 13. 在 HTTP 返回的 draft/profile DTO 中附带本次生æˆçš„ç´ æèµ„产预览信æ¯ã€èƒŒæ™¯èµ„产信æ¯å’Œé»˜è®¤å°é¢ï¼›åŽç»­é‡è¿›è‰ç¨¿é¡µæ—¶ä»Ž work profile çš„æŒä¹…化 `generatedItemAssets` 与 `coverImageSrc` æ¢å¤åŒä¸€æ‰¹ç´ æã€UI 与å°é¢ã€‚历å²éŸ³é¢‘字段åªåšå…¼å®¹ä¼ é€’。 若文本模型ä¸å¯ç”¨æˆ–返回无法解æžï¼ŒåŽç«¯å¿…é¡»é™çº§ä¸º `{themeText}抓大鹅`ã€æœ¬åœ°ä½œå“æè¿°ä¸Žæœ¬åœ°æ ‡ç­¾å…œåº•,ä¸é˜»æ–­ç´ æç”Ÿæˆï¼›æ ‡ç­¾ä»é€šè¿‡ä½œå“标签生æˆå™¨ä¼˜å…ˆç”Ÿæˆï¼Œå¤±è´¥åŽå†ç”¨å…œåº•标签补é½ã€‚ @@ -100,7 +102,7 @@ public/match3d-style-references/painterly-icon.png public/match3d-background-references/pot-fused-reference.png ``` -这张图åªä½œä¸ºå®¹å™¨ UI 图的 VectorEngine `/v1/images/edits` multipart `image` part,用æ¥é”定“大尺寸轻俯视浅盘容器â€çš„æž„图。å‚考图本身是 `1:1` 逿˜Žåº•容器素æï¼Œå¤–轮廓接近画布四边,内å£ä¸ºæ¨ªå‘椭圆;结果页没有真实生æˆå®¹å™¨æ—¶ä¹ŸåªæŠŠå®ƒä½œä¸ºå®¹å™¨é¢„览兜底,ä¸èƒ½å†ä½œä¸º `9:16` èƒŒæ™¯é¢„è§ˆã€‚æ¯æ¬¡è‰ç¨¿ç”Ÿæˆä»ä¼šæ ¹æ® `backgroundPrompt` ç”Ÿæˆæ–°çš„题æåŒ–纯背景图;纯背景图ä¸å†ä¼ å…¥è¯¥å‚考图,也ä¸å¾—生æˆé”…或 UI 元素。 +这张图åªä½œä¸ºå®¹å™¨ UI 图的 VectorEngine `/v1/images/edits` multipart `image` part,用æ¥é”定“大尺寸轻俯视浅盘容器â€çš„æž„图。å‚考图本身是 `1:1` 逿˜Žåº•容器素æï¼Œå¤–轮廓接近画布四边,内å£ä¸ºæ¨ªå‘椭圆;结果页ã€è‰ç¨¿é¢„览和è¿è¡Œæ€æ²¡æœ‰çœŸå®žç”Ÿæˆå®¹å™¨æ—¶éƒ½æŠŠå®ƒä½œä¸ºå®¹å™¨å…œåº•,ä¸èƒ½å†ä½œä¸º `9:16` èƒŒæ™¯é¢„è§ˆã€‚æ¯æ¬¡è‰ç¨¿ç”Ÿæˆä»ä¼šæ ¹æ® `backgroundPrompt` ç”Ÿæˆæ–°çš„题æåŒ–纯背景图;纯背景图ä¸å†ä¼ å…¥è¯¥å‚考图,也ä¸å¾—生æˆé”…或 UI 元素。 ## 5. OSS 路径 @@ -140,7 +142,7 @@ Match3DWorkProfile / PlatformMatch3DGalleryCard è¿è¡Œæ€æŒ‰è¿è¡Œå¿«ç…§ä¸­çš„ `itemTypeId` 稳定排åºåŽï¼ŒæŠŠ `generatedItemAssets` é¡ºåºæ˜ å°„到对应类型。加载æŸä¸ªç‰©å“实例时,从该类型素æçš„ `imageViews[]` 中按实例 id ç¨³å®šéšæœºé€‰æ‹©ä¸€ä¸ªè§†è§’ï¼›è‹¥åŽ†å²æ•°æ®æ²¡æœ‰ `imageViews[]`,则回退到 `imageSrc/imageObjectKey`。没有生æˆå›¾ç‰‡æˆ–图片加载失败时,继续使用默认积木图标兜底。 -è¿è¡Œæ€èƒŒæ™¯ä¼˜å…ˆè¯»å– `backgroundImageSrc` / 顶层 `generatedBackgroundAsset.imageSrc/imageObjectKey`,为空时从 `generatedItemAssets[].backgroundAsset.imageSrc/imageObjectKey` 兜底。中心容器优先读å–顶层 `generatedBackgroundAsset.containerImageSrc/containerImageObjectKey`,å†è¯»å– `generatedItemAssets[].backgroundAsset.containerImageSrc/containerImageObjectKey`;为空或æ¢ç­¾/图片加载失败时继续使用默认圆形容器样å¼ã€‚容器图æˆåŠŸåŠ è½½åŽï¼Œ`Match3DRuntimeShell` 的棋盘容器必须切æ¢ä¸ºé€æ˜Žã€å¯æº¢å‡ºæ‰¿è½½ï¼Œä¸å†å åŠ é»˜è®¤ `rounded-full` 圆形锅壳ã€é‡‘色边框和默认径å‘背景,é¿å… AI 生æˆçš„大尺寸轻俯视容器被è£åˆ‡æˆ–被默认锅视觉覆盖。è¿è¡Œæ€å…¥å£åˆ¤æ–­æ˜¯å¦éœ€è¦è¡¥è¯»ä½œå“详情时,åªèƒ½æŠŠ `imageViews[]` 或 `imageSrc/imageObjectKey` 视为“已有物å“图片素æâ€ï¼›`backgroundMusic.audioSrc`ã€`clickSound.audioSrc`ã€`generatedBackgroundAsset`ã€`backgroundAsset.image*` å’Œ `backgroundAsset.containerImage*` 是éšç‰©å“ç´ æä¸€èµ·ä¼ å…¥çš„附属è¿è¡Œæ€èµ„产,ä¸èƒ½å•ç‹¬è¯æ˜Žç‰©å“ç´ æå·²å®Œæ•´ã€‚也ä¸èƒ½ç»§ç»­åªç”¨åŽ†å² `modelSrc/modelObjectKey` 判断,å¦åˆ™æ–° 2D è‰ç¨¿ä¼šåœ¨è¯•çŽ©æˆ–æŽ¨èæµä¸­è¢«å½“æˆâ€œæ— ç´ æâ€å¹¶å›žé€€é»˜è®¤ç§¯æœ¨ã€‚`Match3DRuntimeShell` åªä¿ç•™é¡¶éƒ¨è¿”回ã€å€’计时ã€é‡å¼€ä¸‰ä¸ªæŽ§ä»¶ï¼›è¿™äº›é¡¶éƒ¨æŽ§ä»¶å’Œåº•部备选æ ç»Ÿä¸€ä½¿ç”¨é¢˜ææ— å…³çš„åŠé€æ˜ŽçŽ»ç’ƒç»„ä»¶æ ·å¼ï¼Œä¸èƒ½éšèƒŒæ™¯é¢˜ææ”¹æˆæœ¨è´¨ã€é‡‘å±žã€æžœå›­ã€ç§‘幻等主题皮肤,也ä¸èƒ½é‡æ–°çƒ˜è¿› AI 背景图。进度ã€ç»„æ•°ã€ç‰ˆæœ¬ç­‰çжæ€ä¿¡æ¯ä¸å¾—å†ä½œä¸ºé¡¶éƒ¨å¸¸é©» UI 出现,é¿å…鮿Œ¡ç”ŸæˆèƒŒæ™¯å’Œä¸­å¿ƒå®¹å™¨ã€‚ +è¿è¡Œæ€èƒŒæ™¯ä¼˜å…ˆè¯»å– `backgroundImageSrc` / 顶层 `generatedBackgroundAsset.imageSrc/imageObjectKey`,为空时从 `generatedItemAssets[].backgroundAsset.imageSrc/imageObjectKey` 兜底。中心容器优先读å–顶层 `generatedBackgroundAsset.containerImageSrc/containerImageObjectKey`,å†è¯»å– `generatedItemAssets[].backgroundAsset.containerImageSrc/containerImageObjectKey`;为空或æ¢ç­¾/图片加载失败时使用 `public/match3d-background-references/pot-fused-reference.png` ä½œä¸ºé€æ˜Žå®¹å™¨å›¾å…œåº•,ä¸å†å›žé€€åˆ°é»˜è®¤åœ†å½¢é”…壳。容器图æˆåŠŸåŠ è½½åŽï¼Œ`Match3DRuntimeShell` 的棋盘容器必须切æ¢ä¸ºé€æ˜Žã€å¯æº¢å‡ºæ‰¿è½½ï¼Œä¸å†å åŠ é»˜è®¤ `rounded-full` 圆形锅壳ã€é‡‘色边框和默认径å‘背景,é¿å… AI 生æˆçš„大尺寸轻俯视容器被è£åˆ‡æˆ–被默认锅视觉覆盖。移动端棋盘宽度应接近å±å¹•宽度并居中,容器图片å…许略超出棋盘承载盒以ä¿ç•™å¤§å°ºå¯¸æµ…盘轮廓。è¿è¡Œæ€å…¥å£åˆ¤æ–­æ˜¯å¦éœ€è¦è¡¥è¯»ä½œå“详情时,åªèƒ½æŠŠ `imageViews[]` 或 `imageSrc/imageObjectKey` 视为“已有物å“图片素æâ€ï¼›`backgroundMusic.audioSrc`ã€`clickSound.audioSrc`ã€`generatedBackgroundAsset`ã€`backgroundAsset.image*` å’Œ `backgroundAsset.containerImage*` 是éšç‰©å“ç´ æä¸€èµ·ä¼ å…¥çš„附属è¿è¡Œæ€èµ„产,ä¸èƒ½å•ç‹¬è¯æ˜Žç‰©å“ç´ æå·²å®Œæ•´ã€‚也ä¸èƒ½ç»§ç»­åªç”¨åŽ†å² `modelSrc/modelObjectKey` 判断,å¦åˆ™æ–° 2D è‰ç¨¿ä¼šåœ¨è¯•çŽ©æˆ–æŽ¨èæµä¸­è¢«å½“æˆâ€œæ— ç´ æâ€å¹¶å›žé€€é»˜è®¤ç§¯æœ¨ã€‚`Match3DRuntimeShell` åªä¿ç•™é¡¶éƒ¨è¿”回ã€å€’计时ã€é‡å¼€ä¸‰ä¸ªæŽ§ä»¶ï¼›è¿™äº›é¡¶éƒ¨æŽ§ä»¶å’Œåº•部备选æ ç»Ÿä¸€ä½¿ç”¨é¢˜ææ— å…³çš„åŠé€æ˜ŽçŽ»ç’ƒç»„ä»¶æ ·å¼ï¼Œä¸èƒ½éšèƒŒæ™¯é¢˜ææ”¹æˆæœ¨è´¨ã€é‡‘å±žã€æžœå›­ã€ç§‘幻等主题皮肤,也ä¸èƒ½é‡æ–°çƒ˜è¿› AI 背景图。进度ã€ç»„æ•°ã€ç‰ˆæœ¬ç­‰çжæ€ä¿¡æ¯ä¸å¾—å†ä½œä¸ºé¡¶éƒ¨å¸¸é©» UI 出现,é¿å…鮿Œ¡ç”ŸæˆèƒŒæ™¯å’Œä¸­å¿ƒå®¹å™¨ã€‚ å‰ç«¯åŠ è½½è§„åˆ™ï¼š @@ -184,7 +186,7 @@ Match3DWorkProfile / PlatformMatch3DGalleryCard 1. `作å“åç§°` 对应 Match3D `gameName`。 2. `ä½œå“æè¿°` 对应 Match3D `summary`,è‰ç¨¿ç”Ÿæˆé˜¶æ®µç”±åŒä¸€æ¬¡ä½œå“生æˆè®¡åˆ’自动填入。 3. `ä½œå“æ ‡ç­¾` 对应 Match3D `tags`,è‰ç¨¿ç”Ÿæˆé˜¶æ®µåœ¨å†™å…¥åç§°å’Œæè¿°åŽè‡ªåŠ¨è°ƒç”¨æ ‡ç­¾ç”Ÿæˆå™¨å¡«å…¥ï¼›ç»“果页ä»å…è®¸ç”¨æˆ·ç»§ç»­ç¼–è¾‘æˆ–å†æ¬¡ AI 生æˆã€‚ -4. å°é¢å›¾ä¸Žä½œå“åç§°ä¸å†æ‹†æˆå·¦å³ä¸¤ä¸ªå¤§æ¨¡å—ï¼›å°é¢åªä½œä¸ºåŒä¸€ Tab 内的å¯é€‰å…¥å£ï¼Œé¿å…和作å“基础信æ¯å‰²è£‚。旧称“碰é¢å›¾â€ç»Ÿä¸€æ”¹ä¸ºâ€œå°é¢å›¾â€ã€‚è‰ç¨¿ç”Ÿæˆé»˜è®¤ä½¿ç”¨ç”Ÿæˆå‡ºçš„中心容器 UI 图作为 `coverImageSrc`。点击å°é¢å›¾å¿…é¡»å¼¹å‡ºç‹¬ç«‹ç¼–è¾‘é¢æ¿ï¼Œä¸å…许在当å‰ä½œå“ä¿¡æ¯é¢æ¿ä¸‹æ–¹å±•开。å°é¢é¢æ¿å¸ƒå±€å¯¹é½æ‹¼å›¾åˆ›ä½œé¡µä¸Šä¼ å¡ï¼šç§»åŠ¨ç«¯ä¼˜å…ˆï¼Œå·¦ä¾§/上方为方形预览å¡ï¼Œé¢„è§ˆå¡æœ¬èº«å°±æ˜¯ä¸Šä¼ çƒ­åŒºï¼›ä¸Šä¼ å›¾ç‰‡åŽï¼Œé¢„览å¡å†…出现和拼图入å£ä¸€è‡´çš„ `AIé‡ç»˜` å¼€å…³ä¸Žåˆ é™¤æŒ‰é’®ï¼Œé¢æ¿åº•部ä¸å†é¢å¤–展示旧 `AIé‡ç»˜` 选项。已有上传图时,å³ä¾§/下方输入框标题为 `AIé‡ç»˜è¦æ±‚`;关闭 AI é‡ç»˜æ—¶åªæŠŠä¸Šä¼ å›¾ Data URL 写入å°é¢å­—段,ä¸è°ƒç”¨ç”Ÿå›¾æ¨¡åž‹ã€‚没有上传图时,输入框标题为 `å°é¢æè¿°`,å¯é€‰æ‹©å¤šå¼ å‚考图åŽè°ƒç”¨ VectorEngine `gpt-image-2-all` 文生图链路,å‚考图通过请求体 `image` 数组传入;å‚è€ƒå›¾æ¥æºæ”¯æŒç›´æŽ¥å¼•用 `物å“ç´ æ` / `UIç´ æ` 中已有图片,也支æŒè‡ªå®šä¹‰ä¸Šä¼ ã€‚上传图 AI é‡ç»˜ä¸Žæ— ä¸Šä¼ å›¾å¤šå‚考图生æˆéƒ½é€šè¿‡ `api-server` çš„ Match3D 作å“å°é¢ç”ŸæˆæŽ¥å£å®Œæˆï¼Œç”Ÿæˆç»“果转存到 `generated-match3d-assets/{sessionId}/{profileId}/cover/{taskId}/cover.png` åŽå†å†™å›ž `coverImageSrc`。 +4. å°é¢å›¾ä¿®æ”¹å½’å…¥å‘å¸ƒé¢æ¿ï¼Œä¸å†ä½œä¸º `作å“ä¿¡æ¯` Tab 内的独立入å£ã€‚结果页底部 `å‘布` æŒ‰é’®å¯¹é½æ‹¼å›¾ï¼šéžå¿™ç¢Œæ€å§‹ç»ˆå¯ç‚¹å‡»ï¼Œç‚¹å‡»åŽæ‰“开独立 `å‘布抓大鹅作å“` 颿¿ï¼›é¢æ¿ä¸Šæ–¹é›†ä¸­å±•示å‘å¸ƒæ£€æŸ¥å’Œé˜»æ–­é¡¹ï¼Œé¢æ¿å†…承载å°é¢ç¼–辑,满足门槛åŽå†ç‚¹å‡» `å‘布到广场`。旧称“碰é¢å›¾â€ç»Ÿä¸€æ”¹ä¸ºâ€œå°é¢å›¾â€ã€‚è‰ç¨¿ç”Ÿæˆé»˜è®¤ä½¿ç”¨ç”Ÿæˆå‡ºçš„中心容器 UI 图作为 `coverImageSrc`。å°é¢ç¼–è¾‘å¸ƒå±€å¯¹é½æ‹¼å›¾åˆ›ä½œé¡µä¸Šä¼ å¡ï¼šç§»åŠ¨ç«¯ä¼˜å…ˆï¼Œå·¦ä¾§/上方为方形预览å¡ï¼Œé¢„è§ˆå¡æœ¬èº«å°±æ˜¯ä¸Šä¼ çƒ­åŒºï¼›ä¸Šä¼ å›¾ç‰‡åŽï¼Œé¢„览å¡å†…出现和拼图入å£ä¸€è‡´çš„ `AIé‡ç»˜` å¼€å…³ä¸Žåˆ é™¤æŒ‰é’®ï¼Œé¢æ¿åº•部ä¸å†é¢å¤–展示旧 `AIé‡ç»˜` 选项。已有上传图时,å³ä¾§/下方输入框标题为 `AIé‡ç»˜è¦æ±‚`;关闭 AI é‡ç»˜æ—¶åªæŠŠä¸Šä¼ å›¾ Data URL 写入å°é¢å­—段,ä¸è°ƒç”¨ç”Ÿå›¾æ¨¡åž‹ã€‚没有上传图时,输入框标题为 `å°é¢æè¿°`,å¯é€‰æ‹©å¤šå¼ å‚考图åŽè°ƒç”¨ VectorEngine `gpt-image-2-all` 文生图链路,å‚考图通过请求体 `image` 数组传入;å‚è€ƒå›¾æ¥æºæ”¯æŒç›´æŽ¥å¼•用 `物å“ç´ æ` / `UIç´ æ` 中已有图片,也支æŒè‡ªå®šä¹‰ä¸Šä¼ ã€‚上传图 AI é‡ç»˜ä¸Žæ— ä¸Šä¼ å›¾å¤šå‚考图生æˆéƒ½é€šè¿‡ `api-server` çš„ Match3D 作å“å°é¢ç”ŸæˆæŽ¥å£å®Œæˆï¼Œç”Ÿæˆç»“果转存到 `generated-match3d-assets/{sessionId}/{profileId}/cover/{taskId}/cover.png` åŽåªå†™å›ž `coverImageSrc` / `coverAssetId` 相关字段。å°é¢ç”Ÿæˆæˆ–上传åŽå‘å¸ƒé¢æ¿ä¿æŒæ‰“开,方便继续完æˆå‘布。å°é¢ç”ŸæˆæŽ¥å£ä¸å¾—å¤ç”¨è‰ç¨¿ç¼–译æµç¨‹ï¼Œä¸å¾—é‡ç®—题æã€éš¾åº¦ã€æ¶ˆé™¤æ¬¡æ•°æˆ– `generated_item_assets_json`ï¼›å‰ç«¯æ”¶åˆ°å°é¢ç”Ÿæˆå›žåŒ…时也åªèƒ½æŠŠ `coverImageSrc` åˆå¹¶åˆ°å½“å‰ç»“果页 profile,ä¸èƒ½ç”¨å›žåŒ…中的旧 `generatedItemAssets`ã€`clearCount` 或 `difficulty` 覆盖当å‰é¡µé¢çжæ€ã€‚ 结果页 `难度é…ç½®` Tab å–代旧 `玩法é…ç½®`,ä¸å†å±•示旧的分散输入项。该 Tab 顶部使用横å‘离散拖动æ¡è°ƒæ•´éš¾åº¦ï¼Œå››ä¸ªåˆ»åº¦åˆ†åˆ«ä¸º `è½»æ¾ / 标准 / 进阶 / 硬核`;拖动æ¡åªèƒ½è½åœ¨è¿™å››ä¸ªç‚¹ä¸Šï¼Œåˆ»åº¦æ ‡ç­¾å¯ç‚¹å‡»åˆ‡æ¢ã€‚该 Tab 必须与创作入å£é¡µä½¿ç”¨åŒä¸€ç»„难度选项,并统一把原“类型素æå›¾ç‰‡ / 局内类型â€ç­‰å£å¾„归一为 `物å“ç§ç±»`: @@ -211,7 +213,7 @@ Match3DWorkProfile / PlatformMatch3DGalleryCard 2. ç´ æå称输入。 3. ä¸å±•示点击音效æç¤ºè¯è¾“入或点击音效生æˆå…¥å£ã€‚ -五视角预览区采用“上方大预览 + 底部缩略图æ â€çš„å¸ƒå±€ï¼šä¸Šæ–¹æ˜¯æ–¹å½¢ç„¦ç‚¹é¢„è§ˆåŒºï¼Œä¸­é—´æ¨ªå‘æŽ’åˆ—å½“å‰ç‰©å“çš„å„视角图片,并用内框标出当å‰ç„¦ç‚¹ï¼›åº•部缩略图æ å›ºå®šéœ²å‡º `4` 个方形槽ä½ï¼Œå¤šå‡ºçš„第 `5` ä¸ªè§†è§’é€šè¿‡æ¨ªå‘æ»šåŠ¨è®¿é—®ã€‚ç‚¹å‡»ç¼©ç•¥å›¾åªåˆ‡æ¢ç„¦ç‚¹è§†è§’,ä¸åœ¨é¢æ¿å†…新增说明文案或é¢å¤–规则区。 +五视角预览区采用“上方大预览 + 底部缩略图æ â€çš„å¸ƒå±€ï¼šä¸Šæ–¹æ–¹å½¢ç„¦ç‚¹é¢„è§ˆåŒºåªæ˜¾ç¤ºå½“å‰é€‰ä¸­çš„å•张大图,用于详细查看物å“形象,图片在方格内放大显示,ä¸å†æ¸²æŸ“ç´ æè‡ªå¸¦ç¼©ç•¥å›¾æ¡†ã€ç„¦ç‚¹å†…框或横å‘五图带;底部缩略图æ å›ºå®šéœ²å‡º `4` 个方形槽ä½ï¼Œå¤šå‡ºçš„第 `5` ä¸ªè§†è§’é€šè¿‡æ¨ªå‘æ»šåŠ¨è®¿é—®ã€‚ç‚¹å‡»ç¼©ç•¥å›¾åªåˆ‡æ¢ç„¦ç‚¹è§†è§’,ä¸åœ¨é¢æ¿å†…新增说明文案或é¢å¤–规则区。 详情页ä¸å†å±•示å‚考图ã€ç”¨é€”ã€æ¨¡åž‹æç¤ºè¯ã€æ–‡ç”Ÿ/图生切æ¢ã€çŠ¶æ€æŸ¥è¯¢ã€ä¸‹è½½åˆ—表ã€taskUuid 或 subscriptionKey。 diff --git a/docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md b/docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md index 2ac13215..fcaebcf5 100644 --- a/docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md +++ b/docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md @@ -29,7 +29,7 @@ | AIRP | 是 | å¦ | ä¿ç•™å…¥å£ï¼Œæ˜¾ç¤ºæ•¬è¯·æœŸå¾… | | 视觉å°è¯´ | 是 | å¦ | ä¿ç•™å…¥å£ï¼Œæ˜¾ç¤ºæ•¬è¯·æœŸå¾…,暂ä¸å…许创建视觉å°è¯´è‰ç¨¿ | | 智能创作 | å¦ | 是 | å…¥å£éšè—,既有 `creative-agent` 链路ä¿ç•™ | -| 汪汪声浪 | 是 | 是 | `bark-battle` æ­£å¼è½»åˆ›ä½œå…¥å£ï¼Œè¿›å…¥å•页é…置表å•å¹¶å‘布åŽå¯åŠ¨å£°æŽ§å¯¹æˆ˜ runtime | +| 汪汪声浪 | 是 | 是 | `bark-battle` æ­£å¼è½»åˆ›ä½œå…¥å£ï¼Œé€‰ä¸­æ¨¡æ¿åŽç›´æŽ¥åœ¨åˆ›ä½œ Tab 内嵌轻é…置表å•,å‘布åŽå¯åŠ¨å£°æŽ§å¯¹æˆ˜ runtime | | å®è´è¯†ç‰© | 是 | 是 | 寓教于ä¹é¦–关模æ¿ï¼Œå¿…须由 `creation_entry_type_config` 默认ç§å­æˆ–åŽå°å…¥å£å¼€å…³ä¿æŒå­˜åœ¨ | ## æŽ’éšœçº¦æŸ @@ -53,5 +53,6 @@ 3. 未开放玩法点击æ€ä¿æŒç¦ç”¨ï¼Œä¸åº”è¿›å…¥é‰´æƒæˆ–创建会è¯é“¾è·¯ã€‚ 4. 已开放玩法点击åŽå¿…é¡»è¿›å…¥å¯¹åº”åˆ›å»ºé“¾è·¯ï¼›è‹¥ç”¨æˆ·æœªç™»å½•ï¼Œå…ˆèµ°ç™»å½•ä¿æŠ¤ã€‚ 5. 创作 Tab 首å±åº”显示“10分钟创作一个精å“互动玩法â€ï¼Œå¹¶é»˜è®¤å±•示拼图创作表å•。 -6. 智能创作入å£éšè—åŽï¼Œä¸åº”出现“Hi, 朋å‹â€â€œé—®ä¸€é—®é™¶æ³¥å„¿â€æˆ–“一å¥è¯ç”Ÿæˆé—ªåº”用â€ç­‰æ—§é¦–页入å£ã€‚ -7. 方洞挑战入å£éšè—åŽï¼Œä¸åº”出现在创作 Tab 模æ¿å…¥å£ã€åˆ›ä½œä¸­å¿ƒé¡¶éƒ¨å¡å¸¦ã€å¹³å°åˆ›ä½œç±»åž‹å¼¹å±‚å’Œåˆ›ä½œé¡µä½œå“æž¶ä¸­ï¼›æ—¢æœ‰ `SH-` 作å“å·ã€å¹¿åœºè¯¦æƒ…和试玩 runtime 链路ä¸å› æ­¤åˆ é™¤ã€‚ +6. æ±ªæ±ªå£°æµªã€æ‹¼å›¾å’ŒæŠ“大鹅都应在创作 Tab çš„æ¨¡æ¿æ¡ä¸‹æ–¹å†…嵌展示对应创作表å•;汪汪声浪ä¸å¾—å†è·³åˆ°ç‹¬ç«‹é…置阶段或弹出整页é…置页é¢ã€‚ +7. 智能创作入å£éšè—åŽï¼Œä¸åº”出现“Hi, 朋å‹â€â€œé—®ä¸€é—®é™¶æ³¥å„¿â€æˆ–“一å¥è¯ç”Ÿæˆé—ªåº”用â€ç­‰æ—§é¦–页入å£ã€‚ +8. 方洞挑战入å£éšè—åŽï¼Œä¸åº”出现在创作 Tab 模æ¿å…¥å£ã€åˆ›ä½œä¸­å¿ƒé¡¶éƒ¨å¡å¸¦ã€å¹³å°åˆ›ä½œç±»åž‹å¼¹å±‚å’Œåˆ›ä½œé¡µä½œå“æž¶ä¸­ï¼›æ—¢æœ‰ `SH-` 作å“å·ã€å¹¿åœºè¯¦æƒ…和试玩 runtime 链路ä¸å› æ­¤åˆ é™¤ã€‚ diff --git a/docs/technical/PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md b/docs/technical/PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md index 946e2957..6044edd4 100644 --- a/docs/technical/PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md +++ b/docs/technical/PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md @@ -11,6 +11,7 @@ 3. 推è页è¿è¡Œæ€ç”»é¢ä¿æŒç‹¬ç«‹å¯äº¤äº’åŒºåŸŸï¼Œä¸æŒ‚å¹³å°åˆ‡æ¢ä½œå“çš„ pointer 手势。 4. 切æ¢ä½œå“çš„çºµå‘æ‰‹åŠ¿åªç»‘定在å¡ç‰‡åº•部作å“ä¿¡æ¯åŒºï¼›åº•部信æ¯åŒºå¯ä»¥æ‰©å¤§è§¦æŽ§é«˜åº¦ï¼Œä½†ä¸å¾—ç»å¯¹å®šä½è¦†ç›–è¿è¡Œæ€ç”»é¢ã€‚ 5. 点赞ã€åˆ†äº«ã€æ”¹é€ æŒ‰é’®ç»§ç»­é˜»æ­¢ pointer 事件冒泡,é¿å…按钮点击误触å‘切æ¢ä½œå“。 +6. 作å“ä¿¡æ¯åŒºé»˜è®¤åªä¿ç•™ç´§å‡‘一行身份和一组æ“作按钮,底部热区ä¸å†å ç”¨è¿‡é«˜å›ºå®šé«˜åº¦ï¼Œé¿å…挤压è¿è¡Œæ€ç”»é¢ã€‚ ## 验收 @@ -19,3 +20,4 @@ 3. 在作å“è¿è¡Œæ€ç”»é¢å†…ç‚¹å‡»ã€æ‹–拽或滑动,åªè§¦å‘作å“自身交互。 4. 在底部作å“ä¿¡æ¯åŒºä¸Šä¸‹æ»‘动,å¯ä»¥åˆ‡æ¢æŽ¨è作å“。 5. 点赞ã€åˆ†äº«ã€æ”¹é€ æŒ‰é’®å¯æ­£å¸¸ç‚¹å‡»ï¼Œä¸è§¦å‘作å“切æ¢ã€‚ +6. 推èå¡ç‰‡çš„视觉主体高度应明显高于底部信æ¯åŒºï¼Œä¸”ä¿¡æ¯åŒºä¸åº”明显压缩首å±è¿è¡Œæ€ã€‚ diff --git a/docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md b/docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md index 4edff6ab..c0362272 100644 --- a/docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md +++ b/docs/technical/PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md @@ -66,7 +66,7 @@ ```text VECTOR_ENGINE_BASE_URL="https://api.vectorengine.ai" VECTOR_ENGINE_API_KEY="YOUR_VECTOR_ENGINE_API_KEY" -VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000 +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 ``` `VECTOR_ENGINE_API_KEY` åªèƒ½å­˜åœ¨äºŽæœ¬åœ°æˆ–部署环境,ä¸å†™å…¥ Git 跟踪文件。若缺少 key,åŽç«¯è¿”回æœåŠ¡ä¸å¯ç”¨é”™è¯¯ï¼Œå‰ç«¯å±•ç¤ºçŽ°æœ‰é”™è¯¯é¢æ¿ã€‚ diff --git a/docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md b/docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md index 41173f5a..7a3e1c2c 100644 --- a/docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md +++ b/docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md @@ -4,7 +4,7 @@ 拼图创作入å£ä¸å†ä½¿ç”¨ Agent å¯¹è¯æ”¶é›†é¢˜æé”šç‚¹ã€‚æ–°æµç¨‹è®©çŽ©å®¶å¡«å†™ä½œå“åç§°ã€ä½œå“æè¿°ã€ç”»é¢æè¿°ä¸‰ç±»ä¿¡æ¯ï¼Œå…¶ä¸­ç”»é¢æè¿°åªæœåŠ¡é¦–å…³ç”»é¢ç”Ÿæˆä¸Žå…³å¡ç”»é¢è¯­ä¹‰ï¼Œä¸å†ä½œä¸ºä½œå“è¯¦æƒ…é¡µçš„ä½œå“æè¿°ã€‚ç”»é¢æè¿°æ”¯æŒä¸Šä¼ å‚考图。玩家确认åŽç›´æŽ¥è¿›å…¥è‰ç¨¿ç”Ÿæˆè¿›åº¦é¡µï¼ŒåŽç»­è‰ç¨¿ç”Ÿæˆã€é¦–图生æˆã€æ­£å¼å›¾é€‰æ‹©ã€ç»“果页编辑和å‘布沿用现有åŽç«¯ç¼–排。 -2026-05-03 åŽå…¥å£è¿›ä¸€æ­¥æ”¶å£ä¸ºç”»é¢æè¿°ç›´åˆ›ï¼šå…¥å£è¡¨å•åªä¿ç•™ç”»é¢æè¿°ã€å‚考图和图片模型选择;作å“åç§°ã€ä½œå“æè¿°ã€ä½œå“标签全部进入结果页补全。若本文件早期段è½ä»æåˆ°å…¥å£å¿…填作å“åç§°æˆ–ä½œå“æè¿°ï¼Œä»¥ `PUZZLE_PICTURE_ONLY_CREATION_AND_AI_TAGS_2026-05-03.md` 为准。 +2026-05-03 åŽå…¥å£è¿›ä¸€æ­¥æ”¶å£ä¸ºç”»é¢æè¿°ç›´åˆ›ï¼šå…¥å£è¡¨å•åªä¿ç•™ç”»é¢æè¿°ã€å‚考图和图片模型选择;作å“åç§°ã€ä½œå“æè¿°ã€ä½œå“标签由è‰ç¨¿ç”Ÿæˆé˜¶æ®µé»˜è®¤è¡¥é½ï¼Œå¹¶ç»§ç»­å…许玩家在结果页编辑。若本文件早期段è½ä»æåˆ°å…¥å£å¿…填作å“åç§°æˆ–ä½œå“æè¿°ï¼Œä»¥ `PUZZLE_PICTURE_ONLY_CREATION_AND_AI_TAGS_2026-05-03.md` 为准。 ## 首访新手引导éšè— @@ -18,9 +18,13 @@ 1. å…¥å£è¡¨å•åªå±•示 `ç”»é¢æè¿°`ã€å‚考图和图片模型选择;`ç”»é¢æè¿°` 是唯一必填字段。 2. 表å•自动ä¿å­˜åªä¿å­˜ `pictureDescription`,ä¸å†ä¿å­˜å…¥å£ä½œå“åç§°ã€ä½œå“æè¿°æˆ–推断标签。 -3. 点击“生æˆè‰ç¨¿â€åŽè¿›å…¥ç”Ÿæˆè¿›åº¦é¡µï¼Œæ­¥éª¤å›ºå®šå¯¹é½åŽç«¯å½“å‰ç¼–排:“编译首关è‰ç¨¿ -> 生æˆå…³å¡å称与 UI 背景æç¤ºè¯ / 生æˆé¦–å…³ç”»é¢ -> 生æˆUI背景 -> 写入正å¼è‰ç¨¿â€ã€‚其中关å¡å称文本生æˆã€UI 背景æç¤ºè¯ç”Ÿæˆä¸Žé¦–关画é¢ç”Ÿæˆå¯å¹¶è¡Œï¼›é¦–关最终å称确定åŽç”Ÿæˆ UI 背景。背景音ä¹ç”Ÿæˆå·²äºŽ 2026-05-14 临时关闭。 +3. 点击“生æˆè‰ç¨¿â€åŽè¿›å…¥ç”Ÿæˆè¿›åº¦é¡µï¼Œæ­¥éª¤å›ºå®šå¯¹é½åŽç«¯å½“å‰ç¼–排:“编译首关è‰ç¨¿ -> 生æˆå…³å¡åç§°ã€ä½œå“æè¿°ã€6 ä¸ªä½œå“æ ‡ç­¾ä¸Ž UI 背景æç¤ºè¯ / 生æˆé¦–å…³ç”»é¢ -> 生æˆUI背景 -> 写入正å¼è‰ç¨¿â€ã€‚其中关å¡å称文本生æˆã€ä½œå“元信æ¯ç”Ÿæˆã€UI 背景æç¤ºè¯ç”Ÿæˆä¸Žé¦–关画é¢ç”Ÿæˆå¯å¹¶è¡Œï¼›é¦–关最终å称确定åŽç”Ÿæˆ UI 背景。背景音ä¹ç”Ÿæˆå·²äºŽ 2026-05-14 临时关闭。 4. 生æˆè¿›åº¦é¡µâ€œå½“剿‹¼å›¾ä¿¡æ¯â€åªå±•ç¤ºç”»é¢æè¿°ï¼›ä¸å¾—展示空作å“åç§°ã€ç©ºä½œå“æè¿°æˆ–旧五锚点结构。 -5. 结果页打开åŽï¼Œä½œå“å称默认使用首关åç§°ï¼Œä½œå“æè¿°ä¸Žä½œå“æ ‡ç­¾ä¿æŒä¸ºç©ºï¼Œç­‰å¾…用户在作å“ä¿¡æ¯ Tab è¡¥å…¨æˆ–è§¦å‘ AI 标签生æˆã€‚ +5. 生æˆè¿›åº¦é¡µä¸å±•ç¤ºâ€œå½“å‰æ‰¹æ¬¡â€æ¨¡å—;移动端åªä¿ç•™â€œé¢„计等待â€å’Œâ€œè®¡æ—¶â€ä¸¤å¼ çжæ€å¡å¹¶æŽ’展示,步骤å¡è¿›å…¥é¡µé¢æ—¶æŒ‰é¡ºåºä»Žå±å¹•左侧滑入。 +6. 结果页打开åŽï¼Œä½œå“å称默认使用首关åç§°ï¼Œä½œå“æè¿°é»˜è®¤ä½¿ç”¨é¦–å…³å‘½å请求返回的 `workDescription`ï¼Œä½œå“æ ‡ç­¾é»˜è®¤ä½¿ç”¨åŒæ¬¡è¯·æ±‚返回的 6 个 `workTags`。若模型ä¸å¯ç”¨æˆ–è¿”å›žéžæ³•字段,æ‰ä¿ç•™ç©ºæè¿° / 空标签,等待用户在作å“ä¿¡æ¯ Tab è¡¥å…¨æˆ–è§¦å‘ AI 标签生æˆã€‚ +7. å…¥å£å­˜åœ¨ä¸¤ç±»å›¾ç‰‡è¾“入:左侧 `拼图画é¢` 是主图上传入å£ï¼Œä»æŒ‰çŽ°æœ‰æ­£æ–¹å½¢è£å‰ªå’Œ `aiRedraw` é€»è¾‘ç”Ÿæˆæˆ–直接使用;当未上传主图时,`ç”»é¢æè¿°` 输入框å³ä¸‹è§’展示 `上传å‚考图` icon,å¯ä¸€æ¬¡é€‰æ‹©å¤šå¼ å‚考图,最多 5 张。 +8. å‚考图上传åŽåªä»¥å°ç¼©ç•¥å›¾å±•ç¤ºåœ¨ç”»é¢æè¿°æ¡†ä¸‹æ–¹ï¼Œç‚¹å‡»ç¼©ç•¥å›¾æ”¾å¤§é¢„è§ˆï¼›æ¯å¼ ç¼©ç•¥å›¾å¯å•独移除。上传主图åŽéšè—这组æè¿°å‚考图,é¿å…主图与å‚考图èŒè´£æ··æ·†ã€‚ +9. å‰ç«¯æäº¤ `referenceImageSrcs` 数组;api-server 兼容旧 `referenceImageSrc` å•图字段,并把旧å•图和新数组去é‡åŽæœ€å¤šä¿ç•™ 5 å¼ ã€‚å½“å‰æ‹¼å›¾ VectorEngine 图生图链路ä»åªä½¿ç”¨ç¬¬ä¸€å¼ æœ‰æ•ˆå‚考图作为实际编辑å‚考图,数组字段用于入å£ä½“验和åŽç»­å¤šå‚考图生æˆèƒ½åŠ›æ‰©å±•ã€‚ ### 2026-04-30 åˆå§‹è¡¨å•è‰ç¨¿ä¿å­˜è¡¥å…… @@ -90,18 +94,19 @@ 12. `compile_puzzle_draft` 中的图片上游失败ä¸å¾—æ˜ å°„æˆ `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时,api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中ä¿ç•™â€œæ‹¼å›¾å›¾ç‰‡ç”Ÿæˆå¤±è´¥ï¼š...â€çš„业务原因,é¿å…生æˆé¡µåªæ˜¾ç¤ºâ€œè¯·æ±‚傿•°ä¸åˆæ³•â€ã€‚ 13. `compile_puzzle_draft` å‰ç½®æ³¥ç‚¹é¢„扣失败ä¸å¾—æ˜ å°„æˆ `400 BAD_REQUEST`。余é¢ä¸è¶³è¿”回 `409 CONFLICT`,SpacetimeDB procedure ä¸å¯ç”¨ã€ç»‘定ä¸åŒ¹é…ã€é’±åŒ…æœåŠ¡å¼‚å¸¸ç­‰ç»Ÿä¸€æŒ‰ `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中ä¿ç•™çœŸå®žé’±åŒ…错误。 14. ç”Ÿæˆæ‹¼å›¾ä½œå“è‰ç¨¿åŠ¨ä½œæ¶‰åŠçš„è¡¨å• seed prompt 与首图 prompt æ¥æºé€‰æ‹©ç»Ÿä¸€æ”¶å£åœ¨ `server-rs/crates/api-server/src/prompt/puzzle/draft.rs`ï¼›`puzzle.rs` åªè´Ÿè´£è°ƒç”¨ SpacetimeDBã€è®¡è´¹ã€å›¾ç‰‡æœåŠ¡å’ŒæŒä¹…化,ä¸å†ç›´æŽ¥æ‹¼è‰ç¨¿ prompt 文本。 -15. `compile_puzzle_draft_with_initial_cover` 中,首关文本å称生æˆä¸Žé¦–关图片生æˆäº’ä¸ç­‰å¾…:首图 prompt åªè¯»å–ç”»é¢æè¿°ï¼ŒOSS ä¸´æ—¶è·¯å¾„ä½¿ç”¨å·²æœ‰åæˆ–确定性兜底åï¼›åŒä¸€æ¬¡é¦–关命å LLM 请求必须返回 `levelName` 与 `uiBackgroundPrompt`,首图返回åŽå†ç”¨å›¾ç‰‡è¯­ä¹‰å°è¯•精修最终关å¡å与 UI 背景æç¤ºè¯ã€‚最终关å¡å确定åŽï¼Œå¿…须继续用 AI 返回的 `uiBackgroundPrompt` 生æˆé¦–å…³ UI èƒŒæ™¯å›¾ï¼›è‹¥å‘½åæ¨¡åž‹æœªè¿”回å¯ç”¨æç¤ºè¯ï¼Œæ‰æŒ‰ä½œå“ã€å…³å¡å’Œæ ‡ç­¾æ‹¼æŽ¥ç¡®å®šæ€§å…œåº•æç¤ºè¯ã€‚写入正å¼è‰ç¨¿å‰æ ¡éªŒ `levels[0].uiBackgroundImageSrc/uiBackgroundImageObjectKey` ä¸ä¸ºç©ºï¼›UI 背景失败时 `compile_puzzle_draft` 返回上游错误,生æˆé¡µåœç•™å¤±è´¥æ€ã€‚背景音ä¹ç”Ÿæˆä¸´æ—¶å…³é—­ï¼Œä¸å†ä½œä¸ºè‰ç¨¿å®Œæˆé—¨æ§›ã€‚ +15. `compile_puzzle_draft_with_initial_cover` 中,首关文本å称生æˆä¸Žé¦–关图片生æˆäº’ä¸ç­‰å¾…:首图 prompt åªè¯»å–ç”»é¢æè¿°ï¼ŒOSS ä¸´æ—¶è·¯å¾„ä½¿ç”¨å·²æœ‰åæˆ–确定性兜底åï¼›åŒä¸€æ¬¡é¦–关命å LLM 请求必须返回 `levelName`ã€`workDescription`ã€6 个 `workTags` 与 `uiBackgroundPrompt`,首图返回åŽå†ç”¨å›¾ç‰‡è¯­ä¹‰å°è¯•精修最终关å¡åã€ä½œå“元信æ¯ä¸Ž UI 背景æç¤ºè¯ã€‚è§£æžå±‚å¿…é¡»æ‹’ç» `levelNam`ã€`levelName` 这类字段å片段被当æˆå…³å¡å。最终关å¡å确定åŽï¼Œå¿…须继续用 AI 返回的 `uiBackgroundPrompt` 生æˆé¦–å…³ UI èƒŒæ™¯å›¾ï¼›è‹¥å‘½åæ¨¡åž‹æœªè¿”回å¯ç”¨æç¤ºè¯ï¼Œæ‰æŒ‰ä½œå“ã€å…³å¡å’Œæ ‡ç­¾æ‹¼æŽ¥ç¡®å®šæ€§å…œåº•æç¤ºè¯ã€‚写入正å¼è‰ç¨¿å‰æ ¡éªŒ `levels[0].uiBackgroundImageSrc/uiBackgroundImageObjectKey` ä¸ä¸ºç©ºï¼›UI 背景失败时 `compile_puzzle_draft` 返回上游错误,生æˆé¡µåœç•™å¤±è´¥æ€ã€‚背景音ä¹ç”Ÿæˆä¸´æ—¶å…³é—­ï¼Œä¸å†ä½œä¸ºè‰ç¨¿å®Œæˆé—¨æ§›ã€‚ 16. `compile_puzzle_draft_with_uploaded_cover` 中,上传图解æžåŽï¼Œæ–‡æœ¬å称生æˆã€å›¾ç‰‡è¯­ä¹‰å称生æˆå’Œä¸Šä¼ å›¾è½¬å­˜ OSS å¯å¹¶è¡Œï¼›ä¸Šä¼ å›¾è½¬å­˜å¤±è´¥å¿…须立å³è¿”回,ä¸å¾—ç»§ç»­è§¦å‘ UI 背景生æˆã€‚上传图转存æˆåŠŸä¸”æœ€ç»ˆå…³å¡å确定åŽï¼ŒåŒæ ·å¿…须生æˆå¹¶æ ¡éªŒé¦–å…³ UI 背景图。自动è‰ç¨¿é˜¶æ®µä¸å†è§¦å‘音ä¹èµ„产生æˆã€‚ +17. `CreatePuzzleAgentSessionRequest` 与 `ExecutePuzzleAgentActionRequest` 兼容新增 `referenceImageSrcs: string[]`。`aiRedraw = true` 且未上传主图时,api-server 从 `referenceImageSrc` 与 `referenceImageSrcs` 中按顺åºå–第一张有效å‚考图进入现有图生图分支;`aiRedraw = false` æ—¶ä»å¿…é¡»ä¾èµ– `referenceImageSrc` 主图,ä¸èƒ½ç”¨æè¿°å‚考图绕过主图上传。 ## 结果页 拼图è‰ç¨¿ç»“果页分为三个一级 Tab: -1. 拼图关å¡åˆ—表:默认展示è‰ç¨¿ç”Ÿæˆå‡ºçš„第一关。列表项å‚考 RPG è‰ç¨¿å¡ç‰‡æ ·å¼ï¼Œæ˜¾ç¤ºç”»é¢å›¾ã€å…³å¡å称和轻é‡çжæ€ã€‚æ”¯æŒæ–°å¢žå…³å¡ã€åˆ é™¤å…³å¡ã€‚点击列表项进入独立关å¡è¯¦æƒ…页,ä¸åœ¨åˆ—表项下方展开。关å¡è¯¦æƒ…页å¯ç¼–辑关å¡åç§°ã€ç”»é¢æè¿°ã€ç”Ÿæˆæˆ–釿–°ç”Ÿæˆç”»é¢ï¼Œå¹¶åœ¨å·²æœ‰æ­£å¼å›¾åŽæ”¯æŒå…³å¡æµ‹è¯•。 -2. 作å“ä¿¡æ¯ï¼šå±•示并编辑作å“åç§°ã€ä½œå“æè¿°ã€ä½œå“标签。 +1. 作å“ä¿¡æ¯ï¼šé»˜è®¤æ‰“开,展示并编辑作å“åç§°ã€ä½œå“æè¿°ã€ä½œå“标签。 +2. 拼图关å¡åˆ—表:展示è‰ç¨¿ç”Ÿæˆå‡ºçš„第一关。列表项å‚考 RPG è‰ç¨¿å¡ç‰‡æ ·å¼ï¼Œæ˜¾ç¤ºç”»é¢å›¾ã€å…³å¡å称和轻é‡çжæ€ã€‚æ”¯æŒæ–°å¢žå…³å¡ã€åˆ é™¤å…³å¡ã€‚点击列表项进入独立关å¡è¯¦æƒ…页,ä¸åœ¨åˆ—表项下方展开。关å¡è¯¦æƒ…页å¯ç¼–辑关å¡åç§°ã€ç”»é¢æè¿°ã€ç”Ÿæˆæˆ–釿–°ç”Ÿæˆç”»é¢ï¼Œå¹¶åœ¨å·²æœ‰æ­£å¼å›¾åŽæ”¯æŒå…³å¡æµ‹è¯•。 3. ç´ æé…ç½®ï¼šå¯¹é½æŠ“å¤§é¹…è‰ç¨¿é¡µç»“构,当å‰åªåŒ…å« `UI` å­ Tabï¼›`背景音ä¹` å­ Tab 已临时éšè—。 -`ç´ æé…ç½® > UI` 展示并编辑拼图è¿è¡Œæ€ UI 背景æç¤ºè¯ã€‚`compile_puzzle_draft` è‰ç¨¿ç¼–译完æˆé¦–图åŽï¼Œ`api-server` 会优先使用首关命å LLM åŒæ¬¡è¿”回的 `uiBackgroundPrompt` 自动生æˆé¦–å…³ 9:16 çº¯èƒŒæ™¯å›¾ï¼›åªæœ‰æ¨¡åž‹æœªè¿”回å¯ç”¨æç¤ºè¯æ—¶ï¼Œæ‰åŸºäºŽä½œå“åç§°ã€ä½œå“æè¿°ã€æ ‡ç­¾å’Œé¦–å…³ä¿¡æ¯æ‹¼æŽ¥å…œåº•æç¤ºè¯ã€‚结果页继续支æŒç”¨æˆ·ä¿®æ”¹æç¤ºè¯å¹¶é€šè¿‡ `generate_puzzle_ui_background` 釿–°ç”Ÿæˆã€‚图片生æˆè°ƒç”¨ VectorEngine `gpt-image-2-all` çš„ `9:16` 图片生æˆé“¾è·¯ã€‚生æˆç»“果写入首关 `levels_json` çš„ `uiBackgroundPrompt`ã€`uiBackgroundImageSrc`ã€`uiBackgroundImageObjectKey`ï¼Œä¸æ–°å¢ž SpacetimeDB 表字段。 +`ç´ æé…ç½® > UI` 展示并编辑拼图è¿è¡Œæ€ UI 背景æç¤ºè¯ã€‚`compile_puzzle_draft` è‰ç¨¿ç¼–译完æˆé¦–图åŽï¼Œ`api-server` 会优先使用首关命å LLM åŒæ¬¡è¿”回的 `uiBackgroundPrompt` 自动生æˆé¦–å…³ 9:16 çº¯èƒŒæ™¯å›¾ï¼Œå¹¶ç”¨åŒæ¬¡è¿”回的 `workDescription` 与 `workTags` 默认填充作å“ä¿¡æ¯ã€‚åªæœ‰æ¨¡åž‹æœªè¿”回å¯ç”¨æç¤ºè¯æ—¶ï¼Œæ‰åŸºäºŽä½œå“åç§°ã€ä½œå“æè¿°ã€æ ‡ç­¾å’Œé¦–å…³ä¿¡æ¯æ‹¼æŽ¥å…œåº•æç¤ºè¯ã€‚结果页继续支æŒç”¨æˆ·ä¿®æ”¹æç¤ºè¯å¹¶é€šè¿‡ `generate_puzzle_ui_background` 釿–°ç”Ÿæˆã€‚图片生æˆè°ƒç”¨ VectorEngine `gpt-image-2-all` çš„ `9:16` 图片生æˆé“¾è·¯ã€‚生æˆç»“果写入首关 `levels_json` çš„ `uiBackgroundPrompt`ã€`uiBackgroundImageSrc`ã€`uiBackgroundImageObjectKey`ï¼Œä¸æ–°å¢ž SpacetimeDB 表字段。 åŽ†å² `levels_json[0].backgroundMusic` 字段继续兼容读å–å’Œè¿è¡Œæ€æ’­æ”¾ï¼Œä½†ç»“æžœé¡µæš‚ä¸æä¾›ç¼–è¾‘æˆ–ç”Ÿæˆå…¥å£ã€‚拼图结果页ä¸å†ä¿ç•™ä¸€çº§ `UI` 或一级 `音ä¹` Tab。 @@ -125,6 +130,12 @@ 3. 自动试玩åªåœ¨å½“å‰ä»å¤„于 `puzzle-generating` 时触å‘;若玩家已返回è‰ç¨¿ Tab 或切到其它页é¢ï¼ŒåŽå°ç”Ÿæˆå®Œæˆåªæ ‡è®°è‰ç¨¿å·²ç”Ÿæˆï¼Œä¸å¾—强行抢å±è¿›å…¥è¯•玩。 4. 若自动å¯åŠ¨è¯•çŽ©å¤±è´¥ï¼Œå‰ç«¯ä¿ç•™è‰ç¨¿ç»“果页作为兜底查看入å£ï¼Œå¹¶å±•示已有错误æ€ï¼Œä¸åº”丢失已生æˆè‰ç¨¿ã€‚ +### 2026-05-14 è¿è¡Œæ€åˆå¹¶å—拖拽补充 + +1. åˆå¹¶å—拖拽时,真实视觉åªç”± `mergedGroups` 生æˆçš„æ•´ä½“ç»å¯¹å±‚承载;棋盘格里的å•å— DOM åªä½œä¸ºé€æ˜Žå®šä½å ä½ã€‚ +2. åˆå¹¶æ ¼å³ä½¿åœ¨ `pointerdown` åŽåŒæ­¥å†™å…¥äº† `selectedPieceId`,也ä¸å¾—应用å•å—选中填充色,å¦åˆ™æ•´ä½“åˆå¹¶å—被拖起åŽä¼šåœ¨åŽŸä½ç½®éœ²å‡ºç²‰çº¢ / 红色底å—。 +3. å•å—æœªåˆå¹¶æ—¶ä»ä¿ç•™é€‰ä¸­æ€ï¼›åˆå¹¶æ ¼æ ·å¼ä¼˜å…ˆçº§å¿…须高于选中æ€ï¼Œå›žå½’测试覆盖 `拖拽åˆå¹¶å¤§å—æ—¶åº•å±‚å•æ ¼ä¸æ˜¾ç¤ºé€‰ä¸­è‰²å—`。 + ### 2026-04-30 å…³å¡åˆ—表å¡ç‰‡äº¤äº’补充 1. å…³å¡åˆ—表å¡ç‰‡çš„删除按钮与关å¡å称放在åŒä¸€ä¿¡æ¯è¡Œï¼ŒæŒ‰é’®å›ºå®šåœ¨å¡ç‰‡å³ä¸‹è§’ï¼›ä¸å¾—å†å•独å ç”¨ä¸€æ•´æ¡åº•部分隔æ ã€‚ @@ -161,7 +172,7 @@ 1. 从拼图创作入å£åªèƒ½çœ‹åˆ°ä½œå“åç§°ã€ä½œå“æè¿°ã€ç”»é¢æè¿°å’Œå‚考图上传,ä¸å‡ºçް Agent èŠå¤©è¾“å…¥ã€è¡¥é½è®¾å®šã€é”šç‚¹é—®ç­”。 2. 点击确认åŽè¿›å…¥æ‹¼å›¾è‰ç¨¿ç”Ÿæˆè¿›åº¦é¡µï¼Œå¹¶è‡ªåŠ¨å®Œæˆè‰ç¨¿ç¼–译ã€é¦–图生æˆã€æ­£å¼å›¾é€‰æ‹©å’Œé¦–å…³ UI 背景图生æˆã€‚ 3. 首图生æˆè¯·æ±‚ä½¿ç”¨çŽ©å®¶ç”»é¢æè¿°ä½œä¸º prompt;上传å‚考图时走图生图;作å“è¯¦æƒ…é¡µå±•ç¤ºçŽ©å®¶ä½œå“æè¿°ã€‚ -4. 结果页包å«â€œæ‹¼å›¾å…³å¡â€â€œä½œå“ä¿¡æ¯â€â€œç´ æé…ç½®â€ä¸‰ä¸ªä¸€çº§ Tabï¼›`ç´ æé…ç½®` 内当å‰åªåŒ…å« `UI` å­ Tab,ä¸å±•示背景音ä¹ç”Ÿæˆå…¥å£ã€‚å…³å¡åˆ—è¡¨é»˜è®¤è‡³å°‘ä¸€å…³ï¼Œæ”¯æŒæ–°å¢žã€åˆ é™¤å’Œè¿›å…¥å…³å¡è¯¦æƒ…。 +4. 结果页一级 Tab 顺åºä¸ºâ€œä½œå“ä¿¡æ¯â€â€œæ‹¼å›¾å…³å¡â€â€œç´ æé…ç½®â€ï¼Œé»˜è®¤æ‰“开“作å“ä¿¡æ¯â€ï¼›`ç´ æé…ç½®` 内当å‰åªåŒ…å« `UI` å­ Tab,ä¸å±•示背景音ä¹ç”Ÿæˆå…¥å£ã€‚å…³å¡åˆ—è¡¨é»˜è®¤è‡³å°‘ä¸€å…³ï¼Œæ”¯æŒæ–°å¢žã€åˆ é™¤å’Œè¿›å…¥å…³å¡è¯¦æƒ…。 5. å…³å¡è¯¦æƒ…页支æŒç”Ÿæˆæˆ–釿–°ç”Ÿæˆç”»é¢ï¼›å·²æœ‰æ­£å¼å›¾åŽæ˜¾ç¤ºå¸åº•â€œå…³å¡æµ‹è¯•â€å…¥å£ã€‚ 6. å‘布ã€ä½œå“测试ã€è‡ªåЍä¿å­˜ä½œå“åç§°ã€ä½œå“æè¿°ã€ä½œå“标签和关å¡åˆ—表ä»å¯ç”¨ã€‚ 7. è‰ç¨¿åˆæ¬¡ç”ŸæˆåŽé¦–关默认带 `uiBackgroundImageSrc`;若åŽç«¯åªè¿”回 `uiBackgroundImageObjectKey` 也必须能在结果页ã€è¯•玩和è¿è¡Œæ€æ­£å¸¸é¢„览;UI Tab å¯ä¿®æ”¹æç¤ºè¯å¹¶é‡æ–°ç”ŸæˆèƒŒæ™¯å›¾ï¼›ç”ŸæˆåŽè¿è¡Œæ€åº”显示 `uiBackgroundImageSrc` 或æ¢ç­¾åŽçš„ `uiBackgroundImageObjectKey`,拼图槽ä½å’Œæ£‹ç›˜è¾¹ç•Œä»ç”±é»˜è®¤è¿è¡Œæ€æ ·å¼ç»˜åˆ¶ã€‚ diff --git a/docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md b/docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md index 714bc2d3..039292c7 100644 --- a/docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md +++ b/docs/technical/PUZZLE_IMAGE_ASSET_PROXY_FIX_2026-04-27.md @@ -2,7 +2,7 @@ 日期:`2026-04-27` -更新:`2026-05-08` +更新:`2026-05-14` ## 背景 @@ -20,6 +20,7 @@ 4. `refreshKey` åªèƒ½ç”¨äºŽè·³è¿‡å‰ç«¯ç­¾å URL ç¼“å­˜å¹¶é‡æ–°è¯·æ±‚ `/api/assets/read-url`,ä¸èƒ½å†ç»™ OSS V4 ç­¾å URL 追加 `_v` ç­‰é¢å¤– queryï¼›OSS 会把 query 纳入签å,é¢å¤–傿•°ä¼šè®©æ–°ç”Ÿæˆå›¾å˜æˆ 403/破图。 5. 历å²ç´ æè¢«é€‰ä¸ºå‚考图åŽï¼Œå‚考图å°é¢„览也必须走 `ResolvedAssetImage`,ä¸èƒ½ä½¿ç”¨è£¸ ``。 6. ç¦æ­¢æ¢å¤ `/generated-puzzle-assets/{*path}` Axum 路由或 Vite 直读代ç†ï¼›æ­£å¼è¯»å–统一走 `/api/assets/read-url`。 +7. 历å²ç´ æå¡ç‰‡çš„æ ‡é¢˜å’Œé€‰ä¸­æ ‡ç­¾åªä»Ž `imageSrc` è§£æžå›¾ç‰‡æ–‡ä»¶åï¼›`ownerLabel` 是账å·å½’属信æ¯ï¼Œä¸èƒ½ä½œä¸ºå›¾ç‰‡å展示。`createdAt` 需è¦å…¼å®¹ `1713686400.000000Z` 这类 SpacetimeDB 时间字符串。 ## åŽç»­çº¦æŸ diff --git a/docs/technical/README.md b/docs/technical/README.md index db0b8520..6170c69e 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -4,12 +4,14 @@ ## 文档列表 +- [ã€å‰ç«¯ä½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md](./ã€å‰ç«¯ä½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md):冻结创作页统一图åƒè¾“入颿¿ `CreativeImageInputPanel` çš„å—æŽ§è¾¹ç•Œã€ä¸»å›¾ä¸Šä¼ ã€ç”»é¢æè¿°ã€å¤šå‚考图ã€AI é‡ç»˜å¼€å…³ã€é¢„览和æäº¤å£å¾„ï¼Œä¾›æ‹¼å›¾ã€æŠ“å¤§é¹…å°é¢å’ŒåŽç»­åˆ›ä½œé¡µå¤ç”¨ã€‚ - [WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md](./WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md):记录微信å°ç¨‹åº `web-view` å£³çš„æœ€å°æŽ¥å…¥èŒƒå›´ã€éœ€è¦å¡«å†™çš„ H5 业务域åã€å¾®ä¿¡åŽå°é…ç½®ã€`npm run check:wechat-miniprogram-auth` å¯é‡å¤ç™»å½•链路 smoke å’ŒåŽç»­åŽŸç”ŸåŒ–è¾¹ç•Œã€‚ - [BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md](./BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md)ï¼šå†»ç»“å¯“æ•™äºŽä¹ `å®è´çˆ±ç”»` 独立本地 Demo è¿è¡Œæ€å®žçŽ°æ–¹æ¡ˆï¼Œæ˜Žç¡®å‘现页默认å¡ç‰‡ã€`/runtime/baby-love-drawing` 路由ã€ç”»æ¿äº¤äº’ã€mocap/é”®é¼ è°ƒè¯•æ˜ å°„ã€æœ¬åœ°ä¿å­˜å’Œ VectorEngine image-2 绘画魔法åŽç«¯ä»£ç†ã€‚ - [BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battleâ€åŽç«¯ DDD 技术方案,明确 `server-rs + Axum + SpacetimeDB` 分层边界ã€shared contractsã€ä½œå“é…ç½®ã€runtime runã€æ´¾ç”Ÿæˆç»©ã€æŽ’行榜ã€`work_play_start` 埋点ã€migration/绑定生æˆç­–略,以åŠä¸ä¿å­˜åŽŸå§‹éº¦å…‹é£ŽéŸ³é¢‘çš„éšç§ä¸Žå作弊约æŸã€‚ - [BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battleâ€2D æµè§ˆå™¨ runtime 技术方案,明确 Phaser + TypeScript + Vite 选型ã€çº¯ TS simulation 与 Phaser renderer/DOM HUD 边界ã€Web Audio 输入适é…ã€ç§»åŠ¨ç«¯æƒé™é™çº§å’ŒåŽç»­æµ‹è¯•验è¯å‘½ä»¤ã€‚ - [PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md](./PUBLIC_WORK_DETAIL_NOT_FOUND_RECOVERY_2026-05-11.md):记录直接访问公开作å“详情深链时作å“ä¸å­˜åœ¨æˆ–已下架的回首页修å¤ï¼Œé¿å…关闭æç¤ºåŽåœåœ¨ `work-detail` 空状æ€ç™½å±ã€‚ - [PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md](./PLATFORM_MOBILE_RECOMMEND_CARD_SAFE_SWIPE_LAYOUT_2026-05-12.md):冻结移动端推è页éšè—顶部å“牌æ ã€æ‰©å¤§æŽ¨èå¡ç‰‡å¯ç”¨é«˜åº¦ï¼Œä»¥åŠåªåœ¨åº•部作å“ä¿¡æ¯åŒºæ‰¿æŽ¥åˆ‡æ¢ä½œå“手势的布局å£å¾„。 +- [ã€å‰ç«¯ä½“验】移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md](./ã€å‰ç«¯ä½“验】移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md):记录移动端输入法弹出时平å°ç”»å¸ƒä¿æŒç¨³å®šé«˜åº¦ï¼Œåªé€šè¿‡ç”»é¢ä½ç§»èšç„¦å½“å‰è¾“入框的实现å£å¾„。 - [BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md](./BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md)ï¼šå†»ç»“å¯“æ•™äºŽä¹ `å®è´è¯†ç‰©` 模æ¿åˆ›ä½œå‘布线程的å‰ç«¯å…¥å£ã€å¥‘约ã€serviceã€ç»“果页ã€å‘布标签和åŽç«¯ image-2 接å£é¢„留边界。 - [CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md):冻结儿童动作识别互动玩法 Demo 固定热身关的开å‘è½åœ°è§„格,覆盖横å±å±•ç¤ºã€æ‘„åƒå¤´èƒŒæ™¯è™šåŒ–ã€è§’色剪影ã€ç»¿è‰²åœ†çޝ 2 ç§’ä¿æŒã€åŠ¨ä½œæ•™å­¦ã€å½“å‰ä¼šè¯å†…空间边界记录和åŽç»­å…³å¡å®‰å…¨æš‚åœè§„则。 - [RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md](./RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md):记录è¿è¡Œæ€è¾“入设备抽象层,明确鼠标ã€è§¦æŽ§ã€mocap 等设备统一归一为通用拖拽语义,玩法组件åªè´Ÿè´£è§£é‡Šç›®æ ‡å’Œè½ç‚¹ã€‚ diff --git a/docs/technical/RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md b/docs/technical/RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md index 39d220f8..3f0b7294 100644 --- a/docs/technical/RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md +++ b/docs/technical/RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md @@ -60,7 +60,7 @@ model = gpt-image-2-all ```text VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai VECTOR_ENGINE_API_KEY=... -VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000 +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 ``` `VECTOR_ENGINE_API_KEY` 缺失时,角色主图与场景图返回 `SERVICE_UNAVAILABLE`,`details.provider = "vector-engine"`。 diff --git a/docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md b/docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md index a405c1f9..11309235 100644 --- a/docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md +++ b/docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md @@ -89,7 +89,7 @@ VectorEngine æ–‡æ¡£è¦æ±‚使用åƒç´ å°ºå¯¸ï¼Œä¸å†ä½¿ç”¨ APIMart 的比例写 ```text VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai VECTOR_ENGINE_API_KEY= -VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000 +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 ``` 说明: diff --git a/docs/technical/ã€å‰ç«¯ä½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md b/docs/technical/ã€å‰ç«¯ä½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md new file mode 100644 index 00000000..59d07556 --- /dev/null +++ b/docs/technical/ã€å‰ç«¯ä½“验】图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ-2026-05-14.md @@ -0,0 +1,32 @@ +# 图åƒç»„件统一å°è£…与å¤ç”¨è¾¹ç•Œ 2026-05-14 + +## 背景 + +æ‹¼å›¾åˆ›ä½œé¡µã€æŠ“å¤§é¹…å°é¢é¡µä»¥åŠåŽç»­æ›´å¤šåˆ›ä½œé¡µï¼Œéƒ½éœ€è¦åŒä¸€å¥—图åƒè¾“入能力:未上传图片时å¯ç”¨ç”»é¢æè¿°ç”Ÿæˆå›¾ç‰‡ï¼Œä¹Ÿå¯å åŠ å¤šå¼ å‚考图;上传图片åŽå¯é€‰æ‹©é‡ç»˜æˆ–直接使用当å‰å›¾ç‰‡ã€‚以å‰è¿™äº›èƒ½åŠ›åˆ†æ•£åœ¨å„页内è”实现,é‡å¤äº†ä¸Šä¼ ã€é¢„览ã€ç§»é™¤ã€å‚考图展示和文案布局。 + +## 组件边界 + +1. 图åƒç»„件负责统一承载图片上传å¡ã€ç”»é¢æè¿°è¾“å…¥ã€å‚考图入å£ã€å‚考图缩略图ã€å‚考图预览ã€AI é‡ç»˜å¼€å…³å’Œæäº¤æŒ‰é’®ã€‚ +2. 图åƒç»„ä»¶åªåšäº¤äº’与表现层,ä¸å†³å®šå…·ä½“生图接å£ï¼Œä¹Ÿä¸ç›´æŽ¥è€¦åˆæŸä¸ªçŽ©æ³•çš„è‰ç¨¿æäº¤é€»è¾‘ã€è®¡è´¹é€»è¾‘或历å²ç´ ææŸ¥è¯¢é€»è¾‘。 +3. 主图读å–ã€è£å‰ªã€å‚考图转 Data URLã€åކå²ç´ æé€‰æ‹©ã€ç”Ÿæˆè¯·æ±‚æäº¤ã€è‡ªåЍä¿å­˜å’ŒåŽç«¯å¥‘约é€ä¼ ä»ç”±å¤–层页é¢è´Ÿè´£ã€‚ +4. 图åƒç»„ä»¶é‡‡ç”¨å—æŽ§æ¨¡å¼ï¼Œå¤–层页é¢ä¼ å…¥å½“å‰å›¾ç‰‡ã€ç”»é¢æè¿°ã€å‚考图数组ã€AI é‡ç»˜çжæ€å’Œå›¾ç‰‡æ¨¡åž‹ç­‰ä¸šåŠ¡çœŸç›¸ï¼Œç»„ä»¶åªå›žè°ƒå˜æ›´ä¸Žæäº¤åŠ¨ä½œã€‚ + +## 统一交互å£å¾„ + +1. æœªä¸Šä¼ ä¸»å›¾æ—¶ï¼Œç”»é¢æè¿°è¾“å…¥æ¡†å³ä¸‹è§’显示å‚考图上传 icon,å…许多选,默认最多 5 张。 +2. å‚考图åªä»¥å°ç¼©ç•¥å›¾å±•ç¤ºï¼Œç‚¹å‡»ç¼©ç•¥å›¾å¯æ”¾å¤§é¢„览,å•å¼ å¯ç§»é™¤ã€‚ +3. 主图上传åŽéšè—è¿™ç»„ç”»é¢æè¿°å‚考图入å£ï¼Œé¿å…主图与å‚考图èŒè´£æ··æ·†ã€‚ +4. 主图存在时,组件显示 AI é‡ç»˜å¼€å…³ï¼›å…³é—­åŽåªä¿ç•™å½“å‰å›¾ç‰‡ç›´æŽ¥æäº¤çš„路径,ä¸å†å±•ç¤ºç”»é¢æè¿°è¾“å…¥ã€‚ +5. 组件åªè´Ÿè´£æŠŠæäº¤æŒ‰é’®ã€é”™è¯¯æç¤ºã€é¢„览弹层和确认弹层组织æˆç»Ÿä¸€ UI,具体按钮文案和费用æç¤ºç”±å¤–层页é¢ä¼ å…¥ã€‚ + +## å¤ç”¨çº¦å®š + +1. 拼图入å£å…ˆæŽ¥å…¥è¯¥ç»„件,ä¿ç•™ç”»é¢æè¿°ç›´ç”Ÿå›¾ã€å‚考图生图ã€ä¸Šä¼ å›¾é‡ç»˜å’Œä¸é‡ç»˜å››ç§è·¯å¾„。 +2. åŽç»­æŠ“大鹅å°é¢ã€å…¶ä»–创作页和模æ¿é¡µå¤ç”¨æ—¶ï¼Œåªæ›¿æ¢å¤–层的æäº¤åŠ¨ä½œã€å›¾ç‰‡æ¨¡åž‹é€‰æ‹©å’Œå¤–éƒ¨é”™è¯¯æ¥æºã€‚ +3. å¦‚æœªæ¥æŸé¡µä¸éœ€è¦åކå²ç´ æå…¥å£ã€å‚è€ƒå›¾å…¥å£æˆ–图片模型选择,应通过 props 关闭,ä¸è¦å¤åˆ¶ä¸€å¥—新的图åƒè¾“入实现。 + +## 验收 + +1. 拼图创作页的图åƒè¾“å…¥æ¨¡å—æ”¹ä¸ºç‹¬ç«‹ç»„ä»¶åŽï¼Œè¡Œä¸ºä¸ŽåŽŸæœ‰é¡µé¢ä¸€è‡´ã€‚ +2. å‚考图多选ã€ç¼©ç•¥å›¾é¢„览ã€ç§»é™¤ã€ä¸»å›¾ä¸Šä¼ ã€AI é‡ç»˜å¼€å…³å’Œæäº¤æŒ‰é’®è¡Œä¸ºä¸é€€åŒ–。 +3. 组件å¯ä»¥åœ¨ä¸æ”¹å†…部实现的情况下接入其它页é¢ï¼Œåªæ›¿æ¢å¤–层业务回调和文案。 diff --git a/docs/technical/ã€å‰ç«¯ä½“验】移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md b/docs/technical/ã€å‰ç«¯ä½“验】移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md new file mode 100644 index 00000000..45216493 --- /dev/null +++ b/docs/technical/ã€å‰ç«¯ä½“验】移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ-2026-05-14.md @@ -0,0 +1,22 @@ +# 移动端输入法ä¸åŽ‹ç¼©ç”»å¸ƒèšç„¦æ–¹æ¡ˆ + +## 背景 + +å¹³å°ä¸»ç«™é‡‡ç”¨æ¸¸æˆå¼å›ºå®šç”»å¸ƒä½“验,根壳原本优先使用 `100dvh`。手机æµè§ˆå™¨å¼¹å‡ºè¾“入法时,`dvh` 会跟éšå¯è§è§†å£å˜å°ï¼Œå¯¼è‡´ä¸Šæ–¹é¡µé¢ã€æŽ¨èå¡ã€åˆ›ä½œé¦–页和底部输入区一起被压缩,画é¢å±‚级失真。 + +用户预期是:点击输入框åŽï¼Œç”»é¢æœ¬èº«ä¸è¦é‡æ–°åŽ‹ç¼©æŽ’ç‰ˆï¼ŒåªæŠŠå½“å‰ç”»é¢ä½ç½®å‘输入框èšç„¦ï¼Œè®©è¾“入框é¿å¼€è¾“入法。 + +## è½åœ°å£å¾„ + +1. 主站å¯åŠ¨æ—¶ç»Ÿä¸€è°ƒç”¨ `stabilizeMobileViewportKeyboardFocus()`,åªåœ¨è§¦æŽ§æˆ–粗指针设备上å¯ç”¨ã€‚ +2. `.platform-viewport-shell` çš„é«˜åº¦ä¼˜å…ˆè¯»å– `--platform-layout-viewport-height`,该值在输入法未打开时记录稳定布局高度;输入法打开期间ä¸è·Ÿéš `visualViewport.height` 缩å°ã€‚ +3. 输入框èšç„¦ä¸” `visualViewport` 明显å˜å°æ—¶ï¼Œè®¡ç®—当å‰è¾“入框与å¯è§è§†å£åº•部的è·ç¦»ï¼Œåªé€šè¿‡ `--platform-keyboard-focus-offset` 坹平尿 ¹å£³åš `translateY`。 +4. 输入法打开期间éšè—移动端底部 dock,é¿å… dock 被整体ä½ç§»åŽé®ä½è¾“入框。 +5. è¯¥æ–¹æ¡ˆä¸æ–°å¢ž UI è¯´æ˜Žæ–‡æ¡ˆï¼Œä¸æ”¹å˜ä¸šåŠ¡ç»„ä»¶ç»“æž„ï¼Œä¹Ÿä¸è¦æ±‚æ¯ä¸ªè¾“入框å•独适é…。 + +## 验收 + +- 手机竖å±ç‚¹å‡»åˆ›ä½œé¦–页底部输入框时,页é¢å†…容ä¸è¢«åŽ‹ç¼©å˜çŸ®ã€‚ +- 输入框éšç”»é¢ä½ç§»å‡ºçŽ°åœ¨è¾“å…¥æ³•ä¸Šæ–¹ï¼Œå¯ç»§ç»­è¾“入和å‘é€ã€‚ +- 输入法关闭åŽï¼Œå¹³å°ç”»å¸ƒå›žåˆ°åŽŸä½ï¼Œåº•部 dock æ¢å¤æ˜¾ç¤ºã€‚ +- 未èšç„¦è¾“入框时,平å°é¦–页ä»ä¿æŒåŽŸæœ‰ç§»åŠ¨ç«¯ `100dvh` / 固定 dock 行为。 diff --git a/packages/shared/src/contracts/puzzleAgentActions.ts b/packages/shared/src/contracts/puzzleAgentActions.ts index 7bec1920..594d66ca 100644 --- a/packages/shared/src/contracts/puzzleAgentActions.ts +++ b/packages/shared/src/contracts/puzzleAgentActions.ts @@ -51,6 +51,7 @@ export type PuzzleAgentActionRequest = workDescription?: string; pictureDescription?: string; referenceImageSrc?: string | null; + referenceImageSrcs?: string[]; imageModel?: string | null; aiRedraw?: boolean; } @@ -61,6 +62,7 @@ export type PuzzleAgentActionRequest = workDescription?: string; pictureDescription?: string; referenceImageSrc?: string | null; + referenceImageSrcs?: string[]; imageModel?: string | null; aiRedraw?: boolean; candidateCount?: number; @@ -70,6 +72,7 @@ export type PuzzleAgentActionRequest = levelId?: string | null; promptText?: string | null; referenceImageSrc?: string | null; + referenceImageSrcs?: string[]; imageModel?: string | null; aiRedraw?: boolean; candidateCount?: number; diff --git a/packages/shared/src/contracts/puzzleAgentSession.ts b/packages/shared/src/contracts/puzzleAgentSession.ts index 779fd19d..f216f263 100644 --- a/packages/shared/src/contracts/puzzleAgentSession.ts +++ b/packages/shared/src/contracts/puzzleAgentSession.ts @@ -50,6 +50,7 @@ export interface CreatePuzzleAgentSessionRequest { workDescription?: string; pictureDescription?: string; referenceImageSrc?: string | null; + referenceImageSrcs?: string[]; imageModel?: string | null; aiRedraw?: boolean; } diff --git a/packages/shared/src/http.ts b/packages/shared/src/http.ts index 5c80d1a9..03448e27 100644 --- a/packages/shared/src/http.ts +++ b/packages/shared/src/http.ts @@ -155,11 +155,11 @@ function readApiErrorDetailMessage(details: unknown) { return ''; } - // åŽç«¯é€šç”¨ message 常用于错误分类;details.message / details.reason - // æ‰æ˜¯ç»™ç”¨æˆ·å®šä½é—®é¢˜çš„业务原因,é…置缺失类错误通常åªå¸¦ reason。 + // åŽç«¯é€šç”¨ message 常用于错误分类;reason 更适åˆç›´æŽ¥å±•示给用户, + // 例如 VectorEngine 网络分类会把底层 reqwest message 留给日志。 return ( - readTrimmedMessage(details.message) || - readTrimmedMessage(details.reason) + readTrimmedMessage(details.reason) || + readTrimmedMessage(details.message) ); } diff --git a/scripts/generate-bark-battle-assets.mjs b/scripts/generate-bark-battle-assets.mjs index 0fc94075..1a605519 100644 --- a/scripts/generate-bark-battle-assets.mjs +++ b/scripts/generate-bark-battle-assets.mjs @@ -35,7 +35,7 @@ function resolveEnv() { return { baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '').trim().replace(/\/+$/u, ''), apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(), - timeoutMs: Number.parseInt(String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || 180000), 10), + timeoutMs: Number.parseInt(String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || 1000000), 10), }; } diff --git a/scripts/generate-child-motion-demo-assets.mjs b/scripts/generate-child-motion-demo-assets.mjs index 33eccbd6..9f1a6690 100644 --- a/scripts/generate-child-motion-demo-assets.mjs +++ b/scripts/generate-child-motion-demo-assets.mjs @@ -13,7 +13,7 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); const assetDir = path.join(repoRoot, 'public', 'child-motion-demo'); const intermediateDir = path.join(repoRoot, 'tmp', 'child-motion-demo-assets'); -const defaultTimeoutMs = 180000; +const defaultTimeoutMs = 1000000; const chromaKeyColor = '#ff00ff'; const layoutReferenceOutput = 'picture-book-stage-layout-v2.png'; diff --git a/scripts/generate-match3d-style-references.mjs b/scripts/generate-match3d-style-references.mjs index f78e638f..653b5fd0 100644 --- a/scripts/generate-match3d-style-references.mjs +++ b/scripts/generate-match3d-style-references.mjs @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); const defaultOutDir = path.join(repoRoot, 'public', 'match3d-style-references'); -const defaultTimeoutMs = 180000; +const defaultTimeoutMs = 1000000; const styleTemplates = [ { diff --git a/scripts/generate-taonier-logo-concepts.mjs b/scripts/generate-taonier-logo-concepts.mjs index a68e13ee..cdc2899f 100644 --- a/scripts/generate-taonier-logo-concepts.mjs +++ b/scripts/generate-taonier-logo-concepts.mjs @@ -9,7 +9,7 @@ const outputDir = path.join( 'branding', 'taonier-logo-concepts', ); -const defaultTimeoutMs = 420000; +const defaultTimeoutMs = 1000000; const dimensionalConcepts = [ { diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index e55e2d65..b8af62a4 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -13,6 +13,7 @@ use platform_speech::{ const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge"; const DEFAULT_AUTH_STORE_PATH: &str = "server-rs/.data/auth-store.json"; const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json"; +pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000; // é›†ä¸­ç®¡ç† api-server çš„å¯åЍé…置,é¿å…å…¥å£å±‚直接散è½çŽ¯å¢ƒå˜é‡è§£æžé€»è¾‘。 #[derive(Clone, Debug)] @@ -248,7 +249,7 @@ impl Default for AppConfig { apimart_image_request_timeout_ms: 180_000, vector_engine_base_url: String::new(), vector_engine_api_key: None, - vector_engine_image_request_timeout_ms: 180_000, + vector_engine_image_request_timeout_ms: DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, vector_engine_audio_request_timeout_ms: 180_000, hyper3d_base_url: "https://api.hyper3d.com/api/v2".to_string(), hyper3d_api_key: None, @@ -675,7 +676,9 @@ impl AppConfig { if let Some(vector_engine_image_request_timeout_ms) = read_first_positive_u64_env(&["VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS"]) { - config.vector_engine_image_request_timeout_ms = vector_engine_image_request_timeout_ms; + // 中文注释:VectorEngine image-2 实测å¯èƒ½è¶…过 500 秒;旧环境文件中常è§çš„ 180 秒值ä¸èƒ½å†æå‰æˆªæ–­çœŸå®žç”Ÿå›¾ã€‚ + config.vector_engine_image_request_timeout_ms = vector_engine_image_request_timeout_ms + .max(DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS); } if let Some(vector_engine_audio_request_timeout_ms) = @@ -1009,7 +1012,7 @@ fn parse_positive_u16(raw: &str) -> Option { #[cfg(test)] mod tests { - use super::{AppConfig, LlmProvider}; + use super::{AppConfig, DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, LlmProvider}; use std::sync::{Mutex, OnceLock}; static ENV_LOCK: OnceLock> = OnceLock::new(); @@ -1094,7 +1097,10 @@ mod tests { config.vector_engine_base_url, "https://vector.internal.example" ); - assert_eq!(config.vector_engine_image_request_timeout_ms, 210_000); + assert_eq!( + config.vector_engine_image_request_timeout_ms, + DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS + ); assert_eq!( config.hyper3d_base_url, "https://model.internal.example/api/v2" diff --git a/server-rs/crates/api-server/src/edutainment_baby_object.rs b/server-rs/crates/api-server/src/edutainment_baby_object.rs index d3ae02e5..cbd0682f 100644 --- a/server-rs/crates/api-server/src/edutainment_baby_object.rs +++ b/server-rs/crates/api-server/src/edutainment_baby_object.rs @@ -15,6 +15,7 @@ use serde_json::{Value, json}; use crate::{ api_response::json_success_body, character_visual_assets::try_apply_background_alpha_to_png, + config::DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, http_error::AppError, openai_image_generation::{ DownloadedOpenAiImage, OpenAiImageSettings, build_openai_image_http_client, @@ -27,7 +28,6 @@ use crate::{ const BABY_OBJECT_MATCH_PROVIDER: &str = "vector-engine-gpt-image-2"; const BABY_OBJECT_MATCH_IMAGE_SIZE: &str = "1024x1024"; const BABY_OBJECT_MATCH_BACKGROUND_IMAGE_SIZE: &str = "1536x1024"; -const BABY_OBJECT_MATCH_VECTOR_ENGINE_REQUEST_TIMEOUT_MS: u64 = 480_000; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -207,7 +207,7 @@ fn build_baby_object_match_negative_prompt() -> &'static str { fn with_baby_object_match_image_timeout(mut settings: OpenAiImageSettings) -> OpenAiImageSettings { settings.request_timeout_ms = settings .request_timeout_ms - .max(BABY_OBJECT_MATCH_VECTOR_ENGINE_REQUEST_TIMEOUT_MS); + .max(DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS); settings } @@ -617,7 +617,7 @@ mod tests { assert_eq!( settings.request_timeout_ms, - BABY_OBJECT_MATCH_VECTOR_ENGINE_REQUEST_TIMEOUT_MS + DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS ); } diff --git a/server-rs/crates/api-server/src/match3d.rs b/server-rs/crates/api-server/src/match3d.rs index af00d89f..1bc4b72f 100644 --- a/server-rs/crates/api-server/src/match3d.rs +++ b/server-rs/crates/api-server/src/match3d.rs @@ -1023,32 +1023,14 @@ pub async fn generate_match3d_cover_image( .await .map_err(|error| match3d_error_response(&request_context, MATCH3D_WORKS_PROVIDER, error))?; - upsert_match3d_draft_snapshot( + let item = update_match3d_work_cover_only( &state, &request_context, - &authenticated, - context.session_id.clone(), - context.owner_user_id.clone(), - profile_id.clone(), - Some(context.profile.game_name), - Some(context.profile.summary), - Some(serde_json::to_string(&context.profile.tags).unwrap_or_default()), - Some(generated_cover.src.clone()), - None, - None, + context.owner_user_id.as_str(), + context.profile, + generated_cover.src.as_str(), ) .await?; - let item = state - .spacetime_client() - .get_match3d_work_detail(profile_id.clone(), context.owner_user_id) - .await - .map_err(|error| { - match3d_error_response( - &request_context, - MATCH3D_WORKS_PROVIDER, - map_match3d_client_error(error), - ) - })?; Ok(json_success_body( Some(&request_context), @@ -1061,6 +1043,39 @@ pub async fn generate_match3d_cover_image( )) } +async fn update_match3d_work_cover_only( + state: &AppState, + request_context: &RequestContext, + owner_user_id: &str, + profile: Match3DWorkProfileRecord, + cover_image_src: &str, +) -> Result { + // 中文注释:å°é¢ç”Ÿæˆæ˜¯å®šå‘å›¾ç‰‡æ§½ä½æ›´æ–°ï¼Œä¸èƒ½å¤ç”¨è‰ç¨¿ç¼–译路径é‡ç®—题æã€éš¾åº¦æˆ–ç´ æ JSON。 + state + .spacetime_client() + .update_match3d_work(Match3DWorkUpdateRecordInput { + profile_id: profile.profile_id, + owner_user_id: owner_user_id.to_string(), + game_name: profile.game_name, + theme_text: profile.theme_text, + summary_text: profile.summary, + tags_json: serde_json::to_string(&normalize_tags(profile.tags)).unwrap_or_default(), + cover_image_src: cover_image_src.to_string(), + cover_asset_id: profile.cover_asset_id.unwrap_or_default(), + clear_count: profile.clear_count, + difficulty: profile.difficulty, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + match3d_error_response( + request_context, + MATCH3D_WORKS_PROVIDER, + map_match3d_client_error(error), + ) + }) +} + pub async fn generate_match3d_background_image_for_work( State(state): State, Path(profile_id): Path, @@ -4804,6 +4819,7 @@ async fn generate_match3d_background_image( "message": "抓大鹅容器 UI 图生æˆå¤±è´¥ï¼šæœªè¿”回图片", })) })?; + let container_image = make_match3d_container_image_transparent(container_image)?; let container_upload = persist_match3d_generated_bytes( state, owner_user_id, @@ -4864,6 +4880,7 @@ async fn generate_match3d_container_image( "message": "抓大鹅容器 UI 图生æˆå¤±è´¥ï¼šæœªè¿”回图片", })) })?; + let container_image = make_match3d_container_image_transparent(container_image)?; let container_upload = persist_match3d_generated_bytes( state, owner_user_id, @@ -4956,10 +4973,40 @@ fn build_match3d_container_generation_prompt(config: &Match3DConfigJson, prompt: .map(|style| format!("整体美术风格å‚考:{style}。")) .unwrap_or_default(); format!( - "{prompt}\n{style_clause}生æˆä¸€å¼  1:1 抓大鹅中心容器 UI 图,åªç»˜åˆ¶ä¸€ä¸ªè´´åˆé¢˜æè®¾å®šçš„圆形或浅盘状竞技容器。严格å‚考输入å‚考图的容器范围和视图角度:容器外轮廓必须接近画布四边,å ç”»å¸ƒå®½åº¦çº¦ 86%-92%ã€é«˜åº¦çº¦ 82%-90%,中心在画布中心略å下,åªä¿ç•™å°‘é‡é€æ˜Žæˆ–纯净留白;视角为轻俯视 3/4 上方视角,能看到圆形碗体外å£ã€åŽšå®žå‰æ²¿å’Œæ¨ªå‘椭圆形内å£ï¼Œä¸èƒ½ç”»æˆæ­£ä¿¯è§†æ‰åœ†ç›˜ã€ä¾§è§†ç¢—ã€å°æ‰˜ç›˜æˆ–居中的å°å®¹å™¨ã€‚å®¹å™¨éœ€è¦æœ‰æ¸…晰外沿ã€å†…侧坿”¾ç½® 2D 物å“的干净空间ã€è½»å¾®é˜´å½±å’Œé«˜è¾¨è¯†è¾¹ç•Œï¼›èƒŒæ™¯å¿…须逿˜Žæ„Ÿæˆ–纯净留白,ä¸èƒ½åšæˆæ•´é¡µèƒŒæ™¯ã€‚ç¦æ­¢æ–‡å­—ã€æ°´å°ã€æŒ‰é’®ã€å€’计时ã€åˆ†æ•°ã€ç‰©å“ã€è§’è‰²ã€æ‰‹ã€æ•™ç¨‹æµ®å±‚å’Œèœå•。" + "{prompt}\n{style_clause}生æˆä¸€å¼  1:1 抓大鹅中心容器 UI 图,åªç»˜åˆ¶ä¸€ä¸ªè´´åˆé¢˜æè®¾å®šçš„圆形或浅盘状竞技容器。严格å‚考输入å‚考图的容器范围和视图角度:容器外轮廓必须接近画布四边,å ç”»å¸ƒå®½åº¦çº¦ 86%-92%ã€é«˜åº¦çº¦ 82%-90%,中心在画布中心略å下,åªä¿ç•™å°‘é‡é€æ˜Žç•™ç™½ï¼›è§†è§’为轻俯视 3/4 上方视角,能看到圆形碗体外å£ã€åŽšå®žå‰æ²¿å’Œæ¨ªå‘椭圆形内å£ï¼Œä¸èƒ½ç”»æˆæ­£ä¿¯è§†æ‰åœ†ç›˜ã€ä¾§è§†ç¢—ã€å°æ‰˜ç›˜æˆ–居中的å°å®¹å™¨ã€‚å®¹å™¨éœ€è¦æœ‰æ¸…晰外沿ã€å†…侧坿”¾ç½® 2D 物å“的干净空间ã€è½»å¾®é˜´å½±å’Œé«˜è¾¨è¯†è¾¹ç•Œï¼›èƒŒæ™¯å¿…é¡»æ˜¯é€æ˜Ž alpha,ä¸å¾—出现白底ã€çº¯è‰²åº•ã€æ¸å˜åº•ã€åœºæ™¯åº•æˆ–æ•´é¡µèƒŒæ™¯ã€‚ç¦æ­¢æ–‡å­—ã€æ°´å°ã€æŒ‰é’®ã€å€’计时ã€åˆ†æ•°ã€ç‰©å“ã€è§’è‰²ã€æ‰‹ã€æ•™ç¨‹æµ®å±‚å’Œèœå•。" ) } +fn make_match3d_container_image_transparent( + image: DownloadedOpenAiImage, +) -> Result { + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "match3d-assets", + "message": format!("抓大鹅容器图解ç å¤±è´¥ï¼š{error}"), + })) + })?; + let mut rgba = source.to_rgba8(); + let (width, height) = rgba.dimensions(); + remove_match3d_container_plain_background(rgba.as_mut(), width as usize, height as usize); + + let mut encoded = std::io::Cursor::new(Vec::new()); + image::DynamicImage::ImageRgba8(rgba) + .write_to(&mut encoded, ImageFormat::Png) + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "match3d-assets", + "message": format!("æŠ“å¤§é¹…å®¹å™¨å›¾é€æ˜ŽåŒ–失败:{error}"), + })) + })?; + + Ok(DownloadedOpenAiImage { + bytes: encoded.into_inner(), + mime_type: "image/png".to_string(), + extension: "png".to_string(), + }) +} + async fn generate_match3d_material_sheet( state: &AppState, config: &Match3DConfigJson, @@ -6232,6 +6279,45 @@ fn remove_match3d_material_green_screen_background( } } + // 中文注释:较厚的抗锯齿绿边å¯èƒ½ä½ŽäºŽ hard 阈值;先沿整张 sheet çš„é€æ˜ŽèƒŒæ™¯å‘å†…åƒæŽ‰ + // 软绿边,å†è¿›å…¥æ ¼å­è£å‰ªï¼Œé¿å…æ¯å¼ åˆ‡å›¾è‡ªå¸¦ç»¿è‰²æè¾¹ã€‚ + let soft_green_cleanup_rounds = (width.min(height) / 40).clamp(4, 14); + for _ in 0..soft_green_cleanup_rounds { + let mut expanded_mask = background_mask.clone(); + let mut changed_this_round = false; + for y in 0..height { + for x in 0..width { + let pixel_index = y * width + x; + if background_mask[pixel_index] != 0 { + continue; + } + let offset = pixel_index * 4; + let pixel = [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + let green_score = green_scores[pixel_index]; + let white_score = white_scores[pixel_index]; + if !is_match3d_material_soft_green_matte_pixel(pixel, green_score, white_score) { + continue; + } + if !touches_match3d_material_background_mask(x, y, width, height, &background_mask) + { + continue; + } + + expanded_mask[pixel_index] = 1; + changed_this_round = true; + } + } + background_mask = expanded_mask; + if !changed_this_round { + break; + } + } + // 中文注释:主体边缘常带一圈绿幕或白底抗锯齿,扩一层软边,é¿å…åˆ‡å‰²åŽæ®‹ç•™æ¯›è¾¹ã€‚ for _ in 0..2 { let mut expanded_mask = background_mask.clone(); @@ -6372,9 +6458,10 @@ fn remove_match3d_material_green_screen_background( } } else { if green_score > 0.04 { - green = green - .max(red.max(blue)) - .max((green - (green - red.max(blue)) * 0.78).round()); + let toned_green = (green - (green - red.max(blue)) * 0.78) + .round() + .max(red.max(blue)); + green = green.min(toned_green).min(red.max(blue) + 18.0); } if white_score > 0.12 { @@ -6417,6 +6504,50 @@ fn remove_match3d_material_green_screen_background( changed } +fn touches_match3d_material_background_mask( + x: usize, + y: usize, + width: usize, + height: usize, + background_mask: &[u8], +) -> bool { + for offset_y in -1i32..=1 { + for offset_x in -1i32..=1 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 { + return true; + } + if background_mask[next_y as usize * width + next_x as usize] != 0 { + return true; + } + } + } + false +} + +fn is_match3d_material_soft_green_matte_pixel( + pixel: [u8; 4], + green_score: f32, + white_score: f32, +) -> bool { + if pixel[3] == 0 || green_score < MATCH3D_MATERIAL_GREEN_SCREEN_MIN_SCORE { + return false; + } + + let red = pixel[0]; + let green = pixel[1]; + let blue = pixel[2]; + let foreground_mix = red.max(blue); + green >= 188 + && white_score < 0.34 + && green.saturating_sub(foreground_mix) >= 42 + && (red >= 48 || blue >= 96 || pixel[3] < 236) +} + fn compute_match3d_material_green_screen_score(pixel: [u8; 4]) -> f32 { if pixel[3] == 0 { return 1.0; @@ -6463,6 +6594,146 @@ fn compute_match3d_material_white_screen_score(pixel: [u8; 4]) -> f32 { clamp_match3d_material_unit(neutrality * (brightness * 0.85 + floor * 0.15)) } +fn remove_match3d_container_plain_background( + pixels: &mut [u8], + width: usize, + height: usize, +) -> bool { + let pixel_count = width.saturating_mul(height); + if pixel_count == 0 || pixels.len() < pixel_count.saturating_mul(4) { + return false; + } + + let mut background_mask = vec![0u8; pixel_count]; + let mut queue = Vec::::new(); + let mut queue_index = 0usize; + + let seed_pixel = |pixel_index: usize, background_mask: &mut [u8], queue: &mut Vec| { + if background_mask[pixel_index] != 0 { + return; + } + let offset = pixel_index * 4; + if is_match3d_container_background_pixel([ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]) { + background_mask[pixel_index] = 1; + queue.push(pixel_index); + } + }; + + for x in 0..width { + seed_pixel(x, &mut background_mask, &mut queue); + seed_pixel((height - 1) * width + x, &mut background_mask, &mut queue); + } + for y in 1..height.saturating_sub(1) { + seed_pixel(y * width, &mut background_mask, &mut queue); + seed_pixel(y * width + width - 1, &mut background_mask, &mut queue); + } + + while queue_index < queue.len() { + let pixel_index = queue[queue_index]; + queue_index += 1; + let x = pixel_index % width; + let y = pixel_index / width; + let neighbors = [ + (x > 0).then(|| pixel_index - 1), + (x + 1 < width).then_some(pixel_index + 1), + (y > 0).then(|| pixel_index - width), + (y + 1 < height).then_some(pixel_index + width), + ]; + + for next_pixel_index in neighbors.into_iter().flatten() { + if background_mask[next_pixel_index] != 0 { + continue; + } + let offset = next_pixel_index * 4; + if is_match3d_container_background_pixel([ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]) { + background_mask[next_pixel_index] = 1; + queue.push(next_pixel_index); + } + } + } + + // 中文注释:图生图å¶å°”ä¼šåœ¨å®¹å™¨è¾¹ç¼˜ç•™ä¸‹ç™½åº•æŠ—é”¯é½¿ï¼Œæ‰©ä¸€å±‚åªæ¸…ç†è¿žåˆ°èƒŒæ™¯çš„æµ…色边。 + for _ in 0..2 { + let mut expanded_mask = background_mask.clone(); + for y in 0..height { + for x in 0..width { + let pixel_index = y * width + x; + if background_mask[pixel_index] != 0 { + continue; + } + let offset = pixel_index * 4; + let pixel = [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + if !is_match3d_container_soft_background_pixel(pixel) { + continue; + } + + let mut adjacent_background_count = 0usize; + for offset_y in -1i32..=1 { + for offset_x in -1i32..=1 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 + || next_x >= width as i32 + || next_y < 0 + || next_y >= height as i32 + { + adjacent_background_count += 1; + continue; + } + if background_mask[next_y as usize * width + next_x as usize] != 0 { + adjacent_background_count += 1; + } + } + } + + if adjacent_background_count >= 3 { + expanded_mask[pixel_index] = 1; + } + } + } + background_mask = expanded_mask; + } + + let mut changed = false; + for pixel_index in 0..pixel_count { + if background_mask[pixel_index] == 0 { + continue; + } + let offset = pixel_index * 4; + if pixels[offset + 3] != 0 { + pixels[offset + 3] = 0; + changed = true; + } + } + changed +} + +fn is_match3d_container_background_pixel(pixel: [u8; 4]) -> bool { + pixel[3] < 16 || compute_match3d_material_white_screen_score(pixel) > 0.34 +} + +fn is_match3d_container_soft_background_pixel(pixel: [u8; 4]) -> bool { + pixel[3] < 80 || compute_match3d_material_white_screen_score(pixel) > 0.18 +} + fn collect_match3d_material_foreground_neighbor_color( pixels: &[u8], width: usize, @@ -7148,6 +7419,51 @@ mod tests { ); } + #[test] + fn match3d_material_sheet_slicing_removes_soft_green_matte_before_crop() { + let width = 500; + let height = 500; + let item_names = vec!["è‰èŽ“".to_string()]; + let mut sheet = image::RgbaImage::from_pixel(width, height, image::Rgba([0, 255, 0, 255])); + for y in 28..72 { + for x in 28..72 { + sheet.put_pixel(x, y, image::Rgba([64, 198, 112, 255])); + } + } + for y in 36..64 { + for x in 36..64 { + sheet.put_pixel(x, y, image::Rgba([220, 32, 48, 255])); + } + } + + let mut encoded = std::io::Cursor::new(Vec::new()); + image::DynamicImage::ImageRgba8(sheet) + .write_to(&mut encoded, ImageFormat::Png) + .expect("sheet should encode"); + let image = DownloadedOpenAiImage { + bytes: encoded.into_inner(), + mime_type: "image/png".to_string(), + extension: "png".to_string(), + }; + + let slices = slice_match3d_material_sheet(&image, &item_names).expect("sheet should slice"); + let decoded = image::load_from_memory(slices[0][0].bytes.as_slice()) + .expect("view should decode") + .to_rgba8(); + + assert!( + decoded.pixels().all(|pixel| { + let [red, green, blue, alpha] = pixel.0; + alpha == 0 || green <= red.max(blue).saturating_add(32) + }), + "æ•´å¼  sheet 去绿åŽå†è£å‰ªï¼Œè¾“出 PNG ä¸èƒ½ä¿ç•™å¯è§è½¯ç»¿è¾¹" + ); + assert!( + decoded.pixels().any(|pixel| pixel.0 == [220, 32, 48, 255]), + "软绿边清ç†ä¸èƒ½è¯¯åˆ ç‰©å“主体" + ); + } + #[test] fn match3d_material_sheet_slicing_cleans_white_matte_edge() { let width = 500; @@ -7193,6 +7509,46 @@ mod tests { ); } + #[test] + fn match3d_container_image_postprocess_removes_plain_background() { + let width = 256; + let height = 256; + let mut image = + image::RgbaImage::from_pixel(width, height, image::Rgba([248, 248, 246, 255])); + for y in 68..190 { + for x in 38..218 { + image.put_pixel(x, y, image::Rgba([160, 104, 54, 255])); + } + } + + let mut encoded = std::io::Cursor::new(Vec::new()); + image::DynamicImage::ImageRgba8(image) + .write_to(&mut encoded, ImageFormat::Png) + .expect("container should encode"); + let processed = make_match3d_container_image_transparent(DownloadedOpenAiImage { + bytes: encoded.into_inner(), + mime_type: "image/png".to_string(), + extension: "png".to_string(), + }) + .expect("container should postprocess"); + let decoded = image::load_from_memory(processed.bytes.as_slice()) + .expect("processed container should decode") + .to_rgba8(); + + assert_eq!(processed.mime_type, "image/png"); + assert_eq!(processed.extension, "png"); + assert_eq!( + decoded.get_pixel(0, 0).0[3], + 0, + "容器图四周白底必须在入库å‰è½¬æˆé€æ˜Ž alpha" + ); + assert_eq!( + decoded.get_pixel(width / 2, height / 2).0[3], + 255, + "容器主体ä¸èƒ½è¢«é€æ˜ŽåŒ–误删" + ); + } + #[test] fn match3d_work_metadata_parses_gpt4o_json() { let metadata = parse_match3d_work_metadata( @@ -7544,12 +7900,12 @@ mod tests { let root_settings = Match3DVectorEngineGeminiImageSettings { base_url: "https://api.vectorengine.cn".to_string(), api_key: "test-key".to_string(), - request_timeout_ms: 180_000, + request_timeout_ms: 1_000_000, }; let v1_settings = Match3DVectorEngineGeminiImageSettings { base_url: "https://api.vectorengine.cn/v1".to_string(), api_key: "test-key".to_string(), - request_timeout_ms: 180_000, + request_timeout_ms: 1_000_000, }; assert_eq!( @@ -7584,7 +7940,9 @@ mod tests { assert!(container_prompt.contains("轻俯视 3/4")); assert!(container_prompt.contains("æ¨ªå‘æ¤­åœ†å½¢å†…å£")); assert!(container_prompt.contains("ä¸èƒ½ç”»æˆæ­£ä¿¯è§†æ‰åœ†ç›˜")); - assert!(container_prompt.contains("ä¸èƒ½åšæˆæ•´é¡µèƒŒæ™¯")); + assert!(container_prompt.contains("逿˜Ž alpha")); + assert!(container_prompt.contains("白底")); + assert!(container_prompt.contains("整页背景")); assert!(container_prompt.contains("ç¦æ­¢æ–‡å­—")); } diff --git a/server-rs/crates/api-server/src/openai_image_generation.rs b/server-rs/crates/api-server/src/openai_image_generation.rs index b7db6bb5..55554701 100644 --- a/server-rs/crates/api-server/src/openai_image_generation.rs +++ b/server-rs/crates/api-server/src/openai_image_generation.rs @@ -628,12 +628,12 @@ mod tests { let root_settings = OpenAiImageSettings { base_url: "https://vector.example".to_string(), api_key: "test-key".to_string(), - request_timeout_ms: 180_000, + request_timeout_ms: 1_000_000, }; let v1_settings = OpenAiImageSettings { base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), - request_timeout_ms: 180_000, + request_timeout_ms: 1_000_000, }; assert_eq!( diff --git a/server-rs/crates/api-server/src/prompt/puzzle/level_name.rs b/server-rs/crates/api-server/src/prompt/puzzle/level_name.rs index c6ca0d1e..d7930b44 100644 --- a/server-rs/crates/api-server/src/prompt/puzzle/level_name.rs +++ b/server-rs/crates/api-server/src/prompt/puzzle/level_name.rs @@ -1,34 +1,38 @@ -/// 拼图首关关å¡å与 UI 背景æç¤ºè¯ç”Ÿæˆæç¤ºè¯ã€‚ +/// 拼图首关关å¡åã€ä½œå“元信æ¯ä¸Ž UI 背景æç¤ºè¯ç”Ÿæˆæç¤ºè¯ã€‚ /// -/// 模型åªè´Ÿè´£æŠŠç”»é¢æè¿°åŽ‹ç¼©æˆå¯ç›´æŽ¥å±•示的中文关å¡å,并产出è¿è¡Œæ€ UI 背景的正å‘视觉æç¤ºè¯ï¼› -/// 写回è‰ç¨¿å’Œä½œå“å¡ç”±ä¸šåŠ¡è·¯ç”±å¤„ç†ã€‚ +/// 模型åªè´Ÿè´£æŠŠç”»é¢æè¿°åŽ‹ç¼©æˆå¯ç›´æŽ¥å±•示的中文关å¡åã€ä½œå“æè¿°ã€ä½œå“标签, +/// 并产出è¿è¡Œæ€ UI 背景的正å‘视觉æç¤ºè¯ï¼›å†™å›žè‰ç¨¿å’Œä½œå“å¡ç”±ä¸šåŠ¡è·¯ç”±å¤„ç†ã€‚ pub(crate) const PUZZLE_FIRST_LEVEL_NAME_SYSTEM_PROMPT: &str = r#"你是一个中文拼图关å¡å‘½å编辑。 ä½ ä¼šæ”¶åˆ°æ‹¼å›¾ç¬¬ä¸€å…³çš„ç”»é¢æè¿°ï¼Œéƒ¨åˆ†è¯·æ±‚è¿˜ä¼šé™„å¸¦å·²ç»ç”Ÿæˆå®Œæˆçš„æ­£å¼å›¾ç‰‡ã€‚请综åˆå›¾ç‰‡å†…å®¹å’Œç”»é¢æè¿°ï¼ŒåŒæ—¶ç”Ÿæˆï¼š - 1 个适åˆç›´æŽ¥å±•示在游æˆå…³å¡å¡ç‰‡ä¸Šçš„中文关å¡å。 +- 1 段适åˆé»˜è®¤å¡«å…¥æ‹¼å›¾è‰ç¨¿çš„ä¸­æ–‡ä½œå“æè¿°ã€‚ +- 6 个适åˆä½œå“广场检索和相似推èçš„ä¸­æ–‡ä½œå“æ ‡ç­¾ã€‚ - 1 æ®µç”¨äºŽç”Ÿæˆ 9:16 拼图è¿è¡Œæ€ UI 纯背景图的中文正å‘视觉æç¤ºè¯ã€‚ 硬约æŸï¼š 1. åªè¾“出 JSON,ä¸è¦è¾“出 Markdownã€è§£é‡Šæˆ–代ç å—。 -2. JSON æ ¼å¼å¿…须是 {"levelName":"å…³å¡å","uiBackgroundPrompt":"æç¤ºè¯"}。 +2. JSON æ ¼å¼å¿…须是 {"levelName":"å…³å¡å","workDescription":"ä½œå“æè¿°","workTags":["标签1","标签2","标签3","标签4","标签5","标签6"],"uiBackgroundPrompt":"æç¤ºè¯"}。 3. levelName 必须是 2 到 8 个中文字符为主。 4. ä¸è¦è¾“出“第一关â€â€œç”»é¢â€â€œæ‹¼å›¾â€â€œä½œå“â€ç­‰æ³›è¯ã€‚ 5. ä¸è¦è¾“出标点ã€å¼•å·ã€ç¼–å·ã€è‹±æ–‡ã€emoji 或空白。 6. å…³å¡åè¦æŠ“ä½ç”»é¢ä¸»ä½“ã€åœºæ™¯å’Œæ°›å›´ï¼Œè¯»èµ·æ¥åƒä¸€ä¸ªå…·ä½“å¯çŽ©çš„å…³å¡ã€‚ -7. uiBackgroundPrompt 必须是 30 到 160 个中文字符,æè¿°é¢˜ææ°›å›´ã€çŽ¯å¢ƒã€è‰²å½©ã€å…‰å½±å’Œç©ºé—´å±‚次。 -8. uiBackgroundPrompt åªå†™æ­£å‘ç”»é¢æè¿°ï¼Œä¸è¦å†™è§„则说明,ä¸è¦å‡ºçŽ°æ‹¼å›¾æ§½ã€æ£‹ç›˜ã€HUDã€æŒ‰é’®ã€æ–‡å­—ã€æ°´å°ã€æ•°å­—ã€æ‹¼å›¾ç¢Žç‰‡ã€å®Œæ•´æ‹¼å›¾å›¾åƒæˆ–教程浮层。 +7. workDescription 必须是 18 到 80 个中文字符,æè¿°è¿™å¥—拼图的画é¢ä¸»é¢˜ã€æ°›å›´å’Œæ¸¸çŽ©æœŸå¾…ï¼Œä¸è¦å¤è¿°å­—段å。 +8. workTags 必须正好 6 个,æ¯ä¸ªæ ‡ç­¾ 2 到 6 个中文字符为主,覆盖题æã€ä¸»ä½“ã€æ°›å›´ã€åœºæ™¯ã€é£Žæ ¼å’Œæ‹¼å›¾è¾¨è¯†ç‚¹ã€‚ +9. uiBackgroundPrompt 必须是 30 到 160 个中文字符,æè¿°é¢˜ææ°›å›´ã€çŽ¯å¢ƒã€è‰²å½©ã€å…‰å½±å’Œç©ºé—´å±‚次。 +10. uiBackgroundPrompt åªå†™æ­£å‘ç”»é¢æè¿°ï¼Œä¸è¦å†™è§„则说明,ä¸è¦å‡ºçŽ°æ‹¼å›¾æ§½ã€æ£‹ç›˜ã€HUDã€æŒ‰é’®ã€æ–‡å­—ã€æ°´å°ã€æ•°å­—ã€æ‹¼å›¾ç¢Žç‰‡ã€å®Œæ•´æ‹¼å›¾å›¾åƒæˆ–教程浮层。 "#; pub(crate) fn build_puzzle_first_level_name_user_prompt(picture_description: &str) -> String { format!( - "ç”»é¢æè¿°ï¼š{picture_description}\n\n请生æˆç¬¬ä¸€å…³å…³å¡åå’Œ UI 背景æç¤ºè¯ã€‚", + "ç”»é¢æè¿°ï¼š{picture_description}\n\n请生æˆç¬¬ä¸€å…³å…³å¡åã€ä½œå“æè¿°ã€6 ä¸ªä½œå“æ ‡ç­¾å’Œ UI 背景æç¤ºè¯ã€‚", picture_description = picture_description.trim(), ) } pub(crate) fn build_puzzle_first_level_name_vision_user_text(picture_description: &str) -> String { format!( - "ç”»é¢æè¿°ï¼š{picture_description}\n\nè¯·è§‚å¯Ÿéšæ¶ˆæ¯é™„å¸¦çš„æ­£å¼æ‹¼å›¾å›¾ç‰‡ï¼Œç”Ÿæˆç¬¬ä¸€å…³å…³å¡åå’Œ UI 背景æç¤ºè¯ã€‚", + "ç”»é¢æè¿°ï¼š{picture_description}\n\nè¯·è§‚å¯Ÿéšæ¶ˆæ¯é™„å¸¦çš„æ­£å¼æ‹¼å›¾å›¾ç‰‡ï¼Œç”Ÿæˆç¬¬ä¸€å…³å…³å¡åã€ä½œå“æè¿°ã€6 ä¸ªä½œå“æ ‡ç­¾å’Œ UI 背景æç¤ºè¯ã€‚", picture_description = picture_description.trim(), ) } @@ -43,6 +47,8 @@ mod tests { assert!(prompt.contains("ç”»é¢æè¿°ï¼šä¸€åªçŒ«åœ¨é›¨å¤œç¯ç‰Œä¸‹å›žå¤´ã€‚")); assert!(prompt.contains("第一关关å¡å")); + assert!(prompt.contains("ä½œå“æè¿°")); + assert!(prompt.contains("6 ä¸ªä½œå“æ ‡ç­¾")); assert!(prompt.contains("UI 背景æç¤ºè¯")); } @@ -52,6 +58,8 @@ mod tests { assert!(prompt.contains("ç”»é¢æè¿°ï¼šä¸€åªçŒ«åœ¨é›¨å¤œç¯ç‰Œä¸‹å›žå¤´ã€‚")); assert!(prompt.contains("æ­£å¼æ‹¼å›¾å›¾ç‰‡")); + assert!(prompt.contains("ä½œå“æè¿°")); + assert!(prompt.contains("6 ä¸ªä½œå“æ ‡ç­¾")); assert!(prompt.contains("UI 背景æç¤ºè¯")); } } diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index 3f52e88d..3e2503ea 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -122,7 +122,9 @@ const PUZZLE_GENERATED_IMAGE_SIZE: &str = "1024*1024"; const PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE: &str = "1024x1024"; const VECTOR_ENGINE_PROVIDER: &str = "vector-engine"; const PUZZLE_LEVEL_NAME_VISION_IMAGE_MAX_SIDE: u32 = 768; +const PUZZLE_LEVEL_NAME_VISION_MAX_TOKENS: u32 = 512; const PUZZLE_REFERENCE_IMAGE_MAX_BYTES: usize = 8 * 1024 * 1024; +const PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT: usize = 5; const PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL: &str = "gpt-image-2"; const PUZZLE_UI_BACKGROUND_PROMPT_FALLBACK_MARKER: &str = "移动端拼图游æˆçº¯èƒŒæ™¯ï¼Œé¢˜ææ°›å›´æ¸…晰,ä¸åŒ…嫿‹¼å›¾æ§½æˆ– UI 元素"; @@ -718,16 +720,20 @@ pub async fn execute_puzzle_agent_action( .as_deref() .map(|value| value.chars().count()) .unwrap_or(0), - has_reference_image = payload - .reference_image_src - .as_deref() - .map(|value| !value.trim().is_empty()) - .unwrap_or(false), + has_reference_image = has_puzzle_reference_images( + payload.reference_image_src.as_deref(), + payload.reference_image_srcs.as_slice(), + ), "拼图 Agent action 开始执行" ); let (operation_type, phase_label, phase_detail, session) = match action.as_str() { "compile_puzzle_draft" => { let ai_redraw = payload.ai_redraw.unwrap_or(true); + let reference_image_sources = collect_puzzle_reference_image_sources( + payload.reference_image_src.as_deref(), + payload.reference_image_srcs.as_slice(), + ); + let primary_reference_image_src = reference_image_sources.first().map(String::as_str); let prompt_text = payload .picture_description .as_deref() @@ -760,7 +766,7 @@ pub async fn execute_puzzle_agent_action( compile_session_id.clone(), owner_user_id.clone(), prompt_text, - payload.reference_image_src.as_deref(), + primary_reference_image_src, payload.image_model.as_deref(), now, ) @@ -891,6 +897,12 @@ pub async fn execute_puzzle_agent_action( payload.prompt_text.as_deref(), &target_level.picture_description, ); + let reference_image_sources = collect_puzzle_reference_image_sources( + payload.reference_image_src.as_deref(), + payload.reference_image_srcs.as_slice(), + ); + let primary_reference_image_src = + reference_image_sources.first().map(String::as_str); // æ‹¼å›¾ç»“æžœé¡µä»Žå¤šå€™é€‰æŠ½å¡æ”¶å£ä¸ºå•图替æ¢ï¼Œå‰ç«¯ä¼ å…¥çš„æ—§ candidateCount åªåšå…¼å®¹å¿½ç•¥ã€‚ let candidate_count = 1; let candidate_start_index = target_level.candidates.len(); @@ -900,7 +912,7 @@ pub async fn execute_puzzle_agent_action( &session.session_id, &target_level.level_name, &prompt, - payload.reference_image_src.as_deref(), + primary_reference_image_src, payload.ai_redraw.unwrap_or(true), payload.image_model.as_deref(), candidate_count, @@ -934,7 +946,7 @@ pub async fn execute_puzzle_agent_action( &build_puzzle_levels_with_primary_update( &draft, &target_level, - payload.reference_image_src.as_deref(), + primary_reference_image_src, ), )?); let candidates_json = serde_json::to_string( @@ -985,7 +997,7 @@ pub async fn execute_puzzle_agent_action( ), target_level.level_id.as_str(), candidates.into_records(), - payload.reference_image_src.as_deref(), + primary_reference_image_src, now, )) } @@ -3067,6 +3079,8 @@ fn build_stable_puzzle_work_ids(session_id: &str) -> (String, String) { #[derive(Clone, Debug, Eq, PartialEq)] struct PuzzleLevelNaming { level_name: String, + work_description: Option, + work_tags: Vec, ui_background_prompt: Option, } @@ -3074,6 +3088,8 @@ impl PuzzleLevelNaming { fn fallback(picture_description: &str) -> Self { Self { level_name: build_fallback_puzzle_first_level_name(picture_description), + work_description: None, + work_tags: Vec::new(), ui_background_prompt: None, } } @@ -3150,7 +3166,7 @@ async fn generate_puzzle_first_level_name_from_image( ]), ]) .with_model(PUZZLE_LEVEL_NAME_VISION_LLM_MODEL) - .with_max_tokens(80), + .with_max_tokens(PUZZLE_LEVEL_NAME_VISION_MAX_TOKENS), ) .await; @@ -3217,6 +3233,9 @@ fn parse_puzzle_level_naming_from_text(text: &str) -> Option trimmed }; let parsed = serde_json::from_str::(json_text).ok(); + if parsed.is_none() && looks_like_puzzle_json_fragment(trimmed) { + return None; + } let raw_name = parsed .as_ref() .and_then(|value| value.get("levelName").and_then(Value::as_str)) @@ -3227,12 +3246,21 @@ fn parse_puzzle_level_naming_from_text(text: &str) -> Option }) .unwrap_or(trimmed); let level_name = normalize_puzzle_first_level_name(raw_name)?; + let work_description = parsed + .as_ref() + .and_then(parse_puzzle_generated_work_description_field); + let work_tags = parsed + .as_ref() + .and_then(parse_puzzle_generated_work_tags_field) + .unwrap_or_default(); let ui_background_prompt = parsed .as_ref() .and_then(parse_puzzle_ui_background_prompt_field); Some(PuzzleLevelNaming { level_name, + work_description, + work_tags, ui_background_prompt, }) } @@ -3250,6 +3278,55 @@ fn parse_puzzle_ui_background_prompt_field(value: &Value) -> Option { .and_then(normalize_puzzle_generated_ui_background_prompt) } +fn parse_puzzle_generated_work_description_field(value: &Value) -> Option { + value + .get("workDescription") + .and_then(Value::as_str) + .or_else(|| value.get("work_description").and_then(Value::as_str)) + .and_then(normalize_puzzle_generated_work_description) +} + +fn normalize_puzzle_generated_work_description(value: &str) -> Option { + let normalized = value + .trim() + .trim_matches(|ch: char| { + ch.is_ascii_punctuation() + || matches!( + ch, + ',' | '。' | 'ã€' | 'ï¼›' | ':' | 'ï¼' | '?' | '“' | 'â€' | '《' | '》' + ) + }) + .split_whitespace() + .collect::>() + .join(""); + let description = normalized.chars().take(80).collect::(); + (description.chars().count() >= 8 && !looks_like_puzzle_json_field_name(&description)) + .then_some(description) +} + +fn parse_puzzle_generated_work_tags_field(value: &Value) -> Option> { + let tags_value = value + .get("workTags") + .or_else(|| value.get("work_tags")) + .or_else(|| value.get("themeTags")) + .or_else(|| value.get("theme_tags")) + .or_else(|| value.get("tags"))?; + let raw_tags = match tags_value { + Value::Array(items) => items + .iter() + .filter_map(Value::as_str) + .map(ToString::to_string) + .collect::>(), + Value::String(text) => text + .split([',', ',', 'ã€', '\n', '|', '/']) + .map(ToString::to_string) + .collect::>(), + _ => Vec::new(), + }; + let tags = normalize_puzzle_generated_work_tag_candidates(raw_tags); + (tags.len() == module_puzzle::PUZZLE_MAX_TAG_COUNT).then_some(tags) +} + fn normalize_puzzle_generated_ui_background_prompt(value: &str) -> Option { let normalized = value .trim() @@ -3331,6 +3408,7 @@ fn normalize_puzzle_first_level_name(value: &str) -> Option { normalized.as_str(), "第一关" | "ç”»é¢" | "拼图" | "作å“" | "å…³å¡" ) + && !looks_like_puzzle_json_field_name(&normalized) { Some(normalized) } else { @@ -3338,6 +3416,52 @@ fn normalize_puzzle_first_level_name(value: &str) -> Option { } } +fn looks_like_puzzle_json_field_name(value: &str) -> bool { + let normalized = value.trim().trim_matches(|ch: char| { + ch.is_ascii_punctuation() + || matches!( + ch, + ',' | '。' | 'ã€' | 'ï¼›' | ':' | 'ï¼' | '?' | '“' | 'â€' | '《' | '》' + ) + }); + let compact = normalized.to_ascii_lowercase().replace('_', ""); + matches!(compact.as_str(), "levelnam" | "levelname") + || [ + "levelname", + "workdescription", + "worktags", + "themetags", + "uibackgroundprompt", + ] + .iter() + .any(|field| { + compact == *field + || (compact.len() >= 6 && field.starts_with(compact.as_str())) + || compact.starts_with(field) + }) +} + +fn looks_like_puzzle_json_fragment(value: &str) -> bool { + let trimmed = value.trim(); + if trimmed.starts_with('{') || trimmed.starts_with('[') { + return true; + } + let lower = trimmed.to_ascii_lowercase(); + [ + "\"levelnam", + "\"levelname\"", + "\"level_name\"", + "\"workdescription\"", + "\"work_description\"", + "\"worktags\"", + "\"work_tags\"", + "\"uibackgroundprompt\"", + "\"ui_background_prompt\"", + ] + .iter() + .any(|field| lower.contains(field)) +} + fn strip_puzzle_level_name_generic_words(mut value: String) -> String { for prefix in ["第一关", "å…³å¡å", "å…³å¡"] { value = value.trim_start_matches(prefix).to_string(); @@ -3406,6 +3530,28 @@ fn build_puzzle_levels_with_primary_update( levels } +fn attach_selected_puzzle_candidate_to_levels( + levels: &mut [PuzzleDraftLevelRecord], + target_level_id: &str, + candidate: &PuzzleGeneratedImageCandidateRecord, +) { + if let Some(index) = levels + .iter() + .position(|level| level.level_id == target_level_id) + .or_else(|| (!levels.is_empty()).then_some(0)) + { + let level = &mut levels[index]; + level.candidates.clear(); + let mut candidate = candidate.clone(); + candidate.selected = true; + level.selected_candidate_id = Some(candidate.candidate_id.clone()); + level.cover_image_src = Some(candidate.image_src.clone()); + level.cover_asset_id = Some(candidate.asset_id.clone()); + level.candidates.push(candidate); + level.generation_status = "ready".to_string(); + } +} + fn resolve_puzzle_initial_ui_background_prompt( draft: &PuzzleResultDraftRecord, target_level: &PuzzleDraftLevelRecord, @@ -3596,7 +3742,8 @@ async fn compile_puzzle_draft_with_initial_cover( ); let (generated_naming, candidates_result) = tokio::join!(level_name_future, candidates_future); target_level.level_name = generated_naming.level_name.clone(); - target_level.ui_background_prompt = generated_naming.ui_background_prompt; + target_level.ui_background_prompt = generated_naming.ui_background_prompt.clone(); + let mut generated_metadata = generated_naming; let candidates = candidates_result?; let selected_candidate_id = candidates .iter() @@ -3620,6 +3767,14 @@ async fn compile_puzzle_draft_with_initial_cover( if refined_naming.ui_background_prompt.is_some() { target_level.ui_background_prompt = refined_naming.ui_background_prompt; } + if refined_naming.work_description.is_some() { + generated_metadata.work_description = refined_naming.work_description; + } + if refined_naming.work_tags.len() == module_puzzle::PUZZLE_MAX_TAG_COUNT { + generated_metadata.work_tags = refined_naming.work_tags; + } + generated_metadata.level_name = target_level.level_name.clone(); + generated_metadata.ui_background_prompt = target_level.ui_background_prompt.clone(); } let generated_level_name = target_level.level_name.clone(); let mut updated_levels = @@ -3639,6 +3794,17 @@ async fn compile_puzzle_draft_with_initial_cover( ui_prompt, ui_background, ); + if let Some(selected_candidate) = candidates + .iter() + .find(|candidate| candidate.record.selected) + .or_else(|| candidates.first()) + { + attach_selected_puzzle_candidate_to_levels( + &mut updated_levels, + target_level.level_id.as_str(), + &selected_candidate.record, + ); + } let ready_level = find_puzzle_level_for_initial_asset_check(&updated_levels, target_level.level_id.as_str()) .ok_or_else(|| { @@ -3650,6 +3816,28 @@ async fn compile_puzzle_draft_with_initial_cover( ensure_puzzle_initial_level_assets_ready(ready_level)?; let levels_json_with_generated_name = Some(serialize_puzzle_level_records_for_module(&updated_levels)?); + let work_title = if draft.work_title.trim().is_empty() + || draft.work_title.trim() == fallback_level_name.trim() + { + generated_level_name.clone() + } else { + draft.work_title.clone() + }; + let work_description = if draft.work_description.trim().is_empty() { + generated_metadata + .work_description + .clone() + .unwrap_or_else(|| draft.work_description.clone()) + } else { + draft.work_description.clone() + }; + let theme_tags = if draft.theme_tags.is_empty() + && generated_metadata.work_tags.len() == module_puzzle::PUZZLE_MAX_TAG_COUNT + { + generated_metadata.work_tags.clone() + } else { + draft.theme_tags.clone() + }; let candidates_json = serde_json::to_string( &candidates .iter() @@ -3707,6 +3895,43 @@ async fn compile_puzzle_draft_with_initial_cover( Err(error) } })?; + let (_, profile_id) = build_stable_puzzle_work_ids(&compiled_session.session_id); + match state + .spacetime_client() + .update_puzzle_work(PuzzleWorkUpsertRecordInput { + profile_id, + owner_user_id: owner_user_id.clone(), + work_title, + work_description: work_description.clone(), + level_name: generated_level_name.clone(), + summary: work_description, + theme_tags, + cover_image_src: ready_level.cover_image_src.clone(), + cover_asset_id: ready_level.cover_asset_id.clone(), + levels_json: levels_json_with_generated_name.clone(), + updated_at_micros: now, + }) + .await + .map_err(map_puzzle_client_error) + { + Ok(_) => {} + Err(error) if is_spacetimedb_connectivity_app_error(&error) => { + tracing::warn!( + provider = PUZZLE_AGENT_API_BASE_PROVIDER, + session_id = %compiled_session.session_id, + owner_user_id = %owner_user_id, + message = %error.body_text(), + "拼图首图生æˆåŽä½œå“å…ƒä¿¡æ¯æŠ•å½±å›žå†™ä¸å¯ç”¨ï¼Œç»§ç»­ä½¿ç”¨ä¼šè¯è‰ç¨¿å¿«ç…§" + ); + } + Err(error) => return Err(error), + } + let saved_session = apply_generated_puzzle_initial_metadata_to_session_snapshot( + saved_session, + &generated_metadata, + fallback_level_name.as_str(), + now, + ); if save_used_fallback { return Ok(saved_session); } @@ -3721,7 +3946,12 @@ async fn compile_puzzle_draft_with_initial_cover( }) .await { - Ok(session) => Ok(session), + Ok(session) => Ok(apply_generated_puzzle_initial_metadata_to_session_snapshot( + session, + &generated_metadata, + fallback_level_name.as_str(), + now, + )), Err(error) if should_skip_asset_operation_billing_for_connectivity(&error) => { tracing::warn!( provider = PUZZLE_AGENT_API_BASE_PROVIDER, @@ -3821,9 +4051,18 @@ async fn compile_puzzle_draft_with_uploaded_cover( if refined_naming.ui_background_prompt.is_some() { generated_naming.ui_background_prompt = refined_naming.ui_background_prompt; } + if refined_naming.work_description.is_some() { + generated_naming.work_description = refined_naming.work_description; + } + if refined_naming.work_tags.len() == module_puzzle::PUZZLE_MAX_TAG_COUNT { + generated_naming.work_tags = refined_naming.work_tags; + } } - target_level.level_name = generated_naming.level_name; - target_level.ui_background_prompt = generated_naming.ui_background_prompt; + target_level.level_name = generated_naming.level_name.clone(); + target_level.ui_background_prompt = generated_naming.ui_background_prompt.clone(); + let mut generated_metadata = generated_naming; + generated_metadata.level_name = target_level.level_name.clone(); + generated_metadata.ui_background_prompt = target_level.ui_background_prompt.clone(); let generated_level_name = target_level.level_name.clone(); let persisted_upload = persisted_upload_result?; let mut updated_levels = @@ -3843,6 +4082,19 @@ async fn compile_puzzle_draft_with_uploaded_cover( ui_prompt, ui_background, ); + attach_selected_puzzle_candidate_to_levels( + &mut updated_levels, + target_level.level_id.as_str(), + &PuzzleGeneratedImageCandidateRecord { + candidate_id: candidate_id.clone(), + image_src: persisted_upload.image_src.clone(), + asset_id: persisted_upload.asset_id.clone(), + prompt: image_prompt.clone(), + actual_prompt: None, + source_type: "uploaded".to_string(), + selected: true, + }, + ); let ready_level = find_puzzle_level_for_initial_asset_check(&updated_levels, target_level.level_id.as_str()) .ok_or_else(|| { @@ -3854,6 +4106,28 @@ async fn compile_puzzle_draft_with_uploaded_cover( ensure_puzzle_initial_level_assets_ready(ready_level)?; let levels_json_with_generated_name = Some(serialize_puzzle_level_records_for_module(&updated_levels)?); + let work_title = if draft.work_title.trim().is_empty() + || draft.work_title.trim() == fallback_level_name.trim() + { + generated_level_name.clone() + } else { + draft.work_title.clone() + }; + let work_description = if draft.work_description.trim().is_empty() { + generated_metadata + .work_description + .clone() + .unwrap_or_else(|| draft.work_description.clone()) + } else { + draft.work_description.clone() + }; + let theme_tags = if draft.theme_tags.is_empty() + && generated_metadata.work_tags.len() == module_puzzle::PUZZLE_MAX_TAG_COUNT + { + generated_metadata.work_tags.clone() + } else { + draft.theme_tags.clone() + }; let candidate = PuzzleGeneratedImageCandidateRecord { candidate_id: candidate_id.clone(), image_src: persisted_upload.image_src, @@ -3916,6 +4190,43 @@ async fn compile_puzzle_draft_with_uploaded_cover( Err(error) } })?; + let (_, profile_id) = build_stable_puzzle_work_ids(&compiled_session.session_id); + match state + .spacetime_client() + .update_puzzle_work(PuzzleWorkUpsertRecordInput { + profile_id, + owner_user_id: owner_user_id.clone(), + work_title, + work_description: work_description.clone(), + level_name: generated_level_name.clone(), + summary: work_description, + theme_tags, + cover_image_src: ready_level.cover_image_src.clone(), + cover_asset_id: ready_level.cover_asset_id.clone(), + levels_json: levels_json_with_generated_name.clone(), + updated_at_micros: now, + }) + .await + .map_err(map_puzzle_client_error) + { + Ok(_) => {} + Err(error) if is_spacetimedb_connectivity_app_error(&error) => { + tracing::warn!( + provider = PUZZLE_AGENT_API_BASE_PROVIDER, + session_id = %compiled_session.session_id, + owner_user_id = %owner_user_id, + message = %error.body_text(), + "拼图上传图è‰ç¨¿ä½œå“å…ƒä¿¡æ¯æŠ•å½±å›žå†™ä¸å¯ç”¨ï¼Œç»§ç»­ä½¿ç”¨ä¼šè¯è‰ç¨¿å¿«ç…§" + ); + } + Err(error) => return Err(error), + } + let saved_session = apply_generated_puzzle_initial_metadata_to_session_snapshot( + saved_session, + &generated_metadata, + fallback_level_name.as_str(), + now, + ); if save_used_fallback { return Ok(saved_session); } @@ -3930,7 +4241,12 @@ async fn compile_puzzle_draft_with_uploaded_cover( }) .await { - Ok(session) => Ok(session), + Ok(session) => Ok(apply_generated_puzzle_initial_metadata_to_session_snapshot( + session, + &generated_metadata, + fallback_level_name.as_str(), + now, + )), Err(error) if should_skip_asset_operation_billing_for_connectivity(&error) => { tracing::warn!( provider = PUZZLE_AGENT_API_BASE_PROVIDER, @@ -4046,6 +4362,53 @@ fn apply_generated_puzzle_first_level_name_to_session_snapshot( session } +fn apply_generated_puzzle_initial_metadata_to_session_snapshot( + mut session: PuzzleAgentSessionRecord, + metadata: &PuzzleLevelNaming, + previous_level_name: &str, + updated_at_micros: i64, +) -> PuzzleAgentSessionRecord { + let Some(draft) = session.draft.as_mut() else { + return session; + }; + apply_generated_puzzle_initial_metadata_to_draft( + draft, + metadata, + previous_level_name, + updated_at_micros, + ); + session.updated_at = format_timestamp_micros(updated_at_micros); + session +} + +fn apply_generated_puzzle_initial_metadata_to_draft( + draft: &mut PuzzleResultDraftRecord, + metadata: &PuzzleLevelNaming, + previous_level_name: &str, + _updated_at_micros: i64, +) { + let should_default_work_title = + draft.work_title.trim().is_empty() || draft.work_title.trim() == previous_level_name.trim(); + if should_default_work_title { + draft.work_title = metadata.level_name.clone(); + } + + if draft.work_description.trim().is_empty() + && let Some(description) = metadata.work_description.as_ref() + { + draft.work_description = description.clone(); + draft.summary = description.clone(); + } + + if draft.theme_tags.is_empty() + && metadata.work_tags.len() == module_puzzle::PUZZLE_MAX_TAG_COUNT + { + draft.theme_tags = metadata.work_tags.clone(); + } + + sync_puzzle_primary_draft_fields_from_level(draft); +} + fn sync_puzzle_primary_draft_fields_from_level(draft: &mut PuzzleResultDraftRecord) { let Some(primary_level) = draft.levels.first() else { return; @@ -4056,6 +4419,16 @@ fn sync_puzzle_primary_draft_fields_from_level(draft: &mut PuzzleResultDraftReco draft.cover_image_src = primary_level.cover_image_src.clone(); draft.cover_asset_id = primary_level.cover_asset_id.clone(); draft.generation_status = primary_level.generation_status.clone(); + draft.summary = draft.work_description.clone(); + if draft.form_draft.is_some() { + draft.form_draft = Some(PuzzleFormDraftRecord { + work_title: (!draft.work_title.trim().is_empty()).then_some(draft.work_title.clone()), + work_description: (!draft.work_description.trim().is_empty()) + .then_some(draft.work_description.clone()), + picture_description: (!primary_level.picture_description.trim().is_empty()) + .then_some(primary_level.picture_description.clone()), + }); + } } fn replace_puzzle_session_draft_snapshot( @@ -4170,6 +4543,9 @@ where { let mut tags = Vec::new(); for candidate in candidates { + if looks_like_puzzle_json_field_name(candidate.as_ref()) { + continue; + } let normalized = normalize_puzzle_tag(candidate.as_ref()); if normalized.is_empty() || tags.iter().any(|tag| tag == &normalized) { continue; @@ -4190,6 +4566,29 @@ where tags } +fn normalize_puzzle_generated_work_tag_candidates( + candidates: impl IntoIterator, +) -> Vec +where + S: AsRef, +{ + let mut tags = Vec::new(); + for candidate in candidates { + let normalized = normalize_puzzle_tag(candidate.as_ref()); + if normalized.is_empty() + || looks_like_puzzle_json_field_name(&normalized) + || tags.iter().any(|tag| tag == &normalized) + { + continue; + } + tags.push(normalized); + if tags.len() >= module_puzzle::PUZZLE_MAX_TAG_COUNT { + break; + } + } + tags +} + fn normalize_puzzle_tag(value: &str) -> String { value .trim() @@ -4931,6 +5330,26 @@ mod tests { )); } + #[test] + fn puzzle_reference_image_sources_are_deduped_and_limited() { + let sources = collect_puzzle_reference_image_sources( + Some("data:image/png;base64,a"), + &[ + "data:image/png;base64,a".to_string(), + "data:image/png;base64,b".to_string(), + "data:image/png;base64,c".to_string(), + "data:image/png;base64,d".to_string(), + "data:image/png;base64,e".to_string(), + "data:image/png;base64,f".to_string(), + ], + ); + + assert_eq!(sources.len(), 5); + assert_eq!(sources[0], "data:image/png;base64,a"); + assert_eq!(sources[1], "data:image/png;base64,b"); + assert!(!sources.contains(&"data:image/png;base64,f".to_string())); + } + #[test] fn puzzle_vector_engine_timeout_maps_to_gateway_timeout() { let error = map_puzzle_vector_engine_request_error( @@ -5008,6 +5427,7 @@ mod tests { action: "generate_puzzle_images".to_string(), prompt_text: None, reference_image_src: None, + reference_image_srcs: Vec::new(), image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()), ai_redraw: None, candidate_count: Some(1), @@ -5055,16 +5475,28 @@ mod tests { parse_puzzle_first_level_name_from_text(r#"{"levelName":"雨夜猫街画é¢"}"#), Some("雨夜猫街".to_string()) ); + assert_eq!( + parse_puzzle_first_level_name_from_text(r#"{"levelNam"#), + None + ); } #[test] - fn puzzle_level_naming_parser_accepts_ui_background_prompt() { + fn puzzle_level_naming_parser_accepts_metadata_and_ui_background_prompt() { let naming = parse_puzzle_level_naming_from_text( - r#"{"levelName":"雨夜猫街","uiBackgroundPrompt":"雨夜è€è¡—延展æˆç«–å±ç©ºé—´ï¼Œæ¹¿æ¶¦çŸ³æ¿è·¯å€’映暖色ç¯ç‰Œï¼Œè¿œå¤„屋æªå’Œè–„é›¾å½¢æˆæŸ”和层次"}"#, + r#"{"levelName":"雨夜猫街","workDescription":"在湿润ç¯ç‰Œä¸ŽçŒ«å½±ä¹‹é—´å®Œæˆä¸€å¥—雨夜街角拼图。","workTags":["雨夜","猫咪","ç¯ç‰Œ","è¡—è§’","暖色","æ’ç”»"],"uiBackgroundPrompt":"雨夜è€è¡—延展æˆç«–å±ç©ºé—´ï¼Œæ¹¿æ¶¦çŸ³æ¿è·¯å€’映暖色ç¯ç‰Œï¼Œè¿œå¤„屋æªå’Œè–„é›¾å½¢æˆæŸ”和层次"}"#, ) .expect("naming should parse"); assert_eq!(naming.level_name, "雨夜猫街"); + assert_eq!( + naming.work_description.as_deref(), + Some("在湿润ç¯ç‰Œä¸ŽçŒ«å½±ä¹‹é—´å®Œæˆä¸€å¥—雨夜街角拼图") + ); + assert_eq!(naming.work_tags.len(), module_puzzle::PUZZLE_MAX_TAG_COUNT); + assert!(naming.work_tags.contains(&"雨夜".to_string())); + assert!(naming.work_tags.contains(&"猫咪".to_string())); + assert!(naming.work_tags.contains(&"ç¯ç‰Œ".to_string())); assert_eq!( naming.ui_background_prompt.as_deref(), Some("雨夜è€è¡—延展æˆç«–å±ç©ºé—´ï¼Œæ¹¿æ¶¦çŸ³æ¿è·¯å€’映暖色ç¯ç‰Œï¼Œè¿œå¤„屋æªå’Œè–„é›¾å½¢æˆæŸ”和层次") @@ -5139,6 +5571,7 @@ mod tests { action: "generate_puzzle_images".to_string(), prompt_text: None, reference_image_src: None, + reference_image_srcs: Vec::new(), image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()), ai_redraw: None, candidate_count: Some(1), @@ -5173,6 +5606,61 @@ mod tests { assert_eq!(draft.levels[0].level_name, "雨夜猫街"); } + #[test] + fn puzzle_initial_metadata_defaults_empty_work_description_and_tags() { + let mut session = PuzzleAgentSessionRecord { + session_id: "puzzle-session-1".to_string(), + seed_text: "ç”»é¢æè¿°ï¼šä¸€åªçŒ«åœ¨é›¨å¤œç¯ç‰Œä¸‹å›žå¤´ã€‚".to_string(), + current_turn: 1, + progress_percent: 94, + stage: "ready_to_publish".to_string(), + anchor_pack: test_puzzle_anchor_pack_record(), + draft: Some(test_puzzle_draft_record()), + messages: Vec::new(), + last_assistant_reply: None, + published_profile_id: None, + suggested_actions: Vec::new(), + result_preview: None, + updated_at: "2024-01-01T00:00:00Z".to_string(), + }; + { + let draft = session.draft.as_mut().expect("draft"); + draft.work_title = "猫画é¢".to_string(); + draft.work_description = String::new(); + draft.summary = String::new(); + draft.theme_tags = Vec::new(); + } + let metadata = PuzzleLevelNaming { + level_name: "雨夜猫街".to_string(), + work_description: Some("在湿润ç¯ç‰Œä¸ŽçŒ«å½±ä¹‹é—´å®Œæˆä¸€å¥—雨夜街角拼图".to_string()), + work_tags: vec![ + "æ’ç”»".to_string(), + "ç¯ç‰Œ".to_string(), + "è¡—è§’".to_string(), + "猫咪".to_string(), + "暖色".to_string(), + "雨夜".to_string(), + ], + ui_background_prompt: None, + }; + + let session = apply_generated_puzzle_initial_metadata_to_session_snapshot( + session, + &metadata, + "猫画é¢", + 1_713_686_401_234_568, + ); + + let draft = session.draft.expect("draft"); + assert_eq!(draft.work_title, "雨夜猫街"); + assert_eq!( + draft.work_description, + "在湿润ç¯ç‰Œä¸ŽçŒ«å½±ä¹‹é—´å®Œæˆä¸€å¥—雨夜街角拼图" + ); + assert_eq!(draft.summary, draft.work_description); + assert_eq!(draft.theme_tags, metadata.work_tags); + } + #[test] fn puzzle_level_audio_asset_roundtrips_between_response_and_module_json() { let level = PuzzleDraftLevelResponse { @@ -5981,6 +6469,40 @@ fn has_puzzle_reference_image(reference_image_src: Option<&str>) -> bool { .unwrap_or(false) } +fn collect_puzzle_reference_image_sources( + legacy_reference_image_src: Option<&str>, + reference_image_srcs: &[String], +) -> Vec { + let mut sources = Vec::new(); + for source in legacy_reference_image_src + .into_iter() + .chain(reference_image_srcs.iter().map(String::as_str)) + { + let normalized = source.trim(); + if normalized.is_empty() { + continue; + } + if !sources + .iter() + .any(|existing: &String| existing == normalized) + { + sources.push(normalized.to_string()); + } + if sources.len() >= PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT { + break; + } + } + sources +} + +fn has_puzzle_reference_images( + legacy_reference_image_src: Option<&str>, + reference_image_srcs: &[String], +) -> bool { + !collect_puzzle_reference_image_sources(legacy_reference_image_src, reference_image_srcs) + .is_empty() +} + fn should_use_puzzle_reference_image_edit( reference_image_src: Option<&str>, use_reference_image_edit: bool, diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index 66d1d47c..acd18718 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -476,6 +476,10 @@ mod tests { RuntimeProfileWalletLedgerSourceType::SnapshotSync.as_str(), "snapshot_sync" ); + assert_eq!( + RuntimeProfileWalletLedgerSourceType::NewUserRegistrationReward.as_str(), + "new_user_registration_reward" + ); assert_eq!( RuntimeProfileWalletLedgerSourceType::PointsRecharge.as_str(), "points_recharge" @@ -494,6 +498,19 @@ mod tests { ); } + #[test] + fn new_user_registration_wallet_reward_starts_with_ten_points() { + assert_eq!(PROFILE_NEW_USER_INITIAL_WALLET_POINTS, 10); + assert_eq!( + calculate_runtime_profile_wallet_balance( + 0, + PROFILE_NEW_USER_INITIAL_WALLET_POINTS as i64, + ) + .expect("new user registration reward should fit wallet balance"), + 10 + ); + } + #[test] fn runtime_profile_beijing_day_key_uses_business_day_boundary() { let before_beijing_midnight = 1_714_927_999_999_999; diff --git a/server-rs/crates/shared-contracts/src/puzzle_agent.rs b/server-rs/crates/shared-contracts/src/puzzle_agent.rs index 0276dccb..32f79da4 100644 --- a/server-rs/crates/shared-contracts/src/puzzle_agent.rs +++ b/server-rs/crates/shared-contracts/src/puzzle_agent.rs @@ -16,6 +16,8 @@ pub struct CreatePuzzleAgentSessionRequest { #[serde(default)] pub reference_image_src: Option, #[serde(default)] + pub reference_image_srcs: Vec, + #[serde(default)] pub image_model: Option, #[serde(default)] pub ai_redraw: Option, @@ -39,6 +41,8 @@ pub struct ExecutePuzzleAgentActionRequest { #[serde(default)] pub reference_image_src: Option, #[serde(default)] + pub reference_image_srcs: Vec, + #[serde(default)] pub image_model: Option, #[serde(default)] pub ai_redraw: Option, diff --git a/server-rs/crates/spacetime-module/src/match3d/mod.rs b/server-rs/crates/spacetime-module/src/match3d/mod.rs index 754d7f2d..e37c3dd1 100644 --- a/server-rs/crates/spacetime-module/src/match3d/mod.rs +++ b/server-rs/crates/spacetime-module/src/match3d/mod.rs @@ -544,6 +544,16 @@ fn update_match3d_work_tx( input: Match3DWorkUpdateInput, ) -> Result { let current = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?; + let next = build_updated_match3d_work_row(¤t, &input)?; + let snapshot = build_work_snapshot(&next)?; + replace_work(ctx, ¤t, next); + Ok(snapshot) +} + +fn build_updated_match3d_work_row( + current: &Match3DWorkProfileRow, + input: &Match3DWorkUpdateInput, +) -> Result { let tags = parse_tags(&input.tags_json)?; let config = Match3DCreatorConfigSnapshot { theme_text: clean_string(&input.theme_text, "ç»å…¸æ¶ˆé™¤"), @@ -563,7 +573,7 @@ fn update_match3d_work_tx( author_display_name: current.author_display_name.clone(), game_name: clean_string(&input.game_name, "æœªå‘½åæŠ“大鹅"), theme_text: config.theme_text.clone(), - summary_text: clean_string(&input.summary_text, "ç»å…¸æ¶ˆé™¤çŽ©æ³•"), + summary_text: input.summary_text.trim().to_string(), tags_json: to_json_string(&tags), cover_image_src: input.cover_image_src.trim().to_string(), cover_asset_id: input.cover_asset_id.trim().to_string(), @@ -576,9 +586,7 @@ fn update_match3d_work_tx( published_at: current.published_at, generated_item_assets_json: current.generated_item_assets_json.clone(), }; - let snapshot = build_work_snapshot(&next)?; - replace_work(ctx, ¤t, next); - Ok(snapshot) + Ok(next) } fn publish_match3d_work_tx( @@ -1881,6 +1889,65 @@ mod tests { ); } + #[test] + fn match3d_work_update_preserves_assets_and_allows_empty_summary() { + let existing = Match3DWorkProfileRow { + profile_id: "profile-1".to_string(), + owner_user_id: "user-1".to_string(), + source_session_id: "session-1".to_string(), + author_display_name: "作者".to_string(), + game_name: "水果抓大鹅".to_string(), + theme_text: "æ°´æžœ".to_string(), + summary_text: "ä¿ç•™æè¿°".to_string(), + tags_json: "[\"æ°´æžœ\"]".to_string(), + cover_image_src: "/old-cover.png".to_string(), + cover_asset_id: "cover-asset-1".to_string(), + clear_count: 12, + difficulty: 4, + config_json: to_json_string(&Match3DCreatorConfigSnapshot { + theme_text: "æ°´æžœ".to_string(), + reference_image_src: None, + clear_count: 12, + difficulty: 4, + asset_style_id: None, + asset_style_label: None, + asset_style_prompt: None, + generate_click_sound: false, + }), + publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(), + play_count: 2, + updated_at: Timestamp::from_micros_since_unix_epoch(1), + published_at: None, + generated_item_assets_json: Some( + r#"[{"itemId":"match3d-item-1","itemName":"è‰èŽ“","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"# + .to_string(), + ), + }; + let input = Match3DWorkUpdateInput { + profile_id: existing.profile_id.clone(), + owner_user_id: existing.owner_user_id.clone(), + game_name: existing.game_name.clone(), + theme_text: existing.theme_text.clone(), + summary_text: " ".to_string(), + tags_json: existing.tags_json.clone(), + cover_image_src: "/new-cover.png".to_string(), + cover_asset_id: existing.cover_asset_id.clone(), + clear_count: existing.clear_count, + difficulty: existing.difficulty, + updated_at_micros: 2, + }; + let next = build_updated_match3d_work_row(&existing, &input).unwrap(); + + assert_eq!(next.summary_text, ""); + assert_eq!(next.cover_image_src, "/new-cover.png"); + assert_eq!(next.clear_count, 12); + assert_eq!(next.difficulty, 4); + assert_eq!( + next.generated_item_assets_json.as_deref(), + existing.generated_item_assets_json.as_deref() + ); + } + #[test] fn match3d_publish_ready_requires_five_image_views_per_item() { let base_work = Match3DWorkProfileRow { diff --git a/spacetime.local.json b/spacetime.local.json index 60c2b4c2..77ed651c 100644 --- a/spacetime.local.json +++ b/spacetime.local.json @@ -1,3 +1,3 @@ { - "database": "genarrative-dev-edu" + "database": "xushi-p4wfr" } diff --git a/src/components/CustomWorldGenerationView.test.tsx b/src/components/CustomWorldGenerationView.test.tsx new file mode 100644 index 00000000..0e589b13 --- /dev/null +++ b/src/components/CustomWorldGenerationView.test.tsx @@ -0,0 +1,129 @@ +/* @vitest-environment jsdom */ + +import { render, screen } from '@testing-library/react'; +import { describe, expect, test } from 'vitest'; + +import type { CustomWorldGenerationProgress } from '../../packages/shared/src/contracts/runtime'; +import { CustomWorldGenerationView } from './CustomWorldGenerationView'; + +function createProgress( + overrides: Partial = {}, +): CustomWorldGenerationProgress { + return { + phaseId: 'draft_foundation', + phaseLabel: 'æ•´ç†è‰ç¨¿', + phaseDetail: '正在整ç†å½“å‰ç”Ÿæˆæ­¥éª¤ã€‚', + batchLabel: '第 2 批', + overallProgress: 42, + completedWeight: 21, + totalWeight: 50, + elapsedMs: 125_000, + estimatedRemainingMs: 75_000, + activeStepIndex: 1, + steps: [ + { + id: 'step-1', + label: '收集设定', + detail: 'æ•´ç†åˆå§‹è¾“入。', + completed: 1, + total: 1, + status: 'completed', + }, + { + id: 'step-2', + label: '编译è‰ç¨¿', + detail: '生æˆé¦–版结构。', + completed: 2, + total: 4, + status: 'active', + }, + { + id: 'step-3', + label: '写回结果', + detail: 'åŒæ­¥ç»“果页。', + completed: 0, + total: 4, + status: 'pending', + }, + ], + ...overrides, + }; +} + +describe('CustomWorldGenerationView', () => { + test.each(['拼图è‰ç¨¿ç”Ÿæˆè¿›åº¦', '抓大鹅è‰ç¨¿ç”Ÿæˆè¿›åº¦'])( + 'hides batch module and keeps wait/timer in one row for %s', + (progressTitle) => { + render( + {}} + onEditSetting={() => {}} + onRetry={() => {}} + settingDescription={null} + settingActionLabel={null} + progressTitle={progressTitle} + />, + ); + + expect(screen.queryByText('当剿‰¹æ¬¡')).toBeNull(); + expect(screen.getByText('预计等待')).toBeTruthy(); + expect(screen.getByText('计时')).toBeTruthy(); + + const statsNode = screen + .getByText('预计等待') + .closest('.custom-world-generation-stats'); + expect(statsNode?.className).toContain( + 'custom-world-generation-stats--two-column', + ); + expect(statsNode?.getAttribute('style')).toContain( + 'grid-template-columns: repeat(2, minmax(0, 1fr))', + ); + + const stepNodes = [ + screen.getByText('收集设定'), + screen.getByText('编译è‰ç¨¿'), + screen.getByText('写回结果'), + ].map((node) => node.closest('.custom-world-generation-step')); + + expect(stepNodes.every(Boolean)).toBe(true); + expect(stepNodes[0]?.getAttribute('style')).toContain( + '--generation-step-delay: 0ms', + ); + expect(stepNodes[1]?.getAttribute('style')).toContain( + '--generation-step-delay: 90ms', + ); + expect(stepNodes[2]?.getAttribute('style')).toContain( + '--generation-step-delay: 180ms', + ); + }, + ); + + test('keeps batch module for other generation pages', () => { + render( + {}} + onEditSetting={() => {}} + onRetry={() => {}} + settingDescription={null} + settingActionLabel={null} + progressTitle="大鱼åƒå°é±¼è‰ç¨¿ç”Ÿæˆè¿›åº¦" + />, + ); + + expect(screen.getByText('当剿‰¹æ¬¡')).toBeTruthy(); + expect( + screen + .getByText('预计等待') + .closest('.custom-world-generation-stats') + ?.className, + ).not.toContain('custom-world-generation-stats--two-column'); + }); +}); diff --git a/src/components/CustomWorldGenerationView.tsx b/src/components/CustomWorldGenerationView.tsx index 41189c00..70ff15d7 100644 --- a/src/components/CustomWorldGenerationView.tsx +++ b/src/components/CustomWorldGenerationView.tsx @@ -1,4 +1,6 @@ import { motion } from 'motion/react'; +import type { CSSProperties } from 'react'; +import { useEffect, useState } from 'react'; import type { CustomWorldGenerationProgress } from '../../packages/shared/src/contracts/runtime'; import type { CustomWorldStructuredAnchorEntry } from '../services/customWorldAgentGenerationProgress'; @@ -24,6 +26,7 @@ interface CustomWorldGenerationViewProps { pausedBadgeLabel?: string; idleBadgeLabel?: string; structuredEmptyText?: string; + hideBatchModule?: boolean; } function formatDuration(ms: number) { @@ -86,6 +89,49 @@ function buildFallbackRenderKey( return normalizedValue ? normalizedValue : fallback; } +function useIsMobileGenerationLayout() { + const [isMobile, setIsMobile] = useState(() => { + if ( + typeof window === 'undefined' || + typeof window.matchMedia !== 'function' + ) { + return false; + } + + return window.matchMedia('(max-width: 639px)').matches; + }); + + useEffect(() => { + if ( + typeof window === 'undefined' || + typeof window.matchMedia !== 'function' + ) { + return undefined; + } + + const mediaQuery = window.matchMedia('(max-width: 639px)'); + const syncMobileLayout = () => { + setIsMobile(mediaQuery.matches); + }; + + syncMobileLayout(); + + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', syncMobileLayout); + return () => { + mediaQuery.removeEventListener('change', syncMobileLayout); + }; + } + + mediaQuery.addListener(syncMobileLayout); + return () => { + mediaQuery.removeListener(syncMobileLayout); + }; + }, []); + + return isMobile; +} + export function CustomWorldGenerationView({ settingText, anchorEntries = [], @@ -107,7 +153,9 @@ export function CustomWorldGenerationView({ pausedBadgeLabel = '生æˆå·²æš‚åœ', idleBadgeLabel = '等待æ“作', structuredEmptyText = '正在整ç†å½“å‰è®¾å®šç»“构,请ç¨åŽã€‚', + hideBatchModule = false, }: CustomWorldGenerationViewProps) { + const isMobileGenerationLayout = useIsMobileGenerationLayout(); const progressValue = getProgressPercentage(progress); const steps = progress?.steps ?? []; const hasStructuredAnchors = anchorEntries.length > 0; @@ -116,6 +164,10 @@ export function CustomWorldGenerationView({ const normalizedSettingDescription = settingDescription?.trim() ?? ''; const hasSettingActionLabel = normalizedSettingActionLabel.length > 0; const hasSettingDescription = normalizedSettingDescription.length > 0; + const shouldHideBatchModule = + hideBatchModule || + progressTitle === '拼图è‰ç¨¿ç”Ÿæˆè¿›åº¦' || + progressTitle === '抓大鹅è‰ç¨¿ç”Ÿæˆè¿›åº¦'; const estimatedWaitText = progress?.estimatedRemainingMs != null ? `预计还需 ${formatDuration(progress.estimatedRemainingMs)}` @@ -179,28 +231,41 @@ export function CustomWorldGenerationView({ /> -
-
-
- 当剿‰¹æ¬¡ +
+ {shouldHideBatchModule ? null : ( +
+
+ 当剿‰¹æ¬¡ +
+
+ {progress?.batchLabel ?? '准备中'} +
-
- {progress?.batchLabel ?? '准备中'} -
-
-
+ )} +
预计等待
-
+
{estimatedWaitText}
-
+
计时
-
+
{elapsedText}
@@ -211,7 +276,7 @@ export function CustomWorldGenerationView({ const stepProgress = getStepProgressPercentage(step); return ( -
@@ -248,7 +329,7 @@ export function CustomWorldGenerationView({
{step.detail}
-
+ ); })}
diff --git a/src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx b/src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx index 65e18de5..ac43de6a 100644 --- a/src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx +++ b/src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx @@ -12,7 +12,7 @@ describe('BarkBattleConfigEditor', () => { render(); expect(screen.getByRole('heading', { name: '汪汪声浪大作战' })).toBeTruthy(); - expect(screen.getByText('è½»é…置作å“')).toBeTruthy(); + expect(screen.getByText('è½»é…ç½®')).toBeTruthy(); expect((screen.getByLabelText('ä½œå“æ ‡é¢˜') as HTMLInputElement).value).toBe('我的声浪竞技场'); expect((screen.getByLabelText('难度预设') as HTMLSelectElement).value).toBe('normal'); expect((screen.getByLabelText('å¼€å¯æŽ’è¡Œæ¦œ') as HTMLInputElement).checked).toBe(true); @@ -47,4 +47,22 @@ describe('BarkBattleConfigEditor', () => { expect(onPublish).not.toHaveBeenCalled(); expect(screen.getByText('è¯·å…ˆå¡«å†™ä½œå“æ ‡é¢˜')).toBeTruthy(); }); + + it('can render as an embedded creation form without a local page header', () => { + const onPublish = vi.fn(); + render( + , + ); + + expect(screen.queryByRole('heading', { name: '汪汪声浪大作战' })).toBeNull(); + expect(screen.queryByRole('button', { name: '返回' })).toBeNull(); + expect(screen.getByLabelText('汪汪声浪轻é…置编辑器')).toBeTruthy(); + expect(screen.getByText('å‘布失败')).toBeTruthy(); + }); }); diff --git a/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx b/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx index 0b5b4bf5..fd2345ff 100644 --- a/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx +++ b/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx @@ -1,3 +1,4 @@ +import { ArrowLeft, Loader2, Trophy, WandSparkles } from 'lucide-react'; import { useMemo, useState } from 'react'; import type { BarkBattleConfigEditorPayload } from '../../../packages/shared/src/contracts/barkBattle'; @@ -6,8 +7,11 @@ import { BarkBattlePreviewCard } from './BarkBattlePreviewCard'; export type BarkBattleConfigEditorProps = { isBusy?: boolean; + error?: string | null; onPublish: (payload: BarkBattleConfigEditorPayload) => void | Promise; onBack?: () => void; + showBackButton?: boolean; + title?: string | null; }; const THEME_OPTIONS = [ @@ -30,8 +34,11 @@ const DIFFICULTY_OPTIONS: Array<{ value: BarkBattleDifficultyPreset; label: stri export function BarkBattleConfigEditor({ isBusy = false, + error: externalError = null, onPublish, onBack, + showBackButton = true, + title: headingTitle = '汪汪声浪大作战', }: BarkBattleConfigEditorProps) { const [title, setTitle] = useState('我的声浪竞技场'); const [description, setDescription] = useState(''); @@ -40,7 +47,7 @@ export function BarkBattleConfigEditor({ const [opponentDogSkinPreset, setOpponentDogSkinPreset] = useState('husky'); const [difficultyPreset, setDifficultyPreset] = useState('normal'); const [leaderboardEnabled, setLeaderboardEnabled] = useState(true); - const [error, setError] = useState(null); + const [localError, setLocalError] = useState(null); const payload = useMemo( () => ({ @@ -65,96 +72,212 @@ export function BarkBattleConfigEditor({ const handlePublish = () => { if (!payload.title) { - setError('è¯·å…ˆå¡«å†™ä½œå“æ ‡é¢˜'); + setLocalError('è¯·å…ˆå¡«å†™ä½œå“æ ‡é¢˜'); return; } - setError(null); + setLocalError(null); void onPublish(payload); }; + const visibleError = localError ?? externalError; return ( -
-
-
-
-
-

è½»é…置作å“

-

汪汪声浪大作战

-

é…置展示ã€çš®è‚¤ã€éš¾åº¦å’ŒæŽ’行榜;公平性规则由åŽç«¯å›ºå®šè£å†³ã€‚

+
+ {showBackButton && onBack ? ( +
+ +
+ ) : null} + +
+ {headingTitle ? ( +
+
+

+ {headingTitle} +

+ + è½»é…ç½® +
- {onBack ? ( - +
+ ) : null} + +
+
+ + +