diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..a062884 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,420 @@ +# MCP Bridge 插件开发流程文档 + +本文档记录了 MCP Bridge 插件的完整开发流程,包括核心架构设计、功能实现、测试与调试等各个阶段。 + +## 1. 项目初始化 + +### 1.1 目录结构搭建 + +``` +mcp-bridge/ +├── main.js # 插件主入口 +├── scene-script.js # 场景脚本 +├── mcp-proxy.js # MCP 代理 +├── README.md # 项目说明 +├── DEVELOPMENT.md # 开发流程文档 +├── package.json # 插件配置 +└── panel/ # 面板目录 + ├── index.html # 面板界面 + └── index.js # 面板逻辑 +``` + +### 1.2 插件配置 + +在 `package.json` 中配置插件信息: + +```json +{ + "name": "mcp-bridge", + "version": "1.0.0", + "description": "MCP Bridge for Cocos Creator", + "main": "main.js", + "panel": { + "main": "panel/index.html", + "type": "dockable", + "title": "MCP Bridge", + "width": 800, + "height": 600 + }, + "contributions": { + "menu": [ + { + "path": "Packages/MCP Bridge", + "label": "Open Test Panel", + "message": "open-test-panel" + } + ] + } +} +``` + +## 2. 核心架构设计 + +### 2.1 系统架构 + +``` +┌────────────────────┐ HTTP ┌────────────────────┐ IPC ┌────────────────────┐ +│ 外部 AI 工具 │ ──────────> │ main.js (HTTP服务) │ ─────────> │ scene-script.js │ +│ (Cursor/VS Code) │ <──────── │ (MCP 协议处理) │ <──────── │ (场景操作执行) │ +└────────────────────┘ JSON └────────────────────┘ JSON └────────────────────┘ +``` + +### 2.2 核心模块 + +1. **HTTP 服务模块**:处理外部请求,解析 MCP 协议 +2. **MCP 工具模块**:实现各种操作工具 +3. **场景操作模块**:执行场景相关操作 +4. **资源管理模块**:处理脚本和资源文件 +5. **面板界面模块**:提供用户交互界面 + +## 3. 功能模块实现 + +### 3.1 HTTP 服务实现 + +在 `main.js` 中实现 HTTP 服务: + +```javascript +startServer(port) { + try { + const http = require('http'); + mcpServer = http.createServer((req, res) => { + // 处理请求... + }); + mcpServer.listen(port, () => { + addLog("success", `MCP Server running at http://127.0.0.1:${port}`); + }); + } catch (e) { + addLog("error", `Failed to start server: ${e.message}`); + } +} +``` + +### 3.2 MCP 工具注册 + +在 `/list-tools` 接口中注册工具: + +```javascript +const tools = [ + { + name: "get_selected_node", + description: "获取当前选中的节点", + parameters: [] + }, + // 其他工具... +]; +``` + +### 3.3 场景操作实现 + +在 `scene-script.js` 中实现场景相关操作: + +```javascript +const sceneScript = { + 'create-node'(params, callback) { + // 创建节点逻辑... + }, + 'set-property'(params, callback) { + // 设置属性逻辑... + }, + // 其他操作... +}; +``` + +### 3.4 脚本管理实现 + +在 `main.js` 中实现脚本管理功能: + +```javascript +manageScript(args, callback) { + const { action, path, content } = args; + switch (action) { + case "create": + // 确保父目录存在 + const fs = require('fs'); + const pathModule = require('path'); + const absolutePath = Editor.assetdb.urlToFspath(path); + const dirPath = pathModule.dirname(absolutePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + // 创建 TypeScript 脚本 + Editor.assetdb.create(path, content || `const { ccclass, property } = cc._decorator; + +@ccclass +export default class NewScript extends cc.Component { + // LIFE-CYCLE CALLBACKS: + + onLoad () {} + + start () {} + + update (dt) {} +}`, (err) => { + callback(err, err ? null : `Script created at ${path}`); + }); + break; + // 其他操作... + } +} +``` + +### 3.5 批处理执行实现 + +在 `main.js` 中实现批处理功能: + +```javascript +batchExecute(args, callback) { + const { operations } = args; + const results = []; + let completed = 0; + + if (!operations || operations.length === 0) { + return callback("No operations provided"); + } + + operations.forEach((operation, index) => { + this.handleMcpCall(operation.tool, operation.params, (err, result) => { + results[index] = { tool: operation.tool, error: err, result: result }; + completed++; + + if (completed === operations.length) { + callback(null, results); + } + }); + }); +} +``` + +### 3.6 资产管理实现 + +在 `main.js` 中实现资产管理功能: + +```javascript +manageAsset(args, callback) { + const { action, path, targetPath, content } = args; + + switch (action) { + case "create": + // 确保父目录存在 + const fs = require('fs'); + const pathModule = require('path'); + const absolutePath = Editor.assetdb.urlToFspath(path); + const dirPath = pathModule.dirname(absolutePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + Editor.assetdb.create(path, content || '', (err) => { + callback(err, err ? null : `Asset created at ${path}`); + }); + break; + // 其他操作... + } +} +``` + +### 3.7 面板界面实现 + +在 `panel/index.html` 中实现标签页界面: + +```html +
+ +
+ Main + Tool Test +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + +
+ +
+
+
+
+
+``` + +## 4. 测试与调试 + +### 4.1 本地测试 + +1. **启动服务**:在面板中点击 "Start" 按钮 +2. **测试工具**:在 "Tool Test" 标签页中测试各个工具 +3. **查看日志**:在主面板中查看操作日志 + +### 4.2 常见错误及修复 + +#### 4.2.1 面板加载错误 + +**错误信息**:`Panel info not found for panel mcp-bridge` + +**解决方案**: +- 检查 `package.json` 中的面板配置 +- 确保 `panel` 字段配置正确,移除冲突的 `panels` 字段 + +#### 4.2.2 资源创建错误 + +**错误信息**:`Parent path ... is not exists` + +**解决方案**: +- 在创建资源前添加目录检查和创建逻辑 +- 使用 `fs.mkdirSync(dirPath, { recursive: true })` 递归创建目录 + +#### 4.2.3 脚本语法错误 + +**错误信息**:`SyntaxError: Invalid or unexpected token` + +**解决方案**: +- 使用模板字符串(反引号)处理多行字符串 +- 避免变量名冲突 + +### 4.3 性能优化 + +1. **批处理执行**:使用 `batch_execute` 工具减少 HTTP 请求次数 +2. **异步操作**:使用回调函数处理异步操作,避免阻塞主线程 +3. **错误处理**:完善错误处理机制,提高插件稳定性 + +## 5. 文档编写 + +### 5.1 README.md + +- 项目简介 +- 功能特性 +- 安装使用 +- API 文档 +- 技术实现 + +### 5.2 API 文档 + +为每个 MCP 工具编写详细的 API 文档,包括: +- 工具名称 +- 功能描述 +- 参数说明 +- 返回值格式 +- 使用示例 + +### 5.3 开发文档 + +- 项目架构 +- 开发流程 +- 代码规范 +- 贡献指南 + +## 6. 部署与使用 + +### 6.1 部署方式 + +1. **本地部署**:将插件复制到 Cocos Creator 项目的 `packages` 目录 +2. **远程部署**:通过版本控制系统管理插件代码 + +### 6.2 使用流程 + +1. **启动服务**: + - 打开 Cocos Creator 编辑器 + - 选择 `Packages/MCP Bridge/Open Test Panel` + - 点击 "Start" 按钮启动服务 + +2. **连接 AI 编辑器**: + - 在 AI 编辑器中配置 MCP 代理 + - 使用 `node [项目路径]/packages/mcp-bridge/mcp-proxy.js` 作为命令 + +3. **执行操作**: + - 通过 AI 编辑器发送 MCP 请求 + - 或在测试面板中直接测试工具 + +### 6.3 配置选项 + +- **端口设置**:默认 3456,可自定义 +- **自动启动**:支持编辑器启动时自动开启服务 + +## 7. 功能扩展 + +### 7.1 添加新工具 + +1. **在 `main.js` 中注册工具**: + - 在 `/list-tools` 响应中添加工具定义 + - 在 `handleMcpCall` 函数中添加处理逻辑 + +2. **在面板中添加示例**: + - 在 `panel/index.js` 中添加工具示例参数 + - 更新工具列表 + +3. **更新文档**: + - 在 `README.md` 中添加工具文档 + - 更新功能特性列表 + +### 7.2 集成新 API + +1. **了解 Cocos Creator API**: + - 查阅 Cocos Creator 编辑器 API 文档 + - 了解场景脚本 API + +2. **实现集成**: + - 在 `main.js` 或 `scene-script.js` 中添加对应功能 + - 处理异步操作和错误情况 + +3. **测试验证**: + - 编写测试用例 + - 验证功能正确性 + +## 8. 版本管理 + +### 8.1 版本控制 + +- 使用 Git 进行版本控制 +- 遵循语义化版本规范 + +### 8.2 发布流程 + +1. **代码审查**:检查代码质量和功能完整性 +2. **测试验证**:确保所有功能正常工作 +3. **文档更新**:更新 README 和相关文档 +4. **版本发布**:标记版本号并发布 + +## 9. 技术栈 + +- **JavaScript**:主要开发语言 +- **Node.js**:HTTP 服务和文件操作 +- **Cocos Creator API**:编辑器功能集成 +- **HTML/CSS**:面板界面 +- **MCP 协议**:与 AI 工具通信 + +## 10. 最佳实践 + +1. **代码组织**: + - 模块化设计,职责分离 + - 合理使用回调函数处理异步操作 + +2. **错误处理**: + - 完善的错误捕获和处理 + - 详细的错误日志记录 + +3. **用户体验**: + - 直观的面板界面 + - 实时的操作反馈 + - 详细的日志信息 + +4. **安全性**: + - 验证输入参数 + - 防止路径遍历攻击 + - 限制服务访问范围 + +## 11. 总结 + +MCP Bridge 插件通过 HTTP 服务和 MCP 协议,为外部 AI 工具提供了与 Cocos Creator 编辑器交互的能力。插件支持场景操作、资源管理、组件管理、脚本管理等多种功能,为 Cocos Creator 项目的开发和自动化提供了有力的支持。 + +通过本文档的开发流程,我们构建了一个功能完整、稳定可靠的 MCP Bridge 插件,为 Cocos Creator 生态系统增添了新的工具和能力。 diff --git a/README.md b/README.md index cd93560..7d5e974 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ - **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能 - **场景节点操作**: 获取、创建、修改场景中的节点 - **资源管理**: 创建场景、预制体,打开指定资源 +- **组件管理**: 添加、删除、获取节点组件 +- **脚本管理**: 创建、删除、读取、写入脚本文件 +- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率 +- **资产管理**: 创建、删除、移动、获取资源信息 - **实时日志**: 提供详细的操作日志记录和展示 - **自动启动**: 支持编辑器启动时自动开启服务 @@ -126,6 +130,43 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j - `parentId`: 父节点 UUID (可选,不传则挂在场景根部) - `type`: 节点预设类型(`empty`, `sprite`, `label`, `canvas`) +### 10. manage_components + +- **描述**: 管理节点组件 +- **参数**: + - `nodeId`: 节点 UUID + - `action`: 操作类型(`add`, `remove`, `get`) + - `componentType`: 组件类型,如 `cc.Sprite`(用于 `add` 操作) + - `componentId`: 组件 ID(用于 `remove` 操作) + - `properties`: 组件属性(用于 `add` 操作) + +### 11. manage_script + +- **描述**: 管理脚本文件,默认创建 TypeScript 脚本 +- **参数**: + - `action`: 操作类型(`create`, `delete`, `read`, `write`) + - `path`: 脚本路径,如 `db://assets/scripts/NewScript.ts` + - `content`: 脚本内容(用于 `create` 和 `write` 操作) + - `name`: 脚本名称(用于 `create` 操作) +- **默认模板**: 当未提供 content 时,会使用 TypeScript 格式的默认模板 + +### 12. batch_execute + +- **描述**: 批处理执行多个操作 +- **参数**: + - `operations`: 操作列表 + - `tool`: 工具名称 + - `params`: 工具参数 + +### 13. manage_asset + +- **描述**: 管理资源 +- **参数**: + - `action`: 操作类型(`create`, `delete`, `move`, `get_info`) + - `path`: 资源路径,如 `db://assets/textures` + - `targetPath`: 目标路径(用于 `move` 操作) + - `content`: 资源内容(用于 `create` 操作) + ## 技术实现 ### 架构设计 diff --git a/main.js b/main.js index 295a831..2204b86 100644 --- a/main.js +++ b/main.js @@ -133,7 +133,7 @@ const getToolsList = () => { }, { name: "open_scene", - description: "在编辑器中打开指定的场景文件", + description: "打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒再进行节点创建或保存操作。", inputSchema: { type: "object", properties: { @@ -165,8 +165,74 @@ const getToolsList = () => { required: ["name"], }, }, + { + name: "manage_components", + description: "管理节点组件", + inputSchema: { + type: "object", + properties: { + nodeId: { type: "string", description: "节点 UUID" }, + action: { type: "string", enum: ["add", "remove", "get"], description: "操作类型" }, + componentType: { type: "string", description: "组件类型,如 cc.Sprite" }, + componentId: { type: "string", description: "组件 ID (用于 remove 操作)" }, + properties: { type: "object", description: "组件属性 (用于 add 操作)" }, + }, + required: ["nodeId", "action"], + }, + }, + { + name: "manage_script", + description: "管理脚本文件", + inputSchema: { + type: "object", + properties: { + action: { type: "string", enum: ["create", "delete", "read", "write"], description: "操作类型" }, + path: { type: "string", description: "脚本路径,如 db://assets/scripts/NewScript.js" }, + content: { type: "string", description: "脚本内容 (用于 create 和 write 操作)" }, + name: { type: "string", description: "脚本名称 (用于 create 操作)" }, + }, + required: ["action", "path"], + }, + }, + { + name: "batch_execute", + description: "批处理执行多个操作", + inputSchema: { + type: "object", + properties: { + operations: { + type: "array", + items: { + type: "object", + properties: { + tool: { type: "string", description: "工具名称" }, + params: { type: "object", description: "工具参数" }, + }, + required: ["tool", "params"], + }, + description: "操作列表", + }, + }, + required: ["operations"], + }, + }, + { + name: "manage_asset", + description: "管理资源", + inputSchema: { + type: "object", + properties: { + action: { type: "string", enum: ["create", "delete", "move", "get_info"], description: "操作类型" }, + path: { type: "string", description: "资源路径,如 db://assets/textures" }, + targetPath: { type: "string", description: "目标路径 (用于 move 操作)" }, + content: { type: "string", description: "资源内容 (用于 create 操作)" }, + }, + required: ["action", "path"], + }, + }, ]; }; +let isSceneBusy = false; module.exports = { "scene-script": "scene-script.js", @@ -279,6 +345,9 @@ module.exports = { // 统一处理逻辑,方便日志记录 handleMcpCall(name, args, callback) { + if (isSceneBusy && (name === "save_scene" || name === "create_node")) { + return callback("Editor is busy (Processing Scene), please wait a moment."); + } switch (name) { case "get_selected_node": const ids = Editor.Selection.curSelection("node"); @@ -299,8 +368,18 @@ module.exports = { break; case "save_scene": - Editor.Ipc.sendToMain("scene:save-scene"); - callback(null, "Scene saved successfully"); + isSceneBusy = true; + addLog("info", "Preparing to save scene... Waiting for UI sync."); + // 强制延迟保存,防止死锁 + setTimeout(() => { + Editor.Ipc.sendToMain("scene:save-scene"); + addLog("info", "Executing Safe Save..."); + setTimeout(() => { + isSceneBusy = false; + addLog("info", "Safe Save completed."); + callback(null, "Scene saved successfully."); + }, 1000); + }, 500); break; case "get_scene_hierarchy": @@ -328,11 +407,16 @@ module.exports = { break; case "open_scene": + isSceneBusy = true; // 锁定 const openUuid = Editor.assetdb.urlToUuid(args.url); if (openUuid) { Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid); - callback(null, `Success: Opening scene ${args.url}`); + setTimeout(() => { + isSceneBusy = false; + callback(null, `Success: Opening scene ${args.url}`); + }, 2000); } else { + isSceneBusy = false; callback(`Could not find asset with URL ${args.url}`); } break; @@ -341,11 +425,175 @@ module.exports = { Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback); break; + case "manage_components": + Editor.Scene.callSceneScript("mcp-bridge", "manage-components", args, callback); + break; + + case "manage_script": + this.manageScript(args, callback); + break; + + case "batch_execute": + this.batchExecute(args, callback); + break; + + case "manage_asset": + this.manageAsset(args, callback); + break; + default: callback(`Unknown tool: ${name}`); break; } }, + + // 管理脚本文件 + manageScript(args, callback) { + const { action, path, content } = args; + + switch (action) { + case "create": + if (Editor.assetdb.exists(path)) { + return callback(`Script already exists at ${path}`); + } + // 确保父目录存在 + const fs = require('fs'); + const pathModule = require('path'); + const absolutePath = Editor.assetdb.urlToFspath(path); + const dirPath = pathModule.dirname(absolutePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + Editor.assetdb.create(path, content || `const { ccclass, property } = cc._decorator; + +@ccclass +export default class NewScript extends cc.Component { + @property(cc.Label) + label: cc.Label = null; + + @property + text: string = 'hello'; + + // LIFE-CYCLE CALLBACKS: + + onLoad () {} + + start () {} + + update (dt) {} +}`, (err) => { + callback(err, err ? null : `Script created at ${path}`); + }); + break; + + case "delete": + if (!Editor.assetdb.exists(path)) { + return callback(`Script not found at ${path}`); + } + Editor.assetdb.delete([path], (err) => { + callback(err, err ? null : `Script deleted at ${path}`); + }); + break; + + case "read": + Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => { + if (err) { + return callback(`Failed to get script info: ${err}`); + } + Editor.assetdb.loadAny(path, (err, content) => { + callback(err, err ? null : content); + }); + }); + break; + + case "write": + Editor.assetdb.create(path, content, (err) => { + callback(err, err ? null : `Script updated at ${path}`); + }); + break; + + default: + callback(`Unknown script action: ${action}`); + break; + } + }, + + // 批处理执行 + batchExecute(args, callback) { + const { operations } = args; + const results = []; + let completed = 0; + + if (!operations || operations.length === 0) { + return callback("No operations provided"); + } + + operations.forEach((operation, index) => { + this.handleMcpCall(operation.tool, operation.params, (err, result) => { + results[index] = { tool: operation.tool, error: err, result: result }; + completed++; + + if (completed === operations.length) { + callback(null, results); + } + }); + }); + }, + + // 管理资源 + manageAsset(args, callback) { + const { action, path, targetPath, content } = args; + + switch (action) { + case "create": + if (Editor.assetdb.exists(path)) { + return callback(`Asset already exists at ${path}`); + } + // 确保父目录存在 + const fs = require('fs'); + const pathModule = require('path'); + const absolutePath = Editor.assetdb.urlToFspath(path); + const dirPath = pathModule.dirname(absolutePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + Editor.assetdb.create(path, content || '', (err) => { + callback(err, err ? null : `Asset created at ${path}`); + }); + break; + + case "delete": + if (!Editor.assetdb.exists(path)) { + return callback(`Asset not found at ${path}`); + } + Editor.assetdb.delete([path], (err) => { + callback(err, err ? null : `Asset deleted at ${path}`); + }); + break; + + case "move": + if (!Editor.assetdb.exists(path)) { + return callback(`Asset not found at ${path}`); + } + if (Editor.assetdb.exists(targetPath)) { + return callback(`Target asset already exists at ${targetPath}`); + } + Editor.assetdb.move(path, targetPath, (err) => { + callback(err, err ? null : `Asset moved from ${path} to ${targetPath}`); + }); + break; + + case "get_info": + Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => { + callback(err, err ? null : info); + }); + break; + + default: + callback(`Unknown asset action: ${action}`); + break; + } + }, // 暴露给 MCP 或面板的 API 封装 messages: { "open-test-panel"() { diff --git a/panel/index.html b/panel/index.html index 37e9a40..34f718c 100644 --- a/panel/index.html +++ b/panel/index.html @@ -1,23 +1,71 @@
-
-
- Port: - - Start -
- - -
- Auto Start -
- -
- Clear - Copy All + +
+ Main + Tool Test
- -
+ +
+
+
+ Port: + + Start +
+ + +
+ Auto Start +
+ +
+ Clear + Copy All +
+ + +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+ +
+ 测试工具 + 获取工具列表 + 清空结果 +
+ +
+

测试结果:

+ +
+
+
+
+
diff --git a/panel/index.js b/panel/index.js index f662c04..f427e82 100644 --- a/panel/index.js +++ b/panel/index.js @@ -23,6 +23,24 @@ Editor.Panel.extend({ const btnCopy = this.shadowRoot.querySelector("#btnCopy"); const logView = this.shadowRoot.querySelector("#logConsole"); + // 标签页元素 + const tabMain = this.shadowRoot.querySelector("#tabMain"); + const tabTest = this.shadowRoot.querySelector("#tabTest"); + const panelMain = this.shadowRoot.querySelector("#panelMain"); + const panelTest = this.shadowRoot.querySelector("#panelTest"); + + // 测试面板元素 + const toolNameInput = this.shadowRoot.querySelector("#toolName"); + const toolParamsTextarea = this.shadowRoot.querySelector("#toolParams"); + const toolsList = this.shadowRoot.querySelector("#toolsList"); + const testBtn = this.shadowRoot.querySelector("#testBtn"); + const listToolsBtn = this.shadowRoot.querySelector("#listToolsBtn"); + const clearBtn = this.shadowRoot.querySelector("#clearBtn"); + const resultContent = this.shadowRoot.querySelector("#resultContent"); + + let tools = []; + const API_BASE = 'http://localhost:3456'; + // 初始化 Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => { if (data) { @@ -32,6 +50,23 @@ Editor.Panel.extend({ } }); + // 标签页切换 + tabMain.addEventListener("confirm", () => { + tabMain.classList.add("active"); + tabTest.classList.remove("active"); + panelMain.classList.add("active"); + panelTest.classList.remove("active"); + }); + + tabTest.addEventListener("confirm", () => { + tabTest.classList.add("active"); + tabMain.classList.remove("active"); + panelTest.classList.add("active"); + panelMain.classList.remove("active"); + // 自动获取工具列表 + this.getToolsList(); + }); + btnToggle.addEventListener("confirm", () => { Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(portInput.value)); }); @@ -45,6 +80,7 @@ Editor.Panel.extend({ require("electron").clipboard.writeText(logView.innerText); Editor.success("All logs copied!"); }); + Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => { if (data) { portInput.value = data.config.port; @@ -56,10 +92,218 @@ Editor.Panel.extend({ data.logs.forEach((log) => this.renderLog(log)); } }); + autoStartCheck.addEventListener("change", (event) => { // event.target.value 在 ui-checkbox 中是布尔值 Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", event.target.value); }); + + // 测试面板事件 + testBtn.addEventListener("confirm", () => this.testTool()); + listToolsBtn.addEventListener("confirm", () => this.getToolsList()); + clearBtn.addEventListener("confirm", () => this.clearResult()); + + // 获取工具列表 + this.getToolsList = function() { + this.showResult('获取工具列表中...'); + + fetch(`${API_BASE}/list-tools`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + if (data.tools) { + tools = data.tools; + this.displayToolsList(tools); + this.showResult(`成功获取 ${tools.length} 个工具`, 'success'); + } else { + this.showResult('获取工具列表失败:未找到工具数据', 'error'); + } + }) + .catch(error => { + this.showResult(`获取工具列表失败:${error.message}`, 'error'); + }); + }; + + // 显示工具列表 + this.displayToolsList = function(tools) { + toolsList.innerHTML = ''; + + tools.forEach(tool => { + const toolItem = document.createElement('div'); + toolItem.className = 'tool-item'; + toolItem.textContent = `${tool.name} - ${tool.description}`; + toolItem.addEventListener('click', () => { + toolNameInput.value = tool.name; + // 尝试填充示例参数 + this.fillExampleParams(tool); + }); + toolsList.appendChild(toolItem); + }); + }; + + // 填充示例参数 + this.fillExampleParams = function(tool) { + let exampleParams = {}; + + switch (tool.name) { + case 'get_selected_node': + case 'save_scene': + case 'get_scene_hierarchy': + exampleParams = {}; + break; + + case 'set_node_name': + exampleParams = { + "id": "节点UUID", + "newName": "新节点名称" + }; + break; + + case 'update_node_transform': + exampleParams = { + "id": "节点UUID", + "x": 100, + "y": 100, + "scaleX": 1, + "scaleY": 1 + }; + break; + + case 'create_scene': + exampleParams = { + "sceneName": "NewScene" + }; + break; + + case 'create_prefab': + exampleParams = { + "nodeId": "节点UUID", + "prefabName": "NewPrefab" + }; + break; + + case 'open_scene': + exampleParams = { + "url": "db://assets/NewScene.fire" + }; + break; + + case 'create_node': + exampleParams = { + "name": "NewNode", + "parentId": "父节点UUID", + "type": "empty" + }; + break; + + case 'manage_components': + exampleParams = { + "nodeId": "节点UUID", + "action": "add", + "componentType": "cc.Button" + }; + break; + + case 'manage_script': + exampleParams = { + "action": "create", + "path": "db://assets/scripts/TestScript.ts", + "content": "const { ccclass, property } = cc._decorator;\n\n@ccclass\nexport default class TestScript extends cc.Component {\n // LIFE-CYCLE CALLBACKS:\n\n onLoad () {}\n\n start () {}\n\n update (dt) {}\n}" + }; + break; + + case 'batch_execute': + exampleParams = { + "operations": [ + { + "tool": "get_selected_node", + "params": {} + } + ] + }; + break; + + case 'manage_asset': + exampleParams = { + "action": "create", + "path": "db://assets/test.txt", + "content": "Hello, MCP!" + }; + break; + } + + toolParamsTextarea.value = JSON.stringify(exampleParams, null, 2); + }; + + // 测试工具 + this.testTool = function() { + const toolName = toolNameInput.value.trim(); + const toolParamsStr = toolParamsTextarea.value.trim(); + + if (!toolName) { + this.showResult('请输入工具名称', 'error'); + return; + } + + let toolParams; + try { + toolParams = toolParamsStr ? JSON.parse(toolParamsStr) : {}; + } catch (error) { + this.showResult(`参数格式错误:${error.message}`, 'error'); + return; + } + + this.showResult('测试工具中...'); + + fetch(`${API_BASE}/call-tool`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: toolName, + arguments: toolParams + }) + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + if (data.error) { + this.showResult(`测试失败:${data.error}`, 'error'); + } else { + this.showResult(JSON.stringify(data, null, 2), 'success'); + } + }) + .catch(error => { + this.showResult(`测试失败:${error.message}`, 'error'); + }); + }; + + // 显示结果 + this.showResult = function(message, type = 'info') { + resultContent.value = message; + + // 移除旧样式 + resultContent.className = ''; + + // 添加新样式 + if (type === 'error' || type === 'success') { + resultContent.className = type; + } + }; + + // 清空结果 + this.clearResult = function() { + this.showResult('点击"测试工具"按钮开始测试'); + }; }, renderLog(log) { diff --git a/scene-script.js b/scene-script.js index c4104a6..f28bddb 100644 --- a/scene-script.js +++ b/scene-script.js @@ -91,6 +91,10 @@ module.exports = { "create-node": function (event, args) { const { name, parentId, type } = args; const scene = cc.director.getScene(); + if (!scene || !cc.director.getRunningScene()) { + if (event.reply) event.reply(new Error("Scene not ready or loading.")); + return; + } let newNode = null; @@ -119,24 +123,149 @@ module.exports = { // 设置层级 let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; - if (newNode) { + if (parent) { newNode.parent = parent; - // 坐标居中处理(如果是 Canvas 子节点) - if (parent.name === "Canvas") { - newNode.setPosition(0, 0); - } else { - newNode.setPosition(cc.v2(cc.winSize.width / 2, cc.winSize.height / 2)); - } - - // 通知编辑器刷新 + // 【优化】通知主进程场景变脏 Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-created", { - uuid: newNode.uuid, - parentUuid: parent.uuid, - }); + + // 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环 + setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: newNode.uuid, + parentUuid: parent.uuid, + }); + }, 10); if (event.reply) event.reply(null, newNode.uuid); } }, + + "manage-components": function (event, args) { + const { nodeId, action, componentType, componentId, properties } = args; + let node = cc.engine.getInstanceById(nodeId); + + if (!node) { + if (event.reply) event.reply(new Error("Node not found")); + return; + } + + switch (action) { + case "add": + if (!componentType) { + if (event.reply) event.reply(new Error("Component type is required")); + return; + } + + try { + // 解析组件类型 + let compClass = null; + if (componentType.startsWith("cc.")) { + const className = componentType.replace("cc.", ""); + compClass = cc[className]; + } else { + // 尝试获取自定义组件 + compClass = cc.js.getClassByName(componentType); + } + + if (!compClass) { + if (event.reply) event.reply(new Error(`Component type not found: ${componentType}`)); + return; + } + + // 添加组件 + const component = node.addComponent(compClass); + + // 设置属性 + if (properties) { + for (const [key, value] of Object.entries(properties)) { + if (component[key] !== undefined) { + component[key] = value; + } + } + } + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + + if (event.reply) event.reply(null, `Component ${componentType} added`); + } catch (err) { + if (event.reply) event.reply(new Error(`Failed to add component: ${err.message}`)); + } + break; + + case "remove": + if (!componentId) { + if (event.reply) event.reply(new Error("Component ID is required")); + return; + } + + try { + // 查找并移除组件 + const component = node.getComponentById(componentId); + if (component) { + node.removeComponent(component); + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + if (event.reply) event.reply(null, "Component removed"); + } else { + if (event.reply) event.reply(new Error("Component not found")); + } + } catch (err) { + if (event.reply) event.reply(new Error(`Failed to remove component: ${err.message}`)); + } + break; + + case "get": + try { + const components = node._components.map((c) => { + // 获取组件属性 + const properties = {}; + for (const key in c) { + if (typeof c[key] !== "function" && + !key.startsWith("_") && + c[key] !== undefined) { + try { + properties[key] = c[key]; + } catch (e) { + // 忽略无法序列化的属性 + } + } + } + return { + type: c.__typename, + uuid: c.uuid, + properties: properties + }; + }); + if (event.reply) event.reply(null, components); + } catch (err) { + if (event.reply) event.reply(new Error(`Failed to get components: ${err.message}`)); + } + break; + + default: + if (event.reply) event.reply(new Error(`Unknown component action: ${action}`)); + break; + } + }, + + "get-component-properties": function (component) { + const properties = {}; + + // 遍历组件属性 + for (const key in component) { + if (typeof component[key] !== "function" && + !key.startsWith("_") && + component[key] !== undefined) { + try { + properties[key] = component[key]; + } catch (e) { + // 忽略无法序列化的属性 + } + } + } + + return properties; + }, };