首尾换行回到按空白裁剪,不再当控制字符报错

- 回退 3f70c6911 的收紧:optional_text 恢复成先 trim、再查 is_control
- prompt 末尾带 \n(\r\n 等)视为普通首尾空白裁掉,与首尾空格同口径;中间的控制字符仍然报 has_control_characters
- 测试改为断言行尾换行被接受且裁成 "生成过场",另加一条中间换行仍被拒的断言

验证:cargo test --bin genarrative-ai-game-creator-shell -- agent::direct_tool_bridge:: 41 passed / 0 failed
This commit is contained in:
2026-10-01 15:56:12 +08:00
parent 7c67a6fcc0
commit 75d9e25634
2 changed files with 23 additions and 13 deletions
@@ -4898,18 +4898,30 @@ mod tests {
GameCreationAppAssetKind::Image
);
let trailing_newline = match resource_generation_input(&json!({
assert_eq!(
resource_generation_input(&json!({
"kind": "video",
"mode": "create",
"prompt": "生成过场\n",
"assetName": "开场"
}))
.expect("首尾换行按普通空白裁掉,不能报控制字符")
.prompt,
"生成过场"
);
let interior_newline = match resource_generation_input(&json!({
"kind": "video",
"mode": "create",
"prompt": "生成过场\n",
"prompt": "生成\n过场",
"assetName": "开场"
})) {
Ok(_) => panic!("落在首尾的控制字符不能被 trim 静默吞掉"),
Ok(_) => panic!("中间的控制字符仍必须被拒绝"),
Err(error) => error.to_user_msg(),
};
assert!(
trailing_newline.contains("prompt 含有控制字符"),
"{trailing_newline}"
interior_newline.contains("prompt 含有控制字符"),
"{interior_newline}"
);
let whitespace_only =
@@ -37,19 +37,17 @@ pub(crate) fn optional_text<F: ToolTextField<E>, E>(
let Some(value) = value.as_str() else {
return Err(field.not_text(value.to_string()));
};
let trimmed = value.trim();
if trimmed.is_empty() {
let value = value.trim();
if value.is_empty() {
return Err(field.empty());
}
// 控制字符必须按原串判定:否则落在首尾的 `\n`/`\t` 会被 trim 静默吞掉,
// 同一个字符在中间报错、在边上通过。
if value.chars().count() > field.max_chars() {
return Err(field.too_long(value.chars().count(), field.max_chars()));
}
if value.chars().any(char::is_control) {
return Err(field.has_control_characters());
}
if trimmed.chars().count() > field.max_chars() {
return Err(field.too_long(trimmed.chars().count(), field.max_chars()));
}
Ok(Some(trimmed.to_string()))
Ok(Some(value.to_string()))
}
/// 一页列表结果:`offset` 是起始下标,`limit` 是这一次最多返回多少项。