use super::*; pub(super) fn map_puzzle_agent_session_response( session: PuzzleAgentSessionRecord, ) -> PuzzleAgentSessionSnapshotResponse { PuzzleAgentSessionSnapshotResponse { session_id: session.session_id, seed_text: session.seed_text, current_turn: session.current_turn, progress_percent: session.progress_percent, stage: session.stage, anchor_pack: map_puzzle_anchor_pack_response(session.anchor_pack), draft: session.draft.map(map_puzzle_result_draft_response), messages: session .messages .into_iter() .map(map_puzzle_agent_message_response) .collect(), last_assistant_reply: session.last_assistant_reply, published_profile_id: session.published_profile_id, suggested_actions: session .suggested_actions .into_iter() .map(map_puzzle_suggested_action_response) .collect(), result_preview: session .result_preview .map(map_puzzle_result_preview_response), updated_at: session.updated_at, } } pub(super) fn map_puzzle_anchor_pack_response( anchor_pack: PuzzleAnchorPackRecord, ) -> PuzzleAnchorPackResponse { PuzzleAnchorPackResponse { theme_promise: map_puzzle_anchor_item_response(anchor_pack.theme_promise), visual_subject: map_puzzle_anchor_item_response(anchor_pack.visual_subject), visual_mood: map_puzzle_anchor_item_response(anchor_pack.visual_mood), composition_hooks: map_puzzle_anchor_item_response(anchor_pack.composition_hooks), tags_and_forbidden: map_puzzle_anchor_item_response(anchor_pack.tags_and_forbidden), } } pub(super) fn map_puzzle_anchor_item_response( anchor: PuzzleAnchorItemRecord, ) -> PuzzleAnchorItemResponse { PuzzleAnchorItemResponse { key: anchor.key, label: anchor.label, value: anchor.value, status: anchor.status, } } pub(super) fn map_puzzle_result_draft_response( draft: PuzzleResultDraftRecord, ) -> PuzzleResultDraftResponse { PuzzleResultDraftResponse { work_title: draft.work_title, work_description: draft.work_description, level_name: draft.level_name, summary: draft.summary, theme_tags: draft.theme_tags, forbidden_directives: draft.forbidden_directives, creator_intent: draft.creator_intent.map(map_puzzle_creator_intent_response), anchor_pack: map_puzzle_anchor_pack_response(draft.anchor_pack), candidates: draft .candidates .into_iter() .map(map_puzzle_generated_image_candidate_response) .collect(), selected_candidate_id: draft.selected_candidate_id, cover_image_src: draft.cover_image_src, cover_asset_id: draft.cover_asset_id, generation_status: draft.generation_status, levels: draft .levels .into_iter() .map(map_puzzle_draft_level_response) .collect(), form_draft: draft.form_draft.map(map_puzzle_form_draft_response), } } pub(super) fn map_puzzle_form_draft_response( draft: PuzzleFormDraftRecord, ) -> PuzzleFormDraftResponse { PuzzleFormDraftResponse { work_title: draft.work_title, work_description: draft.work_description, picture_description: draft.picture_description, } } pub(super) fn map_puzzle_draft_level_response( level: PuzzleDraftLevelRecord, ) -> PuzzleDraftLevelResponse { let generation_status = resolve_puzzle_level_generation_status(&level); PuzzleDraftLevelResponse { level_id: level.level_id, level_name: level.level_name, picture_description: level.picture_description, picture_reference: level.picture_reference, ui_background_prompt: level.ui_background_prompt, ui_background_image_src: level.ui_background_image_src, ui_background_image_object_key: level.ui_background_image_object_key, level_scene_image_src: level.level_scene_image_src, level_scene_image_object_key: level.level_scene_image_object_key, ui_spritesheet_image_src: level.ui_spritesheet_image_src, ui_spritesheet_image_object_key: level.ui_spritesheet_image_object_key, level_background_image_src: level.level_background_image_src, level_background_image_object_key: level.level_background_image_object_key, background_music: level .background_music .map(map_puzzle_audio_asset_record_response), candidates: level .candidates .into_iter() .map(map_puzzle_generated_image_candidate_response) .collect(), selected_candidate_id: level.selected_candidate_id, cover_image_src: level.cover_image_src, cover_asset_id: level.cover_asset_id, generation_status, } } pub(super) fn map_puzzle_audio_asset_record_response( asset: PuzzleAudioAssetRecord, ) -> CreationAudioAsset { CreationAudioAsset { task_id: asset.task_id, provider: asset.provider, asset_object_id: asset.asset_object_id, asset_kind: asset.asset_kind, audio_src: asset.audio_src, prompt: asset.prompt, title: asset.title, updated_at: asset.updated_at, } } pub(super) fn map_puzzle_audio_asset_domain_record( asset: module_puzzle::PuzzleAudioAsset, ) -> PuzzleAudioAssetRecord { PuzzleAudioAssetRecord { task_id: asset.task_id, provider: asset.provider, asset_object_id: asset.asset_object_id, asset_kind: asset.asset_kind, audio_src: asset.audio_src, prompt: asset.prompt, title: asset.title, updated_at: asset.updated_at, } } pub(super) fn puzzle_audio_asset_response_module_json(asset: &Option) -> Value { asset .as_ref() .map(|asset| { json!({ "task_id": asset.task_id, "provider": asset.provider, "asset_object_id": asset.asset_object_id, "asset_kind": asset.asset_kind, "audio_src": asset.audio_src, "prompt": asset.prompt, "title": asset.title, "updated_at": asset.updated_at, }) }) .unwrap_or(Value::Null) } pub(super) fn puzzle_audio_asset_record_module_json( asset: &Option, ) -> Value { asset .as_ref() .map(|asset| { json!({ "task_id": asset.task_id, "provider": asset.provider, "asset_object_id": asset.asset_object_id, "asset_kind": asset.asset_kind, "audio_src": asset.audio_src, "prompt": asset.prompt, "title": asset.title, "updated_at": asset.updated_at, }) }) .unwrap_or(Value::Null) } pub(super) fn map_puzzle_creator_intent_response( intent: PuzzleCreatorIntentRecord, ) -> PuzzleCreatorIntentResponse { PuzzleCreatorIntentResponse { source_mode: intent.source_mode, raw_messages_summary: intent.raw_messages_summary, theme_promise: intent.theme_promise, visual_subject: intent.visual_subject, visual_mood: intent.visual_mood, composition_hooks: intent.composition_hooks, theme_tags: intent.theme_tags, forbidden_directives: intent.forbidden_directives, } } pub(super) fn map_puzzle_generated_image_candidate_response( candidate: PuzzleGeneratedImageCandidateRecord, ) -> PuzzleGeneratedImageCandidateResponse { PuzzleGeneratedImageCandidateResponse { candidate_id: candidate.candidate_id, image_src: candidate.image_src, asset_id: candidate.asset_id, prompt: candidate.prompt, actual_prompt: candidate.actual_prompt, source_type: candidate.source_type, selected: candidate.selected, } } pub(super) fn map_puzzle_agent_message_response( message: PuzzleAgentMessageRecord, ) -> PuzzleAgentMessageResponse { PuzzleAgentMessageResponse { id: message.message_id, role: message.role, kind: message.kind, text: message.text, created_at: message.created_at, } } pub(super) fn map_puzzle_suggested_action_response( action: PuzzleAgentSuggestedActionRecord, ) -> PuzzleAgentSuggestedActionResponse { PuzzleAgentSuggestedActionResponse { id: action.action_id, action_type: action.action_type, label: action.label, } } pub(super) fn map_puzzle_result_preview_response( preview: PuzzleResultPreviewRecord, ) -> PuzzleResultPreviewEnvelopeResponse { PuzzleResultPreviewEnvelopeResponse { draft: map_puzzle_result_draft_response(preview.draft), blockers: preview .blockers .into_iter() .map(map_puzzle_result_preview_blocker_response) .collect(), quality_findings: preview .quality_findings .into_iter() .map(map_puzzle_result_preview_finding_response) .collect(), publish_ready: preview.publish_ready, } } pub(super) fn map_puzzle_result_preview_blocker_response( blocker: PuzzleResultPreviewBlockerRecord, ) -> PuzzleResultPreviewBlockerResponse { PuzzleResultPreviewBlockerResponse { id: blocker.blocker_id, code: blocker.code, message: blocker.message, } } pub(super) fn map_puzzle_result_preview_finding_response( finding: PuzzleResultPreviewFindingRecord, ) -> PuzzleResultPreviewFindingResponse { PuzzleResultPreviewFindingResponse { id: finding.finding_id, severity: finding.severity, code: finding.code, message: finding.message, } } fn resolve_puzzle_work_generation_status(item: &PuzzleWorkProfileRecord) -> Option { let has_viewable_result = item .cover_image_src .as_deref() .map(str::trim) .is_some_and(|value| !value.is_empty()) || item.levels.iter().any(has_puzzle_level_image); if has_viewable_result { return Some("ready".to_string()); } item.levels .iter() .map(resolve_puzzle_level_generation_status) .find(|status| status.as_str() == "generating") .or_else(|| { item.levels .iter() .map(resolve_puzzle_level_generation_status) .find(|status| status.as_str() == "ready") }) .or_else(|| { item.levels .iter() .map(resolve_puzzle_level_generation_status) .find(|status| !status.is_empty()) }) } fn resolve_puzzle_level_generation_status(level: &PuzzleDraftLevelRecord) -> String { if level.generation_status.trim() == "generating" && has_puzzle_level_image(level) { return "ready".to_string(); } level.generation_status.trim().to_string() } fn has_puzzle_level_image(level: &PuzzleDraftLevelRecord) -> bool { let has_cover = level .cover_image_src .as_deref() .map(str::trim) .is_some_and(|value| !value.is_empty()); let has_selected_candidate = level .selected_candidate_id .as_deref() .and_then(|candidate_id| { level .candidates .iter() .find(|candidate| candidate.candidate_id == candidate_id) }) .map(|candidate| candidate.image_src.trim()) .is_some_and(|value| !value.is_empty()); let has_fallback_candidate = level .candidates .last() .map(|candidate| candidate.image_src.trim()) .is_some_and(|value| !value.is_empty()); has_cover || has_selected_candidate || has_fallback_candidate } pub(super) fn map_puzzle_work_summary_response( state: &PuzzleApiState, item: PuzzleWorkProfileRecord, ) -> PuzzleWorkSummaryResponse { let generation_status = resolve_puzzle_work_generation_status(&item); 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, levels: item .levels .iter() .map(|x| map_puzzle_draft_level_response(x.clone())) .collect(), } } 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_puzzle_work_profile_response( state: &PuzzleApiState, item: PuzzleWorkProfileRecord, ) -> PuzzleWorkProfileResponse { let mut summary = map_puzzle_work_summary_response(state, item.clone()); summary.levels = item .levels .into_iter() .map(map_puzzle_draft_level_response) .collect(); PuzzleWorkProfileResponse { summary, anchor_pack: map_puzzle_anchor_pack_response(item.anchor_pack), } } pub(super) fn map_puzzle_run_response(run: PuzzleRunRecord) -> PuzzleRunSnapshotResponse { PuzzleRunSnapshotResponse { run_id: run.run_id, entry_profile_id: run.entry_profile_id, cleared_level_count: run.cleared_level_count, current_level_index: run.current_level_index, current_grid_size: run.current_grid_size, played_profile_ids: run.played_profile_ids, previous_level_tags: run.previous_level_tags, current_level: run.current_level.map(map_puzzle_runtime_level_response), recommended_next_profile_id: run.recommended_next_profile_id, next_level_mode: run.next_level_mode, next_level_profile_id: run.next_level_profile_id, next_level_id: run.next_level_id, recommended_next_works: run .recommended_next_works .into_iter() .map(map_puzzle_recommended_next_work_response) .collect(), leaderboard_entries: run .leaderboard_entries .into_iter() .map(map_puzzle_leaderboard_entry_response) .collect(), } } pub(super) fn map_puzzle_recommended_next_work_response( item: PuzzleRecommendedNextWorkRecord, ) -> PuzzleRecommendedNextWorkResponse { PuzzleRecommendedNextWorkResponse { profile_id: item.profile_id, level_name: item.level_name, author_display_name: item.author_display_name, theme_tags: item.theme_tags, cover_image_src: item.cover_image_src, similarity_score: item.similarity_score, } } pub(super) async fn enrich_puzzle_run_author_name( state: &PuzzleApiState, mut run: PuzzleRunRecord, ) -> PuzzleRunRecord { if let Some(level) = run.current_level.as_mut() { if let Ok(profile) = state .spacetime_client() .get_puzzle_gallery_detail(level.profile_id.clone()) .await { level.author_display_name = resolve_puzzle_work_author_by_user_id( state, &profile.owner_user_id, Some(&profile.author_display_name), None, ) .display_name; } } run } pub(super) fn map_puzzle_runtime_level_response( level: spacetime_client::PuzzleRuntimeLevelRecord, ) -> PuzzleRuntimeLevelSnapshotResponse { let timer_defaults = build_puzzle_runtime_timer_response_defaults(level.level_index, level.grid_size); let time_limit_ms = if level.time_limit_ms == 0 { timer_defaults.time_limit_ms } else { level.time_limit_ms }; let remaining_ms = if level.remaining_ms == 0 && level.status == PuzzleRuntimeLevelStatus::Playing.as_str() { time_limit_ms } else { level.remaining_ms.min(time_limit_ms) }; PuzzleRuntimeLevelSnapshotResponse { run_id: level.run_id, level_index: level.level_index, level_id: level.level_id, grid_size: level.grid_size, profile_id: level.profile_id, level_name: level.level_name, author_display_name: level.author_display_name, theme_tags: level.theme_tags, cover_image_src: level.cover_image_src, ui_background_image_src: level.ui_background_image_src, ui_background_image_object_key: level.ui_background_image_object_key, level_background_image_src: level.level_background_image_src, level_background_image_object_key: level.level_background_image_object_key, ui_spritesheet_image_src: level.ui_spritesheet_image_src, ui_spritesheet_image_object_key: level.ui_spritesheet_image_object_key, background_music: level .background_music .map(map_puzzle_audio_asset_record_response), board: map_puzzle_board_response(level.board), status: level.status, started_at_ms: level.started_at_ms, cleared_at_ms: level.cleared_at_ms, elapsed_ms: level.elapsed_ms, time_limit_ms, remaining_ms, paused_accumulated_ms: level.paused_accumulated_ms, pause_started_at_ms: level.pause_started_at_ms, freeze_accumulated_ms: level.freeze_accumulated_ms, freeze_started_at_ms: level.freeze_started_at_ms, freeze_until_ms: level.freeze_until_ms, leaderboard_entries: level .leaderboard_entries .into_iter() .map(map_puzzle_leaderboard_entry_response) .collect(), } } struct PuzzleRuntimeTimerResponseDefaults { time_limit_ms: u64, } fn build_puzzle_runtime_timer_response_defaults( level_index: u32, grid_size: u32, ) -> PuzzleRuntimeTimerResponseDefaults { let time_limit_ms = if level_index > 0 { module_puzzle::resolve_puzzle_level_time_limit_ms_by_index(level_index) } else { module_puzzle::resolve_puzzle_level_time_limit_ms(grid_size) }; PuzzleRuntimeTimerResponseDefaults { time_limit_ms } } pub(super) fn map_puzzle_leaderboard_entry_response( entry: PuzzleLeaderboardEntryRecord, ) -> PuzzleLeaderboardEntryResponse { PuzzleLeaderboardEntryResponse { rank: entry.rank, nickname: entry.nickname, elapsed_ms: entry.elapsed_ms, visible_tags: entry.visible_tags, is_current_player: entry.is_current_player, } } pub(super) fn map_puzzle_board_response( board: spacetime_client::PuzzleBoardRecord, ) -> PuzzleBoardSnapshotResponse { PuzzleBoardSnapshotResponse { rows: board.rows, cols: board.cols, pieces: board .pieces .into_iter() .map(|piece| PuzzlePieceStateResponse { piece_id: piece.piece_id, correct_row: piece.correct_row, correct_col: piece.correct_col, current_row: piece.current_row, current_col: piece.current_col, merged_group_id: piece.merged_group_id, }) .collect(), merged_groups: board .merged_groups .into_iter() .map(|group| PuzzleMergedGroupStateResponse { group_id: group.group_id, piece_ids: group.piece_ids, occupied_cells: group .occupied_cells .into_iter() .map(|cell| PuzzleCellPositionResponse { row: cell.row, col: cell.col, }) .collect(), }) .collect(), selected_piece_id: board.selected_piece_id, all_tiles_resolved: board.all_tiles_resolved, } } pub(super) fn resolve_author_display_name( state: &PuzzleApiState, authenticated: &AuthenticatedAccessToken, ) -> String { state .auth_user_service() .get_user_by_id(authenticated.claims().user_id()) .ok() .flatten() .map(|user| user.display_name) .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| "玩家".to_string()) } pub(super) fn build_puzzle_welcome_text(seed_text: &str) -> String { if seed_text.trim().is_empty() { return "拼图创作信息已准备好。".to_string(); } "拼图创作信息已准备好。".to_string() }