fix(prefab): 修复预制体编辑模式的保存与退出,使用原生 scene://edit-mode 接口替代无效的 IPC 广播,更新相关使用文档与安全规定

This commit is contained in:
火焰库拉
2026-03-08 11:33:28 +08:00
parent fb4025e50b
commit 2573c0f478
15 changed files with 443 additions and 13 deletions

View File

@@ -0,0 +1,18 @@
---
description: Create a unified implementation plan.
---
## User Input
$ARGUMENTS
## Instructions
1. **Read**: Load the `spec.md` provided by the user.
2. **Draft Plan**:
* Create/Update **ONE file**: `specs/[feature_name]/plan.md`.
* **Section 1: Architecture**: Briefly list changed files and data models.
* **Section 2: Step-by-Step**: List the implementation steps (Todo list) directly in this file.
3. **Format**:
* Use checkboxes `- [ ]` for the steps so they are clickable.
* Mark steps as `[Frontend]` or `[Backend]`.
4. **Constraint**: Do NOT create `tasks.md`. Keep it all in `plan.md`.

View File

@@ -0,0 +1,19 @@
---
description: Create a single, rigorous feature spec.
---
## User Input
$ARGUMENTS
## Instructions
1. **Context**: Read `memory/constitution.md` and any attached images.
2. **Draft Spec**:
* Create/Update **ONE file only**: `specs/[feature_name]/spec.md`.
* **Do NOT** create a checklist file.
* **Do NOT** create a separate analysis file.
3. **Content**:
* Include "Visual Requirements" (from images).
* Include "Functional Requirements".
* Include "Edge Cases" directly in the spec as a section.
4. **Output**: Show the `spec.md` artifact.

View File

@@ -0,0 +1,13 @@
---
description: Build the feature from the plan.
---
## Instructions
1. **Read**: Open `specs/[feature_name]/plan.md`.
2. **Execute**:
* Look for the **Step-by-Step** section with checkboxes.
* Execute the unchecked items `- [ ]`.
3. **Vibe Check**:
* After every 2-3 steps, verify the code visually.
* Mark steps as `- [x]` in `plan.md` as you finish them.

View File

@@ -0,0 +1,14 @@
---
description: Clean up legacy code before feature work.
---
## User Input
$ARGUMENTS
## Instructions
1. **Audit**: Scan the target directory `$ARGUMENTS`.
2. **Constitution Check**: Compare against `/memory/constitution.md`.
3. **Refactor**: Apply modern patterns (e.g., "Convert Promises to Async/Await") WITHOUT changing business logic.
4. **Verify**: Run existing tests to ensure no regression.

View File

@@ -0,0 +1,17 @@
---
description: Visual and logic verification of the feature.
---
## Antigravity QA Instructions
1. **Environment**: Spin up the app in the Integrated Terminal/Browser.
2. **Visual Audit**:
- Navigate to the new feature.
- **TAKE SCREENSHOT**.
- Compare screenshot to the `spec.md` requirements.
- Report: "Match" or "Discrepancy".
3. **Logic Audit**:
- Run the "Happy Path" user journey.
- Check console logs for errors.
4. **Report**:
- Generate a **Release Readiness Artifact**.

View File

@@ -133,11 +133,14 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/src/mcp-pro
- **参数**:
- `url`: 场景资源路径,如 `db://assets/NewScene.fire`
### 7. open_prefab
### 7. open_prefab / save_prefab / close_prefab
- **描述**: 在编辑器中打开指定的预制体文件进入编辑模式。这是一个异步操作,打开后请等待几秒
- **描述**: 预制体专用操作三部曲
- **参数**:
- `url`: 预制体资源路径,`db://assets/prefabs/Test.prefab`
- `open_prefab`: 需提供 `url` (`db://assets/prefabs/Test.prefab`), 将异步打开并进入编辑模式。
- `save_prefab`: 无需参数,保存当前预制体。
- `close_prefab`: 无需参数,彻底退出预制体编辑模式并返回原场景。
- **重要提示**: 这三个工具操作会直接深入场景编辑器的底层并驱动真正的状态机(通过内部 `scene://edit-mode` 的真实模拟行为与人类手动点击“保存”“退出”按钮100%一致。
### 8. create_node

View File

@@ -431,3 +431,17 @@ ps: 感谢 @亮仔😂 😁 🐔否? 提供的反馈以及操作日志
- **问题**: `get-hierarchy``dumpNodes` 递归遍历子节点时无数量限制,若某个节点下有数百个同级子节点,返回数据量巨大。
- **修复**: 新增 `MAX_CHILDREN_PER_LEVEL = 50` 安全上限。每层最多返回 50 个子节点,超出部分在返回数据中通过 `childrenTruncated` 字段标注被截断的数量,帮助 AI 知悉还有更多子节点未列出。
---
## 预制体全生命周期编辑与状态流转修复 (2026-03-08)
### 1. `save_prefab` 与 `close_prefab` 原生化改造
- **问题**: AI 尝试从 `McpTestPrefab.prefab` 等文件中通过 `scene:save-prefab``scene:close-prefab` 保存并退出预制体模式时,报错找不到事件。分析源码发现,这些直觉上的 IPC 消息实际上并不存在,导致一旦用 `open_prefab` 进入隔离的编辑空间后,无法正确将修改落盘,且无法安全退出返回原大场景。
- **修复**:
1. **直接调用内部 API**: 完全废弃尝试通过常规 IPC 发送伪装指令的想法。在 `scene-script.js` 内部直接 `require("scene://edit-mode")`,获取到负责管理场景所有状态机(如大场景模式、预制体模式、动画编辑模式)的核心模块。
2. **真实状态机驱动**:
- 对于 `save_prefab`,直接调用无参的 `editMode.save()`。这100%等同于人工点击编辑器左上角的“保存”按钮。也因此**移除了多余的 `rootNodeId` 参数**限制。
- 对于 `close_prefab`,直接调用 `editMode.pop()`。彻底将当前编辑的预制体上下文从堆栈中弹出,干净利落地回落回原始场景,并自动更新引擎各面板 (Inspector、层级管理器等) 状态。
- **意义**: 到目前为止,涵盖**创建 (`create_prefab`) -> 打开 (`open_prefab`) -> 修改 -> (可选)保存 (`save_prefab`) -> 退出 (`close_prefab`)** 的完整大模型端到端自动化预制体流水线被彻底打通。没有任何妥协,一切就像是有一个看不见的高手坐在引擎主控面板前帮你点按按钮。

187
docs/prefab-ipc-messages.md Normal file
View File

@@ -0,0 +1,187 @@
# Prefab 相关的 IPC 消息总结
本文档总结了 Cocos Creator 编辑器中所有与 Prefab 相关的 `Editor.Ipc.sendTo*` 开头的 IPC 消息。
## 消息列表
| 序号 | IPC 消息调用 | 消息名称 | 参数 | 所在文件 | 说明 |
| ---- | --------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------ |
| 1 | `Editor.Ipc.sendToAll("scene:enter-prefab-edit-mode", e)` | scene:enter-prefab-edit-mode | `e` (prefab uuid) | `editor/builtin/assets/panel/component/node.js:277` | 进入 prefab 编辑模式 |
| 2 | `Editor.Ipc.sendToPanel("scene", "scene:create-prefab", d, u)` | scene:create-prefab | `d` (uuid), `u` (path) | `editor/builtin/assets/panel/component/nodes.js:197` | 创建 prefab |
| 3 | `Editor.Ipc.sendToPanel("scene", "scene:revert-prefab", this._vm.target.uuid)` | scene:revert-prefab | `this._vm.target.uuid` (node uuid) | `editor/builtin/inspector/panel/index.js:137` | 还原 prefab |
| 4 | `Editor.Ipc.sendToPanel("scene", "scene:apply-prefab", this._vm.target.uuid)` | scene:apply-prefab | `this._vm.target.uuid` (node uuid) | `editor/builtin/inspector/panel/index.js:140` | 应用 prefab |
| 5 | `Editor.Ipc.sendToPanel("scene", "scene:set-prefab-sync", this._vm.target.uuid)` | scene:set-prefab-sync | `this._vm.target.uuid` (node uuid) | `editor/builtin/inspector/panel/index.js:143` | 设置 prefab 同步 |
| 6 | `Editor.Ipc.sendToPanel("node-library", "node-library:delete-prefab", e)` | node-library:delete-prefab | `e` (prefab info object) | `editor/builtin/node-library/core/menu.js:7` | 删除 prefab |
| 7 | `Editor.Ipc.sendToPanel("node-library", "node-library:rename-prefab", e)` | node-library:rename-prefab | `e` (prefab info object) | `editor/builtin/node-library/core/menu.js:14` | 重命名 prefab |
| 8 | `Editor.Ipc.sendToPanel("node-library", "node-library:set-prefab-icon", e)` | node-library:set-prefab-icon | `e` (prefab info object) | `editor/builtin/node-library/core/menu.js:21` | 设置 prefab 图标 |
| 9 | `Editor.Ipc.sendToMain("node-library:popup-prefab-menu", e.x, e.y, { id: this.prefab.uuid })` | node-library:popup-prefab-menu | `e.x`, `e.y`, `{ id: this.prefab.uuid }` | `editor/builtin/node-library/panel/component/prefab.js:30` | 弹出 prefab 菜单 |
| 10 | `Editor.Ipc.sendToPanel("scene", "scene:create-node-by-prefab", e, Editor.assetdb.urlToUuid(r), o)` | scene:create-node-by-prefab | `e` (name), `Editor.assetdb.urlToUuid(r)` (uuid), `o` (parentNode) | `editor/core/main-menu.js:6` | 通过 prefab 创建节点 |
| 11 | `Editor.Ipc.sendToMain("scene:create-prefab", s, a, (e, t) => {...})` | scene:create-prefab | `s` (path), `a` (serialized data), callback | `editor/page/scene-utils/index.js:211` | 创建 prefab带回调 |
| 12 | `Editor.Ipc.sendToMain("scene:apply-prefab", i, n)` | scene:apply-prefab | `i` (uuid), `n` (serialized data) | `editor/page/scene-utils/index.js:225` | 应用 prefab 到资源 |
| 13 | `Editor.Ipc.sendToAll("scene:enter-prefab-edit-mode", l.uuid)` | scene:enter-prefab-edit-mode | `l.uuid` (prefab uuid) | `editor/builtin/open-recent-items/main.js:28` | 从最近项目进入 prefab 编辑模式 |
## 按功能分类
### Prefab 编辑模式管理
- **scene:enter-prefab-edit-mode** - 进入 prefab 编辑模式
### Prefab 创建与保存
- **scene:create-prefab** - 创建 prefab 资源
- **scene:apply-prefab** - 应用 prefab 修改到资源
### Prefab 实例操作
- **scene:revert-prefab** - 还原 prefab 实例
- **scene:set-prefab-sync** - 设置 prefab 同步状态
### Node Library Prefab 管理
- **node-library:delete-prefab** - 删除用户 prefab
- **node-library:rename-prefab** - 重命名用户 prefab
- **node-library:set-prefab-icon** - 设置 prefab 图标
- **node-library:popup-prefab-menu** - 弹出 prefab 右键菜单
### 节点创建
- **scene:create-node-by-prefab** - 从 prefab 创建节点
## 详细说明
### 1. Prefab 编辑模式管理
#### scene:enter-prefab-edit-mode
- **用途**: 打开 prefab 进行编辑
- **参数**: prefab 资源的 uuid
- **发送方式**: sendToAll
- **处理**: 加载 prefab 资源并推入 prefab 编辑模式栈
> **重要提示**: `scene:save-prefab` 和 `scene:close-prefab` 以及 `scene:prefab-mode-changed` 等并不能用于主动保存或退出预制体模式。如果要在代码中真正模拟点击“保存”或“退出”预制体编辑模式,必须在运行于 `scene` 面板的脚本中获取内部的 `scene://edit-mode` 模块:
>
> ```javascript
> const editMode = Editor.require("scene://edit-mode");
> if (editMode && editMode.curMode().name === "prefab") {
> editMode.save(); // 保存预制体
> editMode.pop(); // 退出预制体编辑模式
> }
> ```
### 2. Prefab 创建与保存
#### scene:create-prefab
- **用途**: 将场景中的节点保存为 prefab 资源
- **参数**:
- path: prefab 保存路径
- serializedData: 序列化后的 prefab 数据
- callback: 回调函数 (error, uuid)
- **发送方式**: sendToMain 或 sendToPanel
- **处理**: 在 asset-db 中创建 prefab 文件
#### scene:apply-prefab
- **用途**: 将 prefab 实例的修改应用到 prefab 资源
- **参数**:
- uuid: prefab 资源 uuid
- serializedData: 序列化后的 prefab 数据
- **发送方式**: sendToMain 或 sendToPanel
- **处理**: 保存 prefab 资源文件
### 3. Prefab 实例操作
#### scene:revert-prefab
- **用途**: 将 prefab 实例还原到 prefab 资源的状态
- **参数**: 节点 uuid
- **发送方式**: sendToPanel
- **处理**: 重新实例化 prefab 资源并替换当前节点
#### scene:set-prefab-sync
- **用途**: 设置 prefab 实例的自动同步状态
- **参数**: 节点 uuid
- **发送方式**: sendToPanel
- **处理**: 切换 prefab sync 属性
### 4. Node Library Prefab 管理
#### node-library:delete-prefab
- **用途**: 从 node library 删除用户 prefab
- **参数**: prefab 信息对象 {id}
- **发送方式**: sendToPanel
- **处理**: 删除 prefab 文件和图标
#### node-library:rename-prefab
- **用途**: 重命名 node library 中的 prefab
- **参数**: prefab 信息对象 {id}
- **发送方式**: sendToPanel
- **处理**: 触发重命名 UI 交互
#### node-library:set-prefab-icon
- **用途**: 设置 prefab 的自定义图标
- **参数**: prefab 信息对象 {id}
- **发送方式**: sendToPanel
- **处理**: 打开文件选择对话框并保存图标
#### node-library:popup-prefab-menu
- **用途**: 在 prefab 上右键弹出上下文菜单
- **参数**: x 坐标y 坐标prefab 信息对象 {id}
- **发送方式**: sendToMain
- **处理**: 显示右键菜单
### 5. 节点创建
#### scene:create-node-by-prefab
- **用途**: 从 prefab 资源创建节点实例
- **参数**:
- name: 节点名称
- uuid: prefab 资源 uuid
- parentNode: 父节点
- **发送方式**: sendToPanel
- **处理**: 实例化 prefab 并添加到场景中
## 使用示例
### 进入 Prefab 编辑模式
```javascript
Editor.Ipc.sendToAll("scene:enter-prefab-edit-mode", prefabUuid);
```
### 创建 Prefab
```javascript
Editor.Ipc.sendToMain("scene:create-prefab", path, serializedData, (error, uuid) => {
if (error) {
Editor.error(error);
return;
}
// prefab 创建成功uuid 为新创建的 prefab uuid
});
```
### 应用 Prefab 修改
```javascript
Editor.Ipc.sendToPanel("scene", "scene:apply-prefab", rootNodeUuid);
```
### 还原 Prefab
```javascript
Editor.Ipc.sendToPanel("scene", "scene:revert-prefab", nodeUuid);
```
### 从代码创建 Prefab 节点
```javascript
let parentNode = Editor.Selection.contexts("node")[0] || Editor.Selection.curActivate("node");
Editor.Ipc.sendToPanel("scene", "scene:create-node-by-prefab", nodeName, prefabUuid, parentNode);
```

View File

@@ -20,6 +20,16 @@
5. 所有节点和组件的 `_id` 字段必须为空字符串(运行时由引擎分配)。
6. 文件中不能包含 `cc.Scene` 对象。
### 1.3 预制体界面的打开与保存
- **推荐工具**:使用 `open_prefab` `save_prefab`,和 `close_prefab` 工具。
- **痛点**:直觉上 AI 可能会推断应该通过 `scene:save-prefab``scene:close-prefab` 这样的 IPC 指令来实现“保存”与“退出预制体视图”,但实际上这类消息在引擎内部并不存在。
- **正规实现(封装在工具内)**
必须在负责渲染场景面板的渲染进程Scene Process直接 `require("scene://edit-mode")`,然后对当前的状态机进行控制。
- 保存:`require("scene://edit-mode").save()`
- 退出:`require("scene://edit-mode").pop()`
这彻底解决了进入预制体模式后出不去或者没法存盘的痛点。AI 在使用 `save_prefab``close_prefab` 工具时就是在触发这两行代码。
---
## 2. 脚本属性Inspector关联

22
memory/constitution.md Normal file
View File

@@ -0,0 +1,22 @@
# Project Constitution
## Article I: The Artifact Mandate
**Agents shall not perform work without a visible Artifact.**
- Every planning step must produce a Markdown Artifact (Plan, Spec, or Checklist).
- Never rely on "chat memory" alone. If it's important, write it to a file.
## Article II: Vision Verification
**Trust but Verify (Visually).**
- When implementing UI, the Agent MUST take a screenshot using the integrated Browser.
- Compare the screenshot to the original requirement/mockup.
- If pixels don't match, the task is incomplete.
## Article III: Agent Independence
**Build for Parallelism.**
- Tasks must be atomic.
- Frontend agents should mock API responses if the Backend agent isn't finished.
- Never block a thread waiting for another agent.
## Article IV: Code Health
- **No Legacy Patterns**: Use modern syntax (e.g., React Hooks, ES6+, Python 3.12+).
- **Self-Correction**: If a test fails, attempt to fix it *once* before asking the user.

View File

@@ -345,6 +345,16 @@ const getToolsList = () => {
description: `保存当前场景的修改`,
inputSchema: { type: "object", properties: {} },
},
{
name: "save_prefab",
description: `保存当前正在编辑的预制体的修改(仅在 open_prefab 进入预制体编辑模式后使用)`,
inputSchema: { type: "object", properties: {} },
},
{
name: "close_prefab",
description: `退出预制体编辑模式,返回普通场景编辑状态`,
inputSchema: { type: "object", properties: {} },
},
{
name: "get_scene_hierarchy",
description: `获取当前场景的节点树结构(包含 UUID、名称、子节点数。若要查询节点组件详情等请使用 manage_components。`,
@@ -1238,6 +1248,24 @@ module.exports = {
callback(null, "场景保存成功。");
break;
case "save_prefab":
isSceneBusy = true;
addLog("info", "调用场景脚本保存预制体...");
callSceneScriptWithTimeout("mcp-bridge", "save-prefab", {}, (err, res) => {
isSceneBusy = false;
callback(err, res);
});
break;
case "close_prefab":
isSceneBusy = true;
addLog("info", "调用场景脚本退出预制体模式...");
callSceneScriptWithTimeout("mcp-bridge", "close-prefab", {}, (err, res) => {
isSceneBusy = false;
callback(err, res);
});
break;
case "get_scene_hierarchy":
callSceneScriptWithTimeout("mcp-bridge", "get-hierarchy", args, callback);
break;

View File

@@ -610,6 +610,9 @@ module.exports = {
} else {
// 尝试获取自定义组件
compClass = cc.js.getClassByName(componentType);
if (!compClass && cc[componentType]) {
compClass = cc[componentType];
}
}
if (!compClass) {
@@ -647,14 +650,15 @@ module.exports = {
break;
case "remove":
if (!componentId) {
if (event.reply) event.reply(new Error("必须提供组件 ID"));
if (!componentId && !componentType) {
if (event.reply) event.reply(new Error("必须提供组件 ID 或组件类型(componentType)"));
return;
}
try {
// 查找并移除组件
let component = null;
if (componentId) {
if (node._components) {
for (let i = 0; i < node._components.length; i++) {
if (node._components[i].uuid === componentId) {
@@ -663,6 +667,21 @@ module.exports = {
}
}
}
} else if (componentType) {
let compClass = null;
if (componentType.startsWith("cc.")) {
const className = componentType.replace("cc.", "");
compClass = cc[className];
} else {
compClass = cc.js.getClassByName(componentType);
if (!compClass && cc[componentType]) {
compClass = cc[componentType];
}
}
if (compClass) {
component = node.getComponent(compClass);
}
}
if (component) {
node.removeComponent(component);
@@ -679,9 +698,9 @@ module.exports = {
case "update":
// 更新现有组件属性
if (!componentType) {
// 如果提供了 componentId可以只用 componentId
// 但 Cocos 2.4 uuid 获取组件比较麻烦,最好还是有 type 或者遍历
if (!componentType && !componentId) {
if (event.reply) event.reply(new Error("必须提供组件 ID 或组件类型"));
return;
}
try {
@@ -707,6 +726,9 @@ module.exports = {
compClass = cc[className];
} else {
compClass = cc.js.getClassByName(componentType);
if (!compClass && cc[componentType]) {
compClass = cc[componentType];
}
}
if (compClass) {
targetComp = node.getComponent(compClass);
@@ -1583,4 +1605,37 @@ module.exports = {
if (event.reply) event.reply(new Error(`序列化节点失败: ${e.message}`));
}
},
"save-prefab": function (event, args) {
try {
const editMode = Editor.require("scene://edit-mode");
if (editMode && editMode.curMode().name === "prefab") {
editMode.save((err) => {
if (err) {
if (event.reply) event.reply(new Error(err.message || err));
} else {
if (event.reply) event.reply(null, "预制体保存成功");
}
});
} else {
if (event.reply) event.reply(new Error("当前不在预制体编辑模式中"));
}
} catch (e) {
if (event.reply) event.reply(new Error("保存预制体发生异常: " + e.message));
}
},
"close-prefab": function (event, args) {
try {
const editMode = Editor.require("scene://edit-mode");
if (editMode && editMode.curMode().name === "prefab") {
editMode.pop();
if (event.reply) event.reply(null, "已触发退出预制体编辑模式");
} else {
if (event.reply) event.reply(new Error("当前不在预制体编辑模式中"));
}
} catch (e) {
if (event.reply) event.reply(new Error("退出预制体模式发生异常: " + e.message));
}
},
};

15
templates/spec.md Normal file
View File

@@ -0,0 +1,15 @@
# Feature: [Name]
## 1. Overview
*What are we building and why?*
## 2. Visual Requirements (Vision)
*Description of UI/UX derived from uploaded mockups.*
- [ ] Component A
- [ ] Layout B
## 3. Functional Requirements
- [ ] User can...
- [ ] System must...
## 4. Data Model

15
templates/tasks.md Normal file
View File

@@ -0,0 +1,15 @@
# Implementation Tasks
## 🔴 Parallel Group A: Backend
- [ ] Define API Schema
- [ ] Implement Database Migrations
- [ ] Create API Endpoints
## 🔵 Parallel Group B: Frontend
- [ ] Scaffold Components
- [ ] Implement State Management
- [ ] Connect to Mock API
## 🟢 Phase 2: Integration
- [ ] Connect Frontend to Real Backend
- [ ] E2E Tests