收敛UI编辑器结构化LLM动作适配器

统一必需 tool-call 定位与有界 JSON 参数解析

让语义建议、结构识别、组件绑定和多树合并复用稳定机械

保留各操作的 prompt、schema、领域校验与 materializer
This commit is contained in:
2026-09-18 10:23:35 +08:00
parent 16c6b9198e
commit d986dbaeb0
5 changed files with 39 additions and 48 deletions
@@ -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::<HashSet<_>>();
let known_font_ids = state.font_assets.keys().cloned().collect::<HashSet<_>>();
let result = validate_and_materialize(
@@ -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}")
})?;
@@ -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}",
@@ -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}")
@@ -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<serde_json::Value, String> {
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<String, String> {
tokio::task::spawn_blocking(move || read_ui_reference_image_data_url_blocking(&path))
.await