"use strict"; const { IpcManager } = require("./dist/IpcManager"); const http = require("http"); const path = require("path"); const fs = require("fs"); const crypto = require("crypto"); let logBuffer = []; // 存储所有日志 let mcpServer = null; let isSceneBusy = false; let serverConfig = { port: 3456, active: false, }; // 封装日志函数,同时发送给面板和编辑器控制台 function addLog(type, message) { const logEntry = { time: new Date().toLocaleTimeString(), type: type, content: message, }; logBuffer.push(logEntry); Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry); // 【修改】确保所有日志都输出到编辑器控制台,以便用户查看 if (type === "error") { Editor.error(`[MCP] ${message}`); } else if (type === "warn") { Editor.warn(`[MCP] ${message}`); } else { } } function getLogContent() { return logBuffer.map(entry => `[${entry.time}] [${entry.type}] ${entry.content}`).join('\n'); } const getNewSceneTemplate = () => { // 尝试获取 UUID 生成函数 let newId = ""; if (Editor.Utils && Editor.Utils.uuid) { newId = Editor.Utils.uuid(); } else if (Editor.Utils && Editor.Utils.UuidUtils && Editor.Utils.UuidUtils.uuid) { newId = Editor.Utils.UuidUtils.uuid(); } else { // 兜底方案:如果找不到编辑器 API,生成一个随机字符串 newId = Math.random().toString(36).substring(2, 15); } const sceneData = [ { __type__: "cc.SceneAsset", _name: "", _objFlags: 0, _native: "", scene: { __id__: 1 }, }, { __id__: 1, __type__: "cc.Scene", _name: "", _objFlags: 0, _parent: null, _children: [], _active: true, _level: 0, _components: [], autoReleaseAssets: false, _id: newId, }, ]; return JSON.stringify(sceneData); }; const getToolsList = () => { return [ { name: "get_selected_node", description: "获取当前编辑器中选中的节点 ID", inputSchema: { type: "object", properties: {} }, }, { name: "set_node_name", description: "修改指定节点的名称", inputSchema: { type: "object", properties: { id: { type: "string", description: "节点的 UUID" }, newName: { type: "string", description: "新的节点名称" }, }, required: ["id", "newName"], }, }, { name: "save_scene", description: "保存当前场景的修改", inputSchema: { type: "object", properties: {} }, }, { name: "get_scene_hierarchy", description: "获取当前场景的完整节点树结构(包括 UUID、名称和层级关系)", inputSchema: { type: "object", properties: {} }, }, { name: "update_node_transform", description: "修改节点的坐标、缩放或颜色", inputSchema: { type: "object", properties: { id: { type: "string", description: "节点 UUID" }, x: { type: "number" }, y: { type: "number" }, scaleX: { type: "number" }, scaleY: { type: "number" }, color: { type: "string", description: "HEX 颜色代码如 #FF0000" }, }, required: ["id"], }, }, { name: "create_scene", description: "在 assets 目录下创建一个新的场景文件", inputSchema: { type: "object", properties: { sceneName: { type: "string", description: "场景名称" }, }, required: ["sceneName"], }, }, { name: "create_prefab", description: "将场景中的某个节点保存为预制体资源", inputSchema: { type: "object", properties: { nodeId: { type: "string", description: "节点 UUID" }, prefabName: { type: "string", description: "预制体名称" }, }, required: ["nodeId", "prefabName"], }, }, { name: "open_scene", description: "打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒再进行节点创建或保存操作。", inputSchema: { type: "object", properties: { url: { type: "string", description: "场景资源路径,如 db://assets/NewScene.fire", }, }, required: ["url"], }, }, { name: "create_node", description: "在当前场景中创建一个新节点", inputSchema: { type: "object", properties: { name: { type: "string", description: "节点名称" }, parentId: { type: "string", description: "父节点 UUID (可选,不传则挂在场景根部)", }, type: { type: "string", enum: ["empty", "sprite", "label"], description: "节点预设类型", }, }, required: ["name"], }, }, { name: "manage_components", description: "管理节点组件", inputSchema: { type: "object", properties: { nodeId: { type: "string", description: "节点 UUID" }, action: { type: "string", enum: ["add", "remove", "update", "get"], description: "操作类型 (add: 添加组件, remove: 移除组件, update: 更新组件属性, get: 获取组件列表)" }, componentType: { type: "string", description: "组件类型,如 cc.Sprite (add/update 操作需要)" }, componentId: { type: "string", description: "组件 ID (remove/update 操作可选)" }, properties: { type: "object", description: "组件属性 (add/update 操作使用). 支持智能解析: 如果属性类型是组件但提供了节点UUID,会自动查找对应组件。" }, }, required: ["nodeId", "action"], }, }, { name: "manage_script", description: "管理脚本文件。注意:创建或修改脚本后,编辑器需要时间进行编译(通常几秒钟)。新脚本在编译完成前无法作为组件添加到节点。建议在 create 后调用 refresh_editor,或等待一段时间后再使用 manage_components。", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "delete", "read", "write"], description: "操作类型" }, path: { type: "string", description: "脚本路径,如 db://assets/scripts/NewScript.js" }, content: { type: "string", description: "脚本内容 (用于 create 和 write 操作)" }, name: { type: "string", description: "脚本名称 (用于 create 操作)" }, }, required: ["action", "path"], }, }, { name: "batch_execute", description: "批处理执行多个操作", inputSchema: { type: "object", properties: { operations: { type: "array", items: { type: "object", properties: { tool: { type: "string", description: "工具名称" }, params: { type: "object", description: "工具参数" }, }, required: ["tool", "params"], }, description: "操作列表", }, }, required: ["operations"], }, }, { name: "manage_asset", description: "管理资源", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "delete", "move", "get_info"], description: "操作类型" }, path: { type: "string", description: "资源路径,如 db://assets/textures" }, targetPath: { type: "string", description: "目标路径 (用于 move 操作)" }, content: { type: "string", description: "资源内容 (用于 create 操作)" }, }, required: ["action", "path"], }, }, { name: "scene_management", description: "场景管理", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "delete", "duplicate", "get_info"], description: "操作类型", }, path: { type: "string", description: "场景路径,如 db://assets/scenes/NewScene.fire" }, targetPath: { type: "string", description: "目标路径 (用于 duplicate 操作)" }, name: { type: "string", description: "场景名称 (用于 create 操作)" }, }, required: ["action", "path"], }, }, { name: "prefab_management", description: "预制体管理", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "update", "instantiate", "get_info"], description: "操作类型", }, path: { type: "string", description: "预制体路径,如 db://assets/prefabs/NewPrefab.prefab" }, nodeId: { type: "string", description: "节点 ID (用于 create 操作)" }, parentId: { type: "string", description: "父节点 ID (用于 instantiate 操作)" }, }, required: ["action", "path"], }, }, { name: "manage_editor", description: "管理编辑器", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["get_selection", "set_selection", "refresh_editor"], description: "操作类型", }, target: { type: "string", enum: ["node", "asset"], description: "目标类型 (用于 set_selection 操作)", }, properties: { type: "object", description: "操作属性" }, }, required: ["action"], }, }, { name: "find_gameobjects", description: "查找游戏对象", inputSchema: { type: "object", properties: { conditions: { type: "object", description: "查找条件" }, recursive: { type: "boolean", default: true, description: "是否递归查找" }, }, required: ["conditions"], }, }, { name: "manage_material", description: "管理材质", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "delete", "get_info"], description: "操作类型" }, path: { type: "string", description: "材质路径,如 db://assets/materials/NewMaterial.mat" }, properties: { type: "object", description: "材质属性" }, }, required: ["action", "path"], }, }, { name: "manage_texture", description: "管理纹理", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "delete", "get_info"], description: "操作类型" }, path: { type: "string", description: "纹理路径,如 db://assets/textures/NewTexture.png" }, properties: { type: "object", description: "纹理属性" }, }, required: ["action", "path"], }, }, { name: "execute_menu_item", description: "执行菜单项", inputSchema: { type: "object", properties: { menuPath: { type: "string", description: "菜单项路径" }, }, required: ["menuPath"], }, }, { name: "apply_text_edits", description: "应用文本编辑", inputSchema: { type: "object", properties: { filePath: { type: "string", description: "文件路径" }, edits: { type: "array", items: { type: "object" }, description: "编辑操作列表" }, }, required: ["filePath", "edits"], }, }, { name: "read_console", description: "读取控制台", inputSchema: { type: "object", properties: { limit: { type: "number", description: "输出限制" }, type: { type: "string", enum: ["log", "error", "warn"], description: "输出类型" }, }, }, }, { name: "validate_script", description: "验证脚本", inputSchema: { type: "object", properties: { filePath: { type: "string", description: "脚本路径" }, }, required: ["filePath"], }, }, { name: "find_in_file", description: "在项目中全局搜索文本内容", inputSchema: { type: "object", properties: { query: { type: "string", description: "搜索关键词" }, extensions: { type: "array", items: { type: "string" }, description: "文件后缀列表 (例如 ['.js', '.ts'])", default: [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"] }, includeSubpackages: { type: "boolean", default: true, description: "是否搜索子包 (暂时默认搜索 assets 目录)" } }, required: ["query"] } }, { name: "manage_undo", description: "管理编辑器的撤销和重做历史", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["undo", "redo", "begin_group", "end_group", "cancel_group"], description: "操作类型" }, description: { type: "string", description: "撤销组的描述 (用于 begin_group)" } }, required: ["action"] } }, { name: "manage_vfx", description: "管理全场景特效 (粒子系统)", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["create", "update", "get_info"], description: "操作类型" }, nodeId: { type: "string", description: "节点 UUID (用于 update/get_info)" }, properties: { type: "object", description: "粒子系统属性 (用于 create/update)", properties: { duration: { type: "number", description: "发射时长" }, emissionRate: { type: "number", description: "发射速率" }, life: { type: "number", description: "生命周期" }, lifeVar: { type: "number", description: "生命周期变化" }, startColor: { type: "string", description: "起始颜色 (Hex)" }, endColor: { type: "string", description: "结束颜色 (Hex)" }, startSize: { type: "number", description: "起始大小" }, endSize: { type: "number", description: "结束大小" }, speed: { type: "number", description: "速度" }, angle: { type: "number", description: "角度" }, gravity: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } }, file: { type: "string", description: "粒子文件路径 (plist) 或 texture 路径" } } }, name: { type: "string", description: "节点名称 (用于 create)" }, parentId: { type: "string", description: "父节点 ID (用于 create)" } }, required: ["action"] } }, { name: "get_sha", description: "获取指定文件的 SHA-256 哈希值", inputSchema: { type: "object", properties: { path: { type: "string", description: "文件路径,如 db://assets/scripts/Test.ts" } }, required: ["path"] } }, { name: "manage_animation", description: "管理节点的动画组件", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["get_list", "get_info", "play", "stop", "pause", "resume"], description: "操作类型" }, nodeId: { type: "string", description: "节点 UUID" }, clipName: { type: "string", description: "动画剪辑名称 (用于 play)" } }, required: ["action", "nodeId"] } } ]; }; module.exports = { "scene-script": "scene-script.js", load() { addLog("info", "MCP Bridge Plugin Loaded"); // 读取配置 let profile = this.getProfile(); serverConfig.port = profile.get("last-port") || 3456; let autoStart = profile.get("auto-start"); if (autoStart) { addLog("info", "Auto-start is enabled. Initializing server..."); // 延迟一点启动,确保编辑器环境完全就绪 setTimeout(() => { this.startServer(serverConfig.port); }, 1000); } }, // 获取配置文件的辅助函数 getProfile() { // 'local' 表示存储在项目本地(local/mcp-bridge.json) return Editor.Profile.load("profile://local/mcp-bridge.json", "mcp-bridge"); }, unload() { this.stopServer(); }, startServer(port) { if (mcpServer) this.stopServer(); try { mcpServer = http.createServer((req, res) => { res.setHeader("Content-Type", "application/json"); res.setHeader("Access-Control-Allow-Origin", "*"); let body = ""; req.on("data", (chunk) => { body += chunk; }); req.on("end", () => { const url = req.url; if (url === "/list-tools") { const tools = getToolsList(); addLog("info", `AI Client requested tool list`); // 明确返回成功结构 res.writeHead(200); return res.end(JSON.stringify({ tools: tools })); res.writeHead(200); return res.end(JSON.stringify({ tools: tools })); } if (url === "/list-resources") { const resources = this.getResourcesList(); addLog("info", `AI Client requested resource list`); res.writeHead(200); return res.end(JSON.stringify({ resources: resources })); } if (url === "/read-resource") { try { const { uri } = JSON.parse(body || "{}"); addLog("mcp", `READ -> [${uri}]`); this.handleReadResource(uri, (err, content) => { if (err) { addLog("error", `读取失败: ${err}`); res.writeHead(500); return res.end(JSON.stringify({ error: err })); } addLog("success", `读取成功: ${uri}`); res.writeHead(200); // 返回 MCP Resource 格式: { contents: [{ uri, mimeType, text }] } res.end(JSON.stringify({ contents: [{ uri: uri, mimeType: "application/json", text: typeof content === 'string' ? content : JSON.stringify(content) }] })); }); } catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); } return; } if (url === "/call-tool") { try { const { name, arguments: args } = JSON.parse(body || "{}"); addLog("mcp", `REQ -> [${name}]`); this.handleMcpCall(name, args, (err, result) => { const response = { content: [ { type: "text", text: err ? `Error: ${err}` : typeof result === "object" ? JSON.stringify(result, null, 2) : result, }, ], }; if (err) { addLog("error", `RES <- [${name}] 失败: ${err}`); } else { // 成功时尝试捕获简单的结果预览(如果是字符串或简短对象) let preview = ""; if (typeof result === 'string') { preview = result.length > 100 ? result.substring(0, 100) + "..." : result; } else if (typeof result === 'object') { try { const jsonStr = JSON.stringify(result); preview = jsonStr.length > 100 ? jsonStr.substring(0, 100) + "..." : jsonStr; } catch (e) { preview = "Object (Circular/Unserializable)"; } } addLog("success", `RES <- [${name}] 成功 : ${preview}`); } res.writeHead(200); res.end(JSON.stringify(response)); }); } catch (e) { if (e instanceof SyntaxError) { addLog("error", `JSON Parse Error: ${e.message}`); res.writeHead(400); res.end(JSON.stringify({ error: "Invalid JSON" })); } else { addLog("error", `Internal Server Error: ${e.message}`); res.writeHead(500); res.end(JSON.stringify({ error: e.message })); } } return; } // --- 兜底处理 (404) --- res.writeHead(404); res.end(JSON.stringify({ error: "Not Found", url: url })); }); }); mcpServer.on("error", (e) => { addLog("error", `Server Error: ${e.message}`); }); mcpServer.listen(port, () => { serverConfig.active = true; addLog("success", `MCP Server running at http://127.0.0.1:${port}`); Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig); }); // 启动成功后顺便存一下端口 this.getProfile().set("last-port", port); this.getProfile().save(); } catch (e) { addLog("error", `Failed to start server: ${e.message}`); } }, stopServer() { if (mcpServer) { mcpServer.close(); mcpServer = null; serverConfig.active = false; addLog("warn", "MCP Server stopped"); Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig); } }, getResourcesList() { return [ { uri: "cocos://hierarchy", name: "Scene Hierarchy", description: "当前场景层级的 JSON 快照", mimeType: "application/json" }, { uri: "cocos://selection", name: "Current Selection", description: "当前选中节点的 UUID 列表", mimeType: "application/json" }, { uri: "cocos://logs/latest", name: "Editor Logs", description: "最新的编辑器日志 (内存缓存)", mimeType: "text/plain" } ]; }, handleReadResource(uri, callback) { let parsed; try { parsed = new URL(uri); } catch (e) { return callback(`Invalid URI: ${uri}`); } if (parsed.protocol !== "cocos:") { return callback(`Unsupported protocol: ${parsed.protocol}`); } const type = parsed.hostname; // hierarchy, selection, logs switch (type) { case "hierarchy": // 注意: query-hierarchy 是异步的 Editor.Ipc.sendToPanel("scene", "scene:query-hierarchy", (err, sceneId, hierarchy) => { if (err) return callback(err); callback(null, JSON.stringify(hierarchy, null, 2)); }); break; case "selection": const selection = Editor.Selection.curSelection("node"); callback(null, JSON.stringify(selection)); break; case "logs": callback(null, getLogContent()); break; default: callback(`Resource not found: ${uri}`); break; } }, handleMcpCall(name, args, callback) { if (isSceneBusy && (name === "save_scene" || name === "create_node")) { return callback("Editor is busy (Processing Scene), please wait a moment."); } switch (name) { case "get_selected_node": const ids = Editor.Selection.curSelection("node"); callback(null, ids); break; case "set_node_name": // 使用 scene:set-property 以支持撤销 Editor.Ipc.sendToPanel("scene", "scene:set-property", { id: args.id, path: "name", type: "String", value: args.newName, isSubProp: false }); callback(null, `Node name updated to ${args.newName}`); break; case "save_scene": isSceneBusy = true; addLog("info", "Preparing to save scene... Waiting for UI sync."); Editor.Ipc.sendToPanel("scene", "scene:stash-and-save"); isSceneBusy = false; addLog("info", "Safe Save completed."); callback(null, "Scene saved successfully."); break; case "get_scene_hierarchy": Editor.Scene.callSceneScript("mcp-bridge", "get-hierarchy", callback); break; case "update_node_transform": // 直接调用场景脚本更新属性,绕过可能导致 "Unknown object" 的复杂 Undo 系统 Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", args, (err, result) => { if (err) { addLog("error", `Transform update failed: ${err}`); callback(err); } else { callback(null, "Transform updated"); } }); break; case "create_scene": const sceneUrl = `db://assets/${args.sceneName}.fire`; if (Editor.assetdb.exists(sceneUrl)) { return callback("Scene already exists"); } Editor.assetdb.create(sceneUrl, getNewSceneTemplate(), (err) => { callback(err, err ? null : `Standard Scene created at ${sceneUrl}`); }); break; case "create_prefab": const prefabUrl = `db://assets/${args.prefabName}.prefab`; Editor.Ipc.sendToMain("scene:create-prefab", args.nodeId, prefabUrl); callback(null, `Command sent: Creating prefab '${args.prefabName}'`); break; case "open_scene": isSceneBusy = true; // 锁定 const openUuid = Editor.assetdb.urlToUuid(args.url); if (openUuid) { Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid); setTimeout(() => { isSceneBusy = false; callback(null, `Success: Opening scene ${args.url}`); }, 2000); } else { isSceneBusy = false; callback(`Could not find asset with URL ${args.url}`); } break; case "create_node": Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback); break; case "manage_components": Editor.Scene.callSceneScript("mcp-bridge", "manage-components", args, callback); break; case "manage_script": this.manageScript(args, callback); break; case "batch_execute": this.batchExecute(args, callback); break; case "manage_asset": this.manageAsset(args, callback); break; case "scene_management": this.sceneManagement(args, callback); break; case "prefab_management": this.prefabManagement(args, callback); break; case "manage_editor": this.manageEditor(args, callback); break; case "get_sha": this.getSha(args, callback); break; case "manage_animation": this.manageAnimation(args, callback); break; case "find_gameobjects": Editor.Scene.callSceneScript("mcp-bridge", "find-gameobjects", args, callback); break; case "manage_material": this.manageMaterial(args, callback); break; case "manage_texture": this.manageTexture(args, callback); break; case "execute_menu_item": this.executeMenuItem(args, callback); break; case "apply_text_edits": this.applyTextEdits(args, callback); break; case "read_console": this.readConsole(args, callback); break; case "validate_script": this.validateScript(args, callback); break; case "find_in_file": this.findInFile(args, callback); break; case "manage_undo": this.manageUndo(args, callback); break; case "manage_vfx": // 【修复】在主进程预先解析 URL 为 UUID,因为渲染进程(scene-script)无法访问 Editor.assetdb if (args.properties && args.properties.file) { if (typeof args.properties.file === 'string' && args.properties.file.startsWith("db://")) { const uuid = Editor.assetdb.urlToUuid(args.properties.file); if (uuid) { args.properties.file = uuid; // 替换为 UUID } else { console.warn(`Failed to resolve path to UUID: ${args.properties.file}`); } } } // 预先获取默认贴图 UUID (尝试多个可能的路径) 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) { args.defaultSpriteUuid = uuid; addLog("info", `[mcp-bridge] Resolved Default Sprite UUID: ${uuid} from ${path}`); break; } } if (!args.defaultSpriteUuid) { addLog("warn", "[mcp-bridge] Failed to resolve any default sprite UUID."); } Editor.Scene.callSceneScript("mcp-bridge", "manage-vfx", args, callback); break; default: callback(`Unknown tool: ${name}`); break; } }, // 管理脚本文件 manageScript(args, callback) { const { action, path: scriptPath, content } = args; switch (action) { case "create": if (Editor.assetdb.exists(scriptPath)) { return callback(`Script already exists at ${scriptPath}`); } // 确保父目录存在 const absolutePath = Editor.assetdb.urlToFspath(scriptPath); const dirPath = path.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } Editor.assetdb.create( scriptPath, content || `const { ccclass, property } = cc._decorator; @ccclass export default class NewScript extends cc.Component { @property(cc.Label) label: cc.Label = null; @property text: string = 'hello'; // LIFE-CYCLE CALLBACKS: onLoad () {} start () {} update (dt) {} }`, (err) => { if (err) { callback(err); } else { // 【关键修复】创建脚本后,必须刷新 AssetDB 并等待完成, // 否则后续立即挂载脚本的操作(manage_components)会因找不到脚本 UUID 而失败。 Editor.assetdb.refresh(scriptPath, (refreshErr) => { if (refreshErr) { addLog("warn", `Refresh failed after script creation: ${refreshErr}`); } callback(null, `Script created at ${scriptPath}`); }); } }, ); break; case "delete": if (!Editor.assetdb.exists(scriptPath)) { return callback(`Script not found at ${scriptPath}`); } Editor.assetdb.delete([scriptPath], (err) => { callback(err, err ? null : `Script deleted at ${scriptPath}`); }); break; case "read": // 使用 fs 读取,绕过 assetdb.loadAny const readFsPath = Editor.assetdb.urlToFspath(scriptPath); if (!readFsPath || !fs.existsSync(readFsPath)) { return callback(`Script not found at ${scriptPath}`); } try { const content = fs.readFileSync(readFsPath, "utf-8"); callback(null, content); } catch (e) { callback(`Failed to read script: ${e.message}`); } break; case "write": // 使用 fs 写入 + refresh,确保覆盖成功 const writeFsPath = Editor.assetdb.urlToFspath(scriptPath); if (!writeFsPath) { return callback(`Invalid path: ${scriptPath}`); } try { fs.writeFileSync(writeFsPath, content, "utf-8"); Editor.assetdb.refresh(scriptPath, (err) => { if (err) addLog("warn", `Refresh failed after write: ${err}`); callback(null, `Script updated at ${scriptPath}`); }); } catch (e) { callback(`Failed to write script: ${e.message}`); } break; default: callback(`Unknown script action: ${action}`); break; } }, // 批处理执行 batchExecute(args, callback) { const { operations } = args; const results = []; let completed = 0; if (!operations || operations.length === 0) { return callback("No operations provided"); } operations.forEach((operation, index) => { this.handleMcpCall(operation.tool, operation.params, (err, result) => { results[index] = { tool: operation.tool, error: err, result: result }; completed++; if (completed === operations.length) { callback(null, results); } }); }); }, // 管理资源 manageAsset(args, callback) { const { action, path, targetPath, content } = args; switch (action) { case "create": if (Editor.assetdb.exists(path)) { return callback(`Asset already exists at ${path}`); } // 确保父目录存在 const fs = require("fs"); const pathModule = require("path"); const absolutePath = Editor.assetdb.urlToFspath(path); const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } Editor.assetdb.create(path, content || "", (err) => { callback(err, err ? null : `Asset created at ${path}`); }); break; case "delete": if (!Editor.assetdb.exists(path)) { return callback(`Asset not found at ${path}`); } Editor.assetdb.delete([path], (err) => { callback(err, err ? null : `Asset deleted at ${path}`); }); break; case "move": if (!Editor.assetdb.exists(path)) { return callback(`Asset not found at ${path}`); } if (Editor.assetdb.exists(targetPath)) { return callback(`Target asset already exists at ${targetPath}`); } Editor.assetdb.move(path, targetPath, (err) => { callback(err, err ? null : `Asset moved from ${path} to ${targetPath}`); }); break; case "get_info": try { if (!Editor.assetdb.exists(path)) { return callback(`Asset not found: ${path}`); } const uuid = Editor.assetdb.urlToUuid(path); const info = Editor.assetdb.assetInfoByUuid(uuid); if (info) { callback(null, info); } else { // Fallback if API returns nothing but asset exists callback(null, { url: path, uuid: uuid, exists: true }); } } catch (e) { callback(`Error getting asset info: ${e.message}`); } break; default: callback(`Unknown asset action: ${action}`); break; } }, // 场景管理 sceneManagement(args, callback) { const { action, path, targetPath, name } = args; switch (action) { case "create": if (Editor.assetdb.exists(path)) { return callback(`Scene already exists at ${path}`); } // 确保父目录存在 const fs = require("fs"); const pathModule = require("path"); const absolutePath = Editor.assetdb.urlToFspath(path); const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } Editor.assetdb.create(path, getNewSceneTemplate(), (err) => { callback(err, err ? null : `Scene created at ${path}`); }); break; case "delete": if (!Editor.assetdb.exists(path)) { return callback(`Scene not found at ${path}`); } Editor.assetdb.delete([path], (err) => { callback(err, err ? null : `Scene deleted at ${path}`); }); break; case "duplicate": if (!Editor.assetdb.exists(path)) { return callback(`Scene not found at ${path}`); } if (!targetPath) { return callback(`Target path is required for duplicate operation`); } if (Editor.assetdb.exists(targetPath)) { return callback(`Target scene already exists at ${targetPath}`); } // 读取原场景内容 Editor.assetdb.loadAny(path, (err, content) => { if (err) { return callback(`Failed to read scene: ${err}`); } // 确保目标目录存在 const fs = require("fs"); const pathModule = require("path"); const targetAbsolutePath = Editor.assetdb.urlToFspath(targetPath); const targetDirPath = pathModule.dirname(targetAbsolutePath); if (!fs.existsSync(targetDirPath)) { fs.mkdirSync(targetDirPath, { recursive: true }); } // 创建复制的场景 Editor.assetdb.create(targetPath, content, (err) => { callback(err, err ? null : `Scene duplicated from ${path} to ${targetPath}`); }); }); break; case "get_info": if (Editor.assetdb.exists(path)) { const uuid = Editor.assetdb.urlToUuid(path); const info = Editor.assetdb.assetInfoByUuid(uuid); callback(null, info || { url: path, uuid: uuid, exists: true }); } else { callback(`Scene not found: ${path}`); } break; default: callback(`Unknown scene action: ${action}`); break; } }, // 预制体管理 prefabManagement(args, callback) { const { action, path: prefabPath, nodeId, parentId } = args; switch (action) { case "create": if (!nodeId) { return callback(`Node ID is required for create operation`); } if (Editor.assetdb.exists(prefabPath)) { return callback(`Prefab already exists at ${prefabPath}`); } // 确保父目录存在 const absolutePath = Editor.assetdb.urlToFspath(prefabPath); const dirPath = path.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } // 解析目标目录和文件名 // db://assets/folder/PrefabName.prefab -> db://assets/folder, PrefabName const targetDir = prefabPath.substring(0, prefabPath.lastIndexOf('/')); const fileName = prefabPath.substring(prefabPath.lastIndexOf('/') + 1); const prefabName = fileName.replace('.prefab', ''); // 1. 重命名节点以匹配预制体名称 Editor.Ipc.sendToPanel("scene", "scene:set-property", { id: nodeId, path: "name", type: "String", value: prefabName, isSubProp: false }); // 2. 发送创建命令 (参数: [uuids], dirPath) // 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径 setTimeout(() => { Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir); }, 100); // 稍微延迟以确保重命名生效 callback(null, `Command sent: Creating prefab from node ${nodeId} at ${targetDir} as ${prefabName}`); break; case "update": if (!nodeId) { return callback(`Node ID is required for update operation`); } if (!Editor.assetdb.exists(prefabPath)) { return callback(`Prefab not found at ${prefabPath}`); } // 更新预制体 Editor.Ipc.sendToPanel("scene", "scene:update-prefab", nodeId, prefabPath); callback(null, `Command sent: Updating prefab ${prefabPath} from node ${nodeId}`); break; case "instantiate": if (!Editor.assetdb.exists(prefabPath)) { return callback(`Prefab not found at ${prefabPath}`); } // 实例化预制体 const prefabUuid = Editor.assetdb.urlToUuid(prefabPath); Editor.Scene.callSceneScript( "mcp-bridge", "instantiate-prefab", { prefabUuid: prefabUuid, parentId: parentId, }, callback, ); break; case "get_info": if (Editor.assetdb.exists(prefabPath)) { const uuid = Editor.assetdb.urlToUuid(prefabPath); const info = Editor.assetdb.assetInfoByUuid(uuid); // 确保返回对象包含 exists: true,以满足测试验证 const result = info || { url: prefabPath, uuid: uuid }; result.exists = true; callback(null, result); } else { callback(`Prefab not found: ${prefabPath}`); } break; default: callback(`Unknown prefab action: ${action}`); } }, // 管理编辑器 manageEditor(args, callback) { const { action, target, properties } = args; switch (action) { case "get_selection": // 获取当前选中的资源或节点 const nodeSelection = Editor.Selection.curSelection("node"); const assetSelection = Editor.Selection.curSelection("asset"); callback(null, { nodes: nodeSelection, assets: assetSelection, }); break; case "set_selection": // 设置选中状态 if (target === "node" && properties.nodes) { Editor.Selection.select("node", properties.nodes); } else if (target === "asset" && properties.assets) { Editor.Selection.select("asset", properties.assets); } callback(null, "Selection updated"); break; case "refresh_editor": // 刷新编辑器 const refreshPath = (properties && properties.path) ? properties.path : 'db://assets/scripts'; Editor.assetdb.refresh(refreshPath, (err) => { if (err) { addLog("error", `Refresh failed: ${err}`); callback(err); } else { callback(null, `Editor refreshed: ${refreshPath}`); } }); break; default: callback("Unknown action"); break; } }, // 管理材质 manageMaterial(args, callback) { const { action, path, properties } = args; switch (action) { case "create": if (Editor.assetdb.exists(path)) { return callback(`Material already exists at ${path}`); } // 确保父目录存在 const fs = require("fs"); const pathModule = require("path"); const absolutePath = Editor.assetdb.urlToFspath(path); const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } // 创建材质资源 const materialContent = JSON.stringify({ __type__: "cc.Material", _name: "", _objFlags: 0, _native: "", effects: [ { technique: 0, defines: {}, uniforms: properties.uniforms || {}, }, ], }); Editor.assetdb.create(path, materialContent, (err) => { callback(err, err ? null : `Material created at ${path}`); }); break; case "delete": if (!Editor.assetdb.exists(path)) { return callback(`Material not found at ${path}`); } Editor.assetdb.delete([path], (err) => { callback(err, err ? null : `Material deleted at ${path}`); }); break; case "get_info": if (Editor.assetdb.exists(path)) { const uuid = Editor.assetdb.urlToUuid(path); const info = Editor.assetdb.assetInfoByUuid(uuid); callback(null, info || { url: path, uuid: uuid, exists: true }); } else { callback(`Material not found: ${path}`); } break; default: callback(`Unknown material action: ${action}`); break; } }, // 管理纹理 manageTexture(args, callback) { const { action, path, properties } = args; switch (action) { case "create": if (Editor.assetdb.exists(path)) { return callback(`Texture already exists at ${path}`); } // 确保父目录存在 const fs = require("fs"); const pathModule = require("path"); const absolutePath = Editor.assetdb.urlToFspath(path); const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } // 创建纹理资源(简化版,实际需要处理纹理文件) const textureContent = JSON.stringify({ __type__: "cc.Texture2D", _name: "", _objFlags: 0, _native: properties.native || "", width: properties.width || 128, height: properties.height || 128, }); Editor.assetdb.create(path, textureContent, (err) => { callback(err, err ? null : `Texture created at ${path}`); }); break; case "delete": if (!Editor.assetdb.exists(path)) { return callback(`Texture not found at ${path}`); } Editor.assetdb.delete([path], (err) => { callback(err, err ? null : `Texture deleted at ${path}`); }); break; case "get_info": if (Editor.assetdb.exists(path)) { const uuid = Editor.assetdb.urlToUuid(path); const info = Editor.assetdb.assetInfoByUuid(uuid); callback(null, info || { url: path, uuid: uuid, exists: true }); } else { callback(`Texture not found: ${path}`); } break; default: callback(`Unknown texture action: ${action}`); break; } }, // 应用文本编辑 applyTextEdits(args, callback) { const { filePath, edits } = args; // 1. 获取文件系统路径 const fspath = Editor.assetdb.urlToFspath(filePath); if (!fspath) { return callback(`File not found or invalid URL: ${filePath}`); } const fs = require("fs"); if (!fs.existsSync(fspath)) { return callback(`File does not exist: ${fspath}`); } try { // 2. 读取 let updatedContent = fs.readFileSync(fspath, "utf-8"); // 3. 应用编辑 // 必须按倒序应用编辑,否则后续编辑的位置会偏移 (假设edits未排序,这里简单处理,实际上LSP通常建议客户端倒序应用或计算偏移) // 这里假设edits已经按照位置排序或者用户负责,如果需要严谨,应先按 start/position 倒序排序 // 简单排序保险: const sortedEdits = [...edits].sort((a, b) => { const posA = a.position !== undefined ? a.position : a.start; const posB = b.position !== undefined ? b.position : b.start; return posB - posA; // 从大到小 }); sortedEdits.forEach((edit) => { switch (edit.type) { case "insert": updatedContent = updatedContent.slice(0, edit.position) + edit.text + updatedContent.slice(edit.position); break; case "delete": updatedContent = updatedContent.slice(0, edit.start) + updatedContent.slice(edit.end); break; case "replace": updatedContent = updatedContent.slice(0, edit.start) + edit.text + updatedContent.slice(edit.end); break; } }); // 4. 写入 fs.writeFileSync(fspath, updatedContent, "utf-8"); // 5. 通知编辑器资源变化 (重要) Editor.assetdb.refresh(filePath, (err) => { if (err) addLog("warn", `Refresh failed for ${filePath}: ${err}`); callback(null, `Text edits applied to ${filePath}`); }); } catch (err) { callback(`Action failed: ${err.message}`); } }, // 读取控制台 readConsole(args, callback) { const { limit, type } = args; let filteredOutput = logBuffer; if (type) { filteredOutput = filteredOutput.filter((item) => item.type === type); } if (limit) { filteredOutput = filteredOutput.slice(-limit); } callback(null, filteredOutput); }, executeMenuItem(args, callback) { const { menuPath } = args; if (!menuPath) { return callback("Menu path is required"); } addLog("info", `Executing Menu Item: ${menuPath}`); // 菜单项映射表 (Cocos Creator 2.4.x IPC) // 参考: IPC_MESSAGES.md const menuMap = { 'File/New Scene': 'scene:new-scene', 'File/Save Scene': 'scene:stash-and-save', 'File/Save': 'scene:stash-and-save', // 别名 'Edit/Undo': 'scene:undo', 'Edit/Redo': 'scene:redo', 'Edit/Delete': 'scene:delete-selected', 'Delete': 'scene:delete-selected', 'delete': 'scene:delete-selected', 'Node/Create Empty Node': 'scene:create-node-by-classid', // 简化的映射,通常需要参数 'Project/Build': 'app:build-project', }; // 特殊处理 delete-node:UUID 格式 if (menuPath.startsWith("delete-node:")) { const uuid = menuPath.split(":")[1]; if (uuid) { Editor.Scene.callSceneScript('mcp-bridge', 'delete-node', { uuid }, (err, result) => { if (err) callback(err); else callback(null, result || `Node ${uuid} deleted via scene script`); }); return; } } if (menuMap[menuPath]) { const ipcMsg = menuMap[menuPath]; try { Editor.Ipc.sendToMain(ipcMsg); callback(null, `Menu action triggered: ${menuPath} -> ${ipcMsg}`); } catch (err) { callback(`Failed to execute IPC ${ipcMsg}: ${err.message}`); } } else { // 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效) // 或者直接返回不支持的警告 addLog("warn", `Menu item '${menuPath}' not found in supported map. Trying legacy fallback.`); // 尝试通用调用 try { // 注意:Cocos Creator 2.x 的 menu:click 通常需要 Electron 菜单 ID,而不只是路径 // 这里做个尽力而为的尝试 Editor.Ipc.sendToMain('menu:click', menuPath); callback(null, `Generic menu action sent: ${menuPath} (Success guaranteed only for supported items)`); } catch (e) { callback(`Failed to execute menu item: ${menuPath}`); } } }, // 验证脚本 validateScript(args, callback) { const { filePath } = args; // 1. 获取文件系统路径 const fspath = Editor.assetdb.urlToFspath(filePath); if (!fspath) { return callback(`File not found or invalid URL: ${filePath}`); } // 2. 检查文件是否存在 const fs = require("fs"); if (!fs.existsSync(fspath)) { return callback(`File does not exist: ${fspath}`); } // 3. 读取内容并验证 try { const content = fs.readFileSync(fspath, "utf-8"); // 检查空文件 if (!content || content.trim().length === 0) { return callback(null, { valid: false, message: "Script is empty" }); } // 对于 JavaScript 脚本,使用 Function 构造器进行语法验证 if (filePath.endsWith(".js")) { const wrapper = `(function() { ${content} })`; try { new Function(wrapper); callback(null, { valid: true, message: "JavaScript syntax is valid" }); } catch (syntaxErr) { return callback(null, { valid: false, message: syntaxErr.message }); } } // 对于 TypeScript,由于没有内置 TS 编译器,我们进行基础的"防呆"检查 // 并明确告知用户无法进行完整编译验证 else if (filePath.endsWith(".ts")) { // 简单的正则表达式检查:是否有非法字符或明显错误结构 (示例) // 这里暂时只做简单的括号匹配检查或直接通过,但给出一个 Warning // 检查是否有 class 定义 (简单的启发式检查) if (!content.includes('class ') && !content.includes('interface ') && !content.includes('enum ') && !content.includes('export ')) { return callback(null, { valid: true, message: "Warning: TypeScript file seems to lack standard definitions (class/interface), but basic syntax check is skipped due to missing compiler." }); } callback(null, { valid: true, message: "TypeScript basic check passed. (Full compilation validation requires editor build)" }); } else { callback(null, { valid: true, message: "Unknown script type, validation skipped." }); } } catch (err) { callback(null, { valid: false, message: `Read Error: ${err.message}` }); } }, // 暴露给 MCP 或面板的 API 封装 messages: { "scan-ipc-messages"(event) { try { const msgs = IpcManager.getIpcMessages(); if (event.reply) event.reply(null, msgs); } catch (e) { if (event.reply) event.reply(e.message); } }, "test-ipc-message"(event, args) { const { name, params } = args; IpcManager.testIpcMessage(name, params).then((result) => { if (event.reply) event.reply(null, result); }); }, "open-test-panel"() { Editor.Panel.open("mcp-bridge"); }, "toggle-server"(event, port) { if (serverConfig.active) this.stopServer(); else this.startServer(port); }, "clear-logs"() { logBuffer = []; addLog("info", "Logs cleared"); }, // 修改场景中的节点(需要通过 scene-script) "set-node-property"(event, args) { addLog("mcp", `Creating node: ${args.name} (${args.type})`); // 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致 Editor.Scene.callSceneScript("mcp-bridge", "set-property", args, (err, result) => { if (err) { Editor.error("Scene Script Error:", err); } if (event && event.reply) { event.reply(err, result); } }); }, "create-node"(event, args) { addLog("mcp", `Creating node: ${args.name} (${args.type})`); Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => { if (err) addLog("error", `CreateNode Failed: ${err}`); else addLog("success", `Node Created: ${result}`); event.reply(err, result); }); }, "get-server-state"(event) { let profile = this.getProfile(); event.reply(null, { config: serverConfig, logs: logBuffer, autoStart: profile.get("auto-start"), // 返回自动启动状态 }); }, "set-auto-start"(event, value) { this.getProfile().set("auto-start", value); this.getProfile().save(); addLog("info", `Auto-start set to: ${value}`); }, "inspect-apis"() { addLog("info", "[API Inspector] Starting DEEP inspection..."); // 获取函数参数的辅助函数 const getArgs = (func) => { try { const str = func.toString(); const match = str.match(/function\s.*?\(([^)]*)\)/) || str.match(/.*?\(([^)]*)\)/); if (match) { return match[1].split(",").map(arg => arg.trim()).filter(a => a).join(", "); } return `${func.length} args`; } catch (e) { return "?"; } }; // 检查对象的辅助函数 const inspectObj = (name, obj) => { if (!obj) return { name, exists: false }; const props = {}; const proto = Object.getPrototypeOf(obj); // 组合自身属性和原型属性 const allKeys = new Set([...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertyNames(proto || {})]); allKeys.forEach(key => { if (key.startsWith("_")) return; // 跳过私有属性 try { const val = obj[key]; if (typeof val === 'function') { props[key] = `func(${getArgs(val)})`; } else { props[key] = typeof val; } } catch (e) { } }); return { name, exists: true, props }; }; // 1. 检查标准对象 const standardObjects = { "Editor.assetdb": Editor.assetdb, "Editor.Selection": Editor.Selection, "Editor.Ipc": Editor.Ipc, "Editor.Panel": Editor.Panel, "Editor.Scene": Editor.Scene, "Editor.Utils": Editor.Utils, "Editor.remote": Editor.remote }; const report = {}; Object.keys(standardObjects).forEach(key => { report[key] = inspectObj(key, standardObjects[key]); }); // 2. 检查特定论坛提到的 API const forumChecklist = [ "Editor.assetdb.queryInfoByUuid", "Editor.assetdb.assetInfoByUuid", "Editor.assetdb.move", "Editor.assetdb.createOrSave", "Editor.assetdb.delete", "Editor.assetdb.urlToUuid", "Editor.assetdb.uuidToUrl", "Editor.assetdb.fspathToUrl", "Editor.assetdb.urlToFspath", "Editor.remote.assetdb.uuidToUrl", "Editor.Selection.select", "Editor.Selection.clear", "Editor.Selection.curSelection", "Editor.Selection.curGlobalActivate" ]; const checklistResults = {}; forumChecklist.forEach(path => { const parts = path.split("."); let curr = global; // 在主进程中,Editor 是全局的 let exists = true; for (const part of parts) { if (curr && curr[part]) { curr = curr[part]; } else { exists = false; break; } } checklistResults[path] = exists ? (typeof curr === 'function' ? `Available(${getArgs(curr)})` : "Available") : "Missing"; }); addLog("info", `[API Inspector] Standard Objects:\n${JSON.stringify(report, null, 2)}`); addLog("info", `[API Inspector] Forum Checklist:\n${JSON.stringify(checklistResults, null, 2)}`); // 3. 检查内置包 IPC 消息 const ipcReport = {}; const builtinPackages = ["scene", "builder", "assets"]; // 核心内置包 const fs = require("fs"); builtinPackages.forEach(pkgName => { try { const pkgPath = Editor.url(`packages://${pkgName}/package.json`); if (pkgPath && fs.existsSync(pkgPath)) { const pkgData = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); if (pkgData.messages) { ipcReport[pkgName] = Object.keys(pkgData.messages); } else { ipcReport[pkgName] = "No messages defined"; } } else { ipcReport[pkgName] = "Package path not found"; } } catch (e) { ipcReport[pkgName] = `Error: ${e.message}`; } }); addLog("info", `[API Inspector] Built-in IPC Messages:\n${JSON.stringify(ipcReport, null, 2)}`); }, }, // 全局文件搜索 findInFile(args, callback) { const { query, extensions, includeSubpackages } = args; const fs = require('fs'); const path = require('path'); const assetsPath = Editor.assetdb.urlToFspath("db://assets"); const validExtensions = extensions || [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"]; const results = []; const MAX_RESULTS = 500; // 限制返回结果数量,防止溢出 try { // 递归遍历函数 const walk = (dir) => { if (results.length >= MAX_RESULTS) return; const list = fs.readdirSync(dir); list.forEach((file) => { if (results.length >= MAX_RESULTS) return; // 忽略隐藏文件和 node_modules if (file.startsWith('.') || file === 'node_modules' || file === 'bin' || file === 'local') return; const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat && stat.isDirectory()) { walk(filePath); } else { // 检查后缀 const ext = path.extname(file).toLowerCase(); if (validExtensions.includes(ext)) { try { const content = fs.readFileSync(filePath, 'utf8'); // 简单的行匹配 const lines = content.split('\n'); lines.forEach((line, index) => { if (results.length >= MAX_RESULTS) return; if (line.includes(query)) { // 转换为项目相对路径 (db://assets/...) const relativePath = path.relative(assetsPath, filePath); // 统一使用 forward slash const dbPath = "db://assets/" + relativePath.split(path.sep).join('/'); results.push({ filePath: dbPath, line: index + 1, content: line.trim() }); } }); } catch (e) { // 读取文件出错,跳过 } } } }); }; walk(assetsPath); callback(null, results); } catch (err) { callback(`Find in file failed: ${err.message}`); } }, // 管理撤销/重做 manageUndo(args, callback) { const { action, description } = args; try { switch (action) { case "undo": Editor.Ipc.sendToPanel("scene", "scene:undo"); callback(null, "Undo command executed"); break; case "redo": Editor.Ipc.sendToPanel("scene", "scene:redo"); callback(null, "Redo command executed"); break; case "begin_group": // scene:undo-record [id] // 这里的 id 好像是可选的,或者用于区分不同的事务 Editor.Ipc.sendToPanel("scene", "scene:undo-record", description || "MCP Action"); callback(null, `Undo group started: ${description || "MCP Action"}`); break; case "end_group": Editor.Ipc.sendToPanel("scene", "scene:undo-commit"); callback(null, "Undo group committed"); break; case "cancel_group": Editor.Ipc.sendToPanel("scene", "scene:undo-cancel"); callback(null, "Undo group cancelled"); break; default: callback(`Unknown undo action: ${action}`); } } catch (err) { callback(`Undo operation failed: ${err.message}`); } }, // 获取文件 SHA-256 getSha(args, callback) { const { path: url } = args; const fspath = Editor.assetdb.urlToFspath(url); if (!fspath || !fs.existsSync(fspath)) { return callback(`File not found: ${url}`); } try { const fileBuffer = fs.readFileSync(fspath); const hashSum = crypto.createHash('sha256'); hashSum.update(fileBuffer); const sha = hashSum.digest('hex'); callback(null, { path: url, sha: sha }); } catch (err) { callback(`Failed to calculate SHA: ${err.message}`); } }, // 管理动画 manageAnimation(args, callback) { // 转发给场景脚本处理 Editor.Scene.callSceneScript("mcp-bridge", "manage-animation", args, callback); }, };