From dd36de3aa66b78f2c206644f653803b34990a681 Mon Sep 17 00:00:00 2001
From: kdletters
Date: Thu, 10 Sep 2026 20:00:02 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20AGC=20=E5=86=85=E7=BD=AE?=
=?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=A8=A1=E5=BC=8F=E4=B8=8E=E6=8F=92=E4=BB=B6?=
=?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 plugins/ 插件工作区及 agc-cocos-editor 插件包,Cocos 直连模块从宿主源码树迁入插件包
- 新增 server-rs/crates/editor-adapter-api 通用编辑器适配器契约,宿主只保留适配器注册与 RPC 路由
- 新增内置插件登记表与可用开关,状态持久化在 AppData extensions/builtin-plugins.json,状态文件损坏时失败关闭
- 禁用内置插件时先停止进程并拒绝启动,同时从 Runtime 工具目录、工具策略快照、原生函数目录与 DirectProject 工具面移除对应工具
- 插件宿主新增 plugins/ 工作区扫描,内置插件优先于同名 AppData 导入插件,前端只提供可用开关而不提供重命名和卸载入口
- 编辑器操作统一经 host.rpc 路由到插件包自带的 native 适配器,并删除 Cocos 专属 Tauri 命令
- 修正 Windows 打包与开发态 payload 路径,随包映射 plugins 工作区资源
- 同步插件、Cocos 桥接、实施计划文档与 decision log
---
.gitignore | 2 +
.../scripts/check-config.mjs | 8 +-
.../src-tauri/Cargo.lock | 10 +
.../src-tauri/Cargo.toml | 3 +-
apps/ai-game-creator-shell/src-tauri/build.rs | 111 ++++-
.../src-tauri/src/agent/direct_tool_bridge.rs | 75 +++-
.../src-tauri/src/agent/direct_tools_mcp.rs | 30 +-
.../runtime_actions/tool_policy_snapshot.rs | 10 +-
.../src-tauri/src/agent/runtime_tools.rs | 4 +-
.../src-tauri/src/agent_native_tools.rs | 13 +-
.../src-tauri/src/builtin_plugins.rs | 321 ++++++++++++++
.../src-tauri/src/cocos_editor.rs | 172 -------
.../src-tauri/src/editor_adapter/mod.rs | 36 +-
.../src-tauri/src/editor_adapters.rs | 54 +++
.../src-tauri/src/main.rs | 28 +-
.../src-tauri/src/plugin_host.rs | 368 ++++++++++++++-
.../src-tauri/tauri.windows.conf.json | 2 +-
apps/ai-game-creator-shell/src/app/types.ts | 3 +
.../runtime-config/RuntimeConfigDialog.tsx | 381 ++++++++++------
.../src/services/pluginHost.ts | 8 +
.../shared-memory/decision-log.md | 16 +
...AGC Cocos Creator 编辑器桥接模块-2026-09-09.md | 25 +-
...案】AGC通用插件宿主与编辑器适配-2026-09-09.md | 31 +-
...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +-
package-lock.json | 18 +
package.json | 4 +
plugins/README.md | 70 +++
plugins/agc-cocos-editor/README.md | 65 +++
.../native}/cocos-editor-bridge/.gitignore | 0
.../native}/cocos-editor-bridge/Cargo.toml | 1 +
.../native}/cocos-editor-bridge/build.rs | 0
.../native/native_payload.cpp | 0
.../cocos-editor-bridge/payload/bootstrap.cjs | 0
.../payload/bootstrap.test.cjs | 0
.../native/cocos-editor-bridge/src/adapter.rs | 419 ++++++++++++++++++
.../native}/cocos-editor-bridge/src/lib.rs | 4 +
plugins/agc-cocos-editor/package.json | 13 +
.../agc-cocos-editor/panels/cocos-editor.html | 74 ++++
plugins/agc-cocos-editor/plugin.json | 32 ++
.../src/cocos-editor-adapter.mjs | 102 +++++
plugins/agc-cocos-editor/src/entry.mjs | 231 ++++++++++
plugins/agc-cocos-editor/src/entry.test.mjs | 257 +++++++++++
scripts/check-npm-workspaces.mjs | 3 +
scripts/check-npm-workspaces.test.mjs | 2 +
server-rs/Cargo.toml | 2 +-
.../crates/editor-adapter-api/Cargo.toml | 11 +
.../crates/editor-adapter-api/src/lib.rs | 62 +++
47 files changed, 2674 insertions(+), 409 deletions(-)
create mode 100644 apps/ai-game-creator-shell/src-tauri/src/builtin_plugins.rs
delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/cocos_editor.rs
create mode 100644 apps/ai-game-creator-shell/src-tauri/src/editor_adapters.rs
create mode 100644 plugins/README.md
create mode 100644 plugins/agc-cocos-editor/README.md
rename {server-rs/crates => plugins/agc-cocos-editor/native}/cocos-editor-bridge/.gitignore (100%)
rename {server-rs/crates => plugins/agc-cocos-editor/native}/cocos-editor-bridge/Cargo.toml (93%)
rename {server-rs/crates => plugins/agc-cocos-editor/native}/cocos-editor-bridge/build.rs (100%)
rename {server-rs/crates => plugins/agc-cocos-editor/native}/cocos-editor-bridge/native/native_payload.cpp (100%)
rename {server-rs/crates => plugins/agc-cocos-editor/native}/cocos-editor-bridge/payload/bootstrap.cjs (100%)
rename {server-rs/crates => plugins/agc-cocos-editor/native}/cocos-editor-bridge/payload/bootstrap.test.cjs (100%)
create mode 100644 plugins/agc-cocos-editor/native/cocos-editor-bridge/src/adapter.rs
rename {server-rs/crates => plugins/agc-cocos-editor/native}/cocos-editor-bridge/src/lib.rs (99%)
create mode 100644 plugins/agc-cocos-editor/package.json
create mode 100644 plugins/agc-cocos-editor/panels/cocos-editor.html
create mode 100644 plugins/agc-cocos-editor/plugin.json
create mode 100644 plugins/agc-cocos-editor/src/cocos-editor-adapter.mjs
create mode 100644 plugins/agc-cocos-editor/src/entry.mjs
create mode 100644 plugins/agc-cocos-editor/src/entry.test.mjs
create mode 100644 server-rs/crates/editor-adapter-api/Cargo.toml
create mode 100644 server-rs/crates/editor-adapter-api/src/lib.rs
diff --git a/.gitignore b/.gitignore
index a9d1e9e80..2770d44c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,8 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
+/apps/ai-game-creator-shell/src-tauri/resources/plugins/
+/plugins/agc-cocos-editor/native/payload/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs
index 041c69e1b..a6f5baee8 100644
--- a/apps/ai-game-creator-shell/scripts/check-config.mjs
+++ b/apps/ai-game-creator-shell/scripts/check-config.mjs
@@ -126,11 +126,7 @@ const allowedUncalledTauriCommands = [
'call_agc_plugin',
'read_agc_plugin_panel',
'set_agc_plugin_project_path',
- 'prepare_cocos_editor_injection',
- 'ping_cocos_editor',
- 'status_cocos_editor',
- 'execute_cocos_editor_code',
- 'inject_cocos_editor',
+ 'set_agc_plugin_enabled',
];
const sourceExtensions = new Set([
'.json',
@@ -1313,7 +1309,7 @@ const expectedBundledWindowsResources = {
'codex/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
- 'resources/cocos-editor-bridge': 'cocos-editor-bridge',
+ 'resources/plugins': 'plugins',
};
if (tauriConfig.bundle?.resources !== undefined) {
throw new Error(
diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock
index 7a2344a9e..cbaa6255d 100644
--- a/apps/ai-game-creator-shell/src-tauri/Cargo.lock
+++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock
@@ -738,6 +738,7 @@ name = "cocos-editor-bridge"
version = "0.1.0"
dependencies = [
"cc",
+ "editor-adapter-api",
"serde",
"serde_json",
"sha2",
@@ -1216,6 +1217,14 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+[[package]]
+name = "editor-adapter-api"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "either"
version = "1.16.0"
@@ -1721,6 +1730,7 @@ dependencies = [
"base64 0.22.1",
"chromiumoxide",
"cocos-editor-bridge",
+ "editor-adapter-api",
"futures",
"getrandom 0.3.4",
"http",
diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.toml b/apps/ai-game-creator-shell/src-tauri/Cargo.toml
index 7197ad8a2..c524583aa 100644
--- a/apps/ai-game-creator-shell/src-tauri/Cargo.toml
+++ b/apps/ai-game-creator-shell/src-tauri/Cargo.toml
@@ -22,7 +22,8 @@ ts-rs = "12.0.1"
typed_floats = { version = "1.0.7", features = ["serde"] }
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
-cocos-editor-bridge = { path = "../../../server-rs/crates/cocos-editor-bridge", default-features = false }
+cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
+editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
base64 = "0.22"
axum = "0.8"
chromiumoxide = "0.9.1"
diff --git a/apps/ai-game-creator-shell/src-tauri/build.rs b/apps/ai-game-creator-shell/src-tauri/build.rs
index 45dbb5911..4382abeef 100644
--- a/apps/ai-game-creator-shell/src-tauri/build.rs
+++ b/apps/ai-game-creator-shell/src-tauri/build.rs
@@ -182,6 +182,7 @@ fn main() {
);
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
stage_bundled_codex_cli(&manifest_dir);
+ stage_plugin_workspace(&manifest_dir);
stage_cocos_editor_payload(&manifest_dir);
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
@@ -223,13 +224,113 @@ fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
let source = candidates
.iter()
.find(|path| path.is_file())
- .unwrap_or_else(|| panic!("Cocos bridge native payload 未构建:{}", candidates.iter().map(|p| p.display().to_string()).collect::>().join(";")));
- let destination = manifest_dir.join("resources/cocos-editor-bridge/cocos-editor-bridge.dll");
- std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
- .expect("创建 Cocos bridge 资源目录失败");
- std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
+ .unwrap_or_else(|| {
+ panic!(
+ "Cocos bridge native payload 未构建:{}",
+ candidates
+ .iter()
+ .map(|p| p.display().to_string())
+ .collect::>()
+ .join(";")
+ )
+ });
+ for destination in [
+ // 插件工作区里的 payload 是开发态与打包态的唯一真源。
+ manifest_dir
+ .join("../../../plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
+ // 随包资源目录与 tauri.windows.conf.json 的 `resources/plugins` 映射保持一致。
+ manifest_dir
+ .join("resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
+ ] {
+ std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
+ .expect("创建 Cocos bridge payload 目录失败");
+ std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
+ }
println!("cargo:rerun-if-changed={}", source.display());
}
#[cfg(not(windows))]
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
+
+/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
+///
+/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
+/// Cargo target 目录或 node_modules。
+#[cfg(windows)]
+fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
+ let repo_root = manifest_dir
+ .parent()
+ .and_then(|app_root| app_root.parent())
+ .and_then(|apps_dir| apps_dir.parent())
+ .expect("AGC 应用必须位于仓库 apps 目录下")
+ .to_path_buf();
+ let workspace = repo_root.join("plugins");
+ let destination_root = manifest_dir.join("resources/plugins");
+ std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
+ let entries = match std::fs::read_dir(&workspace) {
+ Ok(entries) => entries,
+ Err(_) => return,
+ };
+ for entry in entries.flatten() {
+ let plugin_root = entry.path();
+ if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
+ continue;
+ }
+ let name = entry.file_name();
+ let destination = destination_root.join(&name);
+ copy_plugin_file(
+ &plugin_root.join("plugin.json"),
+ &destination.join("plugin.json"),
+ );
+ for relative in [
+ std::path::PathBuf::from("src"),
+ std::path::PathBuf::from("panels"),
+ std::path::PathBuf::from("native/payload"),
+ ] {
+ copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
+ }
+ println!("cargo:rerun-if-changed={}", plugin_root.display());
+ }
+}
+
+#[cfg(windows)]
+fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
+ let entries = match std::fs::read_dir(source) {
+ Ok(entries) => entries,
+ Err(_) => return,
+ };
+ for entry in entries.flatten() {
+ let target = destination.join(entry.file_name());
+ let path = entry.path();
+ if path.is_dir() {
+ let name = entry.file_name();
+ let name = name.to_string_lossy();
+ if matches!(name.as_ref(), "target" | "node_modules" | ".git") {
+ continue;
+ }
+ std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
+ copy_plugin_tree(&path, &target);
+ } else {
+ // 测试文件不随包分发。
+ let name = entry.file_name();
+ let name = name.to_string_lossy();
+ if name.contains(".test.") {
+ continue;
+ }
+ copy_plugin_file(&path, &target);
+ }
+ }
+}
+
+#[cfg(windows)]
+fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
+ if !source.is_file() {
+ return;
+ }
+ std::fs::create_dir_all(destination.parent().expect("插件资源父目录"))
+ .expect("创建插件资源目录失败");
+ std::fs::copy(source, destination).expect("复制插件资源失败");
+}
+
+#[cfg(not(windows))]
+fn stage_plugin_workspace(_manifest_dir: &std::path::Path) {}
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 b5cd2ae74..5f94f6e0a 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
@@ -2301,35 +2301,56 @@ async fn bridge_web_search_at(root: &Path, arguments: &Value, search_url: &str)
#[cfg(all(windows, feature = "cocos-editor-execute"))]
async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value) -> Value {
+ if !crate::builtin_plugins::cocos_editor_agent_tool_available() {
+ return bridge_tool_result(
+ "Cocos Creator 插件已禁用,agc_cocos_execute 不可用".to_string(),
+ Vec::new(),
+ true,
+ );
+ }
let prepared = (|| {
bridge_reject_unknown_fields(arguments, &["code"])?;
enforce_project_permission_policy(&state.root, "cocos.editor.execute")?;
- let code = arguments.get("code").and_then(Value::as_str)
+ let code = arguments
+ .get("code")
+ .and_then(Value::as_str)
.ok_or_else(|| "code 必须是 JavaScript 函数体".to_string())?;
cocos_editor_bridge::validate_execute_code(code).map_err(|error| error.to_string())?;
Ok::<_, String>(code.to_string())
})();
let code = match prepared {
Ok(code) => code,
- Err(error) => return bridge_tool_result(
- redact_agent_runtime_error(&state.root, &error, 480), Vec::new(), true,
- ),
+ Err(error) => {
+ return bridge_tool_result(
+ redact_agent_runtime_error(&state.root, &error, 480),
+ Vec::new(),
+ true,
+ )
+ }
};
let mut uncertain = state.cocos_execute_uncertain.lock().await;
if *uncertain {
- return bridge_tool_result(json!({
- "status": "needs-reconciliation", "retryAllowed": false,
- "message": "先前 Cocos execute 结果待核对,当前 bridge 不再发送执行命令"
- }).to_string(), Vec::new(), true);
+ return bridge_tool_result(
+ json!({
+ "status": "needs-reconciliation", "retryAllowed": false,
+ "message": "先前 Cocos execute 结果待核对,当前 bridge 不再发送执行命令"
+ })
+ .to_string(),
+ Vec::new(),
+ true,
+ );
}
let root = state.root.clone();
let result = tokio::task::spawn_blocking(move || {
let _lock = acquire_project_write_lock(&root, "direct-cocos.execute")
.map_err(cocos_editor_bridge::BridgeError::InvalidInput)?;
cocos_editor_bridge::execute_cocos_editor_code_for_project(
- root.to_string_lossy().as_ref(), &code, cocos_editor_bridge::DEFAULT_COMMAND_TIMEOUT_MS,
+ root.to_string_lossy().as_ref(),
+ &code,
+ cocos_editor_bridge::DEFAULT_COMMAND_TIMEOUT_MS,
)
- }).await;
+ })
+ .await;
match result {
Ok(Ok(response)) => {
let is_error = !response.ok;
@@ -2338,24 +2359,40 @@ async fn bridge_cocos_execute(state: &DirectToolBridgeState, arguments: &Value)
"requestId": response.request_id,
"result": response.result,
"error": response.error,
- }).to_string();
- bridge_tool_result(redact_agent_runtime_project_paths(&state.root, &text, 32_000), Vec::new(), is_error)
+ })
+ .to_string();
+ bridge_tool_result(
+ redact_agent_runtime_project_paths(&state.root, &text, 32_000),
+ Vec::new(),
+ is_error,
+ )
}
failed => {
let (is_uncertain, error) = match failed {
Ok(Err(error)) => (
- matches!(&error, cocos_editor_bridge::BridgeError::ExecutionUncertain(_)),
+ matches!(
+ &error,
+ cocos_editor_bridge::BridgeError::ExecutionUncertain(_)
+ ),
error.to_string(),
),
- Err(_) => (true, "Cocos execute worker 退出,执行结果需要核对".to_string()),
+ Err(_) => (
+ true,
+ "Cocos execute worker 退出,执行结果需要核对".to_string(),
+ ),
Ok(Ok(_)) => unreachable!(),
};
*uncertain = is_uncertain;
- bridge_tool_result(json!({
- "status": if is_uncertain { "needs-reconciliation" } else { "failed" },
- "retryAllowed": !is_uncertain,
- "message": redact_agent_runtime_error(&state.root, &error, 480),
- }).to_string(), Vec::new(), true)
+ bridge_tool_result(
+ json!({
+ "status": if is_uncertain { "needs-reconciliation" } else { "failed" },
+ "retryAllowed": !is_uncertain,
+ "message": redact_agent_runtime_error(&state.root, &error, 480),
+ })
+ .to_string(),
+ Vec::new(),
+ true,
+ )
}
}
}
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 9db357363..edf69c1b5 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
@@ -432,16 +432,18 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
];
let mut tools = tools;
#[cfg(all(windows, feature = "cocos-editor-execute"))]
- tools.push(json!({
- "name": "agc_cocos_execute",
- "description": "在当前项目已连接的 Cocos Creator 主进程执行 JavaScript 函数体,支持 await 和 return。宿主绑定项目和目标进程,只提交 code。结果待核对或超时后禁止自动重发;使用 Editor.Message 调用 Creator API。",
- "inputSchema": {
- "type": "object",
- "properties": { "code": { "type": "string", "minLength": 1, "maxLength": cocos_editor_bridge::MAX_EXECUTE_CODE_BYTES } },
- "required": ["code"],
- "additionalProperties": false
- }
- }));
+ if crate::builtin_plugins::cocos_editor_agent_tool_available() {
+ tools.push(json!({
+ "name": "agc_cocos_execute",
+ "description": "在当前项目已连接的 Cocos Creator 主进程执行 JavaScript 函数体,支持 await 和 return。宿主绑定项目和目标进程,只提交 code。结果待核对或超时后禁止自动重发;使用 Editor.Message 调用 Creator API。",
+ "inputSchema": {
+ "type": "object",
+ "properties": { "code": { "type": "string", "minLength": 1, "maxLength": cocos_editor_bridge::MAX_EXECUTE_CODE_BYTES } },
+ "required": ["code"],
+ "additionalProperties": false
+ }
+ }));
+ }
if controlled_web_search {
tools.push(json!({
"name": "agc_web_search",
@@ -517,7 +519,9 @@ fn validate_write_file_arguments(arguments: &Value) -> Result<(), String> {
#[cfg(all(windows, feature = "cocos-editor-execute"))]
async fn call_agc_cocos_execute(arguments: &Value) -> Value {
let validated = validate_tool_object_fields(arguments, &["code"]).and_then(|()| {
- let code = arguments.get("code").and_then(Value::as_str)
+ let code = arguments
+ .get("code")
+ .and_then(Value::as_str)
.ok_or_else(|| "code 必须是 JavaScript 函数体".to_string())?;
cocos_editor_bridge::validate_execute_code(code).map_err(|error| error.to_string())
});
@@ -1896,7 +1900,9 @@ mod tests {
"agc_browser_playtest",
]
.into_iter()
- .chain(cfg!(all(windows, feature = "cocos-editor-execute")).then_some("agc_cocos_execute"))
+ .chain(
+ cfg!(all(windows, feature = "cocos-editor-execute")).then_some("agc_cocos_execute")
+ )
.collect::>()
);
let serialized = specs.to_string();
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs
index 25fbc0bbc..49e1a9da9 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs
@@ -17,7 +17,7 @@ mod canvas_asset_kind_contract_tests {
}
pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
- let tools = vec![
+ let mut tools = vec![
GAME_CREATOR_USER_INPUT_REQUEST_TOOL,
"memory.read",
"memory.write",
@@ -64,10 +64,12 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
"agent.action_history",
"agent.run_status",
];
+ // 内置插件被用户禁用后,对应 Runtime 工具不再进入工具目录、Agent 上下文
+ // 和工具策略快照。
+ if crate::builtin_plugins::cocos_editor_agent_tool_available() {
+ tools.push(crate::builtin_plugins::AGC_COCOS_EDITOR_TOOL_NAME);
+ }
tools
- .into_iter()
- .chain(cfg!(feature = "cocos-editor-execute").then_some("cocos.editor.execute"))
- .collect()
}
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs
index 6028ea788..41235d21e 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools.rs
@@ -1,8 +1,8 @@
use super::*;
mod action_history;
-mod command_ops;
mod cocos_editor;
+mod command_ops;
mod context;
mod delegation;
mod delivery;
@@ -21,8 +21,8 @@ mod task_ops;
mod ui_workflow;
pub(in crate::agent) use action_history::*;
-pub(in crate::agent) use command_ops::*;
pub(in crate::agent) use cocos_editor::*;
+pub(in crate::agent) use command_ops::*;
pub(in crate::agent) use context::*;
pub(in crate::agent) use delegation::*;
pub(in crate::agent) use delivery::*;
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs
index 17dce95af..dc2528401 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs
@@ -251,8 +251,17 @@ fn build_agent_runtime_native_capability_registry() -> Result Result<&'static CapabilityRegistry, String>
{
- static REGISTRY: OnceLock, String>> = OnceLock::new();
- REGISTRY
+ // 内置插件开关会改变工具目录,因此按“可用 / 不可用”各缓存一份:切换后立即
+ // 生效,又不需要每次调用都重建 registry。
+ static ENABLED_REGISTRY: OnceLock, String>> = OnceLock::new();
+ static DISABLED_REGISTRY: OnceLock, String>> =
+ OnceLock::new();
+ let cache = if crate::builtin_plugins::cocos_editor_agent_tool_available() {
+ &ENABLED_REGISTRY
+ } else {
+ &DISABLED_REGISTRY
+ };
+ cache
.get_or_init(build_agent_runtime_native_capability_registry)
.as_ref()
.map_err(Clone::clone)
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
new file mode 100644
index 000000000..2107b78c1
--- /dev/null
+++ b/apps/ai-game-creator-shell/src-tauri/src/builtin_plugins.rs
@@ -0,0 +1,321 @@
+//! 内置插件登记表与可用开关。
+//!
+//! 内置插件随 `plugins/` 工作区随包分发,用户不能卸载,只能控制是否可用。
+//! 开关状态持久化在 AppData `extensions/builtin-plugins.json`,同时被两处消费:
+//! 插件宿主(禁用后不能启动,状态显示为 disabled)和 Agent 工具目录(禁用后
+//! 不出现在工具列表与 Agent 上下文里)。
+
+use std::collections::BTreeMap;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::{Mutex, OnceLock};
+
+use serde::{Deserialize, Serialize};
+
+/// Cocos Creator 编辑器插件的插件 id,与 `plugins/agc-cocos-editor/plugin.json` 一致。
+pub(crate) const AGC_COCOS_EDITOR_PLUGIN_ID: &str = "agc-cocos-editor";
+
+/// 该插件在 Agent 侧对应的 Runtime 工具名。
+pub(crate) const AGC_COCOS_EDITOR_TOOL_NAME: &str = "cocos.editor.execute";
+
+const STATE_FILE_NAME: &str = "builtin-plugins.json";
+const STATE_SCHEMA_VERSION: &str = "agc.builtin-plugins.v1";
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(crate) enum BuiltinPlugin {
+ CocosEditor,
+}
+
+impl BuiltinPlugin {
+ pub(crate) fn id(self) -> &'static str {
+ match self {
+ Self::CocosEditor => AGC_COCOS_EDITOR_PLUGIN_ID,
+ }
+ }
+
+ /// 未持久化任何开关时的默认状态。
+ fn default_enabled(self) -> bool {
+ match self {
+ Self::CocosEditor => true,
+ }
+ }
+
+ /// 该插件是否向 Agent 暴露 Runtime 工具。
+ fn exposes_agent_tools(self) -> bool {
+ match self {
+ Self::CocosEditor => true,
+ }
+ }
+}
+
+pub(crate) const BUILTIN_PLUGINS: &[BuiltinPlugin] = &[BuiltinPlugin::CocosEditor];
+
+pub(crate) fn builtin_plugin(id: &str) -> Option {
+ BUILTIN_PLUGINS
+ .iter()
+ .copied()
+ .find(|plugin| plugin.id() == id.trim())
+}
+
+pub(crate) fn is_builtin(id: &str) -> bool {
+ builtin_plugin(id).is_some()
+}
+
+#[derive(Debug, Default)]
+struct BuiltinPluginState {
+ path: Option,
+ enabled: BTreeMap,
+ /// 开关文件不可读或格式不受支持时,内置插件全部按不可用处理。
+ fail_closed: bool,
+}
+
+static STATE: OnceLock> = OnceLock::new();
+
+fn state() -> &'static Mutex {
+ STATE.get_or_init(|| Mutex::new(BuiltinPluginState::default()))
+}
+
+#[derive(Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct BuiltinPluginStateFile {
+ #[serde(default)]
+ schema_version: Option,
+ #[serde(default)]
+ enabled: BTreeMap,
+}
+
+/// 读取 AppData 里的开关状态;文件缺失按默认状态处理,坏文件失败关闭。
+pub(crate) fn initialize(config_dir: &Path) -> Result<(), String> {
+ let root = config_dir.join("extensions");
+ let path = root.join(STATE_FILE_NAME);
+ if let Err(error) = fs::create_dir_all(&root) {
+ mark_fail_closed(Some(path));
+ return Err(format!("准备内置插件目录失败:{error}"));
+ }
+ let loaded = match fs::read(&path) {
+ Ok(bytes) => match serde_json::from_slice::(&bytes) {
+ Ok(file) if file.schema_version.as_deref() == Some(STATE_SCHEMA_VERSION) => file,
+ Ok(_) => {
+ mark_fail_closed(Some(path));
+ return Err(format!(
+ "内置插件开关文件版本不受支持,需要 {STATE_SCHEMA_VERSION}"
+ ));
+ }
+ Err(error) => {
+ mark_fail_closed(Some(path));
+ return Err(format!("内置插件开关文件无效:{error}"));
+ }
+ },
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
+ BuiltinPluginStateFile::default()
+ }
+ Err(error) => {
+ mark_fail_closed(Some(path));
+ return Err(format!("读取内置插件开关失败:{error}"));
+ }
+ };
+ let mut guard = state()
+ .lock()
+ .map_err(|_| "内置插件状态锁已损坏".to_string())?;
+ guard.path = Some(path);
+ guard.fail_closed = false;
+ // 只接受登记表里的 id,避免坏文件把未知对象带进运行时。
+ guard.enabled = loaded
+ .enabled
+ .into_iter()
+ .filter(|(id, _)| is_builtin(id))
+ .collect();
+ Ok(())
+}
+
+fn mark_fail_closed(path: Option) {
+ if let Ok(mut guard) = state().lock() {
+ guard.path = path;
+ guard.fail_closed = true;
+ guard.enabled = BUILTIN_PLUGINS
+ .iter()
+ .map(|plugin| (plugin.id().to_string(), false))
+ .collect();
+ }
+}
+
+pub(crate) fn is_enabled(id: &str) -> bool {
+ let Some(plugin) = builtin_plugin(id) else {
+ return false;
+ };
+ state()
+ .lock()
+ .map(|guard| {
+ if guard.fail_closed {
+ return false;
+ }
+ guard
+ .enabled
+ .get(plugin.id())
+ .copied()
+ .unwrap_or_else(|| plugin.default_enabled())
+ })
+ // 状态锁损坏时同样 fail-closed,避免异常状态重新放开内置能力。
+ .unwrap_or(false)
+}
+
+/// 内置插件的用户开关;`None` 表示该 id 不是内置插件,由来源自己决定启用状态。
+pub(crate) fn toggle_state(id: &str) -> Option {
+ builtin_plugin(id).map(|_| is_enabled(id))
+}
+
+pub(crate) fn set_enabled(id: &str, enabled: bool) -> Result {
+ let Some(plugin) = builtin_plugin(id) else {
+ return Err(format!("{id} 不是内置插件,不能使用内置插件开关"));
+ };
+ let mut guard = state()
+ .lock()
+ .map_err(|_| "内置插件状态锁已损坏".to_string())?;
+ let previous = guard.enabled.get(plugin.id()).copied();
+ guard.enabled.insert(plugin.id().to_string(), enabled);
+ if let Err(error) = persist(&guard) {
+ match previous {
+ Some(value) => {
+ guard.enabled.insert(plugin.id().to_string(), value);
+ }
+ None => {
+ guard.enabled.remove(plugin.id());
+ }
+ }
+ return Err(error);
+ }
+ Ok(enabled)
+}
+
+fn persist(guard: &BuiltinPluginState) -> Result<(), String> {
+ let path = guard
+ .path
+ .clone()
+ .ok_or_else(|| "内置插件开关尚未初始化".to_string())?;
+ let file = BuiltinPluginStateFile {
+ schema_version: Some(STATE_SCHEMA_VERSION.to_string()),
+ enabled: guard.enabled.clone(),
+ };
+ let bytes = serde_json::to_vec_pretty(&file)
+ .map_err(|error| format!("序列化内置插件开关失败:{error}"))?;
+ let temporary = path.with_extension("json.tmp");
+ fs::write(&temporary, bytes).map_err(|error| format!("写入内置插件开关失败:{error}"))?;
+ fs::rename(&temporary, &path).map_err(|error| format!("提交内置插件开关失败:{error}"))?;
+ Ok(())
+}
+
+/// Agent 工具面是否可用:编译期 feature 打开且用户没有禁用该内置插件。
+pub(crate) fn agent_tool_available(plugin: BuiltinPlugin) -> bool {
+ plugin.exposes_agent_tools()
+ && cfg!(feature = "cocos-editor-execute")
+ && is_enabled(plugin.id())
+}
+
+pub(crate) fn cocos_editor_agent_tool_available() -> bool {
+ agent_tool_available(BuiltinPlugin::CocosEditor)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tempfile::tempdir;
+
+ fn test_lock() -> std::sync::MutexGuard<'static, ()> {
+ static LOCK: OnceLock> = OnceLock::new();
+ LOCK.get_or_init(|| Mutex::new(()))
+ .lock()
+ .unwrap_or_else(|error| error.into_inner())
+ }
+
+ #[test]
+ fn builtin_plugins_default_to_enabled_and_reject_unknown_ids() {
+ let _guard = test_lock();
+ let directory = tempdir().expect("temp config");
+ initialize(directory.path()).expect("initialize");
+ assert!(is_builtin(AGC_COCOS_EDITOR_PLUGIN_ID));
+ assert!(is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
+ assert_eq!(toggle_state(AGC_COCOS_EDITOR_PLUGIN_ID), Some(true));
+ assert_eq!(toggle_state("imported-plugin"), None);
+ assert!(!is_enabled("imported-plugin"));
+ assert!(set_enabled("imported-plugin", false).is_err());
+ }
+
+ #[test]
+ fn toggle_state_round_trips_through_appdata_file() {
+ let _guard = test_lock();
+ let directory = tempdir().expect("temp config");
+ initialize(directory.path()).expect("initialize");
+ assert_eq!(
+ set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, false).expect("disable"),
+ false
+ );
+ let path = directory.path().join("extensions").join(STATE_FILE_NAME);
+ let written = fs::read_to_string(&path).expect("state file");
+ assert!(written.contains(STATE_SCHEMA_VERSION));
+ assert!(written.contains(AGC_COCOS_EDITOR_PLUGIN_ID));
+
+ // 重新初始化模拟下次启动读取持久化结果。
+ initialize(directory.path()).expect("re-initialize");
+ assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
+ set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).expect("enable");
+ assert!(is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
+ }
+
+ #[test]
+ fn corrupt_state_file_fails_closed() {
+ let _guard = test_lock();
+ let directory = tempdir().expect("temp config");
+ let root = directory.path().join("extensions");
+ fs::create_dir_all(&root).expect("extensions dir");
+ fs::write(root.join(STATE_FILE_NAME), "{ not json").expect("write corrupt state");
+ assert!(initialize(directory.path()).is_err());
+ assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
+ assert_eq!(toggle_state(AGC_COCOS_EDITOR_PLUGIN_ID), Some(false));
+ }
+
+ #[test]
+ fn unsupported_schema_fails_closed() {
+ let _guard = test_lock();
+ let directory = tempdir().expect("temp config");
+ let root = directory.path().join("extensions");
+ fs::create_dir_all(&root).expect("extensions dir");
+ fs::write(
+ root.join(STATE_FILE_NAME),
+ serde_json::json!({"schemaVersion": "agc.builtin-plugins.v0", "enabled": {}})
+ .to_string(),
+ )
+ .expect("write unsupported state");
+ assert!(initialize(directory.path()).is_err());
+ assert!(!is_enabled(AGC_COCOS_EDITOR_PLUGIN_ID));
+ }
+
+ #[test]
+ fn agent_tool_visibility_follows_the_toggle() {
+ let _guard = test_lock();
+ let directory = tempdir().expect("temp config");
+ initialize(directory.path()).expect("initialize");
+ let tool_visible_when_enabled = cfg!(feature = "cocos-editor-execute");
+
+ set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).expect("enable");
+ assert_eq!(
+ cocos_editor_agent_tool_available(),
+ tool_visible_when_enabled
+ );
+ assert_eq!(
+ crate::agent::agent_runtime_executable_tools().contains(&AGC_COCOS_EDITOR_TOOL_NAME),
+ tool_visible_when_enabled
+ );
+
+ set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, false).expect("disable");
+ assert!(!cocos_editor_agent_tool_available());
+ assert!(
+ !crate::agent::agent_runtime_executable_tools().contains(&AGC_COCOS_EDITOR_TOOL_NAME)
+ );
+
+ set_enabled(AGC_COCOS_EDITOR_PLUGIN_ID, true).expect("re-enable");
+ assert_eq!(
+ crate::agent::agent_runtime_executable_tools().contains(&AGC_COCOS_EDITOR_TOOL_NAME),
+ tool_visible_when_enabled
+ );
+ }
+}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/cocos_editor.rs b/apps/ai-game-creator-shell/src-tauri/src/cocos_editor.rs
deleted file mode 100644
index 48cca6ab8..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/cocos_editor.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-#[cfg(feature = "cocos-editor-injection")]
-use cocos_editor_bridge::CocosEditorInjectionRequest;
-use cocos_editor_bridge::{
- CocosEditorCommandResponse, CocosEditorInjectionResult, CocosEditorProcess,
-};
-use serde::Deserialize;
-#[cfg(feature = "cocos-editor-injection")]
-use tauri::Manager;
-#[cfg(feature = "cocos-editor-injection")]
-const BUNDLED_COCOS_BRIDGE_PAYLOAD: &str = "cocos-editor-bridge/cocos-editor-bridge.dll";
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields, rename_all = "camelCase")]
-pub(crate) struct CocosEditorTargetRequest {
- process_id: u32,
- project_path: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields, rename_all = "camelCase")]
-pub(crate) struct CocosEditorCommandInput {
- process_id: u32,
- project_path: String,
- #[serde(default = "default_command_timeout_ms")]
- timeout_ms: u32,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields, rename_all = "camelCase")]
-pub(crate) struct CocosEditorInjectionInput {
- process_id: u32,
- project_path: String,
- #[serde(default = "default_injection_timeout_ms")]
- timeout_ms: u32,
-}
-
-fn default_command_timeout_ms() -> u32 {
- cocos_editor_bridge::DEFAULT_COMMAND_TIMEOUT_MS
-}
-
-fn default_injection_timeout_ms() -> u32 {
- cocos_editor_bridge::DEFAULT_INJECTION_TIMEOUT_MS
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields, rename_all = "camelCase")]
-pub(crate) struct CocosEditorExecuteInput {
- process_id: u32,
- project_path: String,
- code: String,
- #[serde(default = "default_command_timeout_ms")]
- timeout_ms: u32,
-}
-
-#[tauri::command]
-pub(crate) fn prepare_cocos_editor_injection(
- request: CocosEditorTargetRequest,
-) -> Result {
- #[cfg(feature = "cocos-editor")]
- {
- return cocos_editor_bridge::validate_injection_target(
- request.process_id,
- &request.project_path,
- )
- .map_err(|error| error.to_string());
- }
- #[cfg(not(feature = "cocos-editor"))]
- {
- let _ = (request.process_id, request.project_path);
- Err("Cocos Editor bridge feature 未启用".to_string())
- }
-}
-
-#[tauri::command]
-pub(crate) fn ping_cocos_editor(
- request: CocosEditorCommandInput,
-) -> Result {
- #[cfg(feature = "cocos-editor-execute")]
- {
- return cocos_editor_bridge::ping_cocos_editor(
- request.process_id,
- &request.project_path,
- request.timeout_ms,
- )
- .map_err(|error| error.to_string());
- }
- #[cfg(not(feature = "cocos-editor-execute"))]
- {
- let _ = (request.process_id, request.project_path, request.timeout_ms);
- Err("Cocos Editor execute feature 未启用".to_string())
- }
-}
-
-#[tauri::command]
-pub(crate) fn status_cocos_editor(
- request: CocosEditorCommandInput,
-) -> Result {
- #[cfg(feature = "cocos-editor-execute")]
- {
- return cocos_editor_bridge::status_cocos_editor(
- request.process_id,
- &request.project_path,
- request.timeout_ms,
- )
- .map_err(|error| error.to_string());
- }
- #[cfg(not(feature = "cocos-editor-execute"))]
- {
- let _ = (request.process_id, request.project_path, request.timeout_ms);
- Err("Cocos Editor execute feature 未启用".to_string())
- }
-}
-
-#[tauri::command]
-pub(crate) fn execute_cocos_editor_code(
- request: CocosEditorExecuteInput,
-) -> Result {
- #[cfg(feature = "cocos-editor-execute")]
- {
- return cocos_editor_bridge::execute_cocos_editor_code(
- request.process_id,
- &request.project_path,
- &request.code,
- request.timeout_ms,
- )
- .map_err(|error| error.to_string());
- }
- #[cfg(not(feature = "cocos-editor-execute"))]
- {
- let _ = (
- request.process_id,
- request.project_path,
- request.code,
- request.timeout_ms,
- );
- Err("Cocos Editor execute feature 未启用".to_string())
- }
-}
-
-#[tauri::command]
-pub(crate) fn inject_cocos_editor(
- app: tauri::AppHandle,
- input: CocosEditorInjectionInput,
-) -> Result {
- #[cfg(feature = "cocos-editor-injection")]
- {
- let payload = app
- .path()
- .resource_dir()
- .map_err(|error| format!("解析 AGC 资源目录失败:{error}"))?
- .join(BUNDLED_COCOS_BRIDGE_PAYLOAD);
- if !payload.is_file() {
- return Err(format!(
- "AGC 未随包提供 Cocos bridge payload:{}",
- BUNDLED_COCOS_BRIDGE_PAYLOAD
- ));
- }
- let request = CocosEditorInjectionRequest {
- process_id: input.process_id,
- project_path: input.project_path,
- bridge_dll_path: payload.to_string_lossy().into_owned(),
- timeout_ms: input.timeout_ms,
- };
- return cocos_editor_bridge::inject_bridge_dll(&request).map_err(|error| error.to_string());
- }
- #[cfg(not(feature = "cocos-editor-injection"))]
- {
- let _ = app;
- let _ = (input.process_id, input.project_path, input.timeout_ms);
- Err("Cocos Editor injection feature 未启用;当前构建只支持目标预检".to_string())
- }
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/editor_adapter/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/editor_adapter/mod.rs
index 108099f8f..c3c384ada 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/editor_adapter/mod.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/editor_adapter/mod.rs
@@ -1,36 +1,8 @@
//! Editor adapters used by the generic AGC plugin host.
//!
//! The host owns plugin lifecycle, RPC, permissions and auditing. Adapters
-//! only know how to find and talk to a particular editor.
+//! only know how to find and talk to a particular editor, and ship inside the
+//! plugin package they belong to under the `plugins/` workspace. This module
+//! only re-exports the shared contract so the host stays editor-agnostic.
-use std::path::Path;
-
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub(crate) struct EditorConnectionInfo {
- pub(crate) adapter: String,
- pub(crate) connected: bool,
- pub(crate) pid: Option,
- pub(crate) project_path: Option,
- pub(crate) version: Option,
-}
-
-/// Narrow seam between the generic plugin runtime and a target editor.
-pub(crate) trait EditorAdapter: Send + Sync {
- fn id(&self) -> &'static str;
- fn detect(&self, project_path: &Path) -> Result;
- fn connect(
- &mut self,
- pid: u32,
- project_path: &Path,
- version: &str,
- ) -> Result;
- fn disconnect(&mut self);
- fn translate_rpc(&self, method: &str, params: Value) -> Result;
- fn rpc(&self, _method: &str, _params: Value) -> Result {
- Err("编辑器原生连接尚未建立".to_string())
- }
-}
+pub(crate) use editor_adapter_api::{EditorAdapter, EditorConnectionInfo};
diff --git a/apps/ai-game-creator-shell/src-tauri/src/editor_adapters.rs b/apps/ai-game-creator-shell/src-tauri/src/editor_adapters.rs
new file mode 100644
index 000000000..ba2949e8b
--- /dev/null
+++ b/apps/ai-game-creator-shell/src-tauri/src/editor_adapters.rs
@@ -0,0 +1,54 @@
+//! 已链接的编辑器适配器目录。
+//!
+//! 编辑器专属实现随 `plugins/` 工作区里的插件包分发;其中 native 适配器模块
+//! 目前由宿主在编译期链接(Cargo path 依赖),再按插件 manifest 的 `adapter`
+//! 字段注册到通用插件宿主。宿主只认适配器 id,不包含目标编辑器知识。
+
+#[cfg(feature = "cocos-editor")]
+use std::path::PathBuf;
+
+#[cfg(feature = "cocos-editor")]
+use tauri::Manager;
+
+use crate::plugin_host::PluginHost;
+
+/// 随包 payload 相对资源根目录的位置,与 `tauri.windows.conf.json` 的资源映射保持一致。
+#[allow(dead_code)]
+pub(crate) const COCOS_BRIDGE_PAYLOAD_RELATIVE: &str =
+ "plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll";
+
+pub(crate) fn register_linked_editor_adapters(
+ app: &tauri::AppHandle,
+ host: &PluginHost,
+) -> Result<(), String> {
+ #[cfg(feature = "cocos-editor")]
+ {
+ let adapter =
+ cocos_editor_bridge::CocosEditorAdapter::new(cocos_bridge_payload_candidates(app));
+ host.register_editor_adapter(Box::new(adapter))?;
+ }
+ #[cfg(not(feature = "cocos-editor"))]
+ {
+ let _ = (app, host);
+ }
+ Ok(())
+}
+
+#[cfg(feature = "cocos-editor")]
+fn cocos_bridge_payload_candidates(app: &tauri::AppHandle) -> Vec {
+ let mut candidates = Vec::new();
+ if let Ok(resource_dir) = app.path().resource_dir() {
+ candidates.push(resource_dir.join(COCOS_BRIDGE_PAYLOAD_RELATIVE));
+ }
+ // 开发构建还要能直接从 plugins/ 工作区读取尚未打包的 payload。
+ #[cfg(debug_assertions)]
+ {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ candidates.push(
+ manifest_dir
+ .join("../../../plugins/agc-cocos-editor/native/payload")
+ .join("cocos-editor-bridge.dll"),
+ );
+ }
+ candidates
+}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs
index bcb776d7d..699a042af 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/main.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs
@@ -244,6 +244,7 @@ mod agent;
mod agent_native_tools;
mod assets;
mod browser;
+mod builtin_plugins;
mod cli;
mod client_extensions;
mod collaboration;
@@ -251,7 +252,6 @@ mod command_exec;
mod command_output;
mod command_sandbox;
mod command_sandbox_trampoline;
-mod cocos_editor;
mod commands;
mod config;
mod context_compaction;
@@ -260,6 +260,7 @@ mod context_menu;
mod debug;
mod delegation;
mod editor_adapter;
+mod editor_adapters;
pub mod error_report;
mod git_inspect;
mod goal;
@@ -294,7 +295,6 @@ use collaboration::*;
use command_exec::*;
use command_output::*;
use command_sandbox::*;
-use cocos_editor::*;
use commands::*;
use config::*;
use context_compaction::*;
@@ -308,8 +308,8 @@ use patchset::*;
use platform_session::*;
use plugin_host::{
call_agc_plugin, list_agc_extensions, list_agc_plugins, read_agc_plugin_panel,
- refresh_agc_plugins, reload_agc_plugin, set_agc_plugin_project_path, start_agc_plugin,
- stop_agc_plugin, PluginHost,
+ refresh_agc_plugins, reload_agc_plugin, set_agc_plugin_enabled, set_agc_plugin_project_path,
+ start_agc_plugin, stop_agc_plugin, PluginHost,
};
use preview::*;
use process_session::*;
@@ -2504,9 +2504,23 @@ fn main() {
setup_log.fail("startup.appdata.resolve.failed details=config-dir-uninitialized");
error
})?;
+ if let Err(error) = builtin_plugins::initialize(&config_dir) {
+ app_log!("startup.builtin-plugins.initialize.failed: {error}");
+ }
if let Err(error) = app.state::().initialize(&config_dir) {
app_log!("startup.plugin-host.initialize.failed: {error}");
}
+ if let Some(workspace) = plugin_host::resolve_plugin_workspace(app.handle()) {
+ if let Err(error) = app.state::().set_plugin_workspace(workspace) {
+ app_log!("startup.plugin-host.workspace.failed: {error}");
+ }
+ }
+ if let Err(error) = editor_adapters::register_linked_editor_adapters(
+ app.handle(),
+ app.state::().inner(),
+ ) {
+ app_log!("startup.plugin-host.adapter.failed: {error}");
+ }
load_platform_session_fixture_from_env(&config_dir).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
@@ -2589,6 +2603,7 @@ fn main() {
start_agc_plugin,
stop_agc_plugin,
reload_agc_plugin,
+ set_agc_plugin_enabled,
call_agc_plugin,
read_agc_plugin_panel,
set_agc_plugin_project_path,
@@ -2731,11 +2746,6 @@ fn main() {
report_client_error,
get_pending_error_reports,
ack_error_reports,
- prepare_cocos_editor_injection,
- ping_cocos_editor,
- status_cocos_editor,
- execute_cocos_editor_code,
- inject_cocos_editor,
])
.build(tauri_context);
let app = match app {
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 b49833ae2..451a61ddb 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
@@ -121,6 +121,8 @@ pub(crate) struct PluginSummary {
pub(crate) version: String,
pub(crate) api_version: String,
pub(crate) enabled: bool,
+ /// 内置插件随包分发,不能卸载,只能通过可用开关启停。
+ pub(crate) builtin: bool,
pub(crate) has_runtime: bool,
pub(crate) status: String,
pub(crate) adapter: Option,
@@ -140,6 +142,7 @@ pub(crate) struct AgcExtensionSummary {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) enabled: bool,
+ pub(crate) builtin: bool,
pub(crate) status: String,
pub(crate) plugin: Option,
pub(crate) client_extension: Option,
@@ -206,6 +209,7 @@ struct PluginRecord {
#[derive(Default)]
struct PluginHostState {
root: Option,
+ workspace: Option,
plugins: BTreeMap,
active_project: ProjectContext,
editors: EditorRegistry,
@@ -461,6 +465,84 @@ fn plugin_root(config_dir: &Path) -> Result {
Ok(root)
}
+/// 解析插件工作区目录:环境变量优先,其次随包资源目录 `plugins/`,
+/// 开发构建再回退仓库里的 `plugins/` 工作区。
+pub(crate) fn resolve_plugin_workspace(app: &tauri::AppHandle) -> Option {
+ if let Some(workspace) = std::env::var_os("AGC_PLUGIN_WORKSPACE") {
+ let workspace = PathBuf::from(workspace);
+ if workspace.is_dir() {
+ return Some(workspace);
+ }
+ }
+ if let Ok(resource_dir) = app.path().resource_dir() {
+ let bundled = resource_dir.join("plugins");
+ if bundled.is_dir() {
+ return Some(bundled);
+ }
+ }
+ #[cfg(debug_assertions)]
+ {
+ let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..");
+ let workspace = repo_root.join("plugins");
+ if workspace.is_dir() {
+ return Some(workspace);
+ }
+ }
+ None
+}
+
+/// 统一后的插件来源,屏蔽“AppData 导入”和“plugins/ 工作区”的差别。
+#[derive(Clone, Debug)]
+struct ScannedPluginSource {
+ id: String,
+ name: String,
+ original_name: String,
+ /// `Some` 表示启用状态由来源索引决定;`None` 表示沿用插件 manifest 声明。
+ enabled: Option,
+ root: PathBuf,
+}
+
+impl ScannedPluginSource {
+ fn from_imported(source: crate::client_extensions::ClientPluginSource) -> Self {
+ Self {
+ id: source.item.id,
+ name: source.item.name,
+ original_name: source.item.original_name,
+ enabled: Some(source.item.enabled),
+ root: source.root,
+ }
+ }
+}
+
+/// 扫描 `plugins/` 工作区:每个含根目录 `plugin.json` 的子目录是一个插件包。
+///
+/// 工作区插件随包分发:内置插件按用户可用开关决定启用状态,其它工作区插件
+/// 沿用 manifest 声明。
+fn workspace_plugin_sources(root: &Path) -> Result, String> {
+ let entries = match fs::read_dir(root) {
+ Ok(entries) => entries,
+ Err(error) => return Err(format!("读取插件工作区失败:{error}")),
+ };
+ let mut sources = Vec::new();
+ for entry in entries.flatten() {
+ let plugin_root = entry.path();
+ if !plugin_root.is_dir() || !plugin_root.join(PLUGIN_MANIFEST_FILE_NAME).is_file() {
+ continue;
+ }
+ let id = entry.file_name().to_string_lossy().into_owned();
+ let enabled = crate::builtin_plugins::toggle_state(&id);
+ sources.push(ScannedPluginSource {
+ id: id.clone(),
+ name: id.clone(),
+ original_name: id,
+ enabled,
+ root: plugin_root,
+ });
+ }
+ sources.sort_by(|left, right| left.id.cmp(&right.id));
+ Ok(sources)
+}
+
fn audit_path(root: &Path) -> PathBuf {
root.join(AUDIT_FILE_NAME)
}
@@ -691,19 +773,68 @@ impl PluginHost {
.lock()
.map_err(|_| "插件宿主锁已损坏".to_string())?;
state.root = Some(root.clone());
+ if state.workspace.is_none() {
+ if let Some(workspace) = std::env::var_os("AGC_PLUGIN_WORKSPACE") {
+ let workspace = PathBuf::from(workspace);
+ if workspace.is_dir() {
+ state.workspace = Some(workspace);
+ }
+ }
+ }
+ self.scan_locked(&mut state, &root)
+ }
+
+ /// 注册 `plugins/` 工作区目录,让随包插件无需 AppData 导入即可被发现。
+ pub(crate) fn set_plugin_workspace(&self, workspace: PathBuf) -> Result<(), String> {
+ if !workspace.is_dir() {
+ return Err("插件工作区必须是目录".to_string());
+ }
+ let mut state = self
+ .state
+ .lock()
+ .map_err(|_| "插件宿主锁已损坏".to_string())?;
+ state.workspace = Some(workspace);
+ let root = state
+ .root
+ .clone()
+ .ok_or_else(|| "插件宿主尚未初始化".to_string())?;
self.scan_locked(&mut state, &root)
}
fn scan_locked(&self, state: &mut PluginHostState, root: &Path) -> Result<(), String> {
- let sources = crate::client_extensions::client_plugin_sources_at(root)?;
+ let workspace_sources = match state.workspace.clone() {
+ Some(workspace) => workspace_plugin_sources(&workspace)?,
+ None => Vec::new(),
+ };
+ // 内置插件优先:随包插件不能被 AppData 同名导入覆盖,也不能被卸载。
+ let mut sources = workspace_sources
+ .iter()
+ .filter(|source| crate::builtin_plugins::is_builtin(&source.id))
+ .cloned()
+ .collect::>();
+ for source in crate::client_extensions::client_plugin_sources_at(root)?
+ .into_iter()
+ .map(ScannedPluginSource::from_imported)
+ {
+ if !sources.iter().any(|existing| existing.id == source.id) {
+ sources.push(source);
+ }
+ }
+ for source in workspace_sources {
+ if !sources.iter().any(|existing| existing.id == source.id) {
+ sources.push(source);
+ }
+ }
let mut discovered = BTreeMap::new();
for source in sources {
- let id = source.item.id;
+ let id = source.id.clone();
match read_plugin_manifest(&source.root) {
Ok(mut manifest) => {
- manifest.enabled = source.item.enabled;
- if source.item.name != source.item.original_name {
- manifest.name = source.item.name;
+ if let Some(enabled) = source.enabled {
+ manifest.enabled = enabled;
+ }
+ if source.name != source.original_name {
+ manifest.name = source.name.clone();
}
let existing = state
.plugins
@@ -758,7 +889,7 @@ impl PluginHost {
id: id.clone(),
manifest: PluginManifest {
id,
- name: source.item.name,
+ name: source.name,
version: "0".to_string(),
api_version: PLUGIN_API_VERSION.to_string(),
entry: None,
@@ -824,6 +955,7 @@ impl PluginHost {
id: plugin.id.clone(),
name: plugin.name.clone(),
enabled: plugin.enabled,
+ builtin: plugin.builtin,
status: plugin.status.clone(),
plugin: Some(plugin),
client_extension: None,
@@ -848,6 +980,7 @@ impl PluginHost {
id: extension.id.clone(),
name: extension.name.clone(),
enabled: extension.enabled,
+ builtin: false,
status: extension.status.clone(),
plugin: None,
client_extension: Some(extension),
@@ -933,6 +1066,25 @@ impl PluginHost {
self.start(id)
}
+ /// 内置插件的可用开关:禁用时先停进程,再持久化状态并重新扫描。
+ ///
+ /// 该状态同时被 Agent 工具目录消费,禁用后插件不能启动,对应 Runtime 工具
+ /// 也不再出现在工具列表与 Agent 上下文里。
+ pub(crate) fn set_enabled(
+ &self,
+ id: &str,
+ enabled: bool,
+ ) -> Result, String> {
+ if !crate::builtin_plugins::is_builtin(id) {
+ return Err("只有内置插件可以使用可用开关;导入扩展请使用扩展启用状态".to_string());
+ }
+ if !enabled {
+ let _ = self.stop(id);
+ }
+ crate::builtin_plugins::set_enabled(id, enabled)?;
+ self.refresh()
+ }
+
pub(crate) fn read_panel(
&self,
id: &str,
@@ -1273,7 +1425,12 @@ impl PluginHost {
id.clone(),
event_type.to_string(),
)?;
- Ok(json!({"subscriptionId": id}))
+ let project_path = active_project
+ .lock()
+ .map_err(|_| "项目上下文锁已损坏".to_string())?
+ .clone()
+ .map(|path| path.to_string_lossy().into_owned());
+ Ok(json!({"subscriptionId": id, "projectPath": project_path}))
}
"host.events.unsubscribe" => {
let id = params
@@ -1353,6 +1510,7 @@ impl PluginHost {
version: record.manifest.version.clone(),
api_version: record.manifest.api_version.clone(),
enabled: record.manifest.enabled,
+ builtin: crate::builtin_plugins::is_builtin(&record.id),
has_runtime: record.manifest.entry.is_some(),
status: record.status.clone(),
adapter: record.manifest.adapter.clone(),
@@ -1395,7 +1553,19 @@ impl PluginHost {
if subscribed {
let _ = write_rpc_shared(
&running.stdin,
- &json!({"jsonrpc":"2.0", "method":"host.event", "params":{"type":"project.changed"}}),
+ &json!({
+ "jsonrpc":"2.0",
+ "method":"host.event",
+ "params":{
+ "type":"project.changed",
+ "payload":{"projectPath": state
+ .active_project
+ .lock()
+ .map_err(|_| "项目上下文锁已损坏".to_string())?
+ .as_ref()
+ .map(|path| path.to_string_lossy().into_owned())},
+ },
+ }),
);
}
}
@@ -1542,6 +1712,15 @@ pub(crate) fn reload_agc_plugin(
host.reload(id.trim())
}
+#[tauri::command]
+pub(crate) fn set_agc_plugin_enabled(
+ id: String,
+ enabled: bool,
+ host: State<'_, PluginHost>,
+) -> Result, String> {
+ host.set_enabled(id.trim(), enabled)
+}
+
#[tauri::command]
pub(crate) async fn call_agc_plugin(
id: String,
@@ -1651,6 +1830,57 @@ mod tests {
assert_eq!(list[0].id, id);
}
+ fn write_workspace_plugin(workspace: &Path, name: &str, enabled: bool) {
+ let plugin = workspace.join(name);
+ fs::create_dir_all(&plugin).expect("plugin directory");
+ fs::write(
+ plugin.join("plugin.json"),
+ json!({
+ "$schema": AGENT_PLUGINS_SCHEMA,
+ "name": name,
+ "version": "1.0.0",
+ "extensions": {
+ "world.genarrative.agc": {
+ "entry": "index.js",
+ "permissions": ["ui.register"],
+ "enabled": enabled
+ }
+ }
+ })
+ .to_string(),
+ )
+ .expect("manifest");
+ fs::write(plugin.join("index.js"), "process.stdin.resume();").expect("entry");
+ }
+
+ #[test]
+ fn scans_plugins_workspace_and_honors_manifest_enabled_flag() {
+ let directory = tempdir().expect("temp config");
+ let workspace = directory.path().join("workspace");
+ write_workspace_plugin(&workspace, "sample-plugin", true);
+ write_workspace_plugin(&workspace, "disabled-plugin", false);
+ let host = PluginHost::default();
+ host.initialize(directory.path()).expect("initialize");
+ assert!(host.list().expect("list before workspace").is_empty());
+ host.set_plugin_workspace(workspace.clone())
+ .expect("set workspace");
+ let list = host.list().expect("list after workspace");
+ assert_eq!(list.len(), 2);
+ let enabled = list
+ .iter()
+ .find(|plugin| plugin.id == "sample-plugin")
+ .expect("enabled plugin");
+ assert!(enabled.enabled);
+ assert_eq!(enabled.status, "stopped");
+ assert_eq!(enabled.adapter, None);
+ let disabled = list
+ .iter()
+ .find(|plugin| plugin.id == "disabled-plugin")
+ .expect("disabled plugin");
+ assert!(!disabled.enabled);
+ assert_eq!(disabled.status, "disabled");
+ }
+
#[test]
fn denies_ungranted_host_registration() {
let directory = tempdir().expect("temp config");
@@ -1742,4 +1972,126 @@ setTimeout(() => send({ jsonrpc: '2.0', id: 1, method: 'host.registerCommand', p
let mut reader = std::io::Cursor::new(vec![b'a'; MAX_RPC_BYTES + 1]);
assert!(read_bounded_rpc_line(&mut reader).is_err());
}
+
+ struct StubCocosAdapter;
+
+ impl EditorAdapter for StubCocosAdapter {
+ fn id(&self) -> &'static str {
+ "cocos-editor"
+ }
+
+ fn detect(&self, _project_path: &Path) -> Result {
+ Err("stub adapter 不探测进程".to_string())
+ }
+
+ fn connect(
+ &mut self,
+ _pid: u32,
+ _project_path: &Path,
+ _version: &str,
+ ) -> Result {
+ Err("stub adapter 不建立连接".to_string())
+ }
+
+ fn disconnect(&mut self) {}
+
+ fn translate_rpc(&self, _method: &str, params: Value) -> Result {
+ Ok(params)
+ }
+
+ fn rpc(&self, method: &str, params: Value) -> Result {
+ // 与 native 适配器的 CocosEditorCommandResponse 同形,供插件入口判断 status。
+ Ok(json!({"ok": true, "method": method, "params": params}))
+ }
+ }
+
+ #[test]
+ fn builtin_plugin_toggle_controls_availability() {
+ let directory = tempdir().expect("temp config");
+ 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");
+
+ let summary = |list: Vec| {
+ list.into_iter()
+ .find(|plugin| plugin.id == "agc-cocos-editor")
+ .expect("built-in plugin")
+ };
+ let enabled = summary(host.list().expect("list"));
+ assert!(enabled.builtin);
+ assert!(enabled.enabled);
+
+ let disabled = summary(
+ host.set_enabled("agc-cocos-editor", false)
+ .expect("disable built-in plugin"),
+ );
+ assert!(!disabled.enabled);
+ assert_eq!(disabled.status, "disabled");
+ assert!(host.start("agc-cocos-editor").is_err());
+ assert!(host
+ .set_enabled("imported-extension", false)
+ .expect_err("imported extensions use the extension index")
+ .contains("只有内置插件"));
+
+ let re_enabled = summary(
+ host.set_enabled("agc-cocos-editor", true)
+ .expect("enable built-in plugin"),
+ );
+ assert!(re_enabled.enabled);
+ assert_eq!(re_enabled.status, "stopped");
+ }
+
+ #[test]
+ fn workspace_cocos_plugin_round_trips_editor_rpc() {
+ let directory = tempdir().expect("temp config");
+ 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 plugins workspace");
+ host.register_editor_adapter(Box::new(StubCocosAdapter))
+ .expect("register adapter");
+ let project = fs::canonicalize(directory.path())
+ .expect("canonical project")
+ .to_string_lossy()
+ .into_owned();
+ host.set_active_project(Some(project.clone()))
+ .expect("set active project");
+ host.start("agc-cocos-editor").expect("start plugin");
+ let deadline = std::time::Instant::now() + Duration::from_secs(15);
+ loop {
+ let registered = host.list().expect("list").into_iter().any(|plugin| {
+ plugin.id == "agc-cocos-editor"
+ && plugin.commands.len() == 1
+ && plugin.capabilities.len() == 1
+ && plugin.panels.len() == 1
+ });
+ if registered {
+ break;
+ }
+ assert!(
+ std::time::Instant::now() < deadline,
+ "Cocos 插件未在期限内完成注册"
+ );
+ thread::sleep(Duration::from_millis(25));
+ }
+ let response = host
+ .call(
+ "agc-cocos-editor",
+ "cocos.editor.execute".to_string(),
+ json!({"code": "return 1 + 1;"}),
+ )
+ .expect("cocos execute rpc");
+ assert_eq!(response["status"], "completed");
+ assert_eq!(response["response"]["method"], "editor.execute");
+ assert_eq!(response["response"]["params"]["projectPath"], project);
+ assert_eq!(response["response"]["params"]["code"], "return 1 + 1;");
+ assert_eq!(
+ host.stop("agc-cocos-editor").expect("stop plugin").status,
+ "stopped"
+ );
+ }
}
diff --git a/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json b/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json
index f1b354361..30c71cc2e 100644
--- a/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json
+++ b/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json
@@ -12,7 +12,7 @@
"resources/codex/win-x64/codex-package.json": "codex/win-x64/codex-package.json",
"resources/codex/win-x64/NOTICE.md": "codex/win-x64/NOTICE.md",
"resources/codex/win-x64/manifest.json": "codex/win-x64/manifest.json",
- "resources/cocos-editor-bridge": "cocos-editor-bridge"
+ "resources/plugins": "plugins"
}
}
}
diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts
index 24c2686c0..bd67ab570 100644
--- a/apps/ai-game-creator-shell/src/app/types.ts
+++ b/apps/ai-game-creator-shell/src/app/types.ts
@@ -105,6 +105,8 @@ export type AgcPluginSummary = {
version: string;
apiVersion: string;
enabled: boolean;
+ /** 内置插件随包分发、不能卸载,只能通过可用开关控制。 */
+ builtin: boolean;
hasRuntime: boolean;
status: AgcPluginStatus;
adapter: string | null;
@@ -120,6 +122,7 @@ export type AgcExtensionSummary = {
id: string;
name: string;
enabled: boolean;
+ builtin: boolean;
status: string;
plugin: AgcPluginSummary | null;
clientExtension: ClientExtensionItem | null;
diff --git a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx
index 529803b4d..4a733b7d2 100644
--- a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx
+++ b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx
@@ -37,6 +37,7 @@ import { checkForAppUpdate } from '../../services/appUpdate';
import {
listAgcExtensions,
reloadAgcPlugin,
+ setAgcPluginEnabled,
setAgcPluginProjectPath,
startAgcPlugin,
stopAgcPlugin,
@@ -346,6 +347,27 @@ export function RuntimeConfigDialog({
}
}
+ /** 内置插件没有卸载入口,只用可用开关控制是否对客户端和 Agent 生效。 */
+ async function setBuiltinPluginEnabled(
+ plugin: AgcPluginSummary,
+ enabled: boolean,
+ ) {
+ if (agcPluginsBusy) return;
+ setAgcPluginsBusy(true);
+ try {
+ const plugins = await setAgcPluginEnabled(plugin.id, enabled);
+ setAgcPlugins(plugins);
+ await readClientExtensions();
+ setAgcPluginsStatus(enabled ? '内置插件已启用' : '内置插件已禁用');
+ } catch (error) {
+ setAgcPluginsStatus(
+ error instanceof Error ? error.message : String(error),
+ );
+ } finally {
+ setAgcPluginsBusy(false);
+ }
+ }
+
async function readClientExtensions() {
const invoke = resolveTauriInvoke();
if (!invoke) {
@@ -940,93 +962,40 @@ export function RuntimeConfigDialog({
{clientExtensionsStatus}
) : null}
- {clientExtensionsLoadState === 'loading' ? (
-
- 正在加载扩展
- 正在读取已导入的 Skill 和 MCP。
-
- ) : clientExtensionsLoadState === 'error' ? (
-
- 扩展列表加载失败
-
- {clientExtensionsStatus || '暂时无法读取扩展列表。'}
-
-
- ) : clientExtensions.length > 0 ? (
+ {agcPlugins.some((plugin) => plugin.builtin) ? (
- {clientExtensions.map((item) => {
- const editing = editingExtensionId === item.id;
- const plugin = agcPlugins.find(
- (plugin) => plugin.id === item.id,
- );
- const typeLabel =
- item.extensionType === 'plugin'
- ? 'Plugin'
- : item.extensionType === 'skill'
- ? 'Skill'
- : item.extensionType === 'mcp'
- ? 'MCP'
- : '未识别';
- const statusLabel =
- item.status === 'enabled'
- ? '已启用'
- : item.status === 'disabled'
- ? '已禁用'
- : item.status === 'startup-failed'
- ? '启动失败'
- : '当前不可用';
- return (
+ {agcPlugins
+ .filter((plugin) => plugin.builtin)
+ .map((plugin) => (
- {editing ? (
-
- setEditingExtensionName(
- event.currentTarget.value,
- )
- }
- onKeyDown={(event) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- event.stopPropagation();
- void saveClientExtensionName(item);
- } else if (event.key === 'Escape') {
- event.stopPropagation();
- cancelRenameClientExtension();
- }
- }}
- />
- ) : (
- {item.name}
- )}
-
- {typeLabel} · 来自 {item.sourceName}
-
- {item.lastError ? (
-
- {item.lastError}
+ {plugin.name}
+ 内置 Plugin · 不可卸载
+ {plugin.lastError ? (
+
+ {plugin.lastError}
) : null}
- {plugin?.status === 'running'
- ? '运行中'
- : statusLabel}
+ {!plugin.enabled
+ ? '已禁用'
+ : plugin.status === 'running'
+ ? '运行中'
+ : plugin.status === 'invalid'
+ ? '当前不可用'
+ : '已启用'}
- {plugin?.enabled && plugin.hasRuntime ? (
+ {plugin.enabled && plugin.hasRuntime ? (
<>
>
) : null}
- {plugin?.status === 'running'
+ {plugin.status === 'running'
? plugin.panels.map((panel) => (
- );
- })}
+ ))}
+
+ ) : null}
+ {clientExtensionsLoadState === 'loading' ? (
+
+ 正在加载扩展
+ 正在读取已导入的 Skill 和 MCP。
+
+ ) : clientExtensionsLoadState === 'error' ? (
+
+ 扩展列表加载失败
+
+ {clientExtensionsStatus || '暂时无法读取扩展列表。'}
+
+
+ ) : clientExtensions.length > 0 ? (
+
+ {clientExtensions
+ .filter(
+ (item) =>
+ !(
+ item.extensionType === 'plugin' &&
+ agcPlugins.some(
+ (plugin) =>
+ plugin.builtin && plugin.id === item.id,
+ )
+ ),
+ )
+ .map((item) => {
+ const editing = editingExtensionId === item.id;
+ const plugin = agcPlugins.find(
+ (plugin) => plugin.id === item.id,
+ );
+ const typeLabel =
+ item.extensionType === 'plugin'
+ ? 'Plugin'
+ : item.extensionType === 'skill'
+ ? 'Skill'
+ : item.extensionType === 'mcp'
+ ? 'MCP'
+ : '未识别';
+ const statusLabel =
+ item.status === 'enabled'
+ ? '已启用'
+ : item.status === 'disabled'
+ ? '已禁用'
+ : item.status === 'startup-failed'
+ ? '启动失败'
+ : '当前不可用';
+ return (
+
+
+ {editing ? (
+
+ setEditingExtensionName(
+ event.currentTarget.value,
+ )
+ }
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ event.stopPropagation();
+ void saveClientExtensionName(item);
+ } else if (event.key === 'Escape') {
+ event.stopPropagation();
+ cancelRenameClientExtension();
+ }
+ }}
+ />
+ ) : (
+ {item.name}
+ )}
+
+ {typeLabel} · 来自 {item.sourceName}
+
+ {item.lastError ? (
+
+ {item.lastError}
+
+ ) : null}
+
+
+
+ {plugin?.status === 'running'
+ ? '运行中'
+ : statusLabel}
+
+ {plugin?.enabled && plugin.hasRuntime ? (
+ <>
+
+ void toggleAgcPlugin(plugin)
+ }
+ >
+ {plugin.status === 'running'
+ ? '停止'
+ : '启动'}
+
+
void reloadPlugin(plugin)}
+ >
+ 重载
+
+ >
+ ) : null}
+ {plugin?.status === 'running'
+ ? plugin.panels.map((panel) => (
+
+ setMountedPluginPanel({
+ pluginId: plugin.id,
+ panel,
+ })
+ }
+ >
+ {panel.title}
+
+ ))
+ : null}
+ {editing ? (
+ <>
+
+ void saveClientExtensionName(item)
+ }
+ >
+ 保存
+
+
+ 取消
+
+ >
+ ) : (
+
+ beginRenameClientExtension(item)
+ }
+ >
+
+
+ )}
+ {item.extensionType === 'unknown' ? null : (
+
+ void setClientExtensionEnabled(
+ item,
+ !item.enabled,
+ )
+ }
+ >
+ {item.enabled ? '禁用' : '启用'}
+
+ )}
+
+ void removeClientExtension(item)
+ }
+ >
+
+
+
+
+ );
+ })}
) : (
diff --git a/apps/ai-game-creator-shell/src/services/pluginHost.ts b/apps/ai-game-creator-shell/src/services/pluginHost.ts
index 22a5f61ca..3ca0f9bf7 100644
--- a/apps/ai-game-creator-shell/src/services/pluginHost.ts
+++ b/apps/ai-game-creator-shell/src/services/pluginHost.ts
@@ -45,6 +45,14 @@ export async function reloadAgcPlugin(id: string) {
}) as Promise
;
}
+/** 内置插件的可用开关;禁用后不能启动,对应 Agent 工具也不再出现。 */
+export async function setAgcPluginEnabled(id: string, enabled: boolean) {
+ return invokeOrThrow()('set_agc_plugin_enabled', {
+ id,
+ enabled,
+ }) as Promise;
+}
+
export async function callAgcPlugin(
id: string,
method: string,
diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md
index e2073640d..f52faf5b0 100644
--- a/docs/project-memory/shared-memory/decision-log.md
+++ b/docs/project-memory/shared-memory/decision-log.md
@@ -8211,3 +8211,19 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 注入仅加载随 AGC 资源目录提供的 DLL,结果先标记 `injected-unverified`,必须由 payload 完成握手后才可开放有限 Cocos 操作;Runtime 的 execute 代码有界并受确认策略保护,不开放未受限 eval 或项目扩展自动写入。
- Runtime 第一阶段只广告 `cocos.editor.execute`,代码长度有界、默认走确认策略,项目根和目标 PID 不交给模型;`ping/status` 先作为宿主命令保留,不扩大全局 Agent 工具面。
- DirectProject 的 `agc_tools` 对应入口是 `agc_cocos_execute`,同样只接收 code,并沿用当前项目权限。2026-09-10 已通过临时真实 Creator 3.8.8 验证 Node 的 Windows 调试 handler 激活 Inspector、注入 bootstrap、pipe execute 及关闭 Inspector 后继续执行;此路线尚未替换当前 native DLL 源码。现有 `RequestInterrupt` 回调不能调用 JavaScript,不能把 DLL 加载和窗口线程钩子当作可用握手。执行发送后的未知结果禁止自动重放,Direct bridge 会阻断后续 execute。详细步骤、版本/fuse 和端口边界见 Cocos bridge 技术方案。
+
+## 2026-09-10 Cocos 直连模块改为插件包与独立插件工作区
+
+- 决策:Cocos 直连模块从 AGC 源码树移入插件包 `plugins/agc-cocos-editor`:`plugin.json` 使用 Agent Plugins 清单加 `extensions.world.genarrative.agc`,`src/entry.mjs` 作为 Runtime 入口注册 `cocos.editor.execute` 命令、`cocos.editor.connection` 能力和 `cocos-editor` 面板,native 模块 `native/cocos-editor-bridge` 实现通用 `EditorAdapter`。
+- 决策:新增 `plugins/` 工作区(npm workspace 成员)作为插件唯一存放位置;宿主解析顺序为 `AGC_PLUGIN_WORKSPACE`、随包 `/plugins`、开发构建的仓库 `plugins/`。工作区插件按自身 manifest 声明启用状态,AppData 同名导入插件优先。
+- 决策:通用适配器契约抽到 `server-rs/crates/editor-adapter-api`,宿主 `editor_adapter` 只做 re-export,`editor_adapters.rs` 负责编译期链接注册;AGC 删除 Cocos 专属 Tauri 命令和 `src/cocos_editor.rs`,编辑器操作统一走 `host.rpc` 到 `EditorAdapter::rpc`。
+- 边界:native 适配器当前仍由宿主编译期链接(Cargo path 依赖),动态加载插件 native 模块不在本次范围;Runtime 的 `cocos.editor.execute` 工具与 DirectProject 的 `agc_cocos_execute` 继续使用同一 native 实现,共享项目锁与“结果不确定禁止重放”语义。
+- 验证:插件包 `node --test` 与 native crate 单元测试、宿主工作区扫描与 manifest 启用状态测试、AGC typecheck、`check:npm-workspaces`、编码检查分别执行;真实 Creator 注入验收仍按 Cocos 桥接方案单独执行。
+
+## 2026-09-10 内置插件不可卸载与可用开关
+
+- 决策:`plugins/` 工作区里的插件按内置插件处理,随客户端分发、不能卸载或删除;同名 AppData 导入插件不覆盖内置定义。内置插件在 `PluginSummary` / `AgcExtensionSummary` 里带 `builtin`,前端只显示可用开关。
+- 决策:唯一开关入口为 `set_agc_plugin_enabled`,只接受登记过的内置插件 id,状态持久化到 AppData `extensions/builtin-plugins.json`(`schemaVersion = agc.builtin-plugins.v1`);文件缺失按 manifest `enabled` 处理,坏文件失败关闭。
+- 决策:禁用时先停止运行中的插件进程并让 `start_agc_plugin` 失败;同时把对应 Runtime 工具从 `agent_runtime_executable_tools()` 移除,使其不再进入工具策略快照、原生函数目录和系统提示词工具目录,DirectProject 的 `agc_tools` 规格与 bridge 执行入口同步拒绝。启用后立即恢复,不需要重启客户端。
+- 边界:导入扩展的启用状态仍走既有 `set_client_extension_enabled` 和扩展索引,不并入内置插件开关文件;内置插件开关不改变 manifest、权限或审计协议。
+- 验证:`builtin_plugins` 单测覆盖默认值、持久化往返、坏文件失败关闭和“禁用后工具目录不再出现该工具”;`plugin_host` 单测覆盖禁用后不能启动、导入 id 被拒绝、启用后回到 stopped。
diff --git a/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md b/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md
index 91bf4179a..e29ae9e2e 100644
--- a/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md
+++ b/docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md
@@ -2,7 +2,7 @@
## 目标
-目标是在用户已打开 Cocos Creator 项目时,由 AGC 识别正确的 Creator 主进程,并在进程内注入随包 JavaScript bootstrap;用户不需要在 Cocos 项目中手动安装扩展。桥接核心独立于 Tauri,位于 `server-rs/crates/cocos-editor-bridge`。2026-09-10 已验证通过 Node 自带的运行中 Inspector 激活入口完成引导,具体见本文“Inspector 注入调研”;这条路径尚未替换当前 crate 的 DLL 实现。
+目标是在用户已打开 Cocos Creator 项目时,由 AGC 识别正确的 Creator 主进程,并在进程内注入随包 JavaScript bootstrap;用户不需要在 Cocos 项目中手动安装扩展。桥接核心独立于 Tauri,随 Cocos 插件包分发,位于 `plugins/agc-cocos-editor/native/cocos-editor-bridge`。2026-09-10 已验证通过 Node 自带的运行中 Inspector 激活入口完成引导,具体见本文“Inspector 注入调研”;这条路径尚未替换当前 crate 的 DLL 实现。
## 边界
@@ -20,7 +20,7 @@ Cocos Creator 3.x 是 Electron/Node 编辑器,不能复用 Unity Mono 的 Core
crate 默认不启用任何宿主集成:
```toml
-cocos-editor-bridge = { path = ".../server-rs/crates/cocos-editor-bridge", default-features = false, features = ["process-discovery"] }
+cocos-editor-bridge = { path = ".../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false, features = ["process-discovery"] }
```
- `process-discovery`:启用 Windows Creator 主进程发现;不加载 Windows 注入 API。
@@ -29,11 +29,28 @@ cocos-editor-bridge = { path = ".../server-rs/crates/cocos-editor-bridge", defau
AGC 或其它桌面宿主应将 `windows-injection` 作为单独的发行构建开关,服务端和非桌面构建保持 `default-features = false`。
-当前 AGC Tauri adapter 只暴露 `prepare_cocos_editor_injection`(确认前预检)、`inject_cocos_editor`(确认后注入)、`ping_cocos_editor`、`status_cocos_editor` 和 `execute_cocos_editor_code`。Runtime 只广告一个 `cocos.editor.execute` 工具,代码输入使用当前项目根,目标 PID 由 crate 内部唯一匹配;默认命令权限为 confirm,具体运行档沿用已有 Runtime 策略。进程发现留在 crate 内部作为目标校验步骤,不建立客户端扫描服务或独立发现入口。默认 AGC 构建不启用 Cocos 集成;桌面构建需显式传 `--features cocos-editor`,命令执行需传 `--features cocos-editor-execute`,注入构建再传 `--features cocos-editor-injection`。注入命令不接收 DLL 路径,只加载资源目录中的 `cocos-editor-bridge/cocos-editor-bridge.dll`,避免把 Tauri command 变成任意 DLL 注入器。
+AGC 不再内置 Cocos 专属 Tauri 命令。适配器 `cocos-editor` 由插件包 `plugins/agc-cocos-editor` 提供,实现通用 `EditorAdapter`(`prepare` / `inject` / `ping` / `status` / `execute` / `detect` / `connect` / `disconnect`),由宿主按 manifest 的 `adapter` 字段注册;插件入口通过 `host.rpc` 触发这些操作,宿主校验 `editor.rpc` 权限后路由到 native 模块。
+
+Runtime 只广告一个 `cocos.editor.execute` 工具,代码输入使用当前项目根,目标 PID 由 crate 内部唯一匹配;默认命令权限为 confirm,具体运行档沿用已有 Runtime 策略。进程发现留在 crate 内部作为目标校验步骤,不建立客户端扫描服务或独立发现入口。默认 AGC 构建不启用 Cocos 集成;桌面构建需显式传 `--features cocos-editor`,命令执行需传 `--features cocos-editor-execute`,注入构建再传 `--features cocos-editor-injection`。注入只加载插件包 payload 目录里的 `cocos-editor-bridge.dll`(打包后位于 `/plugins/agc-cocos-editor/native/payload`),适配器拒绝任何其它路径,避免变成任意 DLL 注入器。
+
+## 插件包形态
+
+```text
+plugins/agc-cocos-editor/
+├─ plugin.json Agent Plugins 清单 + AGC Runtime 扩展(adapter=cocos-editor)
+├─ src/entry.mjs 运行时入口:注册命令 / 能力 / 面板,转发 host.rpc
+├─ src/cocos-editor-adapter.mjs 通用请求 → 编辑器请求的翻译与入参校验
+├─ panels/cocos-editor.html 自包含面板
+└─ native/cocos-editor-bridge/ native 模块:进程发现、pipe 协议、注入、EditorAdapter 实现
+```
+
+宿主按 `AGC_PLUGIN_WORKSPACE`、随包 `/plugins`、开发构建仓库 `plugins/` 的顺序解析工作区;插件包内的 `native/payload` 由构建脚本随包映射,生成的 DLL 不入库。插件协议、权限和面板挂载全部复用通用宿主,Cocos 专属逻辑只存在于本插件包:进程名与 `--project` 解析、Creator 版本校验、named pipe 协议和 Windows 注入。
+
+该插件是**内置插件**:随客户端分发、不能卸载,只能通过 `set_agc_plugin_enabled` 控制是否可用。禁用后插件进程停止且不能启动,Runtime 工具 `cocos.editor.execute` 与 DirectProject 的 `agc_cocos_execute` 同时从工具目录、工具策略快照和 Agent 上下文里消失;重新启用后立即恢复。开关状态保存在 AppData `extensions/builtin-plugins.json`。
## 第一阶段命令协议
-DirectProject 的现役 `agc_tools` 目录通过 Windows `cocos-editor-execute` feature 注册 `agc_cocos_execute`,参数只有 `code`。客户端在 blocking worker 内调用 crate,保留项目锁和现有项目权限;当前 bridge 出现执行结果不确定后拒绝后续 execute。旧 Runtime 的对应工具名为 `cocos.editor.execute`,继续使用它已有的 pending action、权限和恢复语义。
+DirectProject 的现役 `agc_tools` 目录通过 Windows `cocos-editor-execute` feature 注册 `agc_cocos_execute`,参数只有 `code`。客户端在 blocking worker 内调用插件 native 模块,保留项目锁和现有项目权限;当前 bridge 出现执行结果不确定后拒绝后续 execute。旧 Runtime 的对应工具名为 `cocos.editor.execute`,继续使用它已有的 pending action、权限和恢复语义;插件入口注册的同名命令走宿主 `host.rpc` → `EditorAdapter` 路径,两条路径共享同一 native 实现和不确定结果阻断语义。
注入 payload 在目标 Creator 主进程内监听 `\\.\pipe\genarrative-cocos-editor-{pid}`,使用换行分隔的 JSON。crate 只生成三种操作:
diff --git a/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md b/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md
index 8703c0fd7..80a3234ec 100644
--- a/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md
+++ b/docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md
@@ -11,8 +11,10 @@ AGC 插件系统由一个通用宿主和一个通用 SDK 组成。宿主统一
```text
apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs
apps/ai-game-creator-shell/src-tauri/src/editor_adapter/mod.rs
-apps/ai-game-creator-shell/src-tauri/src/editor_adapter/mod.rs
+apps/ai-game-creator-shell/src-tauri/src/editor_adapters.rs
packages/agc-plugin-sdk/src/index.ts
+server-rs/crates/editor-adapter-api/src/lib.rs
+plugins/agc-cocos-editor/ (第一个编辑器插件包)
```
现有 DirectProject 的 Skill/MCP 导入仍保留。它们是 Codex 扩展注入链路,不等同于本宿主管理的可运行 AGC Plugin。
@@ -59,6 +61,21 @@ OpenAI 的标准模型是“Plugin 作为可安装包,组合 Skills、可选 M
当前允许的权限为 `events.subscribe`、`project.read`、`editor.rpc`、`ui.register` 和 `capability.register`。未知权限、重复面板 id、非法入口和不支持的 API/适配器会使插件进入 `invalid` 状态,不启动进程。
+### plugins/ 工作区
+
+除 AppData 导入外,宿主还扫描 `plugins/` 工作区:每个含根目录 `plugin.json` 的一级子目录是一个插件包。解析顺序为环境变量 `AGC_PLUGIN_WORKSPACE`、随包资源目录 `/plugins`、开发构建的仓库 `plugins/`。仓库工作区约定见 [`plugins/README.md`](../../../plugins/README.md)。
+
+### 内置插件与可用开关
+
+`plugins/` 工作区里的插件是**内置插件**:随客户端分发,用户不能卸载或删除,只能通过可用开关控制是否生效。开关状态持久化在 AppData `extensions/builtin-plugins.json`(`schemaVersion = agc.builtin-plugins.v1`,`enabled` 是 id 到布尔的映射);文件缺失按插件 manifest 的 `enabled` 处理,坏文件失败关闭。内置插件优先级高于同名导入插件,AppData 里的同名 Plugin 不会覆盖或间接卸载它。
+
+开关同时驱动两处行为:
+
+1. 插件宿主:禁用时先停止运行中的插件进程,状态变为 `disabled`,`start_agc_plugin` 返回“插件已禁用”。内置插件在 `PluginSummary` / `AgcExtensionSummary` 里带 `builtin: true`,前端只显示可用开关,不显示重命名和卸载入口。
+2. Agent 工具面:禁用后对应 Runtime 工具从 `agent_runtime_executable_tools()` 里移除,因此不再进入工具策略快照(`autoTools` / `confirmTools` / `allowedTools`)、原生函数目录和系统提示词中的工具目录;DirectProject 的 `agc_tools` 规格同步移除,bridge 执行入口也会拒绝。启用后立即恢复,不需要重启客户端。
+
+唯一的开关入口是 Tauri 命令 `set_agc_plugin_enabled`,它只接受登记过的内置插件 id;导入扩展继续使用既有 `set_client_extension_enabled`。
+
## 运行和 RPC
宿主以已安装插件目录为 cwd 启动入口;JavaScript 入口使用系统 `node` 执行,其它入口直接执行。环境先清空,再保留 PATH、Windows 系统目录和临时目录等必要变量,并注入插件身份和协议版本;不继承客户端凭据。Windows 复用进程模块的 Job Object,Unix 使用独立进程组,停止/卸载时回收自有进程。
@@ -82,14 +99,18 @@ host.rpc(method, params)
## 编辑器适配器扩展点
-`EditorAdapter` 只定义 `detect`、`connect`、`disconnect`、`translate_rpc` 和原生 `rpc`。宿主只保存适配器 registry,并把插件声明的适配器名称路由到对应实现;具体编辑器如何查找进程、校验 PID/项目/版本、建立连接和翻译编辑器消息,由后续适配器包独立实现。
+`EditorAdapter` 契约位于通用 crate `server-rs/crates/editor-adapter-api`,只定义 `detect`、`connect`、`disconnect`、`translate_rpc` 和原生 `rpc`。宿主只保存适配器 registry,并把插件声明的适配器名称路由到对应实现;具体编辑器如何查找进程、校验 PID/项目/版本、建立连接和翻译编辑器消息,由插件包自带模块实现。
-本次不内置任何目标编辑器适配器,也不包含编辑器专属进程名、注入逻辑或 Tauri 命令。新增适配器不会改变 Plugin 生命周期、SDK 或权限协议。
+宿主源码不包含编辑器专属进程名、注入逻辑或 Tauri 命令。第一个适配器 `cocos-editor` 由 `plugins/agc-cocos-editor` 提供:native 模块实现 `EditorAdapter`,由 `editor_adapters.rs` 在启动时按编译期链接注册。新增适配器不会改变 Plugin 生命周期、SDK 或权限协议。
+
+当前 native 适配器仍由宿主在编译期链接(Cargo path 依赖);动态加载插件 native 模块不在本次范围,插件包格式与宿主协议不受此限制。
## Tauri 命令
`list_agc_extensions` 返回统一的 Plugin/Skill/MCP catalog;`list_agc_plugins`、`refresh_agc_plugins`、`start_agc_plugin`、`stop_agc_plugin`、`reload_agc_plugin`、`call_agc_plugin` 和 `read_agc_plugin_panel` 提供 Runtime Plugin 管理入口;`set_agc_plugin_project_path` 设置当前项目的受控上下文。编辑器适配器通过宿主 registry 和 Plugin RPC 使用,不增加编辑器专属 Tauri 命令。
+编辑器操作统一走 `host.rpc`:插件用 `extensions.world.genarrative.agc.adapter` 或显式 `adapter` 参数选择适配器,宿主校验 `editor.rpc` 权限后调用 `EditorAdapter::rpc`。项目上下文通过 `host.events.subscribe` 的响应和 `project.changed` 事件 payload 下发,插件不需要自己扫描目录。
+
每次启停、RPC 成功/失败和权限拒绝都追加到 AppData `extensions/audit.jsonl`,日志只写插件 id、动作、结果和固定错误摘要,不写 API Key、Cookie、Token 或宿主绝对路径。
## 验收门禁
@@ -101,6 +122,8 @@ OpenAI 官方 Plugins 文档将 Skills、MCP Server 和可选 UI 定义为同一
- Rust:manifest 路径/权限校验、目录扫描、权限拒绝和通用适配器 registry 边界单测;`cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`。
- 前端:`agc-plugin-sdk` TypeScript 编译、宿主服务类型检查,以及 `PluginPanelHost` 的挂载/卸载测试。
+- 内置插件开关:`builtin_plugins` 单测覆盖默认值、持久化往返、坏文件失败关闭,以及“禁用后工具目录里不再出现该工具”;`plugin_host` 单测覆盖禁用后不能启动、启用后回到 stopped。
+- 插件工作区:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml` 覆盖工作区扫描与 manifest 启用状态;`cargo test --manifest-path plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml` 覆盖 Cocos 适配器;`node --test plugins/agc-cocos-editor/src/entry.test.mjs` 覆盖插件入口协议与 manifest 一致性。
- 通用仓库门禁:`npm run check:encoding`、`git diff --check`;发布前仍需单独执行 AGC package smoke 和安装包 smoke。
-当前版本完成统一扩展 catalog、通用宿主、SDK、面板宿主和通用 EditorAdapter registry;目标编辑器适配器属于后续独立实现,不用未验证的连接状态替代真实编辑器验收。
+当前版本完成统一扩展 catalog、通用宿主、SDK、面板宿主、通用 EditorAdapter registry 和 `plugins/` 工作区;`agc-cocos-editor` 是第一个插件包。真实编辑器验收仍按 Cocos 方案文档单独执行,不用未验证的连接状态替代。
diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
index adc5b85cb..7a58b9c82 100644
--- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
+++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
@@ -183,7 +183,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
## 目标
-在 Genarrative 内建设独立桌面 App:普通用户通过项目开发工作台中的陶泥儿对话、资源画布、运行状态和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览;主窗口提供运行时配置入口,用于保存发布版 AppData / Tauri 配置目录里的 LLM 配置及受控开发者 External Editor 配置,设置弹窗同时提供独立“关于”页并显示从客户端构建版本注入的版本号。普通客户素材画布使用平台登录态调用内部编辑器 API,不展示或要求填写画板 Base URL / API Key。任务明细、原始文件、命令日志和专业 Agent 调试控制只通过显式开发调试入口查看,不随普通客户端启动额外打开窗口。v1 的生成闭环仍以 Web 小游戏为主,同时允许用户打开已有 Godot 项目:用户选择的目录始终作为工作区根,`.agent/`、Session、Runtime、文件工具和外围资料都留在该根;客户端检查根目录及一层直接子目录中的普通文件 `project.godot`,将唯一命中的实际目录以工作区相对 `godotProjectRoot` 记录到 manifest。Agent 使用标准运行档继续修改,不创建 `game/`、`assets/`、`memory/`、`exports/` 平行目录;本期不扩展 Unity、Godot 内嵌预览、云同步或插件市场。新增的 Cocos Creator bridge 核心独立为 `server-rs/crates/cocos-editor-bridge`,AGC 仅通过 feature 转发桌面进程发现、受控 execute 和 Windows 注入能力;它不改变服务端路线,也不把原始 pipe、句柄或未绑定项目身份的代码执行面暴露给 Agent。
+在 Genarrative 内建设独立桌面 App:普通用户通过项目开发工作台中的陶泥儿对话、资源画布、运行状态和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览;主窗口提供运行时配置入口,用于保存发布版 AppData / Tauri 配置目录里的 LLM 配置及受控开发者 External Editor 配置,设置弹窗同时提供独立“关于”页并显示从客户端构建版本注入的版本号。普通客户素材画布使用平台登录态调用内部编辑器 API,不展示或要求填写画板 Base URL / API Key。任务明细、原始文件、命令日志和专业 Agent 调试控制只通过显式开发调试入口查看,不随普通客户端启动额外打开窗口。v1 的生成闭环仍以 Web 小游戏为主,同时允许用户打开已有 Godot 项目:用户选择的目录始终作为工作区根,`.agent/`、Session、Runtime、文件工具和外围资料都留在该根;客户端检查根目录及一层直接子目录中的普通文件 `project.godot`,将唯一命中的实际目录以工作区相对 `godotProjectRoot` 记录到 manifest。Agent 使用标准运行档继续修改,不创建 `game/`、`assets/`、`memory/`、`exports/` 平行目录;本期不扩展 Unity、Godot 内嵌预览、云同步或插件市场。新增的 Cocos Creator bridge 核心独立为插件 `plugins/agc-cocos-editor`(native 模块位于其 `native/cocos-editor-bridge`),AGC 仅通过通用插件宿主和 feature 转发桌面进程发现、受控 execute 和 Windows 注入能力;它不改变服务端路线,也不把原始 pipe、句柄或未绑定项目身份的代码执行面暴露给 Agent。
## 技术选择
diff --git a/package-lock.json b/package-lock.json
index 428ad5c34..d81e19d56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"packages/image-canvas-react",
"packages/agc-plugin-sdk",
"packages/shared",
+ "plugins/agc-cocos-editor",
"tools/spine-json-export-validator"
],
"dependencies": {
@@ -5141,6 +5142,10 @@
"resolved": "apps/admin-web",
"link": true
},
+ "node_modules/@genarrative/agc-plugin-cocos-editor": {
+ "resolved": "plugins/agc-cocos-editor",
+ "link": true
+ },
"node_modules/@genarrative/agc-plugin-sdk": {
"resolved": "packages/agc-plugin-sdk",
"link": true
@@ -22927,6 +22932,13 @@
"react-dom": "^19.0.0"
}
},
+ "plugins/agc-cocos-editor": {
+ "name": "@genarrative/agc-plugin-cocos-editor",
+ "version": "0.1.0",
+ "dependencies": {
+ "@genarrative/agc-plugin-sdk": "0.1.0"
+ }
+ },
"tools/spine-json-export-validator": {
"name": "@genarrative/spine-json-export-validator",
"version": "0.1.0",
@@ -26302,6 +26314,12 @@
"vitest": "^0.34.6"
}
},
+ "@genarrative/agc-plugin-cocos-editor": {
+ "version": "file:plugins/agc-cocos-editor",
+ "requires": {
+ "@genarrative/agc-plugin-sdk": "0.1.0"
+ }
+ },
"@genarrative/agc-plugin-sdk": {
"version": "file:packages/agc-plugin-sdk"
},
diff --git a/package.json b/package.json
index d8617556f..f955cbef2 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"packages/image-canvas-react",
"packages/agc-plugin-sdk",
"packages/shared",
+ "plugins/agc-cocos-editor",
"tools/spine-json-export-validator"
],
"scripts": {
@@ -162,6 +163,9 @@
"agc:build": "npm --prefix apps/ai-game-creator-shell run build --",
"agc:skill-pack:check": "npm --prefix apps/ai-game-creator-shell run skill-pack:check",
"agc:skill-pack:sync": "npm --prefix apps/ai-game-creator-shell run skill-pack:sync",
+ "agc:plugins:test": "node --test plugins/agc-cocos-editor/src/entry.test.mjs",
+ "agc:plugins:native-test": "cargo test --manifest-path plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml",
+ "agc:plugins:check": "npm run agc:plugins:test && npm run agc:plugins:native-test",
"agc:check": "npm run ai-game-creator-shell:check",
"agc:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck",
"agc:test": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --",
diff --git a/plugins/README.md b/plugins/README.md
new file mode 100644
index 000000000..a36f86177
--- /dev/null
+++ b/plugins/README.md
@@ -0,0 +1,70 @@
+# AGC 插件工作区
+
+本目录是 AGC 插件的工作区,每个一级子目录是一个 **Agent Plugins 包**,随 AGC 客户端分发。
+
+```text
+plugins/
+└─ agc-cocos-editor/
+ ├─ plugin.json Agent Plugins 标准清单 + AGC Runtime 扩展
+ ├─ package.json npm workspace 成员,声明 SDK 版本契约
+ ├─ panels/ 自包含面板 HTML
+ ├─ src/ 运行时入口与适配器翻译
+ └─ native/ 插件自带 native 模块(Cargo 包)
+```
+
+## 清单格式
+
+`plugin.json` 使用 OpenAI Agent Plugins 标准 schema,AGC 专属字段放在
+`extensions.world.genarrative.agc`:
+
+```json
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "agc-cocos-editor",
+ "version": "0.1.0",
+ "extensions": {
+ "com.openai": {
+ "interface": { "displayName": "Cocos Creator 编辑器桥接" }
+ },
+ "world.genarrative.agc": {
+ "apiVersion": "v1",
+ "entry": "./src/entry.mjs",
+ "adapter": "cocos-editor",
+ "permissions": [
+ "events.subscribe",
+ "editor.rpc",
+ "ui.register",
+ "capability.register"
+ ],
+ "panels": []
+ }
+ }
+}
+```
+
+约束与通用宿主一致:插件目录内最多一个 Plugin;`entry` 必须是包内普通文件;
+未知权限、非法入口或不支持的 `apiVersion` 会让插件进入 `invalid` 状态,不启动进程。
+
+## 宿主如何加载
+
+宿主按以下顺序解析插件工作区,任一命中即生效:
+
+1. 环境变量 `AGC_PLUGIN_WORKSPACE`(本地联调与测试)。
+2. 随包资源目录 `/plugins`(安装包)。
+3. 开发构建的仓库 `plugins/` 目录。
+
+工作区插件是**内置插件**:随客户端分发、不能卸载,只能通过可用开关控制是否生效。
+开关状态保存在 AppData `extensions/builtin-plugins.json`,内置插件优先级高于同名
+导入插件,不会被 AppData 覆盖或删除。插件运行入口由宿主以插件目录为 cwd 启动
+(`.mjs` 用系统 `node`,其它入口直接执行),stdio 上使用一行一个 JSON-RPC 2.0 消息。
+
+## 新增插件
+
+1. 新建 `plugins//`,写 `plugin.json`,`name` 与目录名保持一致。
+2. 运行时插件提供 `src/entry.mjs`,按 `agc.plugin.v1` 协议注册命令、面板和能力;
+ 仅打包 Skill/MCP 的插件可以没有 `entry`,宿主会按 `package` 状态展示。
+3. 需要编辑器原生能力时,在 `native/` 下放插件自己的 Cargo 包,并实现
+ `editor-adapter-api` 的 `EditorAdapter`;`plugin.json` 的 `adapter` 字段必须与
+ `EditorAdapter::id()` 一致。
+4. 在根 `package.json`、`scripts/check-npm-workspaces.mjs` 登记 workspace 成员。
+5. 更新 `docs/technical/` 里的插件或编辑器方案文档。
diff --git a/plugins/agc-cocos-editor/README.md b/plugins/agc-cocos-editor/README.md
new file mode 100644
index 000000000..adb9df1f0
--- /dev/null
+++ b/plugins/agc-cocos-editor/README.md
@@ -0,0 +1,65 @@
+# agc-cocos-editor
+
+Cocos Creator 编辑器桥接插件。用户侧看到的是一个普通 AGC 插件:插件生命周期、UI、
+RPC、权限和能力注册全部由通用宿主负责,只有“如何连接 Cocos Creator”属于本插件。
+
+它同时是 AGC 的**内置插件**:随客户端分发、不能卸载。用户在运行时设置里只能切换
+“是否可用”,禁用后插件不能启动,`cocos.editor.execute` / `agc_cocos_execute` 也会
+从 Agent 工具列表、工具策略快照和上下文里消失;重新启用后立即恢复。
+
+```text
+plugin.json Agent Plugins 清单 + AGC Runtime 扩展
+src/entry.mjs 运行时入口(注册命令 / 能力 / 面板,转发 host.rpc)
+src/cocos-editor-adapter.mjs 通用请求 → Cocos 适配器请求的翻译与入参校验
+panels/cocos-editor.html 自包含面板
+native/cocos-editor-bridge/ 插件自带 native 模块(进程发现、pipe 协议、注入)
+```
+
+## Runtime 契约
+
+| 项 | 值 |
+| --------- | ---------------------------------------------------------------------------------- |
+| 协议 | `agc.plugin.v1`(stdio 行分隔 JSON-RPC 2.0) |
+| 适配器 id | `cocos-editor` |
+| 命令 | `cocos.editor.execute`(`{ code }`) |
+| 能力 | `cocos.editor.connection`(`{ operation, processId?, projectPath?, timeoutMs? }`) |
+| 面板 | `cocos-editor`(`panels/cocos-editor.html`,sidebar) |
+
+`operation` 取值为 `detect`、`connect`、`disconnect`、`prepare`、`ping`、`status`、
+`execute`、`inject`,与 native 适配器的 `COCOS_EDITOR_RPC_METHODS` 一一对应;
+`src/entry.test.mjs` 会校验两边不会漂移。
+
+## 项目上下文
+
+插件从宿主获得当前受控项目路径:
+
+- 注册 `host.events.subscribe { type: 'project.changed' }` 时,宿主在响应里返回当前
+ `projectPath`。
+- 之后宿主设置项目时推送 `project.changed` 事件,payload 带 `projectPath`。
+
+插件不缓存凭据、不扫描文件系统;所有编辑器操作都经 `host.rpc` 交给 native 适配器,
+由适配器做 PID / 项目 / 版本校验。
+
+## Native 模块
+
+`native/cocos-editor-bridge` 是从 AGC 服务端源码树移入本插件包的独立 crate,实现
+`editor-adapter-api::EditorAdapter`:
+
+- `process-discovery`:只把 `CocosCreator.exe` 主进程的 PID、`--project` 和 Creator
+ 版本绑定起来,排除 Electron 子进程。
+- `windows-transport`:注入后通过 named pipe 提供 `ping/status/execute`,执行结果
+ 不确定时返回 `ExecutionUncertain` 并禁止自动重放。
+- `windows-injection`:随包 DLL 的 Windows 注入实现,默认关闭。
+
+AGC 客户端当前在编译期链接本 crate(Cargo path 依赖),由通用宿主按 manifest 的
+`adapter` 字段注册;宿主源码里没有 Cocos 进程名、注入或 Editor.Message 逻辑。
+
+## 本地验证
+
+```bash
+cargo test --manifest-path plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml
+node --test plugins/agc-cocos-editor/src/entry.test.mjs
+```
+
+真实 Creator 验收(注入、Inspector 引导、非空场景读写)仍按
+`docs/technical/【技术方案】AGC Cocos Creator 编辑器桥接模块-2026-09-09.md` 单独执行。
diff --git a/server-rs/crates/cocos-editor-bridge/.gitignore b/plugins/agc-cocos-editor/native/cocos-editor-bridge/.gitignore
similarity index 100%
rename from server-rs/crates/cocos-editor-bridge/.gitignore
rename to plugins/agc-cocos-editor/native/cocos-editor-bridge/.gitignore
diff --git a/server-rs/crates/cocos-editor-bridge/Cargo.toml b/plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml
similarity index 93%
rename from server-rs/crates/cocos-editor-bridge/Cargo.toml
rename to plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml
index ea75061bf..e79717b14 100644
--- a/server-rs/crates/cocos-editor-bridge/Cargo.toml
+++ b/plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml
@@ -35,6 +35,7 @@ windows-injection = [
]
[dependencies]
+editor-adapter-api = { path = "../../../../server-rs/crates/editor-adapter-api" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = { version = "0.10", optional = true }
diff --git a/server-rs/crates/cocos-editor-bridge/build.rs b/plugins/agc-cocos-editor/native/cocos-editor-bridge/build.rs
similarity index 100%
rename from server-rs/crates/cocos-editor-bridge/build.rs
rename to plugins/agc-cocos-editor/native/cocos-editor-bridge/build.rs
diff --git a/server-rs/crates/cocos-editor-bridge/native/native_payload.cpp b/plugins/agc-cocos-editor/native/cocos-editor-bridge/native/native_payload.cpp
similarity index 100%
rename from server-rs/crates/cocos-editor-bridge/native/native_payload.cpp
rename to plugins/agc-cocos-editor/native/cocos-editor-bridge/native/native_payload.cpp
diff --git a/server-rs/crates/cocos-editor-bridge/payload/bootstrap.cjs b/plugins/agc-cocos-editor/native/cocos-editor-bridge/payload/bootstrap.cjs
similarity index 100%
rename from server-rs/crates/cocos-editor-bridge/payload/bootstrap.cjs
rename to plugins/agc-cocos-editor/native/cocos-editor-bridge/payload/bootstrap.cjs
diff --git a/server-rs/crates/cocos-editor-bridge/payload/bootstrap.test.cjs b/plugins/agc-cocos-editor/native/cocos-editor-bridge/payload/bootstrap.test.cjs
similarity index 100%
rename from server-rs/crates/cocos-editor-bridge/payload/bootstrap.test.cjs
rename to plugins/agc-cocos-editor/native/cocos-editor-bridge/payload/bootstrap.test.cjs
diff --git a/plugins/agc-cocos-editor/native/cocos-editor-bridge/src/adapter.rs b/plugins/agc-cocos-editor/native/cocos-editor-bridge/src/adapter.rs
new file mode 100644
index 000000000..636b76064
--- /dev/null
+++ b/plugins/agc-cocos-editor/native/cocos-editor-bridge/src/adapter.rs
@@ -0,0 +1,419 @@
+//! Cocos Creator 专属编辑器适配器。
+//!
+//! 该实现随 Cocos 插件包分发,实现通用的 `editor_adapter_api::EditorAdapter`,
+//! 由 AGC 宿主按插件 manifest 里的 `adapter` 字段注册和路由。宿主本身不包含
+//! Cocos 进程名、注入或 Editor.Message 知识。
+
+use std::path::{Path, PathBuf};
+use std::sync::Mutex;
+
+use editor_adapter_api::{EditorAdapter, EditorConnectionInfo};
+use serde::Deserialize;
+use serde_json::{json, Value};
+
+use crate::{
+ discover_cocos_editors, execute_cocos_editor_code, execute_cocos_editor_code_for_project,
+ inject_bridge_dll, normalize_existing_directory, paths_equal, ping_cocos_editor,
+ status_cocos_editor, validate_execute_code, validate_injection_target,
+ CocosEditorInjectionRequest, DEFAULT_COMMAND_TIMEOUT_MS,
+};
+
+pub const COCOS_EDITOR_ADAPTER_ID: &str = "cocos-editor";
+
+/// 通用 RPC 方法名到适配器方法的别名表。
+pub const COCOS_EDITOR_RPC_METHODS: &[(&str, &str)] = &[
+ ("editor.detect", "detect"),
+ ("editor.connect", "connect"),
+ ("editor.disconnect", "disconnect"),
+ ("editor.prepare", "prepare"),
+ ("editor.ping", "ping"),
+ ("editor.status", "status"),
+ ("editor.execute", "execute"),
+ ("editor.inject", "inject"),
+];
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct CocosEditorConnection {
+ process_id: u32,
+ project_path: String,
+ creator_version: Option,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(deny_unknown_fields, rename_all = "camelCase")]
+struct AdapterRpcParams {
+ #[serde(default)]
+ process_id: Option,
+ #[serde(default)]
+ project_path: Option,
+ #[serde(default)]
+ code: Option,
+ #[serde(default)]
+ payload_path: Option,
+ #[serde(default)]
+ timeout_ms: Option,
+}
+
+impl AdapterRpcParams {
+ fn from_value(params: Value) -> Result {
+ if params.is_null() {
+ return Ok(Self::default());
+ }
+ serde_json::from_value(params).map_err(|error| format!("插件 RPC 参数无效:{error}"))
+ }
+
+ fn timeout_ms(&self) -> u32 {
+ self.timeout_ms
+ .unwrap_or(DEFAULT_COMMAND_TIMEOUT_MS)
+ .clamp(1, 60_000)
+ }
+
+ fn project_path(&self) -> Result {
+ self.project_path
+ .clone()
+ .ok_or_else(|| "缺少 projectPath".to_string())
+ }
+
+ fn process_id(&self) -> Result {
+ self.process_id.ok_or_else(|| "缺少 processId".to_string())
+ }
+}
+
+/// 由插件包自带的 native 模块实现的 Cocos Creator 适配器。
+#[derive(Debug)]
+pub struct CocosEditorAdapter {
+ payload_candidates: Vec,
+ connection: Mutex