From 18908609fcd0e6ee4829a3b02d1d0fb260bf2099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=94=E9=A6=99=E4=B8=B8=E5=AD=90?= <15518898337@163.com> Date: Sat, 6 Jun 2026 22:49:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E9=A1=B5=E8=A1=A8=E5=A4=B4=E8=B7=9F=E9=9A=8F=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 8 ++ .../AdminCreationEntrySwitchPage.test.tsx | 5 +- .../pages/AdminCreationEntrySwitchPage.tsx | 4 + ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 2 +- .../crates/module-runtime/src/application.rs | 21 ++-- server-rs/crates/module-runtime/src/lib.rs | 74 ++++++++++- .../src/creation_entry_config.rs | 117 +++++++++++++++--- .../src/runtime/creation_entry_config.rs | 56 ++++++++- .../unifiedCreationSpecs.test.ts | 8 ++ .../unified-creation/unifiedCreationSpecs.ts | 22 ++-- ...ch3DCreationWorkspace.interaction.test.tsx | 4 +- .../workspaces/Match3DCreationWorkspace.tsx | 2 +- ...zzleCreationWorkspace.interaction.test.tsx | 4 +- .../workspaces/PuzzleCreationWorkspace.tsx | 2 +- 14 files changed, 276 insertions(+), 53 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 0ca4927b..fcc727ec 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1305,3 +1305,11 @@ - å½±å“èŒƒå›´ï¼šæ‹¼æ¶ˆæ¶ˆå·¥ä½œå° payloadã€`shared-contracts` / `packages/shared` 契约ã€api-server 生æˆç¼–排ã€SpacetimeDB session/work snapshotã€æ–‡æ¡£ä¸Žç”Ÿæˆè¿›åº¦å±•示。 - éªŒè¯æ–¹å¼ï¼š`npm run spacetime:generate`ã€`npm run check:encoding`ã€`npm run check:server-rs-ddd`ã€`cargo test -p module-puzzle-clear`ã€`cargo test -p spacetime-client puzzle_clear -- --nocapture`ã€`npm run test -- src/components/puzzle-clear-creation/PuzzleClearWorkspace.test.tsx src/services/miniGameDraftGenerationProgress.test.ts src/routing/appPageRoutes.test.ts src/services/publicWorkCode.test.ts`。 - å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ‹¼æ¶ˆæ¶ˆçŽ©æ³•æ¨¡æ¿PRD-2026-05-30.md`ã€`docs/technical/ã€çŽ©æ³•åˆ›ä½œã€‘æ‹¼æ¶ˆæ¶ˆçŽ©æ³•æ¨¡æ¿æŠ€æœ¯æ–¹æ¡ˆ-2026-05-30.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + +## 2026-06-06 统一创作页表头跟éšåŽå°çŽ©æ³•å…¥å£é…ç½® + +- 背景:统一创作页长期使用固定表头 `想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ`,导致跳一跳等玩法希望按自身语义展示标题时åªèƒ½æ”¹å‰ç«¯æˆ–默认契约。 +- 决策:`creationTypes[].unifiedCreationSpec.title` 继续作为统一创作页表头传输字段,但默认值改为玩法入å£ä¸­æ–‡å称;读å–å’Œä¿å­˜æ—¶å¦‚æžœå‘现旧公共表头或代ç é»˜è®¤ä¸­æ–‡å,则归一为当å‰å…¥å£ `title`,åŽå°æ‰‹åЍé…ç½®æˆå…¶å®ƒæ ‡é¢˜æ—¶ä¿ç•™è‡ªå®šä¹‰å€¼ã€‚ +- å½±å“范围:`shared-contracts` 默认 specã€`module-runtime` å…¥å£é…ç½®å“应ã€`spacetime-module` åŽå°ä¿å­˜å½’一化ã€åŽå°å…¥å£å¼€å…³é¡µæ‘˜è¦å’Œå‰ç«¯ fallback spec。 +- éªŒè¯æ–¹å¼ï¼š`GET /api/creation-entry/config` 中å„玩法 `unifiedCreationSpec.title` 默认等于入å£ä¸­æ–‡åï¼›åŽå°ä¿®æ”¹å…¥å£åç§°åŽï¼Œæœªè‡ªå®šä¹‰è¡¨å¤´çš„统一创作页跟éšå˜åŒ–。 +- å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx index da9362d7..adad5145 100644 --- a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx @@ -27,7 +27,7 @@ vi.mock('../api/adminApiClient', () => ({ const puzzleSpec: UnifiedCreationSpecPayload = { playId: 'puzzle', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '拼图', workspaceStage: 'puzzle-agent-workspace', generationStage: 'puzzle-generating', resultStage: 'puzzle-result', @@ -88,6 +88,9 @@ test('创作入å£åŽå°å±•示并ä¿å­˜ç»Ÿä¸€åˆ›ä½œå¥‘约', async () => { await screen.findByText('pictureDescription'); expect(container.querySelector('.admin-subsection .admin-info-list')).not.toBeNull(); + expect( + container.querySelector('.admin-subsection .admin-info-list')?.textContent, + ).toContain('拼图'); expect(container.querySelector('.admin-panel .admin-panel')).toBeNull(); expect(container.querySelector('.admin-muted')).toBeNull(); diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx index 9de00709..9390f0f1 100644 --- a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx @@ -707,6 +707,10 @@ function UnifiedCreationSpecSummary({specJson}: {specJson: string}) {
玩法
{parsed.spec.playId}
+
+
表头
+
{parsed.spec.title}
+
阶段
diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index 9f950b43..4ed742ed 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -14,7 +14,7 @@ 创作æ¢å¤å‚æ•°åªä¿ç•™ `sessionId`ã€`profileId`ã€`draftId`ã€`workId` è¿™å››ä¸ªç§æœ‰ query。它们åªå…许在åŒä¸€æ¡åˆ›ä½œé“¾è·¯çš„结果页ã€ç”Ÿæˆé¡µã€å·¥ä½œå°ä¹‹é—´ä¿ç•™ï¼›åˆ‡åˆ°é¦–页ã€å…¬å¼€ä½œå“详情ã€runtime 或å¦ä¸€æ¡çŽ©æ³•é“¾è·¯æ—¶å¿…é¡»æ¸…æŽ‰ã€‚ç”Ÿæˆé¡µç­‰å¾…时间统一以生æˆçжæ€é‡Œçš„ `startedAtMs` ä¸ºå‡†ï¼›åˆ›å»ºè¯¥çŠ¶æ€æ—¶ä¼˜å…ˆä½¿ç”¨åŽç«¯ session 下å‘çš„æ—¶é—´æˆ³ï¼Œä½œå“æ‘˜è¦é‡Œçš„ `updatedAt` ä»åªç”¨äºŽæŽ’åºä¸Žæ‘˜è¦å±•示,ä¸ä½œä¸ºå‰ç«¯è‡ªè¡ŒæŽ¨å¯¼ä¸šåŠ¡çŠ¶æ€çš„真相。 -统一创作入å£è¦†ç›–当å‰å¯è¿›å…¥åˆ›ä½œé“¾è·¯çš„已有模æ¿ï¼š`rpg`ã€`big-fish`ã€`puzzle`ã€`match3d`ã€`jump-hop`ã€`wooden-fish`ã€`square-hole`ã€`bark-battle`ã€`visual-novel`ã€`baby-object-match` å’Œ `creative-agent`ï¼›`airp` 仿˜¯æœªå¼€æ”¾å ä½ï¼Œä¸ä½œä¸ºå½“å‰ç»Ÿä¸€åˆ›ä½œé“¾è·¯ç›®æ ‡ã€‚æ‹¼å›¾ã€æŠ“å¤§é¹…ã€è·³ä¸€è·³å’Œæ•²æœ¨é±¼åœ¨å‰ç«¯ç»§ç»­ç»è¿‡ `UnifiedCreationWorkspace` å’Œ `UnifiedGenerationPage`:`UnifiedCreationWorkspace` 作为平å°å£³ä¾èµ–的统一创作编排层,å†å†…部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace`ã€`Match3DCreationWorkspace`ã€`JumpHopCreationWorkspace` å’Œ `WoodenFishCreationWorkspace`。其它已有模æ¿ç”±å¹³å°å£³ç”¨ `UnifiedCreationPage` åŒ…ä½æ—¢æœ‰å·¥ä½œå°ï¼Œå¤ç”¨ç»Ÿä¸€æ ‡é¢˜æ ã€è¿”回入å£ã€é¡µé¢çº§çºµå‘滚动和éšè—å­—æ®µå¥‘çº¦ï¼ŒåŒæ—¶ä¿ç•™å„玩法自己的表å•ã€è‰ç¨¿æ¢å¤å’ŒåŽç»­ç¼–排。创作页字段清å•ç”±åŽç«¯åœ¨ `GET /api/creation-entry/config` çš„ `creationTypes[].unifiedCreationSpec` 下å‘,å‰ç«¯ä»…在该扩展ä½ç¼ºå¤±æ—¶å›žé€€åˆ°æœ¬åœ°é»˜è®¤ spec;字段类型åªä¿ç•™ `text`ã€`select`ã€`image`ã€`audio`。`UnifiedCreationPage` ä¸åœ¨ UI 中é¢å¤–展示字段说明 chip,也ä¸åœ¨å³ä¸Šè§’显示内部 `playId`ã€æ¨¡æ¿ ID 或工作å°é˜¶æ®µå;竖å±ç§»åŠ¨ç«¯å¿…é¡»èƒ½ä»Žæ ‡é¢˜ã€è¡¨å•一路滑到æäº¤æŒ‰é’®ã€‚å„玩法工作å°è´Ÿè´£æ¸²æŸ“真实输入控件ã€ä¸Šä¼ ã€åކå²ç´ æã€æ ¡éªŒå’Œæäº¤ï¼Œä½†è¿”回按钮åªä¿ç•™åœ¨ç»Ÿä¸€é¡µå¤´ï¼Œå·¥ä½œå°å†…部ä¸å†é‡å¤æ¸²æŸ“。暗色创作进度å¡ç‰‡ä½äºŽ `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap,确ä¿ç™½å­—ã€æµ…色边框和进度æ¡åº•色ä¸ä¼šè¢«å…¨å±€è§„åˆ™æ”¹æˆæ·±è‰²ï¼›ä¸è¦åªä¾èµ–通用 `text-white*` ç±»ã€‚æ•²æœ¨é±¼çš„éŸ³æ•ˆå’ŒåŠŸå¾·è¯æ¡é¢æ¿ä¸å¾—放进独立内部滚动容器,移动端应跟éšé¡µé¢è‡ªç„¶æ»šåŠ¨å±•å¼€ã€‚ç”Ÿæˆé¡µç»Ÿä¸€å±•示阶段ã€å½“剿­¥éª¤ã€æ€»è¿›åº¦ã€é”™è¯¯å’Œé‡è¯•动作。 +统一创作入å£è¦†ç›–当å‰å¯è¿›å…¥åˆ›ä½œé“¾è·¯çš„已有模æ¿ï¼š`rpg`ã€`big-fish`ã€`puzzle`ã€`match3d`ã€`jump-hop`ã€`wooden-fish`ã€`square-hole`ã€`bark-battle`ã€`visual-novel`ã€`baby-object-match` å’Œ `creative-agent`ï¼›`airp` 仿˜¯æœªå¼€æ”¾å ä½ï¼Œä¸ä½œä¸ºå½“å‰ç»Ÿä¸€åˆ›ä½œé“¾è·¯ç›®æ ‡ã€‚æ‹¼å›¾ã€æŠ“å¤§é¹…ã€è·³ä¸€è·³å’Œæ•²æœ¨é±¼åœ¨å‰ç«¯ç»§ç»­ç»è¿‡ `UnifiedCreationWorkspace` å’Œ `UnifiedGenerationPage`:`UnifiedCreationWorkspace` 作为平å°å£³ä¾èµ–的统一创作编排层,å†å†…部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace`ã€`Match3DCreationWorkspace`ã€`JumpHopCreationWorkspace` å’Œ `WoodenFishCreationWorkspace`。其它已有模æ¿ç”±å¹³å°å£³ç”¨ `UnifiedCreationPage` åŒ…ä½æ—¢æœ‰å·¥ä½œå°ï¼Œå¤ç”¨ç»Ÿä¸€æ ‡é¢˜æ ã€è¿”回入å£ã€é¡µé¢çº§çºµå‘滚动和éšè—å­—æ®µå¥‘çº¦ï¼ŒåŒæ—¶ä¿ç•™å„玩法自己的表å•ã€è‰ç¨¿æ¢å¤å’ŒåŽç»­ç¼–排。创作页字段清å•和表头由åŽç«¯åœ¨ `GET /api/creation-entry/config` çš„ `creationTypes[].unifiedCreationSpec` 下å‘,å‰ç«¯ä»…在该扩展ä½ç¼ºå¤±æ—¶å›žé€€åˆ°æœ¬åœ°é»˜è®¤ spec;字段类型åªä¿ç•™ `text`ã€`select`ã€`image`ã€`audio`。统一创作页表头属于åŽå°çŽ©æ³•å…¥å£é…置,默认使用åŒä¸€å…¥å£çš„中文åç§°ï¼›æ—§åº“é‡Œä»æŒä¹…化为 `想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ` 或代ç é»˜è®¤ä¸­æ–‡åçš„ spec title,会在读å–å’Œä¿å­˜æ—¶å½’一到当å‰å…¥å£å称,åŽå°å•独写æˆå…¶å®ƒæ ‡é¢˜æ—¶ä¿ç•™è¯¥è‡ªå®šä¹‰å€¼ã€‚`UnifiedCreationPage` ä¸åœ¨ UI 中é¢å¤–展示字段说明 chip,也ä¸åœ¨å³ä¸Šè§’显示内部 `playId`ã€æ¨¡æ¿ ID 或工作å°é˜¶æ®µå;竖å±ç§»åŠ¨ç«¯å¿…é¡»èƒ½ä»Žæ ‡é¢˜ã€è¡¨å•一路滑到æäº¤æŒ‰é’®ã€‚å„玩法工作å°è´Ÿè´£æ¸²æŸ“真实输入控件ã€ä¸Šä¼ ã€åކå²ç´ æã€æ ¡éªŒå’Œæäº¤ï¼Œä½†è¿”回按钮åªä¿ç•™åœ¨ç»Ÿä¸€é¡µå¤´ï¼Œå·¥ä½œå°å†…部ä¸å†é‡å¤æ¸²æŸ“。暗色创作进度å¡ç‰‡ä½äºŽ `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap,确ä¿ç™½å­—ã€æµ…色边框和进度æ¡åº•色ä¸ä¼šè¢«å…¨å±€è§„åˆ™æ”¹æˆæ·±è‰²ï¼›ä¸è¦åªä¾èµ–通用 `text-white*` ç±»ã€‚æ•²æœ¨é±¼çš„éŸ³æ•ˆå’ŒåŠŸå¾·è¯æ¡é¢æ¿ä¸å¾—放进独立内部滚动容器,移动端应跟éšé¡µé¢è‡ªç„¶æ»šåŠ¨å±•å¼€ã€‚ç”Ÿæˆé¡µç»Ÿä¸€å±•示阶段ã€å½“剿­¥éª¤ã€æ€»è¿›åº¦ã€é”™è¯¯å’Œé‡è¯•动作。 åˆ›ä½œè¡¨å•æäº¤å‰çš„æ³¥ç‚¹ä½™é¢å‰ç½®æ ¡éªŒåªå…许用独立弹窗æç¤ºå¤±è´¥åŽŸå› ï¼Œä¸å¾—æŠŠç”¨æˆ·é€€å›žåˆ›ä½œå…¥å£æˆ–玩法模æ¿åˆ—表,也ä¸å¾—清空当å‰è¡¨å•状æ€ã€‚当å‰é€‚ç”¨æ‹¼å›¾ã€æŠ“å¤§é¹…å’Œæ±ªæ±ªå£°æµªç­‰ä¼šåœ¨å‰ç«¯æäº¤å‰æ ¡éªŒæ³¥ç‚¹çš„生æˆå…¥å£ï¼›ä½™é¢ä¸è¶³ã€ä½™é¢è¯»å–失败都应åœç•™åœ¨å½“å‰å·¥ä½œå°ï¼Œç”±ç”¨æˆ·å…³é—­æç¤ºåŽç»§ç»­ç¼–辑或自行补足泥点。 diff --git a/server-rs/crates/module-runtime/src/application.rs b/server-rs/crates/module-runtime/src/application.rs index 2c578dd9..cae5c119 100644 --- a/server-rs/crates/module-runtime/src/application.rs +++ b/server-rs/crates/module-runtime/src/application.rs @@ -12,7 +12,7 @@ use crate::format_utc_micros; use shared_contracts::creation_entry_config::{ CreationEntryConfigResponse, CreationEntryEventBannerResponse, CreationEntryStartCardResponse, CreationEntryTypeModalResponse, CreationEntryTypeResponse, - encode_unified_creation_spec_response, resolve_unified_creation_spec_response, + encode_unified_creation_spec_response, resolve_unified_creation_spec_response_with_entry_title, }; /// 将创作入å£é¢†åŸŸå¿«ç…§è½¬æ¢ä¸ºå‰åŽå°å…±äº«çš„ HTTP 契约å“应。 @@ -45,8 +45,9 @@ pub fn build_creation_entry_config_response( .creation_types .into_iter() .map(|item| { - let unified_creation_spec = resolve_unified_creation_spec_response( + let unified_creation_spec = resolve_unified_creation_spec_response_with_entry_title( item.id.as_str(), + item.title.as_str(), item.unified_creation_spec_json.as_deref(), ); CreationEntryTypeResponse { @@ -161,10 +162,9 @@ fn normalize_creation_entry_announcement_banner_value( ); } - let banner = serde_json::from_value::(Value::Object( - object.clone(), - )) - .map_err(|error| format!("第 {} æ¡å…¬å‘Šå¯¹è±¡éžæ³•:{error}", index + 1))?; + let banner = + serde_json::from_value::(Value::Object(object.clone())) + .map_err(|error| format!("第 {} æ¡å…¬å‘Šå¯¹è±¡éžæ³•:{error}", index + 1))?; normalize_creation_entry_event_banner_response(index, banner) } @@ -243,8 +243,8 @@ pub fn resolve_creation_entry_event_banner_responses( banners } .into_iter() - .map(build_creation_entry_event_banner_response) - .collect() + .map(build_creation_entry_event_banner_response) + .collect() } /// 把领域公告快照转æ¢ä¸º HTTP å“应字段。 @@ -332,10 +332,7 @@ fn normalize_banner_html_code( } let lower_html_code = html_code.to_ascii_lowercase(); if lower_html_code.contains(" Option { let (workspace_stage, generation_stage, result_stage, fields) = match play_id { @@ -172,18 +173,8 @@ pub fn build_phase1_unified_creation_spec(play_id: &str) -> Option Option Option Option<&'static str> { + match play_id { + "rpg" => Some("文字冒险"), + "big-fish" => Some("摸鱼"), + "puzzle" => Some("拼图"), + "match3d" => Some("抓大鹅"), + "jump-hop" => Some("跳一跳"), + "wooden-fish" => Some("敲木鱼"), + "square-hole" => Some("方洞"), + "bark-battle" => Some("汪汪声浪"), + "visual-novel" => Some("视觉å°è¯´"), + "baby-object-match" => Some("å®è´è¯†ç‰©"), + "creative-agent" => Some("智能体创作"), + _ => None, + } +} + +pub fn normalize_unified_creation_spec_title_for_entry( + play_id: &str, + entry_title: &str, + mut spec: UnifiedCreationSpecResponse, +) -> UnifiedCreationSpecResponse { + let entry_title = entry_title.trim(); + if entry_title.is_empty() { + return spec; + } + + let spec_title = spec.title.trim(); + let default_title = default_unified_creation_title(play_id).unwrap_or_default(); + if spec_title == LEGACY_UNIFIED_CREATION_TITLE + || (!default_title.is_empty() && spec_title == default_title) + { + spec.title = entry_title.to_string(); + } + spec +} + pub fn validate_unified_creation_spec_response( spec: &UnifiedCreationSpecResponse, ) -> Result<(), String> { @@ -311,10 +339,27 @@ pub fn resolve_unified_creation_spec_response( play_id: &str, value: Option<&str>, ) -> Option { - match value { + resolve_unified_creation_spec_response_with_entry_title( + play_id, + default_unified_creation_title(play_id).unwrap_or_default(), + value, + ) +} + +pub fn resolve_unified_creation_spec_response_with_entry_title( + play_id: &str, + entry_title: &str, + value: Option<&str>, +) -> Option { + let spec = match value { Some(raw) => decode_unified_creation_spec_response(raw).ok(), None => build_phase1_unified_creation_spec(play_id), - } + }?; + Some(normalize_unified_creation_spec_title_for_entry( + play_id, + entry_title, + spec, + )) } fn unified_creation_field( @@ -338,10 +383,12 @@ mod tests { #[test] fn phase1_unified_creation_specs_cover_existing_templates() { let puzzle = build_phase1_unified_creation_spec("puzzle").expect("puzzle spec"); + assert_eq!(puzzle.title, "拼图"); assert_eq!(puzzle.fields[0].id, "pictureDescription"); assert_eq!(puzzle.fields[1].kind, "image"); let match3d = build_phase1_unified_creation_spec("match3d").expect("match3d spec"); + assert_eq!(match3d.title, "抓大鹅"); assert_eq!( match3d .fields @@ -352,6 +399,7 @@ mod tests { ); let jump_hop = build_phase1_unified_creation_spec("jump-hop").expect("jump-hop spec"); + assert_eq!(jump_hop.title, "跳一跳"); assert!( jump_hop .fields @@ -389,6 +437,45 @@ mod tests { ); } + #[test] + fn unified_creation_spec_title_can_fallback_to_entry_title() { + let raw = r#"{ + "playId": "puzzle", + "title": "想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ", + "workspaceStage": "puzzle-agent-workspace", + "generationStage": "puzzle-generating", + "resultStage": "puzzle-result", + "fields": [ + { + "id": "pictureDescription", + "kind": "text", + "label": "ç”»é¢æè¿°", + "required": true + } + ] + }"#; + + let spec = resolve_unified_creation_spec_response_with_entry_title( + "puzzle", + "定制拼图", + Some(raw), + ) + .expect("puzzle spec"); + + assert_eq!(spec.title, "定制拼图"); + } + + #[test] + fn unified_creation_spec_title_keeps_admin_custom_copy() { + let mut spec = build_phase1_unified_creation_spec("jump-hop").expect("jump-hop spec"); + spec.title = "你的跳一跳是...".to_string(); + + let normalized = + normalize_unified_creation_spec_title_for_entry("jump-hop", "跳一跳", spec); + + assert_eq!(normalized.title, "你的跳一跳是..."); + } + #[test] fn creation_entry_event_banner_defaults_to_structured_render_mode() { let banner = serde_json::from_str::( diff --git a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs index 232fc1e3..0dbf9b72 100644 --- a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs +++ b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs @@ -122,7 +122,8 @@ fn upsert_creation_entry_type_config_in_tx( if input.title.trim().is_empty() { return Err("入壿 ‡é¢˜ä¸èƒ½ä¸ºç©º".to_string()); } - let unified_creation_spec_json = normalize_unified_creation_spec_json(&id, &input)?; + let unified_creation_spec_json = + normalize_unified_creation_spec_json(&id, input.title.trim(), &input)?; let row = CreationEntryTypeConfig { id: id.clone(), title: input.title.trim().to_string(), @@ -297,6 +298,7 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) { migrate_baby_object_match_entry_from_old_coming_soon_default(ctx, now); migrate_wooden_fish_entry_from_old_puzzle_image_default(ctx, now); migrate_jump_hop_entry_from_old_puzzle_default(ctx, now); + migrate_unified_creation_titles_to_entry_defaults(ctx, now); } fn migrate_rpg_entry_from_old_hidden_default(ctx: &ReducerContext, now: Timestamp) { @@ -477,6 +479,51 @@ fn migrate_jump_hop_entry_from_old_puzzle_default(ctx: &ReducerContext, now: Tim }); } +fn migrate_unified_creation_titles_to_entry_defaults(ctx: &ReducerContext, now: Timestamp) { + let rows = ctx + .db + .creation_entry_type_config() + .iter() + .collect::>(); + for row in rows { + let Some(raw_spec_json) = row.unified_creation_spec_json.as_deref() else { + continue; + }; + let Ok(spec) = + shared_contracts::creation_entry_config::decode_unified_creation_spec_response( + raw_spec_json, + ) + else { + continue; + }; + let normalized = + shared_contracts::creation_entry_config::normalize_unified_creation_spec_title_for_entry( + &row.id, + &row.title, + spec.clone(), + ); + if normalized.title == spec.title { + continue; + } + let Ok(unified_creation_spec_json) = + shared_contracts::creation_entry_config::encode_unified_creation_spec_response( + &normalized, + ) + else { + continue; + }; + + ctx.db + .creation_entry_type_config() + .id() + .update(CreationEntryTypeConfig { + unified_creation_spec_json: Some(unified_creation_spec_json), + updated_at: now, + ..row + }); + } +} + fn default_creation_entry_type_configs(now: Timestamp) -> Vec { module_runtime::default_creation_entry_type_snapshots(now.to_micros_since_unix_epoch()) .into_iter() @@ -500,6 +547,7 @@ fn default_creation_entry_type_configs(now: Timestamp) -> Vec Result, String> { let Some(spec_json) = input.unified_creation_spec_json.as_deref() else { @@ -512,6 +560,12 @@ fn normalize_unified_creation_spec_json( let spec = shared_contracts::creation_entry_config::decode_unified_creation_spec_response(normalized)?; + let spec = + shared_contracts::creation_entry_config::normalize_unified_creation_spec_title_for_entry( + id, + entry_title, + spec, + ); shared_contracts::creation_entry_config::validate_unified_creation_spec_for_play(id, &spec)?; shared_contracts::creation_entry_config::encode_unified_creation_spec_response(&spec).map(Some) } diff --git a/src/components/unified-creation/unifiedCreationSpecs.test.ts b/src/components/unified-creation/unifiedCreationSpecs.test.ts index 8e6daa98..611e294d 100644 --- a/src/components/unified-creation/unifiedCreationSpecs.test.ts +++ b/src/components/unified-creation/unifiedCreationSpecs.test.ts @@ -36,41 +36,49 @@ describe('unified creation specs', () => { test('主è¦é“¾è·¯éƒ½æ˜ å°„到统一创作ã€ç”Ÿæˆã€ç»“果阶段', () => { expect(getUnifiedCreationSpec('rpg')).toMatchObject({ + title: '文字冒险', workspaceStage: 'agent-workspace', generationStage: 'custom-world-generating', resultStage: 'custom-world-result', }); expect(getUnifiedCreationSpec('puzzle')).toMatchObject({ + title: '拼图', workspaceStage: 'puzzle-agent-workspace', generationStage: 'puzzle-generating', resultStage: 'puzzle-result', }); expect(getUnifiedCreationSpec('match3d')).toMatchObject({ + title: '抓大鹅', workspaceStage: 'match3d-agent-workspace', generationStage: 'match3d-generating', resultStage: 'match3d-result', }); expect(getUnifiedCreationSpec('jump-hop')).toMatchObject({ + title: '跳一跳', workspaceStage: 'jump-hop-workspace', generationStage: 'jump-hop-generating', resultStage: 'jump-hop-result', }); expect(getUnifiedCreationSpec('wooden-fish')).toMatchObject({ + title: '敲木鱼', workspaceStage: 'wooden-fish-workspace', generationStage: 'wooden-fish-generating', resultStage: 'wooden-fish-result', }); expect(getUnifiedCreationSpec('bark-battle')).toMatchObject({ + title: '汪汪声浪', workspaceStage: 'bark-battle-workspace', generationStage: 'bark-battle-generating', resultStage: 'bark-battle-result', }); expect(getUnifiedCreationSpec('visual-novel')).toMatchObject({ + title: '视觉å°è¯´', workspaceStage: 'visual-novel-agent-workspace', generationStage: 'visual-novel-generating', resultStage: 'visual-novel-result', }); expect(getUnifiedCreationSpec('baby-object-match')).toMatchObject({ + title: 'å®è´è¯†ç‰©', workspaceStage: 'baby-object-match-workspace', generationStage: 'baby-object-match-generating', resultStage: 'baby-object-match-result', diff --git a/src/components/unified-creation/unifiedCreationSpecs.ts b/src/components/unified-creation/unifiedCreationSpecs.ts index 3fb7c204..3b750212 100644 --- a/src/components/unified-creation/unifiedCreationSpecs.ts +++ b/src/components/unified-creation/unifiedCreationSpecs.ts @@ -27,7 +27,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< > = { rpg: { playId: 'rpg', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '文字冒险', workspaceStage: 'agent-workspace', generationStage: 'custom-world-generating', resultStage: 'custom-world-result', @@ -42,7 +42,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'big-fish': { playId: 'big-fish', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '摸鱼', workspaceStage: 'big-fish-agent-workspace', generationStage: 'big-fish-generating', resultStage: 'big-fish-result', @@ -57,7 +57,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, puzzle: { playId: 'puzzle', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '拼图', workspaceStage: 'puzzle-agent-workspace', generationStage: 'puzzle-generating', resultStage: 'puzzle-result', @@ -84,7 +84,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, match3d: { playId: 'match3d', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '抓大鹅', workspaceStage: 'match3d-agent-workspace', generationStage: 'match3d-generating', resultStage: 'match3d-result', @@ -105,7 +105,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'jump-hop': { playId: 'jump-hop', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '跳一跳', workspaceStage: 'jump-hop-workspace', generationStage: 'jump-hop-generating', resultStage: 'jump-hop-result', @@ -120,7 +120,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'wooden-fish': { playId: 'wooden-fish', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '敲木鱼', workspaceStage: 'wooden-fish-workspace', generationStage: 'wooden-fish-generating', resultStage: 'wooden-fish-result', @@ -153,7 +153,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'square-hole': { playId: 'square-hole', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '方洞', workspaceStage: 'square-hole-agent-workspace', generationStage: 'square-hole-generating', resultStage: 'square-hole-result', @@ -168,7 +168,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'bark-battle': { playId: 'bark-battle', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '汪汪声浪', workspaceStage: 'bark-battle-workspace', generationStage: 'bark-battle-generating', resultStage: 'bark-battle-result', @@ -213,7 +213,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'visual-novel': { playId: 'visual-novel', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '视觉å°è¯´', workspaceStage: 'visual-novel-agent-workspace', generationStage: 'visual-novel-generating', resultStage: 'visual-novel-result', @@ -234,7 +234,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'baby-object-match': { playId: 'baby-object-match', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: 'å®è´è¯†ç‰©', workspaceStage: 'baby-object-match-workspace', generationStage: 'baby-object-match-generating', resultStage: 'baby-object-match-result', @@ -255,7 +255,7 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record< }, 'creative-agent': { playId: 'creative-agent', - title: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title: '智能体创作', workspaceStage: 'creative-agent-workspace', generationStage: 'puzzle-generating', resultStage: 'puzzle-result', diff --git a/src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx b/src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx index a36992ad..856dbc5b 100644 --- a/src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx +++ b/src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx @@ -78,7 +78,7 @@ test('match3d workspace submits derived entry form payload instead of agent chat />, ); - expect(screen.getByText('想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ')).toBeTruthy(); + expect(screen.getByText('抓大鹅')).toBeTruthy(); expect(screen.getByLabelText('想åšä¸€ä¸ªä»€ä¹ˆé¢˜æçš„æŠ“大鹅?')).toBeTruthy(); expect(screen.queryByText('2Dç´ æé£Žæ ¼')).toBeNull(); expect(screen.queryByRole('button', { name: 'æ‰å¹³å›¾æ ‡' })).toBeNull(); @@ -130,7 +130,7 @@ test('match3d workspace can defer visible chrome to the unified creation page', expect(workspace?.className).not.toContain('h-full'); expect(workspace?.className).not.toContain('overflow-hidden'); expect(workspace?.className).not.toContain('platform-remap-surface'); - expect(screen.queryByRole('heading', { name: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ' })).toBeNull(); + expect(screen.queryByRole('heading', { name: '抓大鹅' })).toBeNull(); const themeInput = screen.getByLabelText('想åšä¸€ä¸ªä»€ä¹ˆé¢˜æçš„æŠ“大鹅?'); expect(themeInput).toBeTruthy(); expect(themeInput.className).not.toContain('h-full'); diff --git a/src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx b/src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx index 916dd9eb..d9fb2864 100644 --- a/src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx +++ b/src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx @@ -115,7 +115,7 @@ export function Match3DCreationWorkspace({ onCreateFromForm, initialFormPayload = null, showBackButton = true, - title = '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title = '抓大鹅', unifiedChrome = false, }: Match3DCreationWorkspaceProps) { const [formState, setFormState] = useState(() => diff --git a/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx b/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx index 0ed7ebc0..350e7258 100644 --- a/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx +++ b/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx @@ -188,7 +188,7 @@ test('puzzle workspace submits the work form instead of agent chat', () => { expect(screen.queryByLabelText('作å“åç§°')).toBeNull(); expect(screen.queryByLabelText('ä½œå“æè¿°')).toBeNull(); - expect(screen.getByText('想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ')).toBeTruthy(); + expect(screen.getByText('拼图')).toBeTruthy(); expect(screen.queryByText('try')).toBeNull(); expect(screen.queryByText('Template')).toBeNull(); @@ -238,7 +238,7 @@ test('puzzle workspace can defer visible chrome to the unified creation page', ( expect(workspace?.className).not.toContain('platform-remap-surface'); expect(imagePanel?.className).toContain('flex-none'); expect(imagePanel?.className).not.toContain('flex-1'); - expect(screen.queryByRole('heading', { name: '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ' })).toBeNull(); + expect(screen.queryByRole('heading', { name: '拼图' })).toBeNull(); expect(screen.getByLabelText('ç”»é¢æè¿°')).toBeTruthy(); }); diff --git a/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx b/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx index 74c184a6..024ad826 100644 --- a/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx +++ b/src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx @@ -246,7 +246,7 @@ export function PuzzleCreationWorkspace({ onAutoSaveForm, initialFormPayload = null, showBackButton = true, - title = '想åšä¸ªä»€ä¹ˆçŽ©æ³•ï¼Ÿ', + title = '拼图', unifiedChrome = false, }: PuzzleCreationWorkspaceProps) { const [formState, setFormState] = useState(() =>