合并 master 并保留外部生成 worker 模式

合入 master 的生产健康巡检、JumpHop 和 SpacetimeDB 更新
保留外部生成 worker、队列/内联模式与 lease guard 口径
合并 Server-Provision 工具复用、health patrol 和外部生成 worker systemd 配置
补齐 SpacetimeDB 生成绑定并通过本地检查
This commit is contained in:
2026-06-10 21:26:53 +08:00
93 changed files with 7872 additions and 2244 deletions

View File

@@ -472,9 +472,9 @@ fn validate_jump_hop_runtime_ready(
}
validate_jump_hop_default_character_ready(work)?;
validate_jump_hop_tile_atlas_asset_ready(&work.tile_atlas_asset, "tile_atlas_asset")?;
if work.tile_assets.len() < 25 {
if work.tile_assets.len() < 18 {
return Err(SpacetimeClientError::validation_failed(
"jump-hop runtime 需要 25 个地块资产",
"jump-hop runtime 需要 18 个地块资产",
));
}
for (index, asset) in work.tile_assets.iter().enumerate() {
@@ -760,12 +760,12 @@ fn build_compile_input(
draft.default_character = Some(default_jump_hop_default_character());
let tile_atlas_asset = draft.tile_atlas_asset.clone().ok_or_else(|| {
SpacetimeClientError::validation_failed(
"jump-hop compile-draft 缺少真实地图集资产,请先由 api-server 生成并持久化 asset_object",
"jump-hop compile-draft 缺少真实地板贴图图集资产,请先由 api-server 生成并持久化 asset_object",
)
})?;
let tile_assets = if draft.tile_assets.len() < 25 {
let tile_assets = if draft.tile_assets.len() < 18 {
return Err(SpacetimeClientError::validation_failed(
"jump-hop compile-draft 需要 25 个真实地块资产,请先由 api-server 生成并持久化 asset_object",
"jump-hop compile-draft 需要 18 个真实地块资产,请先由 api-server 生成并持久化 asset_object",
));
} else {
draft.tile_assets.clone()
@@ -877,7 +877,7 @@ fn default_draft() -> JumpHopDraftResponse {
style_preset: JumpHopStylePreset::MinimalBlocks,
default_character: Some(default_jump_hop_default_character()),
character_prompt: "内置默认 3D 角色".to_string(),
tile_prompt: "跳一跳主题的正面30度视角主题物体图集物体本身作为跳跃落点".to_string(),
tile_prompt: "跳一跳主题的3D立方体主题身份方块包装图集".to_string(),
end_mood_prompt: None,
character_asset: None,
tile_atlas_asset: None,
@@ -993,7 +993,7 @@ mod tests {
const NOW_MICROS: i64 = 1_763_456_789_000_000;
#[test]
fn jump_hop_action_compile_draft_builds_compile_input_with_25_tile_assets_and_builtin_character()
fn jump_hop_action_compile_draft_builds_compile_input_with_18_tile_assets_and_builtin_character()
{
let session = session_with_draft(draft_without_character_asset());
let payload = action(JumpHopActionType::CompileDraft);
@@ -1027,9 +1027,9 @@ mod tests {
.tile_assets_json
.as_deref()
.unwrap_or("")
.contains("old-tile-25-object")
.contains("old-tile-18-object")
);
assert_eq!(draft.tile_assets.len(), 25);
assert_eq!(draft.tile_assets.len(), 18);
assert_eq!(draft.generation_status, JumpHopGenerationStatus::Ready);
}
@@ -1039,7 +1039,7 @@ mod tests {
let mut payload = action(JumpHopActionType::RegenerateTiles);
payload.tile_prompt = Some("新的地块提示词".to_string());
payload.tile_atlas_asset = Some(tile_atlas_asset("new-tile-atlas", NOW_MICROS));
payload.tile_assets = Some(tile_assets("new", 25));
payload.tile_assets = Some(tile_assets("new", 18));
let (plan, _draft) =
build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
@@ -1081,7 +1081,7 @@ mod tests {
.tile_assets_json
.as_deref()
.unwrap_or("")
.contains("new-tile-25-object")
.contains("new-tile-18-object")
);
}
@@ -1195,7 +1195,7 @@ mod tests {
JumpHopDraftResponse {
profile_id: None,
tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)),
tile_assets: tile_assets("old", 25),
tile_assets: tile_assets("old", 18),
..base_draft()
}
}
@@ -1205,7 +1205,7 @@ mod tests {
profile_id: Some(PROFILE_ID.to_string()),
character_asset: Some(build_jump_hop_default_character_asset(PROFILE_ID, "旧主题")),
tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)),
tile_assets: tile_assets("old", 25),
tile_assets: tile_assets("old", 18),
path: Some(sample_jump_hop_path()),
cover_composite: Some("/generated-jump-hop-assets/old-cover.png".to_string()),
generation_status: JumpHopGenerationStatus::Ready,
@@ -1242,13 +1242,14 @@ mod tests {
index + 1
),
asset_object_id: format!("{prefix}-tile-{:02}-object", index + 1),
source_atlas_cell: format!("row-{}-col-{}", index / 5 + 1, index % 5 + 1),
atlas_row: Some(index as u32 / 5 + 1),
atlas_col: Some(index as u32 % 5 + 1),
source_atlas_cell: format!("row-{}-col-{}", index / 3 + 1, index % 3 + 1),
atlas_row: Some(index as u32 / 3 + 1),
atlas_col: Some(index as u32 % 3 + 1),
visual_width: 256,
visual_height: 192,
top_surface_radius: 42.0,
landing_radius: 34.0,
face_assets: None,
})
.collect()
}

View File

@@ -137,7 +137,7 @@ use std::{
sync::atomic::{AtomicBool, Ordering},
sync::{Arc, Mutex},
thread::JoinHandle,
time::Duration,
time::{Duration, Instant},
};
use module_ai::{
@@ -246,6 +246,7 @@ use tokio::{
sync::{OwnedSemaphorePermit, RwLock, Semaphore, oneshot},
time::timeout,
};
use tracing::warn;
use crate::module_bindings::*;
@@ -258,6 +259,60 @@ pub struct SpacetimeClientConfig {
pub procedure_timeout: Duration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpacetimeClientStage {
Ready,
PoolAcquire,
ConnectBuild,
ConnectHandshake,
ReadModelSubscribe,
ProcedureResult,
ReducerResult,
ReadCache,
}
impl SpacetimeClientStage {
pub fn as_str(self) -> &'static str {
match self {
Self::Ready => "ready",
Self::PoolAcquire => "pool_acquire",
Self::ConnectBuild => "connect_build",
Self::ConnectHandshake => "connect_handshake",
Self::ReadModelSubscribe => "read_model_subscribe",
Self::ProcedureResult => "procedure_result",
Self::ReducerResult => "reducer_result",
Self::ReadCache => "read_cache",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpacetimeClientHealthSnapshot {
pub ok: bool,
pub stage: SpacetimeClientStage,
pub checked_at_micros: i64,
pub elapsed_ms: u64,
pub timeout_ms: u64,
pub error: Option<String>,
pub last_success_at_micros: Option<i64>,
pub last_error: Option<String>,
}
impl SpacetimeClientHealthSnapshot {
pub fn healthy_for_test() -> Self {
Self {
ok: true,
stage: SpacetimeClientStage::Ready,
checked_at_micros: current_unix_micros(),
elapsed_ms: 0,
timeout_ms: 0,
error: None,
last_success_at_micros: Some(current_unix_micros()),
last_error: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthStoreSnapshotRecord {
pub snapshot_json: Option<String>,
@@ -275,6 +330,7 @@ pub struct AuthStoreSnapshotImportRecord {
pub struct SpacetimeClient {
config: SpacetimeClientConfig,
pool: Arc<SpacetimeConnectionPool>,
health_state: Arc<RwLock<SpacetimeClientHealthState>>,
creation_entry_config_cache: Arc<RwLock<Option<CreationEntryConfigRecord>>>,
custom_world_gallery_legacy_sync_attempted: Arc<AtomicBool>,
}
@@ -301,6 +357,24 @@ struct SpacetimeConnectionPool {
permits: Arc<Semaphore>,
}
#[derive(Debug, Default)]
struct SpacetimeClientHealthState {
last_success_at_micros: Option<i64>,
last_error: Option<String>,
}
#[derive(Debug)]
struct SpacetimeStageError {
stage: SpacetimeClientStage,
error: SpacetimeClientError,
}
impl SpacetimeStageError {
fn new(stage: SpacetimeClientStage, error: SpacetimeClientError) -> Self {
Self { stage, error }
}
}
struct PooledConnectionSlot {
connection: Option<PooledConnection>,
in_use: bool,
@@ -346,6 +420,7 @@ impl SpacetimeClient {
Self {
config,
pool,
health_state: Arc::new(RwLock::new(SpacetimeClientHealthState::default())),
creation_entry_config_cache: Arc::new(RwLock::new(None)),
custom_world_gallery_legacy_sync_attempted: Arc::new(AtomicBool::new(false)),
}
@@ -359,29 +434,58 @@ impl SpacetimeClient {
where
T: Send + 'static,
{
let started_at = Instant::now();
let metrics_guard = telemetry::begin_procedure(procedure);
let (sender, receiver) = oneshot::channel();
let result_sender = Arc::new(Mutex::new(Some(sender)));
let final_result = match self.acquire_connection().await {
let final_result = match self
.acquire_connection_with_timeout(self.config.procedure_timeout)
.await
{
Ok(lease) => {
let result = if let Some(connection) = lease.connection.as_ref() {
let (result, failed_stage) = if let Some(connection) = lease.connection.as_ref() {
call(&connection.connection, result_sender.clone());
match timeout(self.config.procedure_timeout, receiver).await {
Ok(inner) => match inner {
Ok(value) => value,
Err(_) => Err(SpacetimeClientError::ConnectDropped),
let stage = SpacetimeClientStage::ProcedureResult;
(
match timeout(self.config.procedure_timeout, receiver).await {
Ok(inner) => match inner {
Ok(value) => value,
Err(_) => Err(SpacetimeClientError::ConnectDropped),
},
Err(_) => Err(Self::resolve_timeout_error(Some(connection), stage)),
},
Err(_) => Err(Self::resolve_timeout_error(Some(connection))),
}
stage,
)
} else {
Err(SpacetimeClientError::Runtime(
"SpacetimeDB 连接租约缺少连接".to_string(),
))
(
Err(SpacetimeClientError::Runtime(
"SpacetimeDB 连接租约缺少连接".to_string(),
)),
SpacetimeClientStage::ProcedureResult,
)
};
self.release_connection(lease).await;
if let Err(error) = &result {
log_spacetime_client_failure(
"procedure",
procedure,
failed_stage,
started_at,
error,
);
}
result
}
Err(error) => Err(error),
Err(error) => {
log_spacetime_client_failure(
"procedure",
procedure,
error.stage,
started_at,
&error.error,
);
Err(error.error)
}
};
metrics_guard.finish(&final_result);
@@ -393,29 +497,58 @@ impl SpacetimeClient {
procedure: &'static str,
call: impl FnOnce(&DbConnection, ReducerResultSender) + Send + 'static,
) -> Result<(), SpacetimeClientError> {
let started_at = Instant::now();
let metrics_guard = telemetry::begin_procedure(procedure);
let (sender, receiver) = oneshot::channel();
let result_sender = Arc::new(Mutex::new(Some(sender)));
let final_result = match self.acquire_connection().await {
let final_result = match self
.acquire_connection_with_timeout(self.config.procedure_timeout)
.await
{
Ok(lease) => {
let result = if let Some(connection) = lease.connection.as_ref() {
let (result, failed_stage) = if let Some(connection) = lease.connection.as_ref() {
call(&connection.connection, result_sender.clone());
match timeout(self.config.procedure_timeout, receiver).await {
Ok(inner) => match inner {
Ok(value) => value,
Err(_) => Err(SpacetimeClientError::ConnectDropped),
let stage = SpacetimeClientStage::ReducerResult;
(
match timeout(self.config.procedure_timeout, receiver).await {
Ok(inner) => match inner {
Ok(value) => value,
Err(_) => Err(SpacetimeClientError::ConnectDropped),
},
Err(_) => Err(Self::resolve_timeout_error(Some(connection), stage)),
},
Err(_) => Err(Self::resolve_timeout_error(Some(connection))),
}
stage,
)
} else {
Err(SpacetimeClientError::Runtime(
"SpacetimeDB 连接租约缺少连接".to_string(),
))
(
Err(SpacetimeClientError::Runtime(
"SpacetimeDB 连接租约缺少连接".to_string(),
)),
SpacetimeClientStage::ReducerResult,
)
};
self.release_connection(lease).await;
if let Err(error) = &result {
log_spacetime_client_failure(
"reducer",
procedure,
failed_stage,
started_at,
error,
);
}
result
}
Err(error) => Err(error),
Err(error) => {
log_spacetime_client_failure(
"reducer",
procedure,
error.stage,
started_at,
&error.error,
);
Err(error.error)
}
};
metrics_guard.finish(&final_result);
@@ -430,11 +563,22 @@ impl SpacetimeClient {
where
T: Send + 'static,
{
let started_at = Instant::now();
let metrics_guard = telemetry::begin_read(read_name);
let lease = match self.acquire_connection().await {
let lease = match self
.acquire_connection_with_timeout(self.config.procedure_timeout)
.await
{
Ok(lease) => lease,
Err(error) => {
let final_result = Err(error);
log_spacetime_client_failure(
"read",
read_name,
error.stage,
started_at,
&error.error,
);
let final_result = Err(error.error);
metrics_guard.finish(&final_result);
return final_result;
}
@@ -448,6 +592,15 @@ impl SpacetimeClient {
};
self.release_connection(lease).await;
if let Err(error) = &final_result {
log_spacetime_client_failure(
"read",
read_name,
SpacetimeClientStage::ReadCache,
started_at,
error,
);
}
metrics_guard.finish(&final_result);
final_result
}
@@ -460,14 +613,75 @@ impl SpacetimeClient {
self.creation_entry_config_cache.read().await.clone()
}
async fn acquire_connection(&self) -> Result<PooledConnectionLease, SpacetimeClientError> {
let permit = timeout(
self.config.procedure_timeout,
self.pool.permits.clone().acquire_owned(),
)
.await
.map_err(|_| SpacetimeClientError::Timeout)?
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
pub async fn health_check(&self, probe_timeout: Duration) -> SpacetimeClientHealthSnapshot {
let timeout = if probe_timeout.is_zero() {
DEFAULT_PROCEDURE_TIMEOUT
} else {
probe_timeout
};
let started_at = Instant::now();
let checked_at_micros = current_unix_micros();
let result = self.acquire_connection_with_timeout(timeout).await;
match result {
Ok(lease) => {
self.release_connection(lease).await;
let mut health_state = self.health_state.write().await;
health_state.last_success_at_micros = Some(checked_at_micros);
health_state.last_error = None;
SpacetimeClientHealthSnapshot {
ok: true,
stage: SpacetimeClientStage::Ready,
checked_at_micros,
elapsed_ms: duration_millis_u64(started_at.elapsed()),
timeout_ms: duration_millis_u64(timeout),
error: None,
last_success_at_micros: health_state.last_success_at_micros,
last_error: health_state.last_error.clone(),
}
}
Err(error) => {
log_spacetime_client_failure(
"health_check",
"spacetime_connection",
error.stage,
started_at,
&error.error,
);
let mut health_state = self.health_state.write().await;
let error_message = error.error.to_string();
health_state.last_error = Some(error_message.clone());
SpacetimeClientHealthSnapshot {
ok: false,
stage: error.stage,
checked_at_micros,
elapsed_ms: duration_millis_u64(started_at.elapsed()),
timeout_ms: duration_millis_u64(timeout),
error: Some(error_message),
last_success_at_micros: health_state.last_success_at_micros,
last_error: health_state.last_error.clone(),
}
}
}
}
async fn acquire_connection_with_timeout(
&self,
operation_timeout: Duration,
) -> Result<PooledConnectionLease, SpacetimeStageError> {
let permit = timeout(operation_timeout, self.pool.permits.clone().acquire_owned())
.await
.map_err(|_| {
SpacetimeStageError::new(
SpacetimeClientStage::PoolAcquire,
SpacetimeClientError::Timeout,
)
})?
.map_err(|error| {
SpacetimeStageError::new(
SpacetimeClientStage::PoolAcquire,
SpacetimeClientError::Runtime(error.to_string()),
)
})?;
loop {
for (slot_index, slot) in self.pool.slots.iter().enumerate() {
@@ -485,7 +699,7 @@ impl SpacetimeClient {
let connection = if let Some(connection) = reusable_connection {
connection
} else {
match self.build_pooled_connection().await {
match self.build_pooled_connection(operation_timeout).await {
Ok(connection) => connection,
Err(error) => {
let mut slot_guard = self.pool.slots[slot_index].lock().await;
@@ -507,7 +721,10 @@ impl SpacetimeClient {
}
}
async fn build_pooled_connection(&self) -> Result<PooledConnection, SpacetimeClientError> {
async fn build_pooled_connection(
&self,
operation_timeout: Duration,
) -> Result<PooledConnection, SpacetimeStageError> {
let config = self.config.clone();
let broken = Arc::new(AtomicBool::new(false));
let (sender, receiver) = oneshot::channel::<Result<(), SpacetimeClientError>>();
@@ -515,7 +732,7 @@ impl SpacetimeClient {
let broken_flag = broken.clone();
let disconnect_sender = connect_sender.clone();
let connection = timeout(
self.config.procedure_timeout,
operation_timeout,
tokio::task::spawn_blocking(move || {
DbConnection::builder()
.with_uri(config.server_url)
@@ -539,17 +756,41 @@ impl SpacetimeClient {
}),
)
.await
.map_err(|_| SpacetimeClientError::Timeout)?
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))??;
.map_err(|_| {
SpacetimeStageError::new(
SpacetimeClientStage::ConnectBuild,
SpacetimeClientError::Timeout,
)
})?
.map_err(|error| {
SpacetimeStageError::new(
SpacetimeClientStage::ConnectBuild,
SpacetimeClientError::Runtime(error.to_string()),
)
})?
.map_err(|error| SpacetimeStageError::new(SpacetimeClientStage::ConnectBuild, error))?;
let runner = connection.run_threaded();
timeout(self.config.procedure_timeout, receiver)
timeout(operation_timeout, receiver)
.await
.map_err(|_| SpacetimeClientError::Timeout)?
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
.map_err(|_| {
SpacetimeStageError::new(
SpacetimeClientStage::ConnectHandshake,
SpacetimeClientError::Timeout,
)
})?
.map_err(|_| {
SpacetimeStageError::new(
SpacetimeClientStage::ConnectHandshake,
SpacetimeClientError::ConnectDropped,
)
})?
.map_err(|error| {
SpacetimeStageError::new(SpacetimeClientStage::ConnectHandshake, error)
})?;
let read_model_subscriptions = self
.subscribe_cached_read_models(&connection, broken.clone())
.subscribe_cached_read_models(&connection, broken.clone(), operation_timeout)
.await?;
Ok(PooledConnection {
@@ -564,7 +805,8 @@ impl SpacetimeClient {
&self,
connection: &DbConnection,
broken: Arc<AtomicBool>,
) -> Result<Vec<SubscriptionHandle>, SpacetimeClientError> {
operation_timeout: Duration,
) -> Result<Vec<SubscriptionHandle>, SpacetimeStageError> {
let mut subscriptions = Vec::new();
for query in [
"SELECT * FROM public_work_gallery_entry",
@@ -581,7 +823,13 @@ impl SpacetimeClient {
"SELECT * FROM big_fish_gallery_view",
] {
let subscription = self
.subscribe_cached_read_model_query(connection, broken.clone(), query, true)
.subscribe_cached_read_model_query(
connection,
broken.clone(),
query,
true,
operation_timeout,
)
.await?;
subscriptions.push(subscription);
}
@@ -602,7 +850,13 @@ impl SpacetimeClient {
"SELECT * FROM asset_object",
] {
if let Ok(subscription) = self
.subscribe_cached_read_model_query(connection, broken.clone(), query, false)
.subscribe_cached_read_model_query(
connection,
broken.clone(),
query,
false,
operation_timeout,
)
.await
{
subscriptions.push(subscription);
@@ -618,7 +872,8 @@ impl SpacetimeClient {
broken: Arc<AtomicBool>,
query: &'static str,
mark_broken_on_error: bool,
) -> Result<SubscriptionHandle, SpacetimeClientError> {
operation_timeout: Duration,
) -> Result<SubscriptionHandle, SpacetimeStageError> {
let (sender, receiver) = oneshot::channel::<Result<(), SpacetimeClientError>>();
let applied_sender = Arc::new(Mutex::new(Some(sender)));
let on_applied_sender = applied_sender.clone();
@@ -640,10 +895,23 @@ impl SpacetimeClient {
})
.subscribe(query);
timeout(self.config.procedure_timeout, receiver)
timeout(operation_timeout, receiver)
.await
.map_err(|_| SpacetimeClientError::Timeout)?
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
.map_err(|_| {
SpacetimeStageError::new(
SpacetimeClientStage::ReadModelSubscribe,
SpacetimeClientError::Timeout,
)
})?
.map_err(|_| {
SpacetimeStageError::new(
SpacetimeClientStage::ReadModelSubscribe,
SpacetimeClientError::ConnectDropped,
)
})?
.map_err(|error| {
SpacetimeStageError::new(SpacetimeClientStage::ReadModelSubscribe, error)
})?;
Ok(subscription)
}
@@ -663,7 +931,10 @@ impl SpacetimeClient {
}
// 超时后必须统一归还租约;若连接已先一步断开则回传断线,否则标记坏连接并回传超时。
fn resolve_timeout_error(connection: Option<&PooledConnection>) -> SpacetimeClientError {
fn resolve_timeout_error(
connection: Option<&PooledConnection>,
_stage: SpacetimeClientStage,
) -> SpacetimeClientError {
if let Some(connection) = connection {
if connection.is_broken() {
return SpacetimeClientError::ConnectDropped;
@@ -686,6 +957,27 @@ fn current_public_work_day() -> i64 {
current_unix_micros().div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS)
}
fn duration_millis_u64(duration: Duration) -> u64 {
duration.as_millis().min(u64::MAX as u128) as u64
}
fn log_spacetime_client_failure(
operation_kind: &'static str,
operation_name: &'static str,
stage: SpacetimeClientStage,
started_at: Instant,
error: &SpacetimeClientError,
) {
warn!(
operation_kind,
operation_name,
spacetime_stage = stage.as_str(),
elapsed_ms = duration_millis_u64(started_at.elapsed()),
error = %error,
"SpacetimeDB client operation failed"
);
}
fn public_work_recent_play_counts(
connection: &DbConnection,
source_type: &str,

View File

@@ -8,9 +8,9 @@ pub use shared_contracts::jump_hop::{
JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse,
JumpHopWorkspaceCreateRequest,
JumpHopTileFaceAsset, JumpHopTileFaceAssets, JumpHopTileFaceKey, JumpHopTileType,
JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, JumpHopWorkProfileResponse,
JumpHopWorkSummaryResponse, JumpHopWorksResponse, JumpHopWorkspaceCreateRequest,
};
pub(crate) fn map_jump_hop_agent_session_procedure_result(
@@ -267,6 +267,33 @@ fn map_tile_asset(snapshot: JumpHopTileAssetSnapshot) -> JumpHopTileAsset {
visual_height: snapshot.visual_height,
top_surface_radius: snapshot.top_surface_radius,
landing_radius: snapshot.landing_radius,
face_assets: snapshot.face_assets.map(map_tile_face_assets),
}
}
fn map_tile_face_assets(snapshot: JumpHopTileFaceAssetsSnapshot) -> JumpHopTileFaceAssets {
JumpHopTileFaceAssets {
top: map_tile_face_asset(snapshot.top),
front: map_tile_face_asset(snapshot.front),
right: map_tile_face_asset(snapshot.right),
back: map_tile_face_asset(snapshot.back),
left: map_tile_face_asset(snapshot.left),
bottom: map_tile_face_asset(snapshot.bottom),
}
}
fn map_tile_face_asset(snapshot: JumpHopTileFaceAssetSnapshot) -> JumpHopTileFaceAsset {
JumpHopTileFaceAsset {
face: parse_tile_face_key(&snapshot.face),
asset_id: snapshot.asset_id,
image_src: snapshot.image_src,
image_object_key: snapshot.image_object_key,
asset_object_id: snapshot.asset_object_id,
generation_provider: snapshot.generation_provider,
prompt: snapshot.prompt,
width: snapshot.width,
height: snapshot.height,
source_atlas_cell: snapshot.source_atlas_cell,
}
}
@@ -405,6 +432,17 @@ fn parse_tile_type(value: &str) -> JumpHopTileType {
}
}
fn parse_tile_face_key(value: &str) -> JumpHopTileFaceKey {
match value {
"front" => JumpHopTileFaceKey::Front,
"right" => JumpHopTileFaceKey::Right,
"back" => JumpHopTileFaceKey::Back,
"left" => JumpHopTileFaceKey::Left,
"bottom" => JumpHopTileFaceKey::Bottom,
_ => JumpHopTileFaceKey::Top,
}
}
fn parse_domain_tile_type(value: crate::module_bindings::JumpHopTileType) -> JumpHopTileType {
match value {
crate::module_bindings::JumpHopTileType::Start => JumpHopTileType::Start,

View File

@@ -30,6 +30,17 @@ impl From<module_runtime::CreationEntryEventBannersAdminUpsertInput>
}
}
/// 将业务层公开作品互动配置保存输入转换为 SpacetimeDB 生成绑定类型。
impl From<module_runtime::PublicWorkInteractionConfigAdminUpsertInput>
for PublicWorkInteractionConfigAdminUpsertInput
{
fn from(input: module_runtime::PublicWorkInteractionConfigAdminUpsertInput) -> Self {
Self {
public_work_interactions_json: input.public_work_interactions_json,
}
}
}
impl From<module_runtime::AdminWorkVisibilityListInput> for AdminWorkVisibilityListInput {
fn from(input: module_runtime::AdminWorkVisibilityListInput) -> Self {
Self {
@@ -323,6 +334,7 @@ pub(crate) fn build_creation_entry_config_record_from_rows(
})
.collect(),
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
public_work_interactions_json: header.public_work_interactions_json,
},
)
}
@@ -376,6 +388,7 @@ fn map_creation_entry_config_snapshot(
})
.collect(),
updated_at_micros: snapshot.updated_at_micros,
public_work_interactions_json: snapshot.public_work_interactions_json,
}
}
@@ -432,6 +445,7 @@ mod tests {
event_starts_at_text: None,
event_ends_at_text: None,
event_banners_json: None,
public_work_interactions_json: None,
}
}
@@ -514,6 +528,7 @@ mod tests {
unified_creation_spec_json: None,
}],
updated_at_micros: 1_000_000,
public_work_interactions_json: None,
});
let jump_hop = record

View File

@@ -476,6 +476,8 @@ pub mod jump_hop_runtime_run_row_type;
pub mod jump_hop_runtime_run_table;
pub mod jump_hop_scoring_type;
pub mod jump_hop_tile_asset_snapshot_type;
pub mod jump_hop_tile_face_asset_snapshot_type;
pub mod jump_hop_tile_face_assets_snapshot_type;
pub mod jump_hop_tile_type_type;
pub mod jump_hop_work_delete_input_type;
pub mod jump_hop_work_get_input_type;
@@ -603,6 +605,7 @@ pub mod public_work_detail_entry_table;
pub mod public_work_detail_entry_type;
pub mod public_work_gallery_entry_table;
pub mod public_work_gallery_entry_type;
pub mod public_work_interaction_config_admin_upsert_input_type;
pub mod public_work_like_table;
pub mod public_work_like_type;
pub mod public_work_play_daily_stat_table;
@@ -1036,6 +1039,7 @@ pub mod upsert_custom_world_profile_reducer;
pub mod upsert_npc_state_and_return_procedure;
pub mod upsert_npc_state_reducer;
pub mod upsert_platform_browse_history_and_return_procedure;
pub mod upsert_public_work_interaction_config_procedure;
pub mod upsert_runtime_setting_and_return_procedure;
pub mod upsert_runtime_snapshot_and_return_procedure;
pub mod upsert_visual_novel_run_snapshot_procedure;
@@ -1596,6 +1600,8 @@ pub use jump_hop_runtime_run_row_type::JumpHopRuntimeRunRow;
pub use jump_hop_runtime_run_table::*;
pub use jump_hop_scoring_type::JumpHopScoring;
pub use jump_hop_tile_asset_snapshot_type::JumpHopTileAssetSnapshot;
pub use jump_hop_tile_face_asset_snapshot_type::JumpHopTileFaceAssetSnapshot;
pub use jump_hop_tile_face_assets_snapshot_type::JumpHopTileFaceAssetsSnapshot;
pub use jump_hop_tile_type_type::JumpHopTileType;
pub use jump_hop_work_delete_input_type::JumpHopWorkDeleteInput;
pub use jump_hop_work_get_input_type::JumpHopWorkGetInput;
@@ -1723,6 +1729,7 @@ pub use public_work_detail_entry_table::*;
pub use public_work_detail_entry_type::PublicWorkDetailEntry;
pub use public_work_gallery_entry_table::*;
pub use public_work_gallery_entry_type::PublicWorkGalleryEntry;
pub use public_work_interaction_config_admin_upsert_input_type::PublicWorkInteractionConfigAdminUpsertInput;
pub use public_work_like_table::*;
pub use public_work_like_type::PublicWorkLike;
pub use public_work_play_daily_stat_table::*;
@@ -2156,6 +2163,7 @@ pub use upsert_custom_world_profile_reducer::upsert_custom_world_profile;
pub use upsert_npc_state_and_return_procedure::upsert_npc_state_and_return;
pub use upsert_npc_state_reducer::upsert_npc_state;
pub use upsert_platform_browse_history_and_return_procedure::upsert_platform_browse_history_and_return;
pub use upsert_public_work_interaction_config_procedure::upsert_public_work_interaction_config;
pub use upsert_runtime_setting_and_return_procedure::upsert_runtime_setting_and_return;
pub use upsert_runtime_snapshot_and_return_procedure::upsert_runtime_snapshot_and_return;
pub use upsert_visual_novel_run_snapshot_procedure::upsert_visual_novel_run_snapshot;

View File

@@ -19,6 +19,7 @@ pub struct CreationEntryConfigSnapshot {
pub event_banners_json: Option<String>,
pub creation_types: Vec<CreationEntryTypeSnapshot>,
pub updated_at_micros: i64,
pub public_work_interactions_json: Option<String>,
}
impl __sdk::InModule for CreationEntryConfigSnapshot {

View File

@@ -22,6 +22,7 @@ pub struct CreationEntryConfig {
pub event_starts_at_text: Option<String>,
pub event_ends_at_text: Option<String>,
pub event_banners_json: Option<String>,
pub public_work_interactions_json: Option<String>,
}
impl __sdk::InModule for CreationEntryConfig {
@@ -47,6 +48,8 @@ pub struct CreationEntryConfigCols {
pub event_starts_at_text: __sdk::__query_builder::Col<CreationEntryConfig, Option<String>>,
pub event_ends_at_text: __sdk::__query_builder::Col<CreationEntryConfig, Option<String>>,
pub event_banners_json: __sdk::__query_builder::Col<CreationEntryConfig, Option<String>>,
pub public_work_interactions_json:
__sdk::__query_builder::Col<CreationEntryConfig, Option<String>>,
}
impl __sdk::__query_builder::HasCols for CreationEntryConfig {
@@ -77,6 +80,10 @@ impl __sdk::__query_builder::HasCols for CreationEntryConfig {
),
event_ends_at_text: __sdk::__query_builder::Col::new(table_name, "event_ends_at_text"),
event_banners_json: __sdk::__query_builder::Col::new(table_name, "event_banners_json"),
public_work_interactions_json: __sdk::__query_builder::Col::new(
table_name,
"public_work_interactions_json",
),
}
}
}

View File

@@ -4,6 +4,8 @@
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::jump_hop_tile_face_assets_snapshot_type::JumpHopTileFaceAssetsSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct JumpHopTileAssetSnapshot {
@@ -19,6 +21,7 @@ pub struct JumpHopTileAssetSnapshot {
pub visual_height: u32,
pub top_surface_radius: f32,
pub landing_radius: f32,
pub face_assets: Option<JumpHopTileFaceAssetsSnapshot>,
}
impl __sdk::InModule for JumpHopTileAssetSnapshot {

View File

@@ -0,0 +1,24 @@
// 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 JumpHopTileFaceAssetSnapshot {
pub face: String,
pub asset_id: String,
pub image_src: String,
pub image_object_key: String,
pub asset_object_id: String,
pub generation_provider: String,
pub prompt: String,
pub width: u32,
pub height: u32,
pub source_atlas_cell: String,
}
impl __sdk::InModule for JumpHopTileFaceAssetSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,22 @@
// 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::jump_hop_tile_face_asset_snapshot_type::JumpHopTileFaceAssetSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct JumpHopTileFaceAssetsSnapshot {
pub top: JumpHopTileFaceAssetSnapshot,
pub front: JumpHopTileFaceAssetSnapshot,
pub right: JumpHopTileFaceAssetSnapshot,
pub back: JumpHopTileFaceAssetSnapshot,
pub left: JumpHopTileFaceAssetSnapshot,
pub bottom: JumpHopTileFaceAssetSnapshot,
}
impl __sdk::InModule for JumpHopTileFaceAssetsSnapshot {
type Module = super::RemoteModule;
}

View File

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

View File

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

View File

@@ -115,6 +115,34 @@ impl SpacetimeClient {
Ok(config)
}
/// 调用 SpacetimeDB procedure 保存公开作品互动配置并刷新缓存。
pub async fn upsert_public_work_interaction_config(
&self,
input: module_runtime::PublicWorkInteractionConfigAdminUpsertInput,
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
let procedure_input: PublicWorkInteractionConfigAdminUpsertInput = input.into();
let config = self
.call_after_connect(
"upsert_public_work_interaction_config",
move |connection, sender| {
connection
.procedures()
.upsert_public_work_interaction_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?;
self.cache_creation_entry_config(config.clone()).await;
Ok(config)
}
pub async fn admin_list_work_visibility(
&self,
admin_user_id: String,