feat: Implement VFX management, Undo/Redo, Find in File; docs: Update README and plans
This commit is contained in:
226
DEVELOPMENT.md
226
DEVELOPMENT.md
@@ -413,8 +413,232 @@ manageAsset(args, callback) {
|
|||||||
- 防止路径遍历攻击
|
- 防止路径遍历攻击
|
||||||
- 限制服务访问范围
|
- 限制服务访问范围
|
||||||
|
|
||||||
## 11. 总结
|
## 11. 开发状态
|
||||||
|
|
||||||
|
### 11.1 已完成的任务
|
||||||
|
|
||||||
|
#### 第一阶段
|
||||||
|
- ✅ HTTP 服务接口实现
|
||||||
|
- ✅ 场景节点操作工具
|
||||||
|
- ✅ 资源管理工具
|
||||||
|
- ✅ 组件管理工具
|
||||||
|
- ✅ 脚本管理工具(默认创建 TypeScript 脚本)
|
||||||
|
- ✅ 批处理执行工具
|
||||||
|
- ✅ 资产管理工具
|
||||||
|
- ✅ 实时日志系统
|
||||||
|
- ✅ 自动启动功能
|
||||||
|
- ✅ 面板界面实现
|
||||||
|
|
||||||
|
#### 第二阶段
|
||||||
|
- ✅ 场景管理工具(scene_management)
|
||||||
|
- 创建场景
|
||||||
|
- 删除场景
|
||||||
|
- 复制场景
|
||||||
|
- 获取场景信息
|
||||||
|
- ✅ 预制体管理工具(prefab_management)
|
||||||
|
- 创建预制体
|
||||||
|
- 更新预制体
|
||||||
|
- 实例化预制体
|
||||||
|
- 获取预制体信息
|
||||||
|
- ✅ 面板布局优化
|
||||||
|
- 响应式设计
|
||||||
|
- 滚动条支持
|
||||||
|
- 小窗口适配
|
||||||
|
- ✅ 移除旧工具
|
||||||
|
- 删除了 create_scene 工具(功能整合到 scene_management)
|
||||||
|
- 删除了 create_prefab 工具(功能整合到 prefab_management)
|
||||||
|
- ✅ README.md 文档更新
|
||||||
|
- ✅ 代码提交到本地仓库
|
||||||
|
|
||||||
|
#### 第三阶段
|
||||||
|
- ✅ 编辑器管理工具(manage_editor)
|
||||||
|
- 获取选中对象
|
||||||
|
- 设置选中状态
|
||||||
|
- 刷新编辑器
|
||||||
|
- ✅ 游戏对象查找工具(find_gameobjects)
|
||||||
|
- 根据名称、标签、组件、激活状态查找节点
|
||||||
|
- 支持递归和非递归查找
|
||||||
|
- ✅ 材质管理工具(manage_material)
|
||||||
|
- 创建、删除、获取材质信息
|
||||||
|
- ✅ 纹理管理工具(manage_texture)
|
||||||
|
- 创建、删除、获取纹理信息
|
||||||
|
- ✅ 菜单项执行工具(execute_menu_item)
|
||||||
|
- 执行 Cocos Creator 编辑器菜单项
|
||||||
|
- ✅ 代码编辑增强工具(apply_text_edits)
|
||||||
|
- 支持插入、删除、替换文本操作
|
||||||
|
- ✅ 控制台读取工具(read_console)
|
||||||
|
- 读取编辑器控制台输出
|
||||||
|
- 支持按类型过滤和限制输出数量
|
||||||
|
- ✅ 脚本验证工具(validate_script)
|
||||||
|
- 验证脚本语法正确性
|
||||||
|
- ✅ 面板工具说明功能
|
||||||
|
- 添加工具说明框
|
||||||
|
- 显示详细的工具描述和参数说明
|
||||||
|
|
||||||
|
### 11.2 未完成的任务
|
||||||
|
|
||||||
|
- ❌ 代码推送到远程仓库(认证错误)
|
||||||
|
- ❌ 测试用例编写
|
||||||
|
- ❌ 性能优化
|
||||||
|
- ❌ 错误处理增强
|
||||||
|
- ❌ 安全配置
|
||||||
|
|
||||||
|
### 11.3 后续需要完成的任务
|
||||||
|
|
||||||
|
#### 高优先级
|
||||||
|
1. **代码推送**:解决远程仓库认证问题,完成代码推送
|
||||||
|
2. **测试用例**:为核心工具编写测试用例
|
||||||
|
3. **安全配置**:添加 IP 白名单和认证机制
|
||||||
|
|
||||||
|
#### 中优先级
|
||||||
|
1. **性能优化**:优化 HTTP 服务响应速度,改进批处理执行效率
|
||||||
|
2. **错误处理**:增强错误处理和恢复机制,提高插件稳定性
|
||||||
|
3. **文档完善**:添加更详细的 API 文档和使用示例,包括新工具的详细说明
|
||||||
|
|
||||||
|
#### 低优先级
|
||||||
|
1. **工具扩展**:添加更多高级工具,如动画管理、物理系统管理等
|
||||||
|
2. **界面美化**:进一步优化面板界面,提升用户体验
|
||||||
|
3. **国际化**:支持多语言,方便国际用户使用
|
||||||
|
4. **插件发布**:准备插件发布到 Cocos 插件商店
|
||||||
|
5. **版本兼容**:适配更多 Cocos Creator 版本
|
||||||
|
|
||||||
|
### 11.4 任务优先级表
|
||||||
|
|
||||||
|
| 任务 | 优先级 | 状态 | 描述 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| 代码推送 | 高 | 未完成 | 解决远程仓库认证问题 |
|
||||||
|
| 测试用例 | 高 | 未完成 | 为核心工具编写测试用例 |
|
||||||
|
| 安全配置 | 高 | 未完成 | 添加 IP 白名单和认证机制 |
|
||||||
|
| 性能优化 | 中 | 未完成 | 优化 HTTP 服务响应速度,改进批处理执行效率 |
|
||||||
|
| 错误处理 | 中 | 未完成 | 增强错误处理和恢复机制,提高插件稳定性 |
|
||||||
|
| 文档完善 | 中 | 未完成 | 添加更详细的 API 文档和使用示例,包括新工具的详细说明 |
|
||||||
|
| 工具扩展 | 低 | 未完成 | 添加更多高级工具,如动画管理、物理系统管理等 |
|
||||||
|
| 界面美化 | 低 | 未完成 | 进一步优化面板界面,提升用户体验 |
|
||||||
|
| 国际化 | 低 | 未完成 | 支持多语言,方便国际用户使用 |
|
||||||
|
| 插件发布 | 低 | 未完成 | 准备插件发布到 Cocos 插件商店 |
|
||||||
|
| 版本兼容 | 低 | 未完成 | 适配更多 Cocos Creator 版本 |
|
||||||
|
| 编辑器管理工具 | 高 | 已完成 | 实现 manage_editor 工具,支持编辑器状态管理 |
|
||||||
|
| 游戏对象查找工具 | 高 | 已完成 | 实现 find_gameobjects 工具,支持根据条件查找节点 |
|
||||||
|
| 材质和纹理管理工具 | 高 | 已完成 | 实现 manage_material 和 manage_texture 工具 |
|
||||||
|
| 菜单项执行工具 | 高 | 已完成 | 实现 execute_menu_item 工具,支持执行编辑器菜单项 |
|
||||||
|
| 代码编辑增强工具 | 中 | 已完成 | 实现 apply_text_edits 工具,支持文本编辑操作 |
|
||||||
|
| 控制台读取工具 | 中 | 已完成 | 实现 read_console 工具,支持读取控制台输出 |
|
||||||
|
| 脚本验证工具 | 中 | 已完成 | 实现 validate_script 工具,支持脚本语法验证 |
|
||||||
|
| 面板工具说明功能 | 低 | 已完成 | 添加工具说明框,显示详细的工具描述和参数说明 |
|
||||||
|
|
||||||
|
## 12. Unity-MCP 对比分析
|
||||||
|
|
||||||
|
### 12.1 Unity-MCP 功能特性
|
||||||
|
|
||||||
|
Unity-MCP 提供了以下核心功能:
|
||||||
|
|
||||||
|
- **资产管理**:管理各种 Unity 资源
|
||||||
|
- **编辑器管理**:控制 Unity 编辑器功能
|
||||||
|
- **游戏对象管理**:创建、修改、查找游戏对象
|
||||||
|
- **组件管理**:添加、移除、修改组件
|
||||||
|
- **材质管理**:创建和修改材质
|
||||||
|
- **预制体管理**:管理预制体资源
|
||||||
|
- **场景管理**:创建、保存、加载场景
|
||||||
|
- **脚本管理**:创建、修改脚本
|
||||||
|
- **ScriptableObject 管理**:管理配置文件
|
||||||
|
- **着色器管理**:管理着色器资源
|
||||||
|
- **VFX 管理**:管理视觉效果
|
||||||
|
- **纹理管理**:管理纹理资源
|
||||||
|
- **批处理执行**:批量执行多个操作
|
||||||
|
- **游戏对象查找**:根据条件查找游戏对象
|
||||||
|
- **文件内容查找**:在文件中查找内容
|
||||||
|
- **控制台读取**:读取 Unity 控制台输出
|
||||||
|
- **Unity 刷新**:刷新 Unity 编辑器
|
||||||
|
- **测试运行**:运行测试用例
|
||||||
|
- **获取测试任务**:获取测试任务信息
|
||||||
|
- **菜单项执行**:执行 Unity 菜单项
|
||||||
|
- **文本编辑应用**:应用文本编辑操作
|
||||||
|
- **脚本编辑应用**:应用脚本编辑操作
|
||||||
|
- **脚本验证**:验证脚本语法
|
||||||
|
- **创建脚本**:创建新脚本
|
||||||
|
- **删除脚本**:删除脚本文件
|
||||||
|
- **获取 SHA**:获取版本控制 SHA 值
|
||||||
|
|
||||||
|
### 12.2 Cocos-MCP 功能特性
|
||||||
|
|
||||||
|
当前 Cocos-MCP 已实现的功能:
|
||||||
|
|
||||||
|
- **场景节点操作**:获取选中节点、设置节点名称、获取场景层级、更新节点变换、创建节点
|
||||||
|
- **组件管理**:添加、移除、获取组件
|
||||||
|
- **资源管理**:创建、删除、移动资源
|
||||||
|
- **脚本管理**:创建、删除、读取、写入脚本(默认创建 TypeScript 脚本)
|
||||||
|
- **批处理执行**:批量执行多个操作
|
||||||
|
- **资产管理**:管理各种资源文件
|
||||||
|
- **场景管理**:创建、删除、复制、获取场景信息
|
||||||
|
- **预制体管理**:创建、更新、实例化、获取预制体信息
|
||||||
|
- **面板界面**:提供主面板和工具测试面板
|
||||||
|
|
||||||
|
### 12.3 功能缺失对比
|
||||||
|
|
||||||
|
| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 可实现性 |
|
||||||
|
|---------|---------------|---------------|--------|
|
||||||
|
| 编辑器管理 | manage_editor | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 游戏对象管理 | find_gameobjects | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 材质管理 | manage_material | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 着色器管理 | manage_shader | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 纹理管理 | manage_texture | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 代码编辑增强 | apply_text_edits, script_apply_edits | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 测试功能 | run_tests, get_test_job | ❌ 缺失 | ⚠️ 部分可实现 |
|
||||||
|
| 控制台读取 | read_console | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 菜单项执行 | execute_menu_item | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| 脚本验证 | validate_script | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
| VFX 管理 | manage_vfx | ❌ 缺失 | ✅ 可实现 |
|
||||||
|
|
||||||
|
### 12.4 功能实现建议
|
||||||
|
|
||||||
|
#### 高优先级功能
|
||||||
|
|
||||||
|
1. **编辑器管理工具** (`manage_editor`)
|
||||||
|
- 功能:控制编辑器状态、执行编辑器操作
|
||||||
|
- 实现方案:使用 `Editor.Ipc` 调用编辑器 API,如 `Editor.Selection`、`Editor.assetdb` 等
|
||||||
|
|
||||||
|
2. **游戏对象查找工具** (`find_gameobjects`)
|
||||||
|
- 功能:根据条件查找场景中的节点
|
||||||
|
- 实现方案:使用场景脚本遍历节点树,根据名称、标签、组件等条件过滤
|
||||||
|
|
||||||
|
3. **材质和纹理管理工具** (`manage_material`, `manage_texture`)
|
||||||
|
- 功能:创建和管理材质、纹理资源
|
||||||
|
- 实现方案:使用 `Editor.assetdb` API 操作资源文件
|
||||||
|
|
||||||
|
4. **菜单项执行工具** (`execute_menu_item`)
|
||||||
|
- 功能:执行 Cocos Creator 菜单项
|
||||||
|
- 实现方案:使用 `Editor.Ipc.sendToMain` 发送菜单命令
|
||||||
|
|
||||||
|
#### 中优先级功能
|
||||||
|
|
||||||
|
1. **代码编辑增强工具** (`apply_text_edits`, `script_apply_edits`)
|
||||||
|
- 功能:应用文本编辑操作到文件
|
||||||
|
- 实现方案:读取文件内容,应用编辑操作,然后写回文件
|
||||||
|
|
||||||
|
2. **控制台读取工具** (`read_console`)
|
||||||
|
- 功能:读取编辑器控制台输出
|
||||||
|
- 实现方案:重定向 `console.log` 等方法,捕获控制台输出
|
||||||
|
|
||||||
|
3. **脚本验证工具** (`validate_script`)
|
||||||
|
- 功能:验证脚本语法正确性
|
||||||
|
- 实现方案:使用 Node.js 的语法解析器或调用外部工具
|
||||||
|
|
||||||
|
#### 低优先级功能
|
||||||
|
|
||||||
|
1. **测试功能** (`run_tests`, `get_test_job`)
|
||||||
|
- 功能:运行测试用例并获取结果
|
||||||
|
- 实现方案:根据 Cocos Creator 的测试框架集成
|
||||||
|
|
||||||
|
2. **VFX 管理工具** (`manage_vfx`)
|
||||||
|
- 功能:管理视觉效果资源
|
||||||
|
- 实现方案:使用 `Editor.assetdb` API 操作 VFX 资源
|
||||||
|
|
||||||
|
## 13. 总结
|
||||||
|
|
||||||
MCP Bridge 插件通过 HTTP 服务和 MCP 协议,为外部 AI 工具提供了与 Cocos Creator 编辑器交互的能力。插件支持场景操作、资源管理、组件管理、脚本管理等多种功能,为 Cocos Creator 项目的开发和自动化提供了有力的支持。
|
MCP Bridge 插件通过 HTTP 服务和 MCP 协议,为外部 AI 工具提供了与 Cocos Creator 编辑器交互的能力。插件支持场景操作、资源管理、组件管理、脚本管理等多种功能,为 Cocos Creator 项目的开发和自动化提供了有力的支持。
|
||||||
|
|
||||||
通过本文档的开发流程,我们构建了一个功能完整、稳定可靠的 MCP Bridge 插件,为 Cocos Creator 生态系统增添了新的工具和能力。
|
通过本文档的开发流程,我们构建了一个功能完整、稳定可靠的 MCP Bridge 插件,为 Cocos Creator 生态系统增添了新的工具和能力。
|
||||||
|
|
||||||
|
目前插件已经完成了核心功能的实现,包括 15 个 MCP 工具,支持从场景操作到资源管理的各种功能。后续将继续完善测试、优化性能,并添加更多高级功能,为开发者提供更强大的工具支持。
|
||||||
|
|
||||||
|
通过与 Unity-MCP 的对比分析,我们识别出了多个可实现的功能,这些功能将进一步增强 Cocos-MCP 的能力,使其与 Unity-MCP 保持功能对等,为 Cocos Creator 开发者提供同样强大的 AI 辅助开发体验。
|
||||||
|
|||||||
630
DEVELOPMENT_PLAN.md
Normal file
630
DEVELOPMENT_PLAN.md
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
# Cocos-MCP 开发计划文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 项目背景
|
||||||
|
|
||||||
|
Cocos-MCP (Model Context Protocol) 插件是一个为 Cocos Creator 编辑器提供外部 AI 工具交互能力的桥梁。通过 HTTP 服务和 MCP 协议,插件允许外部 AI 工具(如 Cursor、VS Code 等)直接与 Cocos Creator 编辑器进行交互,实现场景操作、资源管理、脚本编辑等功能。
|
||||||
|
|
||||||
|
### 1.2 项目目标
|
||||||
|
|
||||||
|
- 提供与 Unity-MCP 对等的功能集,使 Cocos Creator 开发者获得同样强大的 AI 辅助开发体验
|
||||||
|
- 实现编辑器管理、游戏对象查找、材质/纹理管理等高级功能
|
||||||
|
- 优化现有功能,提高插件稳定性和性能
|
||||||
|
- 建立完善的测试和部署流程
|
||||||
|
|
||||||
|
### 1.3 技术栈
|
||||||
|
|
||||||
|
- **开发语言**:JavaScript/TypeScript
|
||||||
|
- **运行环境**:Node.js (Cocos Creator 内置)
|
||||||
|
- **通信协议**:HTTP + JSON (MCP 协议)
|
||||||
|
- **编辑器 API**:Cocos Creator 2.4.x Editor API
|
||||||
|
- **界面技术**:HTML/CSS + Cocos Creator 面板 API
|
||||||
|
|
||||||
|
## 2. 功能分析
|
||||||
|
|
||||||
|
### 2.1 Unity-MCP 功能参考
|
||||||
|
|
||||||
|
Unity-MCP 提供了以下核心功能:
|
||||||
|
|
||||||
|
| 功能类别 | 具体功能 | 描述 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| 编辑器管理 | manage_editor | 控制 Unity 编辑器状态和操作 |
|
||||||
|
| 游戏对象管理 | find_gameobjects | 根据条件查找游戏对象 |
|
||||||
|
| 材质管理 | manage_material | 创建和管理材质资源 |
|
||||||
|
| 着色器管理 | manage_shader | 管理着色器资源 |
|
||||||
|
| 纹理管理 | manage_texture | 管理纹理资源 |
|
||||||
|
| 代码编辑增强 | apply_text_edits | 应用文本编辑操作到文件 |
|
||||||
|
| 测试功能 | run_tests | 运行测试用例 |
|
||||||
|
| 控制台读取 | read_console | 读取编辑器控制台输出 |
|
||||||
|
| 菜单项执行 | execute_menu_item | 执行编辑器菜单项 |
|
||||||
|
| 脚本验证 | validate_script | 验证脚本语法 |
|
||||||
|
| VFX 管理 | manage_vfx | 管理视觉效果资源 |
|
||||||
|
|
||||||
|
### 2.2 Cocos-MCP 现状分析
|
||||||
|
|
||||||
|
#### 已实现功能
|
||||||
|
|
||||||
|
- **场景节点操作**:获取选中节点、设置节点名称、获取场景层级、更新节点变换、创建节点
|
||||||
|
- **组件管理**:添加、移除、获取组件
|
||||||
|
- **资源管理**:创建、删除、移动资源
|
||||||
|
- **脚本管理**:创建、删除、读取、写入脚本(默认创建 TypeScript 脚本)
|
||||||
|
- **批处理执行**:批量执行多个操作
|
||||||
|
- **资产管理**:管理各种资源文件
|
||||||
|
- **场景管理**:创建、删除、复制、获取场景信息
|
||||||
|
- **预制体管理**:创建、更新、实例化、获取预制体信息
|
||||||
|
- **面板界面**:提供主面板和工具测试面板
|
||||||
|
- **编辑器管理**:控制编辑器状态、执行编辑器操作
|
||||||
|
- **游戏对象查找**:根据条件查找场景中的节点
|
||||||
|
- **材质管理**:创建和管理材质资源
|
||||||
|
- **着色器管理**:管理着色器资源
|
||||||
|
- **纹理管理**:管理纹理资源
|
||||||
|
- **代码编辑增强**:应用文本编辑操作到文件
|
||||||
|
- **测试功能**:运行自动化测试用例 (`run_tests.js`)
|
||||||
|
- **控制台读取**:读取编辑器控制台输出
|
||||||
|
- **菜单项执行**:执行编辑器菜单项
|
||||||
|
- **脚本验证**:验证脚本语法
|
||||||
|
|
||||||
|
#### 缺失功能
|
||||||
|
|
||||||
|
- **VFX 管理**:管理视觉效果资源(暂未排期)
|
||||||
|
|
||||||
|
### 2.3 功能优先级排序
|
||||||
|
|
||||||
|
所有高/中优先级功能均已完成。
|
||||||
|
|
||||||
|
### 2.4 Unity-MCP 差异分析 (Gap Analysis)
|
||||||
|
|
||||||
|
通过对比 [Unity-MCP](https://github.com/CoplayDev/unity-mcp),发现以下缺失或可增强的功能:
|
||||||
|
|
||||||
|
1. **全局搜索 (`find_in_file`)**: ✅ 已实现。支持在整个项目中搜索文本内容。
|
||||||
|
2. **ScriptableObject 管理 (`manage_scriptable_object`)**: 虽然 AssetDB 可以创建资源,但缺乏专门针对 ScriptableObject (cc.Asset) 的简便创建工具。
|
||||||
|
3. **Undo/Redo 支持**: ✅ 已实现。通过 `manage_undo` 提供了 `undo/redo` 及事务组支持。
|
||||||
|
4. **VFX 管理 (`manage_vfx`)**: ✅ 已实现。支持粒子系统的创建、修改和信息获取。
|
||||||
|
5. **Git 集成 (`get_sha`)**: 获取当前版本库信息。
|
||||||
|
|
||||||
|
`find_in_file`、`Undo/Redo` 和 `manage_vfx` 已在最新迭代中完成。
|
||||||
|
|
||||||
|
## 3. 开发路线图
|
||||||
|
|
||||||
|
### 3.1 第三阶段开发计划(已完成)
|
||||||
|
|
||||||
|
| 任务 | 状态 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 |
|
||||||
|
| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 |
|
||||||
|
| 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 |
|
||||||
|
| 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 |
|
||||||
|
| 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 |
|
||||||
|
| 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 |
|
||||||
|
| 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 |
|
||||||
|
|
||||||
|
### 3.2 第四阶段开发计划(已完成)
|
||||||
|
|
||||||
|
| 任务 | 状态 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 |
|
||||||
|
| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 |
|
||||||
|
|
||||||
|
### 3.3 差异填补阶段(Gap Filling)- 已完成
|
||||||
|
|
||||||
|
| 任务 | 状态 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| 全局文件搜索 | ✅ 完成 | 实现 find_in_file 工具 |
|
||||||
|
| 撤销/重做支持 | ✅ 完成 | 实现 manage_undo 工具,并重构核心操作支持撤销 |
|
||||||
|
| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 |
|
||||||
|
|
||||||
|
### 3.3 第五阶段开发计划(远期)
|
||||||
|
|
||||||
|
| 任务 | 优先级 | 预计时间 | 描述 |
|
||||||
|
|------|--------|----------|------|
|
||||||
|
| 工具扩展 | 低 | 3 天 | 添加更多高级工具和功能 |
|
||||||
|
| 界面美化 | 低 | 2 天 | 进一步优化面板界面,提升用户体验 |
|
||||||
|
| 国际化支持 | 低 | 2 天 | 添加多语言支持 |
|
||||||
|
| 文档完善 | 中 | 2 天 | 完善 API 文档和使用示例 |
|
||||||
|
| 插件发布 | 高 | 1 天 | 准备插件发布,提交到 Cocos 插件商店 |
|
||||||
|
|
||||||
|
## 4. 技术架构
|
||||||
|
|
||||||
|
### 4.1 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────┐ HTTP ┌────────────────────┐ IPC ┌────────────────────┐
|
||||||
|
│ 外部 AI 工具 │ ──────────> │ main.js (HTTP服务) │ ─────────> │ scene-script.js │
|
||||||
|
│ (Cursor/VS Code) │ <──────── │ (MCP 协议处理) │ <──────── │ (场景操作执行) │
|
||||||
|
└────────────────────┘ JSON └────────────────────┘ JSON └────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 核心模块
|
||||||
|
|
||||||
|
1. **HTTP 服务模块**:处理外部请求,解析 MCP 协议,返回操作结果
|
||||||
|
2. **MCP 工具模块**:实现各种操作工具,包括新增的编辑器管理、游戏对象查找等功能
|
||||||
|
3. **场景操作模块**:执行场景相关操作,如节点查找、组件管理等
|
||||||
|
4. **资源管理模块**:处理脚本、材质、纹理等资源文件的创建和管理
|
||||||
|
5. **面板界面模块**:提供用户交互界面,包括主面板和工具测试面板
|
||||||
|
|
||||||
|
### 4.3 技术实现要点
|
||||||
|
|
||||||
|
- **编辑器 API 调用**:使用 `Editor.Ipc` 与编辑器核心进行通信
|
||||||
|
- **资源操作**:使用 `Editor.assetdb` API 进行资源文件的创建、读取、更新和删除
|
||||||
|
- **场景操作**:通过场景脚本执行节点和组件操作
|
||||||
|
- **异步处理**:使用回调函数处理异步操作,避免阻塞主线程
|
||||||
|
- **错误处理**:完善的错误捕获和处理机制,提高插件稳定性
|
||||||
|
- **性能优化**:使用批处理执行减少 HTTP 请求次数,优化资源操作效率
|
||||||
|
|
||||||
|
## 5. 功能实现方案
|
||||||
|
|
||||||
|
### 5.1 编辑器管理工具 (`manage_editor`)
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
提供对 Cocos Creator 编辑器状态的控制和操作执行能力。
|
||||||
|
|
||||||
|
#### 实现方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 main.js 中添加
|
||||||
|
manageEditor(args, callback) {
|
||||||
|
const { action, target, properties } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "get_selection":
|
||||||
|
// 获取当前选中的资源或节点
|
||||||
|
const nodeSelection = Editor.Selection.curSelection('node');
|
||||||
|
const assetSelection = Editor.Selection.curSelection('asset');
|
||||||
|
callback(null, {
|
||||||
|
nodes: nodeSelection,
|
||||||
|
assets: assetSelection
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "set_selection":
|
||||||
|
// 设置选中状态
|
||||||
|
if (target === 'node' && properties.nodes) {
|
||||||
|
Editor.Selection.select('node', properties.nodes);
|
||||||
|
} else if (target === 'asset' && properties.assets) {
|
||||||
|
Editor.Selection.select('asset', properties.assets);
|
||||||
|
}
|
||||||
|
callback(null, "Selection updated");
|
||||||
|
break;
|
||||||
|
case "refresh_editor":
|
||||||
|
// 刷新编辑器
|
||||||
|
Editor.assetdb.refresh();
|
||||||
|
callback(null, "Editor refreshed");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
callback("Unknown action");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 游戏对象查找工具 (`find_gameobjects`)
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
根据条件查找场景中的节点对象。
|
||||||
|
|
||||||
|
#### 实现方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 scene-script.js 中添加
|
||||||
|
findGameObjects(params, callback) {
|
||||||
|
const { conditions, recursive } = params;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
// 遍历场景根节点
|
||||||
|
cc.director.getScene().children.forEach(child => {
|
||||||
|
searchNode(child, conditions, recursive, result);
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchNode(node, conditions, recursive, result) {
|
||||||
|
// 检查节点是否满足条件
|
||||||
|
let match = true;
|
||||||
|
|
||||||
|
if (conditions.name && !node.name.includes(conditions.name)) {
|
||||||
|
match = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.tag && node.tag !== conditions.tag) {
|
||||||
|
match = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.component && !node.getComponent(conditions.component)) {
|
||||||
|
match = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
result.push({
|
||||||
|
id: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
tag: node.tag,
|
||||||
|
position: node.position,
|
||||||
|
rotation: node.rotation,
|
||||||
|
scale: node.scale
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归搜索子节点
|
||||||
|
if (recursive) {
|
||||||
|
node.children.forEach(child => {
|
||||||
|
searchNode(child, conditions, recursive, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 材质和纹理管理工具 (`manage_material`, `manage_texture`)
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
管理材质和纹理资源,支持创建、修改、删除等操作。
|
||||||
|
|
||||||
|
#### 实现方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 main.js 中添加
|
||||||
|
manageMaterial(args, callback) {
|
||||||
|
const { action, path, properties } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
// 创建材质资源
|
||||||
|
const materialContent = JSON.stringify({
|
||||||
|
__type__: "cc.Material",
|
||||||
|
_name: "",
|
||||||
|
_objFlags: 0,
|
||||||
|
_native: "",
|
||||||
|
effects: [{
|
||||||
|
technique: 0,
|
||||||
|
defines: {},
|
||||||
|
uniforms: properties.uniforms || {}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
const fs = require('fs');
|
||||||
|
const pathModule = require('path');
|
||||||
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||||
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.assetdb.create(path, materialContent, (err) => {
|
||||||
|
callback(err, err ? null : `Material created at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
// 其他操作...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manageTexture(args, callback) {
|
||||||
|
const { action, path, properties } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
// 创建纹理资源(简化版,实际需要处理纹理文件)
|
||||||
|
const textureContent = JSON.stringify({
|
||||||
|
__type__: "cc.Texture2D",
|
||||||
|
_name: "",
|
||||||
|
_objFlags: 0,
|
||||||
|
_native: properties.native || "",
|
||||||
|
width: properties.width || 128,
|
||||||
|
height: properties.height || 128
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
const fs = require('fs');
|
||||||
|
const pathModule = require('path');
|
||||||
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||||
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.assetdb.create(path, textureContent, (err) => {
|
||||||
|
callback(err, err ? null : `Texture created at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
// 其他操作...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 菜单项执行工具 (`execute_menu_item`)
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
执行 Cocos Creator 编辑器的菜单项命令。
|
||||||
|
|
||||||
|
#### 实现方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 main.js 中添加
|
||||||
|
executeMenuItem(args, callback) {
|
||||||
|
const { menuPath } = args;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 执行菜单项
|
||||||
|
Editor.Ipc.sendToMain('menu:click', menuPath);
|
||||||
|
callback(null, `Menu item executed: ${menuPath}`);
|
||||||
|
} catch (err) {
|
||||||
|
callback(`Failed to execute menu item: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 代码编辑增强工具 (`apply_text_edits`)
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
应用文本编辑操作到文件,支持插入、删除、替换等操作。
|
||||||
|
|
||||||
|
#### 实现方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 main.js 中添加
|
||||||
|
applyTextEdits(args, callback) {
|
||||||
|
const { filePath, edits } = args;
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
Editor.assetdb.queryInfoByUrl(filePath, (err, info) => {
|
||||||
|
if (err) {
|
||||||
|
callback(`Failed to get file info: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.assetdb.loadMeta(info.uuid, (err, content) => {
|
||||||
|
if (err) {
|
||||||
|
callback(`Failed to load file: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用编辑操作
|
||||||
|
let updatedContent = content;
|
||||||
|
edits.forEach(edit => {
|
||||||
|
switch (edit.type) {
|
||||||
|
case "insert":
|
||||||
|
updatedContent = updatedContent.slice(0, edit.position) + edit.text + updatedContent.slice(edit.position);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
updatedContent = updatedContent.slice(0, edit.start) + updatedContent.slice(edit.end);
|
||||||
|
break;
|
||||||
|
case "replace":
|
||||||
|
updatedContent = updatedContent.slice(0, edit.start) + edit.text + updatedContent.slice(edit.end);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 写回文件
|
||||||
|
Editor.assetdb.save(info.uuid, updatedContent, (err) => {
|
||||||
|
callback(err, err ? null : `Text edits applied to ${filePath}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 控制台读取工具 (`read_console`)
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
读取编辑器控制台的输出信息。
|
||||||
|
|
||||||
|
#### 实现方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 main.js 中添加
|
||||||
|
// 首先在模块顶部添加控制台输出捕获
|
||||||
|
let consoleOutput = [];
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
|
||||||
|
console.log = function(...args) {
|
||||||
|
consoleOutput.push({ type: 'log', message: args.join(' ') });
|
||||||
|
originalLog.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = function(...args) {
|
||||||
|
consoleOutput.push({ type: 'error', message: args.join(' ') });
|
||||||
|
originalError.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn = function(...args) {
|
||||||
|
consoleOutput.push({ type: 'warn', message: args.join(' ') });
|
||||||
|
originalWarn.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 然后添加 read_console 工具
|
||||||
|
readConsole(args, callback) {
|
||||||
|
const { limit, type } = args;
|
||||||
|
let filteredOutput = consoleOutput;
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
filteredOutput = filteredOutput.filter(item => item.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
filteredOutput = filteredOutput.slice(-limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, filteredOutput);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.7 脚本验证工具 (`validate_script`)
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
验证脚本文件的语法正确性。
|
||||||
|
|
||||||
|
#### 实现方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 main.js 中添加
|
||||||
|
validateScript(args, callback) {
|
||||||
|
const { filePath } = args;
|
||||||
|
|
||||||
|
// 读取脚本内容
|
||||||
|
Editor.assetdb.queryInfoByUrl(filePath, (err, info) => {
|
||||||
|
if (err) {
|
||||||
|
callback(`Failed to get file info: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.assetdb.loadMeta(info.uuid, (err, content) => {
|
||||||
|
if (err) {
|
||||||
|
callback(`Failed to load file: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 对于 JavaScript 脚本,使用 eval 进行简单验证
|
||||||
|
if (filePath.endsWith('.js')) {
|
||||||
|
// 包装在函数中以避免变量污染
|
||||||
|
const wrapper = `(function() { ${content} })`;
|
||||||
|
eval(wrapper);
|
||||||
|
}
|
||||||
|
// 对于 TypeScript 脚本,这里可以添加更复杂的验证逻辑
|
||||||
|
|
||||||
|
callback(null, { valid: true, message: 'Script syntax is valid' });
|
||||||
|
} catch (err) {
|
||||||
|
callback(null, { valid: false, message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.8 常用 IPC 消息参考 (Cocos Creator 2.4.x)
|
||||||
|
|
||||||
|
基于社区资料整理,以下 IPC 消息可用于扩展功能:
|
||||||
|
|
||||||
|
#### 场景操作 (`scene:`)
|
||||||
|
- **创建/实例化**:
|
||||||
|
- `scene:create-node-by-classid` (参数: name, parentUuid)
|
||||||
|
- `scene:create-nodes-by-uuids` (实例化预制体, 参数: [prefabUuid], parentUuid)
|
||||||
|
- **修改**:
|
||||||
|
- `scene:set-property` (参数: {id, path, type, value})
|
||||||
|
- `scene:copy-nodes` / `scene:paste-nodes`
|
||||||
|
- **安全机制**:
|
||||||
|
- `scene:undo` / `scene:redo` (建议集成到 manage_editor)
|
||||||
|
|
||||||
|
#### 资源操作 (`assets:`)
|
||||||
|
- `assets:hint` (高亮资源)
|
||||||
|
- `assets:open-text-file` (打开外部编辑器)
|
||||||
|
|
||||||
|
## 6. 测试策略
|
||||||
|
|
||||||
|
### 6.1 测试目标
|
||||||
|
|
||||||
|
- 验证所有新增功能的正确性和稳定性
|
||||||
|
- 确保现有功能不受影响
|
||||||
|
- 测试插件在不同场景下的性能表现
|
||||||
|
- 验证错误处理机制的有效性
|
||||||
|
|
||||||
|
### 6.2 测试方法
|
||||||
|
|
||||||
|
#### 单元测试
|
||||||
|
|
||||||
|
- 为每个工具函数编写独立的测试用例
|
||||||
|
- 测试各种输入参数和边界情况
|
||||||
|
- 验证函数返回值的正确性
|
||||||
|
|
||||||
|
#### 集成测试
|
||||||
|
|
||||||
|
- 测试工具之间的协作能力
|
||||||
|
- 验证批处理执行的正确性
|
||||||
|
- 测试插件与编辑器的集成稳定性
|
||||||
|
|
||||||
|
#### 性能测试
|
||||||
|
|
||||||
|
- 测试工具执行速度
|
||||||
|
- 验证批处理执行的性能优势
|
||||||
|
- 测试插件在处理大量操作时的表现
|
||||||
|
|
||||||
|
#### 回归测试
|
||||||
|
|
||||||
|
- 确保新增功能不破坏现有功能
|
||||||
|
- 验证修复的 bug 不会再次出现
|
||||||
|
- 测试插件在不同版本 Cocos Creator 中的兼容性
|
||||||
|
|
||||||
|
### 6.3 测试工具
|
||||||
|
|
||||||
|
- **手动测试**:通过面板界面测试工具功能
|
||||||
|
- **脚本测试**:编写测试脚本自动执行测试用例
|
||||||
|
- **性能分析**:使用浏览器开发者工具分析性能瓶颈
|
||||||
|
|
||||||
|
## 7. 部署与维护
|
||||||
|
|
||||||
|
### 7.1 部署方案
|
||||||
|
|
||||||
|
#### 本地部署
|
||||||
|
|
||||||
|
1. 将插件复制到 Cocos Creator 项目的 `packages` 目录
|
||||||
|
2. 重启 Cocos Creator 编辑器
|
||||||
|
3. 在编辑器中打开 MCP Bridge 面板
|
||||||
|
4. 启动 HTTP 服务
|
||||||
|
|
||||||
|
#### 远程部署
|
||||||
|
|
||||||
|
1. 使用 Git 进行版本控制
|
||||||
|
2. 提供插件的 GitHub 仓库地址
|
||||||
|
3. 发布插件到 Cocos 插件商店(未来)
|
||||||
|
|
||||||
|
### 7.2 维护计划
|
||||||
|
|
||||||
|
#### 版本管理
|
||||||
|
|
||||||
|
- 遵循语义化版本规范(MAJOR.MINOR.PATCH)
|
||||||
|
- 定期发布更新版本
|
||||||
|
- 维护详细的版本变更日志
|
||||||
|
|
||||||
|
#### 错误处理
|
||||||
|
|
||||||
|
- 建立错误报告机制
|
||||||
|
- 定期分析错误日志
|
||||||
|
- 及时修复发现的问题
|
||||||
|
|
||||||
|
#### 性能优化
|
||||||
|
|
||||||
|
- 定期分析插件性能
|
||||||
|
- 优化资源操作和网络请求
|
||||||
|
- 提高插件响应速度
|
||||||
|
|
||||||
|
#### 文档维护
|
||||||
|
|
||||||
|
- 保持 README.md 和开发文档的更新
|
||||||
|
- 提供详细的 API 文档
|
||||||
|
- 编写使用教程和示例
|
||||||
|
|
||||||
|
## 8. 风险评估
|
||||||
|
|
||||||
|
### 8.1 潜在风险
|
||||||
|
|
||||||
|
| 风险 | 影响 | 缓解措施 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos Creator 更新,适配新 API |
|
||||||
|
| 性能问题 | 插件响应缓慢 | 优化代码结构,使用批处理执行,避免阻塞操作 |
|
||||||
|
| 安全漏洞 | 未授权访问 | 添加 IP 白名单,实现认证机制,限制服务访问范围 |
|
||||||
|
| 兼容性问题 | 不同版本 Cocos Creator 不兼容 | 测试多个版本,提供版本兼容层 |
|
||||||
|
| 错误处理不完善 | 插件崩溃 | 完善错误捕获和处理机制,提高插件稳定性 |
|
||||||
|
|
||||||
|
### 8.2 应对策略
|
||||||
|
|
||||||
|
- **持续集成**:建立自动化测试流程,及时发现问题
|
||||||
|
- **监控机制**:添加性能监控和错误监控
|
||||||
|
- **用户反馈**:建立用户反馈渠道,收集使用问题
|
||||||
|
- **文档完善**:提供详细的安装和使用文档
|
||||||
|
- **社区支持**:建立社区支持渠道,解答用户问题
|
||||||
|
|
||||||
|
## 9. 结论
|
||||||
|
|
||||||
|
Cocos-MCP 插件的开发计划基于与 Unity-MCP 的功能对比,旨在为 Cocos Creator 开发者提供同样强大的 AI 辅助开发体验。通过分阶段实现编辑器管理、游戏对象查找、材质/纹理管理等高级功能,插件将逐步完善其功能集,成为 Cocos Creator 编辑器的重要扩展工具。
|
||||||
|
|
||||||
|
本开发计划文档为后续的开发工作提供了详细的指导,包括功能实现方案、技术架构设计、测试策略和部署维护计划。通过严格按照计划执行开发工作,我们可以确保插件的质量和稳定性,为 Cocos Creator 生态系统做出贡献。
|
||||||
128
README.md
128
README.md
@@ -17,6 +17,18 @@
|
|||||||
- **资产管理**: 创建、删除、移动、获取资源信息
|
- **资产管理**: 创建、删除、移动、获取资源信息
|
||||||
- **实时日志**: 提供详细的操作日志记录和展示
|
- **实时日志**: 提供详细的操作日志记录和展示
|
||||||
- **自动启动**: 支持编辑器启动时自动开启服务
|
- **自动启动**: 支持编辑器启动时自动开启服务
|
||||||
|
- **编辑器管理**: 获取和设置选中对象,刷新编辑器
|
||||||
|
- **游戏对象查找**: 根据条件查找场景中的节点
|
||||||
|
- **材质管理**: 创建和管理材质资源
|
||||||
|
- **纹理管理**: 创建和管理纹理资源
|
||||||
|
- **菜单项执行**: 执行 Cocos Creator 编辑器菜单项
|
||||||
|
- **代码编辑增强**: 应用文本编辑操作到文件
|
||||||
|
- **控制台读取**: 读取编辑器控制台输出
|
||||||
|
- **脚本验证**: 验证脚本语法正确性
|
||||||
|
- **全局搜索**: 在项目中搜索文本内容
|
||||||
|
- **撤销/重做**: 管理编辑器的撤销栈
|
||||||
|
- **特效管理**: 创建和修改粒子系统
|
||||||
|
- **工具说明**: 测试面板提供详细的工具描述和参数说明
|
||||||
|
|
||||||
## 安装与使用
|
## 安装与使用
|
||||||
|
|
||||||
@@ -172,6 +184,105 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `nodeId`: 节点 ID(用于 `create` 和 `update` 操作)
|
- `nodeId`: 节点 ID(用于 `create` 和 `update` 操作)
|
||||||
- `parentId`: 父节点 ID(用于 `instantiate` 操作)
|
- `parentId`: 父节点 ID(用于 `instantiate` 操作)
|
||||||
|
|
||||||
|
### 14. manage_editor
|
||||||
|
|
||||||
|
- **描述**: 管理编辑器
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型(`get_selection`, `set_selection`, `refresh_editor`)
|
||||||
|
- `target`: 目标类型(`node`, `asset`)(用于 `set_selection` 操作)
|
||||||
|
- `properties`: 操作属性
|
||||||
|
- `nodes`: 节点 UUID 数组(用于 `set_selection` 操作)
|
||||||
|
- `assets`: 资源 UUID 数组(用于 `set_selection` 操作)
|
||||||
|
|
||||||
|
### 15. find_gameobjects
|
||||||
|
|
||||||
|
- **描述**: 查找游戏对象
|
||||||
|
- **参数**:
|
||||||
|
- `conditions`: 查找条件
|
||||||
|
- `name`: 节点名称(包含匹配)
|
||||||
|
- `tag`: 节点标签
|
||||||
|
- `component`: 组件类型
|
||||||
|
- `active`: 激活状态
|
||||||
|
- `recursive`: 是否递归查找(默认:true)
|
||||||
|
|
||||||
|
### 16. manage_material
|
||||||
|
|
||||||
|
- **描述**: 管理材质
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型(`create`, `delete`, `get_info`)
|
||||||
|
- `path`: 材质路径,如 `db://assets/materials/NewMaterial.mat`
|
||||||
|
- `properties`: 材质属性(用于 `create` 操作)
|
||||||
|
- `uniforms`: 材质 uniforms
|
||||||
|
|
||||||
|
### 17. manage_texture
|
||||||
|
|
||||||
|
- **描述**: 管理纹理
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型(`create`, `delete`, `get_info`)
|
||||||
|
- `path`: 纹理路径,如 `db://assets/textures/NewTexture.png`
|
||||||
|
- `properties`: 纹理属性(用于 `create` 操作)
|
||||||
|
- `width`: 宽度
|
||||||
|
- `height`: 高度
|
||||||
|
- `native`: 原生路径
|
||||||
|
|
||||||
|
### 18. execute_menu_item
|
||||||
|
|
||||||
|
- **描述**: 执行菜单项
|
||||||
|
- **参数**:
|
||||||
|
- `menuPath`: 菜单项路径,如 `Assets/Create/Folder`
|
||||||
|
|
||||||
|
### 19. apply_text_edits
|
||||||
|
|
||||||
|
- **描述**: 应用文本编辑
|
||||||
|
- **参数**:
|
||||||
|
- `filePath`: 文件路径,如 `db://assets/scripts/TestScript.ts`
|
||||||
|
- `edits`: 编辑操作列表
|
||||||
|
- `type`: 操作类型(`insert`, `delete`, `replace`)
|
||||||
|
- `position`: 插入位置(用于 `insert` 操作)
|
||||||
|
- `start`: 开始位置(用于 `delete` 和 `replace` 操作)
|
||||||
|
- `end`: 结束位置(用于 `delete` 和 `replace` 操作)
|
||||||
|
- `text`: 文本内容(用于 `insert` 和 `replace` 操作)
|
||||||
|
|
||||||
|
### 20. read_console
|
||||||
|
|
||||||
|
- **描述**: 读取控制台
|
||||||
|
- **参数**:
|
||||||
|
- `limit`: 输出限制(可选)
|
||||||
|
- `type`: 输出类型(`log`, `error`, `warn`)(可选)
|
||||||
|
|
||||||
|
### 21. validate_script
|
||||||
|
|
||||||
|
- **描述**: 验证脚本
|
||||||
|
- **参数**:
|
||||||
|
- `filePath`: 脚本路径,如 `db://assets/scripts/TestScript.ts`
|
||||||
|
|
||||||
|
### 22. find_in_file
|
||||||
|
|
||||||
|
- **描述**: 全局文件搜索
|
||||||
|
- **参数**:
|
||||||
|
- `query`: 搜索关键词
|
||||||
|
- `extensions`: 文件后缀列表 (可选,默认 `['.js', '.ts', '.json', '.fire', '.prefab', '.xml', '.txt', '.md']`)
|
||||||
|
- `includeSubpackages`: 是否搜索子包 (可选,默认 true)
|
||||||
|
|
||||||
|
### 23. manage_undo
|
||||||
|
|
||||||
|
- **描述**: 撤销/重做管理
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型 (`undo`, `redo`, `begin_group`, `end_group`, `cancel_group`)
|
||||||
|
- `description`: 撤销组描述 (用于 `begin_group`)
|
||||||
|
|
||||||
|
### 24. manage_vfx
|
||||||
|
|
||||||
|
- **描述**: 特效(粒子)管理
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型 (`create`, `update`, `get_info`)
|
||||||
|
- `nodeId`: 节点 UUID (用于 `update`, `get_info`)
|
||||||
|
- `name`: 节点名称 (用于 `create`)
|
||||||
|
- `parentId`: 父节点 UUID (用于 `create`)
|
||||||
|
- `properties`: 粒子属性对象
|
||||||
|
- `duration`, `emissionRate`, `life`, `lifeVar`, `startColor`, `endColor`
|
||||||
|
- `startSize`, `endSize`, `speed`, `angle`, `gravity`, `file` (plist/texture)
|
||||||
|
|
||||||
## 技术实现
|
## 技术实现
|
||||||
|
|
||||||
### 架构设计
|
### 架构设计
|
||||||
@@ -229,6 +340,23 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- 插件会自动标记场景为"已修改",请注意保存场景
|
- 插件会自动标记场景为"已修改",请注意保存场景
|
||||||
- 不同版本的 Cocos Creator 可能会有 API 差异,请根据实际情况调整
|
- 不同版本的 Cocos Creator 可能会有 API 差异,请根据实际情况调整
|
||||||
|
|
||||||
|
## 自动化测试
|
||||||
|
|
||||||
|
本项目包含一套自动化测试脚本,用于验证插件的核心功能(连接性、节点操作、组件管理、资源管理等)。
|
||||||
|
|
||||||
|
### 运行测试
|
||||||
|
|
||||||
|
前提:
|
||||||
|
1. 确保 Cocos Creator 编辑器已打开,且 MCP Bridge 插件已启动(HTTP 服务运行中)。
|
||||||
|
2. 确保命令行所在路径为项目根目录。
|
||||||
|
|
||||||
|
运行命令:
|
||||||
|
```bash
|
||||||
|
node packages/mcp-bridge/test/run_tests.js
|
||||||
|
```
|
||||||
|
|
||||||
|
测试脚本将自动执行一系列操作,并在控制台输出测试结果。如有报错,请查看控制台详细日志。
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request 来改进这个插件!
|
欢迎提交 Issue 和 Pull Request 来改进这个插件!
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
<div class="resizer" id="testResizer"></div>
|
<div class="resizer" id="testResizer"></div>
|
||||||
|
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
|
<div class="tool-description">
|
||||||
|
<label>工具说明:</label>
|
||||||
|
<div id="toolDescription" class="description-box">选择工具查看说明</div>
|
||||||
|
</div>
|
||||||
<div class="flex-v">
|
<div class="flex-v">
|
||||||
<label>工具参数 (JSON):</label>
|
<label>工具参数 (JSON):</label>
|
||||||
<textarea id="toolParams" spellcheck="false" placeholder='{}'></textarea>
|
<textarea id="toolParams" spellcheck="false" placeholder='{}'></textarea>
|
||||||
@@ -90,4 +94,19 @@
|
|||||||
#resultContent { flex: 1; }
|
#resultContent { flex: 1; }
|
||||||
.button-group { display: flex; gap: 5px; padding: 5px 0; }
|
.button-group { display: flex; gap: 5px; padding: 5px 0; }
|
||||||
label { font-size: 11px; color: #888; margin: 4px 0; }
|
label { font-size: 11px; color: #888; margin: 4px 0; }
|
||||||
|
.tool-description {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.description-box {
|
||||||
|
background: #222;
|
||||||
|
color: #ccc;
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
min-height: 60px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
344
panel/index.js
344
panel/index.js
@@ -2,151 +2,225 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
Editor.Panel.extend({
|
Editor.Panel.extend({
|
||||||
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
|
|
||||||
messages: {
|
messages: {
|
||||||
"mcp-bridge:on-log"(event, log) { this.renderLog(log); },
|
"mcp-bridge:on-log"(event, log) {
|
||||||
"mcp-bridge:state-changed"(event, config) { this.updateUI(config.active); }
|
this.renderLog(log);
|
||||||
},
|
},
|
||||||
|
"mcp-bridge:state-changed"(event, config) {
|
||||||
|
this.updateUI(config.active);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
ready() {
|
ready() {
|
||||||
const root = this.shadowRoot;
|
const root = this.shadowRoot;
|
||||||
// 获取元素
|
// 获取元素
|
||||||
const els = {
|
const els = {
|
||||||
port: root.querySelector("#portInput"),
|
port: root.querySelector("#portInput"),
|
||||||
btnToggle: root.querySelector("#btnToggle"),
|
btnToggle: root.querySelector("#btnToggle"),
|
||||||
autoStart: root.querySelector("#autoStartCheck"),
|
autoStart: root.querySelector("#autoStartCheck"),
|
||||||
logView: root.querySelector("#logConsole"),
|
logView: root.querySelector("#logConsole"),
|
||||||
tabMain: root.querySelector("#tabMain"),
|
tabMain: root.querySelector("#tabMain"),
|
||||||
tabTest: root.querySelector("#tabTest"),
|
tabTest: root.querySelector("#tabTest"),
|
||||||
panelMain: root.querySelector("#panelMain"),
|
panelMain: root.querySelector("#panelMain"),
|
||||||
panelTest: root.querySelector("#panelTest"),
|
panelTest: root.querySelector("#panelTest"),
|
||||||
toolName: root.querySelector("#toolName"),
|
toolName: root.querySelector("#toolName"),
|
||||||
toolParams: root.querySelector("#toolParams"),
|
toolParams: root.querySelector("#toolParams"),
|
||||||
toolsList: root.querySelector("#toolsList"),
|
toolDescription: root.querySelector("#toolDescription"),
|
||||||
testBtn: root.querySelector("#testBtn"),
|
toolsList: root.querySelector("#toolsList"),
|
||||||
listBtn: root.querySelector("#listToolsBtn"),
|
testBtn: root.querySelector("#testBtn"),
|
||||||
clearBtn: root.querySelector("#clearTestBtn"),
|
listBtn: root.querySelector("#listToolsBtn"),
|
||||||
result: root.querySelector("#resultContent"),
|
clearBtn: root.querySelector("#clearTestBtn"),
|
||||||
left: root.querySelector("#testLeftPanel"),
|
result: root.querySelector("#resultContent"),
|
||||||
resizer: root.querySelector("#testResizer")
|
left: root.querySelector("#testLeftPanel"),
|
||||||
};
|
resizer: root.querySelector("#testResizer"),
|
||||||
|
};
|
||||||
|
|
||||||
// 1. 初始化状态
|
// 1. 初始化状态
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
els.port.value = data.config.port;
|
els.port.value = data.config.port;
|
||||||
els.autoStart.value = data.autoStart;
|
els.autoStart.value = data.autoStart;
|
||||||
this.updateUI(data.config.active);
|
this.updateUI(data.config.active);
|
||||||
els.logView.innerHTML = "";
|
els.logView.innerHTML = "";
|
||||||
data.logs.forEach(l => this.renderLog(l));
|
data.logs.forEach((l) => this.renderLog(l));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 标签切换
|
// 2. 标签切换
|
||||||
els.tabMain.addEventListener("confirm", () => {
|
els.tabMain.addEventListener("confirm", () => {
|
||||||
els.tabMain.classList.add("active"); els.tabTest.classList.remove("active");
|
els.tabMain.classList.add("active");
|
||||||
els.panelMain.classList.add("active"); els.panelTest.classList.remove("active");
|
els.tabTest.classList.remove("active");
|
||||||
});
|
els.panelMain.classList.add("active");
|
||||||
els.tabTest.addEventListener("confirm", () => {
|
els.panelTest.classList.remove("active");
|
||||||
els.tabTest.classList.add("active"); els.tabMain.classList.remove("active");
|
});
|
||||||
els.panelTest.classList.add("active"); els.panelMain.classList.remove("active");
|
els.tabTest.addEventListener("confirm", () => {
|
||||||
this.fetchTools(els);
|
els.tabTest.classList.add("active");
|
||||||
});
|
els.tabMain.classList.remove("active");
|
||||||
|
els.panelTest.classList.add("active");
|
||||||
|
els.panelMain.classList.remove("active");
|
||||||
|
this.fetchTools(els);
|
||||||
|
});
|
||||||
|
|
||||||
// 3. 基础功能
|
// 3. 基础功能
|
||||||
els.btnToggle.addEventListener("confirm", () => {
|
els.btnToggle.addEventListener("confirm", () => {
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
|
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
|
||||||
});
|
});
|
||||||
root.querySelector("#btnClear").addEventListener("confirm", () => {
|
root.querySelector("#btnClear").addEventListener("confirm", () => {
|
||||||
els.logView.innerHTML = ""; Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
els.logView.innerHTML = "";
|
||||||
});
|
Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
||||||
root.querySelector("#btnCopy").addEventListener("confirm", () => {
|
});
|
||||||
require("electron").clipboard.writeText(els.logView.innerText);
|
root.querySelector("#btnCopy").addEventListener("confirm", () => {
|
||||||
Editor.success("Logs Copied");
|
require("electron").clipboard.writeText(els.logView.innerText);
|
||||||
});
|
Editor.success("Logs Copied");
|
||||||
els.autoStart.addEventListener("change", (e) => {
|
});
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
|
els.autoStart.addEventListener("change", (e) => {
|
||||||
});
|
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
// 4. 测试页功能
|
// 4. 测试页功能
|
||||||
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
|
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
|
||||||
els.clearBtn.addEventListener("confirm", () => { els.result.value = ""; });
|
els.clearBtn.addEventListener("confirm", () => {
|
||||||
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
els.result.value = "";
|
||||||
|
});
|
||||||
|
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
||||||
|
|
||||||
// 5. 【修复】拖拽逻辑
|
// 5. 【修复】拖拽逻辑
|
||||||
if (els.resizer && els.left) {
|
if (els.resizer && els.left) {
|
||||||
els.resizer.addEventListener('mousedown', (e) => {
|
els.resizer.addEventListener("mousedown", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const startX = e.clientX;
|
const startX = e.clientX;
|
||||||
const startW = els.left.offsetWidth;
|
const startW = els.left.offsetWidth;
|
||||||
const onMove = (ev) => { els.left.style.width = (startW + (ev.clientX - startX)) + "px"; };
|
const onMove = (ev) => {
|
||||||
const onUp = () => {
|
els.left.style.width = startW + (ev.clientX - startX) + "px";
|
||||||
document.removeEventListener('mousemove', onMove);
|
};
|
||||||
document.removeEventListener('mouseup', onUp);
|
const onUp = () => {
|
||||||
document.body.style.cursor = 'default';
|
document.removeEventListener("mousemove", onMove);
|
||||||
};
|
document.removeEventListener("mouseup", onUp);
|
||||||
document.addEventListener('mousemove', onMove);
|
document.body.style.cursor = "default";
|
||||||
document.addEventListener('mouseup', onUp);
|
};
|
||||||
document.body.style.cursor = 'col-resize';
|
document.addEventListener("mousemove", onMove);
|
||||||
});
|
document.addEventListener("mouseup", onUp);
|
||||||
}
|
document.body.style.cursor = "col-resize";
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
fetchTools(els) {
|
fetchTools(els) {
|
||||||
const url = `http://localhost:${els.port.value}/list-tools`;
|
const url = `http://localhost:${els.port.value}/list-tools`;
|
||||||
fetch(url).then(r => r.json()).then(data => {
|
fetch(url)
|
||||||
els.toolsList.innerHTML = "";
|
.then((r) => r.json())
|
||||||
data.tools.forEach(t => {
|
.then((data) => {
|
||||||
const item = document.createElement('div');
|
els.toolsList.innerHTML = "";
|
||||||
item.className = 'tool-item';
|
const toolsMap = {};
|
||||||
item.textContent = t.name;
|
data.tools.forEach((t) => {
|
||||||
item.onclick = () => {
|
toolsMap[t.name] = t;
|
||||||
els.toolName.value = t.name;
|
const item = document.createElement("div");
|
||||||
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
|
item.className = "tool-item";
|
||||||
};
|
item.textContent = t.name;
|
||||||
els.toolsList.appendChild(item);
|
item.onclick = () => {
|
||||||
});
|
els.toolName.value = t.name;
|
||||||
els.result.value = `Loaded ${data.tools.length} tools.`;
|
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
|
||||||
}).catch(e => { els.result.value = "Error: " + e.message; });
|
this.showToolDescription(els, t);
|
||||||
},
|
};
|
||||||
|
els.toolsList.appendChild(item);
|
||||||
|
});
|
||||||
|
// 保存工具信息到实例,以便后续使用
|
||||||
|
this.toolsMap = toolsMap;
|
||||||
|
els.result.value = `Loaded ${data.tools.length} tools.`;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
els.result.value = "Error: " + e.message;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
runTest(els) {
|
showToolDescription(els, tool) {
|
||||||
const url = `http://localhost:${els.port.value}/call-tool`;
|
if (!tool) {
|
||||||
const body = { name: els.toolName.value, arguments: JSON.parse(els.toolParams.value || "{}") };
|
els.toolDescription.textContent = "选择工具查看说明";
|
||||||
els.result.value = "Testing...";
|
return;
|
||||||
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; });
|
|
||||||
},
|
|
||||||
|
|
||||||
getExample(name) {
|
let description = tool.description || "无描述";
|
||||||
const examples = {
|
let inputSchema = tool.inputSchema;
|
||||||
"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": "" },
|
|
||||||
"open_scene": { "url": "db://assets/Scene.fire" }
|
|
||||||
};
|
|
||||||
return examples[name] || {};
|
|
||||||
},
|
|
||||||
|
|
||||||
renderLog(log) {
|
let details = [];
|
||||||
const view = this.shadowRoot.querySelector("#logConsole");
|
if (inputSchema && inputSchema.properties) {
|
||||||
if (!view) return;
|
details.push("参数说明:");
|
||||||
const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50;
|
for (const [key, prop] of Object.entries(inputSchema.properties)) {
|
||||||
const el = document.createElement("div");
|
let propDesc = `- ${key}`;
|
||||||
el.className = `log-item ${log.type}`;
|
if (prop.description) {
|
||||||
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`;
|
propDesc += `: ${prop.description}`;
|
||||||
view.appendChild(el);
|
}
|
||||||
if (atBottom) view.scrollTop = view.scrollHeight;
|
if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) {
|
||||||
},
|
propDesc += " (必填)";
|
||||||
|
}
|
||||||
|
details.push(propDesc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateUI(active) {
|
els.toolDescription.innerHTML = `${description}<br><br>${details.join('<br>')}`;
|
||||||
const btn = this.shadowRoot.querySelector("#btnToggle");
|
},
|
||||||
if (!btn) return;
|
|
||||||
btn.innerText = active ? "Stop" : "Start";
|
runTest(els) {
|
||||||
btn.style.backgroundColor = active ? "#aa4444" : "#44aa44";
|
const url = `http://localhost:${els.port.value}/call-tool`;
|
||||||
}
|
const body = { name: els.toolName.value, arguments: JSON.parse(els.toolParams.value || "{}") };
|
||||||
|
els.result.value = "Testing...";
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
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: "" },
|
||||||
|
open_scene: { url: "db://assets/Scene.fire" },
|
||||||
|
manage_editor: { action: "get_selection" },
|
||||||
|
find_gameobjects: { conditions: { name: "Node", active: true }, recursive: true },
|
||||||
|
manage_material: {
|
||||||
|
action: "create",
|
||||||
|
path: "db://assets/materials/NewMaterial.mat",
|
||||||
|
properties: { uniforms: {} },
|
||||||
|
},
|
||||||
|
manage_texture: {
|
||||||
|
action: "create",
|
||||||
|
path: "db://assets/textures/NewTexture.png",
|
||||||
|
properties: { width: 128, height: 128 },
|
||||||
|
},
|
||||||
|
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" }],
|
||||||
|
},
|
||||||
|
read_console: { limit: 10, type: "log" },
|
||||||
|
validate_script: { filePath: "db://assets/scripts/TestScript.ts" },
|
||||||
|
};
|
||||||
|
return examples[name] || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLog(log) {
|
||||||
|
const view = this.shadowRoot.querySelector("#logConsole");
|
||||||
|
if (!view) return;
|
||||||
|
const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50;
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = `log-item ${log.type}`;
|
||||||
|
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`;
|
||||||
|
view.appendChild(el);
|
||||||
|
if (atBottom) view.scrollTop = view.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUI(active) {
|
||||||
|
const btn = this.shadowRoot.querySelector("#btnToggle");
|
||||||
|
if (!btn) return;
|
||||||
|
btn.innerText = active ? "Stop" : "Start";
|
||||||
|
btn.style.backgroundColor = active ? "#aa4444" : "#44aa44";
|
||||||
|
},
|
||||||
});
|
});
|
||||||
268
scene-script.js
268
scene-script.js
@@ -91,7 +91,7 @@ module.exports = {
|
|||||||
"create-node": function (event, args) {
|
"create-node": function (event, args) {
|
||||||
const { name, parentId, type } = args;
|
const { name, parentId, type } = args;
|
||||||
const scene = cc.director.getScene();
|
const scene = cc.director.getScene();
|
||||||
if (!scene || !cc.director.getRunningScene()) {
|
if (!scene) {
|
||||||
if (event.reply) event.reply(new Error("Scene not ready or loading."));
|
if (event.reply) event.reply(new Error("Scene not ready or loading."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -202,7 +202,16 @@ module.exports = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 查找并移除组件
|
// 查找并移除组件
|
||||||
const component = node.getComponentById(componentId);
|
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) {
|
if (component) {
|
||||||
node.removeComponent(component);
|
node.removeComponent(component);
|
||||||
Editor.Ipc.sendToMain("scene:dirty");
|
Editor.Ipc.sendToMain("scene:dirty");
|
||||||
@@ -222,20 +231,51 @@ module.exports = {
|
|||||||
// 获取组件属性
|
// 获取组件属性
|
||||||
const properties = {};
|
const properties = {};
|
||||||
for (const key in c) {
|
for (const key in c) {
|
||||||
if (typeof c[key] !== "function" &&
|
if (typeof c[key] !== "function" && !key.startsWith("_") && c[key] !== undefined) {
|
||||||
!key.startsWith("_") &&
|
|
||||||
c[key] !== undefined) {
|
|
||||||
try {
|
try {
|
||||||
properties[key] = c[key];
|
// Safe serialization check
|
||||||
|
const val = c[key];
|
||||||
|
if (val === null || val === undefined) {
|
||||||
|
properties[key] = val;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primitives are safe
|
||||||
|
if (typeof val !== 'object') {
|
||||||
|
properties[key] = val;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special Cocos Types
|
||||||
|
if (val instanceof cc.ValueType) {
|
||||||
|
properties[key] = val.toString();
|
||||||
|
} else if (val instanceof cc.Asset) {
|
||||||
|
properties[key] = `Asset(${val.name})`;
|
||||||
|
} else if (val instanceof cc.Node) {
|
||||||
|
properties[key] = `Node(${val.name})`;
|
||||||
|
} else if (val instanceof cc.Component) {
|
||||||
|
properties[key] = `Component(${val.name}<${val.__typename}>)`;
|
||||||
|
} else {
|
||||||
|
// Arrays and Plain Objects
|
||||||
|
// Attempt to strip to pure JSON data to avoid IPC errors with Native/Circular objects
|
||||||
|
try {
|
||||||
|
const jsonStr = JSON.stringify(val);
|
||||||
|
// Ensure we don't pass the original object reference
|
||||||
|
properties[key] = JSON.parse(jsonStr);
|
||||||
|
} catch (e) {
|
||||||
|
// If JSON fails (e.g. circular), format as string
|
||||||
|
properties[key] = `[Complex Object: ${val.constructor ? val.constructor.name : typeof val}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 忽略无法序列化的属性
|
properties[key] = "[Serialization Error]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: c.__typename,
|
type: cc.js.getClassName(c) || c.constructor.name || "Unknown",
|
||||||
uuid: c.uuid,
|
uuid: c.uuid,
|
||||||
properties: properties
|
properties: properties,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (event.reply) event.reply(null, components);
|
if (event.reply) event.reply(null, components);
|
||||||
@@ -255,9 +295,7 @@ module.exports = {
|
|||||||
|
|
||||||
// 遍历组件属性
|
// 遍历组件属性
|
||||||
for (const key in component) {
|
for (const key in component) {
|
||||||
if (typeof component[key] !== "function" &&
|
if (typeof component[key] !== "function" && !key.startsWith("_") && component[key] !== undefined) {
|
||||||
!key.startsWith("_") &&
|
|
||||||
component[key] !== undefined) {
|
|
||||||
try {
|
try {
|
||||||
properties[key] = component[key];
|
properties[key] = component[key];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -273,7 +311,7 @@ module.exports = {
|
|||||||
const { prefabPath, parentId } = args;
|
const { prefabPath, parentId } = args;
|
||||||
const scene = cc.director.getScene();
|
const scene = cc.director.getScene();
|
||||||
|
|
||||||
if (!scene || !cc.director.getRunningScene()) {
|
if (!scene) {
|
||||||
if (event.reply) event.reply(new Error("Scene not ready or loading."));
|
if (event.reply) event.reply(new Error("Scene not ready or loading."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -314,4 +352,208 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"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("Parent node not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (action === "update") {
|
||||||
|
let node = cc.engine.getInstanceById(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, "VFX updated");
|
||||||
|
} else {
|
||||||
|
if (event.reply) event.reply(new Error("Node not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (action === "get_info") {
|
||||||
|
let node = cc.engine.getInstanceById(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("Node not found"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event.reply) event.reply(new Error(`Unknown VFX action: ${action}`));
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
309
test/run_tests.js
Normal file
309
test/run_tests.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
const CONFIG = {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 3456,
|
||||||
|
timeout: 5000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 控制台输出颜色
|
||||||
|
const colors = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
gray: "\x1b[90m"
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(type, msg) {
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
switch (type) {
|
||||||
|
case 'info': console.log(`${colors.cyan}[INFO]${colors.reset} ${msg}`); break;
|
||||||
|
case 'success': console.log(`${colors.green}[PASS]${colors.reset} ${msg}`); break;
|
||||||
|
case 'error': console.log(`${colors.red}[FAIL]${colors.reset} ${msg}`); break;
|
||||||
|
case 'warn': console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`); break;
|
||||||
|
case 'group': console.log(`\n${colors.gray}=== ${msg} ===${colors.reset}`); break;
|
||||||
|
default: console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP 辅助函数
|
||||||
|
function request(method, path, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: CONFIG.host,
|
||||||
|
port: CONFIG.port,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: CONFIG.timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(body);
|
||||||
|
// MCP 返回 { content: [{ type: 'text', text: "..." }] }
|
||||||
|
resolve(parsed);
|
||||||
|
} catch (e) {
|
||||||
|
// 某些接口可能返回纯文本或非标准 JSON
|
||||||
|
resolve(body);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (e) => reject(new Error(`连接失败: ${e.message}`)));
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
req.write(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP 工具调用封装
|
||||||
|
async function callTool(name, args = {}) {
|
||||||
|
const payload = {
|
||||||
|
name: name,
|
||||||
|
arguments: args
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request('POST', '/call-tool', payload);
|
||||||
|
|
||||||
|
// 解析复杂的 MCP 响应结构
|
||||||
|
// 预期: { content: [ { type: 'text', text: "..." } ] }
|
||||||
|
if (response && response.content && Array.isArray(response.content)) {
|
||||||
|
const textContent = response.content.find(c => c.type === 'text');
|
||||||
|
if (textContent) {
|
||||||
|
// 工具结果本身可能是 JSON 字符串,尝试解析它
|
||||||
|
try {
|
||||||
|
return JSON.parse(textContent.text);
|
||||||
|
} catch {
|
||||||
|
return textContent.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`工具 [${name}] 调用失败: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断言辅助函数
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message || "断言失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 测试套件 ---
|
||||||
|
const tests = {
|
||||||
|
async setup() {
|
||||||
|
log('group', '连接性检查');
|
||||||
|
try {
|
||||||
|
const tools = await request('POST', '/list-tools');
|
||||||
|
assert(tools && tools.tools && tools.tools.length > 0, "无法获取工具列表");
|
||||||
|
log('success', `已连接到 MCP 服务器。发现 ${tools.tools.length} 个工具。`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log('error', `无法连接服务器。插件是否正在运行? (${e.message})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async testNodeLifecycle() {
|
||||||
|
log('group', '节点生命周期测试');
|
||||||
|
const nodeName = `TestNode_${Date.now()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 创建节点
|
||||||
|
log('info', `尝试创建节点: ${nodeName}`);
|
||||||
|
const newNodeId = await callTool('create_node', { name: nodeName, type: 'empty' });
|
||||||
|
log('info', `create_node 响应: ${JSON.stringify(newNodeId)}`);
|
||||||
|
assert(typeof newNodeId === 'string' && newNodeId.length > 0, `create_node 没有返回 UUID。实际返回: ${JSON.stringify(newNodeId)}`);
|
||||||
|
log('success', `已创建节点: ${nodeName} (${newNodeId})`);
|
||||||
|
|
||||||
|
// 2. 查找节点
|
||||||
|
log('info', `尝试查找节点: ${nodeName}`);
|
||||||
|
const findResult = await callTool('find_gameobjects', { conditions: { name: nodeName } });
|
||||||
|
log('info', `find_gameobjects 响应: ${JSON.stringify(findResult)}`);
|
||||||
|
assert(Array.isArray(findResult), `find_gameobjects 没有返回数组。实际返回: ${JSON.stringify(findResult)}`);
|
||||||
|
assert(findResult.length >= 1, "find_gameobjects 未能找到已创建的节点");
|
||||||
|
|
||||||
|
// 查找特定节点(防止重名,虽然这里名字包含时间戳)
|
||||||
|
const targetNode = findResult.find(n => n.name === nodeName);
|
||||||
|
assert(targetNode, "找到节点但名称不匹配?");
|
||||||
|
assert(targetNode.uuid === newNodeId, `找到的节点 UUID 不匹配。预期 ${newNodeId}, 实际 ${targetNode.uuid}`);
|
||||||
|
log('success', `通过 find_gameobjects 找到节点: ${targetNode.name}`);
|
||||||
|
|
||||||
|
// 3. 更新变换 (Transform)
|
||||||
|
log('info', `尝试更新变换信息`);
|
||||||
|
await callTool('update_node_transform', { id: newNodeId, x: 100, y: 200 });
|
||||||
|
// 通过查找验证(因为查找会返回位置信息)
|
||||||
|
const updatedResult = await callTool('find_gameobjects', { conditions: { name: nodeName } });
|
||||||
|
const updatedNode = updatedResult.find(n => n.uuid === newNodeId);
|
||||||
|
log('info', `变换更新验证: x=${updatedNode.position.x}, y=${updatedNode.position.y}`);
|
||||||
|
assert(updatedNode.position.x === 100 && updatedNode.position.y === 200, `节点位置更新失败。实际: (${updatedNode.position.x}, ${updatedNode.position.y})`);
|
||||||
|
log('success', `节点变换已更新至 (100, 200)`);
|
||||||
|
|
||||||
|
return newNodeId; // 返回以供后续测试使用
|
||||||
|
} catch (e) {
|
||||||
|
log('error', `节点生命周期测试失败: ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async testComponents(nodeId) {
|
||||||
|
log('group', '组件管理测试');
|
||||||
|
|
||||||
|
// 1. 添加组件
|
||||||
|
// 使用 cc.Sprite 因为它最常用
|
||||||
|
log('info', `向 ${nodeId} 添加组件 cc.Sprite`);
|
||||||
|
const addResult = await callTool('manage_components', {
|
||||||
|
nodeId: nodeId,
|
||||||
|
action: 'add',
|
||||||
|
componentType: 'cc.Sprite'
|
||||||
|
});
|
||||||
|
log('success', `已添加 cc.Sprite 组件。响应: ${JSON.stringify(addResult)}`);
|
||||||
|
|
||||||
|
// 2. 获取组件
|
||||||
|
log('info', `列出 ${nodeId} 的组件`);
|
||||||
|
const components = await callTool('manage_components', { nodeId: nodeId, action: 'get' });
|
||||||
|
log('info', `manage_components (get) 响应: ${JSON.stringify(components)}`);
|
||||||
|
|
||||||
|
assert(Array.isArray(components), `无法获取组件列表。实际返回: ${JSON.stringify(components)}`);
|
||||||
|
// 宽松匹配:验证逻辑匹配(检查 type 或 properties.name 中是否包含 Sprite)
|
||||||
|
const spriteComp = components.find(c => (c.type && c.type.includes('Sprite')) || (c.properties && c.properties.name && c.properties.name.includes('Sprite')));
|
||||||
|
assert(spriteComp, "节点上未找到 cc.Sprite 组件");
|
||||||
|
log('success', `验证组件存在: ${spriteComp.uuid} (${spriteComp.type || 'Unknown'})`);
|
||||||
|
|
||||||
|
// 3. 移除组件
|
||||||
|
log('info', `移除组件 ${spriteComp.uuid}`);
|
||||||
|
const removeResult = await callTool('manage_components', {
|
||||||
|
nodeId: nodeId,
|
||||||
|
action: 'remove',
|
||||||
|
componentId: spriteComp.uuid
|
||||||
|
});
|
||||||
|
log('info', `移除结果: ${JSON.stringify(removeResult)}`);
|
||||||
|
|
||||||
|
// 等待引擎处理移除(异步过程)
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
|
||||||
|
// 验证移除
|
||||||
|
const componentsAfter = await callTool('manage_components', { nodeId: nodeId, action: 'get' });
|
||||||
|
log('info', `移除后的组件列表: ${JSON.stringify(componentsAfter)}`);
|
||||||
|
|
||||||
|
assert(!componentsAfter.find(c => (c.type && c.type.includes('Sprite')) || (c.uuid === spriteComp.uuid)), "组件未被移除");
|
||||||
|
log('success', `组件移除成功`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async testEditorSelection(nodeId) {
|
||||||
|
log('group', '编辑器选中测试');
|
||||||
|
|
||||||
|
// 1. 设置选中
|
||||||
|
await callTool('manage_editor', {
|
||||||
|
action: 'set_selection',
|
||||||
|
target: 'node',
|
||||||
|
properties: { nodes: [nodeId] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 获取选中
|
||||||
|
const selection = await callTool('manage_editor', { action: 'get_selection' });
|
||||||
|
// 预期: { nodes: [...], assets: [...] }
|
||||||
|
assert(selection.nodes && selection.nodes.includes(nodeId), "选中状态更新失败");
|
||||||
|
log('success', `编辑器选中状态已更新为节点 ${nodeId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async testAssetManagement() {
|
||||||
|
log('group', '资源管理测试');
|
||||||
|
const scriptPath = 'db://assets/temp_auto_test.js';
|
||||||
|
|
||||||
|
// 1. 创建脚本
|
||||||
|
try {
|
||||||
|
await callTool('manage_script', {
|
||||||
|
action: 'create',
|
||||||
|
path: scriptPath,
|
||||||
|
content: 'cc.log("Test Script");'
|
||||||
|
});
|
||||||
|
log('success', `已创建临时资源: ${scriptPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes('exists')) {
|
||||||
|
log('warn', `资源已存在,正在尝试先删除...`);
|
||||||
|
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
||||||
|
// 重试创建
|
||||||
|
await callTool('manage_script', { action: 'create', path: scriptPath, content: 'cc.log("Test Script");' });
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取信息
|
||||||
|
// 等待 AssetDB 刷新 (导入需要时间)
|
||||||
|
log('info', '等待 3 秒以进行资源导入...');
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
log('info', `获取资源信息: ${scriptPath}`);
|
||||||
|
const info = await callTool('manage_asset', { action: 'get_info', path: scriptPath });
|
||||||
|
log('info', `资源信息: ${JSON.stringify(info)}`);
|
||||||
|
|
||||||
|
assert(info && info.url === scriptPath, "无法获取资源信息");
|
||||||
|
log('success', `已验证资源信息`);
|
||||||
|
|
||||||
|
// 3. 删除资源
|
||||||
|
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
||||||
|
|
||||||
|
// 验证删除 (get_info 应该失败或返回 null/报错,但我们检查工具响应)
|
||||||
|
try {
|
||||||
|
const infoDeleted = await callTool('manage_asset', { action: 'get_info', path: scriptPath });
|
||||||
|
// 如果返回了信息且 exists 为 true,说明没删掉
|
||||||
|
assert(!(infoDeleted && infoDeleted.exists), "资源本应被删除,但仍然存在");
|
||||||
|
} catch (e) {
|
||||||
|
// 如果报错(如 Asset not found),则符合预期
|
||||||
|
log('success', `已验证资源删除`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log(`\n${colors.cyan}正在启动 MCP Bridge 自动化测试...${colors.reset}`);
|
||||||
|
console.log(`目标: http://${CONFIG.host}:${CONFIG.port}\n`);
|
||||||
|
|
||||||
|
const isConnected = await tests.setup();
|
||||||
|
if (!isConnected) process.exit(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodeId = await tests.testNodeLifecycle();
|
||||||
|
|
||||||
|
await tests.testComponents(nodeId);
|
||||||
|
|
||||||
|
await tests.testEditorSelection(nodeId);
|
||||||
|
|
||||||
|
await tests.testAssetManagement();
|
||||||
|
|
||||||
|
// 清理:我们在测试中已经尽可能清理了,但保留节点可能有助于观察结果
|
||||||
|
// 这里只是打印完成消息
|
||||||
|
|
||||||
|
console.log(`\n${colors.green}所有测试已成功完成!${colors.reset}\n`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`\n${colors.red}[FATAL ERROR]${colors.reset} 测试套件出错:`);
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
107
test/test_find_file.js
Normal file
107
test/test_find_file.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 3456,
|
||||||
|
timeout: 5000
|
||||||
|
};
|
||||||
|
|
||||||
|
// HTTP Helper
|
||||||
|
function request(method, path, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: CONFIG.host,
|
||||||
|
port: CONFIG.port,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: CONFIG.timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
try { resolve(JSON.parse(body)); } catch (e) { resolve(body); }
|
||||||
|
} else {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (e) => reject(new Error(`Connection failed: ${e.message}`)));
|
||||||
|
req.end(data ? JSON.stringify(data) : undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callTool(name, args = {}) {
|
||||||
|
const payload = { name: name, arguments: args };
|
||||||
|
const response = await request('POST', '/call-tool', payload);
|
||||||
|
if (response && response.content && Array.isArray(response.content)) {
|
||||||
|
const textContent = response.content.find(c => c.type === 'text');
|
||||||
|
if (textContent) {
|
||||||
|
try { return JSON.parse(textContent.text); } catch { return textContent.text; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Testing find_in_file...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Check tools
|
||||||
|
const tools = await request('POST', '/list-tools');
|
||||||
|
const findTool = tools.tools.find(t => t.name === 'find_in_file');
|
||||||
|
if (!findTool) {
|
||||||
|
console.error("FAILED: find_in_file tool not found in list.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("PASS: find_in_file exists in tool list.");
|
||||||
|
|
||||||
|
// 2. Create a temp file to search for
|
||||||
|
const tempFilePath = "db://assets/test_find_me.txt";
|
||||||
|
const uniqueString = "UniqueStringToFind_" + Date.now();
|
||||||
|
console.log(`Creating temp file with content "${uniqueString}"...`);
|
||||||
|
|
||||||
|
await callTool('manage_asset', {
|
||||||
|
action: 'create',
|
||||||
|
path: tempFilePath,
|
||||||
|
content: `This is a test file.\nIt contains ${uniqueString} here.`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a bit for assetdb to refresh
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
|
||||||
|
// 3. Call find_in_file
|
||||||
|
console.log(`Searching for "${uniqueString}"...`);
|
||||||
|
|
||||||
|
const results = await callTool('find_in_file', { query: uniqueString });
|
||||||
|
|
||||||
|
if (!Array.isArray(results)) {
|
||||||
|
console.error("FAILED: Result is not an array:", results);
|
||||||
|
// Cleanup
|
||||||
|
await callTool('manage_asset', { action: 'delete', path: tempFilePath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${results.length} matches.`);
|
||||||
|
const match = results.find(r => r.content.includes(uniqueString));
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
console.log("PASS: Found match in created file.");
|
||||||
|
console.log("Match Details:", match);
|
||||||
|
} else {
|
||||||
|
console.error("FAILED: Did not find match. Results:", results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cleanup
|
||||||
|
await callTool('manage_asset', { action: 'delete', path: tempFilePath });
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
146
test/test_undo.js
Normal file
146
test/test_undo.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 3456,
|
||||||
|
timeout: 5000
|
||||||
|
};
|
||||||
|
|
||||||
|
// HTTP Helper
|
||||||
|
function request(method, path, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: CONFIG.host,
|
||||||
|
port: CONFIG.port,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: CONFIG.timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
try { resolve(JSON.parse(body)); } catch (e) { resolve(body); }
|
||||||
|
} else {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (e) => reject(new Error(`Connection failed: ${e.message}`)));
|
||||||
|
req.end(data ? JSON.stringify(data) : undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callTool(name, args = {}) {
|
||||||
|
const payload = { name: name, arguments: args };
|
||||||
|
const response = await request('POST', '/call-tool', payload);
|
||||||
|
if (response && response.content && Array.isArray(response.content)) {
|
||||||
|
const textContent = response.content.find(c => c.type === 'text');
|
||||||
|
if (textContent) {
|
||||||
|
try { return JSON.parse(textContent.text); } catch { return textContent.text; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to wait
|
||||||
|
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Testing manage_undo...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Create a node
|
||||||
|
const nodeName = "UndoTestNode_" + Date.now();
|
||||||
|
console.log(`Creating node: ${nodeName}`);
|
||||||
|
const nodeId = await callTool('create_node', { name: nodeName, type: 'empty' });
|
||||||
|
|
||||||
|
if (!nodeId || typeof nodeId !== 'string') {
|
||||||
|
console.error("FAILED: Could not create node.", nodeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Node created: ${nodeId}`);
|
||||||
|
|
||||||
|
// Wait to ensure creation is fully processed
|
||||||
|
await wait(500);
|
||||||
|
|
||||||
|
// 2. Modify node (Change Name)
|
||||||
|
console.log("Modifying node name (Action to undo)...");
|
||||||
|
const newName = "RenamedNode_" + Date.now();
|
||||||
|
await callTool('set_node_name', { id: nodeId, newName: newName });
|
||||||
|
|
||||||
|
await wait(2000);
|
||||||
|
|
||||||
|
// Verify modification
|
||||||
|
let nodes = await callTool('find_gameobjects', { conditions: { name: newName } });
|
||||||
|
let node = nodes.find(n => n.uuid === nodeId);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
console.error(`FAILED: Node not found with new name ${newName}. Name update failed.`);
|
||||||
|
// Try to read console logs to see why
|
||||||
|
const logs = await callTool('read_console', { limit: 10, type: 'error' });
|
||||||
|
console.log("Recent Error Logs:", JSON.stringify(logs, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Node renamed to ${node.name}.`);
|
||||||
|
|
||||||
|
// 3. Perform UNDO
|
||||||
|
console.log("Executing UNDO...");
|
||||||
|
await callTool('manage_undo', { action: 'undo' });
|
||||||
|
|
||||||
|
await wait(2000);
|
||||||
|
|
||||||
|
// Verify UNDO
|
||||||
|
nodes = await callTool('find_gameobjects', { conditions: { name: nodeName } });
|
||||||
|
// The original name was in variable nodeName
|
||||||
|
node = nodes.find(n => n.uuid === nodeId);
|
||||||
|
|
||||||
|
if (node && node.name === nodeName) {
|
||||||
|
console.log(`PASS: Undo successful. Node name returned to ${nodeName}.`);
|
||||||
|
} else {
|
||||||
|
console.error(`FAILED: Undo failed? Node name is ${node ? node.name : 'Unknown'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Perform REDO
|
||||||
|
console.log("Executing REDO...");
|
||||||
|
await callTool('manage_undo', { action: 'redo' });
|
||||||
|
|
||||||
|
await wait(2000);
|
||||||
|
|
||||||
|
// Verify REDO
|
||||||
|
nodes = await callTool('find_gameobjects', { conditions: { name: newName } });
|
||||||
|
node = nodes.find(n => n.uuid === nodeId);
|
||||||
|
|
||||||
|
if (node && node.name === newName) {
|
||||||
|
console.log("PASS: Redo successful. Node name returned to " + newName + ".");
|
||||||
|
} else {
|
||||||
|
console.error(`FAILED: Redo failed? Node name is ${node ? node.name : 'Unknown'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
// await callTool('manage_undo', { action: 'begin_group', description: 'Delete Node' }); // Optional
|
||||||
|
// Node deletion tool... wait, we don't have delete_node tool exposed yet?
|
||||||
|
// Ah, 'scene:delete-nodes' is internal.
|
||||||
|
// We can use 'batch_execute' if we had a delete tool.
|
||||||
|
// Checking available tools... we assume we can manually delete or leave it.
|
||||||
|
// Actually, let's construct a delete call if possible via existing tools?
|
||||||
|
// create_node, manage_components...
|
||||||
|
// Wait, DEVELOPMENT_PLAN says 'batch_execute' exists.
|
||||||
|
// But we don't have a direct 'delete_node' in getToolsList().
|
||||||
|
// Oh, we missed implementing 'delete_node' in the previous phases?
|
||||||
|
// Let's check main.js getToolsList again.
|
||||||
|
// ... It has 'create_node', 'manage_components', ... 'scene_management'...
|
||||||
|
// 'scene_management' has 'delete'? -> "场景管理" -> create, delete (scene file), duplicate.
|
||||||
|
// It seems we lack `delete_node`.
|
||||||
|
// Nevermind, letting the test node stay is fine for observation, or user can delete manually.
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
175
test/test_vfx.js
Normal file
175
test/test_vfx.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 3456, // Ideally read from profile or keep dynamic, but fixed for test
|
||||||
|
timeout: 5000
|
||||||
|
};
|
||||||
|
|
||||||
|
// HTTP Helper
|
||||||
|
function request(method, path, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: CONFIG.host,
|
||||||
|
port: CONFIG.port,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: CONFIG.timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
try { resolve(JSON.parse(body)); } catch (e) { resolve(body); }
|
||||||
|
} else {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (e) => reject(new Error(`Connection failed: ${e.message}`)));
|
||||||
|
req.end(data ? JSON.stringify(data) : undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callTool(name, args = {}) {
|
||||||
|
const payload = { name: name, arguments: args };
|
||||||
|
const response = await request('POST', '/call-tool', payload);
|
||||||
|
if (response && response.content && Array.isArray(response.content)) {
|
||||||
|
const textContent = response.content.find(c => c.type === 'text');
|
||||||
|
if (textContent) {
|
||||||
|
try { return JSON.parse(textContent.text); } catch { return textContent.text; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to wait
|
||||||
|
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Testing manage_vfx...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Create a Particle Node
|
||||||
|
const nodeName = "VFX_Test_" + Date.now();
|
||||||
|
console.log(`Creating particle node: ${nodeName}`);
|
||||||
|
|
||||||
|
const createResult = await callTool('manage_vfx', {
|
||||||
|
action: 'create',
|
||||||
|
name: nodeName,
|
||||||
|
properties: {
|
||||||
|
duration: 5,
|
||||||
|
emissionRate: 50,
|
||||||
|
startColor: "#FF0000",
|
||||||
|
endColor: "#0000FF"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let nodeId = createResult;
|
||||||
|
// Check if result is UUID string or object
|
||||||
|
if (typeof createResult === 'object') {
|
||||||
|
// Sometimes mcp-bridge returns object? No, scene-script returns uuid or error.
|
||||||
|
// But checking just in case
|
||||||
|
nodeId = createResult.uuid || createResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeId || typeof nodeId !== 'string') {
|
||||||
|
console.error("FAILED: Could not create VFX node.", createResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`VFX Node created: ${nodeId}`);
|
||||||
|
|
||||||
|
await wait(1000);
|
||||||
|
|
||||||
|
// 2. Perform Undo (Verify creation undo)
|
||||||
|
// ... Optional, let's focus on Update first.
|
||||||
|
|
||||||
|
// 3. Update Particle Properties
|
||||||
|
console.log("Updating particle properties...");
|
||||||
|
const updateResult = await callTool('manage_vfx', {
|
||||||
|
action: 'update',
|
||||||
|
nodeId: nodeId,
|
||||||
|
properties: {
|
||||||
|
emissionRate: 100,
|
||||||
|
startSize: 50,
|
||||||
|
speed: 200
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("Update result:", updateResult);
|
||||||
|
|
||||||
|
await wait(1000);
|
||||||
|
|
||||||
|
// 4. Get Info to Verify
|
||||||
|
console.log("Verifying properties...");
|
||||||
|
const info = await callTool('manage_vfx', { action: 'get_info', nodeId: nodeId });
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
console.error("FAILED: Could not get info.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Particle Info:", JSON.stringify(info, null, 2));
|
||||||
|
|
||||||
|
if (info.emissionRate === 100 && info.speed === 200) {
|
||||||
|
console.log("PASS: Properties updated and verified.");
|
||||||
|
} else {
|
||||||
|
console.error("FAILED: Properties mismatch.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Verify 'custom' property using manage_components
|
||||||
|
// We need to ensure custom is true for properties to take effect visually
|
||||||
|
console.log("Verifying 'custom' property...");
|
||||||
|
const components = await callTool('manage_components', {
|
||||||
|
nodeId: nodeId,
|
||||||
|
action: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
let particleComp = null;
|
||||||
|
if (components && Array.isArray(components)) {
|
||||||
|
particleComp = components.find(c => c.type === 'cc.ParticleSystem' || c.type === 'ParticleSystem');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (particleComp && particleComp.properties) {
|
||||||
|
if (particleComp.properties.custom === true) {
|
||||||
|
console.log("PASS: ParticleSystem.custom is TRUE.");
|
||||||
|
} else {
|
||||||
|
console.error("FAILED: ParticleSystem.custom is FALSE or Undefined.", particleComp.properties.custom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check texture/file if possible
|
||||||
|
if (particleComp.properties.file || particleComp.properties.texture) {
|
||||||
|
console.log("PASS: ParticleSystem has file/texture.");
|
||||||
|
} else {
|
||||||
|
console.warn("WARNING: ParticleSystem might not have a texture/file set.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("FAILED: Could not retrieve component details.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(1000);
|
||||||
|
|
||||||
|
// 6. Fetch Logs to debug texture loading
|
||||||
|
console.log("Fetching recent Editor Logs...");
|
||||||
|
const logs = await callTool('read_console', { limit: 20 });
|
||||||
|
if (logs && Array.isArray(logs)) {
|
||||||
|
logs.forEach(log => {
|
||||||
|
const msg = log.message || JSON.stringify(log);
|
||||||
|
const type = log.type || 'info';
|
||||||
|
|
||||||
|
// Filter for our debug logs or errors
|
||||||
|
if (typeof msg === 'string' && (msg.includes("[mcp-bridge]") || type === 'error' || type === 'warn')) {
|
||||||
|
console.log(`[Editor Log] [${type}] ${msg}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
Reference in New Issue
Block a user