From 58e5bb24f1dc7a989779b9f7b0f9ef84d49da795 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 24 Apr 2026 20:51:46 +0800 Subject: [PATCH] fix: align puzzle and big fish persistence json fields --- .../api-server/src/big_fish_agent_turn.rs | 147 ++++++++++++++---- server-rs/crates/api-server/src/puzzle.rs | 27 ++-- 2 files changed, 134 insertions(+), 40 deletions(-) diff --git a/server-rs/crates/api-server/src/big_fish_agent_turn.rs b/server-rs/crates/api-server/src/big_fish_agent_turn.rs index ae8c2c4c..c38b0f6c 100644 --- a/server-rs/crates/api-server/src/big_fish_agent_turn.rs +++ b/server-rs/crates/api-server/src/big_fish_agent_turn.rs @@ -1,4 +1,4 @@ -use module_big_fish::{BigFishAnchorPack, BigFishCreationStage}; +use module_big_fish::{BigFishAnchorPack, BigFishAnchorStatus, BigFishCreationStage}; use platform_llm::{LlmClient, LlmMessage, LlmStreamDelta, LlmTextRequest}; use serde::{Deserialize, Serialize}; use serde_json::{Value as JsonValue, json}; @@ -226,40 +226,129 @@ fn build_chat_history(messages: &[BigFishAgentMessageRecord]) -> Vec fn parse_big_fish_model_output( parsed: &JsonValue, ) -> Result { - serde_json::from_value::(parsed.clone()) - .map_err(|_| BigFishAgentTurnError::new("大鱼吃小鱼模型结果缺少必要内容,请稍后重试。")) + let reply_text = parsed + .get("replyText") + .and_then(JsonValue::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| BigFishAgentTurnError::new("大鱼吃小鱼聊天结果缺少有效回复,请稍后重试。"))? + .to_string(); + let progress_percent = parsed + .get("progressPercent") + .and_then(JsonValue::as_u64) + .map(|value| value.min(100) as u32) + .unwrap_or(0); + let next_anchor_pack_value = parsed + .get("nextAnchorPack") + .cloned() + .ok_or_else(|| BigFishAgentTurnError::new("大鱼吃小鱼聊天结果缺少 nextAnchorPack。"))?; + let next_anchor_pack = parse_big_fish_model_anchor_pack(&next_anchor_pack_value)?; + Ok(BigFishAgentModelOutput { + reply_text, + progress_percent, + next_anchor_pack, + }) +} + +fn parse_big_fish_model_anchor_pack( + value: &JsonValue, +) -> Result { + Ok(BigFishAnchorPack { + // LLM 与 HTTP 契约使用 camelCase;SpacetimeDB 持久化结构保持 Rust snake_case,边界处必须显式翻译。 + gameplay_promise: parse_big_fish_model_anchor_item(value, "gameplayPromise")?, + ecology_visual_theme: parse_big_fish_model_anchor_item(value, "ecologyVisualTheme")?, + growth_ladder: parse_big_fish_model_anchor_item(value, "growthLadder")?, + risk_tempo: parse_big_fish_model_anchor_item(value, "riskTempo")?, + }) +} + +fn parse_big_fish_model_anchor_item( + pack: &JsonValue, + field_name: &str, +) -> Result { + let value = pack.get(field_name).ok_or_else(|| { + BigFishAgentTurnError::new(format!("大鱼吃小鱼 anchor pack 缺少 {field_name}。")) + })?; + let key = value + .get("key") + .and_then(JsonValue::as_str) + .map(str::trim) + .filter(|text| !text.is_empty()) + .unwrap_or(field_name) + .to_string(); + let label = value + .get("label") + .and_then(JsonValue::as_str) + .map(str::trim) + .filter(|text| !text.is_empty()) + .unwrap_or_else(|| default_big_fish_anchor_label(field_name)) + .to_string(); + let item_value = value + .get("value") + .and_then(JsonValue::as_str) + .map(str::trim) + .unwrap_or_default() + .to_string(); + let status = value + .get("status") + .and_then(JsonValue::as_str) + .map(parse_big_fish_anchor_status) + .unwrap_or(BigFishAnchorStatus::Missing); + + Ok(module_big_fish::BigFishAnchorItem { + key, + label, + value: item_value, + status, + }) +} + +fn default_big_fish_anchor_label(field_name: &str) -> &'static str { + match field_name { + "gameplayPromise" => "玩法承诺", + "ecologyVisualTheme" => "生态视觉主题", + "growthLadder" => "成长阶梯", + "riskTempo" => "风险节奏", + _ => "大鱼锚点", + } } fn serialize_record_anchor_pack(anchor_pack: &spacetime_client::BigFishAnchorPackRecord) -> String { - serde_json::to_string_pretty(&json!({ - "gameplayPromise": { - "key": anchor_pack.gameplay_promise.key, - "label": anchor_pack.gameplay_promise.label, - "value": anchor_pack.gameplay_promise.value, - "status": anchor_pack.gameplay_promise.status, - }, - "ecologyVisualTheme": { - "key": anchor_pack.ecology_visual_theme.key, - "label": anchor_pack.ecology_visual_theme.label, - "value": anchor_pack.ecology_visual_theme.value, - "status": anchor_pack.ecology_visual_theme.status, - }, - "growthLadder": { - "key": anchor_pack.growth_ladder.key, - "label": anchor_pack.growth_ladder.label, - "value": anchor_pack.growth_ladder.value, - "status": anchor_pack.growth_ladder.status, - }, - "riskTempo": { - "key": anchor_pack.risk_tempo.key, - "label": anchor_pack.risk_tempo.label, - "value": anchor_pack.risk_tempo.value, - "status": anchor_pack.risk_tempo.status, - }, - })) + serde_json::to_string_pretty(&map_big_fish_record_anchor_pack(anchor_pack)) .unwrap_or_else(|_| "{}".to_string()) } +fn map_big_fish_record_anchor_pack( + record: &spacetime_client::BigFishAnchorPackRecord, +) -> BigFishAnchorPack { + BigFishAnchorPack { + gameplay_promise: map_big_fish_record_anchor_item(&record.gameplay_promise), + ecology_visual_theme: map_big_fish_record_anchor_item(&record.ecology_visual_theme), + growth_ladder: map_big_fish_record_anchor_item(&record.growth_ladder), + risk_tempo: map_big_fish_record_anchor_item(&record.risk_tempo), + } +} + +fn map_big_fish_record_anchor_item( + record: &spacetime_client::BigFishAnchorItemRecord, +) -> module_big_fish::BigFishAnchorItem { + module_big_fish::BigFishAnchorItem { + key: record.key.clone(), + label: record.label.clone(), + value: record.value.clone(), + status: parse_big_fish_anchor_status(record.status.as_str()), + } +} + +fn parse_big_fish_anchor_status(value: &str) -> BigFishAnchorStatus { + match value { + "confirmed" => BigFishAnchorStatus::Confirmed, + "locked" => BigFishAnchorStatus::Locked, + "inferred" => BigFishAnchorStatus::Inferred, + _ => BigFishAnchorStatus::Missing, + } +} + fn parse_json_response_text(text: &str) -> Result { if let Ok(value) = serde_json::from_str::(text) { return Ok(value); diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index a2f9420e..9d356d17 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -462,17 +462,7 @@ pub async fn execute_puzzle_agent_action( let candidates_json = serde_json::to_string( &candidates .iter() - .map(|candidate| { - json!({ - "candidateId": candidate.candidate_id, - "imageSrc": candidate.image_src, - "assetId": candidate.asset_id, - "prompt": candidate.prompt, - "actualPrompt": candidate.actual_prompt, - "sourceType": candidate.source_type, - "selected": candidate.selected, - }) - }) + .map(to_puzzle_generated_image_candidate) .collect::>(), ) .map_err(|error| { @@ -1473,6 +1463,21 @@ struct PuzzleDownloadedImage { bytes: Vec, } +fn to_puzzle_generated_image_candidate( + candidate: &PuzzleGeneratedImageCandidateRecord, +) -> PuzzleGeneratedImageCandidate { + // SpacetimeDB ???????? module-puzzle ??????????? snake_case ????HTTP ????????? camelCase? + PuzzleGeneratedImageCandidate { + candidate_id: candidate.candidate_id.clone(), + image_src: candidate.image_src.clone(), + asset_id: candidate.asset_id.clone(), + prompt: candidate.prompt.clone(), + actual_prompt: candidate.actual_prompt.clone(), + source_type: candidate.source_type.clone(), + selected: candidate.selected, + } +} + struct GeneratedPuzzleAssetResponse { image_src: String, asset_id: String,