From 760525069725c39b08e314a016d784020967519c Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 13 Sep 2026 00:04:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=93=E5=BC=80=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=97=B6=E7=9A=84=E5=90=8C=E6=AD=A5=E9=98=BB=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将项目目录检查、manifest、revision 和 Planning V2 hydrate 移到 blocking worker 保留同步核心函数供单元测试复用并更新测试调用 同步更新项目打开链路技术方案 --- .../runtime_protocol/planning_policy_v2.rs | 2 +- .../runtime_protocol/planning_session_v2.rs | 13 +++++++- .../src-tauri/src/commands.rs | 23 +++++++++++-- .../src-tauri/src/preview.rs | 10 +++++- .../src-tauri/src/tests/project.rs | 32 ++++++++++--------- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 ++ 6 files changed, 62 insertions(+), 20 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs index ed29b95d3..4b588acfb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs @@ -1908,7 +1908,7 @@ mod tests { ) .expect("first persist"); rewind_session_keep_gdd_file(&root, &session); - let hydrated = hydrate_planning_session_v2( + let hydrated = hydrate_planning_session_v2_sync( root.to_string_lossy().to_string(), Some(session.session_id.clone()), ) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs index 765a14a82..0d5aa7239 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs @@ -1465,7 +1465,18 @@ async fn run_planning_session_v2_command( } #[tauri::command] -pub(crate) fn hydrate_planning_session_v2( +pub(crate) async fn hydrate_planning_session_v2( + project_path: String, + session_id: Option, +) -> Result, String> { + tauri::async_runtime::spawn_blocking(move || { + hydrate_planning_session_v2_sync(project_path, session_id) + }) + .await + .map_err(|error| format!("恢复 Planning V2 后台任务失败:{error}"))? +} + +pub(crate) fn hydrate_planning_session_v2_sync( project_path: String, session_id: Option, ) -> Result, String> { diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index d5fc6b864..2036009d8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -536,7 +536,15 @@ pub(crate) fn is_local_project_directory_non_empty(project_path: String) -> Resu } #[tauri::command] -pub(crate) fn inspect_local_project_directory( +pub(crate) async fn inspect_local_project_directory( + project_path: String, +) -> Result { + tauri::async_runtime::spawn_blocking(move || inspect_local_project_directory_sync(project_path)) + .await + .map_err(|error| format!("检查项目目录后台任务失败:{error}"))? +} + +pub(crate) fn inspect_local_project_directory_sync( project_path: String, ) -> Result { let root = Path::new(project_path.trim()); @@ -840,7 +848,18 @@ mod plan_gdd_markdown_path_tests { } #[tauri::command] -pub(crate) fn get_local_game_manifest( +pub(crate) async fn get_local_game_manifest( + project_path: String, + command_id: Option, +) -> Result { + tauri::async_runtime::spawn_blocking(move || { + get_local_game_manifest_sync(project_path, command_id) + }) + .await + .map_err(|error| format!("读取项目 manifest 后台任务失败:{error}"))? +} + +pub(crate) fn get_local_game_manifest_sync( project_path: String, command_id: Option, ) -> Result { diff --git a/apps/ai-game-creator-shell/src-tauri/src/preview.rs b/apps/ai-game-creator-shell/src-tauri/src/preview.rs index f79c9bc39..a446ec666 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/preview.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/preview.rs @@ -756,7 +756,15 @@ pub(crate) fn get_local_game_preview_status_at( } #[tauri::command] -pub(crate) fn get_local_game_project_revision( +pub(crate) async fn get_local_game_project_revision( + project_path: String, +) -> Result { + tauri::async_runtime::spawn_blocking(move || get_local_game_project_revision_sync(project_path)) + .await + .map_err(|error| format!("读取项目版本后台任务失败:{error}"))? +} + +pub(crate) fn get_local_game_project_revision_sync( project_path: String, ) -> Result { get_local_game_project_revision_at(Path::new(project_path.trim())) diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index b4839ba55..632c8ee11 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -1612,7 +1612,7 @@ fn project_directory_commands_reject_control_characters() { assert!(is_local_project_directory_non_empty(project_path.clone()) .expect_err("non-empty check should reject control characters") .contains("控制字符")); - assert!(inspect_local_project_directory(project_path.clone()) + assert!(inspect_local_project_directory_sync(project_path.clone()) .expect_err("directory inspect should reject control characters") .contains("控制字符")); assert!( @@ -1648,7 +1648,7 @@ fn project_directory_non_empty_check_reports_existing_content() { #[test] fn project_directory_status_distinguishes_missing_file_and_dir() { let root = unique_project_path(); - let missing = inspect_local_project_directory(root.to_string_lossy().to_string()) + let missing = inspect_local_project_directory_sync(root.to_string_lossy().to_string()) .expect("missing status"); assert_eq!( missing, @@ -1670,8 +1670,8 @@ fn project_directory_status_distinguishes_missing_file_and_dir() { ); fs::write(&root, "not a dir").expect("file"); - let file_status = - inspect_local_project_directory(root.to_string_lossy().to_string()).expect("file status"); + let file_status = inspect_local_project_directory_sync(root.to_string_lossy().to_string()) + .expect("file status"); assert!(file_status.exists); assert!(!file_status.is_directory); assert!(!file_status.is_game_creator_project); @@ -1684,8 +1684,8 @@ fn project_directory_status_distinguishes_missing_file_and_dir() { fs::remove_file(&root).expect("remove file"); fs::create_dir_all(&root).expect("dir"); - let dir_status = - inspect_local_project_directory(root.to_string_lossy().to_string()).expect("dir status"); + let dir_status = inspect_local_project_directory_sync(root.to_string_lossy().to_string()) + .expect("dir status"); assert!(dir_status.exists); assert!(dir_status.is_directory); assert!(!dir_status.is_game_creator_project); @@ -1698,7 +1698,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() { fs::create_dir_all(root.join(".agent")).expect("agent dir"); fs::write(root.join(".agent/manifest.json"), "{broken").expect("broken manifest"); let broken_manifest_status = - inspect_local_project_directory(root.to_string_lossy().to_string()) + inspect_local_project_directory_sync(root.to_string_lossy().to_string()) .expect("broken manifest status"); assert!(broken_manifest_status.exists); assert!(broken_manifest_status.is_directory); @@ -1730,7 +1730,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() { .to_string(), ) .expect("run trace"); - let project_status = inspect_local_project_directory(root.to_string_lossy().to_string()) + let project_status = inspect_local_project_directory_sync(root.to_string_lossy().to_string()) .expect("project status"); assert!(project_status.exists); assert!(project_status.is_directory); @@ -1770,7 +1770,7 @@ fn project_directory_status_reports_workspace_relative_godot_root() { fs::write(godot_root.join("project.godot"), "[application]\n") .expect("write nested project.godot"); - let status = inspect_local_project_directory(root.to_string_lossy().to_string()) + let status = inspect_local_project_directory_sync(root.to_string_lossy().to_string()) .expect("inspect nested Godot workspace"); assert!(status.exists); @@ -1790,7 +1790,7 @@ fn project_directory_status_rejects_ambiguous_direct_child_godot_projects() { .expect("write nested project.godot"); } - let error = inspect_local_project_directory(root.to_string_lossy().to_string()) + let error = inspect_local_project_directory_sync(root.to_string_lossy().to_string()) .expect_err("ambiguous Godot workspace must fail inspection"); assert!(error.contains("多个 Godot 项目"), "{error}"); @@ -2718,12 +2718,14 @@ fn local_game_manifest_reads_respect_declared_command_policy() { .expect("write policy"); let project_path = root.to_string_lossy().into_owned(); - let status_error = get_local_game_manifest(project_path.clone(), None) + let status_error = get_local_game_manifest_sync(project_path.clone(), None) .expect_err("default project.status denied"); - let asset_error = get_local_game_manifest(project_path.clone(), Some("asset.list".to_string())) - .expect_err("asset.list denied"); - let unsupported_error = get_local_game_manifest(project_path, Some("file.read".to_string())) - .expect_err("unsupported manifest command denied"); + let asset_error = + get_local_game_manifest_sync(project_path.clone(), Some("asset.list".to_string())) + .expect_err("asset.list denied"); + let unsupported_error = + get_local_game_manifest_sync(project_path, Some("file.read".to_string())) + .expect_err("unsupported manifest command denied"); assert!(status_error.contains("项目权限策略拒绝执行:project.status")); assert!(asset_error.contains("项目权限策略拒绝执行:asset.list")); diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 25b2f6c50..dfd2e1532 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -4,6 +4,8 @@ DirectProject 工作区只恢复自身对话,不按专业 Agent 默认任务占位行批量读取旧会话或生成专业 Agent 文本回执。专业 Agent 结果加载 effect 必须以当前 Runtime 模式为边界,并在模式切换时清空旧结果。仍供开发入口使用的 `read_local_conversation` 在 blocking worker 内完整执行权限校验、会话目录解析和历史读取,避免文件访问或锁等待阻塞 Tauri 窗口线程。 +项目打开链路的目录检查、manifest 读取、项目 revision 读取和 Planning V2 hydrate 也必须通过 blocking worker 执行;它们可能碰到项目写锁,不能在 Tauri 窗口线程同步等待。 + DirectProject 自身的 `read_direct_project_conversation` 也必须在 blocking worker 中执行权限校验、JSONL 历史解析和消息投影,不能因为它只读取一份项目历史就保留同步 Tauri command。 验收覆盖实际 Launcher 打开已有项目:恢复 DirectProject 对话且不调用 `read_local_conversation`;后台读取仍遵守项目权限策略。原生客户端重复打开同一已有项目时验证窗口响应,IPC 测量只记录命令名和耗时,不记录会话内容。