use axum::{ Json, extract::{Extension, Path, State, rejection::JsonRejection}, http::StatusCode, response::Response, }; use module_custom_world::{ CustomWorldThemeMode, empty_agent_anchor_content_json, empty_agent_asset_coverage_json, empty_agent_creator_intent_readiness_json, empty_json_array, empty_json_object, }; use serde_json::{Map, Value, json}; use shared_contracts::runtime::{ CreateCustomWorldAgentSessionRequest, CustomWorldAgentCardDetailResponse, CustomWorldAgentCheckpointResponse, CustomWorldAgentMessageResponse, CustomWorldAgentOperationResponse, CustomWorldAgentSessionResponse, CustomWorldAgentSessionSnapshotResponse, CustomWorldDraftCardDetailResponse, CustomWorldDraftCardDetailSectionResponse, CustomWorldDraftCardSummaryResponse, CustomWorldGalleryCardResponse, CustomWorldGalleryDetailResponse, CustomWorldGalleryResponse, CustomWorldLibraryEntryResponse, CustomWorldLibraryMutationResponse, CustomWorldLibraryResponse, CustomWorldProfileUpsertRequest, CustomWorldPublishGateResponse, CustomWorldResultPreviewBlockerResponse, CustomWorldSupportedActionResponse, CustomWorldWorkSummaryResponse, CustomWorldWorksResponse, ExecuteCustomWorldAgentActionRequest, SendCustomWorldAgentMessageRequest, }; use shared_kernel::build_prefixed_uuid_id; use spacetime_client::{ CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationRecord, CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord, CustomWorldDraftCardDetailRecord, CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord, CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord, CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord, CustomWorldWorkSummaryRecord, SpacetimeClientError, }; use crate::{ api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError, request_context::RequestContext, sse::SseEventBuffer, state::AppState, }; pub async fn get_custom_world_library( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, ) -> Result, Response> { let owner_user_id = authenticated.claims().user_id().to_string(); let entries = state .spacetime_client() .list_custom_world_profiles(owner_user_id) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldLibraryResponse { entries: entries .into_iter() .map(map_custom_world_library_entry_response) .collect(), }, )) } pub async fn get_custom_world_library_detail( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, Path(profile_id): Path, ) -> Result, Response> { if profile_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-library", "message": "profileId is required", })), )); } let detail = state .spacetime_client() .get_custom_world_library_detail(authenticated.claims().user_id().to_string(), profile_id) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldGalleryDetailResponse { entry: map_custom_world_library_entry_response(detail.entry), }, )) } pub async fn put_custom_world_library_profile( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, Path(profile_id): Path, payload: Result, JsonRejection>, ) -> Result, Response> { let Json(payload) = payload.map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-library", "message": error.body_text(), })), ) })?; let owner_user_id = authenticated.claims().user_id().to_string(); if profile_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-library", "message": "profileId is required", })), )); } let metadata = extract_custom_world_metadata(&payload.profile).map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-library", "message": error, })), ) })?; let author_display_name = resolve_author_display_name(&authenticated); let mutation = state .spacetime_client() .upsert_custom_world_profile(CustomWorldProfileUpsertRecordInput { profile_id: profile_id.clone(), owner_user_id: owner_user_id.clone(), source_agent_session_id: None, world_name: metadata.world_name, subtitle: metadata.subtitle, summary_text: metadata.summary_text, theme_mode: metadata.theme_mode, cover_image_src: metadata.cover_image_src, profile_payload_json: serde_json::to_string(&payload.profile).map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-library", "message": format!("profile JSON 序列化失败:{error}"), })), ) })?, playable_npc_count: metadata.playable_npc_count, landmark_count: metadata.landmark_count, author_display_name, updated_at_micros: current_utc_micros(), }) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldLibraryMutationResponse { entry: map_custom_world_library_entry_response(mutation.entry.clone()), entries: vec![map_custom_world_library_entry_response(mutation.entry)], }, )) } pub async fn publish_custom_world_library_profile( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, Path(profile_id): Path, ) -> Result, Response> { let owner_user_id = authenticated.claims().user_id().to_string(); if profile_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-library", "message": "profileId is required", })), )); } let mutation = state .spacetime_client() .publish_custom_world_profile( profile_id, owner_user_id, resolve_author_display_name(&authenticated), current_utc_micros(), ) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldLibraryMutationResponse { entry: map_custom_world_library_entry_response(mutation.entry.clone()), entries: vec![map_custom_world_library_entry_response(mutation.entry)], }, )) } pub async fn unpublish_custom_world_library_profile( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, Path(profile_id): Path, ) -> Result, Response> { let owner_user_id = authenticated.claims().user_id().to_string(); if profile_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-library", "message": "profileId is required", })), )); } let mutation = state .spacetime_client() .unpublish_custom_world_profile( profile_id, owner_user_id, resolve_author_display_name(&authenticated), current_utc_micros(), ) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldLibraryMutationResponse { entry: map_custom_world_library_entry_response(mutation.entry.clone()), entries: vec![map_custom_world_library_entry_response(mutation.entry)], }, )) } pub async fn list_custom_world_gallery( State(state): State, Extension(request_context): Extension, ) -> Result, Response> { let entries = state .spacetime_client() .list_custom_world_gallery_entries() .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldGalleryResponse { entries: entries .into_iter() .map(map_custom_world_gallery_card_response) .collect(), }, )) } pub async fn get_custom_world_gallery_detail( State(state): State, Path((owner_user_id, profile_id)): Path<(String, String)>, Extension(request_context): Extension, ) -> Result, Response> { if owner_user_id.trim().is_empty() || profile_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-gallery", "message": "ownerUserId and profileId are required", })), )); } let detail = state .spacetime_client() .get_custom_world_gallery_detail(owner_user_id, profile_id) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldGalleryDetailResponse { entry: map_custom_world_library_entry_response(detail.entry), }, )) } pub async fn create_custom_world_agent_session( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, payload: Result, JsonRejection>, ) -> Result, Response> { let Json(payload) = payload.map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": error.body_text(), })), ) })?; let seed_text = payload.seed_text.unwrap_or_default().trim().to_string(); let welcome_message_text = build_custom_world_agent_welcome_text(&seed_text); let session = state .spacetime_client() .create_custom_world_agent_session(CustomWorldAgentSessionCreateRecordInput { session_id: build_prefixed_uuid_id("custom-world-agent-session-"), owner_user_id: authenticated.claims().user_id().to_string(), seed_text, welcome_message_id: build_prefixed_uuid_id("message-"), welcome_message_text, anchor_content_json: empty_agent_anchor_content_json(), creator_intent_json: Some(empty_json_object()), creator_intent_readiness_json: empty_agent_creator_intent_readiness_json(), anchor_pack_json: Some(empty_json_object()), lock_state_json: Some(empty_json_object()), draft_profile_json: Some(empty_json_object()), pending_clarifications_json: empty_json_array(), suggested_actions_json: empty_json_array(), recommended_replies_json: empty_json_array(), quality_findings_json: empty_json_array(), asset_coverage_json: empty_agent_asset_coverage_json(), checkpoints_json: empty_json_array(), created_at_micros: current_utc_micros(), }) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldAgentSessionResponse { session: map_custom_world_agent_session_response(session), }, )) } pub async fn get_custom_world_agent_session( State(state): State, Path(session_id): Path, Extension(request_context): Extension, Extension(authenticated): Extension, ) -> Result, Response> { if session_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "sessionId is required", })), )); } let session = state .spacetime_client() .get_custom_world_agent_session(session_id, authenticated.claims().user_id().to_string()) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), map_custom_world_agent_session_response(session), )) } pub async fn get_custom_world_works( State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, ) -> Result, Response> { let items = state .spacetime_client() .list_custom_world_works(authenticated.claims().user_id().to_string()) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldWorksResponse { items: items .into_iter() .map(map_custom_world_work_summary_response) .collect(), }, )) } pub async fn get_custom_world_agent_card_detail( State(state): State, Path((session_id, card_id)): Path<(String, String)>, Extension(request_context): Extension, Extension(authenticated): Extension, ) -> Result, Response> { if session_id.trim().is_empty() || card_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "sessionId and cardId are required", })), )); } let card = state .spacetime_client() .get_custom_world_agent_card_detail( session_id, authenticated.claims().user_id().to_string(), card_id, ) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), CustomWorldAgentCardDetailResponse { card: map_custom_world_draft_card_detail_response(card), }, )) } pub async fn submit_custom_world_agent_message( State(state): State, Path(session_id): Path, Extension(request_context): Extension, Extension(authenticated): Extension, payload: Result, JsonRejection>, ) -> Result, Response> { let Json(payload) = payload.map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": error.body_text(), })), ) })?; if session_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "sessionId is required", })), )); } let client_message_id = payload.client_message_id.trim().to_string(); let message_text = payload.text.trim().to_string(); if client_message_id.is_empty() || message_text.is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "clientMessageId and text are required", })), )); } let operation = state .spacetime_client() .submit_custom_world_agent_message(CustomWorldAgentMessageSubmitRecordInput { session_id, owner_user_id: authenticated.claims().user_id().to_string(), user_message_id: client_message_id, user_message_text: message_text, operation_id: build_prefixed_uuid_id("operation-"), submitted_at_micros: current_utc_micros(), }) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), json!({ "operation": map_custom_world_agent_operation_response(operation), }), )) } pub async fn stream_custom_world_agent_message( State(state): State, Path(session_id): Path, Extension(request_context): Extension, Extension(authenticated): Extension, payload: Result, JsonRejection>, ) -> Result { let Json(payload) = payload.map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": error.body_text(), })), ) })?; if session_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "sessionId is required", })), )); } let client_message_id = payload.client_message_id.trim().to_string(); let message_text = payload.text.trim().to_string(); if client_message_id.is_empty() || message_text.is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "clientMessageId and text are required", })), )); } let owner_user_id = authenticated.claims().user_id().to_string(); state .spacetime_client() .submit_custom_world_agent_message(CustomWorldAgentMessageSubmitRecordInput { session_id: session_id.clone(), owner_user_id: owner_user_id.clone(), user_message_id: client_message_id, user_message_text: message_text, operation_id: build_prefixed_uuid_id("operation-"), submitted_at_micros: current_utc_micros(), }) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; let session = state .spacetime_client() .get_custom_world_agent_session(session_id, owner_user_id) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; let session_response = map_custom_world_agent_session_response(session); let reply_text = resolve_stream_reply_text(&session_response); // 这里先用“一次性构造完整 SSE 文本”的最小兼容方案, // 复用 Stage 7 的同步 deterministic 写表逻辑,保证前端当前的 reader 协议可直接消费。 let mut sse = SseEventBuffer::new(); sse.push_json("reply_delta", &json!({ "text": reply_text })) .map_err(|error| custom_world_error_response(&request_context, error))?; sse.push_json("session", &json!({ "session": session_response })) .map_err(|error| custom_world_error_response(&request_context, error))?; sse.push_json("done", &json!({ "ok": true })) .map_err(|error| custom_world_error_response(&request_context, error))?; Ok(sse.into_response()) } pub async fn get_custom_world_agent_operation( State(state): State, Path((session_id, operation_id)): Path<(String, String)>, Extension(request_context): Extension, Extension(authenticated): Extension, ) -> Result, Response> { if session_id.trim().is_empty() || operation_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "sessionId and operationId are required", })), )); } let operation = state .spacetime_client() .get_custom_world_agent_operation( session_id, authenticated.claims().user_id().to_string(), operation_id, ) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), map_custom_world_agent_operation_response(operation), )) } pub async fn execute_custom_world_agent_action( State(state): State, Path(session_id): Path, Extension(request_context): Extension, Extension(authenticated): Extension, payload: Result, JsonRejection>, ) -> Result, Response> { let Json(payload) = payload.map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": error.body_text(), })), ) })?; if session_id.trim().is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "sessionId is required", })), )); } let action = payload.action.trim().to_string(); if action.is_empty() { return Err(custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": "action is required", })), )); } let payload_json = serde_json::to_string(&payload).map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", "message": format!("action payload JSON 序列化失败:{error}"), })), ) })?; let result = state .spacetime_client() .execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput { session_id, owner_user_id: authenticated.claims().user_id().to_string(), operation_id: build_prefixed_uuid_id("operation-"), action, payload_json: Some(payload_json), submitted_at_micros: current_utc_micros(), }) .await .map_err(|error| { custom_world_error_response(&request_context, map_custom_world_client_error(error)) })?; Ok(json_success_body( Some(&request_context), json!({ "operation": map_custom_world_agent_operation_response(result.operation), }), )) } fn map_custom_world_library_entry_response( entry: CustomWorldLibraryEntryRecord, ) -> CustomWorldLibraryEntryResponse { CustomWorldLibraryEntryResponse { owner_user_id: entry.owner_user_id, profile_id: entry.profile_id, profile: entry.profile, visibility: entry.visibility, published_at: entry.published_at, updated_at: entry.updated_at, author_display_name: entry.author_display_name, world_name: entry.world_name, subtitle: entry.subtitle, summary_text: entry.summary_text, cover_image_src: entry.cover_image_src, theme_mode: entry.theme_mode, playable_npc_count: entry.playable_npc_count, landmark_count: entry.landmark_count, } } fn map_custom_world_gallery_card_response( entry: CustomWorldGalleryEntryRecord, ) -> CustomWorldGalleryCardResponse { CustomWorldGalleryCardResponse { owner_user_id: entry.owner_user_id, profile_id: entry.profile_id, visibility: entry.visibility, published_at: entry.published_at, updated_at: entry.updated_at, author_display_name: entry.author_display_name, world_name: entry.world_name, subtitle: entry.subtitle, summary_text: entry.summary_text, cover_image_src: entry.cover_image_src, theme_mode: entry.theme_mode, playable_npc_count: entry.playable_npc_count, landmark_count: entry.landmark_count, } } fn map_custom_world_work_summary_response( item: CustomWorldWorkSummaryRecord, ) -> CustomWorldWorkSummaryResponse { CustomWorldWorkSummaryResponse { work_id: item.work_id, source_type: item.source_type, status: item.status, title: item.title, subtitle: item.subtitle, summary: item.summary, cover_image_src: item.cover_image_src, cover_render_mode: item.cover_render_mode, cover_character_image_srcs: item.cover_character_image_srcs, updated_at: item.updated_at, published_at: item.published_at, stage: item.stage, stage_label: item.stage_label, playable_npc_count: item.playable_npc_count, landmark_count: item.landmark_count, role_visual_ready_count: item.role_visual_ready_count, role_animation_ready_count: item.role_animation_ready_count, role_asset_summary_label: item.role_asset_summary_label, session_id: item.session_id, profile_id: item.profile_id, can_resume: item.can_resume, can_enter_world: item.can_enter_world, blocker_count: item.blocker_count, publish_ready: item.publish_ready, } } fn map_custom_world_agent_session_response( session: CustomWorldAgentSessionRecord, ) -> CustomWorldAgentSessionSnapshotResponse { CustomWorldAgentSessionSnapshotResponse { session_id: session.session_id, current_turn: session.current_turn, anchor_content: session.anchor_content, progress_percent: session.progress_percent, last_assistant_reply: session.last_assistant_reply, stage: session.stage, focus_card_id: session.focus_card_id, creator_intent: session.creator_intent, creator_intent_readiness: session.creator_intent_readiness, anchor_pack: session.anchor_pack, lock_state: session.lock_state, draft_profile: session.draft_profile, messages: session .messages .into_iter() .map(map_custom_world_agent_message_response) .collect(), draft_cards: session .draft_cards .into_iter() .map(map_custom_world_draft_card_response) .collect(), pending_clarifications: session.pending_clarifications, suggested_actions: session.suggested_actions, recommended_replies: session.recommended_replies, quality_findings: session.quality_findings, asset_coverage: session.asset_coverage, checkpoints: session .checkpoints .into_iter() .map(map_custom_world_agent_checkpoint_response) .collect(), supported_actions: session .supported_actions .into_iter() .map(map_custom_world_supported_action_response) .collect(), publish_gate: session .publish_gate .map(map_custom_world_publish_gate_response), result_preview: session.result_preview, updated_at: session.updated_at, } } fn map_custom_world_publish_gate_response( gate: CustomWorldPublishGateRecord, ) -> CustomWorldPublishGateResponse { CustomWorldPublishGateResponse { profile_id: gate.profile_id, blockers: gate .blockers .into_iter() .map(map_custom_world_result_preview_blocker_response) .collect(), blocker_count: gate.blocker_count, publish_ready: gate.publish_ready, can_enter_world: gate.can_enter_world, } } fn map_custom_world_agent_message_response( message: CustomWorldAgentMessageRecord, ) -> CustomWorldAgentMessageResponse { CustomWorldAgentMessageResponse { id: message.message_id, role: message.role, kind: message.kind, text: message.text, created_at: message.created_at, related_operation_id: message.related_operation_id, } } fn map_custom_world_agent_operation_response( operation: CustomWorldAgentOperationRecord, ) -> CustomWorldAgentOperationResponse { CustomWorldAgentOperationResponse { operation_id: operation.operation_id, operation_type: operation.operation_type, status: operation.status, phase_label: operation.phase_label, phase_detail: operation.phase_detail, progress: operation.progress, error: operation.error_message, } } fn map_custom_world_draft_card_response( card: CustomWorldDraftCardRecord, ) -> CustomWorldDraftCardSummaryResponse { CustomWorldDraftCardSummaryResponse { id: card.card_id, kind: card.kind, title: card.title, subtitle: card.subtitle, summary: card.summary, status: card.status, linked_ids: card.linked_ids, warning_count: card.warning_count, asset_status: card.asset_status, asset_status_label: card.asset_status_label, } } fn map_custom_world_draft_card_detail_response( card: CustomWorldDraftCardDetailRecord, ) -> CustomWorldDraftCardDetailResponse { CustomWorldDraftCardDetailResponse { id: card.card_id, kind: card.kind, title: card.title, sections: card .sections .into_iter() .map(map_custom_world_draft_card_detail_section_response) .collect(), linked_ids: card.linked_ids, locked: card.locked, editable: card.editable, editable_section_ids: card.editable_section_ids, warning_messages: card.warning_messages, asset_status: card.asset_status, asset_status_label: card.asset_status_label, } } fn map_custom_world_draft_card_detail_section_response( section: CustomWorldDraftCardDetailSectionRecord, ) -> CustomWorldDraftCardDetailSectionResponse { CustomWorldDraftCardDetailSectionResponse { id: section.section_id, label: section.label, value: section.value, } } fn map_custom_world_agent_checkpoint_response( checkpoint: CustomWorldAgentCheckpointRecord, ) -> CustomWorldAgentCheckpointResponse { CustomWorldAgentCheckpointResponse { checkpoint_id: checkpoint.checkpoint_id, created_at: checkpoint.created_at, label: checkpoint.label, } } fn map_custom_world_supported_action_response( action: CustomWorldSupportedActionRecord, ) -> CustomWorldSupportedActionResponse { CustomWorldSupportedActionResponse { action: action.action, enabled: action.enabled, reason: action.reason, } } fn map_custom_world_result_preview_blocker_response( blocker: CustomWorldResultPreviewBlockerRecord, ) -> CustomWorldResultPreviewBlockerResponse { CustomWorldResultPreviewBlockerResponse { id: blocker.id, code: blocker.code, message: blocker.message, } } fn resolve_stream_reply_text(session: &CustomWorldAgentSessionSnapshotResponse) -> String { session .last_assistant_reply .clone() .or_else(|| { session .messages .iter() .rev() .find(|message| message.role == "assistant") .map(|message| message.text.clone()) }) .unwrap_or_default() } fn map_custom_world_client_error(error: SpacetimeClientError) -> AppError { let status = match &error { SpacetimeClientError::Procedure(message) if message.contains("custom_world_profile 不存在") => { StatusCode::NOT_FOUND } SpacetimeClientError::Procedure(message) if message.contains("custom_world_agent_session 不存在") => { StatusCode::NOT_FOUND } SpacetimeClientError::Procedure(message) if message.contains("custom_world_agent_operation 不存在") => { StatusCode::NOT_FOUND } SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST, _ => StatusCode::BAD_GATEWAY, }; AppError::from_status(status).with_details(json!({ "provider": "spacetimedb", "message": error.to_string(), })) } fn custom_world_error_response(request_context: &RequestContext, error: AppError) -> Response { error.into_response_with_context(Some(request_context)) } fn resolve_author_display_name(_authenticated: &AuthenticatedAccessToken) -> String { "玩家".to_string() } fn build_custom_world_agent_welcome_text(seed_text: &str) -> String { if seed_text.trim().is_empty() { return "我会先帮你把世界的核心锚点整理出来。你可以从世界钩子、玩家身份、主题氛围、核心冲突、关键关系或标志性元素开始。" .to_string(); } "我已经收到你的世界起点,会先把它整理成创作锚点。你可以继续补充玩家身份、核心冲突、关键关系或标志性元素。".to_string() } struct CustomWorldProfileMetadata { world_name: String, subtitle: String, summary_text: String, cover_image_src: Option, theme_mode: CustomWorldThemeMode, playable_npc_count: u32, landmark_count: u32, } fn extract_custom_world_metadata(profile: &Value) -> Result { let object = profile .as_object() .ok_or_else(|| "profile 必须是 JSON object".to_string())?; let world_name = read_string_field(object, "name").unwrap_or_else(|| "未命名世界".to_string()); let subtitle = read_string_field(object, "subtitle").unwrap_or_default(); let summary_text = read_string_field(object, "summary").unwrap_or_default(); let cover_image_src = resolve_cover_image_src(object); let theme_mode = read_string_field(object, "themeMode") .and_then(|value| CustomWorldThemeMode::from_client_str(&value)) .unwrap_or(CustomWorldThemeMode::Mythic); let playable_npc_count = count_profile_roles(object); let landmark_count = object .get("landmarks") .and_then(Value::as_array) .map(|entries| entries.len() as u32) .unwrap_or(0); Ok(CustomWorldProfileMetadata { world_name, subtitle, summary_text, cover_image_src, theme_mode, playable_npc_count, landmark_count, }) } fn read_string_field(object: &Map, key: &str) -> Option { object .get(key) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn resolve_cover_image_src(object: &Map) -> Option { object .get("cover") .and_then(Value::as_object) .and_then(|cover| read_string_field(cover, "imageSrc")) .or_else(|| { object .get("camp") .and_then(Value::as_object) .and_then(|camp| read_string_field(camp, "imageSrc")) }) .or_else(|| { object .get("landmarks") .and_then(Value::as_array) .and_then(|entries| entries.first()) .and_then(Value::as_object) .and_then(|landmark| read_string_field(landmark, "imageSrc")) }) } fn count_profile_roles(object: &Map) -> u32 { let playable = object .get("playableNpcs") .and_then(Value::as_array) .map(|entries| entries.len() as u32) .unwrap_or(0); let story = object .get("storyNpcs") .and_then(Value::as_array) .map(|entries| entries.len() as u32) .unwrap_or(0); playable.saturating_add(story) } fn current_utc_micros() -> i64 { use std::time::{SystemTime, UNIX_EPOCH}; let duration = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system clock should be after unix epoch"); i64::try_from(duration.as_micros()).expect("current unix micros should fit in i64") }