This commit is contained in:
2026-05-05 14:40:41 +08:00
parent e847fcea6f
commit 07e777fef8
76 changed files with 4246 additions and 444 deletions

View File

@@ -115,7 +115,7 @@ pub async fn begin_story_runtime_session(
story_session_payload_from_record(story_result.session),
vec![story_event_payload_from_record(story_result.event)],
&persisted,
persisted.version,
None,
),
},
))
@@ -257,7 +257,7 @@ pub async fn resolve_story_runtime_action(
story_session_payload_from_record(story_result.session),
vec![story_event_payload_from_record(story_result.event)],
&persisted,
resolved.server_version.max(persisted.version),
Some(resolved.server_version),
),
},
))
@@ -395,7 +395,7 @@ fn build_story_runtime_projection_from_persisted(
story_session: StorySessionPayload,
story_events: Vec<StoryEventPayload>,
record: &RuntimeSnapshotRecord,
server_version: u32,
resolved_version: Option<u32>,
) -> shared_contracts::story::StoryRuntimeProjectionResponse {
let snapshot = story_runtime_snapshot_payload_from_record(record);
let current_story = snapshot.current_story.as_ref();
@@ -405,6 +405,8 @@ fn build_story_runtime_projection_from_persisted(
.or_else(|| Some(story_session.latest_narrative_text.clone()));
let action_result_text = read_story_runtime_current_field(current_story, "resultText");
let toast = read_story_runtime_current_field(current_story, "toast");
let server_version =
resolve_story_runtime_projection_version(&snapshot.game_state, resolved_version);
module_runtime_story::build_story_runtime_projection(
module_runtime_story::StoryRuntimeProjectionSource {
@@ -420,6 +422,15 @@ fn build_story_runtime_projection_from_persisted(
)
}
fn resolve_story_runtime_projection_version(
game_state: &Value,
resolved_version: Option<u32>,
) -> u32 {
module_runtime_story::read_u32_field(game_state, "runtimeActionVersion")
.or(resolved_version)
.unwrap_or(1)
}
fn read_story_runtime_current_text(current_story: Option<&Value>) -> Option<String> {
read_story_runtime_current_field(current_story, "text")
.or_else(|| read_story_runtime_current_field(current_story, "storyText"))
@@ -619,10 +630,12 @@ mod tests {
use time::OffsetDateTime;
use tower::ServiceExt;
use super::require_story_session_owner;
use super::{build_story_runtime_projection_from_persisted, require_story_session_owner};
use crate::{
app::build_router, config::AppConfig, request_context::RequestContext, state::AppState,
};
use module_runtime::RuntimeSnapshotRecord;
use shared_contracts::story::StorySessionPayload;
#[tokio::test]
async fn begin_story_session_requires_authentication() {
@@ -1028,6 +1041,56 @@ mod tests {
);
}
#[test]
fn story_runtime_projection_version_prefers_runtime_action_version() {
let projection = build_story_runtime_projection_from_persisted(
StorySessionPayload {
story_session_id: "storysess_001".to_string(),
runtime_session_id: "runtime_001".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: 9,
created_at: "1.000000Z".to_string(),
updated_at: "3.000000Z".to_string(),
},
vec![],
&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: json!({
"runtimeSessionId": "runtime_001",
"runtimeActionVersion": 7,
"playerHp": 30,
"playerMaxHp": 40,
"playerMana": 10,
"playerMaxMana": 20,
"playerCurrency": 0,
"playerInventory": [],
"playerEquipment": { "weapon": null, "armor": null, "relic": null },
"inBattle": false,
"npcInteractionActive": false,
"storyHistory": []
}),
current_story: None,
game_state_json: "{}".to_string(),
current_story_json: None,
created_at_micros: 1,
updated_at_micros: 3,
},
None,
);
assert_eq!(projection.server_version, 7);
}
#[test]
fn story_session_owner_guard_rejects_mismatched_actor() {
let context = RequestContext::new(