Files
Genarrative/server-rs/crates/module-puzzle/src/creative_templates.rs
2026-05-08 20:48:29 +08:00

205 lines
6.7 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.
//! 拼图创意 Agent 模板协议。
//!
//! 这里只保存拼图模块自己的模板事实HTTP / SSE 展示字段由 api-server
//! 再映射到 shared-contracts避免通用 Agent 复制拼图模板规则。
use serde::{Deserialize, Serialize};
pub const PUZZLE_PHASE1_TEMPLATE_ID: &str = "puzzle.default-creative";
pub const PUZZLE_PHASE1_TEMPLATE_TITLE: &str = "创意拼图";
pub const PUZZLE_FAMILY_KEEPSTAKE_TEMPLATE_ID: &str = "puzzle.family-keepsake";
pub const PUZZLE_TRAVEL_MEMORY_TEMPLATE_ID: &str = "puzzle.travel-memory";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PuzzleCreativePricingUnit {
Point,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleCreativeSupportedLevelMode {
Single,
Multi,
SingleOrMulti,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleCreativeLevelGenerationMode {
SingleLevel,
MultiLevel,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeCostRange {
pub min_points: u32,
pub max_points: u32,
pub pricing_unit: PuzzleCreativePricingUnit,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PuzzleCreativeDraftEditableFieldPath {
#[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, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeImageGenerationPolicy {
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, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeTemplateProtocol {
pub template_id: String,
pub title: String,
pub summary: String,
pub preview_image_src: Option<String>,
pub supported_level_mode: PuzzleCreativeSupportedLevelMode,
pub min_level_count: u32,
pub max_level_count: u32,
pub default_level_count: u32,
pub cost_range: PuzzleCreativeCostRange,
pub required_draft_fields: Vec<PuzzleCreativeDraftEditableFieldPath>,
pub image_policy: PuzzleCreativeImageGenerationPolicy,
}
fn shared_required_draft_fields() -> Vec<PuzzleCreativeDraftEditableFieldPath> {
vec![
PuzzleCreativeDraftEditableFieldPath::WorkTitle,
PuzzleCreativeDraftEditableFieldPath::WorkDescription,
PuzzleCreativeDraftEditableFieldPath::WorkTags,
PuzzleCreativeDraftEditableFieldPath::LevelName,
PuzzleCreativeDraftEditableFieldPath::LevelPictureDescription,
PuzzleCreativeDraftEditableFieldPath::LevelPictureReference,
]
}
fn shared_image_policy() -> PuzzleCreativeImageGenerationPolicy {
PuzzleCreativeImageGenerationPolicy {
allow_uploaded_image_directly: true,
allow_generated_images: true,
allow_per_level_reference_image: true,
default_candidate_count_per_level: 1,
}
}
fn build_template(
template_id: &str,
title: &str,
summary: &str,
default_level_count: u32,
min_points: u32,
max_points: u32,
reason: &str,
) -> PuzzleCreativeTemplateProtocol {
PuzzleCreativeTemplateProtocol {
template_id: template_id.to_string(),
title: title.to_string(),
summary: summary.to_string(),
preview_image_src: None,
supported_level_mode: PuzzleCreativeSupportedLevelMode::SingleOrMulti,
min_level_count: 1,
max_level_count: 6,
default_level_count,
cost_range: PuzzleCreativeCostRange {
min_points,
max_points,
pricing_unit: PuzzleCreativePricingUnit::Point,
reason: reason.to_string(),
},
required_draft_fields: shared_required_draft_fields(),
image_policy: shared_image_policy(),
}
}
pub fn retrieve_puzzle_template_catalog() -> Vec<PuzzleCreativeTemplateProtocol> {
vec![
build_template(
PUZZLE_PHASE1_TEMPLATE_ID,
PUZZLE_PHASE1_TEMPLATE_TITLE,
"把图文灵感整理成可编辑、可试玩的拼图草稿。",
1,
2,
12,
"按关卡数和每关图片生成次数估算,实际扣费以后端任务结算为准",
),
build_template(
PUZZLE_FAMILY_KEEPSTAKE_TEMPLATE_ID,
"家庭纪念拼图",
"把合影、节日或成长瞬间做成温暖的纪念拼图。",
3,
4,
14,
"按纪念主题多关卡和图片候选估算,实际扣费以后端任务结算为准",
),
build_template(
PUZZLE_TRAVEL_MEMORY_TEMPLATE_ID,
"旅行记忆拼图",
"把一次出行拆成地点、风景和故事节点拼图。",
3,
4,
16,
"按旅行节点和每关图片生成次数估算,实际扣费以后端任务结算为准",
),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phase1_template_contains_cost_range_and_editable_fields() {
let template = retrieve_puzzle_template_catalog()
.into_iter()
.find(|template| template.template_id == PUZZLE_PHASE1_TEMPLATE_ID)
.expect("template should exist");
assert_eq!(template.template_id, PUZZLE_PHASE1_TEMPLATE_ID);
assert_eq!(template.cost_range.min_points, 2);
assert_eq!(template.cost_range.max_points, 12);
assert!(
template
.required_draft_fields
.contains(&PuzzleCreativeDraftEditableFieldPath::LevelPictureReference)
);
}
#[test]
fn catalog_exposes_multiple_phase1_puzzle_subtemplates() {
let catalog = retrieve_puzzle_template_catalog();
assert!(catalog.len() >= 3);
assert!(
catalog
.iter()
.any(|template| template.template_id == PUZZLE_FAMILY_KEEPSTAKE_TEMPLATE_ID)
);
assert!(
catalog
.iter()
.any(|template| template.template_id == PUZZLE_TRAVEL_MEMORY_TEMPLATE_ID)
);
assert!(catalog.iter().all(|template| template.supported_level_mode
== PuzzleCreativeSupportedLevelMode::SingleOrMulti));
}
}