feat: 完成第一阶段核心功能实现\n\n- 添加 manage_components 工具\n- 添加 manage_script 工具(默认创建TS脚本)\n- 添加 batch_execute 工具\n- 添加 manage_asset 工具\n- 修复面板布局问题\n- 添加默认父目录创建功能\n- 更新 README 文档\n- 创建 DEVELOPMENT 开发文档

This commit is contained in:
火焰库拉
2026-01-31 16:48:21 +08:00
parent b5f745446c
commit 3b2e78eee7
6 changed files with 1370 additions and 34 deletions

256
main.js
View File

@@ -133,7 +133,7 @@ const getToolsList = () => {
},
{
name: "open_scene",
description: "在编辑器中打开指定的场景文件",
description: "打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒再进行节点创建或保存操作。",
inputSchema: {
type: "object",
properties: {
@@ -165,8 +165,74 @@ const getToolsList = () => {
required: ["name"],
},
},
{
name: "manage_components",
description: "管理节点组件",
inputSchema: {
type: "object",
properties: {
nodeId: { type: "string", description: "节点 UUID" },
action: { type: "string", enum: ["add", "remove", "get"], description: "操作类型" },
componentType: { type: "string", description: "组件类型,如 cc.Sprite" },
componentId: { type: "string", description: "组件 ID (用于 remove 操作)" },
properties: { type: "object", description: "组件属性 (用于 add 操作)" },
},
required: ["nodeId", "action"],
},
},
{
name: "manage_script",
description: "管理脚本文件",
inputSchema: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "delete", "read", "write"], description: "操作类型" },
path: { type: "string", description: "脚本路径,如 db://assets/scripts/NewScript.js" },
content: { type: "string", description: "脚本内容 (用于 create 和 write 操作)" },
name: { type: "string", description: "脚本名称 (用于 create 操作)" },
},
required: ["action", "path"],
},
},
{
name: "batch_execute",
description: "批处理执行多个操作",
inputSchema: {
type: "object",
properties: {
operations: {
type: "array",
items: {
type: "object",
properties: {
tool: { type: "string", description: "工具名称" },
params: { type: "object", description: "工具参数" },
},
required: ["tool", "params"],
},
description: "操作列表",
},
},
required: ["operations"],
},
},
{
name: "manage_asset",
description: "管理资源",
inputSchema: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "delete", "move", "get_info"], description: "操作类型" },
path: { type: "string", description: "资源路径,如 db://assets/textures" },
targetPath: { type: "string", description: "目标路径 (用于 move 操作)" },
content: { type: "string", description: "资源内容 (用于 create 操作)" },
},
required: ["action", "path"],
},
},
];
};
let isSceneBusy = false;
module.exports = {
"scene-script": "scene-script.js",
@@ -279,6 +345,9 @@ module.exports = {
// 统一处理逻辑,方便日志记录
handleMcpCall(name, args, callback) {
if (isSceneBusy && (name === "save_scene" || name === "create_node")) {
return callback("Editor is busy (Processing Scene), please wait a moment.");
}
switch (name) {
case "get_selected_node":
const ids = Editor.Selection.curSelection("node");
@@ -299,8 +368,18 @@ module.exports = {
break;
case "save_scene":
Editor.Ipc.sendToMain("scene:save-scene");
callback(null, "Scene saved successfully");
isSceneBusy = true;
addLog("info", "Preparing to save scene... Waiting for UI sync.");
// 强制延迟保存,防止死锁
setTimeout(() => {
Editor.Ipc.sendToMain("scene:save-scene");
addLog("info", "Executing Safe Save...");
setTimeout(() => {
isSceneBusy = false;
addLog("info", "Safe Save completed.");
callback(null, "Scene saved successfully.");
}, 1000);
}, 500);
break;
case "get_scene_hierarchy":
@@ -328,11 +407,16 @@ module.exports = {
break;
case "open_scene":
isSceneBusy = true; // 锁定
const openUuid = Editor.assetdb.urlToUuid(args.url);
if (openUuid) {
Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid);
callback(null, `Success: Opening scene ${args.url}`);
setTimeout(() => {
isSceneBusy = false;
callback(null, `Success: Opening scene ${args.url}`);
}, 2000);
} else {
isSceneBusy = false;
callback(`Could not find asset with URL ${args.url}`);
}
break;
@@ -341,11 +425,175 @@ module.exports = {
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback);
break;
case "manage_components":
Editor.Scene.callSceneScript("mcp-bridge", "manage-components", args, callback);
break;
case "manage_script":
this.manageScript(args, callback);
break;
case "batch_execute":
this.batchExecute(args, callback);
break;
case "manage_asset":
this.manageAsset(args, callback);
break;
default:
callback(`Unknown tool: ${name}`);
break;
}
},
// 管理脚本文件
manageScript(args, callback) {
const { action, path, content } = args;
switch (action) {
case "create":
if (Editor.assetdb.exists(path)) {
return callback(`Script already exists at ${path}`);
}
// 确保父目录存在
const fs = require('fs');
const pathModule = require('path');
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
Editor.assetdb.create(path, content || `const { ccclass, property } = cc._decorator;
@ccclass
export default class NewScript extends cc.Component {
@property(cc.Label)
label: cc.Label = null;
@property
text: string = 'hello';
// LIFE-CYCLE CALLBACKS:
onLoad () {}
start () {}
update (dt) {}
}`, (err) => {
callback(err, err ? null : `Script created at ${path}`);
});
break;
case "delete":
if (!Editor.assetdb.exists(path)) {
return callback(`Script not found at ${path}`);
}
Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Script deleted at ${path}`);
});
break;
case "read":
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
if (err) {
return callback(`Failed to get script info: ${err}`);
}
Editor.assetdb.loadAny(path, (err, content) => {
callback(err, err ? null : content);
});
});
break;
case "write":
Editor.assetdb.create(path, content, (err) => {
callback(err, err ? null : `Script updated at ${path}`);
});
break;
default:
callback(`Unknown script action: ${action}`);
break;
}
},
// 批处理执行
batchExecute(args, callback) {
const { operations } = args;
const results = [];
let completed = 0;
if (!operations || operations.length === 0) {
return callback("No operations provided");
}
operations.forEach((operation, index) => {
this.handleMcpCall(operation.tool, operation.params, (err, result) => {
results[index] = { tool: operation.tool, error: err, result: result };
completed++;
if (completed === operations.length) {
callback(null, results);
}
});
});
},
// 管理资源
manageAsset(args, callback) {
const { action, path, targetPath, content } = args;
switch (action) {
case "create":
if (Editor.assetdb.exists(path)) {
return callback(`Asset already exists at ${path}`);
}
// 确保父目录存在
const fs = require('fs');
const pathModule = require('path');
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
Editor.assetdb.create(path, content || '', (err) => {
callback(err, err ? null : `Asset created at ${path}`);
});
break;
case "delete":
if (!Editor.assetdb.exists(path)) {
return callback(`Asset not found at ${path}`);
}
Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Asset deleted at ${path}`);
});
break;
case "move":
if (!Editor.assetdb.exists(path)) {
return callback(`Asset not found at ${path}`);
}
if (Editor.assetdb.exists(targetPath)) {
return callback(`Target asset already exists at ${targetPath}`);
}
Editor.assetdb.move(path, targetPath, (err) => {
callback(err, err ? null : `Asset moved from ${path} to ${targetPath}`);
});
break;
case "get_info":
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
callback(err, err ? null : info);
});
break;
default:
callback(`Unknown asset action: ${action}`);
break;
}
},
// 暴露给 MCP 或面板的 API 封装
messages: {
"open-test-panel"() {