use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum PuzzleTemplatePricingUnit { Point, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum PuzzleSupportedLevelMode { Single, Multi, SingleOrMulti, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum PuzzleLevelGenerationMode { SingleLevel, MultiLevel, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PuzzleTemplateCostRange { pub min_points: u32, pub max_points: u32, pub pricing_unit: PuzzleTemplatePricingUnit, pub reason: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum PuzzleDraftEditableFieldPath { #[serde(rename = "workTitle")] WorkTitle, #[serde(rename = "workDescription")] WorkDescription, #[serde(rename = "workTags")] WorkTags, #[serde(rename = "levels[].levelName")] LevelName, #[serde(rename = "levels[].pictureDescription")] LevelPictureDescription, #[serde(rename = "levels[].pictureReference")] LevelPictureReference, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PuzzleTemplateImageGenerationPolicy { pub allow_uploaded_image_directly: bool, pub allow_generated_images: bool, pub allow_per_level_reference_image: bool, pub default_candidate_count_per_level: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PuzzleCreativeTemplateProtocol { pub template_id: String, pub title: String, pub summary: String, #[serde(default)] pub preview_image_src: Option, pub supported_level_mode: PuzzleSupportedLevelMode, pub min_level_count: u32, pub max_level_count: u32, pub default_level_count: u32, pub cost_range: PuzzleTemplateCostRange, pub required_draft_fields: Vec, pub image_policy: PuzzleTemplateImageGenerationPolicy, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PuzzleCreativeTemplateSelection { pub template_id: String, pub title: String, pub reason: String, pub cost_range: PuzzleTemplateCostRange, pub supported_level_mode: PuzzleSupportedLevelMode, pub selected_level_mode: PuzzleLevelGenerationMode, pub planned_level_count: u32, pub requires_user_confirmation: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativePuzzleLevelDraftInput { pub level_name: String, pub picture_description: String, /// 任务 A 冻结:Phase 1 采用正式字段方案,后续拼图草稿落地需补正式 pictureReference 字段。 #[serde(default)] pub picture_reference: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreativePuzzleDraftToolInput { pub template_id: String, pub template_cost_range: PuzzleTemplateCostRange, pub work_title: String, pub work_description: String, pub work_tags: Vec, pub levels: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PuzzleImageGenerationPlanLevel { pub level_id: String, pub level_name: String, pub picture_description: String, pub image_prompt: String, #[serde(default)] pub picture_reference: Option, pub candidate_count: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PuzzleImageGenerationPlan { pub mode: PuzzleLevelGenerationMode, pub template_id: String, pub estimated_cost_range: PuzzleTemplateCostRange, pub levels: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum PuzzleDraftFieldPatchOperation { Set, Append, Replace, Remove, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PuzzleDraftFieldPatch { pub field_path: PuzzleDraftEditableFieldPath, pub operation: PuzzleDraftFieldPatchOperation, #[serde(default)] pub level_id: Option, pub value: serde_json::Value, pub rationale: String, } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn cost_range() -> PuzzleTemplateCostRange { PuzzleTemplateCostRange { min_points: 2, max_points: 12, pricing_unit: PuzzleTemplatePricingUnit::Point, reason: "按关卡数和每关图片生成次数估算".to_string(), } } #[test] fn creative_agent_puzzle_template_protocol_uses_camel_case() { let payload = serde_json::to_value(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, PuzzleDraftEditableFieldPath::LevelPictureReference, ], image_policy: PuzzleTemplateImageGenerationPolicy { allow_uploaded_image_directly: true, allow_generated_images: true, allow_per_level_reference_image: true, default_candidate_count_per_level: 1, }, }) .expect("template should serialize"); assert_eq!(payload["templateId"], json!("puzzle.default-creative")); assert_eq!(payload["previewImageSrc"], json!(null)); assert_eq!(payload["supportedLevelMode"], json!("single_or_multi")); assert_eq!(payload["costRange"]["pricingUnit"], json!("point")); assert_eq!( payload["requiredDraftFields"], json!(["workTitle", "levels[].pictureReference"]) ); assert_eq!( payload["imagePolicy"]["allowPerLevelReferenceImage"], json!(true) ); } #[test] fn creative_agent_puzzle_image_plan_roundtrips() { let plan = PuzzleImageGenerationPlan { mode: PuzzleLevelGenerationMode::MultiLevel, template_id: "puzzle.default-creative".to_string(), estimated_cost_range: cost_range(), levels: vec![PuzzleImageGenerationPlanLevel { level_id: "level-1".to_string(), level_name: "第一关".to_string(), picture_description: "温暖的家庭照片".to_string(), image_prompt: "pixel puzzle, warm family photo".to_string(), picture_reference: Some("asset-ref-1".to_string()), candidate_count: 1, }], }; let payload = serde_json::to_value(&plan).expect("plan should serialize"); assert_eq!(payload["mode"], json!("multi_level")); assert_eq!( payload["levels"][0]["pictureReference"], json!("asset-ref-1") ); let decoded: PuzzleImageGenerationPlan = serde_json::from_value(payload).expect("plan should deserialize"); assert_eq!(decoded, plan); } }