推进 server-rs DDD 分层与新接口接线

This commit is contained in:
Codex
2026-04-29 15:46:16 +08:00
parent 9d3fcfae77
commit f82775b852
89 changed files with 3657 additions and 9636 deletions

View File

@@ -13,7 +13,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_ai_task_procedure_result);
send_once(&sender, mapped);
},
@@ -35,15 +35,12 @@ impl SpacetimeClient {
.reducers
.start_ai_task_then(reducer_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(|inner| inner.map_err(SpacetimeClientError::Runtime));
send_reducer_once(&callback_sender, mapped);
})
{
send_reducer_once(
&sender,
Err(SpacetimeClientError::Procedure(error.to_string())),
);
send_reducer_once(&sender, Err(SpacetimeClientError::from_sdk_error(error)));
}
})
.await
@@ -62,15 +59,12 @@ impl SpacetimeClient {
.reducers
.start_ai_task_stage_then(reducer_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(|inner| inner.map_err(SpacetimeClientError::Runtime));
send_reducer_once(&callback_sender, mapped);
})
{
send_reducer_once(
&sender,
Err(SpacetimeClientError::Procedure(error.to_string())),
);
send_reducer_once(&sender, Err(SpacetimeClientError::from_sdk_error(error)));
}
})
.await
@@ -87,7 +81,7 @@ impl SpacetimeClient {
.procedures()
.append_ai_text_chunk_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_ai_task_procedure_result);
send_once(&sender, mapped);
});
@@ -106,7 +100,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_ai_task_procedure_result);
send_once(&sender, mapped);
},
@@ -126,7 +120,7 @@ impl SpacetimeClient {
.procedures()
.attach_ai_result_reference_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_ai_task_procedure_result);
send_once(&sender, mapped);
});
@@ -145,7 +139,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_ai_task_procedure_result);
send_once(&sender, mapped);
},
@@ -165,7 +159,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_ai_task_procedure_result);
send_once(&sender, mapped);
},
@@ -185,7 +179,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_ai_task_procedure_result);
send_once(&sender, mapped);
},

View File

@@ -12,7 +12,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_asset_history_list_result);
send_once(&sender, mapped);
},
@@ -32,7 +32,7 @@ impl SpacetimeClient {
.procedures()
.confirm_asset_object_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_procedure_result);
send_once(&sender, mapped);
});
@@ -51,7 +51,7 @@ impl SpacetimeClient {
.procedures()
.bind_asset_object_to_entity_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_entity_binding_procedure_result);
send_once(&sender, mapped);
});

View File

@@ -9,7 +9,7 @@ impl SpacetimeClient {
.procedures()
.export_auth_store_snapshot_from_tables_then(move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_auth_store_snapshot_procedure_result);
send_once(&sender, mapped);
});
@@ -25,7 +25,7 @@ impl SpacetimeClient {
.procedures()
.get_auth_store_snapshot_then(move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_auth_store_snapshot_procedure_result);
send_once(&sender, mapped);
});
@@ -48,7 +48,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_auth_store_snapshot_procedure_result);
send_once(&sender, mapped);
},
@@ -65,7 +65,7 @@ impl SpacetimeClient {
.procedures()
.import_auth_store_snapshot_then(move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_auth_store_snapshot_import_procedure_result);
send_once(&sender, mapped);
});

View File

@@ -22,7 +22,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_big_fish_session_procedure_result);
send_once(&sender, mapped);
},
@@ -46,7 +46,7 @@ impl SpacetimeClient {
.procedures()
.get_big_fish_session_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_big_fish_session_procedure_result);
send_once(&sender, mapped);
});
@@ -90,7 +90,7 @@ impl SpacetimeClient {
.procedures()
.list_big_fish_works_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(|result| {
map_big_fish_works_procedure_result(
result,
@@ -119,7 +119,7 @@ impl SpacetimeClient {
.procedures()
.delete_big_fish_work_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(|result| {
map_big_fish_works_procedure_result(
result,
@@ -150,7 +150,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_big_fish_session_procedure_result);
send_once(&sender, mapped);
},
@@ -180,7 +180,7 @@ impl SpacetimeClient {
.procedures()
.finalize_big_fish_agent_message_turn_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_big_fish_session_procedure_result);
send_once(&sender, mapped);
});
@@ -204,7 +204,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_big_fish_session_procedure_result);
send_once(&sender, mapped);
},
@@ -232,7 +232,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_big_fish_session_procedure_result);
send_once(&sender, mapped);
},
@@ -258,7 +258,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_big_fish_session_procedure_result);
send_once(&sender, mapped);
},
@@ -283,7 +283,7 @@ impl SpacetimeClient {
.procedures()
.record_big_fish_play_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(|result| map_big_fish_works_procedure_result(result, None));
send_once(&sender, mapped);
});

View File

@@ -15,7 +15,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_battle_state_procedure_result);
send_once(&sender, mapped);
},
@@ -37,7 +37,7 @@ impl SpacetimeClient {
.procedures()
.get_battle_state_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_battle_state_procedure_result);
send_once(&sender, mapped);
});
@@ -58,7 +58,7 @@ impl SpacetimeClient {
.procedures()
.resolve_combat_action_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_resolve_combat_action_procedure_result);
send_once(&sender, mapped);
});

View File

@@ -16,7 +16,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_runtime_inventory_state_procedure_result);
send_once(&sender, mapped);
},

View File

@@ -50,6 +50,7 @@ pub mod npc;
pub mod puzzle;
pub mod runtime;
pub mod story;
pub mod story_runtime;
use std::{
error::Error,
@@ -424,6 +425,20 @@ impl SpacetimeClient {
}
}
impl SpacetimeClientError {
pub(crate) fn from_sdk_error(error: impl fmt::Display) -> Self {
Self::Procedure(error.to_string())
}
pub(crate) fn procedure_failed(message: Option<String>) -> Self {
Self::Procedure(message.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()))
}
pub(crate) fn missing_snapshot(label: &'static str) -> Self {
Self::Procedure(format!("SpacetimeDB procedure 未返回{label}"))
}
}
impl PooledConnection {
fn is_broken(&self) -> bool {
self.broken.load(Ordering::SeqCst)

View File

@@ -530,16 +530,12 @@ pub(crate) fn map_procedure_result(
result: AssetObjectProcedureResult,
) -> Result<AssetObjectRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result.record.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回对象快照".to_string())
})?;
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("对象快照"))?;
Ok(build_asset_object_record(map_snapshot(snapshot)))
}
@@ -548,16 +544,12 @@ pub(crate) fn map_entity_binding_procedure_result(
result: AssetEntityBindingProcedureResult,
) -> Result<AssetEntityBindingRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result.record.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回绑定快照".to_string())
})?;
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("绑定快照"))?;
Ok(build_asset_entity_binding_record(
map_entity_binding_snapshot(snapshot),
@@ -568,11 +560,7 @@ pub(crate) fn map_asset_history_list_result(
result: AssetHistoryListResult,
) -> Result<Vec<AssetHistoryEntryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
@@ -609,16 +597,12 @@ pub(crate) fn map_auth_store_snapshot_procedure_result(
result: AuthStoreSnapshotProcedureResult,
) -> Result<AuthStoreSnapshotRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let record = result.record.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回认证快照".to_string())
})?;
let record = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("认证快照"))?;
Ok(map_auth_store_snapshot_record(record))
}
@@ -1003,16 +987,12 @@ pub(crate) fn map_ai_task_procedure_result(
result: AiTaskProcedureResult,
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Runtime(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let task = result.task.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回 ai_task 快照".to_string())
})?;
let task = result
.task
.ok_or_else(|| SpacetimeClientError::missing_snapshot("ai_task 快照"))?;
Ok(AiTaskMutationRecord {
task: map_ai_task_snapshot(task),
@@ -1344,18 +1324,12 @@ pub(crate) fn map_big_fish_session_procedure_result(
result: BigFishSessionProcedureResult,
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result.session.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 big fish session 快照".to_string(),
)
})?;
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish session 快照"))?;
Ok(map_big_fish_session_snapshot(session))
}
@@ -1365,18 +1339,12 @@ pub(crate) fn map_big_fish_works_procedure_result(
fallback_owner_user_id: Option<&str>,
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let items_json = result.items_json.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 big fish works 快照".to_string(),
)
})?;
let items_json = result
.items_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish works 快照"))?;
let items = serde_json::from_str::<Vec<CompatibleBigFishWorkSummaryRecord>>(&items_json)
.map_err(|error| {
SpacetimeClientError::Runtime(format!("big fish works items_json 非法: {error}"))
@@ -1392,21 +1360,15 @@ pub(crate) fn map_story_session_procedure_result(
result: StorySessionProcedureResult,
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result.session.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 story session 快照".to_string(),
)
})?;
let event = result.event.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回 story event 快照".to_string())
})?;
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story session 快照"))?;
let event = result
.event
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story event 快照"))?;
Ok(StorySessionResultRecord {
session: map_story_session_snapshot(session),
@@ -1418,18 +1380,12 @@ pub(crate) fn map_story_session_state_procedure_result(
result: StorySessionStateProcedureResult,
) -> Result<StorySessionStateRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result.session.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 story session state 快照".to_string(),
)
})?;
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story session state 快照"))?;
Ok(StorySessionStateRecord {
session: map_story_session_snapshot(session),
@@ -1445,18 +1401,12 @@ pub(crate) fn map_runtime_inventory_state_procedure_result(
result: RuntimeInventoryStateProcedureResult,
) -> Result<RuntimeInventoryStateRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result.snapshot.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 runtime inventory state 快照".to_string(),
)
})?;
let snapshot = result
.snapshot
.ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime inventory state 快照"))?;
Ok(build_runtime_inventory_state_record(
map_runtime_inventory_state_snapshot(snapshot),
@@ -1467,18 +1417,12 @@ pub(crate) fn map_battle_state_procedure_result(
result: BattleStateProcedureResult,
) -> Result<BattleStateRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result.snapshot.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 battle_state 快照".to_string(),
)
})?;
let snapshot = result
.snapshot
.ok_or_else(|| SpacetimeClientError::missing_snapshot("battle_state 快照"))?;
Ok(build_battle_state_record(map_battle_state_snapshot(
snapshot,
@@ -1489,16 +1433,12 @@ pub(crate) fn map_resolve_combat_action_procedure_result(
result: ResolveCombatActionProcedureResult,
) -> Result<ResolveCombatActionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let action_result = result.result.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回战斗结算结果".to_string())
})?;
let action_result = result
.result
.ok_or_else(|| SpacetimeClientError::missing_snapshot("战斗结算结果"))?;
Ok(build_resolve_combat_action_record(
map_resolve_combat_action_result(action_result),
@@ -1509,16 +1449,12 @@ pub(crate) fn map_npc_battle_interaction_procedure_result(
result: NpcBattleInteractionProcedureResult,
) -> Result<NpcBattleInteractionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let interaction_result = result.result.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回 NPC 开战结果".to_string())
})?;
let interaction_result = result
.result
.ok_or_else(|| SpacetimeClientError::missing_snapshot("NPC 开战结果"))?;
Ok(build_npc_battle_interaction_record(
map_npc_battle_interaction_result(interaction_result),

View File

@@ -16,7 +16,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_npc_battle_interaction_procedure_result);
send_once(&sender, mapped);
},

View File

@@ -28,7 +28,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_story_session_procedure_result);
send_once(&sender, mapped);
},
@@ -60,7 +60,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_story_session_procedure_result);
send_once(&sender, mapped);
},
@@ -82,7 +82,7 @@ impl SpacetimeClient {
procedure_input,
move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_story_session_state_procedure_result);
send_once(&sender, mapped);
},

View File

@@ -0,0 +1,227 @@
use module_runtime_story::StoryRuntimeProjectionSource;
use serde_json::Value;
use shared_contracts::{
runtime_story::RuntimeStoryOptionView,
story::{StoryEventPayload, StorySessionPayload},
};
use super::*;
impl SpacetimeClient {
pub async fn get_story_runtime_projection_source(
&self,
story_session_id: String,
actor_user_id: String,
) -> Result<StoryRuntimeProjectionSource, SpacetimeClientError> {
let story_state = self.get_story_session_state(story_session_id).await?;
if story_state.session.actor_user_id != actor_user_id {
return Err(SpacetimeClientError::Runtime(
"story session 不属于当前用户".to_string(),
));
}
let runtime_snapshot =
self.get_runtime_snapshot(actor_user_id)
.await?
.ok_or_else(|| {
SpacetimeClientError::Runtime("当前用户缺少 runtime snapshot".to_string())
})?;
assert_runtime_snapshot_matches_story_session(&story_state.session, &runtime_snapshot)?;
let current_story = runtime_snapshot.current_story.as_ref();
let latest_narrative_text = story_state.session.latest_narrative_text.clone();
let server_version = runtime_snapshot.version.max(story_state.session.version);
Ok(StoryRuntimeProjectionSource {
story_session: build_story_session_payload(story_state.session),
story_events: story_state
.events
.into_iter()
.map(build_story_event_payload)
.collect(),
game_state: runtime_snapshot.game_state,
options: read_runtime_story_options(current_story)?,
server_version,
current_narrative_text: read_current_story_text(current_story)
.or(Some(latest_narrative_text)),
action_result_text: read_current_story_string(current_story, "resultText"),
toast: read_current_story_string(current_story, "toast"),
})
}
}
fn assert_runtime_snapshot_matches_story_session(
session: &StorySessionRecord,
snapshot: &RuntimeSnapshotRecord,
) -> Result<(), SpacetimeClientError> {
let Some(runtime_session_id) = snapshot
.game_state
.as_object()
.and_then(|state| state.get("runtimeSessionId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Err(SpacetimeClientError::Runtime(
"runtime snapshot 缺少 runtimeSessionId".to_string(),
));
};
if runtime_session_id != session.runtime_session_id {
return Err(SpacetimeClientError::Runtime(
"runtime snapshot 与 story session 不匹配".to_string(),
));
}
Ok(())
}
fn build_story_session_payload(record: StorySessionRecord) -> StorySessionPayload {
StorySessionPayload {
story_session_id: record.story_session_id,
runtime_session_id: record.runtime_session_id,
actor_user_id: record.actor_user_id,
world_profile_id: record.world_profile_id,
initial_prompt: record.initial_prompt,
opening_summary: record.opening_summary,
latest_narrative_text: record.latest_narrative_text,
latest_choice_function_id: record.latest_choice_function_id,
status: record.status,
version: record.version,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
fn build_story_event_payload(record: StoryEventRecord) -> StoryEventPayload {
StoryEventPayload {
event_id: record.event_id,
story_session_id: record.story_session_id,
event_kind: record.event_kind,
narrative_text: record.narrative_text,
choice_function_id: record.choice_function_id,
created_at: record.created_at,
}
}
fn read_runtime_story_options(
current_story: Option<&Value>,
) -> Result<Vec<RuntimeStoryOptionView>, SpacetimeClientError> {
let Some(options) = current_story.and_then(|story| story.get("options")) else {
return Ok(Vec::new());
};
serde_json::from_value::<Vec<RuntimeStoryOptionView>>(options.clone()).map_err(|error| {
SpacetimeClientError::Runtime(format!(
"currentStory.options 无法映射为后端选项投影: {error}"
))
})
}
fn read_current_story_text(current_story: Option<&Value>) -> Option<String> {
read_current_story_string(current_story, "text")
.or_else(|| read_current_story_string(current_story, "storyText"))
}
fn read_current_story_string(current_story: Option<&Value>, field: &str) -> Option<String> {
current_story?
.as_object()?
.get(field)?
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn runtime_snapshot_session_guard_accepts_matching_runtime_session() {
let session = story_session_record();
let snapshot = runtime_snapshot_record(json!({ "runtimeSessionId": "runtime_1" }), None);
assert!(assert_runtime_snapshot_matches_story_session(&session, &snapshot).is_ok());
}
#[test]
fn runtime_snapshot_session_guard_rejects_mismatched_runtime_session() {
let session = story_session_record();
let snapshot =
runtime_snapshot_record(json!({ "runtimeSessionId": "runtime_other" }), None);
let error = assert_runtime_snapshot_matches_story_session(&session, &snapshot)
.expect_err("mismatched runtime session should fail");
assert!(error.to_string().contains("不匹配"));
}
#[test]
fn current_story_options_parse_runtime_story_options() {
let options = read_runtime_story_options(Some(&json!({
"text": "守火人抬眼看着你。",
"options": [{
"functionId": "npc_chat",
"actionText": "继续交谈",
"scope": "npc"
}]
})))
.expect("options should parse");
assert_eq!(options[0].function_id, "npc_chat");
assert_eq!(options[0].action_text, "继续交谈");
assert_eq!(options[0].scope, "npc");
}
#[test]
fn current_story_text_prefers_text_then_story_text() {
assert_eq!(
read_current_story_text(Some(&json!({ "text": "正文", "storyText": "备用" })))
.as_deref(),
Some("正文")
);
assert_eq!(
read_current_story_text(Some(&json!({ "storyText": "备用" }))).as_deref(),
Some("备用")
);
}
fn story_session_record() -> StorySessionRecord {
StorySessionRecord {
story_session_id: "storysess_1".to_string(),
runtime_session_id: "runtime_1".to_string(),
actor_user_id: "user_1".to_string(),
world_profile_id: "profile_1".to_string(),
initial_prompt: "进入营地".to_string(),
opening_summary: Some("营地开场".to_string()),
latest_narrative_text: "篝火仍然亮着。".to_string(),
latest_choice_function_id: Some("npc_chat".to_string()),
status: "active".to_string(),
version: 3,
created_at: "1.000000Z".to_string(),
updated_at: "3.000000Z".to_string(),
}
}
fn runtime_snapshot_record(
game_state: Value,
current_story: Option<Value>,
) -> RuntimeSnapshotRecord {
RuntimeSnapshotRecord {
user_id: "user_1".to_string(),
version: 2,
saved_at: "3.000000Z".to_string(),
saved_at_micros: 3,
bottom_tab: "adventure".to_string(),
game_state,
current_story,
game_state_json: "{}".to_string(),
current_story_json: None,
created_at_micros: 1,
updated_at_micros: 3,
}
}
}