把 ui-design-doc 的阻塞 IO 挪出 async 执行器

src-tauri/src/agent/runtime_tools/ui_design_doc.rs 的 from-images 与 into-js 观察函数改为 async,核心逻辑用 tokio::task::spawn_blocking 执行:from-images 要逐张 image::open 解码设计图再写 manifest,into-js 要读整份文档、算 SHA-256 再落盘 JS,原先都在 async 执行器线程上同步跑,会和其它已派发的动作抢执行器

阻塞任务未完成(join 失败)时返回 status=error 的观察,沿用仓库既有的 spawn_blocking 失败兜底写法

src-tauri/src/agent/runtime_actions/action_execution.rs 两个分发分支改为 await;ui_design_doc.rs 的两个用例改成 #[tokio::test] 并 await
This commit is contained in:
2026-09-24 17:47:58 +08:00
parent 4046318118
commit 62b0f103de
2 changed files with 54 additions and 22 deletions
@@ -154,13 +154,15 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
}
"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)
observe_agent_runtime_ui_design_doc_from_images(root, &action.input).await
}
"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),
"ui-design-doc.into-js" => {
observe_agent_runtime_ui_design_doc_into_js(root, &action.input).await
}
"project.index" => observe_agent_runtime_project_snapshot_with_lock(
root,
agent_id,
@@ -28,7 +28,7 @@ struct DesignDocArguments {
design_doc_asset_id: String,
}
pub(in crate::agent) fn observe_agent_runtime_ui_design_doc_from_images(
pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_from_images(
root: &Path,
input: &Value,
) -> AgentRuntimeToolObservation {
@@ -44,11 +44,25 @@ pub(in crate::agent) fn observe_agent_runtime_ui_design_doc_from_images(
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,
}) {
// 新建文档要逐张解码设计图并写 manifest:整段阻塞 IO 交给 blocking 线程池,
// 别把 async 执行器(同一个 runtime 还要继续跑其它动作)占住。
let project_path = root.to_string_lossy().into_owned();
let images = arguments.images;
let created = match tokio::task::spawn_blocking(move || {
create_ui_design_doc_from_images(CreateUiDesignDocFromImagesInput {
project_path,
expected_project_id,
images,
})
})
.await
{
Ok(created) => created,
Err(_) => {
return error_observation(root, tool, "后台创建 UI 设计文档任务未完成".to_string())
}
};
match created {
Ok(created) => {
if revision_before.is_none_or(|before| created.committed_project_revision > before) {
emit_game_creator_manifest_invalidated(root, tool);
@@ -135,7 +149,7 @@ pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_run_workflow(
}
}
pub(in crate::agent) fn observe_agent_runtime_ui_design_doc_into_js(
pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_into_js(
root: &Path,
input: &Value,
) -> AgentRuntimeToolObservation {
@@ -148,11 +162,24 @@ pub(in crate::agent) fn observe_agent_runtime_ui_design_doc_into_js(
Ok(project_id) => project_id,
Err(error) => return rejected(root, 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,
}) {
// 生成代码要读整份文档、算 SHA-256 再落盘,同样是阻塞 IO,放到 blocking 线程池。
let project_path = root.to_string_lossy().into_owned();
let asset_id = arguments.design_doc_asset_id;
let generated = match tokio::task::spawn_blocking(move || {
generate_ui_design_code_at(GenerateUiDesignCodeInput {
project_path,
expected_project_id,
asset_id,
})
})
.await
{
Ok(generated) => generated,
Err(_) => {
return error_observation(root, tool, "后台生成 UI 设计代码任务未完成".to_string())
}
};
match generated {
Ok(result) => AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "ok".to_string(),
@@ -190,13 +217,14 @@ mod tests {
use super::*;
use serde_json::json;
#[test]
fn from_images_rejects_unknown_fields_and_accepts_reference_pairs() {
#[tokio::test]
async 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 }),
);
)
.await;
assert_eq!(rejected.status, "rejected");
assert!(rejected.summary.contains("extra"), "{}", rejected.summary);
@@ -204,21 +232,23 @@ mod tests {
let invalid_project = observe_agent_runtime_ui_design_doc_from_images(
root,
&json!({ "images": [{ "assetId": "a", "path": null }] }),
);
)
.await;
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 共用同一份入参解析,这里解析一次、同步调用一次。
#[tokio::test]
async 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 }),
);
)
.await;
assert_eq!(observation.status, "rejected");
assert!(
observation.summary.contains("输入无效"),