diff --git a/README.md b/README.md index 030256f..5af1c1d 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j ### 4. get_scene_hierarchy -- **描述**: 获取当前场景的完整节点树结构(包括 UUID、名称和层级关系) -- **参数**: 无 +- **描述**: 获取当前场景的完整节点树结构(支持分页避免长数据截断)。如果要查询具体组件属性请配合 manage_components。 +- **参数**: + - `nodeId`: 指定的根节点 UUID。如果不传则获取整个场景的根 (可选)。 + - `depth`: 遍历的深度限制,默认为 2。用来防止过大场景导致返回数据超长 (可选)。 + - `includeDetails`: 是否包含坐标、缩放等杂项详情,默认为 false (可选)。 ### 5. update_node_transform @@ -223,14 +226,13 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j ### 15. find_gameobjects -- **描述**: 查找游戏对象 +- **描述**: 按条件在场景中搜索游戏对象。返回匹配节点的轻量级结构 (UUID, name, active, components 等)。若要获取完整的详细组件属性,请进一步对目标使用 `manage_components`。 - **参数**: - `conditions`: 查找条件 - - `name`: 节点名称(包含匹配) - - `tag`: 节点标签 - - `component`: 组件类型 - - `active`: 激活状态 - - `recursive`: 是否递归查找(默认:true) + - `name`: 节点名称(包含模糊匹配) + - `component`: 包含的组件类名(如 `cc.Sprite`) + - `active`: 布尔值,节点的激活状态 + - `recursive`: 是否递归查找所有的子节点(默认:true) ### 16. manage_material diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md index 09f242b..afb70c4 100644 --- a/UPDATE_LOG.md +++ b/UPDATE_LOG.md @@ -120,3 +120,32 @@ - **优化预制体创建稳定性 (`create_node` + `prefab_management`)**: - 在创建物理目录后强制执行 `Editor.assetdb.refresh`,确保 AssetDB 即时同步。 - 将节点重命名与预制体创建指令之间的安全延迟从 100ms 增加至 300ms,消除了重命名未完成导致创建失败的竞态条件。 + +--- + +## 八、 Token 消耗深度优化 (2026-02-24) + +### 1. 工具描述精简 (`main.js`) +- **问题**: `globalPrecautions` (AI 安全守则) 被硬编码到所有工具的 `description` 中,导致每次环境初始化或查阅工具列表时浪费约 2200 个 CJK Token。 +- **优化**: 收束安全守则的广播范围。目前仅针对高风险的**写操作**(如 `manage_components`, `update_node_transform`, `manage_material`, `create_node` 等)保留警告,低风险或只读分析类工具(如 `get_scene_hierarchy`, `get_selected_node`)已悉数移除该文本。 +- **效果**: `/list-tools` 整体负载字符数缩减近 40%。 + +### 2. 长数据截断保护 (`scene-script.js`) +- **问题**: `manage_components(get)` 会完整序列化多边形坐标集、曲线数据数组以及 Base64 图片,产生极其庞大且对 AI 无用的 JSON 负载。 +- **优化**: + - **数组截断**: 长度超过 10 的数组直接返回 `[Array(length)]`,彻底杜绝数据雪崩。 + - **字符串截断**: 长度超过 200 的字符串限制为截断显示并附带 `...[Truncated, total length: X]` 提示。 + +### 3. 层级树获取瘦身与分页 (`get_scene_hierarchy`) +- **问题**: 请求场景层级时会一次性返回完整 1000+ 节点的深层结构,包括所有变换矩阵。 +- **优化**: + - 支持 `depth` 深度限制(默认 2 层)。 + - 支持 `nodeId` 参数,允许 AI 缩小作用域,从指定根节点向下探测。 + - 添加 `includeDetails` 参数。默认关闭,此时剥离坐标、缩放与尺寸指标,且将冗长的组件详细结构浓缩成简化的名称数组(如 `["Sprite", "Button"]`)。 + +### 4. 查找结果精简 (`find_gameobjects`) +- **优化**: 将原本包含 Transform(位移/缩放/尺寸)全量数据的匹配回传,精简为仅包含核心识别特征的基础集 (`uuid`, `name`, `active`, `components`, `childrenCount`),极大释放了同名大批量查找时的 Token 压力。 + +### 5. 底层鲁棒性大修 +- **问题**: 上述优化在应用过程中暴露出遍历未命名根节点(如 `cc.Scene`)时遭遇 `undefined.startsWith` 报错并引发 IPC 悬挂的致命隐患。 +- **修复**: 在 `dumpNodes` 与 `searchNode` 中增设前置安全屏障,并修复 `cc.js.getClassName(c)` 替代底层的 `__typename` 来兼容 2.4 获取有效类名。修复了 `main.js` 中关于 `get_scene_hierarchy` 的参数传递脱节问题。 diff --git a/main.js b/main.js index 0694cef..74b5b3d 100644 --- a/main.js +++ b/main.js @@ -173,7 +173,7 @@ const getToolsList = () => { return [ { name: "get_selected_node", - description: `${globalPrecautions} 获取当前编辑器中选中的节点 ID。建议获取后立即调用 get_scene_hierarchy 确认该节点是否仍存在于当前场景中。`, + description: `获取当前编辑器中选中的节点 ID。建议获取后立即调用 get_scene_hierarchy 确认该节点是否仍存在于当前场景中。`, inputSchema: { type: "object", properties: {} }, }, { @@ -190,13 +190,20 @@ const getToolsList = () => { }, { name: "save_scene", - description: `${globalPrecautions} 保存当前场景的修改`, + description: `保存当前场景的修改`, inputSchema: { type: "object", properties: {} }, }, { name: "get_scene_hierarchy", - description: `${globalPrecautions} 获取当前场景的完整节点树结构(包括 UUID、名称和层级关系)`, - inputSchema: { type: "object", properties: {} }, + description: `获取当前场景的节点树结构(包含 UUID、名称、子节点数)。若要查询节点组件详情等,请使用 manage_components。`, + inputSchema: { + type: "object", + properties: { + nodeId: { type: "string", description: "指定的根节点 UUID。如果不传则获取整个场景的根。" }, + depth: { type: "number", description: "遍历的深度限制,默认为 2。用来防止过大场景导致返回数据超长。" }, + includeDetails: { type: "boolean", description: "是否包含坐标、缩放等杂项详情,默认为 false。" } + } + }, }, { name: "update_node_transform", @@ -218,7 +225,7 @@ const getToolsList = () => { }, { name: "create_scene", - description: `${globalPrecautions} 在 assets 目录下创建一个新的场景文件。创建并通过 open_scene 打开后,请务必初始化基础节点(如 Canvas 和 Camera)。`, + description: `在 assets 目录下创建一个新的场景文件。创建并通过 open_scene 打开后,请务必初始化基础节点(如 Canvas 和 Camera)。`, inputSchema: { type: "object", properties: { @@ -241,7 +248,7 @@ const getToolsList = () => { }, { name: "open_scene", - description: `${globalPrecautions} 打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒。重要:如果是新创建或空的场景,请务必先创建并初始化基础节点(Canvas/Camera)。`, + description: `打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒。重要:如果是新创建或空的场景,请务必先创建并初始化基础节点(Canvas/Camera)。`, inputSchema: { type: "object", properties: { @@ -255,7 +262,7 @@ const getToolsList = () => { }, { name: "open_prefab", - description: `${globalPrecautions} 在编辑器中打开预制体文件进入编辑模式。注意:这是一个异步操作,打开后请等待几秒。`, + description: `在编辑器中打开预制体文件进入编辑模式。注意:这是一个异步操作,打开后请等待几秒。`, inputSchema: { type: "object", properties: { @@ -424,12 +431,15 @@ const getToolsList = () => { }, { name: "find_gameobjects", - description: `${globalPrecautions} 查找游戏对象`, + description: `按条件在场景中搜索游戏对象。返回匹配节点的轻量级结构 (UUID, name, active, components 等)。若要获取完整的详细组件属性,请进一步对目标使用 manage_components。`, inputSchema: { type: "object", properties: { - conditions: { type: "object", description: "查找条件" }, - recursive: { type: "boolean", default: true, description: "是否递归查找" }, + conditions: { + type: "object", + description: "查找条件。支持的属性:name (节点名称,支持模糊匹配), component (包含的组件类名,如 'cc.Sprite'), active (布尔值,节点的激活状态)。" + }, + recursive: { type: "boolean", default: true, description: "是否递归查找所有子节点" }, }, required: ["conditions"], }, @@ -538,7 +548,7 @@ const getToolsList = () => { }, { name: "read_console", - description: `${globalPrecautions} 读取控制台`, + description: `读取控制台`, inputSchema: { type: "object", properties: { @@ -553,7 +563,7 @@ const getToolsList = () => { }, { name: "validate_script", - description: `${globalPrecautions} 验证脚本`, + description: `验证脚本`, inputSchema: { type: "object", properties: { @@ -564,7 +574,7 @@ const getToolsList = () => { }, { name: "search_project", - description: `${globalPrecautions} 搜索项目文件。支持三种模式:1. 'content' (默认): 搜索文件内容,支持正则表达式;2. 'file_name': 在指定目录下搜索匹配的文件名;3. 'dir_name': 在指定目录下搜索匹配的文件夹名。`, + description: `搜索项目文件。支持三种模式:1. 'content' (默认): 搜索文件内容,支持正则表达式;2. 'file_name': 在指定目录下搜索匹配的文件名;3. 'dir_name': 在指定目录下搜索匹配的文件夹名。`, inputSchema: { type: "object", properties: { @@ -650,7 +660,7 @@ const getToolsList = () => { }, { name: "get_sha", - description: `${globalPrecautions} 获取指定文件的 SHA-256 哈希值`, + description: `获取指定文件的 SHA-256 哈希值`, inputSchema: { type: "object", properties: { @@ -999,7 +1009,7 @@ module.exports = { break; case "get_scene_hierarchy": - callSceneScriptWithTimeout("mcp-bridge", "get-hierarchy", null, callback); + callSceneScriptWithTimeout("mcp-bridge", "get-hierarchy", args, callback); break; case "update_node_transform": @@ -1209,7 +1219,7 @@ module.exports = { Editor.assetdb.create( scriptPath, content || - `const { ccclass, property } = cc._decorator; + `const { ccclass, property } = cc._decorator; @ccclass export default class NewScript extends cc.Component { @@ -2385,7 +2395,7 @@ CCProgram fs %{ } else { props[key] = typeof val; } - } catch (e) {} + } catch (e) { } }); return { name, exists: true, props }; }; diff --git a/scene-script.js b/scene-script.js index a5a6574..378d9fd 100644 --- a/scene-script.js +++ b/scene-script.js @@ -6,1080 +6,1126 @@ * @returns {cc.Node | null} 找到的节点对象或 null */ const findNode = (id) => { - if (!id) return null; - let node = cc.engine.getInstanceById(id); - if (!node && typeof Editor !== "undefined" && Editor.Utils && Editor.Utils.UuidUtils) { - // 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找 - try { - const decompressed = Editor.Utils.UuidUtils.decompressUuid(id); - if (decompressed !== id) { - node = cc.engine.getInstanceById(decompressed); - } - } catch (e) { - // 忽略转换错误 - } - } - return node; + if (!id) return null; + let node = cc.engine.getInstanceById(id); + if (!node && typeof Editor !== "undefined" && Editor.Utils && Editor.Utils.UuidUtils) { + // 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找 + try { + const decompressed = Editor.Utils.UuidUtils.decompressUuid(id); + if (decompressed !== id) { + node = cc.engine.getInstanceById(decompressed); + } + } catch (e) { + // 忽略转换错误 + } + } + return node; }; module.exports = { - /** - * 修改节点的基础属性 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (id, path, value) - */ - "set-property": function (event, args) { - const { id, path, value } = args; - - // 1. 获取节点 - let node = findNode(id); - - if (node) { - // 2. 修改属性 - if (path === "name") { - node.name = value; - } else { - node[path] = value; - } - - // 3. 【解决报错的关键】告诉编辑器场景变脏了(需要保存) - // 在场景进程中,我们发送 IPC 给主进程 - Editor.Ipc.sendToMain("scene:dirty"); - - // 4. 【额外补丁】通知层级管理器(Hierarchy)同步更新节点名称 - // 否则你修改了名字,层级管理器可能还是显示旧名字 - Editor.Ipc.sendToAll("scene:node-changed", { - uuid: id, - }); - - if (event.reply) { - event.reply(null, `节点 ${id} 已更新为 ${value}`); - } - } else { - if (event.reply) { - event.reply(new Error("场景脚本:找不到节点 " + id)); - } - } - }, - /** - * 获取当前场景的完整层级树 - * @param {Object} event IPC 事件对象 - */ - "get-hierarchy": function (event) { - const scene = cc.director.getScene(); - - /** - * 递归遍历并序列化节点树 - * @param {cc.Node} node 目标节点 - * @returns {Object|null} 序列化后的节点数据 - */ - function dumpNodes(node) { - // 【优化】跳过编辑器内部的私有节点,减少数据量 - if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { - return null; - } - - let nodeData = { - name: node.name, - uuid: node.uuid, - active: node.active, - position: { x: Math.round(node.x), y: Math.round(node.y) }, - scale: { x: node.scaleX, y: node.scaleY }, - size: { width: node.width, height: node.height }, - // 记录组件类型,让 AI 知道这是个什么节点 - components: node._components.map((c) => c.__typename), - children: [], - }; - - for (let i = 0; i < node.childrenCount; i++) { - let childData = dumpNodes(node.children[i]); - if (childData) nodeData.children.push(childData); - } - return nodeData; - } - - const hierarchy = dumpNodes(scene); - if (event.reply) event.reply(null, hierarchy); - }, - - /** - * 批量更新节点的变换信息 (坐标、缩放、颜色) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (id, x, y, scaleX, scaleY, color) - */ - "update-node-transform": function (event, 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); - - if (node) { - Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`); - - // 使用 scene:set-property 实现支持 Undo 的属性修改 - // 注意:IPC 消息需要发送到 'scene' 面板 - if (x !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id, - path: "x", - type: "Number", - value: Number(x), - }); - } - if (y !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id, - path: "y", - type: "Number", - value: Number(y), - }); - } - if (args.width !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id, - path: "width", - type: "Number", - value: Number(args.width), - }); - } - if (args.height !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id, - path: "height", - type: "Number", - value: Number(args.height), - }); - } - if (scaleX !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id, - path: "scaleX", - type: "Number", - value: Number(scaleX), - }); - } - if (scaleY !== undefined) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id, - path: "scaleY", - type: "Number", - value: Number(scaleY), - }); - } - if (color) { - const c = new cc.Color().fromHEX(color); - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id: id, - path: "color", - type: "Color", - value: { r: c.r, g: c.g, b: c.b, a: c.a }, - }); - } - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: id }); - - Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`); - if (event.reply) event.reply(null, "变换信息已更新"); - } else { - if (event.reply) event.reply(new Error("找不到节点")); - } - }, - /** - * 在场景中创建新节点 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (name, parentId, type) - */ - "create-node": function (event, args) { - const { name, parentId, type } = args; - const scene = cc.director.getScene(); - if (!scene) { - if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。")); - return; - } - - let newNode = null; - - // 特殊处理:如果是创建 Canvas,自动设置好适配 - if (type === "canvas" || name === "Canvas") { - newNode = new cc.Node("Canvas"); - let canvas = newNode.addComponent(cc.Canvas); - newNode.addComponent(cc.Widget); - // 设置默认设计分辨率 - canvas.designResolution = cc.size(960, 640); - canvas.fitHeight = true; - // 自动在 Canvas 下创建一个 Camera - let camNode = new cc.Node("Main Camera"); - camNode.addComponent(cc.Camera); - camNode.parent = newNode; - } else if (type === "sprite") { - newNode = new cc.Node(name || "新建精灵节点"); - let sprite = newNode.addComponent(cc.Sprite); - // 设置为 CUSTOM 模式 - sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; - // 为精灵设置默认尺寸 - newNode.width = 100; - newNode.height = 100; - - // 加载引擎默认图做占位 - if (args.defaultSpriteUuid) { - cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { - if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { - sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - } - } else if (type === "button") { - newNode = new cc.Node(name || "新建按钮节点"); - let sprite = newNode.addComponent(cc.Sprite); - newNode.addComponent(cc.Button); - - // 设置为 CUSTOM 模式并应用按钮专用尺寸 - sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; - newNode.width = 150; - newNode.height = 50; - - // 设置稍暗的背景颜色 (#A0A0A0),以便于看清其上的文字 - newNode.color = new cc.Color(160, 160, 160); - - // 加载引擎默认图 - if (args.defaultSpriteUuid) { - cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { - if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { - sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - } - } else if (type === "label") { - newNode = new cc.Node(name || "新建文本节点"); - let l = newNode.addComponent(cc.Label); - l.string = "新文本"; - newNode.width = 120; - newNode.height = 40; - } else { - newNode = new cc.Node(name || "新建节点"); - } - - // 设置层级 - let parent = parentId ? findNode(parentId) : scene; - if (parent) { - newNode.parent = parent; - - // 【优化】通知主进程场景变脏 - Editor.Ipc.sendToMain("scene:dirty"); - - // 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环 - setTimeout(() => { - Editor.Ipc.sendToAll("scene:node-created", { - uuid: newNode.uuid, - parentUuid: parent.uuid, - }); - }, 10); - - if (event.reply) event.reply(null, newNode.uuid); - } else { - if (event.reply) event.reply(new Error(`无法创建节点:找不到父节点 ${parentId}`)); - } - }, - - /** - * 管理节点上的组件 (添加、移除、更新属性) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (nodeId, action, componentType, componentId, properties) - */ - "manage-components": function (event, args) { - const { nodeId, action, componentType, componentId, properties } = args; - let node = findNode(nodeId); - - /** - * 辅助函数:应用属性并智能解析 (支持 UUID 资源与节点引用) - * @param {cc.Component} component 目标组件实例 - * @param {Object} props 待更新的属性键值对 - */ - const applyProperties = (component, props) => { - if (!props) return; - // 尝试获取组件类的属性定义 - const compClass = component.constructor; - - for (const [key, value] of Object.entries(props)) { - // 【核心修复】专门处理各类事件属性 (ClickEvents, ScrollEvents 等) - const isEventProp = - Array.isArray(value) && (key.toLowerCase().endsWith("events") || key === "clickEvents"); - - if (isEventProp) { - const eventHandlers = []; - for (const item of value) { - if (typeof item === "object" && (item.target || item.component || item.handler)) { - const handler = new cc.Component.EventHandler(); - - // 解析 Target Node - if (item.target) { - let targetNode = findNode(item.target); - if (!targetNode && item.target instanceof cc.Node) { - targetNode = item.target; - } - - if (targetNode) { - handler.target = targetNode; - Editor.log(`[scene-script] Resolved event target: ${targetNode.name}`); - } - } - - if (item.component) handler.component = item.component; - if (item.handler) handler.handler = item.handler; - if (item.customEventData !== undefined) - handler.customEventData = String(item.customEventData); - - eventHandlers.push(handler); - } else { - // 如果不是对象,原样保留 - eventHandlers.push(item); - } - } - component[key] = eventHandlers; - continue; // 处理完事件数组,跳出本次循环 - } - - // 检查属性是否存在 - if (component[key] !== undefined) { - let finalValue = value; - - // 【核心逻辑】智能类型识别与赋值 - try { - const attrs = (cc.Class.Attr.getClassAttrs && cc.Class.Attr.getClassAttrs(compClass)) || {}; - let propertyType = attrs[key] ? attrs[key].type : null; - if (!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 isAssetArray = - Array.isArray(value) && (key === "materials" || key.toLowerCase().includes("assets")); - - // 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点 - if (!isAsset && !isAssetArray && typeof value === "string" && value.length > 20) { - const lowerKey = key.toLowerCase(); - const assetKeywords = [ - "prefab", - "sprite", - "texture", - "material", - "skeleton", - "spine", - "atlas", - "font", - "audio", - "data", - ]; - if (assetKeywords.some((k) => lowerKey.includes(k))) { - if (!findNode(value)) { - isAsset = true; - } - } - } - - if (isAsset || isAssetArray) { - // 1. 处理资源引用 (单个或数组) - const uuids = isAssetArray ? value : [value]; - const loadedAssets = []; - let loadedCount = 0; - - if (uuids.length === 0) { - component[key] = []; - return; - } - - uuids.forEach((uuid, idx) => { - if (typeof uuid !== "string" || uuid.length < 10) { - loadedCount++; - return; - } - cc.AssetLibrary.loadAsset(uuid, (err, asset) => { - loadedCount++; - if (!err && asset) { - loadedAssets[idx] = asset; - Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`); - } else { - Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`); - } - - if (loadedCount === uuids.length) { - if (isAssetArray) { - // 过滤掉加载失败的 - component[key] = loadedAssets.filter((a) => !!a); - } else { - if (loadedAssets[0]) component[key] = loadedAssets[0]; - } - - // 通知编辑器 UI 更新 - const compIndex = node._components.indexOf(component); - if (compIndex !== -1) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id: node.uuid, - path: `_components.${compIndex}.${key}`, - type: isAssetArray ? "Array" : "Object", - value: isAssetArray ? uuids.map((u) => ({ uuid: u })) : { uuid: value }, - isSubProp: false, - }); - } - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - }); - // 【重要修复】使用 continue 而不是 return,确保处理完 Asset 属性后 - // 还能继续处理后续的普通属性 (如 type, sizeMode 等) - continue; - } else if ( - propertyType && - (propertyType.prototype instanceof cc.Component || - propertyType === cc.Component || - propertyType === cc.Node) - ) { - // 2. 处理节点或组件引用 - const targetNode = findNode(value); - if (targetNode) { - if (propertyType === cc.Node) { - finalValue = targetNode; - } else { - const targetComp = targetNode.getComponent(propertyType); - if (targetComp) { - finalValue = targetComp; - } else { - Editor.warn( - `[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`, - ); - } - } - Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`); - } else if (value && value.length > 20) { - // 如果明确是组件/节点类型但找不到,才报错 - Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`); - } - } else { - // 3. 通用启发式 (找不到类型时的 fallback) - if (typeof value === "string" && value.length > 20) { - const targetNode = findNode(value); - if (targetNode) { - finalValue = targetNode; - Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`); - } else { - // 找不到节点且是 UUID -> 视为资源 - const compIndex = node._components.indexOf(component); - if (compIndex !== -1) { - Editor.Ipc.sendToPanel("scene", "scene:set-property", { - id: node.uuid, - path: `_components.${compIndex}.${key}`, - type: "Object", - value: { uuid: value }, - isSubProp: false, - }); - Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`); - } - return; - } - } - } - } catch (e) { - Editor.warn(`[scene-script] 解析属性 ${key} 失败: ${e.message}`); - } - - component[key] = finalValue; - } - } - }; - - if (!node) { - if (event.reply) event.reply(new Error("找不到节点")); - return; - } - - switch (action) { - case "add": - if (!componentType) { - if (event.reply) event.reply(new Error("必须提供组件类型")); - return; - } - - try { - // 解析组件类型 - let compClass = null; - if (componentType.startsWith("cc.")) { - const className = componentType.replace("cc.", ""); - compClass = cc[className]; - } else { - // 尝试获取自定义组件 - compClass = cc.js.getClassByName(componentType); - } - - if (!compClass) { - if (event.reply) event.reply(new Error(`找不到组件类型: ${componentType}`)); - return; - } - - // 添加组件 - const component = node.addComponent(compClass); - - // 设置属性 - if (properties) { - applyProperties(component, properties); - } - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - - if (event.reply) event.reply(null, `组件 ${componentType} 已添加`); - } catch (err) { - if (event.reply) event.reply(new Error(`添加组件失败: ${err.message}`)); - } - break; - - case "remove": - if (!componentId) { - if (event.reply) event.reply(new Error("必须提供组件 ID")); - return; - } - - try { - // 查找并移除组件 - let component = null; - if (node._components) { - for (let i = 0; i < node._components.length; i++) { - if (node._components[i].uuid === componentId) { - component = node._components[i]; - break; - } - } - } - - if (component) { - node.removeComponent(component); - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - if (event.reply) event.reply(null, "组件已移除"); - } else { - if (event.reply) event.reply(new Error("找不到组件")); - } - } catch (err) { - if (event.reply) event.reply(new Error(`移除组件失败: ${err.message}`)); - } - break; - - case "update": - // 更新现有组件属性 - if (!componentType) { - // 如果提供了 componentId,可以只用 componentId - // 但 Cocos 2.4 uuid 获取组件比较麻烦,最好还是有 type 或者遍历 - } - - try { - let targetComp = null; - - // 1. 尝试通过 componentId 查找 - if (componentId) { - if (node._components) { - for (let i = 0; i < node._components.length; i++) { - if (node._components[i].uuid === componentId) { - targetComp = node._components[i]; - break; - } - } - } - } - - // 2. 尝试通过 type 查找 - if (!targetComp && componentType) { - let compClass = null; - if (componentType.startsWith("cc.")) { - const className = componentType.replace("cc.", ""); - compClass = cc[className]; - } else { - compClass = cc.js.getClassByName(componentType); - } - if (compClass) { - targetComp = node.getComponent(compClass); - } - } - - if (targetComp) { - if (properties) { - applyProperties(targetComp, properties); - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - if (event.reply) event.reply(null, "组件属性已更新"); - } else { - if (event.reply) event.reply(null, "没有需要更新的属性"); - } - } else { - if (event.reply) - event.reply(new Error(`找不到组件 (类型: ${componentType}, ID: ${componentId})`)); - } - } catch (err) { - if (event.reply) event.reply(new Error(`更新组件失败: ${err.message}`)); - } - break; - - case "get": - try { - const components = node._components.map((c) => { - // 获取组件属性 - const properties = {}; - for (const key in c) { - if (typeof c[key] !== "function" && !key.startsWith("_") && c[key] !== undefined) { - try { - // 安全序列化检查 - const val = c[key]; - if (val === null || val === undefined) { - properties[key] = val; - continue; - } - - // 基础类型是安全的 - if (typeof val !== "object") { - properties[key] = val; - continue; - } - - // 特殊 Cocos 类型 - if (val instanceof cc.ValueType) { - properties[key] = val.toString(); - } else if (val instanceof cc.Asset) { - properties[key] = `资源(${val.name})`; - } else if (val instanceof cc.Node) { - properties[key] = `节点(${val.name})`; - } else if (val instanceof cc.Component) { - properties[key] = `组件(${val.name}<${val.__typename}>)`; - } else { - // 数组和普通对象 - // 尝试转换为纯 JSON 数据以避免 IPC 错误(如包含原生对象/循环引用) - try { - const jsonStr = JSON.stringify(val); - // 确保不传递原始对象引用 - properties[key] = JSON.parse(jsonStr); - } catch (e) { - // 如果 JSON 失败(例如循环引用),格式化为字符串 - properties[key] = - `[复杂对象: ${val.constructor ? val.constructor.name : typeof val}]`; - } - } - } catch (e) { - properties[key] = "[Serialization Error]"; - } - } - } - return { - type: cc.js.getClassName(c) || c.constructor.name || "Unknown", - uuid: c.uuid, - properties: properties, - }; - }); - if (event.reply) event.reply(null, components); - } catch (err) { - if (event.reply) event.reply(new Error(`获取组件失败: ${err.message}`)); - } - break; - - default: - if (event.reply) event.reply(new Error(`未知的组件操作类型: ${action}`)); - break; - } - }, - - "get-component-properties": function (component) { - const properties = {}; - - // 遍历组件属性 - for (const key in component) { - if (typeof component[key] !== "function" && !key.startsWith("_") && component[key] !== undefined) { - try { - properties[key] = component[key]; - } catch (e) { - // 忽略无法序列化的属性 - } - } - } - - return properties; - }, - - "instantiate-prefab": function (event, args) { - const { prefabUuid, parentId } = args; - const scene = cc.director.getScene(); - - if (!scene) { - if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。")); - return; - } - - if (!prefabUuid) { - if (event.reply) event.reply(new Error("必须提供预制体 UUID。")); - return; - } - - // 使用 cc.assetManager.loadAny 通过 UUID 加载 (Cocos 2.4+) - // 如果是旧版,可能需要 cc.loader.load({uuid: ...}),但在 2.4 环境下 assetManager 更推荐 - cc.assetManager.loadAny(prefabUuid, (err, prefab) => { - if (err) { - if (event.reply) event.reply(new Error(`加载预制体失败: ${err.message}`)); - return; - } - - // 实例化预制体 - const instance = cc.instantiate(prefab); - if (!instance) { - if (event.reply) event.reply(new Error("实例化预制体失败")); - return; - } - - // 设置父节点 - let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; - if (parent) { - instance.parent = parent; - - // 通知场景变脏 - Editor.Ipc.sendToMain("scene:dirty"); - - // 通知 UI 刷新 - setTimeout(() => { - Editor.Ipc.sendToAll("scene:node-created", { - uuid: instance.uuid, - parentUuid: parent.uuid, - }); - }, 10); - - if (event.reply) event.reply(null, `预制体实例化成功,UUID: ${instance.uuid}`); - } else { - if (event.reply) event.reply(new Error("找不到父节点")); - } - }); - }, - - /** - * 根据特定条件在场景中搜索节点 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (conditions, recursive) - */ - "find-gameobjects": function (event, args) { - const { conditions, recursive = true } = args; - const result = []; - const scene = cc.director.getScene(); - - function searchNode(node) { - // 跳过编辑器内部的私有节点 - if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") { - return; - } - - // 检查节点是否满足条件 - let match = true; - - if (conditions.name && !node.name.includes(conditions.name)) { - match = false; - } - - if (conditions.component) { - let hasComponent = false; - try { - if (conditions.component.startsWith("cc.")) { - const className = conditions.component.replace("cc.", ""); - hasComponent = node.getComponent(cc[className]) !== null; - } else { - hasComponent = node.getComponent(conditions.component) !== null; - } - } catch (e) { - hasComponent = false; - } - if (!hasComponent) { - match = false; - } - } - - if (conditions.active !== undefined && node.active !== conditions.active) { - match = false; - } - - if (match) { - result.push({ - uuid: node.uuid, - name: node.name, - active: node.active, - position: { x: node.x, y: node.y }, - scale: { x: node.scaleX, y: node.scaleY }, - size: { width: node.width, height: node.height }, - components: node._components.map((c) => c.__typename), - }); - } - - // 递归搜索子节点 - if (recursive) { - for (let i = 0; i < node.childrenCount; i++) { - searchNode(node.children[i]); - } - } - } - - // 从场景根节点开始搜索 - if (scene) { - searchNode(scene); - } - - if (event.reply) { - event.reply(null, result); - } - }, - - /** - * 删除指定的场景节点 - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (uuid) - */ - "delete-node": function (event, args) { - const { uuid } = args; - const node = findNode(uuid); - if (node) { - const parent = node.parent; - node.destroy(); - Editor.Ipc.sendToMain("scene:dirty"); - // 延迟通知以确保节点已被移除 - setTimeout(() => { - if (parent) { - Editor.Ipc.sendToAll("scene:node-changed", { uuid: parent.uuid }); - } - // 广播节点删除事件 - Editor.Ipc.sendToAll("scene:node-deleted", { uuid: uuid }); - }, 10); - - if (event.reply) event.reply(null, `节点 ${uuid} 已删除`); - } else { - if (event.reply) event.reply(new Error(`找不到节点: ${uuid}`)); - } - }, - - /** - * 管理高效的全场景特效 (粒子系统) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (action, nodeId, properties, name, parentId) - */ - "manage-vfx": function (event, args) { - const { action, nodeId, properties, name, parentId } = args; - const scene = cc.director.getScene(); - - const applyParticleProperties = (particleSystem, props) => { - if (!props) return; - - if (props.duration !== undefined) particleSystem.duration = props.duration; - if (props.emissionRate !== undefined) particleSystem.emissionRate = props.emissionRate; - if (props.life !== undefined) particleSystem.life = props.life; - if (props.lifeVar !== undefined) particleSystem.lifeVar = props.lifeVar; - - // 【关键修复】启用自定义属性,否则属性修改可能不生效 - particleSystem.custom = true; - - if (props.startColor) particleSystem.startColor = new cc.Color().fromHEX(props.startColor); - if (props.endColor) particleSystem.endColor = new cc.Color().fromHEX(props.endColor); - - if (props.startSize !== undefined) particleSystem.startSize = props.startSize; - if (props.endSize !== undefined) particleSystem.endSize = props.endSize; - - if (props.speed !== undefined) particleSystem.speed = props.speed; - if (props.angle !== undefined) particleSystem.angle = props.angle; - - if (props.gravity) { - if (props.gravity.x !== undefined) particleSystem.gravity.x = props.gravity.x; - if (props.gravity.y !== undefined) particleSystem.gravity.y = props.gravity.y; - } - - // 处理文件/纹理加载 - if (props.file) { - // main.js 已经将 db:// 路径转换为 UUID - // 如果用户直接传递 URL (http/https) 或其他格式,cc.assetManager.loadAny 也能处理 - const uuid = props.file; - cc.assetManager.loadAny(uuid, (err, asset) => { - if (!err) { - if (asset instanceof cc.ParticleAsset) { - particleSystem.file = asset; - } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { - particleSystem.texture = asset; - } - Editor.Ipc.sendToMain("scene:dirty"); - } - }); - } else if (!particleSystem.texture && !particleSystem.file && args.defaultSpriteUuid) { - // 【关键修复】如果没有纹理,加载默认纹理 (UUID 由 main.js 传入) - Editor.log(`[mcp-bridge] Loading default texture with UUID: ${args.defaultSpriteUuid}`); - cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { - if (err) { - Editor.error(`[mcp-bridge] Failed to load default texture: ${err.message}`); - } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { - Editor.log(`[mcp-bridge] Default texture loaded successfully.`); - particleSystem.texture = asset; - Editor.Ipc.sendToMain("scene:dirty"); - } else { - Editor.warn(`[mcp-bridge] Loaded asset is not a texture: ${asset}`); - } - }); - } - }; - - if (action === "create") { - let newNode = new cc.Node(name || "New Particle"); - let particleSystem = newNode.addComponent(cc.ParticleSystem); - - // 设置默认值 - particleSystem.resetSystem(); - particleSystem.custom = true; // 确保新创建的也是 custom 模式 - - applyParticleProperties(particleSystem, properties); - - let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; - if (parent) { - newNode.parent = parent; - Editor.Ipc.sendToMain("scene:dirty"); - setTimeout(() => { - Editor.Ipc.sendToAll("scene:node-created", { - uuid: newNode.uuid, - parentUuid: parent.uuid, - }); - }, 10); - if (event.reply) event.reply(null, newNode.uuid); - } else { - if (event.reply) event.reply(new Error("找不到父节点")); - } - } else if (action === "update") { - let node = findNode(nodeId); - if (node) { - let particleSystem = node.getComponent(cc.ParticleSystem); - if (!particleSystem) { - // 如果没有组件,自动添加 - particleSystem = node.addComponent(cc.ParticleSystem); - } - - applyParticleProperties(particleSystem, properties); - - Editor.Ipc.sendToMain("scene:dirty"); - Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); - if (event.reply) event.reply(null, "特效已更新"); - } else { - if (event.reply) event.reply(new Error("找不到节点")); - } - } else if (action === "get_info") { - let node = findNode(nodeId); - if (node) { - let ps = node.getComponent(cc.ParticleSystem); - if (ps) { - const info = { - duration: ps.duration, - emissionRate: ps.emissionRate, - life: ps.life, - lifeVar: ps.lifeVar, - startColor: ps.startColor.toHEX("#RRGGBB"), - endColor: ps.endColor.toHEX("#RRGGBB"), - startSize: ps.startSize, - endSize: ps.endSize, - speed: ps.speed, - angle: ps.angle, - gravity: { x: ps.gravity.x, y: ps.gravity.y }, - file: ps.file ? ps.file.name : null, - }; - if (event.reply) event.reply(null, info); - } else { - if (event.reply) event.reply(null, { hasParticleSystem: false }); - } - } else { - if (event.reply) event.reply(new Error("找不到节点")); - } - } else { - if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`)); - } - }, - - /** - * 控制节点的动画组件 (播放、暂停、停止等) - * @param {Object} event IPC 事件对象 - * @param {Object} args 参数 (action, nodeId, clipName) - */ - "manage-animation": function (event, args) { - const { action, nodeId, clipName } = args; - const node = findNode(nodeId); - - if (!node) { - if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`)); - return; - } - - const anim = node.getComponent(cc.Animation); - if (!anim) { - if (event.reply) event.reply(new Error(`在节点 ${nodeId} 上找不到 Animation 组件`)); - return; - } - - switch (action) { - case "get_list": - const clips = anim.getClips(); - const clipList = clips.map((c) => ({ - name: c.name, - duration: c.duration, - sample: c.sample, - speed: c.speed, - wrapMode: c.wrapMode, - })); - if (event.reply) event.reply(null, clipList); - break; - - case "get_info": - const currentClip = anim.currentClip; - let isPlaying = false; - // [安全修复] 只有在有当前 Clip 时才获取状态,避免 Animation 组件无 Clip 时的崩溃 - if (currentClip) { - const state = anim.getAnimationState(currentClip.name); - if (state) { - isPlaying = state.isPlaying; - } - } - const info = { - currentClip: currentClip ? currentClip.name : null, - clips: anim.getClips().map((c) => c.name), - playOnLoad: anim.playOnLoad, - isPlaying: isPlaying, - }; - if (event.reply) event.reply(null, info); - break; - - case "play": - if (!clipName) { - anim.play(); - if (event.reply) event.reply(null, "正在播放默认动画剪辑"); - } else { - anim.play(clipName); - if (event.reply) event.reply(null, `正在播放动画剪辑: ${clipName}`); - } - break; - - case "stop": - anim.stop(); - if (event.reply) event.reply(null, "动画已停止"); - break; - - case "pause": - anim.pause(); - if (event.reply) event.reply(null, "动画已暂停"); - break; - - case "resume": - anim.resume(); - if (event.reply) event.reply(null, "动画已恢复播放"); - break; - - default: - if (event.reply) event.reply(new Error(`未知的动画操作类型: ${action}`)); - break; - } - }, + /** + * 修改节点的基础属性 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (id, path, value) + */ + "set-property": function (event, args) { + const { id, path, value } = args; + + // 1. 获取节点 + let node = findNode(id); + + if (node) { + // 2. 修改属性 + if (path === "name") { + node.name = value; + } else { + node[path] = value; + } + + // 3. 【解决报错的关键】告诉编辑器场景变脏了(需要保存) + // 在场景进程中,我们发送 IPC 给主进程 + Editor.Ipc.sendToMain("scene:dirty"); + + // 4. 【额外补丁】通知层级管理器(Hierarchy)同步更新节点名称 + // 否则你修改了名字,层级管理器可能还是显示旧名字 + Editor.Ipc.sendToAll("scene:node-changed", { + uuid: id, + }); + + if (event.reply) { + event.reply(null, `节点 ${id} 已更新为 ${value}`); + } + } else { + if (event.reply) { + event.reply(new Error("场景脚本:找不到节点 " + id)); + } + } + }, + /** + * 获取当前场景的完整层级树 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (nodeId, depth, includeDetails) + */ + "get-hierarchy": function (event, args) { + const { nodeId = null, depth = 2, includeDetails = false } = args || {}; + const scene = cc.director.getScene(); + + let rootNode = scene; + if (nodeId) { + rootNode = findNode(nodeId); + if (!rootNode) { + if (event.reply) event.reply(new Error(`找不到指定的起始节点: ${nodeId}`)); + return; + } + } + + /** + * 递归遍历并序列化节点树 + * @param {cc.Node} node 目标节点 + * @param {number} currentDepth 当前深度 + * @returns {Object|null} 序列化后的节点数据 + */ + function dumpNodes(node, currentDepth) { + // 【优化】跳过编辑器内部的私有节点,减少数据量 + if (!node || !node.name || (typeof node.name === 'string' && (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot"))) { + return null; + } + + let nodeData = { + name: node.name, + uuid: node.uuid, + childrenCount: node.childrenCount, + }; + + const comps = node._components || []; + + // 根据是否需要详情来决定附加哪些数据以节省 Token + if (includeDetails) { + nodeData.active = node.active; + nodeData.position = { x: Math.round(node.x), y: Math.round(node.y) }; + nodeData.scale = { x: node.scaleX, y: node.scaleY }; + nodeData.size = { width: node.width, height: node.height }; + nodeData.components = comps.map((c) => cc.js.getClassName(c)); + } else { + // 简略模式下如果存在组件,至少提供一个极简列表让 AI 知道节点的作用 + if (comps.length > 0) { + nodeData.components = comps.map(c => { + const parts = (cc.js.getClassName(c) || "").split('.'); + return parts[parts.length - 1]; // 只取类名,例如 cc.Sprite -> Sprite + }); + } + } + + // 如果未超出深度限制,继续递归子树 + if (currentDepth < depth && node.childrenCount > 0) { + nodeData.children = []; + for (let i = 0; i < node.childrenCount; i++) { + let childData = dumpNodes(node.children[i], currentDepth + 1); + if (childData) nodeData.children.push(childData); + } + } + + return nodeData; + } + + const hierarchy = dumpNodes(rootNode, 0); + if (event.reply) event.reply(null, hierarchy); + }, + + /** + * 批量更新节点的变换信息 (坐标、缩放、颜色) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (id, x, y, scaleX, scaleY, color) + */ + "update-node-transform": function (event, 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); + + if (node) { + Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`); + + // 使用 scene:set-property 实现支持 Undo 的属性修改 + // 注意:IPC 消息需要发送到 'scene' 面板 + if (x !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "x", + type: "Number", + value: Number(x), + }); + } + if (y !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "y", + type: "Number", + value: Number(y), + }); + } + if (args.width !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "width", + type: "Number", + value: Number(args.width), + }); + } + if (args.height !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "height", + type: "Number", + value: Number(args.height), + }); + } + if (scaleX !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "scaleX", + type: "Number", + value: Number(scaleX), + }); + } + if (scaleY !== undefined) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id, + path: "scaleY", + type: "Number", + value: Number(scaleY), + }); + } + if (color) { + const c = new cc.Color().fromHEX(color); + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id: id, + path: "color", + type: "Color", + value: { r: c.r, g: c.g, b: c.b, a: c.a }, + }); + } + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: id }); + + Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`); + if (event.reply) event.reply(null, "变换信息已更新"); + } else { + if (event.reply) event.reply(new Error("找不到节点")); + } + }, + /** + * 在场景中创建新节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (name, parentId, type) + */ + "create-node": function (event, args) { + const { name, parentId, type } = args; + const scene = cc.director.getScene(); + if (!scene) { + if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。")); + return; + } + + let newNode = null; + + // 特殊处理:如果是创建 Canvas,自动设置好适配 + if (type === "canvas" || name === "Canvas") { + newNode = new cc.Node("Canvas"); + let canvas = newNode.addComponent(cc.Canvas); + newNode.addComponent(cc.Widget); + // 设置默认设计分辨率 + canvas.designResolution = cc.size(960, 640); + canvas.fitHeight = true; + // 自动在 Canvas 下创建一个 Camera + let camNode = new cc.Node("Main Camera"); + camNode.addComponent(cc.Camera); + camNode.parent = newNode; + } else if (type === "sprite") { + newNode = new cc.Node(name || "新建精灵节点"); + let sprite = newNode.addComponent(cc.Sprite); + // 设置为 CUSTOM 模式 + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + // 为精灵设置默认尺寸 + newNode.width = 100; + newNode.height = 100; + + // 加载引擎默认图做占位 + if (args.defaultSpriteUuid) { + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { + sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } + } else if (type === "button") { + newNode = new cc.Node(name || "新建按钮节点"); + let sprite = newNode.addComponent(cc.Sprite); + newNode.addComponent(cc.Button); + + // 设置为 CUSTOM 模式并应用按钮专用尺寸 + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + newNode.width = 150; + newNode.height = 50; + + // 设置稍暗的背景颜色 (#A0A0A0),以便于看清其上的文字 + newNode.color = new cc.Color(160, 160, 160); + + // 加载引擎默认图 + if (args.defaultSpriteUuid) { + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) { + sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset); + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } + } else if (type === "label") { + newNode = new cc.Node(name || "新建文本节点"); + let l = newNode.addComponent(cc.Label); + l.string = "新文本"; + newNode.width = 120; + newNode.height = 40; + } else { + newNode = new cc.Node(name || "新建节点"); + } + + // 设置层级 + let parent = parentId ? findNode(parentId) : scene; + if (parent) { + newNode.parent = parent; + + // 【优化】通知主进程场景变脏 + Editor.Ipc.sendToMain("scene:dirty"); + + // 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环 + setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: newNode.uuid, + parentUuid: parent.uuid, + }); + }, 10); + + if (event.reply) event.reply(null, newNode.uuid); + } else { + if (event.reply) event.reply(new Error(`无法创建节点:找不到父节点 ${parentId}`)); + } + }, + + /** + * 管理节点上的组件 (添加、移除、更新属性) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (nodeId, action, componentType, componentId, properties) + */ + "manage-components": function (event, args) { + const { nodeId, action, componentType, componentId, properties } = args; + let node = findNode(nodeId); + + /** + * 辅助函数:应用属性并智能解析 (支持 UUID 资源与节点引用) + * @param {cc.Component} component 目标组件实例 + * @param {Object} props 待更新的属性键值对 + */ + const applyProperties = (component, props) => { + if (!props) return; + // 尝试获取组件类的属性定义 + const compClass = component.constructor; + + for (const [key, value] of Object.entries(props)) { + // 【核心修复】专门处理各类事件属性 (ClickEvents, ScrollEvents 等) + const isEventProp = + Array.isArray(value) && (key.toLowerCase().endsWith("events") || key === "clickEvents"); + + if (isEventProp) { + const eventHandlers = []; + for (const item of value) { + if (typeof item === "object" && (item.target || item.component || item.handler)) { + const handler = new cc.Component.EventHandler(); + + // 解析 Target Node + if (item.target) { + let targetNode = findNode(item.target); + if (!targetNode && item.target instanceof cc.Node) { + targetNode = item.target; + } + + if (targetNode) { + handler.target = targetNode; + Editor.log(`[scene-script] Resolved event target: ${targetNode.name}`); + } + } + + if (item.component) handler.component = item.component; + if (item.handler) handler.handler = item.handler; + if (item.customEventData !== undefined) + handler.customEventData = String(item.customEventData); + + eventHandlers.push(handler); + } else { + // 如果不是对象,原样保留 + eventHandlers.push(item); + } + } + component[key] = eventHandlers; + continue; // 处理完事件数组,跳出本次循环 + } + + // 检查属性是否存在 + if (component[key] !== undefined) { + let finalValue = value; + + // 【核心逻辑】智能类型识别与赋值 + try { + const attrs = (cc.Class.Attr.getClassAttrs && cc.Class.Attr.getClassAttrs(compClass)) || {}; + let propertyType = attrs[key] ? attrs[key].type : null; + if (!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 isAssetArray = + Array.isArray(value) && (key === "materials" || key.toLowerCase().includes("assets")); + + // 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点 + if (!isAsset && !isAssetArray && typeof value === "string" && value.length > 20) { + const lowerKey = key.toLowerCase(); + const assetKeywords = [ + "prefab", + "sprite", + "texture", + "material", + "skeleton", + "spine", + "atlas", + "font", + "audio", + "data", + ]; + if (assetKeywords.some((k) => lowerKey.includes(k))) { + if (!findNode(value)) { + isAsset = true; + } + } + } + + if (isAsset || isAssetArray) { + // 1. 处理资源引用 (单个或数组) + const uuids = isAssetArray ? value : [value]; + const loadedAssets = []; + let loadedCount = 0; + + if (uuids.length === 0) { + component[key] = []; + return; + } + + uuids.forEach((uuid, idx) => { + if (typeof uuid !== "string" || uuid.length < 10) { + loadedCount++; + return; + } + cc.AssetLibrary.loadAsset(uuid, (err, asset) => { + loadedCount++; + if (!err && asset) { + loadedAssets[idx] = asset; + Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`); + } else { + Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`); + } + + if (loadedCount === uuids.length) { + if (isAssetArray) { + // 过滤掉加载失败的 + component[key] = loadedAssets.filter((a) => !!a); + } else { + if (loadedAssets[0]) component[key] = loadedAssets[0]; + } + + // 通知编辑器 UI 更新 + const compIndex = node._components.indexOf(component); + if (compIndex !== -1) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id: node.uuid, + path: `_components.${compIndex}.${key}`, + type: isAssetArray ? "Array" : "Object", + value: isAssetArray ? uuids.map((u) => ({ uuid: u })) : { uuid: value }, + isSubProp: false, + }); + } + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + }); + // 【重要修复】使用 continue 而不是 return,确保处理完 Asset 属性后 + // 还能继续处理后续的普通属性 (如 type, sizeMode 等) + continue; + } else if ( + propertyType && + (propertyType.prototype instanceof cc.Component || + propertyType === cc.Component || + propertyType === cc.Node) + ) { + // 2. 处理节点或组件引用 + const targetNode = findNode(value); + if (targetNode) { + if (propertyType === cc.Node) { + finalValue = targetNode; + } else { + const targetComp = targetNode.getComponent(propertyType); + if (targetComp) { + finalValue = targetComp; + } else { + Editor.warn( + `[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`, + ); + } + } + Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`); + } else if (value && value.length > 20) { + // 如果明确是组件/节点类型但找不到,才报错 + Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`); + } + } else { + // 3. 通用启发式 (找不到类型时的 fallback) + if (typeof value === "string" && value.length > 20) { + const targetNode = findNode(value); + if (targetNode) { + finalValue = targetNode; + Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`); + } else { + // 找不到节点且是 UUID -> 视为资源 + const compIndex = node._components.indexOf(component); + if (compIndex !== -1) { + Editor.Ipc.sendToPanel("scene", "scene:set-property", { + id: node.uuid, + path: `_components.${compIndex}.${key}`, + type: "Object", + value: { uuid: value }, + isSubProp: false, + }); + Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`); + } + return; + } + } + } + } catch (e) { + Editor.warn(`[scene-script] 解析属性 ${key} 失败: ${e.message}`); + } + + component[key] = finalValue; + } + } + }; + + if (!node) { + if (event.reply) event.reply(new Error("找不到节点")); + return; + } + + switch (action) { + case "add": + if (!componentType) { + if (event.reply) event.reply(new Error("必须提供组件类型")); + return; + } + + try { + // 解析组件类型 + let compClass = null; + if (componentType.startsWith("cc.")) { + const className = componentType.replace("cc.", ""); + compClass = cc[className]; + } else { + // 尝试获取自定义组件 + compClass = cc.js.getClassByName(componentType); + } + + if (!compClass) { + if (event.reply) event.reply(new Error(`找不到组件类型: ${componentType}`)); + return; + } + + // 添加组件 + const component = node.addComponent(compClass); + + // 设置属性 + if (properties) { + applyProperties(component, properties); + } + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + + if (event.reply) event.reply(null, `组件 ${componentType} 已添加`); + } catch (err) { + if (event.reply) event.reply(new Error(`添加组件失败: ${err.message}`)); + } + break; + + case "remove": + if (!componentId) { + if (event.reply) event.reply(new Error("必须提供组件 ID")); + return; + } + + try { + // 查找并移除组件 + let component = null; + if (node._components) { + for (let i = 0; i < node._components.length; i++) { + if (node._components[i].uuid === componentId) { + component = node._components[i]; + break; + } + } + } + + if (component) { + node.removeComponent(component); + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + if (event.reply) event.reply(null, "组件已移除"); + } else { + if (event.reply) event.reply(new Error("找不到组件")); + } + } catch (err) { + if (event.reply) event.reply(new Error(`移除组件失败: ${err.message}`)); + } + break; + + case "update": + // 更新现有组件属性 + if (!componentType) { + // 如果提供了 componentId,可以只用 componentId + // 但 Cocos 2.4 uuid 获取组件比较麻烦,最好还是有 type 或者遍历 + } + + try { + let targetComp = null; + + // 1. 尝试通过 componentId 查找 + if (componentId) { + if (node._components) { + for (let i = 0; i < node._components.length; i++) { + if (node._components[i].uuid === componentId) { + targetComp = node._components[i]; + break; + } + } + } + } + + // 2. 尝试通过 type 查找 + if (!targetComp && componentType) { + let compClass = null; + if (componentType.startsWith("cc.")) { + const className = componentType.replace("cc.", ""); + compClass = cc[className]; + } else { + compClass = cc.js.getClassByName(componentType); + } + if (compClass) { + targetComp = node.getComponent(compClass); + } + } + + if (targetComp) { + if (properties) { + applyProperties(targetComp, properties); + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + if (event.reply) event.reply(null, "组件属性已更新"); + } else { + if (event.reply) event.reply(null, "没有需要更新的属性"); + } + } else { + if (event.reply) + event.reply(new Error(`找不到组件 (类型: ${componentType}, ID: ${componentId})`)); + } + } catch (err) { + if (event.reply) event.reply(new Error(`更新组件失败: ${err.message}`)); + } + break; + + case "get": + try { + const components = node._components.map((c) => { + // 获取组件属性 + const properties = {}; + for (const key in c) { + if (typeof c[key] !== "function" && !key.startsWith("_") && c[key] !== undefined) { + try { + // 安全序列化检查 + const val = c[key]; + if (val === null || val === undefined) { + properties[key] = val; + continue; + } + + // 基础类型是安全的 + if (typeof val !== "object") { + // 【优化】对于超长字符串进行截断 + if (typeof val === "string" && val.length > 200) { + properties[key] = val.substring(0, 50) + `...[Truncated, total length: ${val.length}]`; + } else { + properties[key] = val; + } + continue; + } + + // 特殊 Cocos 类型 + if (val instanceof cc.ValueType) { + properties[key] = val.toString(); + } else if (val instanceof cc.Asset) { + properties[key] = `资源(${val.name})`; + } else if (val instanceof cc.Node) { + properties[key] = `节点(${val.name})`; + } else if (val instanceof cc.Component) { + properties[key] = `组件(${val.name}<${val.__typename}>)`; + } else { + // 数组和普通对象 + // 【优化】对于超长数组直接截断并提示,防止返回巨大的坐标或点集 + if (Array.isArray(val) && val.length > 10) { + properties[key] = `[Array(${val.length})]`; + continue; + } + + // 尝试转换为纯 JSON 数据以避免 IPC 错误(如包含原生对象/循环引用) + try { + const jsonStr = JSON.stringify(val); + if (jsonStr && jsonStr.length > 500) { + properties[key] = `[Large JSON Object, length: ${jsonStr.length}]`; + } else { + // 确保不传递原始对象引用 + properties[key] = JSON.parse(jsonStr); + } + } catch (e) { + // 如果 JSON 失败(例如循环引用),格式化为字符串 + properties[key] = + `[复杂对象: ${val.constructor ? val.constructor.name : typeof val}]`; + } + } + } catch (e) { + properties[key] = "[Serialization Error]"; + } + } + } + return { + type: cc.js.getClassName(c) || c.constructor.name || "Unknown", + uuid: c.uuid, + properties: properties, + }; + }); + if (event.reply) event.reply(null, components); + } catch (err) { + if (event.reply) event.reply(new Error(`获取组件失败: ${err.message}`)); + } + break; + + default: + if (event.reply) event.reply(new Error(`未知的组件操作类型: ${action}`)); + break; + } + }, + + "get-component-properties": function (component) { + const properties = {}; + + // 遍历组件属性 + for (const key in component) { + if (typeof component[key] !== "function" && !key.startsWith("_") && component[key] !== undefined) { + try { + properties[key] = component[key]; + } catch (e) { + // 忽略无法序列化的属性 + } + } + } + + return properties; + }, + + "instantiate-prefab": function (event, args) { + const { prefabUuid, parentId } = args; + const scene = cc.director.getScene(); + + if (!scene) { + if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。")); + return; + } + + if (!prefabUuid) { + if (event.reply) event.reply(new Error("必须提供预制体 UUID。")); + return; + } + + // 使用 cc.assetManager.loadAny 通过 UUID 加载 (Cocos 2.4+) + // 如果是旧版,可能需要 cc.loader.load({uuid: ...}),但在 2.4 环境下 assetManager 更推荐 + cc.assetManager.loadAny(prefabUuid, (err, prefab) => { + if (err) { + if (event.reply) event.reply(new Error(`加载预制体失败: ${err.message}`)); + return; + } + + // 实例化预制体 + const instance = cc.instantiate(prefab); + if (!instance) { + if (event.reply) event.reply(new Error("实例化预制体失败")); + return; + } + + // 设置父节点 + let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; + if (parent) { + instance.parent = parent; + + // 通知场景变脏 + Editor.Ipc.sendToMain("scene:dirty"); + + // 通知 UI 刷新 + setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: instance.uuid, + parentUuid: parent.uuid, + }); + }, 10); + + if (event.reply) event.reply(null, `预制体实例化成功,UUID: ${instance.uuid}`); + } else { + if (event.reply) event.reply(new Error("找不到父节点")); + } + }); + }, + + /** + * 根据特定条件在场景中搜索节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (conditions, recursive) + */ + "find-gameobjects": function (event, args) { + const { conditions, recursive = true } = args; + const result = []; + const scene = cc.director.getScene(); + + function searchNode(node) { + if (!node || !node.name || (typeof node.name === 'string' && (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot"))) { + return; + } + + // 检查节点是否满足条件 + let match = true; + + if (conditions.name && !node.name.includes(conditions.name)) { + match = false; + } + + if (conditions.component) { + let hasComponent = false; + try { + if (conditions.component.startsWith("cc.")) { + const className = conditions.component.replace("cc.", ""); + hasComponent = node.getComponent(cc[className]) !== null; + } else { + hasComponent = node.getComponent(conditions.component) !== null; + } + } catch (e) { + hasComponent = false; + } + if (!hasComponent) { + match = false; + } + } + + if (conditions.active !== undefined && node.active !== conditions.active) { + match = false; + } + + if (match) { + const comps = node._components || []; + result.push({ + uuid: node.uuid, + name: node.name, + active: node.active, + components: comps.map((c) => { + const parts = (cc.js.getClassName(c) || "").split('.'); + return parts[parts.length - 1]; // 简化的组件名 + }), + childrenCount: node.childrenCount + }); + } + + // 递归搜索子节点 + if (recursive) { + for (let i = 0; i < node.childrenCount; i++) { + searchNode(node.children[i]); + } + } + } + + // 从场景根节点开始搜索 + if (scene) { + searchNode(scene); + } + + if (event.reply) { + event.reply(null, result); + } + }, + + /** + * 删除指定的场景节点 + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (uuid) + */ + "delete-node": function (event, args) { + const { uuid } = args; + const node = findNode(uuid); + if (node) { + const parent = node.parent; + node.destroy(); + Editor.Ipc.sendToMain("scene:dirty"); + // 延迟通知以确保节点已被移除 + setTimeout(() => { + if (parent) { + Editor.Ipc.sendToAll("scene:node-changed", { uuid: parent.uuid }); + } + // 广播节点删除事件 + Editor.Ipc.sendToAll("scene:node-deleted", { uuid: uuid }); + }, 10); + + if (event.reply) event.reply(null, `节点 ${uuid} 已删除`); + } else { + if (event.reply) event.reply(new Error(`找不到节点: ${uuid}`)); + } + }, + + /** + * 管理高效的全场景特效 (粒子系统) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (action, nodeId, properties, name, parentId) + */ + "manage-vfx": function (event, args) { + const { action, nodeId, properties, name, parentId } = args; + const scene = cc.director.getScene(); + + const applyParticleProperties = (particleSystem, props) => { + if (!props) return; + + if (props.duration !== undefined) particleSystem.duration = props.duration; + if (props.emissionRate !== undefined) particleSystem.emissionRate = props.emissionRate; + if (props.life !== undefined) particleSystem.life = props.life; + if (props.lifeVar !== undefined) particleSystem.lifeVar = props.lifeVar; + + // 【关键修复】启用自定义属性,否则属性修改可能不生效 + particleSystem.custom = true; + + if (props.startColor) particleSystem.startColor = new cc.Color().fromHEX(props.startColor); + if (props.endColor) particleSystem.endColor = new cc.Color().fromHEX(props.endColor); + + if (props.startSize !== undefined) particleSystem.startSize = props.startSize; + if (props.endSize !== undefined) particleSystem.endSize = props.endSize; + + if (props.speed !== undefined) particleSystem.speed = props.speed; + if (props.angle !== undefined) particleSystem.angle = props.angle; + + if (props.gravity) { + if (props.gravity.x !== undefined) particleSystem.gravity.x = props.gravity.x; + if (props.gravity.y !== undefined) particleSystem.gravity.y = props.gravity.y; + } + + // 处理文件/纹理加载 + if (props.file) { + // main.js 已经将 db:// 路径转换为 UUID + // 如果用户直接传递 URL (http/https) 或其他格式,cc.assetManager.loadAny 也能处理 + const uuid = props.file; + cc.assetManager.loadAny(uuid, (err, asset) => { + if (!err) { + if (asset instanceof cc.ParticleAsset) { + particleSystem.file = asset; + } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { + particleSystem.texture = asset; + } + Editor.Ipc.sendToMain("scene:dirty"); + } + }); + } else if (!particleSystem.texture && !particleSystem.file && args.defaultSpriteUuid) { + // 【关键修复】如果没有纹理,加载默认纹理 (UUID 由 main.js 传入) + Editor.log(`[mcp-bridge] Loading default texture with UUID: ${args.defaultSpriteUuid}`); + cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => { + if (err) { + Editor.error(`[mcp-bridge] Failed to load default texture: ${err.message}`); + } else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) { + Editor.log(`[mcp-bridge] Default texture loaded successfully.`); + particleSystem.texture = asset; + Editor.Ipc.sendToMain("scene:dirty"); + } else { + Editor.warn(`[mcp-bridge] Loaded asset is not a texture: ${asset}`); + } + }); + } + }; + + if (action === "create") { + let newNode = new cc.Node(name || "New Particle"); + let particleSystem = newNode.addComponent(cc.ParticleSystem); + + // 设置默认值 + particleSystem.resetSystem(); + particleSystem.custom = true; // 确保新创建的也是 custom 模式 + + applyParticleProperties(particleSystem, properties); + + let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; + if (parent) { + newNode.parent = parent; + Editor.Ipc.sendToMain("scene:dirty"); + setTimeout(() => { + Editor.Ipc.sendToAll("scene:node-created", { + uuid: newNode.uuid, + parentUuid: parent.uuid, + }); + }, 10); + if (event.reply) event.reply(null, newNode.uuid); + } else { + if (event.reply) event.reply(new Error("找不到父节点")); + } + } else if (action === "update") { + let node = findNode(nodeId); + if (node) { + let particleSystem = node.getComponent(cc.ParticleSystem); + if (!particleSystem) { + // 如果没有组件,自动添加 + particleSystem = node.addComponent(cc.ParticleSystem); + } + + applyParticleProperties(particleSystem, properties); + + Editor.Ipc.sendToMain("scene:dirty"); + Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId }); + if (event.reply) event.reply(null, "特效已更新"); + } else { + if (event.reply) event.reply(new Error("找不到节点")); + } + } else if (action === "get_info") { + let node = findNode(nodeId); + if (node) { + let ps = node.getComponent(cc.ParticleSystem); + if (ps) { + const info = { + duration: ps.duration, + emissionRate: ps.emissionRate, + life: ps.life, + lifeVar: ps.lifeVar, + startColor: ps.startColor.toHEX("#RRGGBB"), + endColor: ps.endColor.toHEX("#RRGGBB"), + startSize: ps.startSize, + endSize: ps.endSize, + speed: ps.speed, + angle: ps.angle, + gravity: { x: ps.gravity.x, y: ps.gravity.y }, + file: ps.file ? ps.file.name : null, + }; + if (event.reply) event.reply(null, info); + } else { + if (event.reply) event.reply(null, { hasParticleSystem: false }); + } + } else { + if (event.reply) event.reply(new Error("找不到节点")); + } + } else { + if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`)); + } + }, + + /** + * 控制节点的动画组件 (播放、暂停、停止等) + * @param {Object} event IPC 事件对象 + * @param {Object} args 参数 (action, nodeId, clipName) + */ + "manage-animation": function (event, args) { + const { action, nodeId, clipName } = args; + const node = findNode(nodeId); + + if (!node) { + if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`)); + return; + } + + const anim = node.getComponent(cc.Animation); + if (!anim) { + if (event.reply) event.reply(new Error(`在节点 ${nodeId} 上找不到 Animation 组件`)); + return; + } + + switch (action) { + case "get_list": + const clips = anim.getClips(); + const clipList = clips.map((c) => ({ + name: c.name, + duration: c.duration, + sample: c.sample, + speed: c.speed, + wrapMode: c.wrapMode, + })); + if (event.reply) event.reply(null, clipList); + break; + + case "get_info": + const currentClip = anim.currentClip; + let isPlaying = false; + // [安全修复] 只有在有当前 Clip 时才获取状态,避免 Animation 组件无 Clip 时的崩溃 + if (currentClip) { + const state = anim.getAnimationState(currentClip.name); + if (state) { + isPlaying = state.isPlaying; + } + } + const info = { + currentClip: currentClip ? currentClip.name : null, + clips: anim.getClips().map((c) => c.name), + playOnLoad: anim.playOnLoad, + isPlaying: isPlaying, + }; + if (event.reply) event.reply(null, info); + break; + + case "play": + if (!clipName) { + anim.play(); + if (event.reply) event.reply(null, "正在播放默认动画剪辑"); + } else { + anim.play(clipName); + if (event.reply) event.reply(null, `正在播放动画剪辑: ${clipName}`); + } + break; + + case "stop": + anim.stop(); + if (event.reply) event.reply(null, "动画已停止"); + break; + + case "pause": + anim.pause(); + if (event.reply) event.reply(null, "动画已暂停"); + break; + + case "resume": + anim.resume(); + if (event.reply) event.reply(null, "动画已恢复播放"); + break; + + default: + if (event.reply) event.reply(new Error(`未知的动画操作类型: ${action}`)); + break; + } + }, }; diff --git a/注意事项.md b/注意事项.md index 357982d..8b6b0ae 100644 --- a/注意事项.md +++ b/注意事项.md @@ -128,3 +128,20 @@ - **背景**:`Editor.Scene.callSceneScript` 的回调依赖 Scene 面板响应 IPC 消息。如果主线程阻塞,Scene 面板无法处理消息,导致 callback 永远不返回,HTTP 连接堆积。 - **解决方案**:所有 `callSceneScript` 调用均通过 `callSceneScriptWithTimeout` 包装,默认 15 秒超时。超时后自动返回错误,释放 HTTP 连接和队列位置。 - **日志标识**:超时会记录 `[超时] callSceneScript "方法名" 超过 15000ms 未响应`。 + +--- + +## 10. Token 消耗与长数据保护防爆机制 + +### 10.1 `get_scene_hierarchy` 深度与层级限制 +- **背景**:在一两千个节点的大型 UI 场景中,无限制地获取全场景树会瞬间消耗十万以上的 Token,导致 AI 丢失上下文甚至触发截断报错。 +- **最佳实践**: + - **默认使用 `depth: 2`** (默认限制) 来逐步探查。 + - **结合 `nodeId` 参数**:找到关键模块(例如 `Canvas/LoginPanel`)的 UUID 后,再单独向该 `nodeId` 请求下一层的结构,而非每次从根部拉取。 + +### 10.2 大对象与长数组截断 +- **背景**:在读取某些特定组件数据(如多边形顶点坐标、Sprite 曲线数据或序列化的内联 Base64 图片)时,JSON 可能会异常庞大。 +- **保护机制**: + - `scene-script.js` 内部在执行 `manage_components(get)` 序列化时,对于**长度超过 10 的 Array** 会强制截断,返回字面量字符串 `"[Array(X)]"`。 + - 对于**长度大于 200 的长字符串**,也会强制缩略并追加 `...[Truncated, total length: X]`。 +- **应对策略**:如果 AI 看到截断提示,这意味着此处为海量无语义数据,**请勿**尝试盲目通过 `update` 覆盖或还原被截断的字段,极易导致源数据被破坏。请仅修改自己能够完全看清的轻量级属性(如 `name`, `x`, `scale` 等)。