1
This commit is contained in:
@@ -19,13 +19,12 @@ use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
use shared_contracts::admin::{
|
||||
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||
AdminUpsertCreationEntryTypeConfigRequest,
|
||||
AdminDatabaseOverviewPayload, AdminDatabaseTableListResponse, AdminDatabaseTableRowPayload,
|
||||
AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload,
|
||||
AdminDebugHeaderInput, AdminDebugHttpRequest, AdminDebugHttpResponse, AdminLoginRequest,
|
||||
AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, AdminServiceOverviewPayload,
|
||||
AdminSessionPayload, AdminTrackingEventEntryPayload, AdminTrackingEventListQuery,
|
||||
AdminTrackingEventListResponse,
|
||||
AdminTrackingEventListResponse, AdminUpsertCreationEntryTypeConfigRequest,
|
||||
};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
@@ -196,13 +195,15 @@ pub async fn admin_list_database_table_rows(
|
||||
Ok(json_success_body(Some(&request_context), response))
|
||||
}
|
||||
|
||||
|
||||
pub async fn admin_get_creation_entry_config(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_admin): Extension<AuthenticatedAdmin>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let config = state.get_creation_entry_config().await.map_err(map_admin_spacetime_error)?;
|
||||
let config = state
|
||||
.get_creation_entry_config()
|
||||
.await
|
||||
.map_err(map_admin_spacetime_error)?;
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminCreationEntryConfigResponse {
|
||||
@@ -1328,8 +1329,10 @@ mod tests {
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use serde_json::json;
|
||||
use shared_contracts::admin::{
|
||||
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||
AdminUpsertCreationEntryTypeConfigRequest,AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
|
||||
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||
AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery,
|
||||
AdminUpsertCreationEntryTypeConfigRequest,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn normalize_debug_path_rejects_absolute_url() {
|
||||
|
||||
@@ -16,8 +16,8 @@ use tracing::{Level, Span, error, info, info_span, warn};
|
||||
use crate::{
|
||||
admin::{
|
||||
admin_debug_http, admin_get_creation_entry_config, admin_list_database_table_rows,
|
||||
admin_list_database_tables, admin_list_tracking_events, admin_login, admin_me, admin_overview,
|
||||
admin_upsert_creation_entry_config, require_admin_auth,
|
||||
admin_list_database_tables, admin_list_tracking_events, admin_login, admin_me,
|
||||
admin_overview, admin_upsert_creation_entry_config, require_admin_auth,
|
||||
},
|
||||
ai_tasks::{
|
||||
append_ai_text_chunk, attach_ai_result_reference, cancel_ai_task, complete_ai_stage,
|
||||
@@ -90,10 +90,10 @@ use crate::{
|
||||
match3d::{
|
||||
click_match3d_item, compile_match3d_agent_draft, create_match3d_agent_session,
|
||||
delete_match3d_work, execute_match3d_agent_action, finish_match3d_time_up,
|
||||
get_match3d_agent_session, get_match3d_run, get_match3d_work_detail, get_match3d_works,
|
||||
list_match3d_gallery, publish_match3d_work, put_match3d_work, restart_match3d_run,
|
||||
start_match3d_run, stop_match3d_run, stream_match3d_agent_message,
|
||||
submit_match3d_agent_message,
|
||||
generate_match3d_work_tags, get_match3d_agent_session, get_match3d_run,
|
||||
get_match3d_work_detail, get_match3d_works, list_match3d_gallery, publish_match3d_work,
|
||||
put_match3d_work, restart_match3d_run, start_match3d_run, stop_match3d_run,
|
||||
stream_match3d_agent_message, submit_match3d_agent_message,
|
||||
},
|
||||
password_entry::password_entry,
|
||||
password_management::{change_password, reset_password},
|
||||
@@ -924,6 +924,13 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/works/tags",
|
||||
post(generate_match3d_work_tags).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/match3d/works/{profile_id}",
|
||||
get(get_match3d_work_detail)
|
||||
@@ -1510,7 +1517,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
)),
|
||||
)
|
||||
.route("/api/auth/password/reset", post(reset_password))
|
||||
// 后端 runtime/API 路由读取入口配置做统一熔断,避免前端隐藏后后端仍可被直接访问。
|
||||
// 后端 runtime/API 路由只按 open 做熔断;visible 仅控制创作页入口展示。
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_creation_entry_route_enabled,
|
||||
@@ -1993,7 +2000,10 @@ mod tests {
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = read_json_response(response).await;
|
||||
assert_eq!(body["error"]["details"]["reason"], "creation_entry_disabled");
|
||||
assert_eq!(
|
||||
body["error"]["details"]["reason"],
|
||||
"creation_entry_disabled"
|
||||
);
|
||||
assert_eq!(body["error"]["details"]["creationTypeId"], "puzzle");
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ use shared_contracts::assets::{
|
||||
AssetBindingPayload, AssetHistoryEntryPayload, AssetHistoryListResponse, AssetHistoryQuery,
|
||||
AssetObjectPayload, AssetReadUrlPayload, BindAssetObjectRequest, BindAssetObjectResponse,
|
||||
ConfirmAssetObjectAccessPolicy, ConfirmAssetObjectRequest, ConfirmAssetObjectResponse,
|
||||
CreateDirectUploadTicketRequest, CreateDirectUploadTicketResponse, DirectUploadTicketPayload,
|
||||
GetAssetReadUrlResponse, GetReadUrlQuery,
|
||||
CreateDirectUploadTicketRequest, CreateDirectUploadTicketResponse, DirectUploadObjectAccess,
|
||||
DirectUploadTicketFormFields, DirectUploadTicketPayload, GetAssetReadUrlResponse,
|
||||
GetReadUrlQuery,
|
||||
};
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
|
||||
@@ -44,7 +45,8 @@ const SUPPORTED_ASSET_HISTORY_KINDS: [&str; 7] = [
|
||||
"square_hole_shape_image",
|
||||
"square_hole_hole_image",
|
||||
];
|
||||
const ASSET_READ_BYTES_MAX_SIZE_BYTES: u64 = 10 * 1024 * 1024;
|
||||
// 中文注释:同源字节读取同时服务图片转 Data URL 与 Match3D 私有 GLB 预览,Rodin GLB 可能明显超过图片上限。
|
||||
const ASSET_READ_BYTES_MAX_SIZE_BYTES: u64 = 120 * 1024 * 1024;
|
||||
const ASSET_READ_BYTES_DEFAULT_EXPIRE_SECONDS: u64 = 300;
|
||||
|
||||
pub async fn create_direct_upload_ticket(
|
||||
@@ -73,7 +75,10 @@ pub async fn create_direct_upload_ticket(
|
||||
path_segments: payload.path_segments,
|
||||
file_name: payload.file_name,
|
||||
content_type: payload.content_type,
|
||||
access: payload.access.unwrap_or(OssObjectAccess::Private),
|
||||
access: payload
|
||||
.access
|
||||
.map(direct_upload_access_to_oss)
|
||||
.unwrap_or(OssObjectAccess::Private),
|
||||
metadata: payload.metadata,
|
||||
max_size_bytes: payload.max_size_bytes,
|
||||
expire_seconds: payload.expire_seconds,
|
||||
@@ -85,7 +90,7 @@ pub async fn create_direct_upload_ticket(
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
})?;
|
||||
let upload = DirectUploadTicketPayload::from(signed);
|
||||
let upload = direct_upload_ticket_payload_from_oss(signed);
|
||||
|
||||
record_asset_tracking_event(
|
||||
&state,
|
||||
@@ -149,11 +154,76 @@ pub async fn get_asset_read_url(
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
GetAssetReadUrlResponse {
|
||||
read: AssetReadUrlPayload::from(signed),
|
||||
read: asset_read_url_payload_from_oss(signed),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn direct_upload_access_to_oss(value: DirectUploadObjectAccess) -> OssObjectAccess {
|
||||
match value {
|
||||
DirectUploadObjectAccess::Public => OssObjectAccess::Public,
|
||||
DirectUploadObjectAccess::Private => OssObjectAccess::Private,
|
||||
}
|
||||
}
|
||||
|
||||
fn direct_upload_access_from_oss(value: OssObjectAccess) -> DirectUploadObjectAccess {
|
||||
match value {
|
||||
OssObjectAccess::Public => DirectUploadObjectAccess::Public,
|
||||
OssObjectAccess::Private => DirectUploadObjectAccess::Private,
|
||||
}
|
||||
}
|
||||
|
||||
fn direct_upload_ticket_payload_from_oss(
|
||||
value: platform_oss::OssPostObjectResponse,
|
||||
) -> DirectUploadTicketPayload {
|
||||
DirectUploadTicketPayload {
|
||||
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: direct_upload_access_from_oss(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: direct_upload_ticket_form_fields_from_oss(value.form_fields),
|
||||
}
|
||||
}
|
||||
|
||||
fn direct_upload_ticket_form_fields_from_oss(
|
||||
value: platform_oss::OssPostObjectFormFields,
|
||||
) -> DirectUploadTicketFormFields {
|
||||
DirectUploadTicketFormFields {
|
||||
key: value.key,
|
||||
policy: value.policy,
|
||||
signature_version: value.signature_version,
|
||||
credential: value.credential,
|
||||
date: value.date,
|
||||
signature: value.signature,
|
||||
success_action_status: value.success_action_status,
|
||||
content_type: value.content_type,
|
||||
metadata: value.metadata,
|
||||
}
|
||||
}
|
||||
|
||||
fn asset_read_url_payload_from_oss(
|
||||
value: platform_oss::OssSignedGetObjectUrlResponse,
|
||||
) -> AssetReadUrlPayload {
|
||||
AssetReadUrlPayload {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_asset_read_bytes(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<GetReadUrlQuery>,
|
||||
|
||||
@@ -8,6 +8,9 @@ use axum::{
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[cfg(test)]
|
||||
use module_runtime::build_creation_entry_config_response;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
||||
state::AppState,
|
||||
@@ -92,8 +95,8 @@ fn creation_entry_error_response(request_context: &RequestContext, error: AppErr
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_creation_entry_config_response(
|
||||
) -> shared_contracts::creation_entry_config::CreationEntryConfigResponse {
|
||||
pub(crate) fn test_creation_entry_config_response()
|
||||
-> shared_contracts::creation_entry_config::CreationEntryConfigResponse {
|
||||
build_creation_entry_config_response(module_runtime::CreationEntryConfigSnapshot {
|
||||
config_id: module_runtime::CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string(),
|
||||
start_card: module_runtime::CreationEntryStartCardSnapshot {
|
||||
@@ -111,7 +114,7 @@ pub(crate) fn test_creation_entry_config_response(
|
||||
test_creation_type("big-fish", false, true, 20),
|
||||
test_creation_type("puzzle", true, true, 30),
|
||||
test_creation_type("match3d", true, true, 40),
|
||||
test_creation_type("square-hole", true, true, 50),
|
||||
test_creation_type("square-hole", false, true, 50),
|
||||
test_creation_type("visual-novel", true, true, 60),
|
||||
test_creation_type("airp", true, false, 70),
|
||||
test_creation_type("creative-agent", false, true, 80),
|
||||
|
||||
@@ -231,12 +231,7 @@ pub(crate) async fn query_task_status(
|
||||
.await?;
|
||||
|
||||
let jobs = extract_job_statuses(&response);
|
||||
let status = normalize_task_status(
|
||||
find_first_string_by_key(&response, "status")
|
||||
.or_else(|| jobs.first().map(|job| job.status.clone()))
|
||||
.as_deref()
|
||||
.unwrap_or("unknown"),
|
||||
);
|
||||
let status = resolve_hyper3d_overall_status(&response, &jobs);
|
||||
|
||||
Ok(contract::Hyper3dTaskStatusResponse {
|
||||
ok: true,
|
||||
@@ -539,6 +534,33 @@ fn extract_job_statuses(payload: &Value) -> Vec<contract::Hyper3dJobStatusPayloa
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_hyper3d_overall_status(
|
||||
payload: &Value,
|
||||
jobs: &[contract::Hyper3dJobStatusPayload],
|
||||
) -> String {
|
||||
if !jobs.is_empty() {
|
||||
if jobs.iter().any(|job| job.status == "failed") {
|
||||
return "failed".to_string();
|
||||
}
|
||||
if jobs.iter().all(|job| job.status == "done") {
|
||||
return "done".to_string();
|
||||
}
|
||||
if jobs.iter().any(|job| job.status == "generating") {
|
||||
return "generating".to_string();
|
||||
}
|
||||
if jobs.iter().any(|job| job.status == "waiting") {
|
||||
return "waiting".to_string();
|
||||
}
|
||||
return "unknown".to_string();
|
||||
}
|
||||
|
||||
normalize_task_status(
|
||||
find_first_string_by_key(payload, "status")
|
||||
.as_deref()
|
||||
.unwrap_or("unknown"),
|
||||
)
|
||||
}
|
||||
|
||||
fn extract_job_uuids(payload: &Value) -> Vec<String> {
|
||||
let mut job_uuids = Vec::new();
|
||||
if let Some(jobs) = find_first_array_by_keys(payload, &["jobs"]) {
|
||||
@@ -580,6 +602,12 @@ fn collect_download_files(value: &Value, output: &mut Vec<contract::Hyper3dDownl
|
||||
.get("url")
|
||||
.or_else(|| object.get("download_url"))
|
||||
.or_else(|| object.get("downloadUrl"))
|
||||
.or_else(|| object.get("file_url"))
|
||||
.or_else(|| object.get("fileUrl"))
|
||||
.or_else(|| object.get("signed_url"))
|
||||
.or_else(|| object.get("signedUrl"))
|
||||
.or_else(|| object.get("presigned_url"))
|
||||
.or_else(|| object.get("presignedUrl"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| value.starts_with("http://") || value.starts_with("https://"));
|
||||
@@ -588,6 +616,9 @@ fn collect_download_files(value: &Value, output: &mut Vec<contract::Hyper3dDownl
|
||||
.get("name")
|
||||
.or_else(|| object.get("file_name"))
|
||||
.or_else(|| object.get("filename"))
|
||||
.or_else(|| object.get("fileName"))
|
||||
.or_else(|| object.get("display_name"))
|
||||
.or_else(|| object.get("displayName"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
@@ -1070,6 +1101,28 @@ mod tests {
|
||||
assert_eq!(files[0].name, "model.glb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_download_files_from_file_url_aliases() {
|
||||
let files = extract_download_files(&json!({
|
||||
"result": {
|
||||
"files": [
|
||||
{
|
||||
"fileName": "rodin-result.glb",
|
||||
"fileUrl": "https://cdn.example/rodin-result.glb?token=1"
|
||||
},
|
||||
{
|
||||
"displayName": "preview.png",
|
||||
"signedUrl": "https://cdn.example/preview.png?token=1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
assert_eq!(files[0].name, "rodin-result.glb");
|
||||
assert_eq!(files[0].url, "https://cdn.example/rodin-result.glb?token=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_status_values() {
|
||||
assert_eq!(normalize_task_status("Waiting"), "waiting");
|
||||
@@ -1077,4 +1130,34 @@ mod tests {
|
||||
assert_eq!(normalize_task_status("Done"), "done");
|
||||
assert_eq!(normalize_task_status("Failed"), "failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_status_done_only_when_all_jobs_done() {
|
||||
let jobs = extract_job_statuses(&json!({
|
||||
"jobs": [
|
||||
{ "uuid": "preview", "status": "Done" },
|
||||
{ "uuid": "model", "status": "Generating" }
|
||||
]
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
resolve_hyper3d_overall_status(&json!({ "status": "Done" }), &jobs),
|
||||
"generating"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_status_failed_when_any_job_failed() {
|
||||
let jobs = extract_job_statuses(&json!({
|
||||
"jobs": [
|
||||
{ "uuid": "preview", "status": "Done" },
|
||||
{ "uuid": "model", "status": "Failed", "message": "bad input" }
|
||||
]
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
resolve_hyper3d_overall_status(&json!({ "status": "Generating" }), &jobs),
|
||||
"failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use axum::{
|
||||
},
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use futures_util::future::try_join_all;
|
||||
use image::{GenericImageView, ImageFormat};
|
||||
use module_match3d::{
|
||||
MATCH3D_MESSAGE_ID_PREFIX, MATCH3D_PROFILE_ID_PREFIX, MATCH3D_RUN_ID_PREFIX,
|
||||
@@ -79,9 +80,12 @@ const MATCH3D_DEFAULT_DIFFICULTY: u32 = 4;
|
||||
const MATCH3D_GENERATED_ITEM_COUNT: usize = 3;
|
||||
const MATCH3D_GENERATED_CLEAR_COUNT: u32 = 3;
|
||||
const MATCH3D_MATERIAL_GRID_SIZE: u32 = 2;
|
||||
const MATCH3D_RODIN_STATUS_MAX_ATTEMPTS: usize = 36;
|
||||
const MATCH3D_RODIN_STATUS_MAX_ATTEMPTS: usize = 120;
|
||||
const MATCH3D_RODIN_STATUS_POLL_INTERVAL_MS: u64 = 5_000;
|
||||
const MATCH3D_RODIN_DOWNLOAD_MAX_ATTEMPTS: usize = 60;
|
||||
const MATCH3D_RODIN_DOWNLOAD_POLL_INTERVAL_MS: u64 = 5_000;
|
||||
const MATCH3D_RODIN_MAX_MODEL_BYTES: usize = 120 * 1024 * 1024;
|
||||
const MATCH3D_WORK_METADATA_LLM_MODEL: &str = "gpt-4o";
|
||||
const MATCH3D_QUESTION_THEME: &str = "你想创作什么题材";
|
||||
const MATCH3D_QUESTION_CLEAR_COUNT: &str = "需要消除多少次才能通关";
|
||||
const MATCH3D_QUESTION_DIFFICULTY: &str = "如果难度是从1-10,你要创作的关卡是难度几";
|
||||
@@ -116,6 +120,12 @@ struct Match3DGeneratedItemAsset {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Match3DGeneratedWorkMetadata {
|
||||
game_name: String,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Match3DGeneratedItemAssetJson {
|
||||
@@ -146,6 +156,16 @@ struct Match3DAssetUpload {
|
||||
object_key: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Match3DGeneratedItemModelSeed {
|
||||
item_id: String,
|
||||
item_name: String,
|
||||
item_slug: String,
|
||||
image_upload: Match3DAssetUpload,
|
||||
image_bytes: Vec<u8>,
|
||||
generated_at_micros: i64,
|
||||
}
|
||||
|
||||
struct Match3DRodinModelAsset {
|
||||
task_uuid: String,
|
||||
subscription_key: String,
|
||||
@@ -172,6 +192,19 @@ pub(crate) struct CompileMatch3DDraftRequest {
|
||||
cover_image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GenerateMatch3DWorkTagsRequest {
|
||||
game_name: String,
|
||||
theme_text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GenerateMatch3DWorkTagsResponse {
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn create_match3d_agent_session(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -571,6 +604,26 @@ pub async fn put_match3d_work(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn generate_match3d_work_tags(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||
payload: Result<Json<GenerateMatch3DWorkTagsRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||||
let tags = generate_match3d_work_tags_for_profile(
|
||||
&state,
|
||||
payload.game_name.as_str(),
|
||||
payload.theme_text.as_str(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
GenerateMatch3DWorkTagsResponse { tags },
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn publish_match3d_work(
|
||||
State(state): State<AppState>,
|
||||
Path(profile_id): Path<String>,
|
||||
@@ -994,11 +1047,8 @@ async fn compile_match3d_draft_for_session(
|
||||
));
|
||||
}
|
||||
|
||||
let tags_json = tags
|
||||
.as_ref()
|
||||
.map(|tags| serde_json::to_string(&normalize_tags(tags.clone())).unwrap_or_default());
|
||||
|
||||
let profile_id = build_prefixed_uuid_id(MATCH3D_PROFILE_ID_PREFIX);
|
||||
let generated_work_metadata = generate_match3d_work_metadata(state, &config).await;
|
||||
let generated_item_assets = generate_match3d_item_assets(
|
||||
state,
|
||||
request_context,
|
||||
@@ -1008,6 +1058,13 @@ async fn compile_match3d_draft_for_session(
|
||||
&config,
|
||||
)
|
||||
.await?;
|
||||
let resolved_game_name =
|
||||
normalize_optional_match3d_text(game_name).unwrap_or(generated_work_metadata.game_name);
|
||||
let resolved_tags = tags
|
||||
.map(normalize_tags)
|
||||
.filter(|items| !items.is_empty())
|
||||
.unwrap_or(generated_work_metadata.tags);
|
||||
let tags_json = Some(serde_json::to_string(&resolved_tags).unwrap_or_default());
|
||||
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
@@ -1016,8 +1073,8 @@ async fn compile_match3d_draft_for_session(
|
||||
owner_user_id,
|
||||
profile_id,
|
||||
author_display_name: resolve_author_display_name(state, authenticated),
|
||||
game_name: game_name.or_else(|| Some(format!("{}抓大鹅", config.theme_text))),
|
||||
summary_text: summary,
|
||||
game_name: Some(resolved_game_name),
|
||||
summary_text: normalize_optional_match3d_text(summary).or_else(|| Some(String::new())),
|
||||
tags_json,
|
||||
cover_image_src,
|
||||
cover_asset_id: None,
|
||||
@@ -1535,11 +1592,11 @@ fn first_positive_integer(text: &str) -> Option<u32> {
|
||||
}
|
||||
|
||||
fn normalize_tags(tags: Vec<String>) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
let mut result: Vec<String> = Vec::new();
|
||||
for tag in tags {
|
||||
let trimmed = tag.trim();
|
||||
if !trimmed.is_empty() && !result.iter().any(|value| value == trimmed) {
|
||||
result.push(trimmed.to_string());
|
||||
let trimmed = normalize_match3d_tag(tag.as_str());
|
||||
if !trimmed.is_empty() && !result.iter().any(|value| value == &trimmed) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
if result.len() >= 6 {
|
||||
break;
|
||||
@@ -1548,6 +1605,138 @@ fn normalize_tags(tags: Vec<String>) -> Vec<String> {
|
||||
result
|
||||
}
|
||||
|
||||
fn normalize_optional_match3d_text(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn normalize_match3d_tag(value: &str) -> String {
|
||||
let trimmed = value.trim();
|
||||
let without_number_prefix = trimmed
|
||||
.char_indices()
|
||||
.find_map(|(index, ch)| {
|
||||
if index == 0 || !matches!(ch, '.' | '、' | ')' | ')') {
|
||||
return None;
|
||||
}
|
||||
let prefix = &trimmed[..index];
|
||||
if prefix.chars().all(|candidate| candidate.is_ascii_digit()) {
|
||||
Some(trimmed[index + ch.len_utf8()..].trim_start())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(trimmed);
|
||||
|
||||
without_number_prefix
|
||||
.trim_matches(|ch: char| {
|
||||
ch.is_ascii_punctuation()
|
||||
|| matches!(
|
||||
ch,
|
||||
',' | '。' | '、' | ';' | ':' | '!' | '?' | '“' | '”' | '《' | '》'
|
||||
)
|
||||
})
|
||||
.trim()
|
||||
.chars()
|
||||
.filter(|ch| !matches!(ch, '#' | '"' | '\'' | '`'))
|
||||
.collect::<String>()
|
||||
.chars()
|
||||
.take(6)
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
fn normalize_match3d_tag_candidates<S>(candidates: impl IntoIterator<Item = S>) -> Vec<String>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut tags = Vec::new();
|
||||
for candidate in candidates {
|
||||
let normalized = normalize_match3d_tag(candidate.as_ref());
|
||||
if normalized.is_empty() || tags.iter().any(|tag| tag == &normalized) {
|
||||
continue;
|
||||
}
|
||||
tags.push(normalized);
|
||||
if tags.len() >= 6 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for fallback in ["抓大鹅", "经典消除", "3D素材", "轻量休闲", "收集", "挑战"] {
|
||||
if tags.len() >= 6 {
|
||||
break;
|
||||
}
|
||||
if !tags.iter().any(|tag| tag == fallback) {
|
||||
tags.push(fallback.to_string());
|
||||
}
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
async fn generate_match3d_work_tags_for_profile(
|
||||
state: &AppState,
|
||||
game_name: &str,
|
||||
theme_text: &str,
|
||||
) -> Vec<String> {
|
||||
let Some(llm_client) = state
|
||||
.creative_agent_gpt5_client()
|
||||
.or_else(|| state.llm_client())
|
||||
else {
|
||||
return fallback_match3d_work_tags(game_name, theme_text);
|
||||
};
|
||||
let user_prompt = format!(
|
||||
"题材设定:{}\n作品名称:{}\n请生成 3 到 6 个适合抓大鹅作品发布的中文短标签。要求:只返回 JSON 数组,每项 2 到 6 个汉字,不要解释。",
|
||||
theme_text, game_name
|
||||
);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
LlmMessage::system("你是抓大鹅作品标签编辑,只返回 JSON 字符串数组。"),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(MATCH3D_WORK_METADATA_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let tags = parse_match3d_tags_from_text(response.content.as_str());
|
||||
if tags.len() >= MATCH3D_MIN_GENERATED_TAG_COUNT {
|
||||
return tags;
|
||||
}
|
||||
fallback_match3d_work_tags(game_name, theme_text)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
provider = MATCH3D_WORKS_PROVIDER,
|
||||
game_name,
|
||||
error = %error,
|
||||
"抓大鹅 AI 标签生成失败,降级使用本地标签"
|
||||
);
|
||||
fallback_match3d_work_tags(game_name, theme_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MATCH3D_MIN_GENERATED_TAG_COUNT: usize = 3;
|
||||
|
||||
fn parse_match3d_tags_from_text(raw: &str) -> Vec<String> {
|
||||
let raw = raw.trim();
|
||||
let json_text = if let Some(start) = raw.find('[')
|
||||
&& let Some(end) = raw.rfind(']')
|
||||
&& end > start
|
||||
{
|
||||
&raw[start..=end]
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let parsed = serde_json::from_str::<Vec<String>>(json_text).unwrap_or_default();
|
||||
normalize_match3d_tag_candidates(parsed)
|
||||
}
|
||||
|
||||
fn fallback_match3d_work_tags(game_name: &str, theme_text: &str) -> Vec<String> {
|
||||
normalize_match3d_tag_candidates([theme_text, game_name, "抓大鹅", "经典消除", "3D素材"])
|
||||
}
|
||||
|
||||
fn serialize_match3d_generated_item_assets(assets: &[Match3DGeneratedItemAsset]) -> Option<String> {
|
||||
if assets.is_empty() {
|
||||
return None;
|
||||
@@ -1634,7 +1823,7 @@ async fn generate_match3d_item_assets(
|
||||
let item_images = slice_match3d_material_sheet(&material_sheet.image, &item_names)
|
||||
.map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error))?;
|
||||
|
||||
let mut item_assets = Vec::with_capacity(item_images.len());
|
||||
let mut model_seeds = Vec::with_capacity(item_images.len());
|
||||
for (index, item_image) in item_images.into_iter().enumerate() {
|
||||
let item_name = item_names
|
||||
.get(index)
|
||||
@@ -1661,36 +1850,61 @@ async fn generate_match3d_item_assets(
|
||||
)
|
||||
.await
|
||||
.map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error))?;
|
||||
let model_asset = generate_match3d_rodin_model_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
&item_slug,
|
||||
&item_name,
|
||||
config,
|
||||
image_bytes,
|
||||
generated_at_micros.saturating_add(100 + index as i64),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error))?;
|
||||
item_assets.push(Match3DGeneratedItemAsset {
|
||||
model_seeds.push(Match3DGeneratedItemModelSeed {
|
||||
item_id,
|
||||
item_name,
|
||||
image_src: Some(image_upload.src),
|
||||
image_object_key: Some(image_upload.object_key),
|
||||
model_src: Some(model_asset.upload.src),
|
||||
model_object_key: Some(model_asset.upload.object_key),
|
||||
model_file_name: Some(model_asset.model_file_name),
|
||||
task_uuid: Some(model_asset.task_uuid),
|
||||
subscription_key: Some(model_asset.subscription_key),
|
||||
status: "model_ready".to_string(),
|
||||
error: None,
|
||||
item_slug,
|
||||
image_upload,
|
||||
image_bytes,
|
||||
generated_at_micros: generated_at_micros.saturating_add(100 + index as i64),
|
||||
});
|
||||
}
|
||||
|
||||
// 中文注释:Rodin 单个模型耗时不可控,必须在图片切割和入库后并行提交所有图生模型,
|
||||
// 避免多个物品的排队和轮询时间串行叠加导致 action 超时。
|
||||
let model_results = try_join_all(model_seeds.into_iter().map(|seed| {
|
||||
async move {
|
||||
let Match3DGeneratedItemModelSeed {
|
||||
item_id,
|
||||
item_name,
|
||||
item_slug,
|
||||
image_upload,
|
||||
image_bytes,
|
||||
generated_at_micros,
|
||||
} = seed;
|
||||
let model_asset = generate_match3d_rodin_model_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
item_slug.as_str(),
|
||||
item_name.as_str(),
|
||||
config,
|
||||
image_bytes,
|
||||
generated_at_micros,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok::<_, AppError>(Match3DGeneratedItemAsset {
|
||||
item_id,
|
||||
item_name,
|
||||
image_src: Some(image_upload.src),
|
||||
image_object_key: Some(image_upload.object_key),
|
||||
model_src: Some(model_asset.upload.src),
|
||||
model_object_key: Some(model_asset.upload.object_key),
|
||||
model_file_name: Some(model_asset.model_file_name),
|
||||
task_uuid: Some(model_asset.task_uuid),
|
||||
subscription_key: Some(model_asset.subscription_key),
|
||||
status: "model_ready".to_string(),
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}))
|
||||
.await
|
||||
.map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error))?;
|
||||
|
||||
// 中文注释:草稿阶段必须同时产出 GLB 模型,结果页直接加载模型预览。
|
||||
Ok(item_assets)
|
||||
Ok(model_results)
|
||||
}
|
||||
|
||||
struct Match3DMaterialSheet {
|
||||
@@ -1702,6 +1916,93 @@ struct Match3DSlicedItemImage {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn generate_match3d_work_metadata(
|
||||
state: &AppState,
|
||||
config: &Match3DConfigJson,
|
||||
) -> Match3DGeneratedWorkMetadata {
|
||||
let Some(llm_client) = state
|
||||
.creative_agent_gpt5_client()
|
||||
.or_else(|| state.llm_client())
|
||||
else {
|
||||
return fallback_match3d_work_metadata(config.theme_text.as_str());
|
||||
};
|
||||
let system_prompt = "你是抓大鹅游戏的作品命名编辑,只返回 JSON。";
|
||||
let user_prompt = format!(
|
||||
"题材设定:{}\n请生成抓大鹅游戏作品元信息。要求:只返回 JSON 对象,字段为 gameName、tags。gameName 为 4 到 12 个中文字符,不要包含“作品”“游戏”;tags 为 3 到 6 个中文短标签,每个 2 到 6 个汉字。不要生成描述。",
|
||||
config.theme_text
|
||||
);
|
||||
let response = llm_client
|
||||
.request_text(
|
||||
LlmTextRequest::new(vec![
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_model(MATCH3D_WORK_METADATA_LLM_MODEL)
|
||||
.with_responses_api(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => parse_match3d_work_metadata(response.content.as_str())
|
||||
.unwrap_or_else(|| fallback_match3d_work_metadata(config.theme_text.as_str())),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
provider = MATCH3D_AGENT_PROVIDER,
|
||||
theme_text = config.theme_text.as_str(),
|
||||
error = %error,
|
||||
"抓大鹅作品名称生成失败,降级使用本地元信息"
|
||||
);
|
||||
fallback_match3d_work_metadata(config.theme_text.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_match3d_work_metadata(raw: &str) -> Option<Match3DGeneratedWorkMetadata> {
|
||||
let raw = raw.trim();
|
||||
let json_text = if let Some(start) = raw.find('{')
|
||||
&& let Some(end) = raw.rfind('}')
|
||||
&& end > start
|
||||
{
|
||||
&raw[start..=end]
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let value = serde_json::from_str::<Value>(json_text).ok()?;
|
||||
let game_name = normalize_match3d_game_name(value.get("gameName")?.as_str()?);
|
||||
if game_name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let tags = value
|
||||
.get("tags")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| normalize_match3d_tag_candidates(items.iter().filter_map(Value::as_str)))
|
||||
.unwrap_or_default();
|
||||
Some(Match3DGeneratedWorkMetadata {
|
||||
game_name,
|
||||
tags: normalize_match3d_tag_candidates(tags),
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_match3d_game_name(raw: &str) -> String {
|
||||
raw.trim()
|
||||
.trim_matches(['"', '\'', '“', '”', '。', ',', ',', '、'])
|
||||
.chars()
|
||||
.filter(|character| !character.is_control())
|
||||
.take(16)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn fallback_match3d_work_metadata(theme_text: &str) -> Match3DGeneratedWorkMetadata {
|
||||
let theme = theme_text.trim();
|
||||
let normalized_theme = if theme.is_empty() { "主题" } else { theme };
|
||||
Match3DGeneratedWorkMetadata {
|
||||
game_name: format!("{normalized_theme}抓大鹅"),
|
||||
tags: normalize_match3d_tag_candidates([normalized_theme, "抓大鹅", "经典消除", "3D素材"]),
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_match3d_item_names(
|
||||
state: &AppState,
|
||||
config: &Match3DConfigJson,
|
||||
@@ -1844,25 +2145,12 @@ async fn generate_match3d_rodin_model_asset(
|
||||
)
|
||||
.await?;
|
||||
|
||||
wait_for_match3d_rodin_model(
|
||||
state,
|
||||
submit_response.subscription_key.as_str(),
|
||||
item_name,
|
||||
)
|
||||
.await?;
|
||||
let download_response = query_downloads(
|
||||
state,
|
||||
hyper3d_contract::Hyper3dDownloadRequest {
|
||||
task_uuid: submit_response.task_uuid.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let model_file = select_match3d_glb_download(
|
||||
&download_response.files,
|
||||
submit_response.task_uuid.as_str(),
|
||||
item_name,
|
||||
)?;
|
||||
let downloaded_model = download_match3d_rodin_model(model_file).await?;
|
||||
wait_for_match3d_rodin_model(state, submit_response.subscription_key.as_str(), item_name)
|
||||
.await?;
|
||||
let model_file =
|
||||
wait_for_match3d_rodin_download_file(state, submit_response.task_uuid.as_str(), item_name)
|
||||
.await?;
|
||||
let downloaded_model = download_match3d_rodin_model(&model_file).await?;
|
||||
let uploaded_model = persist_match3d_generated_bytes(
|
||||
state,
|
||||
owner_user_id,
|
||||
@@ -1940,10 +2228,7 @@ async fn wait_for_match3d_rodin_model(
|
||||
}
|
||||
|
||||
if attempt + 1 < MATCH3D_RODIN_STATUS_MAX_ATTEMPTS {
|
||||
tokio::time::sleep(Duration::from_millis(
|
||||
MATCH3D_RODIN_STATUS_POLL_INTERVAL_MS,
|
||||
))
|
||||
.await;
|
||||
tokio::time::sleep(Duration::from_millis(MATCH3D_RODIN_STATUS_POLL_INTERVAL_MS)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1952,25 +2237,77 @@ async fn wait_for_match3d_rodin_model(
|
||||
)))
|
||||
}
|
||||
|
||||
fn select_match3d_glb_download<'a>(
|
||||
files: &'a [hyper3d_contract::Hyper3dDownloadFilePayload],
|
||||
async fn wait_for_match3d_rodin_download_file(
|
||||
state: &AppState,
|
||||
task_uuid: &str,
|
||||
item_name: &str,
|
||||
) -> Result<&'a hyper3d_contract::Hyper3dDownloadFilePayload, AppError> {
|
||||
) -> Result<hyper3d_contract::Hyper3dDownloadFilePayload, AppError> {
|
||||
for attempt in 0..MATCH3D_RODIN_DOWNLOAD_MAX_ATTEMPTS {
|
||||
let download_response = query_downloads(
|
||||
state,
|
||||
hyper3d_contract::Hyper3dDownloadRequest {
|
||||
task_uuid: task_uuid.to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if let Some(model_file) = find_match3d_glb_download(&download_response.files) {
|
||||
return Ok(model_file.clone());
|
||||
}
|
||||
|
||||
// 中文注释:Rodin 状态 Done 后下载列表偶尔会延迟发布,短轮询避免把已完成任务误判失败。
|
||||
if attempt + 1 < MATCH3D_RODIN_DOWNLOAD_MAX_ATTEMPTS {
|
||||
tokio::time::sleep(Duration::from_millis(
|
||||
MATCH3D_RODIN_DOWNLOAD_POLL_INTERVAL_MS,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Err(match3d_bad_gateway(format!(
|
||||
"{item_name} 3D 模型已完成但未返回可下载模型文件:{task_uuid}"
|
||||
)))
|
||||
}
|
||||
|
||||
fn find_match3d_glb_download(
|
||||
files: &[hyper3d_contract::Hyper3dDownloadFilePayload],
|
||||
) -> Option<&hyper3d_contract::Hyper3dDownloadFilePayload> {
|
||||
files
|
||||
.iter()
|
||||
.find(|file| {
|
||||
file.name.to_ascii_lowercase().ends_with(".glb")
|
||||
|| file.url.to_ascii_lowercase().split('?').next().unwrap_or("").ends_with(".glb")
|
||||
})
|
||||
.or_else(|| files.first())
|
||||
.ok_or_else(|| {
|
||||
match3d_bad_gateway(format!(
|
||||
"{item_name} 3D 模型已完成但未返回可下载模型文件:{task_uuid}"
|
||||
))
|
||||
.find(|file| match3d_download_file_has_extension(file, ".glb"))
|
||||
.or_else(|| {
|
||||
files
|
||||
.iter()
|
||||
.find(|file| !is_match3d_preview_or_image_download(file))
|
||||
})
|
||||
}
|
||||
|
||||
fn match3d_download_file_has_extension(
|
||||
file: &hyper3d_contract::Hyper3dDownloadFilePayload,
|
||||
extension: &str,
|
||||
) -> bool {
|
||||
file.name.to_ascii_lowercase().ends_with(extension)
|
||||
|| file
|
||||
.url
|
||||
.to_ascii_lowercase()
|
||||
.split('?')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.ends_with(extension)
|
||||
}
|
||||
|
||||
fn is_match3d_preview_or_image_download(
|
||||
file: &hyper3d_contract::Hyper3dDownloadFilePayload,
|
||||
) -> bool {
|
||||
let name = file.name.to_ascii_lowercase();
|
||||
let url_path = file.url.to_ascii_lowercase();
|
||||
let url_path = url_path.split('?').next().unwrap_or(url_path.as_str());
|
||||
name.contains("preview")
|
||||
|| url_path.contains("preview")
|
||||
|| [".png", ".jpg", ".jpeg", ".webp", ".gif"]
|
||||
.iter()
|
||||
.any(|extension| name.ends_with(extension) || url_path.ends_with(extension))
|
||||
}
|
||||
|
||||
async fn download_match3d_rodin_model(
|
||||
file: &hyper3d_contract::Hyper3dDownloadFilePayload,
|
||||
) -> Result<Match3DDownloadedModel, AppError> {
|
||||
@@ -2010,17 +2347,22 @@ async fn download_match3d_rodin_model(
|
||||
fn normalize_match3d_model_file_name(raw: &str) -> String {
|
||||
let trimmed = raw.trim().rsplit('/').next().unwrap_or(raw).trim();
|
||||
let without_query = trimmed.split('?').next().unwrap_or(trimmed).trim();
|
||||
let sanitized = sanitize_match3d_asset_segment(without_query, "model");
|
||||
if sanitized.to_ascii_lowercase().ends_with(".glb") {
|
||||
sanitized
|
||||
} else {
|
||||
"model.glb".to_string()
|
||||
}
|
||||
let normalized = without_query.to_ascii_lowercase();
|
||||
let stem = without_query
|
||||
.strip_suffix(".glb")
|
||||
.or_else(|| {
|
||||
normalized
|
||||
.strip_suffix(".glb")
|
||||
.map(|_| &without_query[..without_query.len().saturating_sub(4)])
|
||||
})
|
||||
.unwrap_or(without_query);
|
||||
let sanitized_stem = sanitize_match3d_asset_segment(stem, "model");
|
||||
format!("{sanitized_stem}.glb")
|
||||
}
|
||||
|
||||
fn normalize_match3d_model_content_type(raw: &str) -> String {
|
||||
let normalized = raw.split(';').next().unwrap_or(raw).trim().to_lowercase();
|
||||
if normalized == "model/gltf-binary" || normalized == "application/octet-stream" {
|
||||
if normalized == "model/gltf-binary" {
|
||||
return normalized;
|
||||
}
|
||||
"model/gltf-binary".to_string()
|
||||
@@ -2445,6 +2787,96 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_work_metadata_parses_gpt4o_json() {
|
||||
let metadata = parse_match3d_work_metadata(
|
||||
r#"{"gameName":"果园大鹅宴","tags":["水果","抓大鹅","经典消除","轻量休闲"]}"#,
|
||||
)
|
||||
.expect("metadata should parse");
|
||||
|
||||
assert_eq!(metadata.game_name, "果园大鹅宴");
|
||||
assert_eq!(
|
||||
metadata.tags,
|
||||
vec!["水果", "抓大鹅", "经典消除", "轻量休闲", "3D素材", "收集"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_work_metadata_fallback_keeps_empty_description_boundary() {
|
||||
let metadata = fallback_match3d_work_metadata("水果");
|
||||
|
||||
assert_eq!(metadata.game_name, "水果抓大鹅");
|
||||
assert!(metadata.tags.contains(&"水果".to_string()));
|
||||
assert!(metadata.tags.contains(&"抓大鹅".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_tag_normalization_only_strips_numbered_list_prefix() {
|
||||
assert_eq!(normalize_match3d_tag("3D素材"), "3D素材");
|
||||
assert_eq!(normalize_match3d_tag("1. 3D素材"), "3D素材");
|
||||
assert_eq!(normalize_match3d_tag("2、轻量休闲"), "轻量休闲");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_model_download_metadata_normalizes_to_glb() {
|
||||
assert_eq!(
|
||||
normalize_match3d_model_file_name("https://example.com/Fruit Model.GLB?token=1"),
|
||||
"fruit-model.glb"
|
||||
);
|
||||
assert_eq!(normalize_match3d_model_file_name("模型文件"), "model.glb");
|
||||
assert_eq!(
|
||||
normalize_match3d_model_content_type("application/octet-stream"),
|
||||
"model/gltf-binary"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_match3d_model_content_type("model/gltf-binary; charset=utf-8"),
|
||||
"model/gltf-binary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_model_download_prefers_glb_file() {
|
||||
let files = vec![
|
||||
hyper3d_contract::Hyper3dDownloadFilePayload {
|
||||
name: "preview.png".to_string(),
|
||||
url: "https://cdn.example/preview.png".to_string(),
|
||||
},
|
||||
hyper3d_contract::Hyper3dDownloadFilePayload {
|
||||
name: "model".to_string(),
|
||||
url: "https://cdn.example/model.glb?token=1".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let selected = find_match3d_glb_download(&files).expect("glb download should be selected");
|
||||
|
||||
assert_eq!(selected.url, "https://cdn.example/model.glb?token=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_model_download_falls_back_to_first_file() {
|
||||
let files = vec![hyper3d_contract::Hyper3dDownloadFilePayload {
|
||||
name: "model".to_string(),
|
||||
url: "https://cdn.example/download?id=1".to_string(),
|
||||
}];
|
||||
|
||||
let selected =
|
||||
find_match3d_glb_download(&files).expect("opaque download url should be accepted");
|
||||
|
||||
assert_eq!(selected.url, "https://cdn.example/download?id=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_model_download_does_not_accept_preview_image_only() {
|
||||
let files = vec![hyper3d_contract::Hyper3dDownloadFilePayload {
|
||||
name: "preview.png".to_string(),
|
||||
url: "https://cdn.example/preview.png".to_string(),
|
||||
}];
|
||||
|
||||
let result = find_match3d_glb_download(&files);
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_work_summary_maps_persisted_generated_item_assets() {
|
||||
let response = map_match3d_work_summary_response(Match3DWorkProfileRecord {
|
||||
|
||||
@@ -675,7 +675,6 @@ pub async fn admin_upsert_profile_invite_code(
|
||||
admin.session().username.clone(),
|
||||
payload.invite_code,
|
||||
metadata_json,
|
||||
payload.granted_user_tags,
|
||||
starts_at_micros,
|
||||
expires_at_micros,
|
||||
updated_at_micros as i64,
|
||||
@@ -1124,7 +1123,6 @@ fn build_profile_invite_code_admin_response(
|
||||
user_id: record.user_id,
|
||||
invite_code: record.invite_code,
|
||||
metadata,
|
||||
granted_user_tags: record.granted_user_tags,
|
||||
starts_at: record.starts_at,
|
||||
expires_at: record.expires_at,
|
||||
status: record.status.as_str().to_string(),
|
||||
|
||||
@@ -11,7 +11,6 @@ use module_auth::{
|
||||
RefreshSessionService, WechatAuthService, WechatAuthStateService,
|
||||
};
|
||||
use module_runtime::RuntimeSnapshotRecord;
|
||||
use shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||
#[cfg(test)]
|
||||
use module_runtime::{SAVE_SNAPSHOT_VERSION, format_utc_micros};
|
||||
use platform_agent::MockLangChainRustAgentExecutor;
|
||||
@@ -23,6 +22,7 @@ use platform_auth::{
|
||||
use platform_llm::{LlmClient, LlmConfig, LlmError, LlmProvider};
|
||||
use platform_oss::{OssClient, OssConfig, OssError};
|
||||
use serde_json::Value;
|
||||
use shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||
use shared_contracts::creative_agent::CreativeAgentSessionSnapshot;
|
||||
use spacetime_client::{SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError};
|
||||
use time::OffsetDateTime;
|
||||
@@ -274,7 +274,7 @@ impl AppState {
|
||||
.creation_types
|
||||
.iter()
|
||||
.find(|item| item.id == creation_type_id)
|
||||
.map(|item| item.visible && item.open)
|
||||
.map(|item| item.open)
|
||||
.unwrap_or(true))
|
||||
}
|
||||
|
||||
@@ -291,7 +291,6 @@ impl AppState {
|
||||
.iter_mut()
|
||||
.find(|item| item.id == creation_type_id)
|
||||
{
|
||||
item.visible = enabled;
|
||||
item.open = enabled;
|
||||
} else {
|
||||
config.creation_types.push(
|
||||
|
||||
@@ -751,7 +751,6 @@ pub fn build_runtime_profile_invite_code_record(
|
||||
user_id: snapshot.user_id,
|
||||
invite_code: snapshot.invite_code,
|
||||
metadata_json: snapshot.metadata_json,
|
||||
granted_user_tags: snapshot.granted_user_tags,
|
||||
starts_at: snapshot.starts_at_micros.map(format_utc_micros),
|
||||
starts_at_micros: snapshot.starts_at_micros,
|
||||
expires_at: snapshot.expires_at_micros.map(format_utc_micros),
|
||||
|
||||
@@ -428,7 +428,6 @@ pub fn build_runtime_profile_invite_code_admin_upsert_input(
|
||||
admin_user_id: String,
|
||||
invite_code: String,
|
||||
metadata_json: String,
|
||||
granted_user_tags: Vec<String>,
|
||||
starts_at_micros: Option<i64>,
|
||||
expires_at_micros: Option<i64>,
|
||||
updated_at_micros: i64,
|
||||
@@ -437,7 +436,6 @@ pub fn build_runtime_profile_invite_code_admin_upsert_input(
|
||||
let invite_code =
|
||||
normalize_invite_code(invite_code).ok_or(RuntimeProfileFieldError::MissingInviteCode)?;
|
||||
let metadata_json = normalize_invite_code_metadata_json(metadata_json)?;
|
||||
let granted_user_tags = normalize_profile_user_tags(granted_user_tags)?;
|
||||
crate::commands::validate_runtime_profile_invite_code_validity_window(
|
||||
starts_at_micros,
|
||||
expires_at_micros,
|
||||
@@ -447,7 +445,6 @@ pub fn build_runtime_profile_invite_code_admin_upsert_input(
|
||||
admin_user_id,
|
||||
invite_code,
|
||||
metadata_json,
|
||||
granted_user_tags,
|
||||
starts_at_micros,
|
||||
expires_at_micros,
|
||||
updated_at_micros,
|
||||
@@ -767,13 +764,54 @@ pub fn normalize_invite_code_metadata_json(
|
||||
return Err(RuntimeProfileFieldError::InvalidInviteCodeMetadata);
|
||||
}
|
||||
|
||||
let parsed = serde_json::from_str::<Value>(trimmed)
|
||||
let mut parsed = serde_json::from_str::<Value>(trimmed)
|
||||
.map_err(|_| RuntimeProfileFieldError::InvalidInviteCodeMetadata)?;
|
||||
if !parsed.is_object() {
|
||||
return Err(RuntimeProfileFieldError::InvalidInviteCodeMetadata);
|
||||
}
|
||||
|
||||
serde_json::to_string(&parsed).map_err(|_| RuntimeProfileFieldError::InvalidInviteCodeMetadata)
|
||||
normalize_invite_code_metadata_user_tags(&mut parsed)?;
|
||||
let normalized = serde_json::to_string(&parsed)
|
||||
.map_err(|_| RuntimeProfileFieldError::InvalidInviteCodeMetadata)?;
|
||||
if normalized.len() > PROFILE_INVITE_CODE_METADATA_MAX_BYTES {
|
||||
return Err(RuntimeProfileFieldError::InvalidInviteCodeMetadata);
|
||||
}
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn normalize_invite_code_metadata_user_tags(
|
||||
metadata: &mut Value,
|
||||
) -> Result<(), RuntimeProfileFieldError> {
|
||||
let Some(object) = metadata.as_object_mut() else {
|
||||
return Err(RuntimeProfileFieldError::InvalidInviteCodeMetadata);
|
||||
};
|
||||
// 中文注释:邀请码授予标签复用 metadata,保存时统一收敛成 camelCase 字段。
|
||||
let raw = object
|
||||
.remove("userTags")
|
||||
.or_else(|| object.remove("user_tags"));
|
||||
object.remove("user_tags");
|
||||
|
||||
let Some(raw) = raw else {
|
||||
return Ok(());
|
||||
};
|
||||
let Value::Array(items) = raw else {
|
||||
return Err(RuntimeProfileFieldError::InvalidUserTag);
|
||||
};
|
||||
let mut raw_tags = Vec::new();
|
||||
for item in items {
|
||||
let Value::String(value) = item else {
|
||||
return Err(RuntimeProfileFieldError::InvalidUserTag);
|
||||
};
|
||||
raw_tags.push(value);
|
||||
}
|
||||
let tags = normalize_profile_user_tags(raw_tags)?;
|
||||
if !tags.is_empty() {
|
||||
object.insert(
|
||||
"userTags".to_string(),
|
||||
Value::Array(tags.into_iter().map(Value::String).collect()),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalize_profile_user_tags(
|
||||
@@ -801,7 +839,7 @@ pub fn validate_runtime_profile_invite_code_validity_window(
|
||||
starts_at_micros: Option<i64>,
|
||||
expires_at_micros: Option<i64>,
|
||||
) -> Result<(), RuntimeProfileFieldError> {
|
||||
if matches!((starts_at_micros, expires_at_micros), (Some(starts_at), Some(expires_at)) if starts_at > expires_at)
|
||||
if matches!((starts_at_micros, expires_at_micros), (Some(starts_at), Some(expires_at)) if starts_at >= expires_at)
|
||||
{
|
||||
return Err(RuntimeProfileFieldError::InvalidInviteCodeValidityWindow);
|
||||
}
|
||||
|
||||
@@ -1190,7 +1190,6 @@ pub struct RuntimeProfileInviteCodeAdminUpsertInput {
|
||||
pub admin_user_id: String,
|
||||
pub invite_code: String,
|
||||
pub metadata_json: String,
|
||||
pub granted_user_tags: Vec<String>,
|
||||
pub starts_at_micros: Option<i64>,
|
||||
pub expires_at_micros: Option<i64>,
|
||||
pub updated_at_micros: i64,
|
||||
@@ -1208,7 +1207,6 @@ pub struct RuntimeProfileInviteCodeSnapshot {
|
||||
pub user_id: String,
|
||||
pub invite_code: String,
|
||||
pub metadata_json: String,
|
||||
pub granted_user_tags: Vec<String>,
|
||||
pub starts_at_micros: Option<i64>,
|
||||
pub expires_at_micros: Option<i64>,
|
||||
pub created_at_micros: i64,
|
||||
@@ -1581,7 +1579,6 @@ pub struct RuntimeProfileInviteCodeRecord {
|
||||
pub user_id: String,
|
||||
pub invite_code: String,
|
||||
pub metadata_json: String,
|
||||
pub granted_user_tags: Vec<String>,
|
||||
pub starts_at: Option<String>,
|
||||
pub starts_at_micros: Option<i64>,
|
||||
pub expires_at: Option<String>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use module_runtime::{
|
||||
RuntimeProfileFieldError, RuntimeProfileInviteCodeSnapshot, RuntimeProfileInviteCodeStatus,
|
||||
build_runtime_profile_invite_code_record, resolve_runtime_profile_invite_code_status,
|
||||
build_runtime_profile_invite_code_record, normalize_invite_code_metadata_json,
|
||||
resolve_runtime_profile_invite_code_status,
|
||||
validate_runtime_profile_invite_code_validity_window,
|
||||
};
|
||||
|
||||
@@ -15,11 +16,14 @@ fn invite_code_validity_window_rejects_start_after_expire() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invite_code_validity_window_allows_open_ended_or_equal_boundary() {
|
||||
fn invite_code_validity_window_allows_open_ended_and_rejects_equal_boundary() {
|
||||
assert!(validate_runtime_profile_invite_code_validity_window(None, None).is_ok());
|
||||
assert!(validate_runtime_profile_invite_code_validity_window(Some(10), None).is_ok());
|
||||
assert!(validate_runtime_profile_invite_code_validity_window(None, Some(10)).is_ok());
|
||||
assert!(validate_runtime_profile_invite_code_validity_window(Some(10), Some(10)).is_ok());
|
||||
assert_eq!(
|
||||
validate_runtime_profile_invite_code_validity_window(Some(10), Some(10)),
|
||||
Err(RuntimeProfileFieldError::InvalidInviteCodeValidityWindow)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -48,7 +52,6 @@ fn invite_code_record_formats_window_and_status() {
|
||||
user_id: "user-1".to_string(),
|
||||
invite_code: "SY00000001".to_string(),
|
||||
metadata_json: "{}".to_string(),
|
||||
granted_user_tags: Vec::new(),
|
||||
starts_at_micros: Some(0),
|
||||
expires_at_micros: Some(1_000_000),
|
||||
created_at_micros: 0,
|
||||
@@ -59,3 +62,33 @@ fn invite_code_record_formats_window_and_status() {
|
||||
assert_eq!(record.expires_at.as_deref(), Some("1970-01-01T00:00:01Z"));
|
||||
assert_eq!(record.status, RuntimeProfileInviteCodeStatus::Expired);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invite_code_metadata_normalizes_user_tags() {
|
||||
let normalized = normalize_invite_code_metadata_json(
|
||||
r#"{"source":"admin","user_tags":[" 北科 ","北科",""]}"#.to_string(),
|
||||
)
|
||||
.expect("metadata should normalize");
|
||||
|
||||
assert_eq!(normalized, r#"{"source":"admin","userTags":["北科"]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invite_code_metadata_removes_empty_user_tags() {
|
||||
let normalized = normalize_invite_code_metadata_json(r#"{"userTags":[]}"#.to_string())
|
||||
.expect("empty tags should be valid");
|
||||
|
||||
assert_eq!(normalized, "{}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invite_code_metadata_rejects_invalid_user_tags_shape() {
|
||||
assert_eq!(
|
||||
normalize_invite_code_metadata_json(r#"{"userTags":"北科"}"#.to_string()),
|
||||
Err(RuntimeProfileFieldError::InvalidUserTag)
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_invite_code_metadata_json(r#"{"userTags":["北科",1]}"#.to_string()),
|
||||
Err(RuntimeProfileFieldError::InvalidUserTag)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,5 @@ version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
platform-oss = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -11,7 +11,6 @@ pub struct AdminLoginRequest {
|
||||
|
||||
// 登录成功后返回管理员访问令牌与基础会话信息。
|
||||
|
||||
|
||||
/// 后台创作入口开关列表响应。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -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,
|
||||
@@ -614,57 +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,
|
||||
signature_version: value.signature_version,
|
||||
credential: value.credential,
|
||||
date: value.date,
|
||||
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::*;
|
||||
@@ -708,8 +661,8 @@ 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: "v4",
|
||||
upload: DirectUploadTicketPayload {
|
||||
signature_version: "v4".to_string(),
|
||||
provider: "aliyun-oss",
|
||||
bucket: "genarrative-assets".to_string(),
|
||||
endpoint: "oss-cn-shanghai.aliyuncs.com".to_string(),
|
||||
@@ -717,12 +670,12 @@ mod tests {
|
||||
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(),
|
||||
signature_version: "OSS4-HMAC-SHA256".to_string(),
|
||||
@@ -736,7 +689,7 @@ mod tests {
|
||||
"character_visual".to_string(),
|
||||
)]),
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ pub mod auth;
|
||||
pub mod big_fish;
|
||||
pub mod big_fish_works;
|
||||
pub mod creation_agent_document_input;
|
||||
pub mod creative_agent;
|
||||
pub mod creation_entry_config;
|
||||
pub mod creative_agent;
|
||||
pub mod hyper3d;
|
||||
pub mod llm;
|
||||
pub mod match3d_agent;
|
||||
|
||||
@@ -486,8 +486,6 @@ pub struct AdminUpsertProfileInviteCodeRequest {
|
||||
#[serde(default)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub granted_user_tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub starts_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<String>,
|
||||
@@ -526,7 +524,6 @@ pub struct ProfileInviteCodeAdminResponse {
|
||||
pub user_id: String,
|
||||
pub invite_code: String,
|
||||
pub metadata: serde_json::Value,
|
||||
pub granted_user_tags: Vec<String>,
|
||||
pub starts_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
pub status: String,
|
||||
|
||||
@@ -46,9 +46,7 @@ impl From<module_assets::AssetHistoryListInput> for AssetHistoryListInput {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::CreationEntryTypeAdminUpsertInput>
|
||||
for CreationEntryTypeAdminUpsertInput
|
||||
{
|
||||
impl From<module_runtime::CreationEntryTypeAdminUpsertInput> for CreationEntryTypeAdminUpsertInput {
|
||||
fn from(input: module_runtime::CreationEntryTypeAdminUpsertInput) -> Self {
|
||||
Self {
|
||||
id: input.id,
|
||||
@@ -337,7 +335,6 @@ impl From<module_runtime::RuntimeProfileInviteCodeAdminUpsertInput>
|
||||
admin_user_id: input.admin_user_id,
|
||||
invite_code: input.invite_code,
|
||||
metadata_json: input.metadata_json,
|
||||
granted_user_tags: input.granted_user_tags,
|
||||
starts_at_micros: input.starts_at_micros,
|
||||
expires_at_micros: input.expires_at_micros,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
@@ -723,7 +720,8 @@ pub(crate) fn map_asset_history_list_result(
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub type CreationEntryConfigRecord = shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||
pub type CreationEntryConfigRecord =
|
||||
shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||
|
||||
pub(crate) fn map_creation_entry_config_procedure_result(
|
||||
result: CreationEntryConfigProcedureResult,
|
||||
@@ -2385,7 +2383,6 @@ pub(crate) fn map_runtime_profile_invite_code_snapshot(
|
||||
user_id: snapshot.user_id,
|
||||
invite_code: snapshot.invite_code,
|
||||
metadata_json: snapshot.metadata_json,
|
||||
granted_user_tags: snapshot.granted_user_tags,
|
||||
starts_at_micros: snapshot.starts_at_micros,
|
||||
expires_at_micros: snapshot.expires_at_micros,
|
||||
created_at_micros: snapshot.created_at_micros,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::creation_entry_start_card_snapshot_type::CreationEntryStartCardSnapshot;
|
||||
use super::creation_entry_type_config_snapshot_type::CreationEntryTypeSnapshot;
|
||||
use super::creation_entry_type_modal_snapshot_type::CreationEntryTypeModalSnapshot;
|
||||
use super::creation_entry_type_snapshot_type::CreationEntryTypeSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use super::creation_entry_config_type::CreationEntryConfig;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `creation_entry_config`.
|
||||
///
|
||||
/// Obtain a handle from the [`CreationEntryConfigTableAccess::creation_entry_config`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.creation_entry_config()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.creation_entry_config().on_insert(...)`.
|
||||
pub struct CreationEntryConfigTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<CreationEntryConfig>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `creation_entry_config`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait CreationEntryConfigTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`CreationEntryConfigTableHandle`], which mediates access to the table `creation_entry_config`.
|
||||
fn creation_entry_config(&self) -> CreationEntryConfigTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl CreationEntryConfigTableAccess for super::RemoteTables {
|
||||
fn creation_entry_config(&self) -> CreationEntryConfigTableHandle<'_> {
|
||||
CreationEntryConfigTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<CreationEntryConfig>("creation_entry_config"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreationEntryConfigInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct CreationEntryConfigDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::Table for CreationEntryConfigTableHandle<'ctx> {
|
||||
type Row = CreationEntryConfig;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = CreationEntryConfig> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = CreationEntryConfigInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> CreationEntryConfigInsertCallbackId {
|
||||
CreationEntryConfigInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: CreationEntryConfigInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = CreationEntryConfigDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> CreationEntryConfigDeleteCallbackId {
|
||||
CreationEntryConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: CreationEntryConfigDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreationEntryConfigUpdateCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableWithPrimaryKey for CreationEntryConfigTableHandle<'ctx> {
|
||||
type UpdateCallbackId = CreationEntryConfigUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> CreationEntryConfigUpdateCallbackId {
|
||||
CreationEntryConfigUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: CreationEntryConfigUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the `config_id` unique index on the table `creation_entry_config`,
|
||||
/// which allows point queries on the field of the same name
|
||||
/// via the [`CreationEntryConfigConfigIdUnique::find`] method.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.creation_entry_config().config_id().find(...)`.
|
||||
pub struct CreationEntryConfigConfigIdUnique<'ctx> {
|
||||
imp: __sdk::UniqueConstraintHandle<CreationEntryConfig, String>,
|
||||
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
impl<'ctx> CreationEntryConfigTableHandle<'ctx> {
|
||||
/// Get a handle on the `config_id` unique index on the table `creation_entry_config`.
|
||||
pub fn config_id(&self) -> CreationEntryConfigConfigIdUnique<'ctx> {
|
||||
CreationEntryConfigConfigIdUnique {
|
||||
imp: self.imp.get_unique_constraint::<String>("config_id"),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> CreationEntryConfigConfigIdUnique<'ctx> {
|
||||
/// Find the subscribed row whose `config_id` column value is equal to `col_val`,
|
||||
/// if such a row is present in the client cache.
|
||||
pub fn find(&self, col_val: &String) -> Option<CreationEntryConfig> {
|
||||
self.imp.find(col_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table = client_cache.get_or_make_table::<CreationEntryConfig>("creation_entry_config");
|
||||
_table.add_unique_constraint::<String>("config_id", |row| &row.config_id);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<CreationEntryConfig>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse("TableUpdate<CreationEntryConfig>", "TableUpdate")
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `CreationEntryConfig`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait creation_entry_configQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `CreationEntryConfig`.
|
||||
fn creation_entry_config(&self) -> __sdk::__query_builder::Table<CreationEntryConfig>;
|
||||
}
|
||||
|
||||
impl creation_entry_configQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn creation_entry_config(&self) -> __sdk::__query_builder::Table<CreationEntryConfig> {
|
||||
__sdk::__query_builder::Table::new("creation_entry_config")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct CreationEntryConfig {
|
||||
pub config_id: String,
|
||||
pub start_title: String,
|
||||
pub start_description: String,
|
||||
pub start_idle_badge: String,
|
||||
pub start_busy_badge: String,
|
||||
pub modal_title: String,
|
||||
pub modal_description: String,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for CreationEntryConfig {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `CreationEntryConfig`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct CreationEntryConfigCols {
|
||||
pub config_id: __sdk::__query_builder::Col<CreationEntryConfig, String>,
|
||||
pub start_title: __sdk::__query_builder::Col<CreationEntryConfig, String>,
|
||||
pub start_description: __sdk::__query_builder::Col<CreationEntryConfig, String>,
|
||||
pub start_idle_badge: __sdk::__query_builder::Col<CreationEntryConfig, String>,
|
||||
pub start_busy_badge: __sdk::__query_builder::Col<CreationEntryConfig, String>,
|
||||
pub modal_title: __sdk::__query_builder::Col<CreationEntryConfig, String>,
|
||||
pub modal_description: __sdk::__query_builder::Col<CreationEntryConfig, String>,
|
||||
pub updated_at: __sdk::__query_builder::Col<CreationEntryConfig, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for CreationEntryConfig {
|
||||
type Cols = CreationEntryConfigCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
CreationEntryConfigCols {
|
||||
config_id: __sdk::__query_builder::Col::new(table_name, "config_id"),
|
||||
start_title: __sdk::__query_builder::Col::new(table_name, "start_title"),
|
||||
start_description: __sdk::__query_builder::Col::new(table_name, "start_description"),
|
||||
start_idle_badge: __sdk::__query_builder::Col::new(table_name, "start_idle_badge"),
|
||||
start_busy_badge: __sdk::__query_builder::Col::new(table_name, "start_busy_badge"),
|
||||
modal_title: __sdk::__query_builder::Col::new(table_name, "modal_title"),
|
||||
modal_description: __sdk::__query_builder::Col::new(table_name, "modal_description"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `CreationEntryConfig`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct CreationEntryConfigIxCols {
|
||||
pub config_id: __sdk::__query_builder::IxCol<CreationEntryConfig, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for CreationEntryConfig {
|
||||
type IxCols = CreationEntryConfigIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
CreationEntryConfigIxCols {
|
||||
config_id: __sdk::__query_builder::IxCol::new(table_name, "config_id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for CreationEntryConfig {}
|
||||
@@ -0,0 +1,162 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use super::creation_entry_type_config_type::CreationEntryTypeConfig;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `creation_entry_type_config`.
|
||||
///
|
||||
/// Obtain a handle from the [`CreationEntryTypeConfigTableAccess::creation_entry_type_config`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.creation_entry_type_config()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.creation_entry_type_config().on_insert(...)`.
|
||||
pub struct CreationEntryTypeConfigTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<CreationEntryTypeConfig>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `creation_entry_type_config`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait CreationEntryTypeConfigTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`CreationEntryTypeConfigTableHandle`], which mediates access to the table `creation_entry_type_config`.
|
||||
fn creation_entry_type_config(&self) -> CreationEntryTypeConfigTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl CreationEntryTypeConfigTableAccess for super::RemoteTables {
|
||||
fn creation_entry_type_config(&self) -> CreationEntryTypeConfigTableHandle<'_> {
|
||||
CreationEntryTypeConfigTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<CreationEntryTypeConfig>("creation_entry_type_config"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreationEntryTypeConfigInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct CreationEntryTypeConfigDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::Table for CreationEntryTypeConfigTableHandle<'ctx> {
|
||||
type Row = CreationEntryTypeConfig;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = CreationEntryTypeConfig> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = CreationEntryTypeConfigInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> CreationEntryTypeConfigInsertCallbackId {
|
||||
CreationEntryTypeConfigInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: CreationEntryTypeConfigInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = CreationEntryTypeConfigDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> CreationEntryTypeConfigDeleteCallbackId {
|
||||
CreationEntryTypeConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: CreationEntryTypeConfigDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreationEntryTypeConfigUpdateCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableWithPrimaryKey for CreationEntryTypeConfigTableHandle<'ctx> {
|
||||
type UpdateCallbackId = CreationEntryTypeConfigUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> CreationEntryTypeConfigUpdateCallbackId {
|
||||
CreationEntryTypeConfigUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: CreationEntryTypeConfigUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the `id` unique index on the table `creation_entry_type_config`,
|
||||
/// which allows point queries on the field of the same name
|
||||
/// via the [`CreationEntryTypeConfigIdUnique::find`] method.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.creation_entry_type_config().id().find(...)`.
|
||||
pub struct CreationEntryTypeConfigIdUnique<'ctx> {
|
||||
imp: __sdk::UniqueConstraintHandle<CreationEntryTypeConfig, String>,
|
||||
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
impl<'ctx> CreationEntryTypeConfigTableHandle<'ctx> {
|
||||
/// Get a handle on the `id` unique index on the table `creation_entry_type_config`.
|
||||
pub fn id(&self) -> CreationEntryTypeConfigIdUnique<'ctx> {
|
||||
CreationEntryTypeConfigIdUnique {
|
||||
imp: self.imp.get_unique_constraint::<String>("id"),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> CreationEntryTypeConfigIdUnique<'ctx> {
|
||||
/// Find the subscribed row whose `id` column value is equal to `col_val`,
|
||||
/// if such a row is present in the client cache.
|
||||
pub fn find(&self, col_val: &String) -> Option<CreationEntryTypeConfig> {
|
||||
self.imp.find(col_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table =
|
||||
client_cache.get_or_make_table::<CreationEntryTypeConfig>("creation_entry_type_config");
|
||||
_table.add_unique_constraint::<String>("id", |row| &row.id);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<CreationEntryTypeConfig>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse("TableUpdate<CreationEntryTypeConfig>", "TableUpdate")
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `CreationEntryTypeConfig`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait creation_entry_type_configQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `CreationEntryTypeConfig`.
|
||||
fn creation_entry_type_config(&self) -> __sdk::__query_builder::Table<CreationEntryTypeConfig>;
|
||||
}
|
||||
|
||||
impl creation_entry_type_configQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn creation_entry_type_config(&self) -> __sdk::__query_builder::Table<CreationEntryTypeConfig> {
|
||||
__sdk::__query_builder::Table::new("creation_entry_type_config")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct CreationEntryTypeConfig {
|
||||
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: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for CreationEntryTypeConfig {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `CreationEntryTypeConfig`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct CreationEntryTypeConfigCols {
|
||||
pub id: __sdk::__query_builder::Col<CreationEntryTypeConfig, String>,
|
||||
pub title: __sdk::__query_builder::Col<CreationEntryTypeConfig, String>,
|
||||
pub subtitle: __sdk::__query_builder::Col<CreationEntryTypeConfig, String>,
|
||||
pub badge: __sdk::__query_builder::Col<CreationEntryTypeConfig, String>,
|
||||
pub image_src: __sdk::__query_builder::Col<CreationEntryTypeConfig, String>,
|
||||
pub visible: __sdk::__query_builder::Col<CreationEntryTypeConfig, bool>,
|
||||
pub open: __sdk::__query_builder::Col<CreationEntryTypeConfig, bool>,
|
||||
pub sort_order: __sdk::__query_builder::Col<CreationEntryTypeConfig, i32>,
|
||||
pub updated_at: __sdk::__query_builder::Col<CreationEntryTypeConfig, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for CreationEntryTypeConfig {
|
||||
type Cols = CreationEntryTypeConfigCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
CreationEntryTypeConfigCols {
|
||||
id: __sdk::__query_builder::Col::new(table_name, "id"),
|
||||
title: __sdk::__query_builder::Col::new(table_name, "title"),
|
||||
subtitle: __sdk::__query_builder::Col::new(table_name, "subtitle"),
|
||||
badge: __sdk::__query_builder::Col::new(table_name, "badge"),
|
||||
image_src: __sdk::__query_builder::Col::new(table_name, "image_src"),
|
||||
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||
open: __sdk::__query_builder::Col::new(table_name, "open"),
|
||||
sort_order: __sdk::__query_builder::Col::new(table_name, "sort_order"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `CreationEntryTypeConfig`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct CreationEntryTypeConfigIxCols {
|
||||
pub id: __sdk::__query_builder::IxCol<CreationEntryTypeConfig, String>,
|
||||
pub sort_order: __sdk::__query_builder::IxCol<CreationEntryTypeConfig, i32>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for CreationEntryTypeConfig {
|
||||
type IxCols = CreationEntryTypeConfigIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
CreationEntryTypeConfigIxCols {
|
||||
id: __sdk::__query_builder::IxCol::new(table_name, "id"),
|
||||
sort_order: __sdk::__query_builder::IxCol::new(table_name, "sort_order"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for CreationEntryTypeConfig {}
|
||||
@@ -25,6 +25,7 @@ pub trait get_creation_entry_config {
|
||||
|
||||
fn get_creation_entry_config_then(
|
||||
&self,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
@@ -36,6 +37,7 @@ pub trait get_creation_entry_config {
|
||||
impl get_creation_entry_config for super::RemoteProcedures {
|
||||
fn get_creation_entry_config_then(
|
||||
&self,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
|
||||
@@ -188,6 +188,16 @@ pub mod create_profile_recharge_order_and_return_procedure;
|
||||
pub mod create_puzzle_agent_session_procedure;
|
||||
pub mod create_square_hole_agent_session_procedure;
|
||||
pub mod create_visual_novel_agent_session_procedure;
|
||||
pub mod creation_entry_config_procedure_result_type;
|
||||
pub mod creation_entry_config_snapshot_type;
|
||||
pub mod creation_entry_config_table;
|
||||
pub mod creation_entry_config_type;
|
||||
pub mod creation_entry_start_card_snapshot_type;
|
||||
pub mod creation_entry_type_admin_upsert_input_type;
|
||||
pub mod creation_entry_type_config_table;
|
||||
pub mod creation_entry_type_config_type;
|
||||
pub mod creation_entry_type_modal_snapshot_type;
|
||||
pub mod creation_entry_type_snapshot_type;
|
||||
pub mod custom_world_agent_action_execute_input_type;
|
||||
pub mod custom_world_agent_action_execute_result_type;
|
||||
pub mod custom_world_agent_card_detail_get_input_type;
|
||||
@@ -249,12 +259,6 @@ pub mod custom_world_theme_mode_type;
|
||||
pub mod custom_world_work_summary_snapshot_type;
|
||||
pub mod custom_world_works_list_input_type;
|
||||
pub mod custom_world_works_list_result_type;
|
||||
pub mod creation_entry_config_procedure_result_type;
|
||||
pub mod creation_entry_config_snapshot_type;
|
||||
pub mod creation_entry_start_card_snapshot_type;
|
||||
pub mod creation_entry_type_admin_upsert_input_type;
|
||||
pub mod creation_entry_type_config_snapshot_type;
|
||||
pub mod creation_entry_type_modal_snapshot_type;
|
||||
pub mod database_migration_authorize_operator_input_type;
|
||||
pub mod database_migration_export_input_type;
|
||||
pub mod database_migration_import_chunk_input_type;
|
||||
@@ -296,11 +300,11 @@ pub mod finish_match_3_d_time_up_procedure;
|
||||
pub mod finish_square_hole_time_up_procedure;
|
||||
pub mod generate_big_fish_asset_procedure;
|
||||
pub mod get_auth_store_snapshot_procedure;
|
||||
pub mod get_creation_entry_config_procedure;
|
||||
pub mod get_battle_state_procedure;
|
||||
pub mod get_big_fish_run_procedure;
|
||||
pub mod get_big_fish_session_procedure;
|
||||
pub mod get_chapter_progression_procedure;
|
||||
pub mod get_creation_entry_config_procedure;
|
||||
pub mod get_custom_world_agent_card_detail_procedure;
|
||||
pub mod get_custom_world_agent_operation_procedure;
|
||||
pub mod get_custom_world_agent_session_procedure;
|
||||
@@ -759,8 +763,8 @@ pub mod update_square_hole_work_procedure;
|
||||
pub mod update_visual_novel_work_procedure;
|
||||
pub mod upsert_auth_store_snapshot_procedure;
|
||||
pub mod upsert_chapter_progression_and_return_procedure;
|
||||
pub mod upsert_creation_entry_type_config_procedure;
|
||||
pub mod upsert_chapter_progression_reducer;
|
||||
pub mod upsert_creation_entry_type_config_procedure;
|
||||
pub mod upsert_custom_world_agent_operation_progress_procedure;
|
||||
pub mod upsert_custom_world_profile_and_return_procedure;
|
||||
pub mod upsert_custom_world_profile_reducer;
|
||||
@@ -992,6 +996,16 @@ pub use create_profile_recharge_order_and_return_procedure::create_profile_recha
|
||||
pub use create_puzzle_agent_session_procedure::create_puzzle_agent_session;
|
||||
pub use create_square_hole_agent_session_procedure::create_square_hole_agent_session;
|
||||
pub use create_visual_novel_agent_session_procedure::create_visual_novel_agent_session;
|
||||
pub use creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
|
||||
pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
|
||||
pub use creation_entry_config_table::*;
|
||||
pub use creation_entry_config_type::CreationEntryConfig;
|
||||
pub use creation_entry_start_card_snapshot_type::CreationEntryStartCardSnapshot;
|
||||
pub use creation_entry_type_admin_upsert_input_type::CreationEntryTypeAdminUpsertInput;
|
||||
pub use creation_entry_type_config_table::*;
|
||||
pub use creation_entry_type_config_type::CreationEntryTypeConfig;
|
||||
pub use creation_entry_type_modal_snapshot_type::CreationEntryTypeModalSnapshot;
|
||||
pub use creation_entry_type_snapshot_type::CreationEntryTypeSnapshot;
|
||||
pub use custom_world_agent_action_execute_input_type::CustomWorldAgentActionExecuteInput;
|
||||
pub use custom_world_agent_action_execute_result_type::CustomWorldAgentActionExecuteResult;
|
||||
pub use custom_world_agent_card_detail_get_input_type::CustomWorldAgentCardDetailGetInput;
|
||||
@@ -1053,12 +1067,6 @@ pub use custom_world_theme_mode_type::CustomWorldThemeMode;
|
||||
pub use custom_world_work_summary_snapshot_type::CustomWorldWorkSummarySnapshot;
|
||||
pub use custom_world_works_list_input_type::CustomWorldWorksListInput;
|
||||
pub use custom_world_works_list_result_type::CustomWorldWorksListResult;
|
||||
pub use creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
|
||||
pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
|
||||
pub use creation_entry_start_card_snapshot_type::CreationEntryStartCardSnapshot;
|
||||
pub use creation_entry_type_admin_upsert_input_type::CreationEntryTypeAdminUpsertInput;
|
||||
pub use creation_entry_type_config_snapshot_type::CreationEntryTypeSnapshot;
|
||||
pub use creation_entry_type_modal_snapshot_type::CreationEntryTypeModalSnapshot;
|
||||
pub use database_migration_authorize_operator_input_type::DatabaseMigrationAuthorizeOperatorInput;
|
||||
pub use database_migration_export_input_type::DatabaseMigrationExportInput;
|
||||
pub use database_migration_import_chunk_input_type::DatabaseMigrationImportChunkInput;
|
||||
@@ -1100,11 +1108,11 @@ pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up;
|
||||
pub use finish_square_hole_time_up_procedure::finish_square_hole_time_up;
|
||||
pub use generate_big_fish_asset_procedure::generate_big_fish_asset;
|
||||
pub use get_auth_store_snapshot_procedure::get_auth_store_snapshot;
|
||||
pub use get_creation_entry_config_procedure::get_creation_entry_config;
|
||||
pub use get_battle_state_procedure::get_battle_state;
|
||||
pub use get_big_fish_run_procedure::get_big_fish_run;
|
||||
pub use get_big_fish_session_procedure::get_big_fish_session;
|
||||
pub use get_chapter_progression_procedure::get_chapter_progression;
|
||||
pub use get_creation_entry_config_procedure::get_creation_entry_config;
|
||||
pub use get_custom_world_agent_card_detail_procedure::get_custom_world_agent_card_detail;
|
||||
pub use get_custom_world_agent_operation_procedure::get_custom_world_agent_operation;
|
||||
pub use get_custom_world_agent_session_procedure::get_custom_world_agent_session;
|
||||
@@ -1563,8 +1571,8 @@ pub use update_square_hole_work_procedure::update_square_hole_work;
|
||||
pub use update_visual_novel_work_procedure::update_visual_novel_work;
|
||||
pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot;
|
||||
pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return;
|
||||
pub use upsert_creation_entry_type_config_procedure::upsert_creation_entry_type_config;
|
||||
pub use upsert_chapter_progression_reducer::upsert_chapter_progression;
|
||||
pub use upsert_creation_entry_type_config_procedure::upsert_creation_entry_type_config;
|
||||
pub use upsert_custom_world_agent_operation_progress_procedure::upsert_custom_world_agent_operation_progress;
|
||||
pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return;
|
||||
pub use upsert_custom_world_profile_reducer::upsert_custom_world_profile;
|
||||
@@ -1904,6 +1912,8 @@ pub struct DbUpdate {
|
||||
big_fish_event: __sdk::TableUpdate<BigFishEvent>,
|
||||
big_fish_runtime_run: __sdk::TableUpdate<BigFishRuntimeRun>,
|
||||
chapter_progression: __sdk::TableUpdate<ChapterProgression>,
|
||||
creation_entry_config: __sdk::TableUpdate<CreationEntryConfig>,
|
||||
creation_entry_type_config: __sdk::TableUpdate<CreationEntryTypeConfig>,
|
||||
custom_world_agent_message: __sdk::TableUpdate<CustomWorldAgentMessage>,
|
||||
custom_world_agent_operation: __sdk::TableUpdate<CustomWorldAgentOperation>,
|
||||
custom_world_agent_session: __sdk::TableUpdate<CustomWorldAgentSession>,
|
||||
@@ -2026,6 +2036,12 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
||||
"chapter_progression" => db_update
|
||||
.chapter_progression
|
||||
.append(chapter_progression_table::parse_table_update(table_update)?),
|
||||
"creation_entry_config" => db_update.creation_entry_config.append(
|
||||
creation_entry_config_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"creation_entry_type_config" => db_update.creation_entry_type_config.append(
|
||||
creation_entry_type_config_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"custom_world_agent_message" => db_update.custom_world_agent_message.append(
|
||||
custom_world_agent_message_table::parse_table_update(table_update)?,
|
||||
),
|
||||
@@ -2311,6 +2327,18 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
&self.chapter_progression,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.chapter_progression_id);
|
||||
diff.creation_entry_config = cache
|
||||
.apply_diff_to_table::<CreationEntryConfig>(
|
||||
"creation_entry_config",
|
||||
&self.creation_entry_config,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.config_id);
|
||||
diff.creation_entry_type_config = cache
|
||||
.apply_diff_to_table::<CreationEntryTypeConfig>(
|
||||
"creation_entry_type_config",
|
||||
&self.creation_entry_type_config,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.id);
|
||||
diff.custom_world_agent_message = cache
|
||||
.apply_diff_to_table::<CustomWorldAgentMessage>(
|
||||
"custom_world_agent_message",
|
||||
@@ -2683,6 +2711,12 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"chapter_progression" => db_update
|
||||
.chapter_progression
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"creation_entry_config" => db_update
|
||||
.creation_entry_config
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"creation_entry_type_config" => db_update
|
||||
.creation_entry_type_config
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"custom_world_agent_message" => db_update
|
||||
.custom_world_agent_message
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
@@ -2930,6 +2964,12 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"chapter_progression" => db_update
|
||||
.chapter_progression
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"creation_entry_config" => db_update
|
||||
.creation_entry_config
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"creation_entry_type_config" => db_update
|
||||
.creation_entry_type_config
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"custom_world_agent_message" => db_update
|
||||
.custom_world_agent_message
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
@@ -3143,6 +3183,8 @@ pub struct AppliedDiff<'r> {
|
||||
big_fish_event: __sdk::TableAppliedDiff<'r, BigFishEvent>,
|
||||
big_fish_runtime_run: __sdk::TableAppliedDiff<'r, BigFishRuntimeRun>,
|
||||
chapter_progression: __sdk::TableAppliedDiff<'r, ChapterProgression>,
|
||||
creation_entry_config: __sdk::TableAppliedDiff<'r, CreationEntryConfig>,
|
||||
creation_entry_type_config: __sdk::TableAppliedDiff<'r, CreationEntryTypeConfig>,
|
||||
custom_world_agent_message: __sdk::TableAppliedDiff<'r, CustomWorldAgentMessage>,
|
||||
custom_world_agent_operation: __sdk::TableAppliedDiff<'r, CustomWorldAgentOperation>,
|
||||
custom_world_agent_session: __sdk::TableAppliedDiff<'r, CustomWorldAgentSession>,
|
||||
@@ -3299,6 +3341,16 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
||||
&self.chapter_progression,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<CreationEntryConfig>(
|
||||
"creation_entry_config",
|
||||
&self.creation_entry_config,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<CreationEntryTypeConfig>(
|
||||
"creation_entry_type_config",
|
||||
&self.creation_entry_type_config,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<CustomWorldAgentMessage>(
|
||||
"custom_world_agent_message",
|
||||
&self.custom_world_agent_message,
|
||||
@@ -4265,6 +4317,8 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
big_fish_event_table::register_table(client_cache);
|
||||
big_fish_runtime_run_table::register_table(client_cache);
|
||||
chapter_progression_table::register_table(client_cache);
|
||||
creation_entry_config_table::register_table(client_cache);
|
||||
creation_entry_type_config_table::register_table(client_cache);
|
||||
custom_world_agent_message_table::register_table(client_cache);
|
||||
custom_world_agent_operation_table::register_table(client_cache);
|
||||
custom_world_agent_session_table::register_table(client_cache);
|
||||
@@ -4345,6 +4399,8 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
"big_fish_event",
|
||||
"big_fish_runtime_run",
|
||||
"chapter_progression",
|
||||
"creation_entry_config",
|
||||
"creation_entry_type_config",
|
||||
"custom_world_agent_message",
|
||||
"custom_world_agent_operation",
|
||||
"custom_world_agent_session",
|
||||
|
||||
@@ -14,7 +14,6 @@ pub struct ProfileInviteCode {
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
pub starts_at: Option<__sdk::Timestamp>,
|
||||
pub expires_at: Option<__sdk::Timestamp>,
|
||||
pub granted_user_tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ProfileInviteCode {
|
||||
@@ -32,7 +31,6 @@ pub struct ProfileInviteCodeCols {
|
||||
pub updated_at: __sdk::__query_builder::Col<ProfileInviteCode, __sdk::Timestamp>,
|
||||
pub starts_at: __sdk::__query_builder::Col<ProfileInviteCode, Option<__sdk::Timestamp>>,
|
||||
pub expires_at: __sdk::__query_builder::Col<ProfileInviteCode, Option<__sdk::Timestamp>>,
|
||||
pub granted_user_tags: __sdk::__query_builder::Col<ProfileInviteCode, Vec<String>>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for ProfileInviteCode {
|
||||
@@ -46,7 +44,6 @@ impl __sdk::__query_builder::HasCols for ProfileInviteCode {
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
starts_at: __sdk::__query_builder::Col::new(table_name, "starts_at"),
|
||||
expires_at: __sdk::__query_builder::Col::new(table_name, "expires_at"),
|
||||
granted_user_tags: __sdk::__query_builder::Col::new(table_name, "granted_user_tags"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct RuntimeProfileInviteCodeAdminUpsertInput {
|
||||
pub admin_user_id: String,
|
||||
pub invite_code: String,
|
||||
pub metadata_json: String,
|
||||
pub granted_user_tags: Vec<String>,
|
||||
pub starts_at_micros: Option<i64>,
|
||||
pub expires_at_micros: Option<i64>,
|
||||
pub updated_at_micros: i64,
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct RuntimeProfileInviteCodeSnapshot {
|
||||
pub user_id: String,
|
||||
pub invite_code: String,
|
||||
pub metadata_json: String,
|
||||
pub granted_user_tags: Vec<String>,
|
||||
pub starts_at_micros: Option<i64>,
|
||||
pub expires_at_micros: Option<i64>,
|
||||
pub created_at_micros: i64,
|
||||
|
||||
@@ -29,6 +29,7 @@ pub trait upsert_creation_entry_type_config {
|
||||
fn upsert_creation_entry_type_config_then(
|
||||
&self,
|
||||
input: CreationEntryTypeAdminUpsertInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
@@ -41,6 +42,7 @@ impl upsert_creation_entry_type_config for super::RemoteProcedures {
|
||||
fn upsert_creation_entry_type_config_then(
|
||||
&self,
|
||||
input: CreationEntryTypeAdminUpsertInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct UserAccount {
|
||||
pub password_hash: String,
|
||||
pub password_login_enabled: bool,
|
||||
pub token_version: u64,
|
||||
pub user_tags: Vec<String>,
|
||||
pub user_tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for UserAccount {
|
||||
@@ -44,7 +44,7 @@ pub struct UserAccountCols {
|
||||
pub password_hash: __sdk::__query_builder::Col<UserAccount, String>,
|
||||
pub password_login_enabled: __sdk::__query_builder::Col<UserAccount, bool>,
|
||||
pub token_version: __sdk::__query_builder::Col<UserAccount, u64>,
|
||||
pub user_tags: __sdk::__query_builder::Col<UserAccount, Vec<String>>,
|
||||
pub user_tags: __sdk::__query_builder::Col<UserAccount, Option<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for UserAccount {
|
||||
|
||||
@@ -23,15 +23,14 @@ impl SpacetimeClient {
|
||||
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
||||
let procedure_input: CreationEntryTypeAdminUpsertInput = input.into();
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection.procedures().upsert_creation_entry_type_config_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
connection
|
||||
.procedures()
|
||||
.upsert_creation_entry_type_config_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_creation_entry_config_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -687,7 +686,6 @@ impl SpacetimeClient {
|
||||
admin_user_id: String,
|
||||
invite_code: String,
|
||||
metadata_json: String,
|
||||
granted_user_tags: Vec<String>,
|
||||
starts_at_micros: Option<i64>,
|
||||
expires_at_micros: Option<i64>,
|
||||
updated_at_micros: i64,
|
||||
@@ -696,7 +694,6 @@ impl SpacetimeClient {
|
||||
admin_user_id,
|
||||
invite_code,
|
||||
metadata_json,
|
||||
granted_user_tags,
|
||||
starts_at_micros,
|
||||
expires_at_micros,
|
||||
updated_at_micros,
|
||||
|
||||
@@ -33,6 +33,12 @@ pub struct AuthStoreSnapshotProcedureResult {
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
fn normalize_user_account_tags(
|
||||
tags: Option<Vec<String>>,
|
||||
) -> Result<Vec<String>, module_runtime::RuntimeProfileFieldError> {
|
||||
module_runtime::normalize_profile_user_tags(tags.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct AuthStoreSnapshotImportRecord {
|
||||
pub imported_user_count: u32,
|
||||
@@ -210,8 +216,10 @@ fn import_auth_store_snapshot_tx(
|
||||
password_hash: stored_user.password_hash,
|
||||
password_login_enabled: stored_user.password_login_enabled,
|
||||
token_version: user.token_version,
|
||||
user_tags: module_runtime::normalize_profile_user_tags(user.user_tags)
|
||||
.map_err(|error| error.to_string())?,
|
||||
user_tags: Some(
|
||||
module_runtime::normalize_profile_user_tags(user.user_tags)
|
||||
.map_err(|error| error.to_string())?,
|
||||
),
|
||||
});
|
||||
imported_user_count += 1;
|
||||
|
||||
@@ -341,7 +349,8 @@ fn export_auth_store_snapshot_from_tables_tx(
|
||||
binding_status: user.binding_status,
|
||||
wechat_bound: user.wechat_bound,
|
||||
token_version: user.token_version,
|
||||
user_tags: user.user_tags,
|
||||
user_tags: normalize_user_account_tags(user.user_tags)
|
||||
.map_err(|error| error.to_string())?,
|
||||
};
|
||||
users_by_username.insert(
|
||||
user.username,
|
||||
|
||||
@@ -28,7 +28,8 @@ pub struct UserAccount {
|
||||
pub(crate) password_hash: String,
|
||||
pub(crate) password_login_enabled: bool,
|
||||
pub(crate) token_version: u64,
|
||||
pub(crate) user_tags: Vec<String>,
|
||||
#[default(None::<Vec<String>>)]
|
||||
pub(crate) user_tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
|
||||
@@ -452,8 +452,12 @@ fn compile_match3d_draft_tx(
|
||||
.unwrap_or_else(|| default_tags(&config.theme_text));
|
||||
let game_name =
|
||||
clean_optional(&input.game_name).unwrap_or_else(|| format!("{}抓大鹅", config.theme_text));
|
||||
let summary_text = clean_optional(&input.summary_text)
|
||||
.unwrap_or_else(|| format!("{}主题的经典消除玩法。", config.theme_text));
|
||||
let summary_text = input
|
||||
.summary_text
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let draft = Match3DDraftSnapshot {
|
||||
profile_id: input.profile_id.clone(),
|
||||
game_name: game_name.clone(),
|
||||
|
||||
@@ -1140,7 +1140,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
// 中文注释:账号标签字段晚于认证表加入,旧迁移包默认无标签。
|
||||
object
|
||||
.entry("user_tags".to_string())
|
||||
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "profile_invite_code" {
|
||||
@@ -1149,10 +1149,6 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
object
|
||||
.entry("metadata_json".to_string())
|
||||
.or_insert_with(|| serde_json::Value::String("{}".to_string()));
|
||||
// 中文注释:邀请码授予标签字段晚于邀请表加入,旧迁移包默认不授予标签。
|
||||
object
|
||||
.entry("granted_user_tags".to_string())
|
||||
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
|
||||
}
|
||||
}
|
||||
if table_name == "big_fish_creation_session" {
|
||||
|
||||
@@ -3226,7 +3226,11 @@ fn list_puzzle_leaderboard_entries(
|
||||
.user_account()
|
||||
.user_id()
|
||||
.find(&row.user_id)
|
||||
.map(|account| visible_runtime_profile_user_tags(&account.user_tags))
|
||||
.map(|account| {
|
||||
visible_runtime_profile_user_tags(
|
||||
account.user_tags.as_deref().unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
rank: index as u32 + 1,
|
||||
nickname: row.nickname,
|
||||
|
||||
@@ -188,7 +188,7 @@ fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeC
|
||||
build_creation_entry_type_seed("big-fish", "摸鱼", "轻量闯关玩法", "可创建", "/creation-type-references/big-fish.webp", false, true, 20, now),
|
||||
build_creation_entry_type_seed("puzzle", "拼图", "拼图关卡创作", "可创建", "/creation-type-references/puzzle.webp", true, true, 30, now),
|
||||
build_creation_entry_type_seed("match3d", "抓大鹅", "3D 消除关卡", "可创建", "/creation-type-references/match3d.webp", true, true, 40, now),
|
||||
build_creation_entry_type_seed("square-hole", "方洞", "形状投放挑战", "可创建", "/creation-type-references/square-hole.webp", true, true, 50, now),
|
||||
build_creation_entry_type_seed("square-hole", "方洞", "形状投放挑战", "可创建", "/creation-type-references/square-hole.webp", false, true, 50, now),
|
||||
build_creation_entry_type_seed("visual-novel", "视觉小说", "分支叙事体验", "可创建", "/creation-type-references/visual-novel.webp", true, true, 60, now),
|
||||
build_creation_entry_type_seed("airp", "AI RPG", "原生角色扮演", "即将开放", "/creation-type-references/airp.webp", true, false, 70, now),
|
||||
build_creation_entry_type_seed("creative-agent", "智能体创作", "对话式创作实验", "内测", "/creation-type-references/creative-agent.webp", false, true, 80, now),
|
||||
|
||||
@@ -191,7 +191,6 @@ pub struct ProfileInviteCode {
|
||||
pub(crate) starts_at: Option<Timestamp>,
|
||||
#[default(None::<Timestamp>)]
|
||||
pub(crate) expires_at: Option<Timestamp>,
|
||||
pub(crate) granted_user_tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
@@ -2062,7 +2061,8 @@ fn redeem_profile_referral_invite_code_record(
|
||||
if inviter_code.user_id == invitee_user_id {
|
||||
return Err("不能填写自己的邀请码".to_string());
|
||||
}
|
||||
let granted_user_tags = inviter_code.granted_user_tags.clone();
|
||||
let invite_metadata_user_tags =
|
||||
profile_invite_code_metadata_user_tags(&inviter_code.metadata_json)?;
|
||||
|
||||
let invitee_balance_after = apply_profile_wallet_delta(
|
||||
ctx,
|
||||
@@ -2109,7 +2109,7 @@ fn redeem_profile_referral_invite_code_record(
|
||||
invitee_reward_granted: true,
|
||||
bound_at,
|
||||
});
|
||||
merge_user_account_tags(ctx, &invitee_user_id, granted_user_tags)?;
|
||||
merge_user_account_tags(ctx, &invitee_user_id, invite_metadata_user_tags)?;
|
||||
|
||||
Ok(RuntimeReferralRedeemSnapshot {
|
||||
center: build_profile_referral_invite_center_snapshot(ctx, &invitee_user_id),
|
||||
@@ -2283,7 +2283,6 @@ fn admin_upsert_profile_invite_code_record(
|
||||
input.admin_user_id,
|
||||
input.invite_code,
|
||||
input.metadata_json,
|
||||
input.granted_user_tags,
|
||||
input.starts_at_micros,
|
||||
input.expires_at_micros,
|
||||
input.updated_at_micros,
|
||||
@@ -2320,7 +2319,6 @@ fn admin_upsert_profile_invite_code_record(
|
||||
expires_at: validated_input
|
||||
.expires_at_micros
|
||||
.map(Timestamp::from_micros_since_unix_epoch),
|
||||
granted_user_tags: validated_input.granted_user_tags,
|
||||
});
|
||||
return Ok(build_profile_invite_code_snapshot_from_row(&inserted));
|
||||
}
|
||||
@@ -2337,7 +2335,6 @@ fn admin_upsert_profile_invite_code_record(
|
||||
expires_at: validated_input
|
||||
.expires_at_micros
|
||||
.map(Timestamp::from_micros_since_unix_epoch),
|
||||
granted_user_tags: validated_input.granted_user_tags,
|
||||
});
|
||||
Ok(build_profile_invite_code_snapshot_from_row(&inserted))
|
||||
}
|
||||
@@ -2464,7 +2461,6 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
|
||||
updated_at: ctx.timestamp,
|
||||
starts_at: None,
|
||||
expires_at: None,
|
||||
granted_user_tags: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2483,14 +2479,33 @@ fn merge_user_account_tags(
|
||||
return Err("用户不存在".to_string());
|
||||
};
|
||||
|
||||
account.user_tags.extend(granted_tags);
|
||||
let mut next_tags = account.user_tags.take().unwrap_or_default();
|
||||
next_tags.extend(granted_tags);
|
||||
account.user_tags =
|
||||
normalize_profile_user_tags(account.user_tags).map_err(|error| error.to_string())?;
|
||||
Some(normalize_profile_user_tags(next_tags).map_err(|error| error.to_string())?);
|
||||
ctx.db.user_account().user_id().delete(&account.user_id);
|
||||
ctx.db.user_account().insert(account);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn profile_invite_code_metadata_user_tags(metadata_json: &str) -> Result<Vec<String>, String> {
|
||||
let metadata = serde_json::from_str::<JsonValue>(metadata_json)
|
||||
.map_err(|_| RuntimeProfileFieldError::InvalidInviteCodeMetadata.to_string())?;
|
||||
let tags = metadata
|
||||
.get("userTags")
|
||||
.or_else(|| metadata.get("user_tags"))
|
||||
.and_then(JsonValue::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(JsonValue::as_str)
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
normalize_profile_user_tags(tags).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn validate_profile_invite_code_redeem_time(
|
||||
invite_code: &ProfileInviteCode,
|
||||
now_micros: i64,
|
||||
@@ -3515,7 +3530,6 @@ fn build_profile_invite_code_snapshot_from_row(
|
||||
user_id: row.user_id.clone(),
|
||||
invite_code: row.invite_code.clone(),
|
||||
metadata_json: row.metadata_json.clone(),
|
||||
granted_user_tags: row.granted_user_tags.clone(),
|
||||
starts_at_micros: row
|
||||
.starts_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
|
||||
Reference in New Issue
Block a user