合并 master:Cocos 插件按当前项目类型暴露
- 合入 origin/master c19b32178,使 PR 头包含当前 master tip,修 CI comparison base 门禁(11 秒红) - 冲突 1 处:docs/project-memory/shared-memory/decision-log.md 两侧追加记录,两条逐字保留(HEAD 的 2026-09-11 起多条 + master 的「2026-09-13 Cocos 插件按当前项目类型暴露」),只删 3 行冲突标记 - 验证:npm run check:doc-index 通过(97 份 Markdown);npm run typecheck exit 0;check:encoding 4557 文件通过;git diff --check 干净;全仓无冲突标记
This commit is contained in:
@@ -2353,9 +2353,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,
|
||||
);
|
||||
@@ -2420,9 +2421,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(
|
||||
@@ -2525,7 +2526,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,
|
||||
),
|
||||
|
||||
@@ -1862,7 +1862,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8624,3 +8624,8 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 影响范围:`apps/ai-game-creator-shell/src/features/resource-canvas/{resourceCanvasBottomToolbarModel.ts,ResourceCanvasBottomToolbarView.tsx,ResourceCanvasAssetGenerationPanelView.tsx,ResourceCanvasGenerationPanelView.tsx,resourceCanvasChrome.css}`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、测试 `apps/ai-game-creator-shell/tests/{resourceCanvasBottomToolbar.test.tsx(新增),resourceCanvasGenerationEntry.test.tsx,projectResourceLiveIntegration.test.tsx,appSurface/project-development.suite.ts}`、PRD §3.10 / §7.9 / §8、`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`(新增)、验收用例 S11 / S11a。**未动**:Rust、external v1 / OpenAPI、`packages/`(只消费共享 chrome)、SpacetimeDB。
|
||||
- 验证方式:`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`(423 passed,含新增 6 条:栏目分流与总览不渲染、UI 栏 5 个入口载荷、前置缺失可点击说明且零请求、角色栏 2 个入口、音频入口走既有链路、上传 + 配对读清单,另 1 条工具栏与 Dock 的 CSS 几何契约);`resourceCanvasBottomToolbar.test.tsx` 15 passed(新增);`resourceCanvasGenerationEntry.test.tsx` 11 passed(新增单类型用例 1 条);`projectResourceLiveIntegration.test.tsx` 25 passed(「生成素材」面板改名断言同步更新);`npm run agc:typecheck` 全绿(**其中的 `check-config.mjs` 报错已因本轮落地调用方而消失**)、`npm run check:encoding`、`git diff --check` 干净。未 commit。
|
||||
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`(§3.10 / §7.9 / §8)、`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S11 / S11a / §7.3)。
|
||||
## 2026-09-13 Cocos 插件按当前项目类型暴露
|
||||
|
||||
- 决策:`agc-cocos-editor` 只有在当前受控项目通过 Cocos Creator 根目录识别(`package.json.creator.version` + 普通 `assets/`)时才暴露插件、面板和 Cocos 工具;无项目或其它项目类型均隐藏并失败关闭。
|
||||
- 决策:项目切换离开 Cocos 时立即停止已运行的插件实例;启动、面板读取、插件 RPC、Runtime execute 和 DirectProject MCP 工具目录/执行入口全部再次校验项目类型。Cocos 编辑器操作优先经内置插件入口,禁止回退到项目 `extensions/`、`package.json` 插件或第三方 MCP。
|
||||
- 验证:新增 builtin/plugin host 项目级门禁测试,Direct MCP fixture 补最小 Cocos 工程结构;Rust 定向测试、显式 `cocos-editor-execute` feature 编译、编码检查和 `git diff --check` 已执行。
|
||||
|
||||
@@ -71,7 +71,7 @@ plugins/agc-cocos-editor/
|
||||
|
||||
宿主按 `AGC_PLUGIN_WORKSPACE`、随包 `<resource_dir>/plugins`、开发构建仓库 `plugins/` 的顺序解析工作区;插件包内的 `native/payload` 由构建脚本随包映射,生成的 DLL 不入库。插件协议、权限和面板挂载全部复用通用宿主,Cocos 专属逻辑只存在于本插件包:进程名与 `--project` 解析、Creator 版本校验、named pipe 协议和 Windows 注入。
|
||||
|
||||
该插件是**内置插件**:随客户端分发、不能卸载,只能通过 `set_agc_plugin_enabled` 控制是否可用。禁用后插件进程停止且不能启动,通用 execute 和全部 `cocos_*` 工具从 Agent 目录消失;直接请求隐藏工具也会在宿主执行前被拒绝。开关状态保存在 AppData `extensions/builtin-plugins.json`,隔离 MCP 每次 tools/list 都向绑定宿主询问当前状态。
|
||||
该插件是**内置插件**:随客户端分发、不能卸载,只能通过 `set_agc_plugin_enabled` 控制是否可用。除开关和 feature 外,还必须满足“当前受控项目已识别为 Cocos Creator 项目”这一门禁;没有当前项目或项目类型不是 Cocos 时,插件不出现在插件列表、面板、Agent 工具目录或 MCP tools/list 中,启动、面板读取、RPC 和编辑器执行也会失败关闭。项目切换离开 Cocos 后,已运行实例立即停止。Cocos 编辑器操作统一优先通过该内置插件的 `cocos.editor.execute` / `cocos.editor.operation`(DirectProject 对应 `agc_cocos_execute`);不得改走项目目录 `extensions/`、`package.json` 插件或第三方 MCP。开关状态保存在 AppData `extensions/builtin-plugins.json`,隔离 MCP 每次 tools/list 都向绑定宿主询问当前状态与项目门禁。
|
||||
|
||||
## AGC 项目打开入口
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
Cocos Creator 编辑器桥接插件。用户侧看到的是一个普通 AGC 插件:插件生命周期、UI、
|
||||
RPC、权限和能力注册全部由通用宿主负责,只有“如何连接 Cocos Creator”属于本插件。
|
||||
|
||||
它同时是 AGC 的**内置插件**:随客户端分发、不能卸载。用户在运行时设置里只能切换
|
||||
“是否可用”,禁用后插件不能启动,`agc_cocos_execute` 与全部 `cocos_*` Agent 工具也会
|
||||
从 Agent 工具列表、工具策略快照和上下文里消失;重新启用后立即恢复。
|
||||
它同时是 AGC 的**内置插件**:随客户端分发、不能卸载。只有当前受控目录被识别为 Cocos
|
||||
Creator 项目时才会暴露插件、面板和工具;切换到其它项目会立即停止插件实例并隐藏对应能力。
|
||||
用户在运行时设置里只能切换“是否可用”,禁用后插件不能启动,`agc_cocos_execute` 与全部
|
||||
`cocos_*` Agent 工具也会从 Agent 工具列表、工具策略快照和上下文里消失;重新启用后仍需
|
||||
满足当前项目是 Cocos 的门禁。
|
||||
|
||||
Cocos Creator 项目目录中的 `extensions/`、`package.json` 插件声明或第三方 MCP 包不属于
|
||||
AGC Cocos 桥接来源。Agent 处理 Cocos 请求时只使用客户端登记的 `agc-cocos-editor`
|
||||
|
||||
Reference in New Issue
Block a user