This commit is contained in:
107
server-rs/crates/shared-contracts/src/admin.rs
Normal file
107
server-rs/crates/shared-contracts/src/admin.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
// 管理后台协议统一收口在 shared-contracts,避免页面脚本和 Rust handler 各自手拼字段。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminLoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// 登录成功后返回管理员访问令牌与基础会话信息。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminLoginResponse {
|
||||
pub token: String,
|
||||
pub admin: AdminSessionPayload,
|
||||
}
|
||||
|
||||
// 管理员会话只暴露页面展示和鉴权调试所需的最小字段。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminSessionPayload {
|
||||
pub subject: String,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub roles: Vec<String>,
|
||||
pub issued_at: String,
|
||||
pub expires_at: String,
|
||||
}
|
||||
|
||||
// 页面恢复登录态时读取当前管理员会话。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminMeResponse {
|
||||
pub admin: AdminSessionPayload,
|
||||
}
|
||||
|
||||
// 后台概览统一返回服务信息与数据库信息两块,前端不再额外拼装。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminOverviewResponse {
|
||||
pub service: AdminServiceOverviewPayload,
|
||||
pub database: AdminDatabaseOverviewPayload,
|
||||
}
|
||||
|
||||
// 服务概览描述当前 api-server 与 SpacetimeDB 连接配置。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminServiceOverviewPayload {
|
||||
pub bind_host: String,
|
||||
pub bind_port: u16,
|
||||
pub jwt_issuer: String,
|
||||
pub admin_enabled: bool,
|
||||
pub spacetime_server_url: String,
|
||||
pub spacetime_database: String,
|
||||
}
|
||||
|
||||
// 数据库概览返回真实数据库元信息、表清单与统计错误。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDatabaseOverviewPayload {
|
||||
pub database_identity: Option<String>,
|
||||
pub owner_identity: Option<String>,
|
||||
pub host_type: Option<String>,
|
||||
pub schema_table_names: Vec<String>,
|
||||
pub table_stats: Vec<AdminDatabaseTableStatPayload>,
|
||||
pub fetch_errors: Vec<String>,
|
||||
}
|
||||
|
||||
// 单表统计允许成功和失败并存,避免某张表失败导致整页概览不可用。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDatabaseTableStatPayload {
|
||||
pub table_name: String,
|
||||
pub row_count: Option<u64>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
// 调试请求只允许同源路径、受控请求头和有限请求体。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDebugHttpRequest {
|
||||
pub method: String,
|
||||
pub path: String,
|
||||
pub headers: Option<Vec<AdminDebugHeaderInput>>,
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
// 调试请求头使用显式结构,避免页面直接塞任意对象。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDebugHeaderInput {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
// 调试响应回显状态、响应头与文本/JSON 预览,便于后台排查接口问题。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDebugHttpResponse {
|
||||
pub status: u16,
|
||||
pub status_text: String,
|
||||
pub headers: Vec<AdminDebugHeaderInput>,
|
||||
pub body_text: String,
|
||||
pub body_json: Option<Value>,
|
||||
}
|
||||
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!("你"));
|
||||
}
|
||||
}
|
||||
168
server-rs/crates/shared-contracts/src/api.rs
Normal file
168
server-rs/crates/shared-contracts/src/api.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
pub const API_VERSION: &str = "2026-04-08";
|
||||
pub const API_RESPONSE_ENVELOPE_HEADER: &str = "x-genarrative-response-envelope";
|
||||
pub const X_REQUEST_ID_HEADER: &str = "x-request-id";
|
||||
pub const API_VERSION_HEADER: &str = "x-api-version";
|
||||
pub const ROUTE_VERSION_HEADER: &str = "x-route-version";
|
||||
pub const RESPONSE_TIME_HEADER: &str = "x-response-time-ms";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ApiResponseMeta {
|
||||
#[serde(rename = "apiVersion")]
|
||||
pub api_version: String,
|
||||
#[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
|
||||
pub request_id: Option<String>,
|
||||
#[serde(rename = "routeVersion")]
|
||||
pub route_version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub operation: Option<String>,
|
||||
#[serde(rename = "latencyMs")]
|
||||
pub latency_ms: u64,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
impl ApiResponseMeta {
|
||||
pub fn new(
|
||||
api_version: impl Into<String>,
|
||||
request_id: Option<String>,
|
||||
route_version: impl Into<String>,
|
||||
operation: Option<String>,
|
||||
latency_ms: u64,
|
||||
timestamp: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_version: api_version.into(),
|
||||
request_id,
|
||||
route_version: route_version.into(),
|
||||
operation,
|
||||
latency_ms,
|
||||
timestamp: timestamp.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ApiErrorPayload {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub details: Option<Value>,
|
||||
}
|
||||
|
||||
impl ApiErrorPayload {
|
||||
pub fn new(
|
||||
code: impl Into<String>,
|
||||
message: impl Into<String>,
|
||||
details: Option<Value>,
|
||||
) -> Self {
|
||||
Self {
|
||||
code: code.into(),
|
||||
message: message.into(),
|
||||
details,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ApiSuccessEnvelope<T> {
|
||||
pub ok: bool,
|
||||
pub data: T,
|
||||
pub error: Option<ApiErrorPayload>,
|
||||
pub meta: ApiResponseMeta,
|
||||
}
|
||||
|
||||
impl<T> ApiSuccessEnvelope<T> {
|
||||
pub fn new(data: T, meta: ApiResponseMeta) -> Self {
|
||||
Self {
|
||||
ok: true,
|
||||
data,
|
||||
error: None,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ApiErrorEnvelope {
|
||||
pub ok: bool,
|
||||
pub data: Option<Value>,
|
||||
pub error: ApiErrorPayload,
|
||||
pub meta: ApiResponseMeta,
|
||||
}
|
||||
|
||||
impl ApiErrorEnvelope {
|
||||
pub fn new(error: ApiErrorPayload, meta: ApiResponseMeta) -> Self {
|
||||
Self {
|
||||
ok: false,
|
||||
data: None,
|
||||
error,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LegacyApiErrorResponse {
|
||||
pub error: ApiErrorPayload,
|
||||
pub meta: ApiResponseMeta,
|
||||
}
|
||||
|
||||
impl LegacyApiErrorResponse {
|
||||
pub fn new(error: ApiErrorPayload, meta: ApiResponseMeta) -> Self {
|
||||
Self { error, meta }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn success_envelope_serializes_null_error_field() {
|
||||
let payload = serde_json::to_value(ApiSuccessEnvelope::new(
|
||||
json!({ "service": "genarrative" }),
|
||||
ApiResponseMeta::new(
|
||||
API_VERSION,
|
||||
Some("req-1".to_string()),
|
||||
API_VERSION,
|
||||
Some("GET /healthz".to_string()),
|
||||
12,
|
||||
"2026-04-21T00:00:00Z",
|
||||
),
|
||||
))
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["ok"], Value::Bool(true));
|
||||
assert_eq!(payload["error"], Value::Null);
|
||||
assert_eq!(
|
||||
payload["meta"]["apiVersion"],
|
||||
Value::String(API_VERSION.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_envelope_serializes_null_data_field() {
|
||||
let payload = serde_json::to_value(ApiErrorEnvelope::new(
|
||||
ApiErrorPayload::new("BAD_REQUEST", "请求参数不合法", None),
|
||||
ApiResponseMeta::new(
|
||||
API_VERSION,
|
||||
Some("req-2".to_string()),
|
||||
API_VERSION,
|
||||
Some("POST /api/test".to_string()),
|
||||
21,
|
||||
"2026-04-21T00:00:01Z",
|
||||
),
|
||||
))
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["ok"], Value::Bool(false));
|
||||
assert_eq!(payload["data"], Value::Null);
|
||||
assert_eq!(
|
||||
payload["error"]["code"],
|
||||
Value::String("BAD_REQUEST".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
853
server-rs/crates/shared-contracts/src/assets.rs
Normal file
853
server-rs/crates/shared-contracts/src/assets.rs
Normal file
@@ -0,0 +1,853 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use platform_oss::{
|
||||
OssObjectAccess, OssPostObjectFormFields, OssPostObjectResponse, OssSignedGetObjectUrlResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateDirectUploadTicketRequest {
|
||||
pub legacy_prefix: String,
|
||||
#[serde(default)]
|
||||
pub path_segments: Vec<String>,
|
||||
pub file_name: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub access: Option<OssObjectAccess>,
|
||||
#[serde(default)]
|
||||
pub metadata: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub max_size_bytes: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub expire_seconds: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub success_action_status: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetReadUrlQuery {
|
||||
#[serde(default)]
|
||||
pub object_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub legacy_public_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub expire_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ConfirmAssetObjectAccessPolicy {
|
||||
Private,
|
||||
PublicRead,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConfirmAssetObjectRequest {
|
||||
#[serde(default)]
|
||||
pub bucket: Option<String>,
|
||||
pub object_key: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub content_length: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub content_hash: Option<String>,
|
||||
pub asset_kind: String,
|
||||
#[serde(default)]
|
||||
pub access_policy: Option<ConfirmAssetObjectAccessPolicy>,
|
||||
#[serde(default)]
|
||||
pub source_job_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub owner_user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub entity_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BindAssetObjectRequest {
|
||||
pub asset_object_id: String,
|
||||
pub entity_kind: String,
|
||||
pub entity_id: String,
|
||||
pub slot: String,
|
||||
pub asset_kind: String,
|
||||
#[serde(default)]
|
||||
pub owner_user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum CharacterVisualSourceMode {
|
||||
TextToImage,
|
||||
ImageToImage,
|
||||
Upload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterVisualGenerateRequest {
|
||||
pub character_id: String,
|
||||
pub source_mode: CharacterVisualSourceMode,
|
||||
pub prompt_text: String,
|
||||
#[serde(default)]
|
||||
pub reference_image_data_urls: Vec<String>,
|
||||
pub candidate_count: u32,
|
||||
pub image_model: String,
|
||||
pub size: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterVisualDraftPayload {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub image_src: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterVisualGenerateResponse {
|
||||
pub ok: bool,
|
||||
pub task_id: String,
|
||||
pub model: String,
|
||||
pub prompt: String,
|
||||
pub drafts: Vec<CharacterVisualDraftPayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CharacterAssetJobStatusText {
|
||||
Queued,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAssetJobStatusPayload {
|
||||
pub task_id: String,
|
||||
pub kind: String,
|
||||
pub status: CharacterAssetJobStatusText,
|
||||
pub character_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub animation: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub strategy: Option<String>,
|
||||
pub model: String,
|
||||
pub prompt: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterVisualPublishRequest {
|
||||
pub character_id: String,
|
||||
pub source_mode: CharacterVisualSourceMode,
|
||||
#[serde(default)]
|
||||
pub prompt_text: Option<String>,
|
||||
pub selected_preview_source: String,
|
||||
#[serde(default)]
|
||||
pub preview_sources: Vec<String>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
#[serde(default)]
|
||||
pub update_character_override: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterVisualPublishResponse {
|
||||
pub ok: bool,
|
||||
pub asset_id: String,
|
||||
pub portrait_path: String,
|
||||
pub override_map: Value,
|
||||
pub save_message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationTemplatePayload {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub animation: String,
|
||||
pub prompt_suffix: String,
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationTemplatesResponse {
|
||||
pub ok: bool,
|
||||
pub templates: Vec<CharacterAnimationTemplatePayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationImportVideoRequest {
|
||||
pub character_id: String,
|
||||
pub animation: String,
|
||||
pub video_source: String,
|
||||
#[serde(default)]
|
||||
pub source_label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationImportVideoResponse {
|
||||
pub ok: bool,
|
||||
pub imported_video_path: String,
|
||||
pub draft_id: String,
|
||||
pub save_message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum CharacterAnimationStrategy {
|
||||
ImageSequence,
|
||||
ImageToVideo,
|
||||
MotionTransfer,
|
||||
ReferenceToVideo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationGenerateRequest {
|
||||
pub character_id: String,
|
||||
pub strategy: CharacterAnimationStrategy,
|
||||
pub animation: String,
|
||||
pub prompt_text: String,
|
||||
#[serde(default)]
|
||||
pub character_brief_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub action_template_id: Option<String>,
|
||||
pub visual_source: String,
|
||||
#[serde(default)]
|
||||
pub reference_image_data_urls: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub reference_video_data_urls: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub last_frame_image_data_url: Option<String>,
|
||||
pub frame_count: u32,
|
||||
pub fps: u32,
|
||||
pub duration_seconds: u32,
|
||||
#[serde(rename = "loop")]
|
||||
pub loop_: bool,
|
||||
pub use_chroma_key: bool,
|
||||
pub resolution: String,
|
||||
pub ratio: String,
|
||||
pub image_sequence_model: String,
|
||||
pub video_model: String,
|
||||
pub reference_video_model: String,
|
||||
pub motion_transfer_model: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationGenerateResponse {
|
||||
pub ok: bool,
|
||||
pub task_id: String,
|
||||
pub strategy: CharacterAnimationStrategy,
|
||||
pub model: String,
|
||||
pub prompt: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub image_sources: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preview_video_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationDraftPayload {
|
||||
#[serde(default)]
|
||||
pub frames_data_urls: Vec<String>,
|
||||
pub fps: u32,
|
||||
#[serde(rename = "loop")]
|
||||
pub loop_: bool,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
#[serde(default)]
|
||||
pub frame_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub apply_chroma_key: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub sample_start_ratio: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub sample_end_ratio: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub preview_video_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationPublishRequest {
|
||||
pub character_id: String,
|
||||
pub visual_asset_id: String,
|
||||
pub animations: BTreeMap<String, CharacterAnimationDraftPayload>,
|
||||
#[serde(default)]
|
||||
pub update_character_override: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterAnimationPublishResponse {
|
||||
pub ok: bool,
|
||||
pub animation_set_id: String,
|
||||
pub override_map: Value,
|
||||
pub animation_map: Value,
|
||||
pub save_message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterWorkflowCachePayload {
|
||||
pub character_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_scope_id: Option<String>,
|
||||
pub visual_prompt_text: String,
|
||||
pub animation_prompt_text: String,
|
||||
pub visual_drafts: Vec<CharacterVisualDraftPayload>,
|
||||
pub selected_visual_draft_id: String,
|
||||
pub selected_animation: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_src: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generated_visual_asset_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generated_animation_set_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub animation_map: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterWorkflowCacheSaveRequest {
|
||||
pub character_id: String,
|
||||
#[serde(default)]
|
||||
pub cache_scope_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub visual_prompt_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub animation_prompt_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub visual_drafts: Vec<CharacterVisualDraftPayload>,
|
||||
#[serde(default)]
|
||||
pub selected_visual_draft_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub selected_animation: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub generated_visual_asset_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub generated_animation_set_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub animation_map: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterWorkflowCacheGetResponse {
|
||||
pub ok: bool,
|
||||
pub cache: Option<CharacterWorkflowCachePayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterWorkflowCacheSaveResponse {
|
||||
pub ok: bool,
|
||||
pub cache: CharacterWorkflowCachePayload,
|
||||
pub save_message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateDirectUploadTicketResponse {
|
||||
pub upload: DirectUploadTicketPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DirectUploadTicketPayload {
|
||||
pub signature_version: String,
|
||||
pub provider: String,
|
||||
pub bucket: String,
|
||||
pub endpoint: String,
|
||||
pub host: String,
|
||||
pub object_key: String,
|
||||
pub legacy_public_path: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
pub access: OssObjectAccess,
|
||||
pub key_prefix: String,
|
||||
pub expires_at: String,
|
||||
pub max_size_bytes: u64,
|
||||
pub success_action_status: u16,
|
||||
pub form_fields: DirectUploadTicketFormFields,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DirectUploadTicketFormFields {
|
||||
pub key: String,
|
||||
pub policy: String,
|
||||
#[serde(rename = "OSSAccessKeyId")]
|
||||
pub oss_access_key_id: String,
|
||||
#[serde(rename = "Signature")]
|
||||
pub signature: String,
|
||||
#[serde(rename = "success_action_status")]
|
||||
pub success_action_status: String,
|
||||
#[serde(rename = "Content-Type", skip_serializing_if = "Option::is_none")]
|
||||
pub content_type: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub metadata: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetAssetReadUrlResponse {
|
||||
pub read: AssetReadUrlPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetReadUrlPayload {
|
||||
pub provider: String,
|
||||
pub bucket: String,
|
||||
pub endpoint: String,
|
||||
pub host: String,
|
||||
pub object_key: String,
|
||||
pub expires_at: String,
|
||||
pub signed_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConfirmAssetObjectResponse {
|
||||
pub asset_object: AssetObjectPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetObjectPayload {
|
||||
pub asset_object_id: String,
|
||||
pub bucket: String,
|
||||
pub object_key: String,
|
||||
pub access_policy: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
pub content_length: u64,
|
||||
#[serde(default)]
|
||||
pub content_hash: Option<String>,
|
||||
pub version: u32,
|
||||
#[serde(default)]
|
||||
pub source_job_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub owner_user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub entity_id: Option<String>,
|
||||
pub asset_kind: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BindAssetObjectResponse {
|
||||
pub asset_binding: AssetBindingPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetBindingPayload {
|
||||
pub binding_id: String,
|
||||
pub asset_object_id: String,
|
||||
pub entity_kind: String,
|
||||
pub entity_id: String,
|
||||
pub slot: String,
|
||||
pub asset_kind: String,
|
||||
#[serde(default)]
|
||||
pub owner_user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<OssPostObjectFormFields> for DirectUploadTicketFormFields {
|
||||
fn from(value: OssPostObjectFormFields) -> Self {
|
||||
Self {
|
||||
key: value.key,
|
||||
policy: value.policy,
|
||||
oss_access_key_id: value.oss_access_key_id,
|
||||
signature: value.signature,
|
||||
success_action_status: value.success_action_status,
|
||||
content_type: value.content_type,
|
||||
metadata: value.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OssPostObjectResponse> for DirectUploadTicketPayload {
|
||||
fn from(value: OssPostObjectResponse) -> Self {
|
||||
Self {
|
||||
signature_version: value.signature_version.to_string(),
|
||||
provider: value.provider.to_string(),
|
||||
bucket: value.bucket,
|
||||
endpoint: value.endpoint,
|
||||
host: value.host,
|
||||
object_key: value.object_key,
|
||||
legacy_public_path: value.legacy_public_path,
|
||||
content_type: value.content_type,
|
||||
access: value.access,
|
||||
key_prefix: value.key_prefix,
|
||||
expires_at: value.expires_at,
|
||||
max_size_bytes: value.max_size_bytes,
|
||||
success_action_status: value.success_action_status,
|
||||
form_fields: value.form_fields.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OssSignedGetObjectUrlResponse> for AssetReadUrlPayload {
|
||||
fn from(value: OssSignedGetObjectUrlResponse) -> Self {
|
||||
Self {
|
||||
provider: value.provider.to_string(),
|
||||
bucket: value.bucket,
|
||||
endpoint: value.endpoint,
|
||||
host: value.host,
|
||||
object_key: value.object_key,
|
||||
expires_at: value.expires_at,
|
||||
signed_url: value.signed_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn confirm_asset_object_access_policy_uses_snake_case() {
|
||||
let payload = serde_json::to_value(ConfirmAssetObjectAccessPolicy::PublicRead)
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload, json!("public_read"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_asset_object_request_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(BindAssetObjectRequest {
|
||||
asset_object_id: "assetobj_1".to_string(),
|
||||
entity_kind: "character".to_string(),
|
||||
entity_id: "npc_1".to_string(),
|
||||
slot: "primary_visual".to_string(),
|
||||
asset_kind: "character_visual".to_string(),
|
||||
owner_user_id: Some("user_1".to_string()),
|
||||
profile_id: Some("profile_1".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"assetObjectId": "assetobj_1",
|
||||
"entityKind": "character",
|
||||
"entityId": "npc_1",
|
||||
"slot": "primary_visual",
|
||||
"assetKind": "character_visual",
|
||||
"ownerUserId": "user_1",
|
||||
"profileId": "profile_1"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_upload_ticket_response_keeps_form_fields_shape() {
|
||||
let payload = serde_json::to_value(CreateDirectUploadTicketResponse {
|
||||
upload: DirectUploadTicketPayload::from(OssPostObjectResponse {
|
||||
signature_version: "v1",
|
||||
provider: "aliyun-oss",
|
||||
bucket: "genarrative-assets".to_string(),
|
||||
endpoint: "oss-cn-shanghai.aliyuncs.com".to_string(),
|
||||
host: "https://genarrative-assets.oss-cn-shanghai.aliyuncs.com".to_string(),
|
||||
object_key: "generated-characters/hero/master.png".to_string(),
|
||||
legacy_public_path: "/generated-characters/hero/master.png".to_string(),
|
||||
content_type: Some("image/png".to_string()),
|
||||
access: OssObjectAccess::Private,
|
||||
key_prefix: "generated-characters/hero".to_string(),
|
||||
expires_at: "2026-04-21T00:00:00Z".to_string(),
|
||||
max_size_bytes: 1024,
|
||||
success_action_status: 200,
|
||||
form_fields: OssPostObjectFormFields {
|
||||
key: "generated-characters/hero/master.png".to_string(),
|
||||
policy: "policy".to_string(),
|
||||
oss_access_key_id: "ak".to_string(),
|
||||
signature: "sig".to_string(),
|
||||
success_action_status: "200".to_string(),
|
||||
content_type: Some("image/png".to_string()),
|
||||
metadata: BTreeMap::from([(
|
||||
"x-oss-meta-asset-kind".to_string(),
|
||||
"character_visual".to_string(),
|
||||
)]),
|
||||
},
|
||||
}),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["upload"]["signatureVersion"], json!("v1"));
|
||||
assert_eq!(
|
||||
payload["upload"]["formFields"]["OSSAccessKeyId"],
|
||||
json!("ak")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["upload"]["formFields"]["x-oss-meta-asset-kind"],
|
||||
json!("character_visual")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_asset_object_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ConfirmAssetObjectResponse {
|
||||
asset_object: AssetObjectPayload {
|
||||
asset_object_id: "assetobj_1".to_string(),
|
||||
bucket: "genarrative-assets".to_string(),
|
||||
object_key: "generated-characters/hero/master.png".to_string(),
|
||||
access_policy: "private".to_string(),
|
||||
content_type: Some("image/png".to_string()),
|
||||
content_length: 1024,
|
||||
content_hash: Some("etag-1".to_string()),
|
||||
version: 1,
|
||||
source_job_id: Some("job_1".to_string()),
|
||||
owner_user_id: Some("user_1".to_string()),
|
||||
profile_id: Some("profile_1".to_string()),
|
||||
entity_id: Some("entity_1".to_string()),
|
||||
asset_kind: "character_visual".to_string(),
|
||||
created_at: "1.000000Z".to_string(),
|
||||
updated_at: "1.000000Z".to_string(),
|
||||
},
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["assetObject"]["assetObjectId"], json!("assetobj_1"));
|
||||
assert_eq!(payload["assetObject"]["accessPolicy"], json!("private"));
|
||||
assert_eq!(payload["assetObject"]["contentLength"], json!(1024));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_visual_source_mode_uses_legacy_kebab_case() {
|
||||
let payload = serde_json::to_value(CharacterVisualSourceMode::ImageToImage)
|
||||
.expect("source mode should serialize");
|
||||
|
||||
assert_eq!(payload, json!("image-to-image"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_visual_generate_response_keeps_legacy_shape() {
|
||||
let payload = serde_json::to_value(CharacterVisualGenerateResponse {
|
||||
ok: true,
|
||||
task_id: "visual_1".to_string(),
|
||||
model: "rust-svg-character-visual".to_string(),
|
||||
prompt: "角色提示词".to_string(),
|
||||
drafts: vec![CharacterVisualDraftPayload {
|
||||
id: "candidate-1".to_string(),
|
||||
label: "候选 1".to_string(),
|
||||
image_src: "/generated-character-drafts/hero/visual/visual_1/candidate-01.svg"
|
||||
.to_string(),
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
}],
|
||||
})
|
||||
.expect("response should serialize");
|
||||
|
||||
assert_eq!(payload["ok"], json!(true));
|
||||
assert_eq!(payload["taskId"], json!("visual_1"));
|
||||
assert_eq!(
|
||||
payload["drafts"][0]["imageSrc"],
|
||||
json!("/generated-character-drafts/hero/visual/visual_1/candidate-01.svg")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_templates_response_keeps_legacy_shape() {
|
||||
let payload = serde_json::to_value(CharacterAnimationTemplatesResponse {
|
||||
ok: true,
|
||||
templates: vec![CharacterAnimationTemplatePayload {
|
||||
id: "idle_loop".to_string(),
|
||||
label: "待机循环".to_string(),
|
||||
animation: "idle".to_string(),
|
||||
prompt_suffix: "保持呼吸感。".to_string(),
|
||||
notes: "默认待机模板。".to_string(),
|
||||
}],
|
||||
})
|
||||
.expect("response should serialize");
|
||||
|
||||
assert_eq!(payload["ok"], json!(true));
|
||||
assert_eq!(payload["templates"][0]["id"], json!("idle_loop"));
|
||||
assert_eq!(
|
||||
payload["templates"][0]["promptSuffix"],
|
||||
json!("保持呼吸感。")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_import_video_response_keeps_legacy_shape() {
|
||||
let payload =
|
||||
serde_json::to_value(CharacterAnimationImportVideoResponse {
|
||||
ok: true,
|
||||
imported_video_path:
|
||||
"/generated-character-drafts/hero/animation/idle/import-1/reference.mp4"
|
||||
.to_string(),
|
||||
draft_id: "animation-import-1".to_string(),
|
||||
save_message: "参考视频已导入 OSS 草稿区。".to_string(),
|
||||
})
|
||||
.expect("response should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload["importedVideoPath"],
|
||||
json!("/generated-character-drafts/hero/animation/idle/import-1/reference.mp4")
|
||||
);
|
||||
assert_eq!(payload["draftId"], json!("animation-import-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_workflow_cache_response_keeps_legacy_shape() {
|
||||
let payload = serde_json::to_value(CharacterWorkflowCacheSaveResponse {
|
||||
ok: true,
|
||||
cache: CharacterWorkflowCachePayload {
|
||||
character_id: "hero".to_string(),
|
||||
cache_scope_id: Some("world-01".to_string()),
|
||||
visual_prompt_text: "主形象".to_string(),
|
||||
animation_prompt_text: "待机".to_string(),
|
||||
visual_drafts: vec![CharacterVisualDraftPayload {
|
||||
id: "draft-1".to_string(),
|
||||
label: "候选 1".to_string(),
|
||||
image_src: "/generated-character-drafts/hero/visual/job/candidate.svg"
|
||||
.to_string(),
|
||||
width: 1024,
|
||||
height: 1536,
|
||||
}],
|
||||
selected_visual_draft_id: "draft-1".to_string(),
|
||||
selected_animation: "idle".to_string(),
|
||||
image_src: Some("/generated-characters/hero/master.png".to_string()),
|
||||
generated_visual_asset_id: None,
|
||||
generated_animation_set_id: None,
|
||||
animation_map: Some(json!({ "idle": { "frames": 4 } })),
|
||||
updated_at: Some("2026-04-22T12:00:00Z".to_string()),
|
||||
},
|
||||
save_message: "角色形象生成缓存已更新。".to_string(),
|
||||
})
|
||||
.expect("response should serialize");
|
||||
|
||||
assert_eq!(payload["ok"], json!(true));
|
||||
assert_eq!(payload["cache"]["characterId"], json!("hero"));
|
||||
assert_eq!(payload["cache"]["cacheScopeId"], json!("world-01"));
|
||||
assert_eq!(
|
||||
payload["cache"]["visualDrafts"][0]["imageSrc"],
|
||||
json!("/generated-character-drafts/hero/visual/job/candidate.svg")
|
||||
);
|
||||
assert_eq!(payload["cache"]["animationMap"]["idle"]["frames"], json!(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_strategy_uses_legacy_kebab_case() {
|
||||
let payload = serde_json::to_value(CharacterAnimationStrategy::MotionTransfer)
|
||||
.expect("strategy should serialize");
|
||||
|
||||
assert_eq!(payload, json!("motion-transfer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_generate_response_keeps_image_sequence_shape() {
|
||||
let payload = serde_json::to_value(CharacterAnimationGenerateResponse {
|
||||
ok: true,
|
||||
task_id: "animation_1".to_string(),
|
||||
strategy: CharacterAnimationStrategy::ImageSequence,
|
||||
model: "rust-svg-animation-sequence".to_string(),
|
||||
prompt: "待机动作".to_string(),
|
||||
image_sources: vec![
|
||||
"/generated-character-drafts/hero/animation/idle/job/frame-01.svg".to_string(),
|
||||
],
|
||||
preview_video_path: None,
|
||||
})
|
||||
.expect("response should serialize");
|
||||
|
||||
assert_eq!(payload["ok"], json!(true));
|
||||
assert_eq!(payload["taskId"], json!("animation_1"));
|
||||
assert_eq!(payload["strategy"], json!("image-sequence"));
|
||||
assert_eq!(
|
||||
payload["imageSources"][0],
|
||||
json!("/generated-character-drafts/hero/animation/idle/job/frame-01.svg")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_publish_response_keeps_legacy_shape() {
|
||||
let payload = serde_json::to_value(CharacterAnimationPublishResponse {
|
||||
ok: true,
|
||||
animation_set_id: "animation-set-1".to_string(),
|
||||
override_map: json!({}),
|
||||
animation_map: json!({
|
||||
"idle": {
|
||||
"folder": "idle",
|
||||
"prefix": "frame",
|
||||
"frames": 2,
|
||||
"startFrame": 1,
|
||||
"extension": "svg",
|
||||
"basePath": "/generated-animations/hero/animation-set-1/idle",
|
||||
"frameWidth": 192,
|
||||
"frameHeight": 256,
|
||||
"fps": 8,
|
||||
"loop": true
|
||||
}
|
||||
}),
|
||||
save_message: "基础动作资源已写入 OSS 并绑定当前角色。".to_string(),
|
||||
})
|
||||
.expect("response should serialize");
|
||||
|
||||
assert_eq!(payload["animationSetId"], json!("animation-set-1"));
|
||||
assert_eq!(payload["animationMap"]["idle"]["frames"], json!(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_draft_payload_accepts_backend_extraction_fields() {
|
||||
let payload = serde_json::from_value::<CharacterAnimationDraftPayload>(json!({
|
||||
"fps": 8,
|
||||
"loop": true,
|
||||
"frameWidth": 192,
|
||||
"frameHeight": 256,
|
||||
"frameCount": 8,
|
||||
"applyChromaKey": true,
|
||||
"sampleStartRatio": 0.12,
|
||||
"sampleEndRatio": 0.94,
|
||||
"previewVideoPath": "/generated-character-drafts/hero/animation/idle/task/preview.mp4"
|
||||
}))
|
||||
.expect("draft payload should deserialize without framesDataUrls");
|
||||
|
||||
assert!(payload.frames_data_urls.is_empty());
|
||||
assert_eq!(payload.frame_count, Some(8));
|
||||
assert_eq!(payload.apply_chroma_key, Some(true));
|
||||
assert_eq!(payload.sample_start_ratio, Some(0.12));
|
||||
assert_eq!(payload.sample_end_ratio, Some(0.94));
|
||||
}
|
||||
}
|
||||
266
server-rs/crates/shared-contracts/src/auth.rs
Normal file
266
server-rs/crates/shared-contracts/src/auth.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const AUTH_LOGIN_METHOD_PASSWORD: &str = "password";
|
||||
pub const AUTH_LOGIN_METHOD_PHONE: &str = "phone";
|
||||
pub const AUTH_LOGIN_METHOD_WECHAT: &str = "wechat";
|
||||
pub const AUTH_BINDING_STATUS_ACTIVE: &str = "active";
|
||||
pub const AUTH_BINDING_STATUS_PENDING_BIND_PHONE: &str = "pending_bind_phone";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthLoginOptionsResponse {
|
||||
pub available_login_methods: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthUserPayload {
|
||||
pub id: String,
|
||||
pub public_user_code: String,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub phone_number_masked: Option<String>,
|
||||
pub login_method: String,
|
||||
pub binding_status: String,
|
||||
pub wechat_bound: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PublicUserSummaryPayload {
|
||||
pub id: String,
|
||||
pub public_user_code: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PublicUserSearchResponse {
|
||||
pub user: PublicUserSummaryPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordEntryRequest {
|
||||
pub phone: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordEntryResponse {
|
||||
pub token: String,
|
||||
pub user: AuthUserPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordChangeRequest {
|
||||
pub current_password: Option<String>,
|
||||
pub new_password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordChangeResponse {
|
||||
pub user: AuthUserPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordResetRequest {
|
||||
pub phone: String,
|
||||
pub code: String,
|
||||
pub new_password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordResetResponse {
|
||||
pub token: String,
|
||||
pub user: AuthUserPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthMeResponse {
|
||||
pub user: AuthUserPayload,
|
||||
pub available_login_methods: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthSessionsResponse {
|
||||
pub sessions: Vec<AuthSessionSummaryPayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthSessionSummaryPayload {
|
||||
pub session_id: String,
|
||||
pub client_type: String,
|
||||
pub client_runtime: String,
|
||||
pub client_platform: String,
|
||||
pub client_label: String,
|
||||
pub device_display_name: String,
|
||||
pub mini_program_app_id: Option<String>,
|
||||
pub mini_program_env: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub ip_masked: Option<String>,
|
||||
pub is_current: bool,
|
||||
pub created_at: String,
|
||||
pub last_seen_at: String,
|
||||
pub expires_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RefreshSessionResponse {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct LogoutResponse {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct LogoutAllResponse {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PhoneSendCodeRequest {
|
||||
pub phone: String,
|
||||
pub scene: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PhoneSendCodeResponse {
|
||||
pub ok: bool,
|
||||
pub cooldown_seconds: u64,
|
||||
pub expires_in_seconds: u64,
|
||||
pub provider_request_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PhoneLoginRequest {
|
||||
pub phone: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PhoneLoginResponse {
|
||||
pub token: String,
|
||||
pub user: AuthUserPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WechatStartQuery {
|
||||
pub redirect_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WechatStartResponse {
|
||||
pub authorization_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WechatCallbackQuery {
|
||||
pub state: Option<String>,
|
||||
pub code: Option<String>,
|
||||
pub mock_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WechatBindPhoneRequest {
|
||||
pub phone: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WechatBindPhoneResponse {
|
||||
pub token: String,
|
||||
pub user: AuthUserPayload,
|
||||
}
|
||||
|
||||
pub fn build_available_login_methods(
|
||||
sms_auth_enabled: bool,
|
||||
password_auth_enabled: bool,
|
||||
wechat_auth_enabled: bool,
|
||||
) -> Vec<String> {
|
||||
let mut methods = Vec::new();
|
||||
if sms_auth_enabled {
|
||||
methods.push(AUTH_LOGIN_METHOD_PHONE.to_string());
|
||||
}
|
||||
if password_auth_enabled {
|
||||
methods.push(AUTH_LOGIN_METHOD_PASSWORD.to_string());
|
||||
}
|
||||
if wechat_auth_enabled {
|
||||
methods.push(AUTH_LOGIN_METHOD_WECHAT.to_string());
|
||||
}
|
||||
methods
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn available_login_methods_keep_phone_then_wechat_order() {
|
||||
let methods = build_available_login_methods(true, true, true);
|
||||
|
||||
assert_eq!(
|
||||
methods,
|
||||
vec![
|
||||
AUTH_LOGIN_METHOD_PHONE.to_string(),
|
||||
AUTH_LOGIN_METHOD_PASSWORD.to_string(),
|
||||
AUTH_LOGIN_METHOD_WECHAT.to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn password_entry_request_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(PasswordEntryRequest {
|
||||
phone: "13800138000".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"phone": "13800138000",
|
||||
"password": "secret123"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wechat_callback_query_keeps_provider_compatible_field_names() {
|
||||
let payload = serde_json::to_value(WechatCallbackQuery {
|
||||
state: Some("state-1".to_string()),
|
||||
code: Some("code-1".to_string()),
|
||||
mock_code: Some("mock-1".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"state": "state-1",
|
||||
"code": "code-1",
|
||||
"mock_code": "mock-1"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
240
server-rs/crates/shared-contracts/src/big_fish.rs
Normal file
240
server-rs/crates/shared-contracts/src/big_fish.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateBigFishSessionRequest {
|
||||
#[serde(default)]
|
||||
pub seed_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendBigFishMessageRequest {
|
||||
pub client_message_id: String,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub quick_fill_requested: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecuteBigFishActionRequest {
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub level: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub motion_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubmitBigFishInputRequest {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishAnchorItemResponse {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishAnchorPackResponse {
|
||||
pub gameplay_promise: BigFishAnchorItemResponse,
|
||||
pub ecology_visual_theme: BigFishAnchorItemResponse,
|
||||
pub growth_ladder: BigFishAnchorItemResponse,
|
||||
pub risk_tempo: BigFishAnchorItemResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishLevelBlueprintResponse {
|
||||
pub level: u32,
|
||||
pub name: String,
|
||||
pub one_line_fantasy: String,
|
||||
pub silhouette_direction: String,
|
||||
pub size_ratio: f32,
|
||||
pub visual_prompt_seed: String,
|
||||
pub motion_prompt_seed: String,
|
||||
pub merge_source_level: Option<u32>,
|
||||
pub prey_window: Vec<u32>,
|
||||
pub threat_window: Vec<u32>,
|
||||
pub is_final_level: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishBackgroundBlueprintResponse {
|
||||
pub theme: String,
|
||||
pub color_mood: String,
|
||||
pub foreground_hints: String,
|
||||
pub midground_composition: String,
|
||||
pub background_depth: String,
|
||||
pub safe_play_area_hint: String,
|
||||
pub spawn_edge_hint: String,
|
||||
pub background_prompt_seed: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishRuntimeParamsResponse {
|
||||
pub level_count: u32,
|
||||
pub merge_count_per_upgrade: u32,
|
||||
pub spawn_target_count: u32,
|
||||
pub leader_move_speed: f32,
|
||||
pub follower_catch_up_speed: f32,
|
||||
pub offscreen_cull_seconds: f32,
|
||||
pub prey_spawn_delta_levels: Vec<u32>,
|
||||
pub threat_spawn_delta_levels: Vec<u32>,
|
||||
pub win_level: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishGameDraftResponse {
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub core_fun: String,
|
||||
pub ecology_theme: String,
|
||||
pub levels: Vec<BigFishLevelBlueprintResponse>,
|
||||
pub background: BigFishBackgroundBlueprintResponse,
|
||||
pub runtime_params: BigFishRuntimeParamsResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishAgentMessageResponse {
|
||||
pub id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishAssetSlotResponse {
|
||||
pub slot_id: String,
|
||||
pub asset_kind: String,
|
||||
pub level: Option<u32>,
|
||||
pub motion_key: Option<String>,
|
||||
pub status: String,
|
||||
pub asset_url: Option<String>,
|
||||
pub prompt_snapshot: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishAssetCoverageResponse {
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub required_level_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishSessionSnapshotResponse {
|
||||
pub session_id: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub anchor_pack: BigFishAnchorPackResponse,
|
||||
pub draft: Option<BigFishGameDraftResponse>,
|
||||
pub asset_slots: Vec<BigFishAssetSlotResponse>,
|
||||
pub asset_coverage: BigFishAssetCoverageResponse,
|
||||
pub messages: Vec<BigFishAgentMessageResponse>,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishSessionResponse {
|
||||
pub session: BigFishSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishActionResponse {
|
||||
pub session: BigFishSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishVector2Response {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishRuntimeEntityResponse {
|
||||
pub entity_id: String,
|
||||
pub level: u32,
|
||||
pub position: BigFishVector2Response,
|
||||
pub radius: f32,
|
||||
pub offscreen_seconds: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishRuntimeSnapshotResponse {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub status: String,
|
||||
pub tick: u64,
|
||||
pub player_level: u32,
|
||||
pub win_level: u32,
|
||||
pub leader_entity_id: Option<String>,
|
||||
pub owned_entities: Vec<BigFishRuntimeEntityResponse>,
|
||||
pub wild_entities: Vec<BigFishRuntimeEntityResponse>,
|
||||
pub camera_center: BigFishVector2Response,
|
||||
pub last_input: BigFishVector2Response,
|
||||
pub event_log: Vec<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishRunResponse {
|
||||
pub run: BigFishRuntimeSnapshotResponse,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn big_fish_session_request_uses_camel_case() {
|
||||
let payload = serde_json::to_value(CreateBigFishSessionRequest {
|
||||
seed_text: Some("深海机械鱼".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload, json!({ "seedText": "深海机械鱼" }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_fish_action_request_uses_camel_case() {
|
||||
let payload = serde_json::to_value(ExecuteBigFishActionRequest {
|
||||
action: "big_fish_generate_level_motion".to_string(),
|
||||
level: Some(3),
|
||||
motion_key: Some("move_swim".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["motionKey"], json!("move_swim"));
|
||||
assert_eq!(payload["level"], json!(3));
|
||||
}
|
||||
}
|
||||
26
server-rs/crates/shared-contracts/src/big_fish_works.rs
Normal file
26
server-rs/crates/shared-contracts/src/big_fish_works.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishWorkSummaryResponse {
|
||||
pub work_id: String,
|
||||
pub source_session_id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub summary: String,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
pub status: String,
|
||||
pub updated_at: String,
|
||||
pub publish_ready: bool,
|
||||
pub level_count: u32,
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishWorksResponse {
|
||||
pub items: Vec<BigFishWorkSummaryResponse>,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ParseCreationAgentDocumentInputRequest {
|
||||
pub file_name: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
pub content_base64: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationAgentDocumentInputPayload {
|
||||
pub file_name: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
pub size_bytes: usize,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ParseCreationAgentDocumentInputResponse {
|
||||
pub document: CreationAgentDocumentInputPayload,
|
||||
}
|
||||
16
server-rs/crates/shared-contracts/src/lib.rs
Normal file
16
server-rs/crates/shared-contracts/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod admin;
|
||||
pub mod ai;
|
||||
pub mod api;
|
||||
pub mod assets;
|
||||
pub mod auth;
|
||||
pub mod big_fish;
|
||||
pub mod big_fish_works;
|
||||
pub mod creation_agent_document_input;
|
||||
pub mod llm;
|
||||
pub mod puzzle_agent;
|
||||
pub mod puzzle_gallery;
|
||||
pub mod puzzle_runtime;
|
||||
pub mod puzzle_works;
|
||||
pub mod runtime;
|
||||
pub mod runtime_story;
|
||||
pub mod story;
|
||||
62
server-rs/crates/shared-contracts/src/llm.rs
Normal file
62
server-rs/crates/shared-contracts/src/llm.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LlmChatMessageRole {
|
||||
System,
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct LlmChatMessagePayload {
|
||||
pub role: LlmChatMessageRole,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct LlmChatCompletionRequest {
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stream: bool,
|
||||
pub messages: Vec<LlmChatMessagePayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LlmChatCompletionResponse {
|
||||
pub id: Option<String>,
|
||||
pub model: String,
|
||||
pub content: String,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn llm_chat_completion_request_keeps_openai_compatible_field_names() {
|
||||
let payload = serde_json::to_value(LlmChatCompletionRequest {
|
||||
model: Some("doubao-test".to_string()),
|
||||
stream: false,
|
||||
messages: vec![
|
||||
LlmChatMessagePayload {
|
||||
role: LlmChatMessageRole::System,
|
||||
content: "系统".to_string(),
|
||||
},
|
||||
LlmChatMessagePayload {
|
||||
role: LlmChatMessageRole::User,
|
||||
content: "用户".to_string(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["model"], json!("doubao-test"));
|
||||
assert_eq!(payload["stream"], json!(false));
|
||||
assert_eq!(payload["messages"][0]["role"], json!("system"));
|
||||
}
|
||||
}
|
||||
192
server-rs/crates/shared-contracts/src/puzzle_agent.rs
Normal file
192
server-rs/crates/shared-contracts/src/puzzle_agent.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePuzzleAgentSessionRequest {
|
||||
#[serde(default)]
|
||||
pub seed_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendPuzzleAgentMessageRequest {
|
||||
pub client_message_id: String,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub quick_fill_requested: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecutePuzzleAgentActionRequest {
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub prompt_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub candidate_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub candidate_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub level_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default)]
|
||||
pub theme_tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAnchorItemResponse {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAnchorPackResponse {
|
||||
pub theme_promise: PuzzleAnchorItemResponse,
|
||||
pub visual_subject: PuzzleAnchorItemResponse,
|
||||
pub visual_mood: PuzzleAnchorItemResponse,
|
||||
pub composition_hooks: PuzzleAnchorItemResponse,
|
||||
pub tags_and_forbidden: PuzzleAnchorItemResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleGeneratedImageCandidateResponse {
|
||||
pub candidate_id: String,
|
||||
pub image_src: String,
|
||||
pub asset_id: String,
|
||||
pub prompt: String,
|
||||
#[serde(default)]
|
||||
pub actual_prompt: Option<String>,
|
||||
pub source_type: String,
|
||||
pub selected: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleCreatorIntentResponse {
|
||||
pub source_mode: String,
|
||||
pub raw_messages_summary: String,
|
||||
pub theme_promise: String,
|
||||
pub visual_subject: String,
|
||||
pub visual_mood: Vec<String>,
|
||||
pub composition_hooks: Vec<String>,
|
||||
pub theme_tags: Vec<String>,
|
||||
pub forbidden_directives: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleResultDraftResponse {
|
||||
pub level_name: String,
|
||||
pub summary: String,
|
||||
pub theme_tags: Vec<String>,
|
||||
pub forbidden_directives: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub creator_intent: Option<PuzzleCreatorIntentResponse>,
|
||||
pub anchor_pack: PuzzleAnchorPackResponse,
|
||||
pub candidates: Vec<PuzzleGeneratedImageCandidateResponse>,
|
||||
#[serde(default)]
|
||||
pub selected_candidate_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cover_asset_id: Option<String>,
|
||||
pub generation_status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAgentMessageResponse {
|
||||
pub id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAgentSuggestedActionResponse {
|
||||
pub id: String,
|
||||
pub action_type: String,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleResultPreviewBlockerResponse {
|
||||
pub id: String,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleResultPreviewFindingResponse {
|
||||
pub id: String,
|
||||
pub severity: String,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleResultPreviewEnvelopeResponse {
|
||||
pub draft: PuzzleResultDraftResponse,
|
||||
pub blockers: Vec<PuzzleResultPreviewBlockerResponse>,
|
||||
pub quality_findings: Vec<PuzzleResultPreviewFindingResponse>,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAgentSessionSnapshotResponse {
|
||||
pub session_id: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub anchor_pack: PuzzleAnchorPackResponse,
|
||||
#[serde(default)]
|
||||
pub draft: Option<PuzzleResultDraftResponse>,
|
||||
pub messages: Vec<PuzzleAgentMessageResponse>,
|
||||
#[serde(default)]
|
||||
pub last_assistant_reply: Option<String>,
|
||||
#[serde(default)]
|
||||
pub published_profile_id: Option<String>,
|
||||
pub suggested_actions: Vec<PuzzleAgentSuggestedActionResponse>,
|
||||
#[serde(default)]
|
||||
pub result_preview: Option<PuzzleResultPreviewEnvelopeResponse>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAgentSessionResponse {
|
||||
pub session: PuzzleAgentSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAgentOperationResponse {
|
||||
pub operation_id: String,
|
||||
pub operation_type: String,
|
||||
pub status: String,
|
||||
pub phase_label: String,
|
||||
pub phase_detail: String,
|
||||
pub progress: u32,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleAgentActionResponse {
|
||||
pub operation: PuzzleAgentOperationResponse,
|
||||
/// 操作完成后的最新会话快照,供前端直接更新界面,避免重复拉取完整 session。
|
||||
pub session: PuzzleAgentSessionSnapshotResponse,
|
||||
}
|
||||
15
server-rs/crates/shared-contracts/src/puzzle_gallery.rs
Normal file
15
server-rs/crates/shared-contracts/src/puzzle_gallery.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::puzzle_works::PuzzleWorkSummaryResponse;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleGalleryResponse {
|
||||
pub items: Vec<PuzzleWorkSummaryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleGalleryDetailResponse {
|
||||
pub item: PuzzleWorkSummaryResponse,
|
||||
}
|
||||
107
server-rs/crates/shared-contracts/src/puzzle_runtime.rs
Normal file
107
server-rs/crates/shared-contracts/src/puzzle_runtime.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartPuzzleRunRequest {
|
||||
pub profile_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdvanceLocalPuzzleNextLevelRequest {
|
||||
pub run: PuzzleRunSnapshotResponse,
|
||||
#[serde(default)]
|
||||
pub source_session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapPuzzlePiecesRequest {
|
||||
pub first_piece_id: String,
|
||||
pub second_piece_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DragPuzzlePieceRequest {
|
||||
pub piece_id: String,
|
||||
pub target_row: u32,
|
||||
pub target_col: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleCellPositionResponse {
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzlePieceStateResponse {
|
||||
pub piece_id: String,
|
||||
pub correct_row: u32,
|
||||
pub correct_col: u32,
|
||||
pub current_row: u32,
|
||||
pub current_col: u32,
|
||||
#[serde(default)]
|
||||
pub merged_group_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleMergedGroupStateResponse {
|
||||
pub group_id: String,
|
||||
pub piece_ids: Vec<String>,
|
||||
pub occupied_cells: Vec<PuzzleCellPositionResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleBoardSnapshotResponse {
|
||||
pub rows: u32,
|
||||
pub cols: u32,
|
||||
pub pieces: Vec<PuzzlePieceStateResponse>,
|
||||
pub merged_groups: Vec<PuzzleMergedGroupStateResponse>,
|
||||
#[serde(default)]
|
||||
pub selected_piece_id: Option<String>,
|
||||
pub all_tiles_resolved: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleRuntimeLevelSnapshotResponse {
|
||||
pub run_id: String,
|
||||
pub level_index: u32,
|
||||
pub grid_size: u32,
|
||||
pub profile_id: String,
|
||||
pub level_name: String,
|
||||
pub author_display_name: String,
|
||||
pub theme_tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
pub board: PuzzleBoardSnapshotResponse,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleRunSnapshotResponse {
|
||||
pub run_id: String,
|
||||
pub entry_profile_id: String,
|
||||
pub cleared_level_count: u32,
|
||||
pub current_level_index: u32,
|
||||
pub current_grid_size: u32,
|
||||
pub played_profile_ids: Vec<String>,
|
||||
pub previous_level_tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub current_level: Option<PuzzleRuntimeLevelSnapshotResponse>,
|
||||
#[serde(default)]
|
||||
pub recommended_next_profile_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleRunResponse {
|
||||
pub run: PuzzleRunSnapshotResponse,
|
||||
}
|
||||
65
server-rs/crates/shared-contracts/src/puzzle_works.rs
Normal file
65
server-rs/crates/shared-contracts/src/puzzle_works.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::puzzle_agent::PuzzleAnchorPackResponse;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutPuzzleWorkRequest {
|
||||
pub level_name: String,
|
||||
pub summary: String,
|
||||
pub theme_tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cover_asset_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleWorkSummaryResponse {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
#[serde(default)]
|
||||
pub source_session_id: Option<String>,
|
||||
pub author_display_name: String,
|
||||
pub level_name: String,
|
||||
pub summary: String,
|
||||
pub theme_tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cover_asset_id: Option<String>,
|
||||
pub publication_status: String,
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub published_at: Option<String>,
|
||||
pub play_count: u32,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleWorkProfileResponse {
|
||||
#[serde(flatten)]
|
||||
pub summary: PuzzleWorkSummaryResponse,
|
||||
pub anchor_pack: PuzzleAnchorPackResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleWorksResponse {
|
||||
pub items: Vec<PuzzleWorkSummaryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleWorkDetailResponse {
|
||||
pub item: PuzzleWorkProfileResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleWorkMutationResponse {
|
||||
pub item: PuzzleWorkProfileResponse,
|
||||
}
|
||||
897
server-rs/crates/shared-contracts/src/runtime.rs
Normal file
897
server-rs/crates/shared-contracts/src/runtime.rs
Normal file
@@ -0,0 +1,897 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const RUNTIME_PLATFORM_THEME_LIGHT: &str = "light";
|
||||
pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark";
|
||||
pub const SAVE_SNAPSHOT_VERSION: u32 = 2;
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE: &str = "points_recharge";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD: &str = "invite_inviter_reward";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_TIDE: &str = "tide";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_RIFT: &str = "rift";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MYTHIC: &str = "mythic";
|
||||
pub const CUSTOM_WORLD_VISIBILITY_DRAFT: &str = "draft";
|
||||
pub const CUSTOM_WORLD_VISIBILITY_PUBLISHED: &str = "published";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeSettingsResponse {
|
||||
pub music_volume: f32,
|
||||
pub platform_theme: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutRuntimeSettingsRequest {
|
||||
pub music_volume: f32,
|
||||
pub platform_theme: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SavedGameSnapshotResponse {
|
||||
pub version: u32,
|
||||
pub saved_at: String,
|
||||
pub game_state: serde_json::Value,
|
||||
pub bottom_tab: String,
|
||||
pub current_story: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutSavedGameSnapshotRequest {
|
||||
pub game_state: serde_json::Value,
|
||||
pub bottom_tab: String,
|
||||
#[serde(default)]
|
||||
pub current_story: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub saved_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BasicOkResponse {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlatformBrowseHistoryEntryResponse {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub theme_mode: String,
|
||||
pub author_display_name: String,
|
||||
pub visited_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlatformBrowseHistoryWriteEntryRequest {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub world_name: String,
|
||||
#[serde(default)]
|
||||
pub subtitle: Option<String>,
|
||||
#[serde(default)]
|
||||
pub summary_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub theme_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub author_display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub visited_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlatformBrowseHistoryBatchSyncRequest {
|
||||
pub entries: Vec<PlatformBrowseHistoryWriteEntryRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PlatformBrowseHistoryUpsertRequest {
|
||||
Single(PlatformBrowseHistoryWriteEntryRequest),
|
||||
Batch(PlatformBrowseHistoryBatchSyncRequest),
|
||||
}
|
||||
|
||||
impl PlatformBrowseHistoryUpsertRequest {
|
||||
pub fn into_entries(self) -> Vec<PlatformBrowseHistoryWriteEntryRequest> {
|
||||
match self {
|
||||
Self::Single(entry) => vec![entry],
|
||||
Self::Batch(batch) => batch.entries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlatformBrowseHistoryResponse {
|
||||
pub entries: Vec<PlatformBrowseHistoryEntryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileDashboardSummaryResponse {
|
||||
pub wallet_balance: u64,
|
||||
pub total_play_time_ms: u64,
|
||||
pub played_world_count: u32,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileWalletLedgerEntryResponse {
|
||||
pub id: String,
|
||||
pub amount_delta: i64,
|
||||
pub balance_after: u64,
|
||||
pub source_type: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileWalletLedgerResponse {
|
||||
pub entries: Vec<ProfileWalletLedgerEntryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileRechargeProductResponse {
|
||||
pub product_id: String,
|
||||
pub title: String,
|
||||
pub price_cents: u64,
|
||||
pub kind: String,
|
||||
pub points_amount: u64,
|
||||
pub bonus_points: u64,
|
||||
pub duration_days: u32,
|
||||
pub badge_label: String,
|
||||
pub description: String,
|
||||
pub tier: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileMembershipBenefitResponse {
|
||||
pub benefit_name: String,
|
||||
pub normal_value: String,
|
||||
pub month_value: String,
|
||||
pub season_value: String,
|
||||
pub year_value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileMembershipResponse {
|
||||
pub status: String,
|
||||
pub tier: String,
|
||||
pub started_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileRechargeOrderResponse {
|
||||
pub order_id: String,
|
||||
pub product_id: String,
|
||||
pub product_title: String,
|
||||
pub kind: String,
|
||||
pub amount_cents: u64,
|
||||
pub status: String,
|
||||
pub payment_channel: String,
|
||||
pub paid_at: String,
|
||||
pub created_at: String,
|
||||
pub points_delta: i64,
|
||||
pub membership_expires_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileRechargeCenterResponse {
|
||||
pub wallet_balance: u64,
|
||||
pub membership: ProfileMembershipResponse,
|
||||
pub point_products: Vec<ProfileRechargeProductResponse>,
|
||||
pub membership_products: Vec<ProfileRechargeProductResponse>,
|
||||
pub benefits: Vec<ProfileMembershipBenefitResponse>,
|
||||
pub latest_order: Option<ProfileRechargeOrderResponse>,
|
||||
pub has_points_recharged: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateProfileRechargeOrderRequest {
|
||||
pub product_id: String,
|
||||
#[serde(default)]
|
||||
pub payment_channel: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateProfileRechargeOrderResponse {
|
||||
pub order: ProfileRechargeOrderResponse,
|
||||
pub center: ProfileRechargeCenterResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileReferralInviteCenterResponse {
|
||||
pub invite_code: String,
|
||||
pub invite_link_path: String,
|
||||
pub invited_count: u32,
|
||||
pub rewarded_invite_count: u32,
|
||||
pub today_inviter_reward_count: u32,
|
||||
pub today_inviter_reward_remaining: u32,
|
||||
pub reward_points: u64,
|
||||
pub has_redeemed_code: bool,
|
||||
pub bound_inviter_user_id: Option<String>,
|
||||
pub bound_at: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RedeemProfileReferralInviteCodeRequest {
|
||||
pub invite_code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RedeemProfileReferralInviteCodeResponse {
|
||||
pub center: ProfileReferralInviteCenterResponse,
|
||||
pub invitee_reward_granted: bool,
|
||||
pub inviter_reward_granted: bool,
|
||||
pub invitee_balance_after: u64,
|
||||
pub inviter_balance_after: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfilePlayedWorkSummaryResponse {
|
||||
pub world_key: String,
|
||||
pub owner_user_id: Option<String>,
|
||||
pub profile_id: Option<String>,
|
||||
pub world_type: Option<String>,
|
||||
pub world_title: String,
|
||||
pub world_subtitle: String,
|
||||
pub first_played_at: String,
|
||||
pub last_played_at: String,
|
||||
pub last_observed_play_time_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfilePlayStatsResponse {
|
||||
pub total_play_time_ms: u64,
|
||||
pub played_works: Vec<ProfilePlayedWorkSummaryResponse>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileSaveArchiveSummaryResponse {
|
||||
pub world_key: String,
|
||||
pub owner_user_id: Option<String>,
|
||||
pub profile_id: Option<String>,
|
||||
pub world_type: Option<String>,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub last_played_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileSaveArchiveListResponse {
|
||||
pub entries: Vec<ProfileSaveArchiveSummaryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileSaveArchiveResumeResponse {
|
||||
pub entry: ProfileSaveArchiveSummaryResponse,
|
||||
pub snapshot: SavedGameSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeInventorySlotResponse {
|
||||
pub slot_id: String,
|
||||
pub container_kind: String,
|
||||
pub slot_key: String,
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: String,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<String>,
|
||||
pub source_kind: String,
|
||||
pub source_reference_id: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeInventoryStateResponse {
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub backpack_items: Vec<RuntimeInventorySlotResponse>,
|
||||
pub equipment_items: Vec<RuntimeInventorySlotResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldProfileUpsertRequest {
|
||||
pub profile: serde_json::Value,
|
||||
pub source_agent_session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldLibraryEntryResponse {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub public_work_code: Option<String>,
|
||||
pub author_public_user_code: Option<String>,
|
||||
pub profile: serde_json::Value,
|
||||
pub visibility: String,
|
||||
pub published_at: Option<String>,
|
||||
pub updated_at: String,
|
||||
pub author_display_name: String,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldGalleryCardResponse {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub public_work_code: String,
|
||||
pub author_public_user_code: String,
|
||||
pub visibility: String,
|
||||
pub published_at: Option<String>,
|
||||
pub updated_at: String,
|
||||
pub author_display_name: String,
|
||||
pub world_name: String,
|
||||
pub subtitle: String,
|
||||
pub summary_text: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldLibraryResponse {
|
||||
pub entries: Vec<CustomWorldLibraryEntryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldLibraryMutationResponse {
|
||||
pub entry: CustomWorldLibraryEntryResponse,
|
||||
pub entries: Vec<CustomWorldLibraryEntryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldGalleryResponse {
|
||||
pub entries: Vec<CustomWorldGalleryCardResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldGalleryDetailResponse {
|
||||
pub entry: CustomWorldLibraryEntryResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldWorkSummaryResponse {
|
||||
pub work_id: String,
|
||||
pub source_type: String,
|
||||
pub status: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub summary: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cover_render_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cover_character_image_srcs: Vec<String>,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub stage: Option<String>,
|
||||
pub stage_label: Option<String>,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub role_visual_ready_count: Option<u32>,
|
||||
pub role_animation_ready_count: Option<u32>,
|
||||
pub role_asset_summary_label: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub profile_id: Option<String>,
|
||||
pub can_resume: bool,
|
||||
pub can_enter_world: bool,
|
||||
pub blocker_count: u32,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldWorksResponse {
|
||||
pub items: Vec<CustomWorldWorkSummaryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateCustomWorldAgentSessionRequest {
|
||||
#[serde(default)]
|
||||
pub seed_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendCustomWorldAgentMessageRequest {
|
||||
pub client_message_id: String,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub quick_fill_requested: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub focus_card_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub selected_card_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldAgentMessageResponse {
|
||||
pub id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
pub related_operation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldAgentOperationResponse {
|
||||
pub operation_id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub operation_type: String,
|
||||
pub status: String,
|
||||
pub phase_label: String,
|
||||
pub phase_detail: String,
|
||||
pub progress: u32,
|
||||
pub error: Option<String>,
|
||||
pub started_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldDraftCardSummaryResponse {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub summary: String,
|
||||
pub status: String,
|
||||
pub linked_ids: Vec<String>,
|
||||
pub warning_count: u32,
|
||||
pub asset_status: Option<String>,
|
||||
pub asset_status_label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldDraftCardDetailSectionResponse {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldDraftCardDetailResponse {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub title: String,
|
||||
pub sections: Vec<CustomWorldDraftCardDetailSectionResponse>,
|
||||
pub linked_ids: Vec<String>,
|
||||
pub locked: bool,
|
||||
pub editable: bool,
|
||||
pub editable_section_ids: Vec<String>,
|
||||
pub warning_messages: Vec<String>,
|
||||
pub asset_status: Option<String>,
|
||||
pub asset_status_label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldAgentCardDetailResponse {
|
||||
pub card: CustomWorldDraftCardDetailResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldAgentCheckpointResponse {
|
||||
pub checkpoint_id: String,
|
||||
pub created_at: String,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldSupportedActionResponse {
|
||||
pub action: String,
|
||||
pub enabled: bool,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldResultPreviewBlockerResponse {
|
||||
pub id: String,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldPublishGateResponse {
|
||||
pub profile_id: String,
|
||||
pub blockers: Vec<CustomWorldResultPreviewBlockerResponse>,
|
||||
pub blocker_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub can_enter_world: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldAgentSessionSnapshotResponse {
|
||||
pub session_id: String,
|
||||
pub current_turn: u32,
|
||||
pub anchor_content: serde_json::Value,
|
||||
pub progress_percent: u32,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub stage: String,
|
||||
pub focus_card_id: Option<String>,
|
||||
pub creator_intent: serde_json::Value,
|
||||
pub creator_intent_readiness: serde_json::Value,
|
||||
pub anchor_pack: serde_json::Value,
|
||||
pub lock_state: serde_json::Value,
|
||||
pub draft_profile: serde_json::Value,
|
||||
pub messages: Vec<CustomWorldAgentMessageResponse>,
|
||||
pub draft_cards: Vec<CustomWorldDraftCardSummaryResponse>,
|
||||
pub pending_clarifications: Vec<serde_json::Value>,
|
||||
pub suggested_actions: Vec<serde_json::Value>,
|
||||
pub recommended_replies: Vec<String>,
|
||||
pub quality_findings: Vec<serde_json::Value>,
|
||||
pub asset_coverage: serde_json::Value,
|
||||
pub checkpoints: Vec<CustomWorldAgentCheckpointResponse>,
|
||||
pub supported_actions: Vec<CustomWorldSupportedActionResponse>,
|
||||
pub publish_gate: Option<CustomWorldPublishGateResponse>,
|
||||
pub result_preview: Option<serde_json::Value>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomWorldAgentSessionResponse {
|
||||
pub session: CustomWorldAgentSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecuteCustomWorldAgentActionRequest {
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub draft_profile: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub legacy_result_profile: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub setting_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub card_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub sections: Option<Vec<ExecuteCustomWorldAgentDraftCardSectionPatch>>,
|
||||
#[serde(default)]
|
||||
pub profile: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub role_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub prompt_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub anchor_card_ids: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub role_ids: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub role_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub portrait_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub generated_visual_asset_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub generated_animation_set_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub animation_map: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub scene_ids: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub scene_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub scene_kind: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub generated_scene_asset_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub generated_scene_prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub generated_scene_model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub checkpoint_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecuteCustomWorldAgentDraftCardSectionPatch {
|
||||
pub section_id: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn runtime_settings_request_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(PutRuntimeSettingsRequest {
|
||||
music_volume: 0.42,
|
||||
platform_theme: RUNTIME_PLATFORM_THEME_LIGHT.to_string(),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["platformTheme"], json!("light"));
|
||||
let music_volume = payload["musicVolume"]
|
||||
.as_f64()
|
||||
.expect("musicVolume should serialize as number");
|
||||
assert!((music_volume - 0.42).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_history_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(PlatformBrowseHistoryResponse {
|
||||
entries: vec![PlatformBrowseHistoryEntryResponse {
|
||||
owner_user_id: "owner-1".to_string(),
|
||||
profile_id: "profile-1".to_string(),
|
||||
world_name: "世界".to_string(),
|
||||
subtitle: "".to_string(),
|
||||
summary_text: "".to_string(),
|
||||
cover_image_src: None,
|
||||
theme_mode: BROWSE_HISTORY_THEME_MODE_MYTHIC.to_string(),
|
||||
author_display_name: "玩家".to_string(),
|
||||
visited_at: "2026-04-21T00:00:00Z".to_string(),
|
||||
}],
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["entries"][0]["ownerUserId"], json!("owner-1"));
|
||||
assert_eq!(payload["entries"][0]["themeMode"], json!("mythic"));
|
||||
assert_eq!(
|
||||
payload["entries"][0]["visitedAt"],
|
||||
json!("2026-04-21T00:00:00Z")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_history_upsert_request_accepts_single_or_batch_shape() {
|
||||
let single: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({
|
||||
"ownerUserId": "owner-1",
|
||||
"profileId": "profile-1",
|
||||
"worldName": "世界"
|
||||
}))
|
||||
.expect("single shape should deserialize");
|
||||
let batch: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({
|
||||
"entries": [{
|
||||
"ownerUserId": "owner-1",
|
||||
"profileId": "profile-1",
|
||||
"worldName": "世界"
|
||||
}]
|
||||
}))
|
||||
.expect("batch shape should deserialize");
|
||||
|
||||
assert_eq!(single.into_entries().len(), 1);
|
||||
assert_eq!(batch.into_entries().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_dashboard_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfileDashboardSummaryResponse {
|
||||
wallet_balance: 8,
|
||||
total_play_time_ms: 16,
|
||||
played_world_count: 3,
|
||||
updated_at: Some("2026-04-22T10:00:00Z".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["walletBalance"], json!(8));
|
||||
assert_eq!(payload["totalPlayTimeMs"], json!(16));
|
||||
assert_eq!(payload["playedWorldCount"], json!(3));
|
||||
assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_wallet_ledger_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfileWalletLedgerResponse {
|
||||
entries: vec![ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-1".to_string(),
|
||||
amount_delta: 12,
|
||||
balance_after: 80,
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC.to_string(),
|
||||
created_at: "2026-04-22T10:00:00Z".to_string(),
|
||||
}],
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["entries"][0]["amountDelta"], json!(12));
|
||||
assert_eq!(payload["entries"][0]["balanceAfter"], json!(80));
|
||||
assert_eq!(payload["entries"][0]["sourceType"], json!("snapshot_sync"));
|
||||
assert_eq!(
|
||||
payload["entries"][0]["createdAt"],
|
||||
json!("2026-04-22T10:00:00Z")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_recharge_center_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfileRechargeCenterResponse {
|
||||
wallet_balance: 29,
|
||||
membership: ProfileMembershipResponse {
|
||||
status: "active".to_string(),
|
||||
tier: "month".to_string(),
|
||||
started_at: Some("2026-04-25T10:00:00Z".to_string()),
|
||||
expires_at: Some("2026-05-25T10:00:00Z".to_string()),
|
||||
updated_at: Some("2026-04-25T10:00:00Z".to_string()),
|
||||
},
|
||||
point_products: vec![ProfileRechargeProductResponse {
|
||||
product_id: "points_60".to_string(),
|
||||
title: "60叙世币".to_string(),
|
||||
price_cents: 600,
|
||||
kind: "points".to_string(),
|
||||
points_amount: 60,
|
||||
bonus_points: 60,
|
||||
duration_days: 0,
|
||||
badge_label: "首充双倍".to_string(),
|
||||
description: "首充送60叙世币".to_string(),
|
||||
tier: "normal".to_string(),
|
||||
}],
|
||||
membership_products: vec![],
|
||||
benefits: vec![],
|
||||
latest_order: None,
|
||||
has_points_recharged: false,
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["walletBalance"], json!(29));
|
||||
assert_eq!(
|
||||
payload["membership"]["expiresAt"],
|
||||
json!("2026-05-25T10:00:00Z")
|
||||
);
|
||||
assert_eq!(payload["pointProducts"][0]["productId"], json!("points_60"));
|
||||
assert_eq!(payload["pointProducts"][0]["title"], json!("60叙世币"));
|
||||
assert_eq!(payload["pointProducts"][0]["priceCents"], json!(600));
|
||||
assert_eq!(
|
||||
payload["pointProducts"][0]["description"],
|
||||
json!("首充送60叙世币")
|
||||
);
|
||||
assert_eq!(payload["hasPointsRecharged"], json!(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_profile_recharge_order_request_accepts_optional_channel() {
|
||||
let payload: CreateProfileRechargeOrderRequest = serde_json::from_value(json!({
|
||||
"productId": "member_month"
|
||||
}))
|
||||
.expect("request should deserialize");
|
||||
|
||||
assert_eq!(payload.product_id, "member_month");
|
||||
assert_eq!(payload.payment_channel, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_play_stats_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfilePlayStatsResponse {
|
||||
total_play_time_ms: 18,
|
||||
played_works: vec![ProfilePlayedWorkSummaryResponse {
|
||||
world_key: "builtin:WUXIA".to_string(),
|
||||
owner_user_id: None,
|
||||
profile_id: None,
|
||||
world_type: Some("WUXIA".to_string()),
|
||||
world_title: "武侠世界".to_string(),
|
||||
world_subtitle: "".to_string(),
|
||||
first_played_at: "2026-04-20T10:00:00Z".to_string(),
|
||||
last_played_at: "2026-04-22T10:00:00Z".to_string(),
|
||||
last_observed_play_time_ms: 1200,
|
||||
}],
|
||||
updated_at: Some("2026-04-22T10:00:00Z".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["totalPlayTimeMs"], json!(18));
|
||||
assert_eq!(
|
||||
payload["playedWorks"][0]["worldKey"],
|
||||
json!("builtin:WUXIA")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["playedWorks"][0]["lastObservedPlayTimeMs"],
|
||||
json!(1200)
|
||||
);
|
||||
assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_inventory_state_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(RuntimeInventoryStateResponse {
|
||||
runtime_session_id: "runtime_001".to_string(),
|
||||
actor_user_id: "user_001".to_string(),
|
||||
backpack_items: vec![RuntimeInventorySlotResponse {
|
||||
slot_id: "invslot_001".to_string(),
|
||||
container_kind: "backpack".to_string(),
|
||||
slot_key: "invslot_001".to_string(),
|
||||
item_id: "consumable_heal_potion".to_string(),
|
||||
category: "消耗品".to_string(),
|
||||
name: "疗伤药".to_string(),
|
||||
description: Some("用于恢复少量气血。".to_string()),
|
||||
quantity: 2,
|
||||
rarity: "common".to_string(),
|
||||
tags: vec!["healing".to_string()],
|
||||
stackable: true,
|
||||
stack_key: "heal_potion".to_string(),
|
||||
equipment_slot_id: None,
|
||||
source_kind: "treasure_reward".to_string(),
|
||||
source_reference_id: Some("treasure_001".to_string()),
|
||||
created_at: "2026-04-22T10:00:00Z".to_string(),
|
||||
updated_at: "2026-04-22T10:01:00Z".to_string(),
|
||||
}],
|
||||
equipment_items: vec![],
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["runtimeSessionId"], json!("runtime_001"));
|
||||
assert_eq!(payload["actorUserId"], json!("user_001"));
|
||||
assert_eq!(payload["backpackItems"][0]["slotId"], json!("invslot_001"));
|
||||
assert_eq!(
|
||||
payload["backpackItems"][0]["sourceKind"],
|
||||
json!("treasure_reward")
|
||||
);
|
||||
}
|
||||
}
|
||||
429
server-rs/crates/shared-contracts/src/runtime_story.rs
Normal file
429
server-rs/crates/shared-contracts/src/runtime_story.rs
Normal file
@@ -0,0 +1,429 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStorySnapshotPayload {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub saved_at: Option<String>,
|
||||
pub bottom_tab: String,
|
||||
pub game_state: Value,
|
||||
#[serde(default)]
|
||||
pub current_story: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryStateResolveRequest {
|
||||
pub session_id: String,
|
||||
#[serde(default)]
|
||||
pub client_version: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub snapshot: Option<RuntimeStorySnapshotPayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryChoiceAction {
|
||||
#[serde(rename = "type")]
|
||||
pub action_type: String,
|
||||
pub function_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub payload: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryActionRequest {
|
||||
pub session_id: String,
|
||||
#[serde(default)]
|
||||
pub client_version: Option<u32>,
|
||||
pub action: RuntimeStoryChoiceAction,
|
||||
#[serde(default)]
|
||||
pub snapshot: Option<RuntimeStorySnapshotPayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryAiRequestOptions {
|
||||
#[serde(default)]
|
||||
pub available_options: Vec<Value>,
|
||||
#[serde(default)]
|
||||
pub option_catalog: Vec<Value>,
|
||||
}
|
||||
|
||||
impl Default for RuntimeStoryAiRequestOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
available_options: Vec::new(),
|
||||
option_catalog: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryAiRequest {
|
||||
pub world_type: String,
|
||||
pub character: Value,
|
||||
#[serde(default)]
|
||||
pub monsters: Vec<Value>,
|
||||
#[serde(default)]
|
||||
pub history: Vec<Value>,
|
||||
#[serde(default)]
|
||||
pub choice: String,
|
||||
pub context: Value,
|
||||
#[serde(default)]
|
||||
pub request_options: RuntimeStoryAiRequestOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryAiResponse {
|
||||
pub story_text: String,
|
||||
pub options: Vec<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub encounter: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryOptionView {
|
||||
pub function_id: String,
|
||||
pub action_text: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub detail_text: Option<String>,
|
||||
pub scope: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub interaction: Option<RuntimeStoryOptionInteraction>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub payload: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum RuntimeStoryOptionInteraction {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Npc {
|
||||
npc_id: String,
|
||||
action: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
quest_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryPlayerViewModel {
|
||||
pub hp: i32,
|
||||
pub max_hp: i32,
|
||||
pub mana: i32,
|
||||
pub max_mana: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryCompanionViewModel {
|
||||
pub npc_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub character_id: Option<String>,
|
||||
pub joined_at_affinity: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryEncounterViewModel {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub npc_name: String,
|
||||
pub hostile: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub affinity: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub recruited: Option<bool>,
|
||||
pub interaction_active: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub battle_mode: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryStatusViewModel {
|
||||
pub in_battle: bool,
|
||||
pub npc_interaction_active: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_npc_battle_mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_npc_battle_outcome: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeBattlePresentation {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub damage_dealt: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub damage_taken: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub outcome: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryViewModel {
|
||||
pub player: RuntimeStoryPlayerViewModel,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub encounter: Option<RuntimeStoryEncounterViewModel>,
|
||||
pub companions: Vec<RuntimeStoryCompanionViewModel>,
|
||||
pub available_options: Vec<RuntimeStoryOptionView>,
|
||||
pub status: RuntimeStoryStatusViewModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryPresentation {
|
||||
pub action_text: String,
|
||||
pub result_text: String,
|
||||
pub story_text: String,
|
||||
pub options: Vec<RuntimeStoryOptionView>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub toast: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub battle: Option<RuntimeBattlePresentation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum RuntimeStoryPatch {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
StoryHistoryAppend {
|
||||
action_text: String,
|
||||
result_text: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
NpcAffinityChanged {
|
||||
npc_id: String,
|
||||
previous_affinity: i32,
|
||||
next_affinity: i32,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
BattleResolved {
|
||||
function_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
target_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
damage_dealt: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
damage_taken: Option<i32>,
|
||||
outcome: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
StatusChanged {
|
||||
in_battle: bool,
|
||||
npc_interaction_active: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
current_npc_battle_mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
current_npc_battle_outcome: Option<String>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
EncounterChanged {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
encounter_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoryActionResponse {
|
||||
pub session_id: String,
|
||||
pub server_version: u32,
|
||||
pub view_model: RuntimeStoryViewModel,
|
||||
pub presentation: RuntimeStoryPresentation,
|
||||
pub patches: Vec<RuntimeStoryPatch>,
|
||||
pub snapshot: RuntimeStorySnapshotPayload,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn runtime_story_state_resolve_request_accepts_missing_saved_at() {
|
||||
let payload: RuntimeStoryStateResolveRequest = serde_json::from_value(json!({
|
||||
"sessionId": "runtime-main",
|
||||
"clientVersion": 7,
|
||||
"snapshot": {
|
||||
"bottomTab": "adventure",
|
||||
"gameState": { "runtimeSessionId": "runtime-main" },
|
||||
"currentStory": { "text": "营地里的火光还没有熄灭。" }
|
||||
}
|
||||
}))
|
||||
.expect("payload should deserialize");
|
||||
|
||||
assert_eq!(payload.session_id, "runtime-main");
|
||||
assert_eq!(payload.client_version, Some(7));
|
||||
assert_eq!(
|
||||
payload.snapshot.expect("snapshot should exist").saved_at,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_story_action_request_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(RuntimeStoryActionRequest {
|
||||
session_id: "runtime-main".to_string(),
|
||||
client_version: Some(8),
|
||||
action: RuntimeStoryChoiceAction {
|
||||
action_type: "story_choice".to_string(),
|
||||
function_id: "npc_chat".to_string(),
|
||||
target_id: Some("npc_camp_firekeeper".to_string()),
|
||||
payload: Some(json!({ "optionText": "继续交谈" })),
|
||||
},
|
||||
snapshot: Some(RuntimeStorySnapshotPayload {
|
||||
saved_at: Some("2026-04-22T12:00:00.000Z".to_string()),
|
||||
bottom_tab: "adventure".to_string(),
|
||||
game_state: json!({ "runtimeSessionId": "runtime-main" }),
|
||||
current_story: None,
|
||||
}),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["sessionId"], json!("runtime-main"));
|
||||
assert_eq!(payload["clientVersion"], json!(8));
|
||||
assert_eq!(payload["action"]["type"], json!("story_choice"));
|
||||
assert_eq!(payload["action"]["functionId"], json!("npc_chat"));
|
||||
assert_eq!(payload["action"]["targetId"], json!("npc_camp_firekeeper"));
|
||||
assert_eq!(
|
||||
payload["snapshot"]["savedAt"],
|
||||
json!("2026-04-22T12:00:00.000Z")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_story_ai_request_defaults_optional_arrays() {
|
||||
let payload: RuntimeStoryAiRequest = serde_json::from_value(json!({
|
||||
"worldType": "martial",
|
||||
"character": { "name": "林迟" },
|
||||
"context": { "scene": "camp" }
|
||||
}))
|
||||
.expect("payload should deserialize");
|
||||
|
||||
assert_eq!(payload.world_type, "martial");
|
||||
assert!(payload.monsters.is_empty());
|
||||
assert!(payload.history.is_empty());
|
||||
assert!(payload.request_options.available_options.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_story_action_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(RuntimeStoryActionResponse {
|
||||
session_id: "runtime-main".to_string(),
|
||||
server_version: 8,
|
||||
view_model: RuntimeStoryViewModel {
|
||||
player: RuntimeStoryPlayerViewModel {
|
||||
hp: 32,
|
||||
max_hp: 40,
|
||||
mana: 18,
|
||||
max_mana: 20,
|
||||
},
|
||||
encounter: Some(RuntimeStoryEncounterViewModel {
|
||||
id: "npc_camp_firekeeper".to_string(),
|
||||
kind: "npc".to_string(),
|
||||
npc_name: "守火人".to_string(),
|
||||
hostile: false,
|
||||
affinity: Some(12),
|
||||
recruited: Some(false),
|
||||
interaction_active: true,
|
||||
battle_mode: None,
|
||||
}),
|
||||
companions: vec![RuntimeStoryCompanionViewModel {
|
||||
npc_id: "npc_companion_001".to_string(),
|
||||
character_id: Some("char_companion_001".to_string()),
|
||||
joined_at_affinity: 64,
|
||||
}],
|
||||
available_options: vec![RuntimeStoryOptionView {
|
||||
function_id: "npc_chat".to_string(),
|
||||
action_text: "继续交谈".to_string(),
|
||||
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
|
||||
scope: "npc".to_string(),
|
||||
interaction: Some(RuntimeStoryOptionInteraction::Npc {
|
||||
npc_id: "npc_camp_firekeeper".to_string(),
|
||||
action: "chat".to_string(),
|
||||
quest_id: None,
|
||||
}),
|
||||
payload: Some(json!({ "note": "server-runtime-test" })),
|
||||
disabled: None,
|
||||
reason: None,
|
||||
}],
|
||||
status: RuntimeStoryStatusViewModel {
|
||||
in_battle: false,
|
||||
npc_interaction_active: true,
|
||||
current_npc_battle_mode: None,
|
||||
current_npc_battle_outcome: None,
|
||||
},
|
||||
},
|
||||
presentation: RuntimeStoryPresentation {
|
||||
action_text: "".to_string(),
|
||||
result_text: "".to_string(),
|
||||
story_text: "守火人抬眼看了你一瞬,示意你把想问的话继续说完。".to_string(),
|
||||
options: vec![RuntimeStoryOptionView {
|
||||
function_id: "npc_chat".to_string(),
|
||||
action_text: "继续交谈".to_string(),
|
||||
detail_text: Some("围绕当前话题继续推进关系判断。".to_string()),
|
||||
scope: "npc".to_string(),
|
||||
interaction: Some(RuntimeStoryOptionInteraction::Npc {
|
||||
npc_id: "npc_camp_firekeeper".to_string(),
|
||||
action: "chat".to_string(),
|
||||
quest_id: None,
|
||||
}),
|
||||
payload: Some(json!({ "note": "server-runtime-test" })),
|
||||
disabled: None,
|
||||
reason: None,
|
||||
}],
|
||||
toast: None,
|
||||
battle: None,
|
||||
},
|
||||
patches: vec![RuntimeStoryPatch::StatusChanged {
|
||||
in_battle: false,
|
||||
npc_interaction_active: true,
|
||||
current_npc_battle_mode: None,
|
||||
current_npc_battle_outcome: None,
|
||||
}],
|
||||
snapshot: RuntimeStorySnapshotPayload {
|
||||
saved_at: Some("2026-04-22T12:00:00.000Z".to_string()),
|
||||
bottom_tab: "adventure".to_string(),
|
||||
game_state: json!({ "runtimeSessionId": "runtime-main" }),
|
||||
current_story: Some(json!({
|
||||
"text": "守火人抬眼看了你一瞬,示意你把想问的话继续说完。"
|
||||
})),
|
||||
},
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["sessionId"], json!("runtime-main"));
|
||||
assert_eq!(payload["serverVersion"], json!(8));
|
||||
assert_eq!(payload["viewModel"]["player"]["maxHp"], json!(40));
|
||||
assert_eq!(
|
||||
payload["viewModel"]["availableOptions"][0]["interaction"]["npcId"],
|
||||
json!("npc_camp_firekeeper")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["presentation"]["storyText"],
|
||||
json!("守火人抬眼看了你一瞬,示意你把想问的话继续说完。")
|
||||
);
|
||||
assert_eq!(payload["patches"][0]["type"], json!("status_changed"));
|
||||
assert_eq!(payload["snapshot"]["bottomTab"], json!("adventure"));
|
||||
}
|
||||
}
|
||||
164
server-rs/crates/shared-contracts/src/story.rs
Normal file
164
server-rs/crates/shared-contracts/src/story.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BeginStorySessionRequest {
|
||||
pub runtime_session_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
#[serde(default)]
|
||||
pub opening_summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContinueStoryRequest {
|
||||
pub story_session_id: String,
|
||||
pub narrative_text: String,
|
||||
#[serde(default)]
|
||||
pub choice_function_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StorySessionPayload {
|
||||
pub story_session_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub world_profile_id: String,
|
||||
pub initial_prompt: String,
|
||||
#[serde(default)]
|
||||
pub opening_summary: Option<String>,
|
||||
pub latest_narrative_text: String,
|
||||
#[serde(default)]
|
||||
pub latest_choice_function_id: Option<String>,
|
||||
pub status: String,
|
||||
pub version: u32,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StoryEventPayload {
|
||||
pub event_id: String,
|
||||
pub story_session_id: String,
|
||||
pub event_kind: String,
|
||||
pub narrative_text: String,
|
||||
#[serde(default)]
|
||||
pub choice_function_id: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StorySessionMutationResponse {
|
||||
pub story_session: StorySessionPayload,
|
||||
pub story_event: StoryEventPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StorySessionStateResponse {
|
||||
pub story_session: StorySessionPayload,
|
||||
pub story_events: Vec<StoryEventPayload>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn continue_story_request_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ContinueStoryRequest {
|
||||
story_session_id: "storysess_1".to_string(),
|
||||
narrative_text: "继续前进".to_string(),
|
||||
choice_function_id: Some("npc_chat".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"storySessionId": "storysess_1",
|
||||
"narrativeText": "继续前进",
|
||||
"choiceFunctionId": "npc_chat"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_session_mutation_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(StorySessionMutationResponse {
|
||||
story_session: StorySessionPayload {
|
||||
story_session_id: "storysess_1".to_string(),
|
||||
runtime_session_id: "runtime_1".to_string(),
|
||||
actor_user_id: "user_1".to_string(),
|
||||
world_profile_id: "profile_1".to_string(),
|
||||
initial_prompt: "进入营地".to_string(),
|
||||
opening_summary: Some("营地开场".to_string()),
|
||||
latest_narrative_text: "篝火正在燃烧。".to_string(),
|
||||
latest_choice_function_id: Some("talk".to_string()),
|
||||
status: "active".to_string(),
|
||||
version: 1,
|
||||
created_at: "1.000000Z".to_string(),
|
||||
updated_at: "1.000000Z".to_string(),
|
||||
},
|
||||
story_event: StoryEventPayload {
|
||||
event_id: "storyevt_1".to_string(),
|
||||
story_session_id: "storysess_1".to_string(),
|
||||
event_kind: "session_started".to_string(),
|
||||
narrative_text: "篝火正在燃烧。".to_string(),
|
||||
choice_function_id: Some("talk".to_string()),
|
||||
created_at: "1.000000Z".to_string(),
|
||||
},
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload["storySession"]["storySessionId"],
|
||||
json!("storysess_1")
|
||||
);
|
||||
assert_eq!(payload["storyEvent"]["eventKind"], json!("session_started"));
|
||||
assert_eq!(payload["storyEvent"]["choiceFunctionId"], json!("talk"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_session_state_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(StorySessionStateResponse {
|
||||
story_session: StorySessionPayload {
|
||||
story_session_id: "storysess_1".to_string(),
|
||||
runtime_session_id: "runtime_1".to_string(),
|
||||
actor_user_id: "user_1".to_string(),
|
||||
world_profile_id: "profile_1".to_string(),
|
||||
initial_prompt: "进入营地".to_string(),
|
||||
opening_summary: Some("营地开场".to_string()),
|
||||
latest_narrative_text: "你看见篝火边有人招手。".to_string(),
|
||||
latest_choice_function_id: Some("talk_to_npc".to_string()),
|
||||
status: "active".to_string(),
|
||||
version: 2,
|
||||
created_at: "1.000000Z".to_string(),
|
||||
updated_at: "2.000000Z".to_string(),
|
||||
},
|
||||
story_events: vec![StoryEventPayload {
|
||||
event_id: "storyevt_2".to_string(),
|
||||
story_session_id: "storysess_1".to_string(),
|
||||
event_kind: "story_continued".to_string(),
|
||||
narrative_text: "你看见篝火边有人招手。".to_string(),
|
||||
choice_function_id: Some("talk_to_npc".to_string()),
|
||||
created_at: "2.000000Z".to_string(),
|
||||
}],
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload["storySession"]["latestChoiceFunctionId"],
|
||||
json!("talk_to_npc")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["storyEvents"][0]["eventKind"],
|
||||
json!("story_continued")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user