This commit is contained in:
2026-04-28 20:25:37 +08:00
parent f0471a4f8d
commit 0f013b6eee
45 changed files with 1117 additions and 1047 deletions

View File

@@ -870,24 +870,6 @@ fn normalize_world_attribute_schema(
normalized_slots.push(json!({
"slotId": slot_id,
"name": name,
"definition": json_map_text(raw_slot, "definition")
.or_else(|| json_map_text(&fallback_slot, "definition"))
.unwrap_or_else(|| "这个维度用于描述角色在当前世界中的关键表现。".to_string()),
"positiveSignals": json_map_string_array(raw_slot, "positiveSignals")
.or_else(|| json_map_string_array(&fallback_slot, "positiveSignals"))
.unwrap_or_else(|| vec!["稳定".to_string(), "主动".to_string()]),
"negativeSignals": json_map_string_array(raw_slot, "negativeSignals")
.or_else(|| json_map_string_array(&fallback_slot, "negativeSignals"))
.unwrap_or_else(|| vec!["失衡".to_string(), "被动".to_string()]),
"combatUseText": json_map_text(raw_slot, "combatUseText")
.or_else(|| json_map_text(&fallback_slot, "combatUseText"))
.unwrap_or_else(|| "影响战斗中的推进、承压与应对。".to_string()),
"socialUseText": json_map_text(raw_slot, "socialUseText")
.or_else(|| json_map_text(&fallback_slot, "socialUseText"))
.unwrap_or_else(|| "影响对话中的判断、牵引与立场。".to_string()),
"explorationUseText": json_map_text(raw_slot, "explorationUseText")
.or_else(|| json_map_text(&fallback_slot, "explorationUseText"))
.unwrap_or_else(|| "影响探索中的观察、穿行与续航。".to_string()),
}));
}
@@ -901,9 +883,6 @@ fn normalize_world_attribute_schema(
.and_then(JsonValue::as_i64)
.filter(|value| *value > 0)
.unwrap_or(1),
"schemaName": json_map_text(schema, "schemaName")
.filter(|value| !is_invalid_attribute_schema_name(value))
.unwrap_or_else(|| build_attribute_schema_name(framework, setting_text)),
"generatedFrom": {
"worldType": "CUSTOM",
"worldName": framework_world_name(framework, setting_text),
@@ -945,7 +924,6 @@ fn build_fallback_world_attribute_schema(framework: &JsonValue, setting_text: &s
"id": build_attribute_schema_id(framework, setting_text),
"worldId": format!("custom:{world_name}"),
"schemaVersion": 1,
"schemaName": build_attribute_schema_name(framework, setting_text),
"generatedFrom": {
"worldType": "CUSTOM",
"worldName": world_name,
@@ -954,35 +932,20 @@ fn build_fallback_world_attribute_schema(framework: &JsonValue, setting_text: &s
"conflictCore": conflict_core,
},
"slots": [
build_attribute_slot("axis_a", format!("{prefix}"), format!("承受{prefix}压、正面冲击与长期消耗的底子。"), ["承压", "稳阵"], ["虚浮", "易散"], "顶住正面压力并守住行动空间。", "在强压场面里保持可信和稳固。", "穿过危险环境时维持身体与装备状态。"),
build_attribute_slot("axis_b", format!("{prefix_alt}"), format!("顺应{prefix_alt}势、换位穿行与抢占时机的能力。"), ["借势", "轻快"], ["迟滞", "失位"], "切线换位、闪避、追击和抢先手。", "反应灵活,能顺势调整话术。", "穿越复杂地形、封锁线与危险通路。"),
build_attribute_slot("axis_c", format!("{prefix}"), "看清局势、线索、虚实与隐藏代价的能力。", ["洞察", "辨伪"], ["误读", "迟钝"], "识破破绽并判断战局变化。", "听出隐瞒、试探与交换空间。", "整理线索、辨认路径并推断风险。"),
build_attribute_slot("axis_d", format!("{prefix_alt}"), "在高压变化里仍能推进目标和拍板的胆气。", ["果断", "压前"], ["犹疑", "退缩"], "顶着高压窗口推进突破口。", "在谈判或对峙中定调。", "面对未知异象仍敢继续前探。"),
build_attribute_slot("axis_e", format!("{prefix}"), "与人、物、誓约、地方关系建立牵引的能力。", ["协同", "守诺"], ["疏离", "失信"], "借同伴协同与牵制形成连锁。", "安抚、结盟、交换与维系信任。", "从人情、传闻和旧物中打开线索。"),
build_attribute_slot("axis_f", format!("{prefix_alt}"), "在长线消耗和局势反复中回稳节奏的能力。", ["回稳", "续航"], ["紊乱", "断续"], "久战不乱,把节奏重新拉回手里。", "情绪稳定,不轻易被带偏。", "在漫长探索与恶劣环境里保有余力。"),
build_attribute_slot("axis_a", format!("{prefix}")),
build_attribute_slot("axis_b", format!("{prefix_alt}")),
build_attribute_slot("axis_c", format!("{prefix}")),
build_attribute_slot("axis_d", format!("{prefix_alt}")),
build_attribute_slot("axis_e", format!("{prefix}")),
build_attribute_slot("axis_f", format!("{prefix_alt}")),
],
})
}
fn build_attribute_slot(
slot_id: &str,
name: String,
definition: impl Into<String>,
positive_signals: [&str; 2],
negative_signals: [&str; 2],
combat_use_text: &str,
social_use_text: &str,
exploration_use_text: &str,
) -> JsonValue {
fn build_attribute_slot(slot_id: &str, name: String) -> JsonValue {
json!({
"slotId": slot_id,
"name": name,
"definition": definition.into(),
"positiveSignals": positive_signals,
"negativeSignals": negative_signals,
"combatUseText": combat_use_text,
"socialUseText": social_use_text,
"explorationUseText": exploration_use_text,
})
}
@@ -1008,20 +971,6 @@ fn build_attribute_schema_id(framework: &JsonValue, setting_text: &str) -> Strin
)
}
fn build_attribute_schema_name(framework: &JsonValue, setting_text: &str) -> String {
let source = [
framework_world_name(framework, setting_text),
json_text(framework, "summary").unwrap_or_default(),
json_text(framework, "tone").unwrap_or_default(),
]
.join("");
let terms = collect_attribute_theme_terms(source.as_str());
format!(
"{}六维",
terms.first().cloned().unwrap_or_else(|| "叙境".to_string())
)
}
fn collect_attribute_theme_terms(source: &str) -> Vec<String> {
let mut terms = Vec::new();
let chinese_chars = source
@@ -1062,12 +1011,6 @@ fn is_invalid_attribute_name(name: &str, seen_names: &[String]) -> bool {
.any(|banned| trimmed.contains(banned))
}
fn is_invalid_attribute_schema_name(name: &str) -> bool {
BANNED_ATTRIBUTE_NAMES
.iter()
.any(|banned| name.trim().contains(banned))
}
fn json_map_text(map: &JsonMap<String, JsonValue>, key: &str) -> Option<String> {
map.get(key)
.and_then(JsonValue::as_str)
@@ -1076,18 +1019,6 @@ fn json_map_text(map: &JsonMap<String, JsonValue>, key: &str) -> Option<String>
.map(ToOwned::to_owned)
}
fn json_map_string_array(map: &JsonMap<String, JsonValue>, key: &str) -> Option<Vec<String>> {
let items = map
.get(key)?
.as_array()?
.iter()
.filter_map(|entry| entry.as_str().map(str::trim))
.filter(|entry| !entry.is_empty())
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
if items.is_empty() { None } else { Some(items) }
}
fn first_json_string(value: &JsonValue, key: &str) -> Option<String> {
value
.get(key)
@@ -2492,7 +2423,7 @@ mod tests {
request_capture.clone(),
vec![
llm_response(
r#"{"name":"雾港归航","subtitle":"失灯旧案","summary":"守灯人与群岛议会围绕沉船旧案对峙。","tone":"海雾悬疑","playerGoal":"查清父亲沉船真相","templateWorldType":"WUXIA","majorFactions":["群岛议会","灯塔署"],"coreConflicts":["守灯塔的旧档案被人改写。"],"attributeSchema":{"schemaName":"雾港六维","slots":[{"slotId":"axis_a","name":"灯骨","definition":"承受封航压力与潮湿险境的底子。","positiveSignals":["承压"],"negativeSignals":["虚浮"],"combatUseText":"顶住正面压迫。","socialUseText":"在质问中稳住姿态。","explorationUseText":"穿过潮湿险境。"},{"slotId":"axis_b","name":"潮步","definition":"顺潮换位与穿行的能力。","positiveSignals":["轻快"],"negativeSignals":["迟滞"],"combatUseText":"切线换位。","socialUseText":"顺势调整说法。","explorationUseText":"穿越雾港通路。"},{"slotId":"axis_c","name":"灯识","definition":"辨认灯号和旧档错页的能力。","positiveSignals":["辨伪"],"negativeSignals":["误读"],"combatUseText":"看破破绽。","socialUseText":"听出遮掩。","explorationUseText":"辨认旧档线索。"},{"slotId":"axis_d","name":"雾魄","definition":"在海雾和旧案压力中推进的胆气。","positiveSignals":["果断"],"negativeSignals":["退缩"],"combatUseText":"压上突破口。","socialUseText":"在对峙中定调。","explorationUseText":"敢进陌生雾区。"},{"slotId":"axis_e","name":"旧约","definition":"维系旧友、信物与地方关系的能力。","positiveSignals":["守诺"],"negativeSignals":["疏离"],"combatUseText":"借同伴协同。","socialUseText":"建立信任交换。","explorationUseText":"从人情旧物找线索。"},{"slotId":"axis_f","name":"回澜","definition":"长线消耗中回稳节奏的能力。","positiveSignals":["回稳"],"negativeSignals":["紊乱"],"combatUseText":"久战不乱。","socialUseText":"不被情绪带偏。","explorationUseText":"远行中保有余力。"}]},"camp":{"name":"旧灯塔归舍","description":"海雾边缘的守灯人旧居。"}}"#,
r#"{"name":"雾港归航","subtitle":"失灯旧案","summary":"守灯人与群岛议会围绕沉船旧案对峙。","tone":"海雾悬疑","playerGoal":"查清父亲沉船真相","templateWorldType":"WUXIA","majorFactions":["群岛议会","灯塔署"],"coreConflicts":["守灯塔的旧档案被人改写。"],"attributeSchema":{"slots":[{"name":"灯骨"},{"name":"潮步"},{"name":"灯识"},{"name":"雾魄"},{"name":"旧约"},{"name":"回澜"}]},"camp":{"name":"旧灯塔归舍","description":"海雾边缘的守灯人旧居。"}}"#,
),
llm_response(
r#"{"playableNpcs":[{"name":"岑灯","title":"返乡守灯人","role":"主角代理","description":"追查旧案的人","visualDescription":"灰蓝旧灯披风压着海盐痕,腰侧挂旧海图筒和短灯杖。","actionDescription":"举灯照海图,短杖点地辨认潮声。","sceneVisualDescription":"旧灯塔回廊被海雾压低,墙上挂满潮湿航线图。","initialAffinity":24,"relationshipHooks":["旧案牵连"],"tags":["守灯人"]}]}"#,
@@ -2595,6 +2526,16 @@ mod tests {
.and_then(JsonValue::as_str),
Some("灯骨")
);
assert_eq!(
draft_profile
.get("attributeSchema")
.and_then(|schema| schema.get("slots"))
.and_then(JsonValue::as_array)
.and_then(|entries| entries.first())
.and_then(JsonValue::as_object)
.map(|entry| entry.contains_key("definition")),
Some(false)
);
assert!(
draft_profile
.get("worldHook")