Merge branch 'main' of remote and resolve conflicts across src/
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
# **/dist
|
**/dist
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/package-lock.json
|
**/package-lock.json
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
---
|
|
||||||
description:
|
|
||||||
alwaysApply: true
|
|
||||||
enabled: true
|
|
||||||
updatedAt: 2026-02-03T13:55:50.734Z
|
|
||||||
provider:
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
# MCP Bridge Development Rules
|
|
||||||
|
|
||||||
## 1. 语言规范 (Language)
|
|
||||||
|
|
||||||
* **强制中文**: 所有的对话回复、代码注释、以及生成的文档都必须使用**中文**。
|
|
||||||
* **日志消息**: `addLog()` 的消息内容可以使用中英文混合,确保关键术语清晰。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 关键工作流程 (Critical Workflow)
|
|
||||||
|
|
||||||
### 2.1 插件重载
|
|
||||||
- **必须重载**: 修改 `main.js`, `package.json`, `scene-script.js`, 或 `panel/` 后,**必须**在编辑器中执行「扩展 → 刷新」或重启编辑器。
|
|
||||||
- **热更新不适用**: Cocos Creator 2.x 的插件主进程脚本不支持热更新。
|
|
||||||
|
|
||||||
### 2.2 测试驱动
|
|
||||||
- **测试脚本**: 每个新功能必须在 `test/` 目录下创建独立测试脚本 (如 `test/test_feature.js`)。
|
|
||||||
- **HTTP 验证**: 测试脚本通过 HTTP 请求直接调用插件 API,验证功能正确性。
|
|
||||||
- **运行前提**: 确保 Cocos Creator 编辑器已打开且 MCP Bridge 服务已启动。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 架构与进程隔离 (Architecture & IPC)
|
|
||||||
|
|
||||||
### 3.1 进程职责划分
|
|
||||||
|
|
||||||
| 文件 | 进程 | 可访问 | 不可访问 |
|
|
||||||
|------|------|--------|----------|
|
|
||||||
| `main.js` | 主进程 (Main) | `Editor.assetdb`, `Editor.Ipc`, `require()` | `cc.*` (Cocos 引擎) |
|
|
||||||
| `scene-script.js` | 渲染进程 (Renderer) | `cc.*`, `cc.engine`, `cc.director` | `Editor.assetdb`, `Editor.FileSystem` |
|
|
||||||
|
|
||||||
### 3.2 跨进程通信规则
|
|
||||||
|
|
||||||
```
|
|
||||||
主进程 (main.js) 渲染进程 (scene-script.js)
|
|
||||||
│ │
|
|
||||||
├─ 1. 接收 HTTP 请求 │
|
|
||||||
├─ 2. 解析 db:// 路径为 UUID │
|
|
||||||
│ Editor.assetdb.urlToUuid() │
|
|
||||||
├─ 3. 调用场景脚本 ──────────────────────┤
|
|
||||||
│ Editor.Scene.callSceneScript() │
|
|
||||||
│ ├─ 4. 操作节点/组件
|
|
||||||
│ │ cc.engine.getInstanceById()
|
|
||||||
│ ├─ 5. 通知场景变脏
|
|
||||||
│ │ Editor.Ipc.sendToMain("scene:dirty")
|
|
||||||
└─ 6. 返回结果 ◀────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**核心规则**: 永远在 `main.js` 中将 `db://` 路径转换为 UUID,再传递给 `scene-script.js`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 编码规范 (Coding Standards)
|
|
||||||
|
|
||||||
### 4.1 命名规范
|
|
||||||
|
|
||||||
| 类型 | 规范 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| 函数名 | camelCase | `handleMcpCall`, `manageScript` |
|
|
||||||
| 常量 | SCREAMING_SNAKE_CASE | `MAX_RESULTS`, `DEFAULT_PORT` |
|
|
||||||
| 私有变量 | _camelCase | `_isMoving`, `_timer` |
|
|
||||||
| 布尔变量 | is/has/can 前缀 | `isSceneBusy`, `hasComponent` |
|
|
||||||
| MCP 工具名 | snake_case | `get_selected_node`, `manage_vfx` |
|
|
||||||
| IPC 消息名 | kebab-case | `get-hierarchy`, `create-node` |
|
|
||||||
|
|
||||||
### 4.2 函数组织顺序
|
|
||||||
|
|
||||||
在 `module.exports` 中按以下顺序组织函数:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
module.exports = {
|
|
||||||
// 1. 配置属性
|
|
||||||
"scene-script": "scene-script.js",
|
|
||||||
|
|
||||||
// 2. 生命周期函数
|
|
||||||
load() { },
|
|
||||||
unload() { },
|
|
||||||
|
|
||||||
// 3. 服务器管理
|
|
||||||
startServer(port) { },
|
|
||||||
stopServer() { },
|
|
||||||
|
|
||||||
// 4. 核心处理逻辑
|
|
||||||
handleMcpCall(name, args, callback) { },
|
|
||||||
|
|
||||||
// 5. 工具函数 (按字母顺序)
|
|
||||||
applyTextEdits(args, callback) { },
|
|
||||||
batchExecute(args, callback) { },
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// 6. IPC 消息处理
|
|
||||||
messages: {
|
|
||||||
"open-test-panel"() { },
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 避免重复定义
|
|
||||||
|
|
||||||
> ⚠️ **重要**: `main.js` 已存在重复函数问题,编辑前务必使用 `view_file` 确认上下文,避免创建重复定义。
|
|
||||||
|
|
||||||
**检查清单**:
|
|
||||||
- [ ] 新增函数前,搜索是否已存在同名函数
|
|
||||||
- [ ] 修改函数时,确认只有一个定义
|
|
||||||
- [ ] `messages` 对象中避免重复的消息处理器
|
|
||||||
|
|
||||||
### 4.4 日志规范
|
|
||||||
|
|
||||||
使用 `addLog(type, message)` 替代 `console.log()`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ✅ 正确
|
|
||||||
addLog("info", "服务启动成功");
|
|
||||||
addLog("error", `操作失败: ${err.message}`);
|
|
||||||
addLog("mcp", `REQ -> [${toolName}]`);
|
|
||||||
addLog("success", `RES <- [${toolName}] 成功`);
|
|
||||||
|
|
||||||
// ❌ 错误
|
|
||||||
console.log("服务启动成功"); // 不会被 read_console 捕获
|
|
||||||
```
|
|
||||||
|
|
||||||
| type | 用途 | 颜色 |
|
|
||||||
|------|------|------|
|
|
||||||
| `info` | 一般信息 | 蓝色 |
|
|
||||||
| `success` | 操作成功 | 绿色 |
|
|
||||||
| `warn` | 警告信息 | 黄色 |
|
|
||||||
| `error` | 错误信息 | 红色 |
|
|
||||||
| `mcp` | MCP 请求/响应 | 紫色 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 撤销/重做支持 (Undo/Redo)
|
|
||||||
|
|
||||||
### 5.1 使用 scene:set-property
|
|
||||||
|
|
||||||
对于节点属性修改,优先使用 `scene:set-property` 以获得原生 Undo 支持:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ✅ 支持 Undo
|
|
||||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
|
||||||
id: nodeId,
|
|
||||||
path: "x",
|
|
||||||
type: "Float",
|
|
||||||
value: 100,
|
|
||||||
isSubProp: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// ❌ 不支持 Undo (直接修改)
|
|
||||||
node.x = 100;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 使用 Undo 组
|
|
||||||
|
|
||||||
对于复合操作,使用 Undo 组包装:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
Editor.Ipc.sendToPanel("scene", "scene:undo-record", "Transform Update");
|
|
||||||
try {
|
|
||||||
// 执行多个属性修改
|
|
||||||
Editor.Ipc.sendToPanel("scene", "scene:undo-commit");
|
|
||||||
} catch (e) {
|
|
||||||
Editor.Ipc.sendToPanel("scene", "scene:undo-cancel");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 功能特定规则 (Feature Specifics)
|
|
||||||
|
|
||||||
### 6.1 粒子系统 (VFX)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 必须设置 custom = true,否则属性修改可能不生效
|
|
||||||
particleSystem.custom = true;
|
|
||||||
|
|
||||||
// 确保纹理有效,否则粒子不可见
|
|
||||||
if (!particleSystem.texture && !particleSystem.file) {
|
|
||||||
// 加载默认纹理
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 资源路径解析
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 内置资源可能需要多个路径尝试
|
|
||||||
const defaultPaths = [
|
|
||||||
"db://internal/image/default_sprite_splash",
|
|
||||||
"db://internal/image/default_sprite_splash.png",
|
|
||||||
"db://internal/image/default_particle",
|
|
||||||
"db://internal/image/default_particle.png"
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of defaultPaths) {
|
|
||||||
const uuid = Editor.assetdb.urlToUuid(path);
|
|
||||||
if (uuid) break;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 场景操作时序
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 场景操作后需要延迟通知 UI 刷新
|
|
||||||
newNode.parent = parent;
|
|
||||||
Editor.Ipc.sendToMain("scene:dirty");
|
|
||||||
|
|
||||||
// 使用 setTimeout 让出主循环
|
|
||||||
setTimeout(() => {
|
|
||||||
Editor.Ipc.sendToAll("scene:node-created", {
|
|
||||||
uuid: newNode.uuid,
|
|
||||||
parentUuid: parent.uuid,
|
|
||||||
});
|
|
||||||
}, 10);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 错误处理规范 (Error Handling)
|
|
||||||
|
|
||||||
### 7.1 回调风格统一
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ✅ 标准风格
|
|
||||||
callback(null, result); // 成功
|
|
||||||
callback("Error message"); // 失败 (字符串)
|
|
||||||
callback(new Error("message")); // 失败 (Error 对象)
|
|
||||||
|
|
||||||
// 避免混用
|
|
||||||
callback(err, null); // 不推荐,保持一致性
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 异步操作错误处理
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
Editor.assetdb.queryInfoByUrl(path, (err, info) => {
|
|
||||||
if (err) {
|
|
||||||
addLog("error", `查询资源失败: ${err.message}`);
|
|
||||||
return callback(`Failed to get info: ${err.message}`);
|
|
||||||
}
|
|
||||||
// 继续处理...
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 提交规范 (Git Commit)
|
|
||||||
|
|
||||||
使用 [Conventional Commits](https://conventionalcommits.org/) 格式:
|
|
||||||
|
|
||||||
| 类型 | 用途 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| `feat` | 新功能 | `feat: add manage_vfx tool` |
|
|
||||||
| `fix` | 修复 bug | `fix: resolve duplicate function in main.js` |
|
|
||||||
| `docs` | 文档更新 | `docs: add code review report` |
|
|
||||||
| `refactor` | 重构 | `refactor: split main.js into modules` |
|
|
||||||
| `test` | 测试 | `test: add material management tests` |
|
|
||||||
| `chore` | 杂项 | `chore: update dependencies` |
|
|
||||||
537
DEVELOPMENT.md
537
DEVELOPMENT.md
@@ -1,537 +0,0 @@
|
|||||||
# MCP Bridge 插件开发流程文档
|
|
||||||
|
|
||||||
本文档记录了 MCP Bridge 插件的完整开发流程,包括核心架构设计、功能实现、测试与调试等各个阶段。
|
|
||||||
|
|
||||||
## 0. 项目开发规范 (Project Rules)
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> 所有贡献者必须严格遵守以下规则:
|
|
||||||
|
|
||||||
1. **语言与沟通**: 所有注释、文档、计划、任务及 AI 回复必须使用 **简体中文 (Simplified Chinese)**。
|
|
||||||
2. **技术栈**: 新脚本必须使用 **TypeScript** (`.ts`)。禁止创建新的 `.js` 文件 (除非是构建脚本或测试配置)。
|
|
||||||
3. **文档**: 所有修改或创建的脚本必须包含详细的 JSDoc 格式注释。
|
|
||||||
4. **架构**: 严禁引入新的架构模式或重型外部库。必须复用现有的 Cocos Creator 管理器和工具类。
|
|
||||||
5. **隔离原则**: 保持 `main.js` (主进程) 与 `scene-script.js` (渲染进程) 的严格职责分离。即便是在主进程中,也应通过 IPC 与场景脚本交互。
|
|
||||||
6. **主体校验规则 (Subject Validation Rule)**: 在对节点、组件或属性进行任何“写”操作之前,AI 必须先验证主体的存在性与类型正确性。严禁基于假设进行操作。
|
|
||||||
|
|
||||||
## 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 注释。
|
|
||||||
|
|
||||||
### 2026-02-25: 修复 manage_script 路径引用错误与强制生成 Meta
|
|
||||||
|
|
||||||
- **缺陷修正**: 修复了 `main.js` 中 `manageScript` 处理 `create` 动作时由于变量名解构导致 `path.dirname` 找不到 `path` 引用的问题。现已改为使用预设的 `pathModule` 模块。
|
|
||||||
- **规范强化**: 将 `manage_script` 的工具提示(Prompt)更新为强制要求调用 `refresh_editor` 生成脚本的 `.meta` 文件,以确保新创建的脚本能够被正确挂载为组件,同时不增加整体 Token 消耗。
|
|
||||||
|
|
||||||
### 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. 开发路线图 (Roadmap)
|
|
||||||
|
|
||||||
### 11.1 第三阶段开发计划(已完成)
|
|
||||||
|
|
||||||
| 任务 | 状态 | 描述 |
|
|
||||||
| ---------------------- | ------- | ------------------------------------------------------------------- |
|
|
||||||
| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 |
|
|
||||||
| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 |
|
|
||||||
| 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 |
|
|
||||||
| 菜单项执行工具实现 | ✅ 完成 | 实现 execute_menu_item 工具,支持执行 Cocos Creator 菜单项 |
|
|
||||||
| 代码编辑增强工具实现 | ✅ 完成 | 实现 apply_text_edits 工具,支持文本编辑操作应用 |
|
|
||||||
| 控制台读取工具实现 | ✅ 完成 | 实现 read_console 工具,支持读取编辑器控制台输出 |
|
|
||||||
| 脚本验证工具实现 | ✅ 完成 | 实现 validate_script 工具,支持脚本语法验证 |
|
|
||||||
|
|
||||||
### 11.2 第四阶段开发计划(已完成)
|
|
||||||
|
|
||||||
| 任务 | 状态 | 描述 |
|
|
||||||
| ------------ | ------- | ---------------------------------------------- |
|
|
||||||
| 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 |
|
|
||||||
| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 |
|
|
||||||
|
|
||||||
### 11.3 差异填补阶段(Gap Filling)- 已完成
|
|
||||||
|
|
||||||
| 任务 | 状态 | 描述 |
|
|
||||||
| -------- | ------- | ---------------------------------------------- |
|
|
||||||
| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 |
|
|
||||||
| 文件哈希 | ✅ 完成 | 实现 get_sha 工具,支持文件 SHA-256 计算 |
|
|
||||||
| 动画管理 | ✅ 完成 | 实现 manage_animation 工具,支持动画播放与控制 |
|
|
||||||
|
|
||||||
### 11.4 第六阶段:可靠性与体验优化(已完成)
|
|
||||||
|
|
||||||
| 任务 | 状态 | 描述 |
|
|
||||||
| ---------------- | ------- | -------------------------------------------------------------------------- |
|
|
||||||
| IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) |
|
|
||||||
| 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) |
|
|
||||||
| 组件智能解析修复 | ✅ 完成 | 修复组件属性赋值时的 UUID 类型转换,支持压缩 UUID 及自定义组件 (`$_$ctor`) |
|
|
||||||
|
|
||||||
### 11.5 第七阶段开发计划(已完成)
|
|
||||||
|
|
||||||
| 任务 | 状态 | 描述 |
|
|
||||||
| ---------- | ------- | ----------------------------------------- |
|
|
||||||
| 插件发布 | ✅ 完成 | 准备发布,提交到 Cocos 插件商店 |
|
|
||||||
| 文档完善 | ✅ 完成 | 完善 API 文档 ("Getting Started" 教程) |
|
|
||||||
| 界面美化 | ✅ 完成 | 优化面板 UI 体验 |
|
|
||||||
| 国际化支持 | ✅ 完成 | 添加多语言 (i18n) 支持 (主要是中文本地化) |
|
|
||||||
| 工具扩展 | ✅ 完成 | 添加更多高级工具 |
|
|
||||||
|
|
||||||
## 12. Unity-MCP 对比分析
|
|
||||||
|
|
||||||
### 12.1 功能差距 (Gap Analysis)
|
|
||||||
|
|
||||||
通过与 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 替代 |
|
|
||||||
|
|
||||||
## 13. 风险评估
|
|
||||||
|
|
||||||
### 13.1 潜在风险
|
|
||||||
|
|
||||||
| 风险 | 影响 | 缓解措施 |
|
|
||||||
| --------------- | ------------ | ----------------------------------------- |
|
|
||||||
| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API |
|
|
||||||
| 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 |
|
|
||||||
| 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 |
|
|
||||||
| 兼容性问题 | 多版本不兼容 | 测试主流版本 (2.4.x),提供兼容层 |
|
|
||||||
|
|
||||||
## 14. 结论
|
|
||||||
|
|
||||||
Cocos-MCP 插件的开发计划已顺利完成多个迭代阶段。目前插件实现了包括编辑器管理、场景操作、资源管理在内的全套核心功能,并完成了针对性的可靠性加固(IPC 通信、脚本时序、组件解析)。
|
|
||||||
|
|
||||||
插件功能已趋于稳定,后续工作重点将转向 **发布准备**、**文档体系建设** 以及 **用户体验优化**,力求为 Cocos Creator 开发者提供高质量的 AI 辅助开发工具。
|
|
||||||
46
README.md
46
README.md
@@ -15,7 +15,7 @@
|
|||||||
- **脚本管理**: 创建、删除、读取、写入脚本文件
|
- **脚本管理**: 创建、删除、读取、写入脚本文件
|
||||||
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
|
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
|
||||||
- **资产管理**: 创建、删除、移动、获取资源信息
|
- **资产管理**: 创建、删除、移动、获取资源信息
|
||||||
- **实时日志**: 提供详细的操作日志记录和展示
|
- **实时日志**: 提供详细的操作日志记录和展示,支持持久化写入项目内日志文件
|
||||||
- **自动启动**: 支持编辑器启动时自动开启服务
|
- **自动启动**: 支持编辑器启动时自动开启服务
|
||||||
- **编辑器管理**: 获取和设置选中对象,刷新编辑器
|
- **编辑器管理**: 获取和设置选中对象,刷新编辑器
|
||||||
- **游戏对象查找**: 根据条件查找场景中的节点
|
- **游戏对象查找**: 根据条件查找场景中的节点
|
||||||
@@ -28,6 +28,10 @@
|
|||||||
- **全局搜索**: 在项目中搜索文本内容
|
- **全局搜索**: 在项目中搜索文本内容
|
||||||
- **撤销/重做**: 管理编辑器的撤销栈
|
- **撤销/重做**: 管理编辑器的撤销栈
|
||||||
- **特效管理**: 创建和修改粒子系统
|
- **特效管理**: 创建和修改粒子系统
|
||||||
|
- **并发安全**: 指令队列串行化执行,防止编辑器卡死
|
||||||
|
- **超时保护**: IPC 通信和指令队列均有超时兜底机制
|
||||||
|
- **属性保护**: 组件核心属性黑名单机制,防止 AI 篡改 `node`/`uuid` 等引用导致崩溃
|
||||||
|
- **AI 容错**: 参数别名映射(`operation`→`action`、`save`→`update`/`write`),兼容大模型幻觉
|
||||||
- **工具说明**: 测试面板提供详细的工具描述和参数说明
|
- **工具说明**: 测试面板提供详细的工具描述和参数说明
|
||||||
|
|
||||||
## 安装与使用
|
## 安装与使用
|
||||||
@@ -58,29 +62,29 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
Command: node
|
Command: node
|
||||||
Args: [Cocos Creator 项目的绝对路径]/packages/mcp-bridge/mcp-proxy.js
|
Args: [Cocos Creator 项目的绝对路径]/packages/mcp-bridge/src/mcp-proxy.js
|
||||||
```
|
```
|
||||||
|
|
||||||
例如,在你的项目中,完整路径应该是:
|
例如,在你的项目中,完整路径应该是:
|
||||||
|
|
||||||
```
|
```
|
||||||
Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.js
|
Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/src/mcp-proxy.js
|
||||||
```
|
```
|
||||||
|
|
||||||
### 或者添加 JSON 配置:
|
### 或者添加 JSON 配置:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"cocos-creator": {
|
"cocos-creator": {
|
||||||
"command": "node",
|
"command": "node",
|
||||||
"args": ["[Cocos Creator 项目的绝对路径]/packages/mcp-bridge/mcp-proxy.js"]
|
"args": ["[Cocos Creator 项目的绝对路径]/packages/mcp-bridge/src/mcp-proxy.js"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:请将上述配置中的路径替换为你自己项目中 `mcp-proxy.js` 文件的实际绝对路径。
|
注意:请将上述配置中的路径替换为你自己项目中 `src/mcp-proxy.js` 文件的实际绝对路径。
|
||||||
|
|
||||||
## API 接口
|
## API 接口
|
||||||
|
|
||||||
@@ -369,8 +373,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
|
|
||||||
插件采用了典型的 Cocos Creator 扩展架构,包含以下几个部分:
|
插件采用了典型的 Cocos Creator 扩展架构,包含以下几个部分:
|
||||||
|
|
||||||
- **main.js**: 插件主入口,负责启动 HTTP 服务和处理 MCP 请求
|
- **src/main.js**: 插件主入口,负责启动 HTTP 服务和处理 MCP 请求
|
||||||
- **scene-script.js**: 场景脚本,负责实际执行节点操作
|
- **src/scene-script.js**: 场景脚本,负责实际执行节点操作
|
||||||
|
- **src/mcp-proxy.js**: MCP 代理,负责在 AI 工具和插件之间转发请求
|
||||||
|
- **src/IpcManager.js**: IPC 消息管理器
|
||||||
|
- **src/IpcUi.js**: IPC 测试面板 UI
|
||||||
- **panel/**: 面板界面,提供用户交互界面
|
- **panel/**: 面板界面,提供用户交互界面
|
||||||
- `index.html`: 面板 UI 结构
|
- `index.html`: 面板 UI 结构
|
||||||
- `index.js`: 面板交互逻辑
|
- `index.js`: 面板交互逻辑
|
||||||
@@ -389,9 +396,9 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
### 数据流
|
### 数据流
|
||||||
|
|
||||||
1. 外部工具发送 MCP 请求到插件的 HTTP 接口
|
1. 外部工具发送 MCP 请求到插件的 HTTP 接口
|
||||||
2. main.js 接收请求并解析参数
|
2. src/main.js 接收请求并解析参数
|
||||||
3. 通过 Editor.Scene.callSceneScript 将请求转发给 scene-script.js
|
3. 通过 Editor.Scene.callSceneScript 将请求转发给 src/scene-script.js
|
||||||
4. scene-script.js 在场景线程中执行具体操作
|
4. src/scene-script.js 在场景线程中执行具体操作
|
||||||
5. 将结果返回给外部工具
|
5. 将结果返回给外部工具
|
||||||
|
|
||||||
## 开发指南
|
## 开发指南
|
||||||
@@ -400,19 +407,20 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
|
|
||||||
要在插件中添加新的 MCP 工具,需要:
|
要在插件中添加新的 MCP 工具,需要:
|
||||||
|
|
||||||
1. 在 main.js 的 `/list-tools` 响应中添加工具定义
|
1. 在 src/main.js 的 `/list-tools` 响应中添加工具定义
|
||||||
2. 在 handleMcpCall 函数中添加对应的处理逻辑
|
2. 在 handleMcpCall 函数中添加对应的处理逻辑
|
||||||
3. 如需在场景线程中执行,需要在 scene-script.js 中添加对应函数
|
3. 如需在场景线程中执行,需要在 src/scene-script.js 中添加对应函数
|
||||||
|
|
||||||
### 日志管理
|
### 日志管理
|
||||||
|
|
||||||
插件会通过内置的测试面板(MCP Bridge/Open Panel)实时记录所有操作的日志,包括:
|
插件会通过内置的测试面板(MCP Bridge/Open Panel)实时记录所有操作的日志,并同步持久化写入项目目录 `settings/mcp-bridge.log` 文件,编辑器重启后仍可查阅历史日志。日志记录包括:
|
||||||
|
|
||||||
- 服务启动/停止状态
|
- 服务启动/停止状态
|
||||||
- MCP 客户端请求接收(完整包含工具的 `arguments` 参数,超长自动截断)
|
- MCP 客户端请求接收(完整包含工具的 `arguments` 参数,超长自动截断)
|
||||||
- 场景节点树遍历与耗时信息
|
- 场景节点树遍历与耗时信息
|
||||||
- 工具调用的执行成功/失败状态返回
|
- 工具调用的执行成功/失败状态返回
|
||||||
- IPC 消息和核心底层报错堆栈
|
- IPC 消息和核心底层报错堆栈
|
||||||
|
- 内存保护:日志缓冲区上限 2000 条,超出自动截断旧日志
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
@@ -434,7 +442,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
请查阅 [UPDATE_LOG.md](./UPDATE_LOG.md) 了解详细的版本更新历史、功能优化与修复过程。
|
请查阅 [UPDATE_LOG.md](./docs/UPDATE_LOG.md) 了解详细的版本更新历史、功能优化与修复过程。
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
|
|||||||
141
dist/IpcManager.js
vendored
141
dist/IpcManager.js
vendored
@@ -1,141 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
||||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
||||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
||||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
||||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
||||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0: case 1: t = op; break;
|
|
||||||
case 4: _.label++; return { value: op[1], done: false };
|
|
||||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
||||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
||||||
default:
|
|
||||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
||||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
||||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop(); continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
||||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.IpcManager = void 0;
|
|
||||||
// @ts-ignore
|
|
||||||
var fs = require('fs');
|
|
||||||
// @ts-ignore
|
|
||||||
var path = require('path');
|
|
||||||
/**
|
|
||||||
* IPC 消息管理器
|
|
||||||
* 负责解析 IPC 文档并执行消息测试
|
|
||||||
*/
|
|
||||||
var IpcManager = /** @class */ (function () {
|
|
||||||
function IpcManager() {
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 获取所有 IPC 消息列表
|
|
||||||
* @returns 消息定义列表
|
|
||||||
*/
|
|
||||||
IpcManager.getIpcMessages = function () {
|
|
||||||
// 获取文档路径
|
|
||||||
// @ts-ignore
|
|
||||||
var docPath = Editor.url('packages://mcp-bridge/IPC_MESSAGES.md');
|
|
||||||
if (!fs.existsSync(docPath)) {
|
|
||||||
// @ts-ignore
|
|
||||||
Editor.error("[IPC Manager] Document not found: ".concat(docPath));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
var content = fs.readFileSync(docPath, 'utf-8');
|
|
||||||
var messages = [];
|
|
||||||
// 正则匹配 ### `message-name`
|
|
||||||
var regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g;
|
|
||||||
var match;
|
|
||||||
while ((match = regex.exec(content)) !== null) {
|
|
||||||
var name_1 = match[1];
|
|
||||||
var body = match[2];
|
|
||||||
// 解析用途
|
|
||||||
var purposeMatch = body.match(/- \*\*用途\*\*: (.*)/);
|
|
||||||
var description = purposeMatch ? purposeMatch[1].trim() : "无描述";
|
|
||||||
// 解析参数
|
|
||||||
var paramsMatch = body.match(/- \*\*参数\*\*: (.*)/);
|
|
||||||
var params = paramsMatch ? paramsMatch[1].trim() : "无";
|
|
||||||
// 解析返回值
|
|
||||||
var returnMatch = body.match(/- \*\*返回值\*\*: (.*)/);
|
|
||||||
var returns = returnMatch ? returnMatch[1].trim() : "无";
|
|
||||||
// 解析类型
|
|
||||||
var typeMatch = body.match(/- \*\*类型\*\*: (.*)/);
|
|
||||||
var type = typeMatch ? typeMatch[1].trim() : "未定义";
|
|
||||||
// 解析状态
|
|
||||||
var statusMatch = body.match(/- \*\*状态\*\*: (.*)/);
|
|
||||||
var status_1 = statusMatch ? statusMatch[1].trim() : "未测试";
|
|
||||||
// 过滤掉广播事件和渲染进程监听的事件
|
|
||||||
if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
messages.push({
|
|
||||||
name: name_1,
|
|
||||||
description: description,
|
|
||||||
params: params,
|
|
||||||
returns: returns,
|
|
||||||
type: type,
|
|
||||||
status: status_1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 测试发送 IPC 消息
|
|
||||||
* @param name 消息名称
|
|
||||||
* @param args 参数
|
|
||||||
* @returns Promise<any> 测试结果
|
|
||||||
*/
|
|
||||||
IpcManager.testIpcMessage = function (name_2) {
|
|
||||||
return __awaiter(this, arguments, void 0, function (name, args) {
|
|
||||||
if (args === void 0) { args = null; }
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
return [2 /*return*/, new Promise(function (resolve) {
|
|
||||||
// 简单防呆:防止执行危险操作
|
|
||||||
// 如果消息包含 "delete", "remove", "close", "stop" 且没有明确参数确认,则警告
|
|
||||||
// 但用户要求"快速验证",所以我们默认允许,但如果是无参调用可能有风险
|
|
||||||
// 这里我们尝试使用 Editor.Ipc.sendToMain 或 requestToMain
|
|
||||||
// @ts-ignore
|
|
||||||
// 简单的测试:只是发送看看是否报错。
|
|
||||||
// 对于 request 类型的消息,我们期望有回调
|
|
||||||
// Cocos Creator 2.4 API: Editor.Ipc.sendToMain(message, ...args)
|
|
||||||
try {
|
|
||||||
// @ts-ignore
|
|
||||||
if (Editor.Ipc.sendToMain) {
|
|
||||||
// @ts-ignore
|
|
||||||
Editor.Ipc.sendToMain(name, args);
|
|
||||||
resolve({ success: true, message: "Message sent (sendToMain)" });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resolve({ success: false, message: "Editor.Ipc.sendToMain not available" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
resolve({ success: false, message: "Error: ".concat(e.message) });
|
|
||||||
}
|
|
||||||
})];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return IpcManager;
|
|
||||||
}());
|
|
||||||
exports.IpcManager = IpcManager;
|
|
||||||
353
dist/IpcUi.js
vendored
353
dist/IpcUi.js
vendored
@@ -1,353 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __awaiter =
|
|
||||||
(this && this.__awaiter) ||
|
|
||||||
function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) {
|
|
||||||
return value instanceof P
|
|
||||||
? value
|
|
||||||
: new P(function (resolve) {
|
|
||||||
resolve(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) {
|
|
||||||
try {
|
|
||||||
step(generator.next(value));
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function rejected(value) {
|
|
||||||
try {
|
|
||||||
step(generator["throw"](value));
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function step(result) {
|
|
||||||
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
||||||
}
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator =
|
|
||||||
(this && this.__generator) ||
|
|
||||||
function (thisArg, body) {
|
|
||||||
var _ = {
|
|
||||||
label: 0,
|
|
||||||
sent: function () {
|
|
||||||
if (t[0] & 1) throw t[1];
|
|
||||||
return t[1];
|
|
||||||
},
|
|
||||||
trys: [],
|
|
||||||
ops: [],
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
y,
|
|
||||||
t,
|
|
||||||
g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
||||||
return (
|
|
||||||
(g.next = verb(0)),
|
|
||||||
(g["throw"] = verb(1)),
|
|
||||||
(g["return"] = verb(2)),
|
|
||||||
typeof Symbol === "function" &&
|
|
||||||
(g[Symbol.iterator] = function () {
|
|
||||||
return this;
|
|
||||||
}),
|
|
||||||
g
|
|
||||||
);
|
|
||||||
function verb(n) {
|
|
||||||
return function (v) {
|
|
||||||
return step([n, v]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while ((g && ((g = 0), op[0] && (_ = 0)), _))
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
((f = 1),
|
|
||||||
y &&
|
|
||||||
(t =
|
|
||||||
op[0] & 2
|
|
||||||
? y["return"]
|
|
||||||
: op[0]
|
|
||||||
? y["throw"] || ((t = y["return"]) && t.call(y), 0)
|
|
||||||
: y.next) &&
|
|
||||||
!(t = t.call(y, op[1])).done)
|
|
||||||
)
|
|
||||||
return t;
|
|
||||||
if (((y = 0), t)) op = [op[0] & 2, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
t = op;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
_.label++;
|
|
||||||
return { value: op[1], done: false };
|
|
||||||
case 5:
|
|
||||||
_.label++;
|
|
||||||
y = op[1];
|
|
||||||
op = [0];
|
|
||||||
continue;
|
|
||||||
case 7:
|
|
||||||
op = _.ops.pop();
|
|
||||||
_.trys.pop();
|
|
||||||
continue;
|
|
||||||
default:
|
|
||||||
if (
|
|
||||||
!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) &&
|
|
||||||
(op[0] === 6 || op[0] === 2)
|
|
||||||
) {
|
|
||||||
_ = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
|
|
||||||
_.label = op[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (op[0] === 6 && _.label < t[1]) {
|
|
||||||
_.label = t[1];
|
|
||||||
t = op;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (t && _.label < t[2]) {
|
|
||||||
_.label = t[2];
|
|
||||||
_.ops.push(op);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) {
|
|
||||||
op = [6, e];
|
|
||||||
y = 0;
|
|
||||||
} finally {
|
|
||||||
f = t = 0;
|
|
||||||
}
|
|
||||||
if (op[0] & 5) throw op[1];
|
|
||||||
return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.IpcUi = void 0;
|
|
||||||
// @ts-ignore
|
|
||||||
var Editor = window.Editor;
|
|
||||||
var IpcUi = /** @class */ (function () {
|
|
||||||
function IpcUi(root) {
|
|
||||||
this.logArea = null;
|
|
||||||
this.ipcList = null;
|
|
||||||
this.allMessages = [];
|
|
||||||
this.filterSelect = null;
|
|
||||||
this.paramInput = null;
|
|
||||||
this.root = root;
|
|
||||||
this.bindEvents();
|
|
||||||
}
|
|
||||||
IpcUi.prototype.bindEvents = function () {
|
|
||||||
var _this = this;
|
|
||||||
var btnScan = this.root.querySelector("#btnScanIpc");
|
|
||||||
var btnTest = this.root.querySelector("#btnTestIpc");
|
|
||||||
var cbSelectAll = this.root.querySelector("#cbSelectAllIpc");
|
|
||||||
this.logArea = this.root.querySelector("#ipcLog");
|
|
||||||
this.ipcList = this.root.querySelector("#ipcList");
|
|
||||||
this.filterSelect = this.root.querySelector("#ipcFilter");
|
|
||||||
this.paramInput = this.root.querySelector("#ipcParams");
|
|
||||||
if (btnScan) {
|
|
||||||
btnScan.addEventListener("confirm", function () {
|
|
||||||
return _this.scanMessages();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (btnTest) {
|
|
||||||
btnTest.addEventListener("confirm", function () {
|
|
||||||
return _this.testSelected();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (cbSelectAll) {
|
|
||||||
cbSelectAll.addEventListener("change", function (e) {
|
|
||||||
return _this.toggleAll(e.detail ? e.detail.value : e.target.value === "true" || e.target.checked);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.filterSelect) {
|
|
||||||
this.filterSelect.addEventListener("change", function () {
|
|
||||||
return _this.filterMessages();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
IpcUi.prototype.scanMessages = function () {
|
|
||||||
var _this = this;
|
|
||||||
this.log("正在扫描 IPC 消息...");
|
|
||||||
// @ts-ignore
|
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", function (err, msgs) {
|
|
||||||
if (err) {
|
|
||||||
_this.log("扫描错误: ".concat(err));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (msgs) {
|
|
||||||
_this.allMessages = msgs;
|
|
||||||
_this.filterMessages();
|
|
||||||
_this.log("找到 ".concat(msgs.length, " 条消息。"));
|
|
||||||
} else {
|
|
||||||
_this.log("未找到任何消息。");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
IpcUi.prototype.filterMessages = function () {
|
|
||||||
if (!this.allMessages) return;
|
|
||||||
var filter = this.filterSelect ? this.filterSelect.value : "all";
|
|
||||||
var filtered = this.allMessages;
|
|
||||||
if (filter === "available") {
|
|
||||||
filtered = this.allMessages.filter(function (m) {
|
|
||||||
return m.status === "可用";
|
|
||||||
});
|
|
||||||
} else if (filter === "unavailable") {
|
|
||||||
filtered = this.allMessages.filter(function (m) {
|
|
||||||
return m.status && m.status.includes("不可用");
|
|
||||||
});
|
|
||||||
} else if (filter === "untested") {
|
|
||||||
filtered = this.allMessages.filter(function (m) {
|
|
||||||
return !m.status || m.status === "未测试";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.renderList(filtered);
|
|
||||||
};
|
|
||||||
IpcUi.prototype.renderList = function (msgs) {
|
|
||||||
var _this = this;
|
|
||||||
if (!this.ipcList) return;
|
|
||||||
this.ipcList.innerHTML = "";
|
|
||||||
msgs.forEach(function (msg) {
|
|
||||||
var item = document.createElement("div");
|
|
||||||
item.className = "ipc-item";
|
|
||||||
item.style.padding = "4px";
|
|
||||||
item.style.borderBottom = "1px solid #333";
|
|
||||||
item.style.display = "flex";
|
|
||||||
item.style.alignItems = "center";
|
|
||||||
// Checkbox
|
|
||||||
var checkbox = document.createElement("ui-checkbox");
|
|
||||||
// @ts-ignore
|
|
||||||
checkbox.value = false;
|
|
||||||
checkbox.setAttribute("data-name", msg.name);
|
|
||||||
checkbox.style.marginRight = "8px";
|
|
||||||
// Content
|
|
||||||
var content = document.createElement("div");
|
|
||||||
content.style.flex = "1";
|
|
||||||
content.style.fontSize = "11px";
|
|
||||||
var statusColor = "#888"; // Untested
|
|
||||||
if (msg.status === "可用")
|
|
||||||
statusColor = "#4CAF50"; // Green
|
|
||||||
else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // Red
|
|
||||||
content.innerHTML =
|
|
||||||
'\n <div style="display:flex; justify-content:space-between;">\n <span style="color: #4CAF50; font-weight: bold;">'
|
|
||||||
.concat(msg.name, '</span>\n <span style="color: ')
|
|
||||||
.concat(statusColor, "; font-size: 10px; border: 1px solid ")
|
|
||||||
.concat(statusColor, '; padding: 0 4px; border-radius: 4px;">')
|
|
||||||
.concat(
|
|
||||||
msg.status || "未测试",
|
|
||||||
'</span>\n </div>\n <div style="color: #888;">',
|
|
||||||
)
|
|
||||||
.concat(
|
|
||||||
msg.description || "无描述",
|
|
||||||
'</div>\n <div style="color: #666; font-size: 10px;">参数: ',
|
|
||||||
)
|
|
||||||
.concat(msg.params || "无", "</div>\n ");
|
|
||||||
// Action Button
|
|
||||||
var btnRun = document.createElement("ui-button");
|
|
||||||
btnRun.innerText = "执行";
|
|
||||||
btnRun.className = "tiny";
|
|
||||||
btnRun.style.height = "20px";
|
|
||||||
btnRun.style.lineHeight = "20px";
|
|
||||||
btnRun.addEventListener("confirm", function () {
|
|
||||||
_this.runTest(msg.name);
|
|
||||||
});
|
|
||||||
item.appendChild(checkbox);
|
|
||||||
item.appendChild(content);
|
|
||||||
item.appendChild(btnRun);
|
|
||||||
_this.ipcList.appendChild(item);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
IpcUi.prototype.testSelected = function () {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var checkboxes, toTest, _i, toTest_1, name_1;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
|
|
||||||
toTest = [];
|
|
||||||
checkboxes.forEach(function (cb) {
|
|
||||||
// In Cocos 2.x, ui-checkbox value is boolean
|
|
||||||
if (cb.checked || cb.value === true) {
|
|
||||||
toTest.push(cb.getAttribute("data-name"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (toTest.length === 0) {
|
|
||||||
this.log("未选择任何消息。");
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
this.log("开始批量测试 ".concat(toTest.length, " 条消息..."));
|
|
||||||
((_i = 0), (toTest_1 = toTest));
|
|
||||||
_a.label = 1;
|
|
||||||
case 1:
|
|
||||||
if (!(_i < toTest_1.length)) return [3 /*break*/, 4];
|
|
||||||
name_1 = toTest_1[_i];
|
|
||||||
return [4 /*yield*/, this.runTest(name_1)];
|
|
||||||
case 2:
|
|
||||||
_a.sent();
|
|
||||||
_a.label = 3;
|
|
||||||
case 3:
|
|
||||||
_i++;
|
|
||||||
return [3 /*break*/, 1];
|
|
||||||
case 4:
|
|
||||||
this.log("批量测试完成。");
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
IpcUi.prototype.runTest = function (name) {
|
|
||||||
var _this = this;
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
var params = null;
|
|
||||||
if (_this.paramInput && _this.paramInput.value.trim()) {
|
|
||||||
try {
|
|
||||||
params = JSON.parse(_this.paramInput.value.trim());
|
|
||||||
} catch (e) {
|
|
||||||
_this.log("[错误] 无效的 JSON 参数: ".concat(e));
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_this.log("正在测试: ".concat(name, ",参数: ").concat(JSON.stringify(params), "..."));
|
|
||||||
// @ts-ignore
|
|
||||||
Editor.Ipc.sendToMain(
|
|
||||||
"mcp-bridge:test-ipc-message",
|
|
||||||
{ name: name, params: params },
|
|
||||||
function (err, result) {
|
|
||||||
if (err) {
|
|
||||||
_this.log("[".concat(name, "] 失败: ").concat(err));
|
|
||||||
} else {
|
|
||||||
_this.log("[".concat(name, "] 成功: ").concat(JSON.stringify(result)));
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
IpcUi.prototype.toggleAll = function (checked) {
|
|
||||||
var checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
|
|
||||||
checkboxes.forEach(function (cb) {
|
|
||||||
cb.value = checked;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
IpcUi.prototype.log = function (msg) {
|
|
||||||
if (this.logArea) {
|
|
||||||
// @ts-ignore
|
|
||||||
var time = new Date().toLocaleTimeString();
|
|
||||||
this.logArea.value += "[".concat(time, "] ").concat(msg, "\n");
|
|
||||||
this.logArea.scrollTop = this.logArea.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return IpcUi;
|
|
||||||
})();
|
|
||||||
exports.IpcUi = IpcUi;
|
|
||||||
@@ -192,7 +192,17 @@
|
|||||||
- **修复**: 在 `scene-script.js` 层加固了前置拦截规则:
|
- **修复**: 在 `scene-script.js` 层加固了前置拦截规则:
|
||||||
1. **直接拦截节点**: 当检测到传入 `cc.Node` 或 `Node` 作为组件类型时直接驳回,并返回富含指导意义的中文提示词(如“请使用 create-node 创建节点”)。
|
1. **直接拦截节点**: 当检测到传入 `cc.Node` 或 `Node` 作为组件类型时直接驳回,并返回富含指导意义的中文提示词(如“请使用 create-node 创建节点”)。
|
||||||
2. **继承链校验**: 提取引擎类定义后,强制要求通过 `cc.js.isChildClassOf` 判断该类必须继承自 `cc.Component`。若不合法则即时截断并提示。
|
2. **继承链校验**: 提取引擎类定义后,强制要求通过 `cc.js.isChildClassOf` 判断该类必须继承自 `cc.Component`。若不合法则即时截断并提示。
|
||||||
- **价值**: 通过将冰冷的底层异常翻译为“手把手教 AI 怎么重试”的指导性异常,彻底根治了 AI 在操作组件时乱认对象、反复撞墙的通病。
|
- **价值**: 通过将冰冷的底层异常翻译为"手把手教 AI 怎么重试"的指导性异常,彻底根治了 AI 在操作组件时乱认对象、反复撞墙的通病。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 面板加载修复 (2026-02-24)
|
||||||
|
|
||||||
|
### 1. `panel/index.js` 语法错误修复
|
||||||
|
|
||||||
|
- **问题**: 面板加载时出现 `SyntaxError: Invalid or unexpected token`,导致 MCP Bridge 插件面板完全无法渲染。
|
||||||
|
- **原因**: `index.js` 中存在非法字符或格式错误,被 Cocos Creator 的面板加载器拒绝解析。
|
||||||
|
- **修复**: 清理了文件中的语法问题,确保面板能够正常加载和初始化。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -205,7 +215,7 @@
|
|||||||
|
|
||||||
### 2. 资源管理层 `save` 动作幻觉别名兼容
|
### 2. 资源管理层 `save` 动作幻觉别名兼容
|
||||||
|
|
||||||
- **问题**: AI 偶尔会幻觉以为 `prefab_management`/`manage_script`/`manage_material`/`manage_texture`/`manage_shader` 的更新动作为 `save`,而不是标准定义的 `update` 或 `write`,导致抛出“未知的管理操作”报错。
|
- **问题**: AI 偶尔会幻觉以为 `prefab_management`/`manage_script`/`manage_material`/`manage_texture`/`manage_shader` 的更新动作为 `save`,而不是标准定义的 `update` 或 `write`,导致抛出"未知的管理操作"报错。
|
||||||
- **修复**: 在 `main.js` 所有这些管理工具的核心路由表中,为 `update` 和 `write` 操作均显式添加了 `case "save":` 作为后备兼容,极大地增强了不同大模型在不同提示词上下文环境下的操作容错率。
|
- **修复**: 在 `main.js` 所有这些管理工具的核心路由表中,为 `update` 和 `write` 操作均显式添加了 `case "save":` 作为后备兼容,极大地增强了不同大模型在不同提示词上下文环境下的操作容错率。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -221,3 +231,67 @@
|
|||||||
|
|
||||||
- **问题**: 为了防止大体积结构传递给 AI 时引发 OOM 截断崩溃,`main.js` 后台强行限制了所有日志向 Webview 输出的边界值 (默认不超过 100~500 字符),导致人类开发者从面板查看时无法追溯长内容如 Base64 和完整序列化返回值。
|
- **问题**: 为了防止大体积结构传递给 AI 时引发 OOM 截断崩溃,`main.js` 后台强行限制了所有日志向 Webview 输出的边界值 (默认不超过 100~500 字符),导致人类开发者从面板查看时无法追溯长内容如 Base64 和完整序列化返回值。
|
||||||
- **修复**: 拆分了拦截逻辑。剔除 `argsPreview` 与 `preview` 针对主面板渲染视图输出 `addLog` 时的预备阶段的 `substring` 剪裁。如今编辑器 UI 内将能看到完整、原生的调用参数和回调结果,而对于通过 HTTP 接口返还给 AI 的载荷依然安全拦截。
|
- **修复**: 拆分了拦截逻辑。剔除 `argsPreview` 与 `preview` 针对主面板渲染视图输出 `addLog` 时的预备阶段的 `substring` 剪裁。如今编辑器 UI 内将能看到完整、原生的调用参数和回调结果,而对于通过 HTTP 接口返还给 AI 的载荷依然安全拦截。
|
||||||
|
|
||||||
|
## 日志系统持久化与健壮性优化 (2026-02-27)
|
||||||
|
|
||||||
|
### 1. 日志文件持久化
|
||||||
|
|
||||||
|
- **问题**: 插件的所有运行日志只保存在内存中(`logBuffer`),编辑器重启后日志全部丢失,无法进行会话级别的问题回溯。
|
||||||
|
- **优化**: 在 `main.js` 的 `addLog` 函数中新增文件写入逻辑。所有日志实时追加写入项目目录下的 `settings/mcp-bridge.log` 文件(懒初始化路径)。
|
||||||
|
- **实现细节**:
|
||||||
|
- 新增 `getLogFilePath()` 辅助函数,通过 `Editor.assetdb.urlToFspath` 推导项目根目录,将日志存放在 `settings/` 子目录中。
|
||||||
|
- 日志格式统一为 `[时间戳] [类型] 内容`,与面板日志保持一致。
|
||||||
|
- 文件写入使用 `fs.appendFileSync` 同步追加,失败时静默不影响主流程。
|
||||||
|
|
||||||
|
### 2. 日志缓冲区内存保护
|
||||||
|
|
||||||
|
- **问题**: 长时间运行的编辑器会话中,`logBuffer` 数组无限增长,最终导致内存压力。
|
||||||
|
- **优化**: 在 `addLog` 中增加上限检查,当日志条数超过 2000 时自动截断旧日志,仅保留最近 1500 条。
|
||||||
|
|
||||||
|
### 3. 请求关联计数器 (`_requestCounter`)
|
||||||
|
|
||||||
|
- **优化**: 新增全局 `_requestCounter` 变量,为每个 HTTP 请求分配唯一的自增序号,便于在高并发场景下追踪同一请求的完整生命周期(从入队到执行到响应)。
|
||||||
|
|
||||||
|
### 4. CommandQueue 兜底超时保护
|
||||||
|
|
||||||
|
- **问题**: 原有的 `processNextCommand` 队列机制依赖每个指令主动调用 `done()` 回调来释放队列。如果某个工具函数内部逻辑异常导致 `done()` 未被调用,整个队列将永久停滞。
|
||||||
|
- **优化**: 在 `enqueueCommand` 中为每个入队指令注册 60 秒兜底超时定时器 (`setTimeout`)。超时后强制释放队列位置并记录错误日志 `[CommandQueue] 指令执行超时(60s),强制释放队列`,确保后续指令不被阻塞。
|
||||||
|
- **正常路径**: 指令正常完成时通过 `clearTimeout` 取消定时器,无额外开销。
|
||||||
|
|
||||||
|
### 5. 日志仅输出关键信息到编辑器控制台
|
||||||
|
|
||||||
|
- **优化**: `addLog` 函数不再将所有类型的日志输出到编辑器控制台,仅 `error` 和 `warn` 级别日志通过 `Editor.error()` / `Editor.warn()` 输出,防止 `info` / `success` / `mcp` 类型日志刷屏干扰开发者。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能与可靠性优化 (2026-02-28)
|
||||||
|
|
||||||
|
### 1. CommandQueue 超时保护恢复
|
||||||
|
|
||||||
|
- **问题**: 合并冲突解决时 `enqueueCommand` 中的 60 秒兜底超时保护代码丢失,导致如果工具函数内部异常未调用 `done()`,整个指令队列将永久停滞,后续所有操作将卡死不再响应。
|
||||||
|
- **修复**: 在 `enqueueCommand` 中为每个入队指令注册 `setTimeout(60000)` 超时定时器,正常完成时通过 `clearTimeout` 取消。
|
||||||
|
|
||||||
|
### 2. HTTP 请求体大小限制
|
||||||
|
|
||||||
|
- **问题**: `_handleRequest` 中 `body += chunk` 无上限保护,超大请求体(恶意或异常客户端)可能耗尽编辑器进程内存。
|
||||||
|
- **修复**: 新增 5MB (`5 * 1024 * 1024`) 请求体上限,超出时返回 HTTP 413 并销毁连接。
|
||||||
|
|
||||||
|
### 3. 日志文件轮转机制
|
||||||
|
|
||||||
|
- **问题**: `settings/mcp-bridge.log` 文件持续追加写入,长期使用会无限增长占用磁盘空间。
|
||||||
|
- **修复**: 在 `getLogFilePath()` 初始化时检查文件大小,超过 2MB 自动将旧日志重命名为 `.old` 备份后创建新文件。
|
||||||
|
|
||||||
|
### 4. 清理冗余调试日志
|
||||||
|
|
||||||
|
- **问题**: `scene-script.js` 中 `update-node-transform` 和 `applyProperties` 共有 8 处 `Editor.log` 调试日志,每次操作都输出到编辑器控制台造成刷屏。
|
||||||
|
- **修复**: 移除所有冗余 `Editor.log` 调试输出,保留必要的 `Editor.warn` 警告(如资源加载失败、属性解析失败等)。
|
||||||
|
|
||||||
|
### 5. `applyProperties` 逻辑修复
|
||||||
|
|
||||||
|
- **问题**: `applyProperties` 启发式资源解析分支中使用了 `return` 而非 `continue`,导致处理到该分支后会直接退出整个 `for...of` 循环,跳过后续属性的设置。
|
||||||
|
- **修复**: 将 `return` 改为 `continue`,确保多属性同时更新时所有属性都能被正确处理。
|
||||||
|
|
||||||
|
### 6. `instantiate-prefab` 统一使用 `findNode`
|
||||||
|
|
||||||
|
- **问题**: `instantiate-prefab` 中查找父节点直接调用 `cc.engine.getInstanceById(parentId)`,绕过了 `findNode` 函数的压缩 UUID 解压与兼容逻辑。
|
||||||
|
- **修复**: 统一改用 `findNode(parentId)`,确保所有场景操作对压缩和非压缩 UUID 格式的兼容性一致。
|
||||||
149
mcp-proxy.js
149
mcp-proxy.js
@@ -1,149 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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(`[代理调试] ${msg}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听标准输入以获取 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;
|
|
||||||
|
|
||||||
// 处理握手初始化
|
|
||||||
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") {
|
|
||||||
forwardToCocos("/list-tools", null, id, "GET");
|
|
||||||
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: {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将请求通过 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) : "";
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
hostname: "127.0.0.1",
|
|
||||||
port: COCOS_PORT,
|
|
||||||
path: path,
|
|
||||||
method: method,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (postData) {
|
|
||||||
options.headers["Content-Length"] = Buffer.byteLength(postData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = http.request(options, (res) => {
|
|
||||||
let resData = "";
|
|
||||||
res.on("data", (d) => (resData += d));
|
|
||||||
res.on("end", () => {
|
|
||||||
try {
|
|
||||||
const cocosRes = JSON.parse(resData);
|
|
||||||
|
|
||||||
// 检查关键字段,确保 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将结果发送给 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 } });
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Cocos Creator MCP 桥接插件",
|
"description": "Cocos Creator MCP 桥接插件",
|
||||||
"author": "Firekula",
|
"author": "Firekula",
|
||||||
"main": "main.js",
|
"main": "src/main.js",
|
||||||
"scene-script": "scene-script.js",
|
"scene-script": "src/scene-script.js",
|
||||||
"main-menu": {
|
"main-menu": {
|
||||||
"MCP 桥接器/开启测试面板": {
|
"MCP 桥接器/开启测试面板": {
|
||||||
"message": "mcp-bridge:open-test-panel"
|
"message": "mcp-bridge:open-test-panel"
|
||||||
|
|||||||
594
panel/index.js
594
panel/index.js
@@ -6,323 +6,335 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { IpcUi } = require("../dist/IpcUi");
|
const { IpcUi } = require("../src/IpcUi");
|
||||||
|
|
||||||
Editor.Panel.extend({
|
Editor.Panel.extend({
|
||||||
/**
|
/**
|
||||||
* 面板 CSS 样式
|
* 面板 CSS 样式
|
||||||
*/
|
*/
|
||||||
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 面板 HTML 模板
|
* 面板 HTML 模板
|
||||||
*/
|
*/
|
||||||
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听来自主进程的消息
|
* 监听来自主进程的消息
|
||||||
*/
|
*/
|
||||||
messages: {
|
messages: {
|
||||||
/**
|
/**
|
||||||
* 接收并渲染日志
|
* 接收并渲染日志
|
||||||
* @param {Object} event IPC 事件对象
|
* @param {Object} event IPC 事件对象
|
||||||
* @param {Object} log 日志数据
|
* @param {Object} log 日志数据
|
||||||
*/
|
*/
|
||||||
"mcp-bridge:on-log"(event, log) {
|
"mcp-bridge:on-log"(event, log) {
|
||||||
this.renderLog(log);
|
this.renderLog(log);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务器状态变更通知
|
* 服务器状态变更通知
|
||||||
* @param {Object} event IPC 事件对象
|
* @param {Object} event IPC 事件对象
|
||||||
* @param {Object} config 服务器配置
|
* @param {Object} config 服务器配置
|
||||||
*/
|
*/
|
||||||
"mcp-bridge:state-changed"(event, config) {
|
"mcp-bridge:state-changed"(event, config) {
|
||||||
this.updateUI(config.active);
|
this.updateUI(config.active);
|
||||||
// 如果服务器已启动,更新面板显示的端口为实际运行端口
|
// 如果服务器已启动,更新面板显示的端口为实际运行端口
|
||||||
if (config.active && config.port) {
|
if (config.active && config.port) {
|
||||||
const portInput = this.shadowRoot.querySelector("#portInput");
|
const portInput = this.shadowRoot.querySelector("#portInput");
|
||||||
if (portInput) portInput.value = config.port;
|
if (portInput) portInput.value = config.port;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 面板就绪回调,进行 DOM 绑定与事件初始化
|
* 面板就绪回调,进行 DOM 绑定与事件初始化
|
||||||
*/
|
*/
|
||||||
ready() {
|
ready() {
|
||||||
const root = this.shadowRoot;
|
const root = this.shadowRoot;
|
||||||
// 获取 DOM 元素映射
|
// 获取 DOM 元素映射
|
||||||
const els = {
|
const els = {
|
||||||
port: root.querySelector("#portInput"),
|
port: root.querySelector("#portInput"),
|
||||||
btnToggle: root.querySelector("#btnToggle"),
|
btnToggle: root.querySelector("#btnToggle"),
|
||||||
autoStart: root.querySelector("#autoStartCheck"),
|
autoStart: root.querySelector("#autoStartCheck"),
|
||||||
logView: root.querySelector("#logConsole"),
|
logView: root.querySelector("#logConsole"),
|
||||||
tabMain: root.querySelector("#tabMain"),
|
tabMain: root.querySelector("#tabMain"),
|
||||||
tabTest: root.querySelector("#tabTest"),
|
tabTest: root.querySelector("#tabTest"),
|
||||||
tabIpc: root.querySelector("#tabIpc"),
|
tabIpc: root.querySelector("#tabIpc"),
|
||||||
panelMain: root.querySelector("#panelMain"),
|
panelMain: root.querySelector("#panelMain"),
|
||||||
panelTest: root.querySelector("#panelTest"),
|
panelTest: root.querySelector("#panelTest"),
|
||||||
panelIpc: root.querySelector("#panelIpc"),
|
panelIpc: root.querySelector("#panelIpc"),
|
||||||
toolName: root.querySelector("#toolName"),
|
toolName: root.querySelector("#toolName"),
|
||||||
toolParams: root.querySelector("#toolParams"),
|
toolParams: root.querySelector("#toolParams"),
|
||||||
toolDescription: root.querySelector("#toolDescription"),
|
toolDescription: root.querySelector("#toolDescription"),
|
||||||
toolsList: root.querySelector("#toolsList"),
|
toolsList: root.querySelector("#toolsList"),
|
||||||
testBtn: root.querySelector("#testBtn"),
|
testBtn: root.querySelector("#testBtn"),
|
||||||
listBtn: root.querySelector("#listToolsBtn"),
|
listBtn: root.querySelector("#listToolsBtn"),
|
||||||
clearBtn: root.querySelector("#clearTestBtn"),
|
clearBtn: root.querySelector("#clearTestBtn"),
|
||||||
result: root.querySelector("#resultContent"),
|
result: root.querySelector("#resultContent"),
|
||||||
left: root.querySelector("#testLeftPanel"),
|
left: root.querySelector("#testLeftPanel"),
|
||||||
resizer: root.querySelector("#testResizer"),
|
resizer: root.querySelector("#testResizer"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. 初始化服务器状态与配置
|
// 1. 初始化服务器状态与配置
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
els.port.value = data.config.port;
|
els.port.value = data.config.port;
|
||||||
els.autoStart.value = data.autoStart;
|
els.autoStart.value = data.autoStart;
|
||||||
this.updateUI(data.config.active);
|
this.updateUI(data.config.active);
|
||||||
els.logView.innerHTML = "";
|
els.logView.innerHTML = "";
|
||||||
data.logs.forEach((l) => this.renderLog(l));
|
data.logs.forEach((l) => this.renderLog(l));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化 IPC 调试专用 UI (由 TypeScript 编写并编译到 dist)
|
// 初始化 IPC 调试专用 UI
|
||||||
new IpcUi(root);
|
new IpcUi(root);
|
||||||
|
|
||||||
// 2. 标签页切换逻辑
|
// 2. 标签页切换逻辑
|
||||||
els.tabMain.addEventListener("confirm", () => {
|
els.tabMain.addEventListener("confirm", () => {
|
||||||
els.tabMain.classList.add("active");
|
els.tabMain.classList.add("active");
|
||||||
els.tabTest.classList.remove("active");
|
els.tabTest.classList.remove("active");
|
||||||
els.tabIpc.classList.remove("active");
|
els.tabIpc.classList.remove("active");
|
||||||
els.panelMain.classList.add("active");
|
els.panelMain.classList.add("active");
|
||||||
els.panelTest.classList.remove("active");
|
els.panelTest.classList.remove("active");
|
||||||
els.panelIpc.classList.remove("active");
|
els.panelIpc.classList.remove("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
els.tabTest.addEventListener("confirm", () => {
|
els.tabTest.addEventListener("confirm", () => {
|
||||||
els.tabTest.classList.add("active");
|
els.tabTest.classList.add("active");
|
||||||
els.tabMain.classList.remove("active");
|
els.tabMain.classList.remove("active");
|
||||||
els.tabIpc.classList.remove("active");
|
els.tabIpc.classList.remove("active");
|
||||||
els.panelTest.classList.add("active");
|
els.panelTest.classList.add("active");
|
||||||
els.panelMain.classList.remove("active");
|
els.panelMain.classList.remove("active");
|
||||||
els.panelIpc.classList.remove("active");
|
els.panelIpc.classList.remove("active");
|
||||||
this.fetchTools(els); // 切换到测试页时自动拉取工具列表
|
this.fetchTools(els); // 切换到测试页时自动拉取工具列表
|
||||||
});
|
});
|
||||||
|
|
||||||
els.tabIpc.addEventListener("confirm", () => {
|
els.tabIpc.addEventListener("confirm", () => {
|
||||||
els.tabIpc.classList.add("active");
|
els.tabIpc.classList.add("active");
|
||||||
els.tabMain.classList.remove("active");
|
els.tabMain.classList.remove("active");
|
||||||
els.tabTest.classList.remove("active");
|
els.tabTest.classList.remove("active");
|
||||||
els.panelIpc.classList.add("active");
|
els.panelIpc.classList.add("active");
|
||||||
els.panelMain.classList.remove("active");
|
els.panelMain.classList.remove("active");
|
||||||
els.panelTest.classList.remove("active");
|
els.panelTest.classList.remove("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. 基础控制按钮逻辑
|
// 3. 基础控制按钮逻辑
|
||||||
els.btnToggle.addEventListener("confirm", () => {
|
els.btnToggle.addEventListener("confirm", () => {
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
|
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
root.querySelector("#btnClear").addEventListener("confirm", () => {
|
root.querySelector("#btnClear").addEventListener("confirm", () => {
|
||||||
els.logView.innerHTML = "";
|
els.logView.innerHTML = "";
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
||||||
});
|
});
|
||||||
|
|
||||||
root.querySelector("#btnCopy").addEventListener("confirm", () => {
|
root.querySelector("#btnCopy").addEventListener("confirm", () => {
|
||||||
require("electron").clipboard.writeText(els.logView.innerText);
|
require("electron").clipboard.writeText(els.logView.innerText);
|
||||||
Editor.success("日志已复制到剪贴板");
|
Editor.success("日志已复制到剪贴板");
|
||||||
});
|
});
|
||||||
|
|
||||||
els.autoStart.addEventListener("change", (e) => {
|
els.autoStart.addEventListener("change", (e) => {
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
|
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. API 测试页交互逻辑
|
// 4. API 测试页交互逻辑
|
||||||
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
|
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
|
||||||
els.clearBtn.addEventListener("confirm", () => {
|
els.clearBtn.addEventListener("confirm", () => {
|
||||||
els.result.value = "";
|
els.result.value = "";
|
||||||
});
|
});
|
||||||
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
||||||
|
|
||||||
// API 探查功能 (辅助开发者发现可用内部 IPC)
|
// API 探查功能 (辅助开发者发现可用内部 IPC)
|
||||||
const probeBtn = root.querySelector("#probeApisBtn");
|
const probeBtn = root.querySelector("#probeApisBtn");
|
||||||
if (probeBtn) {
|
if (probeBtn) {
|
||||||
probeBtn.addEventListener("confirm", () => {
|
probeBtn.addEventListener("confirm", () => {
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:inspect-apis");
|
Editor.Ipc.sendToMain("mcp-bridge:inspect-apis");
|
||||||
els.result.value = "API 探查指令已发送。请查看编辑器控制台 (Console) 获取详细报告。";
|
els.result.value = "API 探查指令已发送。请查看编辑器控制台 (Console) 获取详细报告。";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 测试页分栏拖拽缩放逻辑
|
// 5. 测试页分栏拖拽缩放逻辑
|
||||||
if (els.resizer && els.left) {
|
if (els.resizer && els.left) {
|
||||||
els.resizer.addEventListener("mousedown", (e) => {
|
els.resizer.addEventListener("mousedown", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const startX = e.clientX;
|
const startX = e.clientX;
|
||||||
const startW = els.left.offsetWidth;
|
const startW = els.left.offsetWidth;
|
||||||
const onMove = (ev) => {
|
const onMove = (ev) => {
|
||||||
els.left.style.width = startW + (ev.clientX - startX) + "px";
|
els.left.style.width = startW + (ev.clientX - startX) + "px";
|
||||||
};
|
};
|
||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
document.removeEventListener("mousemove", onMove);
|
document.removeEventListener("mousemove", onMove);
|
||||||
document.removeEventListener("mouseup", onUp);
|
document.removeEventListener("mouseup", onUp);
|
||||||
document.body.style.cursor = "default";
|
document.body.style.cursor = "default";
|
||||||
};
|
};
|
||||||
document.addEventListener("mousemove", onMove);
|
document.addEventListener("mousemove", onMove);
|
||||||
document.addEventListener("mouseup", onUp);
|
document.addEventListener("mouseup", onUp);
|
||||||
document.body.style.cursor = "col-resize";
|
document.body.style.cursor = "col-resize";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从本地服务器获取 MCP 工具列表并渲染
|
* 从本地服务器获取 MCP 工具列表并渲染
|
||||||
* @param {Object} els DOM 元素映射
|
* @param {Object} els DOM 元素映射
|
||||||
*/
|
*/
|
||||||
fetchTools(els) {
|
fetchTools(els) {
|
||||||
const url = `http://localhost:${els.port.value}/list-tools`;
|
const url = `http://localhost:${els.port.value}/list-tools`;
|
||||||
els.result.value = "正在获取工具列表...";
|
els.result.value = "正在获取工具列表...";
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
els.toolsList.innerHTML = "";
|
els.toolsList.innerHTML = "";
|
||||||
const toolsMap = {};
|
const toolsMap = {};
|
||||||
data.tools.forEach((t) => {
|
data.tools.forEach((t) => {
|
||||||
toolsMap[t.name] = t;
|
toolsMap[t.name] = t;
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.className = "tool-item";
|
item.className = "tool-item";
|
||||||
item.textContent = t.name;
|
item.textContent = t.name;
|
||||||
item.onclick = () => {
|
item.onclick = () => {
|
||||||
els.toolName.value = t.name;
|
els.toolName.value = t.name;
|
||||||
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
|
els.toolParams.value = JSON.stringify(this.getExample(t.name), null, 2);
|
||||||
this.showToolDescription(els, t);
|
this.showToolDescription(els, t);
|
||||||
};
|
};
|
||||||
els.toolsList.appendChild(item);
|
els.toolsList.appendChild(item);
|
||||||
});
|
});
|
||||||
this.toolsMap = toolsMap;
|
this.toolsMap = toolsMap;
|
||||||
els.result.value = `成功:加载了 ${data.tools.length} 个工具。`;
|
els.result.value = `成功:加载了 ${data.tools.length} 个工具。`;
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
els.result.value = "获取失败: " + e.message;
|
els.result.value = "获取失败: " + e.message;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在面板中展示工具的详细描述与参数定义
|
* 在面板中展示工具的详细描述与参数定义
|
||||||
* @param {Object} els DOM 元素映射
|
* @param {Object} els DOM 元素映射
|
||||||
* @param {Object} tool 工具定义对象
|
* @param {Object} tool 工具定义对象
|
||||||
*/
|
*/
|
||||||
showToolDescription(els, tool) {
|
showToolDescription(els, tool) {
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
els.toolDescription.textContent = "选择工具以查看说明";
|
els.toolDescription.textContent = "选择工具以查看说明";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let description = tool.description || "暂无描述";
|
let description = tool.description || "暂无描述";
|
||||||
let inputSchema = tool.inputSchema;
|
let inputSchema = tool.inputSchema;
|
||||||
|
|
||||||
let details = [];
|
let details = [];
|
||||||
if (inputSchema && inputSchema.properties) {
|
if (inputSchema && inputSchema.properties) {
|
||||||
details.push("<b>参数说明:</b>");
|
details.push("<b>参数说明:</b>");
|
||||||
for (const [key, prop] of Object.entries(inputSchema.properties)) {
|
for (const [key, prop] of Object.entries(inputSchema.properties)) {
|
||||||
let propDesc = `- <code>${key}</code>`;
|
let propDesc = `- <code>${key}</code>`;
|
||||||
if (prop.description) {
|
if (prop.description) {
|
||||||
propDesc += `: ${prop.description}`;
|
propDesc += `: ${prop.description}`;
|
||||||
}
|
}
|
||||||
if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) {
|
if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) {
|
||||||
propDesc += " <span style='color:#f44'>(必填)</span>";
|
propDesc += " <span style='color:#f44'>(必填)</span>";
|
||||||
}
|
}
|
||||||
details.push(propDesc);
|
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 元素映射
|
* @param {Object} els DOM 元素映射
|
||||||
*/
|
*/
|
||||||
runTest(els) {
|
runTest(els) {
|
||||||
const url = `http://localhost:${els.port.value}/call-tool`;
|
const url = `http://localhost:${els.port.value}/call-tool`;
|
||||||
let args;
|
let args;
|
||||||
try {
|
try {
|
||||||
args = JSON.parse(els.toolParams.value || "{}");
|
args = JSON.parse(els.toolParams.value || "{}");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
els.result.value = "JSON 格式错误: " + e.message;
|
els.result.value = "JSON 格式错误: " + e.message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = { name: els.toolName.value, arguments: args };
|
const body = { name: els.toolName.value, arguments: args };
|
||||||
els.result.value = "正在发送请求...";
|
els.result.value = "正在发送请求...";
|
||||||
fetch(url, { method: "POST", body: JSON.stringify(body) })
|
fetch(url, { method: "POST", body: JSON.stringify(body) })
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
els.result.value = JSON.stringify(d, null, 2);
|
els.result.value = JSON.stringify(d, null, 2);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
els.result.value = "测试异常: " + e.message;
|
els.result.value = "测试异常: " + e.message;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定工具的示例参数
|
* 获取指定工具的示例参数
|
||||||
* @param {string} name 工具名称
|
* @param {string} name 工具名称
|
||||||
* @returns {Object} 示例参数对象
|
* @returns {Object} 示例参数对象
|
||||||
*/
|
*/
|
||||||
getExample(name) {
|
getExample(name) {
|
||||||
const examples = {
|
const examples = {
|
||||||
set_node_name: { id: "节点-UUID", newName: "新名称" },
|
set_node_name: { id: "节点-UUID", newName: "新名称" },
|
||||||
update_node_transform: { id: "节点-UUID", x: 0, y: 0, color: "#FF0000" },
|
update_node_transform: { id: "节点-UUID", x: 0, y: 0, color: "#FF0000" },
|
||||||
create_node: { name: "新节点", type: "sprite", parentId: "" },
|
create_node: { name: "新节点", type: "sprite", parentId: "" },
|
||||||
open_scene: { url: "db://assets/Scene.fire" },
|
open_scene: { url: "db://assets/Scene.fire" },
|
||||||
open_prefab: { url: "db://assets/MyPrefab.prefab" },
|
open_prefab: { url: "db://assets/MyPrefab.prefab" },
|
||||||
manage_editor: { action: "get_selection" },
|
manage_editor: { action: "get_selection" },
|
||||||
find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true },
|
find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true },
|
||||||
manage_material: {
|
manage_material: {
|
||||||
action: "create",
|
action: "create",
|
||||||
path: "db://assets/materials/NewMaterial.mat",
|
path: "db://assets/materials/NewMaterial.mat",
|
||||||
properties: { uniforms: {} },
|
properties: { uniforms: {} },
|
||||||
},
|
},
|
||||||
manage_texture: {
|
manage_texture: {
|
||||||
action: "create",
|
action: "create",
|
||||||
path: "db://assets/textures/NewTexture.png",
|
path: "db://assets/textures/NewTexture.png",
|
||||||
properties: { width: 128, height: 128 },
|
properties: { width: 128, height: 128 },
|
||||||
},
|
},
|
||||||
execute_menu_item: { menuPath: "Assets/Create/Folder" },
|
execute_menu_item: { menuPath: "Assets/Create/Folder" },
|
||||||
apply_text_edits: {
|
apply_text_edits: {
|
||||||
filePath: "db://assets/scripts/TestScript.ts",
|
filePath: "db://assets/scripts/TestScript.ts",
|
||||||
edits: [{ type: "insert", position: 0, text: "// 测试注释\n" }],
|
edits: [{ type: "insert", position: 0, text: "// 测试注释\n" }],
|
||||||
},
|
},
|
||||||
read_console: { limit: 10, type: "log" },
|
read_console: { limit: 10, type: "log" },
|
||||||
validate_script: { filePath: "db://assets/scripts/TestScript.ts" },
|
validate_script: { filePath: "db://assets/scripts/TestScript.ts" },
|
||||||
};
|
};
|
||||||
return examples[name] || {};
|
return examples[name] || {};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将日志条目渲染至面板控制台
|
* 将日志条目渲染至面板控制台
|
||||||
* @param {Object} log 日志对象
|
* @param {Object} log 日志对象
|
||||||
*/
|
*/
|
||||||
renderLog(log) {
|
renderLog(log) {
|
||||||
const view = this.shadowRoot.querySelector("#logConsole");
|
const view = this.shadowRoot.querySelector("#logConsole");
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50;
|
// 限制面板日志 DOM 节点数量,防止长时间运行后面板卡顿
|
||||||
const el = document.createElement("div");
|
while (view.childNodes.length > 1000) {
|
||||||
el.className = `log-item ${log.type}`;
|
view.removeChild(view.firstChild);
|
||||||
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`;
|
}
|
||||||
view.appendChild(el);
|
const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50;
|
||||||
if (atBottom) view.scrollTop = view.scrollHeight;
|
const el = document.createElement("div");
|
||||||
},
|
el.className = `log-item ${log.type}`;
|
||||||
|
// 使用 textContent 代替 innerHTML 防止 XSS 注入
|
||||||
|
const timeSpan = document.createElement("span");
|
||||||
|
timeSpan.className = "time";
|
||||||
|
timeSpan.textContent = log.time;
|
||||||
|
const msgSpan = document.createElement("span");
|
||||||
|
msgSpan.className = "msg";
|
||||||
|
msgSpan.textContent = log.content;
|
||||||
|
el.appendChild(timeSpan);
|
||||||
|
el.appendChild(msgSpan);
|
||||||
|
view.appendChild(el);
|
||||||
|
if (atBottom) view.scrollTop = view.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据服务器运行状态更新 UI 按钮文字与样式
|
* 根据服务器运行状态更新 UI 按钮文字与样式
|
||||||
* @param {boolean} active 服务器是否处于激活状态
|
* @param {boolean} active 服务器是否处于激活状态
|
||||||
*/
|
*/
|
||||||
updateUI(active) {
|
updateUI(active) {
|
||||||
const btn = this.shadowRoot.querySelector("#btnToggle");
|
const btn = this.shadowRoot.querySelector("#btnToggle");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
btn.innerText = active ? "停止" : "启动";
|
btn.innerText = active ? "停止" : "启动";
|
||||||
btn.style.backgroundColor = active ? "#aa4444" : "#44aa44";
|
btn.style.backgroundColor = active ? "#aa4444" : "#44aa44";
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
95
src/IpcManager.js
Normal file
95
src/IpcManager.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPC 消息管理器
|
||||||
|
* 负责解析 IPC 文档并执行消息测试
|
||||||
|
*/
|
||||||
|
class IpcManager {
|
||||||
|
/**
|
||||||
|
* 获取所有 IPC 消息列表
|
||||||
|
* @returns {Array} 消息定义列表
|
||||||
|
*/
|
||||||
|
static getIpcMessages() {
|
||||||
|
// 获取文档路径
|
||||||
|
const docPath = Editor.url("packages://mcp-bridge/docs/IPC_MESSAGES.md");
|
||||||
|
if (!fs.existsSync(docPath)) {
|
||||||
|
Editor.error(`[IPC 管理器] 找不到文档文件: ${docPath}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(docPath, "utf-8");
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
// 正则匹配 ### `message-name`
|
||||||
|
const regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const name = match[1];
|
||||||
|
const body = match[2];
|
||||||
|
|
||||||
|
// 解析用途
|
||||||
|
const purposeMatch = body.match(/- \*\*用途\*\*: (.*)/);
|
||||||
|
const description = purposeMatch ? purposeMatch[1].trim() : "无描述";
|
||||||
|
|
||||||
|
// 解析参数
|
||||||
|
const paramsMatch = body.match(/- \*\*参数\*\*: (.*)/);
|
||||||
|
const params = paramsMatch ? paramsMatch[1].trim() : "无";
|
||||||
|
|
||||||
|
// 解析返回值
|
||||||
|
const returnMatch = body.match(/- \*\*返回值\*\*: (.*)/);
|
||||||
|
const returns = returnMatch ? returnMatch[1].trim() : "无";
|
||||||
|
|
||||||
|
// 解析类型
|
||||||
|
const typeMatch = body.match(/- \*\*类型\*\*: (.*)/);
|
||||||
|
const type = typeMatch ? typeMatch[1].trim() : "未定义";
|
||||||
|
|
||||||
|
// 解析状态
|
||||||
|
const statusMatch = body.match(/- \*\*状态\*\*: (.*)/);
|
||||||
|
const status = statusMatch ? statusMatch[1].trim() : "未测试";
|
||||||
|
|
||||||
|
// 过滤掉广播事件和渲染进程监听的事件
|
||||||
|
if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
params,
|
||||||
|
returns,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试发送 IPC 消息
|
||||||
|
* @param {string} name 消息名称
|
||||||
|
* @param {*} args 参数
|
||||||
|
* @returns {Promise} 测试结果
|
||||||
|
*/
|
||||||
|
static testIpcMessage(name, args) {
|
||||||
|
if (args === undefined) args = null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
if (Editor.Ipc.sendToMain) {
|
||||||
|
Editor.Ipc.sendToMain(name, args);
|
||||||
|
resolve({ success: true, message: "消息已发送 (sendToMain)" });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, message: "Editor.Ipc.sendToMain 不可用" });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ success: false, message: `错误: ${e.message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { IpcManager };
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
const fs = require("fs");
|
|
||||||
// @ts-ignore
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IPC 消息管理器
|
|
||||||
* 负责解析 IPC 文档并执行消息测试
|
|
||||||
*/
|
|
||||||
export class IpcManager {
|
|
||||||
/**
|
|
||||||
* 获取所有 IPC 消息列表
|
|
||||||
* @returns 消息定义列表
|
|
||||||
*/
|
|
||||||
public static getIpcMessages(): any[] {
|
|
||||||
// 获取文档路径
|
|
||||||
// @ts-ignore
|
|
||||||
const docPath = Editor.url("packages://mcp-bridge/IPC_MESSAGES.md");
|
|
||||||
if (!fs.existsSync(docPath)) {
|
|
||||||
// @ts-ignore
|
|
||||||
Editor.error(`[IPC 管理器] 找不到文档文件: ${docPath}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = fs.readFileSync(docPath, "utf-8");
|
|
||||||
const messages: any[] = [];
|
|
||||||
|
|
||||||
// 正则匹配 ### `message-name`
|
|
||||||
const regex = /### `(.*?)`\r?\n([\s\S]*?)(?=### `|$)/g;
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = regex.exec(content)) !== null) {
|
|
||||||
const name = match[1];
|
|
||||||
const body = match[2];
|
|
||||||
|
|
||||||
// 解析用途
|
|
||||||
const purposeMatch = body.match(/- \*\*用途\*\*: (.*)/);
|
|
||||||
const description = purposeMatch ? purposeMatch[1].trim() : "无描述";
|
|
||||||
|
|
||||||
// 解析参数
|
|
||||||
const paramsMatch = body.match(/- \*\*参数\*\*: (.*)/);
|
|
||||||
const params = paramsMatch ? paramsMatch[1].trim() : "无";
|
|
||||||
|
|
||||||
// 解析返回值
|
|
||||||
const returnMatch = body.match(/- \*\*返回值\*\*: (.*)/);
|
|
||||||
const returns = returnMatch ? returnMatch[1].trim() : "无";
|
|
||||||
|
|
||||||
// 解析类型
|
|
||||||
const typeMatch = body.match(/- \*\*类型\*\*: (.*)/);
|
|
||||||
const type = typeMatch ? typeMatch[1].trim() : "未定义";
|
|
||||||
|
|
||||||
// 解析状态
|
|
||||||
const statusMatch = body.match(/- \*\*状态\*\*: (.*)/);
|
|
||||||
const status = statusMatch ? statusMatch[1].trim() : "未测试";
|
|
||||||
|
|
||||||
// 过滤掉广播事件和渲染进程监听的事件
|
|
||||||
if (type === "广播事件" || type === "Events listened by Renderer Process" || type === "渲染进程监听") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.push({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
params,
|
|
||||||
returns,
|
|
||||||
type,
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试发送 IPC 消息
|
|
||||||
* @param name 消息名称
|
|
||||||
* @param args 参数
|
|
||||||
* @returns Promise<any> 测试结果
|
|
||||||
*/
|
|
||||||
public static async testIpcMessage(name: string, args: any = null): Promise<any> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// 简单防呆:防止执行危险操作
|
|
||||||
// 如果消息包含 "delete", "remove", "close", "stop" 且没有明确参数确认,则警告
|
|
||||||
// 但用户要求"快速验证",所以我们默认允许,但如果是无参调用可能有风险
|
|
||||||
// 这里我们尝试使用 Editor.Ipc.sendToMain 或 requestToMain
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
// 简单的测试:只是发送看看是否报错。
|
|
||||||
// 对于 request 类型的消息,我们期望有回调
|
|
||||||
// Cocos Creator 2.4 API: Editor.Ipc.sendToMain(message, ...args)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// @ts-ignore
|
|
||||||
if (Editor.Ipc.sendToMain) {
|
|
||||||
// @ts-ignore
|
|
||||||
Editor.Ipc.sendToMain(name, args);
|
|
||||||
resolve({ success: true, message: "消息已发送 (sendToMain)" });
|
|
||||||
} else {
|
|
||||||
resolve({ success: false, message: "Editor.Ipc.sendToMain 不可用" });
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
resolve({ success: false, message: `错误: ${e.message}` });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
226
src/IpcUi.js
Normal file
226
src/IpcUi.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPC 测试面板 UI 管理器
|
||||||
|
* 负责在面板中展示和测试 IPC 消息
|
||||||
|
*/
|
||||||
|
class IpcUi {
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param {ShadowRoot} root Shadow UI 根节点
|
||||||
|
*/
|
||||||
|
constructor(root) {
|
||||||
|
this.root = root;
|
||||||
|
this.logArea = null;
|
||||||
|
this.ipcList = null;
|
||||||
|
this.allMessages = [];
|
||||||
|
this.filterSelect = null;
|
||||||
|
this.paramInput = null;
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 UI 事件
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
this.ipcList = this.root.querySelector("#ipcList");
|
||||||
|
this.filterSelect = this.root.querySelector("#ipcFilter");
|
||||||
|
this.paramInput = this.root.querySelector("#ipcParams");
|
||||||
|
|
||||||
|
if (btnScan) {
|
||||||
|
btnScan.addEventListener("confirm", () => this.scanMessages());
|
||||||
|
}
|
||||||
|
if (btnTest) {
|
||||||
|
btnTest.addEventListener("confirm", () => this.testSelected());
|
||||||
|
}
|
||||||
|
if (cbSelectAll) {
|
||||||
|
cbSelectAll.addEventListener("change", (e) =>
|
||||||
|
this.toggleAll(e.detail ? e.detail.value : e.target.value === "true" || e.target.checked),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.filterSelect) {
|
||||||
|
this.filterSelect.addEventListener("change", () => this.filterMessages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描 IPC 消息
|
||||||
|
*/
|
||||||
|
scanMessages() {
|
||||||
|
this.log("正在扫描 IPC 消息...");
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", (err, msgs) => {
|
||||||
|
if (err) {
|
||||||
|
this.log(`扫描错误: ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msgs) {
|
||||||
|
this.allMessages = msgs;
|
||||||
|
this.filterMessages();
|
||||||
|
this.log(`找到 ${msgs.length} 条消息。`);
|
||||||
|
} else {
|
||||||
|
this.log("未找到任何消息。");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前选择器过滤消息列表
|
||||||
|
*/
|
||||||
|
filterMessages() {
|
||||||
|
if (!this.allMessages) return;
|
||||||
|
const filter = this.filterSelect ? this.filterSelect.value : "all";
|
||||||
|
|
||||||
|
let filtered = this.allMessages;
|
||||||
|
if (filter === "available") {
|
||||||
|
filtered = this.allMessages.filter((m) => m.status === "可用");
|
||||||
|
} else if (filter === "unavailable") {
|
||||||
|
filtered = this.allMessages.filter((m) => m.status && m.status.includes("不可用"));
|
||||||
|
} else if (filter === "untested") {
|
||||||
|
filtered = this.allMessages.filter((m) => !m.status || m.status === "未测试");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderList(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染消息列表 UI
|
||||||
|
* @param {Array} msgs 消息对象数组
|
||||||
|
*/
|
||||||
|
renderList(msgs) {
|
||||||
|
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";
|
||||||
|
|
||||||
|
// 复选框
|
||||||
|
const checkbox = document.createElement("ui-checkbox");
|
||||||
|
checkbox.value = false;
|
||||||
|
checkbox.setAttribute("data-name", msg.name);
|
||||||
|
checkbox.style.marginRight = "8px";
|
||||||
|
|
||||||
|
// 内容区域
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.style.flex = "1";
|
||||||
|
content.style.fontSize = "11px";
|
||||||
|
|
||||||
|
let statusColor = "#888"; // 未测试
|
||||||
|
if (msg.status === "可用")
|
||||||
|
statusColor = "#4CAF50"; // 绿色
|
||||||
|
else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // 红色
|
||||||
|
|
||||||
|
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 || "无描述"}</div>
|
||||||
|
<div style="color: #666; font-size: 10px;">参数: ${msg.params || "无"}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 执行按钮
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试所有选中的 IPC 消息
|
||||||
|
*/
|
||||||
|
async testSelected() {
|
||||||
|
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
|
||||||
|
const toTest = [];
|
||||||
|
checkboxes.forEach((cb) => {
|
||||||
|
// 在 Cocos 2.x 中, ui-checkbox 的值是布尔型
|
||||||
|
if (cb.checked || cb.value === true) {
|
||||||
|
toTest.push(cb.getAttribute("data-name"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toTest.length === 0) {
|
||||||
|
this.log("未选择任何消息。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(`开始批量测试 ${toTest.length} 条消息...`);
|
||||||
|
for (const name of toTest) {
|
||||||
|
await this.runTest(name);
|
||||||
|
}
|
||||||
|
this.log("批量测试完成。");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行单个测试请求
|
||||||
|
* @param {string} name 消息名称
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
runTest(name) {
|
||||||
|
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(`正在测试: ${name},参数: ${JSON.stringify(params)}...`);
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name, params }, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
this.log(`[${name}] 失败: ${err}`);
|
||||||
|
} else {
|
||||||
|
this.log(`[${name}] 成功: ${JSON.stringify(result)}`);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全选/取消全选
|
||||||
|
* @param {boolean} checked 是否选中
|
||||||
|
*/
|
||||||
|
toggleAll(checked) {
|
||||||
|
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
|
||||||
|
checkboxes.forEach((cb) => {
|
||||||
|
cb.value = checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出日志到界面
|
||||||
|
* @param {string} msg 日志消息
|
||||||
|
*/
|
||||||
|
log(msg) {
|
||||||
|
if (this.logArea) {
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
this.logArea.value += `[${time}] ${msg}\n`;
|
||||||
|
this.logArea.scrollTop = this.logArea.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { IpcUi };
|
||||||
226
src/IpcUi.ts
226
src/IpcUi.ts
@@ -1,226 +0,0 @@
|
|||||||
// @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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param root Shadow UI 根节点
|
|
||||||
*/
|
|
||||||
constructor(root: ShadowRoot) {
|
|
||||||
this.root = root;
|
|
||||||
this.bindEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫描 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";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渲染消息列表 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";
|
|
||||||
|
|
||||||
// 复选框
|
|
||||||
const checkbox = document.createElement("ui-checkbox");
|
|
||||||
// @ts-ignore
|
|
||||||
checkbox.value = false;
|
|
||||||
checkbox.setAttribute("data-name", msg.name);
|
|
||||||
checkbox.style.marginRight = "8px";
|
|
||||||
|
|
||||||
// 内容区域
|
|
||||||
const content = document.createElement("div");
|
|
||||||
content.style.flex = "1";
|
|
||||||
content.style.fontSize = "11px";
|
|
||||||
|
|
||||||
let statusColor = "#888"; // 未测试
|
|
||||||
if (msg.status === "可用")
|
|
||||||
statusColor = "#4CAF50"; // 绿色
|
|
||||||
else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // 红色
|
|
||||||
|
|
||||||
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 || "无描述"}</div>
|
|
||||||
<div style="color: #666; font-size: 10px;">参数: ${msg.params || "无"}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 执行按钮
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试所有选中的 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("未选择任何消息。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log(`开始批量测试 ${toTest.length} 条消息...`);
|
|
||||||
for (const name of toTest) {
|
|
||||||
await this.runTest(name);
|
|
||||||
}
|
|
||||||
this.log("批量测试完成。");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 运行单个测试请求
|
|
||||||
* @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(`正在测试: ${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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全选/取消全选
|
|
||||||
* @param checked 是否选中
|
|
||||||
*/
|
|
||||||
private toggleAll(checked: boolean) {
|
|
||||||
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
|
|
||||||
checkboxes.forEach((cb: any) => {
|
|
||||||
cb.value = checked;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 输出日志到界面
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const { IpcManager } = require("./dist/IpcManager");
|
const { IpcManager } = require("./IpcManager");
|
||||||
|
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const pathModule = require("path");
|
const pathModule = require("path");
|
||||||
@@ -29,7 +29,14 @@ let isProcessingCommand = false;
|
|||||||
*/
|
*/
|
||||||
function enqueueCommand(fn) {
|
function enqueueCommand(fn) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
commandQueue.push({ fn, resolve });
|
// 兜底超时保护:防止 fn 内部未调用 done() 导致队列永久停滞
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
addLog("error", "[CommandQueue] 指令执行超时(60s),强制释放队列");
|
||||||
|
isProcessingCommand = false;
|
||||||
|
resolve();
|
||||||
|
processNextCommand();
|
||||||
|
}, 60000);
|
||||||
|
commandQueue.push({ fn, resolve, timeoutId });
|
||||||
processNextCommand();
|
processNextCommand();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -40,15 +47,17 @@ function enqueueCommand(fn) {
|
|||||||
function processNextCommand() {
|
function processNextCommand() {
|
||||||
if (isProcessingCommand || commandQueue.length === 0) return;
|
if (isProcessingCommand || commandQueue.length === 0) return;
|
||||||
isProcessingCommand = true;
|
isProcessingCommand = true;
|
||||||
const { fn, resolve } = commandQueue.shift();
|
const { fn, resolve, timeoutId } = commandQueue.shift();
|
||||||
try {
|
try {
|
||||||
fn(() => {
|
fn(() => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
isProcessingCommand = false;
|
isProcessingCommand = false;
|
||||||
resolve();
|
resolve();
|
||||||
processNextCommand();
|
processNextCommand();
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 防止队列因未捕获异常永久阻塞
|
// 防止队列因未捕获异常永久阻塞
|
||||||
|
clearTimeout(timeoutId);
|
||||||
addLog("error", `[CommandQueue] 指令执行异常: ${e.message}`);
|
addLog("error", `[CommandQueue] 指令执行异常: ${e.message}`);
|
||||||
isProcessingCommand = false;
|
isProcessingCommand = false;
|
||||||
resolve();
|
resolve();
|
||||||
@@ -92,25 +101,85 @@ function callSceneScriptWithTimeout(pluginName, method, args, callback, timeout
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 封装日志函数,同时发送给面板、保存到内存并在编辑器控制台打印
|
* 日志文件路径(懒初始化,在项目 settings 目录下)
|
||||||
* @param {'info' | 'success' | 'warn' | 'error'} type 日志类型
|
* @type {string|null}
|
||||||
|
*/
|
||||||
|
let _logFilePath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志文件路径
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function getLogFilePath() {
|
||||||
|
if (_logFilePath) return _logFilePath;
|
||||||
|
try {
|
||||||
|
const assetsPath = Editor.assetdb.urlToFspath("db://assets");
|
||||||
|
if (assetsPath) {
|
||||||
|
const projectRoot = pathModule.dirname(assetsPath);
|
||||||
|
const settingsDir = pathModule.join(projectRoot, "settings");
|
||||||
|
if (!fs.existsSync(settingsDir)) {
|
||||||
|
fs.mkdirSync(settingsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
_logFilePath = pathModule.join(settingsDir, "mcp-bridge.log");
|
||||||
|
// 日志轮转: 超过 2MB 时备份旧日志并创建新文件
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(_logFilePath)) {
|
||||||
|
const stats = fs.statSync(_logFilePath);
|
||||||
|
if (stats.size > 2 * 1024 * 1024) {
|
||||||
|
const backupPath = _logFilePath + ".old";
|
||||||
|
if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
|
||||||
|
fs.renameSync(_logFilePath, backupPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* 轮转失败不影响主流程 */
|
||||||
|
}
|
||||||
|
return _logFilePath;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 静默失败,不影响主流程
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封装日志函数
|
||||||
|
* - 所有日志发送到 MCP 测试面板 + 内存缓存
|
||||||
|
* - 仅 error / warn 输出到编辑器控制台(防止刷屏)
|
||||||
|
* - 所有日志实时追加写入项目内 settings/mcp-bridge.log 文件(持久化)
|
||||||
|
* @param {'info' | 'success' | 'warn' | 'error' | 'mcp'} type 日志类型
|
||||||
* @param {string} message 日志内容
|
* @param {string} message 日志内容
|
||||||
*/
|
*/
|
||||||
function addLog(type, message) {
|
function addLog(type, message) {
|
||||||
const logEntry = {
|
const logEntry = {
|
||||||
time: new Date().toLocaleTimeString(),
|
time: new Date().toISOString().replace("T", " ").substring(0, 23),
|
||||||
type: type,
|
type: type,
|
||||||
content: message,
|
content: message,
|
||||||
};
|
};
|
||||||
logBuffer.push(logEntry);
|
logBuffer.push(logEntry);
|
||||||
|
// 防止内存泄漏:限制日志缓存上限
|
||||||
|
if (logBuffer.length > 2000) {
|
||||||
|
logBuffer = logBuffer.slice(-1500);
|
||||||
|
}
|
||||||
|
// 发送到面板
|
||||||
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry);
|
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry);
|
||||||
|
|
||||||
// 【修改】确保所有日志都输出到编辑器控制台,以便用户查看
|
// 仅关键信息输出到编辑器控制台(error / warn)
|
||||||
if (type === "error") {
|
if (type === "error") {
|
||||||
Editor.error(`[MCP] ${message}`);
|
Editor.error(`[MCP] ${message}`);
|
||||||
} else if (type === "warn") {
|
} else if (type === "warn") {
|
||||||
Editor.warn(`[MCP] ${message}`);
|
Editor.warn(`[MCP] ${message}`);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
// 持久化到日志文件(实时写入,确保闪退时不丢失)
|
||||||
|
try {
|
||||||
|
const logPath = getLogFilePath();
|
||||||
|
if (logPath) {
|
||||||
|
const line = `[${logEntry.time}] [${type}] ${message}\n`;
|
||||||
|
fs.appendFileSync(logPath, line, "utf8");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 文件写入失败时静默,不影响主流程
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,11 +885,21 @@ module.exports = {
|
|||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
|
const MAX_BODY_SIZE = 5 * 1024 * 1024; // 5MB 请求体上限
|
||||||
let body = "";
|
let body = "";
|
||||||
|
let aborted = false;
|
||||||
req.on("data", (chunk) => {
|
req.on("data", (chunk) => {
|
||||||
body += chunk;
|
body += chunk;
|
||||||
|
if (body.length > MAX_BODY_SIZE) {
|
||||||
|
aborted = true;
|
||||||
|
addLog("error", `[HTTP] 请求体超过 ${MAX_BODY_SIZE} 字节上限,已拒绝`);
|
||||||
|
res.writeHead(413);
|
||||||
|
res.end(JSON.stringify({ error: "请求体过大" }));
|
||||||
|
req.destroy();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
req.on("end", () => {
|
req.on("end", () => {
|
||||||
|
if (aborted) return;
|
||||||
const url = req.url;
|
const url = req.url;
|
||||||
if (url === "/list-tools") {
|
if (url === "/list-tools") {
|
||||||
const tools = getToolsList();
|
const tools = getToolsList();
|
||||||
150
src/mcp-proxy.js
Normal file
150
src/mcp-proxy.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* MCP 桥接代理脚本
|
||||||
|
* 负责在标准 MCP 客户端 (stdin/stdout) 与 Cocos Creator 插件 (HTTP) 之间转发请求。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前 Cocos Creator 插件监听的端口
|
||||||
|
* 支持通过环境变量 MCP_BRIDGE_PORT 或命令行参数指定端口
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
const COCOS_PORT = parseInt(process.env.MCP_BRIDGE_PORT || process.argv[2] || "3456", 10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送调试日志到标准的错误输出流水
|
||||||
|
* @param {string} msg 日志消息
|
||||||
|
*/
|
||||||
|
function debugLog(msg) {
|
||||||
|
process.stderr.write(`[代理调试] ${msg}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听标准输入以获取 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;
|
||||||
|
|
||||||
|
// 处理握手初始化
|
||||||
|
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") {
|
||||||
|
forwardToCocos("/list-tools", null, id, "GET");
|
||||||
|
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: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将请求通过 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) : "";
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
port: COCOS_PORT,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (postData) {
|
||||||
|
options.headers["Content-Length"] = Buffer.byteLength(postData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = http.request(options, (res) => {
|
||||||
|
let resData = "";
|
||||||
|
res.on("data", (d) => (resData += d));
|
||||||
|
res.on("end", () => {
|
||||||
|
try {
|
||||||
|
const cocosRes = JSON.parse(resData);
|
||||||
|
|
||||||
|
// 检查关键字段,确保 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将结果发送给 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 } });
|
||||||
|
}
|
||||||
@@ -143,13 +143,10 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
"update-node-transform": function (event, args) {
|
"update-node-transform": function (event, args) {
|
||||||
const { id, x, y, scaleX, scaleY, color } = args;
|
const { id, x, y, scaleX, scaleY, color } = args;
|
||||||
Editor.log(`[scene-script] update-node-transform called for ${id} with args: ${JSON.stringify(args)}`);
|
|
||||||
|
|
||||||
let node = findNode(id);
|
let node = findNode(id);
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`);
|
|
||||||
|
|
||||||
// 使用 scene:set-property 实现支持 Undo 的属性修改
|
// 使用 scene:set-property 实现支持 Undo 的属性修改
|
||||||
// 注意:IPC 消息需要发送到 'scene' 面板
|
// 注意:IPC 消息需要发送到 'scene' 面板
|
||||||
if (x !== undefined) {
|
if (x !== undefined) {
|
||||||
@@ -213,7 +210,6 @@ module.exports = {
|
|||||||
Editor.Ipc.sendToMain("scene:dirty");
|
Editor.Ipc.sendToMain("scene:dirty");
|
||||||
Editor.Ipc.sendToAll("scene:node-changed", { uuid: id });
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: id });
|
||||||
|
|
||||||
Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`);
|
|
||||||
if (event.reply) event.reply(null, "变换信息已更新");
|
if (event.reply) event.reply(null, "变换信息已更新");
|
||||||
} else {
|
} else {
|
||||||
if (event.reply) event.reply(new Error("找不到节点"));
|
if (event.reply) event.reply(new Error("找不到节点"));
|
||||||
@@ -369,7 +365,6 @@ module.exports = {
|
|||||||
|
|
||||||
if (targetNode) {
|
if (targetNode) {
|
||||||
handler.target = targetNode;
|
handler.target = targetNode;
|
||||||
Editor.log(`[scene-script] Resolved event target: ${targetNode.name}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +472,6 @@ module.exports = {
|
|||||||
asset = new cc.SpriteFrame(asset);
|
asset = new cc.SpriteFrame(asset);
|
||||||
}
|
}
|
||||||
loadedAssets[idx] = asset;
|
loadedAssets[idx] = asset;
|
||||||
Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`);
|
|
||||||
} else {
|
} else {
|
||||||
Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`);
|
Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`);
|
||||||
}
|
}
|
||||||
@@ -529,7 +523,6 @@ module.exports = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`);
|
|
||||||
} else if (value && value.length > 20) {
|
} else if (value && value.length > 20) {
|
||||||
// 如果明确是组件/节点类型但找不到,才报错
|
// 如果明确是组件/节点类型但找不到,才报错
|
||||||
Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`);
|
Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`);
|
||||||
@@ -540,7 +533,6 @@ module.exports = {
|
|||||||
const targetNode = findNode(value);
|
const targetNode = findNode(value);
|
||||||
if (targetNode) {
|
if (targetNode) {
|
||||||
finalValue = targetNode;
|
finalValue = targetNode;
|
||||||
Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`);
|
|
||||||
} else {
|
} else {
|
||||||
// 找不到节点且是 UUID -> 视为资源
|
// 找不到节点且是 UUID -> 视为资源
|
||||||
const compIndex = node._components.indexOf(component);
|
const compIndex = node._components.indexOf(component);
|
||||||
@@ -552,9 +544,8 @@ module.exports = {
|
|||||||
value: { uuid: value },
|
value: { uuid: value },
|
||||||
isSubProp: false,
|
isSubProp: false,
|
||||||
});
|
});
|
||||||
Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`);
|
|
||||||
}
|
}
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -853,7 +844,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置父节点
|
// 设置父节点
|
||||||
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene;
|
let parent = parentId ? findNode(parentId) : scene;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
instance.parent = parent;
|
instance.parent = parent;
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"es5",
|
|
||||||
"es2015.promise"
|
|
||||||
],
|
|
||||||
"target": "es5",
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"strict": false
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user