use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CreateAiTaskRequest { pub task_kind: String, pub request_label: String, pub source_module: String, #[serde(default)] pub source_entity_id: Option, #[serde(default)] pub request_payload_json: Option, #[serde(default)] pub stage_kinds: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AppendAiTextChunkRequest { pub stage_kind: String, pub sequence: u32, pub delta_text: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CompleteAiStageRequest { #[serde(default)] pub text_output: Option, #[serde(default)] pub structured_payload_json: Option, #[serde(default)] pub warning_messages: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AttachAiResultReferenceRequest { pub reference_kind: String, pub reference_id: String, #[serde(default)] pub label: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct FailAiTaskRequest { pub failure_message: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AiTaskStagePayload { pub stage_kind: String, pub label: String, pub detail: String, pub order: u32, pub status: String, #[serde(default)] pub text_output: Option, #[serde(default)] pub structured_payload_json: Option, pub warning_messages: Vec, #[serde(default)] pub started_at: Option, #[serde(default)] pub completed_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AiResultReferencePayload { pub result_ref_id: String, pub task_id: String, pub reference_kind: String, pub reference_id: String, #[serde(default)] pub label: Option, pub created_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AiTextChunkPayload { pub chunk_id: String, pub task_id: String, pub stage_kind: String, pub sequence: u32, pub delta_text: String, pub created_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AiTaskPayload { pub task_id: String, pub task_kind: String, pub owner_user_id: String, pub request_label: String, pub source_module: String, #[serde(default)] pub source_entity_id: Option, #[serde(default)] pub request_payload_json: Option, pub status: String, #[serde(default)] pub failure_message: Option, pub stages: Vec, pub result_references: Vec, #[serde(default)] pub latest_text_output: Option, #[serde(default)] pub latest_structured_payload_json: Option, pub version: u32, pub created_at: String, #[serde(default)] pub started_at: Option, #[serde(default)] pub completed_at: Option, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AiTaskMutationResponse { pub ai_task: AiTaskPayload, #[serde(default)] pub ai_text_chunk: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AiTaskAcceptedResponse { pub accepted: bool, pub task_id: String, pub action: String, #[serde(default)] pub stage_kind: Option, } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn create_ai_task_request_uses_camel_case_fields() { let payload = serde_json::to_value(CreateAiTaskRequest { task_kind: "story_generation".to_string(), request_label: "营地开场".to_string(), source_module: "story".to_string(), source_entity_id: Some("storysess_001".to_string()), request_payload_json: Some("{\"scene\":\"camp\"}".to_string()), stage_kinds: vec!["prepare_prompt".to_string(), "request_model".to_string()], }) .expect("payload should serialize"); assert_eq!( payload, json!({ "taskKind": "story_generation", "requestLabel": "营地开场", "sourceModule": "story", "sourceEntityId": "storysess_001", "requestPayloadJson": "{\"scene\":\"camp\"}", "stageKinds": ["prepare_prompt", "request_model"] }) ); } #[test] fn ai_task_mutation_response_uses_camel_case_fields() { let payload = serde_json::to_value(AiTaskMutationResponse { ai_task: AiTaskPayload { task_id: "aitask_001".to_string(), task_kind: "npc_chat".to_string(), owner_user_id: "user_001".to_string(), request_label: "试探问话".to_string(), source_module: "npc".to_string(), source_entity_id: Some("npc_001".to_string()), request_payload_json: None, status: "running".to_string(), failure_message: None, stages: vec![AiTaskStagePayload { stage_kind: "request_model".to_string(), label: "请求模型".to_string(), detail: "向上游模型发起正式推理请求。".to_string(), order: 1, status: "running".to_string(), text_output: Some("你盯着对方的刀柄。".to_string()), structured_payload_json: None, warning_messages: vec![], started_at: Some("2026-04-22T12:00:00Z".to_string()), completed_at: None, }], result_references: vec![], latest_text_output: Some("你盯着对方的刀柄。".to_string()), latest_structured_payload_json: None, version: 2, created_at: "2026-04-22T12:00:00Z".to_string(), started_at: Some("2026-04-22T12:00:01Z".to_string()), completed_at: None, updated_at: "2026-04-22T12:00:02Z".to_string(), }, ai_text_chunk: Some(AiTextChunkPayload { chunk_id: "aichunk_001".to_string(), task_id: "aitask_001".to_string(), stage_kind: "request_model".to_string(), sequence: 1, delta_text: "你".to_string(), created_at: "2026-04-22T12:00:02Z".to_string(), }), }) .expect("payload should serialize"); assert_eq!(payload["aiTask"]["taskId"], json!("aitask_001")); assert_eq!( payload["aiTask"]["stages"][0]["stageKind"], json!("request_model") ); assert_eq!(payload["aiTextChunk"]["deltaText"], json!("你")); } }