feat: add puzzle clear template runtime
This commit is contained in:
@@ -64,6 +64,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.merge(modules::jump_hop::router(state.clone()))
|
||||
.merge(modules::wooden_fish::router(state.clone()))
|
||||
.merge(modules::public_work::router(state.clone()))
|
||||
.merge(modules::puzzle_clear::router(state.clone()))
|
||||
.merge(modules::puzzle::router(state.clone()))
|
||||
.merge(visual_novel_router(state.clone()))
|
||||
.route(
|
||||
|
||||
@@ -94,13 +94,11 @@ pub async fn generate_character_visual(
|
||||
.map_err(|error| character_visual_error_response(&request_context, error))?;
|
||||
|
||||
let result = async {
|
||||
let settings = require_openai_image_settings(&state)?
|
||||
.with_external_api_audit_context(
|
||||
&request_context,
|
||||
Some(owner_user_id.clone()),
|
||||
Some(character_id.clone()),
|
||||
)
|
||||
;
|
||||
let settings = require_openai_image_settings(&state)?.with_external_api_audit_context(
|
||||
&request_context,
|
||||
Some(owner_user_id.clone()),
|
||||
Some(character_id.clone()),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
|
||||
state
|
||||
@@ -324,10 +322,8 @@ pub(crate) async fn generate_character_primary_visual_for_profile(
|
||||
&model,
|
||||
&prompt,
|
||||
)?;
|
||||
let settings = require_openai_image_settings(state)?.with_external_api_audit_actor(
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(character_id.clone()),
|
||||
);
|
||||
let settings = require_openai_image_settings(state)?
|
||||
.with_external_api_audit_actor(Some(owner_user_id.to_string()), Some(character_id.clone()));
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
state
|
||||
.ai_task_service()
|
||||
|
||||
@@ -72,6 +72,12 @@ 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-clear") {
|
||||
return Some("puzzle-clear");
|
||||
}
|
||||
if normalized.starts_with("/api/creation/puzzle-clear") {
|
||||
return Some("puzzle-clear");
|
||||
}
|
||||
if normalized.starts_with("/api/runtime/puzzle") {
|
||||
return Some("puzzle");
|
||||
}
|
||||
@@ -173,6 +179,14 @@ mod tests {
|
||||
resolve_creation_entry_route_id("/api/runtime/puzzle/works"),
|
||||
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-clear/runs/run-1"),
|
||||
Some("puzzle-clear"),
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_creation_entry_route_id("/api/runtime/match3d/runs/run-1"),
|
||||
Some("match3d"),
|
||||
|
||||
@@ -553,12 +553,11 @@ pub async fn generate_custom_world_scene_image(
|
||||
"scene_image",
|
||||
asset_id.as_str(),
|
||||
async {
|
||||
let settings = require_openai_image_settings(&state)?
|
||||
.with_external_api_audit_context(
|
||||
&request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
normalized.profile_id.clone(),
|
||||
);
|
||||
let settings = require_openai_image_settings(&state)?.with_external_api_audit_context(
|
||||
&request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
normalized.profile_id.clone(),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let reference_image =
|
||||
if let Some(reference_image_src) = normalized.reference_image_src.as_deref() {
|
||||
|
||||
@@ -1052,6 +1052,7 @@ mod tests {
|
||||
external_api_audit_state: None,
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -414,12 +414,11 @@ async fn maybe_generate_jump_hop_assets(
|
||||
|
||||
let settings = require_openai_image_settings(state)
|
||||
.map(|settings| {
|
||||
settings
|
||||
.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.clone()),
|
||||
)
|
||||
settings.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.clone()),
|
||||
)
|
||||
})
|
||||
.map_err(|error| {
|
||||
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
|
||||
|
||||
@@ -64,6 +64,7 @@ mod prompt;
|
||||
mod public_work;
|
||||
mod puzzle;
|
||||
mod puzzle_agent_turn;
|
||||
mod puzzle_clear;
|
||||
mod puzzle_gallery_cache;
|
||||
mod refresh_session;
|
||||
mod registration_reward;
|
||||
|
||||
@@ -755,12 +755,11 @@ async fn generate_match3d_material_sheet_from_level_scene(
|
||||
config: &Match3DConfigJson,
|
||||
background_asset: Option<&Match3DGeneratedBackgroundAsset>,
|
||||
) -> Result<Match3DMaterialSheet, AppError> {
|
||||
let settings = require_openai_image_settings(state)?
|
||||
.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let prompt = build_match3d_item_spritesheet_prompt();
|
||||
let reference = load_match3d_level_scene_reference_image(state, background_asset).await?;
|
||||
|
||||
@@ -304,12 +304,11 @@ pub(super) async fn generate_match3d_cover_image_asset(
|
||||
reference_image_srcs: Vec<String>,
|
||||
) -> Result<Match3DAssetUpload, AppError> {
|
||||
require_match3d_oss_client(state)?;
|
||||
let settings = require_openai_image_settings(state)?
|
||||
.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let cover_prompt = build_match3d_cover_generation_prompt(config, prompt);
|
||||
let generated = if let Some(uploaded_image) = resolve_match3d_reference_image_for_edit(
|
||||
@@ -459,12 +458,11 @@ pub(super) async fn generate_match3d_level_asset_bundle(
|
||||
prompt: &str,
|
||||
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
|
||||
require_match3d_oss_client(state)?;
|
||||
let settings = require_openai_image_settings(state)?
|
||||
.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
|
||||
let level_scene_prompt = build_match3d_level_scene_generation_prompt(config);
|
||||
@@ -607,12 +605,11 @@ pub(super) async fn generate_match3d_container_image(
|
||||
prompt: &str,
|
||||
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
|
||||
require_match3d_oss_client(state)?;
|
||||
let settings = require_openai_image_settings(state)?
|
||||
.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let reference_image = load_match3d_container_reference_image()?;
|
||||
let container_prompt = build_match3d_container_generation_prompt(config, prompt);
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod platform;
|
||||
pub mod profile;
|
||||
pub mod public_work;
|
||||
pub mod puzzle;
|
||||
pub mod puzzle_clear;
|
||||
pub mod square_hole;
|
||||
pub mod story;
|
||||
pub mod wooden_fish;
|
||||
|
||||
116
server-rs/crates/api-server/src/modules/puzzle_clear.rs
Normal file
116
server-rs/crates/api-server/src/modules/puzzle_clear.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use axum::{
|
||||
Router, middleware,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||
puzzle_clear::{
|
||||
advance_puzzle_clear_next_level, create_puzzle_clear_session, execute_puzzle_clear_action,
|
||||
get_puzzle_clear_gallery_detail, get_puzzle_clear_run, get_puzzle_clear_runtime_work,
|
||||
get_puzzle_clear_session, get_puzzle_clear_work, list_puzzle_clear_gallery,
|
||||
list_puzzle_clear_works, mark_puzzle_clear_level_time_up, publish_puzzle_clear_work,
|
||||
retry_puzzle_clear_level, start_puzzle_clear_run, swap_puzzle_clear_cards,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub fn router(state: AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/api/creation/puzzle-clear/sessions",
|
||||
post(create_puzzle_clear_session).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/puzzle-clear/sessions/{session_id}",
|
||||
get(get_puzzle_clear_session).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/puzzle-clear/sessions/{session_id}/actions",
|
||||
post(execute_puzzle_clear_action).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/puzzle-clear/works",
|
||||
get(list_puzzle_clear_works).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/puzzle-clear/works/{profile_id}",
|
||||
get(get_puzzle_clear_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/puzzle-clear/works/{profile_id}/publish",
|
||||
post(publish_puzzle_clear_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/works/{profile_id}",
|
||||
get(get_puzzle_clear_runtime_work),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/runs",
|
||||
post(start_puzzle_clear_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/runs/{run_id}",
|
||||
get(get_puzzle_clear_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/runs/{run_id}/swap",
|
||||
post(swap_puzzle_clear_cards).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/runs/{run_id}/retry-level",
|
||||
post(retry_puzzle_clear_level).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/runs/{run_id}/next-level",
|
||||
post(advance_puzzle_clear_next_level).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/runs/{run_id}/time-up",
|
||||
post(mark_puzzle_clear_level_time_up).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/gallery",
|
||||
get(list_puzzle_clear_gallery),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle-clear/gallery/{public_work_code}",
|
||||
get(get_puzzle_clear_gallery_detail),
|
||||
)
|
||||
}
|
||||
@@ -310,12 +310,11 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
|
||||
level_name: &str,
|
||||
puzzle_image: &PuzzleDownloadedImage,
|
||||
) -> Result<GeneratedPuzzleLevelAssetBundle, AppError> {
|
||||
let settings = require_puzzle_vector_engine_settings(state)?
|
||||
.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(session_id.to_string()),
|
||||
);
|
||||
let settings = require_puzzle_vector_engine_settings(state)?.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(session_id.to_string()),
|
||||
);
|
||||
let http_client = build_puzzle_image_http_client(state, PuzzleImageModel::GptImage2)?;
|
||||
let puzzle_reference = build_puzzle_downloaded_image_reference(puzzle_image);
|
||||
let scene_generated = create_puzzle_vector_engine_image_generation(
|
||||
|
||||
@@ -117,11 +117,9 @@ impl PuzzleVectorEngineSettings {
|
||||
) -> Self {
|
||||
self.external_api_audit_user_id = user_id;
|
||||
self.external_api_audit_profile_id = profile_id;
|
||||
self.external_api_audit_request_id =
|
||||
Some(request_context.request_id().to_string());
|
||||
self.external_api_audit_request_id = Some(request_context.request_id().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub(crate) struct ParsedPuzzleImageDataUrl {
|
||||
|
||||
1729
server-rs/crates/api-server/src/puzzle_clear.rs
Normal file
1729
server-rs/crates/api-server/src/puzzle_clear.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -398,12 +398,11 @@ async fn generate_square_hole_image_data_url(
|
||||
size: &str,
|
||||
failure_context: &str,
|
||||
) -> Result<String, AppError> {
|
||||
let settings = require_openai_image_settings(state)?
|
||||
.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
|
||||
request_context,
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let generated = create_openai_image_generation(
|
||||
&http_client,
|
||||
|
||||
@@ -1292,6 +1292,7 @@ fn current_utc_micros() -> i64 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AppConfig;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
|
||||
#[test]
|
||||
@@ -1461,8 +1462,9 @@ mod tests {
|
||||
assert_eq!(asset.prompt.as_deref(), Some("默认木鱼音"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_draft_uses_default_hit_sound_asset_and_ignores_prompt() {
|
||||
#[tokio::test]
|
||||
async fn wooden_fish_draft_uses_default_hit_sound_asset_and_ignores_prompt() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
let payload = WoodenFishWorkspaceCreateRequest {
|
||||
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
|
||||
work_title: "今日敲木鱼".to_string(),
|
||||
@@ -1475,7 +1477,9 @@ mod tests {
|
||||
floating_words: vec![],
|
||||
};
|
||||
|
||||
let draft = build_wooden_fish_draft(&payload);
|
||||
let draft = build_wooden_fish_draft(&payload, &state)
|
||||
.await
|
||||
.expect("draft should build");
|
||||
|
||||
assert!(draft.hit_sound_prompt.is_none());
|
||||
let asset = draft
|
||||
|
||||
Reference in New Issue
Block a user