feat: 完成第一阶段核心功能实现\n\n- 添加 manage_components 工具\n- 添加 manage_script 工具(默认创建TS脚本)\n- 添加 batch_execute 工具\n- 添加 manage_asset 工具\n- 修复面板布局问题\n- 添加默认父目录创建功能\n- 更新 README 文档\n- 创建 DEVELOPMENT 开发文档
This commit is contained in:
420
DEVELOPMENT.md
Normal file
420
DEVELOPMENT.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# MCP Bridge 插件开发流程文档
|
||||||
|
|
||||||
|
本文档记录了 MCP Bridge 插件的完整开发流程,包括核心架构设计、功能实现、测试与调试等各个阶段。
|
||||||
|
|
||||||
|
## 1. 项目初始化
|
||||||
|
|
||||||
|
### 1.1 目录结构搭建
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp-bridge/
|
||||||
|
├── main.js # 插件主入口
|
||||||
|
├── scene-script.js # 场景脚本
|
||||||
|
├── mcp-proxy.js # MCP 代理
|
||||||
|
├── README.md # 项目说明
|
||||||
|
├── DEVELOPMENT.md # 开发流程文档
|
||||||
|
├── package.json # 插件配置
|
||||||
|
└── panel/ # 面板目录
|
||||||
|
├── index.html # 面板界面
|
||||||
|
└── index.js # 面板逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 插件配置
|
||||||
|
|
||||||
|
在 `package.json` 中配置插件信息:
|
||||||
|
|
||||||
|
```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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 核心架构设计
|
||||||
|
|
||||||
|
### 2.1 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────┐ HTTP ┌────────────────────┐ IPC ┌────────────────────┐
|
||||||
|
│ 外部 AI 工具 │ ──────────> │ main.js (HTTP服务) │ ─────────> │ scene-script.js │
|
||||||
|
│ (Cursor/VS Code) │ <──────── │ (MCP 协议处理) │ <──────── │ (场景操作执行) │
|
||||||
|
└────────────────────┘ JSON └────────────────────┘ JSON └────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 核心模块
|
||||||
|
|
||||||
|
1. **HTTP 服务模块**:处理外部请求,解析 MCP 协议
|
||||||
|
2. **MCP 工具模块**:实现各种操作工具
|
||||||
|
3. **场景操作模块**:执行场景相关操作
|
||||||
|
4. **资源管理模块**:处理脚本和资源文件
|
||||||
|
5. **面板界面模块**:提供用户交互界面
|
||||||
|
|
||||||
|
## 3. 功能模块实现
|
||||||
|
|
||||||
|
### 3.1 HTTP 服务实现
|
||||||
|
|
||||||
|
在 `main.js` 中实现 HTTP 服务:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
startServer(port) {
|
||||||
|
try {
|
||||||
|
const http = require('http');
|
||||||
|
mcpServer = http.createServer((req, res) => {
|
||||||
|
// 处理请求...
|
||||||
|
});
|
||||||
|
mcpServer.listen(port, () => {
|
||||||
|
addLog("success", `MCP Server running at http://127.0.0.1:${port}`);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addLog("error", `Failed to start server: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 MCP 工具注册
|
||||||
|
|
||||||
|
在 `/list-tools` 接口中注册工具:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: "get_selected_node",
|
||||||
|
description: "获取当前选中的节点",
|
||||||
|
parameters: []
|
||||||
|
},
|
||||||
|
// 其他工具...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 场景操作实现
|
||||||
|
|
||||||
|
在 `scene-script.js` 中实现场景相关操作:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const sceneScript = {
|
||||||
|
'create-node'(params, callback) {
|
||||||
|
// 创建节点逻辑...
|
||||||
|
},
|
||||||
|
'set-property'(params, callback) {
|
||||||
|
// 设置属性逻辑...
|
||||||
|
},
|
||||||
|
// 其他操作...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 脚本管理实现
|
||||||
|
|
||||||
|
在 `main.js` 中实现脚本管理功能:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
manageScript(args, callback) {
|
||||||
|
const { action, path, content } = args;
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
// 确保父目录存在
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
// 创建 TypeScript 脚本
|
||||||
|
Editor.assetdb.create(path, content || `const { ccclass, property } = cc._decorator;
|
||||||
|
|
||||||
|
@ccclass
|
||||||
|
export default class NewScript extends cc.Component {
|
||||||
|
// LIFE-CYCLE CALLBACKS:
|
||||||
|
|
||||||
|
onLoad () {}
|
||||||
|
|
||||||
|
start () {}
|
||||||
|
|
||||||
|
update (dt) {}
|
||||||
|
}`, (err) => {
|
||||||
|
callback(err, err ? null : `Script created at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
// 其他操作...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 批处理执行实现
|
||||||
|
|
||||||
|
在 `main.js` 中实现批处理功能:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
batchExecute(args, callback) {
|
||||||
|
const { operations } = args;
|
||||||
|
const results = [];
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
|
if (!operations || operations.length === 0) {
|
||||||
|
return callback("No operations provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
operations.forEach((operation, index) => {
|
||||||
|
this.handleMcpCall(operation.tool, operation.params, (err, result) => {
|
||||||
|
results[index] = { tool: operation.tool, error: err, result: result };
|
||||||
|
completed++;
|
||||||
|
|
||||||
|
if (completed === operations.length) {
|
||||||
|
callback(null, results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 资产管理实现
|
||||||
|
|
||||||
|
在 `main.js` 中实现资产管理功能:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
manageAsset(args, callback) {
|
||||||
|
const { action, path, targetPath, content } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
// 确保父目录存在
|
||||||
|
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, content || '', (err) => {
|
||||||
|
callback(err, err ? null : `Asset created at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
// 其他操作...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 面板界面实现
|
||||||
|
|
||||||
|
在 `panel/index.html` 中实现标签页界面:
|
||||||
|
|
||||||
|
```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 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>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 测试与调试
|
||||||
|
|
||||||
|
### 4.1 本地测试
|
||||||
|
|
||||||
|
1. **启动服务**:在面板中点击 "Start" 按钮
|
||||||
|
2. **测试工具**:在 "Tool Test" 标签页中测试各个工具
|
||||||
|
3. **查看日志**:在主面板中查看操作日志
|
||||||
|
|
||||||
|
### 4.2 常见错误及修复
|
||||||
|
|
||||||
|
#### 4.2.1 面板加载错误
|
||||||
|
|
||||||
|
**错误信息**:`Panel info not found for panel mcp-bridge`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 检查 `package.json` 中的面板配置
|
||||||
|
- 确保 `panel` 字段配置正确,移除冲突的 `panels` 字段
|
||||||
|
|
||||||
|
#### 4.2.2 资源创建错误
|
||||||
|
|
||||||
|
**错误信息**:`Parent path ... is not exists`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 在创建资源前添加目录检查和创建逻辑
|
||||||
|
- 使用 `fs.mkdirSync(dirPath, { recursive: true })` 递归创建目录
|
||||||
|
|
||||||
|
#### 4.2.3 脚本语法错误
|
||||||
|
|
||||||
|
**错误信息**:`SyntaxError: Invalid or unexpected token`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 使用模板字符串(反引号)处理多行字符串
|
||||||
|
- 避免变量名冲突
|
||||||
|
|
||||||
|
### 4.3 性能优化
|
||||||
|
|
||||||
|
1. **批处理执行**:使用 `batch_execute` 工具减少 HTTP 请求次数
|
||||||
|
2. **异步操作**:使用回调函数处理异步操作,避免阻塞主线程
|
||||||
|
3. **错误处理**:完善错误处理机制,提高插件稳定性
|
||||||
|
|
||||||
|
## 5. 文档编写
|
||||||
|
|
||||||
|
### 5.1 README.md
|
||||||
|
|
||||||
|
- 项目简介
|
||||||
|
- 功能特性
|
||||||
|
- 安装使用
|
||||||
|
- API 文档
|
||||||
|
- 技术实现
|
||||||
|
|
||||||
|
### 5.2 API 文档
|
||||||
|
|
||||||
|
为每个 MCP 工具编写详细的 API 文档,包括:
|
||||||
|
- 工具名称
|
||||||
|
- 功能描述
|
||||||
|
- 参数说明
|
||||||
|
- 返回值格式
|
||||||
|
- 使用示例
|
||||||
|
|
||||||
|
### 5.3 开发文档
|
||||||
|
|
||||||
|
- 项目架构
|
||||||
|
- 开发流程
|
||||||
|
- 代码规范
|
||||||
|
- 贡献指南
|
||||||
|
|
||||||
|
## 6. 部署与使用
|
||||||
|
|
||||||
|
### 6.1 部署方式
|
||||||
|
|
||||||
|
1. **本地部署**:将插件复制到 Cocos Creator 项目的 `packages` 目录
|
||||||
|
2. **远程部署**:通过版本控制系统管理插件代码
|
||||||
|
|
||||||
|
### 6.2 使用流程
|
||||||
|
|
||||||
|
1. **启动服务**:
|
||||||
|
- 打开 Cocos Creator 编辑器
|
||||||
|
- 选择 `Packages/MCP Bridge/Open Test Panel`
|
||||||
|
- 点击 "Start" 按钮启动服务
|
||||||
|
|
||||||
|
2. **连接 AI 编辑器**:
|
||||||
|
- 在 AI 编辑器中配置 MCP 代理
|
||||||
|
- 使用 `node [项目路径]/packages/mcp-bridge/mcp-proxy.js` 作为命令
|
||||||
|
|
||||||
|
3. **执行操作**:
|
||||||
|
- 通过 AI 编辑器发送 MCP 请求
|
||||||
|
- 或在测试面板中直接测试工具
|
||||||
|
|
||||||
|
### 6.3 配置选项
|
||||||
|
|
||||||
|
- **端口设置**:默认 3456,可自定义
|
||||||
|
- **自动启动**:支持编辑器启动时自动开启服务
|
||||||
|
|
||||||
|
## 7. 功能扩展
|
||||||
|
|
||||||
|
### 7.1 添加新工具
|
||||||
|
|
||||||
|
1. **在 `main.js` 中注册工具**:
|
||||||
|
- 在 `/list-tools` 响应中添加工具定义
|
||||||
|
- 在 `handleMcpCall` 函数中添加处理逻辑
|
||||||
|
|
||||||
|
2. **在面板中添加示例**:
|
||||||
|
- 在 `panel/index.js` 中添加工具示例参数
|
||||||
|
- 更新工具列表
|
||||||
|
|
||||||
|
3. **更新文档**:
|
||||||
|
- 在 `README.md` 中添加工具文档
|
||||||
|
- 更新功能特性列表
|
||||||
|
|
||||||
|
### 7.2 集成新 API
|
||||||
|
|
||||||
|
1. **了解 Cocos Creator API**:
|
||||||
|
- 查阅 Cocos Creator 编辑器 API 文档
|
||||||
|
- 了解场景脚本 API
|
||||||
|
|
||||||
|
2. **实现集成**:
|
||||||
|
- 在 `main.js` 或 `scene-script.js` 中添加对应功能
|
||||||
|
- 处理异步操作和错误情况
|
||||||
|
|
||||||
|
3. **测试验证**:
|
||||||
|
- 编写测试用例
|
||||||
|
- 验证功能正确性
|
||||||
|
|
||||||
|
## 8. 版本管理
|
||||||
|
|
||||||
|
### 8.1 版本控制
|
||||||
|
|
||||||
|
- 使用 Git 进行版本控制
|
||||||
|
- 遵循语义化版本规范
|
||||||
|
|
||||||
|
### 8.2 发布流程
|
||||||
|
|
||||||
|
1. **代码审查**:检查代码质量和功能完整性
|
||||||
|
2. **测试验证**:确保所有功能正常工作
|
||||||
|
3. **文档更新**:更新 README 和相关文档
|
||||||
|
4. **版本发布**:标记版本号并发布
|
||||||
|
|
||||||
|
## 9. 技术栈
|
||||||
|
|
||||||
|
- **JavaScript**:主要开发语言
|
||||||
|
- **Node.js**:HTTP 服务和文件操作
|
||||||
|
- **Cocos Creator API**:编辑器功能集成
|
||||||
|
- **HTML/CSS**:面板界面
|
||||||
|
- **MCP 协议**:与 AI 工具通信
|
||||||
|
|
||||||
|
## 10. 最佳实践
|
||||||
|
|
||||||
|
1. **代码组织**:
|
||||||
|
- 模块化设计,职责分离
|
||||||
|
- 合理使用回调函数处理异步操作
|
||||||
|
|
||||||
|
2. **错误处理**:
|
||||||
|
- 完善的错误捕获和处理
|
||||||
|
- 详细的错误日志记录
|
||||||
|
|
||||||
|
3. **用户体验**:
|
||||||
|
- 直观的面板界面
|
||||||
|
- 实时的操作反馈
|
||||||
|
- 详细的日志信息
|
||||||
|
|
||||||
|
4. **安全性**:
|
||||||
|
- 验证输入参数
|
||||||
|
- 防止路径遍历攻击
|
||||||
|
- 限制服务访问范围
|
||||||
|
|
||||||
|
## 11. 总结
|
||||||
|
|
||||||
|
MCP Bridge 插件通过 HTTP 服务和 MCP 协议,为外部 AI 工具提供了与 Cocos Creator 编辑器交互的能力。插件支持场景操作、资源管理、组件管理、脚本管理等多种功能,为 Cocos Creator 项目的开发和自动化提供了有力的支持。
|
||||||
|
|
||||||
|
通过本文档的开发流程,我们构建了一个功能完整、稳定可靠的 MCP Bridge 插件,为 Cocos Creator 生态系统增添了新的工具和能力。
|
||||||
41
README.md
41
README.md
@@ -11,6 +11,10 @@
|
|||||||
- **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能
|
- **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能
|
||||||
- **场景节点操作**: 获取、创建、修改场景中的节点
|
- **场景节点操作**: 获取、创建、修改场景中的节点
|
||||||
- **资源管理**: 创建场景、预制体,打开指定资源
|
- **资源管理**: 创建场景、预制体,打开指定资源
|
||||||
|
- **组件管理**: 添加、删除、获取节点组件
|
||||||
|
- **脚本管理**: 创建、删除、读取、写入脚本文件
|
||||||
|
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
|
||||||
|
- **资产管理**: 创建、删除、移动、获取资源信息
|
||||||
- **实时日志**: 提供详细的操作日志记录和展示
|
- **实时日志**: 提供详细的操作日志记录和展示
|
||||||
- **自动启动**: 支持编辑器启动时自动开启服务
|
- **自动启动**: 支持编辑器启动时自动开启服务
|
||||||
|
|
||||||
@@ -126,6 +130,43 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `parentId`: 父节点 UUID (可选,不传则挂在场景根部)
|
- `parentId`: 父节点 UUID (可选,不传则挂在场景根部)
|
||||||
- `type`: 节点预设类型(`empty`, `sprite`, `label`, `canvas`)
|
- `type`: 节点预设类型(`empty`, `sprite`, `label`, `canvas`)
|
||||||
|
|
||||||
|
### 10. manage_components
|
||||||
|
|
||||||
|
- **描述**: 管理节点组件
|
||||||
|
- **参数**:
|
||||||
|
- `nodeId`: 节点 UUID
|
||||||
|
- `action`: 操作类型(`add`, `remove`, `get`)
|
||||||
|
- `componentType`: 组件类型,如 `cc.Sprite`(用于 `add` 操作)
|
||||||
|
- `componentId`: 组件 ID(用于 `remove` 操作)
|
||||||
|
- `properties`: 组件属性(用于 `add` 操作)
|
||||||
|
|
||||||
|
### 11. manage_script
|
||||||
|
|
||||||
|
- **描述**: 管理脚本文件,默认创建 TypeScript 脚本
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型(`create`, `delete`, `read`, `write`)
|
||||||
|
- `path`: 脚本路径,如 `db://assets/scripts/NewScript.ts`
|
||||||
|
- `content`: 脚本内容(用于 `create` 和 `write` 操作)
|
||||||
|
- `name`: 脚本名称(用于 `create` 操作)
|
||||||
|
- **默认模板**: 当未提供 content 时,会使用 TypeScript 格式的默认模板
|
||||||
|
|
||||||
|
### 12. batch_execute
|
||||||
|
|
||||||
|
- **描述**: 批处理执行多个操作
|
||||||
|
- **参数**:
|
||||||
|
- `operations`: 操作列表
|
||||||
|
- `tool`: 工具名称
|
||||||
|
- `params`: 工具参数
|
||||||
|
|
||||||
|
### 13. manage_asset
|
||||||
|
|
||||||
|
- **描述**: 管理资源
|
||||||
|
- **参数**:
|
||||||
|
- `action`: 操作类型(`create`, `delete`, `move`, `get_info`)
|
||||||
|
- `path`: 资源路径,如 `db://assets/textures`
|
||||||
|
- `targetPath`: 目标路径(用于 `move` 操作)
|
||||||
|
- `content`: 资源内容(用于 `create` 操作)
|
||||||
|
|
||||||
## 技术实现
|
## 技术实现
|
||||||
|
|
||||||
### 架构设计
|
### 架构设计
|
||||||
|
|||||||
252
main.js
252
main.js
@@ -133,7 +133,7 @@ const getToolsList = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "open_scene",
|
name: "open_scene",
|
||||||
description: "在编辑器中打开指定的场景文件",
|
description: "打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒再进行节点创建或保存操作。",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -165,8 +165,74 @@ const getToolsList = () => {
|
|||||||
required: ["name"],
|
required: ["name"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "manage_components",
|
||||||
|
description: "管理节点组件",
|
||||||
|
inputSchema: {
|
||||||
|
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 操作)" },
|
||||||
|
},
|
||||||
|
required: ["nodeId", "action"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "manage_script",
|
||||||
|
description: "管理脚本文件",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
action: { type: "string", enum: ["create", "delete", "read", "write"], description: "操作类型" },
|
||||||
|
path: { type: "string", description: "脚本路径,如 db://assets/scripts/NewScript.js" },
|
||||||
|
content: { type: "string", description: "脚本内容 (用于 create 和 write 操作)" },
|
||||||
|
name: { type: "string", description: "脚本名称 (用于 create 操作)" },
|
||||||
|
},
|
||||||
|
required: ["action", "path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "batch_execute",
|
||||||
|
description: "批处理执行多个操作",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
operations: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
tool: { type: "string", description: "工具名称" },
|
||||||
|
params: { type: "object", description: "工具参数" },
|
||||||
|
},
|
||||||
|
required: ["tool", "params"],
|
||||||
|
},
|
||||||
|
description: "操作列表",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["operations"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "manage_asset",
|
||||||
|
description: "管理资源",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
action: { type: "string", enum: ["create", "delete", "move", "get_info"], description: "操作类型" },
|
||||||
|
path: { type: "string", description: "资源路径,如 db://assets/textures" },
|
||||||
|
targetPath: { type: "string", description: "目标路径 (用于 move 操作)" },
|
||||||
|
content: { type: "string", description: "资源内容 (用于 create 操作)" },
|
||||||
|
},
|
||||||
|
required: ["action", "path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
let isSceneBusy = false;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"scene-script": "scene-script.js",
|
"scene-script": "scene-script.js",
|
||||||
@@ -279,6 +345,9 @@ module.exports = {
|
|||||||
|
|
||||||
// 统一处理逻辑,方便日志记录
|
// 统一处理逻辑,方便日志记录
|
||||||
handleMcpCall(name, args, callback) {
|
handleMcpCall(name, args, callback) {
|
||||||
|
if (isSceneBusy && (name === "save_scene" || name === "create_node")) {
|
||||||
|
return callback("Editor is busy (Processing Scene), please wait a moment.");
|
||||||
|
}
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "get_selected_node":
|
case "get_selected_node":
|
||||||
const ids = Editor.Selection.curSelection("node");
|
const ids = Editor.Selection.curSelection("node");
|
||||||
@@ -299,8 +368,18 @@ module.exports = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "save_scene":
|
case "save_scene":
|
||||||
|
isSceneBusy = true;
|
||||||
|
addLog("info", "Preparing to save scene... Waiting for UI sync.");
|
||||||
|
// 强制延迟保存,防止死锁
|
||||||
|
setTimeout(() => {
|
||||||
Editor.Ipc.sendToMain("scene:save-scene");
|
Editor.Ipc.sendToMain("scene:save-scene");
|
||||||
callback(null, "Scene saved successfully");
|
addLog("info", "Executing Safe Save...");
|
||||||
|
setTimeout(() => {
|
||||||
|
isSceneBusy = false;
|
||||||
|
addLog("info", "Safe Save completed.");
|
||||||
|
callback(null, "Scene saved successfully.");
|
||||||
|
}, 1000);
|
||||||
|
}, 500);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "get_scene_hierarchy":
|
case "get_scene_hierarchy":
|
||||||
@@ -328,11 +407,16 @@ module.exports = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "open_scene":
|
case "open_scene":
|
||||||
|
isSceneBusy = true; // 锁定
|
||||||
const openUuid = Editor.assetdb.urlToUuid(args.url);
|
const openUuid = Editor.assetdb.urlToUuid(args.url);
|
||||||
if (openUuid) {
|
if (openUuid) {
|
||||||
Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid);
|
Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid);
|
||||||
|
setTimeout(() => {
|
||||||
|
isSceneBusy = false;
|
||||||
callback(null, `Success: Opening scene ${args.url}`);
|
callback(null, `Success: Opening scene ${args.url}`);
|
||||||
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
|
isSceneBusy = false;
|
||||||
callback(`Could not find asset with URL ${args.url}`);
|
callback(`Could not find asset with URL ${args.url}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -341,11 +425,175 @@ module.exports = {
|
|||||||
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback);
|
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "manage_components":
|
||||||
|
Editor.Scene.callSceneScript("mcp-bridge", "manage-components", args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "manage_script":
|
||||||
|
this.manageScript(args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "batch_execute":
|
||||||
|
this.batchExecute(args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "manage_asset":
|
||||||
|
this.manageAsset(args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
callback(`Unknown tool: ${name}`);
|
callback(`Unknown tool: ${name}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 管理脚本文件
|
||||||
|
manageScript(args, callback) {
|
||||||
|
const { action, path, content } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
if (Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Script already exists at ${path}`);
|
||||||
|
}
|
||||||
|
// 确保父目录存在
|
||||||
|
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, content || `const { ccclass, property } = cc._decorator;
|
||||||
|
|
||||||
|
@ccclass
|
||||||
|
export default class NewScript extends cc.Component {
|
||||||
|
@property(cc.Label)
|
||||||
|
label: cc.Label = null;
|
||||||
|
|
||||||
|
@property
|
||||||
|
text: string = 'hello';
|
||||||
|
|
||||||
|
// LIFE-CYCLE CALLBACKS:
|
||||||
|
|
||||||
|
onLoad () {}
|
||||||
|
|
||||||
|
start () {}
|
||||||
|
|
||||||
|
update (dt) {}
|
||||||
|
}`, (err) => {
|
||||||
|
callback(err, err ? null : `Script created at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
if (!Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Script not found at ${path}`);
|
||||||
|
}
|
||||||
|
Editor.assetdb.delete([path], (err) => {
|
||||||
|
callback(err, err ? null : `Script deleted at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "read":
|
||||||
|
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(`Failed to get script info: ${err}`);
|
||||||
|
}
|
||||||
|
Editor.assetdb.loadAny(path, (err, content) => {
|
||||||
|
callback(err, err ? null : content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "write":
|
||||||
|
Editor.assetdb.create(path, content, (err) => {
|
||||||
|
callback(err, err ? null : `Script updated at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callback(`Unknown script action: ${action}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 批处理执行
|
||||||
|
batchExecute(args, callback) {
|
||||||
|
const { operations } = args;
|
||||||
|
const results = [];
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
|
if (!operations || operations.length === 0) {
|
||||||
|
return callback("No operations provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
operations.forEach((operation, index) => {
|
||||||
|
this.handleMcpCall(operation.tool, operation.params, (err, result) => {
|
||||||
|
results[index] = { tool: operation.tool, error: err, result: result };
|
||||||
|
completed++;
|
||||||
|
|
||||||
|
if (completed === operations.length) {
|
||||||
|
callback(null, results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 管理资源
|
||||||
|
manageAsset(args, callback) {
|
||||||
|
const { action, path, targetPath, content } = args;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "create":
|
||||||
|
if (Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Asset already exists at ${path}`);
|
||||||
|
}
|
||||||
|
// 确保父目录存在
|
||||||
|
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, content || '', (err) => {
|
||||||
|
callback(err, err ? null : `Asset created at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
if (!Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Asset not found at ${path}`);
|
||||||
|
}
|
||||||
|
Editor.assetdb.delete([path], (err) => {
|
||||||
|
callback(err, err ? null : `Asset deleted at ${path}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "move":
|
||||||
|
if (!Editor.assetdb.exists(path)) {
|
||||||
|
return callback(`Asset not found at ${path}`);
|
||||||
|
}
|
||||||
|
if (Editor.assetdb.exists(targetPath)) {
|
||||||
|
return callback(`Target asset already exists at ${targetPath}`);
|
||||||
|
}
|
||||||
|
Editor.assetdb.move(path, targetPath, (err) => {
|
||||||
|
callback(err, err ? null : `Asset moved from ${path} to ${targetPath}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "get_info":
|
||||||
|
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
||||||
|
callback(err, err ? null : info);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callback(`Unknown asset action: ${action}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
// 暴露给 MCP 或面板的 API 封装
|
// 暴露给 MCP 或面板的 API 封装
|
||||||
messages: {
|
messages: {
|
||||||
"open-test-panel"() {
|
"open-test-panel"() {
|
||||||
|
|||||||
254
panel/index.html
254
panel/index.html
@@ -1,4 +1,12 @@
|
|||||||
<div class="mcp-container">
|
<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 id="panelMain" class="tab-content active">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="ctrl-group">
|
<div class="ctrl-group">
|
||||||
<span>Port:</span>
|
<span>Port:</span>
|
||||||
@@ -20,6 +28,46 @@
|
|||||||
<div id="logConsole" class="log-view"></div>
|
<div id="logConsole" class="log-view"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 测试面板内容 -->
|
||||||
|
<div id="panelTest" class="tab-content">
|
||||||
|
<div class="test-container">
|
||||||
|
<div class="test-layout">
|
||||||
|
<!-- 左侧工具列表 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="tool-name-section">
|
||||||
|
<label for="toolName">工具名称:</label>
|
||||||
|
<ui-input id="toolName" placeholder="例如: manage_components"></ui-input>
|
||||||
|
</div>
|
||||||
|
<div class="tools-list" id="toolsList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧输入输出 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="toolParams">工具参数 (JSON格式):</label>
|
||||||
|
<textarea id="toolParams" placeholder="例如: {
|
||||||
|
\"nodeId\": \"节点UUID\",
|
||||||
|
\"action\": \"add\",
|
||||||
|
\"componentType\": \"cc.Button\"
|
||||||
|
}"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<ui-button id="testBtn" class="primary">测试工具</ui-button>
|
||||||
|
<ui-button id="listToolsBtn" class="secondary">获取工具列表</ui-button>
|
||||||
|
<ui-button id="clearBtn" class="secondary">清空结果</ui-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result">
|
||||||
|
<h2>测试结果:</h2>
|
||||||
|
<textarea id="resultContent" placeholder="点击\"测试工具\"按钮开始测试"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -34,6 +82,38 @@
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标签页样式 */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tab-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin-right: 2px;
|
||||||
|
border: none;
|
||||||
|
background-color: #333;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #fff;
|
||||||
|
border-bottom-color: #4CAF50;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.tab-content.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主面板样式 */
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -102,4 +182,178 @@
|
|||||||
border-left-color: #c678dd;
|
border-left-color: #c678dd;
|
||||||
color: #d19a66;
|
color: #d19a66;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 测试面板样式 */
|
||||||
|
.test-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
width: 300px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel .tool-name-section {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel .tools-list {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #222;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel ui-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #222;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel .result {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel .result textarea {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel .button-group {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background-color: #333;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.form-group ui-input,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
height: 150px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.tool-item {
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.tool-item:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.button-group ui-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.button-group ui-button.primary {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.button-group ui-button.secondary {
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #333;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.result h2 {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.result textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #222;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
244
panel/index.js
244
panel/index.js
@@ -23,6 +23,24 @@ Editor.Panel.extend({
|
|||||||
const btnCopy = this.shadowRoot.querySelector("#btnCopy");
|
const btnCopy = this.shadowRoot.querySelector("#btnCopy");
|
||||||
const logView = this.shadowRoot.querySelector("#logConsole");
|
const logView = this.shadowRoot.querySelector("#logConsole");
|
||||||
|
|
||||||
|
// 标签页元素
|
||||||
|
const tabMain = this.shadowRoot.querySelector("#tabMain");
|
||||||
|
const tabTest = this.shadowRoot.querySelector("#tabTest");
|
||||||
|
const panelMain = this.shadowRoot.querySelector("#panelMain");
|
||||||
|
const panelTest = this.shadowRoot.querySelector("#panelTest");
|
||||||
|
|
||||||
|
// 测试面板元素
|
||||||
|
const toolNameInput = this.shadowRoot.querySelector("#toolName");
|
||||||
|
const toolParamsTextarea = this.shadowRoot.querySelector("#toolParams");
|
||||||
|
const toolsList = this.shadowRoot.querySelector("#toolsList");
|
||||||
|
const testBtn = this.shadowRoot.querySelector("#testBtn");
|
||||||
|
const listToolsBtn = this.shadowRoot.querySelector("#listToolsBtn");
|
||||||
|
const clearBtn = this.shadowRoot.querySelector("#clearBtn");
|
||||||
|
const resultContent = this.shadowRoot.querySelector("#resultContent");
|
||||||
|
|
||||||
|
let tools = [];
|
||||||
|
const API_BASE = 'http://localhost:3456';
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -32,6 +50,23 @@ Editor.Panel.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 标签页切换
|
||||||
|
tabMain.addEventListener("confirm", () => {
|
||||||
|
tabMain.classList.add("active");
|
||||||
|
tabTest.classList.remove("active");
|
||||||
|
panelMain.classList.add("active");
|
||||||
|
panelTest.classList.remove("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
tabTest.addEventListener("confirm", () => {
|
||||||
|
tabTest.classList.add("active");
|
||||||
|
tabMain.classList.remove("active");
|
||||||
|
panelTest.classList.add("active");
|
||||||
|
panelMain.classList.remove("active");
|
||||||
|
// 自动获取工具列表
|
||||||
|
this.getToolsList();
|
||||||
|
});
|
||||||
|
|
||||||
btnToggle.addEventListener("confirm", () => {
|
btnToggle.addEventListener("confirm", () => {
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(portInput.value));
|
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(portInput.value));
|
||||||
});
|
});
|
||||||
@@ -45,6 +80,7 @@ Editor.Panel.extend({
|
|||||||
require("electron").clipboard.writeText(logView.innerText);
|
require("electron").clipboard.writeText(logView.innerText);
|
||||||
Editor.success("All logs copied!");
|
Editor.success("All logs copied!");
|
||||||
});
|
});
|
||||||
|
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
portInput.value = data.config.port;
|
portInput.value = data.config.port;
|
||||||
@@ -56,10 +92,218 @@ Editor.Panel.extend({
|
|||||||
data.logs.forEach((log) => this.renderLog(log));
|
data.logs.forEach((log) => this.renderLog(log));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
autoStartCheck.addEventListener("change", (event) => {
|
autoStartCheck.addEventListener("change", (event) => {
|
||||||
// event.target.value 在 ui-checkbox 中是布尔值
|
// event.target.value 在 ui-checkbox 中是布尔值
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", event.target.value);
|
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", event.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 测试面板事件
|
||||||
|
testBtn.addEventListener("confirm", () => this.testTool());
|
||||||
|
listToolsBtn.addEventListener("confirm", () => this.getToolsList());
|
||||||
|
clearBtn.addEventListener("confirm", () => this.clearResult());
|
||||||
|
|
||||||
|
// 获取工具列表
|
||||||
|
this.getToolsList = function() {
|
||||||
|
this.showResult('获取工具列表中...');
|
||||||
|
|
||||||
|
fetch(`${API_BASE}/list-tools`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.tools) {
|
||||||
|
tools = data.tools;
|
||||||
|
this.displayToolsList(tools);
|
||||||
|
this.showResult(`成功获取 ${tools.length} 个工具`, 'success');
|
||||||
|
} else {
|
||||||
|
this.showResult('获取工具列表失败:未找到工具数据', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.showResult(`获取工具列表失败:${error.message}`, 'error');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示工具列表
|
||||||
|
this.displayToolsList = function(tools) {
|
||||||
|
toolsList.innerHTML = '';
|
||||||
|
|
||||||
|
tools.forEach(tool => {
|
||||||
|
const toolItem = document.createElement('div');
|
||||||
|
toolItem.className = 'tool-item';
|
||||||
|
toolItem.textContent = `${tool.name} - ${tool.description}`;
|
||||||
|
toolItem.addEventListener('click', () => {
|
||||||
|
toolNameInput.value = tool.name;
|
||||||
|
// 尝试填充示例参数
|
||||||
|
this.fillExampleParams(tool);
|
||||||
|
});
|
||||||
|
toolsList.appendChild(toolItem);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 填充示例参数
|
||||||
|
this.fillExampleParams = function(tool) {
|
||||||
|
let exampleParams = {};
|
||||||
|
|
||||||
|
switch (tool.name) {
|
||||||
|
case 'get_selected_node':
|
||||||
|
case 'save_scene':
|
||||||
|
case 'get_scene_hierarchy':
|
||||||
|
exampleParams = {};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'set_node_name':
|
||||||
|
exampleParams = {
|
||||||
|
"id": "节点UUID",
|
||||||
|
"newName": "新节点名称"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_node_transform':
|
||||||
|
exampleParams = {
|
||||||
|
"id": "节点UUID",
|
||||||
|
"x": 100,
|
||||||
|
"y": 100,
|
||||||
|
"scaleX": 1,
|
||||||
|
"scaleY": 1
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_scene':
|
||||||
|
exampleParams = {
|
||||||
|
"sceneName": "NewScene"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_prefab':
|
||||||
|
exampleParams = {
|
||||||
|
"nodeId": "节点UUID",
|
||||||
|
"prefabName": "NewPrefab"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'open_scene':
|
||||||
|
exampleParams = {
|
||||||
|
"url": "db://assets/NewScene.fire"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_node':
|
||||||
|
exampleParams = {
|
||||||
|
"name": "NewNode",
|
||||||
|
"parentId": "父节点UUID",
|
||||||
|
"type": "empty"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'manage_components':
|
||||||
|
exampleParams = {
|
||||||
|
"nodeId": "节点UUID",
|
||||||
|
"action": "add",
|
||||||
|
"componentType": "cc.Button"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'manage_script':
|
||||||
|
exampleParams = {
|
||||||
|
"action": "create",
|
||||||
|
"path": "db://assets/scripts/TestScript.ts",
|
||||||
|
"content": "const { ccclass, property } = cc._decorator;\n\n@ccclass\nexport default class TestScript extends cc.Component {\n // LIFE-CYCLE CALLBACKS:\n\n onLoad () {}\n\n start () {}\n\n update (dt) {}\n}"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'batch_execute':
|
||||||
|
exampleParams = {
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"tool": "get_selected_node",
|
||||||
|
"params": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'manage_asset':
|
||||||
|
exampleParams = {
|
||||||
|
"action": "create",
|
||||||
|
"path": "db://assets/test.txt",
|
||||||
|
"content": "Hello, MCP!"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolParamsTextarea.value = JSON.stringify(exampleParams, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试工具
|
||||||
|
this.testTool = function() {
|
||||||
|
const toolName = toolNameInput.value.trim();
|
||||||
|
const toolParamsStr = toolParamsTextarea.value.trim();
|
||||||
|
|
||||||
|
if (!toolName) {
|
||||||
|
this.showResult('请输入工具名称', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let toolParams;
|
||||||
|
try {
|
||||||
|
toolParams = toolParamsStr ? JSON.parse(toolParamsStr) : {};
|
||||||
|
} catch (error) {
|
||||||
|
this.showResult(`参数格式错误:${error.message}`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showResult('测试工具中...');
|
||||||
|
|
||||||
|
fetch(`${API_BASE}/call-tool`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: toolName,
|
||||||
|
arguments: toolParams
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
this.showResult(`测试失败:${data.error}`, 'error');
|
||||||
|
} else {
|
||||||
|
this.showResult(JSON.stringify(data, null, 2), 'success');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.showResult(`测试失败:${error.message}`, 'error');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示结果
|
||||||
|
this.showResult = function(message, type = 'info') {
|
||||||
|
resultContent.value = message;
|
||||||
|
|
||||||
|
// 移除旧样式
|
||||||
|
resultContent.className = '';
|
||||||
|
|
||||||
|
// 添加新样式
|
||||||
|
if (type === 'error' || type === 'success') {
|
||||||
|
resultContent.className = type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空结果
|
||||||
|
this.clearResult = function() {
|
||||||
|
this.showResult('点击"测试工具"按钮开始测试');
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
renderLog(log) {
|
renderLog(log) {
|
||||||
|
|||||||
147
scene-script.js
147
scene-script.js
@@ -91,6 +91,10 @@ module.exports = {
|
|||||||
"create-node": function (event, args) {
|
"create-node": function (event, args) {
|
||||||
const { name, parentId, type } = args;
|
const { name, parentId, type } = args;
|
||||||
const scene = cc.director.getScene();
|
const scene = cc.director.getScene();
|
||||||
|
if (!scene || !cc.director.getRunningScene()) {
|
||||||
|
if (event.reply) event.reply(new Error("Scene not ready or loading."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let newNode = null;
|
let newNode = null;
|
||||||
|
|
||||||
@@ -119,24 +123,149 @@ module.exports = {
|
|||||||
|
|
||||||
// 设置层级
|
// 设置层级
|
||||||
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene;
|
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene;
|
||||||
if (newNode) {
|
if (parent) {
|
||||||
newNode.parent = parent;
|
newNode.parent = parent;
|
||||||
|
|
||||||
// 坐标居中处理(如果是 Canvas 子节点)
|
// 【优化】通知主进程场景变脏
|
||||||
if (parent.name === "Canvas") {
|
|
||||||
newNode.setPosition(0, 0);
|
|
||||||
} else {
|
|
||||||
newNode.setPosition(cc.v2(cc.winSize.width / 2, cc.winSize.height / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知编辑器刷新
|
|
||||||
Editor.Ipc.sendToMain("scene:dirty");
|
Editor.Ipc.sendToMain("scene:dirty");
|
||||||
|
|
||||||
|
// 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环
|
||||||
|
setTimeout(() => {
|
||||||
Editor.Ipc.sendToAll("scene:node-created", {
|
Editor.Ipc.sendToAll("scene:node-created", {
|
||||||
uuid: newNode.uuid,
|
uuid: newNode.uuid,
|
||||||
parentUuid: parent.uuid,
|
parentUuid: parent.uuid,
|
||||||
});
|
});
|
||||||
|
}, 10);
|
||||||
|
|
||||||
if (event.reply) event.reply(null, newNode.uuid);
|
if (event.reply) event.reply(null, newNode.uuid);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"manage-components": function (event, args) {
|
||||||
|
const { nodeId, action, componentType, componentId, properties } = args;
|
||||||
|
let node = cc.engine.getInstanceById(nodeId);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
if (event.reply) event.reply(new Error("Node not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "add":
|
||||||
|
if (!componentType) {
|
||||||
|
if (event.reply) event.reply(new Error("Component type is required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析组件类型
|
||||||
|
let compClass = null;
|
||||||
|
if (componentType.startsWith("cc.")) {
|
||||||
|
const className = componentType.replace("cc.", "");
|
||||||
|
compClass = cc[className];
|
||||||
|
} else {
|
||||||
|
// 尝试获取自定义组件
|
||||||
|
compClass = cc.js.getClassByName(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!compClass) {
|
||||||
|
if (event.reply) event.reply(new Error(`Component type not found: ${componentType}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加组件
|
||||||
|
const component = node.addComponent(compClass);
|
||||||
|
|
||||||
|
// 设置属性
|
||||||
|
if (properties) {
|
||||||
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
|
if (component[key] !== undefined) {
|
||||||
|
component[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.Ipc.sendToMain("scene:dirty");
|
||||||
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
|
||||||
|
|
||||||
|
if (event.reply) event.reply(null, `Component ${componentType} added`);
|
||||||
|
} catch (err) {
|
||||||
|
if (event.reply) event.reply(new Error(`Failed to add component: ${err.message}`));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "remove":
|
||||||
|
if (!componentId) {
|
||||||
|
if (event.reply) event.reply(new Error("Component ID is required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查找并移除组件
|
||||||
|
const component = node.getComponentById(componentId);
|
||||||
|
if (component) {
|
||||||
|
node.removeComponent(component);
|
||||||
|
Editor.Ipc.sendToMain("scene:dirty");
|
||||||
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
|
||||||
|
if (event.reply) event.reply(null, "Component removed");
|
||||||
|
} else {
|
||||||
|
if (event.reply) event.reply(new Error("Component not found"));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (event.reply) event.reply(new Error(`Failed to remove component: ${err.message}`));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "get":
|
||||||
|
try {
|
||||||
|
const components = node._components.map((c) => {
|
||||||
|
// 获取组件属性
|
||||||
|
const properties = {};
|
||||||
|
for (const key in c) {
|
||||||
|
if (typeof c[key] !== "function" &&
|
||||||
|
!key.startsWith("_") &&
|
||||||
|
c[key] !== undefined) {
|
||||||
|
try {
|
||||||
|
properties[key] = c[key];
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略无法序列化的属性
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: c.__typename,
|
||||||
|
uuid: c.uuid,
|
||||||
|
properties: properties
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (event.reply) event.reply(null, components);
|
||||||
|
} catch (err) {
|
||||||
|
if (event.reply) event.reply(new Error(`Failed to get components: ${err.message}`));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (event.reply) event.reply(new Error(`Unknown component action: ${action}`));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"get-component-properties": function (component) {
|
||||||
|
const properties = {};
|
||||||
|
|
||||||
|
// 遍历组件属性
|
||||||
|
for (const key in component) {
|
||||||
|
if (typeof component[key] !== "function" &&
|
||||||
|
!key.startsWith("_") &&
|
||||||
|
component[key] !== undefined) {
|
||||||
|
try {
|
||||||
|
properties[key] = component[key];
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略无法序列化的属性
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user