feat: 修复节点变换属性竞态问题,扩展完整属性支持,增强获取数据工具
- fix: 将 update-node-transform 中 scaleX/scaleY 从异步 IPC 改为直接赋值,修复设置不生效 - fix: 将 color 从异步 IPC 改为直接赋值 (node.color = new cc.Color().fromHEX(color)) - feat: update_node_transform 新增 rotation/anchorX/anchorY/opacity/skewX/skewY 参数 - feat: get_scene_hierarchy includeDetails 新增 rotation/anchor/color/opacity/skew/group 返回 - feat: callSceneScriptWithTimeout 对 panel not found 错误返回友好中文提示 - docs: 更新 UPDATE_LOG.md/注意事项.md/project-rules.md 反映所有修改
This commit is contained in:
@@ -7,18 +7,20 @@ description: "MCP Bridge 插件开发规则和编码规范"
|
||||
|
||||
## 1. 语言规范 (Language)
|
||||
|
||||
* **强制中文**: 所有的对话回复、代码注释、以及生成的文档都必须使用**中文**。
|
||||
* **日志消息**: `addLog()` 的消息内容可以使用中英文混合,确保关键术语清晰。
|
||||
- **强制中文**: 所有的对话回复、代码注释、以及生成的文档都必须使用**中文**。
|
||||
- **日志消息**: `addLog()` 的消息内容可以使用中英文混合,确保关键术语清晰。
|
||||
|
||||
---
|
||||
|
||||
## 2. 关键工作流程 (Critical Workflow)
|
||||
|
||||
### 2.1 插件重载
|
||||
|
||||
- **必须重载**: 修改 `main.js`, `package.json`, `scene-script.js`, 或 `panel/` 后,**必须**在编辑器中执行「扩展 → 刷新」或重启编辑器。
|
||||
- **热更新不适用**: Cocos Creator 2.x 的插件主进程脚本不支持热更新。
|
||||
|
||||
### 2.2 测试驱动
|
||||
|
||||
- **测试脚本**: 每个新功能必须在 `test/` 目录下创建独立测试脚本 (如 `test/test_feature.js`)。
|
||||
- **HTTP 验证**: 测试脚本通过 HTTP 请求直接调用插件 API,验证功能正确性。
|
||||
- **运行前提**: 确保 Cocos Creator 编辑器已打开且 MCP Bridge 服务已启动。
|
||||
@@ -30,7 +32,7 @@ description: "MCP Bridge 插件开发规则和编码规范"
|
||||
### 3.1 进程职责划分
|
||||
|
||||
| 文件 | 进程 | 可访问 | 不可访问 |
|
||||
|------|------|--------|----------|
|
||||
| ----------------- | ------------------- | ------------------------------------------- | ------------------------------------- |
|
||||
| `main.js` | 主进程 (Main) | `Editor.assetdb`, `Editor.Ipc`, `require()` | `cc.*` (Cocos 引擎) |
|
||||
| `scene-script.js` | 渲染进程 (Renderer) | `cc.*`, `cc.engine`, `cc.director` | `Editor.assetdb`, `Editor.FileSystem` |
|
||||
|
||||
@@ -60,10 +62,10 @@ description: "MCP Bridge 插件开发规则和编码规范"
|
||||
### 4.1 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| ---------- | -------------------- | --------------------------------- |
|
||||
| 函数名 | camelCase | `handleMcpCall`, `manageScript` |
|
||||
| 常量 | SCREAMING_SNAKE_CASE | `MAX_RESULTS`, `DEFAULT_PORT` |
|
||||
| 私有变量 | _camelCase | `_isMoving`, `_timer` |
|
||||
| 私有变量 | \_camelCase | `_isMoving`, `_timer` |
|
||||
| 布尔变量 | is/has/can 前缀 | `isSceneBusy`, `hasComponent` |
|
||||
| MCP 工具名 | snake_case | `get_selected_node`, `manage_vfx` |
|
||||
| IPC 消息名 | kebab-case | `get-hierarchy`, `create-node` |
|
||||
@@ -97,7 +99,7 @@ module.exports = {
|
||||
messages: {
|
||||
"open-test-panel"() {},
|
||||
// ...
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -106,6 +108,7 @@ module.exports = {
|
||||
> ⚠️ **重要**: `main.js` 已存在重复函数问题,编辑前务必使用 `view_file` 确认上下文,避免创建重复定义。
|
||||
|
||||
**检查清单**:
|
||||
|
||||
- [ ] 新增函数前,搜索是否已存在同名函数
|
||||
- [ ] 修改函数时,确认只有一个定义
|
||||
- [ ] `messages` 对象中避免重复的消息处理器
|
||||
@@ -126,7 +129,7 @@ console.log("服务启动成功"); // 不会被 read_console 捕获
|
||||
```
|
||||
|
||||
| type | 用途 | 颜色 |
|
||||
|------|------|------|
|
||||
| --------- | ------------- | ---- |
|
||||
| `info` | 一般信息 | 蓝色 |
|
||||
| `success` | 操作成功 | 绿色 |
|
||||
| `warn` | 警告信息 | 黄色 |
|
||||
@@ -148,13 +151,15 @@ Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
path: "x",
|
||||
type: "Float",
|
||||
value: 100,
|
||||
isSubProp: false
|
||||
isSubProp: false,
|
||||
});
|
||||
|
||||
// ❌ 不支持 Undo (直接修改)
|
||||
// ⚠️ 不支持 Undo,但同步生效(update-node-transform 使用此方式)
|
||||
node.x = 100;
|
||||
```
|
||||
|
||||
> **注意**: `update-node-transform` 中所有 13 个属性均使用直接赋值方式,这是为了解决异步 IPC 竞态条件导致属性不生效的问题。此为设计性 trade-off:牺牲 Undo 支持以保证属性即时可靠生效。
|
||||
|
||||
### 5.2 使用 Undo 组
|
||||
|
||||
对于复合操作,使用 Undo 组包装:
|
||||
@@ -193,7 +198,7 @@ const defaultPaths = [
|
||||
"db://internal/image/default_sprite_splash",
|
||||
"db://internal/image/default_sprite_splash.png",
|
||||
"db://internal/image/default_particle",
|
||||
"db://internal/image/default_particle.png"
|
||||
"db://internal/image/default_particle.png",
|
||||
];
|
||||
|
||||
for (const path of defaultPaths) {
|
||||
@@ -253,7 +258,7 @@ Editor.assetdb.queryInfoByUrl(path, (err, info) => {
|
||||
使用 [Conventional Commits](https://conventionalcommits.org/) 格式:
|
||||
|
||||
| 类型 | 用途 | 示例 |
|
||||
|------|------|------|
|
||||
| ---------- | -------- | -------------------------------------------- |
|
||||
| `feat` | 新功能 | `feat: add manage_vfx tool` |
|
||||
| `fix` | 修复 bug | `fix: resolve duplicate function in main.js` |
|
||||
| `docs` | 文档更新 | `docs: add code review report` |
|
||||
@@ -266,8 +271,8 @@ Editor.assetdb.queryInfoByUrl(path, (err, info) => {
|
||||
## 9. 已知问题 (Known Issues)
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| ---------------------------------------- | ------------------------------------- | ---------------------------------- |
|
||||
| "Unknown object to record" 错误 | Cocos 2.4.x Undo 系统与 MCP 交互问题 | 可忽略,不影响功能 |
|
||||
| "sendToMain scene:stash-and-save failed" | 时序问题 | 手动 Ctrl+S 保存 |
|
||||
| 颜色修改不支持 Undo | 使用 scene-script 直接修改 | 待优化 |
|
||||
| `update-node-transform` 不支持 Undo | 为解决异步 IPC 竞态问题,改用直接赋值 | 设计性 trade-off,保证属性即时生效 |
|
||||
| execute_menu_item 仅支持部分菜单 | 缺乏通用菜单 IPC | 添加菜单映射表 |
|
||||
|
||||
@@ -347,5 +347,40 @@
|
||||
|
||||
### 3. Texture2D -> SpriteFrame 子资源 UUID 自动解析
|
||||
|
||||
- **问题**: AI 大模型在查找图片引用时,通常只知道 Texture2D (原图) 的 UUID,而 `cc.Sprite.spriteFrame` 实际引用的是其子资源 SpriteFrame 的 UUID,导致查找结果为空。
|
||||
- **问题**: AI 传入图片 (Texture2D) 的 UUID 时,`cc.Sprite.spriteFrame` 实际引用的是该图片的子资源 SpriteFrame(具有不同的 UUID),导致直接查找返回空结果。
|
||||
- **修复**: 在 `src/main.js` 的 `find_references` 路由中,调用 scene-script 前自动读取目标 UUID 对应资源的 `.meta` 文件,提取所有 `subMetas` 中的子资源 UUID (如 SpriteFrame),作为 `additionalIds` 传递给 scene-script。scene-script 将这些额外 UUID 及其压缩/解压变体一并加入匹配列表,实现 "传入 Texture2D UUID 也能查到 SpriteFrame 引用" 的透明体验。
|
||||
|
||||
---
|
||||
|
||||
## 节点变换属性修复与数据增强 (2026-03-01)
|
||||
|
||||
### 1. `update-node-transform` 属性设置方式修复 (`src/scene-script.js`)
|
||||
|
||||
- **问题**: `update-node-transform` 中 `x`, `y`, `width`, `height`, `scaleX`, `scaleY` 六个属性通过异步 IPC `Editor.Ipc.sendToPanel("scene", "scene:set-property", ...)` 设置,存在 fire-and-forget 问题,函数在属性实际生效前就已返回成功回复。导致宽高不生效(Sprite 的 sizeMode 可能在异步消息到达前重置)、坐标不生效(异步 IPC 竞态条件)、批量操作属性丢失等问题。
|
||||
- **修复**: 将所有属性设置统一改为在 scene-script(渲染进程)中直接对节点属性同步赋值(如 `node.x = Number(x)`),与 `set-property` 和 `create-node` 中的处理方式保持一致。`color` 同步改为直接赋值 `node.color = new cc.Color().fromHEX(color)`。
|
||||
- **已验证**: 全部 7 个属性(x, y, width, height, scaleX, scaleY, color)批量设置后均即时生效。
|
||||
|
||||
### 2. `update_node_transform` 工具参数扩展 (`src/main.js` + `src/scene-script.js`)
|
||||
|
||||
- **问题**: 编辑器属性面板中的 Rotation、Anchor、Opacity、Skew 四类属性无法通过 `update_node_transform` MCP 工具设置和获取。
|
||||
- **修复**:
|
||||
- 在 `src/main.js` 的工具 `inputSchema` 中新增 `rotation`, `anchorX`, `anchorY`, `opacity`, `skewX`, `skewY` 六个参数定义。
|
||||
- 在 `src/scene-script.js` 的 `update-node-transform` 处理函数中新增对应的直接赋值逻辑(`node.angle`, `node.anchorX`, `node.anchorY`, `node.opacity`, `node.skewX`, `node.skewY`)。
|
||||
- **已验证**: 全部 13 个属性(x, y, rotation, width, height, scaleX, scaleY, anchorX, anchorY, color, opacity, skewX, skewY)批量设置后均即时生效。
|
||||
|
||||
### 3. `get_scene_hierarchy` 节点详情数据增强 (`src/scene-script.js`)
|
||||
|
||||
- **问题**: `includeDetails` 模式仅返回 position, scale, size 三类数据,缺少 rotation, anchor, color, opacity, skew, group 信息,无法通过 API 完整验证节点属性。
|
||||
- **修复**: 在 `dumpNodes` 函数的 `includeDetails` 分支中新增六个返回字段:
|
||||
- `rotation`: `node.angle`
|
||||
- `anchor`: `{ x: node.anchorX, y: node.anchorY }`
|
||||
- `color`: `{ r: node.color.r, g: node.color.g, b: node.color.b }`
|
||||
- `opacity`: `node.opacity`
|
||||
- `skew`: `{ x: node.skewX, y: node.skewY }`
|
||||
- `group`: `node.group`
|
||||
- **效果**: `includeDetails` 现在返回与编辑器属性面板完全一致的所有节点属性。
|
||||
|
||||
### 4. Scene 面板未就绪友好提示 (`src/main.js`)
|
||||
|
||||
- **问题**: 插件重载或场景切换期间调用 scene-script 方法时,原始错误 `Error: ipc failed to send, panel not found` 信息晦涩,容易让用户误以为插件出现严重故障。
|
||||
- **修复**: 在 `callSceneScriptWithTimeout` 的回调中检测 `panel not found` 错误,自动替换为友好中文提示:`场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试`。日志级别从 `error` 降为 `warn`。
|
||||
|
||||
@@ -114,8 +114,9 @@
|
||||
|
||||
### 8.2 属性修改方式
|
||||
|
||||
- **核心规则**:在 `scene-script.js` 中严禁直接使用 `node.x = 100`。
|
||||
- **正确做法**:必须通过 `Editor.Ipc.sendToPanel('scene', 'scene:set-property', ...)`。只有这样,修改才会被 Cocos Creator 的 UndoManager 捕获,从而支持撤销。
|
||||
- **`update-node-transform` 的做法**:出于解决异步 IPC 竞态条件的考量,`update-node-transform` 中所有 13 个属性(x, y, rotation, width, height, scaleX, scaleY, anchorX, anchorY, color, opacity, skewX, skewY)均使用**直接赋值**(如 `node.x = 100`)。直接赋值是同步操作,确保属性在函数返回前已生效,但**不支持 Undo**。
|
||||
- **需要 Undo 的场景**:如果业务要求支持撤销,应改用 `Editor.Ipc.sendToPanel('scene', 'scene:set-property', ...)`,但需注意异步竞态风险。
|
||||
- **`get_scene_hierarchy` 的 `includeDetails`**:现在返回与编辑器属性面板完全一致的节点数据,包括 position, rotation, scale, anchor, size, color, opacity, skew, group。
|
||||
|
||||
---
|
||||
|
||||
|
||||
17
src/main.js
17
src/main.js
@@ -89,8 +89,19 @@ function callSceneScriptWithTimeout(pluginName, method, args, callback, timeout
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
// 友好化处理 Scene 面板未就绪的错误(如插件重载、场景切换期间)
|
||||
if (err && typeof err === "object" && err.message && err.message.includes("panel not found")) {
|
||||
const friendlyMsg = `场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试。原始信息: ${err.message}`;
|
||||
addLog("warn", `[scene-script] ${friendlyMsg}`);
|
||||
callback(friendlyMsg);
|
||||
} else if (err && typeof err === "string" && err.includes("panel not found")) {
|
||||
const friendlyMsg = `场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试。原始信息: ${err}`;
|
||||
addLog("warn", `[scene-script] ${friendlyMsg}`);
|
||||
callback(friendlyMsg);
|
||||
} else {
|
||||
callback(err, result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (args === null || args === undefined) {
|
||||
@@ -286,11 +297,17 @@ const getToolsList = () => {
|
||||
id: { type: "string", description: "节点 UUID" },
|
||||
x: { type: "number" },
|
||||
y: { type: "number" },
|
||||
rotation: { type: "number", description: "旋转角度" },
|
||||
width: { type: "number" },
|
||||
height: { type: "number" },
|
||||
scaleX: { type: "number" },
|
||||
scaleY: { type: "number" },
|
||||
anchorX: { type: "number", description: "锚点 X (0~1)" },
|
||||
anchorY: { type: "number", description: "锚点 Y (0~1)" },
|
||||
color: { type: "string", description: "HEX 颜色代码如 #FF0000" },
|
||||
opacity: { type: "number", description: "透明度 (0~255)" },
|
||||
skewX: { type: "number", description: "倾斜 X" },
|
||||
skewY: { type: "number", description: "倾斜 Y" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
|
||||
@@ -107,8 +107,14 @@ module.exports = {
|
||||
if (includeDetails) {
|
||||
nodeData.active = node.active;
|
||||
nodeData.position = { x: Math.round(node.x), y: Math.round(node.y) };
|
||||
nodeData.rotation = node.angle;
|
||||
nodeData.scale = { x: node.scaleX, y: node.scaleY };
|
||||
nodeData.anchor = { x: node.anchorX, y: node.anchorY };
|
||||
nodeData.size = { width: node.width, height: node.height };
|
||||
nodeData.color = { r: node.color.r, g: node.color.g, b: node.color.b };
|
||||
nodeData.opacity = node.opacity;
|
||||
nodeData.skew = { x: node.skewX, y: node.skewY };
|
||||
nodeData.group = node.group;
|
||||
nodeData.components = comps.map((c) => cc.js.getClassName(c));
|
||||
} else {
|
||||
// 简略模式下如果存在组件,至少提供一个极简列表让 AI 知道节点的作用
|
||||
@@ -137,9 +143,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量更新节点的变换信息 (坐标、缩放、颜色)
|
||||
* 批量更新节点的变换信息 (坐标、缩放、颜色等)
|
||||
* @param {Object} event IPC 事件对象
|
||||
* @param {Object} args 参数 (id, x, y, scaleX, scaleY, color)
|
||||
* @param {Object} args 参数 (id, x, y, rotation, scaleX, scaleY, anchorX, anchorY, color, opacity, skewX, skewY, width, height)
|
||||
*/
|
||||
"update-node-transform": function (event, args) {
|
||||
const { id, x, y, scaleX, scaleY, color } = args;
|
||||
@@ -147,64 +153,45 @@ module.exports = {
|
||||
let node = findNode(id);
|
||||
|
||||
if (node) {
|
||||
// 使用 scene:set-property 实现支持 Undo 的属性修改
|
||||
// 注意:IPC 消息需要发送到 'scene' 面板
|
||||
// 直接赋值,确保同步生效
|
||||
if (x !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id,
|
||||
path: "x",
|
||||
type: "Number",
|
||||
value: Number(x),
|
||||
});
|
||||
node.x = Number(x);
|
||||
}
|
||||
if (y !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id,
|
||||
path: "y",
|
||||
type: "Number",
|
||||
value: Number(y),
|
||||
});
|
||||
node.y = Number(y);
|
||||
}
|
||||
if (args.rotation !== undefined) {
|
||||
node.angle = Number(args.rotation);
|
||||
}
|
||||
if (args.width !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id,
|
||||
path: "width",
|
||||
type: "Number",
|
||||
value: Number(args.width),
|
||||
});
|
||||
node.width = Number(args.width);
|
||||
}
|
||||
if (args.height !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id,
|
||||
path: "height",
|
||||
type: "Number",
|
||||
value: Number(args.height),
|
||||
});
|
||||
node.height = Number(args.height);
|
||||
}
|
||||
if (scaleX !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id,
|
||||
path: "scaleX",
|
||||
type: "Number",
|
||||
value: Number(scaleX),
|
||||
});
|
||||
node.scaleX = Number(scaleX);
|
||||
}
|
||||
if (scaleY !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id,
|
||||
path: "scaleY",
|
||||
type: "Number",
|
||||
value: Number(scaleY),
|
||||
});
|
||||
node.scaleY = Number(scaleY);
|
||||
}
|
||||
if (args.anchorX !== undefined) {
|
||||
node.anchorX = Number(args.anchorX);
|
||||
}
|
||||
if (args.anchorY !== undefined) {
|
||||
node.anchorY = Number(args.anchorY);
|
||||
}
|
||||
if (color) {
|
||||
const c = new cc.Color().fromHEX(color);
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id: id,
|
||||
path: "color",
|
||||
type: "Color",
|
||||
value: { r: c.r, g: c.g, b: c.b, a: c.a },
|
||||
});
|
||||
node.color = new cc.Color().fromHEX(color);
|
||||
}
|
||||
if (args.opacity !== undefined) {
|
||||
node.opacity = Number(args.opacity);
|
||||
}
|
||||
if (args.skewX !== undefined) {
|
||||
node.skewX = Number(args.skewX);
|
||||
}
|
||||
if (args.skewY !== undefined) {
|
||||
node.skewY = Number(args.skewY);
|
||||
}
|
||||
|
||||
Editor.Ipc.sendToMain("scene:dirty");
|
||||
|
||||
Reference in New Issue
Block a user