From c19b321785aaf59283c116c4c7925df3da1e565a Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 13 Sep 2026 17:12:37 +0800 Subject: [PATCH] =?UTF-8?q?Cocos=20=E6=8F=92=E4=BB=B6=E6=8C=89=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E9=A1=B9=E7=9B=AE=E7=B1=BB=E5=9E=8B=E6=9A=B4=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Cocos 项目根识别门禁,限制插件、面板、RPC 和编辑器执行范围 让 DirectProject MCP 工具目录按当前项目类型动态过滤 切换离开 Cocos 项目时停止已运行的插件实例 补充项目级测试、Cocos 技术方案和插件说明 --- .../src-tauri/src/agent/direct_tool_bridge.rs | 11 +- .../src-tauri/src/agent/direct_tools_mcp.rs | 8 +- .../src/agent/runtime_tools/cocos_editor.rs | 4 +- .../src-tauri/src/builtin_plugins.rs | 48 +++++++ .../src-tauri/src/plugin_host.rs | 129 +++++++++++++++++- .../shared-memory/decision-log.md | 6 + ...AGC Cocos Creator 编辑器桥接模块-2026-09-09.md | 2 +- plugins/agc-cocos-editor/README.md | 8 +- 8 files changed, 203 insertions(+), 13 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 172c5c674..945ea4043 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -2349,9 +2349,10 @@ async fn bridge_cocos_call( arguments: &Value, operation: Option<&str>, ) -> Value { - if !crate::builtin_plugins::cocos_editor_agent_tool_available() { + if !crate::builtin_plugins::cocos_editor_agent_tool_available_for_project(&state.root) { return bridge_tool_result( - "Cocos Creator 插件已禁用,agc_cocos_execute 不可用".to_string(), + "当前项目不是 Cocos Creator 项目或 Cocos 插件不可用,agc_cocos_execute 不可用" + .to_string(), Vec::new(), true, ); @@ -2416,9 +2417,9 @@ async fn bridge_cocos_call( // validated Inspector/pipe bridge. It does not mutate AGC's project // files or manifest, so it must not wait on `.agent/project.lock`. // File-writing tools keep their own project lock separately. - if !crate::builtin_plugins::cocos_editor_agent_tool_available() { + if !crate::builtin_plugins::cocos_editor_agent_tool_available_for_project(&root) { return Err(cocos_editor_bridge::BridgeError::InvalidInput( - "Cocos Creator 插件已禁用".to_string(), + "当前项目不是 Cocos Creator 项目或 Cocos 插件不可用".to_string(), )); } cocos_editor_bridge::execute_cocos_editor_code_for_project( @@ -2521,7 +2522,7 @@ async fn handle_direct_tool_bridge( let result = match request.tool.as_str() { // 隔离 MCP 只取工具名,不接触真实 AppData 或读取权限。 "builtin.plugins.tools" => bridge_tool_result( - json!({"tools": crate::builtin_plugins::available_agent_tools()}).to_string(), + json!({"tools": crate::builtin_plugins::available_agent_tools_for_project(&state.root)}).to_string(), Vec::new(), false, ), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index 419b668e7..5ec58cc87 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -1892,7 +1892,13 @@ mod tests { let config = tempfile::tempdir().unwrap(); crate::builtin_plugins::initialize(config.path()).unwrap(); let project = crate::tests::canonical_test_tempdir("builtin-mcp-project-"); - // 本用例只访问可用工具摘要和禁用入口,无需初始化完整游戏项目。 + // 工具目录现在按当前项目类型过滤,fixture 必须具备最小 Cocos Creator 结构。 + std::fs::write( + project.path().join("package.json"), + r#"{"creator":{"version":"3.8.8"}}"#, + ) + .unwrap(); + std::fs::create_dir(project.path().join("assets")).unwrap(); std::fs::create_dir_all(project.path().join(".agent")).unwrap(); std::fs::write(project.path().join(".agent/manifest.json"), "{}").unwrap(); let bridge = diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/cocos_editor.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/cocos_editor.rs index 5cf29088e..ffefb5398 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/cocos_editor.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/cocos_editor.rs @@ -33,11 +33,11 @@ pub(in crate::agent) fn observe_agent_runtime_cocos_editor_execute( detail: None, }; } - if !crate::builtin_plugins::cocos_editor_agent_tool_available() { + if !crate::builtin_plugins::cocos_editor_agent_tool_available_for_project(root) { return AgentRuntimeToolObservation { tool: "cocos.editor.execute".to_string(), status: "failed".to_string(), - summary: "Cocos Creator 插件已禁用".to_string(), + summary: "当前项目不是 Cocos Creator 项目或 Cocos 插件不可用".to_string(), detail: None, }; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/builtin_plugins.rs b/apps/ai-game-creator-shell/src-tauri/src/builtin_plugins.rs index 5e34469f5..d236d77c8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/builtin_plugins.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/builtin_plugins.rs @@ -245,6 +245,17 @@ pub(crate) fn cocos_editor_agent_tool_available() -> bool { agent_tool_available(BuiltinPlugin::CocosEditor) } +/// Cocos 编辑器插件只对当前确认为 Cocos Creator 的项目可用。 +/// +/// 项目类型以项目根的真实结构为准,不能仅凭插件开关或编译 feature 推断。 +pub(crate) fn cocos_editor_agent_tool_available_for_project(root: &Path) -> bool { + cocos_editor_agent_tool_available() + && crate::project::discover_local_cocos_project_root(root) + .ok() + .flatten() + .is_some() +} + pub(crate) fn available_agent_tools() -> Vec<&'static str> { if cocos_editor_agent_tool_available() { let mut tools = vec![AGC_COCOS_EDITOR_TOOL_NAME]; @@ -259,6 +270,15 @@ pub(crate) fn available_agent_tools() -> Vec<&'static str> { } } +/// Project-scoped variant used by the isolated DirectProject MCP bridge. +/// Without a project root the safe result is an empty Cocos tool set. +pub(crate) fn available_agent_tools_for_project(root: &Path) -> Vec<&'static str> { + if !cocos_editor_agent_tool_available_for_project(root) { + return Vec::new(); + } + available_agent_tools() +} + #[cfg(test)] pub(crate) use tests::test_lock; @@ -435,4 +455,32 @@ mod tests { tool_visible_when_enabled ); } + + #[test] + fn project_scoped_availability_requires_a_cocos_creator_root() { + let _guard = test_lock(); + let directory = tempdir().expect("temp config"); + initialize(directory.path()).expect("initialize"); + set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).expect("enable"); + let non_cocos = tempdir().expect("non-cocos project"); + assert!(!cocos_editor_agent_tool_available_for_project( + non_cocos.path() + )); + + let cocos = tempdir().expect("cocos project"); + fs::write( + cocos.path().join("package.json"), + r#"{"creator":{"version":"3.8.8"}}"#, + ) + .expect("cocos package"); + fs::create_dir(cocos.path().join("assets")).expect("cocos assets"); + assert_eq!( + cocos_editor_agent_tool_available_for_project(cocos.path()), + cfg!(feature = "cocos-editor-execute") + ); + assert_eq!( + available_agent_tools_for_project(non_cocos.path()), + Vec::<&'static str>::new() + ); + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs b/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs index 5513aa6e3..51838ea81 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs @@ -937,9 +937,23 @@ impl PluginHost { .clone() .ok_or_else(|| "插件宿主尚未初始化".to_string())?; self.scan_locked(&mut state, &root)?; + let cocos_project = state + .active_project + .lock() + .map_err(|_| "项目上下文锁已损坏".to_string())? + .as_deref() + .is_some_and(|path| { + crate::project::discover_local_cocos_project_root(path) + .ok() + .flatten() + .is_some() + }); state .plugins .values() + .filter(|record| { + record.id != crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID || cocos_project + }) .map(|record| self.summary_locked(record)) .collect() } @@ -1003,6 +1017,20 @@ impl PluginHost { .clone() .ok_or_else(|| "插件宿主尚未初始化".to_string())?; let active_project = state.active_project.clone(); + if id == crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID + && !active_project + .lock() + .map_err(|_| "项目上下文锁已损坏".to_string())? + .as_deref() + .is_some_and(|path| { + crate::project::discover_local_cocos_project_root(path) + .ok() + .flatten() + .is_some() + }) + { + return Err("Cocos 编辑器插件只对当前 Cocos Creator 项目可用".to_string()); + } let editors = state.editors.clone(); let record = state .plugins @@ -1100,6 +1128,21 @@ impl PluginHost { .plugins .get(id) .ok_or_else(|| "插件不存在".to_string())?; + if id == crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID + && !state + .active_project + .lock() + .map_err(|_| "项目上下文锁已损坏".to_string())? + .as_deref() + .is_some_and(|path| { + crate::project::discover_local_cocos_project_root(path) + .ok() + .flatten() + .is_some() + }) + { + return Err("Cocos 编辑器插件只对当前 Cocos Creator 项目可用".to_string()); + } if record.running.is_none() || !record.manifest.permissions.contains("ui.register") { return Err("插件面板未激活".to_string()); } @@ -1145,6 +1188,21 @@ impl PluginHost { .root .clone() .ok_or_else(|| "插件宿主尚未初始化".to_string())?; + if id == crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID + && !state + .active_project + .lock() + .map_err(|_| "项目上下文锁已损坏".to_string())? + .as_deref() + .is_some_and(|path| { + crate::project::discover_local_cocos_project_root(path) + .ok() + .flatten() + .is_some() + }) + { + return Err("Cocos 编辑器插件只对当前 Cocos Creator 项目可用".to_string()); + } let record = state .plugins .get_mut(id) @@ -1478,6 +1536,20 @@ impl PluginHost { } "host.rpc" => { let input: EditorRpcInput = descriptor_from_params(params)?; + if manifest.id == crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID + && !active_project + .lock() + .map_err(|_| "项目上下文锁已损坏".to_string())? + .as_deref() + .is_some_and(|path| { + crate::project::discover_local_cocos_project_root(path) + .ok() + .flatten() + .is_some() + }) + { + return Err("Cocos 编辑器插件只对当前 Cocos Creator 项目可用".to_string()); + } let adapter = input .adapter .or_else(|| manifest.adapter.clone()) @@ -1534,7 +1606,7 @@ impl PluginHost { } pub(crate) fn set_active_project(&self, project_path: Option) -> Result<(), String> { - let state = self + let mut state = self .state .lock() .map_err(|_| "插件宿主锁已损坏".to_string())?; @@ -1548,10 +1620,30 @@ impl PluginHost { .map_err(|_| "项目目录不可读".to_string()) }) .transpose()?; + let is_cocos_project = project.as_deref().is_some_and(|path| { + crate::project::discover_local_cocos_project_root(path) + .ok() + .flatten() + .is_some() + }); *state .active_project .lock() .map_err(|_| "项目上下文锁已损坏".to_string())? = project; + if !is_cocos_project { + if let Some(record) = state + .plugins + .get_mut(crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID) + { + if let Some(mut running) = record.running.take() { + let _ = running.child.kill(); + let _ = running.child.wait(); + } + if record.manifest.enabled { + record.status = "stopped".to_string(); + } + } + } for record in state.plugins.values() { if let Some(running) = record.running.as_ref() { let subscribed = running @@ -2020,12 +2112,20 @@ setTimeout(() => send({ jsonrpc: '2.0', id: 1, method: 'host.registerCommand', p fn builtin_plugin_toggle_controls_availability() { let _guard = crate::builtin_plugins::test_lock(); let directory = tempdir().expect("temp config"); + fs::write( + directory.path().join("package.json"), + r#"{"creator":{"version":"3.8.8"}}"#, + ) + .expect("cocos package"); + fs::create_dir(directory.path().join("assets")).expect("cocos assets"); let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../plugins"); let host = PluginHost::default(); crate::builtin_plugins::initialize(directory.path()).expect("builtin plugin state"); host.initialize(directory.path()).expect("initialize"); host.set_plugin_workspace(workspace) .expect("set plugins workspace"); + host.set_active_project(Some(directory.path().to_string_lossy().into_owned())) + .expect("set cocos project"); let summary = |list: Vec| { list.into_iter() @@ -2060,6 +2160,12 @@ setTimeout(() => send({ jsonrpc: '2.0', id: 1, method: 'host.registerCommand', p fn workspace_cocos_plugin_round_trips_editor_rpc() { let _guard = crate::builtin_plugins::test_lock(); let directory = tempdir().expect("temp config"); + fs::write( + directory.path().join("package.json"), + r#"{"creator":{"version":"3.8.8"}}"#, + ) + .expect("cocos package"); + fs::create_dir(directory.path().join("assets")).expect("cocos assets"); crate::builtin_plugins::initialize(directory.path()).expect("builtin state"); let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../plugins"); let host = PluginHost::default(); @@ -2108,4 +2214,25 @@ setTimeout(() => send({ jsonrpc: '2.0', id: 1, method: 'host.registerCommand', p "stopped" ); } + + #[test] + fn cocos_plugin_is_hidden_and_cannot_start_for_non_cocos_project() { + let _guard = crate::builtin_plugins::test_lock(); + let directory = tempdir().expect("temp config"); + crate::builtin_plugins::initialize(directory.path()).expect("builtin state"); + let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../plugins"); + let host = PluginHost::default(); + host.initialize(directory.path()).expect("initialize"); + host.set_plugin_workspace(workspace).expect("set workspace"); + + let project = tempdir().expect("web project"); + host.set_active_project(Some(project.path().to_string_lossy().into_owned())) + .expect("set active project"); + assert!(host + .list() + .expect("list plugins") + .into_iter() + .all(|plugin| plugin.id != "agc-cocos-editor")); + assert!(host.start("agc-cocos-editor").is_err()); + } } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 9954ba7f5..6cc0542ff 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -8250,3 +8250,9 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 不做什么:不放宽 `.agent/project.lock` 的项目级串行化语义,不引入可重入项目锁,不改“同一调用链禁止二次获取 `.agent/project.lock`”的既有约定,不改 AGC 多进程拓扑,不改 `pendingOperations` 语义,也不改“活持有者始终不回收”的既有判据。其它仍用零等待取锁的入口(`command.exec / project.verify / memory / conversation / task / checkpoint / 预览 / UI 编辑器 / 资源编辑器 / Tauri 命令`)不在本次范围。 - 验证方式:`project_lock_recovery` 追加持锁方身份与权限拒绝两条;`direct_tool_bridge` 追加同进程重叠写等待、同轮并行写、有界等待不占 runtime worker(默认 `current_thread` runtime 加心跳任务,同步阻塞会立刻让心跳停摆)、ACL 拒绝不投影成争用四条;`project/write_lock` 追加“重试性只由错误码决定”与“终态改判三条件”两条平台无关用例(平台作参数传入,Linux CI 覆盖 Windows 分支)。Windows 本机定向结果:`project_write_lock` 18 条、`bridge_write_file` 4 条、`parent_wake` 16 条、`waits_across` 3 条全过;CI(`71e9ad313`)四个 job 全绿,其中 Native shell tests 的首轮失败正是第 ⑤ 条平台判据缺陷。 - 关联文档:`docs/project-memory/shared-memory/pitfalls.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、Issue #318。 + +## 2026-09-13 Cocos 插件按当前项目类型暴露 + +- 决策:`agc-cocos-editor` 只有在当前受控项目通过 Cocos Creator 根目录识别(`package.json.creator.version` + 普通 `assets/`)时才暴露插件、面板和 Cocos 工具;无项目或其它项目类型均隐藏并失败关闭。 +- 决策:项目切换离开 Cocos 时立即停止已运行的插件实例;启动、面板读取、插件 RPC、Runtime execute 和 DirectProject MCP 工具目录/执行入口全部再次校验项目类型。Cocos 编辑器操作优先经内置插件入口,禁止回退到项目 `extensions/`、`package.json` 插件或第三方 MCP。 +- 验证:新增 builtin/plugin host 项目级门禁测试,Direct MCP fixture 补最小 Cocos 工程结构;Rust 定向测试、显式 `cocos-editor-execute` feature 编译、编码检查和 `git diff --check` 已执行。 diff --git a/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md b/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md index 74cbe6b64..c1d7604c9 100644 --- a/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md +++ b/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md @@ -71,7 +71,7 @@ plugins/agc-cocos-editor/ 宿主按 `AGC_PLUGIN_WORKSPACE`、随包 `/plugins`、开发构建仓库 `plugins/` 的顺序解析工作区;插件包内的 `native/payload` 由构建脚本随包映射,生成的 DLL 不入库。插件协议、权限和面板挂载全部复用通用宿主,Cocos 专属逻辑只存在于本插件包:进程名与 `--project` 解析、Creator 版本校验、named pipe 协议和 Windows 注入。 -该插件是**内置插件**:随客户端分发、不能卸载,只能通过 `set_agc_plugin_enabled` 控制是否可用。禁用后插件进程停止且不能启动,通用 execute 和全部 `cocos_*` 工具从 Agent 目录消失;直接请求隐藏工具也会在宿主执行前被拒绝。开关状态保存在 AppData `extensions/builtin-plugins.json`,隔离 MCP 每次 tools/list 都向绑定宿主询问当前状态。 +该插件是**内置插件**:随客户端分发、不能卸载,只能通过 `set_agc_plugin_enabled` 控制是否可用。除开关和 feature 外,还必须满足“当前受控项目已识别为 Cocos Creator 项目”这一门禁;没有当前项目或项目类型不是 Cocos 时,插件不出现在插件列表、面板、Agent 工具目录或 MCP tools/list 中,启动、面板读取、RPC 和编辑器执行也会失败关闭。项目切换离开 Cocos 后,已运行实例立即停止。Cocos 编辑器操作统一优先通过该内置插件的 `cocos.editor.execute` / `cocos.editor.operation`(DirectProject 对应 `agc_cocos_execute`);不得改走项目目录 `extensions/`、`package.json` 插件或第三方 MCP。开关状态保存在 AppData `extensions/builtin-plugins.json`,隔离 MCP 每次 tools/list 都向绑定宿主询问当前状态与项目门禁。 ## AGC 项目打开入口 diff --git a/plugins/agc-cocos-editor/README.md b/plugins/agc-cocos-editor/README.md index 859255264..68e885a80 100644 --- a/plugins/agc-cocos-editor/README.md +++ b/plugins/agc-cocos-editor/README.md @@ -3,9 +3,11 @@ Cocos Creator 编辑器桥接插件。用户侧看到的是一个普通 AGC 插件:插件生命周期、UI、 RPC、权限和能力注册全部由通用宿主负责,只有“如何连接 Cocos Creator”属于本插件。 -它同时是 AGC 的**内置插件**:随客户端分发、不能卸载。用户在运行时设置里只能切换 -“是否可用”,禁用后插件不能启动,`agc_cocos_execute` 与全部 `cocos_*` Agent 工具也会 -从 Agent 工具列表、工具策略快照和上下文里消失;重新启用后立即恢复。 +它同时是 AGC 的**内置插件**:随客户端分发、不能卸载。只有当前受控目录被识别为 Cocos +Creator 项目时才会暴露插件、面板和工具;切换到其它项目会立即停止插件实例并隐藏对应能力。 +用户在运行时设置里只能切换“是否可用”,禁用后插件不能启动,`agc_cocos_execute` 与全部 +`cocos_*` Agent 工具也会从 Agent 工具列表、工具策略快照和上下文里消失;重新启用后仍需 +满足当前项目是 Cocos 的门禁。 Cocos Creator 项目目录中的 `extensions/`、`package.json` 插件声明或第三方 MCP 包不属于 AGC Cocos 桥接来源。Agent 处理 Cocos 请求时只使用客户端登记的 `agc-cocos-editor`