This commit is contained in:
223
server-rs/crates/shared-contracts/src/ai.rs
Normal file
223
server-rs/crates/shared-contracts/src/ai.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
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<String>,
|
||||
#[serde(default)]
|
||||
pub request_payload_json: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stage_kinds: Vec<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[serde(default)]
|
||||
pub structured_payload_json: Option<String>,
|
||||
#[serde(default)]
|
||||
pub warning_messages: Vec<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[serde(default)]
|
||||
pub structured_payload_json: Option<String>,
|
||||
pub warning_messages: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub started_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub completed_at: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
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<String>,
|
||||
#[serde(default)]
|
||||
pub request_payload_json: Option<String>,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub failure_message: Option<String>,
|
||||
pub stages: Vec<AiTaskStagePayload>,
|
||||
pub result_references: Vec<AiResultReferencePayload>,
|
||||
#[serde(default)]
|
||||
pub latest_text_output: Option<String>,
|
||||
#[serde(default)]
|
||||
pub latest_structured_payload_json: Option<String>,
|
||||
pub version: u32,
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub started_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub completed_at: Option<String>,
|
||||
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<AiTextChunkPayload>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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!("你"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user