563 lines
19 KiB
Rust
563 lines
19 KiB
Rust
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<String>,
|
|
#[serde(default)]
|
|
pub image_url: Option<String>,
|
|
#[serde(default)]
|
|
pub asset_id: Option<String>,
|
|
#[serde(default)]
|
|
pub thumbnail_url: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[serde(default)]
|
|
pub width: Option<u32>,
|
|
#[serde(default)]
|
|
pub height: Option<u32>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CreativeImageSummary {
|
|
#[serde(default)]
|
|
pub asset_id: Option<String>,
|
|
#[serde(default)]
|
|
pub read_url: Option<String>,
|
|
#[serde(default)]
|
|
pub thumbnail_url: Option<String>,
|
|
#[serde(default)]
|
|
pub width: Option<u32>,
|
|
#[serde(default)]
|
|
pub height: Option<u32>,
|
|
#[serde(default)]
|
|
pub summary: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
pub entry_context: CreativeAgentEntryContext,
|
|
pub images: Vec<CreativeImageSummary>,
|
|
#[serde(default)]
|
|
pub material_summary: Option<String>,
|
|
pub unsupported_capabilities: Vec<CreativeUnsupportedCapability>,
|
|
}
|
|
|
|
#[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<String>,
|
|
}
|
|
|
|
#[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<CreativeAgentMessage>,
|
|
#[serde(default)]
|
|
pub puzzle_template_catalog: Vec<PuzzleCreativeTemplateProtocol>,
|
|
#[serde(default)]
|
|
pub puzzle_template_selection: Option<PuzzleCreativeTemplateSelection>,
|
|
#[serde(default)]
|
|
pub puzzle_image_generation_plan: Option<PuzzleImageGenerationPlan>,
|
|
#[serde(default)]
|
|
pub target_binding: Option<CreativeTargetSessionBinding>,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CreateCreativeAgentSessionRequest {
|
|
#[serde(default)]
|
|
pub text: Option<String>,
|
|
#[serde(default)]
|
|
pub images: Vec<CreativeImageInput>,
|
|
#[serde(default)]
|
|
pub entry_context: Option<CreativeAgentEntryContext>,
|
|
}
|
|
|
|
#[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<CreativeAgentInputPart>,
|
|
}
|
|
|
|
#[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<PuzzleDraftFieldPatch>,
|
|
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<PuzzleCreativeTemplateProtocol>,
|
|
}
|
|
|
|
#[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<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
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")
|
|
);
|
|
}
|
|
}
|