From 532cd08f9bc67e8081671861f250fe540f893ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=AB=E7=84=B0=E5=BA=93=E6=8B=89?= Date: Wed, 4 Feb 2026 01:57:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(manage=5Fanimation):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BD=93=E5=8A=A8=E7=94=BB=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E5=89=AA=E8=BE=91=E6=97=B6=E7=9A=84=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E9=97=AE=E9=A2=98=EF=BC=9B=E6=96=87=E6=A1=A3:=20?= =?UTF-8?q?=E5=9C=A8=20README=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=20get=5Fsha=20?= =?UTF-8?q?=E5=92=8C=20manage=5Fanimation=20=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/rules/project-rules.mdc | 266 ++++++++++++++++++++++++++++++++++ README.md | 17 ++- main.js | 76 ++++++++-- scene-script.js | 78 ++++++++++ 4 files changed, 425 insertions(+), 12 deletions(-) create mode 100644 .trae/rules/project-rules.mdc diff --git a/.trae/rules/project-rules.mdc b/.trae/rules/project-rules.mdc new file mode 100644 index 0000000..ea4643f --- /dev/null +++ b/.trae/rules/project-rules.mdc @@ -0,0 +1,266 @@ +--- +description: +alwaysApply: true +enabled: true +updatedAt: 2026-02-03T13:55:50.734Z +provider: +--- + + +# MCP Bridge Development Rules + +## 1. 语言规范 (Language) + +* **强制中文**: 所有的对话回复、代码注释、以及生成的文档都必须使用**中文**。 +* **日志消息**: `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 服务已启动。 + +--- + +## 3. 架构与进程隔离 (Architecture & IPC) + +### 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` | + +### 3.2 跨进程通信规则 + +``` +主进程 (main.js) 渲染进程 (scene-script.js) + │ │ + ├─ 1. 接收 HTTP 请求 │ + ├─ 2. 解析 db:// 路径为 UUID │ + │ Editor.assetdb.urlToUuid() │ + ├─ 3. 调用场景脚本 ──────────────────────┤ + │ Editor.Scene.callSceneScript() │ + │ ├─ 4. 操作节点/组件 + │ │ cc.engine.getInstanceById() + │ ├─ 5. 通知场景变脏 + │ │ Editor.Ipc.sendToMain("scene:dirty") + └─ 6. 返回结果 ◀────────────────────────┘ +``` + +**核心规则**: 永远在 `main.js` 中将 `db://` 路径转换为 UUID,再传递给 `scene-script.js`。 + +--- + +## 4. 编码规范 (Coding Standards) + +### 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` | + +### 4.2 函数组织顺序 + +在 `module.exports` 中按以下顺序组织函数: + +```javascript +module.exports = { + // 1. 配置属性 + "scene-script": "scene-script.js", + + // 2. 生命周期函数 + load() { }, + unload() { }, + + // 3. 服务器管理 + startServer(port) { }, + stopServer() { }, + + // 4. 核心处理逻辑 + handleMcpCall(name, args, callback) { }, + + // 5. 工具函数 (按字母顺序) + applyTextEdits(args, callback) { }, + batchExecute(args, callback) { }, + // ... + + // 6. IPC 消息处理 + messages: { + "open-test-panel"() { }, + // ... + } +}; +``` + +### 4.3 避免重复定义 + +> ⚠️ **重要**: `main.js` 已存在重复函数问题,编辑前务必使用 `view_file` 确认上下文,避免创建重复定义。 + +**检查清单**: +- [ ] 新增函数前,搜索是否已存在同名函数 +- [ ] 修改函数时,确认只有一个定义 +- [ ] `messages` 对象中避免重复的消息处理器 + +### 4.4 日志规范 + +使用 `addLog(type, message)` 替代 `console.log()`: + +```javascript +// ✅ 正确 +addLog("info", "服务启动成功"); +addLog("error", `操作失败: ${err.message}`); +addLog("mcp", `REQ -> [${toolName}]`); +addLog("success", `RES <- [${toolName}] 成功`); + +// ❌ 错误 +console.log("服务启动成功"); // 不会被 read_console 捕获 +``` + +| type | 用途 | 颜色 | +|------|------|------| +| `info` | 一般信息 | 蓝色 | +| `success` | 操作成功 | 绿色 | +| `warn` | 警告信息 | 黄色 | +| `error` | 错误信息 | 红色 | +| `mcp` | MCP 请求/响应 | 紫色 | + +--- + +## 5. 撤销/重做支持 (Undo/Redo) + +### 5.1 使用 scene:set-property + +对于节点属性修改,优先使用 `scene:set-property` 以获得原生 Undo 支持: + +```javascript +// ✅ 支持 Undo +Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id: nodeId, + path: "x", + type: "Float", + value: 100, + isSubProp: false +}); + +// ❌ 不支持 Undo (直接修改) +node.x = 100; +``` + +### 5.2 使用 Undo 组 + +对于复合操作,使用 Undo 组包装: + +```javascript +Editor.Ipc.sendToPanel("scene", "scene:undo-record", "Transform Update"); +try { + // 执行多个属性修改 + Editor.Ipc.sendToPanel("scene", "scene:undo-commit"); +} catch (e) { + Editor.Ipc.sendToPanel("scene", "scene:undo-cancel"); +} +``` + +--- + +## 6. 功能特定规则 (Feature Specifics) + +### 6.1 粒子系统 (VFX) + +```javascript +// 必须设置 custom = true,否则属性修改可能不生效 +particleSystem.custom = true; + +// 确保纹理有效,否则粒子不可见 +if (!particleSystem.texture && !particleSystem.file) { + // 加载默认纹理 +} +``` + +### 6.2 资源路径解析 + +```javascript +// 内置资源可能需要多个路径尝试 +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" +]; + +for (const path of defaultPaths) { + const uuid = Editor.assetdb.urlToUuid(path); + if (uuid) break; +} +``` + +### 6.3 场景操作时序 + +```javascript +// 场景操作后需要延迟通知 UI 刷新 +newNode.parent = parent; +Editor.Ipc.sendToMain("scene:dirty"); + +// 使用 setTimeout 让出主循环 +setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: newNode.uuid, + parentUuid: parent.uuid, + }); +}, 10); +``` + +--- + +## 7. 错误处理规范 (Error Handling) + +### 7.1 回调风格统一 + +```javascript +// ✅ 标准风格 +callback(null, result); // 成功 +callback("Error message"); // 失败 (字符串) +callback(new Error("message")); // 失败 (Error 对象) + +// 避免混用 +callback(err, null); // 不推荐,保持一致性 +``` + +### 7.2 异步操作错误处理 + +```javascript +Editor.assetdb.queryInfoByUrl(path, (err, info) => { + if (err) { + addLog("error", `查询资源失败: ${err.message}`); + return callback(`Failed to get info: ${err.message}`); + } + // 继续处理... +}); +``` + +--- + +## 8. 提交规范 (Git Commit) + +使用 [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` | diff --git a/README.md b/README.md index 009979b..f22d757 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ### 启动 1. 打开 Cocos Creator 编辑器 -2. 在菜单栏选择 `Packages/MCP Bridge/Open Test Panel` 打开测试面板 +2. 在菜单栏选择 `MCP Bridge/Open Panel` 打开测试面板 3. 在面板中点击 "Start" 按钮启动服务 4. 服务默认运行在端口 3456 上 @@ -284,6 +284,21 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j - `duration`, `emissionRate`, `life`, `lifeVar`, `startColor`, `endColor` - `startSize`, `endSize`, `speed`, `angle`, `gravity`, `file` (plist/texture) +### 25. manage_animation + +- **描述**: 管理节点的动画组件 +- **参数**: + - `action`: 操作类型 (`get_list`, `get_info`, `play`, `stop`, `pause`, `resume`) + - `nodeId`: 节点 UUID + - `clipName`: 动画剪辑名称 (用于 `play` 操作,可选,默认播放 defaultClip) + +### 26. get_sha + +- **描述**: 获取指定文件的 SHA-256 哈希值 +- **参数**: + - `path`: 文件路径,如 `db://assets/scripts/Test.ts` + + ## 技术实现 ### 架构设计 diff --git a/main.js b/main.js index f5de666..ddb975f 100644 --- a/main.js +++ b/main.js @@ -4,6 +4,7 @@ const { IpcManager } = require("./dist/IpcManager"); const http = require("http"); const path = require("path"); const fs = require("fs"); +const crypto = require("crypto"); let logBuffer = []; // 存储所有日志 let mcpServer = null; @@ -449,6 +450,34 @@ const getToolsList = () => { }, required: ["action"] } + }, + { + name: "get_sha", + description: "获取指定文件的 SHA-256 哈希值", + inputSchema: { + type: "object", + properties: { + path: { type: "string", description: "文件路径,如 db://assets/scripts/Test.ts" } + }, + required: ["path"] + } + }, + { + name: "manage_animation", + description: "管理节点的动画组件", + inputSchema: { + type: "object", + properties: { + action: { + type: "string", + enum: ["get_list", "get_info", "play", "stop", "pause", "resume"], + description: "操作类型" + }, + nodeId: { type: "string", description: "节点 UUID" }, + clipName: { type: "string", description: "动画剪辑名称 (用于 play)" } + }, + required: ["action", "nodeId"] + } } ]; }; @@ -707,17 +736,10 @@ module.exports = { case "save_scene": isSceneBusy = true; addLog("info", "Preparing to save scene... Waiting for UI sync."); - // 强制延迟保存,防止死锁 - setTimeout(() => { - // 使用 stash-and-save 替代 save-scene,这更接近 Ctrl+S 的行为 - Editor.Ipc.sendToMain("scene:stash-and-save"); - addLog("info", "Executing Safe Save (Stash)..."); - setTimeout(() => { - isSceneBusy = false; - addLog("info", "Safe Save completed."); - callback(null, "Scene saved successfully."); - }, 1000); - }, 500); + Editor.Ipc.sendToPanel("scene", "scene:stash-and-save"); + isSceneBusy = false; + addLog("info", "Safe Save completed."); + callback(null, "Scene saved successfully."); break; case "get_scene_hierarchy": @@ -798,6 +820,12 @@ module.exports = { case "manage_editor": this.manageEditor(args, callback); break; + case "get_sha": + this.getSha(args, callback); + break; + case "manage_animation": + this.manageAnimation(args, callback); + break; case "find_gameobjects": Editor.Scene.callSceneScript("mcp-bridge", "find-gameobjects", args, callback); @@ -1865,4 +1893,30 @@ export default class NewScript extends cc.Component { callback(`Undo operation failed: ${err.message}`); } }, + + // 获取文件 SHA-256 + getSha(args, callback) { + const { path: url } = args; + const fspath = Editor.assetdb.urlToFspath(url); + + if (!fspath || !fs.existsSync(fspath)) { + return callback(`File not found: ${url}`); + } + + try { + const fileBuffer = fs.readFileSync(fspath); + const hashSum = crypto.createHash('sha256'); + hashSum.update(fileBuffer); + const sha = hashSum.digest('hex'); + callback(null, { path: url, sha: sha }); + } catch (err) { + callback(`Failed to calculate SHA: ${err.message}`); + } + }, + + // 管理动画 + manageAnimation(args, callback) { + // 转发给场景脚本处理 + Editor.Scene.callSceneScript("mcp-bridge", "manage-animation", args, callback); + }, }; diff --git a/scene-script.js b/scene-script.js index 24ac839..0b9b235 100644 --- a/scene-script.js +++ b/scene-script.js @@ -750,4 +750,82 @@ module.exports = { if (event.reply) event.reply(new Error(`Unknown VFX action: ${action}`)); } }, + + "manage-animation": function (event, args) { + const { action, nodeId, clipName } = args; + const node = cc.engine.getInstanceById(nodeId); + + if (!node) { + if (event.reply) event.reply(new Error(`Node not found: ${nodeId}`)); + return; + } + + const anim = node.getComponent(cc.Animation); + if (!anim) { + if (event.reply) event.reply(new Error(`Animation component not found on node: ${nodeId}`)); + return; + } + + switch (action) { + case "get_list": + const clips = anim.getClips(); + const clipList = clips.map(c => ({ + name: c.name, + duration: c.duration, + sample: c.sample, + speed: c.speed, + wrapMode: c.wrapMode + })); + if (event.reply) event.reply(null, clipList); + break; + + case "get_info": + const currentClip = anim.currentClip; + let isPlaying = false; + // [安全修复] 只有在有当前 Clip 时才获取状态,避免 Animation 组件无 Clip 时的崩溃 + if (currentClip) { + const state = anim.getAnimationState(currentClip.name); + if (state) { + isPlaying = state.isPlaying; + } + } + const info = { + currentClip: currentClip ? currentClip.name : null, + clips: anim.getClips().map(c => c.name), + playOnLoad: anim.playOnLoad, + isPlaying: isPlaying + }; + if (event.reply) event.reply(null, info); + break; + + case "play": + if (!clipName) { + anim.play(); + if (event.reply) event.reply(null, "Playing default clip"); + } else { + anim.play(clipName); + if (event.reply) event.reply(null, `Playing clip: ${clipName}`); + } + break; + + case "stop": + anim.stop(); + if (event.reply) event.reply(null, "Animation stopped"); + break; + + case "pause": + anim.pause(); + if (event.reply) event.reply(null, "Animation paused"); + break; + + case "resume": + anim.resume(); + if (event.reply) event.reply(null, "Animation resumed"); + break; + + default: + if (event.reply) event.reply(new Error(`Unknown animation action: ${action}`)); + break; + } + }, };