e9c3dc1120
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码 切换前端 active 入口并解除旧创作页面和路由编译链 移除旧后端路由、worker 与纯业务 crate 依赖 收敛 SpacetimeDB 模块为历史数据壳 同步 Nginx、Pingora、验证门禁与架构文档
1022 lines
36 KiB
Rust
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);
|
|
}
|
|
}
|
|
|
|
}
|