1
This commit is contained in:
@@ -1417,6 +1417,31 @@ pub fn build_custom_world_published_profile_compile_snapshot(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn canonicalize_custom_world_profile_before_save(profile: &mut Value) -> bool {
|
||||
let Some(object) = profile.as_object_mut() else {
|
||||
return false;
|
||||
};
|
||||
let foundation_text = build_creator_intent_foundation_text(object.get("creatorIntent"))
|
||||
.trim()
|
||||
.to_string();
|
||||
if foundation_text.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let current_setting_text = object
|
||||
.get("settingText")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.unwrap_or_default();
|
||||
if current_setting_text == foundation_text {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 中文注释:保存与 session 同步前统一以后端 creatorIntent 锚点重建 settingText,
|
||||
// 避免浏览器继续持有正式 profile canonicalize 规则。
|
||||
object.insert("settingText".to_string(), Value::String(foundation_text));
|
||||
true
|
||||
}
|
||||
|
||||
pub fn empty_agent_anchor_content_json() -> String {
|
||||
r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":null,"hiddenLines":null,"iconicElements":null}"#.to_string()
|
||||
}
|
||||
@@ -1514,6 +1539,154 @@ fn to_object(value: Option<&Value>) -> Option<Map<String, Value>> {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_creator_intent_foundation_text(value: Option<&Value>) -> String {
|
||||
let Some(intent) = value.and_then(Value::as_object) else {
|
||||
return String::new();
|
||||
};
|
||||
if !has_meaningful_creator_intent(intent) {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let relationship_text = intent
|
||||
.get("keyCharacters")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|items| items.first())
|
||||
.and_then(Value::as_object)
|
||||
.map(build_creator_intent_relationship_text)
|
||||
.unwrap_or_default();
|
||||
let player_opening_text = [
|
||||
read_text(intent, "playerPremise"),
|
||||
read_text(intent, "openingSituation"),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
let theme_tone_text = [
|
||||
read_string_list(intent, "themeKeywords").join("、"),
|
||||
read_string_list(intent, "toneDirectives").join("、"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" / ");
|
||||
|
||||
[
|
||||
build_anchor_line(
|
||||
"世界一句话",
|
||||
read_text(intent, "worldHook").unwrap_or_default(),
|
||||
),
|
||||
build_anchor_line("玩家开局", player_opening_text),
|
||||
build_anchor_line("主题气质", theme_tone_text),
|
||||
build_anchor_line(
|
||||
"核心冲突",
|
||||
read_string_list(intent, "coreConflicts").join(";"),
|
||||
),
|
||||
build_anchor_line("关键关系", relationship_text),
|
||||
build_anchor_line(
|
||||
"标志元素",
|
||||
read_string_list(intent, "iconicElements").join("、"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn has_meaningful_creator_intent(intent: &Map<String, Value>) -> bool {
|
||||
[
|
||||
"rawSettingText",
|
||||
"worldHook",
|
||||
"playerPremise",
|
||||
"openingSituation",
|
||||
]
|
||||
.iter()
|
||||
.any(|key| read_text(intent, key).is_some())
|
||||
|| [
|
||||
"themeKeywords",
|
||||
"toneDirectives",
|
||||
"coreConflicts",
|
||||
"iconicElements",
|
||||
"forbiddenDirectives",
|
||||
]
|
||||
.iter()
|
||||
.any(|key| !read_string_list(intent, key).is_empty())
|
||||
|| ["keyFactions", "keyCharacters", "keyLandmarks"]
|
||||
.iter()
|
||||
.any(|key| has_meaningful_creator_seed_array(intent.get(*key)))
|
||||
}
|
||||
|
||||
fn build_creator_intent_relationship_text(character: &Map<String, Value>) -> String {
|
||||
[
|
||||
read_text(character, "name"),
|
||||
read_text(character, "role"),
|
||||
read_text(character, "relationToPlayer").map(|value| format!("与玩家 {value}")),
|
||||
read_text(character, "hiddenHook").map(|value| format!("暗线 {value}")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · ")
|
||||
}
|
||||
|
||||
fn build_anchor_line(label: &str, content: String) -> String {
|
||||
if content.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{label}:{content}")
|
||||
}
|
||||
}
|
||||
|
||||
fn read_text(object: &Map<String, Value>, key: &str) -> Option<String> {
|
||||
object
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn read_string_list(object: &Map<String, Value>, key: &str) -> Vec<String> {
|
||||
object
|
||||
.get(key)
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn has_meaningful_creator_seed_array(value: Option<&Value>) -> bool {
|
||||
value.and_then(Value::as_array).is_some_and(|items| {
|
||||
items.iter().any(|item| {
|
||||
item.as_object().is_some_and(|object| {
|
||||
[
|
||||
"name",
|
||||
"publicGoal",
|
||||
"tension",
|
||||
"notes",
|
||||
"role",
|
||||
"publicMask",
|
||||
"hiddenHook",
|
||||
"relationToPlayer",
|
||||
"purpose",
|
||||
"mood",
|
||||
"secret",
|
||||
]
|
||||
.iter()
|
||||
.any(|key| read_text(object, key).is_some())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_text_field(
|
||||
draft: &Map<String, Value>,
|
||||
legacy: &Map<String, Value>,
|
||||
@@ -1794,6 +1967,52 @@ mod tests {
|
||||
assert_eq!(error, CustomWorldFieldError::MissingAuthorDisplayName);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_profile_before_save_rebuilds_setting_text_from_creator_intent() {
|
||||
let mut profile = serde_json::json!({
|
||||
"id": "cwprof_001",
|
||||
"settingText": "前端旧草稿文案",
|
||||
"creatorIntent": {
|
||||
"rawSettingText": "早期输入",
|
||||
"worldHook": "海图会在午夜改写群岛航路",
|
||||
"themeKeywords": ["海雾", "旧灯塔"],
|
||||
"toneDirectives": ["克制", "悬疑"],
|
||||
"playerPremise": "玩家是失忆领航员",
|
||||
"openingSituation": "正在禁航区醒来",
|
||||
"coreConflicts": ["议会隐瞒沉船真相"],
|
||||
"keyCharacters": [{
|
||||
"name": "顾潮音",
|
||||
"role": "守灯人",
|
||||
"relationToPlayer": "旧识",
|
||||
"hiddenHook": "掌握伪造海图"
|
||||
}],
|
||||
"iconicElements": ["会说谎的罗盘"]
|
||||
}
|
||||
});
|
||||
|
||||
assert!(canonicalize_custom_world_profile_before_save(&mut profile));
|
||||
assert_eq!(
|
||||
profile.get("settingText").and_then(Value::as_str),
|
||||
Some(
|
||||
"世界一句话:海图会在午夜改写群岛航路\n玩家开局:玩家是失忆领航员;正在禁航区醒来\n主题气质:海雾、旧灯塔 / 克制、悬疑\n核心冲突:议会隐瞒沉船真相\n关键关系:顾潮音 · 守灯人 · 与玩家 旧识 · 暗线 掌握伪造海图\n标志元素:会说谎的罗盘"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_profile_before_save_keeps_profile_without_creator_intent() {
|
||||
let mut profile = serde_json::json!({
|
||||
"id": "cwprof_001",
|
||||
"settingText": "用户手写设定"
|
||||
});
|
||||
|
||||
assert!(!canonicalize_custom_world_profile_before_save(&mut profile));
|
||||
assert_eq!(
|
||||
profile.get("settingText").and_then(Value::as_str),
|
||||
Some("用户手写设定")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_list_input_requires_owner_user_id() {
|
||||
let error = validate_custom_world_profile_list_input(&CustomWorldProfileListInput {
|
||||
|
||||
Reference in New Issue
Block a user