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

合入 master 的拼消消、微信能力、OpenSSL 3.2 和 SpacetimeDB 2.4.1 更新
保留外部内容生成 queue/inline、worker lease 与动态扩缩容口径
补齐拼图后台图片生成队列轮询和运行态返回恢复
同步容器、生产运维和 Hermes 共享记忆中的 worker 文档
This commit is contained in:
2026-06-09 16:55:32 +08:00
497 changed files with 66318 additions and 13329 deletions

View File

@@ -58,20 +58,6 @@ pub(crate) fn map_custom_world_library_detail_result(
})
}
pub(crate) fn map_custom_world_gallery_list_result(
result: CustomWorldGalleryListResult,
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.entries
.into_iter()
.map(map_custom_world_gallery_entry_snapshot)
.collect::<Result<Vec<_>, _>>()?)
}
pub(crate) fn map_custom_world_library_mutation_result(
result: CustomWorldLibraryMutationResult,
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {

View File

@@ -1,10 +1,11 @@
use super::*;
pub use shared_contracts::jump_hop::{
JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset,
JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse,
JumpHopDefaultCharacter, JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse,
JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus,
JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath,
JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump,
JumpHopLeaderboardEntry, JumpHopLeaderboardResponse, JumpHopPath, JumpHopPlatform,
JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
@@ -61,15 +62,40 @@ pub(crate) fn map_jump_hop_run_procedure_result(
Ok(map_jump_hop_run_snapshot(run))
}
pub(crate) fn map_jump_hop_leaderboard_procedure_result(
result: JumpHopLeaderboardProcedureResult,
) -> Result<JumpHopLeaderboardResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(JumpHopLeaderboardResponse {
profile_id: result.profile_id,
items: result
.items
.into_iter()
.map(map_jump_hop_leaderboard_entry_snapshot)
.collect(),
viewer_best: result
.viewer_best
.map(map_jump_hop_leaderboard_entry_snapshot),
})
}
pub(crate) fn map_jump_hop_gallery_card_view_row(
row: JumpHopGalleryCardViewRow,
) -> JumpHopGalleryCardResponse {
let theme_text = if row.theme_text.trim().is_empty() {
row.work_title.clone()
} else {
row.theme_text.clone()
};
JumpHopGalleryCardResponse {
public_work_code: row.public_work_code,
work_id: row.work_id,
profile_id: row.profile_id,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
theme_text,
work_title: row.work_title,
work_description: row.work_description,
cover_image_src: empty_string_to_none(row.cover_image_src),
@@ -104,15 +130,22 @@ fn map_jump_hop_session_snapshot(
fn map_jump_hop_work_snapshot(
snapshot: JumpHopWorkSnapshot,
) -> Result<JumpHopWorkProfileResponse, SpacetimeClientError> {
let theme_text = if snapshot.theme_text.trim().is_empty() {
snapshot.work_title.clone()
} else {
snapshot.theme_text.clone()
};
let draft = JumpHopDraftResponse {
template_id: "jump-hop".to_string(),
template_name: "跳一跳".to_string(),
profile_id: Some(snapshot.profile_id.clone()),
theme_text: theme_text.clone(),
work_title: snapshot.work_title.clone(),
work_description: snapshot.work_description.clone(),
theme_tags: snapshot.theme_tags.clone(),
difficulty: parse_difficulty(&snapshot.difficulty),
style_preset: parse_style_preset(&snapshot.style_preset),
default_character: Some(default_jump_hop_character()),
character_prompt: snapshot.character_prompt.clone(),
tile_prompt: snapshot.tile_prompt.clone(),
end_mood_prompt: snapshot.end_mood_prompt.clone(),
@@ -126,6 +159,7 @@ fn map_jump_hop_work_snapshot(
.collect(),
path: Some(map_jump_hop_path(snapshot.path.clone())),
cover_composite: snapshot.cover_composite.clone(),
back_button_asset: snapshot.back_button_asset.clone().map(map_character_asset),
generation_status: parse_generation_status(&snapshot.generation_status),
};
let character_asset = draft
@@ -143,6 +177,7 @@ fn map_jump_hop_work_snapshot(
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
source_session_id: empty_string_to_none(snapshot.source_session_id),
theme_text,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_tags: snapshot.theme_tags,
@@ -159,6 +194,7 @@ fn map_jump_hop_work_snapshot(
},
draft,
path: map_jump_hop_path(snapshot.path),
default_character: Some(default_jump_hop_character()),
character_asset,
tile_atlas_asset,
tile_assets: snapshot
@@ -166,19 +202,27 @@ fn map_jump_hop_work_snapshot(
.into_iter()
.map(map_tile_asset)
.collect(),
back_button_asset: snapshot.back_button_asset.map(map_character_asset),
})
}
fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftResponse {
let theme_text = if snapshot.theme_text.trim().is_empty() {
snapshot.work_title.clone()
} else {
snapshot.theme_text.clone()
};
JumpHopDraftResponse {
template_id: snapshot.template_id,
template_name: snapshot.template_name,
profile_id: snapshot.profile_id,
theme_text,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_tags: snapshot.theme_tags,
difficulty: parse_difficulty(&snapshot.difficulty),
style_preset: parse_style_preset(&snapshot.style_preset),
default_character: Some(default_jump_hop_character()),
character_prompt: snapshot.character_prompt,
tile_prompt: snapshot.tile_prompt,
end_mood_prompt: snapshot.end_mood_prompt,
@@ -191,6 +235,7 @@ fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftRe
.collect(),
path: snapshot.path.map(map_jump_hop_path),
cover_composite: snapshot.cover_composite,
back_button_asset: snapshot.back_button_asset.map(map_character_asset),
generation_status: parse_generation_status(&snapshot.generation_status),
}
}
@@ -211,10 +256,13 @@ fn map_character_asset(snapshot: JumpHopCharacterAssetSnapshot) -> JumpHopCharac
fn map_tile_asset(snapshot: JumpHopTileAssetSnapshot) -> JumpHopTileAsset {
JumpHopTileAsset {
tile_type: parse_tile_type(&snapshot.tile_type),
tile_id: snapshot.tile_id,
image_src: snapshot.image_src,
image_object_key: snapshot.image_object_key,
asset_object_id: snapshot.asset_object_id,
source_atlas_cell: snapshot.source_atlas_cell,
atlas_row: snapshot.atlas_row,
atlas_col: snapshot.atlas_col,
visual_width: snapshot.visual_width,
visual_height: snapshot.visual_height,
top_surface_radius: snapshot.top_surface_radius,
@@ -263,6 +311,8 @@ fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunS
crate::module_bindings::JumpHopRunStatus::Playing => JumpHopRunStatus::Playing,
},
current_platform_index: snapshot.current_platform_index,
successful_jump_count: snapshot.current_platform_index,
duration_ms: jump_hop_duration_ms(snapshot.started_at_ms, snapshot.finished_at_ms),
score: snapshot.score,
combo: snapshot.combo,
path: map_jump_hop_path(snapshot.path),
@@ -286,6 +336,35 @@ fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunS
}
}
fn map_jump_hop_leaderboard_entry_snapshot(
snapshot: JumpHopLeaderboardEntrySnapshot,
) -> JumpHopLeaderboardEntry {
JumpHopLeaderboardEntry {
rank: snapshot.rank,
player_id: snapshot.player_id,
display_name: String::new(),
successful_jump_count: snapshot.successful_jump_count,
duration_ms: snapshot.duration_ms,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn default_jump_hop_character() -> JumpHopDefaultCharacter {
JumpHopDefaultCharacter {
character_id: "jump-hop-default-runner".to_string(),
display_name: "默认角色".to_string(),
model_kind: "builtin-three".to_string(),
body_color: "#f59e0b".to_string(),
accent_color: "#2563eb".to_string(),
}
}
fn jump_hop_duration_ms(started_at_ms: u64, finished_at_ms: Option<u64>) -> u64 {
finished_at_ms
.unwrap_or(started_at_ms)
.saturating_sub(started_at_ms)
}
fn parse_difficulty(value: &str) -> JumpHopDifficulty {
match value {
"easy" => JumpHopDifficulty::Easy,

View File

@@ -0,0 +1,289 @@
use super::*;
pub use shared_contracts::puzzle_clear::{
PuzzleClearActionRequest, PuzzleClearActionResponse, PuzzleClearActionType,
PuzzleClearBoardCell, PuzzleClearBoardSnapshot, PuzzleClearCardAsset, PuzzleClearDraftResponse,
PuzzleClearGenerationStatus, PuzzleClearImageAsset, PuzzleClearNextLevelRequest,
PuzzleClearPatternGroup, PuzzleClearRetryLevelRequest, PuzzleClearRunResponse,
PuzzleClearRunStatus, PuzzleClearRuntimeSnapshotResponse, PuzzleClearSessionResponse,
PuzzleClearSessionSnapshotResponse, PuzzleClearStartRunRequest, PuzzleClearSwapRequest,
PuzzleClearTimeUpRequest, PuzzleClearWorkDetailResponse, PuzzleClearWorkMutationResponse,
PuzzleClearWorkProfileResponse, PuzzleClearWorkSummaryResponse, PuzzleClearWorksResponse,
PuzzleClearWorkspaceCreateRequest,
};
pub(crate) fn map_puzzle_clear_agent_session_procedure_result(
result: PuzzleClearAgentSessionProcedureResult,
) -> Result<PuzzleClearSessionSnapshotResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("puzzle clear agent session 快照"))?;
Ok(map_puzzle_clear_session_snapshot(session))
}
pub(crate) fn map_puzzle_clear_work_procedure_result(
result: PuzzleClearWorkProcedureResult,
) -> Result<PuzzleClearWorkProfileResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let work = result
.work
.ok_or_else(|| SpacetimeClientError::missing_snapshot("puzzle clear work 快照"))?;
Ok(map_puzzle_clear_work_snapshot(work))
}
pub(crate) fn map_puzzle_clear_works_procedure_result(
result: PuzzleClearWorksProcedureResult,
) -> Result<Vec<PuzzleClearWorkProfileResponse>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.items
.into_iter()
.map(map_puzzle_clear_work_snapshot)
.collect())
}
pub(crate) fn map_puzzle_clear_run_procedure_result(
result: PuzzleClearRunProcedureResult,
) -> Result<PuzzleClearRuntimeSnapshotResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let run = result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("puzzle clear run 快照"))?;
Ok(map_puzzle_clear_run_snapshot(run))
}
pub(crate) fn map_puzzle_clear_gallery_card_view_row(
row: PuzzleClearGalleryCardViewRow,
) -> PuzzleClearWorkSummaryResponse {
PuzzleClearWorkSummaryResponse {
runtime_kind: "puzzle-clear".to_string(),
work_id: row.work_id,
profile_id: row.profile_id,
owner_user_id: row.owner_user_id,
source_session_id: None,
work_title: row.work_title,
work_description: row.work_description,
theme_prompt: row.theme_prompt,
cover_image_src: row.cover_image_src,
publication_status: normalize_publication_status(&row.publication_status).to_string(),
play_count: row.play_count,
updated_at: format_timestamp_micros(row.updated_at_micros),
published_at: row.published_at_micros.map(format_timestamp_micros),
publish_ready: true,
generation_status: parse_generation_status(&row.generation_status),
}
}
fn map_puzzle_clear_session_snapshot(
snapshot: PuzzleClearAgentSessionSnapshot,
) -> PuzzleClearSessionSnapshotResponse {
PuzzleClearSessionSnapshotResponse {
session_id: snapshot.session_id,
owner_user_id: snapshot.owner_user_id,
status: parse_generation_status(&snapshot.status),
draft: snapshot.draft.map(map_puzzle_clear_draft_snapshot),
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_puzzle_clear_work_snapshot(
snapshot: PuzzleClearWorkSnapshot,
) -> PuzzleClearWorkProfileResponse {
let atlas_asset = map_image_asset(snapshot.atlas_asset.clone());
let pattern_groups = snapshot
.pattern_groups
.clone()
.into_iter()
.map(map_pattern_group)
.collect::<Vec<_>>();
let card_assets = snapshot
.card_assets
.clone()
.into_iter()
.map(map_card_asset)
.collect::<Vec<_>>();
let board_background_asset = snapshot.board_background_asset.clone().map(map_image_asset);
let draft = PuzzleClearDraftResponse {
template_id: "puzzle-clear".to_string(),
template_name: "拼消消".to_string(),
profile_id: Some(snapshot.profile_id.clone()),
work_title: snapshot.work_title.clone(),
work_description: snapshot.work_description.clone(),
theme_prompt: snapshot.theme_prompt.clone(),
board_background_prompt: snapshot.board_background_prompt.clone(),
generate_board_background: snapshot.generate_board_background,
board_background_asset: board_background_asset.clone(),
card_back_image_src: snapshot.card_back_image_src.clone(),
atlas_asset: Some(atlas_asset.clone()),
pattern_groups: pattern_groups.clone(),
card_assets: card_assets.clone(),
generation_status: parse_generation_status(&snapshot.generation_status),
};
PuzzleClearWorkProfileResponse {
summary: PuzzleClearWorkSummaryResponse {
runtime_kind: "puzzle-clear".to_string(),
work_id: snapshot.work_id,
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
source_session_id: empty_string_to_none(snapshot.source_session_id),
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_prompt: snapshot.theme_prompt,
cover_image_src: snapshot.cover_image_src,
publication_status: normalize_publication_status(&snapshot.publication_status)
.to_string(),
play_count: snapshot.play_count,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
publish_ready: snapshot.publish_ready,
generation_status: parse_generation_status(&snapshot.generation_status),
},
draft,
board_background_asset,
atlas_asset,
pattern_groups,
card_assets,
}
}
fn map_puzzle_clear_draft_snapshot(snapshot: PuzzleClearDraftSnapshot) -> PuzzleClearDraftResponse {
PuzzleClearDraftResponse {
template_id: snapshot.template_id,
template_name: snapshot.template_name,
profile_id: snapshot.profile_id,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_prompt: snapshot.theme_prompt,
board_background_prompt: snapshot.board_background_prompt,
generate_board_background: snapshot.generate_board_background,
board_background_asset: snapshot.board_background_asset.map(map_image_asset),
card_back_image_src: snapshot.card_back_image_src,
atlas_asset: snapshot.atlas_asset.map(map_image_asset),
pattern_groups: snapshot
.pattern_groups
.into_iter()
.map(map_pattern_group)
.collect(),
card_assets: snapshot
.card_assets
.into_iter()
.map(map_card_asset)
.collect(),
generation_status: parse_generation_status(&snapshot.generation_status),
}
}
fn map_image_asset(snapshot: PuzzleClearImageAssetSnapshot) -> PuzzleClearImageAsset {
PuzzleClearImageAsset {
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,
}
}
fn map_pattern_group(snapshot: PuzzleClearPatternGroupSnapshot) -> PuzzleClearPatternGroup {
PuzzleClearPatternGroup {
group_id: snapshot.group_id,
shape: snapshot.shape,
width: snapshot.width,
height: snapshot.height,
atlas_x: snapshot.atlas_x,
atlas_y: snapshot.atlas_y,
atlas_width: snapshot.atlas_width,
atlas_height: snapshot.atlas_height,
}
}
fn map_card_asset(snapshot: PuzzleClearCardAssetSnapshot) -> PuzzleClearCardAsset {
PuzzleClearCardAsset {
card_id: snapshot.card_id,
group_id: snapshot.group_id,
shape: snapshot.shape,
orientation: snapshot.orientation,
part_x: snapshot.part_x,
part_y: snapshot.part_y,
image_src: snapshot.image_src,
image_object_key: snapshot.image_object_key,
asset_object_id: snapshot.asset_object_id,
source_atlas_cell: snapshot.source_atlas_cell,
}
}
fn map_puzzle_clear_run_snapshot(
snapshot: PuzzleClearRuntimeSnapshot,
) -> PuzzleClearRuntimeSnapshotResponse {
PuzzleClearRuntimeSnapshotResponse {
run_id: snapshot.run_id,
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
runtime_mode: None,
status: parse_run_status(&snapshot.status),
level_index: snapshot.level_index,
clears_done: snapshot.clears_done,
target_clears: snapshot.target_clears,
level_duration_seconds: snapshot.level_duration_seconds,
level_started_at_ms: snapshot.level_started_at_ms,
board: PuzzleClearBoardSnapshot {
rows: snapshot.board.rows,
cols: snapshot.board.cols,
cells: snapshot
.board
.cells
.into_iter()
.map(|cell| PuzzleClearBoardCell {
row: cell.row,
col: cell.col,
card: cell.card.map(map_card_asset),
locked_group_id: cell.locked_group_id,
})
.collect(),
},
ready_columns: snapshot
.ready_columns
.into_iter()
.map(|column| column.into_iter().map(map_card_asset).collect())
.collect(),
started_at_ms: snapshot.started_at_ms,
finished_at_ms: snapshot.finished_at_ms,
}
}
fn parse_generation_status(value: &str) -> PuzzleClearGenerationStatus {
match value {
"generating" => PuzzleClearGenerationStatus::Generating,
"ready" => PuzzleClearGenerationStatus::Ready,
"failed" => PuzzleClearGenerationStatus::Failed,
_ => PuzzleClearGenerationStatus::Draft,
}
}
fn parse_run_status(value: &str) -> PuzzleClearRunStatus {
match value {
"level_failed" => PuzzleClearRunStatus::LevelFailed,
"level_cleared" => PuzzleClearRunStatus::LevelCleared,
"finished" => PuzzleClearRunStatus::Finished,
_ => PuzzleClearRunStatus::Playing,
}
}
fn normalize_publication_status(value: &str) -> &str {
match value {
"Published" | "published" => "published",
_ => "draft",
}
}

View File

@@ -296,26 +296,30 @@ pub(crate) fn build_creation_entry_config_record_from_rows(
event_banners_json: header.event_banners_json,
creation_types: 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,
category_id: creation_entry_text_or_default(
item.category_id,
module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_ID,
),
category_label: creation_entry_text_or_default(
item.category_label,
module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_LABEL,
),
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at.to_micros_since_unix_epoch(),
unified_creation_spec_json: item.unified_creation_spec_json,
.map(|item| {
normalize_creation_entry_type_snapshot(
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,
category_id: creation_entry_text_or_default(
item.category_id,
module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_ID,
),
category_label: creation_entry_text_or_default(
item.category_label,
module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_LABEL,
),
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at.to_micros_since_unix_epoch(),
unified_creation_spec_json: item.unified_creation_spec_json,
},
)
})
.collect(),
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
@@ -353,20 +357,22 @@ fn map_creation_entry_config_snapshot(
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,
category_id: item.category_id,
category_label: item.category_label,
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at_micros,
unified_creation_spec_json: item.unified_creation_spec_json,
.map(|item| {
normalize_creation_entry_type_snapshot(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,
category_id: item.category_id,
category_label: item.category_label,
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at_micros,
unified_creation_spec_json: item.unified_creation_spec_json,
})
})
.collect(),
updated_at_micros: snapshot.updated_at_micros,
@@ -380,6 +386,150 @@ fn creation_entry_text_or_default(value: Option<String>, default_value: &str) ->
.unwrap_or_else(|| default_value.to_string())
}
fn normalize_creation_entry_type_snapshot(
item: module_runtime::CreationEntryTypeSnapshot,
) -> module_runtime::CreationEntryTypeSnapshot {
// 中文注释:旧库里残留的跳一跳系统默认入口行仍会从订阅缓存命中,这里统一做读模型纠偏,
// 这样无论走订阅缓存还是 procedure 回退,创作页都只会看到新的跳一跳入口口径。
if item.id == "jump-hop"
&& item.title == "跳一跳"
&& item.subtitle == "俯视角跳跃闯关"
&& item.badge == "可创建"
&& item.image_src == "/creation-type-references/puzzle.webp"
&& item.visible
&& item.open
&& item.sort_order == 45
{
return module_runtime::CreationEntryTypeSnapshot {
subtitle: "主题驱动平台跳跃".to_string(),
image_src: "/creation-type-references/jump-hop.webp".to_string(),
..item
};
}
item
}
#[cfg(test)]
mod tests {
use super::*;
use spacetimedb_sdk::Timestamp;
fn build_creation_entry_header() -> CreationEntryConfig {
CreationEntryConfig {
config_id: "creation-entry-config".to_string(),
start_title: "新建作品".to_string(),
start_description: "选择模板后进入对应的创作表单。".to_string(),
start_idle_badge: "模板 Tab".to_string(),
start_busy_badge: "正在开启".to_string(),
modal_title: "选择创作类型".to_string(),
modal_description: "先选玩法类型,再进入对应创作工作台。".to_string(),
updated_at: Timestamp::from_micros_since_unix_epoch(1_000_000),
event_title: None,
event_description: None,
event_cover_image_src: None,
event_prize_pool_mud_points: 0,
event_starts_at_text: None,
event_ends_at_text: None,
event_banners_json: None,
}
}
fn build_old_jump_hop_row() -> CreationEntryTypeConfig {
CreationEntryTypeConfig {
id: "jump-hop".to_string(),
title: "跳一跳".to_string(),
subtitle: "俯视角跳跃闯关".to_string(),
badge: "可创建".to_string(),
image_src: "/creation-type-references/puzzle.webp".to_string(),
visible: true,
open: true,
sort_order: 45,
updated_at: Timestamp::from_micros_since_unix_epoch(2_000_000),
category_id: Some("recommended".to_string()),
category_label: Some("热门推荐".to_string()),
category_sort_order: 20,
unified_creation_spec_json: None,
}
}
#[test]
fn build_creation_entry_config_record_from_rows_normalizes_old_jump_hop_row() {
let record = build_creation_entry_config_record_from_rows(
build_creation_entry_header(),
vec![build_old_jump_hop_row()],
);
let jump_hop = record
.creation_types
.iter()
.find(|item| item.id == "jump-hop")
.expect("should contain jump-hop");
assert_eq!(jump_hop.subtitle, "主题驱动平台跳跃");
assert_eq!(
jump_hop.image_src,
"/creation-type-references/jump-hop.webp"
);
}
#[test]
fn map_creation_entry_config_snapshot_normalizes_old_jump_hop_snapshot() {
let record = map_creation_entry_config_snapshot(CreationEntryConfigSnapshot {
config_id: "creation-entry-config".to_string(),
start_card: CreationEntryStartCardSnapshot {
title: "新建作品".to_string(),
description: "选择模板后进入对应的创作表单。".to_string(),
idle_badge: "模板 Tab".to_string(),
busy_badge: "正在开启".to_string(),
},
type_modal: CreationEntryTypeModalSnapshot {
title: "选择创作类型".to_string(),
description: "先选玩法类型,再进入对应创作工作台。".to_string(),
},
event_banner: CreationEntryEventBannerSnapshot {
title: "主题创作赛".to_string(),
description: "用温暖的色彩,捏出秋天的故事。".to_string(),
cover_image_src: "/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png".to_string(),
prize_pool_mud_points: 58_000,
starts_at_text: "2024.10.20 10:00".to_string(),
ends_at_text: "2024.11.20 23:59".to_string(),
render_mode: "structured".to_string(),
html_code: None,
},
event_banners_json: None,
creation_types: vec![CreationEntryTypeSnapshot {
id: "jump-hop".to_string(),
title: "跳一跳".to_string(),
subtitle: "俯视角跳跃闯关".to_string(),
badge: "可创建".to_string(),
image_src: "/creation-type-references/puzzle.webp".to_string(),
visible: true,
open: true,
sort_order: 45,
category_id: "recommended".to_string(),
category_label: "热门推荐".to_string(),
category_sort_order: 20,
updated_at_micros: 2_000_000,
unified_creation_spec_json: None,
}],
updated_at_micros: 1_000_000,
});
let jump_hop = record
.creation_types
.iter()
.find(|item| item.id == "jump-hop")
.expect("should contain jump-hop");
assert_eq!(jump_hop.subtitle, "主题驱动平台跳跃");
assert_eq!(
jump_hop.image_src,
"/creation-type-references/jump-hop.webp"
);
}
}
pub(crate) fn map_runtime_setting_procedure_result(
result: RuntimeSettingProcedureResult,
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {