Merge branch 'codex/backend-rewrite-spacetimedb' of http://82.157.175.59:3000/GenarrativeAI/Genarrative into codex/backend-rewrite-spacetimedb

This commit is contained in:
2026-04-24 17:59:55 +08:00
2 changed files with 141 additions and 7 deletions

View File

@@ -251,14 +251,11 @@ fn parse_model_output(parsed: &JsonValue) -> Result<PuzzleAgentModelOutput, Puzz
.and_then(JsonValue::as_u64)
.map(|value| value.min(100) as u32)
.unwrap_or(0);
let next_anchor_pack = parsed
let next_anchor_pack_value = parsed
.get("nextAnchorPack")
.cloned()
.ok_or_else(|| PuzzleAgentTurnError::new("拼图聊天结果缺少 nextAnchorPack。"))
.and_then(|value| {
serde_json::from_value::<PuzzleAnchorPack>(value)
.map_err(|_| PuzzleAgentTurnError::new("拼图 anchor pack 解析失败,请稍后重试。"))
})?;
.ok_or_else(|| PuzzleAgentTurnError::new("拼图聊天结果缺少 nextAnchorPack。"))?;
let next_anchor_pack = parse_model_anchor_pack(&next_anchor_pack_value)?;
Ok(PuzzleAgentModelOutput {
reply_text,
progress_percent,
@@ -266,6 +263,70 @@ fn parse_model_output(parsed: &JsonValue) -> Result<PuzzleAgentModelOutput, Puzz
})
}
fn parse_model_anchor_pack(value: &JsonValue) -> Result<PuzzleAnchorPack, PuzzleAgentTurnError> {
Ok(PuzzleAnchorPack {
// LLM 输出契约面向前端与 prompt使用 camelCaseRust 领域模型仍保持 snake_case
// 因此这里显式做边界翻译,避免把 JSON 命名差异扩散到领域 crate。
theme_promise: parse_model_anchor_item(value, "themePromise")?,
visual_subject: parse_model_anchor_item(value, "visualSubject")?,
visual_mood: parse_model_anchor_item(value, "visualMood")?,
composition_hooks: parse_model_anchor_item(value, "compositionHooks")?,
tags_and_forbidden: parse_model_anchor_item(value, "tagsAndForbidden")?,
})
}
fn parse_model_anchor_item(
pack: &JsonValue,
field_name: &str,
) -> Result<module_puzzle::PuzzleAnchorItem, PuzzleAgentTurnError> {
let value = pack.get(field_name).ok_or_else(|| {
PuzzleAgentTurnError::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_puzzle_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_anchor_status)
.unwrap_or(PuzzleAnchorStatus::Missing);
Ok(module_puzzle::PuzzleAnchorItem {
key,
label,
value: item_value,
status,
})
}
fn default_puzzle_anchor_label(field_name: &str) -> &'static str {
match field_name {
"themePromise" => "题材承诺",
"visualSubject" => "画面主体",
"visualMood" => "视觉气质",
"compositionHooks" => "拼图记忆点",
"tagsAndForbidden" => "标签与禁忌",
_ => "拼图锚点",
}
}
fn resolve_puzzle_agent_stage(progress_percent: u32) -> PuzzleAgentStage {
if progress_percent >= 85 {
PuzzleAgentStage::DraftReady
@@ -366,7 +427,10 @@ fn extract_reply_text_from_partial_json(text: &str) -> Option<String> {
#[cfg(test)]
mod tests {
use super::extract_reply_text_from_partial_json;
use module_puzzle::PuzzleAnchorStatus;
use serde_json::json;
use super::{extract_reply_text_from_partial_json, parse_model_output};
#[test]
fn extract_reply_text_from_partial_json_preserves_chinese_characters() {
@@ -376,4 +440,60 @@ mod tests {
assert_eq!(extracted.as_deref(), Some("夜雨猫咪遗迹"));
}
#[test]
fn parse_model_output_accepts_camel_case_anchor_pack_contract() {
let model_output = json!({
"replyText": "我先把雨夜猫咪的方向收住。",
"progressPercent": 46,
"nextAnchorPack": {
"themePromise": {
"key": "themePromise",
"label": "题材承诺",
"value": "雨夜中的奇幻探索",
"status": "confirmed"
},
"visualSubject": {
"key": "visualSubject",
"label": "画面主体",
"value": "发光猫咪站在遗迹台阶上",
"status": "confirmed"
},
"visualMood": {
"key": "visualMood",
"label": "视觉气质",
"value": "潮湿、梦幻、带轻微悬疑",
"status": "inferred"
},
"compositionHooks": {
"key": "compositionHooks",
"label": "拼图记忆点",
"value": "台阶透视、倒影、远处遗迹门洞",
"status": "inferred"
},
"tagsAndForbidden": {
"key": "tagsAndForbidden",
"label": "标签与禁忌",
"value": "雨夜、猫咪、神庙遗迹;禁止文字水印",
"status": "inferred"
}
}
});
let parsed = parse_model_output(&model_output).expect("camelCase 契约应能解析");
assert_eq!(parsed.progress_percent, 46);
assert_eq!(
parsed.next_anchor_pack.theme_promise.value,
"雨夜中的奇幻探索"
);
assert_eq!(
parsed.next_anchor_pack.theme_promise.status,
PuzzleAnchorStatus::Confirmed
);
assert_eq!(
parsed.next_anchor_pack.tags_and_forbidden.value,
"雨夜、猫咪、神庙遗迹;禁止文字水印"
);
}
}