Files
Genarrative/server-rs/crates/module-creative-agent/src/application.rs
2026-05-08 11:44:42 +08:00

226 lines
7.7 KiB
Rust

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()
);
}
}