Merge origin/master into codex/wechat

This commit is contained in:
2026-05-12 16:20:45 +08:00
993 changed files with 154111 additions and 6329 deletions

View File

@@ -4,7 +4,11 @@ edition.workspace = true
version.workspace = true
license.workspace = true
[features]
# 默认给 api-server 等原生后端暴露资产上传 DTOSpacetimeDB 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 }

View File

@@ -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`

View File

@@ -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>,
}

View File

@@ -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"],

View File

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

View File

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

View File

@@ -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,
}

View 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")
);
}
}

View 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"));
}
}

View File

@@ -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;

View File

@@ -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"));
}
}

View File

@@ -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)]

View File

@@ -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>,

View File

@@ -0,0 +1,230 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum PuzzleTemplatePricingUnit {
Point,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleSupportedLevelMode {
Single,
Multi,
SingleOrMulti,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleLevelGenerationMode {
SingleLevel,
MultiLevel,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleTemplateCostRange {
pub min_points: u32,
pub max_points: u32,
pub pricing_unit: PuzzleTemplatePricingUnit,
pub reason: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PuzzleDraftEditableFieldPath {
#[serde(rename = "workTitle")]
WorkTitle,
#[serde(rename = "workDescription")]
WorkDescription,
#[serde(rename = "workTags")]
WorkTags,
#[serde(rename = "levels[].levelName")]
LevelName,
#[serde(rename = "levels[].pictureDescription")]
LevelPictureDescription,
#[serde(rename = "levels[].pictureReference")]
LevelPictureReference,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleTemplateImageGenerationPolicy {
pub allow_uploaded_image_directly: bool,
pub allow_generated_images: bool,
pub allow_per_level_reference_image: bool,
pub default_candidate_count_per_level: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeTemplateProtocol {
pub template_id: String,
pub title: String,
pub summary: String,
#[serde(default)]
pub preview_image_src: Option<String>,
pub supported_level_mode: PuzzleSupportedLevelMode,
pub min_level_count: u32,
pub max_level_count: u32,
pub default_level_count: u32,
pub cost_range: PuzzleTemplateCostRange,
pub required_draft_fields: Vec<PuzzleDraftEditableFieldPath>,
pub image_policy: PuzzleTemplateImageGenerationPolicy,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeTemplateSelection {
pub template_id: String,
pub title: String,
pub reason: String,
pub cost_range: PuzzleTemplateCostRange,
pub supported_level_mode: PuzzleSupportedLevelMode,
pub selected_level_mode: PuzzleLevelGenerationMode,
pub planned_level_count: u32,
pub requires_user_confirmation: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativePuzzleLevelDraftInput {
pub level_name: String,
pub picture_description: String,
/// 任务 A 冻结Phase 1 采用正式字段方案,后续拼图草稿落地需补正式 pictureReference 字段。
#[serde(default)]
pub picture_reference: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativePuzzleDraftToolInput {
pub template_id: String,
pub template_cost_range: PuzzleTemplateCostRange,
pub work_title: String,
pub work_description: String,
pub work_tags: Vec<String>,
pub levels: Vec<CreativePuzzleLevelDraftInput>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleImageGenerationPlanLevel {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub image_prompt: String,
#[serde(default)]
pub picture_reference: Option<String>,
pub candidate_count: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleImageGenerationPlan {
pub mode: PuzzleLevelGenerationMode,
pub template_id: String,
pub estimated_cost_range: PuzzleTemplateCostRange,
pub levels: Vec<PuzzleImageGenerationPlanLevel>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleDraftFieldPatchOperation {
Set,
Append,
Replace,
Remove,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleDraftFieldPatch {
pub field_path: PuzzleDraftEditableFieldPath,
pub operation: PuzzleDraftFieldPatchOperation,
#[serde(default)]
pub level_id: Option<String>,
pub value: serde_json::Value,
pub rationale: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn cost_range() -> PuzzleTemplateCostRange {
PuzzleTemplateCostRange {
min_points: 2,
max_points: 12,
pricing_unit: PuzzleTemplatePricingUnit::Point,
reason: "按关卡数和每关图片生成次数估算".to_string(),
}
}
#[test]
fn creative_agent_puzzle_template_protocol_uses_camel_case() {
let payload = serde_json::to_value(PuzzleCreativeTemplateProtocol {
template_id: "puzzle.default-creative".to_string(),
title: "创意拼图".to_string(),
summary: "把图文灵感做成拼图".to_string(),
preview_image_src: None,
supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti,
min_level_count: 1,
max_level_count: 6,
default_level_count: 1,
cost_range: cost_range(),
required_draft_fields: vec![
PuzzleDraftEditableFieldPath::WorkTitle,
PuzzleDraftEditableFieldPath::LevelPictureReference,
],
image_policy: PuzzleTemplateImageGenerationPolicy {
allow_uploaded_image_directly: true,
allow_generated_images: true,
allow_per_level_reference_image: true,
default_candidate_count_per_level: 1,
},
})
.expect("template should serialize");
assert_eq!(payload["templateId"], json!("puzzle.default-creative"));
assert_eq!(payload["previewImageSrc"], json!(null));
assert_eq!(payload["supportedLevelMode"], json!("single_or_multi"));
assert_eq!(payload["costRange"]["pricingUnit"], json!("point"));
assert_eq!(
payload["requiredDraftFields"],
json!(["workTitle", "levels[].pictureReference"])
);
assert_eq!(
payload["imagePolicy"]["allowPerLevelReferenceImage"],
json!(true)
);
}
#[test]
fn creative_agent_puzzle_image_plan_roundtrips() {
let plan = PuzzleImageGenerationPlan {
mode: PuzzleLevelGenerationMode::MultiLevel,
template_id: "puzzle.default-creative".to_string(),
estimated_cost_range: cost_range(),
levels: vec![PuzzleImageGenerationPlanLevel {
level_id: "level-1".to_string(),
level_name: "第一关".to_string(),
picture_description: "温暖的家庭照片".to_string(),
image_prompt: "pixel puzzle, warm family photo".to_string(),
picture_reference: Some("asset-ref-1".to_string()),
candidate_count: 1,
}],
};
let payload = serde_json::to_value(&plan).expect("plan should serialize");
assert_eq!(payload["mode"], json!("multi_level"));
assert_eq!(
payload["levels"][0]["pictureReference"],
json!("asset-ref-1")
);
let decoded: PuzzleImageGenerationPlan =
serde_json::from_value(payload).expect("plan should deserialize");
assert_eq!(decoded, plan);
}
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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 {

View 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,
}

View 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,
}

View 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,
}

View 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!("门铃响了。"));
}
}

View File

@@ -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"));
}

View File

@@ -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));
}