文档: 整合开发计划文档 & 修复: TypeScript 编译及可靠性改进
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# **/dist
|
||||
**/node_modules
|
||||
**/package-lock.json
|
||||
202
CODE_REVIEW.md
202
CODE_REVIEW.md
@@ -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` | ✅ | 粒子系统 |
|
||||
283
DEVELOPMENT.md
283
DEVELOPMENT.md
@@ -2,6 +2,17 @@
|
||||
|
||||
本文档记录了 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 目录结构搭建
|
||||
@@ -413,232 +424,88 @@ manageAsset(args, callback) {
|
||||
- 防止路径遍历攻击
|
||||
- 限制服务访问范围
|
||||
|
||||
## 11. 开发状态
|
||||
## 11. 开发路线图 (Roadmap)
|
||||
|
||||
### 11.1 已完成的任务
|
||||
### 11.1 第三阶段开发计划(已完成)
|
||||
|
||||
#### 第一阶段
|
||||
- ✅ HTTP 服务接口实现
|
||||
- ✅ 场景节点操作工具
|
||||
- ✅ 资源管理工具
|
||||
- ✅ 组件管理工具
|
||||
- ✅ 脚本管理工具(默认创建 TypeScript 脚本)
|
||||
- ✅ 批处理执行工具
|
||||
- ✅ 资产管理工具
|
||||
- ✅ 实时日志系统
|
||||
- ✅ 自动启动功能
|
||||
- ✅ 面板界面实现
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 |
|
||||
| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 |
|
||||
| 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 |
|
||||
| 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 |
|
||||
| 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 |
|
||||
| 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 |
|
||||
| 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 |
|
||||
|
||||
#### 第二阶段
|
||||
- ✅ 场景管理工具(scene_management)
|
||||
- 创建场景
|
||||
- 删除场景
|
||||
- 复制场景
|
||||
- 获取场景信息
|
||||
- ✅ 预制体管理工具(prefab_management)
|
||||
- 创建预制体
|
||||
- 更新预制体
|
||||
- 实例化预制体
|
||||
- 获取预制体信息
|
||||
- ✅ 面板布局优化
|
||||
- 响应式设计
|
||||
- 滚动条支持
|
||||
- 小窗口适配
|
||||
- ✅ 移除旧工具
|
||||
- 删除了 create_scene 工具(功能整合到 scene_management)
|
||||
- 删除了 create_prefab 工具(功能整合到 prefab_management)
|
||||
- ✅ README.md 文档更新
|
||||
- ✅ 代码提交到本地仓库
|
||||
### 11.2 第四阶段开发计划(已完成)
|
||||
|
||||
#### 第三阶段
|
||||
- ✅ 编辑器管理工具(manage_editor)
|
||||
- 获取选中对象
|
||||
- 设置选中状态
|
||||
- 刷新编辑器
|
||||
- ✅ 游戏对象查找工具(find_gameobjects)
|
||||
- 根据名称、标签、组件、激活状态查找节点
|
||||
- 支持递归和非递归查找
|
||||
- ✅ 材质管理工具(manage_material)
|
||||
- 创建、删除、获取材质信息
|
||||
- ✅ 纹理管理工具(manage_texture)
|
||||
- 创建、删除、获取纹理信息
|
||||
- ✅ 菜单项执行工具(execute_menu_item)
|
||||
- 执行 Cocos Creator 编辑器菜单项
|
||||
- ✅ 代码编辑增强工具(apply_text_edits)
|
||||
- 支持插入、删除、替换文本操作
|
||||
- ✅ 控制台读取工具(read_console)
|
||||
- 读取编辑器控制台输出
|
||||
- 支持按类型过滤和限制输出数量
|
||||
- ✅ 脚本验证工具(validate_script)
|
||||
- 验证脚本语法正确性
|
||||
- ✅ 面板工具说明功能
|
||||
- 添加工具说明框
|
||||
- 显示详细的工具描述和参数说明
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 |
|
||||
| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 |
|
||||
|
||||
### 11.2 未完成的任务
|
||||
### 11.3 差异填补阶段(Gap Filling)- 已完成
|
||||
|
||||
- ❌ 代码推送到远程仓库(认证错误)
|
||||
- ❌ 测试用例编写
|
||||
- ❌ 性能优化
|
||||
- ❌ 错误处理增强
|
||||
- ❌ 安全配置
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| 全局文件搜索 | ✅ 完成 | 实现 find_in_file 工具 |
|
||||
| 撤销/重做支持 | ✅ 完成 | 实现 manage_undo 工具,并重构核心操作支持撤销 |
|
||||
| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 |
|
||||
|
||||
### 11.3 后续需要完成的任务
|
||||
### 11.4 第六阶段:可靠性与体验优化(已完成)
|
||||
|
||||
#### 高优先级
|
||||
1. **代码推送**:解决远程仓库认证问题,完成代码推送
|
||||
2. **测试用例**:为核心工具编写测试用例
|
||||
3. **安全配置**:添加 IP 白名单和认证机制
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) |
|
||||
| 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) |
|
||||
| 组件智能解析修复 | ✅ 完成 | 修复组件属性赋值时的 UUID 类型转换,支持压缩 UUID 及自定义组件 (`$_$ctor`) |
|
||||
|
||||
#### 中优先级
|
||||
1. **性能优化**:优化 HTTP 服务响应速度,改进批处理执行效率
|
||||
2. **错误处理**:增强错误处理和恢复机制,提高插件稳定性
|
||||
3. **文档完善**:添加更详细的 API 文档和使用示例,包括新工具的详细说明
|
||||
### 11.5 第七阶段开发计划(未来规划)
|
||||
|
||||
#### 低优先级
|
||||
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 工具,支持脚本语法验证 |
|
||||
| 面板工具说明功能 | 低 | 已完成 | 添加工具说明框,显示详细的工具描述和参数说明 |
|
||||
| 任务 | 优先级 | 预计时间 | 描述 |
|
||||
|------|--------|----------|------|
|
||||
| 插件发布 | 高 | 1 天 | 准备发布,提交到 Cocos 插件商店 |
|
||||
| 文档完善 | 中 | 2 天 | 完善 API 文档 ("Getting Started" 教程) |
|
||||
| 界面美化 | 低 | 2 天 | 优化面板 UI 体检 |
|
||||
| 国际化支持 | 低 | 2 天 | 添加多语言 (i18n) 支持 |
|
||||
| 工具扩展 | 低 | 3 天 | 添加更多高级工具 |
|
||||
|
||||
## 12. Unity-MCP 对比分析
|
||||
|
||||
### 12.1 Unity-MCP 功能特性
|
||||
### 12.1 功能差距 (Gap Analysis)
|
||||
|
||||
Unity-MCP 提供了以下核心功能:
|
||||
通过与 Unity-MCP 对比,Cocos-MCP 已实现绝大多数核心功能。
|
||||
|
||||
- **资产管理**:管理各种 Unity 资源
|
||||
- **编辑器管理**:控制 Unity 编辑器功能
|
||||
- **游戏对象管理**:创建、修改、查找游戏对象
|
||||
- **组件管理**:添加、移除、修改组件
|
||||
- **材质管理**:创建和修改材质
|
||||
- **预制体管理**:管理预制体资源
|
||||
- **场景管理**:创建、保存、加载场景
|
||||
- **脚本管理**:创建、修改脚本
|
||||
- **ScriptableObject 管理**:管理配置文件
|
||||
- **着色器管理**:管理着色器资源
|
||||
- **VFX 管理**:管理视觉效果
|
||||
- **纹理管理**:管理纹理资源
|
||||
- **批处理执行**:批量执行多个操作
|
||||
- **游戏对象查找**:根据条件查找游戏对象
|
||||
- **文件内容查找**:在文件中查找内容
|
||||
- **控制台读取**:读取 Unity 控制台输出
|
||||
- **Unity 刷新**:刷新 Unity 编辑器
|
||||
- **测试运行**:运行测试用例
|
||||
- **获取测试任务**:获取测试任务信息
|
||||
- **菜单项执行**:执行 Unity 菜单项
|
||||
- **文本编辑应用**:应用文本编辑操作
|
||||
- **脚本编辑应用**:应用脚本编辑操作
|
||||
- **脚本验证**:验证脚本语法
|
||||
- **创建脚本**:创建新脚本
|
||||
- **删除脚本**:删除脚本文件
|
||||
- **获取 SHA**:获取版本控制 SHA 值
|
||||
| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 |
|
||||
|---------|---------------|---------------|------|
|
||||
| 编辑器管理 | manage_editor | ✅ 已实现 | |
|
||||
| 游戏对象管理 | find_gameobjects | ✅ 已实现 | |
|
||||
| 材质管理 | manage_material | ✅ 已实现 | |
|
||||
| 纹理管理 | manage_texture | ✅ 已实现 | |
|
||||
| 代码编辑 | apply_text_edits | ✅ 已实现 | |
|
||||
| 全局搜索 | find_in_file | ✅ 已实现 | |
|
||||
| 控制台 | read_console | ✅ 已实现 | |
|
||||
| 菜单执行 | execute_menu_item | ✅ 已实现 | |
|
||||
| 脚本验证 | validate_script | ✅ 已实现 | |
|
||||
| 撤销/重做 | undo/redo | ✅ 已实现 | |
|
||||
| VFX 管理 | manage_vfx | ✅ 已实现 | |
|
||||
| Git 集成 | get_sha | ❌ 未实现 | 低优先级 |
|
||||
| ScriptableObject | manage_so | ❌ 未实现 | 使用 AssetDB 替代 |
|
||||
|
||||
### 12.2 Cocos-MCP 功能特性
|
||||
## 13. 风险评估
|
||||
|
||||
当前 Cocos-MCP 已实现的功能:
|
||||
### 13.1 潜在风险
|
||||
|
||||
- **场景节点操作**:获取选中节点、设置节点名称、获取场景层级、更新节点变换、创建节点
|
||||
- **组件管理**:添加、移除、获取组件
|
||||
- **资源管理**:创建、删除、移动资源
|
||||
- **脚本管理**:创建、删除、读取、写入脚本(默认创建 TypeScript 脚本)
|
||||
- **批处理执行**:批量执行多个操作
|
||||
- **资产管理**:管理各种资源文件
|
||||
- **场景管理**:创建、删除、复制、获取场景信息
|
||||
- **预制体管理**:创建、更新、实例化、获取预制体信息
|
||||
- **面板界面**:提供主面板和工具测试面板
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API |
|
||||
| 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 |
|
||||
| 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 |
|
||||
| 兼容性问题 | 多版本不兼容 | 测试主流版本 (2.4.x),提供兼容层 |
|
||||
|
||||
### 12.3 功能缺失对比
|
||||
## 14. 结论
|
||||
|
||||
| 功能类别 | 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 | ❌ 缺失 | ✅ 可实现 |
|
||||
Cocos-MCP 插件的开发计划已顺利完成多个迭代阶段。目前插件实现了包括编辑器管理、场景操作、资源管理在内的全套核心功能,并完成了针对性的可靠性加固(IPC 通信、脚本时序、组件解析)。
|
||||
|
||||
### 12.4 功能实现建议
|
||||
|
||||
#### 高优先级功能
|
||||
|
||||
1. **编辑器管理工具** (`manage_editor`)
|
||||
- 功能:控制编辑器状态、执行编辑器操作
|
||||
- 实现方案:使用 `Editor.Ipc` 调用编辑器 API,如 `Editor.Selection`、`Editor.assetdb` 等
|
||||
|
||||
2. **游戏对象查找工具** (`find_gameobjects`)
|
||||
- 功能:根据条件查找场景中的节点
|
||||
- 实现方案:使用场景脚本遍历节点树,根据名称、标签、组件等条件过滤
|
||||
|
||||
3. **材质和纹理管理工具** (`manage_material`, `manage_texture`)
|
||||
- 功能:创建和管理材质、纹理资源
|
||||
- 实现方案:使用 `Editor.assetdb` API 操作资源文件
|
||||
|
||||
4. **菜单项执行工具** (`execute_menu_item`)
|
||||
- 功能:执行 Cocos Creator 菜单项
|
||||
- 实现方案:使用 `Editor.Ipc.sendToMain` 发送菜单命令
|
||||
|
||||
#### 中优先级功能
|
||||
|
||||
1. **代码编辑增强工具** (`apply_text_edits`, `script_apply_edits`)
|
||||
- 功能:应用文本编辑操作到文件
|
||||
- 实现方案:读取文件内容,应用编辑操作,然后写回文件
|
||||
|
||||
2. **控制台读取工具** (`read_console`)
|
||||
- 功能:读取编辑器控制台输出
|
||||
- 实现方案:重定向 `console.log` 等方法,捕获控制台输出
|
||||
|
||||
3. **脚本验证工具** (`validate_script`)
|
||||
- 功能:验证脚本语法正确性
|
||||
- 实现方案:使用 Node.js 的语法解析器或调用外部工具
|
||||
|
||||
#### 低优先级功能
|
||||
|
||||
1. **测试功能** (`run_tests`, `get_test_job`)
|
||||
- 功能:运行测试用例并获取结果
|
||||
- 实现方案:根据 Cocos Creator 的测试框架集成
|
||||
|
||||
2. **VFX 管理工具** (`manage_vfx`)
|
||||
- 功能:管理视觉效果资源
|
||||
- 实现方案:使用 `Editor.assetdb` API 操作 VFX 资源
|
||||
|
||||
## 13. 总结
|
||||
|
||||
MCP Bridge 插件通过 HTTP 服务和 MCP 协议,为外部 AI 工具提供了与 Cocos Creator 编辑器交互的能力。插件支持场景操作、资源管理、组件管理、脚本管理等多种功能,为 Cocos Creator 项目的开发和自动化提供了有力的支持。
|
||||
|
||||
通过本文档的开发流程,我们构建了一个功能完整、稳定可靠的 MCP Bridge 插件,为 Cocos Creator 生态系统增添了新的工具和能力。
|
||||
|
||||
目前插件已经完成了核心功能的实现,包括 15 个 MCP 工具,支持从场景操作到资源管理的各种功能。后续将继续完善测试、优化性能,并添加更多高级功能,为开发者提供更强大的工具支持。
|
||||
|
||||
通过与 Unity-MCP 的对比分析,我们识别出了多个可实现的功能,这些功能将进一步增强 Cocos-MCP 的能力,使其与 Unity-MCP 保持功能对等,为 Cocos Creator 开发者提供同样强大的 AI 辅助开发体验。
|
||||
插件功能已趋于稳定,后续工作重点将转向 **发布准备**、**文档体系建设** 以及 **用户体验优化**,力求为 Cocos Creator 开发者提供高质量的 AI 辅助开发工具。
|
||||
|
||||
@@ -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
1305
IPC_MESSAGES.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -113,7 +113,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
- `id`: 节点 UUID
|
||||
- `x`, `y`: 坐标
|
||||
- `scaleX`, `scaleY`: 缩放值
|
||||
- `color`: HEX 颜色代码(如 #FF0000)
|
||||
- `color`: HEX 颜色代码(如 #FF0000)(支持撤销操作)
|
||||
|
||||
### 6. open_scene
|
||||
|
||||
@@ -137,7 +137,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
- `action`: 操作类型(`add`, `remove`, `get`)
|
||||
- `componentType`: 组件类型,如 `cc.Sprite`(用于 `add` 操作)
|
||||
- `componentId`: 组件 ID(用于 `remove` 操作)
|
||||
- `properties`: 组件属性(用于 `add` 操作)
|
||||
- `properties`: 组件属性(用于 `add` 操作)。**智能特性**:如果属性期望组件类型但传入节点UUID,插件会自动查找匹配组件。
|
||||
|
||||
### 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
|
||||
|
||||
@@ -255,6 +255,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
- **描述**: 验证脚本
|
||||
- **参数**:
|
||||
- `filePath`: 脚本路径,如 `db://assets/scripts/TestScript.ts`
|
||||
- **注意**:对于 TypeScript 文件,仅进行基础语法结构检查,不进行完整编译验证。
|
||||
|
||||
### 22. find_in_file
|
||||
|
||||
|
||||
141
dist/IpcManager.js
vendored
Normal file
141
dist/IpcManager.js
vendored
Normal 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
233
dist/IpcUi.js
vendored
Normal 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;
|
||||
111
main.js
111
main.js
@@ -1,4 +1,5 @@
|
||||
"use strict";
|
||||
const { IpcManager } = require("./dist/IpcManager");
|
||||
|
||||
const http = require("http");
|
||||
const path = require("path");
|
||||
@@ -12,7 +13,6 @@ let serverConfig = {
|
||||
active: false,
|
||||
};
|
||||
|
||||
// 封装日志函数,同时发送给面板和编辑器控制台
|
||||
// 封装日志函数,同时发送给面板和编辑器控制台
|
||||
function addLog(type, message) {
|
||||
const logEntry = {
|
||||
@@ -182,17 +182,17 @@ const getToolsList = () => {
|
||||
type: "object",
|
||||
properties: {
|
||||
nodeId: { type: "string", description: "节点 UUID" },
|
||||
action: { type: "string", enum: ["add", "remove", "get"], description: "操作类型" },
|
||||
componentType: { type: "string", description: "组件类型,如 cc.Sprite" },
|
||||
componentId: { type: "string", description: "组件 ID (用于 remove 操作)" },
|
||||
properties: { type: "object", description: "组件属性 (用于 add 操作)" },
|
||||
action: { type: "string", enum: ["add", "remove", "update", "get"], description: "操作类型 (add: 添加组件, remove: 移除组件, update: 更新组件属性, get: 获取组件列表)" },
|
||||
componentType: { type: "string", description: "组件类型,如 cc.Sprite (add/update 操作需要)" },
|
||||
componentId: { type: "string", description: "组件 ID (remove/update 操作可选)" },
|
||||
properties: { type: "object", description: "组件属性 (add/update 操作使用). 支持智能解析: 如果属性类型是组件但提供了节点UUID,会自动查找对应组件。" },
|
||||
},
|
||||
required: ["nodeId", "action"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "manage_script",
|
||||
description: "管理脚本文件",
|
||||
description: "管理脚本文件。注意:创建或修改脚本后,编辑器需要时间进行编译(通常几秒钟)。新脚本在编译完成前无法作为组件添加到节点。建议在 create 后调用 refresh_editor,或等待一段时间后再使用 manage_components。",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -914,7 +914,18 @@ export default class NewScript extends cc.Component {
|
||||
update (dt) {}
|
||||
}`,
|
||||
(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;
|
||||
@@ -1460,19 +1471,44 @@ export default class NewScript extends cc.Component {
|
||||
}
|
||||
addLog("info", `Executing Menu Item: ${menuPath}`);
|
||||
|
||||
// 尝试通过 IPC 触发菜单 (Cocos 2.x 常用方式)
|
||||
// 如果是保存场景,直接使用对应的 stash-and-save IPC
|
||||
if (menuPath === 'File/Save Scene') {
|
||||
Editor.Ipc.sendToMain("scene:stash-and-save");
|
||||
} else {
|
||||
// 通用尝试 (可能不工作,取决于编辑器版本)
|
||||
// Editor.Ipc.sendToMain('ui:menu-click', menuPath);
|
||||
// 兜底:仅记录日志,暂不支持通用菜单点击
|
||||
addLog("warn", "Generic menu execution partial support.");
|
||||
// 菜单项映射表 (Cocos Creator 2.4.x IPC)
|
||||
// 参考: IPC_MESSAGES.md
|
||||
const menuMap = {
|
||||
'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 {
|
||||
// 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效)
|
||||
// 或者直接返回不支持的警告
|
||||
addLog("warn", `Menu item '${menuPath}' not found in supported map. Trying legacy fallback.`);
|
||||
|
||||
// 尝试通用调用
|
||||
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) {
|
||||
const { filePath } = args;
|
||||
@@ -1493,25 +1529,56 @@ export default class NewScript extends cc.Component {
|
||||
try {
|
||||
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")) {
|
||||
// 包装在函数中以避免变量污染
|
||||
const wrapper = `(function() { ${content} })`;
|
||||
try {
|
||||
new Function(wrapper); // 使用 Function 构造器比 direct eval稍微安全一点点,虽在这个场景下差别不大
|
||||
new Function(wrapper);
|
||||
callback(null, { valid: true, message: "JavaScript syntax is valid" });
|
||||
} catch (syntaxErr) {
|
||||
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) {
|
||||
callback(null, { valid: false, message: `Read Error: ${err.message}` });
|
||||
}
|
||||
},
|
||||
// 暴露给 MCP 或面板的 API 封装
|
||||
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"() {
|
||||
Editor.Panel.open("mcp-bridge");
|
||||
},
|
||||
|
||||
265
panel/index.html
265
panel/index.html
@@ -2,6 +2,7 @@
|
||||
<div class="tabs">
|
||||
<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="tabIpc" class="tab-button">IPC Test</ui-button>
|
||||
</div>
|
||||
|
||||
<div id="panelMain" class="tab-content active">
|
||||
@@ -22,6 +23,7 @@
|
||||
</div>
|
||||
|
||||
<div id="panelTest" class="tab-content">
|
||||
<!-- ... existing content ... -->
|
||||
<div class="test-layout">
|
||||
<div class="left-panel" id="testLeftPanel">
|
||||
<div class="form-item">
|
||||
@@ -57,47 +59,248 @@
|
||||
</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>
|
||||
|
||||
<style>
|
||||
:host { height: 100%; display: flex; background-color: #2d2d2d; overflow: hidden; }
|
||||
.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; }
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background-color: #2d2d2d;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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 */
|
||||
.toolbar { display: flex; align-items: center; padding: 5px 0; gap: 5px; flex-shrink: 0; }
|
||||
.spacer { 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; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
gap: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
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-layout { display: flex; flex: 1; min-height: 0; }
|
||||
.left-panel { width: 250px; min-width: 150px; max-width: 500px; display: flex; flex-direction: column; flex-shrink: 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; }
|
||||
.test-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 250px;
|
||||
min-width: 150px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 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 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.description-box {
|
||||
background: #222;
|
||||
color: #ccc;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const { IpcUi } = require("../dist/IpcUi");
|
||||
|
||||
Editor.Panel.extend({
|
||||
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||
@@ -24,8 +25,10 @@ Editor.Panel.extend({
|
||||
logView: root.querySelector("#logConsole"),
|
||||
tabMain: root.querySelector("#tabMain"),
|
||||
tabTest: root.querySelector("#tabTest"),
|
||||
tabIpc: root.querySelector("#tabIpc"),
|
||||
panelMain: root.querySelector("#panelMain"),
|
||||
panelTest: root.querySelector("#panelTest"),
|
||||
panelIpc: root.querySelector("#panelIpc"),
|
||||
toolName: root.querySelector("#toolName"),
|
||||
toolParams: root.querySelector("#toolParams"),
|
||||
toolDescription: root.querySelector("#toolDescription"),
|
||||
@@ -49,20 +52,35 @@ Editor.Panel.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize IPC UI
|
||||
new IpcUi(root);
|
||||
|
||||
// 2. 标签切换
|
||||
els.tabMain.addEventListener("confirm", () => {
|
||||
els.tabMain.classList.add("active");
|
||||
els.tabTest.classList.remove("active");
|
||||
els.tabIpc.classList.remove("active");
|
||||
els.panelMain.classList.add("active");
|
||||
els.panelTest.classList.remove("active");
|
||||
els.panelIpc.classList.remove("active");
|
||||
});
|
||||
els.tabTest.addEventListener("confirm", () => {
|
||||
els.tabTest.classList.add("active");
|
||||
els.tabMain.classList.remove("active");
|
||||
els.tabIpc.classList.remove("active");
|
||||
els.panelTest.classList.add("active");
|
||||
els.panelMain.classList.remove("active");
|
||||
els.panelIpc.classList.remove("active");
|
||||
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. 基础功能
|
||||
els.btnToggle.addEventListener("confirm", () => {
|
||||
|
||||
167
scene-script.js
167
scene-script.js
@@ -86,7 +86,16 @@ module.exports = {
|
||||
if (scaleX !== undefined) node.scaleX = Number(scaleX);
|
||||
if (scaleY !== undefined) node.scaleY = Number(scaleY);
|
||||
if (color) {
|
||||
node.color = new cc.Color().fromHEX(color);
|
||||
const c = new cc.Color().fromHEX(color);
|
||||
// 使用 scene:set-property 实现支持 Undo 的颜色修改
|
||||
// 注意:IPC 消息需要发送到场景面板
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id: id,
|
||||
path: "color",
|
||||
type: "Color",
|
||||
value: { r: c.r, g: c.g, b: c.b, a: c.a }
|
||||
});
|
||||
// 既然走了 IPC,就不需要手动 set node.color 了,也不需要重复 dirty
|
||||
}
|
||||
|
||||
Editor.Ipc.sendToMain("scene:dirty");
|
||||
@@ -156,6 +165,102 @@ module.exports = {
|
||||
const { nodeId, action, componentType, componentId, properties } = args;
|
||||
let node = cc.engine.getInstanceById(nodeId);
|
||||
|
||||
// 辅助函数:应用属性并智能解析
|
||||
const applyProperties = (component, props) => {
|
||||
if (!props) return;
|
||||
// 尝试获取组件类的属性定义
|
||||
const compClass = component.constructor;
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
// 检查属性是否存在
|
||||
if (component[key] !== undefined) {
|
||||
let finalValue = value;
|
||||
|
||||
// 【关键修复】智能组件引用赋值
|
||||
// 如果属性期望一个组件 (cc.Component子类) 但传入的是节点/UUID,尝试自动获取组件
|
||||
try {
|
||||
// 检查传入值是否是字符串 (可能是 UUID) 或 Node 对象
|
||||
let targetNode = null;
|
||||
if (typeof value === 'string') {
|
||||
targetNode = cc.engine.getInstanceById(value);
|
||||
|
||||
// Fallback for compressed UUIDs
|
||||
if (!targetNode && Editor.Utils && Editor.Utils.UuidUtils) {
|
||||
try {
|
||||
const decompressed = Editor.Utils.UuidUtils.decompressUuid(value);
|
||||
if (decompressed !== value) {
|
||||
targetNode = cc.engine.getInstanceById(decompressed);
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (targetNode) {
|
||||
Editor.log(`[scene-script] Resolved node: ${value} -> ${targetNode.name}`);
|
||||
} else if (value.length > 20) {
|
||||
Editor.warn(`[scene-script] Failed to resolve node: ${value}`);
|
||||
}
|
||||
} else if (value instanceof cc.Node) {
|
||||
targetNode = value;
|
||||
}
|
||||
|
||||
if (targetNode) {
|
||||
// 尝试获取属性定义类型
|
||||
let typeName = null;
|
||||
|
||||
|
||||
// 优先尝试 getClassAttrs (Cocos 2.x editor environment)
|
||||
if (cc.Class.Attr.getClassAttrs) {
|
||||
const attrs = cc.Class.Attr.getClassAttrs(compClass);
|
||||
// attrs 是整个属性字典 { name: { type: ... } }
|
||||
if (attrs) {
|
||||
if (attrs[key] && attrs[key].type) {
|
||||
typeName = attrs[key].type;
|
||||
} else if (attrs[key + '$_$ctor']) {
|
||||
// 编辑器环境下,自定义组件类型可能存储在 $_$ctor 后缀中
|
||||
typeName = attrs[key + '$_$ctor'];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 兼容性尝试 getClassAttributes
|
||||
else if (cc.Class.Attr.getClassAttributes) {
|
||||
const attrs = cc.Class.Attr.getClassAttributes(compClass, key);
|
||||
if (attrs && attrs.type) {
|
||||
typeName = attrs.type;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeName && (typeName.prototype instanceof cc.Component || typeName === cc.Component)) {
|
||||
|
||||
// 这是一个组件属性
|
||||
const targetComp = targetNode.getComponent(typeName);
|
||||
if (targetComp) {
|
||||
finalValue = targetComp;
|
||||
Editor.log(`[scene-script] Auto-resolved component ${typeName.name} from node ${targetNode.name}`);
|
||||
} else {
|
||||
Editor.warn(`[scene-script] Component ${typeName.name} not found on node ${targetNode.name}`);
|
||||
}
|
||||
} else if (!typeName) {
|
||||
// 无法确切知道类型,尝试常见的组件类型推断 (heuristic)
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (lowerKey.includes('label')) {
|
||||
const l = targetNode.getComponent(cc.Label);
|
||||
if (l) finalValue = l;
|
||||
} else if (lowerKey.includes('sprite')) {
|
||||
const s = targetNode.getComponent(cc.Sprite);
|
||||
if (s) finalValue = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore type check errors
|
||||
}
|
||||
|
||||
component[key] = finalValue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!node) {
|
||||
if (event.reply) event.reply(new Error("Node not found"));
|
||||
return;
|
||||
@@ -189,11 +294,7 @@ module.exports = {
|
||||
|
||||
// 设置属性
|
||||
if (properties) {
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (component[key] !== undefined) {
|
||||
component[key] = value;
|
||||
}
|
||||
}
|
||||
applyProperties(component, properties);
|
||||
}
|
||||
|
||||
Editor.Ipc.sendToMain("scene:dirty");
|
||||
@@ -236,6 +337,60 @@ module.exports = {
|
||||
}
|
||||
break;
|
||||
|
||||
case "update":
|
||||
// 更新现有组件属性
|
||||
if (!componentType) {
|
||||
// 如果提供了 componentId,可以只用 componentId
|
||||
// 但 Cocos 2.4 uuid 获取组件比较麻烦,最好还是有 type 或者遍历
|
||||
}
|
||||
|
||||
try {
|
||||
let targetComp = null;
|
||||
|
||||
// 1. 尝试通过 componentId 查找
|
||||
if (componentId) {
|
||||
if (node._components) {
|
||||
for (let i = 0; i < node._components.length; i++) {
|
||||
if (node._components[i].uuid === componentId) {
|
||||
targetComp = node._components[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 尝试通过 type 查找
|
||||
if (!targetComp && componentType) {
|
||||
let compClass = null;
|
||||
if (componentType.startsWith("cc.")) {
|
||||
const className = componentType.replace("cc.", "");
|
||||
compClass = cc[className];
|
||||
} else {
|
||||
compClass = cc.js.getClassByName(componentType);
|
||||
}
|
||||
if (compClass) {
|
||||
targetComp = node.getComponent(compClass);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetComp) {
|
||||
if (properties) {
|
||||
applyProperties(targetComp, properties);
|
||||
|
||||
Editor.Ipc.sendToMain("scene:dirty");
|
||||
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
|
||||
if (event.reply) event.reply(null, "Component properties updated");
|
||||
} else {
|
||||
if (event.reply) event.reply(null, "No properties to update");
|
||||
}
|
||||
} else {
|
||||
if (event.reply) event.reply(new Error(`Component not found (Type: ${componentType}, ID: ${componentId})`));
|
||||
}
|
||||
} catch (err) {
|
||||
if (event.reply) event.reply(new Error(`Failed to update component: ${err.message}`));
|
||||
}
|
||||
break;
|
||||
|
||||
case "get":
|
||||
try {
|
||||
const components = node._components.map((c) => {
|
||||
|
||||
107
src/IpcManager.ts
Normal file
107
src/IpcManager.ts
Normal 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
192
src/IpcUi.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
175
test/test_vfx.js
175
test/test_vfx.js
@@ -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
20
tsconfig.json
Normal 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/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user