Files
Genarrative/server-rs/crates/api-server/src/puzzle.rs
高物 ae014ac881 Switch to VectorEngine gpt-image-2 and edits
Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
2026-05-22 03:06:41 +08:00

156 lines
9.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::{
collections::BTreeMap,
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, OssSignedGetObjectUrlRequest};
use platform_oss::{OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest};
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,
generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha,
http_error::AppError,
llm_model_routing::{CREATION_TEMPLATE_LLM_MODEL, PUZZLE_LEVEL_NAME_VISION_LLM_MODEL},
openai_image_generation::{
DownloadedOpenAiImage, 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::PuzzleApiState,
work_author::resolve_puzzle_work_author_by_user_id,
work_play_tracking::{WorkPlayTrackingDraft, record_puzzle_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";
#[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_UI_BACKGROUND_PROMPT_FALLBACK_MARKER: &str =
"移动端拼图游戏纯背景,题材氛围清晰,不包含拼图槽或 UI 元素";
const PUZZLE_VECTOR_ENGINE_SQUARE_IMAGE_SIZE: &str = "1024x1024";
const PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE: &str = "1024x1536";
const PUZZLE_LEVEL_SCENE_IMAGE_PROMPT: &str = "参考图作为拼图画面生成对应的拼图游戏关卡画面要求画面中所有元素精致且风格高度一致画面中所有UI细节饱满精致、完成度高、顶级游戏品质\n\n画面元素:\n返回按钮位于顶部左上角顶部中间显示关卡标题“第1关 影”和倒计时时间,右上角显示设置按钮\n画面中间是一个正方形的3*3拼图拼图区域宽度与画面宽度同宽紧贴画面横向边缘拼图区域边界带有边框装饰\n拼图区域下方包含一个下一关按钮,仅在关卡完成时显示\n底部是三个贴合画面主题的道具按钮分别为“提示”、“原图”、“冻结”\n道具按钮上不要显示次数标注,返回按钮和设置按钮旁禁止标注文字";
const PUZZLE_UI_SPRITESHEET_IMAGE_PROMPT: &str = "提取画面中的UI元素将返回按钮、设置按钮、下一关按钮、提示按钮、原图按钮、冻结按钮整理成纯绿色绿幕背景的spritesheet。背景必须是统一纯绿色绿幕高饱和亮绿色接近 #00FF00背景平整无纹理、无渐变、无阴影、无场景内容后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。按钮顺序必须按原图位置从左到右、从上到下排列返回、设置、下一关、提示、原图、冻结。按钮素材内必须保留对应中文文字每个按钮必须是独立完整图形按钮之间保留足够纯绿色绿幕空白不能相互接触、重叠或连成一片方便运行态按自动边界检测识别矩形素材。返回按钮和设置按钮不要额外画白色外圈、白底圆环或浮雕外框直接画扁平图标本体。按钮自身不得使用接近 #00FF00 的高饱和纯绿;绿色题材只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。禁止水印、数字、次数标注、透明背景、背景图、拼图块、棋盘、网格线、按钮外标签和额外按钮。";
const PUZZLE_LEVEL_BACKGROUND_IMAGE_PROMPT: &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;