feat: Implement VFX management, Undo/Redo, Find in File; docs: Update README and plans

This commit is contained in:
火焰库拉
2026-02-01 13:30:11 +08:00
parent 157b99290d
commit a851493966
11 changed files with 3071 additions and 305 deletions

View File

@@ -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 插件,为 Cocos Creator 生态系统增添了新的工具和能力。
目前插件已经完成了核心功能的实现,包括 15 个 MCP 工具,支持从场景操作到资源管理的各种功能。后续将继续完善测试、优化性能,并添加更多高级功能,为开发者提供更强大的工具支持。
通过与 Unity-MCP 的对比分析,我们识别出了多个可实现的功能,这些功能将进一步增强 Cocos-MCP 的能力,使其与 Unity-MCP 保持功能对等,为 Cocos Creator 开发者提供同样强大的 AI 辅助开发体验。

630
DEVELOPMENT_PLAN.md Normal file
View 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
View File

@@ -17,6 +17,18 @@
- **资产管理**: 创建、删除、移动、获取资源信息
- **实时日志**: 提供详细的操作日志记录和展示
- **自动启动**: 支持编辑器启动时自动开启服务
- **编辑器管理**: 获取和设置选中对象,刷新编辑器
- **游戏对象查找**: 根据条件查找场景中的节点
- **材质管理**: 创建和管理材质资源
- **纹理管理**: 创建和管理纹理资源
- **菜单项执行**: 执行 Cocos Creator 编辑器菜单项
- **代码编辑增强**: 应用文本编辑操作到文件
- **控制台读取**: 读取编辑器控制台输出
- **脚本验证**: 验证脚本语法正确性
- **全局搜索**: 在项目中搜索文本内容
- **撤销/重做**: 管理编辑器的撤销栈
- **特效管理**: 创建和修改粒子系统
- **工具说明**: 测试面板提供详细的工具描述和参数说明
## 安装与使用
@@ -172,6 +184,105 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- `nodeId`: 节点 ID用于 `create``update` 操作)
- `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 差异,请根据实际情况调整
## 自动化测试
本项目包含一套自动化测试脚本,用于验证插件的核心功能(连接性、节点操作、组件管理、资源管理等)。
### 运行测试
前提:
1. 确保 Cocos Creator 编辑器已打开,且 MCP Bridge 插件已启动HTTP 服务运行中)。
2. 确保命令行所在路径为项目根目录。
运行命令:
```bash
node packages/mcp-bridge/test/run_tests.js
```
测试脚本将自动执行一系列操作,并在控制台输出测试结果。如有报错,请查看控制台详细日志。
## 贡献
欢迎提交 Issue 和 Pull Request 来改进这个插件!

1022
main.js

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,10 @@
<div class="resizer" id="testResizer"></div>
<div class="right-panel">
<div class="tool-description">
<label>工具说明:</label>
<div id="toolDescription" class="description-box">选择工具查看说明</div>
</div>
<div class="flex-v">
<label>工具参数 (JSON):</label>
<textarea id="toolParams" spellcheck="false" placeholder='{}'></textarea>
@@ -90,4 +94,19 @@
#resultContent { flex: 1; }
.button-group { display: flex; gap: 5px; padding: 5px 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>

View File

@@ -2,151 +2,225 @@
const fs = require("fs");
Editor.Panel.extend({
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"),
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"),
messages: {
"mcp-bridge:on-log"(event, log) { this.renderLog(log); },
"mcp-bridge:state-changed"(event, config) { this.updateUI(config.active); }
},
messages: {
"mcp-bridge:on-log"(event, log) {
this.renderLog(log);
},
"mcp-bridge:state-changed"(event, config) {
this.updateUI(config.active);
},
},
ready() {
const root = this.shadowRoot;
// 获取元素
const els = {
port: root.querySelector("#portInput"),
btnToggle: root.querySelector("#btnToggle"),
autoStart: root.querySelector("#autoStartCheck"),
logView: root.querySelector("#logConsole"),
tabMain: root.querySelector("#tabMain"),
tabTest: root.querySelector("#tabTest"),
panelMain: root.querySelector("#panelMain"),
panelTest: root.querySelector("#panelTest"),
toolName: root.querySelector("#toolName"),
toolParams: root.querySelector("#toolParams"),
toolsList: root.querySelector("#toolsList"),
testBtn: root.querySelector("#testBtn"),
listBtn: root.querySelector("#listToolsBtn"),
clearBtn: root.querySelector("#clearTestBtn"),
result: root.querySelector("#resultContent"),
left: root.querySelector("#testLeftPanel"),
resizer: root.querySelector("#testResizer")
};
ready() {
const root = this.shadowRoot;
// 获取元素
const els = {
port: root.querySelector("#portInput"),
btnToggle: root.querySelector("#btnToggle"),
autoStart: root.querySelector("#autoStartCheck"),
logView: root.querySelector("#logConsole"),
tabMain: root.querySelector("#tabMain"),
tabTest: root.querySelector("#tabTest"),
panelMain: root.querySelector("#panelMain"),
panelTest: root.querySelector("#panelTest"),
toolName: root.querySelector("#toolName"),
toolParams: root.querySelector("#toolParams"),
toolDescription: root.querySelector("#toolDescription"),
toolsList: root.querySelector("#toolsList"),
testBtn: root.querySelector("#testBtn"),
listBtn: root.querySelector("#listToolsBtn"),
clearBtn: root.querySelector("#clearTestBtn"),
result: root.querySelector("#resultContent"),
left: root.querySelector("#testLeftPanel"),
resizer: root.querySelector("#testResizer"),
};
// 1. 初始化状态
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
if (data) {
els.port.value = data.config.port;
els.autoStart.value = data.autoStart;
this.updateUI(data.config.active);
els.logView.innerHTML = "";
data.logs.forEach(l => this.renderLog(l));
}
});
// 1. 初始化状态
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
if (data) {
els.port.value = data.config.port;
els.autoStart.value = data.autoStart;
this.updateUI(data.config.active);
els.logView.innerHTML = "";
data.logs.forEach((l) => this.renderLog(l));
}
});
// 2. 标签切换
els.tabMain.addEventListener("confirm", () => {
els.tabMain.classList.add("active"); els.tabTest.classList.remove("active");
els.panelMain.classList.add("active"); els.panelTest.classList.remove("active");
});
els.tabTest.addEventListener("confirm", () => {
els.tabTest.classList.add("active"); els.tabMain.classList.remove("active");
els.panelTest.classList.add("active"); els.panelMain.classList.remove("active");
this.fetchTools(els);
});
// 2. 标签切换
els.tabMain.addEventListener("confirm", () => {
els.tabMain.classList.add("active");
els.tabTest.classList.remove("active");
els.panelMain.classList.add("active");
els.panelTest.classList.remove("active");
});
els.tabTest.addEventListener("confirm", () => {
els.tabTest.classList.add("active");
els.tabMain.classList.remove("active");
els.panelTest.classList.add("active");
els.panelMain.classList.remove("active");
this.fetchTools(els);
});
// 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("Logs Copied");
});
els.autoStart.addEventListener("change", (e) => {
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
});
// 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("Logs Copied");
});
els.autoStart.addEventListener("change", (e) => {
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
});
// 4. 测试页功能
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
els.clearBtn.addEventListener("confirm", () => { els.result.value = ""; });
els.testBtn.addEventListener("confirm", () => this.runTest(els));
// 4. 测试页功能
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
els.clearBtn.addEventListener("confirm", () => {
els.result.value = "";
});
els.testBtn.addEventListener("confirm", () => this.runTest(els));
// 5. 【修复】拖拽逻辑
if (els.resizer && els.left) {
els.resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
const startX = e.clientX;
const startW = els.left.offsetWidth;
const onMove = (ev) => { els.left.style.width = (startW + (ev.clientX - startX)) + "px"; };
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = 'default';
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
document.body.style.cursor = 'col-resize';
});
}
},
// 5. 【修复】拖拽逻辑
if (els.resizer && els.left) {
els.resizer.addEventListener("mousedown", (e) => {
e.preventDefault();
const startX = e.clientX;
const startW = els.left.offsetWidth;
const onMove = (ev) => {
els.left.style.width = startW + (ev.clientX - startX) + "px";
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
document.body.style.cursor = "default";
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
document.body.style.cursor = "col-resize";
});
}
},
fetchTools(els) {
const url = `http://localhost:${els.port.value}/list-tools`;
fetch(url).then(r => r.json()).then(data => {
els.toolsList.innerHTML = "";
data.tools.forEach(t => {
const item = document.createElement('div');
item.className = 'tool-item';
item.textContent = t.name;
item.onclick = () => {
els.toolName.value = t.name;
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
};
els.toolsList.appendChild(item);
});
els.result.value = `Loaded ${data.tools.length} tools.`;
}).catch(e => { els.result.value = "Error: " + e.message; });
},
fetchTools(els) {
const url = `http://localhost:${els.port.value}/list-tools`;
fetch(url)
.then((r) => r.json())
.then((data) => {
els.toolsList.innerHTML = "";
const toolsMap = {};
data.tools.forEach((t) => {
toolsMap[t.name] = t;
const item = document.createElement("div");
item.className = "tool-item";
item.textContent = t.name;
item.onclick = () => {
els.toolName.value = t.name;
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
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) {
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; });
},
showToolDescription(els, tool) {
if (!tool) {
els.toolDescription.textContent = "选择工具查看说明";
return;
}
let description = tool.description || "无描述";
let inputSchema = tool.inputSchema;
let details = [];
if (inputSchema && inputSchema.properties) {
details.push("参数说明:");
for (const [key, prop] of Object.entries(inputSchema.properties)) {
let propDesc = `- ${key}`;
if (prop.description) {
propDesc += `: ${prop.description}`;
}
if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) {
propDesc += " (必填)";
}
details.push(propDesc);
}
}
els.toolDescription.innerHTML = `${description}<br><br>${details.join('<br>')}`;
},
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" }
};
return examples[name] || {};
},
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 = "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;
});
},
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;
},
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] || {};
},
updateUI(active) {
const btn = this.shadowRoot.querySelector("#btnToggle");
if (!btn) return;
btn.innerText = active ? "Stop" : "Start";
btn.style.backgroundColor = active ? "#aa4444" : "#44aa44";
}
});
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";
},
});

View File

@@ -91,7 +91,7 @@ module.exports = {
"create-node": function (event, args) {
const { name, parentId, type } = args;
const scene = cc.director.getScene();
if (!scene || !cc.director.getRunningScene()) {
if (!scene) {
if (event.reply) event.reply(new Error("Scene not ready or loading."));
return;
}
@@ -202,7 +202,16 @@ module.exports = {
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) {
node.removeComponent(component);
Editor.Ipc.sendToMain("scene:dirty");
@@ -222,20 +231,51 @@ module.exports = {
// 获取组件属性
const properties = {};
for (const key in c) {
if (typeof c[key] !== "function" &&
!key.startsWith("_") &&
c[key] !== undefined) {
if (typeof c[key] !== "function" && !key.startsWith("_") && c[key] !== undefined) {
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) {
// 忽略无法序列化的属性
properties[key] = "[Serialization Error]";
}
}
}
return {
type: c.__typename,
type: cc.js.getClassName(c) || c.constructor.name || "Unknown",
uuid: c.uuid,
properties: properties
properties: properties,
};
});
if (event.reply) event.reply(null, components);
@@ -255,9 +295,7 @@ module.exports = {
// 遍历组件属性
for (const key in component) {
if (typeof component[key] !== "function" &&
!key.startsWith("_") &&
component[key] !== undefined) {
if (typeof component[key] !== "function" && !key.startsWith("_") && component[key] !== undefined) {
try {
properties[key] = component[key];
} catch (e) {
@@ -273,7 +311,7 @@ module.exports = {
const { prefabPath, parentId } = args;
const scene = cc.director.getScene();
if (!scene || !cc.director.getRunningScene()) {
if (!scene) {
if (event.reply) event.reply(new Error("Scene not ready or loading."));
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
View 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
View 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
View 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
View 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();