231 lines
7.6 KiB
Rust
231 lines
7.6 KiB
Rust
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<String>,
|
||
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<PuzzleDraftEditableFieldPath>,
|
||
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<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
pub levels: Vec<CreativePuzzleLevelDraftInput>,
|
||
}
|
||
|
||
#[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<String>,
|
||
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<PuzzleImageGenerationPlanLevel>,
|
||
}
|
||
|
||
#[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<String>,
|
||
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);
|
||
}
|
||
}
|