Files
mcp-bridge/main.js

1923 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 目录下创建一个新的场景文件。创建并通过 open_scene 打开后,请务必初始化基础节点(如 Canvas 和 Camera。",
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: "打开场景文件。注意这是一个异步且耗时的操作打开后请等待几秒。重要如果是新创建或空的场景请务必先创建并初始化基础节点Canvas/Camera。",
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: "管理节点组件。重要提示:在执行 'add' 操作前,请务必先通过 'get' 操作检查节点上是否已存在同类型的组件,以避免重复添加导致逻辑异常。",
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("编辑器正忙(正在处理场景),请稍候。");
}
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, `节点名称已更新为 ${args.newName}`);
break;
case "save_scene":
isSceneBusy = true;
addLog("info", "准备保存场景... 等待 UI 同步。");
Editor.Ipc.sendToPanel("scene", "scene:stash-and-save");
isSceneBusy = false;
addLog("info", "安全保存已完成。");
callback(null, "场景保存成功。");
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, "变换信息已更新");
}
});
break;
case "create_scene":
const sceneUrl = `db://assets/${args.sceneName}.fire`;
if (Editor.assetdb.exists(sceneUrl)) {
return callback("场景已存在");
}
Editor.assetdb.create(sceneUrl, getNewSceneTemplate(), (err) => {
callback(err, err ? null : `标准场景已创建于 ${sceneUrl}`);
});
break;
case "create_prefab":
const prefabUrl = `db://assets/${args.prefabName}.prefab`;
Editor.Ipc.sendToMain("scene:create-prefab", args.nodeId, prefabUrl);
callback(null, `命令已发送:正在创建预制体 '${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, `成功:正在打开场景 ${args.url}`);
}, 2000);
} else {
isSceneBusy = false;
callback(`找不到路径为 ${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);
},
};