Integrate unfinished server-rs refactor worklists
This commit is contained in:
@@ -1,3 +1,167 @@
|
||||
//! 剧情应用编排过渡落位。
|
||||
//! 剧情应用服务与读模型映射。
|
||||
//!
|
||||
//! 这里只返回剧情快照、事件和待投影结果,不直接调用模型或数据库。
|
||||
//! 应用层负责把命令变成快照、事件和前端可消费记录;它不直接调用模型、HTTP、
|
||||
//! SpacetimeDB 或旧 Node 兼容服务。
|
||||
|
||||
use crate::commands::{StoryContinueInput, StorySessionInput, normalize_optional_value};
|
||||
use crate::domain::{INITIAL_STORY_SESSION_VERSION, StorySessionSnapshot, StorySessionStatus};
|
||||
use crate::errors::StorySessionFieldError;
|
||||
use crate::events::{StoryEventKind, StoryEventSnapshot};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::format_timestamp_micros;
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session: Option<StorySessionSnapshot>,
|
||||
pub event: Option<StoryEventSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionStateProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session: Option<StorySessionSnapshot>,
|
||||
pub events: Vec<StoryEventSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StorySessionRecord {
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
pub opening_summary: Option<String>,
|
||||
pub latest_narrative_text: String,
|
||||
pub latest_choice_function_id: Option<String>,
|
||||
pub status: String,
|
||||
pub version: u32,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoryEventRecord {
|
||||
pub event_id: String,
|
||||
pub story_session_id: String,
|
||||
pub event_kind: String,
|
||||
pub narrative_text: String,
|
||||
pub choice_function_id: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StorySessionResultRecord {
|
||||
pub session: StorySessionRecord,
|
||||
pub event: StoryEventRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StorySessionStateRecord {
|
||||
pub session: StorySessionRecord,
|
||||
pub events: Vec<StoryEventRecord>,
|
||||
}
|
||||
|
||||
pub fn build_story_session_snapshot(input: StorySessionInput) -> StorySessionSnapshot {
|
||||
StorySessionSnapshot {
|
||||
story_session_id: input.story_session_id,
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
world_profile_id: input.world_profile_id,
|
||||
initial_prompt: input.initial_prompt,
|
||||
opening_summary: normalize_optional_value(input.opening_summary),
|
||||
latest_narrative_text: String::new(),
|
||||
latest_choice_function_id: None,
|
||||
status: StorySessionStatus::Active,
|
||||
version: INITIAL_STORY_SESSION_VERSION,
|
||||
created_at_micros: input.created_at_micros,
|
||||
updated_at_micros: input.created_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_story_continue(
|
||||
current: StorySessionSnapshot,
|
||||
input: StoryContinueInput,
|
||||
) -> Result<(StorySessionSnapshot, StoryEventSnapshot), StorySessionFieldError> {
|
||||
crate::commands::validate_story_continue_input(&input)?;
|
||||
|
||||
if current.version == 0 {
|
||||
return Err(StorySessionFieldError::InvalidVersion);
|
||||
}
|
||||
|
||||
let event = StoryEventSnapshot {
|
||||
event_id: input.event_id,
|
||||
story_session_id: current.story_session_id.clone(),
|
||||
event_kind: StoryEventKind::StoryContinued,
|
||||
narrative_text: input.narrative_text.clone(),
|
||||
choice_function_id: normalize_optional_value(input.choice_function_id),
|
||||
created_at_micros: input.updated_at_micros,
|
||||
};
|
||||
|
||||
let next = StorySessionSnapshot {
|
||||
latest_narrative_text: input.narrative_text,
|
||||
latest_choice_function_id: event.choice_function_id.clone(),
|
||||
version: current.version + 1,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
..current
|
||||
};
|
||||
|
||||
Ok((next, event))
|
||||
}
|
||||
|
||||
pub fn build_story_session_record(snapshot: StorySessionSnapshot) -> StorySessionRecord {
|
||||
StorySessionRecord {
|
||||
story_session_id: snapshot.story_session_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
world_profile_id: snapshot.world_profile_id,
|
||||
initial_prompt: snapshot.initial_prompt,
|
||||
opening_summary: snapshot.opening_summary,
|
||||
latest_narrative_text: snapshot.latest_narrative_text,
|
||||
latest_choice_function_id: snapshot.latest_choice_function_id,
|
||||
status: snapshot.status.as_str().to_string(),
|
||||
version: snapshot.version,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_story_event_record(snapshot: StoryEventSnapshot) -> StoryEventRecord {
|
||||
StoryEventRecord {
|
||||
event_id: snapshot.event_id,
|
||||
story_session_id: snapshot.story_session_id,
|
||||
event_kind: snapshot.event_kind.as_str().to_string(),
|
||||
narrative_text: snapshot.narrative_text,
|
||||
choice_function_id: snapshot.choice_function_id,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_story_session_result_record(
|
||||
session: StorySessionSnapshot,
|
||||
event: StoryEventSnapshot,
|
||||
) -> StorySessionResultRecord {
|
||||
StorySessionResultRecord {
|
||||
session: build_story_session_record(session),
|
||||
event: build_story_event_record(event),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_story_session_state_record(
|
||||
session: StorySessionSnapshot,
|
||||
events: Vec<StoryEventSnapshot>,
|
||||
) -> StorySessionStateRecord {
|
||||
StorySessionStateRecord {
|
||||
session: build_story_session_record(session),
|
||||
events: events
|
||||
.into_iter()
|
||||
.map(build_story_event_record)
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,148 @@
|
||||
//! 剧情写入命令过渡落位。
|
||||
//! 剧情写入命令与输入归一化。
|
||||
//!
|
||||
//! 用于表达开启剧情会话、继续剧情和归档会话等输入。
|
||||
//! 命令层只处理 story session scoped 的输入结构、字段裁剪和基础校验,不读取数据库,
|
||||
//! 也不兼容旧 `/api/runtime/story/*` 总入口。
|
||||
|
||||
use crate::errors::StorySessionFieldError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::{
|
||||
normalize_optional_string as normalize_shared_optional_string, normalize_required_string,
|
||||
};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionInput {
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
pub opening_summary: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StoryContinueInput {
|
||||
pub story_session_id: String,
|
||||
pub event_id: String,
|
||||
pub narrative_text: String,
|
||||
pub choice_function_id: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionStateInput {
|
||||
pub story_session_id: String,
|
||||
}
|
||||
|
||||
pub fn build_story_session_input(
|
||||
story_session_id: String,
|
||||
runtime_session_id: String,
|
||||
actor_user_id: String,
|
||||
world_profile_id: String,
|
||||
initial_prompt: String,
|
||||
opening_summary: Option<String>,
|
||||
created_at_micros: i64,
|
||||
) -> Result<StorySessionInput, StorySessionFieldError> {
|
||||
let input = StorySessionInput {
|
||||
story_session_id: normalize_required_string(story_session_id).unwrap_or_default(),
|
||||
runtime_session_id: normalize_required_string(runtime_session_id).unwrap_or_default(),
|
||||
actor_user_id: normalize_required_string(actor_user_id).unwrap_or_default(),
|
||||
world_profile_id: normalize_required_string(world_profile_id).unwrap_or_default(),
|
||||
initial_prompt: normalize_required_string(initial_prompt).unwrap_or_default(),
|
||||
opening_summary: normalize_optional_value(opening_summary),
|
||||
created_at_micros,
|
||||
};
|
||||
|
||||
validate_story_session_input(&input)?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn build_story_session_state_input(
|
||||
story_session_id: String,
|
||||
) -> Result<StorySessionStateInput, StorySessionFieldError> {
|
||||
let input = StorySessionStateInput {
|
||||
story_session_id: normalize_required_string(story_session_id).unwrap_or_default(),
|
||||
};
|
||||
|
||||
validate_story_session_state_input(&input)?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn build_story_continue_input(
|
||||
story_session_id: String,
|
||||
event_id: String,
|
||||
narrative_text: String,
|
||||
choice_function_id: Option<String>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<StoryContinueInput, StorySessionFieldError> {
|
||||
let input = StoryContinueInput {
|
||||
story_session_id: normalize_required_string(story_session_id).unwrap_or_default(),
|
||||
event_id: normalize_required_string(event_id).unwrap_or_default(),
|
||||
narrative_text: normalize_required_string(narrative_text).unwrap_or_default(),
|
||||
choice_function_id: normalize_optional_value(choice_function_id),
|
||||
updated_at_micros,
|
||||
};
|
||||
|
||||
validate_story_continue_input(&input)?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn validate_story_session_input(
|
||||
input: &StorySessionInput,
|
||||
) -> Result<(), StorySessionFieldError> {
|
||||
if normalize_required_string(&input.story_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.runtime_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingRuntimeSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.actor_user_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingActorUserId);
|
||||
}
|
||||
if normalize_required_string(&input.world_profile_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingWorldProfileId);
|
||||
}
|
||||
if normalize_required_string(&input.initial_prompt).is_none() {
|
||||
return Err(StorySessionFieldError::MissingInitialPrompt);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_story_session_state_input(
|
||||
input: &StorySessionStateInput,
|
||||
) -> Result<(), StorySessionFieldError> {
|
||||
if normalize_required_string(&input.story_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingSessionId);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_story_continue_input(
|
||||
input: &StoryContinueInput,
|
||||
) -> Result<(), StorySessionFieldError> {
|
||||
if normalize_required_string(&input.story_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.event_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingEventId);
|
||||
}
|
||||
if normalize_required_string(&input.narrative_text).is_none() {
|
||||
return Err(StorySessionFieldError::MissingNarrativeText);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalize_optional_value(value: Option<String>) -> Option<String> {
|
||||
normalize_shared_optional_string(value)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,55 @@
|
||||
//! 剧情领域模型过渡落位。
|
||||
//! 剧情会话领域模型。
|
||||
//!
|
||||
//! 后续迁移 `StorySession`、`StoryEvent` 和剧情推进规则时,只保留剧情聚合内部变化;
|
||||
//! LLM 生成和 SpacetimeDB 写回由外层 adapter 处理。
|
||||
//! 这里仅保存 RPG story session 聚合内的稳定状态和值对象;LLM 生成、HTTP 回包和
|
||||
//! SpacetimeDB 写回都留给外层 adapter,不在领域模型里直接发生副作用。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::build_prefixed_seed_id;
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
/// 剧情会话 ID 的稳定前缀,统一放在领域层,避免 adapter 重复拼接。
|
||||
pub const STORY_SESSION_ID_PREFIX: &str = "storysess_";
|
||||
/// 新建剧情会话快照的初始版本号。
|
||||
pub const INITIAL_STORY_SESSION_VERSION: u32 = 1;
|
||||
|
||||
/// 剧情会话状态,用于判断后续 story command 是否还能继续推进。
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum StorySessionStatus {
|
||||
Active,
|
||||
Completed,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl StorySessionStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Active => "active",
|
||||
Self::Completed => "completed",
|
||||
Self::Archived => "archived",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// story session 的领域快照,SpacetimeDB row 与 HTTP DTO 都从它映射。
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionSnapshot {
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
pub opening_summary: Option<String>,
|
||||
pub latest_narrative_text: String,
|
||||
pub latest_choice_function_id: Option<String>,
|
||||
pub status: StorySessionStatus,
|
||||
pub version: u32,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
pub fn generate_story_session_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(STORY_SESSION_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
//! 剧情领域错误过渡落位。
|
||||
//! 剧情领域错误。
|
||||
//!
|
||||
//! 错误保持纯剧情规则语义,例如会话不存在、状态不允许或输入为空。
|
||||
//! 错误保持纯 story session 规则语义,例如会话字段缺失、事件内容为空或版本非法。
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum StorySessionFieldError {
|
||||
MissingSessionId,
|
||||
MissingRuntimeSessionId,
|
||||
MissingActorUserId,
|
||||
MissingWorldProfileId,
|
||||
MissingInitialPrompt,
|
||||
MissingNarrativeText,
|
||||
MissingEventId,
|
||||
InvalidVersion,
|
||||
}
|
||||
|
||||
impl fmt::Display for StorySessionFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingSessionId => f.write_str("story_session.story_session_id 不能为空"),
|
||||
Self::MissingRuntimeSessionId => {
|
||||
f.write_str("story_session.runtime_session_id 不能为空")
|
||||
}
|
||||
Self::MissingActorUserId => f.write_str("story_session.actor_user_id 不能为空"),
|
||||
Self::MissingWorldProfileId => f.write_str("story_session.world_profile_id 不能为空"),
|
||||
Self::MissingInitialPrompt => f.write_str("story_session.initial_prompt 不能为空"),
|
||||
Self::MissingNarrativeText => f.write_str("story_event.narrative_text 不能为空"),
|
||||
Self::MissingEventId => f.write_str("story_event.event_id 不能为空"),
|
||||
Self::InvalidVersion => f.write_str("story_session.version 必须大于 0"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StorySessionFieldError {}
|
||||
|
||||
@@ -1,3 +1,59 @@
|
||||
//! 剧情领域事件过渡落位。
|
||||
//! 剧情领域事件。
|
||||
//!
|
||||
//! 用于表达剧情会话已开启、剧情已推进和剧情事件已追加等事实。
|
||||
//! 事件只表达 story session 已经发生的领域事实;是否写入 SpacetimeDB event table 或
|
||||
//! 映射成 HTTP/SSE 输出,由外层 adapter 决定。
|
||||
|
||||
use crate::domain::StorySessionSnapshot;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::build_prefixed_seed_id;
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
/// 剧情事件 ID 的稳定前缀。
|
||||
pub const STORY_EVENT_ID_PREFIX: &str = "storyevt_";
|
||||
|
||||
/// 剧情事件类型,当前覆盖开局和续写两条最小主链。
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum StoryEventKind {
|
||||
SessionStarted,
|
||||
StoryContinued,
|
||||
}
|
||||
|
||||
impl StoryEventKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::SessionStarted => "session_started",
|
||||
Self::StoryContinued => "story_continued",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StoryEventSnapshot {
|
||||
pub event_id: String,
|
||||
pub story_session_id: String,
|
||||
pub event_kind: StoryEventKind,
|
||||
pub narrative_text: String,
|
||||
pub choice_function_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
pub fn build_story_started_event(snapshot: &StorySessionSnapshot) -> StoryEventSnapshot {
|
||||
StoryEventSnapshot {
|
||||
event_id: generate_story_event_id(snapshot.created_at_micros),
|
||||
story_session_id: snapshot.story_session_id.clone(),
|
||||
event_kind: StoryEventKind::SessionStarted,
|
||||
narrative_text: snapshot
|
||||
.opening_summary
|
||||
.clone()
|
||||
.unwrap_or_else(|| snapshot.initial_prompt.clone()),
|
||||
choice_function_id: None,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_story_event_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(STORY_EVENT_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
@@ -4,424 +4,11 @@ mod domain;
|
||||
mod errors;
|
||||
mod events;
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::{
|
||||
build_prefixed_seed_id, format_timestamp_micros,
|
||||
normalize_optional_string as normalize_shared_optional_string, normalize_required_string,
|
||||
};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const STORY_SESSION_ID_PREFIX: &str = "storysess_";
|
||||
pub const STORY_EVENT_ID_PREFIX: &str = "storyevt_";
|
||||
pub const INITIAL_STORY_SESSION_VERSION: u32 = 1;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum StorySessionStatus {
|
||||
Active,
|
||||
Completed,
|
||||
Archived,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum StoryEventKind {
|
||||
SessionStarted,
|
||||
StoryContinued,
|
||||
}
|
||||
|
||||
impl StorySessionStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Active => "active",
|
||||
Self::Completed => "completed",
|
||||
Self::Archived => "archived",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StoryEventKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::SessionStarted => "session_started",
|
||||
Self::StoryContinued => "story_continued",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum StorySessionFieldError {
|
||||
MissingSessionId,
|
||||
MissingRuntimeSessionId,
|
||||
MissingActorUserId,
|
||||
MissingWorldProfileId,
|
||||
MissingInitialPrompt,
|
||||
MissingNarrativeText,
|
||||
MissingEventId,
|
||||
InvalidVersion,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionInput {
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
pub opening_summary: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionSnapshot {
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
pub opening_summary: Option<String>,
|
||||
pub latest_narrative_text: String,
|
||||
pub latest_choice_function_id: Option<String>,
|
||||
pub status: StorySessionStatus,
|
||||
pub version: u32,
|
||||
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 StoryContinueInput {
|
||||
pub story_session_id: String,
|
||||
pub event_id: String,
|
||||
pub narrative_text: String,
|
||||
pub choice_function_id: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionStateInput {
|
||||
pub story_session_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StoryEventSnapshot {
|
||||
pub event_id: String,
|
||||
pub story_session_id: String,
|
||||
pub event_kind: StoryEventKind,
|
||||
pub narrative_text: String,
|
||||
pub choice_function_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session: Option<StorySessionSnapshot>,
|
||||
pub event: Option<StoryEventSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StorySessionStateProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session: Option<StorySessionSnapshot>,
|
||||
pub events: Vec<StoryEventSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StorySessionRecord {
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
pub opening_summary: Option<String>,
|
||||
pub latest_narrative_text: String,
|
||||
pub latest_choice_function_id: Option<String>,
|
||||
pub status: String,
|
||||
pub version: u32,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoryEventRecord {
|
||||
pub event_id: String,
|
||||
pub story_session_id: String,
|
||||
pub event_kind: String,
|
||||
pub narrative_text: String,
|
||||
pub choice_function_id: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StorySessionResultRecord {
|
||||
pub session: StorySessionRecord,
|
||||
pub event: StoryEventRecord,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StorySessionStateRecord {
|
||||
pub session: StorySessionRecord,
|
||||
pub events: Vec<StoryEventRecord>,
|
||||
}
|
||||
|
||||
pub fn build_story_session_input(
|
||||
story_session_id: String,
|
||||
runtime_session_id: String,
|
||||
actor_user_id: String,
|
||||
world_profile_id: String,
|
||||
initial_prompt: String,
|
||||
opening_summary: Option<String>,
|
||||
created_at_micros: i64,
|
||||
) -> Result<StorySessionInput, StorySessionFieldError> {
|
||||
let input = StorySessionInput {
|
||||
story_session_id: normalize_required_string(story_session_id).unwrap_or_default(),
|
||||
runtime_session_id: normalize_required_string(runtime_session_id).unwrap_or_default(),
|
||||
actor_user_id: normalize_required_string(actor_user_id).unwrap_or_default(),
|
||||
world_profile_id: normalize_required_string(world_profile_id).unwrap_or_default(),
|
||||
initial_prompt: normalize_required_string(initial_prompt).unwrap_or_default(),
|
||||
opening_summary: normalize_optional_value(opening_summary),
|
||||
created_at_micros,
|
||||
};
|
||||
|
||||
validate_story_session_input(&input)?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn build_story_session_state_input(
|
||||
story_session_id: String,
|
||||
) -> Result<StorySessionStateInput, StorySessionFieldError> {
|
||||
let input = StorySessionStateInput {
|
||||
story_session_id: normalize_required_string(story_session_id).unwrap_or_default(),
|
||||
};
|
||||
|
||||
validate_story_session_state_input(&input)?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn build_story_continue_input(
|
||||
story_session_id: String,
|
||||
event_id: String,
|
||||
narrative_text: String,
|
||||
choice_function_id: Option<String>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<StoryContinueInput, StorySessionFieldError> {
|
||||
let input = StoryContinueInput {
|
||||
story_session_id: normalize_required_string(story_session_id).unwrap_or_default(),
|
||||
event_id: normalize_required_string(event_id).unwrap_or_default(),
|
||||
narrative_text: normalize_required_string(narrative_text).unwrap_or_default(),
|
||||
choice_function_id: normalize_optional_value(choice_function_id),
|
||||
updated_at_micros,
|
||||
};
|
||||
|
||||
validate_story_continue_input(&input)?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn validate_story_session_input(
|
||||
input: &StorySessionInput,
|
||||
) -> Result<(), StorySessionFieldError> {
|
||||
if normalize_required_string(&input.story_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.runtime_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingRuntimeSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.actor_user_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingActorUserId);
|
||||
}
|
||||
if normalize_required_string(&input.world_profile_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingWorldProfileId);
|
||||
}
|
||||
if normalize_required_string(&input.initial_prompt).is_none() {
|
||||
return Err(StorySessionFieldError::MissingInitialPrompt);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_story_session_state_input(
|
||||
input: &StorySessionStateInput,
|
||||
) -> Result<(), StorySessionFieldError> {
|
||||
if normalize_required_string(&input.story_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingSessionId);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_story_continue_input(
|
||||
input: &StoryContinueInput,
|
||||
) -> Result<(), StorySessionFieldError> {
|
||||
if normalize_required_string(&input.story_session_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.event_id).is_none() {
|
||||
return Err(StorySessionFieldError::MissingEventId);
|
||||
}
|
||||
if normalize_required_string(&input.narrative_text).is_none() {
|
||||
return Err(StorySessionFieldError::MissingNarrativeText);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_story_session_snapshot(input: StorySessionInput) -> StorySessionSnapshot {
|
||||
StorySessionSnapshot {
|
||||
story_session_id: input.story_session_id,
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
world_profile_id: input.world_profile_id,
|
||||
initial_prompt: input.initial_prompt,
|
||||
opening_summary: normalize_optional_value(input.opening_summary),
|
||||
latest_narrative_text: String::new(),
|
||||
latest_choice_function_id: None,
|
||||
status: StorySessionStatus::Active,
|
||||
version: INITIAL_STORY_SESSION_VERSION,
|
||||
created_at_micros: input.created_at_micros,
|
||||
updated_at_micros: input.created_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_story_started_event(snapshot: &StorySessionSnapshot) -> StoryEventSnapshot {
|
||||
StoryEventSnapshot {
|
||||
event_id: generate_story_event_id(snapshot.created_at_micros),
|
||||
story_session_id: snapshot.story_session_id.clone(),
|
||||
event_kind: StoryEventKind::SessionStarted,
|
||||
narrative_text: snapshot
|
||||
.opening_summary
|
||||
.clone()
|
||||
.unwrap_or_else(|| snapshot.initial_prompt.clone()),
|
||||
choice_function_id: None,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_story_continue(
|
||||
current: StorySessionSnapshot,
|
||||
input: StoryContinueInput,
|
||||
) -> Result<(StorySessionSnapshot, StoryEventSnapshot), StorySessionFieldError> {
|
||||
validate_story_continue_input(&input)?;
|
||||
|
||||
if current.version == 0 {
|
||||
return Err(StorySessionFieldError::InvalidVersion);
|
||||
}
|
||||
|
||||
let event = StoryEventSnapshot {
|
||||
event_id: input.event_id,
|
||||
story_session_id: current.story_session_id.clone(),
|
||||
event_kind: StoryEventKind::StoryContinued,
|
||||
narrative_text: input.narrative_text.clone(),
|
||||
choice_function_id: normalize_optional_value(input.choice_function_id),
|
||||
created_at_micros: input.updated_at_micros,
|
||||
};
|
||||
|
||||
let next = StorySessionSnapshot {
|
||||
latest_narrative_text: input.narrative_text,
|
||||
latest_choice_function_id: event.choice_function_id.clone(),
|
||||
version: current.version + 1,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
..current
|
||||
};
|
||||
|
||||
Ok((next, event))
|
||||
}
|
||||
|
||||
pub fn generate_story_session_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(STORY_SESSION_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
pub fn generate_story_event_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(STORY_EVENT_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
pub fn build_story_session_record(snapshot: StorySessionSnapshot) -> StorySessionRecord {
|
||||
StorySessionRecord {
|
||||
story_session_id: snapshot.story_session_id,
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
world_profile_id: snapshot.world_profile_id,
|
||||
initial_prompt: snapshot.initial_prompt,
|
||||
opening_summary: snapshot.opening_summary,
|
||||
latest_narrative_text: snapshot.latest_narrative_text,
|
||||
latest_choice_function_id: snapshot.latest_choice_function_id,
|
||||
status: snapshot.status.as_str().to_string(),
|
||||
version: snapshot.version,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_story_event_record(snapshot: StoryEventSnapshot) -> StoryEventRecord {
|
||||
StoryEventRecord {
|
||||
event_id: snapshot.event_id,
|
||||
story_session_id: snapshot.story_session_id,
|
||||
event_kind: snapshot.event_kind.as_str().to_string(),
|
||||
narrative_text: snapshot.narrative_text,
|
||||
choice_function_id: snapshot.choice_function_id,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_story_session_result_record(
|
||||
session: StorySessionSnapshot,
|
||||
event: StoryEventSnapshot,
|
||||
) -> StorySessionResultRecord {
|
||||
StorySessionResultRecord {
|
||||
session: build_story_session_record(session),
|
||||
event: build_story_event_record(event),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_story_session_state_record(
|
||||
session: StorySessionSnapshot,
|
||||
events: Vec<StoryEventSnapshot>,
|
||||
) -> StorySessionStateRecord {
|
||||
StorySessionStateRecord {
|
||||
session: build_story_session_record(session),
|
||||
events: events
|
||||
.into_iter()
|
||||
.map(build_story_event_record)
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_optional_value(value: Option<String>) -> Option<String> {
|
||||
normalize_shared_optional_string(value)
|
||||
}
|
||||
|
||||
impl fmt::Display for StorySessionFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingSessionId => f.write_str("story_session.story_session_id 不能为空"),
|
||||
Self::MissingRuntimeSessionId => {
|
||||
f.write_str("story_session.runtime_session_id 不能为空")
|
||||
}
|
||||
Self::MissingActorUserId => f.write_str("story_session.actor_user_id 不能为空"),
|
||||
Self::MissingWorldProfileId => f.write_str("story_session.world_profile_id 不能为空"),
|
||||
Self::MissingInitialPrompt => f.write_str("story_session.initial_prompt 不能为空"),
|
||||
Self::MissingNarrativeText => f.write_str("story_event.narrative_text 不能为空"),
|
||||
Self::MissingEventId => f.write_str("story_event.event_id 不能为空"),
|
||||
Self::InvalidVersion => f.write_str("story_session.version 必须大于 0"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StorySessionFieldError {}
|
||||
pub use application::*;
|
||||
pub use commands::*;
|
||||
pub use domain::*;
|
||||
pub use errors::*;
|
||||
pub use events::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
Reference in New Issue
Block a user