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 254ebd93e..d1d39fac5 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 @@ -1,6 +1,8 @@ use super::*; use crate::agent::tool::apply_patch::error::ApplyPatchError; -use crate::agent::tool::arguments::{optional_text, page, required_text, ToolPage, ToolTextField}; +use crate::agent::tool::arguments::{ + not_text_repr, optional_text, page, required_text, ToolPage, ToolTextField, +}; use crate::agent::tool::browser_playtest::error::BrowserPlaytestError; #[cfg(all(windows, feature = "cocos-editor-execute"))] use crate::agent::tool::cocos_execute::error::CocosExecuteError; @@ -2398,7 +2400,7 @@ pub(in crate::agent) fn remove_background_input( value .as_str() .ok_or_else(|| RemoveBackgroundError::BackgroundModeNotText { - got: value.to_string(), + got: not_text_repr(value), })?; if !matches!(mode, "complex" | "flat") { return Err(RemoveBackgroundError::BackgroundModeUnsupported { @@ -2418,7 +2420,7 @@ pub(in crate::agent) fn remove_background_input( value .as_str() .ok_or_else(|| RemoveBackgroundError::ScreenColorNotText { - got: value.to_string(), + got: not_text_repr(value), })?; let valid_hex = color.len() == 7 && color.starts_with('#') @@ -2733,7 +2735,7 @@ fn art_preparation_mode( Some(Value::String(value)) => DirectTaonierArtPreparationMode::from_tool_value(Some(value)) .map_err(|_| PrepareGameArtError::ModeUnsupported { got: value.clone() }), Some(value) => Err(PrepareGameArtError::ModeNotText { - got: value.to_string(), + got: not_text_repr(value), }), } } @@ -2914,7 +2916,7 @@ fn bridge_image_generation_kind( Some(Value::String(value)) => value.trim(), Some(value) => { return Err(GenerateImageError::KindNotText { - got: value.to_string(), + got: not_text_repr(value), }) } }; @@ -4988,6 +4990,20 @@ mod tests { oversized_not_text.chars().count() ); + let inline_not_text = art_preparation_mode(&json!({ + "mode": vec!["需要回显但必须截断的模式值"; 40] + })) + .expect_err("mode 不是字符串时必须被拒绝") + .to_user_msg(); + assert!( + inline_not_text.contains("mode 必须是字符串"), + "{inline_not_text}" + ); + assert!( + inline_not_text.contains('…') && inline_not_text.chars().count() < 200, + "内联判类型的字段回显也要限长:{inline_not_text}" + ); + let brief_not_text = required_text(&json!({ "brief": 5 }), PrepareGameArtTextField::Brief) .expect_err("brief 不是字符串时必须被拒绝") .to_user_msg(); 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 8b49b1f30..fd55ead6d 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 @@ -14,6 +14,14 @@ use crate::agent::runtime_state::truncate_agent_runtime_text; /// 诊断 sidecar;`got` 会同时出现在 `to_user_msg` 和序列化后的 `error` 里。 const NOT_TEXT_GOT_MAX_CHARS: usize = 120; +/// 把「本该是字符串、实际给了别的类型」的值压成有界回显,供各工具的 `*NotText` 变体拼文案。 +/// +/// 走共享 [`ToolTextField::not_text`] 的字段自动走这里;像抠图 `backgroundMode`、美术准备 +/// `mode`、生成图片 `kind` 这类在解析里内联判类型的字段,也要用同一份口径。 +pub(crate) fn not_text_repr(value: &Value) -> String { + truncate_agent_runtime_text(&value.to_string(), NOT_TEXT_GOT_MAX_CHARS) +} + /// 一个会被边界校验的文本字段。 pub(crate) trait ToolTextField: Copy { fn key(self) -> &'static str; @@ -42,10 +50,7 @@ pub(crate) fn optional_text, E>( return Ok(None); }; let Some(value) = value.as_str() else { - return Err(field.not_text(truncate_agent_runtime_text( - &value.to_string(), - NOT_TEXT_GOT_MAX_CHARS, - ))); + return Err(field.not_text(not_text_repr(value))); }; let value = value.trim(); if value.is_empty() {