Cocos 插件按当前项目类型暴露
新增 Cocos 项目根识别门禁,限制插件、面板、RPC 和编辑器执行范围 让 DirectProject MCP 工具目录按当前项目类型动态过滤 切换离开 Cocos 项目时停止已运行的插件实例 补充项目级测试、Cocos 技术方案和插件说明
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user