注册 UI 设计文档三个 Agent 工具

- 新增 agent/runtime_tools/ui_design_doc.rs:三个工具的参数解析、项目 ID 与 provider 身份注入、manifest 失效广播
- agent_native_tools 增加三个工具的描述与入参 schema,函数名归一同时处理 '.' 与 '-'
- Runtime 可执行工具目录、并行账本命令映射、design-foundation 自主白名单同步登记
- 新增工具描述可用性与入参校验单测
This commit is contained in:
2026-09-23 21:30:50 +08:00
parent 116d9d5940
commit b8d218f741
7 changed files with 300 additions and 1 deletions
@@ -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)
);
}
}