Files
Genarrative/server-rs/crates/shared-contracts/src/puzzle_creative_template.rs
2026-05-08 11:44:42 +08:00

231 lines
7.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}