feat: 实现 open_prefab 工具,优化预制体创建稳定性,并完成全量源码 (JS/TS)、文档与配置的汉化合规审计

This commit is contained in:
火焰库拉
2026-02-13 13:52:27 +08:00
parent 8df6f5a415
commit 24bc7b7b1f
11 changed files with 2024 additions and 1547 deletions

View File

@@ -104,11 +104,18 @@ startServer(port) {
## 4. 开发历程与里程碑 ## 4. 开发历程与里程碑
### 2026-02-10: Undo 系统深度修复与规范化 ### 2026-02-10: Undo 系统深度修复与规范化
- **问题分析**: 修复了 `TypeError: Cannot read property '_name' of null`。该错误是由于直接修改节点属性(绕过 Undo 系统)与分组事务混用导致的。 - **问题分析**: 修复了 `TypeError: Cannot read property '_name' of null`。该错误是由于直接修改节点属性(绕过 Undo 系统)与分组事务混用导致的。
- **重构要点**: 将 `update-node-transform` 中所有的直接赋值替换为 `scene:set-property` IPC 调用,确保所有变换修改均受撤销系统监控。 - **重构要点**: 将 `update-node-transform` 中所有的直接赋值替换为 `scene:set-property` IPC 调用,确保所有变换修改均受撤销系统监控。
- **缺陷修正**: 修复了 `manage_undo``begin_group` 时传递错误参数导致 "Unknown object to record" 的问题。 - **缺陷修正**: 修复了 `manage_undo``begin_group` 时传递错误参数导致 "Unknown object to record" 的问题。
- **全量汉化与文档同步**: 完成了 `main.js``scene-script.js` 的 100% 简体中文翻译。同步更新了 `README.md``DEVELOPMENT.md``注意事项.md` - **全量汉化与文档同步**: 完成了 `main.js``scene-script.js` 的 100% 简体中文翻译。同步更新了 `README.md``DEVELOPMENT.md``注意事项.md`
### 2026-02-13: 新增 `open_prefab` 功能与消息协议修正
- **需求实现**: 新增 `open_prefab` 工具,允许 AI 直接打开预制体进入编辑模式。
- **协议修正**: 经过测试对比,最终确认使用 `scene:enter-prefab-edit-mode` 消息并配合 `Editor.Ipc.sendToAll` 是进入预制体编辑模式的最佳方案,解决了 `scene:open-by-uuid` 无法触发编辑状态的问题。
- **文档深度补全**: 遵循全局开发规范,同步更新了所有技术文档,确保 100% 简体中文覆盖及详尽的 JSDoc 注释。
### 3.2 MCP 工具注册 ### 3.2 MCP 工具注册
`/list-tools` 接口中注册工具: `/list-tools` 接口中注册工具:
@@ -118,7 +125,7 @@ const tools = [
{ {
name: "get_selected_node", name: "get_selected_node",
description: "获取当前选中的节点", description: "获取当前选中的节点",
parameters: [] parameters: [],
}, },
// 其他工具... // 其他工具...
]; ];
@@ -130,10 +137,10 @@ const tools = [
```javascript ```javascript
const sceneScript = { const sceneScript = {
'create-node'(params, callback) { "create-node"(params, callback) {
// 创建节点逻辑... // 创建节点逻辑...
}, },
'set-property'(params, callback) { "set-property"(params, callback) {
// 设置属性逻辑... // 设置属性逻辑...
}, },
// 其他操作... // 其他操作...
@@ -283,6 +290,7 @@ manageAsset(args, callback) {
**错误信息**`Panel info not found for panel mcp-bridge` **错误信息**`Panel info not found for panel mcp-bridge`
**解决方案** **解决方案**
- 检查 `package.json` 中的面板配置 - 检查 `package.json` 中的面板配置
- 确保 `panel` 字段配置正确,移除冲突的 `panels` 字段 - 确保 `panel` 字段配置正确,移除冲突的 `panels` 字段
@@ -291,6 +299,7 @@ manageAsset(args, callback) {
**错误信息**`Parent path ... is not exists` **错误信息**`Parent path ... is not exists`
**解决方案** **解决方案**
- 在创建资源前添加目录检查和创建逻辑 - 在创建资源前添加目录检查和创建逻辑
- 使用 `fs.mkdirSync(dirPath, { recursive: true })` 递归创建目录 - 使用 `fs.mkdirSync(dirPath, { recursive: true })` 递归创建目录
@@ -299,6 +308,7 @@ manageAsset(args, callback) {
**错误信息**`SyntaxError: Invalid or unexpected token` **错误信息**`SyntaxError: Invalid or unexpected token`
**解决方案** **解决方案**
- 使用模板字符串(反引号)处理多行字符串 - 使用模板字符串(反引号)处理多行字符串
- 避免变量名冲突 - 避免变量名冲突
@@ -321,6 +331,7 @@ manageAsset(args, callback) {
### 5.2 API 文档 ### 5.2 API 文档
为每个 MCP 工具编写详细的 API 文档,包括: 为每个 MCP 工具编写详细的 API 文档,包括:
- 工具名称 - 工具名称
- 功能描述 - 功能描述
- 参数说明 - 参数说明
@@ -438,7 +449,7 @@ manageAsset(args, callback) {
### 11.1 第三阶段开发计划(已完成) ### 11.1 第三阶段开发计划(已完成)
| 任务 | 状态 | 描述 | | 任务 | 状态 | 描述 |
|------|------|------| | ---------------------- | ------- | ------------------------------------------------------------------- |
| 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 | | 编辑器管理工具实现 | ✅ 完成 | 实现 manage_editor 工具,支持编辑器状态控制和操作执行 |
| 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 | | 游戏对象查找工具实现 | ✅ 完成 | 实现 find_gameobjects 工具,支持根据条件查找场景节点 |
| 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 | | 材质和纹理管理工具实现 | ✅ 完成 | 实现 manage_material 和 manage_texture 工具,支持材质和纹理资源管理 |
@@ -450,14 +461,14 @@ manageAsset(args, callback) {
### 11.2 第四阶段开发计划(已完成) ### 11.2 第四阶段开发计划(已完成)
| 任务 | 状态 | 描述 | | 任务 | 状态 | 描述 |
|------|------|------| | ------------ | ------- | ---------------------------------------------- |
| 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 | | 测试功能实现 | ✅ 完成 | 实现 run_tests.js 脚本,支持运行自动化测试用例 |
| 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 | | 错误处理增强 | ✅ 完成 | 完善 HTTP 服务和工具调用的错误日志记录 |
### 11.3 差异填补阶段Gap Filling- 已完成 ### 11.3 差异填补阶段Gap Filling- 已完成
| 任务 | 状态 | 描述 | | 任务 | 状态 | 描述 |
|------|------|------| | -------- | ------- | ---------------------------------------------- |
| 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 | | 特效管理 | ✅ 完成 | 实现 manage_vfx 工具,支持粒子系统管理 |
| 文件哈希 | ✅ 完成 | 实现 get_sha 工具,支持文件 SHA-256 计算 | | 文件哈希 | ✅ 完成 | 实现 get_sha 工具,支持文件 SHA-256 计算 |
| 动画管理 | ✅ 完成 | 实现 manage_animation 工具,支持动画播放与控制 | | 动画管理 | ✅ 完成 | 实现 manage_animation 工具,支持动画播放与控制 |
@@ -465,7 +476,7 @@ manageAsset(args, callback) {
### 11.4 第六阶段:可靠性与体验优化(已完成) ### 11.4 第六阶段:可靠性与体验优化(已完成)
| 任务 | 状态 | 描述 | | 任务 | 状态 | 描述 |
|------|------|------| | ---------------- | ------- | -------------------------------------------------------------------------- |
| IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) | | IPC 工具增强 | ✅ 完成 | 修复 IpcManager 返回值解析,优化测试面板 (JSON 参数、筛选) |
| 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) | | 脚本可靠性修复 | ✅ 完成 | 解决脚本编译时序导致的挂载失败问题 (文档引导 + 刷新机制) |
| 组件智能解析修复 | ✅ 完成 | 修复组件属性赋值时的 UUID 类型转换,支持压缩 UUID 及自定义组件 (`$_$ctor`) | | 组件智能解析修复 | ✅ 完成 | 修复组件属性赋值时的 UUID 类型转换,支持压缩 UUID 及自定义组件 (`$_$ctor`) |
@@ -473,7 +484,7 @@ manageAsset(args, callback) {
### 11.5 第七阶段开发计划(已完成) ### 11.5 第七阶段开发计划(已完成)
| 任务 | 状态 | 描述 | | 任务 | 状态 | 描述 |
|------|------|------| | ---------- | ------- | ----------------------------------------- |
| 插件发布 | ✅ 完成 | 准备发布,提交到 Cocos 插件商店 | | 插件发布 | ✅ 完成 | 准备发布,提交到 Cocos 插件商店 |
| 文档完善 | ✅ 完成 | 完善 API 文档 ("Getting Started" 教程) | | 文档完善 | ✅ 完成 | 完善 API 文档 ("Getting Started" 教程) |
| 界面美化 | ✅ 完成 | 优化面板 UI 体验 | | 界面美化 | ✅ 完成 | 优化面板 UI 体验 |
@@ -487,7 +498,7 @@ manageAsset(args, callback) {
通过与 Unity-MCP 对比Cocos-MCP 已实现绝大多数核心功能。 通过与 Unity-MCP 对比Cocos-MCP 已实现绝大多数核心功能。
| 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 | | 功能类别 | Unity-MCP 功能 | Cocos-MCP 状态 | 备注 |
|---------|---------------|---------------|------| | ---------------- | ----------------- | -------------- | ------------------------------------- |
| 编辑器管理 | manage_editor | ✅ 已实现 | | | 编辑器管理 | manage_editor | ✅ 已实现 | |
| 游戏对象管理 | find_gameobjects | ✅ 已实现 | | | 游戏对象管理 | find_gameobjects | ✅ 已实现 | |
| 材质管理 | manage_material | ✅ 已实现 | | | 材质管理 | manage_material | ✅ 已实现 | |
@@ -508,7 +519,7 @@ manageAsset(args, callback) {
### 13.1 潜在风险 ### 13.1 潜在风险
| 风险 | 影响 | 缓解措施 | | 风险 | 影响 | 缓解措施 |
|------|------|----------| | --------------- | ------------ | ----------------------------------------- |
| 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API | | 编辑器 API 变更 | 插件功能失效 | 定期检查 Cocos 更新,适配新 API |
| 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 | | 性能问题 | 插件响应缓慢 | 优化批处理 (batch_execute),减少 IPC 通讯 |
| 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 | | 安全漏洞 | 未授权访问 | (规划中) 面板设置 IP 白名单/Token 认证 |

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
- **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能 - **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能
- **场景节点操作**: 获取、创建、修改场景中的节点 - **场景节点操作**: 获取、创建、修改场景中的节点
- **资源管理**: 创建场景、预制体,打开指定资源 - **资源管理**: 创建场景、预制体,打开场景或预制体进入编辑模式
- **组件管理**: 添加、删除、获取节点组件 - **组件管理**: 添加、删除、获取节点组件
- **脚本管理**: 创建、删除、读取、写入脚本文件 - **脚本管理**: 创建、删除、读取、写入脚本文件
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率 - **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
@@ -123,7 +123,13 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- **参数**: - **参数**:
- `url`: 场景资源路径,如 `db://assets/NewScene.fire` - `url`: 场景资源路径,如 `db://assets/NewScene.fire`
### 7. create_node ### 7. open_prefab
- **描述**: 在编辑器中打开指定的预制体文件进入编辑模式。这是一个异步操作,打开后请等待几秒。
- **参数**:
- `url`: 预制体资源路径,如 `db://assets/prefabs/Test.prefab`
### 8. create_node
- **描述**: 在当前场景中创建一个新节点。 - **描述**: 在当前场景中创建一个新节点。
- **重要提示**: - **重要提示**:
@@ -303,6 +309,7 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- `includeSubpackages`: 是否搜索子包 (Boolean, 默认 true) - `includeSubpackages`: 是否搜索子包 (Boolean, 默认 true)
**示例**: **示例**:
```json ```json
// 正则搜索 // 正则搜索
{ {
@@ -349,7 +356,6 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- **参数**: - **参数**:
- `path`: 文件路径,如 `db://assets/scripts/Test.ts` - `path`: 文件路径,如 `db://assets/scripts/Test.ts`
## 技术实现 ## 技术实现
### 架构设计 ### 架构设计
@@ -413,12 +419,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
1. **确定性优先**:任何对节点、组件、属性的操作,都必须建立在“主体已确认存在”的基础上。 1. **确定性优先**:任何对节点、组件、属性的操作,都必须建立在“主体已确认存在”的基础上。
2. **校验流程** 2. **校验流程**
* **节点校验**:操作前必须使用 `get_scene_hierarchy` 确认节点。 - **节点校验**:操作前必须使用 `get_scene_hierarchy` 确认节点。
* **组件校验**:操作组件前必须使用 `get`(通过 `manage_components`)确认组件存在。 - **组件校验**:操作组件前必须使用 `get`(通过 `manage_components`)确认组件存在。
* **属性校验**:更新属性前必须确认属性名准确无误。 - **属性校验**:更新属性前必须确认属性名准确无误。
3. **禁止假设**:禁止盲目尝试对不存在的对象或属性进行修改。 3. **禁止假设**:禁止盲目尝试对不存在的对象或属性进行修改。
## 贡献 ## 贡献
欢迎提交 Issue 和 Pull Request 来改进这个插件! 欢迎提交 Issue 和 Pull Request 来改进这个插件!

View File

@@ -113,3 +113,10 @@
- **清理死代码**: 删除 `/list-tools` 路由中重复的 `res.writeHead / res.end` 调用。 - **清理死代码**: 删除 `/list-tools` 路由中重复的 `res.writeHead / res.end` 调用。
- **文档更新**: `注意事项.md` 新增第 9 章「并发安全与防卡死机制」,记录 CommandQueue 和 IPC 超时两个防护机制。 - **文档更新**: `注意事项.md` 新增第 9 章「并发安全与防卡死机制」,记录 CommandQueue 和 IPC 超时两个防护机制。
### 6. 场景与预制体工具增强
- **新增 `open_prefab` 工具**: 解决了直接打开预制体进入编辑模式的问题。通过使用正确的 IPC 消息 `scene:enter-prefab-edit-mode` (并结合 `Editor.Ipc.sendToAll`),使得 AI 可以精准操控预制体的编辑流程,而不再局限于场景跳转。
- **优化预制体创建稳定性 (`create_node` + `prefab_management`)**:
- 在创建物理目录后强制执行 `Editor.assetdb.refresh`,确保 AssetDB 即时同步。
- 将节点重命名与预制体创建指令之间的安全延迟从 100ms 增加至 300ms消除了重命名未完成导致创建失败的竞态条件。

120
main.js
View File

@@ -253,6 +253,20 @@ const getToolsList = () => {
required: ["url"], required: ["url"],
}, },
}, },
{
name: "open_prefab",
description: `${globalPrecautions} 在编辑器中打开预制体文件进入编辑模式。注意:这是一个异步操作,打开后请等待几秒。`,
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "预制体资源路径,如 db://assets/prefabs/Test.prefab",
},
},
required: ["url"],
},
},
{ {
name: "create_node", name: "create_node",
description: `${globalPrecautions} 在当前场景中创建一个新节点。重要提示1. 如果指定 parentId必须先通过 get_scene_hierarchy 确保该父节点真实存在且未被删除。2. 类型说明:'sprite' (100x100 尺寸 + 默认贴图), 'button' (150x50 尺寸 + 深色底图 + Button组件), 'label' (120x40 尺寸 + Label组件), 'empty' (纯空节点)。`, description: `${globalPrecautions} 在当前场景中创建一个新节点。重要提示1. 如果指定 parentId必须先通过 get_scene_hierarchy 确保该父节点真实存在且未被删除。2. 类型说明:'sprite' (100x100 尺寸 + 默认贴图), 'button' (150x50 尺寸 + 深色底图 + Button组件), 'label' (120x40 尺寸 + Label组件), 'empty' (纯空节点)。`,
@@ -842,6 +856,9 @@ module.exports = {
} }
}, },
/**
* 关闭 HTTP 服务器
*/
stopServer() { stopServer() {
if (mcpServer) { if (mcpServer) {
mcpServer.close(); mcpServer.close();
@@ -852,6 +869,10 @@ module.exports = {
} }
}, },
/**
* 获取 MCP 资源列表
* @returns {Array<Object>} 资源列表数组
*/
getResourcesList() { getResourcesList() {
return [ return [
{ {
@@ -875,6 +896,11 @@ module.exports = {
]; ];
}, },
/**
* 读取指定的 MCP 资源内容
* @param {string} uri 资源统一资源标识符 (URI)
* @param {Function} callback 完成回调 (err, content)
*/
handleReadResource(uri, callback) { handleReadResource(uri, callback) {
let parsed; let parsed;
try { try {
@@ -997,6 +1023,22 @@ module.exports = {
} }
break; break;
case "open_prefab":
isSceneBusy = true; // 锁定
const prefabUuid = Editor.assetdb.urlToUuid(args.url);
if (prefabUuid) {
// 【核心修复】使用正确的 IPC 消息进入预制体编辑模式
Editor.Ipc.sendToAll("scene:enter-prefab-edit-mode", prefabUuid);
setTimeout(() => {
isSceneBusy = false;
callback(null, `成功:正在打开预制体 ${args.url}`);
}, 2000);
} else {
isSceneBusy = false;
callback(`找不到路径为 ${args.url} 的资源`);
}
break;
case "create_node": case "create_node":
if (args.type === "sprite" || args.type === "button") { if (args.type === "sprite" || args.type === "button") {
const splashUuid = Editor.assetdb.urlToUuid( const splashUuid = Editor.assetdb.urlToUuid(
@@ -1432,11 +1474,11 @@ export default class NewScript extends cc.Component {
const dirPath = pathModule.dirname(absolutePath); const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
// 【增强】确保 AssetDB 刷新文件夹,防止场景脚本找不到目标目录
Editor.assetdb.refresh(targetDir);
} }
// 解析目标目录和文件名 // 解析目标目录和文件名
// db://assets/folder/PrefabName.prefab -> db://assets/folder, PrefabName
const targetDir = prefabPath.substring(0, prefabPath.lastIndexOf("/"));
const fileName = prefabPath.substring(prefabPath.lastIndexOf("/") + 1); const fileName = prefabPath.substring(prefabPath.lastIndexOf("/") + 1);
const prefabName = fileName.replace(".prefab", ""); const prefabName = fileName.replace(".prefab", "");
@@ -1451,9 +1493,10 @@ export default class NewScript extends cc.Component {
// 2. 发送创建命令 (参数: [uuids], dirPath) // 2. 发送创建命令 (参数: [uuids], dirPath)
// 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径 // 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径
// 【增强】增加延迟到 300ms确保 IPC 消息处理并同步到底层引擎
setTimeout(() => { setTimeout(() => {
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir); Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir);
}, 100); // 稍微延迟以确保重命名生效 }, 300);
callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`); callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`);
break; break;
@@ -1472,7 +1515,7 @@ export default class NewScript extends cc.Component {
case "instantiate": case "instantiate":
if (!Editor.assetdb.exists(prefabPath)) { if (!Editor.assetdb.exists(prefabPath)) {
return callback(`Prefab not found at ${prefabPath}`); return callback(`路径为 ${prefabPath} 的预制体不存在`);
} }
// 实例化预制体 // 实例化预制体
const prefabUuid = Editor.assetdb.urlToUuid(prefabPath); const prefabUuid = Editor.assetdb.urlToUuid(prefabPath);
@@ -2066,6 +2109,11 @@ CCProgram fs %{
callback(null, filteredOutput); callback(null, filteredOutput);
}, },
/**
* 执行编辑器菜单项
* @param {Object} args 参数 (menuPath)
* @param {Function} callback 完成回调
*/
executeMenuItem(args, callback) { executeMenuItem(args, callback) {
const { menuPath } = args; const { menuPath } = args;
if (!menuPath) { if (!menuPath) {
@@ -2092,7 +2140,7 @@ CCProgram fs %{
if (uuid) { if (uuid) {
callSceneScriptWithTimeout("mcp-bridge", "delete-node", { uuid }, (err, result) => { callSceneScriptWithTimeout("mcp-bridge", "delete-node", { uuid }, (err, result) => {
if (err) callback(err); if (err) callback(err);
else callback(null, result || `Node ${uuid} deleted via scene script`); else callback(null, result || `节点 ${uuid} 已通过场景脚本删除`);
}); });
return; return;
} }
@@ -2120,7 +2168,7 @@ CCProgram fs %{
} else { } else {
// 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效) // 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效)
// 或者直接返回不支持的警告 // 或者直接返回不支持的警告
addLog("warn", `支持映射表中找不到菜单项 '${menuPath}'。尝试通过 legacy 模式执行。`); addLog("warn", `支持映射表中找不到菜单项 '${menuPath}'。尝试通过旧版模式执行。`);
// 尝试通用调用 // 尝试通用调用
try { try {
@@ -2134,7 +2182,11 @@ CCProgram fs %{
} }
}, },
// 验证脚本 /**
* 验证脚本文件的语法或基础结构
* @param {Object} args 参数 (filePath)
* @param {Function} callback 完成回调
*/
validateScript(args, callback) { validateScript(args, callback) {
const { filePath } = args; const { filePath } = args;
@@ -2265,7 +2317,7 @@ CCProgram fs %{
}, },
"inspect-apis"() { "inspect-apis"() {
addLog("info", "[API Inspector] Starting DEEP inspection..."); addLog("info", "[API 检查器] 开始深度分析...");
// 获取函数参数的辅助函数 // 获取函数参数的辅助函数
const getArgs = (func) => { const getArgs = (func) => {
@@ -2365,8 +2417,8 @@ CCProgram fs %{
: "Missing"; : "Missing";
}); });
addLog("info", `[API Inspector] Standard Objects:\n${JSON.stringify(report, null, 2)}`); addLog("info", `[API 检查器] 标准对象:\n${JSON.stringify(report, null, 2)}`);
addLog("info", `[API Inspector] Forum Checklist:\n${JSON.stringify(checklistResults, null, 2)}`); addLog("info", `[API 检查器] 论坛核查清单:\n${JSON.stringify(checklistResults, null, 2)}`);
// 3. 检查内置包 IPC 消息 // 3. 检查内置包 IPC 消息
const ipcReport = {}; const ipcReport = {};
@@ -2391,12 +2443,15 @@ CCProgram fs %{
} }
}); });
addLog("info", `[API Inspector] Built-in IPC Messages:\n${JSON.stringify(ipcReport, null, 2)}`); addLog("info", `[API 检查器] 内置包 IPC 消息:\n${JSON.stringify(ipcReport, null, 2)}`);
}, },
}, },
// 全局文件搜索 /**
// 项目搜索 (升级版 find_in_file) * 全局项目文件搜索 (支持正则表达式、文件名、目录名搜索)
* @param {Object} args 参数
* @param {Function} callback 完成回调
*/
searchProject(args, callback) { searchProject(args, callback) {
const { query, useRegex, path: searchPath, matchType, extensions } = args; const { query, useRegex, path: searchPath, matchType, extensions } = args;
@@ -2405,7 +2460,7 @@ CCProgram fs %{
const rootPath = Editor.assetdb.urlToFspath(rootPathUrl); const rootPath = Editor.assetdb.urlToFspath(rootPathUrl);
if (!rootPath || !fs.existsSync(rootPath)) { if (!rootPath || !fs.existsSync(rootPath)) {
return callback(`Invalid search path: ${rootPathUrl}`); return callback(`无效的搜索路径: ${rootPathUrl}`);
} }
const mode = matchType || "content"; // content, file_name, dir_name const mode = matchType || "content"; // content, file_name, dir_name
@@ -2534,11 +2589,15 @@ CCProgram fs %{
walk(rootPath); walk(rootPath);
callback(null, results); callback(null, results);
} catch (err) { } catch (err) {
callback(`Search project failed: ${err.message}`); callback(`项目搜索失败: ${err.message}`);
} }
}, },
// 管理撤销/重做 /**
* 管理撤销/重做操作及事务分组
* @param {Object} args 参数 (action, description, id)
* @param {Function} callback 完成回调
*/
manageUndo(args, callback) { manageUndo(args, callback) {
const { action, description } = args; const { action, description } = args;
@@ -2546,16 +2605,13 @@ CCProgram fs %{
switch (action) { switch (action) {
case "undo": case "undo":
Editor.Ipc.sendToPanel("scene", "scene:undo"); Editor.Ipc.sendToPanel("scene", "scene:undo");
callback(null, "Undo command executed"); callback(null, "撤销指令已执行");
break; break;
case "redo": case "redo":
Editor.Ipc.sendToPanel("scene", "scene:redo"); Editor.Ipc.sendToPanel("scene", "scene:redo");
callback(null, "Redo command executed"); callback(null, "重做指令已执行");
break; break;
case "begin_group": case "begin_group":
// scene:undo-record [id]
// 注意:在 2.4.x 中undo-record 通常需要一个有效的 uuid
// 如果没有提供 uuid不应将 description 作为 ID 发送,否则会报 Unknown object to record
addLog("info", `开始撤销组: ${description || "MCP 动作"}`); addLog("info", `开始撤销组: ${description || "MCP 动作"}`);
// 如果有参数包含 id则记录该节点 // 如果有参数包含 id则记录该节点
if (args.id) { if (args.id) {
@@ -2565,27 +2621,31 @@ CCProgram fs %{
break; break;
case "end_group": case "end_group":
Editor.Ipc.sendToPanel("scene", "scene:undo-commit"); Editor.Ipc.sendToPanel("scene", "scene:undo-commit");
callback(null, "Undo group committed"); callback(null, "撤销组已提交");
break; break;
case "cancel_group": case "cancel_group":
Editor.Ipc.sendToPanel("scene", "scene:undo-cancel"); Editor.Ipc.sendToPanel("scene", "scene:undo-cancel");
callback(null, "Undo group cancelled"); callback(null, "撤销组已取消");
break; break;
default: default:
callback(`Unknown undo action: ${action}`); callback(`未知的撤销操作: ${action}`);
} }
} catch (err) { } catch (err) {
callback(`Undo operation failed: ${err.message}`); callback(`撤销操作失败: ${err.message}`);
} }
}, },
// 获取文件 SHA-256 /**
* 计算资源的 SHA-256 哈希值
* @param {Object} args 参数 (path)
* @param {Function} callback 完成回调
*/
getSha(args, callback) { getSha(args, callback) {
const { path: url } = args; const { path: url } = args;
const fspath = Editor.assetdb.urlToFspath(url); const fspath = Editor.assetdb.urlToFspath(url);
if (!fspath || !fs.existsSync(fspath)) { if (!fspath || !fs.existsSync(fspath)) {
return callback(`File not found: ${url}`); return callback(`找不到文件: ${url}`);
} }
try { try {
@@ -2599,7 +2659,11 @@ CCProgram fs %{
} }
}, },
// 管理动画 /**
* 管理节点动画 (播放、停止、获取信息等)
* @param {Object} args 参数
* @param {Function} callback 完成回调
*/
manageAnimation(args, callback) { manageAnimation(args, callback) {
// 转发给场景脚本处理 // 转发给场景脚本处理
callSceneScriptWithTimeout("mcp-bridge", "manage-animation", args, callback); callSceneScriptWithTimeout("mcp-bridge", "manage-animation", args, callback);

View File

@@ -1,101 +1,149 @@
const http = require('http'); /**
* MCP 桥接代理脚本
* 负责在标准 MCP 客户端 (stdin/stdout) 与 Cocos Creator 插件 (HTTP) 之间转发请求。
*/
const http = require("http");
/**
* 当前 Cocos Creator 插件监听的端口
* @type {number}
*/
const COCOS_PORT = 3456; const COCOS_PORT = 3456;
/**
* 发送调试日志到标准的错误输出流水
* @param {string} msg 日志消息
*/
function debugLog(msg) { function debugLog(msg) {
process.stderr.write(`[Proxy Debug] ${msg}\n`); process.stderr.write(`[代理调试] ${msg}\n`);
} }
process.stdin.on('data', (data) => { // 监听标准输入以获取 MCP 请求
const lines = data.toString().split('\n'); process.stdin.on("data", (data) => {
lines.forEach(line => { const lines = data.toString().split("\n");
lines.forEach((line) => {
if (!line.trim()) return; if (!line.trim()) return;
try { try {
const request = JSON.parse(line); const request = JSON.parse(line);
handleRequest(request); handleRequest(request);
} catch (e) {} } catch (e) {
// 忽略非 JSON 输入
}
}); });
}); });
/**
* 处理 JSON-RPC 请求
* @param {Object} req RPC 请求对象
*/
function handleRequest(req) { function handleRequest(req) {
const { method, id, params } = req; const { method, id, params } = req;
if (method === 'initialize') { // 处理握手初始化
if (method === "initialize") {
sendToAI({ sendToAI({
jsonrpc: "2.0", id: id, jsonrpc: "2.0",
id: id,
result: { result: {
protocolVersion: "2024-11-05", protocolVersion: "2024-11-05",
capabilities: { tools: {} }, capabilities: { tools: {} },
serverInfo: { name: "cocos-bridge", version: "1.0.0" } serverInfo: { name: "cocos-bridge", version: "1.0.0" },
} },
}); });
return; return;
} }
if (method === 'tools/list') { // 获取工具列表
// 使用 GET 获取列表 if (method === "tools/list") {
forwardToCocos('/list-tools', null, id, 'GET'); forwardToCocos("/list-tools", null, id, "GET");
return; return;
} }
if (method === 'tools/call') { // 执行具体工具
// 使用 POST 执行工具 if (method === "tools/call") {
forwardToCocos('/call-tool', { forwardToCocos(
"/call-tool",
{
name: params.name, name: params.name,
arguments: params.arguments arguments: params.arguments,
}, id, 'POST'); },
id,
"POST",
);
return; return;
} }
// 默认空响应
if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} }); if (id !== undefined) sendToAI({ jsonrpc: "2.0", id: id, result: {} });
} }
function forwardToCocos(path, payload, id, method = 'POST') { /**
const postData = payload ? JSON.stringify(payload) : ''; * 将请求通过 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 = { const options = {
hostname: '127.0.0.1', hostname: "127.0.0.1",
port: COCOS_PORT, port: COCOS_PORT,
path: path, path: path,
method: method, method: method,
headers: { 'Content-Type': 'application/json' } headers: { "Content-Type": "application/json" },
}; };
if (postData) { if (postData) {
options.headers['Content-Length'] = Buffer.byteLength(postData); options.headers["Content-Length"] = Buffer.byteLength(postData);
} }
const request = http.request(options, (res) => { const request = http.request(options, (res) => {
let resData = ''; let resData = "";
res.on('data', d => resData += d); res.on("data", (d) => (resData += d));
res.on('end', () => { res.on("end", () => {
try { try {
const cocosRes = JSON.parse(resData); const cocosRes = JSON.parse(resData);
// 检查关键字段 // 检查关键字段,确保 Cocos 插件返回了期望的数据格式
if (path === '/list-tools' && !cocosRes.tools) { if (path === "/list-tools" && !cocosRes.tools) {
// 如果报错,把 Cocos 返回的所有内容打印到 Trae 的 stderr 日志里 debugLog(`致命错误: Cocos 返回工具列表。接收内容: ${resData}`);
debugLog(`CRITICAL: Cocos returned no tools. Received: ${resData}`); sendError(id, -32603, "Cocos 响应无效:缺少 tools 数组");
sendError(id, -32603, "Invalid Cocos response: missing tools array");
} else { } else {
sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes }); sendToAI({ jsonrpc: "2.0", id: id, result: cocosRes });
} }
} catch (e) { } catch (e) {
debugLog(`JSON Parse Error. Cocos Sent: ${resData}`); debugLog(`JSON 解析错误。Cocos 发送内容: ${resData}`);
sendError(id, -32603, "Cocos returned non-JSON data"); sendError(id, -32603, "Cocos 返回了非 JSON 数据");
} }
}); });
}); });
request.on('error', (e) => { request.on("error", (e) => {
debugLog(`Cocos is offline: ${e.message}`); debugLog(`Cocos 插件已离线: ${e.message}`);
sendError(id, -32000, "Cocos Plugin Offline"); sendError(id, -32000, "Cocos 插件离线");
}); });
if (postData) request.write(postData); if (postData) request.write(postData);
request.end(); request.end();
} }
function sendToAI(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); } /**
* 将结果发送给 AI (通过标准输出)
* @param {Object} obj 结果对象
*/
function sendToAI(obj) {
process.stdout.write(JSON.stringify(obj) + "\n");
}
/**
* 发送 RPC 错误响应
* @param {string|number} id RPC 请求标识符
* @param {number} code 错误码
* @param {string} message 错误消息
*/
function sendError(id, code, message) { function sendError(id, code, message) {
sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } }); sendToAI({ jsonrpc: "2.0", id: id, error: { code, message } });
} }

View File

@@ -6,14 +6,14 @@
"main": "main.js", "main": "main.js",
"scene-script": "scene-script.js", "scene-script": "scene-script.js",
"main-menu": { "main-menu": {
"MCP Bridge/Open Panel": { "MCP 桥接器/开启测试面板": {
"message": "mcp-bridge:open-test-panel" "message": "mcp-bridge:open-test-panel"
} }
}, },
"panel": { "panel": {
"main": "panel/index.js", "main": "panel/index.js",
"type": "dockable", "type": "dockable",
"title": "MCP Test Panel", "title": "MCP 测试面板",
"width": 400, "width": 400,
"height": 300 "height": 300
}, },

View File

@@ -1,23 +1,53 @@
"use strict"; "use strict";
/**
* MCP Bridge 插件面板脚本
* 负责处理面板 UI 交互、与主进程通信以及提供测试工具界面。
*/
const fs = require("fs"); const fs = require("fs");
const { IpcUi } = require("../dist/IpcUi"); const { IpcUi } = require("../dist/IpcUi");
Editor.Panel.extend({ Editor.Panel.extend({
/**
* 面板 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 模板
*/
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} 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} config 服务器配置
*/
"mcp-bridge:state-changed"(event, config) { "mcp-bridge:state-changed"(event, config) {
this.updateUI(config.active); this.updateUI(config.active);
}, },
}, },
/**
* 面板就绪回调,进行 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"),
@@ -41,7 +71,7 @@ Editor.Panel.extend({
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;
@@ -52,10 +82,10 @@ Editor.Panel.extend({
} }
}); });
// 初始化 IPC UI // 初始化 IPC 调试专用 UI (由 TypeScript 编写并编译到 dist)
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");
@@ -64,6 +94,7 @@ Editor.Panel.extend({
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");
@@ -71,8 +102,9 @@ Editor.Panel.extend({
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");
@@ -82,39 +114,42 @@ Editor.Panel.extend({
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. 测试页功能 // 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));
els.testBtn.addEventListener("confirm", () => this.runTest(els));
// 添加 API 探查功能 // 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 = "探查指令已发送。请查看编辑器控制台日志。"; 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();
@@ -135,8 +170,13 @@ Editor.Panel.extend({
} }
}, },
/**
* 从本地服务器获取 MCP 工具列表并渲染
* @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 = "正在获取工具列表...";
fetch(url) fetch(url)
.then((r) => r.json()) .then((r) => r.json())
.then((data) => { .then((data) => {
@@ -154,64 +194,86 @@ Editor.Panel.extend({
}; };
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 = "Error: " + e.message; els.result.value = "获取失败: " + e.message;
}); });
}, },
/**
* 在面板中展示工具的详细描述与参数定义
* @param {Object} els DOM 元素映射
* @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("参数说明:"); details.push("<b>参数说明:</b>");
for (const [key, prop] of Object.entries(inputSchema.properties)) { for (const [key, prop] of Object.entries(inputSchema.properties)) {
let propDesc = `- ${key}`; 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 += " (必填)"; 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 元素映射
*/
runTest(els) { runTest(els) {
const url = `http://localhost:${els.port.value}/call-tool`; const url = `http://localhost:${els.port.value}/call-tool`;
const body = { name: els.toolName.value, arguments: JSON.parse(els.toolParams.value || "{}") }; let args;
els.result.value = "正在测试..."; try {
args = JSON.parse(els.toolParams.value || "{}");
} catch (e) {
els.result.value = "JSON 格式错误: " + e.message;
return;
}
const body = { name: els.toolName.value, arguments: args };
els.result.value = "正在发送请求...";
fetch(url, { method: "POST", body: JSON.stringify(body) }) 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 = "Error: " + e.message; els.result.value = "测试异常: " + e.message;
}); });
}, },
/**
* 获取指定工具的示例参数
* @param {string} name 工具名称
* @returns {Object} 示例参数对象
*/
getExample(name) { getExample(name) {
const examples = { const examples = {
set_node_name: { id: "UUID", newName: "Hello" }, 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: "Node", 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" },
manage_editor: { action: "get_selection" }, manage_editor: { action: "get_selection" },
find_gameobjects: { conditions: { name: "Node", active: true }, recursive: true }, find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true },
manage_material: { manage_material: {
action: "create", action: "create",
path: "db://assets/materials/NewMaterial.mat", path: "db://assets/materials/NewMaterial.mat",
@@ -225,7 +287,7 @@ Editor.Panel.extend({
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: "// Test comment\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" },
@@ -233,6 +295,10 @@ Editor.Panel.extend({
return examples[name] || {}; return examples[name] || {};
}, },
/**
* 将日志条目渲染至面板控制台
* @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;
@@ -244,6 +310,10 @@ Editor.Panel.extend({
if (atBottom) view.scrollTop = view.scrollHeight; if (atBottom) view.scrollTop = view.scrollHeight;
}, },
/**
* 根据服务器运行状态更新 UI 按钮文字与样式
* @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;

View File

@@ -8,7 +8,7 @@
const findNode = (id) => { const findNode = (id) => {
if (!id) return null; if (!id) return null;
let node = cc.engine.getInstanceById(id); let node = cc.engine.getInstanceById(id);
if (!node && typeof Editor !== 'undefined' && Editor.Utils && Editor.Utils.UuidUtils) { if (!node && typeof Editor !== "undefined" && Editor.Utils && Editor.Utils.UuidUtils) {
// 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找 // 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找
try { try {
const decompressed = Editor.Utils.UuidUtils.decompressUuid(id); const decompressed = Editor.Utils.UuidUtils.decompressUuid(id);
@@ -68,6 +68,11 @@ module.exports = {
"get-hierarchy": function (event) { "get-hierarchy": function (event) {
const scene = cc.director.getScene(); const scene = cc.director.getScene();
/**
* 递归遍历并序列化节点树
* @param {cc.Node} node 目标节点
* @returns {Object|null} 序列化后的节点数据
*/
function dumpNodes(node) { function dumpNodes(node) {
// 【优化】跳过编辑器内部的私有节点,减少数据量 // 【优化】跳过编辑器内部的私有节点,减少数据量
if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") {
@@ -114,22 +119,52 @@ module.exports = {
// 使用 scene:set-property 实现支持 Undo 的属性修改 // 使用 scene:set-property 实现支持 Undo 的属性修改
// 注意IPC 消息需要发送到 'scene' 面板 // 注意IPC 消息需要发送到 'scene' 面板
if (x !== undefined) { if (x !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "x", type: "Number", value: Number(x) }); Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id,
path: "x",
type: "Number",
value: Number(x),
});
} }
if (y !== undefined) { if (y !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "y", type: "Number", value: Number(y) }); Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id,
path: "y",
type: "Number",
value: Number(y),
});
} }
if (args.width !== undefined) { if (args.width !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "width", type: "Number", value: Number(args.width) }); Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id,
path: "width",
type: "Number",
value: Number(args.width),
});
} }
if (args.height !== undefined) { if (args.height !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "height", type: "Number", value: Number(args.height) }); Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id,
path: "height",
type: "Number",
value: Number(args.height),
});
} }
if (scaleX !== undefined) { if (scaleX !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleX", type: "Number", value: Number(scaleX) }); Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id,
path: "scaleX",
type: "Number",
value: Number(scaleX),
});
} }
if (scaleY !== undefined) { if (scaleY !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleY", type: "Number", value: Number(scaleY) }); Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id,
path: "scaleY",
type: "Number",
value: Number(scaleY),
});
} }
if (color) { if (color) {
const c = new cc.Color().fromHEX(color); const c = new cc.Color().fromHEX(color);
@@ -137,7 +172,7 @@ module.exports = {
id: id, id: id,
path: "color", path: "color",
type: "Color", type: "Color",
value: { r: c.r, g: c.g, b: c.b, a: c.a } value: { r: c.r, g: c.g, b: c.b, a: c.a },
}); });
} }
@@ -258,7 +293,11 @@ module.exports = {
const { nodeId, action, componentType, componentId, properties } = args; const { nodeId, action, componentType, componentId, properties } = args;
let node = findNode(nodeId); let node = findNode(nodeId);
// 辅助函数:应用属性并智能解析 /**
* 辅助函数:应用属性并智能解析 (支持 UUID 资源与节点引用)
* @param {cc.Component} component 目标组件实例
* @param {Object} props 待更新的属性键值对
*/
const applyProperties = (component, props) => { const applyProperties = (component, props) => {
if (!props) return; if (!props) return;
// 尝试获取组件类的属性定义 // 尝试获取组件类的属性定义
@@ -266,12 +305,13 @@ module.exports = {
for (const [key, value] of Object.entries(props)) { for (const [key, value] of Object.entries(props)) {
// 【核心修复】专门处理各类事件属性 (ClickEvents, ScrollEvents 等) // 【核心修复】专门处理各类事件属性 (ClickEvents, ScrollEvents 等)
const isEventProp = Array.isArray(value) && (key.toLowerCase().endsWith('events') || key === 'clickEvents'); const isEventProp =
Array.isArray(value) && (key.toLowerCase().endsWith("events") || key === "clickEvents");
if (isEventProp) { if (isEventProp) {
const eventHandlers = []; const eventHandlers = [];
for (const item of value) { for (const item of value) {
if (typeof item === 'object' && (item.target || item.component || item.handler)) { if (typeof item === "object" && (item.target || item.component || item.handler)) {
const handler = new cc.Component.EventHandler(); const handler = new cc.Component.EventHandler();
// 解析 Target Node // 解析 Target Node
@@ -289,7 +329,8 @@ module.exports = {
if (item.component) handler.component = item.component; if (item.component) handler.component = item.component;
if (item.handler) handler.handler = item.handler; if (item.handler) handler.handler = item.handler;
if (item.customEventData !== undefined) handler.customEventData = String(item.customEventData); if (item.customEventData !== undefined)
handler.customEventData = String(item.customEventData);
eventHandlers.push(handler); eventHandlers.push(handler);
} else { } else {
@@ -309,18 +350,35 @@ module.exports = {
try { try {
const attrs = (cc.Class.Attr.getClassAttrs && cc.Class.Attr.getClassAttrs(compClass)) || {}; const attrs = (cc.Class.Attr.getClassAttrs && cc.Class.Attr.getClassAttrs(compClass)) || {};
let propertyType = attrs[key] ? attrs[key].type : null; let propertyType = attrs[key] ? attrs[key].type : null;
if (!propertyType && attrs[key + '$_$ctor']) { if (!propertyType && attrs[key + "$_$ctor"]) {
propertyType = attrs[key + '$_$ctor']; propertyType = attrs[key + "$_$ctor"];
} }
let isAsset = propertyType && (propertyType.prototype instanceof cc.Asset || propertyType === cc.Asset || propertyType === cc.Prefab || propertyType === cc.SpriteFrame); let isAsset =
let isAssetArray = Array.isArray(value) && (key === 'materials' || key.toLowerCase().includes('assets')); propertyType &&
(propertyType.prototype instanceof cc.Asset ||
propertyType === cc.Asset ||
propertyType === cc.Prefab ||
propertyType === cc.SpriteFrame);
let isAssetArray =
Array.isArray(value) && (key === "materials" || key.toLowerCase().includes("assets"));
// 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点 // 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点
if (!isAsset && !isAssetArray && typeof value === 'string' && value.length > 20) { if (!isAsset && !isAssetArray && typeof value === "string" && value.length > 20) {
const lowerKey = key.toLowerCase(); const lowerKey = key.toLowerCase();
const assetKeywords = ['prefab', 'sprite', 'texture', 'material', 'skeleton', 'spine', 'atlas', 'font', 'audio', 'data']; const assetKeywords = [
if (assetKeywords.some(k => lowerKey.includes(k))) { "prefab",
"sprite",
"texture",
"material",
"skeleton",
"spine",
"atlas",
"font",
"audio",
"data",
];
if (assetKeywords.some((k) => lowerKey.includes(k))) {
if (!findNode(value)) { if (!findNode(value)) {
isAsset = true; isAsset = true;
} }
@@ -339,7 +397,7 @@ module.exports = {
} }
uuids.forEach((uuid, idx) => { uuids.forEach((uuid, idx) => {
if (typeof uuid !== 'string' || uuid.length < 10) { if (typeof uuid !== "string" || uuid.length < 10) {
loadedCount++; loadedCount++;
return; return;
} }
@@ -355,7 +413,7 @@ module.exports = {
if (loadedCount === uuids.length) { if (loadedCount === uuids.length) {
if (isAssetArray) { if (isAssetArray) {
// 过滤掉加载失败的 // 过滤掉加载失败的
component[key] = loadedAssets.filter(a => !!a); component[key] = loadedAssets.filter((a) => !!a);
} else { } else {
if (loadedAssets[0]) component[key] = loadedAssets[0]; if (loadedAssets[0]) component[key] = loadedAssets[0];
} }
@@ -363,12 +421,12 @@ module.exports = {
// 通知编辑器 UI 更新 // 通知编辑器 UI 更新
const compIndex = node._components.indexOf(component); const compIndex = node._components.indexOf(component);
if (compIndex !== -1) { if (compIndex !== -1) {
Editor.Ipc.sendToPanel('scene', 'scene:set-property', { Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id: node.uuid, id: node.uuid,
path: `_components.${compIndex}.${key}`, path: `_components.${compIndex}.${key}`,
type: isAssetArray ? 'Array' : 'Object', type: isAssetArray ? "Array" : "Object",
value: isAssetArray ? uuids.map(u => ({ uuid: u })) : { uuid: value }, value: isAssetArray ? uuids.map((u) => ({ uuid: u })) : { uuid: value },
isSubProp: false isSubProp: false,
}); });
} }
Editor.Ipc.sendToMain("scene:dirty"); Editor.Ipc.sendToMain("scene:dirty");
@@ -378,7 +436,12 @@ module.exports = {
// 【重要修复】使用 continue 而不是 return确保处理完 Asset 属性后 // 【重要修复】使用 continue 而不是 return确保处理完 Asset 属性后
// 还能继续处理后续的普通属性 (如 type, sizeMode 等) // 还能继续处理后续的普通属性 (如 type, sizeMode 等)
continue; continue;
} else if (propertyType && (propertyType.prototype instanceof cc.Component || propertyType === cc.Component || propertyType === cc.Node)) { } else if (
propertyType &&
(propertyType.prototype instanceof cc.Component ||
propertyType === cc.Component ||
propertyType === cc.Node)
) {
// 2. 处理节点或组件引用 // 2. 处理节点或组件引用
const targetNode = findNode(value); const targetNode = findNode(value);
if (targetNode) { if (targetNode) {
@@ -389,7 +452,9 @@ module.exports = {
if (targetComp) { if (targetComp) {
finalValue = targetComp; finalValue = targetComp;
} else { } else {
Editor.warn(`[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`); Editor.warn(
`[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`,
);
} }
} }
Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`); Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`);
@@ -399,7 +464,7 @@ module.exports = {
} }
} else { } else {
// 3. 通用启发式 (找不到类型时的 fallback) // 3. 通用启发式 (找不到类型时的 fallback)
if (typeof value === 'string' && value.length > 20) { if (typeof value === "string" && value.length > 20) {
const targetNode = findNode(value); const targetNode = findNode(value);
if (targetNode) { if (targetNode) {
finalValue = targetNode; finalValue = targetNode;
@@ -408,12 +473,12 @@ module.exports = {
// 找不到节点且是 UUID -> 视为资源 // 找不到节点且是 UUID -> 视为资源
const compIndex = node._components.indexOf(component); const compIndex = node._components.indexOf(component);
if (compIndex !== -1) { if (compIndex !== -1) {
Editor.Ipc.sendToPanel('scene', 'scene:set-property', { Editor.Ipc.sendToPanel("scene", "scene:set-property", {
id: node.uuid, id: node.uuid,
path: `_components.${compIndex}.${key}`, path: `_components.${compIndex}.${key}`,
type: 'Object', type: "Object",
value: { uuid: value }, value: { uuid: value },
isSubProp: false isSubProp: false,
}); });
Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`); Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`);
} }
@@ -553,7 +618,8 @@ module.exports = {
if (event.reply) event.reply(null, "没有需要更新的属性"); if (event.reply) event.reply(null, "没有需要更新的属性");
} }
} else { } else {
if (event.reply) event.reply(new Error(`找不到组件 (类型: ${componentType}, ID: ${componentId})`)); if (event.reply)
event.reply(new Error(`找不到组件 (类型: ${componentType}, ID: ${componentId})`));
} }
} catch (err) { } catch (err) {
if (event.reply) event.reply(new Error(`更新组件失败: ${err.message}`)); if (event.reply) event.reply(new Error(`更新组件失败: ${err.message}`));
@@ -576,7 +642,7 @@ module.exports = {
} }
// 基础类型是安全的 // 基础类型是安全的
if (typeof val !== 'object') { if (typeof val !== "object") {
properties[key] = val; properties[key] = val;
continue; continue;
} }
@@ -599,7 +665,8 @@ module.exports = {
properties[key] = JSON.parse(jsonStr); properties[key] = JSON.parse(jsonStr);
} catch (e) { } catch (e) {
// 如果 JSON 失败(例如循环引用),格式化为字符串 // 如果 JSON 失败(例如循环引用),格式化为字符串
properties[key] = `[复杂对象: ${val.constructor ? val.constructor.name : typeof val}]`; properties[key] =
`[复杂对象: ${val.constructor ? val.constructor.name : typeof val}]`;
} }
} }
} catch (e) { } catch (e) {
@@ -647,7 +714,7 @@ module.exports = {
const scene = cc.director.getScene(); const scene = cc.director.getScene();
if (!scene) { if (!scene) {
if (event.reply) event.reply(new Error("Scene not ready or loading.")); if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。"));
return; return;
} }
@@ -885,7 +952,6 @@ module.exports = {
} else { } else {
if (event.reply) event.reply(new Error("找不到父节点")); if (event.reply) event.reply(new Error("找不到父节点"));
} }
} else if (action === "update") { } else if (action === "update") {
let node = findNode(nodeId); let node = findNode(nodeId);
if (node) { if (node) {
@@ -903,7 +969,6 @@ module.exports = {
} else { } else {
if (event.reply) event.reply(new Error("找不到节点")); if (event.reply) event.reply(new Error("找不到节点"));
} }
} else if (action === "get_info") { } else if (action === "get_info") {
let node = findNode(nodeId); let node = findNode(nodeId);
if (node) { if (node) {
@@ -921,7 +986,7 @@ module.exports = {
speed: ps.speed, speed: ps.speed,
angle: ps.angle, angle: ps.angle,
gravity: { x: ps.gravity.x, y: ps.gravity.y }, gravity: { x: ps.gravity.x, y: ps.gravity.y },
file: ps.file ? ps.file.name : null file: ps.file ? ps.file.name : null,
}; };
if (event.reply) event.reply(null, info); if (event.reply) event.reply(null, info);
} else { } else {
@@ -958,12 +1023,12 @@ module.exports = {
switch (action) { switch (action) {
case "get_list": case "get_list":
const clips = anim.getClips(); const clips = anim.getClips();
const clipList = clips.map(c => ({ const clipList = clips.map((c) => ({
name: c.name, name: c.name,
duration: c.duration, duration: c.duration,
sample: c.sample, sample: c.sample,
speed: c.speed, speed: c.speed,
wrapMode: c.wrapMode wrapMode: c.wrapMode,
})); }));
if (event.reply) event.reply(null, clipList); if (event.reply) event.reply(null, clipList);
break; break;
@@ -980,9 +1045,9 @@ module.exports = {
} }
const info = { const info = {
currentClip: currentClip ? currentClip.name : null, currentClip: currentClip ? currentClip.name : null,
clips: anim.getClips().map(c => c.name), clips: anim.getClips().map((c) => c.name),
playOnLoad: anim.playOnLoad, playOnLoad: anim.playOnLoad,
isPlaying: isPlaying isPlaying: isPlaying,
}; };
if (event.reply) event.reply(null, info); if (event.reply) event.reply(null, info);
break; break;

View File

@@ -1,8 +1,7 @@
// @ts-ignore // @ts-ignore
const fs = require('fs'); const fs = require("fs");
// @ts-ignore // @ts-ignore
const path = require('path'); const path = require("path");
/** /**
* IPC 消息管理器 * IPC 消息管理器
@@ -16,14 +15,14 @@ export class IpcManager {
public static getIpcMessages(): any[] { public static getIpcMessages(): any[] {
// 获取文档路径 // 获取文档路径
// @ts-ignore // @ts-ignore
const docPath = Editor.url('packages://mcp-bridge/IPC_MESSAGES.md'); const docPath = Editor.url("packages://mcp-bridge/IPC_MESSAGES.md");
if (!fs.existsSync(docPath)) { if (!fs.existsSync(docPath)) {
// @ts-ignore // @ts-ignore
Editor.error(`[IPC Manager] Document not found: ${docPath}`); Editor.error(`[IPC 管理器] 找不到文档文件: ${docPath}`);
return []; return [];
} }
const content = fs.readFileSync(docPath, 'utf-8'); const content = fs.readFileSync(docPath, "utf-8");
const messages: any[] = []; const messages: any[] = [];
// 正则匹配 ### `message-name` // 正则匹配 ### `message-name`
@@ -65,7 +64,7 @@ export class IpcManager {
params, params,
returns, returns,
type, type,
status status,
}); });
} }
@@ -95,12 +94,12 @@ export class IpcManager {
if (Editor.Ipc.sendToMain) { if (Editor.Ipc.sendToMain) {
// @ts-ignore // @ts-ignore
Editor.Ipc.sendToMain(name, args); Editor.Ipc.sendToMain(name, args);
resolve({ success: true, message: "Message sent (sendToMain)" }); resolve({ success: true, message: "消息已发送 (sendToMain)" });
} else { } else {
resolve({ success: false, message: "Editor.Ipc.sendToMain not available" }); resolve({ success: false, message: "Editor.Ipc.sendToMain 不可用" });
} }
} catch (e: any) { } catch (e: any) {
resolve({ success: false, message: `Error: ${e.message}` }); resolve({ success: false, message: `错误: ${e.message}` });
} }
}); });
} }

View File

@@ -1,4 +1,3 @@
// @ts-ignore // @ts-ignore
const Editor = window.Editor; const Editor = window.Editor;
@@ -10,11 +9,18 @@ export class IpcUi {
private filterSelect: HTMLSelectElement | null = null; private filterSelect: HTMLSelectElement | null = null;
private paramInput: HTMLTextAreaElement | null = null; private paramInput: HTMLTextAreaElement | null = null;
/**
* 构造函数
* @param root Shadow UI 根节点
*/
constructor(root: ShadowRoot) { constructor(root: ShadowRoot) {
this.root = root; this.root = root;
this.bindEvents(); this.bindEvents();
} }
/**
* 绑定 UI 事件
*/
private bindEvents() { private bindEvents() {
const btnScan = this.root.querySelector("#btnScanIpc"); const btnScan = this.root.querySelector("#btnScanIpc");
const btnTest = this.root.querySelector("#btnTestIpc"); const btnTest = this.root.querySelector("#btnTestIpc");
@@ -31,52 +37,64 @@ export class IpcUi {
btnTest.addEventListener("confirm", () => this.testSelected()); btnTest.addEventListener("confirm", () => this.testSelected());
} }
if (cbSelectAll) { if (cbSelectAll) {
cbSelectAll.addEventListener("change", (e: any) => this.toggleAll(e.detail ? e.detail.value : (e.target.value === 'true' || e.target.checked))); cbSelectAll.addEventListener("change", (e: any) =>
this.toggleAll(e.detail ? e.detail.value : e.target.value === "true" || e.target.checked),
);
} }
if (this.filterSelect) { if (this.filterSelect) {
this.filterSelect.addEventListener("change", () => this.filterMessages()); this.filterSelect.addEventListener("change", () => this.filterMessages());
} }
} }
/**
* 扫描 IPC 消息
*/
private scanMessages() { private scanMessages() {
this.log("Scanning IPC messages..."); this.log("正在扫描 IPC 消息...");
// @ts-ignore // @ts-ignore
Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", (err: any, msgs: any[]) => { Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", (err: any, msgs: any[]) => {
if (err) { if (err) {
this.log(`Scan Error: ${err}`); this.log(`扫描错误: ${err}`);
return; return;
} }
if (msgs) { if (msgs) {
this.allMessages = msgs; this.allMessages = msgs;
this.filterMessages(); this.filterMessages();
this.log(`Found ${msgs.length} messages.`); this.log(`找到 ${msgs.length} 条消息。`);
} else { } else {
this.log("No messages found."); this.log("未找到任何消息。");
} }
}); });
} }
/**
* 根据当前选择器过滤消息列表
*/
private filterMessages() { private filterMessages() {
if (!this.allMessages) return; if (!this.allMessages) return;
const filter = this.filterSelect ? this.filterSelect.value : "all"; const filter = this.filterSelect ? this.filterSelect.value : "all";
let filtered = this.allMessages; let filtered = this.allMessages;
if (filter === "available") { if (filter === "available") {
filtered = this.allMessages.filter(m => m.status === "可用"); filtered = this.allMessages.filter((m) => m.status === "可用");
} else if (filter === "unavailable") { } else if (filter === "unavailable") {
filtered = this.allMessages.filter(m => m.status && m.status.includes("不可用")); filtered = this.allMessages.filter((m) => m.status && m.status.includes("不可用"));
} else if (filter === "untested") { } else if (filter === "untested") {
filtered = this.allMessages.filter(m => !m.status || m.status === "未测试"); filtered = this.allMessages.filter((m) => !m.status || m.status === "未测试");
} }
this.renderList(filtered); this.renderList(filtered);
} }
/**
* 渲染消息列表 UI
* @param msgs 消息对象数组
*/
private renderList(msgs: any[]) { private renderList(msgs: any[]) {
if (!this.ipcList) return; if (!this.ipcList) return;
this.ipcList.innerHTML = ""; this.ipcList.innerHTML = "";
msgs.forEach(msg => { msgs.forEach((msg) => {
const item = document.createElement("div"); const item = document.createElement("div");
item.className = "ipc-item"; item.className = "ipc-item";
item.style.padding = "4px"; item.style.padding = "4px";
@@ -84,34 +102,35 @@ export class IpcUi {
item.style.display = "flex"; item.style.display = "flex";
item.style.alignItems = "center"; item.style.alignItems = "center";
// Checkbox // 复选框
const checkbox = document.createElement("ui-checkbox"); const checkbox = document.createElement("ui-checkbox");
// @ts-ignore // @ts-ignore
checkbox.value = false; checkbox.value = false;
checkbox.setAttribute("data-name", msg.name); checkbox.setAttribute("data-name", msg.name);
checkbox.style.marginRight = "8px"; checkbox.style.marginRight = "8px";
// Content // 内容区域
const content = document.createElement("div"); const content = document.createElement("div");
content.style.flex = "1"; content.style.flex = "1";
content.style.fontSize = "11px"; content.style.fontSize = "11px";
let statusColor = "#888"; // Untested let statusColor = "#888"; // 未测试
if (msg.status === "可用") statusColor = "#4CAF50"; // Green if (msg.status === "可用")
else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // Red statusColor = "#4CAF50"; // 绿色
else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // 红色
content.innerHTML = ` content.innerHTML = `
<div style="display:flex; justify-content:space-between;"> <div style="display:flex; justify-content:space-between;">
<span style="color: #4CAF50; font-weight: bold;">${msg.name}</span> <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> <span style="color: ${statusColor}; font-size: 10px; border: 1px solid ${statusColor}; padding: 0 4px; border-radius: 4px;">${msg.status || "未测试"}</span>
</div> </div>
<div style="color: #888;">${msg.description || "No desc"}</div> <div style="color: #888;">${msg.description || "无描述"}</div>
<div style="color: #666; font-size: 10px;">Params: ${msg.params || "None"}</div> <div style="color: #666; font-size: 10px;">参数: ${msg.params || ""}</div>
`; `;
// Action Button // 执行按钮
const btnRun = document.createElement("ui-button"); const btnRun = document.createElement("ui-button");
btnRun.innerText = "Run"; btnRun.innerText = "执行";
btnRun.className = "tiny"; btnRun.className = "tiny";
btnRun.style.height = "20px"; btnRun.style.height = "20px";
btnRun.style.lineHeight = "20px"; btnRun.style.lineHeight = "20px";
@@ -126,28 +145,35 @@ export class IpcUi {
}); });
} }
/**
* 测试所有选中的 IPC 消息
*/
private async testSelected() { private async testSelected() {
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
const toTest: string[] = []; const toTest: string[] = [];
checkboxes.forEach((cb: any) => { checkboxes.forEach((cb: any) => {
// In Cocos 2.x, ui-checkbox value is boolean // Cocos 2.x, ui-checkbox 的值是布尔型
if (cb.checked || cb.value === true) { if (cb.checked || cb.value === true) {
toTest.push(cb.getAttribute("data-name")); toTest.push(cb.getAttribute("data-name"));
} }
}); });
if (toTest.length === 0) { if (toTest.length === 0) {
this.log("No messages selected."); this.log("未选择任何消息。");
return; return;
} }
this.log(`Starting batch test for ${toTest.length} messages...`); this.log(`开始批量测试 ${toTest.length} 条消息...`);
for (const name of toTest) { for (const name of toTest) {
await this.runTest(name); await this.runTest(name);
} }
this.log("Batch test completed."); this.log("批量测试完成。");
} }
/**
* 运行单个测试请求
* @param name 消息名称
*/
private runTest(name: string): Promise<void> { private runTest(name: string): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
let params = null; let params = null;
@@ -155,25 +181,29 @@ export class IpcUi {
try { try {
params = JSON.parse(this.paramInput.value.trim()); params = JSON.parse(this.paramInput.value.trim());
} catch (e) { } catch (e) {
this.log(`[Error] Invalid JSON Params: ${e}`); this.log(`[错误] 无效的 JSON 参数: ${e}`);
resolve(); resolve();
return; return;
} }
} }
this.log(`Testing: ${name} with params: ${JSON.stringify(params)}...`); this.log(`正在测试: ${name},参数: ${JSON.stringify(params)}...`);
// @ts-ignore // @ts-ignore
Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name, params }, (err: any, result: any) => { Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name, params }, (err: any, result: any) => {
if (err) { if (err) {
this.log(`[${name}] Failed: ${err}`); this.log(`[${name}] 失败: ${err}`);
} else { } else {
this.log(`[${name}] Success: ${JSON.stringify(result)}`); this.log(`[${name}] 成功: ${JSON.stringify(result)}`);
} }
resolve(); resolve();
}); });
}); });
} }
/**
* 全选/取消全选
* @param checked 是否选中
*/
private toggleAll(checked: boolean) { private toggleAll(checked: boolean) {
const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); const checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox");
checkboxes.forEach((cb: any) => { checkboxes.forEach((cb: any) => {
@@ -181,6 +211,10 @@ export class IpcUi {
}); });
} }
/**
* 输出日志到界面
* @param msg 日志消息
*/
private log(msg: string) { private log(msg: string) {
if (this.logArea) { if (this.logArea) {
// @ts-ignore // @ts-ignore