use super::*; pub(super) fn map_match3d_agent_session_response( session: Match3DAgentSessionRecord, ) -> Match3DAgentSessionSnapshotResponse { Match3DAgentSessionSnapshotResponse { session_id: session.session_id, current_turn: session.current_turn, progress_percent: session.progress_percent, stage: session.stage.clone(), anchor_pack: map_match3d_anchor_pack_response_for_turn( session.anchor_pack, session.current_turn, session.stage.as_str(), ), config: session.config.map(map_match3d_config_response), draft: session.draft.map(map_match3d_draft_response), messages: session .messages .into_iter() .map(map_match3d_message_response) .collect(), last_assistant_reply: session.last_assistant_reply, published_profile_id: session.published_profile_id, updated_at: session.updated_at, } } pub(super) fn map_match3d_agent_session_response_with_assets( session: Match3DAgentSessionRecord, generated_item_assets: &[Match3DGeneratedItemAsset], ) -> Match3DAgentSessionSnapshotResponse { let mut response = map_match3d_agent_session_response(session); if let Some(draft) = response.draft.as_mut() { if generated_item_assets.is_empty() { return response; } draft.generated_item_assets = generated_item_assets .iter() .cloned() .map(map_match3d_generated_item_asset_for_agent) .collect(); if draft .cover_image_src .as_deref() .map(str::trim) .unwrap_or_default() .is_empty() { draft.cover_image_src = resolve_match3d_default_cover_image_src(generated_item_assets); } let background_asset = find_match3d_generated_background_asset(generated_item_assets); apply_match3d_background_asset_to_agent_draft(draft, background_asset); } response } pub(super) fn map_match3d_anchor_pack_response_for_turn( anchor: Match3DAnchorPackRecord, current_turn: u32, stage: &str, ) -> Match3DAnchorPackResponse { let is_ready = matches!( stage, "ReadyToCompile" | "ready_to_compile" | "DraftCompiled" | "draft_compiled" | "draft_ready" | "ReadyToPublish" | "ready_to_publish" | "Published" | "published" ); let collected_count = if is_ready { 3 } else { current_turn.min(3) }; Match3DAnchorPackResponse { theme: map_match3d_anchor_item_response_for_collected(anchor.theme, collected_count >= 1), clear_count: map_match3d_anchor_item_response_for_collected( anchor.clear_count, collected_count >= 2, ), difficulty: map_match3d_anchor_item_response_for_collected( anchor.difficulty, collected_count >= 3, ), } } pub(super) fn map_match3d_anchor_item_response( anchor: Match3DAnchorItemRecord, ) -> Match3DAnchorItemResponse { Match3DAnchorItemResponse { key: anchor.key, label: anchor.label, value: anchor.value, status: anchor.status, } } pub(super) fn map_match3d_anchor_item_response_for_collected( anchor: Match3DAnchorItemRecord, collected: bool, ) -> Match3DAnchorItemResponse { if collected { return map_match3d_anchor_item_response(anchor); } Match3DAnchorItemResponse { key: anchor.key, label: anchor.label, value: String::new(), status: "missing".to_string(), } } pub(super) fn map_match3d_config_response( config: Match3DCreatorConfigRecord, ) -> Match3DCreatorConfigResponse { Match3DCreatorConfigResponse { theme_text: config.theme_text, reference_image_src: config.reference_image_src, clear_count: config.clear_count, difficulty: config.difficulty, asset_style_id: config.asset_style_id, asset_style_label: config.asset_style_label, asset_style_prompt: config.asset_style_prompt, generate_click_sound: config.generate_click_sound, } } pub(super) fn map_match3d_draft_response( draft: Match3DResultDraftRecord, ) -> Match3DResultDraftResponse { // 中文注释:session draft 自身也可能携带生成素材快照,不能只依赖 work detail 回读补齐 UI 背景和容器图。 let generated_item_assets = parse_match3d_generated_item_assets(draft.generated_item_assets_json.as_deref()) .into_iter() .map(Match3DGeneratedItemAsset::from) .collect::>(); let background_asset = find_match3d_generated_background_asset(&generated_item_assets); let mut response = Match3DResultDraftResponse { profile_id: draft.profile_id, game_name: draft.game_name, theme_text: draft.theme_text, summary_text: Some(draft.summary_text.clone()), summary: draft.summary_text, tags: draft.tags, cover_image_src: draft.cover_image_src, reference_image_src: draft.reference_image_src, clear_count: draft.clear_count, difficulty: draft.difficulty, total_item_count: draft.total_item_count, publish_ready: draft.publish_ready, blockers: draft.blockers, background_prompt: None, background_image_src: None, background_image_object_key: None, generated_background_asset: None, generated_item_assets: generated_item_assets .iter() .cloned() .map(map_match3d_generated_item_asset_for_agent) .collect(), }; if response .cover_image_src .as_deref() .map(str::trim) .unwrap_or_default() .is_empty() { response.cover_image_src = resolve_match3d_default_cover_image_src(&generated_item_assets); } apply_match3d_background_asset_to_agent_draft(&mut response, background_asset); response } pub(super) fn map_match3d_generated_item_asset_for_agent( asset: Match3DGeneratedItemAsset, ) -> Match3DAgentGeneratedItemAssetResponse { Match3DAgentGeneratedItemAssetResponse { item_id: asset.item_id, item_name: asset.item_name, item_size: asset.item_size, image_src: asset.image_src, image_object_key: asset.image_object_key, image_views: asset .image_views .into_iter() .map(map_match3d_image_view_for_agent) .collect(), model_src: asset.model_src, model_object_key: asset.model_object_key, model_file_name: asset.model_file_name, task_uuid: asset.task_uuid, subscription_key: asset.subscription_key, sound_prompt: asset.sound_prompt, background_music_title: asset.background_music_title, background_music_style: asset.background_music_style, background_music_prompt: asset.background_music_prompt, background_music: asset.background_music, click_sound: asset.click_sound, background_asset: asset .background_asset .map(map_match3d_background_asset_for_agent), status: asset.status, error: asset.error, } } pub(super) fn map_match3d_generated_item_asset_for_work( asset: Match3DGeneratedItemAssetJson, ) -> shared_contracts::match3d_works::Match3DGeneratedItemAssetResponse { shared_contracts::match3d_works::Match3DGeneratedItemAssetResponse { item_id: asset.item_id, item_name: asset.item_name, item_size: asset .item_size .or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())), image_src: asset.image_src, image_object_key: asset.image_object_key, image_views: asset .image_views .into_iter() .map(map_match3d_image_view_for_work) .collect(), model_src: asset.model_src, model_object_key: asset.model_object_key, model_file_name: asset.model_file_name, task_uuid: asset.task_uuid, subscription_key: asset.subscription_key, sound_prompt: asset.sound_prompt, background_music_title: asset.background_music_title, background_music_style: asset.background_music_style, background_music_prompt: asset.background_music_prompt, background_music: asset.background_music, click_sound: asset.click_sound, background_asset: asset .background_asset .map(map_match3d_background_asset_for_work), status: asset.status, error: asset.error, } } pub(super) fn map_match3d_image_view_for_agent( view: Match3DGeneratedItemImageView, ) -> shared_contracts::match3d_agent::Match3DGeneratedItemImageViewResponse { shared_contracts::match3d_agent::Match3DGeneratedItemImageViewResponse { view_id: view.view_id, view_index: view.view_index, image_src: view.image_src, image_object_key: view.image_object_key, } } pub(super) fn map_match3d_image_view_for_work( view: Match3DGeneratedItemImageView, ) -> shared_contracts::match3d_works::Match3DGeneratedItemImageViewResponse { shared_contracts::match3d_works::Match3DGeneratedItemImageViewResponse { view_id: view.view_id, view_index: view.view_index, image_src: view.image_src, image_object_key: view.image_object_key, } } pub(super) fn map_match3d_image_view_from_work( view: shared_contracts::match3d_works::Match3DGeneratedItemImageViewResponse, ) -> Match3DGeneratedItemImageView { Match3DGeneratedItemImageView { view_id: view.view_id, view_index: view.view_index, image_src: view.image_src, image_object_key: view.image_object_key, } } pub(super) fn map_match3d_background_asset_for_agent( asset: Match3DGeneratedBackgroundAsset, ) -> shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse { shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse { prompt: asset.prompt, image_src: asset.image_src, image_object_key: asset.image_object_key, container_prompt: asset.container_prompt, container_image_src: asset.container_image_src, container_image_object_key: asset.container_image_object_key, status: asset.status, error: asset.error, } } pub(super) fn map_match3d_background_asset_for_work( asset: Match3DGeneratedBackgroundAsset, ) -> shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse { shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse { prompt: asset.prompt, image_src: asset.image_src, image_object_key: asset.image_object_key, container_prompt: asset.container_prompt, container_image_src: asset.container_image_src, container_image_object_key: asset.container_image_object_key, status: asset.status, error: asset.error, } } pub(super) fn find_match3d_generated_background_asset( assets: &[Match3DGeneratedItemAsset], ) -> Option { assets .iter() .find_map(|asset| asset.background_asset.clone()) } pub(super) fn resolve_match3d_default_cover_image_src( assets: &[Match3DGeneratedItemAsset], ) -> Option { find_match3d_generated_background_asset(assets).and_then(|asset| { asset .container_image_src .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) .or_else(|| { asset .container_image_object_key .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) }) .or_else(|| { asset .image_src .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) }) .or_else(|| { asset .image_object_key .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) }) }) } pub(super) fn find_match3d_generated_background_asset_json( assets: &[Match3DGeneratedItemAssetJson], ) -> Option { assets .iter() .find_map(|asset| asset.background_asset.clone()) } pub(super) fn apply_match3d_background_asset_to_agent_draft( draft: &mut Match3DResultDraftResponse, background_asset: Option, ) { if let Some(asset) = background_asset { draft.background_prompt = Some(asset.prompt.clone()); draft.background_image_src = asset.image_src.clone(); draft.background_image_object_key = asset.image_object_key.clone(); draft.generated_background_asset = Some(map_match3d_background_asset_for_agent(asset)); } } pub(super) fn build_match3d_work_profile_record_with_assets( mut item: Match3DWorkProfileRecord, assets: &[Match3DGeneratedItemAsset], ) -> Match3DWorkProfileRecord { item.generated_item_assets_json = serialize_match3d_generated_item_assets(assets); if let Some(background_asset) = find_match3d_generated_background_asset(assets) { item.cover_image_src = item.cover_image_src.or_else(|| { background_asset .container_image_src .clone() .or(background_asset.container_image_object_key.clone()) .or(background_asset.image_src.clone()) .or(background_asset.image_object_key.clone()) }); } item } fn match3d_text_present(value: Option<&String>) -> bool { value.is_some_and(|value| !value.trim().is_empty()) } fn match3d_item_asset_has_image(asset: &Match3DGeneratedItemAssetJson) -> bool { match3d_text_present(asset.image_src.as_ref()) || match3d_text_present(asset.image_object_key.as_ref()) || asset.image_views.iter().any(|view| { match3d_text_present(view.image_src.as_ref()) || match3d_text_present(view.image_object_key.as_ref()) }) } fn match3d_background_asset_has_image(asset: &Match3DGeneratedBackgroundAsset) -> bool { match3d_text_present(asset.image_src.as_ref()) || match3d_text_present(asset.image_object_key.as_ref()) || match3d_text_present(asset.container_image_src.as_ref()) || match3d_text_present(asset.container_image_object_key.as_ref()) } fn resolve_match3d_work_generation_status( item: &Match3DWorkProfileRecord, assets: &[Match3DGeneratedItemAssetJson], background_asset: Option<&Match3DGeneratedBackgroundAsset>, ) -> Option { if item.publication_status.eq_ignore_ascii_case("published") { return Some("ready".to_string()); } if assets.is_empty() || !assets.iter().any(match3d_item_asset_has_image) || !background_asset.is_some_and(match3d_background_asset_has_image) { return Some("generating".to_string()); } Some("ready".to_string()) } pub(super) fn map_match3d_message_response( message: Match3DAgentMessageRecord, ) -> Match3DAgentMessageResponse { Match3DAgentMessageResponse { id: message.message_id, role: message.role, kind: message.kind, text: message.text, created_at: message.created_at, } } pub(super) fn map_match3d_work_summary_response( item: Match3DWorkProfileRecord, ) -> Match3DWorkSummaryResponse { let generated_item_asset_json = parse_match3d_generated_item_assets(item.generated_item_assets_json.as_deref()); let background_asset = find_match3d_generated_background_asset_json(&generated_item_asset_json); let generation_status = resolve_match3d_work_generation_status( &item, &generated_item_asset_json, background_asset.as_ref(), ); let generated_background_asset = background_asset .clone() .map(map_match3d_background_asset_for_work); let generated_item_assets = generated_item_asset_json .into_iter() .map(map_match3d_generated_item_asset_for_work) .collect(); Match3DWorkSummaryResponse { work_id: item.work_id, profile_id: item.profile_id, owner_user_id: item.owner_user_id, source_session_id: item.source_session_id, game_name: item.game_name, theme_text: item.theme_text, summary: item.summary, tags: item.tags, cover_image_src: item.cover_image_src, reference_image_src: item.reference_image_src, clear_count: item.clear_count, difficulty: item.difficulty, publication_status: item.publication_status, play_count: item.play_count, updated_at: item.updated_at, published_at: item.published_at, publish_ready: item.publish_ready, generation_status, background_prompt: background_asset.as_ref().map(|asset| asset.prompt.clone()), background_image_src: background_asset .as_ref() .and_then(|asset| asset.image_src.clone()), background_image_object_key: background_asset .as_ref() .and_then(|asset| asset.image_object_key.clone()), generated_background_asset, generated_item_assets, } } pub(super) fn match3d_bad_gateway(message: impl Into) -> AppError { AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ "provider": MATCH3D_AGENT_PROVIDER, "message": message.into(), })) } pub(super) fn map_match3d_work_profile_response( item: Match3DWorkProfileRecord, ) -> Match3DWorkProfileResponse { Match3DWorkProfileResponse { summary: map_match3d_work_summary_response(item), } } pub(super) fn map_match3d_run_response(run: Match3DRunRecord) -> Match3DRunSnapshotResponse { Match3DRunSnapshotResponse { run_id: run.run_id, profile_id: run.profile_id, owner_user_id: run.owner_user_id, status: normalize_match3d_run_status(run.status.as_str()).to_string(), snapshot_version: run.snapshot_version, started_at_ms: run.started_at_ms, duration_limit_ms: run.duration_limit_ms, server_now_ms: run.server_now_ms, remaining_ms: run.remaining_ms, clear_count: run.clear_count, total_item_count: run.total_item_count, cleared_item_count: run.cleared_item_count, items: run .items .into_iter() .map(map_match3d_item_response) .collect(), tray_slots: run .tray_slots .into_iter() .map(map_match3d_tray_slot_response) .collect(), failure_reason: run .failure_reason .map(|reason| normalize_match3d_failure_reason(reason.as_str()).to_string()), last_confirmed_action_id: run.last_confirmed_action_id, } } pub(super) fn map_match3d_item_response( item: Match3DItemSnapshotRecord, ) -> Match3DItemSnapshotResponse { Match3DItemSnapshotResponse { item_instance_id: item.item_instance_id, item_type_id: item.item_type_id, visual_key: item.visual_key, x: item.x, y: item.y, radius: item.radius, layer: item.layer, state: normalize_match3d_item_state(item.state.as_str()).to_string(), clickable: item.clickable, tray_slot_index: item.tray_slot_index, } } pub(super) fn map_match3d_tray_slot_response( slot: Match3DTraySlotRecord, ) -> Match3DTraySlotResponse { Match3DTraySlotResponse { slot_index: slot.slot_index, item_instance_id: slot.item_instance_id, item_type_id: slot.item_type_id, visual_key: slot.visual_key, } } pub(super) fn map_match3d_click_confirmation_response( confirmation: Match3DClickConfirmationRecord, ) -> Match3DClickConfirmationResponse { Match3DClickConfirmationResponse { accepted: confirmation.accepted, reject_reason: confirmation .reject_reason .map(|reason| normalize_match3d_click_reject_reason(reason.as_str()).to_string()), entered_slot_index: confirmation.entered_slot_index, cleared_item_instance_ids: confirmation.cleared_item_instance_ids, run: map_match3d_run_response(confirmation.run), } }