文档: 整合开发计划文档 & 修复: TypeScript 编译及可靠性改进

This commit is contained in:
火焰库拉
2026-02-03 19:55:51 +08:00
parent 5c1605c9f1
commit 720b38e1ff
21 changed files with 3148 additions and 2501 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# **/dist
**/node_modules
**/package-lock.json

View File

@@ -1,202 +0,0 @@
# MCP Bridge 插件代码审查报告
**审查日期**: 2026-02-02
**审查人**: AI Assistant
## 📋 审查概要
| 项目 | 状态 |
|------|------|
| 代码行数 | `main.js` (1560行), `scene-script.js` (560行) |
| 测试脚本 | 4个 (`run_tests.js`, `test_find_file.js`, `test_undo.js`, `test_vfx.js`) |
| 功能完成度 | **高** - 大部分核心功能已实现 |
| 代码质量 | **中等** - 存在重复代码和结构问题 |
---
## 🔴 高优先级问题 (需要立即修复)
### 1. 重复函数定义 (严重)
`main.js` 中发现以下重复定义的函数:
| 函数名 | 第一次出现 | 第二次出现 |
|--------|-----------|-----------|
| `validateScript` | 行 1337-1369 | 行 1424-1456 |
| `get-server-state` (message) | 行 1375-1377 | 行 1408-1415 |
| `executeMenuItem` | 行 1243-1254 | 行 1317-1335 (同名但实现不同) |
> ⚠️ **警告**: 重复函数会导致后定义的覆盖先定义的,可能造成意外行为。第二个 `validateScript` (行 1424-1456) 使用了 `loadMeta` 而不是 `loadAny`,可能无法正确读取脚本内容。
**建议修复**:
- 删除重复的 `validateScript` 函数 (保留行 1337-1369 的版本,它使用 `loadAny`)
- 合并两个 `get-server-state` 消息处理器 (第二个更完整,包含 `autoStart`)
- 审查并合并两个 `executeMenuItem` 的实现
---
### 2. TypeScript 脚本验证缺失
```javascript
// 对于 TypeScript 脚本,这里可以添加更复杂的验证逻辑
callback(null, { valid: true, message: 'Script syntax is valid' });
```
`validate_script` 工具对 TypeScript `.ts` 文件没有实际验证逻辑,直接返回 valid: true。
**建议修复**:
- 集成 TypeScript 编译器进行语法检查
- 或者使用 `require('typescript').transpileModule()` 进行基本验证
---
## 🟡 中优先级问题 (建议修复)
### 3. 菜单项执行功能受限
```javascript
// 通用尝试 (可能不工作,取决于编辑器版本)
// Editor.Ipc.sendToMain('ui:menu-click', menuPath);
// 兜底:仅记录日志,暂不支持通用菜单点击
addLog("warn", "Generic menu execution partial support.");
```
`execute_menu_item` 工具只对 `File/Save Scene` 有实际作用,其他菜单项仅记录日志。
**建议修复**:
- 调研 Cocos Creator 2.4.x 的菜单执行 IPC 接口
- 或者添加常用菜单项的映射表
---
### 4. 颜色属性 Undo 不支持
```javascript
// 暂且忽略颜色的 Undo先保证 Transform 的 Undo。
Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", { id, color }, (err) => {
if (err) addLog("warn", "Color update failed or partial");
});
```
`update_node_transform` 中位置和缩放使用 `scene:set-property` 支持 Undo但颜色通过 scene-script 修改,不支持 Undo。
**建议修复**:
- 研究如何通过 `scene:set-property` 设置颜色属性
---
### 5. 材质/纹理创建功能不完整
`manage_material``manage_texture``create` 操作创建的是 JSON 元数据文件,而不是真正的材质或纹理资源。
```javascript
// 创建材质资源
const materialContent = JSON.stringify({
__type__: "cc.Material",
// ...
});
```
**建议修复**:
- 文档中说明此工具的限制
- 或者使用 Cocos Creator 的资源创建 API
---
## 🟢 低优先级问题 (可优化)
### 6. 缺失的功能模块
根据 `DEVELOPMENT_PLAN.md`,以下功能未完全实现或可扩展:
| 功能 | 状态 | 说明 |
|------|------|------|
| Git 集成 (`get_sha`) | ❌ 未实现 | 获取当前版本库信息 |
| ScriptableObject 管理 | ❌ 未实现 | 类似 Unity 的 ScriptableObject |
| 国际化支持 | ❌ 未实现 | 多语言界面 |
| 插件商店发布 | ❌ 未完成 | 打包和发布流程 |
---
### 7. 代码结构优化建议
1. **拆分 `main.js`**: 1560 行代码过于庞大,建议拆分为:
- `tools/scene-tools.js` - 场景相关工具
- `tools/asset-tools.js` - 资源管理工具
- `tools/editor-tools.js` - 编辑器控制工具
2. **统一错误处理**: 部分函数直接 `return callback(error)`,部分使用 `callback(err, null)`,建议统一风格。
3. **添加 JSDoc 注释**: 核心函数缺少文档注释。
---
### 8. 测试覆盖不完整
目前测试脚本仅覆盖:
- ✅ 基础连接和节点操作 (`run_tests.js`)
- ✅ 文件搜索 (`test_find_file.js`)
- ✅ 撤销/重做 (`test_undo.js`)
- ✅ 粒子系统 (`test_vfx.js`)
未覆盖的功能:
-`manage_material` / `manage_texture`
-`scene_management` / `prefab_management`
-`apply_text_edits`
-`batch_execute`
---
## 📌 修复优先级排序
| 序号 | 问题 | 优先级 | 预计工时 |
|------|------|--------|----------|
| 1 | 删除重复函数 | 🔴 高 | 0.5h |
| 2 | 合并 `get-server-state` | 🔴 高 | 0.5h |
| 3 | TypeScript 验证 | 🟡 中 | 2h |
| 4 | 菜单执行增强 | 🟡 中 | 1h |
| 5 | 颜色 Undo 支持 | 🟡 中 | 1h |
| 6 | 测试覆盖扩展 | 🟢 低 | 4h |
| 7 | 代码拆分重构 | 🟢 低 | 8h |
---
## 🚀 下一步行动建议
1. **立即修复**: 删除 `main.js` 中的重复函数定义
2. **短期**: 完善 TypeScript 验证和测试覆盖
3. **中期**: 优化代码结构,拆分大文件
4. **长期**: 实现 Git 集成,准备插件商店发布
---
## 📝 附录:已实现功能清单
| 工具名称 | 状态 | 说明 |
|---------|------|------|
| `get_selected_node` | ✅ | 获取选中节点 |
| `set_node_name` | ✅ | 修改节点名称 |
| `save_scene` | ✅ | 保存场景 |
| `get_scene_hierarchy` | ✅ | 获取场景层级 |
| `update_node_transform` | ✅ | 修改节点变换 |
| `create_scene` | ✅ | 创建场景 |
| `create_prefab` | ✅ | 创建预制体 |
| `open_scene` | ✅ | 打开场景 |
| `create_node` | ✅ | 创建节点 |
| `manage_components` | ✅ | 管理组件 |
| `manage_script` | ✅ | 管理脚本 |
| `batch_execute` | ✅ | 批量执行 |
| `manage_asset` | ✅ | 管理资源 |
| `scene_management` | ✅ | 场景管理 |
| `prefab_management` | ✅ | 预制体管理 |
| `manage_editor` | ✅ | 编辑器管理 |
| `find_gameobjects` | ✅ | 查找节点 |
| `manage_material` | ⚠️ | 部分实现 |
| `manage_texture` | ⚠️ | 部分实现 |
| `execute_menu_item` | ⚠️ | 部分实现 |
| `apply_text_edits` | ✅ | 文本编辑 |
| `read_console` | ✅ | 读取控制台 |
| `validate_script` | ⚠️ | 仅支持 JS |
| `find_in_file` | ✅ | 文件搜索 |
| `manage_undo` | ✅ | 撤销/重做 |
| `manage_vfx` | ✅ | 粒子系统 |

View File

@@ -2,6 +2,17 @@
本文档记录了 MCP Bridge 插件的完整开发流程,包括核心架构设计、功能实现、测试与调试等各个阶段。 本文档记录了 MCP Bridge 插件的完整开发流程,包括核心架构设计、功能实现、测试与调试等各个阶段。
## 0. 项目开发规范 (Project Rules)
> [!IMPORTANT]
> 所有贡献者必须严格遵守以下规则:
1. **语言与沟通**: 所有注释、文档、计划、任务及 AI 回复必须使用 **简体中文 (Simplified Chinese)**
2. **技术栈**: 新脚本必须使用 **TypeScript** (`.ts`)。禁止创建新的 `.js` 文件 (除非是构建脚本或测试配置)。
3. **文档**: 所有修改或创建的脚本必须包含详细的 JSDoc 格式注释。
4. **架构**: 严禁引入新的架构模式或重型外部库。必须复用现有的 Cocos Creator 管理器和工具类。
5. **隔离原则**: 保持 `main.js` (主进程) 与 `scene-script.js` (渲染进程) 的严格职责分离。即使看似方便,也不要在 `main.js` 中直接操作场景节点对象。
## 1. 项目初始化 ## 1. 项目初始化
### 1.1 目录结构搭建 ### 1.1 目录结构搭建
@@ -413,232 +424,88 @@ manageAsset(args, callback) {
- 防止路径遍历攻击 - 防止路径遍历攻击
- 限制服务访问范围 - 限制服务访问范围
## 11. 开发状态 ## 11. 开发路线图 (Roadmap)
### 11.1 已完成的任务 ### 11.1 第三阶段开发计划(已完成)
#### 第一阶段 | 任务 | 状态 | 描述 |
- ✅ HTTP 服务接口实现 |------|------|------|
- ✅ 场景节点操作工具 | 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 |
- ✅ 资源管理工具 | 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 |
- ✅ 组件管理工具 | 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 |
- ✅ 脚本管理工具(默认创建 TypeScript 脚本) | 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 |
- ✅ 批处理执行工具 | 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 |
- ✅ 资产管理工具 | 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 |
- ✅ 实时日志系统 | 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 |
- ✅ 自动启动功能
- ✅ 面板界面实现
####阶段 ### 11.2阶段开发计划(已完成)
- ✅ 场景管理工具scene_management
- 创建场景
- 删除场景
- 复制场景
- 获取场景信息
- ✅ 预制体管理工具prefab_management
- 创建预制体
- 更新预制体
- 实例化预制体
- 获取预制体信息
- ✅ 面板布局优化
- 响应式设计
- 滚动条支持
- 小窗口适配
- ✅ 移除旧工具
- 删除了 create_scene 工具(功能整合到 scene_management
- 删除了 create_prefab 工具(功能整合到 prefab_management
- ✅ README.md 文档更新
- ✅ 代码提交到本地仓库
#### 第三阶段 | 任务 | 状态 | 描述 |
- ✅ 编辑器管理工具manage_editor |------|------|------|
- 获取选中对象 | 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 |
- 设置选中状态 | 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 |
- 刷新编辑器
- ✅ 游戏对象查找工具find_gameobjects
- 根据名称、标签、组件、激活状态查找节点
- 支持递归和非递归查找
- ✅ 材质管理工具manage_material
- 创建、删除、获取材质信息
- ✅ 纹理管理工具manage_texture
- 创建、删除、获取纹理信息
- ✅ 菜单项执行工具execute_menu_item
- 执行 Cocos Creator 编辑器菜单项
- ✅ 代码编辑增强工具apply_text_edits
- 支持插入、删除、替换文本操作
- ✅ 控制台读取工具read_console
- 读取编辑器控制台输出
- 支持按类型过滤和限制输出数量
- ✅ 脚本验证工具validate_script
- 验证脚本语法正确性
- ✅ 面板工具说明功能
- 添加工具说明框
- 显示详细的工具描述和参数说明
### 11.2 未完成的任务 ### 11.3 差异填补阶段Gap Filling- 已完成
- ❌ 代码推送到远程仓库(认证错误) | 任务 | 状态 | 描述 |
- ❌ 测试用例编写 |------|------|------|
- ❌ 性能优化 | 全局文件搜索 | ✅ 完成 | 实现 find_in_file 工具 |
- ❌ 错误处理增强 | 撤销/重做支持 | ✅ 完成 | 实现 manage_undo 工具,并重构核心操作支持撤销 |
- ❌ 安全配置 | 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 |
### 11.3 后续需要完成的任务 ### 11.4 第六阶段:可靠性与体验优化(已完成)
#### 高优先级 | 任务 | 状态 | 描述 |
1. **代码推送**:解决远程仓库认证问题,完成代码推送 |------|------|------|
2. **测试用例**:为核心工具编写测试用例 | IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) |
3. **安全配置**:添加 IP 白名单和认证机制 | 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) |
| 组件智能解析修复 | ✅ 完成 | 修复组件属性赋值时的 UUID 类型转换,支持压缩 UUID 及自定义组件 (`$_$ctor`) |
#### 中优先级 ### 11.5 第七阶段开发计划(未来规划)
1. **性能优化**:优化 HTTP 服务响应速度,改进批处理执行效率
2. **错误处理**:增强错误处理和恢复机制,提高插件稳定性
3. **文档完善**:添加更详细的 API 文档和使用示例,包括新工具的详细说明
#### 低优先级 | 任务 | 优先级 | 预计时间 | 描述 |
1. **工具扩展**:添加更多高级工具,如动画管理、物理系统管理等 |------|--------|----------|------|
2. **界面美化**:进一步优化面板界面,提升用户体验 | 插件发布 | 高 | 1 天 | 准备发布,提交到 Cocos 插件商店 |
3. **国际化**:支持多语言,方便国际用户使用 | 文档完善 | 中 | 2 天 | 完善 API 文档 ("Getting Started" 教程) |
4. **插件发布**:准备插件发布到 Cocos 插件商店 | 界面美化 | 低 | 2 天 | 优化面板 UI 体检 |
5. **版本兼容**:适配更多 Cocos Creator 版本 | 国际化支持 | 低 | 2 天 | 添加多语言 (i18n) 支持 |
| 工具扩展 | 低 | 3 天 | 添加更多高级工具 |
### 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. Unity-MCP 对比分析
### 12.1 Unity-MCP 功能特性 ### 12.1 功能差距 (Gap Analysis)
Unity-MCP 提供了以下核心功能 通过与 Unity-MCP 对比Cocos-MCP 已实现绝大多数核心功能
- **资产管理**:管理各种 Unity 资源 | 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 |
- **编辑器管理**:控制 Unity 编辑器功能 |---------|---------------|---------------|------|
- **游戏对象管理**:创建、修改、查找游戏对象 | 编辑器管理 | manage_editor | ✅ 已实现 | |
- **组件管理**:添加、移除、修改组件 | 游戏对象管理 | find_gameobjects | ✅ 已实现 | |
- **材质管理**:创建和修改材质 | 材质管理 | manage_material | ✅ 已实现 | |
- **预制体管理**:管理预制体资源 | 纹理管理 | manage_texture | ✅ 已实现 | |
- **场景管理**:创建、保存、加载场景 | 代码编辑 | apply_text_edits | ✅ 已实现 | |
- **脚本管理**:创建、修改脚本 | 全局搜索 | find_in_file | ✅ 已实现 | |
- **ScriptableObject 管理**:管理配置文件 | 控制台 | read_console | ✅ 已实现 | |
- **着色器管理**:管理着色器资源 | 菜单执行 | execute_menu_item | ✅ 已实现 | |
- **VFX 管理**:管理视觉效果 | 脚本验证 | validate_script | ✅ 已实现 | |
- **纹理管理**:管理纹理资源 | 撤销/重做 | undo/redo | ✅ 已实现 | |
- **批处理执行**:批量执行多个操作 | VFX 管理 | manage_vfx | ✅ 已实现 | |
- **游戏对象查找**:根据条件查找游戏对象 | Git 集成 | get_sha | ❌ 未实现 | 低优先级 |
- **文件内容查找**:在文件中查找内容 | ScriptableObject | manage_so | ❌ 未实现 | 使用 AssetDB 替代 |
- **控制台读取**:读取 Unity 控制台输出
- **Unity 刷新**:刷新 Unity 编辑器
- **测试运行**:运行测试用例
- **获取测试任务**:获取测试任务信息
- **菜单项执行**:执行 Unity 菜单项
- **文本编辑应用**:应用文本编辑操作
- **脚本编辑应用**:应用脚本编辑操作
- **脚本验证**:验证脚本语法
- **创建脚本**:创建新脚本
- **删除脚本**:删除脚本文件
- **获取 SHA**:获取版本控制 SHA 值
### 12.2 Cocos-MCP 功能特性 ## 13. 风险评估
当前 Cocos-MCP 已实现的功能: ### 13.1 潜在风险
- **场景节点操作**:获取选中节点、设置节点名称、获取场景层级、更新节点变换、创建节点 | 风险 | 影响 | 缓解措施 |
- **组件管理**:添加、移除、获取组件 |------|------|----------|
- **资源管理**:创建、删除、移动资源 | 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API |
- **脚本管理**:创建、删除、读取、写入脚本(默认创建 TypeScript 脚本) | 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 |
- **批处理执行**:批量执行多个操作 | 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 |
- **资产管理**:管理各种资源文件 | 兼容性问题 | 多版本不兼容 | 测试主流版本 (2.4.x),提供兼容层 |
- **场景管理**:创建、删除、复制、获取场景信息
- **预制体管理**:创建、更新、实例化、获取预制体信息
- **面板界面**:提供主面板和工具测试面板
### 12.3 功能缺失对比 ## 14. 结论
| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 可实现性 | Cocos-MCP 插件的开发计划已顺利完成多个迭代阶段。目前插件实现了包括编辑器管理、场景操作、资源管理在内的全套核心功能并完成了针对性的可靠性加固IPC 通信、脚本时序、组件解析)。
|---------|---------------|---------------|--------|
| 编辑器管理 | 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 功能实现建议 插件功能已趋于稳定,后续工作重点将转向 **发布准备**、**文档体系建设** 以及 **用户体验优化**,力求为 Cocos Creator 开发者提供高质量的 AI 辅助开发工具。
#### 高优先级功能
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 辅助开发体验。

View File

@@ -1,630 +0,0 @@
# 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 生态系统做出贡献。

1305
IPC_MESSAGES.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -113,7 +113,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- `id`: 节点 UUID - `id`: 节点 UUID
- `x`, `y`: 坐标 - `x`, `y`: 坐标
- `scaleX`, `scaleY`: 缩放值 - `scaleX`, `scaleY`: 缩放值
- `color`: HEX 颜色代码(如 #FF0000 - `color`: HEX 颜色代码(如 #FF0000(支持撤销操作)
### 6. open_scene ### 6. open_scene
@@ -137,7 +137,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- `action`: 操作类型(`add`, `remove`, `get` - `action`: 操作类型(`add`, `remove`, `get`
- `componentType`: 组件类型,如 `cc.Sprite`(用于 `add` 操作) - `componentType`: 组件类型,如 `cc.Sprite`(用于 `add` 操作)
- `componentId`: 组件 ID用于 `remove` 操作) - `componentId`: 组件 ID用于 `remove` 操作)
- `properties`: 组件属性(用于 `add` 操作) - `properties`: 组件属性(用于 `add` 操作)。**智能特性**如果属性期望组件类型但传入节点UUID插件会自动查找匹配组件。
### 9. manage_script ### 9. manage_script
@@ -229,7 +229,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- **描述**: 执行菜单项 - **描述**: 执行菜单项
- **参数**: - **参数**:
- `menuPath`: 菜单项路径,如 `Assets/Create/Folder` - `menuPath`: 菜单项路径,如 `Assets/Create/Folder`。支持映射常用命令:`File/New Scene`, `File/Save Scene`, `Edit/Undo`, `Edit/Redo`, `Project/Build` 等。
### 19. apply_text_edits ### 19. apply_text_edits
@@ -255,6 +255,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- **描述**: 验证脚本 - **描述**: 验证脚本
- **参数**: - **参数**:
- `filePath`: 脚本路径,如 `db://assets/scripts/TestScript.ts` - `filePath`: 脚本路径,如 `db://assets/scripts/TestScript.ts`
- **注意**:对于 TypeScript 文件,仅进行基础语法结构检查,不进行完整编译验证。
### 22. find_in_file ### 22. find_in_file

141
dist/IpcManager.js vendored Normal file
View File

@@ -0,0 +1,141 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IpcManager = void 0;
// @ts-ignore
var fs = require('fs');
// @ts-ignore
var path = require('path');
/**
* IPC 消息管理器
* 负责解析 IPC 文档并执行消息测试
*/
var IpcManager = /** @class */ (function () {
function IpcManager() {
}
/**
* 获取所有 IPC 消息列表
* @returns 消息定义列表
*/
IpcManager.getIpcMessages = function () {
// 获取文档路径
// @ts-ignore
var docPath = Editor.url('packages://mcp-bridge/IPC_MESSAGES.md');
if (!fs.existsSync(docPath)) {
// @ts-ignore
Editor.error("[IPC Manager] Document not found: ".concat(docPath));
return [];
}
var content = fs.readFileSync(docPath, 'utf-8');
var messages = [];
// 正则匹配 ### `message-name`
var regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g;
var match;
while ((match = regex.exec(content)) !== null) {
var name_1 = match[1];
var body = match[2];
// 解析用途
var purposeMatch = body.match(/- \*\*用途\*\*: (.*)/);
var description = purposeMatch ? purposeMatch[1].trim() : "无描述";
// 解析参数
var paramsMatch = body.match(/- \*\*参数\*\*: (.*)/);
var params = paramsMatch ? paramsMatch[1].trim() : "无";
// 解析返回值
var returnMatch = body.match(/- \*\*返回值\*\*: (.*)/);
var returns = returnMatch ? returnMatch[1].trim() : "无";
// 解析类型
var typeMatch = body.match(/- \*\*类型\*\*: (.*)/);
var type = typeMatch ? typeMatch[1].trim() : "未定义";
// 解析状态
var statusMatch = body.match(/- \*\*状态\*\*: (.*)/);
var status_1 = statusMatch ? statusMatch[1].trim() : "未测试";
// 过滤掉广播事件和渲染进程监听的事件
if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") {
continue;
}
messages.push({
name: name_1,
description: description,
params: params,
returns: returns,
type: type,
status: status_1
});
}
return messages;
};
/**
* 测试发送 IPC 消息
* @param name 消息名称
* @param args 参数
* @returns Promise<any> 测试结果
*/
IpcManager.testIpcMessage = function (name_2) {
return __awaiter(this, arguments, void 0, function (name, args) {
if (args === void 0) { args = null; }
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve) {
// 简单防呆:防止执行危险操作
// 如果消息包含 "delete", "remove", "close", "stop" 且没有明确参数确认,则警告
// 但用户要求"快速验证",所以我们默认允许,但如果是无参调用可能有风险
// 这里我们尝试使用 Editor.Ipc.sendToMain 或 requestToMain
// @ts-ignore
// 简单的测试:只是发送看看是否报错。
// 对于 request 类型的消息,我们期望有回调
// Cocos Creator 2.4 API: Editor.Ipc.sendToMain(message, ...args)
try {
// @ts-ignore
if (Editor.Ipc.sendToMain) {
// @ts-ignore
Editor.Ipc.sendToMain(name, args);
resolve({ success: true, message: "Message sent (sendToMain)" });
}
else {
resolve({ success: false, message: "Editor.Ipc.sendToMain not available" });
}
}
catch (e) {
resolve({ success: false, message: "Error: ".concat(e.message) });
}
})];
});
});
};
return IpcManager;
}());
exports.IpcManager = IpcManager;

233
dist/IpcUi.js vendored Normal file
View File

@@ -0,0 +1,233 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IpcUi = void 0;
// @ts-ignore
var Editor = window.Editor;
var IpcUi = /** @class */ (function () {
function IpcUi(root) {
this.logArea = null;
this.ipcList = null;
this.allMessages = [];
this.filterSelect = null;
this.paramInput = null;
this.root = root;
this.bindEvents();
}
IpcUi.prototype.bindEvents = function () {
var _this = this;
var btnScan = this.root.querySelector("#btnScanIpc");
var btnTest = this.root.querySelector("#btnTestIpc");
var cbSelectAll = this.root.querySelector("#cbSelectAllIpc");
this.logArea = this.root.querySelector("#ipcLog");
this.ipcList = this.root.querySelector("#ipcList");
this.filterSelect = this.root.querySelector("#ipcFilter");
this.paramInput = this.root.querySelector("#ipcParams");
if (btnScan) {
btnScan.addEventListener("confirm", function () { return _this.scanMessages(); });
}
if (btnTest) {
btnTest.addEventListener("confirm", function () { return _this.testSelected(); });
}
if (cbSelectAll) {
cbSelectAll.addEventListener("change", function (e) { return _this.toggleAll(e.detail ? e.detail.value : (e.target.value === 'true' || e.target.checked)); });
}
if (this.filterSelect) {
this.filterSelect.addEventListener("change", function () { return _this.filterMessages(); });
}
};
IpcUi.prototype.scanMessages = function () {
var _this = this;
this.log("Scanning IPC messages...");
// @ts-ignore
Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", function (err, msgs) {
if (err) {
_this.log("Scan Error: ".concat(err));
return;
}
if (msgs) {
_this.allMessages = msgs;
_this.filterMessages();
_this.log("Found ".concat(msgs.length, " messages."));
}
else {
_this.log("No messages found.");
}
});
};
IpcUi.prototype.filterMessages = function () {
if (!this.allMessages)
return;
var filter = this.filterSelect ? this.filterSelect.value : "all";
var filtered = this.allMessages;
if (filter === "available") {
filtered = this.allMessages.filter(function (m) { return m.status === "可用"; });
}
else if (filter === "unavailable") {
filtered = this.allMessages.filter(function (m) { return m.status && m.status.includes("不可用"); });
}
else if (filter === "untested") {
filtered = this.allMessages.filter(function (m) { return !m.status || m.status === "未测试"; });
}
this.renderList(filtered);
};
IpcUi.prototype.renderList = function (msgs) {
var _this = this;
if (!this.ipcList)
return;
this.ipcList.innerHTML = "";
msgs.forEach(function (msg) {
var item = document.createElement("div");
item.className = "ipc-item";
item.style.padding = "4px";
item.style.borderBottom = "1px solid #333";
item.style.display = "flex";
item.style.alignItems = "center";
// Checkbox
var checkbox = document.createElement("ui-checkbox");
// @ts-ignore
checkbox.value = false;
checkbox.setAttribute("data-name", msg.name);
checkbox.style.marginRight = "8px";
// Content
var content = document.createElement("div");
content.style.flex = "1";
content.style.fontSize = "11px";
var statusColor = "#888"; // Untested
if (msg.status === "可用")
statusColor = "#4CAF50"; // Green
else if (msg.status && msg.status.includes("不可用"))
statusColor = "#F44336"; // Red
content.innerHTML = "\n <div style=\"display:flex; justify-content:space-between;\">\n <span style=\"color: #4CAF50; font-weight: bold;\">".concat(msg.name, "</span>\n <span style=\"color: ").concat(statusColor, "; font-size: 10px; border: 1px solid ").concat(statusColor, "; padding: 0 4px; border-radius: 4px;\">").concat(msg.status || "未测试", "</span>\n </div>\n <div style=\"color: #888;\">").concat(msg.description || "No desc", "</div>\n <div style=\"color: #666; font-size: 10px;\">Params: ").concat(msg.params || "None", "</div>\n ");
// Action Button
var btnRun = document.createElement("ui-button");
btnRun.innerText = "Run";
btnRun.className = "tiny";
btnRun.style.height = "20px";
btnRun.style.lineHeight = "20px";
btnRun.addEventListener("confirm", function () {
_this.runTest(msg.name);
});
item.appendChild(checkbox);
item.appendChild(content);
item.appendChild(btnRun);
_this.ipcList.appendChild(item);
});
};
IpcUi.prototype.testSelected = function () {
return __awaiter(this, void 0, void 0, function () {
var checkboxes, toTest, _i, toTest_1, name_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
toTest = [];
checkboxes.forEach(function (cb) {
// In Cocos 2.x, ui-checkbox value is boolean
if (cb.checked || cb.value === true) {
toTest.push(cb.getAttribute("data-name"));
}
});
if (toTest.length === 0) {
this.log("No messages selected.");
return [2 /*return*/];
}
this.log("Starting batch test for ".concat(toTest.length, " messages..."));
_i = 0, toTest_1 = toTest;
_a.label = 1;
case 1:
if (!(_i < toTest_1.length)) return [3 /*break*/, 4];
name_1 = toTest_1[_i];
return [4 /*yield*/, this.runTest(name_1)];
case 2:
_a.sent();
_a.label = 3;
case 3:
_i++;
return [3 /*break*/, 1];
case 4:
this.log("Batch test completed.");
return [2 /*return*/];
}
});
});
};
IpcUi.prototype.runTest = function (name) {
var _this = this;
return new Promise(function (resolve) {
var params = null;
if (_this.paramInput && _this.paramInput.value.trim()) {
try {
params = JSON.parse(_this.paramInput.value.trim());
}
catch (e) {
_this.log("[Error] Invalid JSON Params: ".concat(e));
resolve();
return;
}
}
_this.log("Testing: ".concat(name, " with params: ").concat(JSON.stringify(params), "..."));
// @ts-ignore
Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name: name, params: params }, function (err, result) {
if (err) {
_this.log("[".concat(name, "] Failed: ").concat(err));
}
else {
_this.log("[".concat(name, "] Success: ").concat(JSON.stringify(result)));
}
resolve();
});
});
};
IpcUi.prototype.toggleAll = function (checked) {
var checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
checkboxes.forEach(function (cb) {
cb.value = checked;
});
};
IpcUi.prototype.log = function (msg) {
if (this.logArea) {
// @ts-ignore
var time = new Date().toLocaleTimeString();
this.logArea.value += "[".concat(time, "] ").concat(msg, "\n");
this.logArea.scrollTop = this.logArea.scrollHeight;
}
};
return IpcUi;
}());
exports.IpcUi = IpcUi;

109
main.js
View File

@@ -1,4 +1,5 @@
"use strict"; "use strict";
const { IpcManager } = require("./dist/IpcManager");
const http = require("http"); const http = require("http");
const path = require("path"); const path = require("path");
@@ -12,7 +13,6 @@ let serverConfig = {
active: false, active: false,
}; };
// 封装日志函数,同时发送给面板和编辑器控制台
// 封装日志函数,同时发送给面板和编辑器控制台 // 封装日志函数,同时发送给面板和编辑器控制台
function addLog(type, message) { function addLog(type, message) {
const logEntry = { const logEntry = {
@@ -182,17 +182,17 @@ const getToolsList = () => {
type: "object", type: "object",
properties: { properties: {
nodeId: { type: "string", description: "节点 UUID" }, nodeId: { type: "string", description: "节点 UUID" },
action: { type: "string", enum: ["add", "remove", "get"], description: "操作类型" }, action: { type: "string", enum: ["add", "remove", "update", "get"], description: "操作类型 (add: 添加组件, remove: 移除组件, update: 更新组件属性, get: 获取组件列表)" },
componentType: { type: "string", description: "组件类型,如 cc.Sprite" }, componentType: { type: "string", description: "组件类型,如 cc.Sprite (add/update 操作需要)" },
componentId: { type: "string", description: "组件 ID (用于 remove 操作)" }, componentId: { type: "string", description: "组件 ID (remove/update 操作可选)" },
properties: { type: "object", description: "组件属性 (用于 add 操作)" }, properties: { type: "object", description: "组件属性 (add/update 操作使用). 支持智能解析: 如果属性类型是组件但提供了节点UUID会自动查找对应组件。" },
}, },
required: ["nodeId", "action"], required: ["nodeId", "action"],
}, },
}, },
{ {
name: "manage_script", name: "manage_script",
description: "管理脚本文件", description: "管理脚本文件。注意:创建或修改脚本后,编辑器需要时间进行编译(通常几秒钟)。新脚本在编译完成前无法作为组件添加到节点。建议在 create 后调用 refresh_editor或等待一段时间后再使用 manage_components。",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
@@ -914,7 +914,18 @@ export default class NewScript extends cc.Component {
update (dt) {} update (dt) {}
}`, }`,
(err) => { (err) => {
callback(err, err ? null : `Script created at ${scriptPath}`); if (err) {
callback(err);
} else {
// 【关键修复】创建脚本后,必须刷新 AssetDB 并等待完成,
// 否则后续立即挂载脚本的操作(manage_components)会因找不到脚本 UUID 而失败。
Editor.assetdb.refresh(scriptPath, (refreshErr) => {
if (refreshErr) {
addLog("warn", `Refresh failed after script creation: ${refreshErr}`);
}
callback(null, `Script created at ${scriptPath}`);
});
}
}, },
); );
break; break;
@@ -1460,19 +1471,44 @@ export default class NewScript extends cc.Component {
} }
addLog("info", `Executing Menu Item: ${menuPath}`); addLog("info", `Executing Menu Item: ${menuPath}`);
// 尝试通过 IPC 触发菜单 (Cocos 2.x 常用方式) // 菜单项映射表 (Cocos Creator 2.4.x IPC)
// 如果是保存场景,直接使用对应的 stash-and-save IPC // 参考: IPC_MESSAGES.md
if (menuPath === 'File/Save Scene') { const menuMap = {
Editor.Ipc.sendToMain("scene:stash-and-save"); 'File/New Scene': 'scene:new-scene',
'File/Save Scene': 'scene:stash-and-save',
'File/Save': 'scene:stash-and-save', // 别名
'Edit/Undo': 'scene:undo',
'Edit/Redo': 'scene:redo',
'Node/Create Empty Node': 'scene:create-node-by-classid', // 简化的映射,通常需要参数
'Project/Build': 'app:build-project',
};
if (menuMap[menuPath]) {
const ipcMsg = menuMap[menuPath];
try {
Editor.Ipc.sendToMain(ipcMsg);
callback(null, `Menu action triggered: ${menuPath} -> ${ipcMsg}`);
} catch (err) {
callback(`Failed to execute IPC ${ipcMsg}: ${err.message}`);
}
} else { } else {
// 通用尝试 (可能不工作,取决于编辑器版本) // 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效)
// Editor.Ipc.sendToMain('ui:menu-click', menuPath); // 或者直接返回不支持的警告
// 兜底:仅记录日志,暂不支持通用菜单点击 addLog("warn", `Menu item '${menuPath}' not found in supported map. Trying legacy fallback.`);
addLog("warn", "Generic menu execution partial support.");
// 尝试通用调用
try {
// 注意Cocos Creator 2.x 的 menu:click 通常需要 Electron 菜单 ID而不只是路径
// 这里做个尽力而为的尝试
Editor.Ipc.sendToMain('menu:click', menuPath);
callback(null, `Generic menu action sent: ${menuPath} (Success guaranteed only for supported items)`);
} catch (e) {
callback(`Failed to execute menu item: ${menuPath}`);
}
} }
callback(null, `Menu action triggered: ${menuPath}`);
}, },
// 验证脚本
// 验证脚本 // 验证脚本
validateScript(args, callback) { validateScript(args, callback) {
const { filePath } = args; const { filePath } = args;
@@ -1493,25 +1529,56 @@ export default class NewScript extends cc.Component {
try { try {
const content = fs.readFileSync(fspath, "utf-8"); const content = fs.readFileSync(fspath, "utf-8");
// 对于 JavaScript 脚本,使用 eval 进行简单验证 // 检查空文件
if (!content || content.trim().length === 0) {
return callback(null, { valid: false, message: "Script is empty" });
}
// 对于 JavaScript 脚本,使用 Function 构造器进行语法验证
if (filePath.endsWith(".js")) { if (filePath.endsWith(".js")) {
// 包装在函数中以避免变量污染
const wrapper = `(function() { ${content} })`; const wrapper = `(function() { ${content} })`;
try { try {
new Function(wrapper); // 使用 Function 构造器比 direct eval稍微安全一点点虽在这个场景下差别不大 new Function(wrapper);
callback(null, { valid: true, message: "JavaScript syntax is valid" });
} catch (syntaxErr) { } catch (syntaxErr) {
return callback(null, { valid: false, message: syntaxErr.message }); return callback(null, { valid: false, message: syntaxErr.message });
} }
} }
// 对于 TypeScript暂不进行复杂编译检查,仅确保文件可读 // 对于 TypeScript由于没有内置 TS 编译器,我们进行基础的"防呆"检查
// 并明确告知用户无法进行完整编译验证
else if (filePath.endsWith(".ts")) {
// 简单的正则表达式检查:是否有非法字符或明显错误结构 (示例)
// 这里暂时只做简单的括号匹配检查或直接通过,但给出一个 Warning
callback(null, { valid: true, message: "Script syntax is valid" }); // 检查是否有 class 定义 (简单的heuristic)
if (!content.includes('class ') && !content.includes('interface ') && !content.includes('enum ') && !content.includes('export ')) {
return callback(null, { valid: true, message: "Warning: TypeScript file seems to lack standard definitions (class/interface), but basic syntax check is skipped due to missing compiler." });
}
callback(null, { valid: true, message: "TypeScript basic check passed. (Full compilation validation requires editor build)" });
} else {
callback(null, { valid: true, message: "Unknown script type, validation skipped." });
}
} catch (err) { } catch (err) {
callback(null, { valid: false, message: `Read Error: ${err.message}` }); callback(null, { valid: false, message: `Read Error: ${err.message}` });
} }
}, },
// 暴露给 MCP 或面板的 API 封装 // 暴露给 MCP 或面板的 API 封装
messages: { messages: {
"scan-ipc-messages"(event) {
try {
const msgs = IpcManager.getIpcMessages();
if (event.reply) event.reply(null, msgs);
} catch (e) {
if (event.reply) event.reply(e.message);
}
},
"test-ipc-message"(event, args) {
const { name, params } = args;
IpcManager.testIpcMessage(name, params).then((result) => {
if (event.reply) event.reply(null, result);
});
},
"open-test-panel"() { "open-test-panel"() {
Editor.Panel.open("mcp-bridge"); Editor.Panel.open("mcp-bridge");
}, },

View File

@@ -2,6 +2,7 @@
<div class="tabs"> <div class="tabs">
<ui-button id="tabMain" class="tab-button active">Main</ui-button> <ui-button id="tabMain" class="tab-button active">Main</ui-button>
<ui-button id="tabTest" class="tab-button">Tool Test</ui-button> <ui-button id="tabTest" class="tab-button">Tool Test</ui-button>
<ui-button id="tabIpc" class="tab-button">IPC Test</ui-button>
</div> </div>
<div id="panelMain" class="tab-content active"> <div id="panelMain" class="tab-content active">
@@ -22,6 +23,7 @@
</div> </div>
<div id="panelTest" class="tab-content"> <div id="panelTest" class="tab-content">
<!-- ... existing content ... -->
<div class="test-layout"> <div class="test-layout">
<div class="left-panel" id="testLeftPanel"> <div class="left-panel" id="testLeftPanel">
<div class="form-item"> <div class="form-item">
@@ -57,47 +59,248 @@
</div> </div>
</div> </div>
</div> </div>
<div id="panelIpc" class="tab-content">
<div class="toolbar">
<ui-button id="btnScanIpc">Scan Messages</ui-button>
<select id="ipcFilter"
style="margin-left: 5px; background: #333; color: #ccc; border: 1px solid #555; height: 25px;">
<option value="all">Check All</option>
<option value="available">Show Available</option>
<option value="unavailable">Show Unavailable</option>
<option value="untested">Show Untested</option>
</select>
<div class="spacer"></div>
<ui-button id="btnTestIpc" class="green">Test Selected</ui-button>
</div>
<div class="ipc-container" style="display:flex; flex:1; flex-direction:column; min-height:0;">
<div class="ipc-list-header" style="padding: 5px; background: #222; border-bottom: 1px solid #444;">
<ui-checkbox id="cbSelectAllIpc">Select All/None</ui-checkbox>
</div>
<div id="ipcList" class="ipc-list"
style="flex:1; overflow-y:auto; background: #1e1e1e; border: 1px solid #444;"></div>
<div style="padding: 5px; border-top: 1px solid #444; display: flex; flex-direction: column;">
<label>Parameters (JSON):</label>
<textarea id="ipcParams" placeholder='e.g. {"uuid": "..."}'
style="height: 50px; width: 100%; box-sizing: border-box; background: #222; color: #ccc; border: 1px solid #444; margin-bottom: 5px;"></textarea>
</div>
<div
style="height: 100px; display:flex; flex-direction:column; border-top: 1px solid #444; padding-top: 5px;">
<label>Test Log:</label>
<textarea id="ipcLog" readonly style="flex:1; font-family:monospace; font-size:10px;"></textarea>
</div>
</div>
</div>
</div> </div>
<style> <style>
:host { height: 100%; display: flex; background-color: #2d2d2d; overflow: hidden; } :host {
.mcp-container { display: flex; flex-direction: column; width: 100%; height: 100%; padding: 5px; box-sizing: border-box; } height: 100%;
.tabs { display: flex; border-bottom: 1px solid #444; margin-bottom: 5px; flex-shrink: 0; } display: flex;
.tab-button { padding: 4px 12px; margin-right: 2px; background: #333; } background-color: #2d2d2d;
.tab-button.active { background: #4CAF50; color: white; } overflow: hidden;
.tab-content { display: none; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; } }
.tab-content.active { display: flex; }
.mcp-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 5px;
box-sizing: border-box;
}
.tabs {
display: flex;
border-bottom: 1px solid #444;
margin-bottom: 5px;
flex-shrink: 0;
}
.tab-button {
padding: 4px 12px;
margin-right: 2px;
background: #333;
}
.tab-button.active {
background: #4CAF50;
color: white;
}
.tab-content {
display: none;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.tab-content.active {
display: flex;
}
/* Main Panel */ /* Main Panel */
.toolbar { display: flex; align-items: center; padding: 5px 0; gap: 5px; flex-shrink: 0; } .toolbar {
.spacer { flex: 1; } display: flex;
.log-view { flex: 1; background: #1a1a1a; margin-top: 5px; overflow-y: auto; padding: 8px; font-family: monospace; font-size: 11px; -webkit-user-select: text; min-height: 0; } align-items: center;
.log-item { border-left: 4px solid #555; padding-left: 8px; margin-bottom: 3px; white-space: pre-wrap; word-break: break-all; } padding: 5px 0;
.log-item.info { border-left-color: #61afef; color: #abb2bf; } gap: 5px;
.log-item.success { border-left-color: #98c379; color: #98c379; } flex-shrink: 0;
.log-item.warn { border-left-color: #e5c07b; color: #e5c07b; } }
.log-item.error { border-left-color: #e06c75; color: #e06c75; }
.log-item.mcp { border-left-color: #c678dd; color: #d19a66; background: rgba(198, 120, 221, 0.05); } .spacer {
.time { color: #5c6370; margin-right: 8px; } flex: 1;
}
.log-view {
flex: 1;
background: #1a1a1a;
margin-top: 5px;
overflow-y: auto;
padding: 8px;
font-family: monospace;
font-size: 11px;
-webkit-user-select: text;
min-height: 0;
}
.log-item {
border-left: 4px solid #555;
padding-left: 8px;
margin-bottom: 3px;
white-space: pre-wrap;
word-break: break-all;
}
.log-item.info {
border-left-color: #61afef;
color: #abb2bf;
}
.log-item.success {
border-left-color: #98c379;
color: #98c379;
}
.log-item.warn {
border-left-color: #e5c07b;
color: #e5c07b;
}
.log-item.error {
border-left-color: #e06c75;
color: #e06c75;
}
.log-item.mcp {
border-left-color: #c678dd;
color: #d19a66;
background: rgba(198, 120, 221, 0.05);
}
.time {
color: #5c6370;
margin-right: 8px;
}
/* Test Panel */ /* Test Panel */
.test-layout { display: flex; flex: 1; min-height: 0; } .test-layout {
.left-panel { width: 250px; min-width: 150px; max-width: 500px; display: flex; flex-direction: column; flex-shrink: 0; min-height: 0; } display: flex;
.resizer { width: 6px; cursor: col-resize; background: #1a1a1a; flex-shrink: 0; } flex: 1;
.resizer:hover { background: #4CAF50; } min-height: 0;
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; padding-left: 5px; } }
.tools-list { flex: 1; background: #222; border: 1px solid #444; overflow-y: auto; margin-top: 5px; }
.tool-item { padding: 6px; border-bottom: 1px solid #333; cursor: pointer; font-size: 11px; } .left-panel {
.tool-item:hover { background: #444; } width: 250px;
.flex-v { display: flex; flex-direction: column; flex: 1; min-height: 0; } min-width: 150px;
textarea { width: 100%; background: #222; color: #ccc; border: 1px solid #444; padding: 5px; font-family: monospace; resize: none; } max-width: 500px;
#toolParams { height: 120px; flex-shrink: 0; } display: flex;
#resultContent { flex: 1; } flex-direction: column;
.button-group { display: flex; gap: 5px; padding: 5px 0; } flex-shrink: 0;
label { font-size: 11px; color: #888; margin: 4px 0; } min-height: 0;
}
.resizer {
width: 6px;
cursor: col-resize;
background: #1a1a1a;
flex-shrink: 0;
}
.resizer:hover {
background: #4CAF50;
}
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
padding-left: 5px;
}
.tools-list {
flex: 1;
background: #222;
border: 1px solid #444;
overflow-y: auto;
margin-top: 5px;
}
.tool-item {
padding: 6px;
border-bottom: 1px solid #333;
cursor: pointer;
font-size: 11px;
}
.tool-item:hover {
background: #444;
}
.flex-v {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
textarea {
width: 100%;
background: #222;
color: #ccc;
border: 1px solid #444;
padding: 5px;
font-family: monospace;
resize: none;
}
#toolParams {
height: 120px;
flex-shrink: 0;
}
#resultContent {
flex: 1;
}
.button-group {
display: flex;
gap: 5px;
padding: 5px 0;
}
label {
font-size: 11px;
color: #888;
margin: 4px 0;
}
.tool-description { .tool-description {
margin-bottom: 10px; margin-bottom: 10px;
} }
.description-box { .description-box {
background: #222; background: #222;
color: #ccc; color: #ccc;

View File

@@ -1,5 +1,6 @@
"use strict"; "use strict";
const fs = require("fs"); const fs = require("fs");
const { IpcUi } = require("../dist/IpcUi");
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"),
@@ -24,8 +25,10 @@ Editor.Panel.extend({
logView: root.querySelector("#logConsole"), logView: root.querySelector("#logConsole"),
tabMain: root.querySelector("#tabMain"), tabMain: root.querySelector("#tabMain"),
tabTest: root.querySelector("#tabTest"), tabTest: root.querySelector("#tabTest"),
tabIpc: root.querySelector("#tabIpc"),
panelMain: root.querySelector("#panelMain"), panelMain: root.querySelector("#panelMain"),
panelTest: root.querySelector("#panelTest"), panelTest: root.querySelector("#panelTest"),
panelIpc: root.querySelector("#panelIpc"),
toolName: root.querySelector("#toolName"), toolName: root.querySelector("#toolName"),
toolParams: root.querySelector("#toolParams"), toolParams: root.querySelector("#toolParams"),
toolDescription: root.querySelector("#toolDescription"), toolDescription: root.querySelector("#toolDescription"),
@@ -49,20 +52,35 @@ Editor.Panel.extend({
} }
}); });
// Initialize IPC UI
new IpcUi(root);
// 2. 标签切换 // 2. 标签切换
els.tabMain.addEventListener("confirm", () => { els.tabMain.addEventListener("confirm", () => {
els.tabMain.classList.add("active"); els.tabMain.classList.add("active");
els.tabTest.classList.remove("active"); els.tabTest.classList.remove("active");
els.tabIpc.classList.remove("active");
els.panelMain.classList.add("active"); els.panelMain.classList.add("active");
els.panelTest.classList.remove("active"); els.panelTest.classList.remove("active");
els.panelIpc.classList.remove("active");
}); });
els.tabTest.addEventListener("confirm", () => { els.tabTest.addEventListener("confirm", () => {
els.tabTest.classList.add("active"); els.tabTest.classList.add("active");
els.tabMain.classList.remove("active"); els.tabMain.classList.remove("active");
els.tabIpc.classList.remove("active");
els.panelTest.classList.add("active"); els.panelTest.classList.add("active");
els.panelMain.classList.remove("active"); els.panelMain.classList.remove("active");
els.panelIpc.classList.remove("active");
this.fetchTools(els); this.fetchTools(els);
}); });
els.tabIpc.addEventListener("confirm", () => {
els.tabIpc.classList.add("active");
els.tabMain.classList.remove("active");
els.tabTest.classList.remove("active");
els.panelIpc.classList.add("active");
els.panelMain.classList.remove("active");
els.panelTest.classList.remove("active");
});
// 3. 基础功能 // 3. 基础功能
els.btnToggle.addEventListener("confirm", () => { els.btnToggle.addEventListener("confirm", () => {

File diff suppressed because it is too large Load Diff

107
src/IpcManager.ts Normal file
View File

@@ -0,0 +1,107 @@
// @ts-ignore
const fs = require('fs');
// @ts-ignore
const path = require('path');
/**
* IPC 消息管理器
* 负责解析 IPC 文档并执行消息测试
*/
export class IpcManager {
/**
* 获取所有 IPC 消息列表
* @returns 消息定义列表
*/
public static getIpcMessages(): any[] {
// 获取文档路径
// @ts-ignore
const docPath = Editor.url('packages://mcp-bridge/IPC_MESSAGES.md');
if (!fs.existsSync(docPath)) {
// @ts-ignore
Editor.error(`[IPC Manager] Document not found: ${docPath}`);
return [];
}
const content = fs.readFileSync(docPath, 'utf-8');
const messages: any[] = [];
// 正则匹配 ### `message-name`
const regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g;
let match;
while ((match = regex.exec(content)) !== null) {
const name = match[1];
const body = match[2];
// 解析用途
const purposeMatch = body.match(/- \*\*用途\*\*: (.*)/);
const description = purposeMatch ? purposeMatch[1].trim() : "无描述";
// 解析参数
const paramsMatch = body.match(/- \*\*参数\*\*: (.*)/);
const params = paramsMatch ? paramsMatch[1].trim() : "无";
// 解析返回值
const returnMatch = body.match(/- \*\*返回值\*\*: (.*)/);
const returns = returnMatch ? returnMatch[1].trim() : "无";
// 解析类型
const typeMatch = body.match(/- \*\*类型\*\*: (.*)/);
const type = typeMatch ? typeMatch[1].trim() : "未定义";
// 解析状态
const statusMatch = body.match(/- \*\*状态\*\*: (.*)/);
const status = statusMatch ? statusMatch[1].trim() : "未测试";
// 过滤掉广播事件和渲染进程监听的事件
if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") {
continue;
}
messages.push({
name,
description,
params,
returns,
type,
status
});
}
return messages;
}
/**
* 测试发送 IPC 消息
* @param name 消息名称
* @param args 参数
* @returns Promise<any> 测试结果
*/
public static async testIpcMessage(name: string, args: any = null): Promise<any> {
return new Promise((resolve) => {
// 简单防呆:防止执行危险操作
// 如果消息包含 "delete", "remove", "close", "stop" 且没有明确参数确认,则警告
// 但用户要求"快速验证",所以我们默认允许,但如果是无参调用可能有风险
// 这里我们尝试使用 Editor.Ipc.sendToMain 或 requestToMain
// @ts-ignore
// 简单的测试:只是发送看看是否报错。
// 对于 request 类型的消息,我们期望有回调
// Cocos Creator 2.4 API: Editor.Ipc.sendToMain(message, ...args)
try {
// @ts-ignore
if (Editor.Ipc.sendToMain) {
// @ts-ignore
Editor.Ipc.sendToMain(name, args);
resolve({ success: true, message: "Message sent (sendToMain)" });
} else {
resolve({ success: false, message: "Editor.Ipc.sendToMain not available" });
}
} catch (e: any) {
resolve({ success: false, message: `Error: ${e.message}` });
}
});
}
}

192
src/IpcUi.ts Normal file
View File

@@ -0,0 +1,192 @@
// @ts-ignore
const Editor = window.Editor;
export class IpcUi {
private root: ShadowRoot;
private logArea: HTMLTextAreaElement | null = null;
private ipcList: HTMLElement | null = null;
private allMessages: any[] = [];
private filterSelect: HTMLSelectElement | null = null;
private paramInput: HTMLTextAreaElement | null = null;
constructor(root: ShadowRoot) {
this.root = root;
this.bindEvents();
}
private bindEvents() {
const btnScan = this.root.querySelector("#btnScanIpc");
const btnTest = this.root.querySelector("#btnTestIpc");
const cbSelectAll = this.root.querySelector("#cbSelectAllIpc");
this.logArea = this.root.querySelector("#ipcLog") as HTMLTextAreaElement;
this.ipcList = this.root.querySelector("#ipcList") as HTMLElement;
this.filterSelect = this.root.querySelector("#ipcFilter") as HTMLSelectElement;
this.paramInput = this.root.querySelector("#ipcParams") as HTMLTextAreaElement;
if (btnScan) {
btnScan.addEventListener("confirm", () => this.scanMessages());
}
if (btnTest) {
btnTest.addEventListener("confirm", () => this.testSelected());
}
if (cbSelectAll) {
cbSelectAll.addEventListener("change", (e: any) => this.toggleAll(e.detail ? e.detail.value : (e.target.value === 'true' || e.target.checked)));
}
if (this.filterSelect) {
this.filterSelect.addEventListener("change", () => this.filterMessages());
}
}
private scanMessages() {
this.log("Scanning IPC messages...");
// @ts-ignore
Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", (err: any, msgs: any[]) => {
if (err) {
this.log(`Scan Error: ${err}`);
return;
}
if (msgs) {
this.allMessages = msgs;
this.filterMessages();
this.log(`Found ${msgs.length} messages.`);
} else {
this.log("No messages found.");
}
});
}
private filterMessages() {
if (!this.allMessages) return;
const filter = this.filterSelect ? this.filterSelect.value : "all";
let filtered = this.allMessages;
if (filter === "available") {
filtered = this.allMessages.filter(m => m.status === "可用");
} else if (filter === "unavailable") {
filtered = this.allMessages.filter(m => m.status && m.status.includes("不可用"));
} else if (filter === "untested") {
filtered = this.allMessages.filter(m => !m.status || m.status === "未测试");
}
this.renderList(filtered);
}
private renderList(msgs: any[]) {
if (!this.ipcList) return;
this.ipcList.innerHTML = "";
msgs.forEach(msg => {
const item = document.createElement("div");
item.className = "ipc-item";
item.style.padding = "4px";
item.style.borderBottom = "1px solid #333";
item.style.display = "flex";
item.style.alignItems = "center";
// Checkbox
const checkbox = document.createElement("ui-checkbox");
// @ts-ignore
checkbox.value = false;
checkbox.setAttribute("data-name", msg.name);
checkbox.style.marginRight = "8px";
// Content
const content = document.createElement("div");
content.style.flex = "1";
content.style.fontSize = "11px";
let statusColor = "#888"; // Untested
if (msg.status === "可用") statusColor = "#4CAF50"; // Green
else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // Red
content.innerHTML = `
<div style="display:flex; justify-content:space-between;">
<span style="color: #4CAF50; font-weight: bold;">${msg.name}</span>
<span style="color: ${statusColor}; font-size: 10px; border: 1px solid ${statusColor}; padding: 0 4px; border-radius: 4px;">${msg.status || "未测试"}</span>
</div>
<div style="color: #888;">${msg.description || "No desc"}</div>
<div style="color: #666; font-size: 10px;">Params: ${msg.params || "None"}</div>
`;
// Action Button
const btnRun = document.createElement("ui-button");
btnRun.innerText = "Run";
btnRun.className = "tiny";
btnRun.style.height = "20px";
btnRun.style.lineHeight = "20px";
btnRun.addEventListener("confirm", () => {
this.runTest(msg.name);
});
item.appendChild(checkbox);
item.appendChild(content);
item.appendChild(btnRun);
this.ipcList!.appendChild(item);
});
}
private async testSelected() {
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
const toTest: string[] = [];
checkboxes.forEach((cb: any) => {
// In Cocos 2.x, ui-checkbox value is boolean
if (cb.checked || cb.value === true) {
toTest.push(cb.getAttribute("data-name"));
}
});
if (toTest.length === 0) {
this.log("No messages selected.");
return;
}
this.log(`Starting batch test for ${toTest.length} messages...`);
for (const name of toTest) {
await this.runTest(name);
}
this.log("Batch test completed.");
}
private runTest(name: string): Promise<void> {
return new Promise((resolve) => {
let params = null;
if (this.paramInput && this.paramInput.value.trim()) {
try {
params = JSON.parse(this.paramInput.value.trim());
} catch (e) {
this.log(`[Error] Invalid JSON Params: ${e}`);
resolve();
return;
}
}
this.log(`Testing: ${name} with params: ${JSON.stringify(params)}...`);
// @ts-ignore
Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name, params }, (err: any, result: any) => {
if (err) {
this.log(`[${name}] Failed: ${err}`);
} else {
this.log(`[${name}] Success: ${JSON.stringify(result)}`);
}
resolve();
});
});
}
private toggleAll(checked: boolean) {
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
checkboxes.forEach((cb: any) => {
cb.value = checked;
});
}
private log(msg: string) {
if (this.logArea) {
// @ts-ignore
const time = new Date().toLocaleTimeString();
this.logArea.value += `[${time}] ${msg}\n`;
this.logArea.scrollTop = this.logArea.scrollHeight;
}
}
}

View File

@@ -1,405 +0,0 @@
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 testScriptOperations() {
log('group', '脚本读写与验证测试 (FS Mode)');
const scriptPath = 'db://assets/auto_test_script.js';
const initialContent = 'cc.log("Initial Content");';
const updatedContent = 'cc.log("Updated Content");';
// 1. 创建脚本
try {
log('info', `创建脚本: ${scriptPath}`);
await callTool('manage_script', {
action: 'create',
path: scriptPath,
content: initialContent
});
log('success', `脚本已创建`);
} 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: initialContent });
} else {
throw e;
}
}
// 等待资源导入
await new Promise(r => setTimeout(r, 2000));
// 2. 验证读取 (FS Read)
log('info', `验证读取内容...`);
const readContent = await callTool('manage_script', { action: 'read', path: scriptPath });
// 注意Content 可能会包含一些编辑器自动添加的 meta 信息或者换行,可以宽松匹配
assert(readContent && readContent.includes("Initial Content"), `读取内容不匹配。实际: ${readContent}`);
log('success', `脚本读取成功`);
// 3. 验证写入 (FS Write + Refresh)
log('info', `验证写入内容...`);
await callTool('manage_script', { action: 'write', path: scriptPath, content: updatedContent });
// 等待刷新
await new Promise(r => setTimeout(r, 1000));
const readUpdated = await callTool('manage_script', { action: 'read', path: scriptPath });
assert(readUpdated && readUpdated.includes("Updated Content"), `写入后读取内容不匹配。实际: ${readUpdated}`);
log('success', `脚本写入成功`);
// 4. 验证脚本语法 (Validation)
log('info', `验证脚本语法...`);
const validation = await callTool('validate_script', { filePath: scriptPath });
log('info', `验证结果: ${JSON.stringify(validation)}`);
assert(validation && validation.valid === true, "脚本验证失败");
log('success', `脚本语法验证通过`);
// 5. 清理
await callTool('manage_asset', { action: 'delete', path: scriptPath });
log('success', `清理临时脚本`);
},
async testPrefabOperations(sourceNodeId) {
log('group', '预制体管理测试 (UUID Mode)');
const prefabPath = 'db://assets/AutoTestPrefab.prefab';
// 确保清理旧的
try {
await callTool('manage_asset', { action: 'delete', path: prefabPath });
} catch (e) { }
// 1. 创建预制体
log('info', `从节点 ${sourceNodeId} 创建预制体: ${prefabPath}`);
await callTool('prefab_management', {
action: 'create',
path: prefabPath,
nodeId: sourceNodeId
});
// 等待预制体生成和导入 (使用轮询机制)
log('info', '等待预制体生成...');
let prefabInfo = null;
// 每 200ms 检查一次,最多尝试 30 次 (6秒)
for (let i = 0; i < 30; i++) {
try {
prefabInfo = await callTool('prefab_management', { action: 'get_info', path: prefabPath });
if (prefabInfo && prefabInfo.exists) {
break;
}
} catch (e) { }
await new Promise(r => setTimeout(r, 200));
}
// 最终断言
assert(prefabInfo && prefabInfo.exists, "预制体创建失败或未找到 (超时)");
log('success', `预制体创建成功: ${prefabInfo.uuid}`);
// 2. 实例化预制体 (使用 UUID 加载)
log('info', `尝试实例化预制体 (UUID: ${prefabInfo.uuid})`);
const result = await callTool('prefab_management', {
action: 'instantiate',
path: prefabPath
});
log('info', `实例化结果: ${JSON.stringify(result)}`);
// 结果通常是一条成功消息字符串
assert(result && result.toLowerCase().includes('success'), "实例化失败");
log('success', `预制体实例化成功`);
// 3. 清理预制体
await callTool('manage_asset', { action: 'delete', path: prefabPath });
log('success', `清理临时预制体`);
},
async testResources() {
log('group', 'MCP Resource 协议测试');
// 1. 列出资源
log('info', '请求资源列表 (/list-resources)');
const listRes = await request('POST', '/list-resources');
log('info', `资源列表响应: ${JSON.stringify(listRes)}`);
assert(listRes && listRes.resources && Array.isArray(listRes.resources), "资源列表格式错误");
const hasHierarchy = listRes.resources.find(r => r.uri === 'cocos://hierarchy');
assert(hasHierarchy, "未找到 cocos://hierarchy 资源");
log('success', `成功获取资源列表 (包含 ${listRes.resources.length} 个资源)`);
// 2. 读取资源: Hierarchy
log('info', '读取资源: cocos://hierarchy');
const hierarchyRes = await request('POST', '/read-resource', { uri: 'cocos://hierarchy' });
assert(hierarchyRes && hierarchyRes.contents && hierarchyRes.contents.length > 0, "读取 Hierarchy 失败");
const hierarchyContent = hierarchyRes.contents[0].text;
assert(hierarchyContent && hierarchyContent.startsWith('['), "Hierarchy 内容应该是 JSON 数组");
log('success', `成功读取场景层级数据`);
// 3. 读取资源: Logs
log('info', '读取资源: cocos://logs/latest');
const logsRes = await request('POST', '/read-resource', { uri: 'cocos://logs/latest' });
assert(logsRes && logsRes.contents && logsRes.contents.length > 0, "读取 Logs 失败");
const logsContent = logsRes.contents[0].text;
assert(typeof logsContent === 'string', "日志内容应该是字符串");
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.testEditorSelection(nodeId);
await tests.testScriptOperations();
await tests.testPrefabOperations(nodeId);
await tests.testResources();
// 清理:我们在测试中已经尽可能清理了,但保留节点可能有助于观察结果
// 这里只是打印完成消息
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();

View File

@@ -1,107 +0,0 @@
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();

View File

@@ -1,146 +0,0 @@
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();

View File

@@ -1,175 +0,0 @@
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();

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": [
"dom",
"es5",
"es2015.promise"
],
"target": "es5",
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"forceConsistentCasingInFileNames": true,
"strict": false
},
"include": [
"src/**/*"
]
}