feat: move creation entry config to database

This commit is contained in:
2026-05-11 11:23:24 +08:00
parent 7f2461313e
commit 793d82cccd
37 changed files with 1458 additions and 204 deletions

View File

@@ -50,6 +50,9 @@ use crate::{
generate_character_visual, get_character_visual_job, publish_character_visual,
},
creation_agent_document_input::parse_creation_agent_document_input,
creation_entry_config::{
get_creation_entry_config_handler, require_creation_entry_route_enabled,
},
creative_agent::{
cancel_creative_agent_session, confirm_creative_puzzle_template,
create_creative_agent_session, get_creative_agent_session, stream_creative_agent_message,
@@ -611,6 +614,10 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/creation-entry/config",
get(get_creation_entry_config_handler),
)
.route(
"/api/runtime/settings",
get(get_runtime_settings)
@@ -1492,6 +1499,11 @@ pub fn build_router(state: AppState) -> Router {
)),
)
.route("/api/auth/password/reset", post(reset_password))
// 后端 runtime/API 路由读取入口配置做统一熔断,避免前端隐藏后后端仍可被直接访问。
.layer(middleware::from_fn_with_state(
state.clone(),
require_creation_entry_route_enabled,
))
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
.layer(middleware::from_fn(normalize_error_response))
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
@@ -1952,6 +1964,28 @@ mod tests {
);
}
#[tokio::test]
async fn creation_entry_route_disabled_returns_service_unavailable() {
let state = AppState::new(AppConfig::default()).expect("state should build");
state.set_test_creation_entry_route_enabled("puzzle", false);
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/runtime/puzzle/works")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
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"]["creationTypeId"], "puzzle");
}
#[tokio::test]
async fn healthz_returns_standard_envelope_when_requested() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));

View File

@@ -0,0 +1,167 @@
use axum::{
Json,
body::Body,
extract::{Extension, State},
http::{Request, StatusCode},
middleware::Next,
response::Response,
};
use serde_json::{Value, json};
use crate::{
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
state::AppState,
};
/// 中文注释:入口配置由 SpacetimeDB 表提供api-server 只负责读取同一份配置并熔断运行态路由。
pub async fn get_creation_entry_config_handler(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
) -> Result<Json<Value>, Response> {
let config = state.get_creation_entry_config().await.map_err(|error| {
creation_entry_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "spacetimedb",
"message": error.to_string(),
})),
)
})?;
Ok(json_success_body(Some(&request_context), config))
}
/// 中文注释api-server 路由熔断只拦运行态/API 请求,不改变前端入口展示规则。
pub async fn require_creation_entry_route_enabled(
State(state): State<AppState>,
request: Request<Body>,
next: Next,
) -> Response {
let path = request.uri().path();
let route_id = resolve_creation_entry_route_id(path);
if route_id.is_some() {
let route_id = route_id.expect("route id should exist");
match state.is_creation_entry_route_enabled(route_id).await {
Ok(true) => {}
Ok(false) => {
return AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
.with_message("该玩法入口暂不可用")
.with_details(json!({
"reason": "creation_entry_disabled",
"creationTypeId": route_id,
}))
.into();
}
Err(error) => {
return AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message("读取玩法入口配置失败")
.with_details(json!({
"provider": "spacetimedb",
"message": error.to_string(),
}))
.into();
}
}
}
next.run(request).await
}
pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
let normalized = path.trim_end_matches('/');
if normalized.starts_with("/api/runtime/puzzle") {
return Some("puzzle");
}
if normalized.starts_with("/api/runtime/match3d") {
return Some("match3d");
}
if normalized.starts_with("/api/runtime/square-hole") {
return Some("square-hole");
}
if normalized.starts_with("/api/runtime/big-fish") {
return Some("big-fish");
}
if normalized.starts_with("/api/runtime/visual-novel") {
return Some("visual-novel");
}
None
}
fn creation_entry_error_response(request_context: &RequestContext, error: AppError) -> Response {
error.into_response_with_context(Some(request_context))
}
#[cfg(test)]
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 {
title: module_runtime::DEFAULT_CREATION_ENTRY_START_TITLE.to_string(),
description: module_runtime::DEFAULT_CREATION_ENTRY_START_DESCRIPTION.to_string(),
idle_badge: module_runtime::DEFAULT_CREATION_ENTRY_START_IDLE_BADGE.to_string(),
busy_badge: module_runtime::DEFAULT_CREATION_ENTRY_START_BUSY_BADGE.to_string(),
},
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
title: module_runtime::DEFAULT_CREATION_ENTRY_MODAL_TITLE.to_string(),
description: module_runtime::DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION.to_string(),
},
creation_types: vec![
test_creation_type("rpg", false, true, 10),
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("visual-novel", true, true, 60),
test_creation_type("airp", true, false, 70),
test_creation_type("creative-agent", false, true, 80),
],
updated_at_micros: 0,
})
}
#[cfg(test)]
fn test_creation_type(
id: &str,
visible: bool,
open: bool,
sort_order: i32,
) -> module_runtime::CreationEntryTypeSnapshot {
module_runtime::CreationEntryTypeSnapshot {
id: id.to_string(),
title: id.to_string(),
subtitle: "测试入口".to_string(),
badge: "测试".to_string(),
image_src: format!("/creation-type-references/{id}.webp"),
visible,
open,
sort_order,
updated_at_micros: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_runtime_paths_to_creation_type_ids() {
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/puzzle/works"),
Some("puzzle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/match3d/runs/run-1"),
Some("match3d"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/square-hole/runs/run-1"),
Some("square-hole"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/visual-novel/works"),
Some("visual-novel"),
);
assert_eq!(resolve_creation_entry_route_id("/healthz"), None);
}
}

View File

@@ -23,6 +23,7 @@ mod creation_agent_anchor_templates;
mod creation_agent_chat;
mod creation_agent_document_input;
mod creation_agent_llm_turn;
mod creation_entry_config;
mod creative_agent;
mod creative_agent_sse;
mod custom_world;

View File

@@ -11,6 +11,7 @@ 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;
@@ -41,6 +42,8 @@ pub struct AppState {
auth_jwt_config: JwtConfig,
admin_runtime: Option<AdminRuntime>,
refresh_cookie_config: RefreshCookieConfig,
#[cfg(test)]
test_creation_entry_config: Arc<Mutex<Option<CreationEntryConfigResponse>>>,
oss_client: Option<OssClient>,
#[cfg_attr(test, allow(dead_code))]
auth_store: InMemoryAuthStore,
@@ -189,6 +192,10 @@ impl AppState {
auth_jwt_config,
admin_runtime,
refresh_cookie_config,
#[cfg(test)]
test_creation_entry_config: Arc::new(Mutex::new(Some(
crate::creation_entry_config::test_creation_entry_config_response(),
))),
oss_client,
auth_store,
password_entry_service,
@@ -221,6 +228,68 @@ impl AppState {
&self.refresh_cookie_config
}
pub async fn get_creation_entry_config(
&self,
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
match self.spacetime_client.get_creation_entry_config().await {
Ok(config) => {
#[cfg(test)]
self.cache_test_creation_entry_config(config.clone());
Ok(config)
}
#[cfg(test)]
Err(_) => Ok(self.read_test_creation_entry_config()),
#[cfg(not(test))]
Err(error) => Err(error),
}
}
pub async fn is_creation_entry_route_enabled(
&self,
creation_type_id: &str,
) -> Result<bool, SpacetimeClientError> {
let config = self.get_creation_entry_config().await?;
Ok(config
.creation_types
.iter()
.find(|item| item.id == creation_type_id)
.map(|item| item.visible && item.open)
.unwrap_or(true))
}
#[cfg(test)]
pub(crate) fn set_test_creation_entry_route_enabled(
&self,
creation_type_id: impl AsRef<str>,
enabled: bool,
) {
let creation_type_id = creation_type_id.as_ref();
let mut config = self.read_test_creation_entry_config();
if let Some(item) = config
.creation_types
.iter_mut()
.find(|item| item.id == creation_type_id)
{
item.visible = enabled;
item.open = enabled;
} else {
config.creation_types.push(
shared_contracts::creation_entry_config::CreationEntryTypeResponse {
id: creation_type_id.to_string(),
title: creation_type_id.to_string(),
subtitle: String::new(),
badge: String::new(),
image_src: format!("/creation-type-references/{creation_type_id}.webp"),
visible: enabled,
open: enabled,
sort_order: i32::try_from(config.creation_types.len()).unwrap_or(i32::MAX),
updated_at_micros: 0,
},
);
}
self.cache_test_creation_entry_config(config);
}
pub fn oss_client(&self) -> Option<&OssClient> {
self.oss_client.as_ref()
}
@@ -488,6 +557,21 @@ impl AppState {
#[cfg(test)]
impl AppState {
fn cache_test_creation_entry_config(&self, config: CreationEntryConfigResponse) {
*self
.test_creation_entry_config
.lock()
.expect("test creation entry config should lock") = Some(config);
}
fn read_test_creation_entry_config(&self) -> CreationEntryConfigResponse {
self.test_creation_entry_config
.lock()
.expect("test creation entry config should lock")
.clone()
.unwrap_or_else(crate::creation_entry_config::test_creation_entry_config_response)
}
pub(crate) async fn seed_test_phone_user_with_password(
&self,
phone_number: &str,

View File

@@ -11,6 +11,7 @@ spacetime-types = ["dep:spacetimedb"]
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
shared-contracts = { workspace = true }
shared-kernel = { workspace = true }
spacetimedb = { workspace = true, optional = true }
time = { workspace = true, features = ["formatting", "parsing"] }

View File

@@ -9,6 +9,42 @@ use std::collections::BTreeMap;
use crate::domain::*;
use crate::errors::RuntimeProfileFieldError;
use crate::format_utc_micros;
use shared_contracts::creation_entry_config::{
CreationEntryConfigResponse, CreationEntryStartCardResponse, CreationEntryTypeModalResponse,
CreationEntryTypeResponse,
};
pub fn build_creation_entry_config_response(
snapshot: CreationEntryConfigSnapshot,
) -> CreationEntryConfigResponse {
CreationEntryConfigResponse {
start_card: CreationEntryStartCardResponse {
title: snapshot.start_card.title,
description: snapshot.start_card.description,
idle_badge: snapshot.start_card.idle_badge,
busy_badge: snapshot.start_card.busy_badge,
},
type_modal: CreationEntryTypeModalResponse {
title: snapshot.type_modal.title,
description: snapshot.type_modal.description,
},
creation_types: snapshot
.creation_types
.into_iter()
.map(|item| CreationEntryTypeResponse {
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
image_src: item.image_src,
visible: item.visible,
open: item.open,
sort_order: item.sort_order,
updated_at_micros: item.updated_at_micros,
})
.collect(),
}
}
pub fn build_runtime_setting_record(snapshot: RuntimeSettingSnapshot) -> RuntimeSettingsRecord {
RuntimeSettingsRecord {

View File

@@ -40,6 +40,62 @@ pub const PROFILE_FEEDBACK_EVIDENCE_MAX_COUNT: usize = 4;
pub const PROFILE_FEEDBACK_EVIDENCE_MAX_BYTES: u64 = 1_048_576;
pub const PROFILE_FEEDBACK_EVIDENCE_TOTAL_MAX_BYTES: u64 = 4_194_304;
pub const CREATION_ENTRY_CONFIG_GLOBAL_ID: &str = "platform_creation_center";
pub const DEFAULT_CREATION_ENTRY_START_TITLE: &str = "新建作品";
pub const DEFAULT_CREATION_ENTRY_START_DESCRIPTION: &str = "选择模板后进入对应的创作表单。";
pub const DEFAULT_CREATION_ENTRY_START_IDLE_BADGE: &str = "模板 Tab";
pub const DEFAULT_CREATION_ENTRY_START_BUSY_BADGE: &str = "正在开启";
pub const DEFAULT_CREATION_ENTRY_MODAL_TITLE: &str = "选择创作类型";
pub const DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION: &str = "先选玩法类型,再进入对应创作工作台。";
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryStartCardSnapshot {
pub title: String,
pub description: String,
pub idle_badge: String,
pub busy_badge: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryTypeModalSnapshot {
pub title: String,
pub description: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryTypeSnapshot {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryConfigSnapshot {
pub config_id: String,
pub start_card: CreationEntryStartCardSnapshot,
pub type_modal: CreationEntryTypeModalSnapshot,
pub creation_types: Vec<CreationEntryTypeSnapshot>,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryConfigProcedureResult {
pub ok: bool,
pub record: Option<CreationEntryConfigSnapshot>,
pub error_message: Option<String>,
}
/// 分析日期维表的纯领域快照。
///
/// date_key 沿用现有北京时间自然日桶floor((occurred_at_micros + 8h) / 1d)。

View File

@@ -0,0 +1,39 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreationEntryConfigResponse {
pub start_card: CreationEntryStartCardResponse,
pub type_modal: CreationEntryTypeModalResponse,
pub creation_types: Vec<CreationEntryTypeResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreationEntryStartCardResponse {
pub title: String,
pub description: String,
pub idle_badge: String,
pub busy_badge: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreationEntryTypeModalResponse {
pub title: String,
pub description: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreationEntryTypeResponse {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
pub updated_at_micros: i64,
}

View File

@@ -7,6 +7,7 @@ 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 hyper3d;
pub mod llm;
pub mod match3d_agent;

View File

@@ -14,7 +14,8 @@ pub use mapper::{
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput,
BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRunRecord,
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record,
BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, CustomWorldAgentActionExecuteRecord,
BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, CreationEntryConfigRecord,
CustomWorldAgentActionExecuteRecord,
CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentCheckpointRecord,
CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord,
CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationProgressRecordInput,

View File

@@ -705,6 +705,58 @@ pub(crate) fn map_asset_history_list_result(
.collect())
}
pub type CreationEntryConfigRecord = shared_contracts::creation_entry_config::CreationEntryConfigResponse;
pub(crate) fn map_creation_entry_config_procedure_result(
result: CreationEntryConfigProcedureResult,
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("创作入口配置快照"))?;
Ok(module_runtime::build_creation_entry_config_response(
map_creation_entry_config_snapshot(snapshot),
))
}
fn map_creation_entry_config_snapshot(
snapshot: CreationEntryConfigSnapshot,
) -> module_runtime::CreationEntryConfigSnapshot {
module_runtime::CreationEntryConfigSnapshot {
config_id: snapshot.config_id,
start_card: module_runtime::CreationEntryStartCardSnapshot {
title: snapshot.start_card.title,
description: snapshot.start_card.description,
idle_badge: snapshot.start_card.idle_badge,
busy_badge: snapshot.start_card.busy_badge,
},
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
title: snapshot.type_modal.title,
description: snapshot.type_modal.description,
},
creation_types: snapshot
.creation_types
.into_iter()
.map(|item| module_runtime::CreationEntryTypeSnapshot {
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
image_src: item.image_src,
visible: item.visible,
open: item.open,
sort_order: item.sort_order,
updated_at_micros: item.updated_at_micros,
})
.collect(),
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_setting_procedure_result(
result: RuntimeSettingProcedureResult,
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {

View File

@@ -0,0 +1,19 @@
// 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};
use super::creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreationEntryConfigProcedureResult {
pub ok: bool,
pub record: Option<CreationEntryConfigSnapshot>,
pub error_message: Option<String>,
}
impl __sdk::InModule for CreationEntryConfigProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,23 @@
// 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};
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;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreationEntryConfigSnapshot {
pub config_id: String,
pub start_card: CreationEntryStartCardSnapshot,
pub type_modal: CreationEntryTypeModalSnapshot,
pub creation_types: Vec<CreationEntryTypeSnapshot>,
pub updated_at_micros: i64,
}
impl __sdk::InModule for CreationEntryConfigSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,18 @@
// 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 CreationEntryStartCardSnapshot {
pub title: String,
pub description: String,
pub idle_badge: String,
pub busy_badge: String,
}
impl __sdk::InModule for CreationEntryStartCardSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,23 @@
// 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 CreationEntryTypeSnapshot {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
pub updated_at_micros: i64,
}
impl __sdk::InModule for CreationEntryTypeSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,16 @@
// 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 CreationEntryTypeModalSnapshot {
pub title: String,
pub description: String,
}
impl __sdk::InModule for CreationEntryTypeModalSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,52 @@
// 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};
use super::creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetCreationEntryConfigArgs {}
impl __sdk::InModule for GetCreationEntryConfigArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_creation_entry_config`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_creation_entry_config {
fn get_creation_entry_config(&self) {
self.get_creation_entry_config_then(|_, _| {});
}
fn get_creation_entry_config_then(
&self,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl get_creation_entry_config for super::RemoteProcedures {
fn get_creation_entry_config_then(
&self,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, CreationEntryConfigProcedureResult>(
"get_creation_entry_config",
GetCreationEntryConfigArgs {},
__callback,
);
}
}

View File

@@ -249,6 +249,11 @@ 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_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;
@@ -290,6 +295,7 @@ 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;
@@ -1045,6 +1051,11 @@ 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_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;
@@ -1086,6 +1097,7 @@ 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;

View File

@@ -1,6 +1,22 @@
use super::*;
impl SpacetimeClient {
pub async fn get_creation_entry_config(
&self,
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.get_creation_entry_config_then(move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_creation_entry_config_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn get_runtime_settings(
&self,
user_id: String,

View File

@@ -1,4 +1,5 @@
use crate::runtime::analytics_date_dimension::analytics_date_dimension;
use crate::runtime::creation_entry_config::{creation_entry_config, creation_entry_type_config};
use crate::*;
use serde::{Deserialize, Serialize};
use spacetimedb::sats::de::serde::DeserializeWrapper;
@@ -167,6 +168,8 @@ macro_rules! migration_tables {
ai_task_event,
runtime_snapshot,
runtime_setting,
creation_entry_config,
creation_entry_type_config,
user_browse_history,
profile_dashboard_state,
profile_wallet_ledger,

View File

@@ -0,0 +1,170 @@
use crate::*;
#[spacetimedb::table(accessor = creation_entry_config)]
pub struct CreationEntryConfig {
#[primary_key]
pub(crate) config_id: String,
pub(crate) start_title: String,
pub(crate) start_description: String,
pub(crate) start_idle_badge: String,
pub(crate) start_busy_badge: String,
pub(crate) modal_title: String,
pub(crate) modal_description: String,
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = creation_entry_type_config,
index(accessor = by_creation_entry_type_sort_order, btree(columns = [sort_order]))
)]
pub struct CreationEntryTypeConfig {
#[primary_key]
pub(crate) id: String,
pub(crate) title: String,
pub(crate) subtitle: String,
pub(crate) badge: String,
pub(crate) image_src: String,
pub(crate) visible: bool,
pub(crate) open: bool,
pub(crate) sort_order: i32,
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::procedure]
pub fn get_creation_entry_config(
ctx: &mut ProcedureContext,
) -> CreationEntryConfigProcedureResult {
match ctx.try_with_tx(|tx| get_or_seed_creation_entry_config_snapshot(tx)) {
Ok(record) => CreationEntryConfigProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => CreationEntryConfigProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
fn get_or_seed_creation_entry_config_snapshot(
ctx: &ReducerContext,
) -> Result<CreationEntryConfigSnapshot, String> {
seed_creation_entry_config_if_missing(ctx);
let header = ctx
.db
.creation_entry_config()
.config_id()
.find(CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string())
.ok_or_else(|| "创作入口配置初始化失败".to_string())?;
let mut creation_types = ctx
.db
.creation_entry_type_config()
.iter()
.map(|row| CreationEntryTypeSnapshot {
id: row.id,
title: row.title,
subtitle: row.subtitle,
badge: row.badge,
image_src: row.image_src,
visible: row.visible,
open: row.open,
sort_order: row.sort_order,
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
})
.collect::<Vec<_>>();
creation_types.sort_by(|left, right| {
left.sort_order
.cmp(&right.sort_order)
.then_with(|| left.id.cmp(&right.id))
});
Ok(CreationEntryConfigSnapshot {
config_id: header.config_id,
start_card: CreationEntryStartCardSnapshot {
title: header.start_title,
description: header.start_description,
idle_badge: header.start_idle_badge,
busy_badge: header.start_busy_badge,
},
type_modal: CreationEntryTypeModalSnapshot {
title: header.modal_title,
description: header.modal_description,
},
creation_types,
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
})
}
fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) {
let now = ctx.timestamp;
if ctx
.db
.creation_entry_config()
.config_id()
.find(CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string())
.is_none()
{
ctx.db.creation_entry_config().insert(CreationEntryConfig {
config_id: CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string(),
start_title: DEFAULT_CREATION_ENTRY_START_TITLE.to_string(),
start_description: DEFAULT_CREATION_ENTRY_START_DESCRIPTION.to_string(),
start_idle_badge: DEFAULT_CREATION_ENTRY_START_IDLE_BADGE.to_string(),
start_busy_badge: DEFAULT_CREATION_ENTRY_START_BUSY_BADGE.to_string(),
modal_title: DEFAULT_CREATION_ENTRY_MODAL_TITLE.to_string(),
modal_description: DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION.to_string(),
updated_at: now,
});
}
for seed in default_creation_entry_type_configs(now) {
if ctx
.db
.creation_entry_type_config()
.id()
.find(&seed.id)
.is_none()
{
ctx.db.creation_entry_type_config().insert(seed);
}
}
}
fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeConfig> {
vec![
build_creation_entry_type_seed("rpg", "文字冒险", "经典 RPG 体验", "内测", "/creation-type-references/rpg.webp", false, true, 10, now),
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("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),
]
}
#[allow(clippy::too_many_arguments)]
fn build_creation_entry_type_seed(
id: &str,
title: &str,
subtitle: &str,
badge: &str,
image_src: &str,
visible: bool,
open: bool,
sort_order: i32,
now: Timestamp,
) -> CreationEntryTypeConfig {
CreationEntryTypeConfig {
id: id.to_string(),
title: title.to_string(),
subtitle: subtitle.to_string(),
badge: badge.to_string(),
image_src: image_src.to_string(),
visible,
open,
sort_order,
updated_at: now,
}
}

View File

@@ -1,10 +1,12 @@
pub mod analytics_date_dimension;
pub mod creation_entry_config;
mod browse_history;
mod profile;
mod settings;
mod snapshots;
pub use analytics_date_dimension::*;
pub use creation_entry_config::*;
pub use browse_history::*;
pub use profile::*;
pub use settings::*;