159 lines
7.1 KiB
Rust
159 lines
7.1 KiB
Rust
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;
|