merge: master into codex/bark-battle
This commit is contained in:
@@ -33,8 +33,8 @@ pub(crate) fn build_ai_task_snapshot_from_row(
|
||||
let mut stages = ctx
|
||||
.db
|
||||
.ai_task_stage()
|
||||
.iter()
|
||||
.filter(|stage| stage.task_id == row.task_id)
|
||||
.by_ai_task_stage_task_id()
|
||||
.filter(&row.task_id)
|
||||
.map(|stage| build_ai_task_stage_snapshot_from_row(&stage))
|
||||
.collect::<Vec<_>>();
|
||||
stages.sort_by_key(|stage| stage.order);
|
||||
@@ -42,8 +42,8 @@ pub(crate) fn build_ai_task_snapshot_from_row(
|
||||
let mut result_references = ctx
|
||||
.db
|
||||
.ai_result_reference()
|
||||
.iter()
|
||||
.filter(|reference| reference.task_id == row.task_id)
|
||||
.by_ai_result_reference_task_id()
|
||||
.filter(&row.task_id)
|
||||
.map(|reference| build_ai_result_reference_snapshot_from_row(&reference))
|
||||
.collect::<Vec<_>>();
|
||||
result_references.sort_by_key(|reference| reference.created_at_micros);
|
||||
|
||||
@@ -318,8 +318,8 @@ pub(crate) fn replace_ai_task_stages(
|
||||
let stage_ids = ctx
|
||||
.db
|
||||
.ai_task_stage()
|
||||
.iter()
|
||||
.filter(|row| row.task_id == task_id)
|
||||
.by_ai_task_stage_task_id()
|
||||
.filter(task_id)
|
||||
.map(|row| row.task_stage_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
for stage_id in stage_ids {
|
||||
@@ -341,7 +341,8 @@ pub(crate) fn collect_ai_stage_text_output(
|
||||
let mut chunks = ctx
|
||||
.db
|
||||
.ai_text_chunk()
|
||||
.iter()
|
||||
.by_ai_text_chunk_task_id()
|
||||
.filter(task_id)
|
||||
.filter(|row| row.task_id == task_id && row.stage_kind == stage_kind)
|
||||
.map(|row| build_ai_text_chunk_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -66,12 +66,16 @@ fn upsert_asset_entity_binding(
|
||||
return Err("asset_entity_binding.asset_object_id 对应的 asset_object 不存在".to_string());
|
||||
}
|
||||
|
||||
// 首版绑定按 entity_kind + entity_id + slot 幂等定位,后续访问量明确后再改为组合索引扫描。
|
||||
let current = ctx.db.asset_entity_binding().iter().find(|row| {
|
||||
row.entity_kind == input.entity_kind
|
||||
&& row.entity_id == input.entity_id
|
||||
&& row.slot == input.slot
|
||||
});
|
||||
let current = ctx
|
||||
.db
|
||||
.asset_entity_binding()
|
||||
.by_entity_slot()
|
||||
.filter((
|
||||
input.entity_kind.as_str(),
|
||||
input.entity_id.as_str(),
|
||||
input.slot.as_str(),
|
||||
))
|
||||
.next();
|
||||
|
||||
let snapshot = match current {
|
||||
Some(existing) => {
|
||||
|
||||
@@ -128,12 +128,12 @@ pub(crate) fn upsert_asset_object(
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
// 这里先保持最小可发布实现:查重语义已经冻结,后续再把实现优化回组合索引扫描。
|
||||
let current = ctx
|
||||
.db
|
||||
.asset_object()
|
||||
.iter()
|
||||
.find(|row| row.bucket == input.bucket && row.object_key == input.object_key);
|
||||
.by_bucket_object_key()
|
||||
.filter((input.bucket.as_str(), input.object_key.as_str()))
|
||||
.next();
|
||||
|
||||
let snapshot = match current {
|
||||
Some(existing) => {
|
||||
@@ -196,8 +196,9 @@ pub(crate) fn upsert_asset_object(
|
||||
pub(crate) fn has_asset_object(ctx: &ReducerContext, asset_object_id: &str) -> bool {
|
||||
ctx.db
|
||||
.asset_object()
|
||||
.iter()
|
||||
.any(|row| row.asset_object_id == asset_object_id)
|
||||
.asset_object_id()
|
||||
.find(&asset_object_id.to_string())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn list_asset_history(
|
||||
@@ -224,8 +225,8 @@ fn list_asset_history(
|
||||
let mut entries = ctx
|
||||
.db
|
||||
.asset_object()
|
||||
.iter()
|
||||
.filter(|row| row.asset_kind == asset_kind)
|
||||
.asset_kind()
|
||||
.filter(&asset_kind.to_string())
|
||||
.map(|row| AssetHistoryEntrySnapshot {
|
||||
asset_object_id: row.asset_object_id,
|
||||
asset_kind: row.asset_kind,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::*;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub(crate) mod tables;
|
||||
@@ -15,7 +14,7 @@ pub fn create_bark_battle_draft(
|
||||
input: BarkBattleDraftCreateInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| create_bark_battle_draft_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Ok(snapshot) => bark_battle_draft_config_result(snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
@@ -26,7 +25,7 @@ pub fn update_bark_battle_draft_config(
|
||||
input: BarkBattleDraftConfigUpsertInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| update_bark_battle_draft_config_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Ok(snapshot) => bark_battle_draft_config_result(snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
@@ -37,7 +36,7 @@ pub fn publish_bark_battle_work(
|
||||
input: BarkBattleWorkPublishInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| publish_bark_battle_work_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Ok(snapshot) => bark_battle_runtime_config_result(snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
@@ -48,7 +47,7 @@ pub fn get_bark_battle_runtime_config(
|
||||
input: BarkBattleRuntimeConfigGetInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_bark_battle_runtime_config_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Ok(snapshot) => bark_battle_runtime_config_result(snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
@@ -59,7 +58,7 @@ pub fn start_bark_battle_run(
|
||||
input: BarkBattleRunStartInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| start_bark_battle_run_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Ok(snapshot) => bark_battle_run_result(snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
@@ -70,7 +69,7 @@ pub fn finish_bark_battle_run(
|
||||
input: BarkBattleRunFinishInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| finish_bark_battle_run_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Ok(snapshot) => bark_battle_run_result(snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
@@ -81,7 +80,7 @@ pub fn get_bark_battle_run(
|
||||
input: BarkBattleRunGetInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_bark_battle_run_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Ok(snapshot) => bark_battle_run_result(snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
@@ -619,10 +618,36 @@ fn validate_json<T: DeserializeOwned>(value: &str, field_name: &str) -> Result<(
|
||||
.map_err(|error| format!("bark_battle {field_name} JSON 无效: {error}"))
|
||||
}
|
||||
|
||||
fn bark_battle_json_result<T: Serialize>(value: &T) -> BarkBattleProcedureResult {
|
||||
fn bark_battle_draft_config_result(
|
||||
draft_config: BarkBattleDraftConfigSnapshot,
|
||||
) -> BarkBattleProcedureResult {
|
||||
BarkBattleProcedureResult {
|
||||
ok: true,
|
||||
row_json: Some(to_json_string(value)),
|
||||
draft_config: Some(draft_config),
|
||||
runtime_config: None,
|
||||
run: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn bark_battle_runtime_config_result(
|
||||
runtime_config: BarkBattleRuntimeConfigSnapshot,
|
||||
) -> BarkBattleProcedureResult {
|
||||
BarkBattleProcedureResult {
|
||||
ok: true,
|
||||
draft_config: None,
|
||||
runtime_config: Some(runtime_config),
|
||||
run: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn bark_battle_run_result(run: BarkBattleRunSnapshot) -> BarkBattleProcedureResult {
|
||||
BarkBattleProcedureResult {
|
||||
ok: true,
|
||||
draft_config: None,
|
||||
runtime_config: None,
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -630,7 +655,9 @@ fn bark_battle_json_result<T: Serialize>(value: &T) -> BarkBattleProcedureResult
|
||||
fn bark_battle_error_result(error: String) -> BarkBattleProcedureResult {
|
||||
BarkBattleProcedureResult {
|
||||
ok: false,
|
||||
row_json: None,
|
||||
draft_config: None,
|
||||
runtime_config: None,
|
||||
run: None,
|
||||
error_message: Some(error),
|
||||
}
|
||||
}
|
||||
@@ -885,7 +912,21 @@ mod tests {
|
||||
|
||||
let result = BarkBattleProcedureResult {
|
||||
ok: true,
|
||||
row_json: Some(input.config_json.clone()),
|
||||
draft_config: Some(BarkBattleDraftConfigSnapshot {
|
||||
draft_id: input.draft_id.clone(),
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
work_id: input.work_id.clone(),
|
||||
config_version: input.config_version,
|
||||
ruleset_version: input.ruleset_version.clone(),
|
||||
difficulty_preset: input.difficulty_preset.clone(),
|
||||
leaderboard_enabled: input.leaderboard_enabled,
|
||||
config_json: input.config_json.clone(),
|
||||
editor_state_json: "{}".to_string(),
|
||||
created_at_micros: 1_700_000,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}),
|
||||
runtime_config: None,
|
||||
run: None,
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
@@ -102,14 +102,16 @@ pub struct BarkBattleRunGetInput {
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct BarkBattleProcedureResult {
|
||||
pub ok: bool,
|
||||
pub row_json: Option<String>,
|
||||
pub draft_config: Option<BarkBattleDraftConfigSnapshot>,
|
||||
pub runtime_config: Option<BarkBattleRuntimeConfigSnapshot>,
|
||||
pub run: Option<BarkBattleRunSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleEditorConfigSnapshot {
|
||||
pub title: String,
|
||||
@@ -129,7 +131,7 @@ pub struct BarkBattleEditorConfigSnapshot {
|
||||
pub leaderboard_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleDraftConfigSnapshot {
|
||||
pub draft_id: String,
|
||||
@@ -145,7 +147,7 @@ pub struct BarkBattleDraftConfigSnapshot {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleRuntimeConfigSnapshot {
|
||||
pub work_id: String,
|
||||
@@ -161,7 +163,7 @@ pub struct BarkBattleRuntimeConfigSnapshot {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleRunSnapshot {
|
||||
pub run_id: String,
|
||||
|
||||
@@ -222,8 +222,8 @@ pub(crate) fn list_big_fish_asset_slots(
|
||||
let mut slots = ctx
|
||||
.db
|
||||
.big_fish_asset_slot()
|
||||
.iter()
|
||||
.filter(|slot| slot.session_id == session_id)
|
||||
.by_big_fish_asset_session_id()
|
||||
.filter(&session_id.to_string())
|
||||
.map(|slot| BigFishAssetSlotSnapshot {
|
||||
slot_id: slot.slot_id,
|
||||
session_id: slot.session_id,
|
||||
|
||||
@@ -16,12 +16,12 @@ pub fn start_big_fish_run(
|
||||
match ctx.try_with_tx(|tx| start_big_fish_run_tx(tx, input.clone())) {
|
||||
Ok(run) => BigFishRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_big_fish_run_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -35,12 +35,12 @@ pub fn get_big_fish_run(
|
||||
match ctx.try_with_tx(|tx| get_big_fish_run_tx(tx, input.clone())) {
|
||||
Ok(run) => BigFishRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_big_fish_run_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -54,12 +54,12 @@ pub fn submit_big_fish_input(
|
||||
match ctx.try_with_tx(|tx| submit_big_fish_input_tx(tx, input.clone())) {
|
||||
Ok(run) => BigFishRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_big_fish_run_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -225,7 +225,3 @@ fn replace_big_fish_runtime_run(
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_big_fish_run_json(run: &BigFishRuntimeSnapshot) -> String {
|
||||
serialize_runtime_snapshot(run).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
|
||||
@@ -8,9 +8,42 @@ use crate::runtime::{
|
||||
};
|
||||
use crate::*;
|
||||
use module_big_fish::{EvaluateBigFishPublishReadinessCommand, evaluate_publish_readiness};
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
const INITIAL_BIG_FISH_CREATION_PROGRESS_PERCENT: u32 = 0;
|
||||
|
||||
/// 大鱼吃小鱼公开广场列表投影。
|
||||
///
|
||||
/// 公开列表从已发布 creation session 生成卡片字段;7 日播放数由
|
||||
/// `api-server` 订阅 `public_work_play_daily_stat` 后在本地聚合。
|
||||
#[spacetimedb::view(accessor = big_fish_gallery_view, public)]
|
||||
pub fn big_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec<BigFishWorkSummarySnapshot> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.by_big_fish_session_stage()
|
||||
.filter(BigFishCreationStage::Published)
|
||||
.filter_map(|row| match build_big_fish_gallery_view_row(ctx, &row) {
|
||||
Ok(snapshot) => Some(snapshot),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"大鱼吃小鱼公开广场 view 跳过损坏的作品投影 session_id={}: {}",
|
||||
row.session_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.source_session_id.cmp(&right.source_session_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_big_fish_session(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -55,21 +88,14 @@ pub fn list_big_fish_works(
|
||||
input: BigFishWorksListInput,
|
||||
) -> BigFishWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| list_big_fish_works_tx(tx, input.clone())) {
|
||||
Ok(items) => match serde_json::to_string(&items) {
|
||||
Ok(items_json) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(items_json),
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(error.to_string()),
|
||||
},
|
||||
Ok(items) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -81,21 +107,14 @@ pub fn delete_big_fish_work(
|
||||
input: BigFishWorkDeleteInput,
|
||||
) -> BigFishWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| delete_big_fish_work_tx(tx, input.clone())) {
|
||||
Ok(items) => match serde_json::to_string(&items) {
|
||||
Ok(items_json) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(items_json),
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(error.to_string()),
|
||||
},
|
||||
Ok(items) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -107,21 +126,14 @@ pub fn record_big_fish_play(
|
||||
input: BigFishPlayRecordInput,
|
||||
) -> BigFishWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| record_big_fish_play_tx(tx, input.clone())) {
|
||||
Ok(items) => match serde_json::to_string(&items) {
|
||||
Ok(items_json) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(items_json),
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(error.to_string()),
|
||||
},
|
||||
Ok(items) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -133,21 +145,14 @@ pub fn record_big_fish_like(
|
||||
input: BigFishWorkLikeRecordInput,
|
||||
) -> BigFishWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| record_big_fish_like_tx(tx, input.clone())) {
|
||||
Ok(items) => match serde_json::to_string(&items) {
|
||||
Ok(items_json) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(items_json),
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(error.to_string()),
|
||||
},
|
||||
Ok(items) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -321,16 +326,20 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
validate_works_list_input(&input).map_err(|error| error.to_string())?;
|
||||
let now_micros = ctx.timestamp.to_micros_since_unix_epoch();
|
||||
|
||||
let mut items = ctx
|
||||
let rows = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.by_big_fish_session_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.collect::<Vec<_>>();
|
||||
let mut items = rows
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
if input.published_only {
|
||||
return row.stage == BigFishCreationStage::Published;
|
||||
}
|
||||
|
||||
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
||||
should_include_big_fish_work(ctx, row)
|
||||
})
|
||||
.map(|row| build_big_fish_work_summary(ctx, &row, now_micros))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
@@ -349,10 +358,11 @@ fn should_include_big_fish_work(ctx: &ReducerContext, row: &BigFishCreationSessi
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db.big_fish_agent_message().iter().any(|message| {
|
||||
message.session_id == row.session_id
|
||||
&& matches!(message.role, BigFishAgentMessageRole::User)
|
||||
})
|
||||
ctx.db
|
||||
.big_fish_agent_message()
|
||||
.by_big_fish_message_session_id()
|
||||
.filter(&row.session_id)
|
||||
.any(|message| matches!(message.role, BigFishAgentMessageRole::User))
|
||||
}
|
||||
|
||||
fn big_fish_session_has_direct_work_content(row: &BigFishCreationSession) -> bool {
|
||||
@@ -387,8 +397,8 @@ pub(crate) fn delete_big_fish_work_tx(
|
||||
for message in ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.by_big_fish_message_session_id()
|
||||
.filter(&input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
@@ -399,8 +409,8 @@ pub(crate) fn delete_big_fish_work_tx(
|
||||
for slot in ctx
|
||||
.db
|
||||
.big_fish_asset_slot()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.by_big_fish_asset_session_id()
|
||||
.filter(&input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db.big_fish_asset_slot().slot_id().delete(&slot.slot_id);
|
||||
@@ -408,8 +418,8 @@ pub(crate) fn delete_big_fish_work_tx(
|
||||
for run in ctx
|
||||
.db
|
||||
.big_fish_runtime_run()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.by_big_fish_run_session_id()
|
||||
.filter(&input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db.big_fish_runtime_run().run_id().delete(&run.run_id);
|
||||
@@ -952,8 +962,8 @@ pub(crate) fn build_big_fish_session_snapshot(
|
||||
let mut messages = ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == row.session_id)
|
||||
.by_big_fish_message_session_id()
|
||||
.filter(&row.session_id)
|
||||
.map(|message| BigFishAgentMessageSnapshot {
|
||||
message_id: message.message_id,
|
||||
session_id: message.session_id,
|
||||
@@ -988,6 +998,16 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
now_micros: i64,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let mut summary = build_big_fish_work_summary_without_recent_count(ctx, row)?;
|
||||
summary.recent_play_count_7d =
|
||||
count_recent_public_work_plays(ctx, "big-fish", &row.session_id, now_micros);
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn build_big_fish_work_summary_without_recent_count(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let draft = row
|
||||
.draft_json
|
||||
@@ -1052,12 +1072,7 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: count_recent_public_work_plays(
|
||||
ctx,
|
||||
"big-fish",
|
||||
&row.session_id,
|
||||
now_micros,
|
||||
),
|
||||
recent_play_count_7d: 0,
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
|
||||
@@ -1065,6 +1080,113 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_big_fish_gallery_view_row(
|
||||
ctx: &AnonymousViewContext,
|
||||
row: &BigFishCreationSession,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let draft = row
|
||||
.draft_json
|
||||
.as_deref()
|
||||
.map(deserialize_draft)
|
||||
.transpose()
|
||||
.map_err(|error| format!("big_fish.draft_json 非法: {error}"))?;
|
||||
let asset_slots = list_big_fish_asset_slots_for_view(ctx, &row.session_id);
|
||||
let coverage = build_asset_coverage(draft.as_ref(), &asset_slots);
|
||||
let cover_image_src = asset_slots
|
||||
.iter()
|
||||
.find(|slot| slot.asset_kind == BigFishAssetKind::StageBackground)
|
||||
.and_then(|slot| slot.asset_url.clone())
|
||||
.or_else(|| {
|
||||
asset_slots
|
||||
.iter()
|
||||
.find(|slot| slot.asset_kind == BigFishAssetKind::LevelMainImage)
|
||||
.and_then(|slot| slot.asset_url.clone())
|
||||
});
|
||||
let title = draft
|
||||
.as_ref()
|
||||
.map(|value| value.title.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "未命名大鱼草稿".to_string());
|
||||
let subtitle = draft
|
||||
.as_ref()
|
||||
.map(|value| value.subtitle.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "等待整理玩法草稿".to_string());
|
||||
let summary = draft
|
||||
.as_ref()
|
||||
.map(|value| value.core_fun.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
row.last_assistant_reply
|
||||
.clone()
|
||||
.unwrap_or_else(|| "继续补齐锚点后即可生成玩法草稿。".to_string())
|
||||
});
|
||||
|
||||
Ok(BigFishWorkSummarySnapshot {
|
||||
work_id: format!("big-fish-work-{}", row.session_id),
|
||||
source_session_id: row.session_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
title,
|
||||
subtitle,
|
||||
summary,
|
||||
cover_image_src,
|
||||
status: if row.stage == BigFishCreationStage::Published {
|
||||
"published".to_string()
|
||||
} else {
|
||||
"draft".to_string()
|
||||
},
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
publish_ready: coverage.publish_ready,
|
||||
level_count: draft
|
||||
.as_ref()
|
||||
.map(|value| value.runtime_params.level_count)
|
||||
.unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT),
|
||||
level_main_image_ready_count: coverage.level_main_image_ready_count,
|
||||
level_motion_ready_count: coverage.level_motion_ready_count,
|
||||
background_ready: coverage.background_ready,
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: 0,
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
})
|
||||
}
|
||||
|
||||
fn list_big_fish_asset_slots_for_view(
|
||||
ctx: &AnonymousViewContext,
|
||||
session_id: &str,
|
||||
) -> Vec<BigFishAssetSlotSnapshot> {
|
||||
let mut slots = ctx
|
||||
.db
|
||||
.big_fish_asset_slot()
|
||||
.by_big_fish_asset_session_id()
|
||||
.filter(session_id)
|
||||
.map(|slot| BigFishAssetSlotSnapshot {
|
||||
slot_id: slot.slot_id,
|
||||
session_id: slot.session_id,
|
||||
asset_kind: slot.asset_kind,
|
||||
level: slot.level,
|
||||
motion_key: slot.motion_key,
|
||||
status: slot.status,
|
||||
asset_url: slot.asset_url,
|
||||
prompt_snapshot: slot.prompt_snapshot,
|
||||
updated_at_micros: slot.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
slots.sort_by_key(|slot| {
|
||||
(
|
||||
slot.level.unwrap_or(0),
|
||||
slot.asset_kind.as_str().to_string(),
|
||||
slot.motion_key.clone().unwrap_or_default(),
|
||||
slot.slot_id.clone(),
|
||||
)
|
||||
});
|
||||
slots
|
||||
}
|
||||
|
||||
fn build_public_big_fish_gallery_list_input() -> BigFishWorksListInput {
|
||||
BigFishWorksListInput {
|
||||
// 中文注释:published_only 分支不会按 owner 过滤;非空占位用于兼容旧部署模块的前置校验。
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::*;
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = big_fish_creation_session,
|
||||
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_big_fish_session_stage, btree(columns = [stage]))
|
||||
)]
|
||||
pub struct BigFishCreationSession {
|
||||
#[primary_key]
|
||||
|
||||
@@ -436,7 +436,8 @@ fn delete_custom_world_agent_session_tx(
|
||||
let published_profile = ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.iter()
|
||||
.by_custom_world_profile_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.find(|row| {
|
||||
row.owner_user_id == input.owner_user_id
|
||||
&& row.source_agent_session_id.as_deref() == Some(input.session_id.as_str())
|
||||
@@ -471,8 +472,8 @@ fn delete_custom_world_agent_session_tx(
|
||||
for message in ctx
|
||||
.db
|
||||
.custom_world_agent_message()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.by_custom_world_agent_message_session_id()
|
||||
.filter(&input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
@@ -483,8 +484,8 @@ fn delete_custom_world_agent_session_tx(
|
||||
for operation in ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.by_custom_world_agent_operation_session_id()
|
||||
.filter(&input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
@@ -495,8 +496,8 @@ fn delete_custom_world_agent_session_tx(
|
||||
for card in ctx
|
||||
.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.by_custom_world_draft_card_session_id()
|
||||
.filter(&input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
@@ -1184,9 +1185,17 @@ fn upsert_custom_world_profile_record(
|
||||
.source_agent_session_id
|
||||
.as_ref()
|
||||
.and_then(|session_id| {
|
||||
ctx.db.custom_world_profile().iter().find(|row| {
|
||||
is_same_agent_draft_profile_candidate(row, &input.owner_user_id, session_id)
|
||||
})
|
||||
ctx.db
|
||||
.custom_world_profile()
|
||||
.by_custom_world_profile_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.find(|row| {
|
||||
is_same_agent_draft_profile_candidate(
|
||||
row,
|
||||
&input.owner_user_id,
|
||||
session_id,
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1534,8 +1543,9 @@ fn list_custom_world_profile_snapshots(
|
||||
let mut entries = ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
||||
.by_custom_world_profile_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.filter(|row| row.deleted_at.is_none())
|
||||
.map(|row| build_custom_world_profile_snapshot(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1676,8 +1686,9 @@ fn get_custom_world_gallery_detail_record_by_code(
|
||||
let gallery_entry = ctx
|
||||
.db
|
||||
.custom_world_gallery_entry()
|
||||
.iter()
|
||||
.find(|row| row.public_work_code == normalized_public_work_code);
|
||||
.by_custom_world_gallery_public_work_code()
|
||||
.filter(&normalized_public_work_code)
|
||||
.next();
|
||||
|
||||
let profile = gallery_entry.as_ref().and_then(|row| {
|
||||
ctx.db
|
||||
@@ -1974,9 +1985,14 @@ fn list_custom_world_work_snapshots(
|
||||
let mut items = Vec::new();
|
||||
let mut active_agent_session_ids = HashSet::new();
|
||||
|
||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id
|
||||
&& row.stage != RpgAgentStage::Published
|
||||
let sessions = ctx
|
||||
.db
|
||||
.custom_world_agent_session()
|
||||
.by_custom_world_agent_session_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.collect::<Vec<_>>();
|
||||
for session in sessions.iter().filter(|row| {
|
||||
row.stage != RpgAgentStage::Published
|
||||
&& should_include_custom_world_agent_session_work(ctx, row)
|
||||
}) {
|
||||
active_agent_session_ids.insert(session.session_id.clone());
|
||||
@@ -2021,8 +2037,9 @@ fn list_custom_world_work_snapshots(
|
||||
for profile in ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
||||
.by_custom_world_profile_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.filter(|row| row.deleted_at.is_none())
|
||||
.filter(|row| should_include_custom_world_profile_work(row, &active_agent_session_ids))
|
||||
{
|
||||
items.push(CustomWorldWorkSummarySnapshot {
|
||||
@@ -2086,16 +2103,20 @@ fn should_include_custom_world_agent_session_work(
|
||||
return true;
|
||||
}
|
||||
|
||||
if ctx.db.custom_world_agent_message().iter().any(|message| {
|
||||
message.session_id == session.session_id
|
||||
&& matches!(message.role, RpgAgentMessageRole::User)
|
||||
}) {
|
||||
if ctx
|
||||
.db
|
||||
.custom_world_agent_message()
|
||||
.by_custom_world_agent_message_session_id()
|
||||
.filter(&session.session_id)
|
||||
.any(|message| matches!(message.role, RpgAgentMessageRole::User))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.by_custom_world_draft_card_session_id()
|
||||
.filter(&session.session_id)
|
||||
.any(|card| card.session_id == session.session_id)
|
||||
}
|
||||
|
||||
@@ -3446,10 +3467,12 @@ fn update_role_asset_cards(
|
||||
label: &str,
|
||||
updated_at_micros: i64,
|
||||
) {
|
||||
for card in
|
||||
ctx.db.custom_world_draft_card().iter().filter(|row| {
|
||||
row.session_id == session_id && row.kind == RpgAgentDraftCardKind::Character
|
||||
})
|
||||
for card in ctx
|
||||
.db
|
||||
.custom_world_draft_card()
|
||||
.by_custom_world_draft_card_session_id()
|
||||
.filter(&session_id.to_string())
|
||||
.filter(|row| row.kind == RpgAgentDraftCardKind::Character)
|
||||
{
|
||||
replace_custom_world_draft_card(
|
||||
ctx,
|
||||
@@ -4590,8 +4613,8 @@ fn resolve_session_work_counts(
|
||||
for card in ctx
|
||||
.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == session.session_id)
|
||||
.by_custom_world_draft_card_session_id()
|
||||
.filter(&session.session_id)
|
||||
{
|
||||
match card.kind {
|
||||
RpgAgentDraftCardKind::Character => {
|
||||
@@ -4827,11 +4850,9 @@ fn sync_missing_custom_world_gallery_entries(ctx: &ReducerContext) -> Result<(),
|
||||
let published_profiles = ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.iter()
|
||||
.filter(|profile| {
|
||||
profile.publication_status == CustomWorldPublicationStatus::Published
|
||||
&& profile.deleted_at.is_none()
|
||||
})
|
||||
.by_custom_world_profile_publication_status()
|
||||
.filter(CustomWorldPublicationStatus::Published)
|
||||
.filter(|profile| profile.deleted_at.is_none())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for profile in published_profiles {
|
||||
@@ -4973,8 +4994,8 @@ fn build_custom_world_agent_session_snapshot(
|
||||
let mut messages = ctx
|
||||
.db
|
||||
.custom_world_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == row.session_id)
|
||||
.by_custom_world_agent_message_session_id()
|
||||
.filter(&row.session_id)
|
||||
.map(|message| build_custom_world_agent_message_snapshot(&message))
|
||||
.collect::<Vec<_>>();
|
||||
messages.sort_by_key(|message| (message.created_at_micros, message.message_id.clone()));
|
||||
@@ -4982,8 +5003,8 @@ fn build_custom_world_agent_session_snapshot(
|
||||
let mut draft_cards = ctx
|
||||
.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.filter(|card| card.session_id == row.session_id)
|
||||
.by_custom_world_draft_card_session_id()
|
||||
.filter(&row.session_id)
|
||||
.map(|card| build_custom_world_draft_card_snapshot(&card))
|
||||
.collect::<Vec<_>>();
|
||||
draft_cards.sort_by_key(|card| (card.created_at_micros, card.card_id.clone()));
|
||||
@@ -4991,8 +5012,8 @@ fn build_custom_world_agent_session_snapshot(
|
||||
let mut operations = ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.iter()
|
||||
.filter(|operation| operation.session_id == row.session_id)
|
||||
.by_custom_world_agent_operation_session_id()
|
||||
.filter(&row.session_id)
|
||||
.map(|operation| build_custom_world_agent_operation_snapshot(&operation))
|
||||
.collect::<Vec<_>>();
|
||||
operations
|
||||
@@ -415,11 +415,9 @@ fn apply_inventory_mutation_tx(
|
||||
let current_slots = ctx
|
||||
.db
|
||||
.inventory_slot()
|
||||
.iter()
|
||||
.filter(|slot| {
|
||||
slot.runtime_session_id == input.runtime_session_id
|
||||
&& slot.actor_user_id == input.actor_user_id
|
||||
})
|
||||
.by_inventory_runtime_session_id()
|
||||
.filter(&input.runtime_session_id)
|
||||
.filter(|slot| slot.actor_user_id == input.actor_user_id)
|
||||
.map(|row| build_inventory_slot_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -587,11 +585,9 @@ fn get_runtime_inventory_state_tx(
|
||||
let slots = ctx
|
||||
.db
|
||||
.inventory_slot()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.runtime_session_id == validated_input.runtime_session_id
|
||||
&& row.actor_user_id == validated_input.actor_user_id
|
||||
})
|
||||
.by_inventory_runtime_session_id()
|
||||
.filter(&validated_input.runtime_session_id)
|
||||
.filter(|row| row.actor_user_id == validated_input.actor_user_id)
|
||||
.map(|row| build_inventory_slot_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -926,8 +922,8 @@ fn get_story_session_state_tx(
|
||||
let mut events = ctx
|
||||
.db
|
||||
.story_event()
|
||||
.iter()
|
||||
.filter(|row| row.story_session_id == input.story_session_id)
|
||||
.by_story_session_id()
|
||||
.filter(&input.story_session_id)
|
||||
.map(|row| build_story_event_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
events.sort_by_key(|event| (event.created_at_micros, event.event_id.clone()));
|
||||
@@ -1439,11 +1435,9 @@ fn inventory_reward_source_already_granted(
|
||||
|
||||
ctx.db
|
||||
.inventory_slot()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.runtime_session_id == first_mutation.runtime_session_id
|
||||
&& row.actor_user_id == first_mutation.actor_user_id
|
||||
})
|
||||
.by_inventory_runtime_session_id()
|
||||
.filter(&first_mutation.runtime_session_id)
|
||||
.filter(|row| row.actor_user_id == first_mutation.actor_user_id)
|
||||
.any(|row| row.source_reference_id.as_deref() == Some(source_reference_id))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,62 @@ use module_match3d::{
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
/// 抓大鹅公开广场列表投影。
|
||||
///
|
||||
/// `match3d_work_profile` 是玩法源表,HTTP gallery 只订阅这个轻量 view,
|
||||
/// 避免每个公开列表请求重新调用 procedure 扫描和组装全量列表。
|
||||
#[spacetimedb::view(accessor = match3d_gallery_view, public)]
|
||||
pub fn match3d_gallery_view(ctx: &AnonymousViewContext) -> Vec<Match3DGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.match3d_work_profile()
|
||||
.by_match3d_work_publication_status()
|
||||
.filter(MATCH3D_PUBLICATION_PUBLISHED)
|
||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"抓大鹅公开广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DGalleryViewRow {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: String,
|
||||
pub cover_asset_id: String,
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
pub generated_item_assets_json: Option<String>,
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_match3d_agent_session(
|
||||
@@ -105,12 +161,12 @@ pub fn list_match3d_works(
|
||||
match ctx.try_with_tx(|tx| list_match3d_works_tx(tx, input.clone())) {
|
||||
Ok(items) => Match3DWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => Match3DWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -135,12 +191,12 @@ pub fn delete_match3d_work(
|
||||
match ctx.try_with_tx(|tx| delete_match3d_work_tx(tx, input.clone())) {
|
||||
Ok(items) => Match3DWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => Match3DWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -178,7 +234,7 @@ pub fn click_match3d_item(
|
||||
Err(message) => Match3DClickItemProcedureResult {
|
||||
ok: false,
|
||||
status: MATCH3D_CLICK_REJECTED_NOT_CLICKABLE.to_string(),
|
||||
run_json: None,
|
||||
run: None,
|
||||
accepted_item_instance_id: None,
|
||||
cleared_item_instance_ids: Vec::new(),
|
||||
failure_reason: None,
|
||||
@@ -459,6 +515,11 @@ fn compile_match3d_draft_tx(
|
||||
config.theme_text.as_str(),
|
||||
);
|
||||
let summary_text = resolve_compile_summary_text(&input.summary_text, existing_work.as_ref());
|
||||
let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros);
|
||||
let generated_item_assets_json = resolve_generated_item_assets_json_for_compile(
|
||||
input.generated_item_assets_json.as_deref(),
|
||||
existing_work.as_ref(),
|
||||
)?;
|
||||
let draft = Match3DDraftSnapshot {
|
||||
profile_id: input.profile_id.clone(),
|
||||
game_name: game_name.clone(),
|
||||
@@ -467,12 +528,9 @@ fn compile_match3d_draft_tx(
|
||||
tags: tags.clone(),
|
||||
clear_count: config.clear_count,
|
||||
difficulty: config.difficulty,
|
||||
// 中文注释:草稿响应本身也携带生成素材快照,避免 HTTP facade 回读 work 详情失败时丢失背景/容器图。
|
||||
generated_item_assets_json: generated_item_assets_json.clone(),
|
||||
};
|
||||
let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros);
|
||||
let generated_item_assets_json = resolve_generated_item_assets_json_for_compile(
|
||||
input.generated_item_assets_json.as_deref(),
|
||||
existing_work.as_ref(),
|
||||
)?;
|
||||
let previous_publication_status = existing_work
|
||||
.as_ref()
|
||||
.map(|work| work.publication_status.clone())
|
||||
@@ -632,17 +690,22 @@ fn list_match3d_works_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: Match3DWorksListInput,
|
||||
) -> Result<Vec<Match3DWorkSnapshot>, String> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.match3d_work_profile()
|
||||
let rows = if input.published_only {
|
||||
ctx.db
|
||||
.match3d_work_profile()
|
||||
.by_match3d_work_publication_status()
|
||||
.filter(&MATCH3D_PUBLICATION_PUBLISHED.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
require_non_empty(&input.owner_user_id, "match3d owner_user_id")?;
|
||||
ctx.db
|
||||
.match3d_work_profile()
|
||||
.by_match3d_work_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut items = rows
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
if input.published_only {
|
||||
row.publication_status == MATCH3D_PUBLICATION_PUBLISHED
|
||||
} else {
|
||||
row.owner_user_id == input.owner_user_id
|
||||
}
|
||||
})
|
||||
.map(|row| build_work_snapshot(&row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_by(|left, right| {
|
||||
@@ -683,10 +746,9 @@ fn delete_match3d_work_tx(
|
||||
for run in ctx
|
||||
.db
|
||||
.match3d_runtime_run()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.profile_id == input.profile_id && row.owner_user_id == input.owner_user_id
|
||||
})
|
||||
.by_match3d_run_profile_id()
|
||||
.filter(&input.profile_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db.match3d_runtime_run().run_id().delete(&run.run_id);
|
||||
@@ -929,8 +991,8 @@ fn build_session_snapshot(
|
||||
let mut messages = ctx
|
||||
.db
|
||||
.match3d_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == row.session_id)
|
||||
.by_match3d_agent_message_session_id()
|
||||
.filter(&row.session_id)
|
||||
.map(|message| Match3DAgentMessageSnapshot {
|
||||
message_id: message.message_id,
|
||||
session_id: message.session_id,
|
||||
@@ -1002,6 +1064,35 @@ fn build_work_snapshot(row: &Match3DWorkProfileRow) -> Result<Match3DWorkSnapsho
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gallery_view_row(row: &Match3DWorkProfileRow) -> Result<Match3DGalleryViewRow, String> {
|
||||
let config = parse_config(&row.config_json)?;
|
||||
Ok(Match3DGalleryViewRow {
|
||||
profile_id: row.profile_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
game_name: row.game_name.clone(),
|
||||
theme_text: row.theme_text.clone(),
|
||||
summary_text: row.summary_text.clone(),
|
||||
tags: parse_tags(&row.tags_json)?,
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
cover_asset_id: row.cover_asset_id.clone(),
|
||||
reference_image_src: config.reference_image_src,
|
||||
clear_count: row.clear_count,
|
||||
difficulty: row.difficulty,
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: is_work_publish_ready(row),
|
||||
play_count: row.play_count,
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
generated_item_assets_json: normalize_generated_item_assets_json(
|
||||
row.generated_item_assets_json.as_deref(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_initial_run_snapshot(
|
||||
run_id: &str,
|
||||
work: &Match3DWorkProfileRow,
|
||||
@@ -1154,10 +1245,10 @@ fn click_result(
|
||||
Match3DClickItemProcedureResult {
|
||||
ok: true,
|
||||
status: status.to_string(),
|
||||
run_json: Some(to_json_string(&snapshot)),
|
||||
failure_reason: snapshot.failure_reason.clone(),
|
||||
run: Some(snapshot),
|
||||
accepted_item_instance_id,
|
||||
cleared_item_instance_ids,
|
||||
failure_reason: snapshot.failure_reason,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1715,7 +1806,7 @@ fn to_json_string<T: Serialize>(value: &T) -> String {
|
||||
fn session_result(session: Match3DAgentSessionSnapshot) -> Match3DAgentSessionProcedureResult {
|
||||
Match3DAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(to_json_string(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1723,7 +1814,7 @@ fn session_result(session: Match3DAgentSessionSnapshot) -> Match3DAgentSessionPr
|
||||
fn session_error(message: String) -> Match3DAgentSessionProcedureResult {
|
||||
Match3DAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -1731,7 +1822,7 @@ fn session_error(message: String) -> Match3DAgentSessionProcedureResult {
|
||||
fn work_result(work: Match3DWorkSnapshot) -> Match3DWorkProcedureResult {
|
||||
Match3DWorkProcedureResult {
|
||||
ok: true,
|
||||
work_json: Some(to_json_string(&work)),
|
||||
work: Some(work),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1739,7 +1830,7 @@ fn work_result(work: Match3DWorkSnapshot) -> Match3DWorkProcedureResult {
|
||||
fn work_error(message: String) -> Match3DWorkProcedureResult {
|
||||
Match3DWorkProcedureResult {
|
||||
ok: false,
|
||||
work_json: None,
|
||||
work: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -1747,7 +1838,7 @@ fn work_error(message: String) -> Match3DWorkProcedureResult {
|
||||
fn run_result(run: Match3DRunSnapshot) -> Match3DRunProcedureResult {
|
||||
Match3DRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(to_json_string(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1755,7 +1846,7 @@ fn run_result(run: Match3DRunSnapshot) -> Match3DRunProcedureResult {
|
||||
fn run_error(message: String) -> Match3DRunProcedureResult {
|
||||
Match3DRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -1889,6 +1980,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_draft_snapshot_keeps_generated_item_assets_json() {
|
||||
let draft = Match3DDraftSnapshot {
|
||||
profile_id: "profile-1".to_string(),
|
||||
game_name: "水果抓大鹅".to_string(),
|
||||
theme_text: "水果".to_string(),
|
||||
summary_text: "水果主题".to_string(),
|
||||
tags: vec!["水果".to_string()],
|
||||
clear_count: 3,
|
||||
difficulty: 3,
|
||||
generated_item_assets_json: Some(
|
||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","backgroundAsset":{"prompt":"果园背景","imageSrc":"/generated-match3d-assets/session/profile/background/background.png","containerImageSrc":"/generated-match3d-assets/session/profile/ui-container/container.png","status":"image_ready"},"status":"image_ready"}]"#
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let row_json = to_json_string(&draft);
|
||||
let restored = parse_json::<Match3DDraftSnapshot>(&row_json, "match3d draft_json").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
restored.generated_item_assets_json.as_deref(),
|
||||
draft.generated_item_assets_json.as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_work_update_preserves_assets_and_allows_empty_summary() {
|
||||
let existing = Match3DWorkProfileRow {
|
||||
@@ -182,43 +182,43 @@ pub struct Match3DRunTimeUpInput {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DAgentSessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session_json: Option<String>,
|
||||
pub session: Option<Match3DAgentSessionSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorkProcedureResult {
|
||||
pub ok: bool,
|
||||
pub work_json: Option<String>,
|
||||
pub work: Option<Match3DWorkSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorksProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub items: Vec<Match3DWorkSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct Match3DRunProcedureResult {
|
||||
pub ok: bool,
|
||||
pub run_json: Option<String>,
|
||||
pub run: Option<Match3DRunSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct Match3DClickItemProcedureResult {
|
||||
pub ok: bool,
|
||||
pub status: String,
|
||||
pub run_json: Option<String>,
|
||||
pub run: Option<Match3DRunSnapshot>,
|
||||
pub accepted_item_instance_id: Option<String>,
|
||||
pub cleared_item_instance_ids: Vec<String>,
|
||||
pub failure_reason: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DCreatorConfigSnapshot {
|
||||
pub theme_text: String,
|
||||
@@ -235,7 +235,7 @@ pub struct Match3DCreatorConfigSnapshot {
|
||||
pub generate_click_sound: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentMessageSnapshot {
|
||||
pub message_id: String,
|
||||
@@ -246,7 +246,7 @@ pub struct Match3DAgentMessageSnapshot {
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DDraftSnapshot {
|
||||
pub profile_id: String,
|
||||
@@ -256,9 +256,11 @@ pub struct Match3DDraftSnapshot {
|
||||
pub tags: Vec<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
#[serde(default)]
|
||||
pub generated_item_assets_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentSessionSnapshot {
|
||||
pub session_id: String,
|
||||
@@ -276,7 +278,7 @@ pub struct Match3DAgentSessionSnapshot {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorkSnapshot {
|
||||
pub profile_id: String,
|
||||
@@ -300,7 +302,7 @@ pub struct Match3DWorkSnapshot {
|
||||
pub generated_item_assets_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DItemSnapshot {
|
||||
pub item_instance_id: String,
|
||||
@@ -314,7 +316,7 @@ pub struct Match3DItemSnapshot {
|
||||
pub clickable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DTraySlotSnapshot {
|
||||
pub slot_index: u32,
|
||||
@@ -323,7 +325,7 @@ pub struct Match3DTraySlotSnapshot {
|
||||
pub visual_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DRunSnapshot {
|
||||
pub run_id: String,
|
||||
|
||||
@@ -31,7 +31,9 @@ use module_runtime::visible_runtime_profile_user_tags;
|
||||
use serde_json::from_str as json_from_str;
|
||||
use serde_json::json;
|
||||
use serde_json::to_string as json_to_string;
|
||||
use spacetimedb::{ProcedureContext, SpacetimeType, Table, Timestamp, TxContext};
|
||||
use spacetimedb::{
|
||||
AnonymousViewContext, ProcedureContext, SpacetimeType, Table, Timestamp, TxContext,
|
||||
};
|
||||
|
||||
use crate::auth::user_account;
|
||||
|
||||
@@ -112,6 +114,93 @@ pub struct PuzzleWorkProfileRow {
|
||||
point_incentive_claimed_points: u64,
|
||||
}
|
||||
|
||||
/// 拼图广场公开详情兼容投影。
|
||||
///
|
||||
/// 该 view 返回完整 `PuzzleWorkProfile`,包含 levels / anchor_pack 等详情级字段。
|
||||
/// 公开列表主路径应订阅更轻量的 `puzzle_gallery_card_view`。
|
||||
#[spacetimedb::view(accessor = puzzle_gallery_view, public)]
|
||||
pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec<PuzzleWorkProfile> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.filter_map(
|
||||
|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) {
|
||||
Ok(profile) => Some(profile),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"拼图广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||
items
|
||||
}
|
||||
|
||||
/// 拼图广场公开列表卡片投影。
|
||||
///
|
||||
/// 该 view 只暴露前端列表首屏需要的公开卡片字段,不携带 levels / anchor_pack
|
||||
/// 等详情级载荷,供 api-server 热点缓存订阅和组装列表窗口。
|
||||
#[spacetimedb::view(accessor = puzzle_gallery_card_view, public)]
|
||||
pub fn puzzle_gallery_card_view(ctx: &AnonymousViewContext) -> Vec<PuzzleGalleryCardViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.filter_map(|row| match build_puzzle_gallery_card_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"拼图广场卡片 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct PuzzleGalleryCardViewRow {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: Option<String>,
|
||||
pub author_display_name: String,
|
||||
pub work_title: String,
|
||||
pub work_description: String,
|
||||
pub level_name: String,
|
||||
pub summary: String,
|
||||
pub theme_tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub cover_asset_id: Option<String>,
|
||||
pub publication_status: PuzzlePublicationStatus,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub point_incentive_total_half_points: u64,
|
||||
pub point_incentive_claimed_points: u64,
|
||||
pub publish_ready: bool,
|
||||
pub generation_status: Option<String>,
|
||||
}
|
||||
|
||||
/// 拼图创作事件类型。
|
||||
///
|
||||
/// 事件表只广播跨层订阅需要的轻量事实,作品真相仍以
|
||||
@@ -187,12 +276,12 @@ pub fn create_puzzle_agent_session(
|
||||
match ctx.try_with_tx(|tx| create_puzzle_agent_session_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -206,12 +295,12 @@ pub fn get_puzzle_agent_session(
|
||||
match ctx.try_with_tx(|tx| get_puzzle_agent_session_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -225,12 +314,12 @@ pub fn submit_puzzle_agent_message(
|
||||
match ctx.try_with_tx(|tx| submit_puzzle_agent_message_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -244,12 +333,12 @@ pub fn finalize_puzzle_agent_message_turn(
|
||||
match ctx.try_with_tx(|tx| finalize_puzzle_agent_message_turn_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -263,12 +352,12 @@ pub fn compile_puzzle_agent_draft(
|
||||
match ctx.try_with_tx(|tx| compile_puzzle_agent_draft_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -284,12 +373,12 @@ pub fn save_puzzle_form_draft(
|
||||
match ctx.try_with_tx(|tx| save_puzzle_form_draft_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -303,12 +392,12 @@ pub fn save_puzzle_generated_images(
|
||||
match ctx.try_with_tx(|tx| save_puzzle_generated_images_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -322,12 +411,12 @@ pub fn save_puzzle_ui_background(
|
||||
match ctx.try_with_tx(|tx| save_puzzle_ui_background_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -341,12 +430,12 @@ pub fn select_puzzle_cover_image(
|
||||
match ctx.try_with_tx(|tx| select_puzzle_cover_image_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -360,12 +449,12 @@ pub fn publish_puzzle_work(
|
||||
match ctx.try_with_tx(|tx| publish_puzzle_work_tx(tx, input.clone())) {
|
||||
Ok(item) => PuzzleWorkProcedureResult {
|
||||
ok: true,
|
||||
item_json: Some(serialize_json(&item)),
|
||||
item: Some(item),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorkProcedureResult {
|
||||
ok: false,
|
||||
item_json: None,
|
||||
item: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -379,12 +468,12 @@ pub fn list_puzzle_works(
|
||||
match ctx.try_with_tx(|tx| list_puzzle_works_tx(tx, input.clone())) {
|
||||
Ok(items) => PuzzleWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(serialize_json(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -398,12 +487,12 @@ pub fn get_puzzle_work_detail(
|
||||
match ctx.try_with_tx(|tx| get_puzzle_work_detail_tx(tx, input.clone())) {
|
||||
Ok(item) => PuzzleWorkProcedureResult {
|
||||
ok: true,
|
||||
item_json: Some(serialize_json(&item)),
|
||||
item: Some(item),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorkProcedureResult {
|
||||
ok: false,
|
||||
item_json: None,
|
||||
item: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -417,12 +506,12 @@ pub fn update_puzzle_work(
|
||||
match ctx.try_with_tx(|tx| update_puzzle_work_tx(tx, input.clone())) {
|
||||
Ok(item) => PuzzleWorkProcedureResult {
|
||||
ok: true,
|
||||
item_json: Some(serialize_json(&item)),
|
||||
item: Some(item),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorkProcedureResult {
|
||||
ok: false,
|
||||
item_json: None,
|
||||
item: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -436,12 +525,12 @@ pub fn delete_puzzle_work(
|
||||
match ctx.try_with_tx(|tx| delete_puzzle_work_tx(tx, input.clone())) {
|
||||
Ok(items) => PuzzleWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(serialize_json(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -452,12 +541,12 @@ pub fn list_puzzle_gallery(ctx: &mut ProcedureContext) -> PuzzleWorksProcedureRe
|
||||
match ctx.try_with_tx(|tx| list_puzzle_gallery_tx(tx)) {
|
||||
Ok(items) => PuzzleWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(serialize_json(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -471,12 +560,12 @@ pub fn get_puzzle_gallery_detail(
|
||||
match ctx.try_with_tx(|tx| get_puzzle_gallery_detail_tx(tx, input.clone())) {
|
||||
Ok(item) => PuzzleWorkProcedureResult {
|
||||
ok: true,
|
||||
item_json: Some(serialize_json(&item)),
|
||||
item: Some(item),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorkProcedureResult {
|
||||
ok: false,
|
||||
item_json: None,
|
||||
item: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -490,12 +579,12 @@ pub fn record_puzzle_work_like(
|
||||
match ctx.try_with_tx(|tx| record_puzzle_work_like_tx(tx, input.clone())) {
|
||||
Ok(item) => PuzzleWorkProcedureResult {
|
||||
ok: true,
|
||||
item_json: Some(serialize_json(&item)),
|
||||
item: Some(item),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorkProcedureResult {
|
||||
ok: false,
|
||||
item_json: None,
|
||||
item: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -509,12 +598,12 @@ pub fn remix_puzzle_work(
|
||||
match ctx.try_with_tx(|tx| remix_puzzle_work_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -528,12 +617,12 @@ pub fn start_puzzle_run(
|
||||
match ctx.try_with_tx(|tx| start_puzzle_run_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -547,12 +636,12 @@ pub fn get_puzzle_run(
|
||||
match ctx.try_with_tx(|tx| get_puzzle_run_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -566,12 +655,12 @@ pub fn swap_puzzle_pieces(
|
||||
match ctx.try_with_tx(|tx| swap_puzzle_pieces_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -585,12 +674,12 @@ pub fn drag_puzzle_piece_or_group(
|
||||
match ctx.try_with_tx(|tx| drag_puzzle_piece_or_group_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -604,12 +693,12 @@ pub fn advance_puzzle_next_level(
|
||||
match ctx.try_with_tx(|tx| advance_puzzle_next_level_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -623,12 +712,12 @@ pub fn update_puzzle_run_pause(
|
||||
match ctx.try_with_tx(|tx| update_puzzle_run_pause_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -642,12 +731,12 @@ pub fn use_puzzle_runtime_prop(
|
||||
match ctx.try_with_tx(|tx| use_puzzle_runtime_prop_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -661,12 +750,12 @@ pub fn claim_puzzle_work_point_incentive(
|
||||
match ctx.try_with_tx(|tx| claim_puzzle_work_point_incentive_tx(tx, input.clone())) {
|
||||
Ok(item) => PuzzleWorkProcedureResult {
|
||||
ok: true,
|
||||
item_json: Some(serialize_json(&item)),
|
||||
item: Some(item),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorkProcedureResult {
|
||||
ok: false,
|
||||
item_json: None,
|
||||
item: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -680,12 +769,12 @@ pub fn submit_puzzle_leaderboard_entry(
|
||||
match ctx.try_with_tx(|tx| submit_puzzle_leaderboard_entry_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -974,6 +1063,7 @@ fn save_puzzle_generated_images_tx(
|
||||
if let Some(levels) = deserialize_optional_levels_input(input.levels_json.as_deref())? {
|
||||
// 中文注释:结果页新增关卡可能还没等到自动保存,生成图时以本次 action 携带的关卡快照作为写回目标。
|
||||
draft.levels = levels;
|
||||
draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
module_puzzle::sync_primary_level_fields(&mut draft);
|
||||
// 中文注释:入口直创会在 api-server 生成首关名后随 levels_json 写入;作品名仍是旧首关名或空值时才跟随首关名,避免覆盖用户手动命名。
|
||||
sync_generated_primary_level_name_as_default_work_title(
|
||||
@@ -1003,6 +1093,7 @@ fn save_puzzle_generated_images_tx(
|
||||
next_level.cover_asset_id = Some(selected.asset_id);
|
||||
}
|
||||
draft = replace_puzzle_level(&draft, next_level).map_err(|error| error.to_string())?;
|
||||
draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥儿主")).publish_ready {
|
||||
@@ -1054,6 +1145,7 @@ fn save_puzzle_ui_background_tx(
|
||||
if let Some(levels) = deserialize_optional_levels_input(input.levels_json.as_deref())? {
|
||||
// 中文注释:UI 背景可以在自动保存前立即生成,写回前优先使用本次 action 携带的关卡快照。
|
||||
draft.levels = levels;
|
||||
draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
module_puzzle::sync_primary_level_fields(&mut draft);
|
||||
}
|
||||
let target_level = selected_puzzle_level(&draft, input.level_id.as_deref())
|
||||
@@ -1066,6 +1158,7 @@ fn save_puzzle_ui_background_tx(
|
||||
(!trimmed.is_empty()).then_some(trimmed)
|
||||
});
|
||||
let draft = replace_puzzle_level(&draft, next_level).map_err(|error| error.to_string())?;
|
||||
let draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥儿主")).publish_ready {
|
||||
@@ -1119,6 +1212,52 @@ fn sync_generated_primary_level_name_as_default_work_title(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_completed_puzzle_level_generation_status(
|
||||
mut draft: PuzzleResultDraft,
|
||||
) -> PuzzleResultDraft {
|
||||
draft.levels = normalize_completed_puzzle_levels_generation_status(draft.levels);
|
||||
module_puzzle::sync_primary_level_fields(&mut draft);
|
||||
draft
|
||||
}
|
||||
|
||||
fn normalize_completed_puzzle_levels_generation_status(
|
||||
mut levels: Vec<module_puzzle::PuzzleDraftLevel>,
|
||||
) -> Vec<module_puzzle::PuzzleDraftLevel> {
|
||||
for level in &mut levels {
|
||||
if level.generation_status.trim() == "generating" && has_completed_puzzle_level_image(level)
|
||||
{
|
||||
level.generation_status = "ready".to_string();
|
||||
}
|
||||
}
|
||||
levels
|
||||
}
|
||||
|
||||
fn has_completed_puzzle_level_image(level: &module_puzzle::PuzzleDraftLevel) -> bool {
|
||||
let has_cover = level
|
||||
.cover_image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_selected_candidate = level
|
||||
.selected_candidate_id
|
||||
.as_deref()
|
||||
.and_then(|candidate_id| {
|
||||
level
|
||||
.candidates
|
||||
.iter()
|
||||
.find(|candidate| candidate.candidate_id == candidate_id)
|
||||
})
|
||||
.map(|candidate| candidate.image_src.trim())
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_fallback_candidate = level
|
||||
.candidates
|
||||
.last()
|
||||
.map(|candidate| candidate.image_src.trim())
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
|
||||
has_cover || has_selected_candidate || has_fallback_candidate
|
||||
}
|
||||
|
||||
fn select_puzzle_cover_image_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleSelectCoverImageInput,
|
||||
@@ -1264,8 +1403,8 @@ fn list_puzzle_works_tx(
|
||||
let mut items = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.by_puzzle_work_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.map(|row| build_puzzle_work_profile_from_row(&row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||
@@ -1302,12 +1441,13 @@ fn update_puzzle_work_tx(
|
||||
if theme_tags.len() > PUZZLE_MAX_TAG_COUNT {
|
||||
return Err("拼图标签数量不合法".to_string());
|
||||
}
|
||||
let levels = deserialize_optional_levels_input(input.levels_json.as_deref())?
|
||||
let mut levels = deserialize_optional_levels_input(input.levels_json.as_deref())?
|
||||
.map(|levels| {
|
||||
normalize_puzzle_levels(levels, &theme_tags).map_err(|error| error.to_string())
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| build_profile_levels_from_row(&row).unwrap_or_default());
|
||||
levels = normalize_completed_puzzle_levels_generation_status(levels);
|
||||
let preview_draft = PuzzleResultDraft {
|
||||
work_title: input.work_title.clone(),
|
||||
work_description: input.work_description.clone(),
|
||||
@@ -1446,8 +1586,8 @@ fn delete_puzzle_work_tx(
|
||||
for message in ctx
|
||||
.db
|
||||
.puzzle_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == *session_id)
|
||||
.by_puzzle_agent_message_session_id()
|
||||
.filter(session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
@@ -1459,10 +1599,9 @@ fn delete_puzzle_work_tx(
|
||||
for run in ctx
|
||||
.db
|
||||
.puzzle_runtime_run()
|
||||
.iter()
|
||||
.filter(|run| {
|
||||
run.owner_user_id == input.owner_user_id && run.entry_profile_id == input.profile_id
|
||||
})
|
||||
.by_puzzle_runtime_run_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.filter(|run| run.entry_profile_id == input.profile_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db.puzzle_runtime_run().run_id().delete(&run.run_id);
|
||||
@@ -1481,8 +1620,8 @@ fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, Str
|
||||
let rows = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.iter()
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.collect::<Vec<_>>();
|
||||
let profile_ids = rows
|
||||
.iter()
|
||||
@@ -2416,6 +2555,72 @@ fn build_puzzle_work_profile_from_row_without_recent_count(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_puzzle_gallery_card_view_row(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<PuzzleGalleryCardViewRow, String> {
|
||||
let levels = build_profile_levels_from_row(row)?;
|
||||
Ok(PuzzleGalleryCardViewRow {
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
work_title: if row.work_title.trim().is_empty() {
|
||||
row.level_name.clone()
|
||||
} else {
|
||||
row.work_title.clone()
|
||||
},
|
||||
work_description: if row.work_description.trim().is_empty() {
|
||||
row.summary.clone()
|
||||
} else {
|
||||
row.work_description.clone()
|
||||
},
|
||||
level_name: row.level_name.clone(),
|
||||
summary: row.summary.clone(),
|
||||
theme_tags: deserialize_theme_tags(&row.theme_tags_json)?,
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
cover_asset_id: row.cover_asset_id.clone(),
|
||||
publication_status: row.publication_status,
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
point_incentive_total_half_points: row.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
||||
publish_ready: row.publish_ready,
|
||||
generation_status: resolve_puzzle_gallery_generation_status(&levels),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_puzzle_gallery_generation_status(
|
||||
levels: &[module_puzzle::PuzzleDraftLevel],
|
||||
) -> Option<String> {
|
||||
if levels.iter().any(has_completed_puzzle_level_image) {
|
||||
return Some("ready".to_string());
|
||||
}
|
||||
|
||||
levels
|
||||
.iter()
|
||||
.map(|level| level.generation_status.trim())
|
||||
.find(|status| *status == "generating")
|
||||
.or_else(|| {
|
||||
levels
|
||||
.iter()
|
||||
.map(|level| level.generation_status.trim())
|
||||
.find(|status| *status == "ready")
|
||||
})
|
||||
.or_else(|| {
|
||||
levels
|
||||
.iter()
|
||||
.map(|level| level.generation_status.trim())
|
||||
.find(|status| !status.is_empty())
|
||||
})
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn build_profile_levels_from_row(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<Vec<module_puzzle::PuzzleDraftLevel>, String> {
|
||||
@@ -2542,8 +2747,8 @@ fn list_session_messages(ctx: &TxContext, session_id: &str) -> Vec<PuzzleAgentMe
|
||||
let mut items = ctx
|
||||
.db
|
||||
.puzzle_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == session_id)
|
||||
.by_puzzle_agent_message_session_id()
|
||||
.filter(&session_id.to_string())
|
||||
.map(|message| PuzzleAgentMessageSnapshot {
|
||||
message_id: message.message_id.clone(),
|
||||
session_id: message.session_id.clone(),
|
||||
@@ -2672,6 +2877,7 @@ fn replace_puzzle_work_profile(
|
||||
}
|
||||
|
||||
fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Result<(), String> {
|
||||
let levels = normalize_completed_puzzle_levels_generation_status(profile.levels);
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
@@ -2694,7 +2900,7 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
theme_tags_json: serialize_json(&profile.theme_tags),
|
||||
cover_image_src: profile.cover_image_src,
|
||||
cover_asset_id: profile.cover_asset_id,
|
||||
levels_json: serialize_json(&profile.levels),
|
||||
levels_json: serialize_json(&levels),
|
||||
publication_status: profile.publication_status,
|
||||
// 二次编辑发布同一个 profile 时,作品内容可以覆盖,但历史游玩数属于
|
||||
// 广场消费数据,不能因为重新发布被清零。
|
||||
@@ -2732,7 +2938,7 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
theme_tags_json: serialize_json(&profile.theme_tags),
|
||||
cover_image_src: profile.cover_image_src,
|
||||
cover_asset_id: profile.cover_asset_id,
|
||||
levels_json: serialize_json(&profile.levels),
|
||||
levels_json: serialize_json(&levels),
|
||||
publication_status: profile.publication_status,
|
||||
play_count: profile.play_count,
|
||||
remix_count: profile.remix_count,
|
||||
@@ -3152,8 +3358,8 @@ fn replace_generated_candidate(
|
||||
fn list_published_puzzle_profiles(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, String> {
|
||||
ctx.db
|
||||
.puzzle_work_profile()
|
||||
.iter()
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.map(|row| build_puzzle_work_profile_from_row(&row))
|
||||
.collect()
|
||||
}
|
||||
@@ -3319,8 +3525,8 @@ fn list_puzzle_leaderboard_entries(
|
||||
let mut rows = ctx
|
||||
.db
|
||||
.puzzle_leaderboard_entry()
|
||||
.iter()
|
||||
.filter(|row| row.profile_id == profile_id && row.grid_size == grid_size)
|
||||
.by_puzzle_leaderboard_profile_grid()
|
||||
.filter((profile_id, grid_size))
|
||||
.collect::<Vec<_>>();
|
||||
rows.sort_by(|left, right| {
|
||||
left.best_elapsed_ms
|
||||
@@ -3382,7 +3588,9 @@ fn deserialize_levels_json(value: &str) -> Result<Vec<module_puzzle::PuzzleDraft
|
||||
if value.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
json_from_str(value).map_err(|error| format!("拼图 levels JSON 非法: {error}"))
|
||||
json_from_str(value)
|
||||
.map(normalize_completed_puzzle_levels_generation_status)
|
||||
.map_err(|error| format!("拼图 levels JSON 非法: {error}"))
|
||||
}
|
||||
|
||||
fn deserialize_optional_levels_input(
|
||||
|
||||
@@ -95,8 +95,8 @@ fn list_platform_browse_history_rows(
|
||||
let mut entries = ctx
|
||||
.db
|
||||
.user_browse_history()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.by_browse_history_user_id()
|
||||
.filter(&validated_input.user_id)
|
||||
.map(|row| build_runtime_browse_history_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -165,8 +165,8 @@ fn clear_platform_browse_history_rows(
|
||||
let row_ids = ctx
|
||||
.db
|
||||
.user_browse_history()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.by_browse_history_user_id()
|
||||
.filter(&validated_input.user_id)
|
||||
.map(|row| row.browse_history_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -215,8 +215,7 @@ fn migrate_visual_novel_entry_from_old_visible_default(ctx: &ReducerContext, now
|
||||
&& row.subtitle == "分支叙事体验"
|
||||
&& row.image_src == "/creation-type-references/visual-novel.webp"
|
||||
&& row.visible
|
||||
&& ((row.badge == "可创建" && row.open)
|
||||
|| (row.badge == "敬请期待" && !row.open))
|
||||
&& ((row.badge == "可创建" && row.open) || (row.badge == "敬请期待" && !row.open))
|
||||
&& row.sort_order == 60;
|
||||
if !still_old_visible_default {
|
||||
return;
|
||||
|
||||
@@ -558,6 +558,33 @@ pub fn record_tracking_event_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
// 高频 route tracking 由 api-server 本机 outbox 批量写入,减少公开列表热路径上的 procedure 调用次数。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn record_tracking_events_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
inputs: Vec<RuntimeTrackingEventInput>,
|
||||
) -> RuntimeTrackingEventBatchProcedureResult {
|
||||
match ctx.try_with_tx(|tx| {
|
||||
let mut accepted_count = 0u32;
|
||||
for input in &inputs {
|
||||
record_tracking_event(tx, input.clone())?;
|
||||
accepted_count = accepted_count.saturating_add(1);
|
||||
}
|
||||
Ok(accepted_count)
|
||||
}) {
|
||||
Ok(accepted_count) => RuntimeTrackingEventBatchProcedureResult {
|
||||
ok: true,
|
||||
accepted_count,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeTrackingEventBatchProcedureResult {
|
||||
ok: false,
|
||||
accepted_count: 0,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 登录成功埋点由认证链路主动调用;任务中心只负责读取和刷新任务进度。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn record_daily_login_tracking_event_and_return(
|
||||
@@ -1079,8 +1106,8 @@ pub(crate) fn list_profile_save_archive_rows(
|
||||
let mut entries = ctx
|
||||
.db
|
||||
.profile_save_archive()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.by_profile_save_archive_user_id()
|
||||
.filter(&validated_input.user_id)
|
||||
.map(|row| build_profile_save_archive_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1104,10 +1131,12 @@ pub(crate) fn resume_profile_save_archive_record(
|
||||
let archive = ctx
|
||||
.db
|
||||
.profile_save_archive()
|
||||
.iter()
|
||||
.find(|row| {
|
||||
row.user_id == validated_input.user_id && row.world_key == validated_input.world_key
|
||||
})
|
||||
.by_profile_save_archive_user_world_key()
|
||||
.filter((
|
||||
validated_input.user_id.as_str(),
|
||||
validated_input.world_key.as_str(),
|
||||
))
|
||||
.next()
|
||||
.ok_or_else(|| "profile_save_archive 对应 world_key 不存在".to_string())?;
|
||||
|
||||
let existing_snapshot = ctx
|
||||
@@ -1537,6 +1566,19 @@ mod tests {
|
||||
assert!(!should_skip_existing_tracking_event_id(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracking_batch_result_reports_accepted_count() {
|
||||
let result = RuntimeTrackingEventBatchProcedureResult {
|
||||
ok: true,
|
||||
accepted_count: 2,
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
assert!(result.ok);
|
||||
assert_eq!(result.accepted_count, 2);
|
||||
assert!(result.error_message.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_public_work_play_counts_group_requested_profiles_in_window() {
|
||||
let now_micros = PUBLIC_WORK_PLAY_DAY_MICROS * 10;
|
||||
@@ -2052,8 +2094,8 @@ fn get_profile_dashboard_snapshot(
|
||||
let played_world_count = ctx
|
||||
.db
|
||||
.profile_played_world()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.by_profile_played_world_user_id()
|
||||
.filter(&validated_input.user_id)
|
||||
.count() as u32;
|
||||
|
||||
Ok(match state {
|
||||
@@ -2084,8 +2126,8 @@ fn list_profile_wallet_ledger_entries(
|
||||
let mut entries = ctx
|
||||
.db
|
||||
.profile_wallet_ledger()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.by_profile_wallet_ledger_user_id()
|
||||
.filter(&validated_input.user_id)
|
||||
.map(|row| build_profile_wallet_ledger_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -2114,8 +2156,8 @@ fn get_profile_play_stats_snapshot(
|
||||
let mut played_works = ctx
|
||||
.db
|
||||
.profile_played_world()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.by_profile_played_world_user_id()
|
||||
.filter(&validated_input.user_id)
|
||||
.map(|row| build_profile_played_world_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -2727,17 +2769,16 @@ fn build_profile_referral_invite_center_snapshot(
|
||||
let code = ensure_profile_invite_code(ctx, user_id);
|
||||
let today_inviter_reward_count =
|
||||
count_today_profile_referral_inviter_rewards(ctx, user_id, ctx.timestamp);
|
||||
let invited_count = ctx
|
||||
let invited_relations = ctx
|
||||
.db
|
||||
.profile_referral_relation()
|
||||
.by_profile_referral_inviter_user_id()
|
||||
.filter(user_id)
|
||||
.collect::<Vec<_>>();
|
||||
let invited_count = invited_relations.len() as u32;
|
||||
let rewarded_invite_count = invited_relations
|
||||
.iter()
|
||||
.filter(|row| row.inviter_user_id == user_id)
|
||||
.count() as u32;
|
||||
let rewarded_invite_count = ctx
|
||||
.db
|
||||
.profile_referral_relation()
|
||||
.iter()
|
||||
.filter(|row| row.inviter_user_id == user_id && row.inviter_reward_granted)
|
||||
.filter(|row| row.inviter_reward_granted)
|
||||
.count() as u32;
|
||||
let bound_relation = ctx
|
||||
.db
|
||||
@@ -2918,7 +2959,8 @@ fn count_today_profile_referral_inviter_rewards(
|
||||
let day_start_micros = runtime_profile_day_start_micros(now.to_micros_since_unix_epoch());
|
||||
ctx.db
|
||||
.profile_wallet_ledger()
|
||||
.iter()
|
||||
.by_profile_wallet_ledger_user_id()
|
||||
.filter(user_id)
|
||||
.filter(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.source_type == RuntimeProfileWalletLedgerSourceType::InviteInviterReward
|
||||
@@ -3422,7 +3464,11 @@ fn query_analytics_metric_buckets(
|
||||
let stats = ctx
|
||||
.db
|
||||
.tracking_daily_stat()
|
||||
.iter()
|
||||
.by_tracking_daily_stat_scope_day()
|
||||
.filter((
|
||||
validated_input.scope_kind,
|
||||
validated_input.scope_id.as_str(),
|
||||
))
|
||||
.filter(|row| {
|
||||
row.event_key.trim() == validated_input.event_key
|
||||
&& row.scope_kind == validated_input.scope_kind
|
||||
@@ -4023,27 +4069,39 @@ fn apply_profile_wallet_signed_delta(
|
||||
}
|
||||
|
||||
fn has_profile_points_recharged(ctx: &ReducerContext, user_id: &str) -> bool {
|
||||
ctx.db.profile_recharge_order().iter().any(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.kind == RuntimeProfileRechargeProductKind::Points
|
||||
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
|
||||
})
|
||||
ctx.db
|
||||
.profile_recharge_order()
|
||||
.by_profile_recharge_order_user_id()
|
||||
.filter(user_id)
|
||||
.any(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.kind == RuntimeProfileRechargeProductKind::Points
|
||||
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
|
||||
})
|
||||
}
|
||||
|
||||
fn has_profile_product_recharged(ctx: &ReducerContext, user_id: &str, product_id: &str) -> bool {
|
||||
ctx.db.profile_recharge_order().iter().any(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.product_id == product_id
|
||||
&& row.kind == RuntimeProfileRechargeProductKind::Points
|
||||
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
|
||||
})
|
||||
ctx.db
|
||||
.profile_recharge_order()
|
||||
.by_profile_recharge_order_user_id()
|
||||
.filter(user_id)
|
||||
.any(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.product_id == product_id
|
||||
&& row.kind == RuntimeProfileRechargeProductKind::Points
|
||||
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
|
||||
})
|
||||
}
|
||||
|
||||
fn has_profile_business_wallet_ledger(ctx: &ReducerContext, user_id: &str) -> bool {
|
||||
ctx.db.profile_wallet_ledger().iter().any(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.source_type != RuntimeProfileWalletLedgerSourceType::SnapshotSync
|
||||
})
|
||||
ctx.db
|
||||
.profile_wallet_ledger()
|
||||
.by_profile_wallet_ledger_user_id()
|
||||
.filter(user_id)
|
||||
.any(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.source_type != RuntimeProfileWalletLedgerSourceType::SnapshotSync
|
||||
})
|
||||
}
|
||||
|
||||
fn latest_profile_recharge_order(
|
||||
@@ -4053,8 +4111,8 @@ fn latest_profile_recharge_order(
|
||||
let mut orders = ctx
|
||||
.db
|
||||
.profile_recharge_order()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == user_id)
|
||||
.by_profile_recharge_order_user_id()
|
||||
.filter(user_id)
|
||||
.collect::<Vec<_>>();
|
||||
orders.sort_by(|left, right| {
|
||||
right
|
||||
|
||||
@@ -26,6 +26,65 @@ use module_square_hole::{
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
/// 方洞挑战公开广场列表投影。
|
||||
///
|
||||
/// HTTP gallery 通过 `spacetime-client` 订阅该 view 后读本地 cache,
|
||||
/// 不再在每个公开列表请求里调用 `list_square_hole_works` procedure。
|
||||
#[spacetimedb::view(accessor = square_hole_gallery_view, public)]
|
||||
pub fn square_hole_gallery_view(ctx: &AnonymousViewContext) -> Vec<SquareHoleGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.square_hole_work_profile()
|
||||
.by_square_hole_work_publication_status()
|
||||
.filter(SQUARE_HOLE_PUBLICATION_PUBLISHED)
|
||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"方洞挑战公开广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleGalleryViewRow {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: String,
|
||||
pub background_prompt: String,
|
||||
pub background_image_src: String,
|
||||
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_square_hole_agent_session(
|
||||
@@ -112,12 +171,12 @@ pub fn list_square_hole_works(
|
||||
match ctx.try_with_tx(|tx| list_square_hole_works_tx(tx, input.clone())) {
|
||||
Ok(items) => SquareHoleWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => SquareHoleWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -142,12 +201,12 @@ pub fn delete_square_hole_work(
|
||||
match ctx.try_with_tx(|tx| delete_square_hole_work_tx(tx, input.clone())) {
|
||||
Ok(items) => SquareHoleWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => SquareHoleWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -185,8 +244,8 @@ pub fn drop_square_hole_shape(
|
||||
Err(message) => SquareHoleDropShapeProcedureResult {
|
||||
ok: false,
|
||||
status: SQUARE_HOLE_DROP_REJECTED.to_string(),
|
||||
run_json: None,
|
||||
feedback_json: None,
|
||||
run: None,
|
||||
feedback: None,
|
||||
failure_reason: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
@@ -743,10 +802,8 @@ fn drop_square_hole_shape_tx(
|
||||
Ok(SquareHoleDropShapeProcedureResult {
|
||||
ok: true,
|
||||
status: status.to_string(),
|
||||
run_json: Some(to_json_string(&next)),
|
||||
feedback_json: Some(to_json_string(&feedback_from_domain(
|
||||
&confirmation.feedback,
|
||||
))),
|
||||
run: Some(next),
|
||||
feedback: Some(feedback_from_domain(&confirmation.feedback)),
|
||||
failure_reason: confirmation
|
||||
.feedback
|
||||
.reject_reason
|
||||
@@ -880,6 +937,38 @@ fn build_work_snapshot(row: &SquareHoleWorkProfileRow) -> Result<SquareHoleWorkS
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gallery_view_row(
|
||||
row: &SquareHoleWorkProfileRow,
|
||||
) -> Result<SquareHoleGalleryViewRow, String> {
|
||||
let config = parse_config(&row.config_json)?;
|
||||
Ok(SquareHoleGalleryViewRow {
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
game_name: row.game_name.clone(),
|
||||
theme_text: row.theme_text.clone(),
|
||||
twist_rule: row.twist_rule.clone(),
|
||||
summary_text: row.summary_text.clone(),
|
||||
tags: parse_tags(&row.tags_json)?,
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
background_prompt: config.background_prompt,
|
||||
background_image_src: config.background_image_src,
|
||||
shape_options: config.shape_options,
|
||||
hole_options: config.hole_options,
|
||||
shape_count: row.shape_count,
|
||||
difficulty: row.difficulty,
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: is_work_publish_ready(row),
|
||||
play_count: row.play_count,
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_run_row(
|
||||
ctx: &ReducerContext,
|
||||
row: SquareHoleRuntimeRunRow,
|
||||
@@ -1502,7 +1591,7 @@ fn session_result(
|
||||
) -> SquareHoleAgentSessionProcedureResult {
|
||||
SquareHoleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(to_json_string(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1510,7 +1599,7 @@ fn session_result(
|
||||
fn session_error(message: String) -> SquareHoleAgentSessionProcedureResult {
|
||||
SquareHoleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -1518,7 +1607,7 @@ fn session_error(message: String) -> SquareHoleAgentSessionProcedureResult {
|
||||
fn work_result(work: SquareHoleWorkSnapshot) -> SquareHoleWorkProcedureResult {
|
||||
SquareHoleWorkProcedureResult {
|
||||
ok: true,
|
||||
work_json: Some(to_json_string(&work)),
|
||||
work: Some(work),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1526,7 +1615,7 @@ fn work_result(work: SquareHoleWorkSnapshot) -> SquareHoleWorkProcedureResult {
|
||||
fn work_error(message: String) -> SquareHoleWorkProcedureResult {
|
||||
SquareHoleWorkProcedureResult {
|
||||
ok: false,
|
||||
work_json: None,
|
||||
work: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -1534,7 +1623,7 @@ fn work_error(message: String) -> SquareHoleWorkProcedureResult {
|
||||
fn run_result(run: SquareHoleRunSnapshot) -> SquareHoleRunProcedureResult {
|
||||
SquareHoleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(to_json_string(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1542,7 +1631,7 @@ fn run_result(run: SquareHoleRunSnapshot) -> SquareHoleRunProcedureResult {
|
||||
fn run_error(message: String) -> SquareHoleRunProcedureResult {
|
||||
SquareHoleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -168,42 +168,42 @@ pub struct SquareHoleRunTimeUpInput {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleAgentSessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session_json: Option<String>,
|
||||
pub session: Option<SquareHoleAgentSessionSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorkProcedureResult {
|
||||
pub ok: bool,
|
||||
pub work_json: Option<String>,
|
||||
pub work: Option<SquareHoleWorkSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleWorksProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub items: Vec<SquareHoleWorkSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct SquareHoleRunProcedureResult {
|
||||
pub ok: bool,
|
||||
pub run_json: Option<String>,
|
||||
pub run: Option<SquareHoleRunSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct SquareHoleDropShapeProcedureResult {
|
||||
pub ok: bool,
|
||||
pub status: String,
|
||||
pub run_json: Option<String>,
|
||||
pub feedback_json: Option<String>,
|
||||
pub run: Option<SquareHoleRunSnapshot>,
|
||||
pub feedback: Option<SquareHoleDropFeedbackSnapshot>,
|
||||
pub failure_reason: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleCreatorConfigSnapshot {
|
||||
pub theme_text: String,
|
||||
@@ -222,7 +222,7 @@ pub struct SquareHoleCreatorConfigSnapshot {
|
||||
pub background_image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeOptionSnapshot {
|
||||
pub option_id: String,
|
||||
@@ -235,7 +235,7 @@ pub struct SquareHoleShapeOptionSnapshot {
|
||||
pub image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleOptionSnapshot {
|
||||
pub hole_id: String,
|
||||
@@ -247,7 +247,7 @@ pub struct SquareHoleHoleOptionSnapshot {
|
||||
pub image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleAgentMessageSnapshot {
|
||||
pub message_id: String,
|
||||
@@ -258,7 +258,7 @@ pub struct SquareHoleAgentMessageSnapshot {
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleDraftSnapshot {
|
||||
pub profile_id: String,
|
||||
@@ -281,7 +281,7 @@ pub struct SquareHoleDraftSnapshot {
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleAgentSessionSnapshot {
|
||||
pub session_id: String,
|
||||
@@ -299,7 +299,7 @@ pub struct SquareHoleAgentSessionSnapshot {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleWorkSnapshot {
|
||||
pub work_id: String,
|
||||
@@ -331,7 +331,7 @@ pub struct SquareHoleWorkSnapshot {
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeSnapshot {
|
||||
pub shape_id: String,
|
||||
@@ -344,7 +344,7 @@ pub struct SquareHoleShapeSnapshot {
|
||||
pub image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleSnapshot {
|
||||
pub hole_id: String,
|
||||
@@ -356,7 +356,7 @@ pub struct SquareHoleHoleSnapshot {
|
||||
pub image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleDropFeedbackSnapshot {
|
||||
pub accepted: bool,
|
||||
@@ -364,7 +364,7 @@ pub struct SquareHoleDropFeedbackSnapshot {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleRunSnapshot {
|
||||
pub run_id: String,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::*;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
pub const VISUAL_NOVEL_SOURCE_IDEA: &str = "idea";
|
||||
pub const VISUAL_NOVEL_SOURCE_DOCUMENT: &str = "document";
|
||||
@@ -166,6 +167,58 @@ pub struct VisualNovelRuntimeEvent {
|
||||
pub(crate) occurred_at: Timestamp,
|
||||
}
|
||||
|
||||
/// 视觉小说公开广场列表投影。
|
||||
///
|
||||
/// 该 view 只暴露已发布作品卡片需要的公开字段,HTTP gallery 订阅后
|
||||
/// 从本地 cache 读取,避免每个列表请求调用 `list_visual_novel_works` procedure。
|
||||
#[spacetimedb::view(accessor = visual_novel_gallery_view, public)]
|
||||
pub fn visual_novel_gallery_view(ctx: &AnonymousViewContext) -> Vec<VisualNovelGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.visual_novel_work_profile()
|
||||
.by_visual_novel_work_publication_status()
|
||||
.filter(VISUAL_NOVEL_PUBLICATION_PUBLISHED)
|
||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"视觉小说公开广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct VisualNovelGalleryViewRow {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: Option<String>,
|
||||
pub author_display_name: String,
|
||||
pub work_title: String,
|
||||
pub work_description: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub source_asset_ids: Vec<String>,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct VisualNovelAgentSessionCreateInput {
|
||||
pub session_id: String,
|
||||
@@ -326,49 +379,65 @@ pub struct VisualNovelRuntimeEventRecordInput {
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct VisualNovelAgentSessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session_json: Option<String>,
|
||||
pub session: Option<VisualNovelAgentSessionSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct VisualNovelWorkProcedureResult {
|
||||
pub ok: bool,
|
||||
pub work_json: Option<String>,
|
||||
pub work: Option<VisualNovelWorkSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct VisualNovelWorksProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub items: Vec<VisualNovelWorkSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct VisualNovelRunProcedureResult {
|
||||
pub ok: bool,
|
||||
pub run_json: Option<String>,
|
||||
pub run: Option<VisualNovelRunSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct VisualNovelHistoryProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub items: Vec<VisualNovelRuntimeHistoryEntrySnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct VisualNovelRuntimeEventProcedureResult {
|
||||
pub ok: bool,
|
||||
pub event_json: Option<String>,
|
||||
pub event: Option<VisualNovelRuntimeEventSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
pub struct VisualNovelJsonField {
|
||||
pub key: String,
|
||||
pub value: VisualNovelJsonValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
pub enum VisualNovelJsonValue {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
Array(Vec<VisualNovelJsonValue>),
|
||||
Object(Vec<VisualNovelJsonField>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelAgentMessageSnapshot {
|
||||
pub message_id: String,
|
||||
@@ -379,7 +448,7 @@ pub struct VisualNovelAgentMessageSnapshot {
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelAgentSessionSnapshot {
|
||||
pub session_id: String,
|
||||
@@ -391,15 +460,15 @@ pub struct VisualNovelAgentSessionSnapshot {
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub messages: Vec<VisualNovelAgentMessageSnapshot>,
|
||||
pub draft: Option<JsonValue>,
|
||||
pub pending_action: Option<JsonValue>,
|
||||
pub draft: Option<VisualNovelJsonValue>,
|
||||
pub pending_action: Option<VisualNovelJsonValue>,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub published_profile_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelWorkSnapshot {
|
||||
pub work_id: String,
|
||||
@@ -412,7 +481,7 @@ pub struct VisualNovelWorkSnapshot {
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub source_asset_ids: Vec<String>,
|
||||
pub draft: JsonValue,
|
||||
pub draft: VisualNovelJsonValue,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
@@ -421,7 +490,7 @@ pub struct VisualNovelWorkSnapshot {
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRuntimeHistoryEntrySnapshot {
|
||||
pub entry_id: String,
|
||||
@@ -431,13 +500,13 @@ pub struct VisualNovelRuntimeHistoryEntrySnapshot {
|
||||
pub turn_index: u32,
|
||||
pub source: String,
|
||||
pub action_text: Option<String>,
|
||||
pub steps: JsonValue,
|
||||
pub steps: VisualNovelJsonValue,
|
||||
pub snapshot_before_hash: Option<String>,
|
||||
pub snapshot_after_hash: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRunSnapshot {
|
||||
pub run_id: String,
|
||||
@@ -448,16 +517,16 @@ pub struct VisualNovelRunSnapshot {
|
||||
pub current_scene_id: Option<String>,
|
||||
pub current_phase_id: Option<String>,
|
||||
pub visible_character_ids: Vec<String>,
|
||||
pub flags: JsonValue,
|
||||
pub metrics: JsonValue,
|
||||
pub flags: VisualNovelJsonValue,
|
||||
pub metrics: VisualNovelJsonValue,
|
||||
pub history: Vec<VisualNovelRuntimeHistoryEntrySnapshot>,
|
||||
pub available_choices: JsonValue,
|
||||
pub available_choices: VisualNovelJsonValue,
|
||||
pub text_mode_enabled: bool,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VisualNovelRuntimeEventSnapshot {
|
||||
pub event_id: String,
|
||||
@@ -467,7 +536,7 @@ pub struct VisualNovelRuntimeEventSnapshot {
|
||||
pub event_kind: String,
|
||||
pub client_event_id: Option<String>,
|
||||
pub history_entry_id: Option<String>,
|
||||
pub payload: JsonValue,
|
||||
pub payload: VisualNovelJsonValue,
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -556,12 +625,12 @@ pub fn list_visual_novel_works(
|
||||
match ctx.try_with_tx(|tx| list_visual_novel_works_tx(tx, input.clone())) {
|
||||
Ok(items) => VisualNovelWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => VisualNovelWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -586,12 +655,12 @@ pub fn delete_visual_novel_work(
|
||||
match ctx.try_with_tx(|tx| delete_visual_novel_work_tx(tx, input.clone())) {
|
||||
Ok(items) => VisualNovelWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => VisualNovelWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -638,12 +707,12 @@ pub fn append_visual_novel_runtime_history_entry(
|
||||
match ctx.try_with_tx(|tx| append_visual_novel_runtime_history_entry_tx(tx, input.clone())) {
|
||||
Ok(items) => VisualNovelHistoryProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => VisualNovelHistoryProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -657,12 +726,12 @@ pub fn list_visual_novel_runtime_history(
|
||||
match ctx.try_with_tx(|tx| list_visual_novel_runtime_history_tx(tx, input.clone())) {
|
||||
Ok(items) => VisualNovelHistoryProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(to_json_string(&items)),
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => VisualNovelHistoryProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -676,12 +745,12 @@ pub fn record_visual_novel_runtime_event(
|
||||
match ctx.try_with_tx(|tx| record_visual_novel_runtime_event_tx(tx, input.clone())) {
|
||||
Ok(event) => VisualNovelRuntimeEventProcedureResult {
|
||||
ok: true,
|
||||
event_json: Some(to_json_string(&event)),
|
||||
event: Some(event),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => VisualNovelRuntimeEventProcedureResult {
|
||||
ok: false,
|
||||
event_json: None,
|
||||
event: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
@@ -1052,17 +1121,22 @@ fn list_visual_novel_works_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: VisualNovelWorksListInput,
|
||||
) -> Result<Vec<VisualNovelWorkSnapshot>, String> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.visual_novel_work_profile()
|
||||
let rows = if input.published_only {
|
||||
ctx.db
|
||||
.visual_novel_work_profile()
|
||||
.by_visual_novel_work_publication_status()
|
||||
.filter(&VISUAL_NOVEL_PUBLICATION_PUBLISHED.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
require_non_empty(&input.owner_user_id, "visual_novel owner_user_id")?;
|
||||
ctx.db
|
||||
.visual_novel_work_profile()
|
||||
.by_visual_novel_work_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut items = rows
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
if input.published_only {
|
||||
row.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED
|
||||
} else {
|
||||
row.owner_user_id == input.owner_user_id
|
||||
}
|
||||
})
|
||||
.map(|row| build_work_snapshot(&row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_by(|left, right| {
|
||||
@@ -1103,10 +1177,9 @@ fn delete_visual_novel_work_tx(
|
||||
for run in ctx
|
||||
.db
|
||||
.visual_novel_runtime_run()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.profile_id == input.profile_id && row.owner_user_id == input.owner_user_id
|
||||
})
|
||||
.by_visual_novel_run_profile_id()
|
||||
.filter(&input.profile_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
delete_run_children(ctx, &run.run_id, &input.owner_user_id);
|
||||
@@ -1385,8 +1458,8 @@ fn build_session_snapshot(
|
||||
let mut messages = ctx
|
||||
.db
|
||||
.visual_novel_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == row.session_id)
|
||||
.by_visual_novel_agent_message_session_id()
|
||||
.filter(&row.session_id)
|
||||
.map(|message| VisualNovelAgentMessageSnapshot {
|
||||
message_id: message.message_id,
|
||||
session_id: message.session_id,
|
||||
@@ -1412,8 +1485,9 @@ fn build_session_snapshot(
|
||||
current_turn: row.current_turn,
|
||||
progress_percent: row.progress_percent,
|
||||
messages,
|
||||
draft: parse_optional_json_value(&row.draft_json)?,
|
||||
pending_action: parse_optional_json_value(&row.pending_action_json)?,
|
||||
draft: parse_optional_json_value(&row.draft_json)?.map(visual_novel_json_from_serde),
|
||||
pending_action: parse_optional_json_value(&row.pending_action_json)?
|
||||
.map(visual_novel_json_from_serde),
|
||||
last_assistant_reply: empty_to_none(&row.last_assistant_reply),
|
||||
published_profile_id: empty_to_none(&row.published_profile_id),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
@@ -1433,7 +1507,32 @@ fn build_work_snapshot(row: &VisualNovelWorkProfileRow) -> Result<VisualNovelWor
|
||||
tags: parse_string_vec_or_empty(&row.tags_json)?,
|
||||
cover_image_src: empty_to_none(&row.cover_image_src),
|
||||
source_asset_ids: parse_string_vec_or_empty(&row.source_asset_ids_json)?,
|
||||
draft: parse_json_value(&row.draft_json)?,
|
||||
draft: visual_novel_json_from_serde(parse_json_value(&row.draft_json)?),
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
play_count: row.play_count,
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gallery_view_row(
|
||||
row: &VisualNovelWorkProfileRow,
|
||||
) -> Result<VisualNovelGalleryViewRow, String> {
|
||||
Ok(VisualNovelGalleryViewRow {
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: empty_to_none(&row.source_session_id),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
work_title: row.work_title.clone(),
|
||||
work_description: row.work_description.clone(),
|
||||
tags: parse_string_vec_or_empty(&row.tags_json)?,
|
||||
cover_image_src: empty_to_none(&row.cover_image_src),
|
||||
source_asset_ids: parse_string_vec_or_empty(&row.source_asset_ids_json)?,
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
play_count: row.play_count,
|
||||
@@ -1458,10 +1557,12 @@ fn build_run_snapshot(
|
||||
current_scene_id: empty_to_none(&row.current_scene_id),
|
||||
current_phase_id: empty_to_none(&row.current_phase_id),
|
||||
visible_character_ids: parse_string_vec_or_empty(&row.visible_character_ids_json)?,
|
||||
flags: parse_json_value_or_object(&row.flags_json)?,
|
||||
metrics: parse_json_value_or_object(&row.metrics_json)?,
|
||||
flags: visual_novel_json_from_serde(parse_json_value_or_object(&row.flags_json)?),
|
||||
metrics: visual_novel_json_from_serde(parse_json_value_or_object(&row.metrics_json)?),
|
||||
history: build_history_snapshots(ctx, &row.run_id, &row.owner_user_id)?,
|
||||
available_choices: parse_json_value_or_array(&row.available_choices_json)?,
|
||||
available_choices: visual_novel_json_from_serde(parse_json_value_or_array(
|
||||
&row.available_choices_json,
|
||||
)?),
|
||||
text_mode_enabled: row.text_mode_enabled,
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
@@ -1476,8 +1577,9 @@ fn build_history_snapshots(
|
||||
let mut items = ctx
|
||||
.db
|
||||
.visual_novel_runtime_history_entry()
|
||||
.iter()
|
||||
.filter(|row| row.run_id == run_id && row.owner_user_id == owner_user_id)
|
||||
.by_visual_novel_history_run_id()
|
||||
.filter(&run_id.to_string())
|
||||
.filter(|row| row.owner_user_id == owner_user_id)
|
||||
.map(|row| build_history_snapshot(&row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_by(|left, right| {
|
||||
@@ -1500,7 +1602,7 @@ fn build_history_snapshot(
|
||||
turn_index: row.turn_index,
|
||||
source: row.source.clone(),
|
||||
action_text: empty_to_none(&row.action_text),
|
||||
steps: parse_json_value_or_array(&row.steps_json)?,
|
||||
steps: visual_novel_json_from_serde(parse_json_value_or_array(&row.steps_json)?),
|
||||
snapshot_before_hash: empty_to_none(&row.snapshot_before_hash),
|
||||
snapshot_after_hash: empty_to_none(&row.snapshot_after_hash),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
@@ -1518,7 +1620,7 @@ fn build_event_snapshot(
|
||||
event_kind: row.event_kind.clone(),
|
||||
client_event_id: empty_to_none(&row.client_event_id),
|
||||
history_entry_id: empty_to_none(&row.history_entry_id),
|
||||
payload: parse_json_value_or_object(&row.payload_json)?,
|
||||
payload: visual_novel_json_from_serde(parse_json_value_or_object(&row.payload_json)?),
|
||||
occurred_at_micros: row.occurred_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
}
|
||||
@@ -1579,8 +1681,9 @@ fn delete_run_children(ctx: &ReducerContext, run_id: &str, owner_user_id: &str)
|
||||
for history in ctx
|
||||
.db
|
||||
.visual_novel_runtime_history_entry()
|
||||
.iter()
|
||||
.filter(|row| row.run_id == run_id && row.owner_user_id == owner_user_id)
|
||||
.by_visual_novel_history_run_id()
|
||||
.filter(&run_id.to_string())
|
||||
.filter(|row| row.owner_user_id == owner_user_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
@@ -1758,6 +1861,30 @@ fn parse_json_value_or_array(value: &str) -> Result<JsonValue, String> {
|
||||
parse_json_value(value)
|
||||
}
|
||||
|
||||
fn visual_novel_json_from_serde(value: JsonValue) -> VisualNovelJsonValue {
|
||||
match value {
|
||||
JsonValue::Null => VisualNovelJsonValue::Null,
|
||||
JsonValue::Bool(value) => VisualNovelJsonValue::Bool(value),
|
||||
JsonValue::Number(value) => VisualNovelJsonValue::Number(value.as_f64().unwrap_or(0.0)),
|
||||
JsonValue::String(value) => VisualNovelJsonValue::String(value),
|
||||
JsonValue::Array(items) => VisualNovelJsonValue::Array(
|
||||
items
|
||||
.into_iter()
|
||||
.map(visual_novel_json_from_serde)
|
||||
.collect(),
|
||||
),
|
||||
JsonValue::Object(object) => VisualNovelJsonValue::Object(
|
||||
object
|
||||
.into_iter()
|
||||
.map(|(key, value)| VisualNovelJsonField {
|
||||
key,
|
||||
value: visual_novel_json_from_serde(value),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn draft_string_field(draft: &JsonValue, key: &str) -> Option<String> {
|
||||
draft
|
||||
.get(key)
|
||||
@@ -1853,7 +1980,7 @@ fn session_result(
|
||||
) -> VisualNovelAgentSessionProcedureResult {
|
||||
VisualNovelAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(to_json_string(&session)),
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1861,7 +1988,7 @@ fn session_result(
|
||||
fn session_error(message: String) -> VisualNovelAgentSessionProcedureResult {
|
||||
VisualNovelAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -1869,7 +1996,7 @@ fn session_error(message: String) -> VisualNovelAgentSessionProcedureResult {
|
||||
fn work_result(work: VisualNovelWorkSnapshot) -> VisualNovelWorkProcedureResult {
|
||||
VisualNovelWorkProcedureResult {
|
||||
ok: true,
|
||||
work_json: Some(to_json_string(&work)),
|
||||
work: Some(work),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1877,7 +2004,7 @@ fn work_result(work: VisualNovelWorkSnapshot) -> VisualNovelWorkProcedureResult
|
||||
fn work_error(message: String) -> VisualNovelWorkProcedureResult {
|
||||
VisualNovelWorkProcedureResult {
|
||||
ok: false,
|
||||
work_json: None,
|
||||
work: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
@@ -1885,7 +2012,7 @@ fn work_error(message: String) -> VisualNovelWorkProcedureResult {
|
||||
fn run_result(run: VisualNovelRunSnapshot) -> VisualNovelRunProcedureResult {
|
||||
VisualNovelRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(to_json_string(&run)),
|
||||
run: Some(run),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
@@ -1893,7 +2020,7 @@ fn run_result(run: VisualNovelRunSnapshot) -> VisualNovelRunProcedureResult {
|
||||
fn run_error(message: String) -> VisualNovelRunProcedureResult {
|
||||
VisualNovelRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
run: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user