Files
Genarrative/server-rs/crates/shared-contracts/src/runtime.rs
kdletters 9f3e34e81a feat: add invite code validity controls
- Add invite code starts/expires fields across contracts, API, Spacetime bindings, and admin UI
- Enforce pending/expired invite code redemption behavior and expose admin status
- Add admin write-operation confirmation guard and documentation
- Add invite code contract/runtime tests
2026-05-04 13:54:40 +08:00

1305 lines
44 KiB
Rust

use serde::{Deserialize, Serialize};
pub const RUNTIME_PLATFORM_THEME_LIGHT: &str = "light";
pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark";
pub const SAVE_SNAPSHOT_VERSION: u32 = 2;
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD: &str =
"new_user_registration_reward";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE: &str = "points_recharge";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD: &str = "invite_inviter_reward";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME: &str =
"asset_operation_consume";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND: &str = "asset_operation_refund";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD: &str = "redeem_code_reward";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM: &str =
"puzzle_author_incentive_claim";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD: &str = "daily_task_reward";
pub const PROFILE_TASK_CYCLE_DAILY: &str = "daily";
pub const PROFILE_TASK_STATUS_INCOMPLETE: &str = "incomplete";
pub const PROFILE_TASK_STATUS_CLAIMABLE: &str = "claimable";
pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
pub const TRACKING_SCOPE_KIND_MODULE: &str = "module";
pub const TRACKING_SCOPE_KIND_USER: &str = "user";
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
pub const BROWSE_HISTORY_THEME_MODE_TIDE: &str = "tide";
pub const BROWSE_HISTORY_THEME_MODE_RIFT: &str = "rift";
pub const BROWSE_HISTORY_THEME_MODE_MYTHIC: &str = "mythic";
pub const CUSTOM_WORLD_VISIBILITY_DRAFT: &str = "draft";
pub const CUSTOM_WORLD_VISIBILITY_PUBLISHED: &str = "published";
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeSettingsResponse {
pub music_volume: f32,
pub platform_theme: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PutRuntimeSettingsRequest {
pub music_volume: f32,
pub platform_theme: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SavedGameSnapshotResponse {
pub version: u32,
pub saved_at: String,
pub game_state: serde_json::Value,
pub bottom_tab: String,
pub current_story: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PutSavedGameSnapshotRequest {
pub game_state: serde_json::Value,
pub bottom_tab: String,
#[serde(default)]
pub current_story: Option<serde_json::Value>,
#[serde(default)]
pub saved_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PutRuntimeSaveCheckpointRequest {
pub session_id: String,
pub bottom_tab: String,
#[serde(default)]
pub saved_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BasicOkResponse {
pub ok: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PlatformBrowseHistoryEntryResponse {
pub owner_user_id: String,
pub profile_id: String,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub cover_image_src: Option<String>,
pub theme_mode: String,
pub author_display_name: String,
pub visited_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PlatformBrowseHistoryWriteEntryRequest {
pub owner_user_id: String,
pub profile_id: String,
pub world_name: String,
#[serde(default)]
pub subtitle: Option<String>,
#[serde(default)]
pub summary_text: Option<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub theme_mode: Option<String>,
#[serde(default)]
pub author_display_name: Option<String>,
#[serde(default)]
pub visited_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PlatformBrowseHistoryBatchSyncRequest {
pub entries: Vec<PlatformBrowseHistoryWriteEntryRequest>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum PlatformBrowseHistoryUpsertRequest {
Single(PlatformBrowseHistoryWriteEntryRequest),
Batch(PlatformBrowseHistoryBatchSyncRequest),
}
impl PlatformBrowseHistoryUpsertRequest {
pub fn into_entries(self) -> Vec<PlatformBrowseHistoryWriteEntryRequest> {
match self {
Self::Single(entry) => vec![entry],
Self::Batch(batch) => batch.entries,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PlatformBrowseHistoryResponse {
pub entries: Vec<PlatformBrowseHistoryEntryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileDashboardSummaryResponse {
pub wallet_balance: u64,
pub total_play_time_ms: u64,
pub played_world_count: u32,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileWalletLedgerEntryResponse {
pub id: String,
pub amount_delta: i64,
pub balance_after: u64,
pub source_type: String,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileWalletLedgerResponse {
pub entries: Vec<ProfileWalletLedgerEntryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRechargeProductResponse {
pub product_id: String,
pub title: String,
pub price_cents: u64,
pub kind: String,
pub points_amount: u64,
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipBenefitResponse {
pub benefit_name: String,
pub normal_value: String,
pub month_value: String,
pub season_value: String,
pub year_value: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipResponse {
pub status: String,
pub tier: String,
pub started_at: Option<String>,
pub expires_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRechargeOrderResponse {
pub order_id: String,
pub product_id: String,
pub product_title: String,
pub kind: String,
pub amount_cents: u64,
pub status: String,
pub payment_channel: String,
pub paid_at: String,
pub created_at: String,
pub points_delta: i64,
pub membership_expires_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRechargeCenterResponse {
pub wallet_balance: u64,
pub membership: ProfileMembershipResponse,
pub point_products: Vec<ProfileRechargeProductResponse>,
pub membership_products: Vec<ProfileRechargeProductResponse>,
pub benefits: Vec<ProfileMembershipBenefitResponse>,
pub latest_order: Option<ProfileRechargeOrderResponse>,
pub has_points_recharged: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateProfileRechargeOrderRequest {
pub product_id: String,
#[serde(default)]
pub payment_channel: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateProfileRechargeOrderResponse {
pub order: ProfileRechargeOrderResponse,
pub center: ProfileRechargeCenterResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileReferralInvitedUserResponse {
pub user_id: String,
pub display_name: String,
pub avatar_url: Option<String>,
pub bound_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileReferralInviteCenterResponse {
pub invite_code: String,
pub invite_link_path: String,
pub invited_count: u32,
pub rewarded_invite_count: u32,
pub today_inviter_reward_count: u32,
pub today_inviter_reward_remaining: u32,
pub reward_points: u64,
pub invited_users: Vec<ProfileReferralInvitedUserResponse>,
pub has_redeemed_code: bool,
pub bound_inviter_user_id: Option<String>,
pub bound_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RedeemProfileReferralInviteCodeRequest {
pub invite_code: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RedeemProfileReferralInviteCodeResponse {
pub center: ProfileReferralInviteCenterResponse,
pub invitee_reward_granted: bool,
pub inviter_reward_granted: bool,
pub invitee_balance_after: u64,
pub inviter_balance_after: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RedeemProfileRewardCodeRequest {
pub code: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RedeemProfileRewardCodeResponse {
pub wallet_balance: u64,
pub amount_granted: u64,
pub ledger_entry: ProfileWalletLedgerEntryResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileTaskItemResponse {
pub task_id: String,
pub title: String,
pub description: String,
pub event_key: String,
pub cycle: String,
pub threshold: u32,
pub progress_count: u32,
pub reward_points: u64,
pub status: String,
pub day_key: i64,
pub claimed_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileTaskCenterResponse {
pub day_key: i64,
pub wallet_balance: u64,
pub tasks: Vec<ProfileTaskItemResponse>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ClaimProfileTaskRewardResponse {
pub task_id: String,
pub day_key: i64,
pub reward_points: u64,
pub wallet_balance: u64,
pub ledger_entry: ProfileWalletLedgerEntryResponse,
pub center: ProfileTaskCenterResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileTaskConfigAdminResponse {
pub task_id: String,
pub title: String,
pub description: String,
pub event_key: String,
pub cycle: String,
pub scope_kind: String,
pub threshold: u32,
pub reward_points: u64,
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
pub created_at: String,
pub updated_by: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileTaskConfigAdminListResponse {
pub entries: Vec<ProfileTaskConfigAdminResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertProfileTaskConfigRequest {
pub task_id: String,
pub title: String,
#[serde(default)]
pub description: Option<String>,
pub event_key: String,
pub cycle: String,
pub scope_kind: String,
pub threshold: u32,
pub reward_points: u64,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub sort_order: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDisableProfileTaskConfigRequest {
pub task_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertProfileRedeemCodeRequest {
pub code: String,
pub mode: String,
pub reward_points: u64,
pub max_uses: u32,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub allowed_user_ids: Vec<String>,
#[serde(default)]
pub allowed_public_user_codes: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertProfileInviteCodeRequest {
pub invite_code: String,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub starts_at: Option<String>,
#[serde(default)]
pub expires_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDisableProfileRedeemCodeRequest {
pub code: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRedeemCodeAdminResponse {
pub code: String,
pub mode: String,
pub reward_points: u64,
pub max_uses: u32,
pub global_used_count: u32,
pub enabled: bool,
pub allowed_user_ids: Vec<String>,
pub created_by: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRedeemCodeAdminListResponse {
pub entries: Vec<ProfileRedeemCodeAdminResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileInviteCodeAdminResponse {
pub user_id: String,
pub invite_code: String,
pub metadata: serde_json::Value,
pub starts_at: Option<String>,
pub expires_at: Option<String>,
pub status: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileInviteCodeAdminListResponse {
pub entries: Vec<ProfileInviteCodeAdminResponse>,
}
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfilePlayedWorkSummaryResponse {
pub world_key: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub world_type: Option<String>,
pub world_title: String,
pub world_subtitle: String,
pub first_played_at: String,
pub last_played_at: String,
pub last_observed_play_time_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfilePlayStatsResponse {
pub total_play_time_ms: u64,
pub played_works: Vec<ProfilePlayedWorkSummaryResponse>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileSaveArchiveSummaryResponse {
pub world_key: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub world_type: Option<String>,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub cover_image_src: Option<String>,
pub last_played_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileSaveArchiveListResponse {
pub entries: Vec<ProfileSaveArchiveSummaryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileSaveArchiveResumeResponse {
pub entry: ProfileSaveArchiveSummaryResponse,
pub snapshot: SavedGameSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeInventorySlotResponse {
pub slot_id: String,
pub container_kind: String,
pub slot_key: String,
pub item_id: String,
pub category: String,
pub name: String,
pub description: Option<String>,
pub quantity: u32,
pub rarity: String,
pub tags: Vec<String>,
pub stackable: bool,
pub stack_key: String,
pub equipment_slot_id: Option<String>,
pub source_kind: String,
pub source_reference_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeInventoryStateResponse {
pub runtime_session_id: String,
pub actor_user_id: String,
pub backpack_items: Vec<RuntimeInventorySlotResponse>,
pub equipment_items: Vec<RuntimeInventorySlotResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldProfileUpsertRequest {
pub profile: serde_json::Value,
pub source_agent_session_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GenerateCustomWorldProfileRequest {
pub setting_text: String,
#[serde(default)]
pub creator_intent: Option<serde_json::Value>,
#[serde(default)]
pub generation_mode: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldLibraryEntryResponse {
pub owner_user_id: String,
pub profile_id: String,
pub public_work_code: Option<String>,
pub author_public_user_code: Option<String>,
pub profile: serde_json::Value,
pub visibility: String,
pub published_at: Option<String>,
pub updated_at: 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: String,
pub playable_npc_count: u32,
pub landmark_count: u32,
#[serde(default)]
pub play_count: u32,
#[serde(default)]
pub remix_count: u32,
#[serde(default)]
pub like_count: u32,
#[serde(default)]
pub recent_play_count_7d: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldGalleryCardResponse {
pub owner_user_id: String,
pub profile_id: String,
pub public_work_code: String,
pub author_public_user_code: String,
pub visibility: String,
pub published_at: Option<String>,
pub updated_at: 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: String,
pub playable_npc_count: u32,
pub landmark_count: u32,
#[serde(default)]
pub play_count: u32,
#[serde(default)]
pub remix_count: u32,
#[serde(default)]
pub like_count: u32,
#[serde(default)]
pub recent_play_count_7d: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldLibraryResponse {
pub entries: Vec<CustomWorldLibraryEntryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldLibraryMutationResponse {
pub entry: CustomWorldLibraryEntryResponse,
pub entries: Vec<CustomWorldLibraryEntryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldGalleryResponse {
pub entries: Vec<CustomWorldGalleryCardResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldGalleryDetailResponse {
pub entry: CustomWorldLibraryEntryResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldWorkSummaryResponse {
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>,
#[serde(default)]
pub cover_render_mode: Option<String>,
#[serde(default)]
pub cover_character_image_srcs: Vec<String>,
pub updated_at: String,
pub published_at: Option<String>,
pub stage: Option<String>,
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,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldWorksResponse {
pub items: Vec<CustomWorldWorkSummaryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateCustomWorldAgentSessionRequest {
#[serde(default)]
pub seed_text: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SendCustomWorldAgentMessageRequest {
pub client_message_id: String,
pub text: String,
#[serde(default)]
pub quick_fill_requested: Option<bool>,
#[serde(default)]
pub focus_card_id: Option<String>,
#[serde(default)]
pub selected_card_ids: Option<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldAgentMessageResponse {
pub id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
pub related_operation_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldAgentOperationResponse {
pub operation_id: String,
#[serde(rename = "type")]
pub operation_type: String,
pub status: String,
pub phase_label: String,
pub phase_detail: String,
pub progress: u32,
pub error: Option<String>,
pub started_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldDraftCardSummaryResponse {
pub id: String,
pub kind: String,
pub title: String,
pub subtitle: String,
pub summary: String,
pub status: String,
pub linked_ids: Vec<String>,
pub warning_count: u32,
pub asset_status: Option<String>,
pub asset_status_label: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldDraftCardDetailSectionResponse {
pub id: String,
pub label: String,
pub value: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldDraftCardDetailResponse {
pub id: String,
pub kind: String,
pub title: String,
pub sections: Vec<CustomWorldDraftCardDetailSectionResponse>,
pub linked_ids: Vec<String>,
pub locked: bool,
pub editable: bool,
pub editable_section_ids: Vec<String>,
pub warning_messages: Vec<String>,
pub asset_status: Option<String>,
pub asset_status_label: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldAgentCardDetailResponse {
pub card: CustomWorldDraftCardDetailResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldAgentCheckpointResponse {
pub checkpoint_id: String,
pub created_at: String,
pub label: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldSupportedActionResponse {
pub action: String,
pub enabled: bool,
pub reason: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldResultPreviewBlockerResponse {
pub id: String,
pub code: String,
pub message: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldPublishGateResponse {
pub profile_id: String,
pub blockers: Vec<CustomWorldResultPreviewBlockerResponse>,
pub blocker_count: u32,
pub publish_ready: bool,
pub can_enter_world: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldCreationResultViewResponse {
pub session: CustomWorldAgentSessionSnapshotResponse,
pub profile: Option<serde_json::Value>,
pub profile_source: String,
pub target_stage: String,
pub generation_view_source: Option<String>,
pub result_view_source: Option<String>,
pub can_autosave_library: bool,
pub can_sync_result_profile: bool,
pub publish_ready: bool,
pub can_enter_world: bool,
pub blocker_count: u32,
pub recovery_action: String,
pub recovery_reason: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldAgentSessionSnapshotResponse {
pub session_id: String,
pub current_turn: u32,
pub anchor_content: serde_json::Value,
pub progress_percent: u32,
pub last_assistant_reply: Option<String>,
pub stage: String,
pub focus_card_id: Option<String>,
pub creator_intent: serde_json::Value,
pub creator_intent_readiness: serde_json::Value,
pub anchor_pack: serde_json::Value,
pub lock_state: serde_json::Value,
pub draft_profile: serde_json::Value,
pub messages: Vec<CustomWorldAgentMessageResponse>,
pub draft_cards: Vec<CustomWorldDraftCardSummaryResponse>,
pub pending_clarifications: Vec<serde_json::Value>,
pub suggested_actions: Vec<serde_json::Value>,
pub recommended_replies: Vec<String>,
pub quality_findings: Vec<serde_json::Value>,
pub asset_coverage: serde_json::Value,
pub checkpoints: Vec<CustomWorldAgentCheckpointResponse>,
pub supported_actions: Vec<CustomWorldSupportedActionResponse>,
pub publish_gate: Option<CustomWorldPublishGateResponse>,
pub result_preview: Option<serde_json::Value>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldAgentSessionResponse {
pub session: CustomWorldAgentSessionSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteCustomWorldAgentActionRequest {
pub action: String,
#[serde(default)]
pub profile_id: Option<String>,
#[serde(default)]
pub draft_profile: Option<serde_json::Value>,
#[serde(default)]
pub legacy_result_profile: Option<serde_json::Value>,
#[serde(default)]
pub setting_text: Option<String>,
#[serde(default)]
pub card_id: Option<String>,
#[serde(default)]
pub sections: Option<Vec<ExecuteCustomWorldAgentDraftCardSectionPatch>>,
#[serde(default)]
pub profile: Option<serde_json::Value>,
#[serde(default)]
pub count: Option<u32>,
#[serde(default)]
pub role_type: Option<String>,
#[serde(default)]
pub prompt_text: Option<String>,
#[serde(default)]
pub anchor_card_ids: Option<Vec<String>>,
#[serde(default)]
pub role_ids: Option<Vec<String>>,
#[serde(default)]
pub role_id: Option<String>,
#[serde(default)]
pub portrait_path: Option<String>,
#[serde(default)]
pub generated_visual_asset_id: Option<String>,
#[serde(default)]
pub generated_animation_set_id: Option<String>,
#[serde(default)]
pub animation_map: Option<serde_json::Value>,
#[serde(default)]
pub scene_ids: Option<Vec<String>>,
#[serde(default)]
pub scene_id: Option<String>,
#[serde(default)]
pub scene_kind: Option<String>,
#[serde(default)]
pub image_src: Option<String>,
#[serde(default)]
pub generated_scene_asset_id: Option<String>,
#[serde(default)]
pub generated_scene_prompt: Option<String>,
#[serde(default)]
pub generated_scene_model: Option<String>,
#[serde(default)]
pub checkpoint_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteCustomWorldAgentDraftCardSectionPatch {
pub section_id: String,
pub value: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn runtime_settings_request_uses_camel_case_fields() {
let payload = serde_json::to_value(PutRuntimeSettingsRequest {
music_volume: 0.42,
platform_theme: RUNTIME_PLATFORM_THEME_LIGHT.to_string(),
})
.expect("payload should serialize");
assert_eq!(payload["platformTheme"], json!("light"));
let music_volume = payload["musicVolume"]
.as_f64()
.expect("musicVolume should serialize as number");
assert!((music_volume - 0.42).abs() < 0.0001);
}
#[test]
fn browse_history_response_uses_camel_case_fields() {
let payload = serde_json::to_value(PlatformBrowseHistoryResponse {
entries: vec![PlatformBrowseHistoryEntryResponse {
owner_user_id: "owner-1".to_string(),
profile_id: "profile-1".to_string(),
world_name: "世界".to_string(),
subtitle: "".to_string(),
summary_text: "".to_string(),
cover_image_src: None,
theme_mode: BROWSE_HISTORY_THEME_MODE_MYTHIC.to_string(),
author_display_name: "玩家".to_string(),
visited_at: "2026-04-21T00:00:00Z".to_string(),
}],
})
.expect("payload should serialize");
assert_eq!(payload["entries"][0]["ownerUserId"], json!("owner-1"));
assert_eq!(payload["entries"][0]["themeMode"], json!("mythic"));
assert_eq!(
payload["entries"][0]["visitedAt"],
json!("2026-04-21T00:00:00Z")
);
}
#[test]
fn browse_history_upsert_request_accepts_single_or_batch_shape() {
let single: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({
"ownerUserId": "owner-1",
"profileId": "profile-1",
"worldName": "世界"
}))
.expect("single shape should deserialize");
let batch: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({
"entries": [{
"ownerUserId": "owner-1",
"profileId": "profile-1",
"worldName": "世界"
}]
}))
.expect("batch shape should deserialize");
assert_eq!(single.into_entries().len(), 1);
assert_eq!(batch.into_entries().len(), 1);
}
#[test]
fn profile_dashboard_response_uses_camel_case_fields() {
let payload = serde_json::to_value(ProfileDashboardSummaryResponse {
wallet_balance: 8,
total_play_time_ms: 16,
played_world_count: 3,
updated_at: Some("2026-04-22T10:00:00Z".to_string()),
})
.expect("payload should serialize");
assert_eq!(payload["walletBalance"], json!(8));
assert_eq!(payload["totalPlayTimeMs"], json!(16));
assert_eq!(payload["playedWorldCount"], json!(3));
assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z"));
}
#[test]
fn profile_wallet_ledger_response_uses_camel_case_fields() {
let payload = serde_json::to_value(ProfileWalletLedgerResponse {
entries: vec![
ProfileWalletLedgerEntryResponse {
id: "ledger-1".to_string(),
amount_delta: 10,
balance_after: 10,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD
.to_string(),
created_at: "2026-04-22T09:59:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-2".to_string(),
amount_delta: 12,
balance_after: 80,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC.to_string(),
created_at: "2026-04-22T10:00:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-3".to_string(),
amount_delta: 30,
balance_after: 110,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD
.to_string(),
created_at: "2026-04-22T10:01:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-4".to_string(),
amount_delta: 30,
balance_after: 140,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD
.to_string(),
created_at: "2026-04-22T10:02:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-5".to_string(),
amount_delta: 60,
balance_after: 200,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE.to_string(),
created_at: "2026-04-22T10:03:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-6".to_string(),
amount_delta: -1,
balance_after: 199,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME
.to_string(),
created_at: "2026-04-22T10:04:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-7".to_string(),
amount_delta: 1,
balance_after: 200,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND
.to_string(),
created_at: "2026-04-22T10:05:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-8".to_string(),
amount_delta: 2,
balance_after: 202,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM
.to_string(),
created_at: "2026-04-22T10:06:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-9".to_string(),
amount_delta: 10,
balance_after: 212,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD.to_string(),
created_at: "2026-04-22T10:07:00Z".to_string(),
},
],
})
.expect("payload should serialize");
assert_eq!(payload["entries"][0]["amountDelta"], json!(10));
assert_eq!(payload["entries"][0]["balanceAfter"], json!(10));
assert_eq!(
payload["entries"][0]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD)
);
assert_eq!(
payload["entries"][1]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC)
);
assert_eq!(
payload["entries"][2]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD)
);
assert_eq!(
payload["entries"][3]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD)
);
assert_eq!(
payload["entries"][4]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE)
);
assert_eq!(
payload["entries"][5]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME)
);
assert_eq!(
payload["entries"][6]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND)
);
assert_eq!(
payload["entries"][7]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM)
);
assert_eq!(
payload["entries"][8]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD)
);
assert_eq!(
payload["entries"][0]["createdAt"],
json!("2026-04-22T09:59:00Z")
);
}
#[test]
fn profile_task_center_response_uses_camel_case_fields() {
let payload = serde_json::to_value(ProfileTaskCenterResponse {
day_key: 20576,
wallet_balance: 18,
tasks: vec![ProfileTaskItemResponse {
task_id: "daily_login".to_string(),
title: "每日登录".to_string(),
description: "".to_string(),
event_key: "daily_login".to_string(),
cycle: PROFILE_TASK_CYCLE_DAILY.to_string(),
threshold: 1,
progress_count: 1,
reward_points: 10,
status: PROFILE_TASK_STATUS_CLAIMABLE.to_string(),
day_key: 20576,
claimed_at: None,
updated_at: "2026-05-03T00:00:00Z".to_string(),
}],
updated_at: "2026-05-03T00:00:00Z".to_string(),
})
.expect("payload should serialize");
assert_eq!(payload["walletBalance"], json!(18));
assert_eq!(payload["tasks"][0]["taskId"], json!("daily_login"));
assert_eq!(payload["tasks"][0]["rewardPoints"], json!(10));
assert_eq!(
payload["tasks"][0]["status"],
json!(PROFILE_TASK_STATUS_CLAIMABLE)
);
}
#[test]
fn admin_task_config_request_accepts_defaults() {
let payload: AdminUpsertProfileTaskConfigRequest = serde_json::from_value(json!({
"taskId": "daily_login",
"title": "每日登录",
"eventKey": "daily_login",
"cycle": "daily",
"scopeKind": "user",
"threshold": 1,
"rewardPoints": 10
}))
.expect("request should deserialize");
assert_eq!(payload.description, None);
assert_eq!(payload.enabled, true);
assert_eq!(payload.sort_order, None);
}
#[test]
fn profile_recharge_center_response_uses_camel_case_fields() {
let payload = serde_json::to_value(ProfileRechargeCenterResponse {
wallet_balance: 29,
membership: ProfileMembershipResponse {
status: "active".to_string(),
tier: "month".to_string(),
started_at: Some("2026-04-25T10:00:00Z".to_string()),
expires_at: Some("2026-05-25T10:00:00Z".to_string()),
updated_at: Some("2026-04-25T10:00:00Z".to_string()),
},
point_products: vec![ProfileRechargeProductResponse {
product_id: "points_60".to_string(),
title: "60光点".to_string(),
price_cents: 600,
kind: "points".to_string(),
points_amount: 60,
bonus_points: 60,
duration_days: 0,
badge_label: "首充双倍".to_string(),
description: "首充送60光点".to_string(),
tier: "normal".to_string(),
}],
membership_products: vec![],
benefits: vec![],
latest_order: None,
has_points_recharged: false,
})
.expect("payload should serialize");
assert_eq!(payload["walletBalance"], json!(29));
assert_eq!(
payload["membership"]["expiresAt"],
json!("2026-05-25T10:00:00Z")
);
assert_eq!(payload["pointProducts"][0]["productId"], json!("points_60"));
assert_eq!(payload["pointProducts"][0]["title"], json!("60光点"));
assert_eq!(payload["pointProducts"][0]["priceCents"], json!(600));
assert_eq!(
payload["pointProducts"][0]["description"],
json!("首充送60光点")
);
assert_eq!(payload["hasPointsRecharged"], json!(false));
}
#[test]
fn create_profile_recharge_order_request_accepts_optional_channel() {
let payload: CreateProfileRechargeOrderRequest = serde_json::from_value(json!({
"productId": "member_month"
}))
.expect("request should deserialize");
assert_eq!(payload.product_id, "member_month");
assert_eq!(payload.payment_channel, None);
}
#[test]
fn profile_play_stats_response_uses_camel_case_fields() {
let payload = serde_json::to_value(ProfilePlayStatsResponse {
total_play_time_ms: 18,
played_works: vec![ProfilePlayedWorkSummaryResponse {
world_key: "builtin:WUXIA".to_string(),
owner_user_id: None,
profile_id: None,
world_type: Some("WUXIA".to_string()),
world_title: "武侠世界".to_string(),
world_subtitle: "".to_string(),
first_played_at: "2026-04-20T10:00:00Z".to_string(),
last_played_at: "2026-04-22T10:00:00Z".to_string(),
last_observed_play_time_ms: 1200,
}],
updated_at: Some("2026-04-22T10:00:00Z".to_string()),
})
.expect("payload should serialize");
assert_eq!(payload["totalPlayTimeMs"], json!(18));
assert_eq!(
payload["playedWorks"][0]["worldKey"],
json!("builtin:WUXIA")
);
assert_eq!(
payload["playedWorks"][0]["lastObservedPlayTimeMs"],
json!(1200)
);
assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z"));
}
#[test]
fn runtime_inventory_state_response_uses_camel_case_fields() {
let payload = serde_json::to_value(RuntimeInventoryStateResponse {
runtime_session_id: "runtime_001".to_string(),
actor_user_id: "user_001".to_string(),
backpack_items: vec![RuntimeInventorySlotResponse {
slot_id: "invslot_001".to_string(),
container_kind: "backpack".to_string(),
slot_key: "invslot_001".to_string(),
item_id: "consumable_heal_potion".to_string(),
category: "消耗品".to_string(),
name: "疗伤药".to_string(),
description: Some("用于恢复少量气血。".to_string()),
quantity: 2,
rarity: "common".to_string(),
tags: vec!["healing".to_string()],
stackable: true,
stack_key: "heal_potion".to_string(),
equipment_slot_id: None,
source_kind: "treasure_reward".to_string(),
source_reference_id: Some("treasure_001".to_string()),
created_at: "2026-04-22T10:00:00Z".to_string(),
updated_at: "2026-04-22T10:01:00Z".to_string(),
}],
equipment_items: vec![],
})
.expect("payload should serialize");
assert_eq!(payload["runtimeSessionId"], json!("runtime_001"));
assert_eq!(payload["actorUserId"], json!("user_001"));
assert_eq!(payload["backpackItems"][0]["slotId"], json!("invslot_001"));
assert_eq!(
payload["backpackItems"][0]["sourceKind"],
json!("treasure_reward")
);
}
}