From decded991ed90d04e39e00b5730c2b82e66db41f Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 7 Jun 2026 22:20:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86=E5=90=8E=E7=AB=AF=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除后端未使用的历史 helper、mapper、handler 和 re-export 将仅测试使用的导入、常量和辅助函数收口到 cfg(test) 补齐 Jump Hop 测试构造体字段并对齐 Match3D 当前素材表测试契约 验证后端 workspace cargo check 与 Match3D、Puzzle 相关测试 --- server-rs/crates/api-server/src/admin.rs | 1 + .../crates/api-server/src/bark_battle.rs | 1 + .../crates/api-server/src/custom_world.rs | 2 +- .../api-server/src/custom_world/mappers.rs | 35 -- .../crates/api-server/src/custom_world_ai.rs | 6 +- .../api-server/src/external_api_audit.rs | 3 + .../api-server/src/generated_asset_sheets.rs | 20 +- .../api-server/src/generated_image_assets.rs | 11 +- server-rs/crates/api-server/src/match3d.rs | 22 +- .../api-server/src/match3d/item_assets.rs | 4 +- .../crates/api-server/src/match3d/tests.rs | 84 ++-- .../src/match3d/vector_engine_gemini.rs | 361 ------------------ .../crates/api-server/src/match3d/works.rs | 49 +-- .../api-server/src/openai_image_generation.rs | 21 +- .../api-server/src/prompt/rpg/runtime_chat.rs | 72 ---- server-rs/crates/api-server/src/puzzle.rs | 4 +- .../crates/api-server/src/puzzle/draft.rs | 23 +- .../crates/api-server/src/puzzle/mappers.rs | 43 --- .../crates/api-server/src/puzzle/tests.rs | 8 +- .../api-server/src/puzzle/vector_engine.rs | 75 +--- .../api-server/src/puzzle_gallery_cache.rs | 6 +- server-rs/crates/api-server/src/state.rs | 7 +- server-rs/crates/api-server/src/telemetry.rs | 27 -- server-rs/crates/api-server/src/tracking.rs | 2 + .../src/vector_engine_audio_generation.rs | 2 - .../generation.rs | 88 +---- .../vector_engine_audio_generation/tasks.rs | 34 +- server-rs/crates/api-server/src/wechat_pay.rs | 2 +- .../crates/api-server/src/wooden_fish.rs | 27 -- .../crates/api-server/src/work_author.rs | 1 + .../crates/spacetime-client/src/jump_hop.rs | 2 + server-rs/crates/spacetime-client/src/lib.rs | 18 - .../src/mapper/custom_world.rs | 14 - .../spacetime-module/src/public_work.rs | 12 - .../spacetime-module/src/runtime/profile.rs | 168 -------- 35 files changed, 109 insertions(+), 1146 deletions(-) diff --git a/server-rs/crates/api-server/src/admin.rs b/server-rs/crates/api-server/src/admin.rs index 69c523f3..f46342a7 100644 --- a/server-rs/crates/api-server/src/admin.rs +++ b/server-rs/crates/api-server/src/admin.rs @@ -884,6 +884,7 @@ fn extract_sql_statement_columns(statement: &Value) -> Vec { .unwrap_or_default() } +#[cfg(test)] fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatabaseTableRowPayload { build_admin_database_table_row_for_table("", row, columns) } diff --git a/server-rs/crates/api-server/src/bark_battle.rs b/server-rs/crates/api-server/src/bark_battle.rs index 56cc47e7..392e894d 100644 --- a/server-rs/crates/api-server/src/bark_battle.rs +++ b/server-rs/crates/api-server/src/bark_battle.rs @@ -1052,6 +1052,7 @@ fn resolve_bark_battle_author_display_name(state: &AppState, owner_user_id: &str resolve_work_author_by_user_id(state, owner_user_id, None, None).display_name } +#[cfg(test)] fn normalize_author_display_name(display_name: Option) -> String { display_name .map(|value| value.trim().to_string()) diff --git a/server-rs/crates/api-server/src/custom_world.rs b/server-rs/crates/api-server/src/custom_world.rs index cb8d50d0..cd6d3240 100644 --- a/server-rs/crates/api-server/src/custom_world.rs +++ b/server-rs/crates/api-server/src/custom_world.rs @@ -37,7 +37,7 @@ use spacetime_client::{ CustomWorldAgentOperationRecord, CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord, CustomWorldDraftCardDetailRecord, CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord, - CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, + CustomWorldLibraryEntryRecord, CustomWorldProfileLikeReportRecordInput, CustomWorldProfilePlayReportRecordInput, CustomWorldProfileRemixRecordInput, CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord, CustomWorldResultPreviewBlockerRecord, diff --git a/server-rs/crates/api-server/src/custom_world/mappers.rs b/server-rs/crates/api-server/src/custom_world/mappers.rs index ee10422e..d078003f 100644 --- a/server-rs/crates/api-server/src/custom_world/mappers.rs +++ b/server-rs/crates/api-server/src/custom_world/mappers.rs @@ -114,41 +114,6 @@ pub(super) fn build_custom_world_library_list_profile_payload( }) } -pub(super) fn map_custom_world_gallery_card_response( - state: &AppState, - entry: CustomWorldGalleryEntryRecord, -) -> CustomWorldGalleryCardResponse { - let author = resolve_work_author_by_user_id( - state, - &entry.owner_user_id, - Some(&entry.author_display_name), - Some(&entry.author_public_user_code), - ); - CustomWorldGalleryCardResponse { - owner_user_id: entry.owner_user_id, - profile_id: entry.profile_id, - public_work_code: entry.public_work_code, - author_public_user_code: author - .public_user_code - .unwrap_or(entry.author_public_user_code), - visibility: entry.visibility, - published_at: entry.published_at, - updated_at: entry.updated_at, - author_display_name: author.display_name, - world_name: entry.world_name, - subtitle: entry.subtitle, - summary_text: entry.summary_text, - cover_image_src: entry.cover_image_src, - theme_mode: entry.theme_mode, - playable_npc_count: entry.playable_npc_count, - landmark_count: entry.landmark_count, - play_count: entry.play_count, - remix_count: entry.remix_count, - like_count: entry.like_count, - recent_play_count_7d: entry.recent_play_count_7d, - } -} - pub(super) fn map_public_work_custom_world_gallery_card_response( state: &AppState, entry: spacetime_client::PublicWorkGalleryEntryRecord, diff --git a/server-rs/crates/api-server/src/custom_world_ai.rs b/server-rs/crates/api-server/src/custom_world_ai.rs index 932f5099..74b93c70 100644 --- a/server-rs/crates/api-server/src/custom_world_ai.rs +++ b/server-rs/crates/api-server/src/custom_world_ai.rs @@ -10,9 +10,9 @@ use axum::{ response::Response, }; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; -use image::{ - DynamicImage, GenericImageView, ImageFormat, codecs::jpeg::JpegEncoder, imageops::FilterType, -}; +use image::{DynamicImage, GenericImageView, codecs::jpeg::JpegEncoder, imageops::FilterType}; +#[cfg(test)] +use image::ImageFormat; use module_assets::{ AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input, build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id, diff --git a/server-rs/crates/api-server/src/external_api_audit.rs b/server-rs/crates/api-server/src/external_api_audit.rs index 78c0a40b..11a104d5 100644 --- a/server-rs/crates/api-server/src/external_api_audit.rs +++ b/server-rs/crates/api-server/src/external_api_audit.rs @@ -1,3 +1,4 @@ +#[cfg(test)] use axum::http::StatusCode; use module_runtime::RuntimeTrackingScopeKind; use platform_image::PlatformImageFailureAudit; @@ -157,6 +158,7 @@ pub(crate) fn build_external_api_failure_draft_from_platform_image_audit( } /// 中文注释:下载图片、OSS 读写等非标准 HTTP 状态统一显式归类,避免 OTLP 低基数 label 误落到 `transport`。 +#[cfg(test)] pub(crate) fn app_error_status_class(status_code: StatusCode) -> &'static str { status_class(Some(status_code.as_u16())) } @@ -304,6 +306,7 @@ fn build_external_api_failure_metadata(failure: &ExternalApiFailureDraft) -> Val metadata } +#[cfg(test)] pub(crate) fn is_retryable_external_api_failure( status_code: Option, timeout: bool, diff --git a/server-rs/crates/api-server/src/generated_asset_sheets.rs b/server-rs/crates/api-server/src/generated_asset_sheets.rs index b5df860e..5c800414 100644 --- a/server-rs/crates/api-server/src/generated_asset_sheets.rs +++ b/server-rs/crates/api-server/src/generated_asset_sheets.rs @@ -9,29 +9,13 @@ use crate::{ #[allow(unused_imports)] pub(crate) use generated_asset_sheets_impl::{ GeneratedAssetSheetAlphaOptions, GeneratedAssetSheetError, GeneratedAssetSheetKeyColor, - GeneratedAssetSheetPersistInput, GeneratedAssetSheetPersistPrompt, - GeneratedAssetSheetPromptInput, GeneratedAssetSheetSliceImage, GeneratedAssetSheetUpload, + GeneratedAssetSheetPersistInput, GeneratedAssetSheetPersistPrompt, GeneratedAssetSheetSliceImage, + GeneratedAssetSheetUpload, apply_generated_asset_sheet_alpha_with_options, apply_generated_asset_sheet_green_screen_alpha, crop_generated_asset_sheet_view_edge_matte, crop_generated_asset_sheet_view_edge_matte_with_options, }; -pub(crate) fn build_generated_asset_sheet_prompt( - input: &GeneratedAssetSheetPromptInput<'_>, -) -> Result { - generated_asset_sheets_impl::build_generated_asset_sheet_prompt(input) - .map_err(map_generated_asset_sheet_error) -} - -pub(crate) fn slice_generated_asset_sheet( - image: &DownloadedOpenAiImage, - item_names: &[String], - grid_size: usize, -) -> Result>, AppError> { - generated_asset_sheets_impl::slice_generated_asset_sheet(image, item_names, grid_size) - .map_err(map_generated_asset_sheet_error) -} - pub(crate) fn slice_generated_asset_sheet_two_items_per_row( image: &DownloadedOpenAiImage, item_names: &[String], diff --git a/server-rs/crates/api-server/src/generated_image_assets.rs b/server-rs/crates/api-server/src/generated_image_assets.rs index 5f4da592..0da9476a 100644 --- a/server-rs/crates/api-server/src/generated_image_assets.rs +++ b/server-rs/crates/api-server/src/generated_image_assets.rs @@ -6,15 +6,8 @@ pub mod helpers { pub use platform_image::generated_assets::helpers::*; } -pub(crate) use adapter::{ - GeneratedImageAssetAdapter, GeneratedImageAssetAdapterBoundary, - GeneratedImageAssetAdapterMetadata, GeneratedImageAssetPersistInput, - GeneratedImageAssetPreparedPut, -}; +pub(crate) use adapter::GeneratedImageAssetAdapter; pub(crate) use helpers::{ - GeneratedImageAssetDataUrl, GeneratedImageAssetHelperError, GeneratedImageAssetImageFormat, - GeneratedImageAssetMetadataInput, GeneratedImageAssetStoragePaths, - build_generated_image_asset_metadata, build_generated_image_asset_storage_paths, - decode_generated_image_asset_data_url, merge_generated_image_asset_metadata, + GeneratedImageAssetDataUrl, decode_generated_image_asset_data_url, normalize_generated_image_asset_mime, }; diff --git a/server-rs/crates/api-server/src/match3d.rs b/server-rs/crates/api-server/src/match3d.rs index 62d73cf7..5f4f2e22 100644 --- a/server-rs/crates/api-server/src/match3d.rs +++ b/server-rs/crates/api-server/src/match3d.rs @@ -74,10 +74,9 @@ use crate::{ generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha, http_error::AppError, openai_image_generation::{ - DownloadedOpenAiImage, OpenAiGeneratedImages, OpenAiReferenceImage, - build_openai_image_http_client, create_openai_image_edit, - create_openai_image_edit_with_references, create_openai_image_generation, - require_openai_image_settings, + DownloadedOpenAiImage, OpenAiReferenceImage, build_openai_image_http_client, + create_openai_image_edit, create_openai_image_edit_with_references, + create_openai_image_generation, require_openai_image_settings, }, platform_errors::map_oss_error, request_context::RequestContext, @@ -87,7 +86,6 @@ use crate::{ }, work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success}, }; - const MATCH3D_AGENT_PROVIDER: &str = "match3d-agent"; const MATCH3D_WORKS_PROVIDER: &str = "match3d-works"; const MATCH3D_RUNTIME_PROVIDER: &str = "match3d-runtime"; @@ -101,7 +99,9 @@ const MATCH3D_MATERIAL_ITEM_BATCH_SIZE: usize = 20; const MATCH3D_ITEM_VIEW_COUNT: usize = 5; const MATCH3D_MATERIAL_GRID_SIZE: u32 = 10; const MATCH3D_MAX_GENERATED_ITEM_COUNT: usize = 20; +#[cfg(test)] const MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_MODEL: &str = "gemini-3-pro-image-preview"; +#[cfg(test)] const MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_ASPECT_RATIO: &str = "1:1"; const MATCH3D_LEGACY_MODEL_DOWNLOAD_TIMEOUT_MS: u64 = 3 * 60_000; const MATCH3D_OSS_PUT_TIMEOUT_MS: u64 = 3 * 60_000; @@ -509,7 +509,9 @@ use self::runtime::*; mod item_assets; use self::item_assets::*; +#[cfg(test)] mod vector_engine_gemini; +#[cfg(test)] use self::vector_engine_gemini::*; fn ensure_non_empty( @@ -528,6 +530,16 @@ fn ensure_non_empty( Ok(()) } +fn match3d_mime_to_extension(mime_type: &str) -> &str { + match mime_type { + "image/png" => "png", + "image/webp" => "webp", + "image/gif" => "gif", + "image/jpeg" | "image/jpg" => "jpg", + _ => "png", + } +} + fn match3d_json( payload: Result, JsonRejection>, request_context: &RequestContext, diff --git a/server-rs/crates/api-server/src/match3d/item_assets.rs b/server-rs/crates/api-server/src/match3d/item_assets.rs index f21ecbbf..670d3ca8 100644 --- a/server-rs/crates/api-server/src/match3d/item_assets.rs +++ b/server-rs/crates/api-server/src/match3d/item_assets.rs @@ -735,10 +735,9 @@ pub(super) struct Match3DMaterialSheet { pub(super) image: DownloadedOpenAiImage, } +#[cfg(test)] pub(super) struct Match3DVectorEngineGeminiImageSettings { pub(super) base_url: String, - pub(super) api_key: String, - pub(super) request_timeout_ms: u64, } #[cfg(test)] @@ -1482,6 +1481,7 @@ pub(super) fn is_match3d_background_asset_ready(asset: &Match3DGeneratedBackgrou .is_some()) } +#[cfg(test)] pub(super) fn build_match3d_material_sheet_prompt( config: &Match3DConfigJson, item_names: &[String], diff --git a/server-rs/crates/api-server/src/match3d/tests.rs b/server-rs/crates/api-server/src/match3d/tests.rs index 226be1f1..3f753d5a 100644 --- a/server-rs/crates/api-server/src/match3d/tests.rs +++ b/server-rs/crates/api-server/src/match3d/tests.rs @@ -1,7 +1,5 @@ use super::*; -use super::*; - fn test_match3d_generated_item_asset(index: u32, name: &str) -> Match3DGeneratedItemAsset { Match3DGeneratedItemAsset { item_id: format!("match3d-item-{index}"), @@ -149,17 +147,17 @@ fn match3d_item_image_path_segments_stay_unique_for_chinese_names() { } #[test] -fn match3d_material_sheet_slicing_uses_fixed_five_by_five_rows() { - let width = 500; - let height = 500; +fn match3d_material_sheet_slicing_uses_fixed_ten_by_ten_two_items_per_row() { + let width = 1000; + let height = 1000; let item_names = vec!["樱桃".to_string(), "苹果".to_string(), "香蕉".to_string()]; let mut sheet = image::RgbaImage::new(width, height); - for row in 0..5 { - for col in 0..5 { + for row in 0..10 { + for col in 0..10 { let color = image::Rgba([ - 32 + row as u8 * 40, - 24 + col as u8 * 36, - 210 - row as u8 * 30, + 24 + row as u8 * 16, + 30 + col as u8 * 14, + 210 - row as u8 * 10, 255, ]); for y in row * 100..(row + 1) * 100 { @@ -182,22 +180,24 @@ fn match3d_material_sheet_slicing_uses_fixed_five_by_five_rows() { let slices = slice_match3d_material_sheet(&image, &item_names).expect("sheet should slice"); assert_eq!(slices.len(), 3); - for (row, views) in slices.iter().enumerate() { + for (item_index, views) in slices.iter().enumerate() { assert_eq!(views.len(), MATCH3D_ITEM_VIEW_COUNT); - for (col, view) in views.iter().enumerate() { + for (view_index, view) in views.iter().enumerate() { let decoded = image::load_from_memory(view.bytes.as_slice()) .expect("view should decode") .to_rgba8(); let pixel = decoded.get_pixel(decoded.width() / 2, decoded.height() / 2); + let source_row = item_index / 2; + let source_col = (item_index % 2) * MATCH3D_ITEM_VIEW_COUNT + view_index; assert_eq!( pixel.0, [ - 32 + row as u8 * 40, - 24 + col as u8 * 36, - 210 - row as u8 * 30, + 24 + source_row as u8 * 16, + 30 + source_col as u8 * 14, + 210 - source_row as u8 * 10, 255, ], - "row {row} col {col} should be cut from the fixed 5*5 grid row" + "item {item_index} view {view_index} should be cut from the fixed 10*10 grid" ); } } @@ -205,8 +205,8 @@ fn match3d_material_sheet_slicing_uses_fixed_five_by_five_rows() { #[test] fn match3d_material_sheet_slicing_keeps_near_edge_foreground_pixels() { - let width = 500; - let height = 500; + let width = 1000; + let height = 1000; let item_names = vec!["樱桃".to_string(), "苹果".to_string(), "香蕉".to_string()]; let mut sheet = image::RgbaImage::from_pixel(width, height, image::Rgba([255, 255, 255, 255])); for y in 1..5 { @@ -689,35 +689,35 @@ fn match3d_legacy_item_asset_without_size_defaults_to_large() { } #[test] -fn match3d_draft_item_plan_rounds_up_to_full_five_item_sheets() { +fn match3d_draft_item_plan_rounds_up_to_full_spritesheet_batch() { let plan = parse_match3d_draft_plan( r#"{"gameName":"果园大鹅宴","tags":["水果","抓大鹅"],"backgroundMusic":{"title":"果园轻舞","style":"轻快, 休闲","prompt":""},"backgroundPrompt":"果园主题竖屏纯背景","items":[{"name":"草莓","soundPrompt":"草莓点击音效"},{"name":"苹果","soundPrompt":"苹果点击音效"},{"name":"香蕉","soundPrompt":"香蕉点击音效"},{"name":"葡萄","soundPrompt":"葡萄点击音效"},{"name":"西瓜","soundPrompt":"西瓜点击音效"},{"name":"梨子","soundPrompt":"梨子点击音效"},{"name":"桃子","soundPrompt":"桃子点击音效"},{"name":"橙子","soundPrompt":"橙子点击音效"},{"name":"蓝莓","soundPrompt":"蓝莓点击音效"}]}"#, &config("水果", 12, 4), ) .expect("draft plan should parse"); - assert_eq!(plan.items.len(), 10); + assert_eq!(plan.items.len(), MATCH3D_MATERIAL_ITEM_BATCH_SIZE); assert_eq!(plan.items[8].name, "蓝莓"); assert_ne!(plan.items[9].name, "蓝莓"); } #[test] -fn match3d_generated_item_count_rounds_up_to_five_multiples() { +fn match3d_generated_item_count_uses_full_spritesheet_batch() { assert_eq!( resolve_match3d_generated_item_count(&config("水果", 8, 2)), - 5 + MATCH3D_MAX_GENERATED_ITEM_COUNT ); assert_eq!( resolve_match3d_generated_item_count(&config("水果", 12, 4)), - 10 + MATCH3D_MAX_GENERATED_ITEM_COUNT ); assert_eq!( resolve_match3d_generated_item_count(&config("水果", 16, 6)), - 15 + MATCH3D_MAX_GENERATED_ITEM_COUNT ); assert_eq!( resolve_match3d_generated_item_count(&config("水果", 21, 8)), - 25 + MATCH3D_MAX_GENERATED_ITEM_COUNT ); } @@ -733,12 +733,12 @@ fn match3d_generated_assets_require_only_images_when_click_sound_is_closed() { } #[test] -fn match3d_item_asset_points_cost_counts_five_item_batches() { +fn match3d_item_asset_points_cost_counts_spritesheet_batches() { assert_eq!(calculate_match3d_item_assets_points_cost(0), 0); assert_eq!(calculate_match3d_item_assets_points_cost(1), 2); - assert_eq!(calculate_match3d_item_assets_points_cost(5), 2); - assert_eq!(calculate_match3d_item_assets_points_cost(6), 4); - assert_eq!(calculate_match3d_item_assets_points_cost(10), 4); + assert_eq!(calculate_match3d_item_assets_points_cost(20), 2); + assert_eq!(calculate_match3d_item_assets_points_cost(21), 4); + assert_eq!(calculate_match3d_item_assets_points_cost(40), 4); } #[test] @@ -777,7 +777,10 @@ fn match3d_item_asset_append_plan_pads_generation_without_persisting_padding() { ); assert_eq!(plan.requested_item_names, vec!["苹果", "香蕉", "梨子"]); - assert_eq!(plan.padded_item_names.len(), 5); + assert_eq!( + plan.padded_item_names.len(), + MATCH3D_MATERIAL_ITEM_BATCH_SIZE + ); assert_eq!(&plan.padded_item_names[..3], ["苹果", "香蕉", "梨子"]); assert_eq!( calculate_match3d_item_assets_points_cost(plan.requested_item_names.len()), @@ -900,28 +903,27 @@ fn match3d_regenerated_asset_keeps_stable_identity_and_side_assets() { } #[test] -fn match3d_material_sheet_prompt_requires_uniform_five_by_five_layout() { +fn match3d_material_sheet_prompt_requires_uniform_ten_by_ten_layout() { let prompt = build_match3d_material_sheet_prompt( &config("水果", 12, 4), &["草莓".to_string(), "苹果".to_string(), "香蕉".to_string()], ); - assert!(prompt.contains("5行*5列")); - assert!(prompt.contains("严格5*5均匀排布")); + assert!(prompt.contains("10行*10列")); + assert!(prompt.contains("素材间距严格均匀分布")); + assert!(prompt.contains("每一行包含两种物品")); + assert!(prompt.contains("每种物品的五个不同形态")); assert!(prompt.contains("绿幕背景")); assert!(prompt.contains("#00FF00")); - assert!(prompt.contains("单个素材格宽度的1/4空白间距")); - assert!(prompt.contains("约25%单格宽度")); - assert!(prompt.contains("禁止主体跨格")); - assert!(prompt.contains("贴边或越界")); + assert!(prompt.contains("严禁出现两种高相似度的物品")); } #[test] -fn match3d_material_sheet_prompt_hardens_pixel_retro_style() { +fn match3d_pixel_retro_style_prompt_hardens_asset_style_and_negative_prompt() { let mut config = config("水果", 12, 4); config.asset_style_id = Some("pixel-retro".to_string()); config.asset_style_label = Some("像素复古".to_string()); - let prompt = build_match3d_material_sheet_prompt(&config, &["草莓".to_string()]); + let prompt = resolve_match3d_asset_style_prompt(&config).expect("style prompt should exist"); let negative_prompt = build_match3d_material_sheet_negative_prompt(&config); assert!(prompt.contains("64x64")); @@ -1004,13 +1006,9 @@ fn match3d_extracts_vector_engine_gemini_inline_image_data() { fn match3d_vector_engine_gemini_url_accepts_root_or_v1_base() { let root_settings = Match3DVectorEngineGeminiImageSettings { base_url: "https://api.vectorengine.cn".to_string(), - api_key: "test-key".to_string(), - request_timeout_ms: 1_000_000, }; let v1_settings = Match3DVectorEngineGeminiImageSettings { base_url: "https://api.vectorengine.cn/v1".to_string(), - api_key: "test-key".to_string(), - request_timeout_ms: 1_000_000, }; assert_eq!( diff --git a/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs b/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs index c3a078a6..e79792e5 100644 --- a/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs +++ b/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs @@ -1,165 +1,5 @@ use super::*; -pub(super) async fn generate_match3d_material_sheet( - state: &AppState, - config: &Match3DConfigJson, - item_names: &[String], -) -> Result { - let settings = require_match3d_vector_engine_gemini_image_settings(state)?; - let http_client = build_match3d_vector_engine_gemini_image_http_client(&settings)?; - let prompt = build_match3d_material_sheet_prompt(config, item_names); - let negative_prompt = build_match3d_material_sheet_negative_prompt(config); - let generated = create_match3d_vector_engine_gemini_image_generation( - &http_client, - &settings, - prompt.as_str(), - negative_prompt.as_str(), - "抓大鹅素材图生成失败", - ) - .await?; - let image = generated.images.into_iter().next().ok_or_else(|| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "vector-engine-gemini", - "message": "抓大鹅素材图生成失败:未返回图片", - })) - })?; - - Ok(Match3DMaterialSheet { - task_id: generated.task_id, - prompt, - image_src: None, - image_object_key: None, - image, - }) -} - -fn require_match3d_vector_engine_gemini_image_settings( - state: &AppState, -) -> Result { - let base_url = state - .config - .vector_engine_base_url - .trim() - .trim_end_matches('/'); - if base_url.is_empty() { - return Err( - AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ - "provider": "vector-engine-gemini", - "reason": "VECTOR_ENGINE_BASE_URL 未配置", - })), - ); - } - - let api_key = state - .config - .vector_engine_api_key - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .ok_or_else(|| { - AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ - "provider": "vector-engine-gemini", - "reason": "VECTOR_ENGINE_API_KEY 未配置", - })) - })?; - - Ok(Match3DVectorEngineGeminiImageSettings { - base_url: base_url.to_string(), - api_key: api_key.to_string(), - request_timeout_ms: state.config.vector_engine_image_request_timeout_ms.max(1), - }) -} - -fn build_match3d_vector_engine_gemini_image_http_client( - settings: &Match3DVectorEngineGeminiImageSettings, -) -> Result { - reqwest::Client::builder() - .timeout(Duration::from_millis(settings.request_timeout_ms)) - .build() - .map_err(|error| { - AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ - "provider": "vector-engine-gemini", - "message": format!("构造抓大鹅 VectorEngine Gemini 图片生成 HTTP 客户端失败:{error}"), - })) - }) -} - -async fn create_match3d_vector_engine_gemini_image_generation( - http_client: &reqwest::Client, - settings: &Match3DVectorEngineGeminiImageSettings, - prompt: &str, - negative_prompt: &str, - failure_context: &str, -) -> Result { - let request_body = build_match3d_vector_engine_gemini_image_request_body( - prompt, - negative_prompt, - MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_ASPECT_RATIO, - ); - let response = http_client - .post(build_match3d_vector_engine_gemini_generate_content_url( - settings, - )) - .query(&[("key", settings.api_key.as_str())]) - .header(header::ACCEPT, "application/json") - .header(header::CONTENT_TYPE, "application/json") - .json(&request_body) - .send() - .await - .map_err(|error| { - map_match3d_vector_engine_gemini_image_request_error(format!( - "{failure_context}:调用 VectorEngine Gemini 图片生成失败:{error}" - )) - })?; - let status = response.status(); - let response_text = response.text().await.map_err(|error| { - map_match3d_vector_engine_gemini_image_request_error(format!( - "{failure_context}:读取 VectorEngine Gemini 图片生成响应失败:{error}" - )) - })?; - if !status.is_success() { - return Err(map_match3d_vector_engine_gemini_image_upstream_error( - status, - response_text.as_str(), - failure_context, - )); - } - - let payload = parse_match3d_json_payload( - response_text.as_str(), - "解析抓大鹅 VectorEngine Gemini 图片生成响应失败", - "vector-engine-gemini", - )?; - let image_urls = extract_match3d_image_urls(&payload); - if !image_urls.is_empty() { - return download_match3d_images_from_urls( - http_client, - format!("vector-engine-gemini-{}", current_utc_micros()), - image_urls, - 1, - "vector-engine-gemini", - ) - .await; - } - - let b64_images = extract_match3d_b64_images(&payload); - if !b64_images.is_empty() { - return Ok(match3d_images_from_base64( - format!("vector-engine-gemini-{}", current_utc_micros()), - b64_images, - 1, - )); - } - - Err( - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "vector-engine-gemini", - "message": "抓大鹅 VectorEngine Gemini 图片生成未返回图片", - "rawExcerpt": trim_match3d_upstream_excerpt(response_text.as_str(), 800), - })), - ) -} - pub(super) fn build_match3d_vector_engine_gemini_image_request_body( prompt: &str, negative_prompt: &str, @@ -201,125 +41,6 @@ fn build_match3d_vector_engine_gemini_prompt(prompt: &str, negative_prompt: &str format!("{prompt}\n避免:{negative_prompt}") } -async fn download_match3d_images_from_urls( - http_client: &reqwest::Client, - task_id: String, - image_urls: Vec, - candidate_count: u32, - provider: &str, -) -> Result { - let mut images = Vec::with_capacity(candidate_count.clamp(1, 4) as usize); - for image_url in image_urls - .into_iter() - .take(candidate_count.clamp(1, 4) as usize) - { - images - .push(download_match3d_remote_image(http_client, image_url.as_str(), provider).await?); - } - Ok(OpenAiGeneratedImages { - task_id, - actual_prompt: None, - images, - }) -} - -async fn download_match3d_remote_image( - http_client: &reqwest::Client, - image_url: &str, - provider: &str, -) -> Result { - let response = http_client.get(image_url).send().await.map_err(|error| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": provider, - "message": format!("下载抓大鹅生成图片失败:{error}"), - })) - })?; - let status = response.status(); - let content_type = response - .headers() - .get(header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .unwrap_or("image/png") - .to_string(); - let body = response.bytes().await.map_err(|error| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": provider, - "message": format!("读取抓大鹅生成图片内容失败:{error}"), - })) - })?; - if !status.is_success() { - return Err( - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": provider, - "message": "下载抓大鹅生成图片失败", - "status": status.as_u16(), - })), - ); - } - - let mime_type = normalize_match3d_downloaded_image_mime_type(content_type.as_str()); - Ok(DownloadedOpenAiImage { - extension: match3d_mime_to_extension(mime_type.as_str()).to_string(), - mime_type, - bytes: body.to_vec(), - }) -} - -fn match3d_images_from_base64( - task_id: String, - b64_images: Vec, - candidate_count: u32, -) -> OpenAiGeneratedImages { - let images = b64_images - .into_iter() - .take(candidate_count.clamp(1, 4) as usize) - .filter_map(|raw| decode_match3d_base64_image(raw.as_str())) - .collect(); - OpenAiGeneratedImages { - task_id, - actual_prompt: None, - images, - } -} - -fn decode_match3d_base64_image(raw: &str) -> Option { - let bytes = BASE64_STANDARD.decode(raw.trim()).ok()?; - let mime_type = infer_match3d_image_mime_type(bytes.as_slice()).to_string(); - Some(DownloadedOpenAiImage { - extension: match3d_mime_to_extension(mime_type.as_str()).to_string(), - mime_type, - bytes, - }) -} - -fn parse_match3d_json_payload( - raw_text: &str, - failure_context: &str, - provider: &str, -) -> Result { - serde_json::from_str::(raw_text).map_err(|error| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": provider, - "message": format!("{failure_context}:{error}"), - "rawExcerpt": trim_match3d_upstream_excerpt(raw_text, 800), - })) - }) -} - -fn extract_match3d_image_urls(payload: &Value) -> Vec { - let mut urls = Vec::new(); - collect_match3d_strings_by_key(payload, "url", &mut urls); - collect_match3d_strings_by_key(payload, "image", &mut urls); - collect_match3d_strings_by_key(payload, "image_url", &mut urls); - let mut deduped = Vec::new(); - for url in urls { - if (url.starts_with("http://") || url.starts_with("https://")) && !deduped.contains(&url) { - deduped.push(url); - } - } - deduped -} - pub(super) fn extract_match3d_b64_images(payload: &Value) -> Vec { let mut values = Vec::new(); collect_match3d_strings_by_key(payload, "b64_json", &mut values); @@ -365,12 +86,6 @@ fn collect_match3d_inline_image_data(payload: &Value, results: &mut Vec) } } -fn find_first_match3d_string_by_key(payload: &Value, target_key: &str) -> Option { - let mut results = Vec::new(); - collect_match3d_strings_by_key(payload, target_key, &mut results); - results.into_iter().next() -} - fn collect_match3d_strings_by_key(payload: &Value, target_key: &str, results: &mut Vec) { match payload { Value::Array(entries) => { @@ -408,79 +123,3 @@ fn collect_match3d_strings_by_key(payload: &Value, target_key: &str, results: &m _ => {} } } - -fn map_match3d_vector_engine_gemini_image_request_error(message: String) -> AppError { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "vector-engine-gemini", - "message": message, - })) -} - -fn map_match3d_vector_engine_gemini_image_upstream_error( - upstream_status: reqwest::StatusCode, - raw_text: &str, - fallback_message: &str, -) -> AppError { - let message = parse_match3d_api_error_message(raw_text, fallback_message); - let raw_excerpt = trim_match3d_upstream_excerpt(raw_text, 800); - tracing::warn!( - provider = "vector-engine-gemini", - upstream_status = upstream_status.as_u16(), - message = %message, - raw_excerpt = %raw_excerpt, - "抓大鹅 VectorEngine Gemini 图片生成上游请求失败" - ); - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "vector-engine-gemini", - "upstreamStatus": upstream_status.as_u16(), - "message": message, - "rawExcerpt": raw_excerpt, - })) -} - -fn parse_match3d_api_error_message(raw_text: &str, fallback_message: &str) -> String { - let trimmed = raw_text.trim(); - if trimmed.is_empty() { - return fallback_message.to_string(); - } - if let Ok(payload) = serde_json::from_str::(trimmed) { - for key in ["message", "code"] { - if let Some(value) = find_first_match3d_string_by_key(&payload, key) { - return if key == "message" { - value - } else { - format!("{fallback_message}({value})") - }; - } - } - } - trimmed.to_string() -} - -fn trim_match3d_upstream_excerpt(raw_text: &str, max_chars: usize) -> String { - raw_text.chars().take(max_chars).collect() -} - -fn normalize_match3d_downloaded_image_mime_type(content_type: &str) -> String { - let mime_type = content_type - .split(';') - .next() - .map(str::trim) - .unwrap_or("image/png"); - match mime_type { - "image/png" | "image/webp" | "image/jpeg" | "image/jpg" | "image/gif" => { - mime_type.to_string() - } - _ => "image/png".to_string(), - } -} - -pub(super) fn match3d_mime_to_extension(mime_type: &str) -> &str { - match mime_type { - "image/png" => "png", - "image/webp" => "webp", - "image/gif" => "gif", - "image/jpeg" | "image/jpg" => "jpg", - _ => "png", - } -} diff --git a/server-rs/crates/api-server/src/match3d/works.rs b/server-rs/crates/api-server/src/match3d/works.rs index bcdea311..05e5f35a 100644 --- a/server-rs/crates/api-server/src/match3d/works.rs +++ b/server-rs/crates/api-server/src/match3d/works.rs @@ -189,54 +189,6 @@ pub(super) fn resolve_author_display_name( .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| "玩家".to_string()) } -pub(super) async fn ensure_match3d_background_asset( - state: &AppState, - request_context: &RequestContext, - authenticated: &AuthenticatedAccessToken, - owner_user_id: &str, - session_id: &str, - profile_id: &str, - config: &Match3DConfigJson, - background_prompt: &str, - mut assets: Vec, -) -> Result, Response> { - let normalized_prompt = normalize_match3d_background_prompt(background_prompt); - let resolved_prompt = if normalized_prompt.is_empty() { - build_fallback_match3d_background_prompt(config) - } else { - normalized_prompt - }; - if let Some(existing_background) = find_match3d_generated_background_asset(&assets) { - if is_match3d_background_asset_ready(&existing_background) { - return Ok(assets); - } - } - - let generated_background = generate_match3d_level_asset_bundle( - state, - request_context, - owner_user_id, - session_id, - profile_id, - config, - &resolved_prompt, - ) - .await - .map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error))?; - attach_match3d_background_asset_to_assets(&mut assets, generated_background); - persist_match3d_generated_item_assets_snapshot( - state, - request_context, - authenticated, - session_id, - owner_user_id, - profile_id, - &assets, - ) - .await?; - Ok(assets) -} - pub(super) async fn resolve_or_generate_match3d_level_asset_bundle( state: &AppState, request_context: &RequestContext, @@ -769,6 +721,7 @@ pub(super) fn build_match3d_background_from_scene_prompt() -> String { "移除画面中的所有UI组件和容器中的内含物,完整保留容器和背景,补全被UI覆盖的背景内容".to_string() } +#[cfg(test)] pub(super) fn build_match3d_background_generation_prompt( config: &Match3DConfigJson, prompt: &str, diff --git a/server-rs/crates/api-server/src/openai_image_generation.rs b/server-rs/crates/api-server/src/openai_image_generation.rs index 406d4ef3..ea026cf6 100644 --- a/server-rs/crates/api-server/src/openai_image_generation.rs +++ b/server-rs/crates/api-server/src/openai_image_generation.rs @@ -2,9 +2,12 @@ use axum::http::StatusCode; use platform_image::{ DownloadedImage, GeneratedImages, PlatformImageError, PlatformImageStatusHint, ReferenceImage, VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_client, - build_vector_engine_image_request_body, create_vector_engine_image_edit, - create_vector_engine_image_edit_with_references, create_vector_engine_image_generation, - download_remote_image as download_platform_image_remote_image, vector_engine_images_edit_url, + create_vector_engine_image_edit, create_vector_engine_image_edit_with_references, + create_vector_engine_image_generation, +}; +#[cfg(test)] +use platform_image::{ + build_vector_engine_image_request_body, vector_engine_images_edit_url, vector_engine_images_generation_url, }; use serde_json::{Value, json}; @@ -233,15 +236,7 @@ pub(crate) async fn create_openai_image_edit_with_references( .await } -pub(crate) async fn download_remote_image( - http_client: &reqwest::Client, - image_url: &str, -) -> Result { - download_platform_image_remote_image(http_client, image_url) - .await - .map_err(map_platform_image_error) -} - +#[cfg(test)] pub(crate) fn build_openai_image_request_body( prompt: &str, negative_prompt: Option<&str>, @@ -430,10 +425,12 @@ pub(crate) fn map_platform_image_error(error: PlatformImageError) -> AppError { AppError::from_status(status).with_details(details) } +#[cfg(test)] fn vector_engine_images_generation_url_for_test(settings: &OpenAiImageSettings) -> String { vector_engine_images_generation_url(&settings.provider_settings()) } +#[cfg(test)] fn vector_engine_images_edit_url_for_test(settings: &OpenAiImageSettings) -> String { vector_engine_images_edit_url(&settings.provider_settings()) } diff --git a/server-rs/crates/api-server/src/prompt/rpg/runtime_chat.rs b/server-rs/crates/api-server/src/prompt/rpg/runtime_chat.rs index f02d87eb..3470122c 100644 --- a/server-rs/crates/api-server/src/prompt/rpg/runtime_chat.rs +++ b/server-rs/crates/api-server/src/prompt/rpg/runtime_chat.rs @@ -1,16 +1,5 @@ use serde_json::{Value, json}; -#[derive(Clone, Debug)] -pub(crate) struct RuntimeStoryTextPromptParams<'a> { - pub world_type: &'a str, - pub character: Value, - pub monsters: Value, - pub history: Value, - pub choice: Value, - pub context: Value, - pub available_options: Value, -} - #[derive(Clone, Debug)] pub(crate) struct RuntimeNpcDialoguePromptParams<'a> { pub world_type: &'a str, @@ -25,42 +14,6 @@ pub(crate) struct RuntimeNpcDialoguePromptParams<'a> { pub available_options: Vec, } -#[derive(Clone, Debug)] -pub(crate) struct RuntimeReasonedStoryPromptParams<'a> { - pub world_type: &'a str, - pub character: &'a Value, - pub monsters: Vec, - pub history: Vec, - pub context: Value, - pub choice: &'a str, - pub result_summary: &'a str, - pub requested_option: Value, - pub available_options: Vec, -} - -pub(crate) fn runtime_story_director_system_prompt(initial: bool) -> &'static str { - if initial { - "你是游戏运行时剧情导演。请用中文输出一段可直接展示给玩家的开局剧情,不要输出 JSON。" - } else { - "你是游戏运行时剧情导演。请用中文根据玩家选择续写一段剧情,不要输出 JSON。" - } -} - -pub(crate) fn build_runtime_story_director_user_prompt( - params: RuntimeStoryTextPromptParams<'_>, -) -> String { - json!({ - "worldType": params.world_type, - "character": params.character, - "monsters": params.monsters, - "history": params.history, - "choice": params.choice, - "context": params.context, - "availableOptions": params.available_options, - }) - .to_string() -} - pub(crate) fn runtime_npc_dialogue_system_prompt() -> &'static str { "你是游戏运行时 NPC 对话导演。只输出中文正文,不要输出 JSON、Markdown 或规则说明;不要新增系统尚未结算的奖励、任务结果或战斗结果。" } @@ -200,31 +153,6 @@ pub(crate) fn build_npc_recruit_dialogue_user_prompt( ) } -pub(crate) fn runtime_reasoned_story_system_prompt() -> &'static str { - "你是游戏运行时剧情导演。只输出中文剧情正文,不要输出 JSON、Markdown 或规则说明;必须尊重已结算的战斗 outcome、伤害和状态,不要发明额外奖励。" -} - -pub(crate) fn build_runtime_reasoned_story_user_prompt( - params: RuntimeReasonedStoryPromptParams<'_>, -) -> String { - let state_prompt = json!({ - "worldType": params.world_type, - "character": params.character, - "monsters": params.monsters, - "history": params.history, - "context": params.context, - "choice": params.choice, - "resultSummary": params.result_summary, - "requestedOption": params.requested_option, - "availableOptions": params.available_options, - }) - .to_string(); - - format!( - "请基于以下运行时状态,为这一轮战斗结算生成一段 120 字以内的结果叙事,并自然引出下一组选项。\n{state_prompt}" - ) -} - pub(crate) const NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT: &str = r#"你是角色扮演 RPG 里的当前 NPC。 你只输出这名 NPC 此刻会对玩家说的一轮回复。 只输出纯中文口语回复正文,不要输出角色名、引号、旁白、动作描写、Markdown、JSON 或解释。 diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index be4b8cb0..dc1be22a 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -23,7 +23,7 @@ use module_puzzle::{PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus}; use platform_llm::{LlmMessage, LlmMessageContentPart, LlmTextRequest}; use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest}; use platform_oss::{OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest}; -use serde_json::{Map, Value, json}; +use serde_json::{Value, json}; use shared_contracts::{ creation_audio::CreationAudioAsset, puzzle_agent::{ @@ -58,7 +58,7 @@ use spacetime_client::{ PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftCompileFailureRecordInput, PuzzleDraftLevelRecord, PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, - PuzzleGalleryCardRecord, PuzzleGeneratedImageCandidateRecord, + PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, diff --git a/server-rs/crates/api-server/src/puzzle/draft.rs b/server-rs/crates/api-server/src/puzzle/draft.rs index 43bc146d..b7a66dae 100644 --- a/server-rs/crates/api-server/src/puzzle/draft.rs +++ b/server-rs/crates/api-server/src/puzzle/draft.rs @@ -977,6 +977,7 @@ pub(crate) fn attach_selected_puzzle_candidate_to_levels( } } +#[cfg(test)] pub(crate) fn resolve_puzzle_initial_ui_background_prompt( draft: &PuzzleResultDraftRecord, target_level: &PuzzleDraftLevelRecord, @@ -1042,6 +1043,7 @@ pub(crate) fn build_puzzle_ui_background_generation_prompt( ) } +#[cfg(test)] pub(crate) fn attach_puzzle_level_ui_background( levels: &mut [PuzzleDraftLevelRecord], level_id: &str, @@ -1083,27 +1085,6 @@ pub(crate) fn attach_puzzle_level_asset_bundle( level.ui_background_image_object_key = Some(generated.level_background.object_key); } -pub(crate) async fn generate_puzzle_initial_ui_background_required( - state: &PuzzleApiState, - request_context: &RequestContext, - owner_user_id: &str, - session_id: &str, - draft: &PuzzleResultDraftRecord, - target_level: &PuzzleDraftLevelRecord, -) -> Result<(String, GeneratedPuzzleUiBackgroundResponse), AppError> { - let prompt = resolve_puzzle_initial_ui_background_prompt(draft, target_level); - let generated = generate_puzzle_ui_background_image( - state, - request_context, - owner_user_id, - session_id, - target_level.level_name.as_str(), - prompt.as_str(), - ) - .await?; - Ok((prompt, generated)) -} - pub(crate) async fn generate_puzzle_level_asset_bundle_required( state: &PuzzleApiState, request_context: &RequestContext, diff --git a/server-rs/crates/api-server/src/puzzle/mappers.rs b/server-rs/crates/api-server/src/puzzle/mappers.rs index 89ae4291..acb6f50d 100644 --- a/server-rs/crates/api-server/src/puzzle/mappers.rs +++ b/server-rs/crates/api-server/src/puzzle/mappers.rs @@ -396,49 +396,6 @@ pub(super) fn map_puzzle_work_summary_response( } } -pub(super) fn map_puzzle_gallery_card_response( - state: &PuzzleApiState, - item: PuzzleGalleryCardRecord, -) -> PuzzleWorkSummaryResponse { - let author = resolve_puzzle_work_author_by_user_id( - state, - &item.owner_user_id, - Some(&item.author_display_name), - None, - ); - PuzzleWorkSummaryResponse { - work_id: item.work_id, - profile_id: item.profile_id, - owner_user_id: item.owner_user_id, - source_session_id: item.source_session_id, - author_display_name: author.display_name, - work_title: item.work_title, - work_description: item.work_description, - level_name: item.level_name, - summary: item.summary, - theme_tags: item.theme_tags, - cover_image_src: item.cover_image_src, - cover_asset_id: item.cover_asset_id, - publication_status: item.publication_status, - updated_at: item.updated_at, - published_at: item.published_at, - play_count: item.play_count, - remix_count: item.remix_count, - like_count: item.like_count, - recent_play_count_7d: item.recent_play_count_7d, - point_incentive_total_half_points: item.point_incentive_total_half_points, - point_incentive_claimed_points: item.point_incentive_claimed_points, - point_incentive_total_points: item.point_incentive_total_half_points as f64 / 2.0, - point_incentive_claimable_points: item - .point_incentive_total_half_points - .saturating_div(2) - .saturating_sub(item.point_incentive_claimed_points), - publish_ready: item.publish_ready, - generation_status: item.generation_status, - levels: Vec::new(), - } -} - pub(super) fn map_public_work_puzzle_gallery_card_response( state: &PuzzleApiState, item: spacetime_client::PublicWorkGalleryEntryRecord, diff --git a/server-rs/crates/api-server/src/puzzle/tests.rs b/server-rs/crates/api-server/src/puzzle/tests.rs index b5b902b9..bec54d44 100644 --- a/server-rs/crates/api-server/src/puzzle/tests.rs +++ b/server-rs/crates/api-server/src/puzzle/tests.rs @@ -44,7 +44,6 @@ fn puzzle_vector_engine_create_request_never_embeds_reference_image() { mime_type: "image/png".to_string(), bytes_len: cursor.get_ref().len(), bytes: cursor.into_inner(), - signed_read_url: None, }; let body = build_puzzle_vector_engine_image_request_body( @@ -197,15 +196,11 @@ fn puzzle_ui_spritesheet_postprocess_turns_green_screen_transparent() { } #[test] -fn puzzle_vector_engine_create_request_never_embeds_signed_reference_url() { +fn puzzle_vector_engine_create_request_never_embeds_reference_payload() { let reference_image = PuzzleResolvedReferenceImage { mime_type: "image/png".to_string(), bytes_len: 4, bytes: b"test".to_vec(), - signed_read_url: Some( - "https://oss.example/generated-puzzle-assets/reference.png?x-oss-signature=abc" - .to_string(), - ), }; let body = build_puzzle_vector_engine_image_request_body( @@ -583,7 +578,6 @@ fn puzzle_uploaded_cover_can_reuse_resolved_history_image() { mime_type: "image/png".to_string(), bytes_len: 8, bytes: b"pngbytes".to_vec(), - signed_read_url: None, }; let downloaded = PuzzleDownloadedImage::from_resolved_reference_image(resolved); diff --git a/server-rs/crates/api-server/src/puzzle/vector_engine.rs b/server-rs/crates/api-server/src/puzzle/vector_engine.rs index 4338966b..285e3105 100644 --- a/server-rs/crates/api-server/src/puzzle/vector_engine.rs +++ b/server-rs/crates/api-server/src/puzzle/vector_engine.rs @@ -45,7 +45,6 @@ pub(crate) struct PuzzleResolvedReferenceImage { pub(crate) mime_type: String, pub(crate) bytes_len: usize, pub(crate) bytes: Vec, - pub(crate) signed_read_url: Option, } pub(crate) struct GeneratedPuzzleImageCandidate { @@ -318,10 +317,10 @@ pub(crate) fn build_puzzle_downloaded_image_reference( mime_type: image.mime_type.clone(), bytes_len: image.bytes.len(), bytes: image.bytes.clone(), - signed_read_url: None, } } +#[cfg(test)] pub(crate) fn build_puzzle_vector_engine_image_request_body( image_model: PuzzleImageModel, prompt: &str, @@ -330,7 +329,7 @@ pub(crate) fn build_puzzle_vector_engine_image_request_body( candidate_count: u32, reference_image: Option<&PuzzleResolvedReferenceImage>, ) -> Value { - let body = Map::from_iter([ + let body = serde_json::Map::from_iter([ ( "model".to_string(), Value::String(image_model.request_model_name().to_string()), @@ -415,32 +414,6 @@ pub(crate) fn collect_puzzle_reference_image_sources( sources } -pub(crate) fn collect_legacy_puzzle_reference_image_sources( - legacy_reference_image_src: Option<&str>, - reference_image_srcs: &[String], -) -> Vec { - let mut sources = Vec::new(); - for source in legacy_reference_image_src - .into_iter() - .chain(reference_image_srcs.iter().map(String::as_str)) - { - let normalized = source.trim(); - if normalized.is_empty() { - continue; - } - if !sources - .iter() - .any(|existing: &String| existing == normalized) - { - sources.push(normalized.to_string()); - } - if sources.len() >= PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT { - break; - } - } - sources -} - pub(crate) fn has_puzzle_reference_images( legacy_reference_image_src: Option<&str>, reference_image_srcs: &[String], @@ -463,6 +436,7 @@ pub(crate) fn should_use_puzzle_reference_image_generation( use_reference_image_generation && has_puzzle_reference_image(reference_image_src) } +#[cfg(test)] pub(crate) fn build_puzzle_vector_engine_prompt(prompt: &str, negative_prompt: &str) -> String { let prompt = prompt.trim(); let negative_prompt = negative_prompt.trim(); @@ -525,7 +499,6 @@ pub(crate) async fn resolve_puzzle_reference_image( mime_type: parsed.mime_type, bytes_len, bytes: parsed.bytes, - signed_read_url: None, }); } @@ -758,7 +731,6 @@ async fn download_signed_puzzle_reference_image( mime_type, bytes_len, bytes: body.to_vec(), - signed_read_url: Some(signed_read_url), }) } @@ -1075,47 +1047,6 @@ pub(crate) fn decode_puzzle_base64(value: &str) -> Option> { Some(output) } -pub(crate) fn find_first_puzzle_string_by_key(payload: &Value, target_key: &str) -> Option { - let mut results = Vec::new(); - collect_puzzle_strings_by_key(payload, target_key, &mut results); - results.into_iter().next() -} - -pub(crate) fn collect_puzzle_strings_by_key( - payload: &Value, - target_key: &str, - results: &mut Vec, -) { - match payload { - Value::Array(entries) => { - for entry in entries { - collect_puzzle_strings_by_key(entry, target_key, results); - } - } - Value::Object(object) => { - for (key, value) in object { - if key == target_key { - collect_puzzle_string_values(value, results); - } - collect_puzzle_strings_by_key(value, target_key, results); - } - } - _ => {} - } -} - -pub(crate) fn collect_puzzle_string_values(payload: &Value, results: &mut Vec) { - match payload { - Value::String(text) => results.push(text.to_string()), - Value::Array(items) => { - for item in items { - collect_puzzle_string_values(item, results); - } - } - _ => {} - } -} - pub(crate) fn normalize_puzzle_downloaded_image_mime_type(content_type: &str) -> String { let mime_type = content_type .split(';') diff --git a/server-rs/crates/api-server/src/puzzle_gallery_cache.rs b/server-rs/crates/api-server/src/puzzle_gallery_cache.rs index adb24caf..4c66badb 100644 --- a/server-rs/crates/api-server/src/puzzle_gallery_cache.rs +++ b/server-rs/crates/api-server/src/puzzle_gallery_cache.rs @@ -10,9 +10,11 @@ use shared_contracts::{ puzzle_works::PuzzleWorkSummaryResponse, }; use tokio::{ - sync::{Mutex, MutexGuard, OwnedMutexGuard, RwLock}, + sync::{Mutex, MutexGuard, RwLock}, time, }; +#[cfg(test)] +use tokio::sync::OwnedMutexGuard; use crate::{api_response::json_success_data_bytes_response, request_context::RequestContext}; @@ -69,6 +71,7 @@ impl PuzzleGalleryCache { }) } + #[cfg(test)] pub async fn read_stale_response(&self) -> Option { let guard = self.inner.read().await; let entry = guard.as_ref()?; @@ -77,6 +80,7 @@ impl PuzzleGalleryCache { }) } + #[cfg(test)] pub fn try_acquire_owned_rebuild_guard(&self) -> Option> { self.rebuild_lock.clone().try_lock_owned().ok() } diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 51cab967..e19693a6 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -12,9 +12,10 @@ use axum::extract::FromRef; use module_ai::{AiTaskService, InMemoryAiTaskStore}; use module_auth::{ AuthUserService, InMemoryAuthStore, PasswordEntryService, PhoneAuthService, - RefreshAuthStoreSnapshotResult, RefreshSessionService, WechatAuthService, - WechatAuthStateService, + RefreshSessionService, WechatAuthService, WechatAuthStateService, }; +#[cfg(not(test))] +use module_auth::RefreshAuthStoreSnapshotResult; use module_runtime::RuntimeSnapshotRecord; #[cfg(test)] use module_runtime::{SAVE_SNAPSHOT_VERSION, format_utc_micros}; @@ -316,6 +317,7 @@ pub enum AppStateInitError { } impl AppState { + #[cfg(test)] pub fn new(config: AppConfig) -> Result { Self::new_with_empty_auth_store(config) } @@ -661,6 +663,7 @@ impl AppState { Ok(()) } + #[cfg(not(test))] pub fn refresh_auth_store_from_snapshot_json( &self, snapshot_json: &str, diff --git a/server-rs/crates/api-server/src/telemetry.rs b/server-rs/crates/api-server/src/telemetry.rs index d4a34db4..b9d04b94 100644 --- a/server-rs/crates/api-server/src/telemetry.rs +++ b/server-rs/crates/api-server/src/telemetry.rs @@ -97,22 +97,10 @@ pub(crate) fn record_puzzle_gallery_cache_hit() { puzzle_gallery_cache_metrics().hits.add(1, &[]); } -pub(crate) fn record_puzzle_gallery_cache_stale_hit() { - puzzle_gallery_cache_metrics().stale_hits.add(1, &[]); -} - pub(crate) fn record_puzzle_gallery_cache_miss() { puzzle_gallery_cache_metrics().misses.add(1, &[]); } -pub(crate) fn record_puzzle_gallery_cache_refresh_started() { - puzzle_gallery_cache_metrics().refreshes_started.add(1, &[]); -} - -pub(crate) fn record_puzzle_gallery_cache_refresh_failed() { - puzzle_gallery_cache_metrics().refreshes_failed.add(1, &[]); -} - pub(crate) fn record_puzzle_gallery_cache_rebuild( duration: std::time::Duration, data_bytes: usize, @@ -208,10 +196,7 @@ struct HttpMetrics { struct PuzzleGalleryCacheMetrics { hits: Counter, - stale_hits: Counter, misses: Counter, - refreshes_started: Counter, - refreshes_failed: Counter, rebuilds: Counter, rebuild_duration: opentelemetry::metrics::Histogram, data_json_bytes: opentelemetry::metrics::Histogram, @@ -301,22 +286,10 @@ fn puzzle_gallery_cache_metrics() -> &'static PuzzleGalleryCacheMetrics { .u64_counter("genarrative.puzzle_gallery.cache.hits") .with_description("Puzzle gallery response cache hits") .build(), - stale_hits: meter - .u64_counter("genarrative.puzzle_gallery.cache.stale_hits") - .with_description("Puzzle gallery stale response cache hits") - .build(), misses: meter .u64_counter("genarrative.puzzle_gallery.cache.misses") .with_description("Puzzle gallery response cache misses") .build(), - refreshes_started: meter - .u64_counter("genarrative.puzzle_gallery.cache.refreshes_started") - .with_description("Puzzle gallery background refresh start count") - .build(), - refreshes_failed: meter - .u64_counter("genarrative.puzzle_gallery.cache.refreshes_failed") - .with_description("Puzzle gallery background refresh failure count") - .build(), rebuilds: meter .u64_counter("genarrative.puzzle_gallery.cache.rebuilds") .with_description("Puzzle gallery response cache rebuild count") diff --git a/server-rs/crates/api-server/src/tracking.rs b/server-rs/crates/api-server/src/tracking.rs index f878902a..d1f3e7e8 100644 --- a/server-rs/crates/api-server/src/tracking.rs +++ b/server-rs/crates/api-server/src/tracking.rs @@ -1,4 +1,5 @@ use axum::http::{Method, StatusCode}; +#[cfg(not(test))] use module_auth::AuthLoginMethod; use module_runtime::RuntimeTrackingScopeKind; use serde_json::{Value, json}; @@ -553,6 +554,7 @@ fn is_dynamic_path_segment(segment: &str) -> bool { || lower.starts_with("session") } +#[cfg(not(test))] pub async fn record_daily_login_tracking_event_after_success( state: &AppState, request_context: &RequestContext, diff --git a/server-rs/crates/api-server/src/vector_engine_audio_generation.rs b/server-rs/crates/api-server/src/vector_engine_audio_generation.rs index c0072aef..000f7498 100644 --- a/server-rs/crates/api-server/src/vector_engine_audio_generation.rs +++ b/server-rs/crates/api-server/src/vector_engine_audio_generation.rs @@ -18,7 +18,5 @@ pub use handlers::{ publish_visual_novel_background_music_asset, publish_visual_novel_sound_effect_asset, }; -#[allow(unused_imports)] -pub(crate) use generation::generate_background_music_asset_for_creation; pub(crate) use generation::generate_sound_effect_asset_for_creation; pub(crate) use types::GeneratedCreationAudioTarget; diff --git a/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs b/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs index 7aca5791..63fdd53e 100644 --- a/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs +++ b/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs @@ -9,7 +9,7 @@ use super::{ clock::{current_utc_iso_text, current_utc_micros}, errors::{map_platform_audio_error, vector_engine_bad_gateway}, publish::wait_for_generated_audio_asset, - tasks::{create_background_music_task_response, create_sound_effect_task_response}, + tasks::create_sound_effect_task_response, types::{AudioAssetBindingTarget, AudioAssetSlot, GeneratedCreationAudioTarget}, }; @@ -86,92 +86,6 @@ pub(crate) async fn generate_sound_effect_asset_for_creation( outcome } -pub(crate) async fn generate_background_music_asset_for_creation( - state: &AppState, - owner_user_id: &str, - prompt: String, - title: String, - tags: Option, - model: Option, - target: GeneratedCreationAudioTarget, -) -> Result { - let started_at_micros = current_utc_micros(); - let normalized_prompt = platform_audio::normalize_limited_text_allow_empty( - &prompt, - "prompt", - platform_audio::SUNO_PROMPT_MAX_CHARS, - ) - .map_err(map_platform_audio_error)?; - let normalized_title = platform_audio::normalize_limited_text( - &title, - "title", - platform_audio::SUNO_TITLE_MAX_CHARS, - ) - .map_err(map_platform_audio_error)?; - let request_payload = json!({ - "kind": "background_music", - "promptChars": normalized_prompt.chars().count(), - "titleChars": normalized_title.chars().count(), - "hasTags": tags.as_ref().is_some_and(|value| !value.trim().is_empty()), - "model": model, - "targetEntityKind": target.entity_kind, - "targetEntityId": target.entity_id, - "targetSlot": target.slot, - "targetAssetKind": target.asset_kind, - }); - let outcome = async { - let task = create_background_music_task_response( - state, - normalized_prompt.clone(), - normalized_title.clone(), - tags, - model, - ) - .await?; - let target = AudioAssetBindingTarget { - storage_scope: target.entity_kind.clone(), - entity_kind: target.entity_kind, - entity_id: target.entity_id, - slot: target.slot, - asset_kind: target.asset_kind, - profile_id: target.profile_id, - storage_prefix: target.storage_prefix, - }; - let generated = wait_for_generated_audio_asset( - state, - owner_user_id, - task.task_id.clone(), - AudioAssetSlot::BackgroundMusic, - target, - ) - .await?; - let audio_src = generated - .audio_src - .ok_or_else(|| vector_engine_bad_gateway("背景音乐生成完成但缺少播放地址"))?; - - Ok::<_, AppError>(creation_audio::CreationAudioAsset { - task_id: generated.task_id, - provider: generated.provider, - asset_object_id: generated.asset_object_id, - asset_kind: generated.asset_kind, - audio_src, - prompt: Some(normalized_prompt), - title: Some(normalized_title), - updated_at: Some(current_utc_iso_text()), - }) - } - .await; - record_creation_audio_generation_run( - state, - "background_music", - request_payload, - started_at_micros, - &outcome, - ) - .await; - outcome -} - async fn record_creation_audio_generation_run( state: &AppState, operation: &'static str, diff --git a/server-rs/crates/api-server/src/vector_engine_audio_generation/tasks.rs b/server-rs/crates/api-server/src/vector_engine_audio_generation/tasks.rs index 0d52dccd..fec797c0 100644 --- a/server-rs/crates/api-server/src/vector_engine_audio_generation/tasks.rs +++ b/server-rs/crates/api-server/src/vector_engine_audio_generation/tasks.rs @@ -1,42 +1,10 @@ -use platform_audio::{BackgroundMusicTaskRequest, SoundEffectTaskRequest}; +use platform_audio::SoundEffectTaskRequest; use shared_contracts::creation_audio; use crate::{http_error::AppError, state::AppState}; use super::{errors::map_platform_audio_error, settings::require_vector_engine_audio_settings}; -pub(super) async fn create_background_music_task_response( - state: &AppState, - prompt: String, - title: String, - tags: Option, - model: Option, -) -> Result { - let settings = require_vector_engine_audio_settings(state)?; - let http_client = platform_audio::build_vector_engine_audio_http_client(&settings) - .map_err(map_platform_audio_error)?; - let task = platform_audio::submit_background_music_task( - &http_client, - &settings, - BackgroundMusicTaskRequest { - prompt, - title, - tags, - model, - instrumental: true, - }, - ) - .await - .map_err(map_platform_audio_error)?; - - Ok(creation_audio::AudioGenerationTaskResponse { - kind: creation_audio::CreationAudioGenerationKind::BackgroundMusic, - task_id: task.task_id, - provider: task.provider, - status: task.status, - }) -} - pub(super) async fn create_sound_effect_task_response( state: &AppState, prompt: String, diff --git a/server-rs/crates/api-server/src/wechat_pay.rs b/server-rs/crates/api-server/src/wechat_pay.rs index eba24beb..2b6cffd3 100644 --- a/server-rs/crates/api-server/src/wechat_pay.rs +++ b/server-rs/crates/api-server/src/wechat_pay.rs @@ -287,7 +287,7 @@ pub(crate) struct WechatMiniProgramMessagePushQuery { #[derive(Debug, Deserialize)] struct WechatMiniProgramEncryptedMessage { #[serde(rename = "ToUserName", alias = "to_user_name", default)] - to_user_name: Option, + _to_user_name: Option, #[serde(rename = "Encrypt", alias = "encrypt")] encrypt: String, } diff --git a/server-rs/crates/api-server/src/wooden_fish.rs b/server-rs/crates/api-server/src/wooden_fish.rs index 1a30d2cb..a0e60220 100644 --- a/server-rs/crates/api-server/src/wooden_fish.rs +++ b/server-rs/crates/api-server/src/wooden_fish.rs @@ -229,33 +229,6 @@ pub async fn list_wooden_fish_works( )) } -pub async fn delete_wooden_fish_work( - State(state): State, - Path(profile_id): Path, - Extension(request_context): Extension, - Extension(authenticated): Extension, -) -> Result, Response> { - ensure_non_empty(&request_context, &profile_id, "profileId")?; - let works = state - .spacetime_client() - .delete_wooden_fish_work(profile_id, authenticated.claims().user_id().to_string()) - .await - .map_err(|error| { - wooden_fish_error_response( - &request_context, - WOODEN_FISH_CREATION_PROVIDER, - map_wooden_fish_client_error(error), - ) - })?; - - Ok(json_success_body( - Some(&request_context), - WoodenFishWorksResponse { - items: works.into_iter().map(|work| work.summary).collect(), - }, - )) -} - pub async fn get_wooden_fish_runtime_work( State(state): State, Path(profile_id): Path, diff --git a/server-rs/crates/api-server/src/work_author.rs b/server-rs/crates/api-server/src/work_author.rs index c758e3ca..572fa555 100644 --- a/server-rs/crates/api-server/src/work_author.rs +++ b/server-rs/crates/api-server/src/work_author.rs @@ -87,6 +87,7 @@ fn orphan_work_author_summary() -> WorkAuthorSummary { } /// 中文注释:运维回填只处理空作者或认证仓储不可再解析的历史 owner_user_id,避免把有效作品误转给占位账号。 +#[cfg(test)] pub fn should_rebind_orphan_work_owner( auth_user_service: &module_auth::AuthUserService, owner_user_id: &str, diff --git a/server-rs/crates/spacetime-client/src/jump_hop.rs b/server-rs/crates/spacetime-client/src/jump_hop.rs index df224a68..9f1aeef1 100644 --- a/server-rs/crates/spacetime-client/src/jump_hop.rs +++ b/server-rs/crates/spacetime-client/src/jump_hop.rs @@ -1177,6 +1177,7 @@ mod tests { tile_atlas_asset: None, tile_assets: None, cover_composite: None, + back_button_asset: None, } } @@ -1273,6 +1274,7 @@ mod tests { tile_assets: Vec::new(), path: None, cover_composite: None, + back_button_asset: None, generation_status: JumpHopGenerationStatus::Draft, } } diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index bb0c0a13..20361ba8 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -174,24 +174,6 @@ use module_npc::{ NpcStanceProfile as DomainNpcStanceProfile, NpcStateSnapshot as DomainNpcStateSnapshot, ResolveNpcInteractionInput as DomainResolveNpcInteractionInput, }; -use module_puzzle::{ - PuzzleAgentMessageSnapshot as DomainPuzzleAgentMessageSnapshot, - PuzzleAgentSessionSnapshot as DomainPuzzleAgentSessionSnapshot, - PuzzleAgentSuggestedAction as DomainPuzzleAgentSuggestedAction, - PuzzleAnchorItem as DomainPuzzleAnchorItem, PuzzleAnchorPack as DomainPuzzleAnchorPack, - PuzzleBoardSnapshot as DomainPuzzleBoardSnapshot, - PuzzleCellPosition as DomainPuzzleCellPosition, - PuzzleCreatorIntent as DomainPuzzleCreatorIntent, PuzzleDraftLevel as DomainPuzzleDraftLevel, - PuzzleGeneratedImageCandidate as DomainPuzzleGeneratedImageCandidate, - PuzzleMergedGroupState as DomainPuzzleMergedGroupState, - PuzzlePieceState as DomainPuzzlePieceState, PuzzleResultDraft as DomainPuzzleResultDraft, - PuzzleResultPreviewBlocker as DomainPuzzleResultPreviewBlocker, - PuzzleResultPreviewEnvelope as DomainPuzzleResultPreviewEnvelope, - PuzzleResultPreviewFinding as DomainPuzzleResultPreviewFinding, - PuzzleRunSnapshot as DomainPuzzleRunSnapshot, - PuzzleRuntimeLevelSnapshot as DomainPuzzleRuntimeLevelSnapshot, - PuzzleWorkProfile as DomainPuzzleWorkProfile, -}; use module_runtime::{ AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse, RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord, diff --git a/server-rs/crates/spacetime-client/src/mapper/custom_world.rs b/server-rs/crates/spacetime-client/src/mapper/custom_world.rs index 6b084df0..6ffc112f 100644 --- a/server-rs/crates/spacetime-client/src/mapper/custom_world.rs +++ b/server-rs/crates/spacetime-client/src/mapper/custom_world.rs @@ -58,20 +58,6 @@ pub(crate) fn map_custom_world_library_detail_result( }) } -pub(crate) fn map_custom_world_gallery_list_result( - result: CustomWorldGalleryListResult, -) -> Result, SpacetimeClientError> { - if !result.ok { - return Err(SpacetimeClientError::procedure_failed(result.error_message)); - } - - Ok(result - .entries - .into_iter() - .map(map_custom_world_gallery_entry_snapshot) - .collect::, _>>()?) -} - pub(crate) fn map_custom_world_library_mutation_result( result: CustomWorldLibraryMutationResult, ) -> Result { diff --git a/server-rs/crates/spacetime-module/src/public_work.rs b/server-rs/crates/spacetime-module/src/public_work.rs index 21dea02e..2ab5a708 100644 --- a/server-rs/crates/spacetime-module/src/public_work.rs +++ b/server-rs/crates/spacetime-module/src/public_work.rs @@ -553,18 +553,6 @@ fn map_match3d_gallery_entry(row: Match3DGalleryViewRow) -> PublicWorkGalleryEnt } } -fn map_match3d_detail_entry(row: Match3DGalleryViewRow) -> PublicWorkDetailEntry { - let detail_payload_json = json_string(json!({ - "sourceType": "match3d", - "themeText": row.theme_text, - "referenceImageSrc": row.reference_image_src, - "clearCount": row.clear_count, - "difficulty": row.difficulty, - "generatedItemAssetsReady": row.generated_item_assets_json.as_ref().is_some_and(|value| !value.trim().is_empty()), - })); - gallery_to_detail(map_match3d_gallery_entry(row), detail_payload_json) -} - fn map_square_hole_gallery_entry(row: SquareHoleGalleryViewRow) -> PublicWorkGalleryEntry { let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros); diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index d1bbb3c3..73595d0d 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -1912,174 +1912,6 @@ pub(crate) fn build_profile_save_archive_snapshot_from_row( } } -fn read_string_from_json(value: Option<&JsonValue>) -> Option { - value - .and_then(JsonValue::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToString::to_string) -} - -fn resolve_profile_world_snapshot_meta( - game_state: Option<&serde_json::Map>, -) -> Option { - let game_state = game_state?; - let custom_world_profile = game_state - .get("customWorldProfile") - .and_then(JsonValue::as_object); - - if let Some(custom_world_profile) = custom_world_profile { - let profile_id = read_string_from_json(custom_world_profile.get("id")); - let world_title = read_string_from_json(custom_world_profile.get("name")) - .or_else(|| read_string_from_json(custom_world_profile.get("title"))); - if profile_id.is_some() || world_title.is_some() { - let world_title = world_title.unwrap_or_else(|| "自定义世界".to_string()); - return Some(RuntimeProfileWorldSnapshotMeta { - world_key: profile_id - .as_ref() - .map(|profile_id| format!("custom:{profile_id}")) - .unwrap_or_else(|| format!("custom:{world_title}")), - owner_user_id: None, - profile_id, - world_type: Some("CUSTOM".to_string()), - world_title, - world_subtitle: read_string_from_json(custom_world_profile.get("summary")) - .or_else(|| read_string_from_json(custom_world_profile.get("settingText"))) - .unwrap_or_default(), - }); - } - } - - let world_type = read_string_from_json(game_state.get("worldType"))?; - let current_scene_preset = game_state - .get("currentScenePreset") - .and_then(JsonValue::as_object); - - Some(RuntimeProfileWorldSnapshotMeta { - world_key: format!("builtin:{world_type}"), - owner_user_id: None, - profile_id: None, - world_type: Some(world_type.clone()), - world_title: current_scene_preset - .and_then(|preset| read_string_from_json(preset.get("name"))) - .unwrap_or_else(|| build_builtin_world_title(&world_type)), - world_subtitle: current_scene_preset - .and_then(|preset| { - read_string_from_json(preset.get("summary")) - .or_else(|| read_string_from_json(preset.get("description"))) - }) - .unwrap_or_default(), - }) -} - -fn resolve_profile_save_archive_meta( - game_state: &JsonValue, - current_story_json: Option<&str>, -) -> Option { - if is_non_persistent_runtime_snapshot(game_state) { - return None; - } - - let game_state_object = game_state.as_object(); - let world_meta = resolve_profile_world_snapshot_meta(game_state_object)?; - let story_engine_memory = game_state_object - .and_then(|state| state.get("storyEngineMemory")) - .and_then(JsonValue::as_object); - let continue_game_digest = story_engine_memory - .and_then(|memory| read_string_from_json(memory.get("continueGameDigest"))); - let current_story_text = parse_optional_json_str(current_story_json) - .ok() - .flatten() - .and_then(|story| story.as_object().cloned()) - .and_then(|story| read_string_from_json(story.get("text"))); - let custom_world_profile = game_state_object - .and_then(|state| state.get("customWorldProfile")) - .and_then(JsonValue::as_object); - - if let Some(custom_world_profile) = custom_world_profile { - let world_name = read_string_from_json(custom_world_profile.get("name")) - .or_else(|| read_string_from_json(custom_world_profile.get("title"))) - .unwrap_or_else(|| world_meta.world_title.clone()); - let subtitle = read_string_from_json(custom_world_profile.get("summary")) - .or_else(|| read_string_from_json(custom_world_profile.get("settingText"))) - .unwrap_or_else(|| world_meta.world_subtitle.clone()); - let summary_text = continue_game_digest - .or(current_story_text) - .or_else(|| { - if subtitle.is_empty() { - None - } else { - Some(subtitle.clone()) - } - }) - .unwrap_or_else(|| DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT.to_string()); - - return Some(RuntimeProfileSaveArchiveMeta { - world_key: world_meta.world_key, - owner_user_id: world_meta.owner_user_id, - profile_id: world_meta.profile_id, - world_type: world_meta.world_type, - world_name, - subtitle, - summary_text, - cover_image_src: read_string_from_json(custom_world_profile.get("coverImageSrc")), - }); - } - - let summary_text = continue_game_digest - .or(current_story_text) - .or_else(|| { - if world_meta.world_subtitle.is_empty() { - None - } else { - Some(world_meta.world_subtitle.clone()) - } - }) - .unwrap_or_else(|| DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT.to_string()); - let current_scene_preset = game_state_object - .and_then(|state| state.get("currentScenePreset")) - .and_then(JsonValue::as_object); - - Some(RuntimeProfileSaveArchiveMeta { - world_key: world_meta.world_key, - owner_user_id: world_meta.owner_user_id, - profile_id: world_meta.profile_id, - world_type: world_meta.world_type, - world_name: world_meta.world_title, - subtitle: world_meta.world_subtitle.clone(), - summary_text, - cover_image_src: current_scene_preset - .and_then(|preset| read_string_from_json(preset.get("imageSrc"))), - }) -} - -fn is_non_persistent_runtime_snapshot(game_state: &JsonValue) -> bool { - let Some(game_state) = game_state.as_object() else { - return false; - }; - - if game_state - .get("runtimePersistenceDisabled") - .and_then(JsonValue::as_bool) - .unwrap_or(false) - { - return true; - } - - matches!( - read_string_from_json(game_state.get("runtimeMode")).as_deref(), - Some("preview") | Some("test") - ) -} - -fn build_builtin_world_title(world_type: &str) -> String { - match world_type { - "WUXIA" => "武侠世界".to_string(), - "XIANXIA" => "仙侠世界".to_string(), - _ => "叙事世界".to_string(), - } -} - fn get_profile_dashboard_snapshot( ctx: &ReducerContext, input: RuntimeProfileDashboardGetInput,