diff --git a/.gemini/rules/project-rules.md b/.gemini/rules/project-rules.md index a8c019e..e3f7132 100644 --- a/.gemini/rules/project-rules.md +++ b/.gemini/rules/project-rules.md @@ -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 服务已启动。 @@ -29,10 +31,10 @@ 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` | +| 文件 | 进程 | 可访问 | 不可访问 | +| ----------------- | ------------------- | ------------------------------------------- | ------------------------------------- | +| `main.js` | 主进程 (Main) | `Editor.assetdb`, `Editor.Ipc`, `require()` | `cc.*` (Cocos 引擎) | +| `scene-script.js` | 渲染进程 (Renderer) | `cc.*`, `cc.engine`, `cc.director` | `Editor.assetdb`, `Editor.FileSystem` | ### 3.2 跨进程通信规则 @@ -59,14 +61,14 @@ description: "MCP Bridge 插件开发规则和编码规范" ### 4.1 命名规范 -| 类型 | 规范 | 示例 | -|------|------|------| -| 函数名 | camelCase | `handleMcpCall`, `manageScript` | -| 常量 | SCREAMING_SNAKE_CASE | `MAX_RESULTS`, `DEFAULT_PORT` | -| 私有变量 | _camelCase | `_isMoving`, `_timer` | -| 布尔变量 | is/has/can 前缀 | `isSceneBusy`, `hasComponent` | -| MCP 工具名 | snake_case | `get_selected_node`, `manage_vfx` | -| IPC 消息名 | kebab-case | `get-hierarchy`, `create-node` | +| 类型 | 规范 | 示例 | +| ---------- | -------------------- | --------------------------------- | +| 函数名 | camelCase | `handleMcpCall`, `manageScript` | +| 常量 | SCREAMING_SNAKE_CASE | `MAX_RESULTS`, `DEFAULT_PORT` | +| 私有变量 | \_camelCase | `_isMoving`, `_timer` | +| 布尔变量 | is/has/can 前缀 | `isSceneBusy`, `hasComponent` | +| MCP 工具名 | snake_case | `get_selected_node`, `manage_vfx` | +| IPC 消息名 | kebab-case | `get-hierarchy`, `create-node` | ### 4.2 函数组织顺序 @@ -76,28 +78,28 @@ description: "MCP Bridge 插件开发规则和编码规范" module.exports = { // 1. 配置属性 "scene-script": "scene-script.js", - + // 2. 生命周期函数 - load() { }, - unload() { }, - + load() {}, + unload() {}, + // 3. 服务器管理 - startServer(port) { }, - stopServer() { }, - + startServer(port) {}, + stopServer() {}, + // 4. 核心处理逻辑 - handleMcpCall(name, args, callback) { }, - + handleMcpCall(name, args, callback) {}, + // 5. 工具函数 (按字母顺序) - applyTextEdits(args, callback) { }, - batchExecute(args, callback) { }, + applyTextEdits(args, callback) {}, + batchExecute(args, callback) {}, // ... - + // 6. IPC 消息处理 messages: { - "open-test-panel"() { }, + "open-test-panel"() {}, // ... - } + }, }; ``` @@ -106,6 +108,7 @@ module.exports = { > ⚠️ **重要**: `main.js` 已存在重复函数问题,编辑前务必使用 `view_file` 确认上下文,避免创建重复定义。 **检查清单**: + - [ ] 新增函数前,搜索是否已存在同名函数 - [ ] 修改函数时,确认只有一个定义 - [ ] `messages` 对象中避免重复的消息处理器 @@ -122,16 +125,16 @@ addLog("mcp", `REQ -> [${toolName}]`); addLog("success", `RES <- [${toolName}] 成功`); // ❌ 错误 -console.log("服务启动成功"); // 不会被 read_console 捕获 +console.log("服务启动成功"); // 不会被 read_console 捕获 ``` -| type | 用途 | 颜色 | -|------|------|------| -| `info` | 一般信息 | 蓝色 | -| `success` | 操作成功 | 绿色 | -| `warn` | 警告信息 | 黄色 | -| `error` | 错误信息 | 红色 | -| `mcp` | MCP 请求/响应 | 紫色 | +| type | 用途 | 颜色 | +| --------- | ------------- | ---- | +| `info` | 一般信息 | 蓝色 | +| `success` | 操作成功 | 绿色 | +| `warn` | 警告信息 | 黄色 | +| `error` | 错误信息 | 红色 | +| `mcp` | MCP 请求/响应 | 紫色 | --- @@ -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) { @@ -226,12 +231,12 @@ setTimeout(() => { ```javascript // ✅ 标准风格 -callback(null, result); // 成功 -callback("Error message"); // 失败 (字符串) -callback(new Error("message")); // 失败 (Error 对象) +callback(null, result); // 成功 +callback("Error message"); // 失败 (字符串) +callback(new Error("message")); // 失败 (Error 对象) // 避免混用 -callback(err, null); // 不推荐,保持一致性 +callback(err, null); // 不推荐,保持一致性 ``` ### 7.2 异步操作错误处理 @@ -252,22 +257,22 @@ 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` | -| `refactor` | 重构 | `refactor: split main.js into modules` | -| `test` | 测试 | `test: add material management tests` | -| `chore` | 杂项 | `chore: update dependencies` | +| 类型 | 用途 | 示例 | +| ---------- | -------- | -------------------------------------------- | +| `feat` | 新功能 | `feat: add manage_vfx tool` | +| `fix` | 修复 bug | `fix: resolve duplicate function in main.js` | +| `docs` | 文档更新 | `docs: add code review report` | +| `refactor` | 重构 | `refactor: split main.js into modules` | +| `test` | 测试 | `test: add material management tests` | +| `chore` | 杂项 | `chore: update dependencies` | --- ## 9. 已知问题 (Known Issues) -| 问题 | 原因 | 解决方案 | -|------|------|----------| -| "Unknown object to record" 错误 | Cocos 2.4.x Undo 系统与 MCP 交互问题 | 可忽略,不影响功能 | -| "sendToMain scene:stash-and-save failed" | 时序问题 | 手动 Ctrl+S 保存 | -| 颜色修改不支持 Undo | 使用 scene-script 直接修改 | 待优化 | -| execute_menu_item 仅支持部分菜单 | 缺乏通用菜单 IPC | 添加菜单映射表 | +| 问题 | 原因 | 解决方案 | +| ---------------------------------------- | ------------------------------------- | ---------------------------------- | +| "Unknown object to record" 错误 | Cocos 2.4.x Undo 系统与 MCP 交互问题 | 可忽略,不影响功能 | +| "sendToMain scene:stash-and-save failed" | 时序问题 | 手动 Ctrl+S 保存 | +| `update-node-transform` 不支持 Undo | 为解决异步 IPC 竞态问题,改用直接赋值 | 设计性 trade-off,保证属性即时生效 | +| execute_menu_item 仅支持部分菜单 | 缺乏通用菜单 IPC | 添加菜单映射表 | diff --git a/docs/UPDATE_LOG.md b/docs/UPDATE_LOG.md index 6997f54..5384530 100644 --- a/docs/UPDATE_LOG.md +++ b/docs/UPDATE_LOG.md @@ -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`。 diff --git a/docs/注意事项.md b/docs/注意事项.md index 6b6ced7..718377e 100644 --- a/docs/注意事项.md +++ b/docs/注意事项.md @@ -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。 --- diff --git a/src/main.js b/src/main.js index ee97d47..f6b32c0 100644 --- a/src/main.js +++ b/src/main.js @@ -89,7 +89,18 @@ function callSceneScriptWithTimeout(pluginName, method, args, callback, timeout if (!settled) { settled = true; clearTimeout(timer); - callback(err, result); + // 友好化处理 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); + } } }; @@ -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"], }, diff --git a/src/scene-script.js b/src/scene-script.js index 4fb8248..d6eadf9 100644 --- a/src/scene-script.js +++ b/src/scene-script.js @@ -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");