use std::{error::Error, fmt}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; #[cfg(feature = "spacetime-types")] use spacetimedb::SpacetimeType; pub const MAX_PROGRESS_PERCENT: u32 = 100; #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CustomWorldPublicationStatus { Draft, Published, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CustomWorldThemeMode { Martial, Arcane, Machina, Tide, Rift, Mythic, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CustomWorldGenerationMode { Fast, Full, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CustomWorldSessionStatus { Clarifying, ReadyToGenerate, Generating, Completed, GenerationError, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RpgAgentStage { CollectingIntent, Clarifying, FoundationReview, ObjectRefining, VisualRefining, LongTailReview, ReadyToPublish, Published, Error, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RpgAgentMessageRole { User, Assistant, System, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RpgAgentMessageKind { Chat, Clarification, Summary, Checkpoint, Warning, ActionResult, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RpgAgentOperationType { ProcessMessage, DraftFoundation, UpdateDraftCard, SyncResultProfile, GenerateCharacters, GenerateLandmarks, DeleteCharacters, DeleteLandmarks, GenerateRoleAssets, SyncRoleAssets, GenerateSceneAssets, SyncSceneAssets, ExpandLongTail, PublishWorld, RevertCheckpoint, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RpgAgentOperationStatus { Queued, Running, Completed, Failed, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RpgAgentDraftCardKind { World, Camp, Faction, Character, Landmark, Thread, Chapter, SceneChapter, Carrier, SidequestSeed, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RpgAgentDraftCardStatus { Suggested, Confirmed, Locked, Warning, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CustomWorldRoleAssetStatus { Missing, VisualReady, AnimationsReady, Complete, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum CustomWorldFieldError { MissingProfileId, MissingSessionId, MissingOwnerUserId, MissingPublicWorkCode, MissingAction, MissingWorldName, MissingDraftProfileJson, MissingProfilePayloadJson, MissingSettingText, MissingQuestionSnapshotJson, MissingAnchorContentJson, MissingCreatorIntentReadinessJson, MissingAssetCoverageJson, MissingPendingClarificationsJson, MissingMessageId, MissingMessageText, MissingOperationId, MissingPhaseLabel, InvalidProgressPercent, MissingCardId, MissingCardTitle, MissingCardSummary, MissingLinkedIdsJson, MissingAuthorDisplayName, InvalidDraftProfileJson, InvalidLegacyResultProfileJson, InvalidJsonPayload, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfileSnapshot { pub profile_id: String, pub owner_user_id: String, pub public_work_code: Option, pub author_public_user_code: Option, pub source_agent_session_id: Option, pub publication_status: CustomWorldPublicationStatus, pub world_name: String, pub subtitle: String, pub summary_text: String, pub theme_mode: CustomWorldThemeMode, pub cover_image_src: Option, pub profile_payload_json: String, pub playable_npc_count: u32, pub landmark_count: u32, pub play_count: u32, pub remix_count: u32, pub like_count: u32, pub author_display_name: String, pub published_at_micros: Option, pub deleted_at_micros: Option, pub created_at_micros: i64, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldGalleryEntrySnapshot { pub profile_id: String, pub owner_user_id: String, pub public_work_code: String, pub author_public_user_code: String, pub author_display_name: String, pub world_name: String, pub subtitle: String, pub summary_text: String, pub cover_image_src: Option, pub theme_mode: CustomWorldThemeMode, pub playable_npc_count: u32, pub landmark_count: u32, pub play_count: u32, pub remix_count: u32, pub like_count: u32, pub published_at_micros: i64, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldLibraryMutationResult { pub ok: bool, pub entry: Option, pub gallery_entry: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfileListResult { pub ok: bool, pub entries: Vec, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldGalleryListResult { pub ok: bool, pub entries: Vec, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishBlockerSnapshot { pub blocker_id: String, pub code: String, pub message: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishGateSnapshot { pub profile_id: String, pub blockers: Vec, pub blocker_count: u32, pub publish_ready: bool, pub can_enter_world: bool, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldWorkSummarySnapshot { pub work_id: String, pub source_type: String, pub status: String, pub title: String, pub subtitle: String, pub summary: String, pub cover_image_src: Option, pub cover_render_mode: Option, pub cover_character_image_srcs_json: String, pub updated_at_micros: i64, pub published_at_micros: Option, pub stage: Option, pub stage_label: Option, pub playable_npc_count: u32, pub landmark_count: u32, pub role_visual_ready_count: Option, pub role_animation_ready_count: Option, pub role_asset_summary_label: Option, pub session_id: Option, pub profile_id: Option, pub can_resume: bool, pub can_enter_world: bool, pub blocker_count: u32, pub publish_ready: bool, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldWorksListResult { pub ok: bool, pub items: Vec, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentMessageSnapshot { pub message_id: String, pub session_id: String, pub role: RpgAgentMessageRole, pub kind: RpgAgentMessageKind, pub text: String, pub related_operation_id: Option, pub created_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentOperationSnapshot { pub operation_id: String, pub session_id: String, pub operation_type: RpgAgentOperationType, pub status: RpgAgentOperationStatus, pub phase_label: String, pub phase_detail: String, pub progress: u32, pub error_message: Option, pub created_at_micros: i64, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldDraftCardSnapshot { pub card_id: String, pub session_id: String, pub kind: RpgAgentDraftCardKind, pub status: RpgAgentDraftCardStatus, pub title: String, pub subtitle: String, pub summary: String, pub linked_ids_json: String, pub warning_count: u32, pub asset_status: Option, pub asset_status_label: Option, pub detail_payload_json: Option, pub created_at_micros: i64, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldDraftCardDetailSectionSnapshot { pub section_id: String, pub label: String, pub value: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldDraftCardDetailSnapshot { pub card_id: String, pub kind: RpgAgentDraftCardKind, pub title: String, pub sections: Vec, pub linked_ids_json: String, pub locked: bool, pub editable: bool, pub editable_section_ids_json: String, pub warning_messages_json: String, pub asset_status: Option, pub asset_status_label: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldDraftCardDetailResult { pub ok: bool, pub card: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentSessionSnapshot { pub session_id: String, pub owner_user_id: String, pub seed_text: String, pub current_turn: u32, pub progress_percent: u32, pub stage: RpgAgentStage, pub focus_card_id: Option, pub anchor_content_json: String, pub creator_intent_json: Option, pub creator_intent_readiness_json: String, pub anchor_pack_json: Option, pub lock_state_json: Option, pub draft_profile_json: Option, pub last_assistant_reply: Option, pub publish_gate_json: Option, pub result_preview_json: Option, pub pending_clarifications_json: String, pub quality_findings_json: String, pub suggested_actions_json: String, pub recommended_replies_json: String, pub asset_coverage_json: String, pub checkpoints_json: String, pub supported_actions_json: String, pub messages: Vec, pub draft_cards: Vec, pub operations: Vec, pub created_at_micros: i64, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentSessionProcedureResult { pub ok: bool, pub session: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfileUpsertInput { pub profile_id: String, pub owner_user_id: String, pub public_work_code: Option, pub author_public_user_code: Option, pub source_agent_session_id: Option, pub world_name: String, pub subtitle: String, pub summary_text: String, pub theme_mode: CustomWorldThemeMode, pub cover_image_src: Option, pub profile_payload_json: String, pub playable_npc_count: u32, pub landmark_count: u32, pub author_display_name: String, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfilePublishInput { pub profile_id: String, pub owner_user_id: String, pub public_work_code: Option, pub author_public_user_code: String, pub author_display_name: String, pub published_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfileUnpublishInput { pub profile_id: String, pub owner_user_id: String, pub author_display_name: String, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfileDeleteInput { pub profile_id: String, pub owner_user_id: String, pub deleted_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfileListInput { pub owner_user_id: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldLibraryDetailInput { pub owner_user_id: String, pub profile_id: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldGalleryDetailInput { pub owner_user_id: String, pub profile_id: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldGalleryDetailByCodeInput { pub public_work_code: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfileRemixInput { pub source_owner_user_id: String, pub source_profile_id: String, pub target_owner_user_id: String, pub target_profile_id: String, pub author_display_name: String, pub remixed_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldProfilePlayRecordInput { pub owner_user_id: String, pub profile_id: String, pub played_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentSessionCreateInput { pub session_id: String, pub owner_user_id: String, pub seed_text: String, pub welcome_message_id: String, pub welcome_message_text: String, pub anchor_content_json: String, pub creator_intent_json: Option, pub creator_intent_readiness_json: String, pub anchor_pack_json: Option, pub lock_state_json: Option, pub draft_profile_json: Option, pub pending_clarifications_json: String, pub suggested_actions_json: String, pub recommended_replies_json: String, pub quality_findings_json: String, pub asset_coverage_json: String, pub checkpoints_json: String, pub created_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentSessionGetInput { pub session_id: String, pub owner_user_id: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentMessageSubmitInput { pub session_id: String, pub owner_user_id: String, pub user_message_id: String, pub user_message_text: String, pub operation_id: String, pub submitted_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentMessageFinalizeInput { pub session_id: String, pub owner_user_id: String, pub operation_id: String, pub assistant_message_id: Option, pub assistant_reply_text: Option, pub phase_label: String, pub phase_detail: String, pub operation_status: RpgAgentOperationStatus, pub operation_progress: u32, pub stage: RpgAgentStage, pub progress_percent: u32, pub focus_card_id: Option, pub anchor_content_json: String, pub creator_intent_json: Option, pub creator_intent_readiness_json: String, pub anchor_pack_json: Option, pub draft_profile_json: Option, pub pending_clarifications_json: String, pub suggested_actions_json: String, pub recommended_replies_json: String, pub quality_findings_json: String, pub asset_coverage_json: String, pub error_message: Option, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentOperationGetInput { pub session_id: String, pub owner_user_id: String, pub operation_id: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentOperationProgressInput { pub session_id: String, pub owner_user_id: String, pub operation_id: String, pub operation_type: RpgAgentOperationType, pub operation_status: RpgAgentOperationStatus, pub phase_label: String, pub phase_detail: String, pub operation_progress: u32, pub error_message: Option, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentOperationProcedureResult { pub ok: bool, pub operation: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldWorksListInput { pub owner_user_id: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentCardDetailGetInput { pub session_id: String, pub owner_user_id: String, pub card_id: String, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentActionExecuteInput { pub session_id: String, pub owner_user_id: String, pub operation_id: String, pub action: String, pub payload_json: Option, pub submitted_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldAgentActionExecuteResult { pub ok: bool, pub operation: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishedProfileCompileInput { pub session_id: String, pub profile_id: String, pub owner_user_id: String, pub draft_profile_json: String, pub legacy_result_profile_json: Option, pub setting_text: String, pub author_display_name: String, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishedProfileCompileSnapshot { pub profile_id: String, pub owner_user_id: String, pub world_name: String, pub subtitle: String, pub summary_text: String, pub theme_mode: CustomWorldThemeMode, pub cover_image_src: Option, pub playable_npc_count: u32, pub landmark_count: u32, pub author_display_name: String, pub compiled_profile_payload_json: String, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishedProfileCompileResult { pub ok: bool, pub record: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishWorldInput { pub session_id: String, pub profile_id: String, pub owner_user_id: String, pub public_work_code: Option, pub author_public_user_code: String, pub draft_profile_json: String, pub legacy_result_profile_json: Option, pub setting_text: String, pub author_display_name: String, pub published_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishWorldResult { pub ok: bool, pub compiled_record: Option, pub entry: Option, pub gallery_entry: Option, pub session_stage: Option, pub error_message: Option, } impl CustomWorldPublicationStatus { pub fn as_str(&self) -> &'static str { match self { Self::Draft => "draft", Self::Published => "published", } } } impl CustomWorldThemeMode { pub fn as_str(&self) -> &'static str { match self { Self::Martial => "martial", Self::Arcane => "arcane", Self::Machina => "machina", Self::Tide => "tide", Self::Rift => "rift", Self::Mythic => "mythic", } } pub fn from_client_str(value: &str) -> Option { match value.trim().to_ascii_lowercase().as_str() { "martial" => Some(Self::Martial), "arcane" => Some(Self::Arcane), "machina" => Some(Self::Machina), "tide" => Some(Self::Tide), "rift" => Some(Self::Rift), "mythic" => Some(Self::Mythic), _ => None, } } } impl CustomWorldGenerationMode { pub fn as_str(&self) -> &'static str { match self { Self::Fast => "fast", Self::Full => "full", } } } impl CustomWorldSessionStatus { pub fn as_str(&self) -> &'static str { match self { Self::Clarifying => "clarifying", Self::ReadyToGenerate => "ready_to_generate", Self::Generating => "generating", Self::Completed => "completed", Self::GenerationError => "generation_error", } } } impl RpgAgentStage { pub fn as_str(&self) -> &'static str { match self { Self::CollectingIntent => "collecting_intent", Self::Clarifying => "clarifying", Self::FoundationReview => "foundation_review", Self::ObjectRefining => "object_refining", Self::VisualRefining => "visual_refining", Self::LongTailReview => "long_tail_review", Self::ReadyToPublish => "ready_to_publish", Self::Published => "published", Self::Error => "error", } } } impl RpgAgentMessageRole { pub fn as_str(&self) -> &'static str { match self { Self::User => "user", Self::Assistant => "assistant", Self::System => "system", } } } impl RpgAgentMessageKind { pub fn as_str(&self) -> &'static str { match self { Self::Chat => "chat", Self::Clarification => "clarification", Self::Summary => "summary", Self::Checkpoint => "checkpoint", Self::Warning => "warning", Self::ActionResult => "action_result", } } } impl RpgAgentOperationType { pub fn as_str(&self) -> &'static str { match self { Self::ProcessMessage => "process_message", Self::DraftFoundation => "draft_foundation", Self::UpdateDraftCard => "update_draft_card", Self::SyncResultProfile => "sync_result_profile", Self::GenerateCharacters => "generate_characters", Self::GenerateLandmarks => "generate_landmarks", Self::DeleteCharacters => "delete_characters", Self::DeleteLandmarks => "delete_landmarks", Self::GenerateRoleAssets => "generate_role_assets", Self::SyncRoleAssets => "sync_role_assets", Self::GenerateSceneAssets => "generate_scene_assets", Self::SyncSceneAssets => "sync_scene_assets", Self::ExpandLongTail => "expand_long_tail", Self::PublishWorld => "publish_world", Self::RevertCheckpoint => "revert_checkpoint", } } } impl RpgAgentOperationStatus { pub fn as_str(&self) -> &'static str { match self { Self::Queued => "queued", Self::Running => "running", Self::Completed => "completed", Self::Failed => "failed", } } } impl RpgAgentDraftCardKind { pub fn as_str(&self) -> &'static str { match self { Self::World => "world", Self::Camp => "camp", Self::Faction => "faction", Self::Character => "character", Self::Landmark => "landmark", Self::Thread => "thread", Self::Chapter => "chapter", Self::SceneChapter => "scene_chapter", Self::Carrier => "carrier", Self::SidequestSeed => "sidequest_seed", } } } impl RpgAgentDraftCardStatus { pub fn as_str(&self) -> &'static str { match self { Self::Suggested => "suggested", Self::Confirmed => "confirmed", Self::Locked => "locked", Self::Warning => "warning", } } } impl CustomWorldRoleAssetStatus { pub fn as_str(&self) -> &'static str { match self { Self::Missing => "missing", Self::VisualReady => "visual_ready", Self::AnimationsReady => "animations_ready", Self::Complete => "complete", } } } pub fn validate_custom_world_profile_fields( profile_id: &str, owner_user_id: &str, world_name: &str, profile_payload_json: &str, ) -> Result<(), CustomWorldFieldError> { if profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if world_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingWorldName); } if profile_payload_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfilePayloadJson); } Ok(()) } pub fn validate_custom_world_published_profile_compile_input( input: &CustomWorldPublishedProfileCompileInput, ) -> Result<(), CustomWorldFieldError> { if input.session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.draft_profile_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingDraftProfileJson); } if input.setting_text.trim().is_empty() { return Err(CustomWorldFieldError::MissingSettingText); } if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } Ok(()) } pub fn validate_custom_world_publish_world_input( input: &CustomWorldPublishWorldInput, ) -> Result<(), CustomWorldFieldError> { if input.author_public_user_code.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } validate_custom_world_published_profile_compile_input( &CustomWorldPublishedProfileCompileInput { session_id: input.session_id.clone(), profile_id: input.profile_id.clone(), owner_user_id: input.owner_user_id.clone(), draft_profile_json: input.draft_profile_json.clone(), legacy_result_profile_json: input.legacy_result_profile_json.clone(), setting_text: input.setting_text.clone(), author_display_name: input.author_display_name.clone(), updated_at_micros: input.published_at_micros, }, ) } pub fn validate_custom_world_profile_upsert_input( input: &CustomWorldProfileUpsertInput, ) -> Result<(), CustomWorldFieldError> { validate_custom_world_profile_fields( &input.profile_id, &input.owner_user_id, &input.world_name, &input.profile_payload_json, )?; if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } Ok(()) } pub fn validate_custom_world_profile_publish_input( input: &CustomWorldProfilePublishInput, ) -> Result<(), CustomWorldFieldError> { if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } if input.author_public_user_code.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_profile_unpublish_input( input: &CustomWorldProfileUnpublishInput, ) -> Result<(), CustomWorldFieldError> { if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } Ok(()) } pub fn validate_custom_world_profile_delete_input( input: &CustomWorldProfileDeleteInput, ) -> Result<(), CustomWorldFieldError> { if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_profile_list_input( input: &CustomWorldProfileListInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_library_detail_input( input: &CustomWorldLibraryDetailInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } Ok(()) } pub fn validate_custom_world_gallery_detail_input( input: &CustomWorldGalleryDetailInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } Ok(()) } pub fn validate_custom_world_gallery_detail_by_code_input( input: &CustomWorldGalleryDetailByCodeInput, ) -> Result<(), CustomWorldFieldError> { if input.public_work_code.trim().is_empty() { return Err(CustomWorldFieldError::MissingPublicWorkCode); } Ok(()) } pub fn validate_custom_world_session_fields( session_id: &str, owner_user_id: &str, setting_text: &str, question_snapshot_json: &str, ) -> Result<(), CustomWorldFieldError> { if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if setting_text.trim().is_empty() { return Err(CustomWorldFieldError::MissingSettingText); } if question_snapshot_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingQuestionSnapshotJson); } Ok(()) } pub fn validate_custom_world_agent_session_fields( session_id: &str, owner_user_id: &str, anchor_content_json: &str, creator_intent_readiness_json: &str, pending_clarifications_json: &str, asset_coverage_json: &str, progress_percent: u32, ) -> Result<(), CustomWorldFieldError> { if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if anchor_content_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingAnchorContentJson); } if creator_intent_readiness_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingCreatorIntentReadinessJson); } if pending_clarifications_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingPendingClarificationsJson); } if asset_coverage_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingAssetCoverageJson); } if progress_percent > MAX_PROGRESS_PERCENT { return Err(CustomWorldFieldError::InvalidProgressPercent); } Ok(()) } pub fn validate_custom_world_agent_session_create_input( input: &CustomWorldAgentSessionCreateInput, ) -> Result<(), CustomWorldFieldError> { validate_custom_world_agent_session_fields( &input.session_id, &input.owner_user_id, &input.anchor_content_json, &input.creator_intent_readiness_json, &input.pending_clarifications_json, &input.asset_coverage_json, 0, )?; validate_custom_world_agent_message_fields( &input.welcome_message_id, &input.session_id, &input.welcome_message_text, )?; ensure_json_object(&input.anchor_content_json)?; ensure_optional_json_object(input.creator_intent_json.as_deref())?; ensure_json_object(&input.creator_intent_readiness_json)?; ensure_optional_json_object(input.anchor_pack_json.as_deref())?; ensure_optional_json_object(input.lock_state_json.as_deref())?; ensure_optional_json_object(input.draft_profile_json.as_deref())?; ensure_json_array(&input.pending_clarifications_json)?; ensure_json_array(&input.suggested_actions_json)?; ensure_json_array(&input.recommended_replies_json)?; ensure_json_array(&input.quality_findings_json)?; ensure_json_object(&input.asset_coverage_json)?; ensure_json_array(&input.checkpoints_json)?; Ok(()) } pub fn validate_custom_world_agent_session_get_input( input: &CustomWorldAgentSessionGetInput, ) -> Result<(), CustomWorldFieldError> { if input.session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_agent_message_submit_input( input: &CustomWorldAgentMessageSubmitInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } validate_custom_world_agent_message_fields( &input.user_message_id, &input.session_id, &input.user_message_text, )?; validate_custom_world_agent_operation_fields( &input.operation_id, &input.session_id, "消息已处理", MAX_PROGRESS_PERCENT, )?; Ok(()) } pub fn validate_custom_world_agent_message_finalize_input( input: &CustomWorldAgentMessageFinalizeInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } match input.operation_status { RpgAgentOperationStatus::Completed => { validate_custom_world_agent_message_fields( input.assistant_message_id.as_deref().unwrap_or_default(), &input.session_id, input.assistant_reply_text.as_deref().unwrap_or_default(), )?; } RpgAgentOperationStatus::Failed => {} _ => { validate_custom_world_agent_message_fields( input.assistant_message_id.as_deref().unwrap_or_default(), &input.session_id, input.assistant_reply_text.as_deref().unwrap_or_default(), )?; } } validate_custom_world_agent_operation_fields( &input.operation_id, &input.session_id, &input.phase_label, input.operation_progress, )?; validate_custom_world_agent_session_fields( &input.session_id, &input.owner_user_id, &input.anchor_content_json, &input.creator_intent_readiness_json, &input.pending_clarifications_json, &input.asset_coverage_json, input.progress_percent, )?; ensure_json_object(&input.anchor_content_json)?; ensure_optional_json_object(input.creator_intent_json.as_deref())?; ensure_json_object(&input.creator_intent_readiness_json)?; ensure_optional_json_object(input.anchor_pack_json.as_deref())?; ensure_optional_json_object(input.draft_profile_json.as_deref())?; ensure_json_array(&input.pending_clarifications_json)?; ensure_json_array(&input.suggested_actions_json)?; ensure_json_array(&input.recommended_replies_json)?; ensure_json_array(&input.quality_findings_json)?; ensure_json_object(&input.asset_coverage_json)?; Ok(()) } pub fn validate_custom_world_agent_operation_get_input( input: &CustomWorldAgentOperationGetInput, ) -> Result<(), CustomWorldFieldError> { if input.session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.operation_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOperationId); } Ok(()) } pub fn validate_custom_world_agent_operation_progress_input( input: &CustomWorldAgentOperationProgressInput, ) -> Result<(), CustomWorldFieldError> { validate_custom_world_agent_operation_get_input(&CustomWorldAgentOperationGetInput { session_id: input.session_id.clone(), owner_user_id: input.owner_user_id.clone(), operation_id: input.operation_id.clone(), })?; validate_custom_world_agent_operation_fields( &input.operation_id, &input.session_id, &input.phase_label, input.operation_progress, )?; Ok(()) } pub fn validate_custom_world_works_list_input( input: &CustomWorldWorksListInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_agent_card_detail_get_input( input: &CustomWorldAgentCardDetailGetInput, ) -> Result<(), CustomWorldFieldError> { if input.session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.card_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingCardId); } Ok(()) } pub fn validate_custom_world_agent_action_execute_input( input: &CustomWorldAgentActionExecuteInput, ) -> Result<(), CustomWorldFieldError> { validate_custom_world_agent_operation_get_input(&CustomWorldAgentOperationGetInput { session_id: input.session_id.clone(), owner_user_id: input.owner_user_id.clone(), operation_id: input.operation_id.clone(), })?; if input.action.trim().is_empty() { return Err(CustomWorldFieldError::MissingAction); } ensure_optional_json_object(input.payload_json.as_deref())?; Ok(()) } pub fn validate_custom_world_agent_message_fields( message_id: &str, session_id: &str, text: &str, ) -> Result<(), CustomWorldFieldError> { if message_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingMessageId); } if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if text.trim().is_empty() { return Err(CustomWorldFieldError::MissingMessageText); } Ok(()) } pub fn validate_custom_world_agent_operation_fields( operation_id: &str, session_id: &str, phase_label: &str, progress: u32, ) -> Result<(), CustomWorldFieldError> { if operation_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOperationId); } if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if phase_label.trim().is_empty() { return Err(CustomWorldFieldError::MissingPhaseLabel); } if progress > MAX_PROGRESS_PERCENT { return Err(CustomWorldFieldError::InvalidProgressPercent); } Ok(()) } pub fn validate_custom_world_draft_card_fields( card_id: &str, session_id: &str, title: &str, summary: &str, linked_ids_json: &str, ) -> Result<(), CustomWorldFieldError> { if card_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingCardId); } if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if title.trim().is_empty() { return Err(CustomWorldFieldError::MissingCardTitle); } if summary.trim().is_empty() { return Err(CustomWorldFieldError::MissingCardSummary); } if linked_ids_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingLinkedIdsJson); } Ok(()) } pub fn validate_custom_world_gallery_entry_fields( profile_id: &str, owner_user_id: &str, author_display_name: &str, world_name: &str, ) -> Result<(), CustomWorldFieldError> { if profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } if world_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingWorldName); } Ok(()) } pub fn build_custom_world_published_profile_compile_snapshot( input: CustomWorldPublishedProfileCompileInput, ) -> Result { validate_custom_world_published_profile_compile_input(&input)?; let draft = parse_required_json_object( &input.draft_profile_json, CustomWorldFieldError::InvalidDraftProfileJson, )?; let legacy = parse_optional_json_object( input.legacy_result_profile_json.clone(), CustomWorldFieldError::InvalidLegacyResultProfileJson, )?; let world_name = resolve_text_field(&draft, &legacy, "name") .ok_or(CustomWorldFieldError::MissingWorldName)?; let subtitle = resolve_text_field(&draft, &legacy, "subtitle").unwrap_or_default(); let summary_text = resolve_text_field(&draft, &legacy, "summary").unwrap_or_default(); let cover_image_src = resolve_cover_image_src(&draft, &legacy); let theme_mode = resolve_theme_mode(&legacy); let playable_npc_count = count_distinct_roles(draft.get("playableNpcs"), draft.get("storyNpcs")); let landmark_count = to_array(draft.get("landmarks")).len() as u32; let compiled_payload_json = build_compiled_profile_payload_json( &input, &draft, &legacy, &world_name, &subtitle, &summary_text, )?; Ok(CustomWorldPublishedProfileCompileSnapshot { profile_id: input.profile_id, owner_user_id: input.owner_user_id, world_name, subtitle, summary_text, theme_mode, cover_image_src, playable_npc_count, landmark_count, author_display_name: input.author_display_name, compiled_profile_payload_json: compiled_payload_json, updated_at_micros: input.updated_at_micros, }) } pub fn canonicalize_custom_world_profile_before_save(profile: &mut Value) -> bool { let Some(object) = profile.as_object_mut() else { return false; }; let foundation_text = build_creator_intent_foundation_text(object.get("creatorIntent")) .trim() .to_string(); if foundation_text.is_empty() { return false; } let current_setting_text = object .get("settingText") .and_then(Value::as_str) .map(str::trim) .unwrap_or_default(); if current_setting_text == foundation_text { return false; } // 中文注释:保存与 session 同步前统一以后端 creatorIntent 锚点重建 settingText, // 避免浏览器继续持有正式 profile canonicalize 规则。 object.insert("settingText".to_string(), Value::String(foundation_text)); true } pub fn empty_agent_anchor_content_json() -> String { r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":null,"hiddenLines":null,"iconicElements":null}"#.to_string() } pub fn empty_agent_creator_intent_readiness_json() -> String { r#"{"isReady":false,"completedKeys":[],"missingKeys":[]}"#.to_string() } pub fn empty_agent_asset_coverage_json() -> String { r#"{"roleAssets":[],"sceneAssets":[],"allRoleAssetsReady":false,"allSceneAssetsReady":false}"# .to_string() } pub fn empty_json_object() -> String { "{}".to_string() } pub fn empty_json_array() -> String { "[]".to_string() } pub fn normalize_optional_json_slice(value: Option) -> Option { value.and_then(|value| { let value = value.trim().to_string(); if value.is_empty() { None } else { Some(value) } }) } fn ensure_json_object(value: &str) -> Result<(), CustomWorldFieldError> { match serde_json::from_str::(value) { Ok(Value::Object(_)) => Ok(()), _ => Err(CustomWorldFieldError::InvalidJsonPayload), } } fn ensure_optional_json_object(value: Option<&str>) -> Result<(), CustomWorldFieldError> { match value.map(str::trim).filter(|value| !value.is_empty()) { Some(value) => ensure_json_object(value), None => Ok(()), } } fn ensure_json_array(value: &str) -> Result<(), CustomWorldFieldError> { match serde_json::from_str::(value) { Ok(Value::Array(_)) => Ok(()), _ => Err(CustomWorldFieldError::InvalidJsonPayload), } } fn parse_required_json_object( value: &str, error: CustomWorldFieldError, ) -> Result, CustomWorldFieldError> { match serde_json::from_str::(value) { Ok(Value::Object(object)) => Ok(object), _ => Err(error), } } fn parse_optional_json_object( value: Option, error: CustomWorldFieldError, ) -> Result, CustomWorldFieldError> { match normalize_optional_json_slice(value) { Some(value) => parse_required_json_object(&value, error), None => Ok(Map::new()), } } fn to_text(value: Option<&Value>) -> Option { match value { Some(Value::String(value)) => { let trimmed = value.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } } _ => None, } } fn to_array(value: Option<&Value>) -> Vec { match value { Some(Value::Array(items)) => items.clone(), _ => Vec::new(), } } fn to_object(value: Option<&Value>) -> Option> { match value { Some(Value::Object(object)) => Some(object.clone()), _ => None, } } fn build_creator_intent_foundation_text(value: Option<&Value>) -> String { let Some(intent) = value.and_then(Value::as_object) else { return String::new(); }; if !has_meaningful_creator_intent(intent) { return String::new(); } let relationship_text = intent .get("keyCharacters") .and_then(Value::as_array) .and_then(|items| items.first()) .and_then(Value::as_object) .map(build_creator_intent_relationship_text) .unwrap_or_default(); let player_opening_text = [ read_text(intent, "playerPremise"), read_text(intent, "openingSituation"), ] .into_iter() .flatten() .collect::>() .join(";"); let theme_tone_text = [ read_string_list(intent, "themeKeywords").join("、"), read_string_list(intent, "toneDirectives").join("、"), ] .into_iter() .filter(|value| !value.is_empty()) .collect::>() .join(" / "); [ build_anchor_line( "世界一句话", read_text(intent, "worldHook").unwrap_or_default(), ), build_anchor_line("玩家开局", player_opening_text), build_anchor_line("主题气质", theme_tone_text), build_anchor_line( "核心冲突", read_string_list(intent, "coreConflicts").join(";"), ), build_anchor_line("关键关系", relationship_text), build_anchor_line( "标志元素", read_string_list(intent, "iconicElements").join("、"), ), ] .into_iter() .filter(|value| !value.is_empty()) .collect::>() .join("\n") } fn has_meaningful_creator_intent(intent: &Map) -> bool { [ "rawSettingText", "worldHook", "playerPremise", "openingSituation", ] .iter() .any(|key| read_text(intent, key).is_some()) || [ "themeKeywords", "toneDirectives", "coreConflicts", "iconicElements", "forbiddenDirectives", ] .iter() .any(|key| !read_string_list(intent, key).is_empty()) || ["keyFactions", "keyCharacters", "keyLandmarks"] .iter() .any(|key| has_meaningful_creator_seed_array(intent.get(*key))) } fn build_creator_intent_relationship_text(character: &Map) -> String { [ read_text(character, "name"), read_text(character, "role"), read_text(character, "relationToPlayer").map(|value| format!("与玩家 {value}")), read_text(character, "hiddenHook").map(|value| format!("暗线 {value}")), ] .into_iter() .flatten() .collect::>() .join(" · ") } fn build_anchor_line(label: &str, content: String) -> String { if content.is_empty() { String::new() } else { format!("{label}:{content}") } } fn read_text(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 read_string_list(object: &Map, key: &str) -> Vec { object .get(key) .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .collect::>() }) .unwrap_or_default() } fn has_meaningful_creator_seed_array(value: Option<&Value>) -> bool { value.and_then(Value::as_array).is_some_and(|items| { items.iter().any(|item| { item.as_object().is_some_and(|object| { [ "name", "publicGoal", "tension", "notes", "role", "publicMask", "hiddenHook", "relationToPlayer", "purpose", "mood", "secret", ] .iter() .any(|key| read_text(object, key).is_some()) }) }) }) } fn resolve_text_field( draft: &Map, legacy: &Map, key: &str, ) -> Option { to_text(draft.get(key)).or_else(|| to_text(legacy.get(key))) } fn resolve_theme_mode(legacy: &Map) -> CustomWorldThemeMode { to_text(legacy.get("themeMode")) .and_then(|value| CustomWorldThemeMode::from_client_str(&value)) .unwrap_or(CustomWorldThemeMode::Mythic) } fn resolve_cover_image_src( draft: &Map, legacy: &Map, ) -> Option { if let Some(camp) = to_object(draft.get("camp")) { if let Some(image_src) = to_text(camp.get("imageSrc")) { return Some(image_src); } } for landmark in to_array(draft.get("landmarks")) { if let Value::Object(landmark) = landmark { if let Some(image_src) = to_text(landmark.get("imageSrc")) { return Some(image_src); } } } if let Some(cover) = to_object(legacy.get("cover")) { if let Some(image_src) = to_text(cover.get("imageSrc")) { return Some(image_src); } } to_text(legacy.get("coverImageSrc")) } fn count_distinct_roles(playable: Option<&Value>, story: Option<&Value>) -> u32 { let mut seen = std::collections::BTreeSet::new(); for role in to_array(playable).into_iter().chain(to_array(story)) { if let Value::Object(role) = role { let key = to_text(role.get("id")) .or_else(|| to_text(role.get("name"))) .unwrap_or_else(|| format!("role-{}", seen.len())); seen.insert(key); } } seen.len() as u32 } fn build_compiled_profile_payload_json( input: &CustomWorldPublishedProfileCompileInput, draft: &Map, legacy: &Map, world_name: &str, subtitle: &str, summary_text: &str, ) -> Result { let mut payload = legacy.clone(); payload.insert("id".to_string(), Value::String(input.profile_id.clone())); payload.insert( "settingText".to_string(), Value::String(input.setting_text.trim().to_string()), ); payload.insert("name".to_string(), Value::String(world_name.to_string())); payload.insert("subtitle".to_string(), Value::String(subtitle.to_string())); payload.insert( "summary".to_string(), Value::String(summary_text.to_string()), ); payload.insert( "updatedAtMicros".to_string(), Value::Number(input.updated_at_micros.into()), ); for key in ["tone", "playerGoal"] { if let Some(value) = draft.get(key) { payload.insert(key.to_string(), value.clone()); } } for key in [ "majorFactions", "coreConflicts", "playableNpcs", "storyNpcs", "landmarks", "camp", ] { if let Some(value) = draft.get(key) { payload.insert(key.to_string(), value.clone()); } } if let Some(scene_chapters) = draft .get("sceneChapterBlueprints") .or_else(|| draft.get("sceneChapters")) { payload.insert("sceneChapterBlueprints".to_string(), scene_chapters.clone()); } serde_json::to_string(&Value::Object(payload)) .map_err(|_| CustomWorldFieldError::InvalidDraftProfileJson) } impl fmt::Display for CustomWorldFieldError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::MissingProfileId => f.write_str("custom_world.profile_id 不能为空"), Self::MissingSessionId => f.write_str("custom_world.session_id 不能为空"), Self::MissingOwnerUserId => f.write_str("custom_world.owner_user_id 不能为空"), Self::MissingPublicWorkCode => { f.write_str("custom_world_gallery_detail.public_work_code 不能为空") } Self::MissingAction => f.write_str("custom_world_agent_action.action 不能为空"), Self::MissingWorldName => f.write_str("custom_world.world_name 不能为空"), Self::MissingDraftProfileJson => { f.write_str("custom_world.compile.draft_profile_json 不能为空") } Self::MissingProfilePayloadJson => { f.write_str("custom_world.profile_payload_json 不能为空") } Self::MissingSettingText => f.write_str("custom_world.setting_text 不能为空"), Self::MissingQuestionSnapshotJson => { f.write_str("custom_world.question_snapshot_json 不能为空") } Self::MissingAnchorContentJson => { f.write_str("custom_world.anchor_content_json 不能为空") } Self::MissingCreatorIntentReadinessJson => { f.write_str("custom_world.creator_intent_readiness_json 不能为空") } Self::MissingAssetCoverageJson => { f.write_str("custom_world.asset_coverage_json 不能为空") } Self::MissingPendingClarificationsJson => { f.write_str("custom_world.pending_clarifications_json 不能为空") } Self::MissingMessageId => f.write_str("custom_world_agent_message.message_id 不能为空"), Self::MissingMessageText => f.write_str("custom_world_agent_message.text 不能为空"), Self::MissingOperationId => { f.write_str("custom_world_agent_operation.operation_id 不能为空") } Self::MissingPhaseLabel => { f.write_str("custom_world_agent_operation.phase_label 不能为空") } Self::InvalidProgressPercent => f.write_str("progress 必须位于 0~100"), Self::MissingCardId => f.write_str("custom_world_draft_card.card_id 不能为空"), Self::MissingCardTitle => f.write_str("custom_world_draft_card.title 不能为空"), Self::MissingCardSummary => f.write_str("custom_world_draft_card.summary 不能为空"), Self::MissingLinkedIdsJson => { f.write_str("custom_world_draft_card.linked_ids_json 不能为空") } Self::MissingAuthorDisplayName => { f.write_str("custom_world_gallery_entry.author_display_name 不能为空") } Self::InvalidDraftProfileJson => { f.write_str("custom_world.compile.draft_profile_json 不是合法 JSON object") } Self::InvalidLegacyResultProfileJson => { f.write_str("custom_world.compile.legacy_result_profile_json 不是合法 JSON object") } Self::InvalidJsonPayload => f.write_str("custom_world JSON payload 结构非法"), } } } impl Error for CustomWorldFieldError {} #[cfg(test)] mod tests { use super::*; #[test] fn profile_validation_rejects_blank_owner() { let error = validate_custom_world_profile_fields( "cwprof_001", " ", "裂潮边城", "{\"id\":\"cwprof_001\"}", ) .expect_err("blank owner should fail"); assert_eq!(error, CustomWorldFieldError::MissingOwnerUserId); } #[test] fn agent_session_validation_rejects_progress_over_hundred() { let error = validate_custom_world_agent_session_fields( "custom-world-agent-session-001", "user_001", "{}", "{}", "[]", "{}", 101, ) .expect_err("progress greater than 100 should fail"); assert_eq!(error, CustomWorldFieldError::InvalidProgressPercent); } #[test] fn enum_string_values_match_current_contract() { assert_eq!( RpgAgentOperationType::PublishWorld.as_str(), "publish_world" ); assert_eq!(RpgAgentStage::ReadyToPublish.as_str(), "ready_to_publish"); assert_eq!(RpgAgentMessageRole::Assistant.as_str(), "assistant"); assert_eq!(RpgAgentMessageKind::ActionResult.as_str(), "action_result"); assert_eq!( RpgAgentDraftCardKind::SceneChapter.as_str(), "scene_chapter" ); assert_eq!( CustomWorldRoleAssetStatus::VisualReady.as_str(), "visual_ready" ); assert_eq!(CustomWorldThemeMode::Rift.as_str(), "rift"); } #[test] fn agent_session_create_input_validates_required_json_shapes() { let input = CustomWorldAgentSessionCreateInput { session_id: "custom-world-agent-session-001".to_string(), owner_user_id: "user_001".to_string(), seed_text: "".to_string(), welcome_message_id: "message-001".to_string(), welcome_message_text: "你好!我是你的世界设定助手。".to_string(), 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: 1, }; validate_custom_world_agent_session_create_input(&input) .expect("valid skeleton input should pass"); } #[test] fn profile_upsert_input_requires_author_display_name() { let error = validate_custom_world_profile_upsert_input(&CustomWorldProfileUpsertInput { profile_id: "cwprof_001".to_string(), owner_user_id: "user_001".to_string(), public_work_code: None, author_public_user_code: None, source_agent_session_id: None, world_name: "裂潮边城".to_string(), subtitle: "港口余烬".to_string(), summary_text: "一座被裂潮与旧械共同撕扯的沿海城邦。".to_string(), theme_mode: CustomWorldThemeMode::Tide, cover_image_src: None, profile_payload_json: "{\"id\":\"cwprof_001\"}".to_string(), playable_npc_count: 3, landmark_count: 2, author_display_name: " ".to_string(), updated_at_micros: 1, }) .expect_err("blank author display name should fail"); assert_eq!(error, CustomWorldFieldError::MissingAuthorDisplayName); } #[test] fn canonicalize_profile_before_save_rebuilds_setting_text_from_creator_intent() { let mut profile = serde_json::json!({ "id": "cwprof_001", "settingText": "前端旧草稿文案", "creatorIntent": { "rawSettingText": "早期输入", "worldHook": "海图会在午夜改写群岛航路", "themeKeywords": ["海雾", "旧灯塔"], "toneDirectives": ["克制", "悬疑"], "playerPremise": "玩家是失忆领航员", "openingSituation": "正在禁航区醒来", "coreConflicts": ["议会隐瞒沉船真相"], "keyCharacters": [{ "name": "顾潮音", "role": "守灯人", "relationToPlayer": "旧识", "hiddenHook": "掌握伪造海图" }], "iconicElements": ["会说谎的罗盘"] } }); assert!(canonicalize_custom_world_profile_before_save(&mut profile)); assert_eq!( profile.get("settingText").and_then(Value::as_str), Some( "世界一句话:海图会在午夜改写群岛航路\n玩家开局:玩家是失忆领航员;正在禁航区醒来\n主题气质:海雾、旧灯塔 / 克制、悬疑\n核心冲突:议会隐瞒沉船真相\n关键关系:顾潮音 · 守灯人 · 与玩家 旧识 · 暗线 掌握伪造海图\n标志元素:会说谎的罗盘" ) ); } #[test] fn canonicalize_profile_before_save_keeps_profile_without_creator_intent() { let mut profile = serde_json::json!({ "id": "cwprof_001", "settingText": "用户手写设定" }); assert!(!canonicalize_custom_world_profile_before_save(&mut profile)); assert_eq!( profile.get("settingText").and_then(Value::as_str), Some("用户手写设定") ); } #[test] fn profile_list_input_requires_owner_user_id() { let error = validate_custom_world_profile_list_input(&CustomWorldProfileListInput { owner_user_id: " ".to_string(), }) .expect_err("blank owner user id should fail"); assert_eq!(error, CustomWorldFieldError::MissingOwnerUserId); } #[test] fn profile_delete_input_requires_profile_and_owner() { let error = validate_custom_world_profile_delete_input(&CustomWorldProfileDeleteInput { profile_id: " ".to_string(), owner_user_id: "user_001".to_string(), deleted_at_micros: 1, }) .expect_err("blank profile id should fail"); assert_eq!(error, CustomWorldFieldError::MissingProfileId); } #[test] fn agent_message_finalize_requires_valid_json_payloads() { let error = validate_custom_world_agent_message_finalize_input( &CustomWorldAgentMessageFinalizeInput { session_id: "session_001".to_string(), owner_user_id: "user_001".to_string(), operation_id: "operation_001".to_string(), assistant_message_id: Some("message_001".to_string()), assistant_reply_text: Some("已生成回复".to_string()), phase_label: "消息已处理".to_string(), phase_detail: "这一轮已完成推理并写回".to_string(), operation_status: RpgAgentOperationStatus::Completed, operation_progress: 100, stage: RpgAgentStage::FoundationReview, progress_percent: 100, focus_card_id: None, anchor_content_json: "[]".to_string(), creator_intent_json: Some("{}".to_string()), creator_intent_readiness_json: "{}".to_string(), anchor_pack_json: Some("{}".to_string()), draft_profile_json: Some("{}".to_string()), pending_clarifications_json: "[]".to_string(), suggested_actions_json: "[]".to_string(), recommended_replies_json: "[]".to_string(), quality_findings_json: "[]".to_string(), asset_coverage_json: "{}".to_string(), error_message: None, updated_at_micros: 1, }, ) .expect_err("invalid anchor content should fail"); assert_eq!(error, CustomWorldFieldError::InvalidJsonPayload); } #[test] fn agent_message_finalize_allows_missing_assistant_reply_when_failed() { validate_custom_world_agent_message_finalize_input(&CustomWorldAgentMessageFinalizeInput { session_id: "session_001".to_string(), owner_user_id: "user_001".to_string(), operation_id: "operation_001".to_string(), assistant_message_id: None, assistant_reply_text: None, phase_label: "消息处理失败".to_string(), phase_detail: "当前模型不可用,请稍后重试。".to_string(), operation_status: RpgAgentOperationStatus::Failed, operation_progress: 100, stage: RpgAgentStage::Clarifying, progress_percent: 20, focus_card_id: None, anchor_content_json: "{}".to_string(), creator_intent_json: Some("{}".to_string()), creator_intent_readiness_json: "{}".to_string(), anchor_pack_json: Some("{}".to_string()), draft_profile_json: Some("{}".to_string()), pending_clarifications_json: "[]".to_string(), suggested_actions_json: "[]".to_string(), recommended_replies_json: "[]".to_string(), quality_findings_json: "[]".to_string(), asset_coverage_json: "{}".to_string(), error_message: Some("当前模型不可用,请稍后重试。".to_string()), updated_at_micros: 1, }) .expect("failed finalize should allow empty assistant message"); } #[test] fn published_profile_compile_merges_legacy_theme_and_latest_assets() { let snapshot = build_custom_world_published_profile_compile_snapshot( CustomWorldPublishedProfileCompileInput { session_id: "session_001".to_string(), profile_id: "agent-draft-session_001".to_string(), owner_user_id: "user_001".to_string(), draft_profile_json: r#"{ "name":"潮雾列岛", "subtitle":"旧灯塔与失控航路", "summary":"第一版世界底稿已经整理完成。", "tone":"压抑、潮湿、悬疑", "playerGoal":"查清沉船与禁航区异动的真相。", "playableNpcs":[{"id":"playable-1","name":"沈砺","imageSrc":"/generated/playable-1.png"}], "storyNpcs":[{"id":"story-1","name":"顾潮音"}], "landmarks":[{"id":"landmark-1","name":"回潮旧灯塔","imageSrc":"/generated/landmark-1.png"}], "camp":{"id":"camp-1","name":"回潮暂栖所","imageSrc":"/generated/camp.png"}, "sceneChapters":[{"id":"scene-chapter-1","sceneId":"landmark-1","title":"灯塔初章"}] }"#.to_string(), legacy_result_profile_json: Some( r#"{ "id":"legacy_profile", "themeMode":"tide", "themePack":{"id":"theme-pack:tide"}, "storyGraph":{"visibleThreads":[{"id":"thread-1"}]} }"# .to_string(), ), setting_text: "被海雾吞没的旧航路群岛".to_string(), author_display_name: "测试玩家".to_string(), updated_at_micros: 42, }, ) .expect("compile should succeed"); assert_eq!(snapshot.world_name, "潮雾列岛"); assert_eq!(snapshot.theme_mode, CustomWorldThemeMode::Tide); assert_eq!( snapshot.cover_image_src.as_deref(), Some("/generated/camp.png") ); assert_eq!(snapshot.playable_npc_count, 2); assert_eq!(snapshot.landmark_count, 1); assert!( snapshot .compiled_profile_payload_json .contains("\"sceneChapterBlueprints\"") ); assert!( snapshot .compiled_profile_payload_json .contains("\"themePack\"") ); } #[test] fn published_profile_compile_defaults_theme_to_mythic_without_legacy_theme() { let snapshot = build_custom_world_published_profile_compile_snapshot( CustomWorldPublishedProfileCompileInput { session_id: "session_002".to_string(), profile_id: "profile_002".to_string(), owner_user_id: "user_002".to_string(), draft_profile_json: r#"{ "name":"裂帆荒湾", "subtitle":"雾岸残潮", "summary":"港湾里还剩最后一条能退走的潮沟。", "playableNpcs":[], "storyNpcs":[], "landmarks":[{"id":"landmark-1","name":"裂帆湾","imageSrc":"/generated/landmark-cover.png"}] }"# .to_string(), legacy_result_profile_json: None, setting_text: "被潮沟切开的荒湾".to_string(), author_display_name: "玩家二号".to_string(), updated_at_micros: 84, }, ) .expect("compile should succeed"); assert_eq!(snapshot.theme_mode, CustomWorldThemeMode::Mythic); assert_eq!( snapshot.cover_image_src.as_deref(), Some("/generated/landmark-cover.png") ); } }