Cocos 插件按当前项目类型暴露
Project CI / Repository checks (push) Successful in 2m21s
Project CI / Frontend tests (push) Successful in 2m41s
Project CI / Backend tests (push) Successful in 5m21s
Project CI / Native shell tests (push) Successful in 19m48s

新增 Cocos 项目根识别门禁,限制插件、面板、RPC 和编辑器执行范围
让 DirectProject MCP 工具目录按当前项目类型动态过滤
切换离开 Cocos 项目时停止已运行的插件实例
补充项目级测试、Cocos 技术方案和插件说明
This commit is contained in:
2026-09-13 17:12:37 +08:00
parent 96b45e6ffc
commit c19b321785
8 changed files with 203 additions and 13 deletions
@@ -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,
),
@@ -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 =
@@ -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,
};
}
@@ -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()
);
}
}
@@ -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<String>) -> 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<PluginSummary>| {
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());
}
}