diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs index 797f8c44d..ef68263f8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/binding.rs @@ -2,7 +2,7 @@ use crate::config::build_game_creator_llm_client_from_llm_config; use crate::config::load_game_creator_app_config; use crate::ui_editor::commands::utils::{ parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm, - strict_json_schema, + required_tool_call_arguments, strict_json_schema, }; use crate::ui_editor::component::text::FontSource; use crate::ui_editor::component::{Component, NodeComponent}; @@ -406,12 +406,8 @@ pub(crate) async fn bind_components_impl_with_provider( .await } .map_err(|error| format!("组件绑定失败:{error}"))?; - let call = response - .tool_calls - .iter() - .find(|call| call.name == "bind_ui_components") - .ok_or_else(|| "LLM 未返回 bind_ui_components 工具调用".to_string())?; - let parsed = parse_binding_response(&call.arguments, editable_nodes.len())?; + let arguments = required_tool_call_arguments(&response, "bind_ui_components")?; + let parsed = parse_binding_response(arguments, editable_nodes.len())?; let known_sprite_ids = state.sprite_assets.keys().cloned().collect::>(); let known_font_ids = state.font_assets.keys().cloned().collect::>(); let result = validate_and_materialize( diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs index 254d43774..1b1af1ea2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs @@ -1,7 +1,7 @@ use crate::config::build_game_creator_llm_client_from_llm_config; use crate::config::load_game_creator_app_config; use crate::ui_editor::commands::utils::{ - parse_limited_llm_tool_arguments, request_ui_editor_llm, strict_json_schema, + request_ui_editor_llm, required_tool_arguments, strict_json_schema, }; use crate::ui_editor::state::{State, UITree}; use platform_llm::{LlmFunctionTool, LlmMessage, LlmRunRequest, LlmToolChoice}; @@ -487,15 +487,7 @@ pub(crate) async fn merge_ui_impl_with_provider( app_log!("ui_merge.error stage=llm_request error={error}"); format!("UI 树合并失败:{error}") })?; - let call = response - .tool_calls - .iter() - .find(|call| call.name == MERGE_TOOL_NAME) - .ok_or_else(|| { - app_log!("ui_merge.error stage=parse_tool_call reason=missing_tool_call"); - format!("LLM 未返回 {MERGE_TOOL_NAME} 工具调用") - })?; - let arguments = parse_limited_llm_tool_arguments(&call.arguments).map_err(|error| { + let arguments = required_tool_arguments(&response, MERGE_TOOL_NAME).map_err(|error| { app_log!("ui_merge.error stage=parse_arguments error={error}"); format!("UI 合并工具参数无效:{error}") })?; diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs index f7acf69e9..039679e31 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs @@ -1,7 +1,7 @@ use crate::config::build_game_creator_llm_client_from_llm_config; use crate::config::load_game_creator_app_config; use crate::ui_editor::commands::utils::{ - parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm, + read_ui_reference_image_data_url, request_ui_editor_llm, required_tool_arguments, strict_json_schema, }; use crate::ui_editor::component::{Component, NodeComponent}; @@ -13,7 +13,7 @@ use crate::ui_editor::layout::offset::NodeOffset; use crate::ui_editor::layout::transform::Transform; use crate::ui_editor::resource::ui_design_image::UIDesignImage; use crate::ui_editor::state::{State, UITree}; -use crate::ui_editor::utils::{random_node_id, NodeId, UIDesignImageId}; +use crate::ui_editor::utils::{random_node_id, UIDesignImageId}; use nalgebra::{Point2, Vector2}; use platform_llm::{ LlmFunctionTool, LlmMessage, LlmMessageContentPart, LlmRunRequest, LlmToolChoice, @@ -770,27 +770,14 @@ pub(crate) async fn recognize_ui_impl_with_provider( !response.text.trim().is_empty(), response.tool_calls.len() ); - let call = response - .tool_calls - .iter() - .find(|call| call.name == "recognize_ui_structure") - .ok_or_else(|| { + let arguments = + required_tool_arguments(&response, "recognize_ui_structure").map_err(|error| { app_log!( - "ui_recognition.error stage=parse_tool_call root={} reason=missing_tool_call", + "ui_recognition.error stage=parse_arguments root={} error={error}", root_id.as_str() ); - format!( - "根界面图 {} 的 LLM 未返回 recognize_ui_structure 工具调用", - root_id.as_str() - ) + format!("根界面图 {} 的识别工具参数无效:{error}", root_id.as_str()) })?; - let arguments = parse_limited_llm_tool_arguments(&call.arguments).map_err(|error| { - app_log!( - "ui_recognition.error stage=parse_arguments root={} error={error}", - root_id.as_str() - ); - format!("根界面图 {} 的识别工具参数无效:{error}", root_id.as_str()) - })?; validate_recognition_response_shape(&arguments).map_err(|error| { app_log!( "ui_recognition.error stage=validate_arguments root={} error={error}", diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/ui_design_suggestion.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/ui_design_suggestion.rs index 613d44b7b..749c12f0f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/ui_design_suggestion.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/ui_design_suggestion.rs @@ -1,7 +1,7 @@ use crate::config::build_game_creator_llm_client_from_llm_config; use crate::config::load_game_creator_app_config; use crate::ui_editor::commands::utils::{ - parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm, + read_ui_reference_image_data_url, request_ui_editor_llm, required_tool_arguments, strict_json_schema, }; use crate::ui_editor::resource::ui_design_image::UIDesignImageRole; @@ -214,18 +214,11 @@ pub(crate) async fn suggest_ui_design_semantic_impl( !response.text.trim().is_empty(), response.tool_calls.len() ); - let call = response - .tool_calls - .iter() - .find(|call| call.name == "suggest_ui_design_semantics") - .ok_or_else(|| { - app_log!("ui_design_suggestion.error stage=parse_tool_call reason=missing_tool_call"); - "LLM 响应无效(详情:未返回 suggest_ui_design_semantics 工具调用)".to_string() + let arguments = + required_tool_arguments(&response, "suggest_ui_design_semantics").map_err(|error| { + app_log!("ui_design_suggestion.error stage=parse_arguments error={error}"); + format!("LLM 响应无效(详情:UI 语义建议工具参数无效:{error})") })?; - let arguments = parse_limited_llm_tool_arguments(&call.arguments).map_err(|error| { - app_log!("ui_design_suggestion.error stage=parse_arguments error={error}"); - format!("LLM 响应无效(详情:UI 语义建议工具参数无效:{error})") - })?; validate_suggestion_response_shape(&arguments).map_err(|error| { app_log!("ui_design_suggestion.error stage=validate_arguments error={error}"); format!("LLM 响应无效(详情:{error})") diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/utils.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/utils.rs index 852a23c9d..f5d876e2f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/utils.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/utils.rs @@ -95,6 +95,29 @@ pub(crate) fn parse_limited_llm_tool_arguments( serde_json::from_str(arguments).map_err(|error| format!("LLM 工具参数不是有效 JSON:{error}")) } +/// Stable structured-action adapter: locate the required tool call and parse its +/// bounded JSON arguments. Prompt, schema and materialization stay in each +/// operation-specific command. +pub(crate) fn required_tool_arguments( + response: &LlmRunResponse, + tool_name: &str, +) -> Result { + let arguments = required_tool_call_arguments(response, tool_name)?; + parse_limited_llm_tool_arguments(arguments) +} + +pub(crate) fn required_tool_call_arguments<'a>( + response: &'a LlmRunResponse, + tool_name: &str, +) -> Result<&'a str, String> { + response + .tool_calls + .iter() + .find(|call| call.name == tool_name) + .map(|call| call.arguments.as_str()) + .ok_or_else(|| format!("LLM 未返回 {tool_name} 工具调用")) +} + pub(crate) async fn read_ui_reference_image_data_url(path: PathBuf) -> Result { tokio::task::spawn_blocking(move || read_ui_reference_image_data_url_blocking(&path)) .await