Merge origin/master into codex/wechat
This commit is contained in:
@@ -4,7 +4,11 @@ edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
# 默认给 api-server 等原生后端暴露资产上传 DTO;SpacetimeDB WASM 路径通过 workspace 依赖关闭默认 feature。
|
||||
default = ["oss-contracts"]
|
||||
oss-contracts = []
|
||||
|
||||
[dependencies]
|
||||
platform-oss = { path = "../platform-oss" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -95,3 +95,4 @@
|
||||
1. `shared-contracts` 只放协议类型与兼容结构,不承接业务规则、供应商适配或状态写入逻辑。
|
||||
2. 各模块 crate 对外暴露的协议优先复用这里的共享定义,避免重复散落。
|
||||
3. 前端兼容契约一旦进入本 crate,就必须与任务清单和基线文档同步维护。
|
||||
4. `assets` 模块依赖 `platform-oss` 的稳定返回类型,默认通过 `oss-contracts` feature 给 `api-server` 使用;SpacetimeDB WASM 构建链路必须通过 workspace 依赖关闭默认 feature,避免把 `platform-oss` / `reqwest` / `wasm-bindgen` 带进 `spacetime-module`。
|
||||
|
||||
@@ -10,6 +10,43 @@ pub struct AdminLoginRequest {
|
||||
}
|
||||
|
||||
// 登录成功后返回管理员访问令牌与基础会话信息。
|
||||
|
||||
/// 后台创作入口开关列表响应。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminCreationEntryConfigResponse {
|
||||
pub entries: Vec<AdminCreationEntryTypeConfigPayload>,
|
||||
}
|
||||
|
||||
/// 后台单个创作入口开关配置。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminCreationEntryTypeConfigPayload {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub badge: String,
|
||||
pub image_src: String,
|
||||
pub visible: bool,
|
||||
pub open: bool,
|
||||
pub sort_order: i32,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
/// 后台保存创作入口开关配置请求。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminUpsertCreationEntryTypeConfigRequest {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub badge: String,
|
||||
pub image_src: String,
|
||||
pub visible: bool,
|
||||
pub open: bool,
|
||||
pub sort_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminLoginResponse {
|
||||
@@ -77,6 +114,42 @@ pub struct AdminDatabaseTableStatPayload {
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
// 后台表清单独立用于“表查询”页,避免页面必须先拉完整总览。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDatabaseTableListResponse {
|
||||
pub tables: Vec<String>,
|
||||
pub fetch_errors: Vec<String>,
|
||||
}
|
||||
|
||||
// 后台通用表查询参数,用户输入不进入 SQL,只在 API Server 内存中过滤。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDatabaseTableRowsQuery {
|
||||
pub limit: Option<u32>,
|
||||
pub search: Option<String>,
|
||||
pub filters: Option<String>,
|
||||
}
|
||||
|
||||
// 后台通用表查询响应,cells 使用列名映射,raw 保留原始行便于详情排障。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDatabaseTableRowsResponse {
|
||||
pub table_name: String,
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<AdminDatabaseTableRowPayload>,
|
||||
pub total_returned: usize,
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
// 单行查询结果,值统一用 JSON 承载以兼容不同表字段类型。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDatabaseTableRowPayload {
|
||||
pub cells: Value,
|
||||
pub raw: Value,
|
||||
}
|
||||
|
||||
// 调试请求只允许同源路径、受控请求头和有限请求体。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -105,3 +178,39 @@ pub struct AdminDebugHttpResponse {
|
||||
pub body_text: String,
|
||||
pub body_json: Option<Value>,
|
||||
}
|
||||
|
||||
// 后台埋点明细查询参数只保留运营筛选需要的只读字段。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminTrackingEventListQuery {
|
||||
pub event_key: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
pub scope_kind: Option<String>,
|
||||
pub scope_id: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// 单条埋点原始事件明细,字段与 tracking_event 表一一对应并补充事件名称。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminTrackingEventEntryPayload {
|
||||
pub event_id: String,
|
||||
pub event_key: String,
|
||||
pub event_title: String,
|
||||
pub scope_kind: String,
|
||||
pub scope_id: String,
|
||||
pub day_key: i64,
|
||||
pub user_id: Option<String>,
|
||||
pub owner_user_id: Option<String>,
|
||||
pub profile_id: Option<String>,
|
||||
pub module_key: Option<String>,
|
||||
pub metadata_json: String,
|
||||
pub occurred_at: String,
|
||||
}
|
||||
|
||||
// 后台埋点明细列表响应,前端导出 Excel 时直接使用 entries。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminTrackingEventListResponse {
|
||||
pub entries: Vec<AdminTrackingEventEntryPayload>,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use platform_oss::{
|
||||
OssObjectAccess, OssPostObjectFormFields, OssPostObjectResponse, OssSignedGetObjectUrlResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -16,7 +13,7 @@ pub struct CreateDirectUploadTicketRequest {
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub access: Option<OssObjectAccess>,
|
||||
pub access: Option<DirectUploadObjectAccess>,
|
||||
#[serde(default)]
|
||||
pub metadata: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
@@ -45,6 +42,13 @@ pub enum ConfirmAssetObjectAccessPolicy {
|
||||
PublicRead,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DirectUploadObjectAccess {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConfirmAssetObjectRequest {
|
||||
@@ -513,7 +517,7 @@ pub struct DirectUploadTicketPayload {
|
||||
pub legacy_public_path: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
pub access: OssObjectAccess,
|
||||
pub access: DirectUploadObjectAccess,
|
||||
pub key_prefix: String,
|
||||
pub expires_at: String,
|
||||
pub max_size_bytes: u64,
|
||||
@@ -525,9 +529,13 @@ pub struct DirectUploadTicketPayload {
|
||||
pub struct DirectUploadTicketFormFields {
|
||||
pub key: String,
|
||||
pub policy: String,
|
||||
#[serde(rename = "OSSAccessKeyId")]
|
||||
pub oss_access_key_id: String,
|
||||
#[serde(rename = "Signature")]
|
||||
#[serde(rename = "x-oss-signature-version")]
|
||||
pub signature_version: String,
|
||||
#[serde(rename = "x-oss-credential")]
|
||||
pub credential: String,
|
||||
#[serde(rename = "x-oss-date")]
|
||||
pub date: String,
|
||||
#[serde(rename = "x-oss-signature")]
|
||||
pub signature: String,
|
||||
#[serde(rename = "success_action_status")]
|
||||
pub success_action_status: String,
|
||||
@@ -610,55 +618,6 @@ pub struct AssetBindingPayload {
|
||||
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::*;
|
||||
@@ -702,24 +661,26 @@ mod tests {
|
||||
#[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",
|
||||
upload: DirectUploadTicketPayload {
|
||||
signature_version: "v4".to_string(),
|
||||
provider: "aliyun-oss".to_string(),
|
||||
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,
|
||||
access: DirectUploadObjectAccess::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 {
|
||||
form_fields: DirectUploadTicketFormFields {
|
||||
key: "generated-characters/hero/master.png".to_string(),
|
||||
policy: "policy".to_string(),
|
||||
oss_access_key_id: "ak".to_string(),
|
||||
signature_version: "OSS4-HMAC-SHA256".to_string(),
|
||||
credential: "ak/20260507/cn-shanghai/oss/aliyun_v4_request".to_string(),
|
||||
date: "20260507T120000Z".to_string(),
|
||||
signature: "sig".to_string(),
|
||||
success_action_status: "200".to_string(),
|
||||
content_type: Some("image/png".to_string()),
|
||||
@@ -728,14 +689,18 @@ mod tests {
|
||||
"character_visual".to_string(),
|
||||
)]),
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["upload"]["signatureVersion"], json!("v1"));
|
||||
assert_eq!(payload["upload"]["signatureVersion"], json!("v4"));
|
||||
assert_eq!(
|
||||
payload["upload"]["formFields"]["OSSAccessKeyId"],
|
||||
json!("ak")
|
||||
payload["upload"]["formFields"]["x-oss-signature-version"],
|
||||
json!("OSS4-HMAC-SHA256")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["upload"]["formFields"]["x-oss-credential"],
|
||||
json!("ak/20260507/cn-shanghai/oss/aliyun_v4_request")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["upload"]["formFields"]["x-oss-meta-asset-kind"],
|
||||
|
||||
@@ -17,6 +17,8 @@ pub struct CreationAgentDocumentInputPayload {
|
||||
pub content_type: Option<String>,
|
||||
pub size_bytes: usize,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub source_asset_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
||||
128
server-rs/crates/shared-contracts/src/creation_audio.rs
Normal file
128
server-rs/crates/shared-contracts/src/creation_audio.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreationAudioGenerationKind {
|
||||
BackgroundMusic,
|
||||
SoundEffect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationAudioAsset {
|
||||
pub task_id: String,
|
||||
pub provider: String,
|
||||
#[serde(default)]
|
||||
pub asset_object_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_kind: Option<String>,
|
||||
pub audio_src: String,
|
||||
#[serde(default)]
|
||||
pub prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateBackgroundMusicRequest {
|
||||
pub prompt: String,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub tags: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateSoundEffectRequest {
|
||||
pub prompt: String,
|
||||
#[serde(default)]
|
||||
pub duration: Option<u8>,
|
||||
#[serde(default)]
|
||||
pub seed: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AudioGenerationTaskResponse {
|
||||
pub kind: CreationAudioGenerationKind,
|
||||
pub task_id: String,
|
||||
pub provider: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreationAudioStoragePrefix {
|
||||
PuzzleAssets,
|
||||
#[serde(rename = "match3d_assets")]
|
||||
Match3DAssets,
|
||||
CustomWorldScenes,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PublishGeneratedAudioAssetRequest {
|
||||
pub entity_kind: String,
|
||||
pub entity_id: String,
|
||||
pub slot: String,
|
||||
pub asset_kind: String,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub storage_prefix: Option<CreationAudioStoragePrefix>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GeneratedAudioAssetResponse {
|
||||
pub kind: CreationAudioGenerationKind,
|
||||
pub task_id: String,
|
||||
pub provider: String,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub asset_object_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_kind: Option<String>,
|
||||
#[serde(default)]
|
||||
pub audio_src: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn creation_audio_contracts_use_camel_case_fields() {
|
||||
let request = PublishGeneratedAudioAssetRequest {
|
||||
entity_kind: "match3d_item".to_string(),
|
||||
entity_id: "match3d-item-1".to_string(),
|
||||
slot: "click_sound".to_string(),
|
||||
asset_kind: "match3d_click_sound".to_string(),
|
||||
profile_id: Some("profile-1".to_string()),
|
||||
storage_prefix: Some(CreationAudioStoragePrefix::Match3DAssets),
|
||||
};
|
||||
let payload = serde_json::to_value(request).expect("request should serialize");
|
||||
assert_eq!(payload["entityKind"], json!("match3d_item"));
|
||||
assert_eq!(payload["storagePrefix"], json!("match3d_assets"));
|
||||
|
||||
let asset = CreationAudioAsset {
|
||||
task_id: "task-1".to_string(),
|
||||
provider: "vector-engine-suno".to_string(),
|
||||
asset_object_id: Some("assetobj_1".to_string()),
|
||||
asset_kind: Some("puzzle_background_music".to_string()),
|
||||
audio_src: "/generated-puzzle-assets/a.mp3".to_string(),
|
||||
prompt: Some("轻快音乐".to_string()),
|
||||
title: Some("拼图音乐".to_string()),
|
||||
updated_at: Some("2026-05-11T00:00:00Z".to_string()),
|
||||
};
|
||||
let payload = serde_json::to_value(asset).expect("asset should serialize");
|
||||
assert_eq!(payload["taskId"], json!("task-1"));
|
||||
assert_eq!(payload["audioSrc"], json!("/generated-puzzle-assets/a.mp3"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryConfigResponse {
|
||||
pub start_card: CreationEntryStartCardResponse,
|
||||
pub type_modal: CreationEntryTypeModalResponse,
|
||||
pub creation_types: Vec<CreationEntryTypeResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryStartCardResponse {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub idle_badge: String,
|
||||
pub busy_badge: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryTypeModalResponse {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryTypeResponse {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub badge: String,
|
||||
pub image_src: String,
|
||||
pub visible: bool,
|
||||
pub open: bool,
|
||||
pub sort_order: i32,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
562
server-rs/crates/shared-contracts/src/creative_agent.rs
Normal file
562
server-rs/crates/shared-contracts/src/creative_agent.rs
Normal file
@@ -0,0 +1,562 @@
|
||||
use crate::puzzle_creative_template::{
|
||||
PuzzleCreativeTemplateProtocol, PuzzleCreativeTemplateSelection, PuzzleDraftFieldPatch,
|
||||
PuzzleImageGenerationPlan, PuzzleTemplateCostRange,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreativeAgentStage {
|
||||
Idle,
|
||||
Perceiving,
|
||||
Thinking,
|
||||
Remembering,
|
||||
SelectingPuzzleTemplate,
|
||||
WaitingTemplateConfirmation,
|
||||
PlanningPuzzleLevels,
|
||||
Acting,
|
||||
Reflecting,
|
||||
Collaborating,
|
||||
TargetReady,
|
||||
WaitingUser,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreativeAgentEntryContext {
|
||||
CreationHome,
|
||||
PuzzleWorkspace,
|
||||
GalleryRemix,
|
||||
DraftRestore,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CreativeAgentMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreativeAgentMessageKind {
|
||||
Chat,
|
||||
Stage,
|
||||
ActionResult,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreativeAgentInputPartType {
|
||||
InputText,
|
||||
InputImage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentInputPart {
|
||||
#[serde(rename = "type")]
|
||||
pub part_type: CreativeAgentInputPartType,
|
||||
#[serde(default)]
|
||||
pub text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub thumbnail_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeImageInput {
|
||||
pub asset_id: String,
|
||||
pub read_url: String,
|
||||
#[serde(default)]
|
||||
pub thumbnail_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub width: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub height: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeImageSummary {
|
||||
#[serde(default)]
|
||||
pub asset_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub read_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub thumbnail_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub width: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub height: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreativeUnsupportedPlayType {
|
||||
Rpg,
|
||||
#[serde(rename = "match3d")]
|
||||
Match3d,
|
||||
BigFish,
|
||||
SquareHole,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CreativeCapabilityStatus {
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeUnsupportedCapability {
|
||||
pub play_type: CreativeUnsupportedPlayType,
|
||||
pub title: String,
|
||||
pub status: CreativeCapabilityStatus,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeInputSummary {
|
||||
#[serde(default)]
|
||||
pub text: Option<String>,
|
||||
pub entry_context: CreativeAgentEntryContext,
|
||||
pub images: Vec<CreativeImageSummary>,
|
||||
#[serde(default)]
|
||||
pub material_summary: Option<String>,
|
||||
pub unsupported_capabilities: Vec<CreativeUnsupportedCapability>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentMessage {
|
||||
pub id: String,
|
||||
pub role: CreativeAgentMessageRole,
|
||||
pub kind: CreativeAgentMessageKind,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CreativeTargetPlayType {
|
||||
Puzzle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum CreativeTargetStage {
|
||||
PuzzleAgentWorkspace,
|
||||
PuzzleResult,
|
||||
PuzzleRuntime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeTargetSessionBinding {
|
||||
pub play_type: CreativeTargetPlayType,
|
||||
pub target_session_id: String,
|
||||
pub target_stage: CreativeTargetStage,
|
||||
#[serde(default)]
|
||||
pub result_profile_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentSessionSnapshot {
|
||||
pub session_id: String,
|
||||
pub stage: CreativeAgentStage,
|
||||
pub input_summary: CreativeInputSummary,
|
||||
pub messages: Vec<CreativeAgentMessage>,
|
||||
#[serde(default)]
|
||||
pub puzzle_template_catalog: Vec<PuzzleCreativeTemplateProtocol>,
|
||||
#[serde(default)]
|
||||
pub puzzle_template_selection: Option<PuzzleCreativeTemplateSelection>,
|
||||
#[serde(default)]
|
||||
pub puzzle_image_generation_plan: Option<PuzzleImageGenerationPlan>,
|
||||
#[serde(default)]
|
||||
pub target_binding: Option<CreativeTargetSessionBinding>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateCreativeAgentSessionRequest {
|
||||
#[serde(default)]
|
||||
pub text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub images: Vec<CreativeImageInput>,
|
||||
#[serde(default)]
|
||||
pub entry_context: Option<CreativeAgentEntryContext>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentSessionResponse {
|
||||
pub session: CreativeAgentSessionSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StreamCreativeAgentMessageRequest {
|
||||
pub client_message_id: String,
|
||||
pub content: Vec<CreativeAgentInputPart>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConfirmCreativePuzzleTemplateRequest {
|
||||
pub selection: PuzzleCreativeTemplateSelection,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeDraftEditStreamRequest {
|
||||
pub client_message_id: String,
|
||||
pub instruction: String,
|
||||
pub target_puzzle_session_id: String,
|
||||
pub current_draft: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeDraftEditResult {
|
||||
pub edit_instructions: Vec<PuzzleDraftFieldPatch>,
|
||||
pub session: CreativeAgentSessionSnapshot,
|
||||
pub puzzle_session: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CreativeAgentSseEventType {
|
||||
Stage,
|
||||
AgentMessageDelta,
|
||||
ThoughtSummaryDelta,
|
||||
PuzzleTemplateCatalog,
|
||||
PuzzleTemplateSelection,
|
||||
PuzzleCostRange,
|
||||
PuzzleLevelPlan,
|
||||
ToolStarted,
|
||||
ToolCompleted,
|
||||
Reflection,
|
||||
TargetSession,
|
||||
Session,
|
||||
Error,
|
||||
Done,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentSseEnvelope {
|
||||
pub event: CreativeAgentSseEventType,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentStageEvent {
|
||||
pub session_id: String,
|
||||
pub stage: CreativeAgentStage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentMessageDeltaEvent {
|
||||
pub session_id: String,
|
||||
pub message_id: String,
|
||||
pub role: CreativeAgentMessageRole,
|
||||
pub kind: CreativeAgentMessageKind,
|
||||
pub text_delta: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentThoughtSummaryDeltaEvent {
|
||||
pub session_id: String,
|
||||
pub thought_id: String,
|
||||
pub text_delta: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentTemplateCatalogEvent {
|
||||
pub session_id: String,
|
||||
pub templates: Vec<PuzzleCreativeTemplateProtocol>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentTemplateSelectionEvent {
|
||||
pub session_id: String,
|
||||
pub selection: PuzzleCreativeTemplateSelection,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentCostRangeEvent {
|
||||
pub session_id: String,
|
||||
pub cost_range: PuzzleTemplateCostRange,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentLevelPlanEvent {
|
||||
pub session_id: String,
|
||||
pub plan: PuzzleImageGenerationPlan,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentToolEvent {
|
||||
pub session_id: String,
|
||||
pub tool_call_id: String,
|
||||
pub tool_name: String,
|
||||
#[serde(default)]
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentReflectionEvent {
|
||||
pub session_id: String,
|
||||
pub pass: bool,
|
||||
pub summary: String,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentTargetSessionEvent {
|
||||
pub session_id: String,
|
||||
pub binding: CreativeTargetSessionBinding,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentErrorEvent {
|
||||
#[serde(default)]
|
||||
pub session_id: Option<String>,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub recoverable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativeAgentDoneEvent {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::puzzle_creative_template::{
|
||||
PuzzleCreativeTemplateProtocol, PuzzleDraftEditableFieldPath, PuzzleLevelGenerationMode,
|
||||
PuzzleSupportedLevelMode, PuzzleTemplateImageGenerationPolicy, PuzzleTemplatePricingUnit,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn cost_range() -> PuzzleTemplateCostRange {
|
||||
PuzzleTemplateCostRange {
|
||||
min_points: 2,
|
||||
max_points: 12,
|
||||
pricing_unit: PuzzleTemplatePricingUnit::Point,
|
||||
reason: "按关卡数和每关图片生成次数估算".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn template_selection() -> PuzzleCreativeTemplateSelection {
|
||||
PuzzleCreativeTemplateSelection {
|
||||
template_id: "puzzle.default-creative".to_string(),
|
||||
title: "创意拼图".to_string(),
|
||||
reason: "素材适合拆成可试玩的拼图关卡".to_string(),
|
||||
cost_range: cost_range(),
|
||||
supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti,
|
||||
selected_level_mode: PuzzleLevelGenerationMode::SingleLevel,
|
||||
planned_level_count: 1,
|
||||
requires_user_confirmation: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn template_protocol() -> PuzzleCreativeTemplateProtocol {
|
||||
PuzzleCreativeTemplateProtocol {
|
||||
template_id: "puzzle.default-creative".to_string(),
|
||||
title: "创意拼图".to_string(),
|
||||
summary: "把图文灵感做成拼图".to_string(),
|
||||
preview_image_src: None,
|
||||
supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti,
|
||||
min_level_count: 1,
|
||||
max_level_count: 6,
|
||||
default_level_count: 1,
|
||||
cost_range: cost_range(),
|
||||
required_draft_fields: vec![PuzzleDraftEditableFieldPath::WorkTitle],
|
||||
image_policy: PuzzleTemplateImageGenerationPolicy {
|
||||
allow_uploaded_image_directly: true,
|
||||
allow_generated_images: true,
|
||||
allow_per_level_reference_image: true,
|
||||
default_candidate_count_per_level: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creative_agent_session_snapshot_uses_camel_case() {
|
||||
let snapshot = CreativeAgentSessionSnapshot {
|
||||
session_id: "creative-session-1".to_string(),
|
||||
stage: CreativeAgentStage::WaitingTemplateConfirmation,
|
||||
input_summary: CreativeInputSummary {
|
||||
text: Some("把这张旅行照做成拼图".to_string()),
|
||||
entry_context: CreativeAgentEntryContext::CreationHome,
|
||||
images: vec![CreativeImageSummary {
|
||||
asset_id: Some("asset-1".to_string()),
|
||||
read_url: Some("https://example.test/image.png".to_string()),
|
||||
thumbnail_url: None,
|
||||
width: Some(1024),
|
||||
height: Some(768),
|
||||
summary: Some("一张旅行照片".to_string()),
|
||||
}],
|
||||
material_summary: Some("旅行纪念素材".to_string()),
|
||||
unsupported_capabilities: vec![CreativeUnsupportedCapability {
|
||||
play_type: CreativeUnsupportedPlayType::BigFish,
|
||||
title: "大鱼吃小鱼".to_string(),
|
||||
status: CreativeCapabilityStatus::Unsupported,
|
||||
reason: "Phase 1 只开放拼图模板".to_string(),
|
||||
}],
|
||||
},
|
||||
messages: vec![CreativeAgentMessage {
|
||||
id: "message-1".to_string(),
|
||||
role: CreativeAgentMessageRole::Assistant,
|
||||
kind: CreativeAgentMessageKind::Chat,
|
||||
text: "我会先选择拼图模板。".to_string(),
|
||||
created_at: "2026-05-05T00:00:00Z".to_string(),
|
||||
}],
|
||||
puzzle_template_catalog: vec![template_protocol()],
|
||||
puzzle_template_selection: Some(template_selection()),
|
||||
puzzle_image_generation_plan: None,
|
||||
target_binding: Some(CreativeTargetSessionBinding {
|
||||
play_type: CreativeTargetPlayType::Puzzle,
|
||||
target_session_id: "puzzle-session-1".to_string(),
|
||||
target_stage: CreativeTargetStage::PuzzleResult,
|
||||
result_profile_id: None,
|
||||
}),
|
||||
updated_at: "2026-05-05T00:00:01Z".to_string(),
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(&snapshot).expect("snapshot should serialize");
|
||||
assert_eq!(payload["sessionId"], json!("creative-session-1"));
|
||||
assert_eq!(payload["stage"], json!("waiting_template_confirmation"));
|
||||
assert_eq!(
|
||||
payload["inputSummary"]["entryContext"],
|
||||
json!("creation_home")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["inputSummary"]["unsupportedCapabilities"][0]["playType"],
|
||||
json!("big_fish")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["puzzleTemplateCatalog"][0]["templateId"],
|
||||
json!("puzzle.default-creative")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["puzzleTemplateSelection"]["selectedLevelMode"],
|
||||
json!("single_level")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["targetBinding"]["targetStage"],
|
||||
json!("puzzle-result")
|
||||
);
|
||||
|
||||
let decoded: CreativeAgentSessionSnapshot =
|
||||
serde_json::from_value(payload).expect("snapshot should deserialize");
|
||||
assert_eq!(decoded, snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creative_agent_sse_events_serialize_event_names() {
|
||||
let event = CreativeAgentSseEnvelope {
|
||||
event: CreativeAgentSseEventType::PuzzleTemplateSelection,
|
||||
data: serde_json::to_value(CreativeAgentTemplateSelectionEvent {
|
||||
session_id: "creative-session-1".to_string(),
|
||||
selection: template_selection(),
|
||||
})
|
||||
.expect("event data should serialize"),
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(event).expect("event should serialize");
|
||||
assert_eq!(payload["event"], json!("puzzle_template_selection"));
|
||||
assert_eq!(
|
||||
payload["data"]["selection"]["costRange"]["pricingUnit"],
|
||||
json!("point")
|
||||
);
|
||||
|
||||
let thought_event = CreativeAgentSseEnvelope {
|
||||
event: CreativeAgentSseEventType::ThoughtSummaryDelta,
|
||||
data: serde_json::to_value(CreativeAgentThoughtSummaryDeltaEvent {
|
||||
session_id: "creative-session-1".to_string(),
|
||||
thought_id: "thought-1".to_string(),
|
||||
text_delta: "正在理解素材".to_string(),
|
||||
})
|
||||
.expect("event data should serialize"),
|
||||
};
|
||||
let thought_payload = serde_json::to_value(thought_event).expect("event should serialize");
|
||||
assert_eq!(thought_payload["event"], json!("thought_summary_delta"));
|
||||
assert_eq!(thought_payload["data"]["thoughtId"], json!("thought-1"));
|
||||
|
||||
let catalog_event = CreativeAgentSseEnvelope {
|
||||
event: CreativeAgentSseEventType::PuzzleTemplateCatalog,
|
||||
data: serde_json::to_value(CreativeAgentTemplateCatalogEvent {
|
||||
session_id: "creative-session-1".to_string(),
|
||||
templates: vec![template_protocol()],
|
||||
})
|
||||
.expect("event data should serialize"),
|
||||
};
|
||||
let catalog_payload = serde_json::to_value(catalog_event).expect("event should serialize");
|
||||
assert_eq!(catalog_payload["event"], json!("puzzle_template_catalog"));
|
||||
assert_eq!(
|
||||
catalog_payload["data"]["templates"][0]["templateId"],
|
||||
json!("puzzle.default-creative")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creative_agent_multimodal_parts_keep_image_url_camel_case() {
|
||||
let request = StreamCreativeAgentMessageRequest {
|
||||
client_message_id: "client-message-1".to_string(),
|
||||
content: vec![
|
||||
CreativeAgentInputPart {
|
||||
part_type: CreativeAgentInputPartType::InputText,
|
||||
text: Some("做一张拼图".to_string()),
|
||||
image_url: None,
|
||||
asset_id: None,
|
||||
thumbnail_url: None,
|
||||
},
|
||||
CreativeAgentInputPart {
|
||||
part_type: CreativeAgentInputPartType::InputImage,
|
||||
text: None,
|
||||
image_url: Some("https://example.test/image.png".to_string()),
|
||||
asset_id: Some("asset-1".to_string()),
|
||||
thumbnail_url: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(request).expect("request should serialize");
|
||||
assert_eq!(payload["clientMessageId"], json!("client-message-1"));
|
||||
assert_eq!(payload["content"][0]["type"], json!("input_text"));
|
||||
assert_eq!(payload["content"][1]["type"], json!("input_image"));
|
||||
assert_eq!(
|
||||
payload["content"][1]["imageUrl"],
|
||||
json!("https://example.test/image.png")
|
||||
);
|
||||
}
|
||||
}
|
||||
169
server-rs/crates/shared-contracts/src/hyper3d.rs
Normal file
169
server-rs/crates/shared-contracts/src/hyper3d.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Hyper3dGenerationMode {
|
||||
TextToModel,
|
||||
ImageToModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dTextToModelRequest {
|
||||
pub prompt: String,
|
||||
#[serde(default)]
|
||||
pub negative_prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub seed: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub geometry_file_format: Option<String>,
|
||||
#[serde(default)]
|
||||
pub material: Option<String>,
|
||||
#[serde(default)]
|
||||
pub quality: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mesh_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub addons: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub bbox_condition: Option<Vec<f32>>,
|
||||
#[serde(default)]
|
||||
pub preview_render: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dImageToModelRequest {
|
||||
#[serde(default)]
|
||||
pub image_data_urls: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub image_urls: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub condition_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub seed: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub geometry_file_format: Option<String>,
|
||||
#[serde(default)]
|
||||
pub material: Option<String>,
|
||||
#[serde(default)]
|
||||
pub quality: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mesh_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub addons: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub bbox_condition: Option<Vec<f32>>,
|
||||
#[serde(default)]
|
||||
pub preview_render: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dTaskSubmitResponse {
|
||||
pub ok: bool,
|
||||
pub provider: String,
|
||||
pub mode: Hyper3dGenerationMode,
|
||||
pub task_uuid: String,
|
||||
pub subscription_key: String,
|
||||
pub job_uuids: Vec<String>,
|
||||
pub message: Option<String>,
|
||||
pub tier: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dTaskStatusRequest {
|
||||
pub subscription_key: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dTaskStatusResponse {
|
||||
pub ok: bool,
|
||||
pub provider: String,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub jobs: Vec<Hyper3dJobStatusPayload>,
|
||||
pub raw: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dJobStatusPayload {
|
||||
pub uuid: Option<String>,
|
||||
pub status: String,
|
||||
pub progress: Option<f32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dDownloadRequest {
|
||||
pub task_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dDownloadResponse {
|
||||
pub ok: bool,
|
||||
pub provider: String,
|
||||
#[serde(default)]
|
||||
pub files: Vec<Hyper3dDownloadFilePayload>,
|
||||
pub raw: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hyper3dDownloadFilePayload {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn text_to_model_request_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(Hyper3dTextToModelRequest {
|
||||
prompt: "低多边形宝箱".to_string(),
|
||||
negative_prompt: Some("文字".to_string()),
|
||||
seed: Some(42),
|
||||
geometry_file_format: Some("glb".to_string()),
|
||||
material: Some("PBR".to_string()),
|
||||
quality: Some("medium".to_string()),
|
||||
mesh_mode: Some("Quad".to_string()),
|
||||
addons: vec!["HighPack".to_string()],
|
||||
bbox_condition: Some(vec![1.0, 1.0, 1.0]),
|
||||
preview_render: Some(true),
|
||||
})
|
||||
.expect("request should serialize");
|
||||
|
||||
assert_eq!(payload["geometryFileFormat"], json!("glb"));
|
||||
assert_eq!(payload["meshMode"], json!("Quad"));
|
||||
assert_eq!(payload["bboxCondition"], json!([1.0, 1.0, 1.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_response_keeps_mode_as_kebab_case() {
|
||||
let payload = serde_json::to_value(Hyper3dTaskSubmitResponse {
|
||||
ok: true,
|
||||
provider: "hyper3d-rodin".to_string(),
|
||||
mode: Hyper3dGenerationMode::ImageToModel,
|
||||
task_uuid: "task-1".to_string(),
|
||||
subscription_key: "sub-1".to_string(),
|
||||
job_uuids: vec!["job-1".to_string()],
|
||||
message: Some("submitted".to_string()),
|
||||
tier: "Gen-2".to_string(),
|
||||
})
|
||||
.expect("response should serialize");
|
||||
|
||||
assert_eq!(payload["mode"], json!("image-to-model"));
|
||||
assert_eq!(payload["subscriptionKey"], json!("sub-1"));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
pub mod admin;
|
||||
pub mod ai;
|
||||
pub mod api;
|
||||
#[cfg(feature = "oss-contracts")]
|
||||
pub mod assets;
|
||||
pub mod auth;
|
||||
pub mod big_fish;
|
||||
pub mod big_fish_works;
|
||||
pub mod creation_agent_document_input;
|
||||
pub mod creation_audio;
|
||||
pub mod creation_entry_config;
|
||||
pub mod creative_agent;
|
||||
pub mod hyper3d;
|
||||
pub mod llm;
|
||||
pub mod match3d_agent;
|
||||
pub mod match3d_runtime;
|
||||
pub mod match3d_works;
|
||||
pub mod puzzle_agent;
|
||||
pub mod puzzle_creative_template;
|
||||
pub mod puzzle_gallery;
|
||||
pub mod puzzle_runtime;
|
||||
pub mod puzzle_works;
|
||||
pub mod runtime;
|
||||
pub mod runtime_story;
|
||||
pub mod square_hole_agent;
|
||||
pub mod square_hole_runtime;
|
||||
pub mod square_hole_works;
|
||||
pub mod story;
|
||||
pub mod visual_novel;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::creation_audio::CreationAudioAsset;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateMatch3DAgentSessionRequest {
|
||||
@@ -13,6 +15,12 @@ pub struct CreateMatch3DAgentSessionRequest {
|
||||
pub clear_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub difficulty: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub asset_style_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_style_label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_style_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -52,6 +60,12 @@ pub struct Match3DCreatorConfigResponse {
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
#[serde(default)]
|
||||
pub asset_style_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_style_label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_style_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -73,6 +87,36 @@ pub struct Match3DResultDraftResponse {
|
||||
pub total_item_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub generated_item_assets: Vec<Match3DGeneratedItemAssetResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DGeneratedItemAssetResponse {
|
||||
pub item_id: String,
|
||||
pub item_name: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_object_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_object_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_file_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub task_uuid: Option<String>,
|
||||
#[serde(default)]
|
||||
pub subscription_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_music: Option<CreationAudioAsset>,
|
||||
#[serde(default)]
|
||||
pub click_sound: Option<CreationAudioAsset>,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -147,6 +191,9 @@ mod tests {
|
||||
reference_image_src: Some("data:image/png;base64,abc".to_string()),
|
||||
clear_count: Some(4),
|
||||
difficulty: Some(3),
|
||||
asset_style_id: Some("clay-toy".to_string()),
|
||||
asset_style_label: Some("黏土手作".to_string()),
|
||||
asset_style_prompt: Some("圆润黏土手作风".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
@@ -157,5 +204,6 @@ mod tests {
|
||||
json!("data:image/png;base64,abc")
|
||||
);
|
||||
assert_eq!(payload["clearCount"], json!(4));
|
||||
assert_eq!(payload["assetStyleId"], json!("clay-toy"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::creation_audio::CreationAudioAsset;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutMatch3DWorkRequest {
|
||||
@@ -16,6 +18,12 @@ pub struct PutMatch3DWorkRequest {
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutMatch3DAudioAssetsRequest {
|
||||
pub generated_item_assets: Vec<Match3DGeneratedItemAssetResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorkSummaryResponse {
|
||||
@@ -40,6 +48,36 @@ pub struct Match3DWorkSummaryResponse {
|
||||
#[serde(default)]
|
||||
pub published_at: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub generated_item_assets: Vec<Match3DGeneratedItemAssetResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DGeneratedItemAssetResponse {
|
||||
pub item_id: String,
|
||||
pub item_name: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_object_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_object_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_file_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub task_uuid: Option<String>,
|
||||
#[serde(default)]
|
||||
pub subscription_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_music: Option<CreationAudioAsset>,
|
||||
#[serde(default)]
|
||||
pub click_sound: Option<CreationAudioAsset>,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::creation_audio::CreationAudioAsset;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePuzzleAgentSessionRequest {
|
||||
@@ -15,6 +17,8 @@ pub struct CreatePuzzleAgentSessionRequest {
|
||||
pub reference_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ai_redraw: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -37,6 +41,8 @@ pub struct ExecutePuzzleAgentActionRequest {
|
||||
#[serde(default)]
|
||||
pub image_model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ai_redraw: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub candidate_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub candidate_id: Option<String>,
|
||||
@@ -145,6 +151,10 @@ pub struct PuzzleDraftLevelResponse {
|
||||
pub level_id: String,
|
||||
pub level_name: String,
|
||||
pub picture_description: String,
|
||||
#[serde(default)]
|
||||
pub picture_reference: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_music: Option<CreationAudioAsset>,
|
||||
pub candidates: Vec<PuzzleGeneratedImageCandidateResponse>,
|
||||
#[serde(default)]
|
||||
pub selected_candidate_id: Option<String>,
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PuzzleTemplatePricingUnit {
|
||||
Point,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PuzzleSupportedLevelMode {
|
||||
Single,
|
||||
Multi,
|
||||
SingleOrMulti,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PuzzleLevelGenerationMode {
|
||||
SingleLevel,
|
||||
MultiLevel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleTemplateCostRange {
|
||||
pub min_points: u32,
|
||||
pub max_points: u32,
|
||||
pub pricing_unit: PuzzleTemplatePricingUnit,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PuzzleDraftEditableFieldPath {
|
||||
#[serde(rename = "workTitle")]
|
||||
WorkTitle,
|
||||
#[serde(rename = "workDescription")]
|
||||
WorkDescription,
|
||||
#[serde(rename = "workTags")]
|
||||
WorkTags,
|
||||
#[serde(rename = "levels[].levelName")]
|
||||
LevelName,
|
||||
#[serde(rename = "levels[].pictureDescription")]
|
||||
LevelPictureDescription,
|
||||
#[serde(rename = "levels[].pictureReference")]
|
||||
LevelPictureReference,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleTemplateImageGenerationPolicy {
|
||||
pub allow_uploaded_image_directly: bool,
|
||||
pub allow_generated_images: bool,
|
||||
pub allow_per_level_reference_image: bool,
|
||||
pub default_candidate_count_per_level: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleCreativeTemplateProtocol {
|
||||
pub template_id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
#[serde(default)]
|
||||
pub preview_image_src: Option<String>,
|
||||
pub supported_level_mode: PuzzleSupportedLevelMode,
|
||||
pub min_level_count: u32,
|
||||
pub max_level_count: u32,
|
||||
pub default_level_count: u32,
|
||||
pub cost_range: PuzzleTemplateCostRange,
|
||||
pub required_draft_fields: Vec<PuzzleDraftEditableFieldPath>,
|
||||
pub image_policy: PuzzleTemplateImageGenerationPolicy,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleCreativeTemplateSelection {
|
||||
pub template_id: String,
|
||||
pub title: String,
|
||||
pub reason: String,
|
||||
pub cost_range: PuzzleTemplateCostRange,
|
||||
pub supported_level_mode: PuzzleSupportedLevelMode,
|
||||
pub selected_level_mode: PuzzleLevelGenerationMode,
|
||||
pub planned_level_count: u32,
|
||||
pub requires_user_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativePuzzleLevelDraftInput {
|
||||
pub level_name: String,
|
||||
pub picture_description: String,
|
||||
/// 任务 A 冻结:Phase 1 采用正式字段方案,后续拼图草稿落地需补正式 pictureReference 字段。
|
||||
#[serde(default)]
|
||||
pub picture_reference: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreativePuzzleDraftToolInput {
|
||||
pub template_id: String,
|
||||
pub template_cost_range: PuzzleTemplateCostRange,
|
||||
pub work_title: String,
|
||||
pub work_description: String,
|
||||
pub work_tags: Vec<String>,
|
||||
pub levels: Vec<CreativePuzzleLevelDraftInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleImageGenerationPlanLevel {
|
||||
pub level_id: String,
|
||||
pub level_name: String,
|
||||
pub picture_description: String,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub picture_reference: Option<String>,
|
||||
pub candidate_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleImageGenerationPlan {
|
||||
pub mode: PuzzleLevelGenerationMode,
|
||||
pub template_id: String,
|
||||
pub estimated_cost_range: PuzzleTemplateCostRange,
|
||||
pub levels: Vec<PuzzleImageGenerationPlanLevel>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PuzzleDraftFieldPatchOperation {
|
||||
Set,
|
||||
Append,
|
||||
Replace,
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleDraftFieldPatch {
|
||||
pub field_path: PuzzleDraftEditableFieldPath,
|
||||
pub operation: PuzzleDraftFieldPatchOperation,
|
||||
#[serde(default)]
|
||||
pub level_id: Option<String>,
|
||||
pub value: serde_json::Value,
|
||||
pub rationale: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn cost_range() -> PuzzleTemplateCostRange {
|
||||
PuzzleTemplateCostRange {
|
||||
min_points: 2,
|
||||
max_points: 12,
|
||||
pricing_unit: PuzzleTemplatePricingUnit::Point,
|
||||
reason: "按关卡数和每关图片生成次数估算".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creative_agent_puzzle_template_protocol_uses_camel_case() {
|
||||
let payload = serde_json::to_value(PuzzleCreativeTemplateProtocol {
|
||||
template_id: "puzzle.default-creative".to_string(),
|
||||
title: "创意拼图".to_string(),
|
||||
summary: "把图文灵感做成拼图".to_string(),
|
||||
preview_image_src: None,
|
||||
supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti,
|
||||
min_level_count: 1,
|
||||
max_level_count: 6,
|
||||
default_level_count: 1,
|
||||
cost_range: cost_range(),
|
||||
required_draft_fields: vec![
|
||||
PuzzleDraftEditableFieldPath::WorkTitle,
|
||||
PuzzleDraftEditableFieldPath::LevelPictureReference,
|
||||
],
|
||||
image_policy: PuzzleTemplateImageGenerationPolicy {
|
||||
allow_uploaded_image_directly: true,
|
||||
allow_generated_images: true,
|
||||
allow_per_level_reference_image: true,
|
||||
default_candidate_count_per_level: 1,
|
||||
},
|
||||
})
|
||||
.expect("template should serialize");
|
||||
|
||||
assert_eq!(payload["templateId"], json!("puzzle.default-creative"));
|
||||
assert_eq!(payload["previewImageSrc"], json!(null));
|
||||
assert_eq!(payload["supportedLevelMode"], json!("single_or_multi"));
|
||||
assert_eq!(payload["costRange"]["pricingUnit"], json!("point"));
|
||||
assert_eq!(
|
||||
payload["requiredDraftFields"],
|
||||
json!(["workTitle", "levels[].pictureReference"])
|
||||
);
|
||||
assert_eq!(
|
||||
payload["imagePolicy"]["allowPerLevelReferenceImage"],
|
||||
json!(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creative_agent_puzzle_image_plan_roundtrips() {
|
||||
let plan = PuzzleImageGenerationPlan {
|
||||
mode: PuzzleLevelGenerationMode::MultiLevel,
|
||||
template_id: "puzzle.default-creative".to_string(),
|
||||
estimated_cost_range: cost_range(),
|
||||
levels: vec![PuzzleImageGenerationPlanLevel {
|
||||
level_id: "level-1".to_string(),
|
||||
level_name: "第一关".to_string(),
|
||||
picture_description: "温暖的家庭照片".to_string(),
|
||||
image_prompt: "pixel puzzle, warm family photo".to_string(),
|
||||
picture_reference: Some("asset-ref-1".to_string()),
|
||||
candidate_count: 1,
|
||||
}],
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(&plan).expect("plan should serialize");
|
||||
assert_eq!(payload["mode"], json!("multi_level"));
|
||||
assert_eq!(
|
||||
payload["levels"][0]["pictureReference"],
|
||||
json!("asset-ref-1")
|
||||
);
|
||||
|
||||
let decoded: PuzzleImageGenerationPlan =
|
||||
serde_json::from_value(payload).expect("plan should deserialize");
|
||||
assert_eq!(decoded, plan);
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,8 @@ pub struct PuzzleLeaderboardEntryResponse {
|
||||
pub nickname: String,
|
||||
pub elapsed_ms: u64,
|
||||
#[serde(default)]
|
||||
pub visible_tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub is_current_player: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -127,3 +127,23 @@ pub struct PuzzleWorkDetailResponse {
|
||||
pub struct PuzzleWorkMutationResponse {
|
||||
pub item: PuzzleWorkProfileResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleOnboardingGenerateRequest {
|
||||
pub prompt_text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleOnboardingGenerateResponse {
|
||||
pub item: PuzzleWorkSummaryResponse,
|
||||
pub level: PuzzleDraftLevelResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleOnboardingSaveRequest {
|
||||
pub prompt_text: String,
|
||||
pub item: PuzzleWorkSummaryResponse,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,22 @@ pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND: &str = "asse
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD: &str = "redeem_code_reward";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM: &str =
|
||||
"puzzle_author_incentive_claim";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD: &str = "daily_task_reward";
|
||||
pub const PROFILE_TASK_CYCLE_DAILY: &str = "daily";
|
||||
pub const PROFILE_TASK_STATUS_INCOMPLETE: &str = "incomplete";
|
||||
pub const PROFILE_TASK_STATUS_CLAIMABLE: &str = "claimable";
|
||||
pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
|
||||
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
|
||||
pub const PROFILE_FEEDBACK_STATUS_OPEN: &str = "open";
|
||||
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
|
||||
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
|
||||
pub const TRACKING_SCOPE_KIND_MODULE: &str = "module";
|
||||
pub const TRACKING_SCOPE_KIND_USER: &str = "user";
|
||||
pub const ANALYTICS_GRANULARITY_DAY: &str = "day";
|
||||
pub const ANALYTICS_GRANULARITY_WEEK: &str = "week";
|
||||
pub const ANALYTICS_GRANULARITY_MONTH: &str = "month";
|
||||
pub const ANALYTICS_GRANULARITY_QUARTER: &str = "quarter";
|
||||
pub const ANALYTICS_GRANULARITY_YEAR: &str = "year";
|
||||
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";
|
||||
@@ -239,6 +255,49 @@ pub struct CreateProfileRechargeOrderResponse {
|
||||
pub center: ProfileRechargeCenterResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileFeedbackEvidenceItemRequest {
|
||||
pub file_name: String,
|
||||
pub content_type: String,
|
||||
pub size_bytes: u64,
|
||||
pub data_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubmitProfileFeedbackRequest {
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub contact_phone: Option<String>,
|
||||
#[serde(default)]
|
||||
pub evidence_items: Vec<ProfileFeedbackEvidenceItemRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileFeedbackEvidenceItemResponse {
|
||||
pub evidence_id: String,
|
||||
pub file_name: String,
|
||||
pub content_type: String,
|
||||
pub size_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileFeedbackSubmissionResponse {
|
||||
pub feedback_id: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub evidence_items: Vec<ProfileFeedbackEvidenceItemResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubmitProfileFeedbackResponse {
|
||||
pub feedback: ProfileFeedbackSubmissionResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileReferralInvitedUserResponse {
|
||||
@@ -295,6 +354,116 @@ pub struct RedeemProfileRewardCodeResponse {
|
||||
pub ledger_entry: ProfileWalletLedgerEntryResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileTaskItemResponse {
|
||||
pub task_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub event_key: String,
|
||||
pub cycle: String,
|
||||
pub threshold: u32,
|
||||
pub progress_count: u32,
|
||||
pub reward_points: u64,
|
||||
pub status: String,
|
||||
pub day_key: i64,
|
||||
pub claimed_at: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileTaskCenterResponse {
|
||||
pub day_key: i64,
|
||||
pub wallet_balance: u64,
|
||||
pub tasks: Vec<ProfileTaskItemResponse>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClaimProfileTaskRewardResponse {
|
||||
pub task_id: String,
|
||||
pub day_key: i64,
|
||||
pub reward_points: u64,
|
||||
pub wallet_balance: u64,
|
||||
pub ledger_entry: ProfileWalletLedgerEntryResponse,
|
||||
pub center: ProfileTaskCenterResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileTaskConfigAdminResponse {
|
||||
pub task_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub event_key: String,
|
||||
pub cycle: String,
|
||||
pub scope_kind: String,
|
||||
pub threshold: u32,
|
||||
pub reward_points: u64,
|
||||
pub enabled: bool,
|
||||
pub sort_order: i32,
|
||||
pub created_by: String,
|
||||
pub created_at: String,
|
||||
pub updated_by: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileTaskConfigAdminListResponse {
|
||||
pub entries: Vec<ProfileTaskConfigAdminResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AnalyticsMetricQueryRequest {
|
||||
pub event_key: String,
|
||||
pub scope_kind: String,
|
||||
pub scope_id: String,
|
||||
pub granularity: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AnalyticsBucketMetricResponse {
|
||||
pub bucket_key: String,
|
||||
pub bucket_start_date_key: i64,
|
||||
pub bucket_end_date_key: i64,
|
||||
pub value: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AnalyticsMetricQueryResponse {
|
||||
pub buckets: Vec<AnalyticsBucketMetricResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminUpsertProfileTaskConfigRequest {
|
||||
pub task_id: String,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
pub event_key: String,
|
||||
pub cycle: String,
|
||||
pub scope_kind: String,
|
||||
pub threshold: u32,
|
||||
pub reward_points: u64,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDisableProfileTaskConfigRequest {
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminUpsertProfileRedeemCodeRequest {
|
||||
@@ -316,6 +485,10 @@ pub struct AdminUpsertProfileInviteCodeRequest {
|
||||
pub invite_code: String,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub starts_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -339,16 +512,31 @@ pub struct ProfileRedeemCodeAdminResponse {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileRedeemCodeAdminListResponse {
|
||||
pub entries: Vec<ProfileRedeemCodeAdminResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileInviteCodeAdminResponse {
|
||||
pub user_id: String,
|
||||
pub invite_code: String,
|
||||
pub metadata: serde_json::Value,
|
||||
pub starts_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileInviteCodeAdminListResponse {
|
||||
pub entries: Vec<ProfileInviteCodeAdminResponse>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -958,6 +1146,13 @@ mod tests {
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:06:00Z".to_string(),
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-9".to_string(),
|
||||
amount_delta: 10,
|
||||
balance_after: 212,
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD.to_string(),
|
||||
created_at: "2026-04-22T10:07:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
@@ -996,12 +1191,66 @@ mod tests {
|
||||
payload["entries"][7]["sourceType"],
|
||||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["entries"][8]["sourceType"],
|
||||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["entries"][0]["createdAt"],
|
||||
json!("2026-04-22T09:59:00Z")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_task_center_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfileTaskCenterResponse {
|
||||
day_key: 20576,
|
||||
wallet_balance: 18,
|
||||
tasks: vec![ProfileTaskItemResponse {
|
||||
task_id: "daily_login".to_string(),
|
||||
title: "每日登录".to_string(),
|
||||
description: "".to_string(),
|
||||
event_key: "daily_login".to_string(),
|
||||
cycle: PROFILE_TASK_CYCLE_DAILY.to_string(),
|
||||
threshold: 1,
|
||||
progress_count: 1,
|
||||
reward_points: 10,
|
||||
status: PROFILE_TASK_STATUS_CLAIMABLE.to_string(),
|
||||
day_key: 20576,
|
||||
claimed_at: None,
|
||||
updated_at: "2026-05-03T00:00:00Z".to_string(),
|
||||
}],
|
||||
updated_at: "2026-05-03T00:00:00Z".to_string(),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["walletBalance"], json!(18));
|
||||
assert_eq!(payload["tasks"][0]["taskId"], json!("daily_login"));
|
||||
assert_eq!(payload["tasks"][0]["rewardPoints"], json!(10));
|
||||
assert_eq!(
|
||||
payload["tasks"][0]["status"],
|
||||
json!(PROFILE_TASK_STATUS_CLAIMABLE)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_task_config_request_accepts_defaults() {
|
||||
let payload: AdminUpsertProfileTaskConfigRequest = serde_json::from_value(json!({
|
||||
"taskId": "daily_login",
|
||||
"title": "每日登录",
|
||||
"eventKey": "daily_login",
|
||||
"cycle": "daily",
|
||||
"scopeKind": "user",
|
||||
"threshold": 1,
|
||||
"rewardPoints": 10
|
||||
}))
|
||||
.expect("request should deserialize");
|
||||
|
||||
assert_eq!(payload.description, None);
|
||||
assert_eq!(payload.enabled, true);
|
||||
assert_eq!(payload.sort_order, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_recharge_center_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfileRechargeCenterResponse {
|
||||
@@ -1058,6 +1307,41 @@ mod tests {
|
||||
assert_eq!(payload.payment_channel, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_feedback_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(SubmitProfileFeedbackResponse {
|
||||
feedback: ProfileFeedbackSubmissionResponse {
|
||||
feedback_id: "feedback:user-1:1".to_string(),
|
||||
status: PROFILE_FEEDBACK_STATUS_OPEN.to_string(),
|
||||
created_at: "2026-05-08T10:00:00Z".to_string(),
|
||||
evidence_items: vec![ProfileFeedbackEvidenceItemResponse {
|
||||
evidence_id: "feedback:user-1:1:evidence:01".to_string(),
|
||||
file_name: "问题截图.png".to_string(),
|
||||
content_type: "image/png".to_string(),
|
||||
size_bytes: 128,
|
||||
}],
|
||||
},
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload["feedback"]["feedbackId"],
|
||||
json!("feedback:user-1:1")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["feedback"]["status"],
|
||||
json!(PROFILE_FEEDBACK_STATUS_OPEN)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["feedback"]["evidenceItems"][0]["contentType"],
|
||||
json!("image/png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["feedback"]["evidenceItems"][0]["sizeBytes"],
|
||||
json!(128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_play_stats_response_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(ProfilePlayStatsResponse {
|
||||
|
||||
171
server-rs/crates/shared-contracts/src/square_hole_agent.rs
Normal file
171
server-rs/crates/shared-contracts/src/square_hole_agent.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateSquareHoleSessionRequest {
|
||||
#[serde(default)]
|
||||
pub seed_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub theme_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub twist_rule: Option<String>,
|
||||
#[serde(default)]
|
||||
pub shape_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub difficulty: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendSquareHoleMessageRequest {
|
||||
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 ExecuteSquareHoleActionRequest {
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub game_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub regenerate_visual_assets: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub visual_asset_slot: Option<String>,
|
||||
#[serde(default)]
|
||||
pub visual_asset_option_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeOptionResponse {
|
||||
pub option_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub target_hole_id: String,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleOptionResponse {
|
||||
pub hole_id: String,
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleAnchorItemResponse {
|
||||
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 SquareHoleAnchorPackResponse {
|
||||
pub theme: SquareHoleAnchorItemResponse,
|
||||
pub twist_rule: SquareHoleAnchorItemResponse,
|
||||
pub shape_count: SquareHoleAnchorItemResponse,
|
||||
pub difficulty: SquareHoleAnchorItemResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleCreatorConfigResponse {
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
#[serde(default)]
|
||||
pub shape_options: Vec<SquareHoleShapeOptionResponse>,
|
||||
#[serde(default)]
|
||||
pub hole_options: Vec<SquareHoleHoleOptionResponse>,
|
||||
#[serde(default)]
|
||||
pub background_prompt: String,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleResultDraftResponse {
|
||||
pub profile_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_prompt: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub shape_options: Vec<SquareHoleShapeOptionResponse>,
|
||||
#[serde(default)]
|
||||
pub hole_options: Vec<SquareHoleHoleOptionResponse>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleAgentMessageResponse {
|
||||
pub id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleSessionSnapshotResponse {
|
||||
pub session_id: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub anchor_pack: SquareHoleAnchorPackResponse,
|
||||
pub config: SquareHoleCreatorConfigResponse,
|
||||
#[serde(default)]
|
||||
pub draft: Option<SquareHoleResultDraftResponse>,
|
||||
pub messages: Vec<SquareHoleAgentMessageResponse>,
|
||||
#[serde(default)]
|
||||
pub last_assistant_reply: Option<String>,
|
||||
#[serde(default)]
|
||||
pub published_profile_id: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleSessionResponse {
|
||||
pub session: SquareHoleSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleActionResponse {
|
||||
pub session: SquareHoleSessionSnapshotResponse,
|
||||
}
|
||||
96
server-rs/crates/shared-contracts/src/square_hole_runtime.rs
Normal file
96
server-rs/crates/shared-contracts/src/square_hole_runtime.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartSquareHoleRunRequest {
|
||||
pub profile_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DropSquareHoleShapeRequest {
|
||||
#[serde(default)]
|
||||
pub run_id: Option<String>,
|
||||
pub hole_id: String,
|
||||
pub client_snapshot_version: u64,
|
||||
pub client_event_id: String,
|
||||
pub dropped_at_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StopSquareHoleRunRequest {
|
||||
pub client_action_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeSnapshotResponse {
|
||||
pub shape_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub target_hole_id: String,
|
||||
pub color: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleSnapshotResponse {
|
||||
pub hole_id: String,
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleDropFeedbackResponse {
|
||||
pub accepted: bool,
|
||||
#[serde(default)]
|
||||
pub reject_reason: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleRunSnapshotResponse {
|
||||
pub run_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub status: String,
|
||||
pub snapshot_version: u64,
|
||||
pub started_at_ms: u64,
|
||||
pub duration_limit_ms: u64,
|
||||
pub remaining_ms: u64,
|
||||
pub total_shape_count: u32,
|
||||
pub completed_shape_count: u32,
|
||||
pub combo: u32,
|
||||
pub best_combo: u32,
|
||||
pub score: u32,
|
||||
pub rule_label: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub current_shape: Option<SquareHoleShapeSnapshotResponse>,
|
||||
pub holes: Vec<SquareHoleHoleSnapshotResponse>,
|
||||
#[serde(default)]
|
||||
pub last_feedback: Option<SquareHoleDropFeedbackResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleRunResponse {
|
||||
pub run: SquareHoleRunSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleDropResponse {
|
||||
pub feedback: SquareHoleDropFeedbackResponse,
|
||||
pub run: SquareHoleRunSnapshotResponse,
|
||||
}
|
||||
113
server-rs/crates/shared-contracts/src/square_hole_works.rs
Normal file
113
server-rs/crates/shared-contracts/src/square_hole_works.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeOptionResponse {
|
||||
pub option_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub target_hole_id: String,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleOptionResponse {
|
||||
pub hole_id: String,
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutSquareHoleWorkRequest {
|
||||
pub game_name: String,
|
||||
#[serde(default)]
|
||||
pub theme_text: Option<String>,
|
||||
pub twist_rule: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub shape_options: Option<Vec<SquareHoleShapeOptionResponse>>,
|
||||
#[serde(default)]
|
||||
pub hole_options: Option<Vec<SquareHoleHoleOptionResponse>>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegenerateSquareHoleWorkImageRequest {
|
||||
pub visual_asset_slot: String,
|
||||
#[serde(default)]
|
||||
pub visual_asset_option_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleWorkSummaryResponse {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
#[serde(default)]
|
||||
pub source_session_id: Option<String>,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub background_prompt: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub shape_options: Vec<SquareHoleShapeOptionResponse>,
|
||||
#[serde(default)]
|
||||
pub hole_options: Vec<SquareHoleHoleOptionResponse>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub play_count: u32,
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub published_at: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleWorkProfileResponse {
|
||||
#[serde(flatten)]
|
||||
pub summary: SquareHoleWorkSummaryResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleWorksResponse {
|
||||
pub items: Vec<SquareHoleWorkSummaryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleWorkDetailResponse {
|
||||
pub item: SquareHoleWorkProfileResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleWorkMutationResponse {
|
||||
pub item: SquareHoleWorkProfileResponse,
|
||||
}
|
||||
777
server-rs/crates/shared-contracts/src/visual_novel.rs
Normal file
777
server-rs/crates/shared-contracts/src/visual_novel.rs
Normal file
@@ -0,0 +1,777 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelSourceMode {
|
||||
Idea,
|
||||
Document,
|
||||
Blank,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelCharacterRole {
|
||||
Protagonist,
|
||||
Main,
|
||||
Supporting,
|
||||
Antagonist,
|
||||
Background,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelAssetSource {
|
||||
PlatformAsset,
|
||||
Generated,
|
||||
External,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelAudioGenerationKind {
|
||||
BackgroundMusic,
|
||||
SoundEffect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelSceneAvailability {
|
||||
Opening,
|
||||
Always,
|
||||
PhaseLocked,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelAttributePanelMode {
|
||||
Off,
|
||||
PlatformWhitelist,
|
||||
TemplateConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelValidationSeverity {
|
||||
Error,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelAgentStatus {
|
||||
Collecting,
|
||||
Drafting,
|
||||
Ready,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelAgentMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelAgentMessageKind {
|
||||
Chat,
|
||||
Summary,
|
||||
ActionResult,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelAgentActionKind {
|
||||
GenerateDraft,
|
||||
PatchWorld,
|
||||
PatchCharacter,
|
||||
PatchScene,
|
||||
PatchStoryPhase,
|
||||
GenerateSceneImage,
|
||||
GenerateCharacterImage,
|
||||
CompileWorkProfile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelAgentPhase {
|
||||
Perception,
|
||||
Reasoning,
|
||||
Drafting,
|
||||
Reflection,
|
||||
Finalizing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelRunMode {
|
||||
Test,
|
||||
Play,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelRunStatus {
|
||||
Active,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisualNovelRuntimeActionKind {
|
||||
Choice,
|
||||
FreeText,
|
||||
Continue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelTransitionKind {
|
||||
Fade,
|
||||
Cut,
|
||||
Flash,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VisualNovelHistorySource {
|
||||
Player,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum VisualNovelFlagValue {
|
||||
String(String),
|
||||
Number(f64),
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelDraftPatch {
|
||||
pub path: String,
|
||||
pub op: String,
|
||||
#[serde(default)]
|
||||
pub value: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelValidationIssue {
|
||||
pub issue_id: String,
|
||||
pub code: String,
|
||||
pub severity: VisualNovelValidationSeverity,
|
||||
pub path: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelChoiceDraft {
|
||||
pub choice_id: String,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub action_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelCharacterImageAsset {
|
||||
pub asset_id: String,
|
||||
pub image_src: String,
|
||||
#[serde(default)]
|
||||
pub expression: Option<String>,
|
||||
pub source: VisualNovelAssetSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelWorldDraft {
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub background: String,
|
||||
pub premise: String,
|
||||
pub literary_style: String,
|
||||
pub player_role: String,
|
||||
pub default_tone: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelCharacterDraft {
|
||||
pub character_id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub gender: Option<String>,
|
||||
pub role: VisualNovelCharacterRole,
|
||||
pub appearance: String,
|
||||
pub personality: String,
|
||||
pub tone: String,
|
||||
pub background: String,
|
||||
pub relationship_to_player: String,
|
||||
pub image_assets: Vec<VisualNovelCharacterImageAsset>,
|
||||
#[serde(default)]
|
||||
pub default_expression: Option<String>,
|
||||
pub is_player_visible: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelSceneDraft {
|
||||
pub scene_id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub music_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ambient_sound_src: Option<String>,
|
||||
pub availability: VisualNovelSceneAvailability,
|
||||
pub phase_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelStoryPhaseDraft {
|
||||
pub phase_id: String,
|
||||
pub title: String,
|
||||
pub goal: String,
|
||||
pub summary: String,
|
||||
pub entry_condition: String,
|
||||
pub exit_condition: String,
|
||||
pub scene_ids: Vec<String>,
|
||||
pub character_ids: Vec<String>,
|
||||
pub suggested_choices: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelOpeningDraft {
|
||||
#[serde(default)]
|
||||
pub scene_id: Option<String>,
|
||||
pub narration: String,
|
||||
#[serde(default)]
|
||||
pub speaker_character_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub first_dialogue: Option<String>,
|
||||
pub initial_choices: Vec<VisualNovelChoiceDraft>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRuntimeConfigDraft {
|
||||
pub text_mode_enabled: bool,
|
||||
pub default_text_mode: bool,
|
||||
pub max_history_entries: u32,
|
||||
pub max_assistant_step_count_per_turn: u32,
|
||||
pub allow_free_text_action: bool,
|
||||
pub allow_history_regeneration: bool,
|
||||
pub attribute_panel_mode: VisualNovelAttributePanelMode,
|
||||
pub save_archive_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelResultDraft {
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
pub work_title: String,
|
||||
pub work_description: String,
|
||||
pub work_tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
pub source_mode: VisualNovelSourceMode,
|
||||
pub source_asset_ids: Vec<String>,
|
||||
pub world: VisualNovelWorldDraft,
|
||||
pub characters: Vec<VisualNovelCharacterDraft>,
|
||||
pub scenes: Vec<VisualNovelSceneDraft>,
|
||||
pub story_phases: Vec<VisualNovelStoryPhaseDraft>,
|
||||
pub opening: VisualNovelOpeningDraft,
|
||||
pub runtime_config: VisualNovelRuntimeConfigDraft,
|
||||
pub publish_ready: bool,
|
||||
pub validation_issues: Vec<VisualNovelValidationIssue>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelAgentMessage {
|
||||
pub id: String,
|
||||
pub role: VisualNovelAgentMessageRole,
|
||||
pub kind: VisualNovelAgentMessageKind,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelAgentPendingAction {
|
||||
pub action_id: String,
|
||||
pub kind: VisualNovelAgentActionKind,
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub target_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub payload: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelAgentSessionSnapshot {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_mode: VisualNovelSourceMode,
|
||||
pub status: VisualNovelAgentStatus,
|
||||
pub messages: Vec<VisualNovelAgentMessage>,
|
||||
#[serde(default)]
|
||||
pub draft: Option<VisualNovelResultDraft>,
|
||||
#[serde(default)]
|
||||
pub pending_action: Option<VisualNovelAgentPendingAction>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateVisualNovelSessionRequest {
|
||||
pub source_mode: VisualNovelSourceMode,
|
||||
#[serde(default)]
|
||||
pub seed_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub source_asset_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelSessionResponse {
|
||||
pub session: VisualNovelAgentSessionSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelWorkSummary {
|
||||
pub runtime_kind: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub publish_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub published_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelWorkDetail {
|
||||
pub work_id: String,
|
||||
pub summary: VisualNovelWorkSummary,
|
||||
#[serde(default)]
|
||||
pub source_session_id: Option<String>,
|
||||
pub author_display_name: String,
|
||||
pub source_asset_ids: Vec<String>,
|
||||
pub draft: VisualNovelResultDraft,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelWorksResponse {
|
||||
pub works: Vec<VisualNovelWorkSummary>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelWorkResponse {
|
||||
pub work: VisualNovelWorkDetail,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateVisualNovelWorkRequest {
|
||||
pub draft: VisualNovelResultDraft,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelCompileResponse {
|
||||
pub session: VisualNovelAgentSessionSnapshot,
|
||||
pub work: VisualNovelWorkDetail,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateVisualNovelBackgroundMusicRequest {
|
||||
pub prompt: String,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub tags: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateVisualNovelSoundEffectRequest {
|
||||
pub prompt: String,
|
||||
#[serde(default)]
|
||||
pub duration: Option<u8>,
|
||||
#[serde(default)]
|
||||
pub seed: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelAudioGenerationTaskResponse {
|
||||
pub kind: VisualNovelAudioGenerationKind,
|
||||
pub task_id: String,
|
||||
pub provider: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PublishVisualNovelGeneratedAudioAssetRequest {
|
||||
pub scene_id: String,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelGeneratedAudioAssetResponse {
|
||||
pub kind: VisualNovelAudioGenerationKind,
|
||||
pub task_id: String,
|
||||
pub provider: String,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub asset_object_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub asset_kind: Option<String>,
|
||||
#[serde(default)]
|
||||
pub audio_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendVisualNovelMessageRequest {
|
||||
pub client_message_id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecuteVisualNovelAgentActionRequest {
|
||||
#[serde(default)]
|
||||
pub action_id: Option<String>,
|
||||
pub kind: VisualNovelAgentActionKind,
|
||||
#[serde(default)]
|
||||
pub target_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub payload: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
rename_all = "snake_case",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum VisualNovelAgentStreamEvent {
|
||||
Start {
|
||||
session_id: String,
|
||||
},
|
||||
Phase {
|
||||
phase: VisualNovelAgentPhase,
|
||||
},
|
||||
TextDelta {
|
||||
text: String,
|
||||
},
|
||||
DraftPatch {
|
||||
patch: VisualNovelDraftPatch,
|
||||
},
|
||||
ActionRequired {
|
||||
action: VisualNovelAgentPendingAction,
|
||||
},
|
||||
Complete {
|
||||
session: VisualNovelAgentSessionSnapshot,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
retryable: bool,
|
||||
},
|
||||
Done {},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
rename_all = "snake_case",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum VisualNovelRuntimeStep {
|
||||
SceneChange {
|
||||
scene_id: String,
|
||||
#[serde(default)]
|
||||
background_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
music_src: Option<String>,
|
||||
},
|
||||
Narration {
|
||||
text: String,
|
||||
},
|
||||
Dialogue {
|
||||
character_id: String,
|
||||
character_name: String,
|
||||
#[serde(default)]
|
||||
expression: Option<String>,
|
||||
text: String,
|
||||
},
|
||||
Transition {
|
||||
transition_kind: VisualNovelTransitionKind,
|
||||
#[serde(default)]
|
||||
text: Option<String>,
|
||||
},
|
||||
Choice {
|
||||
choices: Vec<VisualNovelChoiceDraft>,
|
||||
},
|
||||
Flag {
|
||||
key: String,
|
||||
value: VisualNovelFlagValue,
|
||||
},
|
||||
Metric {
|
||||
key: String,
|
||||
delta: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelHistoryEntry {
|
||||
pub entry_id: String,
|
||||
pub run_id: String,
|
||||
pub turn_index: u32,
|
||||
pub source: VisualNovelHistorySource,
|
||||
#[serde(default)]
|
||||
pub action_text: Option<String>,
|
||||
pub steps: Vec<VisualNovelRuntimeStep>,
|
||||
#[serde(default)]
|
||||
pub snapshot_before_hash: Option<String>,
|
||||
#[serde(default)]
|
||||
pub snapshot_after_hash: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRunSnapshot {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub mode: VisualNovelRunMode,
|
||||
pub status: VisualNovelRunStatus,
|
||||
#[serde(default)]
|
||||
pub current_scene_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub current_phase_id: Option<String>,
|
||||
pub visible_character_ids: Vec<String>,
|
||||
pub flags: BTreeMap<String, VisualNovelFlagValue>,
|
||||
pub metrics: BTreeMap<String, f64>,
|
||||
pub history: Vec<VisualNovelHistoryEntry>,
|
||||
pub available_choices: Vec<VisualNovelChoiceDraft>,
|
||||
pub text_mode_enabled: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRuntimeActionRequest {
|
||||
pub action_kind: VisualNovelRuntimeActionKind,
|
||||
#[serde(default)]
|
||||
pub choice_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub text: Option<String>,
|
||||
pub client_event_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelStartRunRequest {
|
||||
pub profile_id: String,
|
||||
pub mode: VisualNovelRunMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRunResponse {
|
||||
pub run: VisualNovelRunSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelHistoryResponse {
|
||||
pub history: Vec<VisualNovelHistoryEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRegenerateRequest {
|
||||
pub history_entry_id: String,
|
||||
pub client_event_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelSaveArchiveState {
|
||||
pub runtime_kind: String,
|
||||
pub profile_id: String,
|
||||
pub run_id: String,
|
||||
#[serde(default)]
|
||||
pub current_scene_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub current_phase_id: Option<String>,
|
||||
pub history_cursor: u32,
|
||||
#[serde(default)]
|
||||
pub snapshot_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
rename_all = "snake_case",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum VisualNovelRuntimeStreamEvent {
|
||||
Start { run_id: String },
|
||||
RawText { text: String },
|
||||
Step { step: VisualNovelRuntimeStep },
|
||||
Snapshot { run: VisualNovelRunSnapshot },
|
||||
Complete { run: VisualNovelRunSnapshot },
|
||||
Error { message: String, retryable: bool },
|
||||
Done {},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn result_draft_and_runtime_step_use_contract_case() {
|
||||
let draft = VisualNovelResultDraft {
|
||||
profile_id: Some("vn-profile-1".to_string()),
|
||||
work_title: "雨夜书店".to_string(),
|
||||
work_description: "一段视觉小说测试底稿".to_string(),
|
||||
work_tags: vec!["悬疑".to_string()],
|
||||
cover_image_src: None,
|
||||
source_mode: VisualNovelSourceMode::Idea,
|
||||
source_asset_ids: Vec::new(),
|
||||
world: VisualNovelWorldDraft {
|
||||
title: "雨夜书店".to_string(),
|
||||
summary: "主角在雨夜进入一间只在午夜出现的书店。".to_string(),
|
||||
background: "城市边缘的旧街区。".to_string(),
|
||||
premise: "找回遗失的名字。".to_string(),
|
||||
literary_style: "细腻、轻悬疑".to_string(),
|
||||
player_role: "误入书店的读者".to_string(),
|
||||
default_tone: "克制而温柔".to_string(),
|
||||
},
|
||||
characters: Vec::new(),
|
||||
scenes: Vec::new(),
|
||||
story_phases: Vec::new(),
|
||||
opening: VisualNovelOpeningDraft {
|
||||
scene_id: None,
|
||||
narration: "雨声落下。".to_string(),
|
||||
speaker_character_id: None,
|
||||
first_dialogue: None,
|
||||
initial_choices: Vec::new(),
|
||||
},
|
||||
runtime_config: VisualNovelRuntimeConfigDraft {
|
||||
text_mode_enabled: true,
|
||||
default_text_mode: false,
|
||||
max_history_entries: 80,
|
||||
max_assistant_step_count_per_turn: 8,
|
||||
allow_free_text_action: true,
|
||||
allow_history_regeneration: true,
|
||||
attribute_panel_mode: VisualNovelAttributePanelMode::Off,
|
||||
save_archive_enabled: true,
|
||||
},
|
||||
publish_ready: false,
|
||||
validation_issues: Vec::new(),
|
||||
updated_at: "2026-05-05T00:00:00Z".to_string(),
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(draft).expect("draft should serialize");
|
||||
assert_eq!(payload["profileId"], json!("vn-profile-1"));
|
||||
assert_eq!(payload["sourceMode"], json!("idea"));
|
||||
assert_eq!(payload["runtimeConfig"]["attributePanelMode"], json!("off"));
|
||||
|
||||
let step = VisualNovelRuntimeStep::SceneChange {
|
||||
scene_id: "scene-1".to_string(),
|
||||
background_image_src: None,
|
||||
music_src: None,
|
||||
};
|
||||
let step_payload = serde_json::to_value(step).expect("step should serialize");
|
||||
assert_eq!(step_payload["type"], json!("scene_change"));
|
||||
assert_eq!(step_payload["sceneId"], json!("scene-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_generation_contracts_use_camel_case_fields() {
|
||||
let request = CreateVisualNovelSoundEffectRequest {
|
||||
prompt: "雨声".to_string(),
|
||||
duration: Some(5),
|
||||
seed: Some(12),
|
||||
};
|
||||
let payload = serde_json::to_value(request).expect("request should serialize");
|
||||
assert_eq!(payload["duration"], json!(5));
|
||||
assert_eq!(payload["seed"], json!(12));
|
||||
|
||||
let response = VisualNovelGeneratedAudioAssetResponse {
|
||||
kind: VisualNovelAudioGenerationKind::SoundEffect,
|
||||
task_id: "task-1".to_string(),
|
||||
provider: "vector-engine-vidu".to_string(),
|
||||
status: "completed".to_string(),
|
||||
asset_object_id: Some("assetobj_1".to_string()),
|
||||
asset_kind: Some("visual_novel_ambient_sound".to_string()),
|
||||
audio_src: Some("/generated-custom-world-scenes/a.wav".to_string()),
|
||||
};
|
||||
let payload = serde_json::to_value(response).expect("response should serialize");
|
||||
assert_eq!(payload["kind"], json!("sound_effect"));
|
||||
assert_eq!(payload["taskId"], json!("task-1"));
|
||||
assert_eq!(payload["assetObjectId"], json!("assetobj_1"));
|
||||
assert_eq!(
|
||||
payload["audioSrc"],
|
||||
json!("/generated-custom-world-scenes/a.wav")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_stream_event_uses_tagged_envelope() {
|
||||
let event = VisualNovelRuntimeStreamEvent::Step {
|
||||
step: VisualNovelRuntimeStep::Narration {
|
||||
text: "门铃响了。".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(event).expect("event should serialize");
|
||||
assert_eq!(payload["type"], json!("step"));
|
||||
assert_eq!(payload["step"]["type"], json!("narration"));
|
||||
assert_eq!(payload["step"]["text"], json!("门铃响了。"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use serde_json::json;
|
||||
use shared_contracts::runtime::{
|
||||
AdminUpsertProfileInviteCodeRequest, ProfileInviteCodeAdminResponse,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn admin_upsert_invite_code_request_accepts_optional_validity_window() {
|
||||
let request: AdminUpsertProfileInviteCodeRequest = serde_json::from_value(json!({
|
||||
"inviteCode": "SY00000001",
|
||||
"metadata": { "note": "测试" },
|
||||
"startsAt": "2026-05-04T00:00:00Z",
|
||||
"expiresAt": null
|
||||
}))
|
||||
.expect("邀请码管理请求应接受 startsAt/expiresAt");
|
||||
|
||||
assert_eq!(request.starts_at.as_deref(), Some("2026-05-04T00:00:00Z"));
|
||||
assert_eq!(request.expires_at, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_invite_code_response_serializes_window_and_status_as_camel_case() {
|
||||
let response = ProfileInviteCodeAdminResponse {
|
||||
user_id: "user-1".to_string(),
|
||||
invite_code: "SY00000001".to_string(),
|
||||
metadata: json!({}),
|
||||
starts_at: Some("2026-05-04T00:00:00Z".to_string()),
|
||||
expires_at: None,
|
||||
status: "active".to_string(),
|
||||
created_at: "2026-05-04T00:00:00Z".to_string(),
|
||||
updated_at: "2026-05-04T00:00:00Z".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(response).expect("邀请码管理响应应可序列化");
|
||||
assert_eq!(value["startsAt"], json!("2026-05-04T00:00:00Z"));
|
||||
assert_eq!(value["expiresAt"], json!(null));
|
||||
assert_eq!(value["status"], json!("active"));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use serde_json::json;
|
||||
use shared_contracts::runtime::{
|
||||
AdminUpsertProfileTaskConfigRequest, ProfileTaskConfigAdminResponse, TRACKING_SCOPE_KIND_USER,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn admin_upsert_profile_task_config_keeps_personal_task_scope_user() {
|
||||
let request: AdminUpsertProfileTaskConfigRequest = serde_json::from_value(json!({
|
||||
"taskId": "daily_login",
|
||||
"title": "每日登录",
|
||||
"description": "",
|
||||
"eventKey": "daily_login",
|
||||
"cycle": "daily",
|
||||
"scopeKind": "user",
|
||||
"threshold": 1,
|
||||
"rewardPoints": 10,
|
||||
"enabled": true,
|
||||
"sortOrder": 10
|
||||
}))
|
||||
.expect("个人任务配置请求应接受 user scope");
|
||||
|
||||
assert_eq!(request.scope_kind, TRACKING_SCOPE_KIND_USER);
|
||||
|
||||
let value = serde_json::to_value(request).expect("个人任务配置请求应可序列化");
|
||||
assert_eq!(value["scopeKind"], json!(TRACKING_SCOPE_KIND_USER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_task_config_admin_response_serializes_scope_kind_as_user() {
|
||||
let response = ProfileTaskConfigAdminResponse {
|
||||
task_id: "daily_login".to_string(),
|
||||
title: "每日登录".to_string(),
|
||||
description: "".to_string(),
|
||||
event_key: "daily_login".to_string(),
|
||||
cycle: "daily".to_string(),
|
||||
scope_kind: TRACKING_SCOPE_KIND_USER.to_string(),
|
||||
threshold: 1,
|
||||
reward_points: 10,
|
||||
enabled: true,
|
||||
sort_order: 10,
|
||||
created_by: "admin".to_string(),
|
||||
created_at: "2026-05-04T00:00:00Z".to_string(),
|
||||
updated_by: "admin".to_string(),
|
||||
updated_at: "2026-05-04T00:00:00Z".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(response).expect("个人任务配置响应应可序列化");
|
||||
assert_eq!(value["scopeKind"], json!(TRACKING_SCOPE_KIND_USER));
|
||||
assert_eq!(value["taskId"], json!("daily_login"));
|
||||
assert_eq!(value["rewardPoints"], json!(10));
|
||||
}
|
||||
Reference in New Issue
Block a user