非字符串入参的回显截断到 120 字符,不再把整个对象原样带进诊断

- arguments::optional_text 里的 not_text 改为传截断后的序列化值:类型错只需要看出「传了什么」
- got 同时出现在 to_user_msg 与序列化 error 里,限长后工具结果与诊断 sidecar 都不会被超大数组/对象灌爆
- 复用 runtime_state::truncate_agent_runtime_text,不新增第二套截断实现
- 测试补一条:超大数组回显必须带截断标记且长度可控

验证:cargo test --bin genarrative-ai-game-creator-shell -- non_text_field_values 通过;cargo fmt --check 通过
This commit is contained in:
2026-10-01 16:51:50 +08:00
parent 8834f844e6
commit 97c3fc83c1
2 changed files with 34 additions and 1 deletions
@@ -4965,6 +4965,29 @@ mod tests {
);
assert!(!prompt_not_text.contains("不能为空"), "{prompt_not_text}");
let oversized_not_text = match resource_generation_input(&json!({
"kind": "video",
"mode": "create",
"prompt": vec!["需要回显但必须截断的字段内容"; 40],
"assetName": "开场"
})) {
Ok(_) => panic!("prompt 不是字符串时必须被拒绝"),
Err(error) => error.to_user_msg(),
};
assert!(
oversized_not_text.contains("prompt 必须是字符串"),
"{oversized_not_text}"
);
assert!(
oversized_not_text.contains('…'),
"超长回显必须带截断标记:{oversized_not_text}"
);
assert!(
oversized_not_text.chars().count() < 200,
"超长回显必须被截断:{}",
oversized_not_text.chars().count()
);
let brief_not_text = required_text(&json!({ "brief": 5 }), PrepareGameArtTextField::Brief)
.expect_err("brief 不是字符串时必须被拒绝")
.to_user_msg();
@@ -6,6 +6,13 @@
use serde_json::Value;
use super::error::ToolPageRejection;
use crate::agent::runtime_state::truncate_agent_runtime_text;
/// 字段给了非字符串值时,回显进诊断的那个值最多保留多少字符。
///
/// 类型错只需要让模型看出「传进来的是什么」,不必把整个对象或数组原样带进工具结果与
/// 诊断 sidecar;`got` 会同时出现在 `to_user_msg` 和序列化后的 `error` 里。
const NOT_TEXT_GOT_MAX_CHARS: usize = 120;
/// 一个会被边界校验的文本字段。
pub(crate) trait ToolTextField<E>: Copy {
@@ -35,7 +42,10 @@ pub(crate) fn optional_text<F: ToolTextField<E>, E>(
return Ok(None);
};
let Some(value) = value.as_str() else {
return Err(field.not_text(value.to_string()));
return Err(field.not_text(truncate_agent_runtime_text(
&value.to_string(),
NOT_TEXT_GOT_MAX_CHARS,
)));
};
let value = value.trim();
if value.is_empty() {