This commit is contained in:
2026-04-26 20:50:58 +08:00
parent a3a9bfa194
commit 67161bd6d1
142 changed files with 3349 additions and 10674 deletions

View File

@@ -75,7 +75,7 @@ pub async fn generate_custom_world_foundation_draft(
.await?;
framework["storyNpcs"] = JsonValue::Array(story_outlines.clone());
let landmarks = generate_foundation_landmark_seed_entries(
let generated_scene_entries = generate_foundation_landmark_seed_entries(
llm_client,
&framework,
FOUNDATION_DRAFT_LANDMARK_COUNT,
@@ -83,7 +83,7 @@ pub async fn generate_custom_world_foundation_draft(
&mut on_progress,
)
.await?;
framework["landmarks"] = JsonValue::Array(landmarks.clone());
framework["landmarks"] = JsonValue::Array(generated_scene_entries.clone());
let playable_narrative = expand_foundation_role_entries(
llm_client,
@@ -137,7 +137,7 @@ pub async fn generate_custom_world_foundation_draft(
framework,
playable_detailed,
story_detailed,
landmarks,
generated_scene_entries,
session,
setting_text.as_str(),
);
@@ -304,6 +304,7 @@ async fn generate_foundation_landmark_seed_entries(
}
let batch_count = (total_count - merged_entries.len()).min(FOUNDATION_LANDMARK_BATCH_SIZE);
let forbidden_names = names_from_entries(&merged_entries);
let is_opening_batch = batch_index == 0 && merged_entries.is_empty();
emit_foundation_draft_progress(
on_progress,
"生成关键场景",
@@ -319,13 +320,19 @@ async fn generate_foundation_landmark_seed_entries(
);
let raw = request_foundation_json_stage(
llm_client,
build_custom_world_landmark_seed_batch_prompt(framework, batch_count, &forbidden_names),
build_custom_world_landmark_seed_batch_prompt(
framework,
batch_count,
&forbidden_names,
is_opening_batch,
),
format!("agent-foundation-landmark-seed-batch-{}", batch_index + 1).as_str(),
|response_text| {
build_custom_world_landmark_seed_batch_json_repair_prompt(
response_text,
batch_count,
&forbidden_names,
is_opening_batch,
)
},
format!(
@@ -714,7 +721,7 @@ fn build_foundation_draft_profile_from_framework(
framework: JsonValue,
playable_detailed: Vec<JsonValue>,
story_detailed: Vec<JsonValue>,
landmarks: Vec<JsonValue>,
generated_scene_entries: Vec<JsonValue>,
session: &CustomWorldAgentSessionRecord,
setting_text: &str,
) -> JsonMap<String, JsonValue> {
@@ -793,9 +800,14 @@ fn build_foundation_draft_profile_from_framework(
setting_text,
),
);
let camp = framework.get("camp").cloned().unwrap_or_else(
let fallback_camp = framework.get("camp").cloned().unwrap_or_else(
|| json!({ "name": "开局归处", "description": "玩家进入世界后的第一处落脚点。" }),
);
let playable_detailed = assign_role_ids(playable_detailed, "playable-npc");
let story_detailed = assign_role_ids(story_detailed, "story-npc");
let scene_role_refs = collect_scene_role_refs(&story_detailed);
let (camp, landmarks) =
split_generated_scenes_into_camp_and_landmarks(fallback_camp, generated_scene_entries);
object.insert("camp".to_string(), camp.clone());
object.insert(
"playableNpcs".to_string(),
@@ -803,7 +815,7 @@ fn build_foundation_draft_profile_from_framework(
);
object.insert("storyNpcs".to_string(), JsonValue::Array(story_detailed));
let scene_chapter_blueprints =
build_scene_chapter_blueprints_from_camp_and_landmarks(&camp, &landmarks);
build_scene_chapter_blueprints_from_camp_and_landmarks(&camp, &landmarks, &scene_role_refs);
object.insert("landmarks".to_string(), JsonValue::Array(landmarks));
object.insert("chapters".to_string(), JsonValue::Array(Vec::new()));
object.insert(
@@ -1095,9 +1107,50 @@ fn stable_ascii_slug(value: &str) -> String {
format!("{hash:08x}")
}
fn split_generated_scenes_into_camp_and_landmarks(
fallback_camp: JsonValue,
generated_scene_entries: Vec<JsonValue>,
) -> (JsonValue, Vec<JsonValue>) {
let mut entries = generated_scene_entries.into_iter();
let opening_scene = entries.next().unwrap_or(fallback_camp);
let camp = normalize_generated_opening_scene(opening_scene);
let landmarks = entries.collect::<Vec<_>>();
(camp, landmarks)
}
fn normalize_generated_opening_scene(scene: JsonValue) -> JsonValue {
let mut object = scene.as_object().cloned().unwrap_or_default();
let name = object
.get("name")
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("开局归处")
.to_string();
let description = object
.get("description")
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("玩家进入世界后的第一处落脚点。")
.to_string();
object.insert("id".to_string(), JsonValue::String("camp-1".to_string()));
object.insert("kind".to_string(), JsonValue::String("camp".to_string()));
object.insert("name".to_string(), JsonValue::String(name));
object.insert("description".to_string(), JsonValue::String(description));
JsonValue::Object(object)
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SceneRoleRef {
id: String,
name: String,
}
fn build_scene_chapter_blueprints_from_camp_and_landmarks(
camp: &JsonValue,
landmarks: &[JsonValue],
scene_role_refs: &[SceneRoleRef],
) -> Vec<JsonValue> {
let mut blueprints = Vec::with_capacity(landmarks.len() + 1);
blueprints.push(build_scene_chapter_blueprint_from_scene(
@@ -1105,12 +1158,19 @@ fn build_scene_chapter_blueprints_from_camp_and_landmarks(
0,
"camp",
"开局归处",
scene_role_refs,
));
blueprints.extend(build_scene_chapter_blueprints_from_landmarks(
landmarks,
scene_role_refs,
));
blueprints.extend(build_scene_chapter_blueprints_from_landmarks(landmarks));
blueprints
}
fn build_scene_chapter_blueprints_from_landmarks(landmarks: &[JsonValue]) -> Vec<JsonValue> {
fn build_scene_chapter_blueprints_from_landmarks(
landmarks: &[JsonValue],
scene_role_refs: &[SceneRoleRef],
) -> Vec<JsonValue> {
// 幕背景描述必须来自关键场景生成步骤,不能在草稿合成阶段再用规则句拼接。
landmarks
.iter()
@@ -1121,6 +1181,7 @@ fn build_scene_chapter_blueprints_from_landmarks(landmarks: &[JsonValue]) -> Vec
chapter_index,
"saved-landmark",
"关键场景",
scene_role_refs,
)
})
.collect()
@@ -1131,6 +1192,7 @@ fn build_scene_chapter_blueprint_from_scene(
chapter_index: usize,
id_prefix: &str,
fallback_name_prefix: &str,
scene_role_refs: &[SceneRoleRef],
) -> JsonValue {
let scene_name = json_text(scene, "name")
.unwrap_or_else(|| format!("{}{}", fallback_name_prefix, chapter_index + 1));
@@ -1144,6 +1206,13 @@ fn build_scene_chapter_blueprint_from_scene(
let act_npc_names = json_string_array(scene, "actNPCNames")
.or_else(|| json_string_array(scene, "sceneNpcNames"))
.unwrap_or_default();
let resolved_act_roles = resolve_scene_act_roles(&act_npc_names, scene_role_refs);
let scene_npc_ids = dedupe_text_values(
&resolved_act_roles
.iter()
.map(|role| role.id.clone())
.collect::<Vec<_>>(),
);
json!({
"id": scene_id.clone(),
@@ -1152,13 +1221,14 @@ fn build_scene_chapter_blueprint_from_scene(
"summary": summary,
"sceneTaskDescription": scene_task_description,
"linkedLandmarkIds": [scene_id.clone()],
"sceneNpcIds": scene_npc_ids,
"acts": (0..3)
.map(|act_index| build_scene_act_blueprint_from_landmark(
&scene_id,
&summary,
&act_prompts,
&act_events,
&act_npc_names,
&resolved_act_roles,
act_index,
))
.collect::<Vec<_>>(),
@@ -1170,7 +1240,7 @@ fn build_scene_act_blueprint_from_landmark(
scene_summary: &str,
act_prompts: &[String],
act_events: &[String],
act_npc_names: &[String],
act_roles: &[SceneRoleRef],
act_index: usize,
) -> JsonValue {
let act_title = if act_index == 0 {
@@ -1184,10 +1254,17 @@ fn build_scene_act_blueprint_from_landmark(
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let opposite_npc_id = act_npc_names
let opposite_role = act_roles
.get(act_index)
.or_else(|| act_npc_names.first())
.cloned()
.or_else(|| act_roles.first())
.cloned();
let opposite_npc_id = opposite_role
.as_ref()
.map(|role| role.id.clone())
.unwrap_or_default();
let opposite_role_name = opposite_role
.as_ref()
.map(|role| role.name.clone())
.unwrap_or_default();
let event_description = act_events
.get(act_index)
@@ -1196,12 +1273,16 @@ fn build_scene_act_blueprint_from_landmark(
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| {
build_default_act_event_description(scene_summary, opposite_npc_id.as_str(), act_index)
build_default_act_event_description(
scene_summary,
opposite_role_name.as_str(),
act_index,
)
});
let background_prompt = prompt.unwrap_or_else(|| {
build_default_act_background_prompt(
scene_summary,
opposite_npc_id.as_str(),
opposite_role_name.as_str(),
event_description.as_str(),
act_index,
)
@@ -1212,21 +1293,23 @@ fn build_scene_act_blueprint_from_landmark(
"title": act_title,
"summary": scene_summary,
"backgroundPromptText": background_prompt,
"encounterNpcIds": build_act_encounter_npc_ids(act_npc_names, opposite_npc_id.as_str()),
"encounterNpcIds": build_act_encounter_npc_ids(act_roles, opposite_npc_id.as_str()),
"primaryNpcId": opposite_npc_id,
"oppositeNpcId": opposite_npc_id,
"primaryRoleName": opposite_role_name,
"oppositeRoleName": opposite_role_name,
"eventDescription": event_description,
})
}
fn build_act_encounter_npc_ids(act_npc_names: &[String], primary_npc_id: &str) -> Vec<String> {
let mut names = Vec::with_capacity(act_npc_names.len().max(1));
fn build_act_encounter_npc_ids(act_roles: &[SceneRoleRef], primary_npc_id: &str) -> Vec<String> {
let mut names = Vec::with_capacity(act_roles.len().max(1));
let primary = primary_npc_id.trim();
if !primary.is_empty() {
names.push(primary.to_string());
}
for name in act_npc_names {
let normalized = name.trim();
for role in act_roles {
let normalized = role.id.trim();
if normalized.is_empty() || names.iter().any(|item| item == normalized) {
continue;
}
@@ -1235,6 +1318,98 @@ fn build_act_encounter_npc_ids(act_npc_names: &[String], primary_npc_id: &str) -
names
}
fn assign_role_ids(entries: Vec<JsonValue>, id_prefix: &str) -> Vec<JsonValue> {
entries
.into_iter()
.enumerate()
.map(|(index, entry)| assign_role_id(entry, id_prefix, index))
.collect()
}
fn assign_role_id(mut entry: JsonValue, id_prefix: &str, index: usize) -> JsonValue {
let name = json_text(&entry, "name").unwrap_or_else(|| format!("角色{}", index + 1));
let fallback_id = format!("{}-{}", id_prefix, stable_ascii_slug(name.as_str()));
let Some(object) = entry.as_object_mut() else {
return json!({
"id": fallback_id,
"name": name,
});
};
if object
.get("id")
.and_then(JsonValue::as_str)
.map(str::trim)
.is_none_or(str::is_empty)
{
object.insert("id".to_string(), JsonValue::String(fallback_id));
}
entry
}
fn collect_scene_role_refs(entries: &[JsonValue]) -> Vec<SceneRoleRef> {
entries
.iter()
.filter_map(|entry| {
let name = json_text(entry, "name")?;
let id = json_text(entry, "id")?;
Some(SceneRoleRef { id, name })
})
.collect()
}
fn resolve_scene_act_roles(
requested_role_names: &[String],
scene_role_refs: &[SceneRoleRef],
) -> Vec<SceneRoleRef> {
let mut resolved = requested_role_names
.iter()
.filter_map(|name| resolve_scene_role_ref(name, scene_role_refs))
.collect::<Vec<_>>();
if resolved.is_empty() {
resolved.extend(scene_role_refs.iter().take(3).cloned());
}
dedupe_scene_role_refs(resolved)
}
fn resolve_scene_role_ref(
name_or_id: &str,
scene_role_refs: &[SceneRoleRef],
) -> Option<SceneRoleRef> {
let normalized = name_or_id.trim();
if normalized.is_empty() {
return None;
}
scene_role_refs
.iter()
.find(|role| role.name == normalized || role.id == normalized)
.cloned()
}
fn dedupe_scene_role_refs(entries: Vec<SceneRoleRef>) -> Vec<SceneRoleRef> {
let mut seen = Vec::new();
let mut result = Vec::new();
for entry in entries {
if entry.id.trim().is_empty() || seen.iter().any(|id| id == &entry.id) {
continue;
}
seen.push(entry.id.clone());
result.push(entry);
}
result
}
fn dedupe_text_values(values: &[String]) -> Vec<String> {
let mut result = Vec::new();
for value in values {
let normalized = value.trim();
if normalized.is_empty() || result.iter().any(|item| item == normalized) {
continue;
}
result.push(normalized.to_string());
}
result
}
fn build_default_scene_task_description(scene_name: &str, scene_summary: &str) -> String {
if scene_summary.trim().is_empty() {
return format!(
@@ -1374,66 +1549,15 @@ fn normalize_framework_shape(framework: &mut JsonValue, setting_text: &str) {
"description".to_string(),
JsonValue::String(camp_description.clone()),
);
if !camp
.get("sceneTaskDescription")
.and_then(JsonValue::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty())
{
camp.insert(
"sceneTaskDescription".to_string(),
JsonValue::String(build_default_scene_task_description(
camp_name.as_str(),
camp_description.as_str(),
)),
);
}
if !camp
.get("actBackgroundPromptTexts")
.and_then(JsonValue::as_array)
.is_some_and(|items| items.len() == 3)
{
// 中文注释:开局场景也必须进入逐幕生图队列;模型漏字段时用 camp 信息生成可用的三幕画面描述。
camp.insert(
"actBackgroundPromptTexts".to_string(),
JsonValue::Array(
(0..3)
.map(|index| {
let event_description = build_default_act_event_description(
camp_description.as_str(),
"开局关键角色",
index,
);
JsonValue::String(build_default_act_background_prompt(
camp_description.as_str(),
"开局关键角色",
event_description.as_str(),
index,
))
})
.collect(),
),
);
}
if !camp
.get("actEventDescriptions")
.and_then(JsonValue::as_array)
.is_some_and(|items| items.len() == 3)
{
camp.insert(
"actEventDescriptions".to_string(),
JsonValue::Array(
(0..3)
.map(|index| {
JsonValue::String(build_default_act_event_description(
camp_description.as_str(),
"开局关键角色",
index,
))
})
.collect(),
),
);
// 中文注释framework 只保留开局归处占位;完整开局场景任务与三幕内容统一交给场景批生成阶段。
for generated_scene_key in [
"sceneTaskDescription",
"actBackgroundPromptTexts",
"actEventDescriptions",
"actNPCNames",
"sceneNpcNames",
] {
camp.remove(generated_scene_key);
}
}
}
@@ -2024,7 +2148,18 @@ mod tests {
"actNPCNames": ["灯童丁", "档吏庚", "灯童丁"]
})];
let blueprints = build_scene_chapter_blueprints_from_landmarks(&landmarks);
let scene_role_refs = vec![
SceneRoleRef {
id: "story-npc-lamp-child".to_string(),
name: "灯童丁".to_string(),
},
SceneRoleRef {
id: "story-npc-archive-clerk".to_string(),
name: "档吏庚".to_string(),
},
];
let blueprints =
build_scene_chapter_blueprints_from_landmarks(&landmarks, &scene_role_refs);
let acts = blueprints[0]
.get("acts")
.and_then(JsonValue::as_array)
@@ -2043,10 +2178,23 @@ mod tests {
"首次进入雾港码头时,查明黑潮船骨与灯童丁目击证词的关联。"
))
);
assert_eq!(acts[0].get("oppositeNpcId"), Some(&json!("灯童丁")));
assert_eq!(acts[0].get("primaryNpcId"), Some(&json!("灯童丁")));
assert_eq!(acts[1].get("oppositeNpcId"), Some(&json!("档吏庚")));
assert_eq!(acts[1].get("primaryNpcId"), Some(&json!("档吏庚")));
assert_eq!(
acts[0].get("oppositeNpcId"),
Some(&json!("story-npc-lamp-child"))
);
assert_eq!(
acts[0].get("primaryNpcId"),
Some(&json!("story-npc-lamp-child"))
);
assert_eq!(acts[0].get("primaryRoleName"), Some(&json!("灯童丁")));
assert_eq!(
acts[1].get("oppositeNpcId"),
Some(&json!("story-npc-archive-clerk"))
);
assert_eq!(
acts[1].get("primaryNpcId"),
Some(&json!("story-npc-archive-clerk"))
);
assert_eq!(
acts[0].get("eventDescription"),
Some(&json!(
@@ -2081,7 +2229,16 @@ mod tests {
"actBackgroundPromptTexts": ["斗技台晨雾未散,石阶旁少年们列队观望。", "木桩与兵器架围出训练区,族徽旗帜在风里猎猎。", "暮色压下斗技场,中央擂台留出一对一交锋空间。"]
})];
let blueprints = build_scene_chapter_blueprints_from_camp_and_landmarks(camp, &landmarks);
let scene_role_refs = vec![SceneRoleRef {
id: "story-npc-mentor".to_string(),
name: "药师长老".to_string(),
}];
let blueprints = build_scene_chapter_blueprints_from_camp_and_landmarks(
camp,
&landmarks,
&scene_role_refs,
);
let opening_chapter = &blueprints[0];
let opening_acts = opening_chapter
.get("acts")
@@ -2106,6 +2263,18 @@ mod tests {
.and_then(JsonValue::as_str)
.is_some_and(|value| !value.trim().is_empty())
}));
assert!(opening_acts.iter().all(|act| {
act.get("primaryNpcId")
.and_then(JsonValue::as_str)
.is_some_and(|value| value == "story-npc-mentor")
}));
assert!(opening_acts.iter().all(|act| {
act.get("encounterNpcIds")
.and_then(JsonValue::as_array)
.and_then(|items| items.first())
.and_then(JsonValue::as_str)
.is_some_and(|value| value == "story-npc-mentor")
}));
assert_eq!(blueprints.len(), 2);
}
@@ -2377,7 +2546,11 @@ mod tests {
assert!(request_text.contains("attributeSchema"));
assert!(request_text.contains("可扮演角色框架名单"));
assert!(request_text.contains("场景角色框架名单"));
assert!(request_text.contains("关键场景框架名单"));
assert!(request_text.contains("场景框架名单"));
assert!(request_text.contains("第一条场景必须是玩家进入世界时所在的开局场景"));
assert!(request_text.contains("camp 只表示玩家开局时的落脚处占位"));
assert!(!request_text.contains("camp.sceneTaskDescription"));
assert!(!request_text.contains("camp.actBackgroundPromptTexts"));
assert!(request_text.contains("actNPCNames"));
assert!(!request_text.contains("\"sceneNpcNames\""));
assert!(request_text.contains("connectedLandmarkNames"));
@@ -2434,6 +2607,43 @@ mod tests {
.and_then(JsonValue::as_str)
.is_some()
);
assert_eq!(
draft_profile
.get("camp")
.and_then(|entry| entry.get("name"))
.and_then(JsonValue::as_str),
Some("旧灯塔")
);
assert_eq!(
draft_profile
.get("camp")
.and_then(|entry| entry.get("id"))
.and_then(JsonValue::as_str),
Some("camp-1")
);
assert_eq!(
draft_profile
.get("camp")
.and_then(|entry| entry.get("sceneTaskDescription"))
.and_then(JsonValue::as_str),
Some("首次进入旧灯塔时,追查被篡改的灯火航线记录。")
);
assert_eq!(
draft_profile
.get("landmarks")
.and_then(JsonValue::as_array)
.map(Vec::len),
Some(1)
);
assert_eq!(
draft_profile
.get("landmarks")
.and_then(JsonValue::as_array)
.and_then(|entries| entries.first())
.and_then(|entry| entry.get("name"))
.and_then(JsonValue::as_str),
Some("沉船湾")
);
assert_eq!(
draft_profile
.get("sceneChapterBlueprints")
@@ -2462,19 +2672,57 @@ mod tests {
.and_then(JsonValue::as_array)
.and_then(|items| items.first())
.and_then(JsonValue::as_str),
Some("灯童丁")
Some("船魂戊")
);
assert_eq!(
draft_profile
.get("sceneChapterBlueprints")
.and_then(JsonValue::as_array)
.and_then(|entries| entries.get(1))
.and_then(|entries| entries.first())
.and_then(|entry| entry.get("acts"))
.and_then(JsonValue::as_array)
.and_then(|acts| acts.get(1))
.and_then(|act| act.get("primaryNpcId"))
.and_then(JsonValue::as_str),
Some("档吏庚")
Some("story-npc-0192680e")
);
assert_eq!(
draft_profile
.get("sceneChapterBlueprints")
.and_then(JsonValue::as_array)
.and_then(|entries| entries.first())
.and_then(|entry| entry.get("acts"))
.and_then(JsonValue::as_array)
.and_then(|acts| acts.first())
.and_then(|act| act.get("primaryNpcId"))
.and_then(JsonValue::as_str),
Some("story-npc-01b5406b")
);
assert_eq!(
draft_profile
.get("sceneChapterBlueprints")
.and_then(JsonValue::as_array)
.and_then(|entries| entries.first())
.and_then(|entry| entry.get("acts"))
.and_then(JsonValue::as_array)
.and_then(|acts| acts.first())
.and_then(|act| act.get("encounterNpcIds"))
.and_then(JsonValue::as_array)
.and_then(|items| items.first())
.and_then(JsonValue::as_str),
Some("story-npc-01b5406b")
);
assert_eq!(
draft_profile
.get("sceneChapterBlueprints")
.and_then(JsonValue::as_array)
.and_then(|entries| entries.first())
.and_then(|entry| entry.get("acts"))
.and_then(JsonValue::as_array)
.and_then(|acts| acts.first())
.and_then(|act| act.get("primaryRoleName"))
.and_then(JsonValue::as_str),
Some("灯童丁")
);
assert_eq!(
draft_profile
@@ -2486,8 +2734,54 @@ mod tests {
.and_then(|acts| acts.first())
.and_then(|act| act.get("primaryNpcId"))
.and_then(JsonValue::as_str),
Some("灯童丁")
Some("story-npc-01fc0701")
);
assert_eq!(
draft_profile
.get("sceneChapterBlueprints")
.and_then(JsonValue::as_array)
.and_then(|entries| entries.get(1))
.and_then(|entry| entry.get("acts"))
.and_then(JsonValue::as_array)
.and_then(|acts| acts.get(1))
.and_then(|act| act.get("primaryNpcId"))
.and_then(JsonValue::as_str),
Some("story-npc-01acae6c")
);
}
#[test]
fn generated_scene_batch_first_entry_becomes_opening_camp() {
let fallback_camp = json!({
"name": "世界骨架占位归处",
"description": "只来自 framework 的轻量占位。"
});
let generated_scenes = vec![
json!({
"name": "旧灯塔",
"description": "雾中仍亮着错位灯火",
"sceneTaskDescription": "首次进入旧灯塔时,追查被篡改的灯火航线记录。",
"actBackgroundPromptTexts": ["", "", ""],
"actEventDescriptions": ["", "", ""],
}),
json!({
"name": "沉船湾",
"description": "退潮后露出旧船骨"
}),
];
let (camp, landmarks) =
split_generated_scenes_into_camp_and_landmarks(fallback_camp, generated_scenes);
assert_eq!(camp.get("id"), Some(&json!("camp-1")));
assert_eq!(camp.get("kind"), Some(&json!("camp")));
assert_eq!(camp.get("name"), Some(&json!("旧灯塔")));
assert_eq!(
camp.get("sceneTaskDescription"),
Some(&json!("首次进入旧灯塔时,追查被篡改的灯火航线记录。"))
);
assert_eq!(landmarks.len(), 1);
assert_eq!(landmarks[0].get("name"), Some(&json!("沉船湾")));
}
fn llm_response(content: &str) -> String {