fix: sync rust api-server runtime and bindings

This commit is contained in:
2026-04-23 20:32:06 +08:00
parent 9d25a47b23
commit 27e84c46a0
82 changed files with 9534 additions and 2222 deletions

View File

@@ -995,15 +995,14 @@ fn upsert_custom_world_profile_record(
.find(&input.profile_id)
.filter(|row| row.owner_user_id == input.owner_user_id)
.or_else(|| {
input.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,
)
input
.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)
})
})
})
});
let next_row = match current {
@@ -1432,18 +1431,16 @@ fn list_custom_world_work_snapshots(
let mut items = Vec::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)
{
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 gate = build_custom_world_publish_gate_from_session(&session);
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
let title = resolve_session_work_title(&session, draft_profile.as_ref());
let summary = resolve_session_work_summary(&session, draft_profile.as_ref());
let stage_label = Some(resolve_rpg_agent_stage_label(session.stage).to_string());
let subtitle = resolve_session_work_subtitle(draft_profile.as_ref(), stage_label.as_deref());
let subtitle =
resolve_session_work_subtitle(draft_profile.as_ref(), stage_label.as_deref());
let (playable_npc_count, landmark_count) =
resolve_session_work_counts(ctx, &session, draft_profile.as_ref());
@@ -1516,8 +1513,16 @@ fn list_custom_world_work_snapshots(
.updated_at_micros
.cmp(&left.updated_at_micros)
.then_with(|| {
let left_rank = if left.source_type == "agent_session" { 0 } else { 1 };
let right_rank = if right.source_type == "agent_session" { 0 } else { 1 };
let left_rank = if left.source_type == "agent_session" {
0
} else {
1
};
let right_rank = if right.source_type == "agent_session" {
0
} else {
1
};
left_rank.cmp(&right_rank)
})
.then(left.work_id.cmp(&right.work_id))
@@ -1578,7 +1583,9 @@ fn execute_custom_world_agent_action_tx(
match input.action.trim() {
"draft_foundation" => execute_draft_foundation_action(ctx, &session, &input, &payload),
"update_draft_card" => execute_update_draft_card_action(ctx, &session, &input, &payload),
"sync_result_profile" => execute_sync_result_profile_action(ctx, &session, &input, &payload),
"sync_result_profile" => {
execute_sync_result_profile_action(ctx, &session, &input, &payload)
}
"publish_world" => execute_publish_world_action(ctx, &session, &input, &payload),
"revert_checkpoint" => execute_revert_checkpoint_action(ctx, &session, &input, &payload),
"generate_characters"
@@ -1603,18 +1610,16 @@ fn execute_draft_foundation_action(
}
let updated_at = input.submitted_at_micros;
let draft_profile = if let Some(profile) = payload.get("draftProfile").and_then(JsonValue::as_object) {
profile.clone()
} else if let Some(existing) = parse_optional_session_object(session.draft_profile_json.as_deref()) {
ensure_minimal_draft_profile(existing, &session.seed_text)
} else {
build_minimal_draft_profile_from_seed(&session.seed_text)
};
let draft_profile_json =
serde_json::to_string(&JsonValue::Object(draft_profile.clone())).map_err(|error| {
format!("draft_foundation 无法序列化 draft_profile_json: {error}")
let draft_profile = payload
.get("draftProfile")
.and_then(JsonValue::as_object)
.cloned()
.ok_or_else(|| {
"draft_foundation requires externally generated payload.draftProfile".to_string()
})?;
let draft_profile_json = serde_json::to_string(&JsonValue::Object(draft_profile.clone()))
.map_err(|error| format!("draft_foundation 无法序列化 draft_profile_json: {error}"))?;
let gate = summarize_publish_gate_from_json(
&input.session_id,
RpgAgentStage::ObjectRefining,
@@ -1627,8 +1632,12 @@ fn execute_draft_foundation_action(
progress_percent: Some(100),
stage: Some(RpgAgentStage::ObjectRefining),
draft_profile_json: Some(Some(draft_profile_json.clone())),
last_assistant_reply: Some(Some("世界底稿已整理完成,接下来可以继续细化卡片和发布预览。".to_string())),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
last_assistant_reply: Some(Some(
"世界底稿已整理完成,接下来可以继续细化卡片和发布预览。".to_string(),
)),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(
&gate,
))?)),
result_preview_json: Some(build_result_preview_json(
Some(&draft_profile),
&gate,
@@ -1675,7 +1684,8 @@ fn execute_update_draft_card_action(
) -> Result<CustomWorldAgentOperationSnapshot, String> {
ensure_refining_stage(session.stage, "update_draft_card")?;
let card_id = read_required_payload_text(payload, "cardId", "update_draft_card requires cardId")?;
let card_id =
read_required_payload_text(payload, "cardId", "update_draft_card requires cardId")?;
let card = ctx
.db
.custom_world_draft_card()
@@ -1691,7 +1701,8 @@ fn execute_update_draft_card_action(
return Err("update_draft_card requires sections".to_string());
}
let mut detail_object = parse_optional_session_object(card.detail_payload_json.as_deref()).unwrap_or_default();
let mut detail_object =
parse_optional_session_object(card.detail_payload_json.as_deref()).unwrap_or_default();
let mut detail_sections = detail_object
.get("sections")
.and_then(JsonValue::as_array)
@@ -1735,27 +1746,36 @@ fn execute_update_draft_card_action(
}
detail_object.insert("id".to_string(), JsonValue::String(card.card_id.clone()));
detail_object.insert("kind".to_string(), JsonValue::String(card.kind.as_str().to_string()));
detail_object.insert(
"kind".to_string(),
JsonValue::String(card.kind.as_str().to_string()),
);
detail_object.insert("title".to_string(), JsonValue::String(card.title.clone()));
detail_object.insert("sections".to_string(), JsonValue::Array(detail_sections.clone()));
detail_object.insert(
"sections".to_string(),
JsonValue::Array(detail_sections.clone()),
);
detail_object.insert(
"linkedIds".to_string(),
serde_json::from_str::<JsonValue>(&card.linked_ids_json).unwrap_or_else(|_| JsonValue::Array(Vec::new())),
serde_json::from_str::<JsonValue>(&card.linked_ids_json)
.unwrap_or_else(|_| JsonValue::Array(Vec::new())),
);
detail_object.insert("locked".to_string(), JsonValue::Bool(false));
detail_object.insert("editable".to_string(), JsonValue::Bool(false));
detail_object.insert("editableSectionIds".to_string(), JsonValue::Array(Vec::new()));
detail_object.insert(
"editableSectionIds".to_string(),
JsonValue::Array(Vec::new()),
);
detail_object.insert("warningMessages".to_string(), JsonValue::Array(Vec::new()));
let updated_title = extract_detail_section_value(&detail_sections, "title").unwrap_or_else(|| card.title.clone());
let updated_subtitle =
extract_detail_section_value(&detail_sections, "subtitle").unwrap_or_else(|| card.subtitle.clone());
let updated_summary =
extract_detail_section_value(&detail_sections, "summary").unwrap_or_else(|| card.summary.clone());
let detail_payload_json =
serde_json::to_string(&JsonValue::Object(detail_object)).map_err(|error| {
format!("update_draft_card 无法序列化 detail_payload_json: {error}")
})?;
let updated_title = extract_detail_section_value(&detail_sections, "title")
.unwrap_or_else(|| card.title.clone());
let updated_subtitle = extract_detail_section_value(&detail_sections, "subtitle")
.unwrap_or_else(|| card.subtitle.clone());
let updated_summary = extract_detail_section_value(&detail_sections, "summary")
.unwrap_or_else(|| card.summary.clone());
let detail_payload_json = serde_json::to_string(&JsonValue::Object(detail_object))
.map_err(|error| format!("update_draft_card 无法序列化 detail_payload_json: {error}"))?;
replace_custom_world_draft_card(
ctx,
@@ -1778,7 +1798,14 @@ fn execute_update_draft_card_action(
},
);
let next_session = sync_session_draft_profile_from_card_update(session, &card, &updated_title, &updated_subtitle, &updated_summary, input.submitted_at_micros)?;
let next_session = sync_session_draft_profile_from_card_update(
session,
&card,
&updated_title,
&updated_subtitle,
&updated_summary,
input.submitted_at_micros,
)?;
replace_custom_world_agent_session(ctx, session, next_session);
append_custom_world_action_result_message(
@@ -1816,7 +1843,10 @@ fn execute_sync_result_profile_action(
.ok_or_else(|| "sync_result_profile requires profile".to_string())?;
if let Some(stable_profile_id) = resolve_stable_agent_draft_profile_id(session) {
// 结果页回写时必须沿用当前草稿的稳定身份,避免把同一草稿写成新条目。
profile.insert("id".to_string(), JsonValue::String(stable_profile_id.clone()));
profile.insert(
"id".to_string(),
JsonValue::String(stable_profile_id.clone()),
);
upsert_nested_result_profile_id(&mut profile, &stable_profile_id);
}
let draft_profile = ensure_minimal_draft_profile(profile, &session.seed_text);
@@ -1830,9 +1860,13 @@ fn execute_sync_result_profile_action(
let next_session = rebuild_custom_world_agent_session_row(
session,
CustomWorldAgentSessionPatch {
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(draft_profile.clone()))?)),
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(
draft_profile.clone(),
))?)),
last_assistant_reply: Some(Some("结果页草稿已同步回当前会话。".to_string())),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(
&gate,
))?)),
result_preview_json: Some(build_result_preview_json(
Some(&draft_profile),
&gate,
@@ -1871,12 +1905,14 @@ fn execute_sync_result_profile_action(
}
fn resolve_stable_agent_draft_profile_id(session: &CustomWorldAgentSession) -> Option<String> {
parse_optional_session_object(session.draft_profile_json.as_deref()).and_then(|profile| {
read_optional_text_field(&profile, &["legacyResultProfile.id", "id"])
})
parse_optional_session_object(session.draft_profile_json.as_deref())
.and_then(|profile| read_optional_text_field(&profile, &["legacyResultProfile.id", "id"]))
}
fn upsert_nested_result_profile_id(profile: &mut JsonMap<String, JsonValue>, stable_profile_id: &str) {
fn upsert_nested_result_profile_id(
profile: &mut JsonMap<String, JsonValue>,
stable_profile_id: &str,
) {
let legacy_result_profile = profile
.entry("legacyResultProfile".to_string())
.or_insert_with(|| JsonValue::Object(JsonMap::new()));
@@ -1907,12 +1943,13 @@ fn execute_publish_world_action(
) -> Result<CustomWorldAgentOperationSnapshot, String> {
ensure_publishable_stage(session.stage, "publish_world")?;
let draft_profile = if let Some(explicit) = payload.get("draftProfile").and_then(JsonValue::as_object) {
explicit.clone()
} else {
parse_optional_session_object(session.draft_profile_json.as_deref())
.ok_or_else(|| "publish_world requires draft_profile_json".to_string())?
};
let draft_profile =
if let Some(explicit) = payload.get("draftProfile").and_then(JsonValue::as_object) {
explicit.clone()
} else {
parse_optional_session_object(session.draft_profile_json.as_deref())
.ok_or_else(|| "publish_world requires draft_profile_json".to_string())?
};
let gate = summarize_publish_gate_from_json(
&session.session_id,
session.stage,
@@ -1972,7 +2009,10 @@ fn execute_publish_world_action(
&session.session_id,
RpgAgentOperationType::PublishWorld,
"世界已发布",
&format!("正式世界档案已写入作品库:{}", publish_result.1.profile_id),
&format!(
"正式世界档案已写入作品库:{}",
publish_result.1.profile_id
),
input.submitted_at_micros,
);
@@ -2046,9 +2086,15 @@ fn execute_revert_checkpoint_action(
.map(|value| serialize_json_value(&JsonValue::Object(value.clone())))
.transpose()?,
),
last_assistant_reply: Some(Some("已恢复到所选 checkpoint 的世界草稿状态。".to_string())),
quality_findings_json: Some(serialize_json_value(&JsonValue::Array(restored_quality_findings))?),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
last_assistant_reply: Some(Some(
"已恢复到所选 checkpoint 的世界草稿状态。".to_string(),
)),
quality_findings_json: Some(serialize_json_value(&JsonValue::Array(
restored_quality_findings,
))?),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(
&gate,
))?)),
result_preview_json: Some(build_result_preview_json(
restored_draft_profile.as_ref(),
&gate,
@@ -2099,7 +2145,10 @@ fn execute_placeholder_custom_world_action(
ctx,
&session.session_id,
&input.operation_id,
&format!("动作 {} 已接入最小兼容占位,后续会继续补真实编排。", input.action),
&format!(
"动作 {} 已接入最小兼容占位,后续会继续补真实编排。",
input.action
),
input.submitted_at_micros,
);
let operation = build_and_insert_custom_world_operation(
@@ -2201,7 +2250,8 @@ fn summarize_publish_gate_from_json(
blockers.push(CustomWorldPublishBlockerSnapshot {
blocker_id: "publish_missing_player_premise".to_string(),
code: "publish_missing_player_premise".to_string(),
message: "当前世界缺少玩家身份与切入前提,发布前需要先补齐玩家 premise。".to_string(),
message: "当前世界缺少玩家身份与切入前提,发布前需要先补齐玩家 premise。"
.to_string(),
});
}
if !json_array_has_non_empty_text(profile.get("coreConflicts")) {
@@ -2342,8 +2392,10 @@ fn build_supported_actions_json(
let has_checkpoint = checkpoints
.iter()
.any(|entry| entry.get("snapshot").is_some());
let draft_refining_enabled =
matches!(stage, RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining);
let draft_refining_enabled = matches!(
stage,
RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining
);
let long_tail_enabled = matches!(
stage,
RpgAgentStage::ObjectRefining
@@ -2462,8 +2514,10 @@ fn build_custom_world_draft_card_detail_snapshot(
card: &CustomWorldDraftCard,
) -> Result<CustomWorldDraftCardDetailSnapshot, String> {
if let Some(detail_payload_json) = card.detail_payload_json.as_deref() {
let detail_value = serde_json::from_str::<JsonValue>(detail_payload_json)
.map_err(|error| format!("custom_world_draft_card.detail_payload_json 非法: {error}"))?;
let detail_value =
serde_json::from_str::<JsonValue>(detail_payload_json).map_err(|error| {
format!("custom_world_draft_card.detail_payload_json 非法: {error}")
})?;
if let Some(object) = detail_value.as_object() {
let sections = object
.get("sections")
@@ -2501,8 +2555,14 @@ fn build_custom_world_draft_card_detail_snapshot(
.to_string(),
sections,
linked_ids_json: card.linked_ids_json.clone(),
locked: object.get("locked").and_then(JsonValue::as_bool).unwrap_or(false),
editable: object.get("editable").and_then(JsonValue::as_bool).unwrap_or(false),
locked: object
.get("locked")
.and_then(JsonValue::as_bool)
.unwrap_or(false),
editable: object
.get("editable")
.and_then(JsonValue::as_bool)
.unwrap_or(false),
editable_section_ids_json: serialize_json_value(
object
.get("editableSectionIds")
@@ -2534,7 +2594,9 @@ fn build_custom_world_draft_card_detail_snapshot(
})
}
fn build_fallback_card_sections(card: &CustomWorldDraftCard) -> Vec<CustomWorldDraftCardDetailSectionSnapshot> {
fn build_fallback_card_sections(
card: &CustomWorldDraftCard,
) -> Vec<CustomWorldDraftCardDetailSectionSnapshot> {
vec![
CustomWorldDraftCardDetailSectionSnapshot {
section_id: "title".to_string(),
@@ -2578,7 +2640,9 @@ fn rebuild_custom_world_agent_session_row(
current_turn: current.current_turn,
progress_percent: patch.progress_percent.unwrap_or(current.progress_percent),
stage: patch.stage.unwrap_or(current.stage),
focus_card_id: patch.focus_card_id.unwrap_or_else(|| current.focus_card_id.clone()),
focus_card_id: patch
.focus_card_id
.unwrap_or_else(|| current.focus_card_id.clone()),
anchor_content_json: patch
.anchor_content_json
.unwrap_or_else(|| current.anchor_content_json.clone()),
@@ -2588,8 +2652,12 @@ fn rebuild_custom_world_agent_session_row(
creator_intent_readiness_json: patch
.creator_intent_readiness_json
.unwrap_or_else(|| current.creator_intent_readiness_json.clone()),
anchor_pack_json: patch.anchor_pack_json.unwrap_or_else(|| current.anchor_pack_json.clone()),
lock_state_json: patch.lock_state_json.unwrap_or_else(|| current.lock_state_json.clone()),
anchor_pack_json: patch
.anchor_pack_json
.unwrap_or_else(|| current.anchor_pack_json.clone()),
lock_state_json: patch
.lock_state_json
.unwrap_or_else(|| current.lock_state_json.clone()),
draft_profile_json: patch
.draft_profile_json
.unwrap_or_else(|| current.draft_profile_json.clone()),
@@ -2741,7 +2809,8 @@ fn upsert_world_foundation_card(
status: RpgAgentDraftCardStatus::Confirmed,
title: read_optional_text_field(draft_profile, &["name", "title"])
.unwrap_or_else(|| "世界底稿".to_string()),
subtitle: read_optional_text_field(draft_profile, &["subtitle"]).unwrap_or_default(),
subtitle: read_optional_text_field(draft_profile, &["subtitle"])
.unwrap_or_default(),
summary: read_optional_text_field(draft_profile, &["summary"])
.unwrap_or_else(|| "第一版世界底稿已生成。".to_string()),
linked_ids_json: "[]".to_string(),
@@ -2754,24 +2823,27 @@ fn upsert_world_foundation_card(
},
);
} else {
ctx.db.custom_world_draft_card().insert(CustomWorldDraftCard {
card_id,
session_id: session_id.to_string(),
kind: RpgAgentDraftCardKind::World,
status: RpgAgentDraftCardStatus::Confirmed,
title: read_optional_text_field(draft_profile, &["name", "title"])
.unwrap_or_else(|| "世界底稿".to_string()),
subtitle: read_optional_text_field(draft_profile, &["subtitle"]).unwrap_or_default(),
summary: read_optional_text_field(draft_profile, &["summary"])
.unwrap_or_else(|| "第一版世界底稿已生成。".to_string()),
linked_ids_json: "[]".to_string(),
warning_count: 0,
asset_status: None,
asset_status_label: None,
detail_payload_json: Some(detail_payload_json),
created_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
});
ctx.db
.custom_world_draft_card()
.insert(CustomWorldDraftCard {
card_id,
session_id: session_id.to_string(),
kind: RpgAgentDraftCardKind::World,
status: RpgAgentDraftCardStatus::Confirmed,
title: read_optional_text_field(draft_profile, &["name", "title"])
.unwrap_or_else(|| "世界底稿".to_string()),
subtitle: read_optional_text_field(draft_profile, &["subtitle"])
.unwrap_or_default(),
summary: read_optional_text_field(draft_profile, &["summary"])
.unwrap_or_else(|| "第一版世界底稿已生成。".to_string()),
linked_ids_json: "[]".to_string(),
warning_count: 0,
asset_status: None,
asset_status_label: None,
detail_payload_json: Some(detail_payload_json),
created_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
});
}
Ok(())
@@ -2788,7 +2860,10 @@ fn sync_session_draft_profile_from_card_update(
let mut draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref())
.unwrap_or_else(|| build_minimal_draft_profile_from_seed(&session.seed_text));
if card.kind == RpgAgentDraftCardKind::World {
draft_profile.insert("name".to_string(), JsonValue::String(updated_title.to_string()));
draft_profile.insert(
"name".to_string(),
JsonValue::String(updated_title.to_string()),
);
draft_profile.insert(
"subtitle".to_string(),
JsonValue::String(updated_subtitle.to_string()),
@@ -2808,8 +2883,12 @@ fn sync_session_draft_profile_from_card_update(
rebuild_custom_world_agent_session_row(
session,
CustomWorldAgentSessionPatch {
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(draft_profile.clone()))?)),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(
draft_profile.clone(),
))?)),
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(
&gate,
))?)),
result_preview_json: Some(build_result_preview_json(
Some(&draft_profile),
&gate,
@@ -2824,7 +2903,10 @@ fn sync_session_draft_profile_from_card_update(
}
fn ensure_refining_stage(stage: RpgAgentStage, action: &str) -> Result<(), String> {
if matches!(stage, RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining) {
if matches!(
stage,
RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining
) {
Ok(())
} else {
Err(format!(
@@ -2933,10 +3015,7 @@ fn read_required_payload_text(
.ok_or_else(|| error_message.to_string())
}
fn read_optional_text_field(
object: &JsonMap<String, JsonValue>,
keys: &[&str],
) -> Option<String> {
fn read_optional_text_field(object: &JsonMap<String, JsonValue>, keys: &[&str]) -> Option<String> {
for key in keys {
let mut current = JsonValue::Object(object.clone());
let mut found = true;
@@ -2949,7 +3028,11 @@ fn read_optional_text_field(
}
}
if found {
if let Some(value) = current.as_str().map(str::trim).filter(|value| !value.is_empty()) {
if let Some(value) = current
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Some(value.to_string());
}
}
@@ -3144,21 +3227,28 @@ fn append_checkpoint_json(current: &str, checkpoint: &JsonValue) -> Result<Strin
fn extract_detail_section_value(sections: &[JsonValue], target_id: &str) -> Option<String> {
sections.iter().find_map(|entry| {
let object = entry.as_object()?;
(object.get("id").and_then(JsonValue::as_str) == Some(target_id))
.then(|| {
object
.get("value")
.and_then(JsonValue::as_str)
.unwrap_or_default()
.to_string()
})
(object.get("id").and_then(JsonValue::as_str) == Some(target_id)).then(|| {
object
.get("value")
.and_then(JsonValue::as_str)
.unwrap_or_default()
.to_string()
})
})
}
fn json_array_has_non_empty_text(value: Option<&JsonValue>) -> bool {
value
.and_then(JsonValue::as_array)
.map(|entries| entries.iter().any(|entry| entry.as_str().map(str::trim).filter(|text| !text.is_empty()).is_some()))
.map(|entries| {
entries.iter().any(|entry| {
entry
.as_str()
.map(str::trim)
.filter(|text| !text.is_empty())
.is_some()
})
})
.unwrap_or(false)
}
@@ -3341,12 +3431,14 @@ fn build_custom_world_agent_session_snapshot(
recommended_replies_json: row.recommended_replies_json.clone(),
asset_coverage_json: row.asset_coverage_json.clone(),
checkpoints_json: row.checkpoints_json.clone(),
supported_actions_json: serialize_json_value(&JsonValue::Array(build_supported_actions_json(
row.stage,
row.progress_percent,
&build_custom_world_publish_gate_from_session(row),
&parse_json_array_or_empty(&row.checkpoints_json),
)))
supported_actions_json: serialize_json_value(&JsonValue::Array(
build_supported_actions_json(
row.stage,
row.progress_percent,
&build_custom_world_publish_gate_from_session(row),
&parse_json_array_or_empty(&row.checkpoints_json),
),
))
.unwrap_or_else(|_| "[]".to_string()),
messages,
draft_cards,