Files
Genarrative/server-rs/crates/module-custom-world/src/lib.rs

1971 lines
66 KiB
Rust

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<String>,
pub author_public_user_code: Option<String>,
pub source_agent_session_id: Option<String>,
pub publication_status: CustomWorldPublicationStatus,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub theme_mode: CustomWorldThemeMode,
pub cover_image_src: Option<String>,
pub profile_payload_json: String,
pub playable_npc_count: u32,
pub landmark_count: u32,
pub author_display_name: String,
pub published_at_micros: Option<i64>,
pub deleted_at_micros: Option<i64>,
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<String>,
pub theme_mode: CustomWorldThemeMode,
pub playable_npc_count: u32,
pub landmark_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<CustomWorldProfileSnapshot>,
pub gallery_entry: Option<CustomWorldGalleryEntrySnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomWorldProfileListResult {
pub ok: bool,
pub entries: Vec<CustomWorldProfileSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomWorldGalleryListResult {
pub ok: bool,
pub entries: Vec<CustomWorldGalleryEntrySnapshot>,
pub error_message: Option<String>,
}
#[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<CustomWorldPublishBlockerSnapshot>,
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<String>,
pub cover_render_mode: Option<String>,
pub cover_character_image_srcs_json: String,
pub updated_at_micros: i64,
pub published_at_micros: Option<i64>,
pub stage: Option<RpgAgentStage>,
pub stage_label: Option<String>,
pub playable_npc_count: u32,
pub landmark_count: u32,
pub role_visual_ready_count: Option<u32>,
pub role_animation_ready_count: Option<u32>,
pub role_asset_summary_label: Option<String>,
pub session_id: Option<String>,
pub profile_id: Option<String>,
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<CustomWorldWorkSummarySnapshot>,
pub error_message: Option<String>,
}
#[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<String>,
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<String>,
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<CustomWorldRoleAssetStatus>,
pub asset_status_label: Option<String>,
pub detail_payload_json: Option<String>,
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<CustomWorldDraftCardDetailSectionSnapshot>,
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<CustomWorldRoleAssetStatus>,
pub asset_status_label: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomWorldDraftCardDetailResult {
pub ok: bool,
pub card: Option<CustomWorldDraftCardDetailSnapshot>,
pub error_message: Option<String>,
}
#[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<String>,
pub anchor_content_json: String,
pub creator_intent_json: Option<String>,
pub creator_intent_readiness_json: String,
pub anchor_pack_json: Option<String>,
pub lock_state_json: Option<String>,
pub draft_profile_json: Option<String>,
pub last_assistant_reply: Option<String>,
pub publish_gate_json: Option<String>,
pub result_preview_json: Option<String>,
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<CustomWorldAgentMessageSnapshot>,
pub draft_cards: Vec<CustomWorldDraftCardSnapshot>,
pub operations: Vec<CustomWorldAgentOperationSnapshot>,
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<CustomWorldAgentSessionSnapshot>,
pub error_message: Option<String>,
}
#[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<String>,
pub author_public_user_code: Option<String>,
pub source_agent_session_id: Option<String>,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub theme_mode: CustomWorldThemeMode,
pub cover_image_src: Option<String>,
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<String>,
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 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<String>,
pub creator_intent_readiness_json: String,
pub anchor_pack_json: Option<String>,
pub lock_state_json: Option<String>,
pub draft_profile_json: Option<String>,
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<String>,
pub assistant_reply_text: Option<String>,
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<String>,
pub anchor_content_json: String,
pub creator_intent_json: Option<String>,
pub creator_intent_readiness_json: String,
pub anchor_pack_json: Option<String>,
pub draft_profile_json: Option<String>,
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<String>,
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<String>,
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<CustomWorldAgentOperationSnapshot>,
pub error_message: Option<String>,
}
#[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<String>,
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<CustomWorldAgentOperationSnapshot>,
pub error_message: Option<String>,
}
#[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<String>,
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<String>,
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<CustomWorldPublishedProfileCompileSnapshot>,
pub error_message: Option<String>,
}
#[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<String>,
pub author_public_user_code: String,
pub draft_profile_json: String,
pub legacy_result_profile_json: Option<String>,
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<CustomWorldPublishedProfileCompileSnapshot>,
pub entry: Option<CustomWorldProfileSnapshot>,
pub gallery_entry: Option<CustomWorldGalleryEntrySnapshot>,
pub session_stage: Option<RpgAgentStage>,
pub error_message: Option<String>,
}
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<Self> {
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<CustomWorldPublishedProfileCompileSnapshot, CustomWorldFieldError> {
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 empty_agent_anchor_content_json() -> String {
r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":[],"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<String>) -> Option<String> {
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>(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>(value) {
Ok(Value::Array(_)) => Ok(()),
_ => Err(CustomWorldFieldError::InvalidJsonPayload),
}
}
fn parse_required_json_object(
value: &str,
error: CustomWorldFieldError,
) -> Result<Map<String, Value>, CustomWorldFieldError> {
match serde_json::from_str::<Value>(value) {
Ok(Value::Object(object)) => Ok(object),
_ => Err(error),
}
}
fn parse_optional_json_object(
value: Option<String>,
error: CustomWorldFieldError,
) -> Result<Map<String, Value>, 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<String> {
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<Value> {
match value {
Some(Value::Array(items)) => items.clone(),
_ => Vec::new(),
}
}
fn to_object(value: Option<&Value>) -> Option<Map<String, Value>> {
match value {
Some(Value::Object(object)) => Some(object.clone()),
_ => None,
}
}
fn resolve_text_field(
draft: &Map<String, Value>,
legacy: &Map<String, Value>,
key: &str,
) -> Option<String> {
to_text(draft.get(key)).or_else(|| to_text(legacy.get(key)))
}
fn resolve_theme_mode(legacy: &Map<String, Value>) -> 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<String, Value>,
legacy: &Map<String, Value>,
) -> Option<String> {
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<String, Value>,
legacy: &Map<String, Value>,
world_name: &str,
subtitle: &str,
summary_text: &str,
) -> Result<String, CustomWorldFieldError> {
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 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")
);
}
}