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` 是这一次最多返回多少项。