diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs index fe56125ba..ad7117320 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs @@ -153,6 +153,14 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_ observe_agent_runtime_account_asset_library(root, &action.input).await } "canvas.asset_import" => observe_agent_runtime_asset_import(root, &action.input).await, + "ui-design-doc.from-images" => { + observe_agent_runtime_ui_design_doc_from_images(root, &action.input) + } + "ui-design-doc.run-workflow" => { + observe_agent_runtime_ui_design_doc_run_workflow(root, agent_id, run_id, &action.input) + .await + } + "ui-design-doc.into-js" => observe_agent_runtime_ui_design_doc_into_js(root, &action.input), "project.index" => observe_agent_runtime_project_snapshot_with_lock( root, agent_id, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs index 1bf44ebf5..b4f364493 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs @@ -103,6 +103,9 @@ pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id( "preview.validate" => Some("preview.validate"), "image.inspect" => Some("image.inspect"), "canvas.asset_generate" => Some("canvas.asset_generate"), + "ui-design-doc.from-images" => Some("asset.register"), + "ui-design-doc.run-workflow" => Some("asset.register"), + "ui-design-doc.into-js" => Some("file.write"), "blackboard.write" => Some("memory.write"), "agent.message" => Some("conversation.write"), "agent.delegate" => Some("agent.delegate"), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs index 19ef93917..8028b7901 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs @@ -59,6 +59,9 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> { "preview.validate", "image.inspect", "canvas.asset_generate", + "ui-design-doc.from-images", + "ui-design-doc.run-workflow", + "ui-design-doc.into-js", "blackboard.write", "agent.message", "agent.delegate", @@ -109,6 +112,7 @@ pub(crate) fn agent_runtime_acceptance_evidence_tools() -> BTreeSet<&'static str "image.inspect", "canvas.asset_generate", "canvas.asset_import", + "ui-design-doc.run-workflow", ] .into_iter() .collect() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs index 42ba83c59..9d2fa1efb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs @@ -19,6 +19,7 @@ mod process_ops; mod project_ops; mod run_status; mod task_ops; +mod ui_design_doc; pub(in crate::agent) use action_history::*; pub(in crate::agent) use cocos_editor::*; @@ -38,6 +39,7 @@ pub(in crate::agent) use process_ops::*; pub(in crate::agent) use project_ops::*; pub(in crate::agent) use run_status::*; pub(in crate::agent) use task_ops::*; +pub(in crate::agent) use ui_design_doc::*; #[cfg(test)] pub(crate) use delegation::observe_agent_runtime_agent_delegate_at_locked; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs index c4d33154b..7def2949a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs @@ -48,6 +48,9 @@ fn autonomous_design_foundation_command_is_allowed(command_id: &str) -> bool { | "canvas.asset_generate" | "canvas.asset_import" | "asset.register" + | "ui-design-doc.from-images" + | "ui-design-doc.run-workflow" + | "ui-design-doc.into-js" | "agent.audit" | "agent.action_history" | "agent.run_status" diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/ui_design_doc.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/ui_design_doc.rs new file mode 100644 index 000000000..2e7caf72f --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/ui_design_doc.rs @@ -0,0 +1,222 @@ +//! UI 设计文档三个工具在 Runtime 侧的参数解析与调用。 +//! +//! 工具名形如 `ui-design-doc.<动作>`;项目根目录、项目 ID 与 provider 身份一律由 +//! Runtime 注入,模型只给设计图引用与目标文档 assetId。 + +use super::*; +use crate::ui_editor::design_doc::{ + create_ui_design_doc_from_images, run_ui_design_doc_workflow, CreateUiDesignDocFromImagesInput, + RunUiDesignDocWorkflowInput, UiDesignImageReference, +}; +use crate::ui_editor::persistence::{generate_ui_design_code_at, GenerateUiDesignCodeInput}; +use serde::Deserialize; +use serde_json::Value; + +pub(in crate::agent) const UI_DESIGN_DOC_FROM_IMAGES_TOOL: &str = "ui-design-doc.from-images"; +pub(in crate::agent) const UI_DESIGN_DOC_RUN_WORKFLOW_TOOL: &str = "ui-design-doc.run-workflow"; +pub(in crate::agent) const UI_DESIGN_DOC_INTO_JS_TOOL: &str = "ui-design-doc.into-js"; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct FromImagesArguments { + images: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct DesignDocArguments { + design_doc_asset_id: String, +} + +pub(in crate::agent) fn observe_agent_runtime_ui_design_doc_from_images( + root: &Path, + input: &Value, +) -> AgentRuntimeToolObservation { + let tool = UI_DESIGN_DOC_FROM_IMAGES_TOOL; + let arguments = match serde_json::from_value::(input.clone()) { + Ok(arguments) => arguments, + Err(error) => return rejected(tool, format!("{tool} 输入无效:{error}")), + }; + let expected_project_id = match game_creator_agent_runtime_context_project_id(root) { + Ok(project_id) => project_id, + Err(error) => return rejected(tool, error), + }; + let revision_before = read_game_creator_agent_runtime_project_revision(root) + .ok() + .map(|snapshot| snapshot.revision); + match create_ui_design_doc_from_images(CreateUiDesignDocFromImagesInput { + project_path: root.to_string_lossy().into_owned(), + expected_project_id, + images: arguments.images, + }) { + Ok(created) => { + if revision_before.is_none_or(|before| created.committed_project_revision > before) { + emit_game_creator_manifest_invalidated(root, tool); + } + AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "ok".to_string(), + summary: format!( + "已新建 UI 设计文档 {}:{} 张设计图,路径 {}", + created.asset.id, + created.image_ids.len(), + created.relative_path + ), + detail: serde_json::to_string(&serde_json::json!({ + "assetId": created.asset.id, + "relativePath": created.relative_path, + "imageIds": created.image_ids, + })) + .ok(), + } + } + Err(error) => error_observation(root, tool, error), + } +} + +pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_run_workflow( + root: &Path, + agent_id: &str, + run_id: &str, + input: &Value, +) -> AgentRuntimeToolObservation { + let tool = UI_DESIGN_DOC_RUN_WORKFLOW_TOOL; + let arguments = match serde_json::from_value::(input.clone()) { + Ok(arguments) => arguments, + Err(error) => return rejected(tool, format!("{tool} 输入无效:{error}")), + }; + let expected_project_id = match game_creator_agent_runtime_context_project_id(root) { + Ok(project_id) => project_id, + Err(error) => return rejected(tool, error), + }; + let revision_before = read_game_creator_agent_runtime_project_revision(root) + .ok() + .map(|snapshot| snapshot.revision); + match run_ui_design_doc_workflow( + RunUiDesignDocWorkflowInput { + project_path: root.to_string_lossy().into_owned(), + expected_project_id, + asset_id: arguments.design_doc_asset_id, + }, + Some((agent_id, run_id)), + ) + .await + { + Ok(result) => { + if revision_before.is_none_or(|before| result.revision > before) { + emit_game_creator_manifest_invalidated(root, tool); + } + let pending = result.backfill_errors.len(); + AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "ok".to_string(), + summary: format!( + "UI 设计文档工作流完成:识别 {} 棵树,回填 {} 个节点,{} 个待人工处理{}", + result.recognized_tree_count, + result.bound_node_count, + result.problematic_node_count, + if pending > 0 { + format!(",{pending} 项未回填") + } else { + String::new() + } + ), + detail: serde_json::to_string(&result).ok(), + } + } + Err(error) => error_observation(root, tool, error), + } +} + +pub(in crate::agent) fn observe_agent_runtime_ui_design_doc_into_js( + root: &Path, + input: &Value, +) -> AgentRuntimeToolObservation { + let tool = UI_DESIGN_DOC_INTO_JS_TOOL; + let arguments = match serde_json::from_value::(input.clone()) { + Ok(arguments) => arguments, + Err(error) => return rejected(tool, format!("{tool} 输入无效:{error}")), + }; + let expected_project_id = match game_creator_agent_runtime_context_project_id(root) { + Ok(project_id) => project_id, + Err(error) => return rejected(tool, error), + }; + match generate_ui_design_code_at(GenerateUiDesignCodeInput { + project_path: root.to_string_lossy().into_owned(), + expected_project_id, + asset_id: arguments.design_doc_asset_id, + }) { + Ok(result) => AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "ok".to_string(), + summary: format!( + "已生成 UI 设计代码 {}:{} 棵树 / {} 个节点", + result.relative_path, result.tree_count, result.node_count + ), + detail: serde_json::to_string(&result).ok(), + }, + Err(error) => error_observation(root, tool, error), + } +} + +/// 输入解析失败:模型只给了参数,摘要里不含宿主路径,无需再脱敏。 +fn rejected(tool: &str, summary: String) -> AgentRuntimeToolObservation { + AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "rejected".to_string(), + summary, + detail: None, + } +} + +fn error_observation(root: &Path, tool: &str, error: String) -> AgentRuntimeToolObservation { + AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "error".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 500), + detail: None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn from_images_rejects_unknown_fields_and_accepts_reference_pairs() { + let root = Path::new("."); + let rejected = observe_agent_runtime_ui_design_doc_from_images( + root, + &json!({ "images": [{ "assetId": "a", "path": null }], "extra": 1 }), + ); + assert_eq!(rejected.status, "rejected"); + assert!(rejected.summary.contains("extra"), "{}", rejected.summary); + + // 参数合法但项目不存在:说明参数已经过解析,失败发生在项目校验之后。 + let invalid_project = observe_agent_runtime_ui_design_doc_from_images( + root, + &json!({ "images": [{ "assetId": "a", "path": null }] }), + ); + assert_eq!(invalid_project.status, "rejected"); + assert!(!invalid_project.summary.contains("输入无效")); + } + + #[test] + fn design_doc_asset_id_must_be_a_string() { + // run-workflow 与 into-js 共用同一份入参解析,这里解析一次、同步调用一次。 + assert!( + serde_json::from_value::(json!({ "designDocAssetId": 1 })).is_err() + ); + let observation = observe_agent_runtime_ui_design_doc_into_js( + Path::new("."), + &json!({ "designDocAssetId": 1 }), + ); + assert_eq!(observation.status, "rejected"); + assert!( + observation.summary.contains("输入无效"), + "{}", + observation.summary + ); + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs index 407bf2eff..a9d4ff8da 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs @@ -225,9 +225,11 @@ pub(crate) fn native_runtime_function_name(tool: &str) -> Option { } fn native_runtime_function_name_for_tool(tool: &str) -> String { + // 工具 ID 允许 '-'(如 `ui-design-doc.from-images`),函数名只能是 + // `[A-Za-z_][A-Za-z0-9_]*`,两种分隔符都要归一成下划线。 format!( "{AGENT_RUNTIME_NATIVE_TOOL_PREFIX}{}", - tool.replace('.', "_") + tool.replace(['.', '-'], "_") ) } @@ -1065,6 +1067,9 @@ fn runtime_tool_description(tool: &str) -> &'static str { "canvas.asset_generate" => { prompt_text!("nativeTools.canvas.asset_generate.description") } + "ui-design-doc.from-images" => prompt_text!("uiDesignDoc.from_images.description"), + "ui-design-doc.run-workflow" => prompt_text!("uiDesignDoc.run_workflow.description"), + "ui-design-doc.into-js" => prompt_text!("uiDesignDoc.into_js.description"), "cocos.editor.execute" => { prompt_text!("nativeTools.cocos.editor.execute.description") } @@ -1171,6 +1176,30 @@ fn runtime_tool_input_schema(tool: &str) -> Value { } } }), + "ui-design-doc.from-images" => json!({ + "type": "object", + // 每张设计图给 assetId 或 path 之一,另一个显式传 null。 + "required": ["images"], + "additionalProperties": false, + "properties": { + "images": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": { + "type": "object", + "required": ["assetId", "path"], + "additionalProperties": false, + "properties": { + "assetId": { "type": ["string", "null"], "minLength": 1, "maxLength": 512 }, + "path": { "type": ["string", "null"], "minLength": 1, "maxLength": 512 } + } + } + } + } + }), + "ui-design-doc.run-workflow" => one_string_input_schema("designDocAssetId"), + "ui-design-doc.into-js" => one_string_input_schema("designDocAssetId"), "project.search" => json!({ "type": "object", "required": ["query", "path", "maxResults", "caseSensitive"], "additionalProperties": false, "properties": { @@ -2159,4 +2188,32 @@ mod tests { }) ); } + + #[test] + fn native_ui_design_doc_tools_expose_dash_free_function_names() { + // 工具 ID 允许 '-',Function Calling 的函数名不允许;两者不能混用同一套归一。 + let fallback = runtime_tool_description("ui-design-doc.unknown"); + for tool in [ + "ui-design-doc.from-images", + "ui-design-doc.run-workflow", + "ui-design-doc.into-js", + ] { + let function_name = native_runtime_function_name_for_tool(tool); + assert!( + !function_name.contains(['.', '-']), + "{tool} 的函数名仍有非法字符:{function_name}" + ); + assert_ne!( + runtime_tool_description(tool), + fallback, + "{tool} 缺少工具描述" + ); + assert_eq!(runtime_tool_input_schema(tool)["type"], json!("object")); + } + assert_eq!( + runtime_tool_input_schema("ui-design-doc.from-images")["properties"]["images"] + ["maxItems"], + json!(4) + ); + } }