注册 UI 设计文档三个 Agent 工具
- 新增 agent/runtime_tools/ui_design_doc.rs:三个工具的参数解析、项目 ID 与 provider 身份注入、manifest 失效广播 - agent_native_tools 增加三个工具的描述与入参 schema,函数名归一同时处理 '.' 与 '-' - Runtime 可执行工具目录、并行账本命令映射、design-foundation 自主白名单同步登记 - 新增工具描述可用性与入参校验单测
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<UiDesignImageReference>,
|
||||
}
|
||||
|
||||
#[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::<FromImagesArguments>(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::<DesignDocArguments>(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::<DesignDocArguments>(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::<DesignDocArguments>(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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -225,9 +225,11 @@ pub(crate) fn native_runtime_function_name(tool: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user