diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 179408f..2234dea 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -37,26 +37,26 @@ mcp-bridge/ ```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" - } - ] - } + "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" + } + ] + } } ``` @@ -104,23 +104,30 @@ startServer(port) { ## 4. 开发历程与里程碑 ### 2026-02-10: Undo 系统深度修复与规范化 + - **问题分析**: 修复了 `TypeError: Cannot read property '_name' of null`。该错误是由于直接修改节点属性(绕过 Undo 系统)与分组事务混用导致的。 - **重构要点**: 将 `update-node-transform` 中所有的直接赋值替换为 `scene:set-property` IPC 调用,确保所有变换修改均受撤销系统监控。 - **缺陷修正**: 修复了 `manage_undo` 在 `begin_group` 时传递错误参数导致 "Unknown object to record" 的问题。 - **全量汉化与文档同步**: 完成了 `main.js` 和 `scene-script.js` 的 100% 简体中文翻译。同步更新了 `README.md`、`DEVELOPMENT.md` 及 `注意事项.md`。 +### 2026-02-13: 新增 `open_prefab` 功能与消息协议修正 + +- **需求实现**: 新增 `open_prefab` 工具,允许 AI 直接打开预制体进入编辑模式。 +- **协议修正**: 经过测试对比,最终确认使用 `scene:enter-prefab-edit-mode` 消息并配合 `Editor.Ipc.sendToAll` 是进入预制体编辑模式的最佳方案,解决了 `scene:open-by-uuid` 无法触发编辑状态的问题。 +- **文档深度补全**: 遵循全局开发规范,同步更新了所有技术文档,确保 100% 简体中文覆盖及详尽的 JSDoc 注释。 + ### 3.2 MCP 工具注册 在 `/list-tools` 接口中注册工具: ```javascript const tools = [ - { - name: "get_selected_node", - description: "获取当前选中的节点", - parameters: [] - }, - // 其他工具... + { + name: "get_selected_node", + description: "获取当前选中的节点", + parameters: [], + }, + // 其他工具... ]; ``` @@ -130,13 +137,13 @@ const tools = [ ```javascript const sceneScript = { - 'create-node'(params, callback) { - // 创建节点逻辑... - }, - 'set-property'(params, callback) { - // 设置属性逻辑... - }, - // 其他操作... + "create-node"(params, callback) { + // 创建节点逻辑... + }, + "set-property"(params, callback) { + // 设置属性逻辑... + }, + // 其他操作... }; ``` @@ -238,33 +245,33 @@ manageAsset(args, callback) { ```html
- -
- Main - Tool Test -
+ +
+ Main + Tool Test +
- -
- -
+ +
+ +
- -
-
-
- -
- -
- - -
- -
-
-
-
+ +
+
+
+ +
+ +
+ + +
+ +
+
+
+
``` @@ -283,6 +290,7 @@ manageAsset(args, callback) { **错误信息**:`Panel info not found for panel mcp-bridge` **解决方案**: + - 检查 `package.json` 中的面板配置 - 确保 `panel` 字段配置正确,移除冲突的 `panels` 字段 @@ -291,6 +299,7 @@ manageAsset(args, callback) { **错误信息**:`Parent path ... is not exists` **解决方案**: + - 在创建资源前添加目录检查和创建逻辑 - 使用 `fs.mkdirSync(dirPath, { recursive: true })` 递归创建目录 @@ -299,6 +308,7 @@ manageAsset(args, callback) { **错误信息**:`SyntaxError: Invalid or unexpected token` **解决方案**: + - 使用模板字符串(反引号)处理多行字符串 - 避免变量名冲突 @@ -321,6 +331,7 @@ manageAsset(args, callback) { ### 5.2 API 文档 为每个 MCP 工具编写详细的 API 文档,包括: + - 工具名称 - 功能描述 - 参数说明 @@ -344,17 +355,17 @@ manageAsset(args, callback) { ### 6.2 使用流程 1. **启动服务**: - - 打开 Cocos Creator 编辑器 - - 选择 `Packages/MCP Bridge/Open Test Panel` - - 点击 "Start" 按钮启动服务 + - 打开 Cocos Creator 编辑器 + - 选择 `Packages/MCP Bridge/Open Test Panel` + - 点击 "Start" 按钮启动服务 2. **连接 AI 编辑器**: - - 在 AI 编辑器中配置 MCP 代理 - - 使用 `node [项目路径]/packages/mcp-bridge/mcp-proxy.js` 作为命令 + - 在 AI 编辑器中配置 MCP 代理 + - 使用 `node [项目路径]/packages/mcp-bridge/mcp-proxy.js` 作为命令 3. **执行操作**: - - 通过 AI 编辑器发送 MCP 请求 - - 或在测试面板中直接测试工具 + - 通过 AI 编辑器发送 MCP 请求 + - 或在测试面板中直接测试工具 ### 6.3 配置选项 @@ -366,30 +377,30 @@ manageAsset(args, callback) { ### 7.1 添加新工具 1. **在 `main.js` 中注册工具**: - - 在 `/list-tools` 响应中添加工具定义 - - 在 `handleMcpCall` 函数中添加处理逻辑 + - 在 `/list-tools` 响应中添加工具定义 + - 在 `handleMcpCall` 函数中添加处理逻辑 2. **在面板中添加示例**: - - 在 `panel/index.js` 中添加工具示例参数 - - 更新工具列表 + - 在 `panel/index.js` 中添加工具示例参数 + - 更新工具列表 3. **更新文档**: - - 在 `README.md` 中添加工具文档 - - 更新功能特性列表 + - 在 `README.md` 中添加工具文档 + - 更新功能特性列表 ### 7.2 集成新 API 1. **了解 Cocos Creator API**: - - 查阅 Cocos Creator 编辑器 API 文档 - - 了解场景脚本 API + - 查阅 Cocos Creator 编辑器 API 文档 + - 了解场景脚本 API 2. **实现集成**: - - 在 `main.js` 或 `scene-script.js` 中添加对应功能 - - 处理异步操作和错误情况 + - 在 `main.js` 或 `scene-script.js` 中添加对应功能 + - 处理异步操作和错误情况 3. **测试验证**: - - 编写测试用例 - - 验证功能正确性 + - 编写测试用例 + - 验证功能正确性 ## 8. 版本管理 @@ -416,69 +427,69 @@ manageAsset(args, callback) { ## 10. 最佳实践 1. **代码组织**: - - 模块化设计,职责分离 - - 合理使用回调函数处理异步操作 + - 模块化设计,职责分离 + - 合理使用回调函数处理异步操作 2. **错误处理**: - - 完善的错误捕获和处理 - - 详细的错误日志记录 + - 完善的错误捕获和处理 + - 详细的错误日志记录 3. **用户体验**: - - 直观的面板界面 - - 实时的操作反馈 - - 详细的日志信息 + - 直观的面板界面 + - 实时的操作反馈 + - 详细的日志信息 4. **安全性**: - - 验证输入参数 - - 防止路径遍历攻击 - - 限制服务访问范围 + - 验证输入参数 + - 防止路径遍历攻击 + - 限制服务访问范围 ## 11. 开发路线图 (Roadmap) ### 11.1 第三阶段开发计划(已完成) -| 任务 | 状态 | 描述 | -|------|------|------| -| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 | -| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 | +| 任务 | 状态 | 描述 | +| ---------------------- | ------- | ------------------------------------------------------------------- | +| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 | +| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 | | 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 | -| 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 | -| 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 | -| 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 | -| 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 | +| 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 | +| 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 | +| 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 | +| 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 | ### 11.2 第四阶段开发计划(已完成) -| 任务 | 状态 | 描述 | -|------|------|------| +| 任务 | 状态 | 描述 | +| ------------ | ------- | ---------------------------------------------- | | 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 | -| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 | +| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 | ### 11.3 差异填补阶段(Gap Filling)- 已完成 -| 任务 | 状态 | 描述 | -|------|------|------| -| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 | -| 文件哈希 | ✅ 完成 | 实现 get_sha 工具,支持文件 SHA-256 计算 | +| 任务 | 状态 | 描述 | +| -------- | ------- | ---------------------------------------------- | +| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 | +| 文件哈希 | ✅ 完成 | 实现 get_sha 工具,支持文件 SHA-256 计算 | | 动画管理 | ✅ 完成 | 实现 manage_animation 工具,支持动画播放与控制 | ### 11.4 第六阶段:可靠性与体验优化(已完成) -| 任务 | 状态 | 描述 | -|------|------|------| -| IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) | -| 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) | +| 任务 | 状态 | 描述 | +| ---------------- | ------- | -------------------------------------------------------------------------- | +| IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) | +| 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) | | 组件智能解析修复 | ✅ 完成 | 修复组件属性赋值时的 UUID 类型转换,支持压缩 UUID 及自定义组件 (`$_$ctor`) | ### 11.5 第七阶段开发计划(已完成) -| 任务 | 状态 | 描述 | -|------|------|------| -| 插件发布 | ✅ 完成 | 准备发布,提交到 Cocos 插件商店 | -| 文档完善 | ✅ 完成 | 完善 API 文档 ("Getting Started" 教程) | -| 界面美化 | ✅ 完成 | 优化面板 UI 体验 | +| 任务 | 状态 | 描述 | +| ---------- | ------- | ----------------------------------------- | +| 插件发布 | ✅ 完成 | 准备发布,提交到 Cocos 插件商店 | +| 文档完善 | ✅ 完成 | 完善 API 文档 ("Getting Started" 教程) | +| 界面美化 | ✅ 完成 | 优化面板 UI 体验 | | 国际化支持 | ✅ 完成 | 添加多语言 (i18n) 支持 (主要是中文本地化) | -| 工具扩展 | ✅ 完成 | 添加更多高级工具 | +| 工具扩展 | ✅ 完成 | 添加更多高级工具 | ## 12. Unity-MCP 对比分析 @@ -486,33 +497,33 @@ manageAsset(args, callback) { 通过与 Unity-MCP 对比,Cocos-MCP 已实现绝大多数核心功能。 -| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 | -|---------|---------------|---------------|------| -| 编辑器管理 | manage_editor | ✅ 已实现 | | -| 游戏对象管理 | find_gameobjects | ✅ 已实现 | | -| 材质管理 | manage_material | ✅ 已实现 | | -| 纹理管理 | manage_texture | ✅ 已实现 | | -| 代码编辑 | apply_text_edits | ✅ 已实现 | | -| 全局搜索 | search_project | ✅ 已实现 | 升级版,支持正则和路径限定 | -| 控制台 | read_console | ✅ 已实现 | | -| 菜单执行 | execute_menu_item | ✅ 已实现 | 移除不稳定映射,推荐 delete-node:UUID | -| 脚本验证 | validate_script | ✅ 已实现 | | -| 撤销/重做 | undo/redo | ✅ 已实现 | | -| VFX 管理 | manage_vfx | ✅ 已实现 | | -| Git 集成 | get_sha | ✅ 已实现 | 虽然优先级中等,但已根据需求完成 | -| 动画管理 | manage_animation | ✅ 已实现 | 支持播放、暂停、停止及信息获取 | -| ScriptableObject | manage_so | ❌ 未实现 | 使用 AssetDB 替代 | +| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 | +| ---------------- | ----------------- | -------------- | ------------------------------------- | +| 编辑器管理 | manage_editor | ✅ 已实现 | | +| 游戏对象管理 | find_gameobjects | ✅ 已实现 | | +| 材质管理 | manage_material | ✅ 已实现 | | +| 纹理管理 | manage_texture | ✅ 已实现 | | +| 代码编辑 | apply_text_edits | ✅ 已实现 | | +| 全局搜索 | search_project | ✅ 已实现 | 升级版,支持正则和路径限定 | +| 控制台 | read_console | ✅ 已实现 | | +| 菜单执行 | execute_menu_item | ✅ 已实现 | 移除不稳定映射,推荐 delete-node:UUID | +| 脚本验证 | validate_script | ✅ 已实现 | | +| 撤销/重做 | undo/redo | ✅ 已实现 | | +| VFX 管理 | manage_vfx | ✅ 已实现 | | +| Git 集成 | get_sha | ✅ 已实现 | 虽然优先级中等,但已根据需求完成 | +| 动画管理 | manage_animation | ✅ 已实现 | 支持播放、暂停、停止及信息获取 | +| ScriptableObject | manage_so | ❌ 未实现 | 使用 AssetDB 替代 | ## 13. 风险评估 ### 13.1 潜在风险 -| 风险 | 影响 | 缓解措施 | -|------|------|----------| -| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API | -| 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 | -| 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 | -| 兼容性问题 | 多版本不兼容 | 测试主流版本 (2.4.x),提供兼容层 | +| 风险 | 影响 | 缓解措施 | +| --------------- | ------------ | ----------------------------------------- | +| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API | +| 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 | +| 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 | +| 兼容性问题 | 多版本不兼容 | 测试主流版本 (2.4.x),提供兼容层 | ## 14. 结论 diff --git a/IPC_MESSAGES.md b/IPC_MESSAGES.md index bc0aa64..a85f1c3 100644 --- a/IPC_MESSAGES.md +++ b/IPC_MESSAGES.md @@ -15,12 +15,13 @@ - [Metrics相关IPC消息](#metrics相关ipc消息) - [Package Template相关IPC消息](#package-template相关ipc消息) - [Additional Scene相关IPC消息](#additional-scene相关ipc消息) -- [Broadcast Events](#broadcast-events) -- [Events listened by Renderer Process](#events-listened-by-renderer-process) +- [广播事件](#广播事件) +- [渲染进程监听的事件](#渲染进程监听的事件) ## App相关IPC消息 ### `app:explore-project` + - **用途**: 打开项目所在文件夹 - **参数**: 无 - **返回值**: 无 @@ -28,6 +29,7 @@ - **状态**: 可用 ### `app:explore-app` + - **用途**: 打开应用程序所在文件夹 - **参数**: 无 - **返回值**: 无 @@ -35,6 +37,7 @@ - **状态**: 可用 ### `app:build-project` + - **用途**: 构建项目 - **参数**: 包含构建选项的对象 - **返回值**: 无 @@ -42,6 +45,7 @@ - **状态**: 可用 ### `app:query-cocos-templates` + - **用途**: 查询Cocos模板 - **参数**: 无 - **返回值**: 模板列表 @@ -49,6 +53,7 @@ - **状态**: 可用 ### `app:query-android-apilevels` + - **用途**: 查询Android API级别 - **参数**: 无 - **返回值**: API级别列表 @@ -56,6 +61,7 @@ - **状态**: 可用 ### `app:query-android-instant-apilevels` + - **用途**: 查询Android Instant API级别 - **参数**: 无 - **返回值**: API级别列表 @@ -63,6 +69,7 @@ - **状态**: 可用 ### `app:compile-project` + - **用途**: 编译项目 - **参数**: 编译选项对象 - **返回值**: 无 @@ -70,6 +77,7 @@ - **状态**: 可用 ### `app:open-cocos-console-log` + - **用途**: 打开Cocos控制台日志 - **参数**: 无 - **返回值**: 无 @@ -77,6 +85,7 @@ - **状态**: 可用 ### `app:stop-compile` + - **用途**: 停止编译 - **参数**: 无 - **返回值**: 无 @@ -84,6 +93,7 @@ - **状态**: 可用 ### `app:run-project` + - **用途**: 运行项目 - **参数**: 包含平台信息的对象 - **返回值**: 无 @@ -91,6 +101,7 @@ - **状态**: 可用 ### `app:save-keystore` + - **用途**: 保存密钥库 - **参数**: 密钥库信息 - **返回值**: 错误信息或成功确认 @@ -98,6 +109,7 @@ - **状态**: 可用 ### `app:update-build-preview-path` + - **用途**: 更新构建预览路径 - **参数**: 预览路径 - **返回值**: 无 @@ -105,6 +117,7 @@ - **状态**: 可用 ### `app:update-android-instant-preview-path` + - **用途**: 更新Android Instant预览路径 - **参数**: 预览路径 - **返回值**: 无 @@ -112,6 +125,7 @@ - **状态**: 可用 ### `app:play-on-device` + - **用途**: 在设备上播放 - **参数**: 平台信息 - **返回值**: 无 @@ -119,6 +133,7 @@ - **状态**: 可用 ### `app:reload-on-device` + - **用途**: 在设备上重新加载 - **参数**: 无 - **返回值**: 无 @@ -126,6 +141,7 @@ - **状态**: 可用 ### `app:query-plugin-scripts` + - **用途**: 查询插件脚本 - **参数**: 构建平台名称 - **返回值**: 插件脚本列表 @@ -133,6 +149,7 @@ - **状态**: 可用 ### `app:rebuild-editor-engine` + - **用途**: 重建编辑器引擎 - **参数**: 回调函数 - **返回值**: 错误信息 @@ -142,6 +159,7 @@ ## Asset-DB相关IPC消息 ### `asset-db:explore` + - **用途**: 打开资源所在文件夹 - **参数**: 资源URL - **返回值**: 无 @@ -149,6 +167,7 @@ - **状态**: 可用 ### `asset-db:exists` + - **用途**: 查询资源是否存在 - **参数**: 资源URL - **返回值**: 存在性布尔值 @@ -156,6 +175,7 @@ - **状态**: 可用 ### `asset-db:query-path-by-url` + - **用途**: 根据URL查询路径 - **参数**: 资源URL - **返回值**: 文件系统路径 @@ -163,6 +183,7 @@ - **状态**: 可用 ### `asset-db:query-uuid-by-url` + - **用途**: 根据URL查询UUID - **参数**: 资源URL - **返回值**: UUID字符串 @@ -170,6 +191,7 @@ - **状态**: 可用 ### `asset-db:query-path-by-uuid` + - **用途**: 根据UUID查询路径 - **参数**: UUID字符串 - **返回值**: 文件系统路径 @@ -177,6 +199,7 @@ - **状态**: 可用 ### `asset-db:query-url-by-uuid` + - **用途**: 根据UUID查询URL - **参数**: UUID字符串 - **返回值**: 资源URL @@ -184,6 +207,7 @@ - **状态**: 可用 ### `asset-db:query-info-by-uuid` + - **用途**: 根据UUID查询资源信息 - **参数**: UUID字符串 - **返回值**: 资源信息对象 @@ -191,6 +215,7 @@ - **状态**: 可用 ### `asset-db:query-meta-info-by-uuid` + - **用途**: 根据UUID查询元数据信息 - **参数**: UUID字符串 - **返回值**: 元数据信息对象 @@ -198,6 +223,7 @@ - **状态**: 可用 ### `asset-db:deep-query` + - **用途**: 深度查询资源 - **参数**: 无 - **返回值**: 完整资源数据库信息 @@ -205,6 +231,7 @@ - **状态**: 可用 ### `asset-db:query-assets` + - **用途**: 查询资源 - **参数**: 查询条件、类型 - **返回值**: 资源列表 @@ -212,6 +239,7 @@ - **状态**: 可用 ### `asset-db:query-mounts` + - **用途**: 查询挂载点 - **参数**: 无 - **返回值**: 挂载点列表 @@ -219,6 +247,7 @@ - **状态**: 可用 ### `asset-db:import-assets` + - **参数**: 导入路径列表、目标路径、刷新标志 - **用途**: 导入资源 - **返回值**: 无 @@ -226,6 +255,7 @@ - **状态**: 可用 ### `asset-db:create-asset` + - **用途**: 创建资源 - **参数**: URL、内容 - **返回值**: 错误信息或创建结果 @@ -233,6 +263,7 @@ - **状态**: 可用 ### `asset-db:move-asset` + - **用途**: 移动资源 - **参数**: 源URL、目标URL、是否显示错误对话框 - **返回值**: 错误信息或移动结果 @@ -240,6 +271,7 @@ - **状态**: 可用 ### `asset-db:delete-assets` + - **用途**: 删除资源 - **参数**: URL数组 - **返回值**: 错误信息或删除结果 @@ -247,6 +279,7 @@ - **状态**: 可用 ### `asset-db:save-exists` + - **用途**: 保存已存在的资源 - **参数**: URL、内容 - **返回值**: 错误信息或保存结果 @@ -254,6 +287,7 @@ - **状态**: 可用 ### `asset-db:create-or-save` + - **用途**: 创建或保存资源 - **参数**: URL、内容 - **返回值**: 错误信息或操作结果 @@ -261,6 +295,7 @@ - **状态**: 可用 ### `asset-db:save-meta` + - **用途**: 保存元数据 - **参数**: UUID、元数据对象 - **返回值**: 无 @@ -268,6 +303,7 @@ - **状态**: 可用 ### `asset-db:refresh` + - **用途**: 刷新资源 - **参数**: 路径(可选) - **返回值**: 错误信息或刷新结果 @@ -275,6 +311,7 @@ - **状态**: 可用 ### `asset-db:attach-mountpath` + - **用途**: 挂载路径 - **参数**: 挂载路径对象 - **返回值**: 错误信息或挂载结果 @@ -282,6 +319,7 @@ - **状态**: 可用 ### `asset-db:unattach-mountpath` + - **用途**: 取消挂载路径 - **参数**: 挂载路径 - **返回值**: 错误信息或取消挂载结果 @@ -289,6 +327,7 @@ - **状态**: 可用 ### `asset-db:query-watch-state` + - **用途**: 查询监听状态 - **参数**: 无 - **返回值**: 无(向主窗口发送状态) @@ -296,6 +335,7 @@ - **状态**: 可用 ### `asset-db:asset-changed` + - **用途**: 资源变更处理 - **参数**: 变更详情 - **返回值**: 无 @@ -303,6 +343,7 @@ - **状态**: 可用 ### `asset-db:asset-uuid-changed` + - **用途**: 资源UUID变更处理 - **参数**: 变更详情 - **返回值**: 无 @@ -310,6 +351,7 @@ - **状态**: 可用 ### `asset-db:assets-moved` + - **用途**: 资源移动处理 - **参数**: 移动详情数组 - **返回值**: 无 @@ -317,6 +359,7 @@ - **状态**: 可用 ### `asset-db:assets-created` + - **用途**: 资源创建处理 - **参数**: 创建详情数组 - **返回值**: 无 @@ -324,6 +367,7 @@ - **状态**: 可用 ### `asset-db:assets-deleted` + - **用途**: 资源删除处理 - **参数**: 删除详情数组 - **返回值**: 无 @@ -331,6 +375,7 @@ - **状态**: 可用 ### `asset-db:script-import-failed` + - **用途**: 脚本导入失败处理 - **参数**: 失败详情 - **返回值**: 无 @@ -338,6 +383,7 @@ - **状态**: 可用 ### `asset-db:meta-backup` + - **用途**: 元数据备份处理 - **参数**: 备份详情数组 - **返回值**: 无 @@ -347,6 +393,7 @@ ## Dashboard相关IPC消息 ### `app:query-recent` + - **用途**: 查询最近打开的项目 - **参数**: 无 - **返回值**: 最近项目列表 @@ -354,6 +401,7 @@ - **状态**: 不可用 (无响应) ### `app:query-templates` + - **用途**: 查询项目模板 - **参数**: 无 - **返回值**: 模板列表 @@ -361,6 +409,7 @@ - **状态**: 不可用 (无响应) ### `app:create-project` + - **用途**: 创建项目 - **参数**: 项目配置对象 - **返回值**: 错误信息或创建结果 @@ -368,6 +417,7 @@ - **状态**: 不可用 (无响应) ### `app:open-project` + - **用途**: 打开项目 - **参数**: 项目路径、是否需要登录 - **返回值**: 错误信息或打开结果 @@ -375,6 +425,7 @@ - **状态**: 不可用 (无响应) ### `app:delete-project` + - **用途**: 删除项目 - **参数**: 项目路径 - **返回值**: 无 @@ -382,6 +433,7 @@ - **状态**: 不可用 (无响应) ### `app:close-project` + - **用途**: 关闭项目 - **参数**: 项目路径 - **返回值**: 无 @@ -389,6 +441,7 @@ - **状态**: 不可用 (无响应) ### `app:window-minimize` + - **用途**: 最小化窗口 - **参数**: 无 - **返回值**: 无 @@ -396,6 +449,7 @@ - **状态**: 不可用 (无响应) ### `app:window-close` + - **用途**: 关闭窗口 - **参数**: 无 - **返回值**: 无 @@ -403,6 +457,7 @@ - **状态**: 不可用 (无响应) ### `app:get-last-create` + - **用途**: 获取上次创建的项目 - **参数**: 无 - **返回值**: 上次创建的项目信息 @@ -410,6 +465,7 @@ - **状态**: 不可用 (无响应) ### `app:open-manual-doc` + - **用途**: 打开手册文档 - **参数**: 文档路径 - **返回值**: 无 @@ -417,6 +473,7 @@ - **状态**: 不可用 (无响应) ### `app:open-api-doc` + - **用途**: 打开API文档 - **参数**: 文档路径 - **返回值**: 无 @@ -424,6 +481,7 @@ - **状态**: 不可用 (无响应) ### `app:query-last-create-path` + - **用途**: 查询上次创建的路径 - **参数**: 无 - **返回值**: 上次创建的路径 @@ -433,6 +491,7 @@ ## Scene相关IPC消息 ### `scene:is-ready` + - **用途**: 检查场景编辑器是否准备就绪 - **参数**: 无 - **返回值**: 准备就绪状态 @@ -440,6 +499,7 @@ - **状态**: 不可用 (无响应) ### `scene:new-scene` + - **用途**: 创建新场景 - **参数**: 无 - **返回值**: 无 @@ -447,6 +507,7 @@ - **状态**: 不可用 (无响应) ### `scene:saved` + - **用途**: 场景保存完成后的通知 - **参数**: 无 - **返回值**: 无 @@ -454,6 +515,7 @@ - **状态**: 未测试 ### `scene:play-on-device` + - **用途**: 在设备上播放场景 - **参数**: 无 - **返回值**: 无 @@ -461,6 +523,7 @@ - **状态**: 未测试 ### `scene:reload-on-device` + - **用途**: 在设备上重新加载场景 - **参数**: 无 - **返回值**: 无 @@ -468,6 +531,7 @@ - **状态**: 未测试 ### `scene:preview-server-scene-stashed` + - **用途**: 预览服务器场景暂存 - **参数**: 无 - **返回值**: 无 @@ -475,6 +539,7 @@ - **状态**: 不可用 (无响应) ### `scene:load-package-scene-script` + - **用途**: 加载包场景脚本 - **参数**: 脚本路径、包名 - **返回值**: 无 @@ -482,6 +547,7 @@ - **状态**: 不可用 (无响应) ### `scene:unload-package-scene-script` + - **用途**: 卸载包场景脚本 - **参数**: 脚本路径 - **返回值**: 无 @@ -489,6 +555,7 @@ - **状态**: 不可用 (无响应) ### `scene:soft-reload` + - **用途**: 软重载场景 - **参数**: 重载参数 - **返回值**: 无 @@ -496,6 +563,7 @@ - **状态**: 不可用 (无响应) ### `scene:enter-prefab-edit-mode` + - **用途**: 进入预制件编辑模式 - **参数**: 预制件UUID - **返回值**: 无 @@ -503,6 +571,7 @@ - **状态**: 可用 ### `scene:stash-and-save` + - **用途**: 暂存并保存场景 - **参数**: 无 - **返回值**: 无 @@ -510,6 +579,7 @@ - **状态**: 不可用 (无响应) ### `scene:print-simulator-log` + - **用途**: 打印模拟器日志 - **参数**: 日志信息、日志类型 - **返回值**: 无 @@ -517,6 +587,7 @@ - **状态**: 不可用 (无响应) ### `scene:generate-texture-packer-preview-files` + - **用途**: 生成纹理打包预览文件 - **参数**: 资源路径 - **返回值**: 错误信息或操作结果 @@ -524,6 +595,7 @@ - **状态**: 不可用 (无响应) ### `scene:query-texture-packer-preview-files` + - **用途**: 查询纹理打包预览文件 - **参数**: 资源路径 - **返回值**: 预览文件信息 @@ -531,6 +603,7 @@ - **状态**: 可用 ### `scene:export-particle-plist` + - **用途**: 导出粒子plist文件 - **参数**: 粒子数据 - **返回值**: 无 @@ -538,6 +611,7 @@ - **状态**: 可用 ### `scene:update-edit-mode` + - **用途**: 更新编辑模式 - **参数**: 编辑模式信息 - **返回值**: 无 @@ -545,6 +619,7 @@ - **状态**: 可用 ### `scene:undo` + - **用途**: 撤销操作 - **参数**: 无 - **返回值**: 无 @@ -552,6 +627,7 @@ - **状态**: 可用 ### `scene:redo` + - **用途**: 重做操作 - **参数**: 无 - **返回值**: 无 @@ -559,6 +635,7 @@ - **状态**: 可用 ### `scene:undo-record` + - **用途**: 记录撤销操作 - **参数**: 对象ID、操作信息 - **返回值**: 无 @@ -566,6 +643,7 @@ - **状态**: 可用 ### `scene:undo-commit` + - **用途**: 提交撤销操作 - **参数**: 无 - **返回值**: 无 @@ -573,6 +651,7 @@ - **状态**: 可用 ### `scene:undo-cancel` + - **用途**: 取消撤销操作 - **参数**: 无 - **返回值**: 无 @@ -580,6 +659,7 @@ - **状态**: 可用 ### `scene:query-dirty-state` + - **用途**: 查询场景脏状态 - **参数**: 无 - **返回值**: 脏状态信息 @@ -587,6 +667,7 @@ - **状态**: 可用 ### `scene:query-group-list` + - **用途**: 查询分组列表 - **参数**: 无 - **返回值**: 分组列表 @@ -594,6 +675,7 @@ - **状态**: 可用 ### `scene:query-hierarchy` + - **用途**: 查询场景层级结构 - **参数**: 无 - **返回值**: 场景UUID和节点层次 @@ -601,6 +683,7 @@ - **状态**: 可用 ### `scene:query-nodes-by-comp-name` + - **用途**: 根据组件名称查询节点 - **参数**: 组件名称 - **返回值**: 节点UUID列表 @@ -608,6 +691,7 @@ - **状态**: 可用 ### `scene:query-node` + - **用途**: 查询节点信息 - **参数**: 节点UUID - **返回值**: 节点信息 @@ -615,6 +699,7 @@ - **状态**: 可用 ### `scene:query-node-info` + - **用途**: 查询节点详细信息 - **参数**: 节点UUID、类型 - **返回值**: 节点详细信息 @@ -622,6 +707,7 @@ - **状态**: 可用 ### `scene:query-node-functions` + - **用途**: 查询节点函数 - **参数**: 节点UUID - **返回值**: 节点函数列表 @@ -629,6 +715,7 @@ - **状态**: 可用 ### `scene:choose-last-rigid-body` + - **用途**: 选择上一个刚体 - **参数**: 当前节点UUID - **返回值**: 无 @@ -636,6 +723,7 @@ - **状态**: 可用 ### `scene:choose-next-rigid-body` + - **用途**: 选择下一个刚体 - **参数**: 当前节点UUID - **返回值**: 无 @@ -643,6 +731,7 @@ - **状态**: 可用 ### `scene:is-child-class-of` + - **用途**: 检查是否为子类 - **参数**: 类ID、父类ID - **返回值**: 是否为子类的布尔值 @@ -650,6 +739,7 @@ - **状态**: 可用 ### `scene:has-copied-component` + - **用途**: 检查是否有复制的组件 - **参数**: 无 - **返回值**: 是否有复制组件的布尔值 @@ -657,6 +747,7 @@ - **状态**: 可用 ### `scene:query-animation-hierarchy` + - **用途**: 查询动画层级 - **参数**: 节点UUID - **返回值**: 动画层级结构 @@ -664,6 +755,7 @@ - **状态**: 可用 ### `scene:query-animation-list` + - **用途**: 查询动画列表 - **参数**: 节点UUID - **返回值**: 动画剪辑UUID列表 @@ -671,6 +763,7 @@ - **状态**: 可用 ### `scene:query-animation-properties` + - **用途**: 查询动画属性 - **参数**: 节点UUID - **返回值**: 动画属性列表 @@ -678,6 +771,7 @@ - **状态**: 可用 ### `scene:query-animation-record` + - **用途**: 查询动画记录状态 - **参数**: 无 - **返回值**: 动画记录状态信息 @@ -685,6 +779,7 @@ - **状态**: 可用 ### `scene:query-animation-clip` + - **用途**: 查询动画剪辑 - **参数**: 动画剪辑UUID - **返回值**: 动画剪辑序列化数据 @@ -692,6 +787,7 @@ - **状态**: 可用 ### `scene:query-asset-info` + - **用途**: 查询资源信息 - **参数**: 资源UUID - **返回值**: 资源信息 @@ -699,6 +795,7 @@ - **状态**: 可用 ### `scene:query-nodes-by-usedby-uuid` + - **用途**: 根据使用的资源UUID查询节点 - **参数**: 资源UUID - **返回值**: 使用该资源的节点UUID列表 @@ -706,6 +803,7 @@ - **状态**: 可用 ### `scene:create-nodes-by-uuids` + - **用途**: 根据UUID创建节点 - **参数**: UUID列表、位置、父节点、选项 - **返回值**: 无 @@ -713,6 +811,7 @@ - **状态**: 可用 ### `scene:create-node-by-classid` + - **用途**: 根据类ID创建节点 - **参数**: 类ID、位置、父节点、选项 - **返回值**: 无 @@ -720,6 +819,7 @@ - **状态**: 可用 ### `scene:create-node-by-prefab` + - **用途**: 根据预制件创建节点 - **参数**: 预制件名称、位置、父节点、选项 - **返回值**: 无 @@ -727,6 +827,7 @@ - **状态**: 可用 ### `scene:new-property` + - **用途**: 创建新属性 - **参数**: 属性信息 - **返回值**: 无 @@ -734,6 +835,7 @@ - **状态**: 可用 ### `scene:reset-property` + - **用途**: 重置属性 - **参数**: 属性信息 - **返回值**: 无 @@ -741,6 +843,7 @@ - **状态**: 可用 ### `scene:set-property` + - **用途**: 设置属性 - **参数**: 属性信息 - **返回值**: 无 @@ -748,6 +851,7 @@ - **状态**: 可用 ### `scene:add-component` + - **用途**: 添加组件 - **参数**: 节点UUID、组件名称 - **返回值**: 无 @@ -755,6 +859,7 @@ - **状态**: 可用 ### `scene:remove-component` + - **用途**: 移除组件 - **参数**: 节点UUID、组件ID - **返回值**: 无 @@ -762,6 +867,7 @@ - **状态**: 可用 ### `scene:reset-node` + - **用途**: 重置节点 - **参数**: 节点UUID - **返回值**: 无 @@ -769,6 +875,7 @@ - **状态**: 可用 ### `scene:reset-all` + - **用途**: 重置所有组件 - **参数**: 节点UUID - **返回值**: 无 @@ -776,6 +883,7 @@ - **状态**: 可用 ### `scene:move-up-component` + - **用途**: 向上移动组件 - **参数**: 节点UUID、组件ID - **返回值**: 无 @@ -783,6 +891,7 @@ - **状态**: 可用 ### `scene:move-down-component` + - **用途**: 向下移动组件 - **参数**: 节点UUID、组件ID - **返回值**: 无 @@ -790,6 +899,7 @@ - **状态**: 可用 ### `scene:reset-component` + - **用途**: 重置组件 - **参数**: 节点UUID、组件ID - **返回值**: 无 @@ -797,6 +907,7 @@ - **状态**: 可用 ### `scene:copy-component` + - **用途**: 复制组件 - **参数**: 组件ID - **返回值**: 无 @@ -804,6 +915,7 @@ - **状态**: 可用 ### `scene:paste-component` + - **用途**: 粘贴组件 - **参数**: 节点UUID - **返回值**: 无 @@ -811,6 +923,7 @@ - **状态**: 可用 ### `scene:move-nodes` + - **用途**: 移动节点 - **参数**: 节点UUID列表、位置、父节点 - **返回值**: 无 @@ -818,6 +931,7 @@ - **状态**: 不可用 (无响应) ### `scene:delete-nodes` + - **用途**: 删除节点 - **参数**: 节点UUID列表 - **返回值**: 无 @@ -825,6 +939,7 @@ - **状态**: 不可用 (无响应) ### `scene:copy-nodes` + - **用途**: 复制节点 - **参数**: 节点UUID列表 - **返回值**: 无 @@ -832,6 +947,7 @@ - **状态**: 不可用 (无响应) ### `scene:paste-nodes` + - **用途**: 粘贴节点 - **参数**: 无 - **返回值**: 无 @@ -839,6 +955,7 @@ - **状态**: 不可用 (无响应) ### `scene:duplicate-nodes` + - **用途**: 复制节点(Duplicate) - **参数**: 节点UUID列表 - **返回值**: 无 @@ -846,6 +963,7 @@ - **状态**: 不可用 (无响应) ### `scene:create-prefab` + - **用途**: 创建预制件 - **参数**: 节点UUID、预制件路径 - **返回值**: 无 @@ -853,6 +971,7 @@ - **状态**: 可用 ### `scene:apply-prefab` + - **用途**: 应用预制件 - **参数**: 节点UUID、预制件路径 - **返回值**: 无 @@ -860,6 +979,7 @@ - **状态**: 可用 ### `scene:revert-prefab` + - **用途**: 还原预制件 - **参数**: 节点UUID、预制件路径 - **返回值**: 无 @@ -867,6 +987,7 @@ - **状态**: 不可用 (无响应) ### `scene:set-prefab-sync` + - **用途**: 设置预制件同步 - **参数**: 节点UUID - **返回值**: 无 @@ -874,6 +995,7 @@ - **状态**: 不可用 (无响应) ### `scene:break-prefab-instance` + - **用途**: 打破预制件实例关联 - **参数**: 无 - **返回值**: 无 @@ -881,6 +1003,7 @@ - **状态**: 不可用 (无响应) ### `scene:link-prefab` + - **用途**: 链接预制件 - **参数**: 无 - **返回值**: 无 @@ -888,6 +1011,7 @@ - **状态**: 不可用 (无响应) ### `scene:regenerate-polygon-points` + - **用途**: 重新生成多边形点 - **参数**: 节点UUID - **返回值**: 无 @@ -895,6 +1019,7 @@ - **状态**: 不可用 (无响应) ### `scene:search-skeleton-animation-clips` + - **用途**: 搜索骨骼动画剪辑 - **参数**: 节点UUID - **返回值**: 无 @@ -902,6 +1027,7 @@ - **状态**: 不可用 (无响应) ### `scene:change-node-lock` + - **用途**: 更改节点锁定状态 - **参数**: 节点UUID、锁定状态 - **返回值**: 无 @@ -909,6 +1035,7 @@ - **状态**: 不可用 (无响应) ### `scene:copy-editor-camera-data-to-nodes` + - **用途**: 将编辑器相机数据复制到节点 - **参数**: 无 - **返回值**: 无 @@ -916,6 +1043,7 @@ - **状态**: 可用 ### `scene:set-group-sync` + - **用途**: 设置组同步 - **参数**: 节点UUID、组名 - **返回值**: 无 @@ -923,6 +1051,7 @@ - **状态**: 不可用 (无响应) ### `scene:generate_attached_node` + - **用途**: 生成附加节点 - **参数**: 节点UUID - **返回值**: 无 @@ -932,6 +1061,7 @@ ## Editor相关IPC消息 ### `editor:dragstart` + - **用途**: 开始拖拽操作 - **参数**: 无 - **返回值**: 无 @@ -939,6 +1069,7 @@ - **状态**: 不可用 (无响应) ### `editor:dragend` + - **用途**: 结束拖拽操作 - **参数**: 无 - **返回值**: 无 @@ -946,6 +1077,7 @@ - **状态**: 不可用 (无响应) ### `editor:project-profile-updated` + - **用途**: 项目配置文件更新 - **参数**: 配置对象 - **返回值**: 无 @@ -955,6 +1087,7 @@ ## Selection相关IPC消息 ### `selection:selected` + - **用途**: 选择节点 - **参数**: 类型、ID - **返回值**: 无 @@ -962,6 +1095,7 @@ - **状态**: 可用 ### `selection:unselected` + - **用途**: 取消选择节点 - **参数**: 类型、ID - **返回值**: 无 @@ -969,6 +1103,7 @@ - **状态**: 不可用 (无响应) ### `selection:activated` + - **用途**: 激活节点 - **参数**: 类型、ID - **返回值**: 无 @@ -976,6 +1111,7 @@ - **状态**: 不可用 (无响应) ### `selection:deactivated` + - **用途**: 取消激活节点 - **参数**: 类型、ID - **返回值**: 无 @@ -983,6 +1119,7 @@ - **状态**: 不可用 (无响应) ### `selection:hoverin` + - **用途**: 鼠标悬停进入节点 - **参数**: 类型、ID - **返回值**: 无 @@ -990,6 +1127,7 @@ - **状态**: 不可用 (无响应) ### `selection:hoverout` + - **用途**: 鼠标悬停离开节点 - **参数**: 类型、ID - **返回值**: 无 @@ -999,6 +1137,7 @@ ## Scene-Animation相关IPC消息 ### `scene-animation:query-animation-time` + - **用途**: 查询动画时间 - **参数**: 动画剪辑信息 - **返回值**: 时间信息 @@ -1006,6 +1145,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:animation-time-changed` + - **用途**: 动画时间改变 - **参数**: 新的时间值 - **返回值**: 无 @@ -1013,6 +1153,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:animation-clip-changed` + - **用途**: 动画剪辑改变 - **参数**: 剪辑信息 - **返回值**: 无 @@ -1020,6 +1161,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:save-clip` + - **用途**: 保存动画剪辑 - **参数**: 无 - **返回值**: 无 @@ -1027,6 +1169,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:set-animation-speed` + - **用途**: 设置动画速度 - **参数**: 速度值 - **返回值**: 无 @@ -1034,6 +1177,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:change-animation-record` + - **用途**: 更改动画记录状态 - **参数**: 记录状态 - **返回值**: 无 @@ -1041,6 +1185,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:mount-clip` + - **用途**: 挂载动画剪辑 - **参数**: 剪辑信息、选项 - **返回值**: 无 @@ -1048,6 +1193,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:change-animation-state` + - **用途**: 更改动画播放状态 - **参数**: 状态(播放/暂停) - **返回值**: 无 @@ -1055,6 +1201,7 @@ - **状态**: 不可用 (无响应) ### `scene-animation:change-animation-current-clip` + - **用途**: 更改当前动画剪辑 - **参数**: 剪辑信息 - **返回值**: 无 @@ -1064,6 +1211,7 @@ ## Scene-Layout相关IPC消息 ### `scene-layout:center-nodes` + - **用途**: 将摄像机中心对准节点 - **参数**: 节点列表 - **返回值**: 无 @@ -1073,6 +1221,7 @@ ## Metrics相关IPC消息 ### `metrics:track-event` + - **用途**: 跟踪事件 - **参数**: 事件类别、动作、标签等 - **返回值**: 无 @@ -1080,6 +1229,7 @@ - **状态**: 可用 ### `editor:renderer-console-error` + - **用途**: 发送渲染器控制台错误 - **参数**: 错误堆栈信息 - **返回值**: 无 @@ -1087,6 +1237,7 @@ - **状态**: 可用 ### `metrics:track-exception` + - **用途**: 跟踪异常 - **参数**: 异常信息 - **返回值**: 无 @@ -1096,6 +1247,7 @@ ## Package Template相关IPC消息 ### `package-template:clicked` + - **用途**: 包模板被点击 - **参数**: 无 - **返回值**: 无 @@ -1103,6 +1255,7 @@ - **状态**: 不可用 (无响应) ### `package-template:hello` + - **用途**: 包模板的问候消息 - **参数**: 事件对象 - **返回值**: 无 @@ -1110,6 +1263,7 @@ - **状态**: 未测试 ### `package-template:open` + - **用途**: 打开包模板面板 - **参数**: 无 - **返回值**: 无 @@ -1117,6 +1271,7 @@ - **状态**: 不可用 (无响应) ### `package-template:say-hello` + - **用途**: 发送问候语 - **参数**: 无 - **返回值**: 无 @@ -1126,6 +1281,7 @@ ## Additional Scene相关IPC消息 ### `asset-db:asset-changed` + - **用途**: 资源变更处理 - **参数**: 变更详情 - **返回值**: 无 @@ -1133,6 +1289,7 @@ - **状态**: 可用 ### `asset-db:assets-moved` + - **用途**: 资源移动处理 - **参数**: 移动详情数组 - **返回值**: 无 @@ -1140,6 +1297,7 @@ - **状态**: 可用 ### `asset-db:assets-created` + - **用途**: 资源创建处理 - **参数**: 创建详情数组 - **返回值**: 无 @@ -1147,6 +1305,7 @@ - **状态**: 可用 ### `asset-db:assets-deleted` + - **用途**: 资源删除处理 - **参数**: 删除详情数组 - **返回值**: 无 @@ -1154,6 +1313,7 @@ - **状态**: 可用 ### `editor:ready` + - **用途**: 编辑器就绪通知 - **参数**: 无 - **返回值**: 无 @@ -1161,6 +1321,7 @@ - **状态**: 未测试 ### `editor:console-failed` + - **用途**: 控制台失败消息 - **参数**: 失败信息 - **返回值**: 无 @@ -1168,6 +1329,7 @@ - **状态**: 未测试 ### `editor:console-warn` + - **用途**: 控制台警告消息 - **参数**: 警告信息 - **返回值**: 无 @@ -1175,6 +1337,7 @@ - **状态**: 未测试 ### `editor:console-error` + - **用途**: 控制台错误消息 - **参数**: 错误信息 - **返回值**: 无 @@ -1182,6 +1345,7 @@ - **状态**: 未测试 ### `editor:console-clear` + - **用途**: 清空控制台 - **参数**: 无 - **返回值**: 无 @@ -1189,6 +1353,7 @@ - **状态**: 未测试 ### `compiler:state-changed` + - **用途**: 编译器状态变化 - **参数**: 状态 - **返回值**: 无 @@ -1196,6 +1361,7 @@ - **状态**: 未测试 ### `preview-server:preview-port-changed` + - **用途**: 预览服务器端口变化 - **参数**: 无 - **返回值**: 无 @@ -1203,6 +1369,7 @@ - **状态**: 未测试 ### `preview-server:connects-changed` + - **用途**: 预览服务器连接数变化 - **参数**: 连接数 - **返回值**: 无 @@ -1210,6 +1377,7 @@ - **状态**: 未测试 ### `im-plugin:update-im-html` + - **用途**: 更新IM插件HTML - **参数**: HTML内容 - **返回值**: 无 @@ -1217,6 +1385,7 @@ - **状态**: 未测试 ### `asset-db:state-changed` + - **用途**: 资源数据库状态变化 - **参数**: 状态 - **返回值**: 无 @@ -1224,6 +1393,7 @@ - **状态**: 未测试 ### `asset-db:watch-state-changed` + - **用途**: 资源数据库监听状态变化 - **参数**: 状态 - **返回值**: 无 @@ -1231,6 +1401,7 @@ - **状态**: 未测试 ### `editor:online-status-changed` + - **用途**: 编辑器在线状态变化 - **参数**: 状态 ('online' 或 'offline') - **返回值**: 无 @@ -1238,6 +1409,7 @@ - **状态**: 未测试 ### `scene:node-component-updated` + - **用途**: 节点组件更新 - **参数**: 包含节点、组件和属性信息的对象 - **返回值**: 无 @@ -1245,6 +1417,7 @@ - **状态**: 未测试 ### `scene:node-component-added` + - **用途**: 节点组件添加 - **参数**: 包含节点和组件信息的对象 - **返回值**: 无 @@ -1252,13 +1425,14 @@ - **状态**: 未测试 ### `scene:node-component-removed` + - **用途**: 节点组件移除 - **参数**: 包含节点和组件信息的对象 - **返回值**: 无 - **类型**: 广播事件 - **状态**: 未测试 -## Broadcast Events +## 广播事件 以下是专门用于广播给渲染进程的事件,通常由主进程发出,渲染进程接收: @@ -1286,7 +1460,7 @@ - `scene:node-component-removed` - `package-template:hello` -## Events listened by Renderer Process +## 渲染进程监听的事件 以下是渲染进程主动监听的事件,这些事件通常由主进程发出: diff --git a/README.md b/README.md index 5747e0e..af26bb6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能 - **场景节点操作**: 获取、创建、修改场景中的节点 -- **资源管理**: 创建场景、预制体,打开指定资源 +- **资源管理**: 创建场景、预制体,打开场景或预制体进入编辑模式 - **组件管理**: 添加、删除、获取节点组件 - **脚本管理**: 创建、删除、读取、写入脚本文件 - **批处理执行**: 批量执行多个 MCP 工具操作,提高效率 @@ -123,10 +123,16 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j - **参数**: - `url`: 场景资源路径,如 `db://assets/NewScene.fire` -### 7. create_node +### 7. open_prefab + +- **描述**: 在编辑器中打开指定的预制体文件进入编辑模式。这是一个异步操作,打开后请等待几秒。 +- **参数**: + - `url`: 预制体资源路径,如 `db://assets/prefabs/Test.prefab` + +### 8. create_node - **描述**: 在当前场景中创建一个新节点。 -- **重要提示**: +- **重要提示**: 1. 如果指定了 `parentId`,必须先通过 `get_scene_hierarchy` 确认该 UUID 对应的父节点仍然存在。 2. **预设类型差异**: - `empty`: 纯空节点,无组件,不带贴图。 @@ -247,15 +253,15 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j - **描述**: 管理纹理 - **参数**: - - `action`: 操作类型(`create`, `delete`, `get_info`, `update`) - - `path`: 纹理路径,如 `db://assets/textures/NewTexture.png` - - `properties`: 纹理属性(用于 `create`/`update` 操作) - - `type`: 纹理类型(如 `sprite`, `texture`, `raw`)(用于 `update`) - - `border`: 九宫格边距数组 `[top, bottom, left, right]` (用于 `update`,仅当 type 为 sprite 时有效) - - `subMetas`: (内部使用) - - `width`: 宽度 (用于 `create` 生成占位图) - - `height`: 高度 (用于 `create` 生成占位图) - - `native`: 原生路径 + - `action`: 操作类型(`create`, `delete`, `get_info`, `update`) + - `path`: 纹理路径,如 `db://assets/textures/NewTexture.png` + - `properties`: 纹理属性(用于 `create`/`update` 操作) + - `type`: 纹理类型(如 `sprite`, `texture`, `raw`)(用于 `update`) + - `border`: 九宫格边距数组 `[top, bottom, left, right]` (用于 `update`,仅当 type 为 sprite 时有效) + - `subMetas`: (内部使用) + - `width`: 宽度 (用于 `create` 生成占位图) + - `height`: 高度 (用于 `create` 生成占位图) + - `native`: 原生路径 ### 18. execute_menu_item @@ -303,6 +309,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j - `includeSubpackages`: 是否搜索子包 (Boolean, 默认 true) **示例**: + ```json // 正则搜索 { @@ -349,7 +356,6 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j - **参数**: - `path`: 文件路径,如 `db://assets/scripts/Test.ts` - ## 技术实现 ### 架构设计 @@ -413,12 +419,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j 1. **确定性优先**:任何对节点、组件、属性的操作,都必须建立在“主体已确认存在”的基础上。 2. **校验流程**: - * **节点校验**:操作前必须使用 `get_scene_hierarchy` 确认节点。 - * **组件校验**:操作组件前必须使用 `get`(通过 `manage_components`)确认组件存在。 - * **属性校验**:更新属性前必须确认属性名准确无误。 + - **节点校验**:操作前必须使用 `get_scene_hierarchy` 确认节点。 + - **组件校验**:操作组件前必须使用 `get`(通过 `manage_components`)确认组件存在。 + - **属性校验**:更新属性前必须确认属性名准确无误。 3. **禁止假设**:禁止盲目尝试对不存在的对象或属性进行修改。 - ## 贡献 欢迎提交 Issue 和 Pull Request 来改进这个插件! diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md index ebac93c..09f242b 100644 --- a/UPDATE_LOG.md +++ b/UPDATE_LOG.md @@ -113,3 +113,10 @@ - **清理死代码**: 删除 `/list-tools` 路由中重复的 `res.writeHead / res.end` 调用。 - **文档更新**: `注意事项.md` 新增第 9 章「并发安全与防卡死机制」,记录 CommandQueue 和 IPC 超时两个防护机制。 + +### 6. 场景与预制体工具增强 + +- **新增 `open_prefab` 工具**: 解决了直接打开预制体进入编辑模式的问题。通过使用正确的 IPC 消息 `scene:enter-prefab-edit-mode` (并结合 `Editor.Ipc.sendToAll`),使得 AI 可以精准操控预制体的编辑流程,而不再局限于场景跳转。 +- **优化预制体创建稳定性 (`create_node` + `prefab_management`)**: + - 在创建物理目录后强制执行 `Editor.assetdb.refresh`,确保 AssetDB 即时同步。 + - 将节点重命名与预制体创建指令之间的安全延迟从 100ms 增加至 300ms,消除了重命名未完成导致创建失败的竞态条件。 diff --git a/main.js b/main.js index a227880..df25c80 100644 --- a/main.js +++ b/main.js @@ -253,6 +253,20 @@ const getToolsList = () => { required: ["url"], }, }, + { + name: "open_prefab", + description: `${globalPrecautions} 在编辑器中打开预制体文件进入编辑模式。注意:这是一个异步操作,打开后请等待几秒。`, + inputSchema: { + type: "object", + properties: { + url: { + type: "string", + description: "预制体资源路径,如 db://assets/prefabs/Test.prefab", + }, + }, + required: ["url"], + }, + }, { name: "create_node", description: `${globalPrecautions} 在当前场景中创建一个新节点。重要提示:1. 如果指定 parentId,必须先通过 get_scene_hierarchy 确保该父节点真实存在且未被删除。2. 类型说明:'sprite' (100x100 尺寸 + 默认贴图), 'button' (150x50 尺寸 + 深色底图 + Button组件), 'label' (120x40 尺寸 + Label组件), 'empty' (纯空节点)。`, @@ -842,6 +856,9 @@ module.exports = { } }, + /** + * 关闭 HTTP 服务器 + */ stopServer() { if (mcpServer) { mcpServer.close(); @@ -852,6 +869,10 @@ module.exports = { } }, + /** + * 获取 MCP 资源列表 + * @returns {Array} 资源列表数组 + */ getResourcesList() { return [ { @@ -875,6 +896,11 @@ module.exports = { ]; }, + /** + * 读取指定的 MCP 资源内容 + * @param {string} uri 资源统一资源标识符 (URI) + * @param {Function} callback 完成回调 (err, content) + */ handleReadResource(uri, callback) { let parsed; try { @@ -997,6 +1023,22 @@ module.exports = { } break; + case "open_prefab": + isSceneBusy = true; // 锁定 + const prefabUuid = Editor.assetdb.urlToUuid(args.url); + if (prefabUuid) { + // 【核心修复】使用正确的 IPC 消息进入预制体编辑模式 + Editor.Ipc.sendToAll("scene:enter-prefab-edit-mode", prefabUuid); + setTimeout(() => { + isSceneBusy = false; + callback(null, `成功:正在打开预制体 ${args.url}`); + }, 2000); + } else { + isSceneBusy = false; + callback(`找不到路径为 ${args.url} 的资源`); + } + break; + case "create_node": if (args.type === "sprite" || args.type === "button") { const splashUuid = Editor.assetdb.urlToUuid( @@ -1432,11 +1474,11 @@ export default class NewScript extends cc.Component { const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); + // 【增强】确保 AssetDB 刷新文件夹,防止场景脚本找不到目标目录 + Editor.assetdb.refresh(targetDir); } // 解析目标目录和文件名 - // db://assets/folder/PrefabName.prefab -> db://assets/folder, PrefabName - const targetDir = prefabPath.substring(0, prefabPath.lastIndexOf("/")); const fileName = prefabPath.substring(prefabPath.lastIndexOf("/") + 1); const prefabName = fileName.replace(".prefab", ""); @@ -1451,9 +1493,10 @@ export default class NewScript extends cc.Component { // 2. 发送创建命令 (参数: [uuids], dirPath) // 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径 + // 【增强】增加延迟到 300ms,确保 IPC 消息处理并同步到底层引擎 setTimeout(() => { Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir); - }, 100); // 稍微延迟以确保重命名生效 + }, 300); callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`); break; @@ -1472,7 +1515,7 @@ export default class NewScript extends cc.Component { case "instantiate": if (!Editor.assetdb.exists(prefabPath)) { - return callback(`Prefab not found at ${prefabPath}`); + return callback(`路径为 ${prefabPath} 的预制体不存在`); } // 实例化预制体 const prefabUuid = Editor.assetdb.urlToUuid(prefabPath); @@ -2066,6 +2109,11 @@ CCProgram fs %{ callback(null, filteredOutput); }, + /** + * 执行编辑器菜单项 + * @param {Object} args 参数 (menuPath) + * @param {Function} callback 完成回调 + */ executeMenuItem(args, callback) { const { menuPath } = args; if (!menuPath) { @@ -2092,7 +2140,7 @@ CCProgram fs %{ if (uuid) { callSceneScriptWithTimeout("mcp-bridge", "delete-node", { uuid }, (err, result) => { if (err) callback(err); - else callback(null, result || `Node ${uuid} deleted via scene script`); + else callback(null, result || `节点 ${uuid} 已通过场景脚本删除`); }); return; } @@ -2120,7 +2168,7 @@ CCProgram fs %{ } else { // 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效) // 或者直接返回不支持的警告 - addLog("warn", `支持映射表中找不到菜单项 '${menuPath}'。尝试通过 legacy 模式执行。`); + addLog("warn", `支持映射表中找不到菜单项 '${menuPath}'。尝试通过旧版模式执行。`); // 尝试通用调用 try { @@ -2134,7 +2182,11 @@ CCProgram fs %{ } }, - // 验证脚本 + /** + * 验证脚本文件的语法或基础结构 + * @param {Object} args 参数 (filePath) + * @param {Function} callback 完成回调 + */ validateScript(args, callback) { const { filePath } = args; @@ -2265,7 +2317,7 @@ CCProgram fs %{ }, "inspect-apis"() { - addLog("info", "[API Inspector] Starting DEEP inspection..."); + addLog("info", "[API 检查器] 开始深度分析..."); // 获取函数参数的辅助函数 const getArgs = (func) => { @@ -2365,8 +2417,8 @@ CCProgram fs %{ : "Missing"; }); - addLog("info", `[API Inspector] Standard Objects:\n${JSON.stringify(report, null, 2)}`); - addLog("info", `[API Inspector] Forum Checklist:\n${JSON.stringify(checklistResults, null, 2)}`); + addLog("info", `[API 检查器] 标准对象:\n${JSON.stringify(report, null, 2)}`); + addLog("info", `[API 检查器] 论坛核查清单:\n${JSON.stringify(checklistResults, null, 2)}`); // 3. 检查内置包 IPC 消息 const ipcReport = {}; @@ -2391,12 +2443,15 @@ CCProgram fs %{ } }); - addLog("info", `[API Inspector] Built-in IPC Messages:\n${JSON.stringify(ipcReport, null, 2)}`); + addLog("info", `[API 检查器] 内置包 IPC 消息:\n${JSON.stringify(ipcReport, null, 2)}`); }, }, - // 全局文件搜索 - // 项目搜索 (升级版 find_in_file) + /** + * 全局项目文件搜索 (支持正则表达式、文件名、目录名搜索) + * @param {Object} args 参数 + * @param {Function} callback 完成回调 + */ searchProject(args, callback) { const { query, useRegex, path: searchPath, matchType, extensions } = args; @@ -2405,7 +2460,7 @@ CCProgram fs %{ const rootPath = Editor.assetdb.urlToFspath(rootPathUrl); if (!rootPath || !fs.existsSync(rootPath)) { - return callback(`Invalid search path: ${rootPathUrl}`); + return callback(`无效的搜索路径: ${rootPathUrl}`); } const mode = matchType || "content"; // content, file_name, dir_name @@ -2534,11 +2589,15 @@ CCProgram fs %{ walk(rootPath); callback(null, results); } catch (err) { - callback(`Search project failed: ${err.message}`); + callback(`项目搜索失败: ${err.message}`); } }, - // 管理撤销/重做 + /** + * 管理撤销/重做操作及事务分组 + * @param {Object} args 参数 (action, description, id) + * @param {Function} callback 完成回调 + */ manageUndo(args, callback) { const { action, description } = args; @@ -2546,16 +2605,13 @@ CCProgram fs %{ switch (action) { case "undo": Editor.Ipc.sendToPanel("scene", "scene:undo"); - callback(null, "Undo command executed"); + callback(null, "撤销指令已执行"); break; case "redo": Editor.Ipc.sendToPanel("scene", "scene:redo"); - callback(null, "Redo command executed"); + callback(null, "重做指令已执行"); break; case "begin_group": - // scene:undo-record [id] - // 注意:在 2.4.x 中,undo-record 通常需要一个有效的 uuid - // 如果没有提供 uuid,不应将 description 作为 ID 发送,否则会报 Unknown object to record addLog("info", `开始撤销组: ${description || "MCP 动作"}`); // 如果有参数包含 id,则记录该节点 if (args.id) { @@ -2565,27 +2621,31 @@ CCProgram fs %{ break; case "end_group": Editor.Ipc.sendToPanel("scene", "scene:undo-commit"); - callback(null, "Undo group committed"); + callback(null, "撤销组已提交"); break; case "cancel_group": Editor.Ipc.sendToPanel("scene", "scene:undo-cancel"); - callback(null, "Undo group cancelled"); + callback(null, "撤销组已取消"); break; default: - callback(`Unknown undo action: ${action}`); + callback(`未知的撤销操作: ${action}`); } } catch (err) { - callback(`Undo operation failed: ${err.message}`); + callback(`撤销操作失败: ${err.message}`); } }, - // 获取文件 SHA-256 + /** + * 计算资源的 SHA-256 哈希值 + * @param {Object} args 参数 (path) + * @param {Function} callback 完成回调 + */ getSha(args, callback) { const { path: url } = args; const fspath = Editor.assetdb.urlToFspath(url); if (!fspath || !fs.existsSync(fspath)) { - return callback(`File not found: ${url}`); + return callback(`找不到文件: ${url}`); } try { @@ -2599,7 +2659,11 @@ CCProgram fs %{ } }, - // 管理动画 + /** + * 管理节点动画 (播放、停止、获取信息等) + * @param {Object} args 参数 + * @param {Function} callback 完成回调 + */ manageAnimation(args, callback) { // 转发给场景脚本处理 callSceneScriptWithTimeout("mcp-bridge", "manage-animation", args, callback); diff --git a/mcp-proxy.js b/mcp-proxy.js index 1e833b4..f956650 100644 --- a/mcp-proxy.js +++ b/mcp-proxy.js @@ -1,101 +1,149 @@ -const http = require('http'); -const COCOS_PORT = 3456; +/** + * MCP 桥接代理脚本 + * 负责在标准 MCP 客户端 (stdin/stdout) 与 Cocos Creator 插件 (HTTP) 之间转发请求。 + */ +const http = require("http"); + +/** + * 当前 Cocos Creator 插件监听的端口 + * @type {number} + */ +const COCOS_PORT = 3456; + +/** + * 发送调试日志到标准的错误输出流水 + * @param {string} msg 日志消息 + */ function debugLog(msg) { - process.stderr.write(`[Proxy Debug] ${msg}\n`); + process.stderr.write(`[代理调试] ${msg}\n`); } -process.stdin.on('data', (data) => { - const lines = data.toString().split('\n'); - lines.forEach(line => { - if (!line.trim()) return; - try { - const request = JSON.parse(line); - handleRequest(request); - } catch (e) {} - }); +// 监听标准输入以获取 MCP 请求 +process.stdin.on("data", (data) => { + const lines = data.toString().split("\n"); + lines.forEach((line) => { + if (!line.trim()) return; + try { + const request = JSON.parse(line); + handleRequest(request); + } catch (e) { + // 忽略非 JSON 输入 + } + }); }); +/** + * 处理 JSON-RPC 请求 + * @param {Object} req RPC 请求对象 + */ function handleRequest(req) { - const { method, id, params } = req; + const { method, id, params } = req; - if (method === 'initialize') { - sendToAI({ - jsonrpc: "2.0", id: id, - result: { - protocolVersion: "2024-11-05", - capabilities: { tools: {} }, - serverInfo: { name: "cocos-bridge", version: "1.0.0" } - } - }); - return; - } + // 处理握手初始化 + if (method === "initialize") { + sendToAI({ + jsonrpc: "2.0", + id: id, + result: { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "cocos-bridge", version: "1.0.0" }, + }, + }); + return; + } - if (method === 'tools/list') { - // 使用 GET 获取列表 - forwardToCocos('/list-tools', null, id, 'GET'); - return; - } + // 获取工具列表 + if (method === "tools/list") { + forwardToCocos("/list-tools", null, id, "GET"); + return; + } - if (method === 'tools/call') { - // 使用 POST 执行工具 - forwardToCocos('/call-tool', { - name: params.name, - arguments: params.arguments - }, id, 'POST'); - return; - } + // 执行具体工具 + if (method === "tools/call") { + forwardToCocos( + "/call-tool", + { + name: params.name, + arguments: params.arguments, + }, + id, + "POST", + ); + return; + } - if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} }); + // 默认空响应 + if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} }); } -function forwardToCocos(path, payload, id, method = 'POST') { - const postData = payload ? JSON.stringify(payload) : ''; - - const options = { - hostname: '127.0.0.1', - port: COCOS_PORT, - path: path, - method: method, - headers: { 'Content-Type': 'application/json' } - }; +/** + * 将请求通过 HTTP 转发给 Cocos Creator 插件 + * @param {string} path API 路径 + * @param {Object|null} payload 发送的数据体 + * @param {string|number} id RPC 请求标识符 + * @param {string} method HTTP 方法 (默认 POST) + */ +function forwardToCocos(path, payload, id, method = "POST") { + const postData = payload ? JSON.stringify(payload) : ""; - if (postData) { - options.headers['Content-Length'] = Buffer.byteLength(postData); - } + const options = { + hostname: "127.0.0.1", + port: COCOS_PORT, + path: path, + method: method, + headers: { "Content-Type": "application/json" }, + }; - const request = http.request(options, (res) => { - let resData = ''; - res.on('data', d => resData += d); - res.on('end', () => { - try { - const cocosRes = JSON.parse(resData); - - // 检查关键字段 - if (path === '/list-tools' && !cocosRes.tools) { - // 如果报错,把 Cocos 返回的所有内容打印到 Trae 的 stderr 日志里 - debugLog(`CRITICAL: Cocos returned no tools. Received: ${resData}`); - sendError(id, -32603, "Invalid Cocos response: missing tools array"); - } else { - sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes }); - } - } catch (e) { - debugLog(`JSON Parse Error. Cocos Sent: ${resData}`); - sendError(id, -32603, "Cocos returned non-JSON data"); - } - }); - }); + if (postData) { + options.headers["Content-Length"] = Buffer.byteLength(postData); + } - request.on('error', (e) => { - debugLog(`Cocos is offline: ${e.message}`); - sendError(id, -32000, "Cocos Plugin Offline"); - }); + const request = http.request(options, (res) => { + let resData = ""; + res.on("data", (d) => (resData += d)); + res.on("end", () => { + try { + const cocosRes = JSON.parse(resData); - if (postData) request.write(postData); - request.end(); + // 检查关键字段,确保 Cocos 插件返回了期望的数据格式 + if (path === "/list-tools" && !cocosRes.tools) { + debugLog(`致命错误: Cocos 未返回工具列表。接收内容: ${resData}`); + sendError(id, -32603, "Cocos 响应无效:缺少 tools 数组"); + } else { + sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes }); + } + } catch (e) { + debugLog(`JSON 解析错误。Cocos 发送内容: ${resData}`); + sendError(id, -32603, "Cocos 返回了非 JSON 数据"); + } + }); + }); + + request.on("error", (e) => { + debugLog(`Cocos 插件已离线: ${e.message}`); + sendError(id, -32000, "Cocos 插件离线"); + }); + + if (postData) request.write(postData); + request.end(); } -function sendToAI(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); } +/** + * 将结果发送给 AI (通过标准输出) + * @param {Object} obj 结果对象 + */ +function sendToAI(obj) { + process.stdout.write(JSON.stringify(obj) + "\n"); +} + +/** + * 发送 RPC 错误响应 + * @param {string|number} id RPC 请求标识符 + * @param {number} code 错误码 + * @param {string} message 错误消息 + */ function sendError(id, code, message) { - sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } }); -} \ No newline at end of file + sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } }); +} diff --git a/package.json b/package.json index 8791777..85ddd51 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,14 @@ "main": "main.js", "scene-script": "scene-script.js", "main-menu": { - "MCP Bridge/Open Panel": { + "MCP 桥接器/开启测试面板": { "message": "mcp-bridge:open-test-panel" } }, "panel": { "main": "panel/index.js", "type": "dockable", - "title": "MCP Test Panel", + "title": "MCP 测试面板", "width": 400, "height": 300 }, diff --git a/panel/index.js b/panel/index.js index 0e5e87d..664802a 100644 --- a/panel/index.js +++ b/panel/index.js @@ -1,23 +1,53 @@ "use strict"; + +/** + * MCP Bridge 插件面板脚本 + * 负责处理面板 UI 交互、与主进程通信以及提供测试工具界面。 + */ + const fs = require("fs"); const { IpcUi } = require("../dist/IpcUi"); Editor.Panel.extend({ + /** + * 面板 CSS 样式 + */ style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"), + + /** + * 面板 HTML 模板 + */ template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"), + /** + * 监听来自主进程的消息 + */ messages: { + /** + * 接收并渲染日志 + * @param {Object} event IPC 事件对象 + * @param {Object} log 日志数据 + */ "mcp-bridge:on-log"(event, log) { this.renderLog(log); }, + + /** + * 服务器状态变更通知 + * @param {Object} event IPC 事件对象 + * @param {Object} config 服务器配置 + */ "mcp-bridge:state-changed"(event, config) { this.updateUI(config.active); }, }, + /** + * 面板就绪回调,进行 DOM 绑定与事件初始化 + */ ready() { const root = this.shadowRoot; - // 获取 DOM 元素 + // 获取 DOM 元素映射 const els = { port: root.querySelector("#portInput"), btnToggle: root.querySelector("#btnToggle"), @@ -41,7 +71,7 @@ Editor.Panel.extend({ resizer: root.querySelector("#testResizer"), }; - // 1. 初始化状态 + // 1. 初始化服务器状态与配置 Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => { if (data) { els.port.value = data.config.port; @@ -52,10 +82,10 @@ Editor.Panel.extend({ } }); - // 初始化 IPC UI + // 初始化 IPC 调试专用 UI (由 TypeScript 编写并编译到 dist) new IpcUi(root); - // 2. 标签切换 + // 2. 标签页切换逻辑 els.tabMain.addEventListener("confirm", () => { els.tabMain.classList.add("active"); els.tabTest.classList.remove("active"); @@ -64,6 +94,7 @@ Editor.Panel.extend({ els.panelTest.classList.remove("active"); els.panelIpc.classList.remove("active"); }); + els.tabTest.addEventListener("confirm", () => { els.tabTest.classList.add("active"); els.tabMain.classList.remove("active"); @@ -71,8 +102,9 @@ Editor.Panel.extend({ els.panelTest.classList.add("active"); els.panelMain.classList.remove("active"); els.panelIpc.classList.remove("active"); - this.fetchTools(els); + this.fetchTools(els); // 切换到测试页时自动拉取工具列表 }); + els.tabIpc.addEventListener("confirm", () => { els.tabIpc.classList.add("active"); els.tabMain.classList.remove("active"); @@ -82,39 +114,42 @@ Editor.Panel.extend({ els.panelTest.classList.remove("active"); }); - // 3. 基础功能 + // 3. 基础控制按钮逻辑 els.btnToggle.addEventListener("confirm", () => { Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value)); }); + root.querySelector("#btnClear").addEventListener("confirm", () => { els.logView.innerHTML = ""; Editor.Ipc.sendToMain("mcp-bridge:clear-logs"); }); + root.querySelector("#btnCopy").addEventListener("confirm", () => { require("electron").clipboard.writeText(els.logView.innerText); - Editor.success("日志已复制"); + Editor.success("日志已复制到剪贴板"); }); + els.autoStart.addEventListener("change", (e) => { Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value); }); - // 4. 测试页功能 + // 4. API 测试页交互逻辑 els.listBtn.addEventListener("confirm", () => this.fetchTools(els)); els.clearBtn.addEventListener("confirm", () => { els.result.value = ""; }); els.testBtn.addEventListener("confirm", () => this.runTest(els)); - els.testBtn.addEventListener("confirm", () => this.runTest(els)); - // 添加 API 探查功能 + + // API 探查功能 (辅助开发者发现可用内部 IPC) const probeBtn = root.querySelector("#probeApisBtn"); if (probeBtn) { probeBtn.addEventListener("confirm", () => { Editor.Ipc.sendToMain("mcp-bridge:inspect-apis"); - els.result.value = "探查指令已发送。请查看编辑器控制台日志。"; + els.result.value = "API 探查指令已发送。请查看编辑器控制台 (Console) 获取详细报告。"; }); } - // 5. 【修复】拖拽逻辑 + // 5. 测试页分栏拖拽缩放逻辑 if (els.resizer && els.left) { els.resizer.addEventListener("mousedown", (e) => { e.preventDefault(); @@ -135,8 +170,13 @@ Editor.Panel.extend({ } }, + /** + * 从本地服务器获取 MCP 工具列表并渲染 + * @param {Object} els DOM 元素映射 + */ fetchTools(els) { const url = `http://localhost:${els.port.value}/list-tools`; + els.result.value = "正在获取工具列表..."; fetch(url) .then((r) => r.json()) .then((data) => { @@ -154,64 +194,86 @@ Editor.Panel.extend({ }; els.toolsList.appendChild(item); }); - // 保存工具映射表,以便后续检索 this.toolsMap = toolsMap; - els.result.value = `成功加载 ${data.tools.length} 个工具。`; + els.result.value = `成功:加载了 ${data.tools.length} 个工具。`; }) .catch((e) => { - els.result.value = "Error: " + e.message; + els.result.value = "获取失败: " + e.message; }); }, + /** + * 在面板中展示工具的详细描述与参数定义 + * @param {Object} els DOM 元素映射 + * @param {Object} tool 工具定义对象 + */ showToolDescription(els, tool) { if (!tool) { - els.toolDescription.textContent = "选择工具查看说明"; + els.toolDescription.textContent = "选择工具以查看说明"; return; } - let description = tool.description || "无描述"; + let description = tool.description || "暂无描述"; let inputSchema = tool.inputSchema; let details = []; if (inputSchema && inputSchema.properties) { - details.push("参数说明:"); + details.push("参数说明:"); for (const [key, prop] of Object.entries(inputSchema.properties)) { - let propDesc = `- ${key}`; + let propDesc = `- ${key}`; if (prop.description) { propDesc += `: ${prop.description}`; } if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) { - propDesc += " (必填)"; + propDesc += " (必填)"; } details.push(propDesc); } } - els.toolDescription.innerHTML = `${description}

${details.join('
')}`; + els.toolDescription.innerHTML = `${description}

${details.join("
")}`; }, + /** + * 执行工具测试请求 + * @param {Object} els DOM 元素映射 + */ runTest(els) { const url = `http://localhost:${els.port.value}/call-tool`; - const body = { name: els.toolName.value, arguments: JSON.parse(els.toolParams.value || "{}") }; - els.result.value = "正在测试..."; + let args; + try { + args = JSON.parse(els.toolParams.value || "{}"); + } catch (e) { + els.result.value = "JSON 格式错误: " + e.message; + return; + } + + const body = { name: els.toolName.value, arguments: args }; + els.result.value = "正在发送请求..."; fetch(url, { method: "POST", body: JSON.stringify(body) }) .then((r) => r.json()) .then((d) => { els.result.value = JSON.stringify(d, null, 2); }) .catch((e) => { - els.result.value = "Error: " + e.message; + els.result.value = "测试异常: " + e.message; }); }, + /** + * 获取指定工具的示例参数 + * @param {string} name 工具名称 + * @returns {Object} 示例参数对象 + */ getExample(name) { const examples = { - set_node_name: { id: "UUID", newName: "Hello" }, - update_node_transform: { id: "UUID", x: 0, y: 0, color: "#FF0000" }, - create_node: { name: "Node", type: "sprite", parentId: "" }, + set_node_name: { id: "节点-UUID", newName: "新名称" }, + update_node_transform: { id: "节点-UUID", x: 0, y: 0, color: "#FF0000" }, + create_node: { name: "新节点", type: "sprite", parentId: "" }, open_scene: { url: "db://assets/Scene.fire" }, + open_prefab: { url: "db://assets/MyPrefab.prefab" }, manage_editor: { action: "get_selection" }, - find_gameobjects: { conditions: { name: "Node", active: true }, recursive: true }, + find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true }, manage_material: { action: "create", path: "db://assets/materials/NewMaterial.mat", @@ -225,7 +287,7 @@ Editor.Panel.extend({ execute_menu_item: { menuPath: "Assets/Create/Folder" }, apply_text_edits: { filePath: "db://assets/scripts/TestScript.ts", - edits: [{ type: "insert", position: 0, text: "// Test comment\n" }], + edits: [{ type: "insert", position: 0, text: "// 测试注释\n" }], }, read_console: { limit: 10, type: "log" }, validate_script: { filePath: "db://assets/scripts/TestScript.ts" }, @@ -233,6 +295,10 @@ Editor.Panel.extend({ return examples[name] || {}; }, + /** + * 将日志条目渲染至面板控制台 + * @param {Object} log 日志对象 + */ renderLog(log) { const view = this.shadowRoot.querySelector("#logConsole"); if (!view) return; @@ -244,6 +310,10 @@ Editor.Panel.extend({ if (atBottom) view.scrollTop = view.scrollHeight; }, + /** + * 根据服务器运行状态更新 UI 按钮文字与样式 + * @param {boolean} active 服务器是否处于激活状态 + */ updateUI(active) { const btn = this.shadowRoot.querySelector("#btnToggle"); if (!btn) return; diff --git a/scene-script.js b/scene-script.js index 6c20697..a5a6574 100644 --- a/scene-script.js +++ b/scene-script.js @@ -6,1015 +6,1080 @@ * @returns {cc.Node | null} 找到的节点对象或 null */ const findNode = (id) => { - if (!id) return null; - let node = cc.engine.getInstanceById(id); - if (!node && typeof Editor !== 'undefined' && Editor.Utils && Editor.Utils.UuidUtils) { - // 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找 - try { - const decompressed = Editor.Utils.UuidUtils.decompressUuid(id); - if (decompressed !== id) { - node = cc.engine.getInstanceById(decompressed); - } - } catch (e) { - // 忽略转换错误 - } - } - return node; + if (!id) return null; + let node = cc.engine.getInstanceById(id); + if (!node && typeof Editor !== "undefined" && Editor.Utils && Editor.Utils.UuidUtils) { + // 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找 + try { + const decompressed = Editor.Utils.UuidUtils.decompressUuid(id); + if (decompressed !== id) { + node = cc.engine.getInstanceById(decompressed); + } + } catch (e) { + // 忽略转换错误 + } + } + return node; }; module.exports = { - /** - * 修改节点的基础属性 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (id, path, value) - */ - "set-property": function (event, args) { - const { id, path, value } = args; - - // 1. 获取节点 - let node = findNode(id); - - if (node) { - // 2. 修改属性 - if (path === "name") { - node.name = value; - } else { - node[path] = value; - } - - // 3. 【解决报错的关键】告诉编辑器场景变脏了(需要保存) - // 在场景进程中,我们发送 IPC 给主进程 - Editor.Ipc.sendToMain("scene:dirty"); - - // 4. 【额外补丁】通知层级管理器(Hierarchy)同步更新节点名称 - // 否则你修改了名字,层级管理器可能还是显示旧名字 - Editor.Ipc.sendToAll("scene:node-changed", { - uuid: id, - }); - - if (event.reply) { - event.reply(null, `节点 ${id} 已更新为 ${value}`); - } - } else { - if (event.reply) { - event.reply(new Error("场景脚本:找不到节点 " + id)); - } - } - }, - /** - * 获取当前场景的完整层级树 - * @param {Object} event IPC 事件对象 - */ - "get-hierarchy": function (event) { - const scene = cc.director.getScene(); - - function dumpNodes(node) { - // 【优化】跳过编辑器内部的私有节点,减少数据量 - if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { - return null; - } - - let nodeData = { - name: node.name, - uuid: node.uuid, - active: node.active, - position: { x: Math.round(node.x), y: Math.round(node.y) }, - scale: { x: node.scaleX, y: node.scaleY }, - size: { width: node.width, height: node.height }, - // 记录组件类型,让 AI 知道这是个什么节点 - components: node._components.map((c) => c.__typename), - children: [], - }; - - for (let i = 0; i < node.childrenCount; i++) { - let childData = dumpNodes(node.children[i]); - if (childData) nodeData.children.push(childData); - } - return nodeData; - } - - const hierarchy = dumpNodes(scene); - if (event.reply) event.reply(null, hierarchy); - }, - - /** - * 批量更新节点的变换信息 (坐标、缩放、颜色) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (id, x, y, scaleX, scaleY, color) - */ - "update-node-transform": function (event, args) { - const { id, x, y, scaleX, scaleY, color } = args; - Editor.log(`[scene-script] update-node-transform called for ${id} with args: ${JSON.stringify(args)}`); - - let node = findNode(id); - - if (node) { - Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`); - - // 使用 scene:set-property 实现支持 Undo 的属性修改 - // 注意:IPC 消息需要发送到 'scene' 面板 - if (x !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "x", type: "Number", value: Number(x) }); - } - if (y !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "y", type: "Number", value: Number(y) }); - } - if (args.width !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "width", type: "Number", value: Number(args.width) }); - } - if (args.height !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "height", type: "Number", value: Number(args.height) }); - } - if (scaleX !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleX", type: "Number", value: Number(scaleX) }); - } - if (scaleY !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleY", type: "Number", value: Number(scaleY) }); - } - 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 } - }); - } - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: id }); - - Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`); - if (event.reply) event.reply(null, "变换信息已更新"); - } else { - if (event.reply) event.reply(new Error("找不到节点")); - } - }, - /** - * 在场景中创建新节点 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (name, parentId, type) - */ - "create-node": function (event, args) { - const { name, parentId, type } = args; - const scene = cc.director.getScene(); - if (!scene) { - if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。")); - return; - } - - let newNode = null; - - // 特殊处理:如果是创建 Canvas,自动设置好适配 - if (type === "canvas" || name === "Canvas") { - newNode = new cc.Node("Canvas"); - let canvas = newNode.addComponent(cc.Canvas); - newNode.addComponent(cc.Widget); - // 设置默认设计分辨率 - canvas.designResolution = cc.size(960, 640); - canvas.fitHeight = true; - // 自动在 Canvas 下创建一个 Camera - let camNode = new cc.Node("Main Camera"); - camNode.addComponent(cc.Camera); - camNode.parent = newNode; - } else if (type === "sprite") { - newNode = new cc.Node(name || "新建精灵节点"); - let sprite = newNode.addComponent(cc.Sprite); - // 设置为 CUSTOM 模式 - sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; - // 为精灵设置默认尺寸 - newNode.width = 100; - newNode.height = 100; - - // 加载引擎默认图做占位 - if (args.defaultSpriteUuid) { - cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { - if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { - sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - } - } else if (type === "button") { - newNode = new cc.Node(name || "新建按钮节点"); - let sprite = newNode.addComponent(cc.Sprite); - newNode.addComponent(cc.Button); - - // 设置为 CUSTOM 模式并应用按钮专用尺寸 - sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; - newNode.width = 150; - newNode.height = 50; - - // 设置稍暗的背景颜色 (#A0A0A0),以便于看清其上的文字 - newNode.color = new cc.Color(160, 160, 160); - - // 加载引擎默认图 - if (args.defaultSpriteUuid) { - cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { - if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { - sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - } - } else if (type === "label") { - newNode = new cc.Node(name || "新建文本节点"); - let l = newNode.addComponent(cc.Label); - l.string = "新文本"; - newNode.width = 120; - newNode.height = 40; - } else { - newNode = new cc.Node(name || "新建节点"); - } - - // 设置层级 - let parent = parentId ? findNode(parentId) : scene; - if (parent) { - newNode.parent = parent; - - // 【优化】通知主进程场景变脏 - Editor.Ipc.sendToMain("scene:dirty"); - - // 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环 - setTimeout(() => { - Editor.Ipc.sendToAll("scene:node-created", { - uuid: newNode.uuid, - parentUuid: parent.uuid, - }); - }, 10); - - if (event.reply) event.reply(null, newNode.uuid); - } else { - if (event.reply) event.reply(new Error(`无法创建节点:找不到父节点 ${parentId}`)); - } - }, - - /** - * 管理节点上的组件 (添加、移除、更新属性) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (nodeId, action, componentType, componentId, properties) - */ - "manage-components": function (event, args) { - const { nodeId, action, componentType, componentId, properties } = args; - let node = findNode(nodeId); - - // 辅助函数:应用属性并智能解析 - const applyProperties = (component, props) => { - if (!props) return; - // 尝试获取组件类的属性定义 - const compClass = component.constructor; - - for (const [key, value] of Object.entries(props)) { - // 【核心修复】专门处理各类事件属性 (ClickEvents, ScrollEvents 等) - const isEventProp = Array.isArray(value) && (key.toLowerCase().endsWith('events') || key === 'clickEvents'); - - if (isEventProp) { - const eventHandlers = []; - for (const item of value) { - if (typeof item === 'object' && (item.target || item.component || item.handler)) { - const handler = new cc.Component.EventHandler(); - - // 解析 Target Node - if (item.target) { - let targetNode = findNode(item.target); - if (!targetNode && item.target instanceof cc.Node) { - targetNode = item.target; - } - - if (targetNode) { - handler.target = targetNode; - Editor.log(`[scene-script] Resolved event target: ${targetNode.name}`); - } - } - - if (item.component) handler.component = item.component; - if (item.handler) handler.handler = item.handler; - if (item.customEventData !== undefined) handler.customEventData = String(item.customEventData); - - eventHandlers.push(handler); - } else { - // 如果不是对象,原样保留 - eventHandlers.push(item); - } - } - component[key] = eventHandlers; - continue; // 处理完事件数组,跳出本次循环 - } - - // 检查属性是否存在 - if (component[key] !== undefined) { - let finalValue = value; - - // 【核心逻辑】智能类型识别与赋值 - try { - const attrs = (cc.Class.Attr.getClassAttrs && cc.Class.Attr.getClassAttrs(compClass)) || {}; - let propertyType = attrs[key] ? attrs[key].type : null; - if (!propertyType && attrs[key + '$_$ctor']) { - propertyType = attrs[key + '$_$ctor']; - } - - let isAsset = propertyType && (propertyType.prototype instanceof cc.Asset || propertyType === cc.Asset || propertyType === cc.Prefab || propertyType === cc.SpriteFrame); - let isAssetArray = Array.isArray(value) && (key === 'materials' || key.toLowerCase().includes('assets')); - - // 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点 - if (!isAsset && !isAssetArray && typeof value === 'string' && value.length > 20) { - const lowerKey = key.toLowerCase(); - const assetKeywords = ['prefab', 'sprite', 'texture', 'material', 'skeleton', 'spine', 'atlas', 'font', 'audio', 'data']; - if (assetKeywords.some(k => lowerKey.includes(k))) { - if (!findNode(value)) { - isAsset = true; - } - } - } - - if (isAsset || isAssetArray) { - // 1. 处理资源引用 (单个或数组) - const uuids = isAssetArray ? value : [value]; - const loadedAssets = []; - let loadedCount = 0; - - if (uuids.length === 0) { - component[key] = []; - return; - } - - uuids.forEach((uuid, idx) => { - if (typeof uuid !== 'string' || uuid.length < 10) { - loadedCount++; - return; - } - cc.AssetLibrary.loadAsset(uuid, (err, asset) => { - loadedCount++; - if (!err && asset) { - loadedAssets[idx] = asset; - Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`); - } else { - Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`); - } - - if (loadedCount === uuids.length) { - if (isAssetArray) { - // 过滤掉加载失败的 - component[key] = loadedAssets.filter(a => !!a); - } else { - if (loadedAssets[0]) component[key] = loadedAssets[0]; - } - - // 通知编辑器 UI 更新 - const compIndex = node._components.indexOf(component); - if (compIndex !== -1) { - Editor.Ipc.sendToPanel('scene', 'scene:set-property', { - id: node.uuid, - path: `_components.${compIndex}.${key}`, - type: isAssetArray ? 'Array' : 'Object', - value: isAssetArray ? uuids.map(u => ({ uuid: u })) : { uuid: value }, - isSubProp: false - }); - } - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - }); - // 【重要修复】使用 continue 而不是 return,确保处理完 Asset 属性后 - // 还能继续处理后续的普通属性 (如 type, sizeMode 等) - continue; - } else if (propertyType && (propertyType.prototype instanceof cc.Component || propertyType === cc.Component || propertyType === cc.Node)) { - // 2. 处理节点或组件引用 - const targetNode = findNode(value); - if (targetNode) { - if (propertyType === cc.Node) { - finalValue = targetNode; - } else { - const targetComp = targetNode.getComponent(propertyType); - if (targetComp) { - finalValue = targetComp; - } else { - Editor.warn(`[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`); - } - } - Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`); - } else if (value && value.length > 20) { - // 如果明确是组件/节点类型但找不到,才报错 - Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`); - } - } else { - // 3. 通用启发式 (找不到类型时的 fallback) - if (typeof value === 'string' && value.length > 20) { - const targetNode = findNode(value); - if (targetNode) { - finalValue = targetNode; - Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`); - } else { - // 找不到节点且是 UUID -> 视为资源 - const compIndex = node._components.indexOf(component); - if (compIndex !== -1) { - Editor.Ipc.sendToPanel('scene', 'scene:set-property', { - id: node.uuid, - path: `_components.${compIndex}.${key}`, - type: 'Object', - value: { uuid: value }, - isSubProp: false - }); - Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`); - } - return; - } - } - } - } catch (e) { - Editor.warn(`[scene-script] 解析属性 ${key} 失败: ${e.message}`); - } - - component[key] = finalValue; - } - } - }; - - if (!node) { - if (event.reply) event.reply(new Error("找不到节点")); - return; - } - - switch (action) { - case "add": - if (!componentType) { - if (event.reply) event.reply(new Error("必须提供组件类型")); - 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(`找不到组件类型: ${componentType}`)); - return; - } - - // 添加组件 - const component = node.addComponent(compClass); - - // 设置属性 - if (properties) { - applyProperties(component, properties); - } - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - - if (event.reply) event.reply(null, `组件 ${componentType} 已添加`); - } catch (err) { - if (event.reply) event.reply(new Error(`添加组件失败: ${err.message}`)); - } - break; - - case "remove": - if (!componentId) { - if (event.reply) event.reply(new Error("必须提供组件 ID")); - return; - } - - try { - // 查找并移除组件 - let component = null; - if (node._components) { - for (let i = 0; i < node._components.length; i++) { - if (node._components[i].uuid === componentId) { - component = node._components[i]; - break; - } - } - } - - if (component) { - node.removeComponent(component); - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - if (event.reply) event.reply(null, "组件已移除"); - } else { - if (event.reply) event.reply(new Error("找不到组件")); - } - } catch (err) { - if (event.reply) event.reply(new Error(`移除组件失败: ${err.message}`)); - } - break; - - case "update": - // 更新现有组件属性 - if (!componentType) { - // 如果提供了 componentId,可以只用 componentId - // 但 Cocos 2.4 uuid 获取组件比较麻烦,最好还是有 type 或者遍历 - } - - try { - let targetComp = null; - - // 1. 尝试通过 componentId 查找 - if (componentId) { - if (node._components) { - for (let i = 0; i < node._components.length; i++) { - if (node._components[i].uuid === componentId) { - targetComp = node._components[i]; - break; - } - } - } - } - - // 2. 尝试通过 type 查找 - if (!targetComp && componentType) { - let compClass = null; - if (componentType.startsWith("cc.")) { - const className = componentType.replace("cc.", ""); - compClass = cc[className]; - } else { - compClass = cc.js.getClassByName(componentType); - } - if (compClass) { - targetComp = node.getComponent(compClass); - } - } - - if (targetComp) { - if (properties) { - applyProperties(targetComp, properties); - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - if (event.reply) event.reply(null, "组件属性已更新"); - } else { - if (event.reply) event.reply(null, "没有需要更新的属性"); - } - } else { - if (event.reply) event.reply(new Error(`找不到组件 (类型: ${componentType}, ID: ${componentId})`)); - } - } catch (err) { - if (event.reply) event.reply(new Error(`更新组件失败: ${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 { - // 安全序列化检查 - const val = c[key]; - if (val === null || val === undefined) { - properties[key] = val; - continue; - } - - // 基础类型是安全的 - if (typeof val !== 'object') { - properties[key] = val; - continue; - } - - // 特殊 Cocos 类型 - if (val instanceof cc.ValueType) { - properties[key] = val.toString(); - } else if (val instanceof cc.Asset) { - properties[key] = `资源(${val.name})`; - } else if (val instanceof cc.Node) { - properties[key] = `节点(${val.name})`; - } else if (val instanceof cc.Component) { - properties[key] = `组件(${val.name}<${val.__typename}>)`; - } else { - // 数组和普通对象 - // 尝试转换为纯 JSON 数据以避免 IPC 错误(如包含原生对象/循环引用) - try { - const jsonStr = JSON.stringify(val); - // 确保不传递原始对象引用 - properties[key] = JSON.parse(jsonStr); - } catch (e) { - // 如果 JSON 失败(例如循环引用),格式化为字符串 - properties[key] = `[复杂对象: ${val.constructor ? val.constructor.name : typeof val}]`; - } - } - } catch (e) { - properties[key] = "[Serialization Error]"; - } - } - } - return { - type: cc.js.getClassName(c) || c.constructor.name || "Unknown", - uuid: c.uuid, - properties: properties, - }; - }); - if (event.reply) event.reply(null, components); - } catch (err) { - if (event.reply) event.reply(new Error(`获取组件失败: ${err.message}`)); - } - break; - - default: - if (event.reply) event.reply(new Error(`未知的组件操作类型: ${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; - }, - - "instantiate-prefab": function (event, args) { - const { prefabUuid, parentId } = args; - const scene = cc.director.getScene(); - - if (!scene) { - if (event.reply) event.reply(new Error("Scene not ready or loading.")); - return; - } - - if (!prefabUuid) { - if (event.reply) event.reply(new Error("必须提供预制体 UUID。")); - return; - } - - // 使用 cc.assetManager.loadAny 通过 UUID 加载 (Cocos 2.4+) - // 如果是旧版,可能需要 cc.loader.load({uuid: ...}),但在 2.4 环境下 assetManager 更推荐 - cc.assetManager.loadAny(prefabUuid, (err, prefab) => { - if (err) { - if (event.reply) event.reply(new Error(`加载预制体失败: ${err.message}`)); - return; - } - - // 实例化预制体 - const instance = cc.instantiate(prefab); - if (!instance) { - if (event.reply) event.reply(new Error("实例化预制体失败")); - return; - } - - // 设置父节点 - let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; - if (parent) { - instance.parent = parent; - - // 通知场景变脏 - Editor.Ipc.sendToMain("scene:dirty"); - - // 通知 UI 刷新 - setTimeout(() => { - Editor.Ipc.sendToAll("scene:node-created", { - uuid: instance.uuid, - parentUuid: parent.uuid, - }); - }, 10); - - if (event.reply) event.reply(null, `预制体实例化成功,UUID: ${instance.uuid}`); - } else { - if (event.reply) event.reply(new Error("找不到父节点")); - } - }); - }, - - /** - * 根据特定条件在场景中搜索节点 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (conditions, recursive) - */ - "find-gameobjects": function (event, args) { - const { conditions, recursive = true } = args; - const result = []; - const scene = cc.director.getScene(); - - function searchNode(node) { - // 跳过编辑器内部的私有节点 - if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { - return; - } - - // 检查节点是否满足条件 - let match = true; - - if (conditions.name && !node.name.includes(conditions.name)) { - match = false; - } - - if (conditions.component) { - let hasComponent = false; - try { - if (conditions.component.startsWith("cc.")) { - const className = conditions.component.replace("cc.", ""); - hasComponent = node.getComponent(cc[className]) !== null; - } else { - hasComponent = node.getComponent(conditions.component) !== null; - } - } catch (e) { - hasComponent = false; - } - if (!hasComponent) { - match = false; - } - } - - if (conditions.active !== undefined && node.active !== conditions.active) { - match = false; - } - - if (match) { - result.push({ - uuid: node.uuid, - name: node.name, - active: node.active, - position: { x: node.x, y: node.y }, - scale: { x: node.scaleX, y: node.scaleY }, - size: { width: node.width, height: node.height }, - components: node._components.map((c) => c.__typename), - }); - } - - // 递归搜索子节点 - if (recursive) { - for (let i = 0; i < node.childrenCount; i++) { - searchNode(node.children[i]); - } - } - } - - // 从场景根节点开始搜索 - if (scene) { - searchNode(scene); - } - - if (event.reply) { - event.reply(null, result); - } - }, - - /** - * 删除指定的场景节点 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (uuid) - */ - "delete-node": function (event, args) { - const { uuid } = args; - const node = findNode(uuid); - if (node) { - const parent = node.parent; - node.destroy(); - Editor.Ipc.sendToMain("scene:dirty"); - // 延迟通知以确保节点已被移除 - setTimeout(() => { - if (parent) { - Editor.Ipc.sendToAll("scene:node-changed", { uuid: parent.uuid }); - } - // 广播节点删除事件 - Editor.Ipc.sendToAll("scene:node-deleted", { uuid: uuid }); - }, 10); - - if (event.reply) event.reply(null, `节点 ${uuid} 已删除`); - } else { - if (event.reply) event.reply(new Error(`找不到节点: ${uuid}`)); - } - }, - - /** - * 管理高效的全场景特效 (粒子系统) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (action, nodeId, properties, name, parentId) - */ - "manage-vfx": function (event, args) { - const { action, nodeId, properties, name, parentId } = args; - const scene = cc.director.getScene(); - - const applyParticleProperties = (particleSystem, props) => { - if (!props) return; - - if (props.duration !== undefined) particleSystem.duration = props.duration; - if (props.emissionRate !== undefined) particleSystem.emissionRate = props.emissionRate; - if (props.life !== undefined) particleSystem.life = props.life; - if (props.lifeVar !== undefined) particleSystem.lifeVar = props.lifeVar; - - // 【关键修复】启用自定义属性,否则属性修改可能不生效 - particleSystem.custom = true; - - if (props.startColor) particleSystem.startColor = new cc.Color().fromHEX(props.startColor); - if (props.endColor) particleSystem.endColor = new cc.Color().fromHEX(props.endColor); - - if (props.startSize !== undefined) particleSystem.startSize = props.startSize; - if (props.endSize !== undefined) particleSystem.endSize = props.endSize; - - if (props.speed !== undefined) particleSystem.speed = props.speed; - if (props.angle !== undefined) particleSystem.angle = props.angle; - - if (props.gravity) { - if (props.gravity.x !== undefined) particleSystem.gravity.x = props.gravity.x; - if (props.gravity.y !== undefined) particleSystem.gravity.y = props.gravity.y; - } - - // 处理文件/纹理加载 - if (props.file) { - // main.js 已经将 db:// 路径转换为 UUID - // 如果用户直接传递 URL (http/https) 或其他格式,cc.assetManager.loadAny 也能处理 - const uuid = props.file; - cc.assetManager.loadAny(uuid, (err, asset) => { - if (!err) { - if (asset instanceof cc.ParticleAsset) { - particleSystem.file = asset; - } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { - particleSystem.texture = asset; - } - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - } else if (!particleSystem.texture && !particleSystem.file && args.defaultSpriteUuid) { - // 【关键修复】如果没有纹理,加载默认纹理 (UUID 由 main.js 传入) - Editor.log(`[mcp-bridge] Loading default texture with UUID: ${args.defaultSpriteUuid}`); - cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { - if (err) { - Editor.error(`[mcp-bridge] Failed to load default texture: ${err.message}`); - } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { - Editor.log(`[mcp-bridge] Default texture loaded successfully.`); - particleSystem.texture = asset; - Editor.Ipc.sendToMain("scene:dirty"); - } else { - Editor.warn(`[mcp-bridge] Loaded asset is not a texture: ${asset}`); - } - }); - } - }; - - if (action === "create") { - let newNode = new cc.Node(name || "New Particle"); - let particleSystem = newNode.addComponent(cc.ParticleSystem); - - // 设置默认值 - particleSystem.resetSystem(); - particleSystem.custom = true; // 确保新创建的也是 custom 模式 - - applyParticleProperties(particleSystem, properties); - - let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; - if (parent) { - newNode.parent = parent; - Editor.Ipc.sendToMain("scene:dirty"); - setTimeout(() => { - Editor.Ipc.sendToAll("scene:node-created", { - uuid: newNode.uuid, - parentUuid: parent.uuid, - }); - }, 10); - if (event.reply) event.reply(null, newNode.uuid); - } else { - if (event.reply) event.reply(new Error("找不到父节点")); - } - - } else if (action === "update") { - let node = findNode(nodeId); - if (node) { - let particleSystem = node.getComponent(cc.ParticleSystem); - if (!particleSystem) { - // 如果没有组件,自动添加 - particleSystem = node.addComponent(cc.ParticleSystem); - } - - applyParticleProperties(particleSystem, properties); - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - if (event.reply) event.reply(null, "特效已更新"); - } else { - if (event.reply) event.reply(new Error("找不到节点")); - } - - } else if (action === "get_info") { - let node = findNode(nodeId); - if (node) { - let ps = node.getComponent(cc.ParticleSystem); - if (ps) { - const info = { - duration: ps.duration, - emissionRate: ps.emissionRate, - life: ps.life, - lifeVar: ps.lifeVar, - startColor: ps.startColor.toHEX("#RRGGBB"), - endColor: ps.endColor.toHEX("#RRGGBB"), - startSize: ps.startSize, - endSize: ps.endSize, - speed: ps.speed, - angle: ps.angle, - gravity: { x: ps.gravity.x, y: ps.gravity.y }, - file: ps.file ? ps.file.name : null - }; - if (event.reply) event.reply(null, info); - } else { - if (event.reply) event.reply(null, { hasParticleSystem: false }); - } - } else { - if (event.reply) event.reply(new Error("找不到节点")); - } - } else { - if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`)); - } - }, - - /** - * 控制节点的动画组件 (播放、暂停、停止等) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (action, nodeId, clipName) - */ - "manage-animation": function (event, args) { - const { action, nodeId, clipName } = args; - const node = findNode(nodeId); - - if (!node) { - if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`)); - return; - } - - const anim = node.getComponent(cc.Animation); - if (!anim) { - if (event.reply) event.reply(new Error(`在节点 ${nodeId} 上找不到 Animation 组件`)); - 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, "正在播放默认动画剪辑"); - } else { - anim.play(clipName); - if (event.reply) event.reply(null, `正在播放动画剪辑: ${clipName}`); - } - break; - - case "stop": - anim.stop(); - if (event.reply) event.reply(null, "动画已停止"); - break; - - case "pause": - anim.pause(); - if (event.reply) event.reply(null, "动画已暂停"); - break; - - case "resume": - anim.resume(); - if (event.reply) event.reply(null, "动画已恢复播放"); - break; - - default: - if (event.reply) event.reply(new Error(`未知的动画操作类型: ${action}`)); - break; - } - }, + /** + * 修改节点的基础属性 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (id, path, value) + */ + "set-property": function (event, args) { + const { id, path, value } = args; + + // 1. 获取节点 + let node = findNode(id); + + if (node) { + // 2. 修改属性 + if (path === "name") { + node.name = value; + } else { + node[path] = value; + } + + // 3. 【解决报错的关键】告诉编辑器场景变脏了(需要保存) + // 在场景进程中,我们发送 IPC 给主进程 + Editor.Ipc.sendToMain("scene:dirty"); + + // 4. 【额外补丁】通知层级管理器(Hierarchy)同步更新节点名称 + // 否则你修改了名字,层级管理器可能还是显示旧名字 + Editor.Ipc.sendToAll("scene:node-changed", { + uuid: id, + }); + + if (event.reply) { + event.reply(null, `节点 ${id} 已更新为 ${value}`); + } + } else { + if (event.reply) { + event.reply(new Error("场景脚本:找不到节点 " + id)); + } + } + }, + /** + * 获取当前场景的完整层级树 + * @param {Object} event IPC 事件对象 + */ + "get-hierarchy": function (event) { + const scene = cc.director.getScene(); + + /** + * 递归遍历并序列化节点树 + * @param {cc.Node} node 目标节点 + * @returns {Object|null} 序列化后的节点数据 + */ + function dumpNodes(node) { + // 【优化】跳过编辑器内部的私有节点,减少数据量 + if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { + return null; + } + + let nodeData = { + name: node.name, + uuid: node.uuid, + active: node.active, + position: { x: Math.round(node.x), y: Math.round(node.y) }, + scale: { x: node.scaleX, y: node.scaleY }, + size: { width: node.width, height: node.height }, + // 记录组件类型,让 AI 知道这是个什么节点 + components: node._components.map((c) => c.__typename), + children: [], + }; + + for (let i = 0; i < node.childrenCount; i++) { + let childData = dumpNodes(node.children[i]); + if (childData) nodeData.children.push(childData); + } + return nodeData; + } + + const hierarchy = dumpNodes(scene); + if (event.reply) event.reply(null, hierarchy); + }, + + /** + * 批量更新节点的变换信息 (坐标、缩放、颜色) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (id, x, y, scaleX, scaleY, color) + */ + "update-node-transform": function (event, args) { + const { id, x, y, scaleX, scaleY, color } = args; + Editor.log(`[scene-script] update-node-transform called for ${id} with args: ${JSON.stringify(args)}`); + + let node = findNode(id); + + if (node) { + Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`); + + // 使用 scene:set-property 实现支持 Undo 的属性修改 + // 注意:IPC 消息需要发送到 'scene' 面板 + if (x !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "x", + type: "Number", + value: Number(x), + }); + } + if (y !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "y", + type: "Number", + value: Number(y), + }); + } + if (args.width !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "width", + type: "Number", + value: Number(args.width), + }); + } + if (args.height !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "height", + type: "Number", + value: Number(args.height), + }); + } + if (scaleX !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "scaleX", + type: "Number", + value: Number(scaleX), + }); + } + if (scaleY !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "scaleY", + type: "Number", + value: Number(scaleY), + }); + } + 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 }, + }); + } + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: id }); + + Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`); + if (event.reply) event.reply(null, "变换信息已更新"); + } else { + if (event.reply) event.reply(new Error("找不到节点")); + } + }, + /** + * 在场景中创建新节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (name, parentId, type) + */ + "create-node": function (event, args) { + const { name, parentId, type } = args; + const scene = cc.director.getScene(); + if (!scene) { + if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。")); + return; + } + + let newNode = null; + + // 特殊处理:如果是创建 Canvas,自动设置好适配 + if (type === "canvas" || name === "Canvas") { + newNode = new cc.Node("Canvas"); + let canvas = newNode.addComponent(cc.Canvas); + newNode.addComponent(cc.Widget); + // 设置默认设计分辨率 + canvas.designResolution = cc.size(960, 640); + canvas.fitHeight = true; + // 自动在 Canvas 下创建一个 Camera + let camNode = new cc.Node("Main Camera"); + camNode.addComponent(cc.Camera); + camNode.parent = newNode; + } else if (type === "sprite") { + newNode = new cc.Node(name || "新建精灵节点"); + let sprite = newNode.addComponent(cc.Sprite); + // 设置为 CUSTOM 模式 + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + // 为精灵设置默认尺寸 + newNode.width = 100; + newNode.height = 100; + + // 加载引擎默认图做占位 + if (args.defaultSpriteUuid) { + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { + sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } + } else if (type === "button") { + newNode = new cc.Node(name || "新建按钮节点"); + let sprite = newNode.addComponent(cc.Sprite); + newNode.addComponent(cc.Button); + + // 设置为 CUSTOM 模式并应用按钮专用尺寸 + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + newNode.width = 150; + newNode.height = 50; + + // 设置稍暗的背景颜色 (#A0A0A0),以便于看清其上的文字 + newNode.color = new cc.Color(160, 160, 160); + + // 加载引擎默认图 + if (args.defaultSpriteUuid) { + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { + sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } + } else if (type === "label") { + newNode = new cc.Node(name || "新建文本节点"); + let l = newNode.addComponent(cc.Label); + l.string = "新文本"; + newNode.width = 120; + newNode.height = 40; + } else { + newNode = new cc.Node(name || "新建节点"); + } + + // 设置层级 + let parent = parentId ? findNode(parentId) : scene; + if (parent) { + newNode.parent = parent; + + // 【优化】通知主进程场景变脏 + Editor.Ipc.sendToMain("scene:dirty"); + + // 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环 + setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: newNode.uuid, + parentUuid: parent.uuid, + }); + }, 10); + + if (event.reply) event.reply(null, newNode.uuid); + } else { + if (event.reply) event.reply(new Error(`无法创建节点:找不到父节点 ${parentId}`)); + } + }, + + /** + * 管理节点上的组件 (添加、移除、更新属性) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (nodeId, action, componentType, componentId, properties) + */ + "manage-components": function (event, args) { + const { nodeId, action, componentType, componentId, properties } = args; + let node = findNode(nodeId); + + /** + * 辅助函数:应用属性并智能解析 (支持 UUID 资源与节点引用) + * @param {cc.Component} component 目标组件实例 + * @param {Object} props 待更新的属性键值对 + */ + const applyProperties = (component, props) => { + if (!props) return; + // 尝试获取组件类的属性定义 + const compClass = component.constructor; + + for (const [key, value] of Object.entries(props)) { + // 【核心修复】专门处理各类事件属性 (ClickEvents, ScrollEvents 等) + const isEventProp = + Array.isArray(value) && (key.toLowerCase().endsWith("events") || key === "clickEvents"); + + if (isEventProp) { + const eventHandlers = []; + for (const item of value) { + if (typeof item === "object" && (item.target || item.component || item.handler)) { + const handler = new cc.Component.EventHandler(); + + // 解析 Target Node + if (item.target) { + let targetNode = findNode(item.target); + if (!targetNode && item.target instanceof cc.Node) { + targetNode = item.target; + } + + if (targetNode) { + handler.target = targetNode; + Editor.log(`[scene-script] Resolved event target: ${targetNode.name}`); + } + } + + if (item.component) handler.component = item.component; + if (item.handler) handler.handler = item.handler; + if (item.customEventData !== undefined) + handler.customEventData = String(item.customEventData); + + eventHandlers.push(handler); + } else { + // 如果不是对象,原样保留 + eventHandlers.push(item); + } + } + component[key] = eventHandlers; + continue; // 处理完事件数组,跳出本次循环 + } + + // 检查属性是否存在 + if (component[key] !== undefined) { + let finalValue = value; + + // 【核心逻辑】智能类型识别与赋值 + try { + const attrs = (cc.Class.Attr.getClassAttrs && cc.Class.Attr.getClassAttrs(compClass)) || {}; + let propertyType = attrs[key] ? attrs[key].type : null; + if (!propertyType && attrs[key + "$_$ctor"]) { + propertyType = attrs[key + "$_$ctor"]; + } + + let isAsset = + propertyType && + (propertyType.prototype instanceof cc.Asset || + propertyType === cc.Asset || + propertyType === cc.Prefab || + propertyType === cc.SpriteFrame); + let isAssetArray = + Array.isArray(value) && (key === "materials" || key.toLowerCase().includes("assets")); + + // 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点 + if (!isAsset && !isAssetArray && typeof value === "string" && value.length > 20) { + const lowerKey = key.toLowerCase(); + const assetKeywords = [ + "prefab", + "sprite", + "texture", + "material", + "skeleton", + "spine", + "atlas", + "font", + "audio", + "data", + ]; + if (assetKeywords.some((k) => lowerKey.includes(k))) { + if (!findNode(value)) { + isAsset = true; + } + } + } + + if (isAsset || isAssetArray) { + // 1. 处理资源引用 (单个或数组) + const uuids = isAssetArray ? value : [value]; + const loadedAssets = []; + let loadedCount = 0; + + if (uuids.length === 0) { + component[key] = []; + return; + } + + uuids.forEach((uuid, idx) => { + if (typeof uuid !== "string" || uuid.length < 10) { + loadedCount++; + return; + } + cc.AssetLibrary.loadAsset(uuid, (err, asset) => { + loadedCount++; + if (!err && asset) { + loadedAssets[idx] = asset; + Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`); + } else { + Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`); + } + + if (loadedCount === uuids.length) { + if (isAssetArray) { + // 过滤掉加载失败的 + component[key] = loadedAssets.filter((a) => !!a); + } else { + if (loadedAssets[0]) component[key] = loadedAssets[0]; + } + + // 通知编辑器 UI 更新 + const compIndex = node._components.indexOf(component); + if (compIndex !== -1) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id: node.uuid, + path: `_components.${compIndex}.${key}`, + type: isAssetArray ? "Array" : "Object", + value: isAssetArray ? uuids.map((u) => ({ uuid: u })) : { uuid: value }, + isSubProp: false, + }); + } + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + }); + // 【重要修复】使用 continue 而不是 return,确保处理完 Asset 属性后 + // 还能继续处理后续的普通属性 (如 type, sizeMode 等) + continue; + } else if ( + propertyType && + (propertyType.prototype instanceof cc.Component || + propertyType === cc.Component || + propertyType === cc.Node) + ) { + // 2. 处理节点或组件引用 + const targetNode = findNode(value); + if (targetNode) { + if (propertyType === cc.Node) { + finalValue = targetNode; + } else { + const targetComp = targetNode.getComponent(propertyType); + if (targetComp) { + finalValue = targetComp; + } else { + Editor.warn( + `[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`, + ); + } + } + Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`); + } else if (value && value.length > 20) { + // 如果明确是组件/节点类型但找不到,才报错 + Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`); + } + } else { + // 3. 通用启发式 (找不到类型时的 fallback) + if (typeof value === "string" && value.length > 20) { + const targetNode = findNode(value); + if (targetNode) { + finalValue = targetNode; + Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`); + } else { + // 找不到节点且是 UUID -> 视为资源 + const compIndex = node._components.indexOf(component); + if (compIndex !== -1) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id: node.uuid, + path: `_components.${compIndex}.${key}`, + type: "Object", + value: { uuid: value }, + isSubProp: false, + }); + Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`); + } + return; + } + } + } + } catch (e) { + Editor.warn(`[scene-script] 解析属性 ${key} 失败: ${e.message}`); + } + + component[key] = finalValue; + } + } + }; + + if (!node) { + if (event.reply) event.reply(new Error("找不到节点")); + return; + } + + switch (action) { + case "add": + if (!componentType) { + if (event.reply) event.reply(new Error("必须提供组件类型")); + 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(`找不到组件类型: ${componentType}`)); + return; + } + + // 添加组件 + const component = node.addComponent(compClass); + + // 设置属性 + if (properties) { + applyProperties(component, properties); + } + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + + if (event.reply) event.reply(null, `组件 ${componentType} 已添加`); + } catch (err) { + if (event.reply) event.reply(new Error(`添加组件失败: ${err.message}`)); + } + break; + + case "remove": + if (!componentId) { + if (event.reply) event.reply(new Error("必须提供组件 ID")); + return; + } + + try { + // 查找并移除组件 + let component = null; + if (node._components) { + for (let i = 0; i < node._components.length; i++) { + if (node._components[i].uuid === componentId) { + component = node._components[i]; + break; + } + } + } + + if (component) { + node.removeComponent(component); + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + if (event.reply) event.reply(null, "组件已移除"); + } else { + if (event.reply) event.reply(new Error("找不到组件")); + } + } catch (err) { + if (event.reply) event.reply(new Error(`移除组件失败: ${err.message}`)); + } + break; + + case "update": + // 更新现有组件属性 + if (!componentType) { + // 如果提供了 componentId,可以只用 componentId + // 但 Cocos 2.4 uuid 获取组件比较麻烦,最好还是有 type 或者遍历 + } + + try { + let targetComp = null; + + // 1. 尝试通过 componentId 查找 + if (componentId) { + if (node._components) { + for (let i = 0; i < node._components.length; i++) { + if (node._components[i].uuid === componentId) { + targetComp = node._components[i]; + break; + } + } + } + } + + // 2. 尝试通过 type 查找 + if (!targetComp && componentType) { + let compClass = null; + if (componentType.startsWith("cc.")) { + const className = componentType.replace("cc.", ""); + compClass = cc[className]; + } else { + compClass = cc.js.getClassByName(componentType); + } + if (compClass) { + targetComp = node.getComponent(compClass); + } + } + + if (targetComp) { + if (properties) { + applyProperties(targetComp, properties); + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + if (event.reply) event.reply(null, "组件属性已更新"); + } else { + if (event.reply) event.reply(null, "没有需要更新的属性"); + } + } else { + if (event.reply) + event.reply(new Error(`找不到组件 (类型: ${componentType}, ID: ${componentId})`)); + } + } catch (err) { + if (event.reply) event.reply(new Error(`更新组件失败: ${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 { + // 安全序列化检查 + const val = c[key]; + if (val === null || val === undefined) { + properties[key] = val; + continue; + } + + // 基础类型是安全的 + if (typeof val !== "object") { + properties[key] = val; + continue; + } + + // 特殊 Cocos 类型 + if (val instanceof cc.ValueType) { + properties[key] = val.toString(); + } else if (val instanceof cc.Asset) { + properties[key] = `资源(${val.name})`; + } else if (val instanceof cc.Node) { + properties[key] = `节点(${val.name})`; + } else if (val instanceof cc.Component) { + properties[key] = `组件(${val.name}<${val.__typename}>)`; + } else { + // 数组和普通对象 + // 尝试转换为纯 JSON 数据以避免 IPC 错误(如包含原生对象/循环引用) + try { + const jsonStr = JSON.stringify(val); + // 确保不传递原始对象引用 + properties[key] = JSON.parse(jsonStr); + } catch (e) { + // 如果 JSON 失败(例如循环引用),格式化为字符串 + properties[key] = + `[复杂对象: ${val.constructor ? val.constructor.name : typeof val}]`; + } + } + } catch (e) { + properties[key] = "[Serialization Error]"; + } + } + } + return { + type: cc.js.getClassName(c) || c.constructor.name || "Unknown", + uuid: c.uuid, + properties: properties, + }; + }); + if (event.reply) event.reply(null, components); + } catch (err) { + if (event.reply) event.reply(new Error(`获取组件失败: ${err.message}`)); + } + break; + + default: + if (event.reply) event.reply(new Error(`未知的组件操作类型: ${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; + }, + + "instantiate-prefab": function (event, args) { + const { prefabUuid, parentId } = args; + const scene = cc.director.getScene(); + + if (!scene) { + if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。")); + return; + } + + if (!prefabUuid) { + if (event.reply) event.reply(new Error("必须提供预制体 UUID。")); + return; + } + + // 使用 cc.assetManager.loadAny 通过 UUID 加载 (Cocos 2.4+) + // 如果是旧版,可能需要 cc.loader.load({uuid: ...}),但在 2.4 环境下 assetManager 更推荐 + cc.assetManager.loadAny(prefabUuid, (err, prefab) => { + if (err) { + if (event.reply) event.reply(new Error(`加载预制体失败: ${err.message}`)); + return; + } + + // 实例化预制体 + const instance = cc.instantiate(prefab); + if (!instance) { + if (event.reply) event.reply(new Error("实例化预制体失败")); + return; + } + + // 设置父节点 + let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; + if (parent) { + instance.parent = parent; + + // 通知场景变脏 + Editor.Ipc.sendToMain("scene:dirty"); + + // 通知 UI 刷新 + setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: instance.uuid, + parentUuid: parent.uuid, + }); + }, 10); + + if (event.reply) event.reply(null, `预制体实例化成功,UUID: ${instance.uuid}`); + } else { + if (event.reply) event.reply(new Error("找不到父节点")); + } + }); + }, + + /** + * 根据特定条件在场景中搜索节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (conditions, recursive) + */ + "find-gameobjects": function (event, args) { + const { conditions, recursive = true } = args; + const result = []; + const scene = cc.director.getScene(); + + function searchNode(node) { + // 跳过编辑器内部的私有节点 + if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { + return; + } + + // 检查节点是否满足条件 + let match = true; + + if (conditions.name && !node.name.includes(conditions.name)) { + match = false; + } + + if (conditions.component) { + let hasComponent = false; + try { + if (conditions.component.startsWith("cc.")) { + const className = conditions.component.replace("cc.", ""); + hasComponent = node.getComponent(cc[className]) !== null; + } else { + hasComponent = node.getComponent(conditions.component) !== null; + } + } catch (e) { + hasComponent = false; + } + if (!hasComponent) { + match = false; + } + } + + if (conditions.active !== undefined && node.active !== conditions.active) { + match = false; + } + + if (match) { + result.push({ + uuid: node.uuid, + name: node.name, + active: node.active, + position: { x: node.x, y: node.y }, + scale: { x: node.scaleX, y: node.scaleY }, + size: { width: node.width, height: node.height }, + components: node._components.map((c) => c.__typename), + }); + } + + // 递归搜索子节点 + if (recursive) { + for (let i = 0; i < node.childrenCount; i++) { + searchNode(node.children[i]); + } + } + } + + // 从场景根节点开始搜索 + if (scene) { + searchNode(scene); + } + + if (event.reply) { + event.reply(null, result); + } + }, + + /** + * 删除指定的场景节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (uuid) + */ + "delete-node": function (event, args) { + const { uuid } = args; + const node = findNode(uuid); + if (node) { + const parent = node.parent; + node.destroy(); + Editor.Ipc.sendToMain("scene:dirty"); + // 延迟通知以确保节点已被移除 + setTimeout(() => { + if (parent) { + Editor.Ipc.sendToAll("scene:node-changed", { uuid: parent.uuid }); + } + // 广播节点删除事件 + Editor.Ipc.sendToAll("scene:node-deleted", { uuid: uuid }); + }, 10); + + if (event.reply) event.reply(null, `节点 ${uuid} 已删除`); + } else { + if (event.reply) event.reply(new Error(`找不到节点: ${uuid}`)); + } + }, + + /** + * 管理高效的全场景特效 (粒子系统) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (action, nodeId, properties, name, parentId) + */ + "manage-vfx": function (event, args) { + const { action, nodeId, properties, name, parentId } = args; + const scene = cc.director.getScene(); + + const applyParticleProperties = (particleSystem, props) => { + if (!props) return; + + if (props.duration !== undefined) particleSystem.duration = props.duration; + if (props.emissionRate !== undefined) particleSystem.emissionRate = props.emissionRate; + if (props.life !== undefined) particleSystem.life = props.life; + if (props.lifeVar !== undefined) particleSystem.lifeVar = props.lifeVar; + + // 【关键修复】启用自定义属性,否则属性修改可能不生效 + particleSystem.custom = true; + + if (props.startColor) particleSystem.startColor = new cc.Color().fromHEX(props.startColor); + if (props.endColor) particleSystem.endColor = new cc.Color().fromHEX(props.endColor); + + if (props.startSize !== undefined) particleSystem.startSize = props.startSize; + if (props.endSize !== undefined) particleSystem.endSize = props.endSize; + + if (props.speed !== undefined) particleSystem.speed = props.speed; + if (props.angle !== undefined) particleSystem.angle = props.angle; + + if (props.gravity) { + if (props.gravity.x !== undefined) particleSystem.gravity.x = props.gravity.x; + if (props.gravity.y !== undefined) particleSystem.gravity.y = props.gravity.y; + } + + // 处理文件/纹理加载 + if (props.file) { + // main.js 已经将 db:// 路径转换为 UUID + // 如果用户直接传递 URL (http/https) 或其他格式,cc.assetManager.loadAny 也能处理 + const uuid = props.file; + cc.assetManager.loadAny(uuid, (err, asset) => { + if (!err) { + if (asset instanceof cc.ParticleAsset) { + particleSystem.file = asset; + } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { + particleSystem.texture = asset; + } + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } else if (!particleSystem.texture && !particleSystem.file && args.defaultSpriteUuid) { + // 【关键修复】如果没有纹理,加载默认纹理 (UUID 由 main.js 传入) + Editor.log(`[mcp-bridge] Loading default texture with UUID: ${args.defaultSpriteUuid}`); + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (err) { + Editor.error(`[mcp-bridge] Failed to load default texture: ${err.message}`); + } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { + Editor.log(`[mcp-bridge] Default texture loaded successfully.`); + particleSystem.texture = asset; + Editor.Ipc.sendToMain("scene:dirty"); + } else { + Editor.warn(`[mcp-bridge] Loaded asset is not a texture: ${asset}`); + } + }); + } + }; + + if (action === "create") { + let newNode = new cc.Node(name || "New Particle"); + let particleSystem = newNode.addComponent(cc.ParticleSystem); + + // 设置默认值 + particleSystem.resetSystem(); + particleSystem.custom = true; // 确保新创建的也是 custom 模式 + + applyParticleProperties(particleSystem, properties); + + let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; + if (parent) { + newNode.parent = parent; + Editor.Ipc.sendToMain("scene:dirty"); + setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: newNode.uuid, + parentUuid: parent.uuid, + }); + }, 10); + if (event.reply) event.reply(null, newNode.uuid); + } else { + if (event.reply) event.reply(new Error("找不到父节点")); + } + } else if (action === "update") { + let node = findNode(nodeId); + if (node) { + let particleSystem = node.getComponent(cc.ParticleSystem); + if (!particleSystem) { + // 如果没有组件,自动添加 + particleSystem = node.addComponent(cc.ParticleSystem); + } + + applyParticleProperties(particleSystem, properties); + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + if (event.reply) event.reply(null, "特效已更新"); + } else { + if (event.reply) event.reply(new Error("找不到节点")); + } + } else if (action === "get_info") { + let node = findNode(nodeId); + if (node) { + let ps = node.getComponent(cc.ParticleSystem); + if (ps) { + const info = { + duration: ps.duration, + emissionRate: ps.emissionRate, + life: ps.life, + lifeVar: ps.lifeVar, + startColor: ps.startColor.toHEX("#RRGGBB"), + endColor: ps.endColor.toHEX("#RRGGBB"), + startSize: ps.startSize, + endSize: ps.endSize, + speed: ps.speed, + angle: ps.angle, + gravity: { x: ps.gravity.x, y: ps.gravity.y }, + file: ps.file ? ps.file.name : null, + }; + if (event.reply) event.reply(null, info); + } else { + if (event.reply) event.reply(null, { hasParticleSystem: false }); + } + } else { + if (event.reply) event.reply(new Error("找不到节点")); + } + } else { + if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`)); + } + }, + + /** + * 控制节点的动画组件 (播放、暂停、停止等) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (action, nodeId, clipName) + */ + "manage-animation": function (event, args) { + const { action, nodeId, clipName } = args; + const node = findNode(nodeId); + + if (!node) { + if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`)); + return; + } + + const anim = node.getComponent(cc.Animation); + if (!anim) { + if (event.reply) event.reply(new Error(`在节点 ${nodeId} 上找不到 Animation 组件`)); + 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, "正在播放默认动画剪辑"); + } else { + anim.play(clipName); + if (event.reply) event.reply(null, `正在播放动画剪辑: ${clipName}`); + } + break; + + case "stop": + anim.stop(); + if (event.reply) event.reply(null, "动画已停止"); + break; + + case "pause": + anim.pause(); + if (event.reply) event.reply(null, "动画已暂停"); + break; + + case "resume": + anim.resume(); + if (event.reply) event.reply(null, "动画已恢复播放"); + break; + + default: + if (event.reply) event.reply(new Error(`未知的动画操作类型: ${action}`)); + break; + } + }, }; diff --git a/src/IpcManager.ts b/src/IpcManager.ts index cdd4aca..5a3a309 100644 --- a/src/IpcManager.ts +++ b/src/IpcManager.ts @@ -1,107 +1,106 @@ - // @ts-ignore -const fs = require('fs'); +const fs = require("fs"); // @ts-ignore -const path = require('path'); +const path = require("path"); /** * IPC 消息管理器 * 负责解析 IPC 文档并执行消息测试 */ export class IpcManager { - /** - * 获取所有 IPC 消息列表 - * @returns 消息定义列表 - */ - public static getIpcMessages(): any[] { - // 获取文档路径 - // @ts-ignore - const docPath = Editor.url('packages://mcp-bridge/IPC_MESSAGES.md'); - if (!fs.existsSync(docPath)) { - // @ts-ignore - Editor.error(`[IPC Manager] Document not found: ${docPath}`); - return []; - } + /** + * 获取所有 IPC 消息列表 + * @returns 消息定义列表 + */ + public static getIpcMessages(): any[] { + // 获取文档路径 + // @ts-ignore + const docPath = Editor.url("packages://mcp-bridge/IPC_MESSAGES.md"); + if (!fs.existsSync(docPath)) { + // @ts-ignore + Editor.error(`[IPC 管理器] 找不到文档文件: ${docPath}`); + return []; + } - const content = fs.readFileSync(docPath, 'utf-8'); - const messages: any[] = []; + const content = fs.readFileSync(docPath, "utf-8"); + const messages: any[] = []; - // 正则匹配 ### `message-name` - const regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g; - let match; + // 正则匹配 ### `message-name` + const regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g; + let match; - while ((match = regex.exec(content)) !== null) { - const name = match[1]; - const body = match[2]; + while ((match = regex.exec(content)) !== null) { + const name = match[1]; + const body = match[2]; - // 解析用途 - const purposeMatch = body.match(/- \*\*用途\*\*: (.*)/); - const description = purposeMatch ? purposeMatch[1].trim() : "无描述"; + // 解析用途 + const purposeMatch = body.match(/- \*\*用途\*\*: (.*)/); + const description = purposeMatch ? purposeMatch[1].trim() : "无描述"; - // 解析参数 - const paramsMatch = body.match(/- \*\*参数\*\*: (.*)/); - const params = paramsMatch ? paramsMatch[1].trim() : "无"; + // 解析参数 + const paramsMatch = body.match(/- \*\*参数\*\*: (.*)/); + const params = paramsMatch ? paramsMatch[1].trim() : "无"; - // 解析返回值 - const returnMatch = body.match(/- \*\*返回值\*\*: (.*)/); - const returns = returnMatch ? returnMatch[1].trim() : "无"; + // 解析返回值 + const returnMatch = body.match(/- \*\*返回值\*\*: (.*)/); + const returns = returnMatch ? returnMatch[1].trim() : "无"; - // 解析类型 - const typeMatch = body.match(/- \*\*类型\*\*: (.*)/); - const type = typeMatch ? typeMatch[1].trim() : "未定义"; + // 解析类型 + const typeMatch = body.match(/- \*\*类型\*\*: (.*)/); + const type = typeMatch ? typeMatch[1].trim() : "未定义"; - // 解析状态 - const statusMatch = body.match(/- \*\*状态\*\*: (.*)/); - const status = statusMatch ? statusMatch[1].trim() : "未测试"; + // 解析状态 + const statusMatch = body.match(/- \*\*状态\*\*: (.*)/); + const status = statusMatch ? statusMatch[1].trim() : "未测试"; - // 过滤掉广播事件和渲染进程监听的事件 - if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") { - continue; - } + // 过滤掉广播事件和渲染进程监听的事件 + if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") { + continue; + } - messages.push({ - name, - description, - params, - returns, - type, - status - }); - } + messages.push({ + name, + description, + params, + returns, + type, + status, + }); + } - return messages; - } + return messages; + } - /** - * 测试发送 IPC 消息 - * @param name 消息名称 - * @param args 参数 - * @returns Promise 测试结果 - */ - public static async testIpcMessage(name: string, args: any = null): Promise { - return new Promise((resolve) => { - // 简单防呆:防止执行危险操作 - // 如果消息包含 "delete", "remove", "close", "stop" 且没有明确参数确认,则警告 - // 但用户要求"快速验证",所以我们默认允许,但如果是无参调用可能有风险 - // 这里我们尝试使用 Editor.Ipc.sendToMain 或 requestToMain + /** + * 测试发送 IPC 消息 + * @param name 消息名称 + * @param args 参数 + * @returns Promise 测试结果 + */ + public static async testIpcMessage(name: string, args: any = null): Promise { + return new Promise((resolve) => { + // 简单防呆:防止执行危险操作 + // 如果消息包含 "delete", "remove", "close", "stop" 且没有明确参数确认,则警告 + // 但用户要求"快速验证",所以我们默认允许,但如果是无参调用可能有风险 + // 这里我们尝试使用 Editor.Ipc.sendToMain 或 requestToMain - // @ts-ignore - // 简单的测试:只是发送看看是否报错。 - // 对于 request 类型的消息,我们期望有回调 - // Cocos Creator 2.4 API: Editor.Ipc.sendToMain(message, ...args) + // @ts-ignore + // 简单的测试:只是发送看看是否报错。 + // 对于 request 类型的消息,我们期望有回调 + // Cocos Creator 2.4 API: Editor.Ipc.sendToMain(message, ...args) - try { - // @ts-ignore - if (Editor.Ipc.sendToMain) { - // @ts-ignore - Editor.Ipc.sendToMain(name, args); - resolve({ success: true, message: "Message sent (sendToMain)" }); - } else { - resolve({ success: false, message: "Editor.Ipc.sendToMain not available" }); - } - } catch (e: any) { - resolve({ success: false, message: `Error: ${e.message}` }); - } - }); - } + try { + // @ts-ignore + if (Editor.Ipc.sendToMain) { + // @ts-ignore + Editor.Ipc.sendToMain(name, args); + resolve({ success: true, message: "消息已发送 (sendToMain)" }); + } else { + resolve({ success: false, message: "Editor.Ipc.sendToMain 不可用" }); + } + } catch (e: any) { + resolve({ success: false, message: `错误: ${e.message}` }); + } + }); + } } diff --git a/src/IpcUi.ts b/src/IpcUi.ts index df228b5..eb23128 100644 --- a/src/IpcUi.ts +++ b/src/IpcUi.ts @@ -1,192 +1,226 @@ - // @ts-ignore const Editor = window.Editor; export class IpcUi { - private root: ShadowRoot; - private logArea: HTMLTextAreaElement | null = null; - private ipcList: HTMLElement | null = null; - private allMessages: any[] = []; - private filterSelect: HTMLSelectElement | null = null; - private paramInput: HTMLTextAreaElement | null = null; + private root: ShadowRoot; + private logArea: HTMLTextAreaElement | null = null; + private ipcList: HTMLElement | null = null; + private allMessages: any[] = []; + private filterSelect: HTMLSelectElement | null = null; + private paramInput: HTMLTextAreaElement | null = null; - constructor(root: ShadowRoot) { - this.root = root; - this.bindEvents(); - } + /** + * 构造函数 + * @param root Shadow UI 根节点 + */ + constructor(root: ShadowRoot) { + this.root = root; + this.bindEvents(); + } - private bindEvents() { - const btnScan = this.root.querySelector("#btnScanIpc"); - const btnTest = this.root.querySelector("#btnTestIpc"); - const cbSelectAll = this.root.querySelector("#cbSelectAllIpc"); - this.logArea = this.root.querySelector("#ipcLog") as HTMLTextAreaElement; - this.ipcList = this.root.querySelector("#ipcList") as HTMLElement; - this.filterSelect = this.root.querySelector("#ipcFilter") as HTMLSelectElement; - this.paramInput = this.root.querySelector("#ipcParams") as HTMLTextAreaElement; + /** + * 绑定 UI 事件 + */ + private bindEvents() { + const btnScan = this.root.querySelector("#btnScanIpc"); + const btnTest = this.root.querySelector("#btnTestIpc"); + const cbSelectAll = this.root.querySelector("#cbSelectAllIpc"); + this.logArea = this.root.querySelector("#ipcLog") as HTMLTextAreaElement; + this.ipcList = this.root.querySelector("#ipcList") as HTMLElement; + this.filterSelect = this.root.querySelector("#ipcFilter") as HTMLSelectElement; + this.paramInput = this.root.querySelector("#ipcParams") as HTMLTextAreaElement; - if (btnScan) { - btnScan.addEventListener("confirm", () => this.scanMessages()); - } - if (btnTest) { - btnTest.addEventListener("confirm", () => this.testSelected()); - } - if (cbSelectAll) { - cbSelectAll.addEventListener("change", (e: any) => this.toggleAll(e.detail ? e.detail.value : (e.target.value === 'true' || e.target.checked))); - } - if (this.filterSelect) { - this.filterSelect.addEventListener("change", () => this.filterMessages()); - } - } + if (btnScan) { + btnScan.addEventListener("confirm", () => this.scanMessages()); + } + if (btnTest) { + btnTest.addEventListener("confirm", () => this.testSelected()); + } + if (cbSelectAll) { + cbSelectAll.addEventListener("change", (e: any) => + this.toggleAll(e.detail ? e.detail.value : e.target.value === "true" || e.target.checked), + ); + } + if (this.filterSelect) { + this.filterSelect.addEventListener("change", () => this.filterMessages()); + } + } - private scanMessages() { - this.log("Scanning IPC messages..."); - // @ts-ignore - Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", (err: any, msgs: any[]) => { - if (err) { - this.log(`Scan Error: ${err}`); - return; - } - if (msgs) { - this.allMessages = msgs; - this.filterMessages(); - this.log(`Found ${msgs.length} messages.`); - } else { - this.log("No messages found."); - } - }); - } + /** + * 扫描 IPC 消息 + */ + private scanMessages() { + this.log("正在扫描 IPC 消息..."); + // @ts-ignore + Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", (err: any, msgs: any[]) => { + if (err) { + this.log(`扫描错误: ${err}`); + return; + } + if (msgs) { + this.allMessages = msgs; + this.filterMessages(); + this.log(`找到 ${msgs.length} 条消息。`); + } else { + this.log("未找到任何消息。"); + } + }); + } - private filterMessages() { - if (!this.allMessages) return; - const filter = this.filterSelect ? this.filterSelect.value : "all"; + /** + * 根据当前选择器过滤消息列表 + */ + private filterMessages() { + if (!this.allMessages) return; + const filter = this.filterSelect ? this.filterSelect.value : "all"; - let filtered = this.allMessages; - if (filter === "available") { - filtered = this.allMessages.filter(m => m.status === "可用"); - } else if (filter === "unavailable") { - filtered = this.allMessages.filter(m => m.status && m.status.includes("不可用")); - } else if (filter === "untested") { - filtered = this.allMessages.filter(m => !m.status || m.status === "未测试"); - } + let filtered = this.allMessages; + if (filter === "available") { + filtered = this.allMessages.filter((m) => m.status === "可用"); + } else if (filter === "unavailable") { + filtered = this.allMessages.filter((m) => m.status && m.status.includes("不可用")); + } else if (filter === "untested") { + filtered = this.allMessages.filter((m) => !m.status || m.status === "未测试"); + } - this.renderList(filtered); - } + this.renderList(filtered); + } - private renderList(msgs: any[]) { - if (!this.ipcList) return; - this.ipcList.innerHTML = ""; + /** + * 渲染消息列表 UI + * @param msgs 消息对象数组 + */ + private renderList(msgs: any[]) { + if (!this.ipcList) return; + this.ipcList.innerHTML = ""; - msgs.forEach(msg => { - const item = document.createElement("div"); - item.className = "ipc-item"; - item.style.padding = "4px"; - item.style.borderBottom = "1px solid #333"; - item.style.display = "flex"; - item.style.alignItems = "center"; + msgs.forEach((msg) => { + const item = document.createElement("div"); + item.className = "ipc-item"; + item.style.padding = "4px"; + item.style.borderBottom = "1px solid #333"; + item.style.display = "flex"; + item.style.alignItems = "center"; - // Checkbox - const checkbox = document.createElement("ui-checkbox"); - // @ts-ignore - checkbox.value = false; - checkbox.setAttribute("data-name", msg.name); - checkbox.style.marginRight = "8px"; + // 复选框 + const checkbox = document.createElement("ui-checkbox"); + // @ts-ignore + checkbox.value = false; + checkbox.setAttribute("data-name", msg.name); + checkbox.style.marginRight = "8px"; - // Content - const content = document.createElement("div"); - content.style.flex = "1"; - content.style.fontSize = "11px"; + // 内容区域 + const content = document.createElement("div"); + content.style.flex = "1"; + content.style.fontSize = "11px"; - let statusColor = "#888"; // Untested - if (msg.status === "可用") statusColor = "#4CAF50"; // Green - else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // Red + let statusColor = "#888"; // 未测试 + if (msg.status === "可用") + statusColor = "#4CAF50"; // 绿色 + else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // 红色 - content.innerHTML = ` + content.innerHTML = `
${msg.name} ${msg.status || "未测试"}
-
${msg.description || "No desc"}
-
Params: ${msg.params || "None"}
+
${msg.description || "无描述"}
+
参数: ${msg.params || "无"}
`; - // Action Button - const btnRun = document.createElement("ui-button"); - btnRun.innerText = "Run"; - btnRun.className = "tiny"; - btnRun.style.height = "20px"; - btnRun.style.lineHeight = "20px"; - btnRun.addEventListener("confirm", () => { - this.runTest(msg.name); - }); + // 执行按钮 + const btnRun = document.createElement("ui-button"); + btnRun.innerText = "执行"; + btnRun.className = "tiny"; + btnRun.style.height = "20px"; + btnRun.style.lineHeight = "20px"; + btnRun.addEventListener("confirm", () => { + this.runTest(msg.name); + }); - item.appendChild(checkbox); - item.appendChild(content); - item.appendChild(btnRun); - this.ipcList!.appendChild(item); - }); - } + item.appendChild(checkbox); + item.appendChild(content); + item.appendChild(btnRun); + this.ipcList!.appendChild(item); + }); + } - private async testSelected() { - const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); - const toTest: string[] = []; - checkboxes.forEach((cb: any) => { - // In Cocos 2.x, ui-checkbox value is boolean - if (cb.checked || cb.value === true) { - toTest.push(cb.getAttribute("data-name")); - } - }); + /** + * 测试所有选中的 IPC 消息 + */ + private async testSelected() { + const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); + const toTest: string[] = []; + checkboxes.forEach((cb: any) => { + // 在 Cocos 2.x 中, ui-checkbox 的值是布尔型 + if (cb.checked || cb.value === true) { + toTest.push(cb.getAttribute("data-name")); + } + }); - if (toTest.length === 0) { - this.log("No messages selected."); - return; - } + if (toTest.length === 0) { + this.log("未选择任何消息。"); + return; + } - this.log(`Starting batch test for ${toTest.length} messages...`); - for (const name of toTest) { - await this.runTest(name); - } - this.log("Batch test completed."); - } + this.log(`开始批量测试 ${toTest.length} 条消息...`); + for (const name of toTest) { + await this.runTest(name); + } + this.log("批量测试完成。"); + } - private runTest(name: string): Promise { - return new Promise((resolve) => { - let params = null; - if (this.paramInput && this.paramInput.value.trim()) { - try { - params = JSON.parse(this.paramInput.value.trim()); - } catch (e) { - this.log(`[Error] Invalid JSON Params: ${e}`); - resolve(); - return; - } - } + /** + * 运行单个测试请求 + * @param name 消息名称 + */ + private runTest(name: string): Promise { + return new Promise((resolve) => { + let params = null; + if (this.paramInput && this.paramInput.value.trim()) { + try { + params = JSON.parse(this.paramInput.value.trim()); + } catch (e) { + this.log(`[错误] 无效的 JSON 参数: ${e}`); + resolve(); + return; + } + } - this.log(`Testing: ${name} with params: ${JSON.stringify(params)}...`); - // @ts-ignore - Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name, params }, (err: any, result: any) => { - if (err) { - this.log(`[${name}] Failed: ${err}`); - } else { - this.log(`[${name}] Success: ${JSON.stringify(result)}`); - } - resolve(); - }); - }); - } + this.log(`正在测试: ${name},参数: ${JSON.stringify(params)}...`); + // @ts-ignore + Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name, params }, (err: any, result: any) => { + if (err) { + this.log(`[${name}] 失败: ${err}`); + } else { + this.log(`[${name}] 成功: ${JSON.stringify(result)}`); + } + resolve(); + }); + }); + } - private toggleAll(checked: boolean) { - const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); - checkboxes.forEach((cb: any) => { - cb.value = checked; - }); - } + /** + * 全选/取消全选 + * @param checked 是否选中 + */ + private toggleAll(checked: boolean) { + const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); + checkboxes.forEach((cb: any) => { + cb.value = checked; + }); + } - private log(msg: string) { - if (this.logArea) { - // @ts-ignore - const time = new Date().toLocaleTimeString(); - this.logArea.value += `[${time}] ${msg}\n`; - this.logArea.scrollTop = this.logArea.scrollHeight; - } - } + /** + * 输出日志到界面 + * @param msg 日志消息 + */ + private log(msg: string) { + if (this.logArea) { + // @ts-ignore + const time = new Date().toLocaleTimeString(); + this.logArea.value += `[${time}] ${msg}\n`; + this.logArea.scrollTop = this.logArea.scrollHeight; + } + } }