2026-01-29 13:47:38 +08:00
|
|
|
|
"use strict";
|
2026-02-03 19:55:51 +08:00
|
|
|
|
const { IpcManager } = require("./dist/IpcManager");
|
2026-01-29 13:47:38 +08:00
|
|
|
|
|
|
|
|
|
|
const http = require("http");
|
2026-02-10 09:14:50 +08:00
|
|
|
|
const pathModule = require("path");
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const fs = require("fs");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
const crypto = require("crypto");
|
2026-01-29 14:26:28 +08:00
|
|
|
|
|
2026-01-29 14:53:06 +08:00
|
|
|
|
let logBuffer = []; // 存储所有日志
|
|
|
|
|
|
let mcpServer = null;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
let isSceneBusy = false;
|
2026-01-29 14:53:06 +08:00
|
|
|
|
let serverConfig = {
|
|
|
|
|
|
port: 3456,
|
|
|
|
|
|
active: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-12 22:55:08 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 指令队列 - 确保所有 MCP 工具调用串行执行
|
|
|
|
|
|
* 防止 AssetDB.refresh 等异步重操作被并发请求打断,导致编辑器卡死
|
|
|
|
|
|
* @see mcp_freeze_analysis.md
|
|
|
|
|
|
*/
|
|
|
|
|
|
let commandQueue = [];
|
|
|
|
|
|
let isProcessingCommand = false;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将一个异步操作加入队列,保证串行执行
|
|
|
|
|
|
* @param {Function} fn 接受 done 回调的函数,fn(done) 中操作完成后必须调用 done()
|
|
|
|
|
|
* @returns {Promise} 操作完成后 resolve
|
|
|
|
|
|
*/
|
|
|
|
|
|
function enqueueCommand(fn) {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
commandQueue.push({ fn, resolve });
|
|
|
|
|
|
processNextCommand();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从队列中取出下一个指令并执行
|
|
|
|
|
|
*/
|
|
|
|
|
|
function processNextCommand() {
|
|
|
|
|
|
if (isProcessingCommand || commandQueue.length === 0) return;
|
|
|
|
|
|
isProcessingCommand = true;
|
|
|
|
|
|
const { fn, resolve } = commandQueue.shift();
|
|
|
|
|
|
try {
|
|
|
|
|
|
fn(() => {
|
|
|
|
|
|
isProcessingCommand = false;
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
processNextCommand();
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 防止队列因未捕获异常永久阻塞
|
|
|
|
|
|
addLog("error", `[CommandQueue] 指令执行异常: ${e.message}`);
|
|
|
|
|
|
isProcessingCommand = false;
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
processNextCommand();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 带超时保护的 callSceneScript 包装
|
|
|
|
|
|
* 防止 Scene 面板阻塞时 callback 永不返回,导致 HTTP 连接堆积
|
|
|
|
|
|
* @param {string} pluginName 插件名
|
|
|
|
|
|
* @param {string} method 方法名
|
|
|
|
|
|
* @param {*} args 参数(可以是对象或 null)
|
|
|
|
|
|
* @param {Function} callback 回调 (err, result)
|
|
|
|
|
|
* @param {number} timeout 超时毫秒数,默认 15000
|
|
|
|
|
|
*/
|
|
|
|
|
|
function callSceneScriptWithTimeout(pluginName, method, args, callback, timeout = 15000) {
|
|
|
|
|
|
let settled = false;
|
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
|
if (!settled) {
|
|
|
|
|
|
settled = true;
|
|
|
|
|
|
addLog("error", `[超时] callSceneScript "${method}" 超过 ${timeout}ms 未响应`);
|
|
|
|
|
|
callback(`操作超时: ${method} (${timeout}ms)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, timeout);
|
|
|
|
|
|
|
|
|
|
|
|
// callSceneScript 支持 3 参数(无 args)和 4 参数两种调用形式
|
|
|
|
|
|
const wrappedCallback = (err, result) => {
|
|
|
|
|
|
if (!settled) {
|
|
|
|
|
|
settled = true;
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
|
callback(err, result);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (args === null || args === undefined) {
|
|
|
|
|
|
Editor.Scene.callSceneScript(pluginName, method, wrappedCallback);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Editor.Scene.callSceneScript(pluginName, method, args, wrappedCallback);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 封装日志函数,同时发送给面板、保存到内存并在编辑器控制台打印
|
|
|
|
|
|
* @param {'info' | 'success' | 'warn' | 'error'} type 日志类型
|
|
|
|
|
|
* @param {string} message 日志内容
|
|
|
|
|
|
*/
|
2026-01-29 14:53:06 +08:00
|
|
|
|
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);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 【修改】确保所有日志都输出到编辑器控制台,以便用户查看
|
2026-01-29 14:53:06 +08:00
|
|
|
|
if (type === "error") {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
Editor.error(`[MCP] ${message}`);
|
|
|
|
|
|
} else if (type === "warn") {
|
|
|
|
|
|
Editor.warn(`[MCP] ${message}`);
|
|
|
|
|
|
} else {
|
2026-01-29 14:53:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取完整的日志内容(文本格式)
|
|
|
|
|
|
* @returns {string} 拼接后的日志字符串
|
|
|
|
|
|
*/
|
2026-02-02 14:34:34 +08:00
|
|
|
|
function getLogContent() {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
return logBuffer.map((entry) => `[${entry.time}] [${entry.type}] ${entry.content}`).join("\n");
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成新场景的 JSON 模板数据
|
|
|
|
|
|
* @returns {string} 场景数据的 JSON 字符串
|
|
|
|
|
|
*/
|
2026-01-29 14:26:28 +08:00
|
|
|
|
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);
|
|
|
|
|
|
};
|
2026-01-29 13:47:38 +08:00
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有支持的 MCP 工具列表定义
|
|
|
|
|
|
* @returns {Array<Object>} 工具定义数组
|
|
|
|
|
|
*/
|
2026-01-29 15:55:38 +08:00
|
|
|
|
const getToolsList = () => {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const globalPrecautions =
|
|
|
|
|
|
"【AI 安全守则】: 1. 执行任何写操作前必须先通过 get_scene_hierarchy 或 manage_components(get) 验证主体存在。 2. 严禁基于假设盲目猜测属性名。 3. 资源属性(如 cc.Prefab)必须通过 UUID 进行赋值。";
|
2026-01-29 15:55:38 +08:00
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "get_selected_node",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 获取当前编辑器中选中的节点 ID。建议获取后立即调用 get_scene_hierarchy 确认该节点是否仍存在于当前场景中。`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: { type: "object", properties: {} },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "set_node_name",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 修改指定节点的名称`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
id: { type: "string", description: "节点的 UUID" },
|
|
|
|
|
|
newName: { type: "string", description: "新的节点名称" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["id", "newName"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "save_scene",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 保存当前场景的修改`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: { type: "object", properties: {} },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "get_scene_hierarchy",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 获取当前场景的完整节点树结构(包括 UUID、名称和层级关系)`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: { type: "object", properties: {} },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "update_node_transform",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 修改节点的坐标、缩放或颜色。执行前必须调用 get_scene_hierarchy 确保 node ID 有效。`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
id: { type: "string", description: "节点 UUID" },
|
|
|
|
|
|
x: { type: "number" },
|
|
|
|
|
|
y: { type: "number" },
|
2026-02-10 14:00:02 +08:00
|
|
|
|
width: { type: "number" },
|
|
|
|
|
|
height: { type: "number" },
|
2026-01-29 15:55:38 +08:00
|
|
|
|
scaleX: { type: "number" },
|
|
|
|
|
|
scaleY: { type: "number" },
|
|
|
|
|
|
color: { type: "string", description: "HEX 颜色代码如 #FF0000" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["id"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "create_scene",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 在 assets 目录下创建一个新的场景文件。创建并通过 open_scene 打开后,请务必初始化基础节点(如 Canvas 和 Camera)。`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
sceneName: { type: "string", description: "场景名称" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["sceneName"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "create_prefab",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 将场景中的某个节点保存为预制体资源`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
nodeId: { type: "string", description: "节点 UUID" },
|
|
|
|
|
|
prefabName: { type: "string", description: "预制体名称" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["nodeId", "prefabName"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "open_scene",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒。重要:如果是新创建或空的场景,请务必先创建并初始化基础节点(Canvas/Camera)。`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
url: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
description: "场景资源路径,如 db://assets/NewScene.fire",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["url"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "create_node",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 在当前场景中创建一个新节点。重要提示:1. 如果指定 parentId,必须先通过 get_scene_hierarchy 确保该父节点真实存在且未被删除。2. 类型说明:'sprite' (100x100 尺寸 + 默认贴图), 'button' (150x50 尺寸 + 深色底图 + Button组件), 'label' (120x40 尺寸 + Label组件), 'empty' (纯空节点)。`,
|
2026-01-29 15:55:38 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
name: { type: "string", description: "节点名称" },
|
|
|
|
|
|
parentId: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
description: "父节点 UUID (可选,不传则挂在场景根部)",
|
|
|
|
|
|
},
|
|
|
|
|
|
type: {
|
|
|
|
|
|
type: "string",
|
2026-02-07 23:14:12 +08:00
|
|
|
|
enum: ["empty", "sprite", "label", "button"],
|
2026-01-29 15:55:38 +08:00
|
|
|
|
description: "节点预设类型",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["name"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-31 16:48:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: "manage_components",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理节点组件。重要提示:1. 操作前必须调用 get_scene_hierarchy 确认 nodeId 对应的节点仍然存在。2. 添加前先用 'get' 检查是否已存在。3. 添加 cc.Sprite 后必须设置 spriteFrame 属性,否则节点不显示。4. 创建按钮时,请确保目标节点有足够的 width 和 height 作为点击区域。5. 赋值或更新属性前,必须确保目标属性在组件上真实存在,严禁盲目操作不存在的属性。6. 对于资源类属性(如 cc.Prefab, sp.SkeletonData),传递资源的 UUID。插件会自动进行异步加载并正确序列化,避免 Inspector 出现 Type Error。`,
|
2026-01-31 16:48:21 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
nodeId: { type: "string", description: "节点 UUID" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["add", "remove", "update", "get"],
|
|
|
|
|
|
description:
|
|
|
|
|
|
"操作类型 (add: 添加组件, remove: 移除组件, update: 更新组件属性, get: 获取组件列表)",
|
|
|
|
|
|
},
|
2026-02-03 19:55:51 +08:00
|
|
|
|
componentType: { type: "string", description: "组件类型,如 cc.Sprite (add/update 操作需要)" },
|
|
|
|
|
|
componentId: { type: "string", description: "组件 ID (remove/update 操作可选)" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
properties: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
description:
|
|
|
|
|
|
"组件属性 (add/update 操作使用). 支持智能解析: 如果属性类型是组件但提供了节点UUID,会自动查找对应组件。",
|
|
|
|
|
|
},
|
2026-01-31 16:48:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
required: ["nodeId", "action"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "manage_script",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理脚本文件。注意:创建或修改脚本后,编辑器需要时间进行编译(通常几秒钟)。新脚本在编译完成前无法作为组件添加到节点。建议在 create 后调用 refresh_editor,或等待一段时间后再使用 manage_components。`,
|
2026-01-31 16:48:21 +08:00
|
|
|
|
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",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 批处理执行多个操作`,
|
2026-01-31 16:48:21 +08:00
|
|
|
|
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",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理资源`,
|
2026-01-31 16:48:21 +08:00
|
|
|
|
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"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-31 19:36:55 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: "scene_management",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 场景管理`,
|
2026-01-31 19:36:55 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-01 13:30:11 +08:00
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["create", "delete", "duplicate", "get_info"],
|
|
|
|
|
|
description: "操作类型",
|
|
|
|
|
|
},
|
2026-01-31 19:36:55 +08:00
|
|
|
|
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",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 预制体管理`,
|
2026-01-31 19:36:55 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-01 13:30:11 +08:00
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["create", "update", "instantiate", "get_info"],
|
|
|
|
|
|
description: "操作类型",
|
|
|
|
|
|
},
|
2026-01-31 19:36:55 +08:00
|
|
|
|
path: { type: "string", description: "预制体路径,如 db://assets/prefabs/NewPrefab.prefab" },
|
|
|
|
|
|
nodeId: { type: "string", description: "节点 ID (用于 create 操作)" },
|
|
|
|
|
|
parentId: { type: "string", description: "父节点 ID (用于 instantiate 操作)" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["action", "path"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: "manage_editor",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理编辑器`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["get_selection", "set_selection", "refresh_editor"],
|
|
|
|
|
|
description: "操作类型",
|
|
|
|
|
|
},
|
|
|
|
|
|
target: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["node", "asset"],
|
|
|
|
|
|
description: "目标类型 (用于 set_selection 操作)",
|
|
|
|
|
|
},
|
2026-02-12 23:18:02 +08:00
|
|
|
|
properties: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
description:
|
|
|
|
|
|
"操作属性。refresh_editor 支持 properties.path 指定刷新路径(如 'db://assets/scripts/MyScript.ts' 或 'db://assets/resources')。不传则默认刷新 'db://assets'(全量刷新,大型项目可能耗时数分钟,建议尽量指定具体路径)。",
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
required: ["action"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "find_gameobjects",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 查找游戏对象`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
conditions: { type: "object", description: "查找条件" },
|
|
|
|
|
|
recursive: { type: "boolean", default: true, description: "是否递归查找" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["conditions"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "manage_material",
|
2026-02-10 09:14:50 +08:00
|
|
|
|
description: `${globalPrecautions} 管理材质。支持创建、获取信息以及更新 Shader、Defines 和 Uniforms 参数。`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["create", "delete", "get_info", "update"],
|
|
|
|
|
|
description: "操作类型",
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
path: { type: "string", description: "材质路径,如 db://assets/materials/NewMaterial.mat" },
|
2026-02-10 09:14:50 +08:00
|
|
|
|
properties: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
description: "材质属性 (add/update 操作使用)",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
shaderUuid: { type: "string", description: "关联的 Shader (Effect) UUID" },
|
|
|
|
|
|
defines: { type: "object", description: "预编译宏定义" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
uniforms: { type: "object", description: "Uniform 参数列表" },
|
|
|
|
|
|
},
|
2026-02-10 09:14:50 +08:00
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
required: ["action", "path"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "manage_texture",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理纹理`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["create", "delete", "get_info", "update"],
|
|
|
|
|
|
description: "操作类型",
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
path: { type: "string", description: "纹理路径,如 db://assets/textures/NewTexture.png" },
|
|
|
|
|
|
properties: { type: "object", description: "纹理属性" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["action", "path"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-02-10 09:14:50 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: "manage_shader",
|
|
|
|
|
|
description: `${globalPrecautions} 管理着色器 (Effect)。支持创建、读取、更新、删除和获取信息。`,
|
|
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["create", "delete", "read", "write", "get_info"],
|
|
|
|
|
|
description: "操作类型",
|
|
|
|
|
|
},
|
2026-02-10 09:14:50 +08:00
|
|
|
|
path: { type: "string", description: "着色器路径,如 db://assets/effects/NewEffect.effect" },
|
|
|
|
|
|
content: { type: "string", description: "着色器内容 (create/write)" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["action", "path"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
name: "execute_menu_item",
|
2026-02-10 23:20:56 +08:00
|
|
|
|
description: `${globalPrecautions} 执行菜单项。对于节点删除,请使用 "delete-node:UUID" 格式以确保精确执行。对于保存、撤销等操作,请优先使用专用工具 (save_scene, manage_undo)。`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
menuPath: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
description: "菜单项路径 (支持 'Project/Build' 或 'delete-node:UUID')",
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
required: ["menuPath"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "apply_text_edits",
|
2026-02-10 23:20:56 +08:00
|
|
|
|
description: `${globalPrecautions} 对文件应用文本编辑。**专用于修改脚本源代码 (.js, .ts) 或文本文件**。如果要修改场景节点属性,请使用 'manage_components'。`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-10 23:20:56 +08:00
|
|
|
|
edits: {
|
|
|
|
|
|
type: "array",
|
|
|
|
|
|
items: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
type: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["insert", "delete", "replace"],
|
|
|
|
|
|
description: "操作类型",
|
|
|
|
|
|
},
|
2026-02-10 23:20:56 +08:00
|
|
|
|
start: { type: "number", description: "起始偏移量 (字符索引)" },
|
|
|
|
|
|
end: { type: "number", description: "结束偏移量 (delete/replace 用)" },
|
|
|
|
|
|
position: { type: "number", description: "插入位置 (insert 用)" },
|
|
|
|
|
|
text: { type: "string", description: "要插入或替换的文本" },
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
description: "编辑操作列表。请严格使用偏移量(offset)而非行号。",
|
|
|
|
|
|
},
|
|
|
|
|
|
filePath: { type: "string", description: "文件路径 (db://...)" },
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
required: ["filePath", "edits"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "read_console",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 读取控制台`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
limit: { type: "number", description: "输出限制" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
type: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["info", "warn", "error", "success", "mcp"],
|
|
|
|
|
|
description: "输出类型 (info, warn, error, success, mcp)",
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "validate_script",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 验证脚本`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
filePath: { type: "string", description: "脚本路径" },
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ["filePath"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-02-11 00:36:56 +08:00
|
|
|
|
name: "search_project",
|
|
|
|
|
|
description: `${globalPrecautions} 搜索项目文件。支持三种模式:1. 'content' (默认): 搜索文件内容,支持正则表达式;2. 'file_name': 在指定目录下搜索匹配的文件名;3. 'dir_name': 在指定目录下搜索匹配的文件夹名。`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
query: { type: "string", description: "搜索关键词或正则表达式模式" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
useRegex: {
|
|
|
|
|
|
type: "boolean",
|
|
|
|
|
|
description:
|
|
|
|
|
|
"是否将 query 视为正则表达式 (仅在 matchType 为 'content', 'file_name' 或 'dir_name' 时生效)",
|
|
|
|
|
|
},
|
|
|
|
|
|
path: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
description: "搜索起点路径,例如 'db://assets/scripts'。默认为 'db://assets'",
|
|
|
|
|
|
},
|
2026-02-11 00:36:56 +08:00
|
|
|
|
matchType: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["content", "file_name", "dir_name"],
|
2026-02-12 22:55:08 +08:00
|
|
|
|
description:
|
|
|
|
|
|
"匹配模式:'content' (内容关键词/正则), 'file_name' (搜索文件名), 'dir_name' (搜索文件夹名)",
|
2026-02-11 00:36:56 +08:00
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
extensions: {
|
|
|
|
|
|
type: "array",
|
|
|
|
|
|
items: { type: "string" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
description:
|
|
|
|
|
|
"限定文件后缀 (如 ['.js', '.ts'])。仅在 matchType 为 'content' 或 'file_name' 时有效。",
|
|
|
|
|
|
default: [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"],
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
includeSubpackages: { type: "boolean", default: true, description: "是否递归搜索子目录" },
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
required: ["query"],
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "manage_undo",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理编辑器的撤销和重做历史`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["undo", "redo", "begin_group", "end_group", "cancel_group"],
|
2026-02-12 22:55:08 +08:00
|
|
|
|
description: "操作类型",
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
description: { type: "string", description: "撤销组的描述 (用于 begin_group)" },
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
required: ["action"],
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "manage_vfx",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理全场景特效 (粒子系统)。重要提示:在创建或更新前,必须通过 get_scene_hierarchy 或 manage_components 确认父节点或目标节点的有效性。严禁对不存在的对象进行操作。`,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["create", "update", "get_info"],
|
2026-02-12 22:55:08 +08:00
|
|
|
|
description: "操作类型",
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
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" } } },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
file: { type: "string", description: "粒子文件路径 (plist) 或 texture 路径" },
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
name: { type: "string", description: "节点名称 (用于 create)" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
parentId: { type: "string", description: "父节点 ID (用于 create)" },
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
required: ["action"],
|
|
|
|
|
|
},
|
2026-02-04 01:57:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "get_sha",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 获取指定文件的 SHA-256 哈希值`,
|
2026-02-04 01:57:12 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
path: { type: "string", description: "文件路径,如 db://assets/scripts/Test.ts" },
|
2026-02-04 01:57:12 +08:00
|
|
|
|
},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
required: ["path"],
|
|
|
|
|
|
},
|
2026-02-04 01:57:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "manage_animation",
|
2026-02-10 00:38:38 +08:00
|
|
|
|
description: `${globalPrecautions} 管理节点的动画组件。重要提示:在执行 play/pause 等操作前,必须先确认节点及其 Animation 组件存在。严禁操作空引用。`,
|
2026-02-04 01:57:12 +08:00
|
|
|
|
inputSchema: {
|
|
|
|
|
|
type: "object",
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
action: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
enum: ["get_list", "get_info", "play", "stop", "pause", "resume"],
|
2026-02-12 22:55:08 +08:00
|
|
|
|
description: "操作类型",
|
2026-02-04 01:57:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
nodeId: { type: "string", description: "节点 UUID" },
|
2026-02-12 22:55:08 +08:00
|
|
|
|
clipName: { type: "string", description: "动画剪辑名称 (用于 play)" },
|
2026-02-04 01:57:12 +08:00
|
|
|
|
},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
required: ["action", "nodeId"],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-29 15:55:38 +08:00
|
|
|
|
];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-29 13:47:38 +08:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
"scene-script": "scene-script.js",
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 插件加载时的回调
|
|
|
|
|
|
*/
|
2026-01-29 13:47:38 +08:00
|
|
|
|
load() {
|
2026-01-29 14:53:06 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取插件配置文件的辅助函数
|
|
|
|
|
|
* @returns {Object} Editor.Profile 实例
|
|
|
|
|
|
*/
|
2026-01-29 14:53:06 +08:00
|
|
|
|
getProfile() {
|
|
|
|
|
|
// 'local' 表示存储在项目本地(local/mcp-bridge.json)
|
|
|
|
|
|
return Editor.Profile.load("profile://local/mcp-bridge.json", "mcp-bridge");
|
2026-01-29 13:47:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 插件卸载时的回调
|
|
|
|
|
|
*/
|
2026-01-29 13:47:38 +08:00
|
|
|
|
unload() {
|
2026-01-29 14:53:06 +08:00
|
|
|
|
this.stopServer();
|
2026-01-29 13:47:38 +08:00
|
|
|
|
},
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 启动 HTTP 服务器
|
|
|
|
|
|
* @param {number} port 监听端口
|
|
|
|
|
|
*/
|
2026-01-29 14:53:06 +08:00
|
|
|
|
startServer(port) {
|
|
|
|
|
|
if (mcpServer) this.stopServer();
|
2026-01-29 13:47:38 +08:00
|
|
|
|
|
2026-01-29 14:53:06 +08:00
|
|
|
|
try {
|
|
|
|
|
|
mcpServer = http.createServer((req, res) => {
|
|
|
|
|
|
res.setHeader("Content-Type", "application/json");
|
2026-01-29 15:55:38 +08:00
|
|
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
2026-01-29 13:47:38 +08:00
|
|
|
|
|
2026-01-29 14:53:06 +08:00
|
|
|
|
let body = "";
|
|
|
|
|
|
req.on("data", (chunk) => {
|
|
|
|
|
|
body += chunk;
|
|
|
|
|
|
});
|
|
|
|
|
|
req.on("end", () => {
|
2026-01-29 15:55:38 +08:00
|
|
|
|
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 }));
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
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 }] }
|
2026-02-12 22:55:08 +08:00
|
|
|
|
res.end(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
contents: [
|
|
|
|
|
|
{
|
|
|
|
|
|
uri: uri,
|
|
|
|
|
|
mimeType: "application/json",
|
|
|
|
|
|
text: typeof content === "string" ? content : JSON.stringify(content),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
res.writeHead(500);
|
|
|
|
|
|
res.end(JSON.stringify({ error: e.message }));
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
2026-01-29 15:55:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (url === "/call-tool") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { name, arguments: args } = JSON.parse(body || "{}");
|
2026-02-12 22:55:08 +08:00
|
|
|
|
addLog("mcp", `REQ -> [${name}] (队列长度: ${commandQueue.length})`);
|
|
|
|
|
|
|
|
|
|
|
|
// 【关键修复】所有 MCP 指令通过队列串行化执行,
|
|
|
|
|
|
// 防止 AssetDB.refresh 等异步操作被并发请求打断导致编辑器卡死
|
|
|
|
|
|
enqueueCommand((done) => {
|
|
|
|
|
|
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)";
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-12 22:55:08 +08:00
|
|
|
|
addLog("success", `RES <- [${name}] 成功 : ${preview}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-12 22:55:08 +08:00
|
|
|
|
res.writeHead(200);
|
|
|
|
|
|
res.end(JSON.stringify(response));
|
|
|
|
|
|
done(); // 当前指令完成,释放队列给下一个指令
|
|
|
|
|
|
});
|
2026-01-29 15:55:38 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
2026-02-01 13:30:11 +08:00
|
|
|
|
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 }));
|
|
|
|
|
|
}
|
2026-01-29 13:47:38 +08:00
|
|
|
|
}
|
2026-01-29 15:55:38 +08:00
|
|
|
|
return;
|
2026-01-29 13:47:38 +08:00
|
|
|
|
}
|
2026-01-29 15:55:38 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 兜底处理 (404) ---
|
|
|
|
|
|
res.writeHead(404);
|
|
|
|
|
|
res.end(JSON.stringify({ error: "Not Found", url: url }));
|
2026-01-29 14:53:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-29 15:55:38 +08:00
|
|
|
|
mcpServer.on("error", (e) => {
|
|
|
|
|
|
addLog("error", `Server Error: ${e.message}`);
|
|
|
|
|
|
});
|
2026-01-29 14:53:06 +08:00
|
|
|
|
mcpServer.listen(port, () => {
|
|
|
|
|
|
serverConfig.active = true;
|
2026-01-29 15:55:38 +08:00
|
|
|
|
addLog("success", `MCP Server running at http://127.0.0.1:${port}`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
getResourcesList() {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
uri: "cocos://hierarchy",
|
|
|
|
|
|
name: "Scene Hierarchy",
|
|
|
|
|
|
description: "当前场景层级的 JSON 快照",
|
2026-02-12 22:55:08 +08:00
|
|
|
|
mimeType: "application/json",
|
2026-02-02 14:34:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
uri: "cocos://selection",
|
|
|
|
|
|
name: "Current Selection",
|
|
|
|
|
|
description: "当前选中节点的 UUID 列表",
|
2026-02-12 22:55:08 +08:00
|
|
|
|
mimeType: "application/json",
|
2026-02-02 14:34:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
uri: "cocos://logs/latest",
|
|
|
|
|
|
name: "Editor Logs",
|
|
|
|
|
|
description: "最新的编辑器日志 (内存缓存)",
|
2026-02-12 22:55:08 +08:00
|
|
|
|
mimeType: "text/plain",
|
|
|
|
|
|
},
|
2026-02-02 14:34:34 +08:00
|
|
|
|
];
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理来自 HTTP 的 MCP 调用请求
|
|
|
|
|
|
* @param {string} name 工具名称
|
|
|
|
|
|
* @param {Object} args 工具参数
|
|
|
|
|
|
* @param {Function} callback 完成回调 (err, result)
|
|
|
|
|
|
*/
|
2026-01-29 14:53:06 +08:00
|
|
|
|
handleMcpCall(name, args, callback) {
|
2026-01-31 16:48:21 +08:00
|
|
|
|
if (isSceneBusy && (name === "save_scene" || name === "create_node")) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
return callback("编辑器正忙(正在处理场景),请稍候。");
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
2026-01-29 14:53:06 +08:00
|
|
|
|
switch (name) {
|
|
|
|
|
|
case "get_selected_node":
|
|
|
|
|
|
const ids = Editor.Selection.curSelection("node");
|
|
|
|
|
|
callback(null, ids);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "set_node_name":
|
2026-02-01 13:30:11 +08:00
|
|
|
|
// 使用 scene:set-property 以支持撤销
|
|
|
|
|
|
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
|
|
|
|
|
id: args.id,
|
|
|
|
|
|
path: "name",
|
|
|
|
|
|
type: "String",
|
|
|
|
|
|
value: args.newName,
|
2026-02-12 22:55:08 +08:00
|
|
|
|
isSubProp: false,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
});
|
2026-02-07 22:29:17 +08:00
|
|
|
|
callback(null, `节点名称已更新为 ${args.newName}`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "save_scene":
|
2026-01-31 16:48:21 +08:00
|
|
|
|
isSceneBusy = true;
|
2026-02-07 22:29:17 +08:00
|
|
|
|
addLog("info", "准备保存场景... 等待 UI 同步。");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
Editor.Ipc.sendToPanel("scene", "scene:stash-and-save");
|
|
|
|
|
|
isSceneBusy = false;
|
2026-02-07 22:29:17 +08:00
|
|
|
|
addLog("info", "安全保存已完成。");
|
|
|
|
|
|
callback(null, "场景保存成功。");
|
2026-01-29 14:53:06 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "get_scene_hierarchy":
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "get-hierarchy", null, callback);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "update_node_transform":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 直接调用场景脚本更新属性,绕过可能导致 "Unknown object" 的复杂 Undo 系统
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "update-node-transform", args, (err, result) => {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
if (err) {
|
|
|
|
|
|
addLog("error", `Transform update failed: ${err}`);
|
|
|
|
|
|
callback(err);
|
|
|
|
|
|
} else {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
callback(null, "变换信息已更新");
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
2026-01-29 14:53:06 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "create_scene":
|
|
|
|
|
|
const sceneUrl = `db://assets/${args.sceneName}.fire`;
|
|
|
|
|
|
if (Editor.assetdb.exists(sceneUrl)) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
return callback("场景已存在");
|
2026-01-29 14:53:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
Editor.assetdb.create(sceneUrl, getNewSceneTemplate(), (err) => {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
callback(err, err ? null : `标准场景已创建于 ${sceneUrl}`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "create_prefab":
|
|
|
|
|
|
const prefabUrl = `db://assets/${args.prefabName}.prefab`;
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:create-prefab", args.nodeId, prefabUrl);
|
2026-02-07 22:29:17 +08:00
|
|
|
|
callback(null, `命令已发送:正在创建预制体 '${args.prefabName}'`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "open_scene":
|
2026-01-31 16:48:21 +08:00
|
|
|
|
isSceneBusy = true; // 锁定
|
2026-01-29 14:53:06 +08:00
|
|
|
|
const openUuid = Editor.assetdb.urlToUuid(args.url);
|
|
|
|
|
|
if (openUuid) {
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isSceneBusy = false;
|
2026-02-07 22:29:17 +08:00
|
|
|
|
callback(null, `成功:正在打开场景 ${args.url}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}, 2000);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
} else {
|
2026-01-31 16:48:21 +08:00
|
|
|
|
isSceneBusy = false;
|
2026-02-07 22:29:17 +08:00
|
|
|
|
callback(`找不到路径为 ${args.url} 的资源`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "create_node":
|
2026-02-07 23:14:12 +08:00
|
|
|
|
if (args.type === "sprite" || args.type === "button") {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const splashUuid = Editor.assetdb.urlToUuid(
|
|
|
|
|
|
"db://internal/image/default_sprite_splash.png/default_sprite_splash",
|
|
|
|
|
|
);
|
2026-02-07 23:14:12 +08:00
|
|
|
|
args.defaultSpriteUuid = splashUuid;
|
|
|
|
|
|
}
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "create-node", args, callback);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
2026-01-31 16:48:21 +08:00
|
|
|
|
case "manage_components":
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "manage-components", args, callback);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-01-31 19:36:55 +08:00
|
|
|
|
case "scene_management":
|
|
|
|
|
|
this.sceneManagement(args, callback);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "prefab_management":
|
|
|
|
|
|
this.prefabManagement(args, callback);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
case "manage_editor":
|
|
|
|
|
|
this.manageEditor(args, callback);
|
|
|
|
|
|
break;
|
2026-02-04 01:57:12 +08:00
|
|
|
|
case "get_sha":
|
|
|
|
|
|
this.getSha(args, callback);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "manage_animation":
|
|
|
|
|
|
this.manageAnimation(args, callback);
|
|
|
|
|
|
break;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
|
|
|
|
|
case "find_gameobjects":
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "find-gameobjects", args, callback);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "manage_material":
|
|
|
|
|
|
this.manageMaterial(args, callback);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "manage_texture":
|
|
|
|
|
|
this.manageTexture(args, callback);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-02-10 09:14:50 +08:00
|
|
|
|
case "manage_shader":
|
|
|
|
|
|
this.manageShader(args, callback);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-02-11 00:36:56 +08:00
|
|
|
|
case "search_project":
|
|
|
|
|
|
this.searchProject(args, callback);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "manage_undo":
|
|
|
|
|
|
this.manageUndo(args, callback);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "manage_vfx":
|
|
|
|
|
|
// 【修复】在主进程预先解析 URL 为 UUID,因为渲染进程(scene-script)无法访问 Editor.assetdb
|
|
|
|
|
|
if (args.properties && args.properties.file) {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (typeof args.properties.file === "string" && args.properties.file.startsWith("db://")) {
|
2026-02-01 13:30:11 +08:00
|
|
|
|
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",
|
2026-02-12 22:55:08 +08:00
|
|
|
|
"db://internal/image/default_particle.png",
|
2026-02-01 13:30:11 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "manage-vfx", args, callback);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
2026-01-29 14:53:06 +08:00
|
|
|
|
default:
|
|
|
|
|
|
callback(`Unknown tool: ${name}`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-31 16:48:21 +08:00
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 管理项目中的脚本文件 (TS/JS)
|
|
|
|
|
|
* @param {Object} args 参数
|
|
|
|
|
|
* @param {Function} callback 完成回调
|
|
|
|
|
|
*/
|
2026-01-31 16:48:21 +08:00
|
|
|
|
manageScript(args, callback) {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const { action, path: scriptPath, content } = args;
|
2026-01-31 16:48:21 +08:00
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "create":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
if (Editor.assetdb.exists(scriptPath)) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback(`脚本已存在: ${scriptPath}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保父目录存在
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const absolutePath = Editor.assetdb.urlToFspath(scriptPath);
|
|
|
|
|
|
const dirPath = path.dirname(absolutePath);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
if (!fs.existsSync(dirPath)) {
|
|
|
|
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
Editor.assetdb.create(
|
2026-02-02 14:34:34 +08:00
|
|
|
|
scriptPath,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
content ||
|
2026-02-12 22:55:08 +08:00
|
|
|
|
`const { ccclass, property } = cc._decorator;
|
2026-01-31 16:48:21 +08:00
|
|
|
|
|
|
|
|
|
|
@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) {}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}`,
|
|
|
|
|
|
(err) => {
|
2026-02-03 19:55:51 +08:00
|
|
|
|
if (err) {
|
|
|
|
|
|
callback(err);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 【关键修复】创建脚本后,必须刷新 AssetDB 并等待完成,
|
|
|
|
|
|
// 否则后续立即挂载脚本的操作(manage_components)会因找不到脚本 UUID 而失败。
|
|
|
|
|
|
Editor.assetdb.refresh(scriptPath, (refreshErr) => {
|
|
|
|
|
|
if (refreshErr) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
addLog("warn", `脚本创建后刷新失败: ${refreshErr}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(null, `脚本已创建: ${scriptPath}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "delete":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
if (!Editor.assetdb.exists(scriptPath)) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback(`找不到脚本: ${scriptPath}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
Editor.assetdb.delete([scriptPath], (err) => {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(err, err ? null : `脚本已删除: ${scriptPath}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "read":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 使用 fs 读取,绕过 assetdb.loadAny
|
|
|
|
|
|
const readFsPath = Editor.assetdb.urlToFspath(scriptPath);
|
|
|
|
|
|
if (!readFsPath || !fs.existsSync(readFsPath)) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback(`找不到脚本: ${scriptPath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = fs.readFileSync(readFsPath, "utf-8");
|
|
|
|
|
|
callback(null, content);
|
|
|
|
|
|
} catch (e) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(`读取脚本失败: ${e.message}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-01-31 16:48:21 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "write":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 使用 fs 写入 + refresh,确保覆盖成功
|
|
|
|
|
|
const writeFsPath = Editor.assetdb.urlToFspath(scriptPath);
|
|
|
|
|
|
if (!writeFsPath) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`路径无效: ${scriptPath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
fs.writeFileSync(writeFsPath, content, "utf-8");
|
|
|
|
|
|
Editor.assetdb.refresh(scriptPath, (err) => {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
if (err) addLog("warn", `写入脚本后刷新失败: ${err}`);
|
|
|
|
|
|
callback(null, `脚本已更新: ${scriptPath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(`写入脚本失败: ${e.message}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-01-31 16:48:21 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
2026-02-10 09:14:50 +08:00
|
|
|
|
callback(`未知的脚本操作类型: ${action}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
2026-02-12 22:55:08 +08:00
|
|
|
|
* 批量执行多个 MCP 工具操作(串行链式执行)
|
|
|
|
|
|
* 【重要修复】原并行 forEach 会导致多个 AssetDB 操作同时执行引发编辑器卡死,
|
|
|
|
|
|
* 改为串行执行确保每个操作完成后再执行下一个
|
2026-02-07 23:14:12 +08:00
|
|
|
|
* @param {Object} args 参数 (operations 数组)
|
|
|
|
|
|
* @param {Function} callback 完成回调
|
|
|
|
|
|
*/
|
2026-01-31 16:48:21 +08:00
|
|
|
|
batchExecute(args, callback) {
|
|
|
|
|
|
const { operations } = args;
|
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!operations || operations.length === 0) {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
return callback("未提供任何操作指令");
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 22:55:08 +08:00
|
|
|
|
let index = 0;
|
|
|
|
|
|
const next = () => {
|
|
|
|
|
|
if (index >= operations.length) {
|
|
|
|
|
|
return callback(null, results);
|
|
|
|
|
|
}
|
|
|
|
|
|
const operation = operations[index];
|
2026-01-31 16:48:21 +08:00
|
|
|
|
this.handleMcpCall(operation.tool, operation.params, (err, result) => {
|
|
|
|
|
|
results[index] = { tool: operation.tool, error: err, result: result };
|
2026-02-12 22:55:08 +08:00
|
|
|
|
index++;
|
|
|
|
|
|
next();
|
2026-01-31 16:48:21 +08:00
|
|
|
|
});
|
2026-02-12 22:55:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
next();
|
2026-01-31 16:48:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 通用的资源管理函数 (创建、删除、移动等)
|
|
|
|
|
|
* @param {Object} args 参数
|
|
|
|
|
|
* @param {Function} callback 完成回调
|
|
|
|
|
|
*/
|
2026-01-31 16:48:21 +08:00
|
|
|
|
manageAsset(args, callback) {
|
|
|
|
|
|
const { action, path, targetPath, content } = args;
|
|
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "create":
|
|
|
|
|
|
if (Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`资源已存在: ${path}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保父目录存在
|
2026-02-01 13:30:11 +08:00
|
|
|
|
const fs = require("fs");
|
|
|
|
|
|
const pathModule = require("path");
|
2026-01-31 16:48:21 +08:00
|
|
|
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
|
|
|
|
|
const dirPath = pathModule.dirname(absolutePath);
|
|
|
|
|
|
if (!fs.existsSync(dirPath)) {
|
|
|
|
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
Editor.assetdb.create(path, content || "", (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `资源已创建: ${path}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "delete":
|
|
|
|
|
|
if (!Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到资源: ${path}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
Editor.assetdb.delete([path], (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `资源已删除: ${path}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "move":
|
|
|
|
|
|
if (!Editor.assetdb.exists(path)) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback(`找不到资源: ${path}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (Editor.assetdb.exists(targetPath)) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback(`目标资源已存在: ${targetPath}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
Editor.assetdb.move(path, targetPath, (err) => {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(err, err ? null : `资源已从 ${path} 移动到 ${targetPath}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "get_info":
|
2026-02-01 13:30:11 +08:00
|
|
|
|
try {
|
|
|
|
|
|
if (!Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到资源: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
const uuid = Editor.assetdb.urlToUuid(path);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
|
|
|
|
|
if (info) {
|
|
|
|
|
|
callback(null, info);
|
|
|
|
|
|
} else {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
// 备选方案:如果 API 未返回信息但资源确实存在
|
2026-02-02 14:34:34 +08:00
|
|
|
|
callback(null, { url: path, uuid: uuid, exists: true });
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
} catch (e) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`获取资源信息失败: ${e.message}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-01-31 16:48:21 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
2026-02-10 09:14:50 +08:00
|
|
|
|
callback(`未知的资源管理操作: ${action}`);
|
2026-01-31 16:48:21 +08:00
|
|
|
|
break;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 场景相关的资源管理 (创建、克隆场景等)
|
|
|
|
|
|
* @param {Object} args 参数
|
|
|
|
|
|
* @param {Function} callback 完成回调
|
|
|
|
|
|
*/
|
2026-02-01 13:30:11 +08:00
|
|
|
|
sceneManagement(args, callback) {
|
|
|
|
|
|
const { action, path, targetPath, name } = args;
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "create":
|
|
|
|
|
|
if (Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`场景已存在: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保父目录存在
|
|
|
|
|
|
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) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `场景已创建: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
case "delete":
|
|
|
|
|
|
if (!Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到场景: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
Editor.assetdb.delete([path], (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `场景已删除: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
case "duplicate":
|
|
|
|
|
|
if (!Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到场景: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!targetPath) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback("复制操作需要目标路径");
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (Editor.assetdb.exists(targetPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`目标场景已存在: ${targetPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-10 09:14:50 +08:00
|
|
|
|
// 【修复】Cocos 2.4.x 主进程中 Editor.assetdb 没有 loadAny
|
|
|
|
|
|
// 直接使用 fs 读取物理文件
|
|
|
|
|
|
try {
|
|
|
|
|
|
const sourceFsPath = Editor.assetdb.urlToFspath(path);
|
|
|
|
|
|
if (!sourceFsPath || !fs.existsSync(sourceFsPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`定位源场景文件失败: ${path}`);
|
2026-01-31 19:36:55 +08:00
|
|
|
|
}
|
2026-02-10 09:14:50 +08:00
|
|
|
|
const content = fs.readFileSync(sourceFsPath, "utf-8");
|
|
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
// 确保目标目录存在
|
|
|
|
|
|
const targetAbsolutePath = Editor.assetdb.urlToFspath(targetPath);
|
|
|
|
|
|
const targetDirPath = pathModule.dirname(targetAbsolutePath);
|
|
|
|
|
|
if (!fs.existsSync(targetDirPath)) {
|
|
|
|
|
|
fs.mkdirSync(targetDirPath, { recursive: true });
|
2026-01-31 19:36:55 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
// 创建复制的场景
|
|
|
|
|
|
Editor.assetdb.create(targetPath, content, (err) => {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (err) return callback(err);
|
|
|
|
|
|
// 【增加】关键刷新,确保数据库能查到新文件
|
|
|
|
|
|
Editor.assetdb.refresh(targetPath, (refreshErr) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(refreshErr, refreshErr ? null : `场景已从 ${path} 复制到 ${targetPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
2026-01-31 19:36:55 +08:00
|
|
|
|
});
|
2026-02-10 09:14:50 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
callback(`Duplicate failed: ${e.message}`);
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
case "get_info":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
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 {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到场景: ${path}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
default:
|
2026-01-31 19:36:55 +08:00
|
|
|
|
callback(`Unknown scene action: ${action}`);
|
|
|
|
|
|
break;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 预制体管理
|
|
|
|
|
|
prefabManagement(args, callback) {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const { action, path: prefabPath, nodeId, parentId } = args;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "create":
|
|
|
|
|
|
if (!nodeId) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback("创建预制体需要节点 ID");
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
if (Editor.assetdb.exists(prefabPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`预制体已存在: ${prefabPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保父目录存在
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const absolutePath = Editor.assetdb.urlToFspath(prefabPath);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
const dirPath = pathModule.dirname(absolutePath);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
if (!fs.existsSync(dirPath)) {
|
|
|
|
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 解析目标目录和文件名
|
|
|
|
|
|
// db://assets/folder/PrefabName.prefab -> db://assets/folder, PrefabName
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const targetDir = prefabPath.substring(0, prefabPath.lastIndexOf("/"));
|
|
|
|
|
|
const fileName = prefabPath.substring(prefabPath.lastIndexOf("/") + 1);
|
|
|
|
|
|
const prefabName = fileName.replace(".prefab", "");
|
2026-02-02 14:34:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 重命名节点以匹配预制体名称
|
|
|
|
|
|
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
|
|
|
|
|
id: nodeId,
|
|
|
|
|
|
path: "name",
|
|
|
|
|
|
type: "String",
|
|
|
|
|
|
value: prefabName,
|
2026-02-12 22:55:08 +08:00
|
|
|
|
isSubProp: false,
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 发送创建命令 (参数: [uuids], dirPath)
|
|
|
|
|
|
// 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir);
|
|
|
|
|
|
}, 100); // 稍微延迟以确保重命名生效
|
|
|
|
|
|
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "update":
|
|
|
|
|
|
if (!nodeId) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback("更新预制体需要节点 ID");
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
if (!Editor.assetdb.exists(prefabPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到预制体: ${prefabPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 更新预制体
|
2026-02-02 14:34:34 +08:00
|
|
|
|
Editor.Ipc.sendToPanel("scene", "scene:update-prefab", nodeId, prefabPath);
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(null, `指令已发送: 从节点 ${nodeId} 更新预制体 ${prefabPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "instantiate":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
if (!Editor.assetdb.exists(prefabPath)) {
|
|
|
|
|
|
return callback(`Prefab not found at ${prefabPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 实例化预制体
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const prefabUuid = Editor.assetdb.urlToUuid(prefabPath);
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout(
|
2026-02-01 13:30:11 +08:00
|
|
|
|
"mcp-bridge",
|
|
|
|
|
|
"instantiate-prefab",
|
|
|
|
|
|
{
|
2026-02-02 14:34:34 +08:00
|
|
|
|
prefabUuid: prefabUuid,
|
2026-02-01 13:30:11 +08:00
|
|
|
|
parentId: parentId,
|
|
|
|
|
|
},
|
|
|
|
|
|
callback,
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "get_info":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
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 {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到预制体: ${prefabPath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
2026-02-10 09:14:50 +08:00
|
|
|
|
callback(`未知的预制体管理操作: ${action}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 管理编辑器状态 (选中对象、刷新等)
|
|
|
|
|
|
* @param {Object} args 参数
|
|
|
|
|
|
* @param {Function} callback 完成回调
|
|
|
|
|
|
*/
|
2026-02-01 13:30:11 +08:00
|
|
|
|
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":
|
|
|
|
|
|
// 设置选中状态
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (target === "node") {
|
|
|
|
|
|
const ids = properties.ids || properties.nodes;
|
|
|
|
|
|
if (ids) Editor.Selection.select("node", ids);
|
|
|
|
|
|
} else if (target === "asset") {
|
|
|
|
|
|
const ids = properties.ids || properties.assets;
|
|
|
|
|
|
if (ids) Editor.Selection.select("asset", ids);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(null, "选中状态已更新");
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case "refresh_editor":
|
2026-02-12 23:18:02 +08:00
|
|
|
|
// 刷新编辑器资源数据库
|
|
|
|
|
|
// 支持指定路径以避免大型项目全量刷新耗时过长
|
|
|
|
|
|
// 示例: properties.path = 'db://assets/scripts/MyScript.ts' (刷新单个文件)
|
|
|
|
|
|
// properties.path = 'db://assets/resources' (刷新某个目录)
|
|
|
|
|
|
// 不传 (默认 'db://assets',全量刷新)
|
|
|
|
|
|
const refreshPath = properties && properties.path ? properties.path : "db://assets";
|
|
|
|
|
|
addLog("info", `[refresh_editor] 开始刷新: ${refreshPath}`);
|
2026-02-02 10:14:17 +08:00
|
|
|
|
Editor.assetdb.refresh(refreshPath, (err) => {
|
|
|
|
|
|
if (err) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
addLog("error", `刷新失败: ${err}`);
|
2026-02-02 10:14:17 +08:00
|
|
|
|
callback(err);
|
|
|
|
|
|
} else {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(null, `编辑器已刷新: ${refreshPath}`);
|
2026-02-02 10:14:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
2026-02-10 09:14:50 +08:00
|
|
|
|
callback("未知的编辑器管理操作");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 管理着色器 (Effect)
|
|
|
|
|
|
manageShader(args, callback) {
|
|
|
|
|
|
const { action, path: effectPath, content } = args;
|
|
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "create":
|
|
|
|
|
|
if (Editor.assetdb.exists(effectPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`Effect 已存在: ${effectPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保父目录存在
|
|
|
|
|
|
const absolutePath = Editor.assetdb.urlToFspath(effectPath);
|
|
|
|
|
|
const dirPath = pathModule.dirname(absolutePath);
|
|
|
|
|
|
if (!fs.existsSync(dirPath)) {
|
|
|
|
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const defaultEffect = `CCEffect %{
|
|
|
|
|
|
techniques:
|
|
|
|
|
|
- passes:
|
|
|
|
|
|
- vert: vs
|
|
|
|
|
|
frag: fs
|
|
|
|
|
|
blendState:
|
|
|
|
|
|
targets:
|
|
|
|
|
|
- blend: true
|
|
|
|
|
|
rasterizerState:
|
|
|
|
|
|
cullMode: none
|
|
|
|
|
|
properties:
|
|
|
|
|
|
texture: { value: white }
|
|
|
|
|
|
mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
|
|
|
|
|
|
}%
|
|
|
|
|
|
|
|
|
|
|
|
CCProgram vs %{
|
|
|
|
|
|
precision highp float;
|
|
|
|
|
|
#include <cc-global>
|
|
|
|
|
|
attribute vec3 a_position;
|
|
|
|
|
|
attribute vec2 a_uv0;
|
|
|
|
|
|
varying vec2 v_uv0;
|
|
|
|
|
|
void main () {
|
|
|
|
|
|
gl_Position = cc_matViewProj * vec4(a_position, 1.0);
|
|
|
|
|
|
v_uv0 = a_uv0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}%
|
|
|
|
|
|
|
|
|
|
|
|
CCProgram fs %{
|
|
|
|
|
|
precision highp float;
|
|
|
|
|
|
uniform sampler2D texture;
|
|
|
|
|
|
uniform Constant {
|
|
|
|
|
|
vec4 mainColor;
|
|
|
|
|
|
};
|
|
|
|
|
|
varying vec2 v_uv0;
|
|
|
|
|
|
void main () {
|
|
|
|
|
|
gl_FragColor = mainColor * texture2D(texture, v_uv0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}%`;
|
|
|
|
|
|
|
|
|
|
|
|
Editor.assetdb.create(effectPath, content || defaultEffect, (err) => {
|
|
|
|
|
|
if (err) return callback(err);
|
|
|
|
|
|
Editor.assetdb.refresh(effectPath, (refreshErr) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(refreshErr, refreshErr ? null : `Effect 已创建: ${effectPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "read":
|
|
|
|
|
|
if (!Editor.assetdb.exists(effectPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到 Effect: ${effectPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
const fspath = Editor.assetdb.urlToFspath(effectPath);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = fs.readFileSync(fspath, "utf-8");
|
|
|
|
|
|
callback(null, data);
|
|
|
|
|
|
} catch (e) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`读取 Effect 失败: ${e.message}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "write":
|
|
|
|
|
|
if (!Editor.assetdb.exists(effectPath)) {
|
|
|
|
|
|
return callback(`Effect not found: ${effectPath}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const writeFsPath = Editor.assetdb.urlToFspath(effectPath);
|
|
|
|
|
|
try {
|
|
|
|
|
|
fs.writeFileSync(writeFsPath, content, "utf-8");
|
|
|
|
|
|
Editor.assetdb.refresh(effectPath, (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `Effect 已更新: ${effectPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`更新 Effect 失败: ${e.message}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "delete":
|
|
|
|
|
|
if (!Editor.assetdb.exists(effectPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到 Effect: ${effectPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
Editor.assetdb.delete([effectPath], (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `Effect 已删除: ${effectPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "get_info":
|
|
|
|
|
|
if (Editor.assetdb.exists(effectPath)) {
|
|
|
|
|
|
const uuid = Editor.assetdb.urlToUuid(effectPath);
|
|
|
|
|
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
|
|
|
|
|
callback(null, info || { url: effectPath, uuid: uuid, exists: true });
|
|
|
|
|
|
} else {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`找不到 Effect: ${effectPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
callback(`Unknown shader action: ${action}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 管理材质
|
|
|
|
|
|
manageMaterial(args, callback) {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
const { action, path: matPath, properties = {} } = args;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "create":
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (Editor.assetdb.exists(matPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`材质已存在: ${matPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保父目录存在
|
2026-02-10 09:14:50 +08:00
|
|
|
|
const absolutePath = Editor.assetdb.urlToFspath(matPath);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
const dirPath = pathModule.dirname(absolutePath);
|
|
|
|
|
|
if (!fs.existsSync(dirPath)) {
|
|
|
|
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
|
|
|
|
}
|
2026-02-10 09:14:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 构造 Cocos 2.4.x 材质内容
|
|
|
|
|
|
const materialData = {
|
2026-02-01 13:30:11 +08:00
|
|
|
|
__type__: "cc.Material",
|
|
|
|
|
|
_name: "",
|
|
|
|
|
|
_objFlags: 0,
|
|
|
|
|
|
_native: "",
|
2026-02-10 09:14:50 +08:00
|
|
|
|
_effectAsset: properties.shaderUuid ? { __uuid__: properties.shaderUuid } : null,
|
|
|
|
|
|
_techniqueIndex: 0,
|
|
|
|
|
|
_techniqueData: {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
0: {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
defines: properties.defines || {},
|
2026-02-12 22:55:08 +08:00
|
|
|
|
props: properties.uniforms || {},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-02-10 09:14:50 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Editor.assetdb.create(matPath, JSON.stringify(materialData, null, 2), (err) => {
|
|
|
|
|
|
if (err) return callback(err);
|
|
|
|
|
|
Editor.assetdb.refresh(matPath, (refreshErr) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(refreshErr, refreshErr ? null : `材质已创建: ${matPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
2026-02-01 13:30:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
2026-02-10 09:14:50 +08:00
|
|
|
|
|
|
|
|
|
|
case "update":
|
|
|
|
|
|
if (!Editor.assetdb.exists(matPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到材质: ${matPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
const fspath = Editor.assetdb.urlToFspath(matPath);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = fs.readFileSync(fspath, "utf-8");
|
|
|
|
|
|
const matData = JSON.parse(content);
|
|
|
|
|
|
|
|
|
|
|
|
// 确保结构存在
|
|
|
|
|
|
if (!matData._techniqueData) matData._techniqueData = {};
|
|
|
|
|
|
if (!matData._techniqueData["0"]) matData._techniqueData["0"] = {};
|
|
|
|
|
|
const tech = matData._techniqueData["0"];
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 Shader
|
|
|
|
|
|
if (properties.shaderUuid) {
|
|
|
|
|
|
matData._effectAsset = { __uuid__: properties.shaderUuid };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 Defines
|
|
|
|
|
|
if (properties.defines) {
|
|
|
|
|
|
tech.defines = Object.assign(tech.defines || {}, properties.defines);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 Props/Uniforms
|
|
|
|
|
|
if (properties.uniforms) {
|
|
|
|
|
|
tech.props = Object.assign(tech.props || {}, properties.uniforms);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(fspath, JSON.stringify(matData, null, 2), "utf-8");
|
|
|
|
|
|
Editor.assetdb.refresh(matPath, (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `材质已更新: ${matPath}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`更新材质失败: ${e.message}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
case "delete":
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (!Editor.assetdb.exists(matPath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到材质: ${matPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-10 09:14:50 +08:00
|
|
|
|
Editor.assetdb.delete([matPath], (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `材质已删除: ${matPath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
2026-02-10 09:14:50 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
case "get_info":
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (Editor.assetdb.exists(matPath)) {
|
|
|
|
|
|
const uuid = Editor.assetdb.urlToUuid(matPath);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
callback(null, info || { url: matPath, uuid: uuid, exists: true });
|
2026-02-02 14:34:34 +08:00
|
|
|
|
} else {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`找不到材质: ${matPath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
2026-02-10 09:14:50 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
default:
|
|
|
|
|
|
callback(`Unknown material action: ${action}`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 管理纹理
|
|
|
|
|
|
manageTexture(args, callback) {
|
|
|
|
|
|
const { action, path, properties } = args;
|
|
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "create":
|
|
|
|
|
|
if (Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`纹理已存在: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保父目录存在
|
|
|
|
|
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
|
|
|
|
|
const dirPath = pathModule.dirname(absolutePath);
|
|
|
|
|
|
if (!fs.existsSync(dirPath)) {
|
|
|
|
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
|
|
|
|
}
|
2026-02-10 14:00:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 准备文件内容 (优先使用 properties.content,否则使用默认 1x1)
|
2026-02-12 22:55:08 +08:00
|
|
|
|
let base64Data =
|
|
|
|
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
2026-02-10 14:00:02 +08:00
|
|
|
|
if (properties && properties.content) {
|
|
|
|
|
|
base64Data = properties.content;
|
|
|
|
|
|
}
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const buffer = Buffer.from(base64Data, "base64");
|
2026-02-10 09:14:50 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-10 14:00:02 +08:00
|
|
|
|
// 2. 写入物理文件
|
2026-02-10 09:14:50 +08:00
|
|
|
|
fs.writeFileSync(absolutePath, buffer);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 3. 刷新该资源以生成 Meta
|
|
|
|
|
|
Editor.assetdb.refresh(path, (err, results) => {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (err) return callback(err);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 4. 如果有 9-slice 设置,更新 Meta
|
|
|
|
|
|
if (properties && (properties.border || properties.type)) {
|
|
|
|
|
|
const uuid = Editor.assetdb.urlToUuid(path);
|
2026-02-11 01:09:42 +08:00
|
|
|
|
if (!uuid) return callback(null, `纹理已创建,但未能立即获取 UUID。`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 稍微延迟确保 Meta 已生成
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const meta = Editor.assetdb.loadMeta(uuid);
|
|
|
|
|
|
if (meta) {
|
|
|
|
|
|
let changed = false;
|
|
|
|
|
|
if (properties.type) {
|
|
|
|
|
|
meta.type = properties.type;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置 9-slice (border)
|
|
|
|
|
|
// 注意:Cocos 2.4 纹理 Meta 中 subMetas 下通常有一个与纹理同名的 key (或者主要的一个 key)
|
|
|
|
|
|
if (properties.border) {
|
|
|
|
|
|
// 确保类型是 sprite
|
2026-02-12 22:55:08 +08:00
|
|
|
|
meta.type = "sprite";
|
2026-02-10 14:00:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 找到 SpriteFrame 的 subMeta
|
|
|
|
|
|
const subKeys = Object.keys(meta.subMetas);
|
|
|
|
|
|
if (subKeys.length > 0) {
|
|
|
|
|
|
const subMeta = meta.subMetas[subKeys[0]];
|
|
|
|
|
|
subMeta.border = properties.border; // [top, bottom, left, right]
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (changed) {
|
|
|
|
|
|
Editor.assetdb.saveMeta(uuid, JSON.stringify(meta), (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
if (err) Editor.warn(`保存资源 Meta 失败 ${path}: ${err}`);
|
|
|
|
|
|
callback(null, `纹理已创建并更新 Meta: ${path}`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(null, `纹理已创建: ${path}`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
}, 100);
|
|
|
|
|
|
} else {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(null, `纹理已创建: ${path}`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
}
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`写入纹理文件失败: ${e.message}`);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case "delete":
|
|
|
|
|
|
if (!Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到纹理: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
Editor.assetdb.delete([path], (err) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(err, err ? null : `纹理已删除: ${path}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "get_info":
|
2026-02-02 14:34:34 +08:00
|
|
|
|
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 {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`找不到纹理: ${path}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
2026-02-10 14:00:02 +08:00
|
|
|
|
case "update":
|
|
|
|
|
|
if (!Editor.assetdb.exists(path)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到纹理: ${path}`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
const uuid = Editor.assetdb.urlToUuid(path);
|
|
|
|
|
|
let meta = Editor.assetdb.loadMeta(uuid);
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: 如果 Editor.assetdb.loadMeta 失败 (API 偶尔不稳定),尝试直接读取文件系统中的 .meta 文件
|
|
|
|
|
|
if (!meta) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fspath = Editor.assetdb.urlToFspath(path);
|
|
|
|
|
|
const metaPath = fspath + ".meta";
|
|
|
|
|
|
if (fs.existsSync(metaPath)) {
|
|
|
|
|
|
const metaContent = fs.readFileSync(metaPath, "utf-8");
|
|
|
|
|
|
meta = JSON.parse(metaContent);
|
|
|
|
|
|
addLog("info", `[manage_texture] Loaded meta from fs fallback: ${metaPath}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
addLog("warn", `[manage_texture] Meta fs fallback failed: ${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!meta) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`加载资源 Meta 失败: ${path}`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let changed = false;
|
|
|
|
|
|
if (properties) {
|
|
|
|
|
|
// 更新类型
|
|
|
|
|
|
if (properties.type) {
|
|
|
|
|
|
if (meta.type !== properties.type) {
|
|
|
|
|
|
meta.type = properties.type;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 9-slice border
|
|
|
|
|
|
if (properties.border) {
|
|
|
|
|
|
// 确保类型是 sprite
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (meta.type !== "sprite") {
|
|
|
|
|
|
meta.type = "sprite";
|
2026-02-10 14:00:02 +08:00
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 找到 SubMeta
|
|
|
|
|
|
// Cocos Meta 结构: { subMetas: { "textureName": { ... } } }
|
|
|
|
|
|
// 注意:Cocos 2.x 的 meta 结构因版本而异,旧版可能使用 border: [t, b, l, r] 数组,
|
|
|
|
|
|
// 而新版 (如 2.3.x+) 通常使用 borderTop, borderBottom 等独立字段。
|
|
|
|
|
|
// 此处逻辑实现了兼容性处理。
|
|
|
|
|
|
const subKeys = Object.keys(meta.subMetas);
|
|
|
|
|
|
if (subKeys.length > 0) {
|
|
|
|
|
|
const subMeta = meta.subMetas[subKeys[0]];
|
|
|
|
|
|
const newBorder = properties.border; // [top, bottom, left, right]
|
|
|
|
|
|
|
|
|
|
|
|
// 方式 1: standard array style
|
|
|
|
|
|
if (subMeta.border !== undefined) {
|
|
|
|
|
|
const oldBorder = subMeta.border;
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (
|
|
|
|
|
|
!oldBorder ||
|
2026-02-10 14:00:02 +08:00
|
|
|
|
oldBorder[0] !== newBorder[0] ||
|
|
|
|
|
|
oldBorder[1] !== newBorder[1] ||
|
|
|
|
|
|
oldBorder[2] !== newBorder[2] ||
|
2026-02-12 22:55:08 +08:00
|
|
|
|
oldBorder[3] !== newBorder[3]
|
|
|
|
|
|
) {
|
2026-02-10 14:00:02 +08:00
|
|
|
|
subMeta.border = newBorder;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 方式 2: individual fields style (common in 2.3.x)
|
|
|
|
|
|
else if (subMeta.borderTop !== undefined) {
|
|
|
|
|
|
// top, bottom, left, right
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (
|
|
|
|
|
|
subMeta.borderTop !== newBorder[0] ||
|
2026-02-10 14:00:02 +08:00
|
|
|
|
subMeta.borderBottom !== newBorder[1] ||
|
|
|
|
|
|
subMeta.borderLeft !== newBorder[2] ||
|
2026-02-12 22:55:08 +08:00
|
|
|
|
subMeta.borderRight !== newBorder[3]
|
|
|
|
|
|
) {
|
2026-02-10 14:00:02 +08:00
|
|
|
|
subMeta.borderTop = newBorder[0];
|
|
|
|
|
|
subMeta.borderBottom = newBorder[1];
|
|
|
|
|
|
subMeta.borderLeft = newBorder[2];
|
|
|
|
|
|
subMeta.borderRight = newBorder[3];
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 方式 3: 如果都没有,尝试写入 individual fields
|
|
|
|
|
|
else {
|
|
|
|
|
|
subMeta.borderTop = newBorder[0];
|
|
|
|
|
|
subMeta.borderBottom = newBorder[1];
|
|
|
|
|
|
subMeta.borderLeft = newBorder[2];
|
|
|
|
|
|
subMeta.borderRight = newBorder[3];
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (changed) {
|
|
|
|
|
|
// 使用 saveMeta 或者 fs 写入
|
|
|
|
|
|
// 为了安全,如果 loadMeta 失败了,safeMeta 可能也会失败,所以这里尽量用 API,不行再 fallback (暂且只用 API)
|
|
|
|
|
|
Editor.assetdb.saveMeta(uuid, JSON.stringify(meta), (err) => {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
if (err) return callback(`保存 Meta 失败: ${err}`);
|
|
|
|
|
|
callback(null, `纹理已更新: ${path}`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(null, `资源不需要更新: ${path}`);
|
2026-02-10 14:00:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
default:
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(`未知的纹理操作类型: ${action}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 对文件应用一系列精确的文本编辑操作
|
|
|
|
|
|
* @param {Object} args 参数
|
|
|
|
|
|
* @param {Function} callback 完成回调
|
|
|
|
|
|
*/
|
2026-02-01 13:30:11 +08:00
|
|
|
|
applyTextEdits(args, callback) {
|
|
|
|
|
|
const { filePath, edits } = args;
|
|
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 1. 获取文件系统路径
|
|
|
|
|
|
const fspath = Editor.assetdb.urlToFspath(filePath);
|
|
|
|
|
|
if (!fspath) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback(`找不到文件或 URL 无效: ${filePath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const fs = require("fs");
|
|
|
|
|
|
if (!fs.existsSync(fspath)) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback(`文件不存在: ${fspath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 2. 读取
|
|
|
|
|
|
let updatedContent = fs.readFileSync(fspath, "utf-8");
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 应用编辑
|
|
|
|
|
|
// 必须按倒序应用编辑,否则后续编辑的位置会偏移 (假设edits未排序,这里简单处理,实际上LSP通常建议客户端倒序应用或计算偏移)
|
|
|
|
|
|
// 这里假设edits已经按照位置排序或者用户负责,如果需要严谨,应先按 start/position 倒序排序
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 简单排序保险:
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const sortedEdits = [...edits].sort((a, b) => {
|
|
|
|
|
|
const posA = a.position !== undefined ? a.position : a.start;
|
|
|
|
|
|
const posB = b.position !== undefined ? b.position : b.start;
|
2026-02-03 20:04:45 +08:00
|
|
|
|
return posB - posA; // 从大到小
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
sortedEdits.forEach((edit) => {
|
|
|
|
|
|
switch (edit.type) {
|
|
|
|
|
|
case "insert":
|
|
|
|
|
|
updatedContent =
|
2026-02-12 22:55:08 +08:00
|
|
|
|
updatedContent.slice(0, edit.position) + edit.text + updatedContent.slice(edit.position);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
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;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 4. 写入
|
|
|
|
|
|
fs.writeFileSync(fspath, updatedContent, "utf-8");
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 5. 通知编辑器资源变化 (重要)
|
|
|
|
|
|
Editor.assetdb.refresh(filePath, (err) => {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
if (err) addLog("warn", `刷新失败 ${filePath}: ${err}`);
|
|
|
|
|
|
callback(null, `文本编辑已应用: ${filePath}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
});
|
2026-02-02 14:34:34 +08:00
|
|
|
|
} catch (err) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(`操作失败: ${err.message}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
// 读取控制台
|
|
|
|
|
|
readConsole(args, callback) {
|
|
|
|
|
|
const { limit, type } = args;
|
|
|
|
|
|
let filteredOutput = logBuffer;
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
if (type) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
// [优化] 支持别名映射
|
|
|
|
|
|
const targetType = type === "log" ? "info" : type;
|
|
|
|
|
|
filteredOutput = filteredOutput.filter((item) => item.type === targetType);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-01-31 19:36:55 +08:00
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
if (limit) {
|
|
|
|
|
|
filteredOutput = filteredOutput.slice(-limit);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
callback(null, filteredOutput);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 10:14:17 +08:00
|
|
|
|
executeMenuItem(args, callback) {
|
|
|
|
|
|
const { menuPath } = args;
|
|
|
|
|
|
if (!menuPath) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
return callback("菜单路径是必填项");
|
2026-02-02 10:14:17 +08:00
|
|
|
|
}
|
2026-02-11 00:36:56 +08:00
|
|
|
|
addLog("info", `执行菜单项: ${menuPath}`);
|
2026-02-02 10:14:17 +08:00
|
|
|
|
|
2026-02-03 19:55:51 +08:00
|
|
|
|
// 菜单项映射表 (Cocos Creator 2.4.x IPC)
|
|
|
|
|
|
// 参考: IPC_MESSAGES.md
|
|
|
|
|
|
const menuMap = {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
"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-nodes",
|
|
|
|
|
|
Delete: "scene:delete-nodes",
|
|
|
|
|
|
delete: "scene:delete-nodes",
|
2026-02-03 19:55:51 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 特殊处理 delete-node:UUID 格式
|
|
|
|
|
|
if (menuPath.startsWith("delete-node:")) {
|
|
|
|
|
|
const uuid = menuPath.split(":")[1];
|
|
|
|
|
|
if (uuid) {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "delete-node", { uuid }, (err, result) => {
|
2026-02-03 20:04:45 +08:00
|
|
|
|
if (err) callback(err);
|
|
|
|
|
|
else callback(null, result || `Node ${uuid} deleted via scene script`);
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 19:55:51 +08:00
|
|
|
|
if (menuMap[menuPath]) {
|
|
|
|
|
|
const ipcMsg = menuMap[menuPath];
|
|
|
|
|
|
try {
|
2026-02-10 00:38:38 +08:00
|
|
|
|
// 获取当前选中的节点进行删除(如果该消息是删除操作)
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (ipcMsg === "scene:delete-nodes") {
|
2026-02-10 00:38:38 +08:00
|
|
|
|
const selection = Editor.Selection.curSelection("node");
|
|
|
|
|
|
if (selection.length > 0) {
|
|
|
|
|
|
Editor.Ipc.sendToMain(ipcMsg, selection);
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(null, `菜单动作已触发: ${menuPath} -> ${ipcMsg} (影响 ${selection.length} 个节点)`);
|
2026-02-10 00:38:38 +08:00
|
|
|
|
} else {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback("没有选中任何节点进行删除");
|
2026-02-10 00:38:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Editor.Ipc.sendToMain(ipcMsg);
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(null, `菜单动作已触发: ${menuPath} -> ${ipcMsg}`);
|
2026-02-10 00:38:38 +08:00
|
|
|
|
}
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} catch (err) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(`执行 IPC ${ipcMsg} 失败: ${err.message}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-02 10:14:17 +08:00
|
|
|
|
} else {
|
2026-02-03 19:55:51 +08:00
|
|
|
|
// 对于未在映射表中的菜单,尝试通用的 menu:click (虽然不一定有效)
|
|
|
|
|
|
// 或者直接返回不支持的警告
|
2026-02-11 00:36:56 +08:00
|
|
|
|
addLog("warn", `支持映射表中找不到菜单项 '${menuPath}'。尝试通过 legacy 模式执行。`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 尝试通用调用
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 注意:Cocos Creator 2.x 的 menu:click 通常需要 Electron 菜单 ID,而不只是路径
|
|
|
|
|
|
// 这里做个尽力而为的尝试
|
2026-02-12 22:55:08 +08:00
|
|
|
|
Editor.Ipc.sendToMain("menu:click", menuPath);
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(null, `通用菜单动作已发送: ${menuPath} (仅支持项保证成功)`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} catch (e) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(`执行菜单项失败: ${menuPath}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-02 10:14:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-01 13:30:11 +08:00
|
|
|
|
// 验证脚本
|
|
|
|
|
|
validateScript(args, callback) {
|
|
|
|
|
|
const { filePath } = args;
|
|
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 1. 获取文件系统路径
|
|
|
|
|
|
const fspath = Editor.assetdb.urlToFspath(filePath);
|
|
|
|
|
|
if (!fspath) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`找不到文件或 URL 无效: ${filePath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 2. 检查文件是否存在
|
|
|
|
|
|
if (!fs.existsSync(fspath)) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(`文件不存在: ${fspath}`);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
// 3. 读取内容并验证
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = fs.readFileSync(fspath, "utf-8");
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-03 19:55:51 +08:00
|
|
|
|
// 检查空文件
|
|
|
|
|
|
if (!content || content.trim().length === 0) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
return callback(null, { valid: false, message: "脚本内容为空" });
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对于 JavaScript 脚本,使用 Function 构造器进行语法验证
|
2026-02-02 14:34:34 +08:00
|
|
|
|
if (filePath.endsWith(".js")) {
|
|
|
|
|
|
const wrapper = `(function() { ${content} })`;
|
|
|
|
|
|
try {
|
2026-02-03 19:55:51 +08:00
|
|
|
|
new Function(wrapper);
|
2026-02-11 01:09:42 +08:00
|
|
|
|
callback(null, { valid: true, message: "JavaScript 语法验证通过" });
|
2026-02-02 14:34:34 +08:00
|
|
|
|
} catch (syntaxErr) {
|
|
|
|
|
|
return callback(null, { valid: false, message: syntaxErr.message });
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-03 19:55:51 +08:00
|
|
|
|
// 对于 TypeScript,由于没有内置 TS 编译器,我们进行基础的"防呆"检查
|
|
|
|
|
|
// 并明确告知用户无法进行完整编译验证
|
|
|
|
|
|
else if (filePath.endsWith(".ts")) {
|
|
|
|
|
|
// 简单的正则表达式检查:是否有非法字符或明显错误结构 (示例)
|
|
|
|
|
|
// 这里暂时只做简单的括号匹配检查或直接通过,但给出一个 Warning
|
|
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 检查是否有 class 定义 (简单的启发式检查)
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (
|
|
|
|
|
|
!content.includes("class ") &&
|
|
|
|
|
|
!content.includes("interface ") &&
|
|
|
|
|
|
!content.includes("enum ") &&
|
|
|
|
|
|
!content.includes("export ")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return callback(null, {
|
|
|
|
|
|
valid: true,
|
|
|
|
|
|
message:
|
|
|
|
|
|
"警告: TypeScript 文件似乎缺少标准定义 (class/interface/export),但由于缺少编译器,已跳过基础语法检查。",
|
|
|
|
|
|
});
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callback(null, {
|
|
|
|
|
|
valid: true,
|
|
|
|
|
|
message: "TypeScript 基础检查通过。(完整编译验证需要通过编辑器构建流程)",
|
|
|
|
|
|
});
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(null, { valid: true, message: "未知的脚本类型,跳过验证。" });
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
} catch (err) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(null, { valid: false, message: `读取错误: ${err.message}` });
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 暴露给 MCP 或面板的 API 封装
|
2026-01-29 14:53:06 +08:00
|
|
|
|
messages: {
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"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);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-01-29 14:53:06 +08:00
|
|
|
|
"open-test-panel"() {
|
|
|
|
|
|
Editor.Panel.open("mcp-bridge");
|
|
|
|
|
|
},
|
2026-02-02 14:34:34 +08:00
|
|
|
|
|
2026-01-29 14:53:06 +08:00
|
|
|
|
"toggle-server"(event, port) {
|
|
|
|
|
|
if (serverConfig.active) this.stopServer();
|
|
|
|
|
|
else this.startServer(port);
|
|
|
|
|
|
},
|
|
|
|
|
|
"clear-logs"() {
|
|
|
|
|
|
logBuffer = [];
|
2026-02-11 00:36:56 +08:00
|
|
|
|
addLog("info", "日志已清理");
|
2026-01-29 14:53:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 修改场景中的节点(需要通过 scene-script)
|
|
|
|
|
|
"set-node-property"(event, args) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
addLog("mcp", `设置节点属性: ${args.name} (${args.type})`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
// 确保第一个参数 '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);
|
2026-01-29 13:47:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-01-29 14:53:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
"create-node"(event, args) {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
addLog("mcp", `创建节点: ${args.name} (${args.type})`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => {
|
2026-02-11 01:09:42 +08:00
|
|
|
|
if (err) addLog("error", `创建节点失败: ${err}`);
|
|
|
|
|
|
else addLog("success", `节点已创建: ${result}`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
event.reply(err, result);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
"get-server-state"(event) {
|
|
|
|
|
|
let profile = this.getProfile();
|
|
|
|
|
|
event.reply(null, {
|
|
|
|
|
|
config: serverConfig,
|
|
|
|
|
|
logs: logBuffer,
|
|
|
|
|
|
autoStart: profile.get("auto-start"), // 返回自动启动状态
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-01-29 13:47:38 +08:00
|
|
|
|
|
2026-01-29 14:53:06 +08:00
|
|
|
|
"set-auto-start"(event, value) {
|
|
|
|
|
|
this.getProfile().set("auto-start", value);
|
|
|
|
|
|
this.getProfile().save();
|
2026-02-11 00:36:56 +08:00
|
|
|
|
addLog("info", `自动启动已设置为: ${value}`);
|
2026-01-29 14:53:06 +08:00
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-02 14:34:34 +08:00
|
|
|
|
"inspect-apis"() {
|
|
|
|
|
|
addLog("info", "[API Inspector] Starting DEEP inspection...");
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 获取函数参数的辅助函数
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const getArgs = (func) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const str = func.toString();
|
|
|
|
|
|
const match = str.match(/function\s.*?\(([^)]*)\)/) || str.match(/.*?\(([^)]*)\)/);
|
|
|
|
|
|
if (match) {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
return match[1]
|
|
|
|
|
|
.split(",")
|
|
|
|
|
|
.map((arg) => arg.trim())
|
|
|
|
|
|
.filter((a) => a)
|
|
|
|
|
|
.join(", ");
|
2026-02-02 14:34:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
return `${func.length} args`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return "?";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 检查对象的辅助函数
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const inspectObj = (name, obj) => {
|
|
|
|
|
|
if (!obj) return { name, exists: false };
|
|
|
|
|
|
const props = {};
|
|
|
|
|
|
const proto = Object.getPrototypeOf(obj);
|
|
|
|
|
|
|
|
|
|
|
|
// 组合自身属性和原型属性
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const allKeys = new Set([
|
|
|
|
|
|
...Object.getOwnPropertyNames(obj),
|
|
|
|
|
|
...Object.getOwnPropertyNames(proto || {}),
|
|
|
|
|
|
]);
|
2026-02-02 14:34:34 +08:00
|
|
|
|
|
2026-02-12 22:55:08 +08:00
|
|
|
|
allKeys.forEach((key) => {
|
2026-02-03 20:04:45 +08:00
|
|
|
|
if (key.startsWith("_")) return; // 跳过私有属性
|
2026-02-02 14:34:34 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const val = obj[key];
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (typeof val === "function") {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
props[key] = `func(${getArgs(val)})`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
props[key] = typeof val;
|
|
|
|
|
|
}
|
2026-02-12 22:55:08 +08:00
|
|
|
|
} catch (e) {}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
return { name, exists: true, props };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 1. 检查标准对象
|
2026-02-02 14:34:34 +08:00
|
|
|
|
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,
|
2026-02-12 22:55:08 +08:00
|
|
|
|
"Editor.remote": Editor.remote,
|
2026-02-02 14:34:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const report = {};
|
2026-02-12 22:55:08 +08:00
|
|
|
|
Object.keys(standardObjects).forEach((key) => {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
report[key] = inspectObj(key, standardObjects[key]);
|
|
|
|
|
|
});
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 2. 检查特定论坛提到的 API
|
2026-02-02 14:34:34 +08:00
|
|
|
|
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",
|
2026-02-12 22:55:08 +08:00
|
|
|
|
"Editor.Selection.curGlobalActivate",
|
2026-02-02 14:34:34 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const checklistResults = {};
|
2026-02-12 22:55:08 +08:00
|
|
|
|
forumChecklist.forEach((path) => {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const parts = path.split(".");
|
2026-02-03 20:04:45 +08:00
|
|
|
|
let curr = global; // 在主进程中,Editor 是全局的
|
2026-02-02 14:34:34 +08:00
|
|
|
|
let exists = true;
|
|
|
|
|
|
for (const part of parts) {
|
|
|
|
|
|
if (curr && curr[part]) {
|
|
|
|
|
|
curr = curr[part];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
exists = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-12 22:55:08 +08:00
|
|
|
|
checklistResults[path] = exists
|
|
|
|
|
|
? typeof curr === "function"
|
|
|
|
|
|
? `Available(${getArgs(curr)})`
|
|
|
|
|
|
: "Available"
|
|
|
|
|
|
: "Missing";
|
2026-02-02 14:34:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
addLog("info", `[API Inspector] Standard Objects:\n${JSON.stringify(report, null, 2)}`);
|
|
|
|
|
|
addLog("info", `[API Inspector] Forum Checklist:\n${JSON.stringify(checklistResults, null, 2)}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 3. 检查内置包 IPC 消息
|
2026-02-02 14:34:34 +08:00
|
|
|
|
const ipcReport = {};
|
|
|
|
|
|
const builtinPackages = ["scene", "builder", "assets"]; // 核心内置包
|
|
|
|
|
|
const fs = require("fs");
|
|
|
|
|
|
|
2026-02-12 22:55:08 +08:00
|
|
|
|
builtinPackages.forEach((pkgName) => {
|
2026-02-01 13:30:11 +08:00
|
|
|
|
try {
|
2026-02-02 14:34:34 +08:00
|
|
|
|
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";
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
2026-02-02 14:34:34 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
ipcReport[pkgName] = `Error: ${e.message}`;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-02-02 14:34:34 +08:00
|
|
|
|
|
|
|
|
|
|
addLog("info", `[API Inspector] Built-in IPC Messages:\n${JSON.stringify(ipcReport, null, 2)}`);
|
|
|
|
|
|
},
|
2026-02-01 13:30:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 全局文件搜索
|
2026-02-11 00:36:56 +08:00
|
|
|
|
// 项目搜索 (升级版 find_in_file)
|
|
|
|
|
|
searchProject(args, callback) {
|
|
|
|
|
|
const { query, useRegex, path: searchPath, matchType, extensions } = args;
|
|
|
|
|
|
|
|
|
|
|
|
// 默认值
|
|
|
|
|
|
const rootPathUrl = searchPath || "db://assets";
|
|
|
|
|
|
const rootPath = Editor.assetdb.urlToFspath(rootPathUrl);
|
|
|
|
|
|
|
|
|
|
|
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
|
|
|
|
return callback(`Invalid search path: ${rootPathUrl}`);
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-11 00:36:56 +08:00
|
|
|
|
const mode = matchType || "content"; // content, file_name, dir_name
|
2026-02-01 13:30:11 +08:00
|
|
|
|
const validExtensions = extensions || [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"];
|
|
|
|
|
|
const results = [];
|
2026-02-11 00:36:56 +08:00
|
|
|
|
const MAX_RESULTS = 500;
|
|
|
|
|
|
|
|
|
|
|
|
let regex = null;
|
|
|
|
|
|
if (useRegex) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
regex = new RegExp(query);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return callback(`Invalid regex: ${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const checkMatch = (text) => {
|
|
|
|
|
|
if (useRegex) return regex.test(text);
|
|
|
|
|
|
return text.includes(query);
|
|
|
|
|
|
};
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const walk = (dir) => {
|
|
|
|
|
|
if (results.length >= MAX_RESULTS) return;
|
|
|
|
|
|
|
|
|
|
|
|
const list = fs.readdirSync(dir);
|
|
|
|
|
|
list.forEach((file) => {
|
|
|
|
|
|
if (results.length >= MAX_RESULTS) return;
|
|
|
|
|
|
|
2026-02-11 00:36:56 +08:00
|
|
|
|
// 忽略隐藏文件和常用忽略目录
|
2026-02-12 22:55:08 +08:00
|
|
|
|
if (
|
|
|
|
|
|
file.startsWith(".") ||
|
|
|
|
|
|
file === "node_modules" ||
|
|
|
|
|
|
file === "bin" ||
|
|
|
|
|
|
file === "local" ||
|
|
|
|
|
|
file === "library" ||
|
|
|
|
|
|
file === "temp"
|
|
|
|
|
|
)
|
|
|
|
|
|
return;
|
2026-02-01 13:30:11 +08:00
|
|
|
|
|
2026-02-10 09:14:50 +08:00
|
|
|
|
const filePath = pathModule.join(dir, file);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
const stat = fs.statSync(filePath);
|
|
|
|
|
|
|
|
|
|
|
|
if (stat && stat.isDirectory()) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
// 目录名搜索
|
|
|
|
|
|
if (mode === "dir_name") {
|
|
|
|
|
|
if (checkMatch(file)) {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const relativePath = pathModule.relative(
|
|
|
|
|
|
Editor.assetdb.urlToFspath("db://assets"),
|
|
|
|
|
|
filePath,
|
|
|
|
|
|
);
|
|
|
|
|
|
const dbPath = "db://assets/" + relativePath.split(pathModule.sep).join("/");
|
2026-02-11 00:36:56 +08:00
|
|
|
|
results.push({
|
|
|
|
|
|
filePath: dbPath,
|
|
|
|
|
|
type: "directory",
|
2026-02-12 22:55:08 +08:00
|
|
|
|
name: file,
|
2026-02-11 00:36:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 递归
|
2026-02-01 13:30:11 +08:00
|
|
|
|
walk(filePath);
|
|
|
|
|
|
} else {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
const ext = pathModule.extname(file).toLowerCase();
|
2026-02-11 00:36:56 +08:00
|
|
|
|
|
|
|
|
|
|
// 文件名搜索
|
|
|
|
|
|
if (mode === "file_name") {
|
|
|
|
|
|
if (validExtensions && validExtensions.length > 0 && !validExtensions.includes(ext)) {
|
|
|
|
|
|
// 如果指定了后缀,则必须匹配
|
|
|
|
|
|
// (Logic kept simple: if extensions provided, filter by them. If not provided, search all files or default list?)
|
|
|
|
|
|
// Let's stick to validExtensions for file_name search too to avoid noise, or maybe allow all if extensions is explicitly null?
|
|
|
|
|
|
// Schema default is null. Let's start with checkMatch(file) directly if no extensions provided.
|
|
|
|
|
|
// Actually validExtensions has a default list. Let's respect it if it was default, but for file_name maybe we want all?
|
|
|
|
|
|
// Let's use validExtensions only if mode is content. For file_name, usually we search everything unless filtered.
|
|
|
|
|
|
// But to be safe and consistent with previous find_in_file, let's respect validExtensions.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 简化逻辑:对文件名搜索,也检查后缀(如果用户未传则用默认列表)
|
|
|
|
|
|
if (validExtensions.includes(ext)) {
|
|
|
|
|
|
if (checkMatch(file)) {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const relativePath = pathModule.relative(
|
|
|
|
|
|
Editor.assetdb.urlToFspath("db://assets"),
|
|
|
|
|
|
filePath,
|
|
|
|
|
|
);
|
|
|
|
|
|
const dbPath = "db://assets/" + relativePath.split(pathModule.sep).join("/");
|
2026-02-11 00:36:56 +08:00
|
|
|
|
results.push({
|
|
|
|
|
|
filePath: dbPath,
|
|
|
|
|
|
type: "file",
|
2026-02-12 22:55:08 +08:00
|
|
|
|
name: file,
|
2026-02-11 00:36:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果需要搜索非文本文件(如 .png),可以传入 extensions=['.png']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 内容搜索
|
|
|
|
|
|
else if (mode === "content") {
|
|
|
|
|
|
if (validExtensions.includes(ext)) {
|
|
|
|
|
|
try {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const content = fs.readFileSync(filePath, "utf8");
|
|
|
|
|
|
const lines = content.split("\n");
|
2026-02-11 00:36:56 +08:00
|
|
|
|
lines.forEach((line, index) => {
|
|
|
|
|
|
if (results.length >= MAX_RESULTS) return;
|
|
|
|
|
|
if (checkMatch(line)) {
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const relativePath = pathModule.relative(
|
|
|
|
|
|
Editor.assetdb.urlToFspath("db://assets"),
|
|
|
|
|
|
filePath,
|
|
|
|
|
|
);
|
|
|
|
|
|
const dbPath =
|
|
|
|
|
|
"db://assets/" + relativePath.split(pathModule.sep).join("/");
|
2026-02-11 00:36:56 +08:00
|
|
|
|
results.push({
|
|
|
|
|
|
filePath: dbPath,
|
|
|
|
|
|
line: index + 1,
|
2026-02-12 22:55:08 +08:00
|
|
|
|
content: line.trim(),
|
2026-02-11 00:36:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// Skip read error
|
|
|
|
|
|
}
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-11 00:36:56 +08:00
|
|
|
|
walk(rootPath);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
callback(null, results);
|
|
|
|
|
|
} catch (err) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(`Search project failed: ${err.message}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 管理撤销/重做
|
|
|
|
|
|
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]
|
2026-02-11 01:09:42 +08:00
|
|
|
|
// 注意:在 2.4.x 中,undo-record 通常需要一个有效的 uuid
|
|
|
|
|
|
// 如果没有提供 uuid,不应将 description 作为 ID 发送,否则会报 Unknown object to record
|
|
|
|
|
|
addLog("info", `开始撤销组: ${description || "MCP 动作"}`);
|
|
|
|
|
|
// 如果有参数包含 id,则记录该节点
|
|
|
|
|
|
if (args.id) {
|
|
|
|
|
|
Editor.Ipc.sendToPanel("scene", "scene:undo-record", args.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
callback(null, `撤销组已启动: ${description || "MCP 动作"}`);
|
2026-02-01 13:30:11 +08:00
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-04 01:57:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取文件 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);
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const hashSum = crypto.createHash("sha256");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
hashSum.update(fileBuffer);
|
2026-02-12 22:55:08 +08:00
|
|
|
|
const sha = hashSum.digest("hex");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
callback(null, { path: url, sha: sha });
|
|
|
|
|
|
} catch (err) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
callback(`计算 SHA 失败: ${err.message}`);
|
2026-02-04 01:57:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 管理动画
|
|
|
|
|
|
manageAnimation(args, callback) {
|
|
|
|
|
|
// 转发给场景脚本处理
|
2026-02-12 22:55:08 +08:00
|
|
|
|
callSceneScriptWithTimeout("mcp-bridge", "manage-animation", args, callback);
|
2026-02-04 01:57:12 +08:00
|
|
|
|
},
|
2026-01-29 13:47:38 +08:00
|
|
|
|
};
|