docs: 更新 README 和 UPDATE_LOG 文档,新增日志持久化与面板修复记录

This commit is contained in:
火焰库拉
2026-02-27 23:04:25 +08:00
parent 9268340af3
commit 002f081290
7 changed files with 4191 additions and 4304 deletions

View File

@@ -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` |

View File

@@ -15,7 +15,7 @@
- **脚本管理**: 创建、删除、读取、写入脚本文件
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
- **资产管理**: 创建、删除、移动、获取资源信息
- **实时日志**: 提供详细的操作日志记录和展示
- **实时日志**: 提供详细的操作日志记录和展示,支持持久化写入项目内日志文件
- **自动启动**: 支持编辑器启动时自动开启服务
- **编辑器管理**: 获取和设置选中对象,刷新编辑器
- **游戏对象查找**: 根据条件查找场景中的节点
@@ -28,6 +28,8 @@
- **全局搜索**: 在项目中搜索文本内容
- **撤销/重做**: 管理编辑器的撤销栈
- **特效管理**: 创建和修改粒子系统
- **并发安全**: 指令队列串行化执行,防止编辑器卡死
- **超时保护**: IPC 通信和指令队列均有超时兜底机制
- **工具说明**: 测试面板提供详细的工具描述和参数说明
## 安装与使用
@@ -406,13 +408,14 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
### 日志管理
插件会通过内置的测试面板MCP Bridge/Open Panel实时记录所有操作的日志包括
插件会通过内置的测试面板MCP Bridge/Open Panel实时记录所有操作的日志并同步持久化写入项目目录 `settings/mcp-bridge.log` 文件,编辑器重启后仍可查阅历史日志。日志记录包括:
- 服务启动/停止状态
- MCP 客户端请求接收(完整包含工具的 `arguments` 参数,超长自动截断)
- 场景节点树遍历与耗时信息
- 工具调用的执行成功/失败状态返回
- IPC 消息和核心底层报错堆栈
- 内存保护:日志缓冲区上限 2000 条,超出自动截断旧日志
## 注意事项

View File

@@ -192,4 +192,46 @@
- **修复**: 在 `scene-script.js` 层加固了前置拦截规则:
1. **直接拦截节点**: 当检测到传入 `cc.Node``Node` 作为组件类型时直接驳回,并返回富含指导意义的中文提示词(如“请使用 create-node 创建节点”)。
2. **继承链校验**: 提取引擎类定义后,强制要求通过 `cc.js.isChildClassOf` 判断该类必须继承自 `cc.Component`。若不合法则即时截断并提示。
- **价值**: 通过将冰冷的底层异常翻译为手把手教 AI 怎么重试的指导性异常,彻底根治了 AI 在操作组件时乱认对象、反复撞墙的通病。
- **价值**: 通过将冰冷的底层异常翻译为"手把手教 AI 怎么重试"的指导性异常,彻底根治了 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-24)
### 1. `panel/index.js` 语法错误修复
- **问题**: 面板加载时出现 `SyntaxError: Invalid or unexpected token`,导致 MCP Bridge 插件面板完全无法渲染。
- **原因**: `index.js` 中存在非法字符或格式错误,被 Cocos Creator 的面板加载器拒绝解析。
- **修复**: 清理了文件中的语法问题,确保面板能够正常加载和初始化。

128
main.js
View File

@@ -6,7 +6,8 @@ const pathModule = require("path");
const fs = require("fs");
const crypto = require("crypto");
let logBuffer = []; // 存储所有日志
let logBuffer = []; // 存储所有日志(上限 2000 条,超出自动截断旧日志)
let _requestCounter = 0; // 请求关联计数器
let mcpServer = null;
let isSceneBusy = false;
let serverConfig = {
@@ -29,7 +30,14 @@ let isProcessingCommand = false;
*/
function enqueueCommand(fn) {
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();
});
}
@@ -40,15 +48,17 @@ function enqueueCommand(fn) {
function processNextCommand() {
if (isProcessingCommand || commandQueue.length === 0) return;
isProcessingCommand = true;
const { fn, resolve } = commandQueue.shift();
const { fn, resolve, timeoutId } = commandQueue.shift();
try {
fn(() => {
clearTimeout(timeoutId);
isProcessingCommand = false;
resolve();
processNextCommand();
});
} catch (e) {
// 防止队列因未捕获异常永久阻塞
clearTimeout(timeoutId);
addLog("error", `[CommandQueue] 指令执行异常: ${e.message}`);
isProcessingCommand = false;
resolve();
@@ -92,25 +102,72 @@ function callSceneScriptWithTimeout(pluginName, method, args, callback, timeout
}
/**
* 封装日志函数,同时发送给面板、保存到内存并在编辑器控制台打印
* @param {'info' | 'success' | 'warn' | 'error'} type 日志类型
* 日志文件路径(懒初始化,在项目 settings 目录下)
* @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");
return _logFilePath;
}
} catch (e) {
// 静默失败,不影响主流程
}
return null;
}
/**
* 封装日志函数
* - 所有日志发送到 MCP 测试面板 + 内存缓存
* - 仅 error / warn 输出到编辑器控制台(防止刷屏)
* - 所有日志追加写入项目内 settings/mcp-bridge.log 文件(持久化)
* @param {'info' | 'success' | 'warn' | 'error' | 'mcp'} type 日志类型
* @param {string} message 日志内容
*/
function addLog(type, message) {
const logEntry = {
time: new Date().toLocaleTimeString(),
time: new Date().toISOString().replace("T", " ").substring(0, 23),
type: type,
content: message,
};
logBuffer.push(logEntry);
// 防止内存泄漏:限制日志缓存上限
if (logBuffer.length > 2000) {
logBuffer = logBuffer.slice(-1500);
}
// 发送到面板
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry);
// 【修改】确保所有日志都输出到编辑器控制台,以便用户查看
// 仅关键信息输出到编辑器控制台error / warn
if (type === "error") {
Editor.error(`[MCP] ${message}`);
} else if (type === "warn") {
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) {
// 文件写入失败时静默,不影响主流程
}
}
@@ -164,13 +221,15 @@ const getNewSceneTemplate = () => {
};
/**
* 获取所有支持的 MCP 工具列表定义
* 获取所有支持的 MCP 工具列表定义(懒加载缓存)
* @returns {Array<Object>} 工具定义数组
*/
let _toolsListCache = null;
const getToolsList = () => {
if (_toolsListCache) return _toolsListCache;
const globalPrecautions =
"【AI 安全守则】: 1. 执行任何写操作前必须先通过 get_scene_hierarchy 或 manage_components(get) 验证主体存在。 2. 严禁基于假设盲目猜测属性名。 3. 资源属性(如 cc.Prefab必须通过 UUID 进行赋值。 4. 严禁频繁刷新全局资源 (refresh_editor),必须通过 properties.path 指定具体修改的文件或目录以防止编辑器长期卡死。";
return [
_toolsListCache = [
{
name: "get_selected_node",
description: `获取当前编辑器中选中的节点 ID。建议获取后立即调用 get_scene_hierarchy 确认该节点是否仍存在于当前场景中。`,
@@ -691,6 +750,7 @@ const getToolsList = () => {
},
},
];
return _toolsListCache;
};
module.exports = {
@@ -786,11 +846,40 @@ module.exports = {
res.setHeader("Access-Control-Allow-Origin", "*");
let body = "";
let bodySize = 0;
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB 防护
let bodyOverflow = false;
req.on("data", (chunk) => {
bodySize += chunk.length;
if (bodySize > MAX_BODY_SIZE) {
if (!bodyOverflow) {
bodyOverflow = true;
addLog("error", `[HTTP] 请求体过大 (>${MAX_BODY_SIZE} bytes),已拒绝`);
req.destroy();
res.writeHead(413);
res.end(JSON.stringify({ error: "Request body too large" }));
}
return;
}
body += chunk;
});
req.on("end", () => {
if (bodyOverflow) return;
const url = req.url;
// 健康检查端点
if (url === "/health") {
res.writeHead(200);
return res.end(
JSON.stringify({
status: "ok",
queueLength: commandQueue.length,
isProcessing: isProcessingCommand,
isSceneBusy: isSceneBusy,
uptime: process.uptime(),
logCount: logBuffer.length,
}),
);
}
if (url === "/list-tools") {
const tools = getToolsList();
addLog("info", `AI Client requested tool list`);
@@ -847,10 +936,13 @@ module.exports = {
argsPreview = "[无法序列化的参数]";
}
}
addLog("mcp", `REQ -> [${name}] (队列长度: ${commandQueue.length}) 参数: ${argsPreview}`);
const reqId = `R${++_requestCounter}`;
addLog("mcp", `REQ -> [${name}] #${reqId} (队列长度: ${commandQueue.length}) 参数: ${argsPreview}`);
enqueueCommand((done) => {
const startTime = Date.now();
this.handleMcpCall(name, args, (err, result) => {
const elapsed = Date.now() - startTime;
const response = {
content: [
{
@@ -864,7 +956,7 @@ module.exports = {
],
};
if (err) {
addLog("error", `RES <- [${name}] 失败: ${err}`);
addLog("error", `RES <- [${name}] #${reqId} 失败 (${elapsed}ms): ${err}`);
} else {
let preview = "";
if (typeof result === "string") {
@@ -877,7 +969,7 @@ module.exports = {
preview = "Object (Circular/Unserializable)";
}
}
addLog("success", `RES <- [${name}] 成功 : ${preview}`);
addLog("success", `RES <- [${name}] #${reqId} 成功 (${elapsed}ms): ${preview}`);
}
res.writeHead(200);
res.end(JSON.stringify(response));
@@ -1018,9 +1110,12 @@ module.exports = {
isSceneBusy = true;
addLog("info", "准备保存场景... 等待 UI 同步。");
Editor.Ipc.sendToPanel("scene", "scene:stash-and-save");
// 给场景保存留出时间后再解除锁定stash-and-save 是异步 IPC
setTimeout(() => {
isSceneBusy = false;
addLog("info", "安全保存已完成。");
callback(null, "场景保存成功。");
}, 1500);
break;
case "get_scene_hierarchy":
@@ -2515,6 +2610,8 @@ CCProgram fs %{
return callback(`无效的搜索路径: ${rootPathUrl}`);
}
// 缓存 assets 根路径,避免在每次匹配时重复调用 urlToFspath
const assetsRoot = Editor.assetdb.urlToFspath("db://assets");
const mode = matchType || "content"; // content, file_name, dir_name
const validExtensions = extensions || [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"];
const results = [];
@@ -2616,10 +2713,7 @@ CCProgram fs %{
lines.forEach((line, index) => {
if (results.length >= MAX_RESULTS) return;
if (checkMatch(line)) {
const relativePath = pathModule.relative(
Editor.assetdb.urlToFspath("db://assets"),
filePath,
);
const relativePath = pathModule.relative(assetsRoot, filePath);
const dbPath =
"db://assets/" + relativePath.split(pathModule.sep).join("/");
results.push({

View File

@@ -7,9 +7,10 @@ const http = require("http");
/**
* 当前 Cocos Creator 插件监听的端口
* 支持通过环境变量 MCP_BRIDGE_PORT 或命令行参数指定端口
* @type {number}
*/
const COCOS_PORT = 3456;
const COCOS_PORT = parseInt(process.env.MCP_BRIDGE_PORT || process.argv[2] || "3456", 10);
/**
* 发送调试日志到标准的错误输出流水

View File

@@ -307,10 +307,22 @@ Editor.Panel.extend({
renderLog(log) {
const view = this.shadowRoot.querySelector("#logConsole");
if (!view) return;
// 限制面板日志 DOM 节点数量,防止长时间运行后面板卡顿
while (view.childNodes.length > 1000) {
view.removeChild(view.firstChild);
}
const atBottom = view.scrollHeight - view.scrollTop <= view.clientHeight + 50;
const el = document.createElement("div");
el.className = `log-item ${log.type}`;
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`;
// 使用 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;
},

View File

@@ -216,7 +216,7 @@ module.exports = {
Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`);
if (event.reply) event.reply(null, "变换信息已更新");
} else {
if (event.reply) event.reply(new Error("找不到节点"));
if (event.reply) event.reply(new Error(`找不到节点 (UUID: ${id})`));
}
},
/**
@@ -533,14 +533,14 @@ module.exports = {
};
if (!node) {
if (event.reply) event.reply(new Error("找不到节点"));
if (event.reply) event.reply(new Error(`找不到节点 (UUID: ${nodeId}, action: ${action})`));
return;
}
switch (action) {
case "add":
if (!componentType) {
if (event.reply) event.reply(new Error("必须提供组件类型"));
if (event.reply) event.reply(new Error(`必须提供组件类型 (nodeId: ${nodeId}, action: ${action})`));
return;
}
@@ -605,7 +605,7 @@ module.exports = {
case "remove":
if (!componentId) {
if (event.reply) event.reply(new Error("必须提供组件 ID"));
if (event.reply) event.reply(new Error(`必须提供组件 ID (nodeId: ${nodeId})`));
return;
}
@@ -627,7 +627,8 @@ module.exports = {
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
if (event.reply) event.reply(null, "组件已移除");
} else {
if (event.reply) event.reply(new Error("找不到组件"));
if (event.reply)
event.reply(new Error(`找不到组件 (nodeId: ${nodeId}, componentId: ${componentId})`));
}
} catch (err) {
if (event.reply) event.reply(new Error(`移除组件失败: ${err.message}`));