feat: implement MCP Resources Protocol support and localize documentation
This commit is contained in:
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
616
main.js
616
main.js
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
let logBuffer = []; // 存储所有日志
|
let logBuffer = []; // 存储所有日志
|
||||||
let mcpServer = null;
|
let mcpServer = null;
|
||||||
@@ -11,6 +12,7 @@ let serverConfig = {
|
|||||||
active: false,
|
active: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 封装日志函数,同时发送给面板和编辑器控制台
|
||||||
// 封装日志函数,同时发送给面板和编辑器控制台
|
// 封装日志函数,同时发送给面板和编辑器控制台
|
||||||
function addLog(type, message) {
|
function addLog(type, message) {
|
||||||
const logEntry = {
|
const logEntry = {
|
||||||
@@ -20,13 +22,20 @@ function addLog(type, message) {
|
|||||||
};
|
};
|
||||||
logBuffer.push(logEntry);
|
logBuffer.push(logEntry);
|
||||||
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry);
|
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry);
|
||||||
// 【修改】移除 Editor.log,保持编辑器控制台干净
|
|
||||||
// 仅在非常严重的系统错误时才输出到编辑器
|
// 【修改】确保所有日志都输出到编辑器控制台,以便用户查看
|
||||||
if (type === "error") {
|
if (type === "error") {
|
||||||
Editor.error(`[MCP] ${message}`); // 如果你完全不想在编辑器看,可以注释掉
|
Editor.error(`[MCP] ${message}`);
|
||||||
|
} else if (type === "warn") {
|
||||||
|
Editor.warn(`[MCP] ${message}`);
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLogContent() {
|
||||||
|
return logBuffer.map(entry => `[${entry.time}] [${entry.type}] ${entry.content}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
const getNewSceneTemplate = () => {
|
const getNewSceneTemplate = () => {
|
||||||
// 尝试获取 UUID 生成函数
|
// 尝试获取 UUID 生成函数
|
||||||
let newId = "";
|
let newId = "";
|
||||||
@@ -492,6 +501,41 @@ module.exports = {
|
|||||||
// 明确返回成功结构
|
// 明确返回成功结构
|
||||||
res.writeHead(200);
|
res.writeHead(200);
|
||||||
return res.end(JSON.stringify({ tools: tools }));
|
return res.end(JSON.stringify({ tools: tools }));
|
||||||
|
res.writeHead(200);
|
||||||
|
return res.end(JSON.stringify({ tools: tools }));
|
||||||
|
}
|
||||||
|
if (url === "/list-resources") {
|
||||||
|
const resources = this.getResourcesList();
|
||||||
|
addLog("info", `AI Client requested resource list`);
|
||||||
|
res.writeHead(200);
|
||||||
|
return res.end(JSON.stringify({ resources: resources }));
|
||||||
|
}
|
||||||
|
if (url === "/read-resource") {
|
||||||
|
try {
|
||||||
|
const { uri } = JSON.parse(body || "{}");
|
||||||
|
addLog("mcp", `READ -> [${uri}]`);
|
||||||
|
this.handleReadResource(uri, (err, content) => {
|
||||||
|
if (err) {
|
||||||
|
addLog("error", `读取失败: ${err}`);
|
||||||
|
res.writeHead(500);
|
||||||
|
return res.end(JSON.stringify({ error: err }));
|
||||||
|
}
|
||||||
|
addLog("success", `读取成功: ${uri}`);
|
||||||
|
res.writeHead(200);
|
||||||
|
// 返回 MCP Resource 格式: { contents: [{ uri, mimeType, text }] }
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
contents: [{
|
||||||
|
uri: uri,
|
||||||
|
mimeType: "application/json",
|
||||||
|
text: typeof content === 'string' ? content : JSON.stringify(content)
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ error: e.message }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (url === "/call-tool") {
|
if (url === "/call-tool") {
|
||||||
try {
|
try {
|
||||||
@@ -577,7 +621,67 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 统一处理逻辑,方便日志记录
|
getResourcesList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
uri: "cocos://hierarchy",
|
||||||
|
name: "Scene Hierarchy",
|
||||||
|
description: "当前场景层级的 JSON 快照",
|
||||||
|
mimeType: "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: "cocos://selection",
|
||||||
|
name: "Current Selection",
|
||||||
|
description: "当前选中节点的 UUID 列表",
|
||||||
|
mimeType: "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: "cocos://logs/latest",
|
||||||
|
name: "Editor Logs",
|
||||||
|
description: "最新的编辑器日志 (内存缓存)",
|
||||||
|
mimeType: "text/plain"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
handleReadResource(uri, callback) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URL(uri);
|
||||||
|
} catch (e) {
|
||||||
|
return callback(`Invalid URI: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.protocol !== "cocos:") {
|
||||||
|
return callback(`Unsupported protocol: ${parsed.protocol}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = parsed.hostname; // hierarchy, selection, logs
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "hierarchy":
|
||||||
|
// 注意: query-hierarchy 是异步的
|
||||||
|
Editor.Ipc.sendToPanel("scene", "scene:query-hierarchy", (err, sceneId, hierarchy) => {
|
||||||
|
if (err) return callback(err);
|
||||||
|
callback(null, JSON.stringify(hierarchy, null, 2));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "selection":
|
||||||
|
const selection = Editor.Selection.curSelection("node");
|
||||||
|
callback(null, JSON.stringify(selection));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "logs":
|
||||||
|
callback(null, getLogContent());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callback(`Resource not found: ${uri}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleMcpCall(name, args, callback) {
|
handleMcpCall(name, args, callback) {
|
||||||
if (isSceneBusy && (name === "save_scene" || name === "create_node")) {
|
if (isSceneBusy && (name === "save_scene" || name === "create_node")) {
|
||||||
return callback("Editor is busy (Processing Scene), please wait a moment.");
|
return callback("Editor is busy (Processing Scene), please wait a moment.");
|
||||||
@@ -621,37 +725,15 @@ module.exports = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "update_node_transform":
|
case "update_node_transform":
|
||||||
const { id, x, y, scaleX, scaleY, color } = args;
|
// 直接调用场景脚本更新属性,绕过可能导致 "Unknown object" 的复杂 Undo 系统
|
||||||
// 将多个属性修改打包到一个 Undo 组中
|
Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", args, (err, result) => {
|
||||||
Editor.Ipc.sendToPanel("scene", "scene:undo-record", "Transform Update");
|
if (err) {
|
||||||
|
addLog("error", `Transform update failed: ${err}`);
|
||||||
try {
|
callback(err);
|
||||||
// 注意:Cocos Creator 属性类型通常首字母大写,如 'Float', 'String', 'Boolean'
|
} else {
|
||||||
// 也有可能支持 'Number',但 'Float' 更保险
|
callback(null, "Transform updated");
|
||||||
if (x !== undefined) Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "x", type: "Float", value: x, isSubProp: false });
|
|
||||||
if (y !== undefined) Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "y", type: "Float", value: y, isSubProp: false });
|
|
||||||
if (scaleX !== undefined) Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleX", type: "Float", value: scaleX, isSubProp: false });
|
|
||||||
if (scaleY !== undefined) Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleY", type: "Float", value: scaleY, isSubProp: false });
|
|
||||||
if (color) {
|
|
||||||
// 颜色稍微复杂,传递 hex 字符串可能需要 Color 对象转换,但 set-property 也许可以直接接受 info
|
|
||||||
// 安全起见,颜色还是走 scene-script 或者尝试直接 set-property
|
|
||||||
// 这里的 color 是 Hex String。尝试传 String 让编辑器解析?
|
|
||||||
// 通常编辑器需要 cc.Color 对象或 {r,g,b,a}
|
|
||||||
// 暂时保留 color 通过 scene-script 处理? 或者跳过?
|
|
||||||
// 为了保持一致性,还是走 scene-script 更新颜色,但这样颜色可能无法 undo。
|
|
||||||
// 改进:使用 scene script 处理颜色,但尝试手动 record?
|
|
||||||
// 暂且忽略颜色的 Undo,先保证 Transform 的 Undo。
|
|
||||||
Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", { id, color }, (err) => {
|
|
||||||
if (err) addLog("warn", "Color update failed or partial");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
Editor.Ipc.sendToPanel("scene", "scene:undo-commit");
|
|
||||||
callback(null, "Transform updated");
|
|
||||||
} catch (e) {
|
|
||||||
Editor.Ipc.sendToPanel("scene", "scene:undo-cancel");
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "create_scene":
|
case "create_scene":
|
||||||
@@ -797,23 +879,21 @@ module.exports = {
|
|||||||
|
|
||||||
// 管理脚本文件
|
// 管理脚本文件
|
||||||
manageScript(args, callback) {
|
manageScript(args, callback) {
|
||||||
const { action, path, content } = args;
|
const { action, path: scriptPath, content } = args;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "create":
|
case "create":
|
||||||
if (Editor.assetdb.exists(path)) {
|
if (Editor.assetdb.exists(scriptPath)) {
|
||||||
return callback(`Script already exists at ${path}`);
|
return callback(`Script already exists at ${scriptPath}`);
|
||||||
}
|
}
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
const fs = require("fs");
|
const absolutePath = Editor.assetdb.urlToFspath(scriptPath);
|
||||||
const pathModule = require("path");
|
const dirPath = path.dirname(absolutePath);
|
||||||
const absolutePath = Editor.assetdb.urlToFspath(path);
|
|
||||||
const dirPath = pathModule.dirname(absolutePath);
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
Editor.assetdb.create(
|
Editor.assetdb.create(
|
||||||
path,
|
scriptPath,
|
||||||
content ||
|
content ||
|
||||||
`const { ccclass, property } = cc._decorator;
|
`const { ccclass, property } = cc._decorator;
|
||||||
|
|
||||||
@@ -834,35 +914,50 @@ export default class NewScript extends cc.Component {
|
|||||||
update (dt) {}
|
update (dt) {}
|
||||||
}`,
|
}`,
|
||||||
(err) => {
|
(err) => {
|
||||||
callback(err, err ? null : `Script created at ${path}`);
|
callback(err, err ? null : `Script created at ${scriptPath}`);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "delete":
|
case "delete":
|
||||||
if (!Editor.assetdb.exists(path)) {
|
if (!Editor.assetdb.exists(scriptPath)) {
|
||||||
return callback(`Script not found at ${path}`);
|
return callback(`Script not found at ${scriptPath}`);
|
||||||
}
|
}
|
||||||
Editor.assetdb.delete([path], (err) => {
|
Editor.assetdb.delete([scriptPath], (err) => {
|
||||||
callback(err, err ? null : `Script deleted at ${path}`);
|
callback(err, err ? null : `Script deleted at ${scriptPath}`);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "read":
|
case "read":
|
||||||
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
// 使用 fs 读取,绕过 assetdb.loadAny
|
||||||
if (err) {
|
const readFsPath = Editor.assetdb.urlToFspath(scriptPath);
|
||||||
return callback(`Failed to get script info: ${err}`);
|
if (!readFsPath || !fs.existsSync(readFsPath)) {
|
||||||
}
|
return callback(`Script not found at ${scriptPath}`);
|
||||||
Editor.assetdb.loadAny(path, (err, content) => {
|
}
|
||||||
callback(err, err ? null : content);
|
try {
|
||||||
});
|
const content = fs.readFileSync(readFsPath, "utf-8");
|
||||||
});
|
callback(null, content);
|
||||||
|
} catch (e) {
|
||||||
|
callback(`Failed to read script: ${e.message}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "write":
|
case "write":
|
||||||
Editor.assetdb.create(path, content, (err) => {
|
// 使用 fs 写入 + refresh,确保覆盖成功
|
||||||
callback(err, err ? null : `Script updated at ${path}`);
|
const writeFsPath = Editor.assetdb.urlToFspath(scriptPath);
|
||||||
});
|
if (!writeFsPath) {
|
||||||
|
return callback(`Invalid path: ${scriptPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(writeFsPath, content, "utf-8");
|
||||||
|
Editor.assetdb.refresh(scriptPath, (err) => {
|
||||||
|
if (err) addLog("warn", `Refresh failed after write: ${err}`);
|
||||||
|
callback(null, `Script updated at ${scriptPath}`);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback(`Failed to write script: ${e.message}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -936,18 +1031,20 @@ export default class NewScript extends cc.Component {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case "get_info":
|
case "get_info":
|
||||||
try {
|
try {
|
||||||
if (!Editor.assetdb.exists(path)) {
|
if (!Editor.assetdb.exists(path)) {
|
||||||
return callback(`Asset not found: ${path}`);
|
return callback(`Asset not found: ${path}`);
|
||||||
}
|
}
|
||||||
const uuid = Editor.assetdb.urlToUuid(path);
|
const uuid = Editor.assetdb.urlToUuid(path);
|
||||||
// Return basic info constructed manually to avoid API compatibility issues
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||||
callback(null, {
|
if (info) {
|
||||||
url: path,
|
callback(null, info);
|
||||||
uuid: uuid,
|
} else {
|
||||||
exists: true
|
// Fallback if API returns nothing but asset exists
|
||||||
});
|
callback(null, { url: path, uuid: uuid, exists: true });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(`Error getting asset info: ${e.message}`);
|
callback(`Error getting asset info: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -1021,9 +1118,13 @@ export default class NewScript extends cc.Component {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "get_info":
|
case "get_info":
|
||||||
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
if (Editor.assetdb.exists(path)) {
|
||||||
callback(err, err ? null : info);
|
const uuid = Editor.assetdb.urlToUuid(path);
|
||||||
});
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||||
|
callback(null, info || { url: path, uuid: uuid, exists: true });
|
||||||
|
} else {
|
||||||
|
callback(`Scene not found: ${path}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1034,51 +1135,70 @@ export default class NewScript extends cc.Component {
|
|||||||
|
|
||||||
// 预制体管理
|
// 预制体管理
|
||||||
prefabManagement(args, callback) {
|
prefabManagement(args, callback) {
|
||||||
const { action, path, nodeId, parentId } = args;
|
const { action, path: prefabPath, nodeId, parentId } = args;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "create":
|
case "create":
|
||||||
if (!nodeId) {
|
if (!nodeId) {
|
||||||
return callback(`Node ID is required for create operation`);
|
return callback(`Node ID is required for create operation`);
|
||||||
}
|
}
|
||||||
if (Editor.assetdb.exists(path)) {
|
if (Editor.assetdb.exists(prefabPath)) {
|
||||||
return callback(`Prefab already exists at ${path}`);
|
return callback(`Prefab already exists at ${prefabPath}`);
|
||||||
}
|
}
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
const fs = require("fs");
|
const absolutePath = Editor.assetdb.urlToFspath(prefabPath);
|
||||||
const pathModule = require("path");
|
const dirPath = path.dirname(absolutePath);
|
||||||
const absolutePath = Editor.assetdb.urlToFspath(path);
|
|
||||||
const dirPath = pathModule.dirname(absolutePath);
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
// 从节点创建预制体
|
|
||||||
Editor.Ipc.sendToMain("scene:create-prefab", nodeId, path);
|
// 解析目标目录和文件名
|
||||||
callback(null, `Command sent: Creating prefab from node ${nodeId} at ${path}`);
|
// db://assets/folder/PrefabName.prefab -> db://assets/folder, PrefabName
|
||||||
|
const targetDir = prefabPath.substring(0, prefabPath.lastIndexOf('/'));
|
||||||
|
const fileName = prefabPath.substring(prefabPath.lastIndexOf('/') + 1);
|
||||||
|
const prefabName = fileName.replace('.prefab', '');
|
||||||
|
|
||||||
|
// 1. 重命名节点以匹配预制体名称
|
||||||
|
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||||
|
id: nodeId,
|
||||||
|
path: "name",
|
||||||
|
type: "String",
|
||||||
|
value: prefabName,
|
||||||
|
isSubProp: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 发送创建命令 (参数: [uuids], dirPath)
|
||||||
|
// 注意: scene:create-prefab 第三个参数必须是 db:// 目录路径
|
||||||
|
setTimeout(() => {
|
||||||
|
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir);
|
||||||
|
}, 100); // 稍微延迟以确保重命名生效
|
||||||
|
|
||||||
|
callback(null, `Command sent: Creating prefab from node ${nodeId} at ${targetDir} as ${prefabName}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "update":
|
case "update":
|
||||||
if (!nodeId) {
|
if (!nodeId) {
|
||||||
return callback(`Node ID is required for update operation`);
|
return callback(`Node ID is required for update operation`);
|
||||||
}
|
}
|
||||||
if (!Editor.assetdb.exists(path)) {
|
if (!Editor.assetdb.exists(prefabPath)) {
|
||||||
return callback(`Prefab not found at ${path}`);
|
return callback(`Prefab not found at ${prefabPath}`);
|
||||||
}
|
}
|
||||||
// 更新预制体
|
// 更新预制体
|
||||||
Editor.Ipc.sendToMain("scene:update-prefab", nodeId, path);
|
Editor.Ipc.sendToPanel("scene", "scene:update-prefab", nodeId, prefabPath);
|
||||||
callback(null, `Command sent: Updating prefab ${path} from node ${nodeId}`);
|
callback(null, `Command sent: Updating prefab ${prefabPath} from node ${nodeId}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "instantiate":
|
case "instantiate":
|
||||||
if (!Editor.assetdb.exists(path)) {
|
if (!Editor.assetdb.exists(prefabPath)) {
|
||||||
return callback(`Prefab not found at ${path}`);
|
return callback(`Prefab not found at ${prefabPath}`);
|
||||||
}
|
}
|
||||||
// 实例化预制体
|
// 实例化预制体
|
||||||
|
const prefabUuid = Editor.assetdb.urlToUuid(prefabPath);
|
||||||
Editor.Scene.callSceneScript(
|
Editor.Scene.callSceneScript(
|
||||||
"mcp-bridge",
|
"mcp-bridge",
|
||||||
"instantiate-prefab",
|
"instantiate-prefab",
|
||||||
{
|
{
|
||||||
prefabPath: path,
|
prefabUuid: prefabUuid,
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
},
|
},
|
||||||
callback,
|
callback,
|
||||||
@@ -1086,9 +1206,16 @@ export default class NewScript extends cc.Component {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "get_info":
|
case "get_info":
|
||||||
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
if (Editor.assetdb.exists(prefabPath)) {
|
||||||
callback(err, err ? null : info);
|
const uuid = Editor.assetdb.urlToUuid(prefabPath);
|
||||||
});
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||||
|
// 确保返回对象包含 exists: true,以满足测试验证
|
||||||
|
const result = info || { url: prefabPath, uuid: uuid };
|
||||||
|
result.exists = true;
|
||||||
|
callback(null, result);
|
||||||
|
} else {
|
||||||
|
callback(`Prefab not found: ${prefabPath}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1181,9 +1308,13 @@ export default class NewScript extends cc.Component {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "get_info":
|
case "get_info":
|
||||||
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
if (Editor.assetdb.exists(path)) {
|
||||||
callback(err, err ? null : info);
|
const uuid = Editor.assetdb.urlToUuid(path);
|
||||||
});
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||||
|
callback(null, info || { url: path, uuid: uuid, exists: true });
|
||||||
|
} else {
|
||||||
|
callback(`Material not found: ${path}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
callback(`Unknown material action: ${action}`);
|
callback(`Unknown material action: ${action}`);
|
||||||
@@ -1230,9 +1361,13 @@ export default class NewScript extends cc.Component {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "get_info":
|
case "get_info":
|
||||||
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
|
if (Editor.assetdb.exists(path)) {
|
||||||
callback(err, err ? null : info);
|
const uuid = Editor.assetdb.urlToUuid(path);
|
||||||
});
|
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||||
|
callback(null, info || { url: path, uuid: uuid, exists: true });
|
||||||
|
} else {
|
||||||
|
callback(`Texture not found: ${path}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
callback(`Unknown texture action: ${action}`);
|
callback(`Unknown texture action: ${action}`);
|
||||||
@@ -1240,62 +1375,66 @@ export default class NewScript extends cc.Component {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 执行菜单项
|
|
||||||
executeMenuItem(args, callback) {
|
|
||||||
const { menuPath } = args;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 执行菜单项
|
|
||||||
Editor.Ipc.sendToMain("menu:click", menuPath);
|
|
||||||
callback(null, `Menu item executed: ${menuPath}`);
|
|
||||||
} catch (err) {
|
|
||||||
callback(`Failed to execute menu item: ${err.message}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 应用文本编辑
|
// 应用文本编辑
|
||||||
applyTextEdits(args, callback) {
|
applyTextEdits(args, callback) {
|
||||||
const { filePath, edits } = args;
|
const { filePath, edits } = args;
|
||||||
|
|
||||||
// 读取文件内容
|
// 1. 获取文件系统路径
|
||||||
Editor.assetdb.queryInfoByUrl(filePath, (err, info) => {
|
const fspath = Editor.assetdb.urlToFspath(filePath);
|
||||||
if (err) {
|
if (!fspath) {
|
||||||
callback(`Failed to get file info: ${err.message}`);
|
return callback(`File not found or invalid URL: ${filePath}`);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Editor.assetdb.loadAny(filePath, (err, content) => {
|
const fs = require("fs");
|
||||||
if (err) {
|
if (!fs.existsSync(fspath)) {
|
||||||
callback(`Failed to load file: ${err.message}`);
|
return callback(`File does not exist: ${fspath}`);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 应用编辑操作
|
try {
|
||||||
let updatedContent = content;
|
// 2. 读取
|
||||||
edits.forEach((edit) => {
|
let updatedContent = fs.readFileSync(fspath, "utf-8");
|
||||||
switch (edit.type) {
|
|
||||||
case "insert":
|
|
||||||
updatedContent =
|
|
||||||
updatedContent.slice(0, edit.position) +
|
|
||||||
edit.text +
|
|
||||||
updatedContent.slice(edit.position);
|
|
||||||
break;
|
|
||||||
case "delete":
|
|
||||||
updatedContent = updatedContent.slice(0, edit.start) + updatedContent.slice(edit.end);
|
|
||||||
break;
|
|
||||||
case "replace":
|
|
||||||
updatedContent =
|
|
||||||
updatedContent.slice(0, edit.start) + edit.text + updatedContent.slice(edit.end);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 写回文件
|
// 3. 应用编辑
|
||||||
Editor.assetdb.create(filePath, updatedContent, (err) => {
|
// 必须按倒序应用编辑,否则后续编辑的位置会偏移 (假设edits未排序,这里简单处理,实际上LSP通常建议客户端倒序应用或计算偏移)
|
||||||
callback(err, err ? null : `Text edits applied to ${filePath}`);
|
// 这里假设edits已经按照位置排序或者用户负责,如果需要严谨,应先按 start/position 倒序排序
|
||||||
});
|
// 简单做个排序保险:
|
||||||
|
const sortedEdits = [...edits].sort((a, b) => {
|
||||||
|
const posA = a.position !== undefined ? a.position : a.start;
|
||||||
|
const posB = b.position !== undefined ? b.position : b.start;
|
||||||
|
return posB - posA; // big to small
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
sortedEdits.forEach((edit) => {
|
||||||
|
switch (edit.type) {
|
||||||
|
case "insert":
|
||||||
|
updatedContent =
|
||||||
|
updatedContent.slice(0, edit.position) +
|
||||||
|
edit.text +
|
||||||
|
updatedContent.slice(edit.position);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
updatedContent = updatedContent.slice(0, edit.start) + updatedContent.slice(edit.end);
|
||||||
|
break;
|
||||||
|
case "replace":
|
||||||
|
updatedContent =
|
||||||
|
updatedContent.slice(0, edit.start) + edit.text + updatedContent.slice(edit.end);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 写入
|
||||||
|
fs.writeFileSync(fspath, updatedContent, "utf-8");
|
||||||
|
|
||||||
|
// 5. 通知编辑器资源变化 (重要)
|
||||||
|
Editor.assetdb.refresh(filePath, (err) => {
|
||||||
|
if (err) addLog("warn", `Refresh failed for ${filePath}: ${err}`);
|
||||||
|
callback(null, `Text edits applied to ${filePath}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
callback(`Action failed: ${err.message}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 读取控制台
|
// 读取控制台
|
||||||
@@ -1338,43 +1477,45 @@ export default class NewScript extends cc.Component {
|
|||||||
validateScript(args, callback) {
|
validateScript(args, callback) {
|
||||||
const { filePath } = args;
|
const { filePath } = args;
|
||||||
|
|
||||||
// 读取脚本内容
|
// 1. 获取文件系统路径
|
||||||
Editor.assetdb.queryInfoByUrl(filePath, (err, info) => {
|
const fspath = Editor.assetdb.urlToFspath(filePath);
|
||||||
if (err) {
|
if (!fspath) {
|
||||||
callback(`Failed to get file info: ${err.message}`);
|
return callback(`File not found or invalid URL: ${filePath}`);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Editor.assetdb.loadAny(filePath, (err, content) => {
|
// 2. 检查文件是否存在
|
||||||
if (err) {
|
const fs = require("fs");
|
||||||
callback(`Failed to load file: ${err.message}`);
|
if (!fs.existsSync(fspath)) {
|
||||||
return;
|
return callback(`File does not exist: ${fspath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 读取内容并验证
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(fspath, "utf-8");
|
||||||
|
|
||||||
|
// 对于 JavaScript 脚本,使用 eval 进行简单验证
|
||||||
|
if (filePath.endsWith(".js")) {
|
||||||
|
// 包装在函数中以避免变量污染
|
||||||
|
const wrapper = `(function() { ${content} })`;
|
||||||
try {
|
try {
|
||||||
// 对于 JavaScript 脚本,使用 eval 进行简单验证
|
new Function(wrapper); // 使用 Function 构造器比 direct eval稍微安全一点点,虽在这个场景下差别不大
|
||||||
if (filePath.endsWith(".js")) {
|
} catch (syntaxErr) {
|
||||||
// 包装在函数中以避免变量污染
|
return callback(null, { valid: false, message: syntaxErr.message });
|
||||||
const wrapper = `(function() { ${content} })`;
|
|
||||||
eval(wrapper);
|
|
||||||
}
|
|
||||||
// 对于 TypeScript 脚本,这里可以添加更复杂的验证逻辑
|
|
||||||
|
|
||||||
callback(null, { valid: true, message: "Script syntax is valid" });
|
|
||||||
} catch (err) {
|
|
||||||
callback(null, { valid: false, message: err.message });
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
// 对于 TypeScript,暂不进行复杂编译检查,仅确保文件可读
|
||||||
|
|
||||||
|
callback(null, { valid: true, message: "Script syntax is valid" });
|
||||||
|
} catch (err) {
|
||||||
|
callback(null, { valid: false, message: `Read Error: ${err.message}` });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// 暴露给 MCP 或面板的 API 封装
|
// 暴露给 MCP 或面板的 API 封装
|
||||||
messages: {
|
messages: {
|
||||||
"open-test-panel"() {
|
"open-test-panel"() {
|
||||||
Editor.Panel.open("mcp-bridge");
|
Editor.Panel.open("mcp-bridge");
|
||||||
},
|
},
|
||||||
"get-server-state"(event) {
|
|
||||||
event.reply(null, { config: serverConfig, logs: logBuffer });
|
|
||||||
},
|
|
||||||
"toggle-server"(event, port) {
|
"toggle-server"(event, port) {
|
||||||
if (serverConfig.active) this.stopServer();
|
if (serverConfig.active) this.stopServer();
|
||||||
else this.startServer(port);
|
else this.startServer(port);
|
||||||
@@ -1419,42 +1560,129 @@ export default class NewScript extends cc.Component {
|
|||||||
this.getProfile().save();
|
this.getProfile().save();
|
||||||
addLog("info", `Auto-start set to: ${value}`);
|
addLog("info", `Auto-start set to: ${value}`);
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
// 验证脚本
|
"inspect-apis"() {
|
||||||
validateScript(args, callback) {
|
addLog("info", "[API Inspector] Starting DEEP inspection...");
|
||||||
const { filePath } = args;
|
|
||||||
|
|
||||||
// 读取脚本内容
|
|
||||||
Editor.assetdb.queryInfoByUrl(filePath, (err, info) => {
|
|
||||||
if (err) {
|
|
||||||
callback(`Failed to get file info: ${err.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Editor.assetdb.loadMeta(info.uuid, (err, content) => {
|
|
||||||
if (err) {
|
|
||||||
callback(`Failed to load file: ${err.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Helper to get function arguments
|
||||||
|
const getArgs = (func) => {
|
||||||
try {
|
try {
|
||||||
// 对于 JavaScript 脚本,使用 eval 进行简单验证
|
const str = func.toString();
|
||||||
if (filePath.endsWith('.js')) {
|
const match = str.match(/function\s.*?\(([^)]*)\)/) || str.match(/.*?\(([^)]*)\)/);
|
||||||
// 包装在函数中以避免变量污染
|
if (match) {
|
||||||
const wrapper = `(function() { ${content} })`;
|
return match[1].split(",").map(arg => arg.trim()).filter(a => a).join(", ");
|
||||||
eval(wrapper);
|
|
||||||
}
|
}
|
||||||
// 对于 TypeScript 脚本,这里可以添加更复杂的验证逻辑
|
return `${func.length} args`;
|
||||||
|
} catch (e) {
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
callback(null, { valid: true, message: 'Script syntax is valid' });
|
// Helper to inspect an object
|
||||||
} catch (err) {
|
const inspectObj = (name, obj) => {
|
||||||
callback(null, { valid: false, message: err.message });
|
if (!obj) return { name, exists: false };
|
||||||
|
const props = {};
|
||||||
|
const proto = Object.getPrototypeOf(obj);
|
||||||
|
|
||||||
|
// 组合自身属性和原型属性
|
||||||
|
const allKeys = new Set([...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertyNames(proto || {})]);
|
||||||
|
|
||||||
|
allKeys.forEach(key => {
|
||||||
|
if (key.startsWith("_")) return; // Skip private
|
||||||
|
try {
|
||||||
|
const val = obj[key];
|
||||||
|
if (typeof val === 'function') {
|
||||||
|
props[key] = `func(${getArgs(val)})`;
|
||||||
|
} else {
|
||||||
|
props[key] = typeof val;
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
});
|
||||||
|
return { name, exists: true, props };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Inspect Standard Objects
|
||||||
|
const standardObjects = {
|
||||||
|
"Editor.assetdb": Editor.assetdb,
|
||||||
|
"Editor.Selection": Editor.Selection,
|
||||||
|
"Editor.Ipc": Editor.Ipc,
|
||||||
|
"Editor.Panel": Editor.Panel,
|
||||||
|
"Editor.Scene": Editor.Scene,
|
||||||
|
"Editor.Utils": Editor.Utils,
|
||||||
|
"Editor.remote": Editor.remote
|
||||||
|
};
|
||||||
|
|
||||||
|
const report = {};
|
||||||
|
Object.keys(standardObjects).forEach(key => {
|
||||||
|
report[key] = inspectObj(key, standardObjects[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Check Specific Forum APIs
|
||||||
|
const forumChecklist = [
|
||||||
|
"Editor.assetdb.queryInfoByUuid",
|
||||||
|
"Editor.assetdb.assetInfoByUuid",
|
||||||
|
"Editor.assetdb.move",
|
||||||
|
"Editor.assetdb.createOrSave",
|
||||||
|
"Editor.assetdb.delete",
|
||||||
|
"Editor.assetdb.urlToUuid",
|
||||||
|
"Editor.assetdb.uuidToUrl",
|
||||||
|
"Editor.assetdb.fspathToUrl",
|
||||||
|
"Editor.assetdb.urlToFspath",
|
||||||
|
"Editor.remote.assetdb.uuidToUrl",
|
||||||
|
"Editor.Selection.select",
|
||||||
|
"Editor.Selection.clear",
|
||||||
|
"Editor.Selection.curSelection",
|
||||||
|
"Editor.Selection.curGlobalActivate"
|
||||||
|
];
|
||||||
|
|
||||||
|
const checklistResults = {};
|
||||||
|
forumChecklist.forEach(path => {
|
||||||
|
const parts = path.split(".");
|
||||||
|
let curr = global; // In main process, Editor is global
|
||||||
|
let exists = true;
|
||||||
|
for (const part of parts) {
|
||||||
|
if (curr && curr[part]) {
|
||||||
|
curr = curr[part];
|
||||||
|
} else {
|
||||||
|
exists = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checklistResults[path] = exists ? (typeof curr === 'function' ? `Available(${getArgs(curr)})` : "Available") : "Missing";
|
||||||
|
});
|
||||||
|
|
||||||
|
addLog("info", `[API Inspector] Standard Objects:\n${JSON.stringify(report, null, 2)}`);
|
||||||
|
addLog("info", `[API Inspector] Forum Checklist:\n${JSON.stringify(checklistResults, null, 2)}`);
|
||||||
|
|
||||||
|
// 3. Inspect Built-in Package IPCs
|
||||||
|
const ipcReport = {};
|
||||||
|
const builtinPackages = ["scene", "builder", "assets"]; // 核心内置包
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
builtinPackages.forEach(pkgName => {
|
||||||
|
try {
|
||||||
|
const pkgPath = Editor.url(`packages://${pkgName}/package.json`);
|
||||||
|
if (pkgPath && fs.existsSync(pkgPath)) {
|
||||||
|
const pkgData = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
||||||
|
if (pkgData.messages) {
|
||||||
|
ipcReport[pkgName] = Object.keys(pkgData.messages);
|
||||||
|
} else {
|
||||||
|
ipcReport[pkgName] = "No messages defined";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ipcReport[pkgName] = "Package path not found";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ipcReport[pkgName] = `Error: ${e.message}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
addLog("info", `[API Inspector] Built-in IPC Messages:\n${JSON.stringify(ipcReport, null, 2)}`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 全局文件搜索
|
// 全局文件搜索
|
||||||
findInFile(args, callback) {
|
findInFile(args, callback) {
|
||||||
const { query, extensions, includeSubpackages } = args;
|
const { query, extensions, includeSubpackages } = args;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
<ui-button id="testBtn" class="green">测试工具</ui-button>
|
<ui-button id="testBtn" class="green">测试工具</ui-button>
|
||||||
<ui-button id="listToolsBtn">刷新列表</ui-button>
|
<ui-button id="listToolsBtn">刷新列表</ui-button>
|
||||||
<ui-button id="clearTestBtn">清空结果</ui-button>
|
<ui-button id="clearTestBtn">清空结果</ui-button>
|
||||||
|
<ui-button id="probeApisBtn">探查 API</ui-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-v">
|
<div class="flex-v">
|
||||||
<label>测试结果:</label>
|
<label>测试结果:</label>
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ Editor.Panel.extend({
|
|||||||
els.result.value = "";
|
els.result.value = "";
|
||||||
});
|
});
|
||||||
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
els.testBtn.addEventListener("confirm", () => this.runTest(els));
|
||||||
|
// 添加探查功能
|
||||||
|
const probeBtn = root.querySelector("#probeApisBtn");
|
||||||
|
if (probeBtn) {
|
||||||
|
probeBtn.addEventListener("confirm", () => {
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:inspect-apis");
|
||||||
|
els.result.value = "Probe command sent. Check console logs.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 【修复】拖拽逻辑
|
// 5. 【修复】拖拽逻辑
|
||||||
if (els.resizer && els.left) {
|
if (els.resizer && els.left) {
|
||||||
|
|||||||
@@ -68,23 +68,34 @@ module.exports = {
|
|||||||
|
|
||||||
"update-node-transform": function (event, args) {
|
"update-node-transform": function (event, args) {
|
||||||
const { id, x, y, scaleX, scaleY, color } = args;
|
const { id, x, y, scaleX, scaleY, color } = args;
|
||||||
|
Editor.log(`[scene-script] update-node-transform called for ${id} with args: ${JSON.stringify(args)}`);
|
||||||
|
|
||||||
let node = cc.engine.getInstanceById(id);
|
let node = cc.engine.getInstanceById(id);
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
if (x !== undefined) node.x = x;
|
Editor.log(`[scene-script] Node found: ${node.name}, Current Pos: (${node.x}, ${node.y})`);
|
||||||
if (y !== undefined) node.y = y;
|
|
||||||
if (scaleX !== undefined) node.scaleX = scaleX;
|
if (x !== undefined) {
|
||||||
if (scaleY !== undefined) node.scaleY = scaleY;
|
node.x = Number(x); // 强制转换确保类型正确
|
||||||
|
Editor.log(`[scene-script] Set x to ${node.x}`);
|
||||||
|
}
|
||||||
|
if (y !== undefined) {
|
||||||
|
node.y = Number(y);
|
||||||
|
Editor.log(`[scene-script] Set y to ${node.y}`);
|
||||||
|
}
|
||||||
|
if (scaleX !== undefined) node.scaleX = Number(scaleX);
|
||||||
|
if (scaleY !== undefined) node.scaleY = Number(scaleY);
|
||||||
if (color) {
|
if (color) {
|
||||||
// color 格式如 "#FF0000"
|
|
||||||
node.color = new cc.Color().fromHEX(color);
|
node.color = new cc.Color().fromHEX(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
Editor.Ipc.sendToMain("scene:dirty");
|
Editor.Ipc.sendToMain("scene:dirty");
|
||||||
Editor.Ipc.sendToAll("scene:node-changed", { uuid: id });
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: id });
|
||||||
|
|
||||||
|
Editor.log(`[scene-script] Update complete. New Pos: (${node.x}, ${node.y})`);
|
||||||
if (event.reply) event.reply(null, "Transform updated");
|
if (event.reply) event.reply(null, "Transform updated");
|
||||||
} else {
|
} else {
|
||||||
|
Editor.error(`[scene-script] Node not found: ${id}`);
|
||||||
if (event.reply) event.reply(new Error("Node not found"));
|
if (event.reply) event.reply(new Error("Node not found"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -308,7 +319,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"instantiate-prefab": function (event, args) {
|
"instantiate-prefab": function (event, args) {
|
||||||
const { prefabPath, parentId } = args;
|
const { prefabUuid, parentId } = args;
|
||||||
const scene = cc.director.getScene();
|
const scene = cc.director.getScene();
|
||||||
|
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
@@ -316,8 +327,14 @@ module.exports = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载预制体资源
|
if (!prefabUuid) {
|
||||||
cc.loader.loadRes(prefabPath.replace("db://assets/", "").replace(".prefab", ""), cc.Prefab, (err, prefab) => {
|
if (event.reply) event.reply(new Error("Prefab UUID is required."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 cc.assetManager.loadAny 通过 UUID 加载 (Cocos 2.4+)
|
||||||
|
// 如果是旧版,可能需要 cc.loader.load({uuid: ...}),但在 2.4 环境下 assetManager 更推荐
|
||||||
|
cc.assetManager.loadAny(prefabUuid, (err, prefab) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (event.reply) event.reply(new Error(`Failed to load prefab: ${err.message}`));
|
if (event.reply) event.reply(new Error(`Failed to load prefab: ${err.message}`));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -229,53 +229,143 @@ const tests = {
|
|||||||
log('success', `编辑器选中状态已更新为节点 ${nodeId}`);
|
log('success', `编辑器选中状态已更新为节点 ${nodeId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async testAssetManagement() {
|
async testScriptOperations() {
|
||||||
log('group', '资源管理测试');
|
log('group', '脚本读写与验证测试 (FS Mode)');
|
||||||
const scriptPath = 'db://assets/temp_auto_test.js';
|
const scriptPath = 'db://assets/auto_test_script.js';
|
||||||
|
const initialContent = 'cc.log("Initial Content");';
|
||||||
|
const updatedContent = 'cc.log("Updated Content");';
|
||||||
|
|
||||||
// 1. 创建脚本
|
// 1. 创建脚本
|
||||||
try {
|
try {
|
||||||
|
log('info', `创建脚本: ${scriptPath}`);
|
||||||
await callTool('manage_script', {
|
await callTool('manage_script', {
|
||||||
action: 'create',
|
action: 'create',
|
||||||
path: scriptPath,
|
path: scriptPath,
|
||||||
content: 'cc.log("Test Script");'
|
content: initialContent
|
||||||
});
|
});
|
||||||
log('success', `已创建临时资源: ${scriptPath}`);
|
log('success', `脚本已创建`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message.includes('exists')) {
|
if (e.message.includes('exists')) {
|
||||||
log('warn', `资源已存在,正在尝试先删除...`);
|
log('warn', `脚本已存在,尝试删除重建...`);
|
||||||
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
||||||
// 重试创建
|
await callTool('manage_script', { action: 'create', path: scriptPath, content: initialContent });
|
||||||
await callTool('manage_script', { action: 'create', path: scriptPath, content: 'cc.log("Test Script");' });
|
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取信息
|
// 等待资源导入
|
||||||
// 等待 AssetDB 刷新 (导入需要时间)
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
log('info', '等待 3 秒以进行资源导入...');
|
|
||||||
await new Promise(r => setTimeout(r, 3000));
|
|
||||||
|
|
||||||
log('info', `获取资源信息: ${scriptPath}`);
|
// 2. 验证读取 (FS Read)
|
||||||
const info = await callTool('manage_asset', { action: 'get_info', path: scriptPath });
|
log('info', `验证读取内容...`);
|
||||||
log('info', `资源信息: ${JSON.stringify(info)}`);
|
const readContent = await callTool('manage_script', { action: 'read', path: scriptPath });
|
||||||
|
// 注意:Content 可能会包含一些编辑器自动添加的 meta 信息或者换行,可以宽松匹配
|
||||||
|
assert(readContent && readContent.includes("Initial Content"), `读取内容不匹配。实际: ${readContent}`);
|
||||||
|
log('success', `脚本读取成功`);
|
||||||
|
|
||||||
assert(info && info.url === scriptPath, "无法获取资源信息");
|
// 3. 验证写入 (FS Write + Refresh)
|
||||||
log('success', `已验证资源信息`);
|
log('info', `验证写入内容...`);
|
||||||
|
await callTool('manage_script', { action: 'write', path: scriptPath, content: updatedContent });
|
||||||
|
|
||||||
// 3. 删除资源
|
// 等待刷新
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
const readUpdated = await callTool('manage_script', { action: 'read', path: scriptPath });
|
||||||
|
assert(readUpdated && readUpdated.includes("Updated Content"), `写入后读取内容不匹配。实际: ${readUpdated}`);
|
||||||
|
log('success', `脚本写入成功`);
|
||||||
|
|
||||||
|
// 4. 验证脚本语法 (Validation)
|
||||||
|
log('info', `验证脚本语法...`);
|
||||||
|
const validation = await callTool('validate_script', { filePath: scriptPath });
|
||||||
|
log('info', `验证结果: ${JSON.stringify(validation)}`);
|
||||||
|
assert(validation && validation.valid === true, "脚本验证失败");
|
||||||
|
log('success', `脚本语法验证通过`);
|
||||||
|
|
||||||
|
// 5. 清理
|
||||||
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
||||||
|
log('success', `清理临时脚本`);
|
||||||
|
},
|
||||||
|
|
||||||
// 验证删除 (get_info 应该失败或返回 null/报错,但我们检查工具响应)
|
async testPrefabOperations(sourceNodeId) {
|
||||||
|
log('group', '预制体管理测试 (UUID Mode)');
|
||||||
|
const prefabPath = 'db://assets/AutoTestPrefab.prefab';
|
||||||
|
|
||||||
|
// 确保清理旧的
|
||||||
try {
|
try {
|
||||||
const infoDeleted = await callTool('manage_asset', { action: 'get_info', path: scriptPath });
|
await callTool('manage_asset', { action: 'delete', path: prefabPath });
|
||||||
// 如果返回了信息且 exists 为 true,说明没删掉
|
} catch (e) { }
|
||||||
assert(!(infoDeleted && infoDeleted.exists), "资源本应被删除,但仍然存在");
|
|
||||||
} catch (e) {
|
// 1. 创建预制体
|
||||||
// 如果报错(如 Asset not found),则符合预期
|
log('info', `从节点 ${sourceNodeId} 创建预制体: ${prefabPath}`);
|
||||||
log('success', `已验证资源删除`);
|
await callTool('prefab_management', {
|
||||||
|
action: 'create',
|
||||||
|
path: prefabPath,
|
||||||
|
nodeId: sourceNodeId
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待预制体生成和导入 (使用轮询机制)
|
||||||
|
log('info', '等待预制体生成...');
|
||||||
|
let prefabInfo = null;
|
||||||
|
|
||||||
|
// 每 200ms 检查一次,最多尝试 30 次 (6秒)
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
try {
|
||||||
|
prefabInfo = await callTool('prefab_management', { action: 'get_info', path: prefabPath });
|
||||||
|
if (prefabInfo && prefabInfo.exists) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 最终断言
|
||||||
|
assert(prefabInfo && prefabInfo.exists, "预制体创建失败或未找到 (超时)");
|
||||||
|
log('success', `预制体创建成功: ${prefabInfo.uuid}`);
|
||||||
|
|
||||||
|
// 2. 实例化预制体 (使用 UUID 加载)
|
||||||
|
log('info', `尝试实例化预制体 (UUID: ${prefabInfo.uuid})`);
|
||||||
|
const result = await callTool('prefab_management', {
|
||||||
|
action: 'instantiate',
|
||||||
|
path: prefabPath
|
||||||
|
});
|
||||||
|
log('info', `实例化结果: ${JSON.stringify(result)}`);
|
||||||
|
// 结果通常是一条成功消息字符串
|
||||||
|
assert(result && result.toLowerCase().includes('success'), "实例化失败");
|
||||||
|
log('success', `预制体实例化成功`);
|
||||||
|
|
||||||
|
// 3. 清理预制体
|
||||||
|
await callTool('manage_asset', { action: 'delete', path: prefabPath });
|
||||||
|
log('success', `清理临时预制体`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async testResources() {
|
||||||
|
log('group', 'MCP Resource 协议测试');
|
||||||
|
|
||||||
|
// 1. 列出资源
|
||||||
|
log('info', '请求资源列表 (/list-resources)');
|
||||||
|
const listRes = await request('POST', '/list-resources');
|
||||||
|
log('info', `资源列表响应: ${JSON.stringify(listRes)}`);
|
||||||
|
assert(listRes && listRes.resources && Array.isArray(listRes.resources), "资源列表格式错误");
|
||||||
|
const hasHierarchy = listRes.resources.find(r => r.uri === 'cocos://hierarchy');
|
||||||
|
assert(hasHierarchy, "未找到 cocos://hierarchy 资源");
|
||||||
|
log('success', `成功获取资源列表 (包含 ${listRes.resources.length} 个资源)`);
|
||||||
|
|
||||||
|
// 2. 读取资源: Hierarchy
|
||||||
|
log('info', '读取资源: cocos://hierarchy');
|
||||||
|
const hierarchyRes = await request('POST', '/read-resource', { uri: 'cocos://hierarchy' });
|
||||||
|
assert(hierarchyRes && hierarchyRes.contents && hierarchyRes.contents.length > 0, "读取 Hierarchy 失败");
|
||||||
|
const hierarchyContent = hierarchyRes.contents[0].text;
|
||||||
|
assert(hierarchyContent && hierarchyContent.startsWith('['), "Hierarchy 内容应该是 JSON 数组");
|
||||||
|
log('success', `成功读取场景层级数据`);
|
||||||
|
|
||||||
|
// 3. 读取资源: Logs
|
||||||
|
log('info', '读取资源: cocos://logs/latest');
|
||||||
|
const logsRes = await request('POST', '/read-resource', { uri: 'cocos://logs/latest' });
|
||||||
|
assert(logsRes && logsRes.contents && logsRes.contents.length > 0, "读取 Logs 失败");
|
||||||
|
const logsContent = logsRes.contents[0].text;
|
||||||
|
assert(typeof logsContent === 'string', "日志内容应该是字符串");
|
||||||
|
log('success', `成功读取编辑器日志`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,7 +383,13 @@ async function run() {
|
|||||||
|
|
||||||
await tests.testEditorSelection(nodeId);
|
await tests.testEditorSelection(nodeId);
|
||||||
|
|
||||||
await tests.testAssetManagement();
|
await tests.testEditorSelection(nodeId);
|
||||||
|
|
||||||
|
await tests.testScriptOperations();
|
||||||
|
|
||||||
|
await tests.testPrefabOperations(nodeId);
|
||||||
|
|
||||||
|
await tests.testResources();
|
||||||
|
|
||||||
// 清理:我们在测试中已经尽可能清理了,但保留节点可能有助于观察结果
|
// 清理:我们在测试中已经尽可能清理了,但保留节点可能有助于观察结果
|
||||||
// 这里只是打印完成消息
|
// 这里只是打印完成消息
|
||||||
|
|||||||
Reference in New Issue
Block a user