feat: 实现 open_prefab 工具,优化预制体创建稳定性,并完成全量源码 (JS/TS)、文档与配置的汉化合规审计
This commit is contained in:
279
DEVELOPMENT.md
279
DEVELOPMENT.md
@@ -37,26 +37,26 @@ mcp-bridge/
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mcp-bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP Bridge for Cocos Creator",
|
||||
"main": "main.js",
|
||||
"panel": {
|
||||
"main": "panel/index.html",
|
||||
"type": "dockable",
|
||||
"title": "MCP Bridge",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
},
|
||||
"contributions": {
|
||||
"menu": [
|
||||
{
|
||||
"path": "Packages/MCP Bridge",
|
||||
"label": "Open Test Panel",
|
||||
"message": "open-test-panel"
|
||||
}
|
||||
]
|
||||
}
|
||||
"name": "mcp-bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP Bridge for Cocos Creator",
|
||||
"main": "main.js",
|
||||
"panel": {
|
||||
"main": "panel/index.html",
|
||||
"type": "dockable",
|
||||
"title": "MCP Bridge",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
},
|
||||
"contributions": {
|
||||
"menu": [
|
||||
{
|
||||
"path": "Packages/MCP Bridge",
|
||||
"label": "Open Test Panel",
|
||||
"message": "open-test-panel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,23 +104,30 @@ startServer(port) {
|
||||
## 4. 开发历程与里程碑
|
||||
|
||||
### 2026-02-10: Undo 系统深度修复与规范化
|
||||
|
||||
- **问题分析**: 修复了 `TypeError: Cannot read property '_name' of null`。该错误是由于直接修改节点属性(绕过 Undo 系统)与分组事务混用导致的。
|
||||
- **重构要点**: 将 `update-node-transform` 中所有的直接赋值替换为 `scene:set-property` IPC 调用,确保所有变换修改均受撤销系统监控。
|
||||
- **缺陷修正**: 修复了 `manage_undo` 在 `begin_group` 时传递错误参数导致 "Unknown object to record" 的问题。
|
||||
- **全量汉化与文档同步**: 完成了 `main.js` 和 `scene-script.js` 的 100% 简体中文翻译。同步更新了 `README.md`、`DEVELOPMENT.md` 及 `注意事项.md`。
|
||||
|
||||
### 2026-02-13: 新增 `open_prefab` 功能与消息协议修正
|
||||
|
||||
- **需求实现**: 新增 `open_prefab` 工具,允许 AI 直接打开预制体进入编辑模式。
|
||||
- **协议修正**: 经过测试对比,最终确认使用 `scene:enter-prefab-edit-mode` 消息并配合 `Editor.Ipc.sendToAll` 是进入预制体编辑模式的最佳方案,解决了 `scene:open-by-uuid` 无法触发编辑状态的问题。
|
||||
- **文档深度补全**: 遵循全局开发规范,同步更新了所有技术文档,确保 100% 简体中文覆盖及详尽的 JSDoc 注释。
|
||||
|
||||
### 3.2 MCP 工具注册
|
||||
|
||||
在 `/list-tools` 接口中注册工具:
|
||||
|
||||
```javascript
|
||||
const tools = [
|
||||
{
|
||||
name: "get_selected_node",
|
||||
description: "获取当前选中的节点",
|
||||
parameters: []
|
||||
},
|
||||
// 其他工具...
|
||||
{
|
||||
name: "get_selected_node",
|
||||
description: "获取当前选中的节点",
|
||||
parameters: [],
|
||||
},
|
||||
// 其他工具...
|
||||
];
|
||||
```
|
||||
|
||||
@@ -130,13 +137,13 @@ const tools = [
|
||||
|
||||
```javascript
|
||||
const sceneScript = {
|
||||
'create-node'(params, callback) {
|
||||
// 创建节点逻辑...
|
||||
},
|
||||
'set-property'(params, callback) {
|
||||
// 设置属性逻辑...
|
||||
},
|
||||
// 其他操作...
|
||||
"create-node"(params, callback) {
|
||||
// 创建节点逻辑...
|
||||
},
|
||||
"set-property"(params, callback) {
|
||||
// 设置属性逻辑...
|
||||
},
|
||||
// 其他操作...
|
||||
};
|
||||
```
|
||||
|
||||
@@ -238,33 +245,33 @@ manageAsset(args, callback) {
|
||||
|
||||
```html
|
||||
<div class="mcp-container">
|
||||
<!-- 标签页 -->
|
||||
<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>
|
||||
</div>
|
||||
<!-- 标签页 -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 主面板内容 -->
|
||||
<div id="panelMain" class="tab-content active">
|
||||
<!-- 主面板内容... -->
|
||||
</div>
|
||||
<!-- 主面板内容 -->
|
||||
<div id="panelMain" class="tab-content active">
|
||||
<!-- 主面板内容... -->
|
||||
</div>
|
||||
|
||||
<!-- 测试面板内容 -->
|
||||
<div id="panelTest" class="tab-content">
|
||||
<div class="test-container">
|
||||
<div class="test-layout">
|
||||
<!-- 左侧工具列表 -->
|
||||
<div class="left-panel">
|
||||
<!-- 工具列表... -->
|
||||
</div>
|
||||
|
||||
<!-- 右侧输入输出 -->
|
||||
<div class="right-panel">
|
||||
<!-- 输入输出区域... -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 测试面板内容 -->
|
||||
<div id="panelTest" class="tab-content">
|
||||
<div class="test-container">
|
||||
<div class="test-layout">
|
||||
<!-- 左侧工具列表 -->
|
||||
<div class="left-panel">
|
||||
<!-- 工具列表... -->
|
||||
</div>
|
||||
|
||||
<!-- 右侧输入输出 -->
|
||||
<div class="right-panel">
|
||||
<!-- 输入输出区域... -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -283,6 +290,7 @@ manageAsset(args, callback) {
|
||||
**错误信息**:`Panel info not found for panel mcp-bridge`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 检查 `package.json` 中的面板配置
|
||||
- 确保 `panel` 字段配置正确,移除冲突的 `panels` 字段
|
||||
|
||||
@@ -291,6 +299,7 @@ manageAsset(args, callback) {
|
||||
**错误信息**:`Parent path ... is not exists`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 在创建资源前添加目录检查和创建逻辑
|
||||
- 使用 `fs.mkdirSync(dirPath, { recursive: true })` 递归创建目录
|
||||
|
||||
@@ -299,6 +308,7 @@ manageAsset(args, callback) {
|
||||
**错误信息**:`SyntaxError: Invalid or unexpected token`
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 使用模板字符串(反引号)处理多行字符串
|
||||
- 避免变量名冲突
|
||||
|
||||
@@ -321,6 +331,7 @@ manageAsset(args, callback) {
|
||||
### 5.2 API 文档
|
||||
|
||||
为每个 MCP 工具编写详细的 API 文档,包括:
|
||||
|
||||
- 工具名称
|
||||
- 功能描述
|
||||
- 参数说明
|
||||
@@ -344,17 +355,17 @@ manageAsset(args, callback) {
|
||||
### 6.2 使用流程
|
||||
|
||||
1. **启动服务**:
|
||||
- 打开 Cocos Creator 编辑器
|
||||
- 选择 `Packages/MCP Bridge/Open Test Panel`
|
||||
- 点击 "Start" 按钮启动服务
|
||||
- 打开 Cocos Creator 编辑器
|
||||
- 选择 `Packages/MCP Bridge/Open Test Panel`
|
||||
- 点击 "Start" 按钮启动服务
|
||||
|
||||
2. **连接 AI 编辑器**:
|
||||
- 在 AI 编辑器中配置 MCP 代理
|
||||
- 使用 `node [项目路径]/packages/mcp-bridge/mcp-proxy.js` 作为命令
|
||||
- 在 AI 编辑器中配置 MCP 代理
|
||||
- 使用 `node [项目路径]/packages/mcp-bridge/mcp-proxy.js` 作为命令
|
||||
|
||||
3. **执行操作**:
|
||||
- 通过 AI 编辑器发送 MCP 请求
|
||||
- 或在测试面板中直接测试工具
|
||||
- 通过 AI 编辑器发送 MCP 请求
|
||||
- 或在测试面板中直接测试工具
|
||||
|
||||
### 6.3 配置选项
|
||||
|
||||
@@ -366,30 +377,30 @@ manageAsset(args, callback) {
|
||||
### 7.1 添加新工具
|
||||
|
||||
1. **在 `main.js` 中注册工具**:
|
||||
- 在 `/list-tools` 响应中添加工具定义
|
||||
- 在 `handleMcpCall` 函数中添加处理逻辑
|
||||
- 在 `/list-tools` 响应中添加工具定义
|
||||
- 在 `handleMcpCall` 函数中添加处理逻辑
|
||||
|
||||
2. **在面板中添加示例**:
|
||||
- 在 `panel/index.js` 中添加工具示例参数
|
||||
- 更新工具列表
|
||||
- 在 `panel/index.js` 中添加工具示例参数
|
||||
- 更新工具列表
|
||||
|
||||
3. **更新文档**:
|
||||
- 在 `README.md` 中添加工具文档
|
||||
- 更新功能特性列表
|
||||
- 在 `README.md` 中添加工具文档
|
||||
- 更新功能特性列表
|
||||
|
||||
### 7.2 集成新 API
|
||||
|
||||
1. **了解 Cocos Creator API**:
|
||||
- 查阅 Cocos Creator 编辑器 API 文档
|
||||
- 了解场景脚本 API
|
||||
- 查阅 Cocos Creator 编辑器 API 文档
|
||||
- 了解场景脚本 API
|
||||
|
||||
2. **实现集成**:
|
||||
- 在 `main.js` 或 `scene-script.js` 中添加对应功能
|
||||
- 处理异步操作和错误情况
|
||||
- 在 `main.js` 或 `scene-script.js` 中添加对应功能
|
||||
- 处理异步操作和错误情况
|
||||
|
||||
3. **测试验证**:
|
||||
- 编写测试用例
|
||||
- 验证功能正确性
|
||||
- 编写测试用例
|
||||
- 验证功能正确性
|
||||
|
||||
## 8. 版本管理
|
||||
|
||||
@@ -416,69 +427,69 @@ manageAsset(args, callback) {
|
||||
## 10. 最佳实践
|
||||
|
||||
1. **代码组织**:
|
||||
- 模块化设计,职责分离
|
||||
- 合理使用回调函数处理异步操作
|
||||
- 模块化设计,职责分离
|
||||
- 合理使用回调函数处理异步操作
|
||||
|
||||
2. **错误处理**:
|
||||
- 完善的错误捕获和处理
|
||||
- 详细的错误日志记录
|
||||
- 完善的错误捕获和处理
|
||||
- 详细的错误日志记录
|
||||
|
||||
3. **用户体验**:
|
||||
- 直观的面板界面
|
||||
- 实时的操作反馈
|
||||
- 详细的日志信息
|
||||
- 直观的面板界面
|
||||
- 实时的操作反馈
|
||||
- 详细的日志信息
|
||||
|
||||
4. **安全性**:
|
||||
- 验证输入参数
|
||||
- 防止路径遍历攻击
|
||||
- 限制服务访问范围
|
||||
- 验证输入参数
|
||||
- 防止路径遍历攻击
|
||||
- 限制服务访问范围
|
||||
|
||||
## 11. 开发路线图 (Roadmap)
|
||||
|
||||
### 11.1 第三阶段开发计划(已完成)
|
||||
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 |
|
||||
| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 |
|
||||
| 任务 | 状态 | 描述 |
|
||||
| ---------------------- | ------- | ------------------------------------------------------------------- |
|
||||
| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 |
|
||||
| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 |
|
||||
| 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 |
|
||||
| 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 |
|
||||
| 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 |
|
||||
| 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 |
|
||||
| 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 |
|
||||
| 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 |
|
||||
| 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 |
|
||||
| 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 |
|
||||
| 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 |
|
||||
|
||||
### 11.2 第四阶段开发计划(已完成)
|
||||
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| 任务 | 状态 | 描述 |
|
||||
| ------------ | ------- | ---------------------------------------------- |
|
||||
| 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 |
|
||||
| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 |
|
||||
| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 |
|
||||
|
||||
### 11.3 差异填补阶段(Gap Filling)- 已完成
|
||||
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 |
|
||||
| 文件哈希 | ✅ 完成 | 实现 get_sha 工具,支持文件 SHA-256 计算 |
|
||||
| 任务 | 状态 | 描述 |
|
||||
| -------- | ------- | ---------------------------------------------- |
|
||||
| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 |
|
||||
| 文件哈希 | ✅ 完成 | 实现 get_sha 工具,支持文件 SHA-256 计算 |
|
||||
| 动画管理 | ✅ 完成 | 实现 manage_animation 工具,支持动画播放与控制 |
|
||||
|
||||
### 11.4 第六阶段:可靠性与体验优化(已完成)
|
||||
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) |
|
||||
| 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) |
|
||||
| 任务 | 状态 | 描述 |
|
||||
| ---------------- | ------- | -------------------------------------------------------------------------- |
|
||||
| IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) |
|
||||
| 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) |
|
||||
| 组件智能解析修复 | ✅ 完成 | 修复组件属性赋值时的 UUID 类型转换,支持压缩 UUID 及自定义组件 (`$_$ctor`) |
|
||||
|
||||
### 11.5 第七阶段开发计划(已完成)
|
||||
|
||||
| 任务 | 状态 | 描述 |
|
||||
|------|------|------|
|
||||
| 插件发布 | ✅ 完成 | 准备发布,提交到 Cocos 插件商店 |
|
||||
| 文档完善 | ✅ 完成 | 完善 API 文档 ("Getting Started" 教程) |
|
||||
| 界面美化 | ✅ 完成 | 优化面板 UI 体验 |
|
||||
| 任务 | 状态 | 描述 |
|
||||
| ---------- | ------- | ----------------------------------------- |
|
||||
| 插件发布 | ✅ 完成 | 准备发布,提交到 Cocos 插件商店 |
|
||||
| 文档完善 | ✅ 完成 | 完善 API 文档 ("Getting Started" 教程) |
|
||||
| 界面美化 | ✅ 完成 | 优化面板 UI 体验 |
|
||||
| 国际化支持 | ✅ 完成 | 添加多语言 (i18n) 支持 (主要是中文本地化) |
|
||||
| 工具扩展 | ✅ 完成 | 添加更多高级工具 |
|
||||
| 工具扩展 | ✅ 完成 | 添加更多高级工具 |
|
||||
|
||||
## 12. Unity-MCP 对比分析
|
||||
|
||||
@@ -486,33 +497,33 @@ manageAsset(args, callback) {
|
||||
|
||||
通过与 Unity-MCP 对比,Cocos-MCP 已实现绝大多数核心功能。
|
||||
|
||||
| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 |
|
||||
|---------|---------------|---------------|------|
|
||||
| 编辑器管理 | manage_editor | ✅ 已实现 | |
|
||||
| 游戏对象管理 | find_gameobjects | ✅ 已实现 | |
|
||||
| 材质管理 | manage_material | ✅ 已实现 | |
|
||||
| 纹理管理 | manage_texture | ✅ 已实现 | |
|
||||
| 代码编辑 | apply_text_edits | ✅ 已实现 | |
|
||||
| 全局搜索 | search_project | ✅ 已实现 | 升级版,支持正则和路径限定 |
|
||||
| 控制台 | read_console | ✅ 已实现 | |
|
||||
| 菜单执行 | execute_menu_item | ✅ 已实现 | 移除不稳定映射,推荐 delete-node:UUID |
|
||||
| 脚本验证 | validate_script | ✅ 已实现 | |
|
||||
| 撤销/重做 | undo/redo | ✅ 已实现 | |
|
||||
| VFX 管理 | manage_vfx | ✅ 已实现 | |
|
||||
| Git 集成 | get_sha | ✅ 已实现 | 虽然优先级中等,但已根据需求完成 |
|
||||
| 动画管理 | manage_animation | ✅ 已实现 | 支持播放、暂停、停止及信息获取 |
|
||||
| ScriptableObject | manage_so | ❌ 未实现 | 使用 AssetDB 替代 |
|
||||
| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 |
|
||||
| ---------------- | ----------------- | -------------- | ------------------------------------- |
|
||||
| 编辑器管理 | manage_editor | ✅ 已实现 | |
|
||||
| 游戏对象管理 | find_gameobjects | ✅ 已实现 | |
|
||||
| 材质管理 | manage_material | ✅ 已实现 | |
|
||||
| 纹理管理 | manage_texture | ✅ 已实现 | |
|
||||
| 代码编辑 | apply_text_edits | ✅ 已实现 | |
|
||||
| 全局搜索 | search_project | ✅ 已实现 | 升级版,支持正则和路径限定 |
|
||||
| 控制台 | read_console | ✅ 已实现 | |
|
||||
| 菜单执行 | execute_menu_item | ✅ 已实现 | 移除不稳定映射,推荐 delete-node:UUID |
|
||||
| 脚本验证 | validate_script | ✅ 已实现 | |
|
||||
| 撤销/重做 | undo/redo | ✅ 已实现 | |
|
||||
| VFX 管理 | manage_vfx | ✅ 已实现 | |
|
||||
| Git 集成 | get_sha | ✅ 已实现 | 虽然优先级中等,但已根据需求完成 |
|
||||
| 动画管理 | manage_animation | ✅ 已实现 | 支持播放、暂停、停止及信息获取 |
|
||||
| ScriptableObject | manage_so | ❌ 未实现 | 使用 AssetDB 替代 |
|
||||
|
||||
## 13. 风险评估
|
||||
|
||||
### 13.1 潜在风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API |
|
||||
| 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 |
|
||||
| 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 |
|
||||
| 兼容性问题 | 多版本不兼容 | 测试主流版本 (2.4.x),提供兼容层 |
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
| --------------- | ------------ | ----------------------------------------- |
|
||||
| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API |
|
||||
| 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 |
|
||||
| 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 |
|
||||
| 兼容性问题 | 多版本不兼容 | 测试主流版本 (2.4.x),提供兼容层 |
|
||||
|
||||
## 14. 结论
|
||||
|
||||
|
||||
182
IPC_MESSAGES.md
182
IPC_MESSAGES.md
File diff suppressed because it is too large
Load Diff
39
README.md
39
README.md
@@ -10,7 +10,7 @@
|
||||
|
||||
- **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能
|
||||
- **场景节点操作**: 获取、创建、修改场景中的节点
|
||||
- **资源管理**: 创建场景、预制体,打开指定资源
|
||||
- **资源管理**: 创建场景、预制体,打开场景或预制体进入编辑模式
|
||||
- **组件管理**: 添加、删除、获取节点组件
|
||||
- **脚本管理**: 创建、删除、读取、写入脚本文件
|
||||
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
|
||||
@@ -123,10 +123,16 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
- **参数**:
|
||||
- `url`: 场景资源路径,如 `db://assets/NewScene.fire`
|
||||
|
||||
### 7. create_node
|
||||
### 7. open_prefab
|
||||
|
||||
- **描述**: 在编辑器中打开指定的预制体文件进入编辑模式。这是一个异步操作,打开后请等待几秒。
|
||||
- **参数**:
|
||||
- `url`: 预制体资源路径,如 `db://assets/prefabs/Test.prefab`
|
||||
|
||||
### 8. create_node
|
||||
|
||||
- **描述**: 在当前场景中创建一个新节点。
|
||||
- **重要提示**:
|
||||
- **重要提示**:
|
||||
1. 如果指定了 `parentId`,必须先通过 `get_scene_hierarchy` 确认该 UUID 对应的父节点仍然存在。
|
||||
2. **预设类型差异**:
|
||||
- `empty`: 纯空节点,无组件,不带贴图。
|
||||
@@ -247,15 +253,15 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
|
||||
- **描述**: 管理纹理
|
||||
- **参数**:
|
||||
- `action`: 操作类型(`create`, `delete`, `get_info`, `update`)
|
||||
- `path`: 纹理路径,如 `db://assets/textures/NewTexture.png`
|
||||
- `properties`: 纹理属性(用于 `create`/`update` 操作)
|
||||
- `type`: 纹理类型(如 `sprite`, `texture`, `raw`)(用于 `update`)
|
||||
- `border`: 九宫格边距数组 `[top, bottom, left, right]` (用于 `update`,仅当 type 为 sprite 时有效)
|
||||
- `subMetas`: (内部使用)
|
||||
- `width`: 宽度 (用于 `create` 生成占位图)
|
||||
- `height`: 高度 (用于 `create` 生成占位图)
|
||||
- `native`: 原生路径
|
||||
- `action`: 操作类型(`create`, `delete`, `get_info`, `update`)
|
||||
- `path`: 纹理路径,如 `db://assets/textures/NewTexture.png`
|
||||
- `properties`: 纹理属性(用于 `create`/`update` 操作)
|
||||
- `type`: 纹理类型(如 `sprite`, `texture`, `raw`)(用于 `update`)
|
||||
- `border`: 九宫格边距数组 `[top, bottom, left, right]` (用于 `update`,仅当 type 为 sprite 时有效)
|
||||
- `subMetas`: (内部使用)
|
||||
- `width`: 宽度 (用于 `create` 生成占位图)
|
||||
- `height`: 高度 (用于 `create` 生成占位图)
|
||||
- `native`: 原生路径
|
||||
|
||||
### 18. execute_menu_item
|
||||
|
||||
@@ -303,6 +309,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
- `includeSubpackages`: 是否搜索子包 (Boolean, 默认 true)
|
||||
|
||||
**示例**:
|
||||
|
||||
```json
|
||||
// 正则搜索
|
||||
{
|
||||
@@ -349,7 +356,6 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
- **参数**:
|
||||
- `path`: 文件路径,如 `db://assets/scripts/Test.ts`
|
||||
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 架构设计
|
||||
@@ -413,12 +419,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
|
||||
1. **确定性优先**:任何对节点、组件、属性的操作,都必须建立在“主体已确认存在”的基础上。
|
||||
2. **校验流程**:
|
||||
* **节点校验**:操作前必须使用 `get_scene_hierarchy` 确认节点。
|
||||
* **组件校验**:操作组件前必须使用 `get`(通过 `manage_components`)确认组件存在。
|
||||
* **属性校验**:更新属性前必须确认属性名准确无误。
|
||||
- **节点校验**:操作前必须使用 `get_scene_hierarchy` 确认节点。
|
||||
- **组件校验**:操作组件前必须使用 `get`(通过 `manage_components`)确认组件存在。
|
||||
- **属性校验**:更新属性前必须确认属性名准确无误。
|
||||
3. **禁止假设**:禁止盲目尝试对不存在的对象或属性进行修改。
|
||||
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个插件!
|
||||
|
||||
@@ -113,3 +113,10 @@
|
||||
|
||||
- **清理死代码**: 删除 `/list-tools` 路由中重复的 `res.writeHead / res.end` 调用。
|
||||
- **文档更新**: `注意事项.md` 新增第 9 章「并发安全与防卡死机制」,记录 CommandQueue 和 IPC 超时两个防护机制。
|
||||
|
||||
### 6. 场景与预制体工具增强
|
||||
|
||||
- **新增 `open_prefab` 工具**: 解决了直接打开预制体进入编辑模式的问题。通过使用正确的 IPC 消息 `scene:enter-prefab-edit-mode` (并结合 `Editor.Ipc.sendToAll`),使得 AI 可以精准操控预制体的编辑流程,而不再局限于场景跳转。
|
||||
- **优化预制体创建稳定性 (`create_node` + `prefab_management`)**:
|
||||
- 在创建物理目录后强制执行 `Editor.assetdb.refresh`,确保 AssetDB 即时同步。
|
||||
- 将节点重命名与预制体创建指令之间的安全延迟从 100ms 增加至 300ms,消除了重命名未完成导致创建失败的竞态条件。
|
||||
|
||||
120
main.js
120
main.js
@@ -253,6 +253,20 @@ const getToolsList = () => {
|
||||
required: ["url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "open_prefab",
|
||||
description: `${globalPrecautions} 在编辑器中打开预制体文件进入编辑模式。注意:这是一个异步操作,打开后请等待几秒。`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "预制体资源路径,如 db://assets/prefabs/Test.prefab",
|
||||
},
|
||||
},
|
||||
required: ["url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_node",
|
||||
description: `${globalPrecautions} 在当前场景中创建一个新节点。重要提示:1. 如果指定 parentId,必须先通过 get_scene_hierarchy 确保该父节点真实存在且未被删除。2. 类型说明:'sprite' (100x100 尺寸 + 默认贴图), 'button' (150x50 尺寸 + 深色底图 + Button组件), 'label' (120x40 尺寸 + Label组件), 'empty' (纯空节点)。`,
|
||||
@@ -842,6 +856,9 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭 HTTP 服务器
|
||||
*/
|
||||
stopServer() {
|
||||
if (mcpServer) {
|
||||
mcpServer.close();
|
||||
@@ -852,6 +869,10 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取 MCP 资源列表
|
||||
* @returns {Array<Object>} 资源列表数组
|
||||
*/
|
||||
getResourcesList() {
|
||||
return [
|
||||
{
|
||||
@@ -875,6 +896,11 @@ module.exports = {
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取指定的 MCP 资源内容
|
||||
* @param {string} uri 资源统一资源标识符 (URI)
|
||||
* @param {Function} callback 完成回调 (err, content)
|
||||
*/
|
||||
handleReadResource(uri, callback) {
|
||||
let parsed;
|
||||
try {
|
||||
@@ -997,6 +1023,22 @@ module.exports = {
|
||||
}
|
||||
break;
|
||||
|
||||
case "open_prefab":
|
||||
isSceneBusy = true; // 锁定
|
||||
const prefabUuid = Editor.assetdb.urlToUuid(args.url);
|
||||
if (prefabUuid) {
|
||||
// 【核心修复】使用正确的 IPC 消息进入预制体编辑模式
|
||||
Editor.Ipc.sendToAll("scene:enter-prefab-edit-mode", prefabUuid);
|
||||
setTimeout(() => {
|
||||
isSceneBusy = false;
|
||||
callback(null, `成功:正在打开预制体 ${args.url}`);
|
||||
}, 2000);
|
||||
} else {
|
||||
isSceneBusy = false;
|
||||
callback(`找不到路径为 ${args.url} 的资源`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "create_node":
|
||||
if (args.type === "sprite" || args.type === "button") {
|
||||
const splashUuid = Editor.assetdb.urlToUuid(
|
||||
@@ -1432,11 +1474,11 @@ export default class NewScript extends cc.Component {
|
||||
const dirPath = pathModule.dirname(absolutePath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
// 【增强】确保 AssetDB 刷新文件夹,防止场景脚本找不到目标目录
|
||||
Editor.assetdb.refresh(targetDir);
|
||||
}
|
||||
|
||||
// 解析目标目录和文件名
|
||||
// db://assets/folder/PrefabName.prefab -> db://assets/folder, PrefabName
|
||||
const targetDir = prefabPath.substring(0, prefabPath.lastIndexOf("/"));
|
||||
const fileName = prefabPath.substring(prefabPath.lastIndexOf("/") + 1);
|
||||
const prefabName = fileName.replace(".prefab", "");
|
||||
|
||||
@@ -1451,9 +1493,10 @@ export default class NewScript extends cc.Component {
|
||||
|
||||
// 2. 发送创建命令 (参数: [uuids], dirPath)
|
||||
// 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径
|
||||
// 【增强】增加延迟到 300ms,确保 IPC 消息处理并同步到底层引擎
|
||||
setTimeout(() => {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir);
|
||||
}, 100); // 稍微延迟以确保重命名生效
|
||||
}, 300);
|
||||
|
||||
callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`);
|
||||
break;
|
||||
@@ -1472,7 +1515,7 @@ export default class NewScript extends cc.Component {
|
||||
|
||||
case "instantiate":
|
||||
if (!Editor.assetdb.exists(prefabPath)) {
|
||||
return callback(`Prefab not found at ${prefabPath}`);
|
||||
return callback(`路径为 ${prefabPath} 的预制体不存在`);
|
||||
}
|
||||
// 实例化预制体
|
||||
const prefabUuid = Editor.assetdb.urlToUuid(prefabPath);
|
||||
@@ -2066,6 +2109,11 @@ CCProgram fs %{
|
||||
callback(null, filteredOutput);
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行编辑器菜单项
|
||||
* @param {Object} args 参数 (menuPath)
|
||||
* @param {Function} callback 完成回调
|
||||
*/
|
||||
executeMenuItem(args, callback) {
|
||||
const { menuPath } = args;
|
||||
if (!menuPath) {
|
||||
@@ -2092,7 +2140,7 @@ CCProgram fs %{
|
||||
if (uuid) {
|
||||
callSceneScriptWithTimeout("mcp-bridge", "delete-node", { uuid }, (err, result) => {
|
||||
if (err) callback(err);
|
||||
else callback(null, result || `Node ${uuid} deleted via scene script`);
|
||||
else callback(null, result || `节点 ${uuid} 已通过场景脚本删除`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2120,7 +2168,7 @@ CCProgram fs %{
|
||||
} else {
|
||||
// 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效)
|
||||
// 或者直接返回不支持的警告
|
||||
addLog("warn", `支持映射表中找不到菜单项 '${menuPath}'。尝试通过 legacy 模式执行。`);
|
||||
addLog("warn", `支持映射表中找不到菜单项 '${menuPath}'。尝试通过旧版模式执行。`);
|
||||
|
||||
// 尝试通用调用
|
||||
try {
|
||||
@@ -2134,7 +2182,11 @@ CCProgram fs %{
|
||||
}
|
||||
},
|
||||
|
||||
// 验证脚本
|
||||
/**
|
||||
* 验证脚本文件的语法或基础结构
|
||||
* @param {Object} args 参数 (filePath)
|
||||
* @param {Function} callback 完成回调
|
||||
*/
|
||||
validateScript(args, callback) {
|
||||
const { filePath } = args;
|
||||
|
||||
@@ -2265,7 +2317,7 @@ CCProgram fs %{
|
||||
},
|
||||
|
||||
"inspect-apis"() {
|
||||
addLog("info", "[API Inspector] Starting DEEP inspection...");
|
||||
addLog("info", "[API 检查器] 开始深度分析...");
|
||||
|
||||
// 获取函数参数的辅助函数
|
||||
const getArgs = (func) => {
|
||||
@@ -2365,8 +2417,8 @@ CCProgram fs %{
|
||||
: "Missing";
|
||||
});
|
||||
|
||||
addLog("info", `[API Inspector] Standard Objects:\n${JSON.stringify(report, null, 2)}`);
|
||||
addLog("info", `[API Inspector] Forum Checklist:\n${JSON.stringify(checklistResults, null, 2)}`);
|
||||
addLog("info", `[API 检查器] 标准对象:\n${JSON.stringify(report, null, 2)}`);
|
||||
addLog("info", `[API 检查器] 论坛核查清单:\n${JSON.stringify(checklistResults, null, 2)}`);
|
||||
|
||||
// 3. 检查内置包 IPC 消息
|
||||
const ipcReport = {};
|
||||
@@ -2391,12 +2443,15 @@ CCProgram fs %{
|
||||
}
|
||||
});
|
||||
|
||||
addLog("info", `[API Inspector] Built-in IPC Messages:\n${JSON.stringify(ipcReport, null, 2)}`);
|
||||
addLog("info", `[API 检查器] 内置包 IPC 消息:\n${JSON.stringify(ipcReport, null, 2)}`);
|
||||
},
|
||||
},
|
||||
|
||||
// 全局文件搜索
|
||||
// 项目搜索 (升级版 find_in_file)
|
||||
/**
|
||||
* 全局项目文件搜索 (支持正则表达式、文件名、目录名搜索)
|
||||
* @param {Object} args 参数
|
||||
* @param {Function} callback 完成回调
|
||||
*/
|
||||
searchProject(args, callback) {
|
||||
const { query, useRegex, path: searchPath, matchType, extensions } = args;
|
||||
|
||||
@@ -2405,7 +2460,7 @@ CCProgram fs %{
|
||||
const rootPath = Editor.assetdb.urlToFspath(rootPathUrl);
|
||||
|
||||
if (!rootPath || !fs.existsSync(rootPath)) {
|
||||
return callback(`Invalid search path: ${rootPathUrl}`);
|
||||
return callback(`无效的搜索路径: ${rootPathUrl}`);
|
||||
}
|
||||
|
||||
const mode = matchType || "content"; // content, file_name, dir_name
|
||||
@@ -2534,11 +2589,15 @@ CCProgram fs %{
|
||||
walk(rootPath);
|
||||
callback(null, results);
|
||||
} catch (err) {
|
||||
callback(`Search project failed: ${err.message}`);
|
||||
callback(`项目搜索失败: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 管理撤销/重做
|
||||
/**
|
||||
* 管理撤销/重做操作及事务分组
|
||||
* @param {Object} args 参数 (action, description, id)
|
||||
* @param {Function} callback 完成回调
|
||||
*/
|
||||
manageUndo(args, callback) {
|
||||
const { action, description } = args;
|
||||
|
||||
@@ -2546,16 +2605,13 @@ CCProgram fs %{
|
||||
switch (action) {
|
||||
case "undo":
|
||||
Editor.Ipc.sendToPanel("scene", "scene:undo");
|
||||
callback(null, "Undo command executed");
|
||||
callback(null, "撤销指令已执行");
|
||||
break;
|
||||
case "redo":
|
||||
Editor.Ipc.sendToPanel("scene", "scene:redo");
|
||||
callback(null, "Redo command executed");
|
||||
callback(null, "重做指令已执行");
|
||||
break;
|
||||
case "begin_group":
|
||||
// scene:undo-record [id]
|
||||
// 注意:在 2.4.x 中,undo-record 通常需要一个有效的 uuid
|
||||
// 如果没有提供 uuid,不应将 description 作为 ID 发送,否则会报 Unknown object to record
|
||||
addLog("info", `开始撤销组: ${description || "MCP 动作"}`);
|
||||
// 如果有参数包含 id,则记录该节点
|
||||
if (args.id) {
|
||||
@@ -2565,27 +2621,31 @@ CCProgram fs %{
|
||||
break;
|
||||
case "end_group":
|
||||
Editor.Ipc.sendToPanel("scene", "scene:undo-commit");
|
||||
callback(null, "Undo group committed");
|
||||
callback(null, "撤销组已提交");
|
||||
break;
|
||||
case "cancel_group":
|
||||
Editor.Ipc.sendToPanel("scene", "scene:undo-cancel");
|
||||
callback(null, "Undo group cancelled");
|
||||
callback(null, "撤销组已取消");
|
||||
break;
|
||||
default:
|
||||
callback(`Unknown undo action: ${action}`);
|
||||
callback(`未知的撤销操作: ${action}`);
|
||||
}
|
||||
} catch (err) {
|
||||
callback(`Undo operation failed: ${err.message}`);
|
||||
callback(`撤销操作失败: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文件 SHA-256
|
||||
/**
|
||||
* 计算资源的 SHA-256 哈希值
|
||||
* @param {Object} args 参数 (path)
|
||||
* @param {Function} callback 完成回调
|
||||
*/
|
||||
getSha(args, callback) {
|
||||
const { path: url } = args;
|
||||
const fspath = Editor.assetdb.urlToFspath(url);
|
||||
|
||||
if (!fspath || !fs.existsSync(fspath)) {
|
||||
return callback(`File not found: ${url}`);
|
||||
return callback(`找不到文件: ${url}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2599,7 +2659,11 @@ CCProgram fs %{
|
||||
}
|
||||
},
|
||||
|
||||
// 管理动画
|
||||
/**
|
||||
* 管理节点动画 (播放、停止、获取信息等)
|
||||
* @param {Object} args 参数
|
||||
* @param {Function} callback 完成回调
|
||||
*/
|
||||
manageAnimation(args, callback) {
|
||||
// 转发给场景脚本处理
|
||||
callSceneScriptWithTimeout("mcp-bridge", "manage-animation", args, callback);
|
||||
|
||||
210
mcp-proxy.js
210
mcp-proxy.js
@@ -1,101 +1,149 @@
|
||||
const http = require('http');
|
||||
const COCOS_PORT = 3456;
|
||||
/**
|
||||
* MCP 桥接代理脚本
|
||||
* 负责在标准 MCP 客户端 (stdin/stdout) 与 Cocos Creator 插件 (HTTP) 之间转发请求。
|
||||
*/
|
||||
|
||||
const http = require("http");
|
||||
|
||||
/**
|
||||
* 当前 Cocos Creator 插件监听的端口
|
||||
* @type {number}
|
||||
*/
|
||||
const COCOS_PORT = 3456;
|
||||
|
||||
/**
|
||||
* 发送调试日志到标准的错误输出流水
|
||||
* @param {string} msg 日志消息
|
||||
*/
|
||||
function debugLog(msg) {
|
||||
process.stderr.write(`[Proxy Debug] ${msg}\n`);
|
||||
process.stderr.write(`[代理调试] ${msg}\n`);
|
||||
}
|
||||
|
||||
process.stdin.on('data', (data) => {
|
||||
const lines = data.toString().split('\n');
|
||||
lines.forEach(line => {
|
||||
if (!line.trim()) return;
|
||||
try {
|
||||
const request = JSON.parse(line);
|
||||
handleRequest(request);
|
||||
} catch (e) {}
|
||||
});
|
||||
// 监听标准输入以获取 MCP 请求
|
||||
process.stdin.on("data", (data) => {
|
||||
const lines = data.toString().split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (!line.trim()) return;
|
||||
try {
|
||||
const request = JSON.parse(line);
|
||||
handleRequest(request);
|
||||
} catch (e) {
|
||||
// 忽略非 JSON 输入
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理 JSON-RPC 请求
|
||||
* @param {Object} req RPC 请求对象
|
||||
*/
|
||||
function handleRequest(req) {
|
||||
const { method, id, params } = req;
|
||||
const { method, id, params } = req;
|
||||
|
||||
if (method === 'initialize') {
|
||||
sendToAI({
|
||||
jsonrpc: "2.0", id: id,
|
||||
result: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "cocos-bridge", version: "1.0.0" }
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 处理握手初始化
|
||||
if (method === "initialize") {
|
||||
sendToAI({
|
||||
jsonrpc: "2.0",
|
||||
id: id,
|
||||
result: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "cocos-bridge", version: "1.0.0" },
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'tools/list') {
|
||||
// 使用 GET 获取列表
|
||||
forwardToCocos('/list-tools', null, id, 'GET');
|
||||
return;
|
||||
}
|
||||
// 获取工具列表
|
||||
if (method === "tools/list") {
|
||||
forwardToCocos("/list-tools", null, id, "GET");
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'tools/call') {
|
||||
// 使用 POST 执行工具
|
||||
forwardToCocos('/call-tool', {
|
||||
name: params.name,
|
||||
arguments: params.arguments
|
||||
}, id, 'POST');
|
||||
return;
|
||||
}
|
||||
// 执行具体工具
|
||||
if (method === "tools/call") {
|
||||
forwardToCocos(
|
||||
"/call-tool",
|
||||
{
|
||||
name: params.name,
|
||||
arguments: params.arguments,
|
||||
},
|
||||
id,
|
||||
"POST",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} });
|
||||
// 默认空响应
|
||||
if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} });
|
||||
}
|
||||
|
||||
function forwardToCocos(path, payload, id, method = 'POST') {
|
||||
const postData = payload ? JSON.stringify(payload) : '';
|
||||
|
||||
const options = {
|
||||
hostname: '127.0.0.1',
|
||||
port: COCOS_PORT,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
};
|
||||
/**
|
||||
* 将请求通过 HTTP 转发给 Cocos Creator 插件
|
||||
* @param {string} path API 路径
|
||||
* @param {Object|null} payload 发送的数据体
|
||||
* @param {string|number} id RPC 请求标识符
|
||||
* @param {string} method HTTP 方法 (默认 POST)
|
||||
*/
|
||||
function forwardToCocos(path, payload, id, method = "POST") {
|
||||
const postData = payload ? JSON.stringify(payload) : "";
|
||||
|
||||
if (postData) {
|
||||
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
||||
}
|
||||
const options = {
|
||||
hostname: "127.0.0.1",
|
||||
port: COCOS_PORT,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
};
|
||||
|
||||
const request = http.request(options, (res) => {
|
||||
let resData = '';
|
||||
res.on('data', d => resData += d);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const cocosRes = JSON.parse(resData);
|
||||
|
||||
// 检查关键字段
|
||||
if (path === '/list-tools' && !cocosRes.tools) {
|
||||
// 如果报错,把 Cocos 返回的所有内容打印到 Trae 的 stderr 日志里
|
||||
debugLog(`CRITICAL: Cocos returned no tools. Received: ${resData}`);
|
||||
sendError(id, -32603, "Invalid Cocos response: missing tools array");
|
||||
} else {
|
||||
sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes });
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(`JSON Parse Error. Cocos Sent: ${resData}`);
|
||||
sendError(id, -32603, "Cocos returned non-JSON data");
|
||||
}
|
||||
});
|
||||
});
|
||||
if (postData) {
|
||||
options.headers["Content-Length"] = Buffer.byteLength(postData);
|
||||
}
|
||||
|
||||
request.on('error', (e) => {
|
||||
debugLog(`Cocos is offline: ${e.message}`);
|
||||
sendError(id, -32000, "Cocos Plugin Offline");
|
||||
});
|
||||
const request = http.request(options, (res) => {
|
||||
let resData = "";
|
||||
res.on("data", (d) => (resData += d));
|
||||
res.on("end", () => {
|
||||
try {
|
||||
const cocosRes = JSON.parse(resData);
|
||||
|
||||
if (postData) request.write(postData);
|
||||
request.end();
|
||||
// 检查关键字段,确保 Cocos 插件返回了期望的数据格式
|
||||
if (path === "/list-tools" && !cocosRes.tools) {
|
||||
debugLog(`致命错误: Cocos 未返回工具列表。接收内容: ${resData}`);
|
||||
sendError(id, -32603, "Cocos 响应无效:缺少 tools 数组");
|
||||
} else {
|
||||
sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes });
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(`JSON 解析错误。Cocos 发送内容: ${resData}`);
|
||||
sendError(id, -32603, "Cocos 返回了非 JSON 数据");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
request.on("error", (e) => {
|
||||
debugLog(`Cocos 插件已离线: ${e.message}`);
|
||||
sendError(id, -32000, "Cocos 插件离线");
|
||||
});
|
||||
|
||||
if (postData) request.write(postData);
|
||||
request.end();
|
||||
}
|
||||
|
||||
function sendToAI(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
|
||||
/**
|
||||
* 将结果发送给 AI (通过标准输出)
|
||||
* @param {Object} obj 结果对象
|
||||
*/
|
||||
function sendToAI(obj) {
|
||||
process.stdout.write(JSON.stringify(obj) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 RPC 错误响应
|
||||
* @param {string|number} id RPC 请求标识符
|
||||
* @param {number} code 错误码
|
||||
* @param {string} message 错误消息
|
||||
*/
|
||||
function sendError(id, code, message) {
|
||||
sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } });
|
||||
}
|
||||
sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } });
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
"main": "main.js",
|
||||
"scene-script": "scene-script.js",
|
||||
"main-menu": {
|
||||
"MCP Bridge/Open Panel": {
|
||||
"MCP 桥接器/开启测试面板": {
|
||||
"message": "mcp-bridge:open-test-panel"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"main": "panel/index.js",
|
||||
"type": "dockable",
|
||||
"title": "MCP Test Panel",
|
||||
"title": "MCP 测试面板",
|
||||
"width": 400,
|
||||
"height": 300
|
||||
},
|
||||
|
||||
128
panel/index.js
128
panel/index.js
@@ -1,23 +1,53 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* MCP Bridge 插件面板脚本
|
||||
* 负责处理面板 UI 交互、与主进程通信以及提供测试工具界面。
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const { IpcUi } = require("../dist/IpcUi");
|
||||
|
||||
Editor.Panel.extend({
|
||||
/**
|
||||
* 面板 CSS 样式
|
||||
*/
|
||||
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||
|
||||
/**
|
||||
* 面板 HTML 模板
|
||||
*/
|
||||
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||
|
||||
/**
|
||||
* 监听来自主进程的消息
|
||||
*/
|
||||
messages: {
|
||||
/**
|
||||
* 接收并渲染日志
|
||||
* @param {Object} event IPC 事件对象
|
||||
* @param {Object} log 日志数据
|
||||
*/
|
||||
"mcp-bridge:on-log"(event, log) {
|
||||
this.renderLog(log);
|
||||
},
|
||||
|
||||
/**
|
||||
* 服务器状态变更通知
|
||||
* @param {Object} event IPC 事件对象
|
||||
* @param {Object} config 服务器配置
|
||||
*/
|
||||
"mcp-bridge:state-changed"(event, config) {
|
||||
this.updateUI(config.active);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 面板就绪回调,进行 DOM 绑定与事件初始化
|
||||
*/
|
||||
ready() {
|
||||
const root = this.shadowRoot;
|
||||
// 获取 DOM 元素
|
||||
// 获取 DOM 元素映射
|
||||
const els = {
|
||||
port: root.querySelector("#portInput"),
|
||||
btnToggle: root.querySelector("#btnToggle"),
|
||||
@@ -41,7 +71,7 @@ Editor.Panel.extend({
|
||||
resizer: root.querySelector("#testResizer"),
|
||||
};
|
||||
|
||||
// 1. 初始化状态
|
||||
// 1. 初始化服务器状态与配置
|
||||
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||
if (data) {
|
||||
els.port.value = data.config.port;
|
||||
@@ -52,10 +82,10 @@ Editor.Panel.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化 IPC UI
|
||||
// 初始化 IPC 调试专用 UI (由 TypeScript 编写并编译到 dist)
|
||||
new IpcUi(root);
|
||||
|
||||
// 2. 标签切换
|
||||
// 2. 标签页切换逻辑
|
||||
els.tabMain.addEventListener("confirm", () => {
|
||||
els.tabMain.classList.add("active");
|
||||
els.tabTest.classList.remove("active");
|
||||
@@ -64,6 +94,7 @@ Editor.Panel.extend({
|
||||
els.panelTest.classList.remove("active");
|
||||
els.panelIpc.classList.remove("active");
|
||||
});
|
||||
|
||||
els.tabTest.addEventListener("confirm", () => {
|
||||
els.tabTest.classList.add("active");
|
||||
els.tabMain.classList.remove("active");
|
||||
@@ -71,8 +102,9 @@ Editor.Panel.extend({
|
||||
els.panelTest.classList.add("active");
|
||||
els.panelMain.classList.remove("active");
|
||||
els.panelIpc.classList.remove("active");
|
||||
this.fetchTools(els);
|
||||
this.fetchTools(els); // 切换到测试页时自动拉取工具列表
|
||||
});
|
||||
|
||||
els.tabIpc.addEventListener("confirm", () => {
|
||||
els.tabIpc.classList.add("active");
|
||||
els.tabMain.classList.remove("active");
|
||||
@@ -82,39 +114,42 @@ Editor.Panel.extend({
|
||||
els.panelTest.classList.remove("active");
|
||||
});
|
||||
|
||||
// 3. 基础功能
|
||||
// 3. 基础控制按钮逻辑
|
||||
els.btnToggle.addEventListener("confirm", () => {
|
||||
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
|
||||
});
|
||||
|
||||
root.querySelector("#btnClear").addEventListener("confirm", () => {
|
||||
els.logView.innerHTML = "";
|
||||
Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
||||
});
|
||||
|
||||
root.querySelector("#btnCopy").addEventListener("confirm", () => {
|
||||
require("electron").clipboard.writeText(els.logView.innerText);
|
||||
Editor.success("日志已复制");
|
||||
Editor.success("日志已复制到剪贴板");
|
||||
});
|
||||
|
||||
els.autoStart.addEventListener("change", (e) => {
|
||||
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
|
||||
});
|
||||
|
||||
// 4. 测试页功能
|
||||
// 4. API 测试页交互逻辑
|
||||
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
|
||||
els.clearBtn.addEventListener("confirm", () => {
|
||||
els.result.value = "";
|
||||
});
|
||||
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
||||
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
||||
// 添加 API 探查功能
|
||||
|
||||
// API 探查功能 (辅助开发者发现可用内部 IPC)
|
||||
const probeBtn = root.querySelector("#probeApisBtn");
|
||||
if (probeBtn) {
|
||||
probeBtn.addEventListener("confirm", () => {
|
||||
Editor.Ipc.sendToMain("mcp-bridge:inspect-apis");
|
||||
els.result.value = "探查指令已发送。请查看编辑器控制台日志。";
|
||||
els.result.value = "API 探查指令已发送。请查看编辑器控制台 (Console) 获取详细报告。";
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 【修复】拖拽逻辑
|
||||
// 5. 测试页分栏拖拽缩放逻辑
|
||||
if (els.resizer && els.left) {
|
||||
els.resizer.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
@@ -135,8 +170,13 @@ Editor.Panel.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从本地服务器获取 MCP 工具列表并渲染
|
||||
* @param {Object} els DOM 元素映射
|
||||
*/
|
||||
fetchTools(els) {
|
||||
const url = `http://localhost:${els.port.value}/list-tools`;
|
||||
els.result.value = "正在获取工具列表...";
|
||||
fetch(url)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
@@ -154,64 +194,86 @@ Editor.Panel.extend({
|
||||
};
|
||||
els.toolsList.appendChild(item);
|
||||
});
|
||||
// 保存工具映射表,以便后续检索
|
||||
this.toolsMap = toolsMap;
|
||||
els.result.value = `成功加载 ${data.tools.length} 个工具。`;
|
||||
els.result.value = `成功:加载了 ${data.tools.length} 个工具。`;
|
||||
})
|
||||
.catch((e) => {
|
||||
els.result.value = "Error: " + e.message;
|
||||
els.result.value = "获取失败: " + e.message;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 在面板中展示工具的详细描述与参数定义
|
||||
* @param {Object} els DOM 元素映射
|
||||
* @param {Object} tool 工具定义对象
|
||||
*/
|
||||
showToolDescription(els, tool) {
|
||||
if (!tool) {
|
||||
els.toolDescription.textContent = "选择工具查看说明";
|
||||
els.toolDescription.textContent = "选择工具以查看说明";
|
||||
return;
|
||||
}
|
||||
|
||||
let description = tool.description || "无描述";
|
||||
let description = tool.description || "暂无描述";
|
||||
let inputSchema = tool.inputSchema;
|
||||
|
||||
let details = [];
|
||||
if (inputSchema && inputSchema.properties) {
|
||||
details.push("参数说明:");
|
||||
details.push("<b>参数说明:</b>");
|
||||
for (const [key, prop] of Object.entries(inputSchema.properties)) {
|
||||
let propDesc = `- ${key}`;
|
||||
let propDesc = `- <code>${key}</code>`;
|
||||
if (prop.description) {
|
||||
propDesc += `: ${prop.description}`;
|
||||
}
|
||||
if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) {
|
||||
propDesc += " (必填)";
|
||||
propDesc += " <span style='color:#f44'>(必填)</span>";
|
||||
}
|
||||
details.push(propDesc);
|
||||
}
|
||||
}
|
||||
|
||||
els.toolDescription.innerHTML = `${description}<br><br>${details.join('<br>')}`;
|
||||
els.toolDescription.innerHTML = `${description}<br><br>${details.join("<br>")}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行工具测试请求
|
||||
* @param {Object} els DOM 元素映射
|
||||
*/
|
||||
runTest(els) {
|
||||
const url = `http://localhost:${els.port.value}/call-tool`;
|
||||
const body = { name: els.toolName.value, arguments: JSON.parse(els.toolParams.value || "{}") };
|
||||
els.result.value = "正在测试...";
|
||||
let args;
|
||||
try {
|
||||
args = JSON.parse(els.toolParams.value || "{}");
|
||||
} catch (e) {
|
||||
els.result.value = "JSON 格式错误: " + e.message;
|
||||
return;
|
||||
}
|
||||
|
||||
const body = { name: els.toolName.value, arguments: args };
|
||||
els.result.value = "正在发送请求...";
|
||||
fetch(url, { method: "POST", body: JSON.stringify(body) })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
els.result.value = JSON.stringify(d, null, 2);
|
||||
})
|
||||
.catch((e) => {
|
||||
els.result.value = "Error: " + e.message;
|
||||
els.result.value = "测试异常: " + e.message;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取指定工具的示例参数
|
||||
* @param {string} name 工具名称
|
||||
* @returns {Object} 示例参数对象
|
||||
*/
|
||||
getExample(name) {
|
||||
const examples = {
|
||||
set_node_name: { id: "UUID", newName: "Hello" },
|
||||
update_node_transform: { id: "UUID", x: 0, y: 0, color: "#FF0000" },
|
||||
create_node: { name: "Node", type: "sprite", parentId: "" },
|
||||
set_node_name: { id: "节点-UUID", newName: "新名称" },
|
||||
update_node_transform: { id: "节点-UUID", x: 0, y: 0, color: "#FF0000" },
|
||||
create_node: { name: "新节点", type: "sprite", parentId: "" },
|
||||
open_scene: { url: "db://assets/Scene.fire" },
|
||||
open_prefab: { url: "db://assets/MyPrefab.prefab" },
|
||||
manage_editor: { action: "get_selection" },
|
||||
find_gameobjects: { conditions: { name: "Node", active: true }, recursive: true },
|
||||
find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true },
|
||||
manage_material: {
|
||||
action: "create",
|
||||
path: "db://assets/materials/NewMaterial.mat",
|
||||
@@ -225,7 +287,7 @@ Editor.Panel.extend({
|
||||
execute_menu_item: { menuPath: "Assets/Create/Folder" },
|
||||
apply_text_edits: {
|
||||
filePath: "db://assets/scripts/TestScript.ts",
|
||||
edits: [{ type: "insert", position: 0, text: "// Test comment\n" }],
|
||||
edits: [{ type: "insert", position: 0, text: "// 测试注释\n" }],
|
||||
},
|
||||
read_console: { limit: 10, type: "log" },
|
||||
validate_script: { filePath: "db://assets/scripts/TestScript.ts" },
|
||||
@@ -233,6 +295,10 @@ Editor.Panel.extend({
|
||||
return examples[name] || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* 将日志条目渲染至面板控制台
|
||||
* @param {Object} log 日志对象
|
||||
*/
|
||||
renderLog(log) {
|
||||
const view = this.shadowRoot.querySelector("#logConsole");
|
||||
if (!view) return;
|
||||
@@ -244,6 +310,10 @@ Editor.Panel.extend({
|
||||
if (atBottom) view.scrollTop = view.scrollHeight;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据服务器运行状态更新 UI 按钮文字与样式
|
||||
* @param {boolean} active 服务器是否处于激活状态
|
||||
*/
|
||||
updateUI(active) {
|
||||
const btn = this.shadowRoot.querySelector("#btnToggle");
|
||||
if (!btn) return;
|
||||
|
||||
2081
scene-script.js
2081
scene-script.js
File diff suppressed because it is too large
Load Diff
@@ -1,107 +1,106 @@
|
||||
|
||||
// @ts-ignore
|
||||
const fs = require('fs');
|
||||
const fs = require("fs");
|
||||
// @ts-ignore
|
||||
const path = require('path');
|
||||
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 [];
|
||||
}
|
||||
/**
|
||||
* 获取所有 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 管理器] 找不到文档文件: ${docPath}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(docPath, 'utf-8');
|
||||
const messages: any[] = [];
|
||||
const content = fs.readFileSync(docPath, "utf-8");
|
||||
const messages: any[] = [];
|
||||
|
||||
// 正则匹配 ### `message-name`
|
||||
const regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g;
|
||||
let match;
|
||||
// 正则匹配 ### `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];
|
||||
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 purposeMatch = body.match(/- \*\*用途\*\*: (.*)/);
|
||||
const description = purposeMatch ? purposeMatch[1].trim() : "无描述";
|
||||
|
||||
// 解析参数
|
||||
const paramsMatch = body.match(/- \*\*参数\*\*: (.*)/);
|
||||
const params = paramsMatch ? paramsMatch[1].trim() : "无";
|
||||
// 解析参数
|
||||
const paramsMatch = body.match(/- \*\*参数\*\*: (.*)/);
|
||||
const params = paramsMatch ? paramsMatch[1].trim() : "无";
|
||||
|
||||
// 解析返回值
|
||||
const returnMatch = body.match(/- \*\*返回值\*\*: (.*)/);
|
||||
const returns = returnMatch ? returnMatch[1].trim() : "无";
|
||||
// 解析返回值
|
||||
const returnMatch = body.match(/- \*\*返回值\*\*: (.*)/);
|
||||
const returns = returnMatch ? returnMatch[1].trim() : "无";
|
||||
|
||||
// 解析类型
|
||||
const typeMatch = body.match(/- \*\*类型\*\*: (.*)/);
|
||||
const type = typeMatch ? typeMatch[1].trim() : "未定义";
|
||||
// 解析类型
|
||||
const typeMatch = body.match(/- \*\*类型\*\*: (.*)/);
|
||||
const type = typeMatch ? typeMatch[1].trim() : "未定义";
|
||||
|
||||
// 解析状态
|
||||
const statusMatch = body.match(/- \*\*状态\*\*: (.*)/);
|
||||
const status = statusMatch ? statusMatch[1].trim() : "未测试";
|
||||
// 解析状态
|
||||
const statusMatch = body.match(/- \*\*状态\*\*: (.*)/);
|
||||
const status = statusMatch ? statusMatch[1].trim() : "未测试";
|
||||
|
||||
// 过滤掉广播事件和渲染进程监听的事件
|
||||
if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") {
|
||||
continue;
|
||||
}
|
||||
// 过滤掉广播事件和渲染进程监听的事件
|
||||
if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") {
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
name,
|
||||
description,
|
||||
params,
|
||||
returns,
|
||||
type,
|
||||
status
|
||||
});
|
||||
}
|
||||
messages.push({
|
||||
name,
|
||||
description,
|
||||
params,
|
||||
returns,
|
||||
type,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
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
|
||||
/**
|
||||
* 测试发送 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)
|
||||
// @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}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
if (Editor.Ipc.sendToMain) {
|
||||
// @ts-ignore
|
||||
Editor.Ipc.sendToMain(name, args);
|
||||
resolve({ success: true, message: "消息已发送 (sendToMain)" });
|
||||
} else {
|
||||
resolve({ success: false, message: "Editor.Ipc.sendToMain 不可用" });
|
||||
}
|
||||
} catch (e: any) {
|
||||
resolve({ success: false, message: `错误: ${e.message}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
354
src/IpcUi.ts
354
src/IpcUi.ts
@@ -1,192 +1,226 @@
|
||||
|
||||
// @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;
|
||||
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();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
* @param root Shadow UI 根节点
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* 绑定 UI 事件
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 扫描 IPC 消息
|
||||
*/
|
||||
private scanMessages() {
|
||||
this.log("正在扫描 IPC 消息...");
|
||||
// @ts-ignore
|
||||
Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", (err: any, msgs: any[]) => {
|
||||
if (err) {
|
||||
this.log(`扫描错误: ${err}`);
|
||||
return;
|
||||
}
|
||||
if (msgs) {
|
||||
this.allMessages = msgs;
|
||||
this.filterMessages();
|
||||
this.log(`找到 ${msgs.length} 条消息。`);
|
||||
} else {
|
||||
this.log("未找到任何消息。");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private filterMessages() {
|
||||
if (!this.allMessages) return;
|
||||
const filter = this.filterSelect ? this.filterSelect.value : "all";
|
||||
/**
|
||||
* 根据当前选择器过滤消息列表
|
||||
*/
|
||||
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 === "未测试");
|
||||
}
|
||||
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);
|
||||
}
|
||||
this.renderList(filtered);
|
||||
}
|
||||
|
||||
private renderList(msgs: any[]) {
|
||||
if (!this.ipcList) return;
|
||||
this.ipcList.innerHTML = "";
|
||||
/**
|
||||
* 渲染消息列表 UI
|
||||
* @param msgs 消息对象数组
|
||||
*/
|
||||
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";
|
||||
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";
|
||||
// 复选框
|
||||
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";
|
||||
// 内容区域
|
||||
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
|
||||
let statusColor = "#888"; // 未测试
|
||||
if (msg.status === "可用")
|
||||
statusColor = "#4CAF50"; // 绿色
|
||||
else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // 红色
|
||||
|
||||
content.innerHTML = `
|
||||
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>
|
||||
<div style="color: #888;">${msg.description || "无描述"}</div>
|
||||
<div style="color: #666; font-size: 10px;">参数: ${msg.params || "无"}</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);
|
||||
});
|
||||
// 执行按钮
|
||||
const btnRun = document.createElement("ui-button");
|
||||
btnRun.innerText = "执行";
|
||||
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);
|
||||
});
|
||||
}
|
||||
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"));
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 测试所有选中的 IPC 消息
|
||||
*/
|
||||
private async testSelected() {
|
||||
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
|
||||
const toTest: string[] = [];
|
||||
checkboxes.forEach((cb: any) => {
|
||||
// 在 Cocos 2.x 中, ui-checkbox 的值是布尔型
|
||||
if (cb.checked || cb.value === true) {
|
||||
toTest.push(cb.getAttribute("data-name"));
|
||||
}
|
||||
});
|
||||
|
||||
if (toTest.length === 0) {
|
||||
this.log("No messages selected.");
|
||||
return;
|
||||
}
|
||||
if (toTest.length === 0) {
|
||||
this.log("未选择任何消息。");
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`Starting batch test for ${toTest.length} messages...`);
|
||||
for (const name of toTest) {
|
||||
await this.runTest(name);
|
||||
}
|
||||
this.log("Batch test completed.");
|
||||
}
|
||||
this.log(`开始批量测试 ${toTest.length} 条消息...`);
|
||||
for (const name of toTest) {
|
||||
await this.runTest(name);
|
||||
}
|
||||
this.log("批量测试完成。");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 运行单个测试请求
|
||||
* @param name 消息名称
|
||||
*/
|
||||
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(`[错误] 无效的 JSON 参数: ${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();
|
||||
});
|
||||
});
|
||||
}
|
||||
this.log(`正在测试: ${name},参数: ${JSON.stringify(params)}...`);
|
||||
// @ts-ignore
|
||||
Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name, params }, (err: any, result: any) => {
|
||||
if (err) {
|
||||
this.log(`[${name}] 失败: ${err}`);
|
||||
} else {
|
||||
this.log(`[${name}] 成功: ${JSON.stringify(result)}`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private toggleAll(checked: boolean) {
|
||||
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
|
||||
checkboxes.forEach((cb: any) => {
|
||||
cb.value = checked;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 全选/取消全选
|
||||
* @param checked 是否选中
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 输出日志到界面
|
||||
* @param msg 日志消息
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user