后端重写提交

This commit is contained in:
2026-04-22 12:34:49 +08:00
parent cf8da3f50f
commit 997a8daada
438 changed files with 53355 additions and 865 deletions

View File

@@ -0,0 +1,517 @@
use serde::{Deserialize, Serialize};
pub const RUNTIME_PLATFORM_THEME_LIGHT: &str = "light";
pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync";
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 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 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 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,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldLibraryEntryResponse {
pub owner_user_id: String,
pub profile_id: 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,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomWorldGalleryCardResponse {
pub owner_user_id: String,
pub profile_id: 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,
}
#[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 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>,
}
#[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 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 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 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,
}
#[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: 12,
balance_after: 80,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC.to_string(),
created_at: "2026-04-22T10:00:00Z".to_string(),
}],
})
.expect("payload should serialize");
assert_eq!(payload["entries"][0]["amountDelta"], json!(12));
assert_eq!(payload["entries"][0]["balanceAfter"], json!(80));
assert_eq!(payload["entries"][0]["sourceType"], json!("snapshot_sync"));
assert_eq!(
payload["entries"][0]["createdAt"],
json!("2026-04-22T10:00:00Z")
);
}
#[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")
);
}
}