1971 lines
66 KiB
Rust
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")
|
|
);
|
|
}
|
|
}
|