Files
Genarrative/server-rs/crates/api-server/src/modules/play_flow.rs
T
kdletters e9c3dc1120 退役旧创作模板业务
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码
切换前端 active 入口并解除旧创作页面和路由编译链
移除旧后端路由、worker 与纯业务 crate 依赖
收敛 SpacetimeDB 模块为历史数据壳
同步 Nginx、Pingora、验证门禁与架构文档
2026-07-17 22:07:52 +08:00

1022 lines
36 KiB
Rust

// 旧创作模板聚合路由完整保留在源码中,但不再参与正式编译。
#[cfg(any())]
mod retired_creation_play_flow {
use axum::{
Router,
body::Body,
extract::DefaultBodyLimit,
http::Request,
middleware::{self, Next},
response::Response,
routing::{get, post},
};
use crate::{
ai_tasks::{
append_ai_text_chunk, attach_ai_result_reference, cancel_ai_task, complete_ai_stage,
complete_ai_task, create_ai_task, fail_ai_task, start_ai_task, start_ai_task_stage,
},
assets::get_asset_history,
auth::require_bearer_auth,
character_animation_assets::{
generate_character_animation, generate_editor_character_animation, generate_editor_video,
get_character_animation_job, get_character_workflow_cache,
import_character_animation_video, list_character_animation_templates,
publish_character_animation, put_role_asset_workflow, resolve_role_asset_workflow,
save_character_workflow_cache,
},
character_visual_assets::{
generate_character_visual, get_character_visual_job, publish_character_visual,
},
creation_agent_document_input::parse_creation_agent_document_input,
creation_entry_config::get_creation_entry_config_handler,
hyper3d_generation::{
get_hyper3d_downloads, get_hyper3d_task_status, submit_hyper3d_image_to_model,
submit_hyper3d_text_to_model,
},
runtime_browse_history::{
delete_runtime_browse_history, get_runtime_browse_history, post_runtime_browse_history,
},
runtime_chat::stream_runtime_npc_chat_turn,
runtime_chat_plain::{
generate_runtime_character_chat_suggestions, generate_runtime_character_chat_summary,
stream_runtime_character_chat_reply, stream_runtime_npc_chat_dialogue,
stream_runtime_npc_recruit_dialogue,
},
runtime_inventory::get_runtime_inventory_state,
runtime_profile::get_profile_play_stats,
runtime_save::{delete_runtime_snapshot, get_runtime_snapshot, put_runtime_snapshot},
runtime_save::{list_profile_save_archives, resume_profile_save_archive},
runtime_settings::{get_runtime_settings, put_runtime_settings},
state::AppState,
vector_engine_audio_generation::{
generate_editor_background_music, generate_editor_sound_effect,
},
};
const HYPER3D_IMAGE_TO_MODEL_BODY_LIMIT_BYTES: usize = 56 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PlayFlowDomainAdapter {
pub play_id: &'static str,
pub module_key: &'static str,
pub creation_route_prefixes: &'static [&'static str],
pub runtime_route_prefixes: &'static [&'static str],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PlayFlowStage {
Creation,
Runtime,
CreationEntryConfig,
CreationSupport,
RuntimeSupport,
AiTask,
PublicReadModel,
RuntimeInventory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PlayFlowRequestContext {
pub stage: PlayFlowStage,
pub play_id: Option<&'static str>,
pub module_key: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CreationEntryRouteMatcher {
Exact(&'static str),
PrefixAndSuffix {
prefix: &'static str,
suffix: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CreationEntryRouteRule {
play_id: &'static str,
matcher: CreationEntryRouteMatcher,
}
impl CreationEntryRouteMatcher {
fn matches(self, normalized_path: &str) -> bool {
match self {
Self::Exact(path) => normalized_path == path,
Self::PrefixAndSuffix { prefix, suffix } => {
normalized_path.starts_with(prefix) && normalized_path.ends_with(suffix)
}
}
}
}
impl PlayFlowDomainAdapter {
fn creation_matches(self, normalized_path: &str) -> bool {
self.creation_route_prefixes
.iter()
.any(|prefix| normalized_path.starts_with(prefix))
}
fn runtime_matches(self, normalized_path: &str) -> bool {
self.runtime_route_prefixes
.iter()
.any(|prefix| normalized_path.starts_with(prefix))
}
}
// 中文注释:平台玩法流程先在这里统一注册;HTTP handler 仍在最后一步分发到各领域模块执行。
pub(crate) const PLAY_FLOW_DOMAIN_ADAPTERS: &[PlayFlowDomainAdapter] = &[
PlayFlowDomainAdapter {
play_id: "rpg",
module_key: "custom_world",
creation_route_prefixes: &[
"/api/runtime/custom-world/agent",
"/api/runtime/custom-world/profile",
"/api/runtime/custom-world-gallery/",
],
runtime_route_prefixes: &[
"/api/runtime/custom-world-library",
"/api/runtime/custom-world-gallery",
"/api/runtime/custom-world/",
],
},
PlayFlowDomainAdapter {
play_id: "creative-agent",
module_key: "story",
creation_route_prefixes: &["/api/runtime/creative-agent"],
runtime_route_prefixes: &["/api/story"],
},
PlayFlowDomainAdapter {
play_id: "big-fish",
module_key: "big_fish",
creation_route_prefixes: &["/api/runtime/big-fish/agent"],
runtime_route_prefixes: &["/api/runtime/big-fish"],
},
PlayFlowDomainAdapter {
play_id: "puzzle",
module_key: "puzzle",
creation_route_prefixes: &[
"/api/runtime/puzzle/agent",
"/api/runtime/puzzle/onboarding",
"/api/runtime/puzzle/gallery/",
],
runtime_route_prefixes: &["/api/runtime/puzzle"],
},
PlayFlowDomainAdapter {
play_id: "square-hole",
module_key: "square_hole",
creation_route_prefixes: &["/api/creation/square-hole"],
runtime_route_prefixes: &["/api/runtime/square-hole"],
},
PlayFlowDomainAdapter {
play_id: "wooden-fish",
module_key: "wooden_fish",
creation_route_prefixes: &["/api/creation/wooden-fish"],
runtime_route_prefixes: &["/api/runtime/wooden-fish"],
},
PlayFlowDomainAdapter {
play_id: "puzzle-clear",
module_key: "puzzle_clear",
creation_route_prefixes: &["/api/creation/puzzle-clear"],
runtime_route_prefixes: &["/api/runtime/puzzle-clear"],
},
PlayFlowDomainAdapter {
play_id: "bark-battle",
module_key: "bark_battle",
creation_route_prefixes: &["/api/creation/bark-battle"],
runtime_route_prefixes: &["/api/runtime/bark-battle"],
},
PlayFlowDomainAdapter {
play_id: "visual-novel",
module_key: "visual_novel",
creation_route_prefixes: &["/api/creation/visual-novel", "/api/creation/audio"],
runtime_route_prefixes: &["/api/runtime/visual-novel"],
},
PlayFlowDomainAdapter {
play_id: "baby-object-match",
module_key: "edutainment",
creation_route_prefixes: &["/api/creation/edutainment/baby-object-match"],
runtime_route_prefixes: &[],
},
PlayFlowDomainAdapter {
play_id: "baby-love-drawing",
module_key: "edutainment",
creation_route_prefixes: &["/api/creation/edutainment/baby-love-drawing"],
runtime_route_prefixes: &[],
},
];
const NEW_CREATION_ROUTE_RULES: &[CreationEntryRouteRule] = &[
CreationEntryRouteRule {
play_id: "puzzle",
matcher: CreationEntryRouteMatcher::Exact("/api/runtime/puzzle/agent/sessions"),
},
CreationEntryRouteRule {
play_id: "puzzle",
matcher: CreationEntryRouteMatcher::Exact("/api/runtime/puzzle/onboarding/generate"),
},
CreationEntryRouteRule {
play_id: "puzzle",
matcher: CreationEntryRouteMatcher::PrefixAndSuffix {
prefix: "/api/runtime/puzzle/gallery/",
suffix: "/remix",
},
},
CreationEntryRouteRule {
play_id: "big-fish",
matcher: CreationEntryRouteMatcher::Exact("/api/runtime/big-fish/agent/sessions"),
},
CreationEntryRouteRule {
play_id: "big-fish",
matcher: CreationEntryRouteMatcher::PrefixAndSuffix {
prefix: "/api/runtime/big-fish/gallery/",
suffix: "/remix",
},
},
CreationEntryRouteRule {
play_id: "rpg",
matcher: CreationEntryRouteMatcher::Exact("/api/runtime/custom-world/agent/sessions"),
},
CreationEntryRouteRule {
play_id: "rpg",
matcher: CreationEntryRouteMatcher::Exact("/api/runtime/custom-world/profile"),
},
CreationEntryRouteRule {
play_id: "rpg",
matcher: CreationEntryRouteMatcher::PrefixAndSuffix {
prefix: "/api/runtime/custom-world-gallery/",
suffix: "/remix",
},
},
CreationEntryRouteRule {
play_id: "square-hole",
matcher: CreationEntryRouteMatcher::Exact("/api/creation/square-hole/sessions"),
},
CreationEntryRouteRule {
play_id: "bark-battle",
matcher: CreationEntryRouteMatcher::Exact("/api/creation/bark-battle/drafts"),
},
CreationEntryRouteRule {
play_id: "wooden-fish",
matcher: CreationEntryRouteMatcher::Exact("/api/creation/wooden-fish/sessions"),
},
CreationEntryRouteRule {
play_id: "puzzle-clear",
matcher: CreationEntryRouteMatcher::Exact("/api/creation/puzzle-clear/sessions"),
},
CreationEntryRouteRule {
play_id: "visual-novel",
matcher: CreationEntryRouteMatcher::Exact("/api/creation/visual-novel/sessions"),
},
CreationEntryRouteRule {
play_id: "baby-object-match",
matcher: CreationEntryRouteMatcher::Exact(
"/api/creation/edutainment/baby-object-match/assets",
),
},
CreationEntryRouteRule {
play_id: "baby-love-drawing",
matcher: CreationEntryRouteMatcher::Exact(
"/api/creation/edutainment/baby-love-drawing/magic",
),
},
];
pub fn router(state: AppState) -> Router<AppState> {
assert_play_flow_domain_registry();
Router::new()
.merge(play_flow_support_router(state.clone()))
.merge(super::public_work::router(state.clone()))
.merge(super::story::router(state.clone()))
.merge(super::custom_world::router(state.clone()))
.merge(super::big_fish::router(state.clone()))
.merge(super::bark_battle::router(state.clone()))
.merge(super::square_hole::router(state.clone()))
.merge(super::wooden_fish::router(state.clone()))
.merge(super::puzzle_clear::router(state.clone()))
.merge(super::puzzle::router(state.clone()))
.merge(super::visual_novel::router(state.clone()))
.merge(super::edutainment::router(state.clone()))
.route(
"/api/runtime/sessions/{runtime_session_id}/inventory",
get(get_runtime_inventory_state)
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
)
.layer(middleware::from_fn(attach_play_flow_request_context))
}
fn play_flow_support_router(state: AppState) -> Router<AppState> {
Router::new()
.route(
"/api/creation-entry/config",
get(get_creation_entry_config_handler),
)
.route(
"/api/runtime/chat/character/suggestions",
post(generate_runtime_character_chat_suggestions).route_layer(
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
),
)
.route(
"/api/runtime/chat/character/summary",
post(generate_runtime_character_chat_summary).route_layer(
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
),
)
.route(
"/api/runtime/chat/character/reply/stream",
post(stream_runtime_character_chat_reply).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/chat/npc/dialogue/stream",
post(stream_runtime_npc_chat_dialogue).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/chat/npc/turn/stream",
post(stream_runtime_npc_chat_turn).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/chat/npc/recruit/stream",
post(stream_runtime_npc_recruit_dialogue).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/creation-agent/document-inputs/parse",
post(parse_creation_agent_document_input).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks",
post(create_ai_task).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/start",
post(start_ai_task).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/stages/{stage_kind}/start",
post(start_ai_task_stage).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/chunks",
post(append_ai_text_chunk).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/stages/{stage_kind}/complete",
post(complete_ai_stage).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/references",
post(attach_ai_result_reference).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/complete",
post(complete_ai_task).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/fail",
post(fail_ai_task).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/ai/tasks/{task_id}/cancel",
post(cancel_ai_task).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/assets/character-visual/generate",
post(generate_character_visual),
)
.route(
"/api/assets/character-visual/jobs/{task_id}",
get(get_character_visual_job),
)
.route(
"/api/assets/character-visual/publish",
post(publish_character_visual),
)
.route(
"/api/assets/character-animation/generate",
post(generate_character_animation),
)
.route(
"/api/editor/character-animations/generations",
post(generate_editor_character_animation).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/videos/generations",
post(generate_editor_video).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/audios/sound-effects/generations",
post(generate_editor_sound_effect).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/audios/background-music/generations",
post(generate_editor_background_music).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/assets/character-animation/jobs/{task_id}",
get(get_character_animation_job),
)
.route(
"/api/assets/character-animation/publish",
post(publish_character_animation),
)
.route(
"/api/assets/character-animation/import-video",
post(import_character_animation_video),
)
.route(
"/api/assets/character-animation/templates",
get(list_character_animation_templates),
)
.route(
"/api/assets/character-workflow-cache",
post(save_character_workflow_cache),
)
.route(
"/api/assets/character-workflow-cache/{character_id}",
get(get_character_workflow_cache),
)
.route(
"/api/assets/history",
get(get_asset_history).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/custom-world/asset-studio/role/{character_id}/workflow",
post(resolve_role_asset_workflow).put(put_role_asset_workflow),
)
.route(
"/api/assets/hyper3d/text-to-model",
post(submit_hyper3d_text_to_model).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/assets/hyper3d/image-to-model",
post(submit_hyper3d_image_to_model)
.layer(DefaultBodyLimit::max(
HYPER3D_IMAGE_TO_MODEL_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/assets/hyper3d/status",
post(get_hyper3d_task_status).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/assets/hyper3d/download",
post(get_hyper3d_downloads).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/settings",
get(get_runtime_settings)
.put(put_runtime_settings)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/save/snapshot",
get(get_runtime_snapshot)
.put(put_runtime_snapshot)
.delete(delete_runtime_snapshot)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/browse-history",
get(get_runtime_browse_history)
.post(post_runtime_browse_history)
.delete(delete_runtime_browse_history)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/save-archives",
get(list_profile_save_archives).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/save-archives/{world_key}",
post(resume_profile_save_archive).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/play-stats",
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
}
pub(crate) fn play_flow_domain_adapters() -> &'static [PlayFlowDomainAdapter] {
PLAY_FLOW_DOMAIN_ADAPTERS
}
fn assert_play_flow_domain_registry() {
debug_assert!(
play_flow_domain_adapters().iter().all(|adapter| {
!adapter.play_id.is_empty()
&& !adapter.module_key.is_empty()
&& (!adapter.creation_route_prefixes.is_empty()
|| !adapter.runtime_route_prefixes.is_empty())
}),
"play flow domain adapters must declare identity and at least one route prefix"
);
}
pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
let normalized = path.trim_end_matches('/');
NEW_CREATION_ROUTE_RULES
.iter()
.find(|rule| rule.matcher.matches(normalized))
.map(|rule| rule.play_id)
}
pub(crate) fn resolve_play_flow_request_context(path: &str) -> Option<PlayFlowRequestContext> {
let normalized = path.trim_end_matches('/');
if let Some(context) = resolve_play_flow_support_context(normalized) {
return Some(context);
}
if normalized == "/api/public-works" || normalized.starts_with("/api/public-works/") {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::PublicReadModel,
play_id: None,
module_key: "public_work",
});
}
if normalized.starts_with("/api/runtime/sessions/") && normalized.ends_with("/inventory") {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeInventory,
play_id: None,
module_key: "runtime_inventory",
});
}
play_flow_domain_adapters()
.iter()
.find(|adapter| adapter.creation_matches(normalized))
.map(|adapter| PlayFlowRequestContext {
stage: PlayFlowStage::Creation,
play_id: Some(adapter.play_id),
module_key: adapter.module_key,
})
.or_else(|| {
play_flow_domain_adapters()
.iter()
.find(|adapter| adapter.runtime_matches(normalized))
.map(|adapter| PlayFlowRequestContext {
stage: PlayFlowStage::Runtime,
play_id: Some(adapter.play_id),
module_key: adapter.module_key,
})
})
}
fn resolve_play_flow_support_context(normalized_path: &str) -> Option<PlayFlowRequestContext> {
if normalized_path == "/api/creation-entry/config" {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationEntryConfig,
play_id: None,
module_key: "creation_entry_config",
});
}
if normalized_path.starts_with("/api/runtime/chat/") {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: Some("rpg"),
module_key: "runtime_chat",
});
}
if normalized_path == "/api/runtime/creation-agent/document-inputs/parse" {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: Some("creative-agent"),
module_key: "creation_agent_document_input",
});
}
if normalized_path == "/api/ai/tasks" || normalized_path.starts_with("/api/ai/tasks/") {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::AiTask,
play_id: None,
module_key: "ai_tasks",
});
}
if normalized_path.starts_with("/api/assets/character-visual/") {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "character_visual_assets",
});
}
if normalized_path.starts_with("/api/assets/character-animation/") {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "character_animation_assets",
});
}
if normalized_path == "/api/assets/character-workflow-cache"
|| normalized_path.starts_with("/api/assets/character-workflow-cache/")
|| normalized_path.starts_with("/api/runtime/custom-world/asset-studio/")
{
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: Some("rpg"),
module_key: "custom_world_asset_studio",
});
}
if normalized_path == "/api/assets/history" {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "asset_history",
});
}
if normalized_path.starts_with("/api/assets/hyper3d/") {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "hyper3d_generation",
});
}
if normalized_path == "/api/runtime/settings" {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_settings",
});
}
if normalized_path == "/api/runtime/save/snapshot" {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_save",
});
}
if normalized_path == "/api/profile/browse-history" {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_browse_history",
});
}
if normalized_path == "/api/profile/save-archives"
|| normalized_path.starts_with("/api/profile/save-archives/")
{
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_save_archives",
});
}
if normalized_path == "/api/profile/play-stats" {
return Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "profile_play_stats",
});
}
None
}
async fn attach_play_flow_request_context(mut request: Request<Body>, next: Next) -> Response {
if let Some(context) = resolve_play_flow_request_context(request.uri().path()) {
request.extensions_mut().insert(context);
}
next.run(request).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn play_flow_registry_keeps_current_domain_adapters_together() {
let adapters = play_flow_domain_adapters();
for play_id in [
"rpg",
"creative-agent",
"big-fish",
"puzzle",
"square-hole",
"wooden-fish",
"puzzle-clear",
"bark-battle",
"visual-novel",
"baby-object-match",
"baby-love-drawing",
] {
assert!(
adapters.iter().any(|adapter| adapter.play_id == play_id),
"{play_id} should be registered in the unified play flow"
);
}
}
#[test]
fn resolves_new_creation_paths_before_domain_fanout() {
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/puzzle/agent/sessions"),
Some("puzzle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/puzzle-clear/sessions"),
Some("puzzle-clear"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/puzzle/gallery/profile-1/remix"),
Some("puzzle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/square-hole/sessions"),
Some("square-hole"),
);
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-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"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/story/sessions/runtime"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/chat/npc/turn/stream"),
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"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/puzzle-clear/runs/run-1"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/wooden-fish/sessions"),
Some("wooden-fish"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/edutainment/baby-object-match/assets"),
Some("baby-object-match"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/edutainment/baby-love-drawing/magic"),
Some("baby-love-drawing"),
);
assert_eq!(resolve_creation_entry_route_id("/healthz"), None);
}
#[test]
fn resolves_unified_play_flow_context_before_domain_handlers() {
assert_eq!(
resolve_play_flow_request_context("/api/public-works/JH-12345678"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::PublicReadModel,
play_id: None,
module_key: "public_work",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/runtime/sessions/runtime-1/inventory"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeInventory,
play_id: None,
module_key: "runtime_inventory",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/creation-entry/config"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationEntryConfig,
play_id: None,
module_key: "creation_entry_config",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/runtime/chat/npc/turn/stream"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: Some("rpg"),
module_key: "runtime_chat",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/runtime/creation-agent/document-inputs/parse"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: Some("creative-agent"),
module_key: "creation_agent_document_input",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/ai/tasks/aitask_001/complete"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::AiTask,
play_id: None,
module_key: "ai_tasks",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/assets/character-workflow-cache/role-1"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: Some("rpg"),
module_key: "custom_world_asset_studio",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/assets/character-visual/generate"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "character_visual_assets",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/assets/history"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "asset_history",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/assets/character-animation/jobs/task-1"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "character_animation_assets",
}),
);
assert_eq!(
resolve_play_flow_request_context(
"/api/runtime/custom-world/asset-studio/role/role-1/workflow"
),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: Some("rpg"),
module_key: "custom_world_asset_studio",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/assets/hyper3d/text-to-model"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::CreationSupport,
play_id: None,
module_key: "hyper3d_generation",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/runtime/settings"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_settings",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/runtime/save/snapshot"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_save",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/profile/browse-history"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_browse_history",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/profile/save-archives/world-1"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "runtime_save_archives",
}),
);
assert_eq!(
resolve_play_flow_request_context("/api/profile/play-stats"),
Some(PlayFlowRequestContext {
stage: PlayFlowStage::RuntimeSupport,
play_id: None,
module_key: "profile_play_stats",
}),
);
assert_eq!(resolve_play_flow_request_context("/api/profile/me"), None);
}
}
}