1
This commit is contained in:
225
server-rs/crates/module-creative-agent/src/application.rs
Normal file
225
server-rs/crates/module-creative-agent/src/application.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use shared_kernel::{normalize_optional_string, normalize_required_string};
|
||||
|
||||
use crate::{
|
||||
CreativeAgentError, CreativeAgentMessageAppendInput, CreativeAgentMessageKind,
|
||||
CreativeAgentMessageRole, CreativeAgentStage, CreativeAgentStageUpdateInput,
|
||||
CreativeAgentTargetBindInput, CreativeAgentTemplateConfirmInput, CreativeTargetPlayType,
|
||||
};
|
||||
|
||||
pub fn validate_create_session(
|
||||
session_id: &str,
|
||||
owner_user_id: &str,
|
||||
) -> Result<(String, String), CreativeAgentError> {
|
||||
let session_id =
|
||||
normalize_required_string(session_id).ok_or(CreativeAgentError::MissingSessionId)?;
|
||||
let owner_user_id =
|
||||
normalize_required_string(owner_user_id).ok_or(CreativeAgentError::MissingOwnerUserId)?;
|
||||
Ok((session_id, owner_user_id))
|
||||
}
|
||||
|
||||
pub fn validate_append_message(
|
||||
input: &CreativeAgentMessageAppendInput,
|
||||
) -> Result<(), CreativeAgentError> {
|
||||
validate_create_session(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.message_id).is_none() {
|
||||
return Err(CreativeAgentError::MissingMessageId);
|
||||
}
|
||||
if normalize_required_string(&input.text).is_none() {
|
||||
return Err(CreativeAgentError::MissingMessageText);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_stage_update(
|
||||
current: CreativeAgentStage,
|
||||
input: &CreativeAgentStageUpdateInput,
|
||||
) -> Result<(), CreativeAgentError> {
|
||||
validate_create_session(&input.session_id, &input.owner_user_id)?;
|
||||
validate_stage_transition(current, input.stage)
|
||||
}
|
||||
|
||||
pub fn validate_template_confirmation(
|
||||
current: CreativeAgentStage,
|
||||
input: &CreativeAgentTemplateConfirmInput,
|
||||
) -> Result<(), CreativeAgentError> {
|
||||
validate_create_session(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.template_selection_json).is_none() {
|
||||
return Err(CreativeAgentError::MissingTemplateSelection);
|
||||
}
|
||||
if !input.template_selection_json.contains("\"costRange\"")
|
||||
&& !input.template_selection_json.contains("\"cost_range\"")
|
||||
{
|
||||
return Err(CreativeAgentError::MissingCostRange);
|
||||
}
|
||||
validate_stage_transition(current, CreativeAgentStage::PlanningPuzzleLevels)
|
||||
}
|
||||
|
||||
pub fn validate_target_binding(
|
||||
current_stage: CreativeAgentStage,
|
||||
template_selection_json: Option<&str>,
|
||||
input: &CreativeAgentTargetBindInput,
|
||||
) -> Result<(), CreativeAgentError> {
|
||||
validate_create_session(&input.session_id, &input.owner_user_id)?;
|
||||
if input.play_type != CreativeTargetPlayType::Puzzle {
|
||||
return Err(CreativeAgentError::UnsupportedTargetPlayType);
|
||||
}
|
||||
if normalize_required_string(&input.target_session_id).is_none() {
|
||||
return Err(CreativeAgentError::MissingTargetSessionId);
|
||||
}
|
||||
if normalize_optional_string(template_selection_json.map(str::to_string)).is_none() {
|
||||
return Err(CreativeAgentError::TemplateNotConfirmed);
|
||||
}
|
||||
// 中文注释:绑定目标 session 是“草稿已创建”的持久化标记,只允许在行动链路之后发生。
|
||||
if !matches!(
|
||||
current_stage,
|
||||
CreativeAgentStage::PlanningPuzzleLevels
|
||||
| CreativeAgentStage::Acting
|
||||
| CreativeAgentStage::Reflecting
|
||||
| CreativeAgentStage::Collaborating
|
||||
| CreativeAgentStage::TargetReady
|
||||
) {
|
||||
return Err(CreativeAgentError::InvalidStageTransition);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_stage_transition(
|
||||
current: CreativeAgentStage,
|
||||
next: CreativeAgentStage,
|
||||
) -> Result<(), CreativeAgentError> {
|
||||
if current == next {
|
||||
return Ok(());
|
||||
}
|
||||
if matches!(
|
||||
next,
|
||||
CreativeAgentStage::Failed | CreativeAgentStage::WaitingUser
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
let allowed = matches!(
|
||||
(current, next),
|
||||
(CreativeAgentStage::Idle, CreativeAgentStage::Perceiving)
|
||||
| (
|
||||
CreativeAgentStage::Idle,
|
||||
CreativeAgentStage::SelectingPuzzleTemplate
|
||||
)
|
||||
| (CreativeAgentStage::Perceiving, CreativeAgentStage::Thinking)
|
||||
| (
|
||||
CreativeAgentStage::Thinking,
|
||||
CreativeAgentStage::Remembering
|
||||
)
|
||||
| (
|
||||
CreativeAgentStage::Thinking,
|
||||
CreativeAgentStage::SelectingPuzzleTemplate
|
||||
)
|
||||
| (
|
||||
CreativeAgentStage::Remembering,
|
||||
CreativeAgentStage::SelectingPuzzleTemplate
|
||||
)
|
||||
| (
|
||||
CreativeAgentStage::SelectingPuzzleTemplate,
|
||||
CreativeAgentStage::WaitingTemplateConfirmation
|
||||
)
|
||||
| (
|
||||
CreativeAgentStage::WaitingTemplateConfirmation,
|
||||
CreativeAgentStage::PlanningPuzzleLevels
|
||||
)
|
||||
| (
|
||||
CreativeAgentStage::PlanningPuzzleLevels,
|
||||
CreativeAgentStage::Acting
|
||||
)
|
||||
| (CreativeAgentStage::Acting, CreativeAgentStage::Reflecting)
|
||||
| (
|
||||
CreativeAgentStage::Reflecting,
|
||||
CreativeAgentStage::Collaborating
|
||||
)
|
||||
| (
|
||||
CreativeAgentStage::Collaborating,
|
||||
CreativeAgentStage::Acting
|
||||
)
|
||||
| (
|
||||
CreativeAgentStage::Reflecting,
|
||||
CreativeAgentStage::TargetReady
|
||||
)
|
||||
| (CreativeAgentStage::Acting, CreativeAgentStage::TargetReady)
|
||||
| (
|
||||
CreativeAgentStage::PlanningPuzzleLevels,
|
||||
CreativeAgentStage::TargetReady
|
||||
)
|
||||
);
|
||||
if allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CreativeAgentError::InvalidStageTransition)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_message_role(value: CreativeAgentMessageRole) -> &'static str {
|
||||
value.as_str()
|
||||
}
|
||||
|
||||
pub fn normalize_message_kind(value: CreativeAgentMessageKind) -> &'static str {
|
||||
value.as_str()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{CreativeAgentTargetBindInput, CreativeTargetStage};
|
||||
|
||||
#[test]
|
||||
fn template_confirmation_requires_cost_range() {
|
||||
let input = CreativeAgentTemplateConfirmInput {
|
||||
session_id: "creative-session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
template_selection_json: r#"{"templateId":"puzzle.default-creative"}"#.to_string(),
|
||||
updated_at_micros: 1,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
validate_template_confirmation(CreativeAgentStage::WaitingTemplateConfirmation, &input,),
|
||||
Err(CreativeAgentError::MissingCostRange)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_binding_requires_confirmed_template() {
|
||||
let input = CreativeAgentTargetBindInput {
|
||||
binding_id: "creative-binding-1".to_string(),
|
||||
session_id: "creative-session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
play_type: CreativeTargetPlayType::Puzzle,
|
||||
target_session_id: "puzzle-session-1".to_string(),
|
||||
target_stage: CreativeTargetStage::PuzzleResult,
|
||||
result_profile_id: None,
|
||||
created_at_micros: 1,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
validate_target_binding(CreativeAgentStage::Acting, None, &input),
|
||||
Err(CreativeAgentError::TemplateNotConfirmed)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase1_stage_path_allows_template_to_target_ready() {
|
||||
assert!(
|
||||
validate_stage_transition(
|
||||
CreativeAgentStage::WaitingTemplateConfirmation,
|
||||
CreativeAgentStage::PlanningPuzzleLevels,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
validate_stage_transition(
|
||||
CreativeAgentStage::PlanningPuzzleLevels,
|
||||
CreativeAgentStage::Acting
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
validate_stage_transition(CreativeAgentStage::Acting, CreativeAgentStage::TargetReady)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user