use crate::puzzle_creative_template::{ PuzzleCreativeTemplateProtocol, PuzzleCreativeTemplateSelection, PuzzleDraftFieldPatch, PuzzleImageGenerationPlan, PuzzleTemplateCostRange, }; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CreativeAgentStage { Idle, Perceiving, Thinking, Remembering, SelectingPuzzleTemplate, WaitingTemplateConfirmation, PlanningPuzzleLevels, Acting, Reflecting, Collaborating, TargetReady, WaitingUser, Failed, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CreativeAgentEntryContext { CreationHome, PuzzleWorkspace, GalleryRemix, DraftRestore, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum CreativeAgentMessageRole { User, Assistant, System, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CreativeAgentMessageKind { Chat, Stage, ActionResult, Warning, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CreativeAgentInputPartType { InputText, InputImage, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentInputPart { #[serde(rename = "type")] pub part_type: CreativeAgentInputPartType, #[serde(default)] pub text: Option, #[serde(default)] pub image_url: Option, #[serde(default)] pub asset_id: Option, #[serde(default)] pub thumbnail_url: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeImageInput { pub asset_id: String, pub read_url: String, #[serde(default)] pub thumbnail_url: Option, #[serde(default)] pub width: Option, #[serde(default)] pub height: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeImageSummary { #[serde(default)] pub asset_id: Option, #[serde(default)] pub read_url: Option, #[serde(default)] pub thumbnail_url: Option, #[serde(default)] pub width: Option, #[serde(default)] pub height: Option, #[serde(default)] pub summary: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CreativeUnsupportedPlayType { Rpg, #[serde(rename = "match3d")] Match3d, BigFish, SquareHole, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum CreativeCapabilityStatus { Unsupported, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeUnsupportedCapability { pub play_type: CreativeUnsupportedPlayType, pub title: String, pub status: CreativeCapabilityStatus, pub reason: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeInputSummary { #[serde(default)] pub text: Option, pub entry_context: CreativeAgentEntryContext, pub images: Vec, #[serde(default)] pub material_summary: Option, pub unsupported_capabilities: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentMessage { pub id: String, pub role: CreativeAgentMessageRole, pub kind: CreativeAgentMessageKind, pub text: String, pub created_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum CreativeTargetPlayType { Puzzle, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum CreativeTargetStage { PuzzleAgentWorkspace, PuzzleResult, PuzzleRuntime, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeTargetSessionBinding { pub play_type: CreativeTargetPlayType, pub target_session_id: String, pub target_stage: CreativeTargetStage, #[serde(default)] pub result_profile_id: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentSessionSnapshot { pub session_id: String, pub stage: CreativeAgentStage, pub input_summary: CreativeInputSummary, pub messages: Vec, #[serde(default)] pub puzzle_template_catalog: Vec, #[serde(default)] pub puzzle_template_selection: Option, #[serde(default)] pub puzzle_image_generation_plan: Option, #[serde(default)] pub target_binding: Option, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreateCreativeAgentSessionRequest { #[serde(default)] pub text: Option, #[serde(default)] pub images: Vec, #[serde(default)] pub entry_context: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentSessionResponse { pub session: CreativeAgentSessionSnapshot, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct StreamCreativeAgentMessageRequest { pub client_message_id: String, pub content: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ConfirmCreativePuzzleTemplateRequest { pub selection: PuzzleCreativeTemplateSelection, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeDraftEditStreamRequest { pub client_message_id: String, pub instruction: String, pub target_puzzle_session_id: String, pub current_draft: serde_json::Value, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeDraftEditResult { pub edit_instructions: Vec, pub session: CreativeAgentSessionSnapshot, pub puzzle_session: serde_json::Value, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CreativeAgentSseEventType { Stage, AgentMessageDelta, ThoughtSummaryDelta, PuzzleTemplateCatalog, PuzzleTemplateSelection, PuzzleCostRange, PuzzleLevelPlan, ToolStarted, ToolCompleted, Reflection, TargetSession, Session, Error, Done, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentSseEnvelope { pub event: CreativeAgentSseEventType, pub data: serde_json::Value, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentStageEvent { pub session_id: String, pub stage: CreativeAgentStage, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentMessageDeltaEvent { pub session_id: String, pub message_id: String, pub role: CreativeAgentMessageRole, pub kind: CreativeAgentMessageKind, pub text_delta: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentThoughtSummaryDeltaEvent { pub session_id: String, pub thought_id: String, pub text_delta: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentTemplateCatalogEvent { pub session_id: String, pub templates: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentTemplateSelectionEvent { pub session_id: String, pub selection: PuzzleCreativeTemplateSelection, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentCostRangeEvent { pub session_id: String, pub cost_range: PuzzleTemplateCostRange, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentLevelPlanEvent { pub session_id: String, pub plan: PuzzleImageGenerationPlan, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentToolEvent { pub session_id: String, pub tool_call_id: String, pub tool_name: String, #[serde(default)] pub summary: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentReflectionEvent { pub session_id: String, pub pass: bool, pub summary: String, pub warnings: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentTargetSessionEvent { pub session_id: String, pub binding: CreativeTargetSessionBinding, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentErrorEvent { #[serde(default)] pub session_id: Option, pub code: String, pub message: String, pub recoverable: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativeAgentDoneEvent { pub session_id: String, } #[cfg(test)] mod tests { use super::*; use crate::puzzle_creative_template::{ PuzzleCreativeTemplateProtocol, PuzzleDraftEditableFieldPath, PuzzleLevelGenerationMode, PuzzleSupportedLevelMode, PuzzleTemplateImageGenerationPolicy, PuzzleTemplatePricingUnit, }; use serde_json::json; fn cost_range() -> PuzzleTemplateCostRange { PuzzleTemplateCostRange { min_points: 2, max_points: 12, pricing_unit: PuzzleTemplatePricingUnit::Point, reason: "按关卡数和每关图片生成次数估算".to_string(), } } fn template_selection() -> PuzzleCreativeTemplateSelection { PuzzleCreativeTemplateSelection { template_id: "puzzle.default-creative".to_string(), title: "创意拼图".to_string(), reason: "素材适合拆成可试玩的拼图关卡".to_string(), cost_range: cost_range(), supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti, selected_level_mode: PuzzleLevelGenerationMode::SingleLevel, planned_level_count: 1, requires_user_confirmation: true, } } fn template_protocol() -> PuzzleCreativeTemplateProtocol { PuzzleCreativeTemplateProtocol { template_id: "puzzle.default-creative".to_string(), title: "创意拼图".to_string(), summary: "把图文灵感做成拼图".to_string(), preview_image_src: None, supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti, min_level_count: 1, max_level_count: 6, default_level_count: 1, cost_range: cost_range(), required_draft_fields: vec![PuzzleDraftEditableFieldPath::WorkTitle], image_policy: PuzzleTemplateImageGenerationPolicy { allow_uploaded_image_directly: true, allow_generated_images: true, allow_per_level_reference_image: true, default_candidate_count_per_level: 1, }, } } #[test] fn creative_agent_session_snapshot_uses_camel_case() { let snapshot = CreativeAgentSessionSnapshot { session_id: "creative-session-1".to_string(), stage: CreativeAgentStage::WaitingTemplateConfirmation, input_summary: CreativeInputSummary { text: Some("把这张旅行照做成拼图".to_string()), entry_context: CreativeAgentEntryContext::CreationHome, images: vec![CreativeImageSummary { asset_id: Some("asset-1".to_string()), read_url: Some("https://example.test/image.png".to_string()), thumbnail_url: None, width: Some(1024), height: Some(768), summary: Some("一张旅行照片".to_string()), }], material_summary: Some("旅行纪念素材".to_string()), unsupported_capabilities: vec![CreativeUnsupportedCapability { play_type: CreativeUnsupportedPlayType::BigFish, title: "大鱼吃小鱼".to_string(), status: CreativeCapabilityStatus::Unsupported, reason: "Phase 1 只开放拼图模板".to_string(), }], }, messages: vec![CreativeAgentMessage { id: "message-1".to_string(), role: CreativeAgentMessageRole::Assistant, kind: CreativeAgentMessageKind::Chat, text: "我会先选择拼图模板。".to_string(), created_at: "2026-05-05T00:00:00Z".to_string(), }], puzzle_template_catalog: vec![template_protocol()], puzzle_template_selection: Some(template_selection()), puzzle_image_generation_plan: None, target_binding: Some(CreativeTargetSessionBinding { play_type: CreativeTargetPlayType::Puzzle, target_session_id: "puzzle-session-1".to_string(), target_stage: CreativeTargetStage::PuzzleResult, result_profile_id: None, }), updated_at: "2026-05-05T00:00:01Z".to_string(), }; let payload = serde_json::to_value(&snapshot).expect("snapshot should serialize"); assert_eq!(payload["sessionId"], json!("creative-session-1")); assert_eq!(payload["stage"], json!("waiting_template_confirmation")); assert_eq!( payload["inputSummary"]["entryContext"], json!("creation_home") ); assert_eq!( payload["inputSummary"]["unsupportedCapabilities"][0]["playType"], json!("big_fish") ); assert_eq!( payload["puzzleTemplateCatalog"][0]["templateId"], json!("puzzle.default-creative") ); assert_eq!( payload["puzzleTemplateSelection"]["selectedLevelMode"], json!("single_level") ); assert_eq!( payload["targetBinding"]["targetStage"], json!("puzzle-result") ); let decoded: CreativeAgentSessionSnapshot = serde_json::from_value(payload).expect("snapshot should deserialize"); assert_eq!(decoded, snapshot); } #[test] fn creative_agent_sse_events_serialize_event_names() { let event = CreativeAgentSseEnvelope { event: CreativeAgentSseEventType::PuzzleTemplateSelection, data: serde_json::to_value(CreativeAgentTemplateSelectionEvent { session_id: "creative-session-1".to_string(), selection: template_selection(), }) .expect("event data should serialize"), }; let payload = serde_json::to_value(event).expect("event should serialize"); assert_eq!(payload["event"], json!("puzzle_template_selection")); assert_eq!( payload["data"]["selection"]["costRange"]["pricingUnit"], json!("point") ); let thought_event = CreativeAgentSseEnvelope { event: CreativeAgentSseEventType::ThoughtSummaryDelta, data: serde_json::to_value(CreativeAgentThoughtSummaryDeltaEvent { session_id: "creative-session-1".to_string(), thought_id: "thought-1".to_string(), text_delta: "正在理解素材".to_string(), }) .expect("event data should serialize"), }; let thought_payload = serde_json::to_value(thought_event).expect("event should serialize"); assert_eq!(thought_payload["event"], json!("thought_summary_delta")); assert_eq!(thought_payload["data"]["thoughtId"], json!("thought-1")); let catalog_event = CreativeAgentSseEnvelope { event: CreativeAgentSseEventType::PuzzleTemplateCatalog, data: serde_json::to_value(CreativeAgentTemplateCatalogEvent { session_id: "creative-session-1".to_string(), templates: vec![template_protocol()], }) .expect("event data should serialize"), }; let catalog_payload = serde_json::to_value(catalog_event).expect("event should serialize"); assert_eq!(catalog_payload["event"], json!("puzzle_template_catalog")); assert_eq!( catalog_payload["data"]["templates"][0]["templateId"], json!("puzzle.default-creative") ); } #[test] fn creative_agent_multimodal_parts_keep_image_url_camel_case() { let request = StreamCreativeAgentMessageRequest { client_message_id: "client-message-1".to_string(), content: vec![ CreativeAgentInputPart { part_type: CreativeAgentInputPartType::InputText, text: Some("做一张拼图".to_string()), image_url: None, asset_id: None, thumbnail_url: None, }, CreativeAgentInputPart { part_type: CreativeAgentInputPartType::InputImage, text: None, image_url: Some("https://example.test/image.png".to_string()), asset_id: Some("asset-1".to_string()), thumbnail_url: None, }, ], }; let payload = serde_json::to_value(request).expect("request should serialize"); assert_eq!(payload["clientMessageId"], json!("client-message-1")); assert_eq!(payload["content"][0]["type"], json!("input_text")); assert_eq!(payload["content"][1]["type"], json!("input_image")); assert_eq!( payload["content"][1]["imageUrl"], json!("https://example.test/image.png") ); } }