diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 1394d891..6e3c3458 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -34,6 +34,8 @@ RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts` 生成任务在用户离开生成页后异步完成时,平台壳层必须弹出 `PlatformTaskCompletionDialog`。完成弹窗同样要带来源,例如某个草稿或生成会话,并提供复制按钮复制“来源 + 状态”;如果用户仍停留在生成页并被自动带入结果页或试玩页,生成页 / 结果页本身即为完成反馈,不再额外叠加完成弹窗。 +入口配置中的 `open=false` 表示关闭新建创作入口,不表示下架已有草稿、私有作品或公开作品。api-server 的入口熔断只允许拦截新建创作、新建草稿、首次生成入口和 Remix 成草稿等会产生新创作的请求;公开广场列表、公开详情、点赞、已发布作品启动、运行态过程请求、存档 / 浏览记录和已有作品回读不能因为创作入口关闭而返回 `creation_entry_disabled`。平台首页如果遇到旧服务端返回的 `creation_entry_disabled`,只能降级为空列表或隐藏入口,不弹平台级错误弹窗。 + `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 a9fbf3d5..6bc88ace 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/platform-entry/platformDraftGenerationShelfModel.test.ts b/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts index 7242e7b1..0a2f4949 100644 --- a/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts +++ b/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts @@ -337,6 +337,24 @@ describe('platformDraftGenerationShelfModel', () => { ).toMatchObject({ type: 'load-detail', }); + + expect( + resolveJumpHopDraftOpenIntent({ + item: buildJumpHopWork({ sourceSessionId: null }), + notices: { + 'jump-hop:jump-hop-work-base': { + status: 'failed', + seen: false, + }, + }, + generation: emptyGenerationFacts({ + activeSessionId: null, + hasActiveGenerationFailure: true, + }), + }), + ).toMatchObject({ + type: 'load-detail', + }); }); test('resolveWoodenFishDraftOpenIntent uses profile fallback and failure fallback stage', () => { diff --git a/src/components/platform-entry/platformDraftGenerationShelfModel.ts b/src/components/platform-entry/platformDraftGenerationShelfModel.ts index de16f098..da69f6aa 100644 --- a/src/components/platform-entry/platformDraftGenerationShelfModel.ts +++ b/src/components/platform-entry/platformDraftGenerationShelfModel.ts @@ -897,8 +897,9 @@ export function resolveJumpHopDraftOpenIntent(params: { noticeIds, 'failed', ); + const activeSessionId = normalizeDraftNoticeId(generation.activeSessionId); const isCurrentSession = - sourceSessionId === normalizeDraftNoticeId(generation.activeSessionId); + sourceSessionId !== null && sourceSessionId === activeSessionId; if ( hasFailedNotice &&