From deadce9cf14bc023f51dcb202699354945a77aed Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 3 Jun 2026 20:45:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E7=AA=84=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E5=85=B3=E9=97=AD=E7=86=94=E6=96=AD=E8=8C=83?= =?UTF-8?q?=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 8 ++ ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 4 + server-rs/crates/api-server/src/app.rs | 30 ++++- .../api-server/src/creation_entry_config.rs | 120 ++++++++++-------- .../CustomWorldCreationHub.test.tsx | 49 ++++++- .../CustomWorldCreationStartCard.tsx | 79 +++++++++--- .../PlatformEntryCreationTypeModal.tsx | 13 +- .../PlatformEntryFlowShellImpl.tsx | 24 +++- ...gEntryFlowShell.agent.interaction.test.tsx | 21 +++ 9 files changed, 267 insertions(+), 81 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 6f798308..bb78521f 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-03 创作入å£å…³é—­ä¸ä¸‹æž¶å·²å‘å¸ƒä½œå“ + +- 背景:`creation_entry_disabled` 曾由 api-server 按 runtime 路由å‰ç¼€ç»Ÿä¸€ç†”断,导致用户进入平å°é¦–页或å¯åЍ已å‘å¸ƒä½œå“æ—¶ä¹Ÿå¯èƒ½çœ‹åˆ°â€œåˆ›ä½œå…¥å£å·²å…³é—­â€é”™è¯¯ã€‚ +- 决策:入å£é…置的 `open=false` åªè¡¨ç¤ºå…³é—­æ–°å»ºåˆ›ä½œå…¥å£ï¼Œä¸è¡¨ç¤ºä¸‹æž¶å·²æœ‰è‰ç¨¿ã€ç§æœ‰ä½œå“或公开作å“。åŽç«¯ç†”æ–­åªæ‹¦æ–°å»ºåˆ›ä½œã€æ–°å»ºè‰ç¨¿ã€é¦–次生æˆå…¥å£å’Œ Remix æˆè‰ç¨¿ç­‰ä¼šäº§ç”Ÿæ–°åˆ›ä½œçš„请求;公开广场ã€å…¬å¼€è¯¦æƒ…ã€ç‚¹èµžã€å·²å‘布作å“å¯åЍã€è¿è¡Œæ€è¿‡ç¨‹è¯·æ±‚ã€å­˜æ¡£ / æµè§ˆè®°å½•和已有作å“回读ä¸å› åˆ›ä½œå…¥å£å…³é—­è€Œå¤±è´¥ã€‚å‰ç«¯å¹³å°é¦–页é‡åˆ°æ—§æœåŠ¡ç«¯è¿”å›žçš„ `creation_entry_disabled` åªé™çº§ï¼Œä¸å¼¹å¹³å°çº§é”™è¯¯å¼¹çª—ï¼›å…³é—­æ€æ¨¡æ¿å¡å¿…须明显ç¦ç”¨å¹¶å±•示 `暂未开放`,ä¸å¾—继续显示泥点消耗。 +- å½±å“范围:`server-rs/crates/api-server/src/creation_entry_config.rs`ã€`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/custom-world-home/CustomWorldCreationStartCard.tsx`ã€åˆ›ä½œå…¥å£ç›¸å…³æµ‹è¯•与玩法链路文档。 +- éªŒè¯æ–¹å¼ï¼šå…³é—­ä»»ä¸€åˆ›ä½œå…¥å£åŽï¼Œæ–°å»ºåˆ›ä½œè¯·æ±‚返回 `creation_entry_disabled`;公开作å“列表 / 详情 / å¯åЍ / è¿è¡Œæ€åŠ¨ä½œä¸è¿”回该错误;进入平å°é¦–页ä¸å¼¹â€œå¹³å°é¦–页:creation_entry_disabledâ€ï¼›å…³é—­æ€å…¥å£å¡æ˜¾ç¤ºé”定状æ€ä¸”䏿˜¾ç¤º `10-20泥点数`。 +- å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + ## 2026-06-03 最近创作åªå¤ç”¨åˆ›ä½œæ¨¡æ¿å…¥å£ - 背景:底部加å·åˆ›ä½œå…¥å£çš„â€œæœ€è¿‘åˆ›ä½œâ€æœ€åˆç”±çœŸå®žä½œå“架摘è¦é©±åŠ¨ï¼Œä½†é¡µé¢æ›¾æŒ‰ä½œå“æ ‡é¢˜ã€æ‘˜è¦å’Œç”ŸæˆçŠ¶æ€æ¸²æŸ“独立最近创作å¡ï¼Œå’Œå…¶å®ƒæ¨¡æ¿é¡µç­¾çš„å¡ç‰‡æ ·å¼åŠç‚¹å‡»è¯­ä¹‰ä¸ä¸€è‡´ã€‚ diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index 561b7133..53ce1a7c 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -20,6 +20,10 @@ 生æˆä»»åŠ¡åœ¨ç”¨æˆ·ç¦»å¼€ç”Ÿæˆé¡µåŽå¼‚æ­¥å®Œæˆæ—¶ï¼Œå¹³å°å£³å±‚必须弹出 `PlatformTaskCompletionDialog`。完æˆå¼¹çª—åŒæ ·è¦å¸¦æ¥æºï¼Œä¾‹å¦‚æŸä¸ªè‰ç¨¿æˆ–生æˆä¼šè¯ï¼Œå¹¶æä¾›å¤åˆ¶æŒ‰é’®å¤åˆ¶â€œæ¥æº + 状æ€â€ï¼›å¦‚果用户ä»åœç•™åœ¨ç”Ÿæˆé¡µå¹¶è¢«è‡ªåŠ¨å¸¦å…¥ç»“æžœé¡µæˆ–è¯•çŽ©é¡µï¼Œç”Ÿæˆé¡µ / 结果页本身å³ä¸ºå®Œæˆå馈,ä¸å†é¢å¤–å åŠ å®Œæˆå¼¹çª—。 +å…¥å£é…置中的 `open=false` 表示关闭新建创作入å£ï¼Œä¸è¡¨ç¤ºä¸‹æž¶å·²æœ‰è‰ç¨¿ã€ç§æœ‰ä½œå“或公开作å“。api-server 的入å£ç†”æ–­åªå…è®¸æ‹¦æˆªæ–°å»ºåˆ›ä½œã€æ–°å»ºè‰ç¨¿ã€é¦–次生æˆå…¥å£å’Œ Remix æˆè‰ç¨¿ç­‰ä¼šäº§ç”Ÿæ–°åˆ›ä½œçš„请求;公开广场列表ã€å…¬å¼€è¯¦æƒ…ã€ç‚¹èµžã€å·²å‘布作å“å¯åЍã€è¿è¡Œæ€è¿‡ç¨‹è¯·æ±‚ã€å­˜æ¡£ / æµè§ˆè®°å½•和已有作å“回读ä¸èƒ½å› ä¸ºåˆ›ä½œå…¥å£å…³é—­è€Œè¿”回 `creation_entry_disabled`。平å°é¦–页如果é‡åˆ°æ—§æœåŠ¡ç«¯è¿”å›žçš„ `creation_entry_disabled`,åªèƒ½é™çº§ä¸ºç©ºåˆ—表或éšè—å…¥å£ï¼Œä¸å¼¹å¹³å°çº§é”™è¯¯å¼¹çª—。 + +创作入å£é¡µçš„关闭æ€å¡ç‰‡å¿…须有明显差异:å¡ç‰‡ç¦ç”¨ç‚¹å‡»ï¼Œå±•示åŽå°é…ç½®çš„å…³é—­æ€ badge 或 `暂未开放`,ä¸å†æ˜¾ç¤º `10-20泥点数` 这类å¯åˆ›å»ºæˆæœ¬æç¤ºï¼›å¼€æ”¾æ€å¡ç‰‡ä»ä¸æ˜¾ç¤ºæ™®é€š `å¯åˆ›å»º / å¯åˆ›ä½œ` badge。 + `PlatformEntryFlowShellImpl.tsx` 仿˜¯å¹³å°å…¥å£ç¼–排壳,åŽç»­ç»´æŠ¤æ—¶åº”优先把独立 UI 片段ã€å…¬å¼€ä½œå“映射ã€è‰ç¨¿ç”Ÿæˆ notice å’Œè¿è¡Œæ€çŠ¶æ€ helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或åŒç›®å½•ç´§é‚» helper 文件。拆分åªå…è®¸æ”¹å˜æ–‡ä»¶ç»„ç»‡ï¼Œä¸æ”¹å˜å…¥å£é…置事实æºã€é»˜è®¤å¯¼å‡ºã€propsã€é¡µé¢é˜¶æ®µã€UI 文案或现有交互;其中拼图首访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`。 `platformEntryCreationTypes.ts` åªåšå‰ç«¯å±•示派生,分组时必须把åŽç«¯ `creationTypes` 里的 `categoryId` / `categoryLabel` 当作å¯ç¼ºå¤±å­—段处ç†ï¼Œç©ºå€¼ç»Ÿä¸€å›žé€€åˆ° `recommended` / `热门推è`ï¼Œå¹¶æŠŠåŽ†å² `recent` / `最近创作` 归一到推è分类。`最近创作` ä¸å±žäºŽæ¨¡æ¿åˆ†ç±»é¡µç­¾ï¼Œåªèƒ½ç”± 7 天内的真实è‰ç¨¿ / ä½œå“æž¶åŽç«¯æ•°æ®å†³å®šæ˜¯å¦å±•示;展示内容ä»ç„¶ä»ŽåŽç«¯å…¥å£é…置的模æ¿å¡ä¸­ç­›é€‰ï¼Œä¸è¯»å–æˆ–æ¸²æŸ“ä½œå“æ ‡é¢˜ã€ä½œå“摘è¦ã€è‰ç¨¿é˜¶æ®µæ–‡æ¡ˆã€‚ diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 106c7d77..602b608a 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -740,7 +740,8 @@ mod tests { let response = app .oneshot( Request::builder() - .uri("/api/runtime/puzzle/works") + .method("POST") + .uri("/api/runtime/puzzle/agent/sessions") .body(Body::empty()) .expect("request should build"), ) @@ -756,6 +757,31 @@ mod tests { assert_eq!(body["error"]["details"]["creationTypeId"], "puzzle"); } + #[tokio::test] + async fn disabled_creation_entry_does_not_block_published_runtime_routes() { + let state = AppState::new(AppConfig::default()).expect("state should build"); + state.set_test_creation_entry_route_enabled("puzzle", false); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/runtime/puzzle/runs") + .body(Body::empty()) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_ne!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = read_json_response(response).await; + assert_ne!( + body["error"]["details"]["reason"], + "creation_entry_disabled" + ); + } + #[tokio::test] async fn disabled_visual_novel_creation_route_returns_service_unavailable() { let app = build_router(AppState::new(AppConfig::default()).expect("state should build")); @@ -789,7 +815,7 @@ mod tests { } #[tokio::test] - async fn disabled_rpg_route_returns_service_unavailable() { + async fn disabled_rpg_creation_route_returns_service_unavailable() { let state = AppState::new(AppConfig::default()).expect("state should build"); state.set_test_creation_entry_route_enabled("rpg", false); let app = build_router(state); diff --git a/server-rs/crates/api-server/src/creation_entry_config.rs b/server-rs/crates/api-server/src/creation_entry_config.rs index 8707b709..70b4d70d 100644 --- a/server-rs/crates/api-server/src/creation_entry_config.rs +++ b/server-rs/crates/api-server/src/creation_entry_config.rs @@ -34,7 +34,7 @@ pub async fn get_creation_entry_config_handler( Ok(json_success_body(Some(&request_context), config)) } -/// 中文注释:api-server è·¯ç”±ç†”æ–­åªæ‹¦åˆ›ä½œ/è¿è¡Œæ€ API è¯·æ±‚ï¼Œä¸æ”¹å˜å‰ç«¯å…¥å£å±•示规则。 +/// 中文注释:api-server è·¯ç”±ç†”æ–­åªæ‹¦æ–°å»ºåˆ›ä½œå…¥å£ï¼Œä¸é™åˆ¶å·²æœ‰ä½œå“读å–ã€å‘å¸ƒä½œå“æ¸¸çŽ©æˆ–å…¬å¼€å¹¿åœºæµè§ˆã€‚ pub async fn require_creation_entry_route_enabled( State(state): State, request: Request, @@ -72,54 +72,56 @@ pub async fn require_creation_entry_route_enabled( pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> { let normalized = path.trim_end_matches('/'); - if normalized.starts_with("/api/runtime/puzzle") { + if normalized == "/api/runtime/puzzle/agent/sessions" + || normalized == "/api/runtime/puzzle/onboarding/generate" + { return Some("puzzle"); } - if normalized.starts_with("/api/runtime/match3d") { - return Some("match3d"); + if normalized.starts_with("/api/runtime/puzzle/gallery/") + && normalized.ends_with("/remix") + { + return Some("puzzle"); } - if normalized.starts_with("/api/runtime/bark-battle") { - return Some("bark-battle"); - } - if normalized.starts_with("/api/creation/bark-battle") { - return Some("bark-battle"); - } - if normalized.starts_with("/api/runtime/wooden-fish") { - return Some("wooden-fish"); - } - if normalized.starts_with("/api/creation/wooden-fish") { - return Some("wooden-fish"); - } - if normalized.starts_with("/api/runtime/square-hole") { - return Some("square-hole"); - } - if normalized.starts_with("/api/runtime/jump-hop") { - return Some("jump-hop"); - } - if normalized.starts_with("/api/creation/jump-hop") { - return Some("jump-hop"); - } - if normalized.starts_with("/api/runtime/big-fish") { + if normalized == "/api/runtime/big-fish/agent/sessions" { return Some("big-fish"); } - if normalized.starts_with("/api/runtime/custom-world") - || normalized.starts_with("/api/runtime/custom-world-library") - || normalized.starts_with("/api/runtime/custom-world-gallery") - || normalized.starts_with("/api/runtime/chat") - || normalized.starts_with("/api/story") + if normalized.starts_with("/api/runtime/big-fish/gallery/") + && normalized.ends_with("/remix") + { + return Some("big-fish"); + } + if normalized == "/api/runtime/custom-world/agent/sessions" + || normalized == "/api/runtime/custom-world/profile" { return Some("rpg"); } - if normalized.starts_with("/api/runtime/visual-novel") { + if normalized.starts_with("/api/runtime/custom-world-gallery/") + && normalized.ends_with("/remix") + { + return Some("rpg"); + } + if normalized == "/api/creation/match3d/sessions" { + return Some("match3d"); + } + if normalized == "/api/creation/square-hole/sessions" { + return Some("square-hole"); + } + if normalized == "/api/creation/bark-battle/drafts" { + return Some("bark-battle"); + } + if normalized == "/api/creation/wooden-fish/sessions" { + return Some("wooden-fish"); + } + if normalized == "/api/creation/jump-hop/sessions" { + return Some("jump-hop"); + } + if normalized == "/api/creation/visual-novel/sessions" { return Some("visual-novel"); } - if normalized.starts_with("/api/creation/visual-novel") { - return Some("visual-novel"); - } - if normalized.starts_with("/api/creation/edutainment/baby-object-match") { + if normalized == "/api/creation/edutainment/baby-object-match/assets" { return Some("baby-object-match"); } - if normalized.starts_with("/api/creation/edutainment/baby-love-drawing") { + if normalized == "/api/creation/edutainment/baby-love-drawing/magic" { return Some("baby-love-drawing"); } None @@ -171,58 +173,68 @@ mod tests { use super::*; #[test] - fn resolves_runtime_paths_to_creation_type_ids() { + fn resolves_new_creation_paths_to_creation_type_ids() { assert_eq!( - resolve_creation_entry_route_id("/api/runtime/puzzle/works"), + resolve_creation_entry_route_id("/api/runtime/puzzle/agent/sessions"), Some("puzzle"), ); assert_eq!( - resolve_creation_entry_route_id("/api/runtime/match3d/runs/run-1"), + resolve_creation_entry_route_id("/api/runtime/puzzle/gallery/profile-1/remix"), + Some("puzzle"), + ); + assert_eq!( + resolve_creation_entry_route_id("/api/creation/match3d/sessions"), Some("match3d"), ); assert_eq!( - resolve_creation_entry_route_id("/api/runtime/square-hole/runs/run-1"), + resolve_creation_entry_route_id("/api/creation/square-hole/sessions"), Some("square-hole"), ); - assert_eq!( - resolve_creation_entry_route_id("/api/runtime/visual-novel/works"), - Some("visual-novel"), - ); assert_eq!( resolve_creation_entry_route_id("/api/creation/visual-novel/sessions"), Some("visual-novel"), ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/big-fish/agent/sessions"), + Some("big-fish"), + ); assert_eq!( resolve_creation_entry_route_id("/api/runtime/custom-world/agent/sessions"), Some("rpg"), ); assert_eq!( - resolve_creation_entry_route_id("/api/runtime/custom-world-library/profile-1"), + resolve_creation_entry_route_id( + "/api/runtime/custom-world-gallery/user-1/profile-1/remix" + ), Some("rpg"), ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/custom-world-library/profile-1"), + None, + ); assert_eq!( resolve_creation_entry_route_id("/api/runtime/custom-world-gallery/user-1/profile-1"), - Some("rpg"), + None, ); assert_eq!( resolve_creation_entry_route_id("/api/story/sessions/runtime"), - Some("rpg"), + None, ); assert_eq!( resolve_creation_entry_route_id("/api/runtime/chat/npc/turn/stream"), - Some("rpg"), - ); - assert_eq!( - resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"), - Some("bark-battle"), + None, ); assert_eq!( resolve_creation_entry_route_id("/api/creation/bark-battle/drafts"), Some("bark-battle"), ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"), + None, + ); assert_eq!( resolve_creation_entry_route_id("/api/runtime/wooden-fish/runs/run-1"), - Some("wooden-fish"), + None, ); assert_eq!( resolve_creation_entry_route_id("/api/creation/wooden-fish/sessions"), diff --git a/src/components/custom-world-home/CustomWorldCreationHub.test.tsx b/src/components/custom-world-home/CustomWorldCreationHub.test.tsx index 43d0ebea..28e35014 100644 --- a/src/components/custom-world-home/CustomWorldCreationHub.test.tsx +++ b/src/components/custom-world-home/CustomWorldCreationHub.test.tsx @@ -243,6 +243,8 @@ test('creation start card renders reference-aligned banner and template metadata expect(html).toContain('拼图关å¡åˆ›ä½œ'); expect(html).toContain('10-20泥点数'); expect(html).toContain('å³å°†å¼€æ”¾'); + expect(html).toContain('data-locked="true"'); + expect(html).toContain('暂未开放'); expect(html).not.toContain('å¯åˆ›å»º'); expect(html).not.toContain('å¯åˆ›ä½œ'); expect(html).not.toContain('creation-event-banner__counter'); @@ -250,6 +252,49 @@ test('creation start card renders reference-aligned banner and template metadata expect(html).not.toContain('platform-creation-reference-card'); }); +test('locked creation template card replaces mud point cost with unavailable state', () => { + const lockedEntryConfig = { + ...testEntryConfig, + creationTypes: [ + { + id: 'airp', + title: 'AI RPG', + subtitle: '原生角色扮演', + badge: 'å³å°†å¼€æ”¾', + imageSrc: '/creation-type-references/airp.webp', + visible: true, + open: false, + sortOrder: 70, + categoryId: 'recommended', + categoryLabel: '热门推è', + categorySortOrder: 20, + updatedAtMicros: 1, + }, + ], + } satisfies CreationEntryConfig; + const html = renderToStaticMarkup( + {}} + onCreateType={noopCreateType} + onOpenDraft={() => {}} + onEnterPublished={() => {}} + entryConfig={lockedEntryConfig} + creationTypes={derivePlatformCreationTypes( + lockedEntryConfig.creationTypes, + )} + mode="start-only" + />, + ); + + expect(html).toContain('data-locked="true"'); + expect(html).toContain('å³å°†å¼€æ”¾'); + expect(html).toContain('暂未开放'); + expect(html).not.toContain('10-20泥点数'); +}); + test('creation start card falls back to legacy single banner when eventBanners is empty', () => { const html = renderToStaticMarkup( { diff --git a/src/components/custom-world-home/CustomWorldCreationStartCard.tsx b/src/components/custom-world-home/CustomWorldCreationStartCard.tsx index 4b423f45..76f5254b 100644 --- a/src/components/custom-world-home/CustomWorldCreationStartCard.tsx +++ b/src/components/custom-world-home/CustomWorldCreationStartCard.tsx @@ -1,4 +1,4 @@ -import { Coins, Trophy } from 'lucide-react'; +import { Coins, LockKeyhole, Trophy } from 'lucide-react'; import { type UIEvent, useEffect, useMemo, useRef, useState } from 'react'; import type { @@ -100,17 +100,17 @@ export function CustomWorldCreationStartCard({ activeCategoryId ?? (hasRecentCreationTypes ? CREATION_ENTRY_RECENT_TAB_ID - : creationTypeGroups[0]?.id ?? null); + : (creationTypeGroups[0]?.id ?? null)); const isRecentTabActive = hasRecentCreationTypes && activeTabId === CREATION_ENTRY_RECENT_TAB_ID; const activeGroup = isRecentTabActive ? null - : creationTypeGroups.find((group) => group.id === activeTabId) ?? + : (creationTypeGroups.find((group) => group.id === activeTabId) ?? creationTypeGroups[0] ?? - null; + null); const visibleCreationTypes = isRecentTabActive ? recentCreationTypes - : activeGroup?.items ?? []; + : (activeGroup?.items ?? []); const eventBanners = useMemo( () => resolveCreationEntryEventBanners(entryConfig), [entryConfig], @@ -318,18 +318,20 @@ export function CustomWorldCreationStartCard({
{visibleCreationTypes.map((item) => { const disabled = item.locked || busy; + const lockedBadge = item.badge.trim() || '暂未开放'; return (