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 插件通过 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
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` 操作) - `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 来改进这个插件!

1020
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="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>

View File

@@ -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";
},
}); });

View File

@@ -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
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();