feat: 增强 MCP Bridge 工具功能并完成全量中文化与文档更新

- 新增 manage_shader 工具,支持着色器资源的完整生命周期管理。
- 增强 manage_material 工具,深度适配 Cocos Creator 2.4.x 材质结构并支持增量更新。
- 优化 manage_components 工具,支持资源数组(如 materials)的智能异步加载与 UI 同步。
- 修复了材质 Inspector 显示异常、场景克隆崩溃及工具路由缺失等关键 Bug。
- 完成源码注释、错误提示及 MCP 工具描述的 100% 简体中文化。
- 更新 README.md 并新增 UPDATE_LOG.md 技术修复日志。
This commit is contained in:
火焰库拉
2026-02-10 09:14:50 +08:00
parent 23c6ea13f9
commit 256c91e9f5
4 changed files with 380 additions and 102 deletions

335
main.js
View File

@@ -2,7 +2,7 @@
const { IpcManager } = require("./dist/IpcManager");
const http = require("http");
const path = require("path");
const pathModule = require("path");
const fs = require("fs");
const crypto = require("crypto");
@@ -329,13 +329,21 @@ const getToolsList = () => {
},
{
name: "manage_material",
description: `${globalPrecautions} 管理材质`,
description: `${globalPrecautions} 管理材质。支持创建、获取信息以及更新 Shader、Defines 和 Uniforms 参数。`,
inputSchema: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "delete", "get_info"], description: "操作类型" },
action: { type: "string", enum: ["create", "delete", "get_info", "update"], description: "操作类型" },
path: { type: "string", description: "材质路径,如 db://assets/materials/NewMaterial.mat" },
properties: { type: "object", description: "材质属性" },
properties: {
type: "object",
description: "材质属性 (add/update 操作使用)",
properties: {
shaderUuid: { type: "string", description: "关联的 Shader (Effect) UUID" },
defines: { type: "object", description: "预编译宏定义" },
uniforms: { type: "object", description: "Uniform 参数列表" }
}
},
},
required: ["action", "path"],
},
@@ -353,6 +361,19 @@ const getToolsList = () => {
required: ["action", "path"],
},
},
{
name: "manage_shader",
description: `${globalPrecautions} 管理着色器 (Effect)。支持创建、读取、更新、删除和获取信息。`,
inputSchema: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "delete", "read", "write", "get_info"], description: "操作类型" },
path: { type: "string", description: "着色器路径,如 db://assets/effects/NewEffect.effect" },
content: { type: "string", description: "着色器内容 (create/write)" },
},
required: ["action", "path"],
},
},
{
name: "execute_menu_item",
description: `${globalPrecautions} 执行菜单项`,
@@ -879,6 +900,10 @@ module.exports = {
this.manageTexture(args, callback);
break;
case "manage_shader":
this.manageShader(args, callback);
break;
case "execute_menu_item":
this.executeMenuItem(args, callback);
break;
@@ -1044,7 +1069,7 @@ export default class NewScript extends cc.Component {
break;
default:
callback(`Unknown script action: ${action}`);
callback(`未知的脚本操作类型: ${action}`);
break;
}
},
@@ -1060,7 +1085,7 @@ export default class NewScript extends cc.Component {
let completed = 0;
if (!operations || operations.length === 0) {
return callback("No operations provided");
return callback("未提供任何操作指令");
}
operations.forEach((operation, index) => {
@@ -1133,7 +1158,7 @@ export default class NewScript extends cc.Component {
if (info) {
callback(null, info);
} else {
// Fallback if API returns nothing but asset exists
// 备选方案:如果 API 未返回信息但资源确实存在
callback(null, { url: path, uuid: uuid, exists: true });
}
} catch (e) {
@@ -1142,7 +1167,7 @@ export default class NewScript extends cc.Component {
break;
default:
callback(`Unknown asset action: ${action}`);
callback(`未知的资源管理操作: ${action}`);
break;
}
},
@@ -1161,8 +1186,6 @@ export default class NewScript extends cc.Component {
return callback(`Scene already exists at ${path}`);
}
// 确保父目录存在
const fs = require("fs");
const pathModule = require("path");
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
@@ -1192,14 +1215,16 @@ export default class NewScript extends cc.Component {
if (Editor.assetdb.exists(targetPath)) {
return callback(`Target scene already exists at ${targetPath}`);
}
// 读取原场景内容
Editor.assetdb.loadAny(path, (err, content) => {
if (err) {
return callback(`Failed to read scene: ${err}`);
// 【修复】Cocos 2.4.x 主进程中 Editor.assetdb 没有 loadAny
// 直接使用 fs 读取物理文件
try {
const sourceFsPath = Editor.assetdb.urlToFspath(path);
if (!sourceFsPath || !fs.existsSync(sourceFsPath)) {
return callback(`Failed to locate source scene file: ${path}`);
}
const content = fs.readFileSync(sourceFsPath, "utf-8");
// 确保目标目录存在
const fs = require("fs");
const pathModule = require("path");
const targetAbsolutePath = Editor.assetdb.urlToFspath(targetPath);
const targetDirPath = pathModule.dirname(targetAbsolutePath);
if (!fs.existsSync(targetDirPath)) {
@@ -1207,9 +1232,15 @@ export default class NewScript extends cc.Component {
}
// 创建复制的场景
Editor.assetdb.create(targetPath, content, (err) => {
callback(err, err ? null : `Scene duplicated from ${path} to ${targetPath}`);
if (err) return callback(err);
// 【增加】关键刷新,确保数据库能查到新文件
Editor.assetdb.refresh(targetPath, (refreshErr) => {
callback(refreshErr, refreshErr ? null : `Scene duplicated from ${path} to ${targetPath}`);
});
});
});
} catch (e) {
callback(`Duplicate failed: ${e.message}`);
}
break;
case "get_info":
@@ -1242,7 +1273,7 @@ export default class NewScript extends cc.Component {
}
// 确保父目录存在
const absolutePath = Editor.assetdb.urlToFspath(prefabPath);
const dirPath = path.dirname(absolutePath);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
@@ -1314,7 +1345,7 @@ export default class NewScript extends cc.Component {
break;
default:
callback(`Unknown prefab action: ${action}`);
callback(`未知的预制体管理操作: ${action}`);
}
},
@@ -1338,10 +1369,12 @@ export default class NewScript extends cc.Component {
break;
case "set_selection":
// 设置选中状态
if (target === "node" && properties.nodes) {
Editor.Selection.select("node", properties.nodes);
} else if (target === "asset" && properties.assets) {
Editor.Selection.select("asset", properties.assets);
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);
}
callback(null, "Selection updated");
break;
@@ -1358,63 +1391,224 @@ export default class NewScript extends cc.Component {
});
break;
default:
callback("Unknown action");
callback("未知的编辑器管理操作");
break;
}
},
// 管理着色器 (Effect)
manageShader(args, callback) {
const { action, path: effectPath, content } = args;
switch (action) {
case "create":
if (Editor.assetdb.exists(effectPath)) {
return callback(`Effect already exists at ${effectPath}`);
}
// 确保父目录存在
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) => {
callback(refreshErr, refreshErr ? null : `Effect created at ${effectPath}`);
});
});
break;
case "read":
if (!Editor.assetdb.exists(effectPath)) {
return callback(`Effect not found: ${effectPath}`);
}
const fspath = Editor.assetdb.urlToFspath(effectPath);
try {
const data = fs.readFileSync(fspath, "utf-8");
callback(null, data);
} catch (e) {
callback(`Failed to read effect: ${e.message}`);
}
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) => {
callback(err, err ? null : `Effect updated at ${effectPath}`);
});
} catch (e) {
callback(`Failed to write effect: ${e.message}`);
}
break;
case "delete":
if (!Editor.assetdb.exists(effectPath)) {
return callback(`Effect not found: ${effectPath}`);
}
Editor.assetdb.delete([effectPath], (err) => {
callback(err, err ? null : `Effect deleted: ${effectPath}`);
});
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 {
callback(`Effect not found: ${effectPath}`);
}
break;
default:
callback(`Unknown shader action: ${action}`);
break;
}
},
// 管理材质
manageMaterial(args, callback) {
const { action, path, properties } = args;
const { action, path: matPath, properties = {} } = args;
switch (action) {
case "create":
if (Editor.assetdb.exists(path)) {
return callback(`Material already exists at ${path}`);
if (Editor.assetdb.exists(matPath)) {
return callback(`Material already exists at ${matPath}`);
}
// 确保父目录存在
const fs = require("fs");
const pathModule = require("path");
const absolutePath = Editor.assetdb.urlToFspath(path);
const absolutePath = Editor.assetdb.urlToFspath(matPath);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
// 创建材质资源
const materialContent = JSON.stringify({
// 构造 Cocos 2.4.x 材质内容
const materialData = {
__type__: "cc.Material",
_name: "",
_objFlags: 0,
_native: "",
effects: [
{
technique: 0,
defines: {},
uniforms: properties.uniforms || {},
},
],
});
Editor.assetdb.create(path, materialContent, (err) => {
callback(err, err ? null : `Material created at ${path}`);
_effectAsset: properties.shaderUuid ? { __uuid__: properties.shaderUuid } : null,
_techniqueIndex: 0,
_techniqueData: {
"0": {
defines: properties.defines || {},
props: properties.uniforms || {}
}
}
};
Editor.assetdb.create(matPath, JSON.stringify(materialData, null, 2), (err) => {
if (err) return callback(err);
Editor.assetdb.refresh(matPath, (refreshErr) => {
callback(refreshErr, refreshErr ? null : `Material created at ${matPath}`);
});
});
break;
case "update":
if (!Editor.assetdb.exists(matPath)) {
return callback(`Material not found at ${matPath}`);
}
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) => {
callback(err, err ? null : `Material updated at ${matPath}`);
});
} catch (e) {
callback(`Failed to update material: ${e.message}`);
}
break;
case "delete":
if (!Editor.assetdb.exists(path)) {
return callback(`Material not found at ${path}`);
if (!Editor.assetdb.exists(matPath)) {
return callback(`Material not found at ${matPath}`);
}
Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Material deleted at ${path}`);
Editor.assetdb.delete([matPath], (err) => {
callback(err, err ? null : `Material deleted at ${matPath}`);
});
break;
case "get_info":
if (Editor.assetdb.exists(path)) {
const uuid = Editor.assetdb.urlToUuid(path);
if (Editor.assetdb.exists(matPath)) {
const uuid = Editor.assetdb.urlToUuid(matPath);
const info = Editor.assetdb.assetInfoByUuid(uuid);
callback(null, info || { url: path, uuid: uuid, exists: true });
callback(null, info || { url: matPath, uuid: uuid, exists: true });
} else {
callback(`Material not found: ${path}`);
callback(`Material not found: ${matPath}`);
}
break;
default:
callback(`Unknown material action: ${action}`);
break;
@@ -1431,25 +1625,25 @@ export default class NewScript extends cc.Component {
return callback(`Texture already exists at ${path}`);
}
// 确保父目录存在
const fs = require("fs");
const pathModule = require("path");
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
// 创建纹理资源(简化版,实际需要处理纹理文件
const textureContent = JSON.stringify({
__type__: "cc.Texture2D",
_name: "",
_objFlags: 0,
_native: properties.native || "",
width: properties.width || 128,
height: properties.height || 128,
});
Editor.assetdb.create(path, textureContent, (err) => {
callback(err, err ? null : `Texture created at ${path}`);
});
// 【修复】Cocos 2.4.x 无法直接用 Editor.assetdb.create 创建带后缀的纹理(会校验内容
// 我们需要先物理写入一个 1x1 的透明图片,再刷新数据库
const base64Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
const buffer = Buffer.from(base64Data, 'base64');
try {
fs.writeFileSync(absolutePath, buffer);
Editor.assetdb.refresh(path, (err) => {
if (err) return callback(err);
callback(null, `Texture created and refreshed at ${path}`);
});
} catch (e) {
callback(`Failed to write texture file: ${e.message}`);
}
break;
case "delete":
if (!Editor.assetdb.exists(path)) {
@@ -1637,7 +1831,6 @@ export default class NewScript extends cc.Component {
}
// 2. 检查文件是否存在
const fs = require("fs");
if (!fs.existsSync(fspath)) {
return callback(`File does not exist: ${fspath}`);
}
@@ -1870,8 +2063,6 @@ export default class NewScript extends cc.Component {
// 全局文件搜索
findInFile(args, callback) {
const { query, extensions, includeSubpackages } = args;
const fs = require('fs');
const path = require('path');
const assetsPath = Editor.assetdb.urlToFspath("db://assets");
const validExtensions = extensions || [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"];
@@ -1890,14 +2081,14 @@ export default class NewScript extends cc.Component {
// 忽略隐藏文件和 node_modules
if (file.startsWith('.') || file === 'node_modules' || file === 'bin' || file === 'local') return;
const filePath = path.join(dir, file);
const filePath = pathModule.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
walk(filePath);
} else {
// 检查后缀
const ext = path.extname(file).toLowerCase();
const ext = pathModule.extname(file).toLowerCase();
if (validExtensions.includes(ext)) {
try {
const content = fs.readFileSync(filePath, 'utf8');
@@ -1907,9 +2098,9 @@ export default class NewScript extends cc.Component {
if (results.length >= MAX_RESULTS) return;
if (line.includes(query)) {
// 转换为项目相对路径 (db://assets/...)
const relativePath = path.relative(assetsPath, filePath);
const relativePath = pathModule.relative(assetsPath, filePath);
// 统一使用 forward slash
const dbPath = "db://assets/" + relativePath.split(path.sep).join('/');
const dbPath = "db://assets/" + relativePath.split(pathModule.sep).join('/');
results.push({
filePath: dbPath,