This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -17,6 +17,8 @@ pub struct CreationAgentDocumentInputPayload {
pub content_type: Option<String>,
pub size_bytes: usize,
pub text: String,
#[serde(default)]
pub source_asset_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]

View File

@@ -0,0 +1,563 @@
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")
);
}
}

View File

@@ -6,11 +6,13 @@ pub mod auth;
pub mod big_fish;
pub mod big_fish_works;
pub mod creation_agent_document_input;
pub mod creative_agent;
pub mod llm;
pub mod match3d_agent;
pub mod match3d_runtime;
pub mod match3d_works;
pub mod puzzle_agent;
pub mod puzzle_creative_template;
pub mod puzzle_gallery;
pub mod puzzle_runtime;
pub mod puzzle_works;
@@ -20,3 +22,4 @@ pub mod square_hole_agent;
pub mod square_hole_runtime;
pub mod square_hole_works;
pub mod story;
pub mod visual_novel;

View File

@@ -15,6 +15,8 @@ pub struct CreatePuzzleAgentSessionRequest {
pub reference_image_src: Option<String>,
#[serde(default)]
pub image_model: Option<String>,
#[serde(default)]
pub ai_redraw: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -37,6 +39,8 @@ pub struct ExecutePuzzleAgentActionRequest {
#[serde(default)]
pub image_model: Option<String>,
#[serde(default)]
pub ai_redraw: Option<bool>,
#[serde(default)]
pub candidate_count: Option<u32>,
#[serde(default)]
pub candidate_id: Option<String>,
@@ -145,6 +149,8 @@ pub struct PuzzleDraftLevelResponse {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
#[serde(default)]
pub picture_reference: Option<String>,
pub candidates: Vec<PuzzleGeneratedImageCandidateResponse>,
#[serde(default)]
pub selected_candidate_id: Option<String>,

View File

@@ -0,0 +1,230 @@
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);
}
}

View File

@@ -0,0 +1,687 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelSourceMode {
Idea,
Document,
Blank,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelCharacterRole {
Protagonist,
Main,
Supporting,
Antagonist,
Background,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAssetSource {
PlatformAsset,
Generated,
External,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelSceneAvailability {
Opening,
Always,
PhaseLocked,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAttributePanelMode {
Off,
PlatformWhitelist,
TemplateConfig,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelValidationSeverity {
Error,
Warning,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelAgentStatus {
Collecting,
Drafting,
Ready,
Failed,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelAgentMessageRole {
User,
Assistant,
System,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAgentMessageKind {
Chat,
Summary,
ActionResult,
Warning,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAgentActionKind {
GenerateDraft,
PatchWorld,
PatchCharacter,
PatchScene,
PatchStoryPhase,
GenerateSceneImage,
GenerateCharacterImage,
CompileWorkProfile,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelAgentPhase {
Perception,
Reasoning,
Drafting,
Reflection,
Finalizing,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelRunMode {
Test,
Play,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelRunStatus {
Active,
Completed,
Failed,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelRuntimeActionKind {
Choice,
FreeText,
Continue,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelTransitionKind {
Fade,
Cut,
Flash,
None,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelHistorySource {
Player,
Assistant,
System,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum VisualNovelFlagValue {
String(String),
Number(f64),
Bool(bool),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelDraftPatch {
pub path: String,
pub op: String,
#[serde(default)]
pub value: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelValidationIssue {
pub issue_id: String,
pub code: String,
pub severity: VisualNovelValidationSeverity,
pub path: String,
pub message: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelChoiceDraft {
pub choice_id: String,
pub text: String,
#[serde(default)]
pub action_hint: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCharacterImageAsset {
pub asset_id: String,
pub image_src: String,
#[serde(default)]
pub expression: Option<String>,
pub source: VisualNovelAssetSource,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorldDraft {
pub title: String,
pub summary: String,
pub background: String,
pub premise: String,
pub literary_style: String,
pub player_role: String,
pub default_tone: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCharacterDraft {
pub character_id: String,
pub name: String,
#[serde(default)]
pub gender: Option<String>,
pub role: VisualNovelCharacterRole,
pub appearance: String,
pub personality: String,
pub tone: String,
pub background: String,
pub relationship_to_player: String,
pub image_assets: Vec<VisualNovelCharacterImageAsset>,
#[serde(default)]
pub default_expression: Option<String>,
pub is_player_visible: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSceneDraft {
pub scene_id: String,
pub name: String,
pub description: String,
#[serde(default)]
pub background_image_src: Option<String>,
#[serde(default)]
pub music_src: Option<String>,
#[serde(default)]
pub ambient_sound_src: Option<String>,
pub availability: VisualNovelSceneAvailability,
pub phase_ids: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelStoryPhaseDraft {
pub phase_id: String,
pub title: String,
pub goal: String,
pub summary: String,
pub entry_condition: String,
pub exit_condition: String,
pub scene_ids: Vec<String>,
pub character_ids: Vec<String>,
pub suggested_choices: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelOpeningDraft {
#[serde(default)]
pub scene_id: Option<String>,
pub narration: String,
#[serde(default)]
pub speaker_character_id: Option<String>,
#[serde(default)]
pub first_dialogue: Option<String>,
pub initial_choices: Vec<VisualNovelChoiceDraft>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRuntimeConfigDraft {
pub text_mode_enabled: bool,
pub default_text_mode: bool,
pub max_history_entries: u32,
pub max_assistant_step_count_per_turn: u32,
pub allow_free_text_action: bool,
pub allow_history_regeneration: bool,
pub attribute_panel_mode: VisualNovelAttributePanelMode,
pub save_archive_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelResultDraft {
#[serde(default)]
pub profile_id: Option<String>,
pub work_title: String,
pub work_description: String,
pub work_tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
pub source_mode: VisualNovelSourceMode,
pub source_asset_ids: Vec<String>,
pub world: VisualNovelWorldDraft,
pub characters: Vec<VisualNovelCharacterDraft>,
pub scenes: Vec<VisualNovelSceneDraft>,
pub story_phases: Vec<VisualNovelStoryPhaseDraft>,
pub opening: VisualNovelOpeningDraft,
pub runtime_config: VisualNovelRuntimeConfigDraft,
pub publish_ready: bool,
pub validation_issues: Vec<VisualNovelValidationIssue>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelAgentMessage {
pub id: String,
pub role: VisualNovelAgentMessageRole,
pub kind: VisualNovelAgentMessageKind,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelAgentPendingAction {
pub action_id: String,
pub kind: VisualNovelAgentActionKind,
pub label: String,
#[serde(default)]
pub target_id: Option<String>,
#[serde(default)]
pub payload: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelAgentSessionSnapshot {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: VisualNovelSourceMode,
pub status: VisualNovelAgentStatus,
pub messages: Vec<VisualNovelAgentMessage>,
#[serde(default)]
pub draft: Option<VisualNovelResultDraft>,
#[serde(default)]
pub pending_action: Option<VisualNovelAgentPendingAction>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateVisualNovelSessionRequest {
pub source_mode: VisualNovelSourceMode,
#[serde(default)]
pub seed_text: Option<String>,
#[serde(default)]
pub source_asset_ids: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSessionResponse {
pub session: VisualNovelAgentSessionSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorkSummary {
pub runtime_kind: String,
pub profile_id: String,
pub owner_user_id: String,
pub title: String,
pub description: String,
#[serde(default)]
pub cover_image_src: Option<String>,
pub tags: Vec<String>,
pub publish_status: String,
pub publish_ready: bool,
pub play_count: u32,
pub updated_at: String,
#[serde(default)]
pub published_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorkDetail {
pub work_id: String,
pub summary: VisualNovelWorkSummary,
#[serde(default)]
pub source_session_id: Option<String>,
pub author_display_name: String,
pub source_asset_ids: Vec<String>,
pub draft: VisualNovelResultDraft,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorksResponse {
pub works: Vec<VisualNovelWorkSummary>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorkResponse {
pub work: VisualNovelWorkDetail,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UpdateVisualNovelWorkRequest {
pub draft: VisualNovelResultDraft,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCompileResponse {
pub session: VisualNovelAgentSessionSnapshot,
pub work: VisualNovelWorkDetail,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SendVisualNovelMessageRequest {
pub client_message_id: String,
pub text: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteVisualNovelAgentActionRequest {
#[serde(default)]
pub action_id: Option<String>,
pub kind: VisualNovelAgentActionKind,
#[serde(default)]
pub target_id: Option<String>,
#[serde(default)]
pub payload: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum VisualNovelAgentStreamEvent {
Start {
session_id: String,
},
Phase {
phase: VisualNovelAgentPhase,
},
TextDelta {
text: String,
},
DraftPatch {
patch: VisualNovelDraftPatch,
},
ActionRequired {
action: VisualNovelAgentPendingAction,
},
Complete {
session: VisualNovelAgentSessionSnapshot,
},
Error {
message: String,
retryable: bool,
},
Done {},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum VisualNovelRuntimeStep {
SceneChange {
scene_id: String,
#[serde(default)]
background_image_src: Option<String>,
#[serde(default)]
music_src: Option<String>,
},
Narration {
text: String,
},
Dialogue {
character_id: String,
character_name: String,
#[serde(default)]
expression: Option<String>,
text: String,
},
Transition {
transition_kind: VisualNovelTransitionKind,
#[serde(default)]
text: Option<String>,
},
Choice {
choices: Vec<VisualNovelChoiceDraft>,
},
Flag {
key: String,
value: VisualNovelFlagValue,
},
Metric {
key: String,
delta: f64,
},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelHistoryEntry {
pub entry_id: String,
pub run_id: String,
pub turn_index: u32,
pub source: VisualNovelHistorySource,
#[serde(default)]
pub action_text: Option<String>,
pub steps: Vec<VisualNovelRuntimeStep>,
#[serde(default)]
pub snapshot_before_hash: Option<String>,
#[serde(default)]
pub snapshot_after_hash: Option<String>,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRunSnapshot {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: VisualNovelRunMode,
pub status: VisualNovelRunStatus,
#[serde(default)]
pub current_scene_id: Option<String>,
#[serde(default)]
pub current_phase_id: Option<String>,
pub visible_character_ids: Vec<String>,
pub flags: BTreeMap<String, VisualNovelFlagValue>,
pub metrics: BTreeMap<String, f64>,
pub history: Vec<VisualNovelHistoryEntry>,
pub available_choices: Vec<VisualNovelChoiceDraft>,
pub text_mode_enabled: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRuntimeActionRequest {
pub action_kind: VisualNovelRuntimeActionKind,
#[serde(default)]
pub choice_id: Option<String>,
#[serde(default)]
pub text: Option<String>,
pub client_event_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelStartRunRequest {
pub profile_id: String,
pub mode: VisualNovelRunMode,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRunResponse {
pub run: VisualNovelRunSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelHistoryResponse {
pub history: Vec<VisualNovelHistoryEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRegenerateRequest {
pub history_entry_id: String,
pub client_event_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSaveArchiveState {
pub runtime_kind: String,
pub profile_id: String,
pub run_id: String,
#[serde(default)]
pub current_scene_id: Option<String>,
#[serde(default)]
pub current_phase_id: Option<String>,
pub history_cursor: u32,
#[serde(default)]
pub snapshot_hash: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum VisualNovelRuntimeStreamEvent {
Start { run_id: String },
RawText { text: String },
Step { step: VisualNovelRuntimeStep },
Snapshot { run: VisualNovelRunSnapshot },
Complete { run: VisualNovelRunSnapshot },
Error { message: String, retryable: bool },
Done {},
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn result_draft_and_runtime_step_use_contract_case() {
let draft = VisualNovelResultDraft {
profile_id: Some("vn-profile-1".to_string()),
work_title: "雨夜书店".to_string(),
work_description: "一段视觉小说测试底稿".to_string(),
work_tags: vec!["悬疑".to_string()],
cover_image_src: None,
source_mode: VisualNovelSourceMode::Idea,
source_asset_ids: Vec::new(),
world: VisualNovelWorldDraft {
title: "雨夜书店".to_string(),
summary: "主角在雨夜进入一间只在午夜出现的书店。".to_string(),
background: "城市边缘的旧街区。".to_string(),
premise: "找回遗失的名字。".to_string(),
literary_style: "细腻、轻悬疑".to_string(),
player_role: "误入书店的读者".to_string(),
default_tone: "克制而温柔".to_string(),
},
characters: Vec::new(),
scenes: Vec::new(),
story_phases: Vec::new(),
opening: VisualNovelOpeningDraft {
scene_id: None,
narration: "雨声落下。".to_string(),
speaker_character_id: None,
first_dialogue: None,
initial_choices: Vec::new(),
},
runtime_config: VisualNovelRuntimeConfigDraft {
text_mode_enabled: true,
default_text_mode: false,
max_history_entries: 80,
max_assistant_step_count_per_turn: 8,
allow_free_text_action: true,
allow_history_regeneration: true,
attribute_panel_mode: VisualNovelAttributePanelMode::Off,
save_archive_enabled: true,
},
publish_ready: false,
validation_issues: Vec::new(),
updated_at: "2026-05-05T00:00:00Z".to_string(),
};
let payload = serde_json::to_value(draft).expect("draft should serialize");
assert_eq!(payload["profileId"], json!("vn-profile-1"));
assert_eq!(payload["sourceMode"], json!("idea"));
assert_eq!(payload["runtimeConfig"]["attributePanelMode"], json!("off"));
let step = VisualNovelRuntimeStep::SceneChange {
scene_id: "scene-1".to_string(),
background_image_src: None,
music_src: None,
};
let step_payload = serde_json::to_value(step).expect("step should serialize");
assert_eq!(step_payload["type"], json!("scene_change"));
assert_eq!(step_payload["sceneId"], json!("scene-1"));
}
#[test]
fn runtime_stream_event_uses_tagged_envelope() {
let event = VisualNovelRuntimeStreamEvent::Step {
step: VisualNovelRuntimeStep::Narration {
text: "门铃响了。".to_string(),
},
};
let payload = serde_json::to_value(event).expect("event should serialize");
assert_eq!(payload["type"], json!("step"));
assert_eq!(payload["step"]["type"], json!("narration"));
assert_eq!(payload["step"]["text"], json!("门铃响了。"));
}
}