use std::{ collections::BTreeMap, error::Error as StdError, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use axum::{ Json, extract::{Extension, Path as AxumPath, State, rejection::JsonRejection}, http::{HeaderName, StatusCode, header}, response::{ IntoResponse, Response, sse::{Event, Sse}, }, }; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; 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, }; use module_puzzle::{PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus}; use platform_llm::{LlmMessage, LlmMessageContentPart, LlmTextRequest}; use platform_oss::{ LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest, OssSignedGetObjectUrlRequest, }; use serde_json::{Map, Value, json}; use shared_contracts::{ creation_audio::CreationAudioAsset, puzzle_agent::{ CreatePuzzleAgentSessionRequest, ExecutePuzzleAgentActionRequest, PuzzleAgentActionResponse, PuzzleAgentMessageResponse, PuzzleAgentOperationResponse, PuzzleAgentSessionResponse, PuzzleAgentSessionSnapshotResponse, PuzzleAgentSuggestedActionResponse, PuzzleAnchorItemResponse, PuzzleAnchorPackResponse, PuzzleCreatorIntentResponse, PuzzleDraftLevelResponse, PuzzleFormDraftResponse, PuzzleGeneratedImageCandidateResponse, PuzzleResultDraftResponse, PuzzleResultPreviewBlockerResponse, PuzzleResultPreviewEnvelopeResponse, PuzzleResultPreviewFindingResponse, SendPuzzleAgentMessageRequest, }, puzzle_gallery::PuzzleGalleryDetailResponse, puzzle_runtime::{ AdvancePuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse, PuzzleRecommendedNextWorkResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest, UsePuzzleRuntimePropRequest, }, puzzle_works::{ PutPuzzleWorkRequest, PuzzleOnboardingGenerateRequest, PuzzleOnboardingGenerateResponse, PuzzleOnboardingSaveRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse, PuzzleWorkProfileResponse, PuzzleWorkSummaryResponse, PuzzleWorksResponse, }, }; use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros}; use spacetime_client::{ PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord, PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, PuzzleGalleryCardRecord, PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleSelectCoverImageRecordInput, PuzzleUiBackgroundSaveRecordInput, PuzzleWorkLikeReportRecordInput, PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, SpacetimeClientError, }; use std::convert::Infallible; use crate::{ ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter}, api_response::json_success_body, asset_billing::{ execute_billable_asset_operation, execute_billable_asset_operation_with_cost, should_skip_asset_operation_billing_for_connectivity, }, auth::AuthenticatedAccessToken, http_error::AppError, llm_model_routing::{CREATION_TEMPLATE_LLM_MODEL, PUZZLE_LEVEL_NAME_VISION_LLM_MODEL}, openai_image_generation::{ DownloadedOpenAiImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, build_openai_image_http_client, create_openai_image_generation, require_openai_image_settings, }, platform_errors::map_oss_error, prompt::puzzle::{ draft::{ PuzzleFormSeedPromptParts, build_puzzle_form_seed_prompt, resolve_puzzle_draft_cover_prompt, resolve_puzzle_level_image_prompt, }, image::{PUZZLE_DEFAULT_NEGATIVE_PROMPT, build_puzzle_image_prompt}, level_name::{ PUZZLE_FIRST_LEVEL_NAME_SYSTEM_PROMPT, build_puzzle_first_level_name_user_prompt, build_puzzle_first_level_name_vision_user_text, }, tags::{PUZZLE_TAG_GENERATION_SYSTEM_PROMPT, build_puzzle_tag_generation_user_prompt}, }, puzzle_agent_turn::{ PuzzleAgentTurnRequest, build_failed_finalize_record_input, build_finalize_record_input, run_puzzle_agent_turn, }, puzzle_gallery_cache::{build_puzzle_gallery_window_response, puzzle_gallery_cached_json}, request_context::RequestContext, state::AppState, vector_engine_audio_generation::{ GeneratedCreationAudioTarget, generate_background_music_asset_for_creation, }, work_author::resolve_work_author_by_user_id, work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success}, }; const PUZZLE_AGENT_API_BASE_PROVIDER: &str = "puzzle-agent"; const PUZZLE_WORKS_PROVIDER: &str = "puzzle-works"; const PUZZLE_GALLERY_PROVIDER: &str = "puzzle-gallery"; const PUZZLE_RUNTIME_PROVIDER: &str = "puzzle-runtime"; const PUZZLE_IMAGE_MODEL_GPT_IMAGE_2: &str = "gpt-image-2"; const PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW: &str = "gemini-3.1-flash-image-preview"; const PUZZLE_IMAGE_GENERATION_POINTS_COST: u64 = 2; const PUZZLE_ENTITY_KIND: &str = "puzzle_work"; const PUZZLE_BACKGROUND_MUSIC_ASSET_KIND: &str = "puzzle_background_music"; const PUZZLE_BACKGROUND_MUSIC_SLOT: &str = "background_music"; #[cfg(test)] const PUZZLE_GENERATED_IMAGE_SIZE: &str = "1024*1024"; const PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE: &str = "1024x1024"; const VECTOR_ENGINE_PROVIDER: &str = "vector-engine"; const PUZZLE_LEVEL_NAME_VISION_IMAGE_MAX_SIDE: u32 = 768; const PUZZLE_LEVEL_NAME_VISION_MAX_TOKENS: u32 = 512; const PUZZLE_REFERENCE_IMAGE_MAX_BYTES: usize = 8 * 1024 * 1024; const PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT: usize = 5; const PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL: &str = "gpt-image-2"; const PUZZLE_UI_BACKGROUND_PROMPT_FALLBACK_MARKER: &str = "移动端拼图游戏纯背景,题材氛围清晰,不包含拼图槽或 UI 元素"; mod handlers; pub(crate) use self::handlers::*; mod mappers; use self::mappers::*; mod draft; use self::draft::*; mod tags; use self::tags::*; mod generation; mod vector_engine; use self::generation::*; use self::vector_engine::*; #[cfg(test)] mod tests;