From 3f70c691190e0905abbec4fbda6eda500fce2f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 1 Oct 2026 15:18:02 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E5=B0=BE=E7=9A=84=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=8D=E5=86=8D=E8=A2=AB=20trim=20?= =?UTF-8?q?=E9=9D=99=E9=BB=98=E5=90=9E=E6=8E=89=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=8C=89=E6=8E=A7=E5=88=B6=E5=AD=97=E7=AC=A6=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - optional_text 原来先 trim 再查 is_control:`"prompt\n"`、`"\tprompt"` 这类落在首尾的控制字符被静默裁掉、当成合法值,而同一个字符出现在中间却会报错 - 改成先按原串查控制字符,再用 trim 做空值与长度判定:控制字符在哪个位置都报 has_control_characters - 纯空白值("\n\t"、" ")仍然算空值、报原来的 empty 错误;首尾的普通空格仍按原样裁掉 - 测试补三条断言:prompt 末尾带 \n 必须被拒、纯空白 brief 仍报空、brief 首尾普通空格仍会被裁剪 验证:cargo test --bin genarrative-ai-game-creator-shell -- agent:: 953 passed / 0 failed --- .../src-tauri/src/agent/direct_tool_bridge.rs | 32 +++++++++++++++++++ .../src-tauri/src/agent/tool/arguments.rs | 14 ++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 17883e93d..4c791e640 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -4860,6 +4860,38 @@ mod tests { #[test] fn non_text_field_values_report_a_type_error_instead_of_an_empty_value() { + let trailing_newline = match resource_generation_input(&json!({ + "kind": "video", + "mode": "create", + "prompt": "生成过场\n", + "assetName": "开场" + })) { + Ok(_) => panic!("落在首尾的控制字符不能被 trim 静默吞掉"), + Err(error) => error.to_user_msg(), + }; + assert!( + trailing_newline.contains("prompt 含有控制字符"), + "{trailing_newline}" + ); + + let whitespace_only = + required_text(&json!({ "brief": "\n\t" }), PrepareGameArtTextField::Brief) + .expect_err("纯空白仍然按空值处理") + .to_user_msg(); + assert!( + whitespace_only.contains("brief 不能为空"), + "{whitespace_only}" + ); + + assert_eq!( + required_text( + &json!({ "brief": " 美术包 " }), + PrepareGameArtTextField::Brief + ) + .expect("首尾普通空格仍正常裁剪"), + "美术包" + ); + let prompt_not_text = match resource_generation_input(&json!({ "kind": "video", "mode": "create", diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/tool/arguments.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/tool/arguments.rs index 1460a49ea..67c29cbcb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/tool/arguments.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/tool/arguments.rs @@ -37,17 +37,19 @@ pub(crate) fn optional_text, E>( let Some(value) = value.as_str() else { return Err(field.not_text(value.to_string())); }; - let value = value.trim(); - if value.is_empty() { + let trimmed = value.trim(); + if trimmed.is_empty() { return Err(field.empty()); } - if value.chars().count() > field.max_chars() { - return Err(field.too_long(value.chars().count(), field.max_chars())); - } + // 控制字符必须按原串判定:否则落在首尾的 `\n`/`\t` 会被 trim 静默吞掉, + // 同一个字符在中间报错、在边上通过。 if value.chars().any(char::is_control) { return Err(field.has_control_characters()); } - Ok(Some(value.to_string())) + if trimmed.chars().count() > field.max_chars() { + return Err(field.too_long(trimmed.chars().count(), field.max_chars())); + } + Ok(Some(trimmed.to_string())) } /// 一页列表结果:`offset` 是起始下标,`limit` 是这一次最多返回多少项。