diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/main.js b/main.js index 69b949b..3001097 100644 --- a/main.js +++ b/main.js @@ -2,6 +2,7 @@ const http = require("http"); const path = require("path"); +const fs = require("fs"); let logBuffer = []; // 存储所有日志 let mcpServer = null; @@ -11,6 +12,7 @@ let serverConfig = { active: false, }; +// 封装日志函数,同时发送给面板和编辑器控制台 // 封装日志函数,同时发送给面板和编辑器控制台 function addLog(type, message) { const logEntry = { @@ -20,13 +22,20 @@ function addLog(type, message) { }; logBuffer.push(logEntry); Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry); - // 【修改】移除 Editor.log,保持编辑器控制台干净 - // 仅在非常严重的系统错误时才输出到编辑器 + + // 【修改】确保所有日志都输出到编辑器控制台,以便用户查看 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 = () => { // 尝试获取 UUID 生成函数 let newId = ""; @@ -492,6 +501,41 @@ module.exports = { // 明确返回成功结构 res.writeHead(200); return res.end(JSON.stringify({ tools: tools })); + res.writeHead(200); + return res.end(JSON.stringify({ tools: tools })); + } + if (url === "/list-resources") { + const resources = this.getResourcesList(); + addLog("info", `AI Client requested resource list`); + res.writeHead(200); + return res.end(JSON.stringify({ resources: resources })); + } + if (url === "/read-resource") { + try { + const { uri } = JSON.parse(body || "{}"); + addLog("mcp", `READ -> [${uri}]`); + this.handleReadResource(uri, (err, content) => { + if (err) { + addLog("error", `读取失败: ${err}`); + res.writeHead(500); + return res.end(JSON.stringify({ error: err })); + } + addLog("success", `读取成功: ${uri}`); + res.writeHead(200); + // 返回 MCP Resource 格式: { contents: [{ uri, mimeType, text }] } + res.end(JSON.stringify({ + contents: [{ + uri: uri, + mimeType: "application/json", + text: typeof content === 'string' ? content : JSON.stringify(content) + }] + })); + }); + } catch (e) { + res.writeHead(500); + res.end(JSON.stringify({ error: e.message })); + } + return; } if (url === "/call-tool") { try { @@ -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) { if (isSceneBusy && (name === "save_scene" || name === "create_node")) { return callback("Editor is busy (Processing Scene), please wait a moment."); @@ -621,37 +725,15 @@ module.exports = { break; case "update_node_transform": - const { id, x, y, scaleX, scaleY, color } = args; - // 将多个属性修改打包到一个 Undo 组中 - Editor.Ipc.sendToPanel("scene", "scene:undo-record", "Transform Update"); - - try { - // 注意:Cocos Creator 属性类型通常首字母大写,如 'Float', 'String', 'Boolean' - // 也有可能支持 'Number',但 'Float' 更保险 - 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"); - }); + // 直接调用场景脚本更新属性,绕过可能导致 "Unknown object" 的复杂 Undo 系统 + Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", args, (err, result) => { + if (err) { + addLog("error", `Transform update failed: ${err}`); + callback(err); + } else { + callback(null, "Transform updated"); } - - Editor.Ipc.sendToPanel("scene", "scene:undo-commit"); - callback(null, "Transform updated"); - } catch (e) { - Editor.Ipc.sendToPanel("scene", "scene:undo-cancel"); - callback(e); - } + }); break; case "create_scene": @@ -797,23 +879,21 @@ module.exports = { // 管理脚本文件 manageScript(args, callback) { - const { action, path, content } = args; + const { action, path: scriptPath, content } = args; switch (action) { case "create": - if (Editor.assetdb.exists(path)) { - return callback(`Script already exists at ${path}`); + if (Editor.assetdb.exists(scriptPath)) { + return callback(`Script already exists at ${scriptPath}`); } // 确保父目录存在 - const fs = require("fs"); - const pathModule = require("path"); - const absolutePath = Editor.assetdb.urlToFspath(path); - const dirPath = pathModule.dirname(absolutePath); + const absolutePath = Editor.assetdb.urlToFspath(scriptPath); + const dirPath = path.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } Editor.assetdb.create( - path, + scriptPath, content || `const { ccclass, property } = cc._decorator; @@ -834,35 +914,50 @@ export default class NewScript extends cc.Component { update (dt) {} }`, (err) => { - callback(err, err ? null : `Script created at ${path}`); + callback(err, err ? null : `Script created at ${scriptPath}`); }, ); break; case "delete": - if (!Editor.assetdb.exists(path)) { - return callback(`Script not found at ${path}`); + if (!Editor.assetdb.exists(scriptPath)) { + return callback(`Script not found at ${scriptPath}`); } - Editor.assetdb.delete([path], (err) => { - callback(err, err ? null : `Script deleted at ${path}`); + Editor.assetdb.delete([scriptPath], (err) => { + callback(err, err ? null : `Script deleted at ${scriptPath}`); }); 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); - }); - }); + // 使用 fs 读取,绕过 assetdb.loadAny + const readFsPath = Editor.assetdb.urlToFspath(scriptPath); + if (!readFsPath || !fs.existsSync(readFsPath)) { + return callback(`Script not found at ${scriptPath}`); + } + try { + const content = fs.readFileSync(readFsPath, "utf-8"); + callback(null, content); + } catch (e) { + callback(`Failed to read script: ${e.message}`); + } break; case "write": - Editor.assetdb.create(path, content, (err) => { - callback(err, err ? null : `Script updated at ${path}`); - }); + // 使用 fs 写入 + refresh,确保覆盖成功 + const writeFsPath = Editor.assetdb.urlToFspath(scriptPath); + if (!writeFsPath) { + return callback(`Invalid path: ${scriptPath}`); + } + + try { + fs.writeFileSync(writeFsPath, content, "utf-8"); + Editor.assetdb.refresh(scriptPath, (err) => { + if (err) addLog("warn", `Refresh failed after write: ${err}`); + callback(null, `Script updated at ${scriptPath}`); + }); + } catch (e) { + callback(`Failed to write script: ${e.message}`); + } break; default: @@ -936,18 +1031,20 @@ export default class NewScript extends cc.Component { }); break; + case "get_info": try { if (!Editor.assetdb.exists(path)) { return callback(`Asset not found: ${path}`); } const uuid = Editor.assetdb.urlToUuid(path); - // Return basic info constructed manually to avoid API compatibility issues - callback(null, { - url: path, - uuid: uuid, - exists: true - }); + const info = Editor.assetdb.assetInfoByUuid(uuid); + if (info) { + callback(null, info); + } else { + // Fallback if API returns nothing but asset exists + callback(null, { url: path, uuid: uuid, exists: true }); + } } catch (e) { callback(`Error getting asset info: ${e.message}`); } @@ -1021,9 +1118,13 @@ export default class NewScript extends cc.Component { break; case "get_info": - Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => { - callback(err, err ? null : info); - }); + if (Editor.assetdb.exists(path)) { + const uuid = Editor.assetdb.urlToUuid(path); + const info = Editor.assetdb.assetInfoByUuid(uuid); + callback(null, info || { url: path, uuid: uuid, exists: true }); + } else { + callback(`Scene not found: ${path}`); + } break; default: @@ -1034,51 +1135,70 @@ export default class NewScript extends cc.Component { // 预制体管理 prefabManagement(args, callback) { - const { action, path, nodeId, parentId } = args; + const { action, path: prefabPath, nodeId, parentId } = args; switch (action) { case "create": if (!nodeId) { return callback(`Node ID is required for create operation`); } - if (Editor.assetdb.exists(path)) { - return callback(`Prefab already exists at ${path}`); + if (Editor.assetdb.exists(prefabPath)) { + return callback(`Prefab already exists at ${prefabPath}`); } // 确保父目录存在 - const fs = require("fs"); - const pathModule = require("path"); - const absolutePath = Editor.assetdb.urlToFspath(path); - const dirPath = pathModule.dirname(absolutePath); + const absolutePath = Editor.assetdb.urlToFspath(prefabPath); + const dirPath = path.dirname(absolutePath); if (!fs.existsSync(dirPath)) { 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; case "update": if (!nodeId) { return callback(`Node ID is required for update operation`); } - if (!Editor.assetdb.exists(path)) { - return callback(`Prefab not found at ${path}`); + if (!Editor.assetdb.exists(prefabPath)) { + return callback(`Prefab not found at ${prefabPath}`); } // 更新预制体 - Editor.Ipc.sendToMain("scene:update-prefab", nodeId, path); - callback(null, `Command sent: Updating prefab ${path} from node ${nodeId}`); + Editor.Ipc.sendToPanel("scene", "scene:update-prefab", nodeId, prefabPath); + callback(null, `Command sent: Updating prefab ${prefabPath} from node ${nodeId}`); break; case "instantiate": - if (!Editor.assetdb.exists(path)) { - return callback(`Prefab not found at ${path}`); + if (!Editor.assetdb.exists(prefabPath)) { + return callback(`Prefab not found at ${prefabPath}`); } // 实例化预制体 + const prefabUuid = Editor.assetdb.urlToUuid(prefabPath); Editor.Scene.callSceneScript( "mcp-bridge", "instantiate-prefab", { - prefabPath: path, + prefabUuid: prefabUuid, parentId: parentId, }, callback, @@ -1086,9 +1206,16 @@ export default class NewScript extends cc.Component { break; case "get_info": - Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => { - callback(err, err ? null : info); - }); + if (Editor.assetdb.exists(prefabPath)) { + const uuid = Editor.assetdb.urlToUuid(prefabPath); + const info = Editor.assetdb.assetInfoByUuid(uuid); + // 确保返回对象包含 exists: true,以满足测试验证 + const result = info || { url: prefabPath, uuid: uuid }; + result.exists = true; + callback(null, result); + } else { + callback(`Prefab not found: ${prefabPath}`); + } break; default: @@ -1181,9 +1308,13 @@ export default class NewScript extends cc.Component { }); break; case "get_info": - Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => { - callback(err, err ? null : info); - }); + if (Editor.assetdb.exists(path)) { + const uuid = Editor.assetdb.urlToUuid(path); + const info = Editor.assetdb.assetInfoByUuid(uuid); + callback(null, info || { url: path, uuid: uuid, exists: true }); + } else { + callback(`Material not found: ${path}`); + } break; default: callback(`Unknown material action: ${action}`); @@ -1230,9 +1361,13 @@ export default class NewScript extends cc.Component { }); break; case "get_info": - Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => { - callback(err, err ? null : info); - }); + if (Editor.assetdb.exists(path)) { + const uuid = Editor.assetdb.urlToUuid(path); + const info = Editor.assetdb.assetInfoByUuid(uuid); + callback(null, info || { url: path, uuid: uuid, exists: true }); + } else { + callback(`Texture not found: ${path}`); + } break; default: callback(`Unknown texture action: ${action}`); @@ -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) { const { filePath, edits } = args; - // 读取文件内容 - Editor.assetdb.queryInfoByUrl(filePath, (err, info) => { - if (err) { - callback(`Failed to get file info: ${err.message}`); - return; - } + // 1. 获取文件系统路径 + const fspath = Editor.assetdb.urlToFspath(filePath); + if (!fspath) { + return callback(`File not found or invalid URL: ${filePath}`); + } - Editor.assetdb.loadAny(filePath, (err, content) => { - if (err) { - callback(`Failed to load file: ${err.message}`); - return; - } + const fs = require("fs"); + if (!fs.existsSync(fspath)) { + return callback(`File does not exist: ${fspath}`); + } - // 应用编辑操作 - let updatedContent = content; - edits.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; - } - }); + try { + // 2. 读取 + let updatedContent = fs.readFileSync(fspath, "utf-8"); - // 写回文件 - Editor.assetdb.create(filePath, updatedContent, (err) => { - callback(err, err ? null : `Text edits applied to ${filePath}`); - }); + // 3. 应用编辑 + // 必须按倒序应用编辑,否则后续编辑的位置会偏移 (假设edits未排序,这里简单处理,实际上LSP通常建议客户端倒序应用或计算偏移) + // 这里假设edits已经按照位置排序或者用户负责,如果需要严谨,应先按 start/position 倒序排序 + // 简单做个排序保险: + const sortedEdits = [...edits].sort((a, b) => { + const posA = a.position !== undefined ? a.position : a.start; + const posB = b.position !== undefined ? b.position : b.start; + return posB - posA; // 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) { const { filePath } = args; - // 读取脚本内容 - Editor.assetdb.queryInfoByUrl(filePath, (err, info) => { - if (err) { - callback(`Failed to get file info: ${err.message}`); - return; - } + // 1. 获取文件系统路径 + const fspath = Editor.assetdb.urlToFspath(filePath); + if (!fspath) { + return callback(`File not found or invalid URL: ${filePath}`); + } - Editor.assetdb.loadAny(filePath, (err, content) => { - if (err) { - callback(`Failed to load file: ${err.message}`); - return; - } + // 2. 检查文件是否存在 + const fs = require("fs"); + if (!fs.existsSync(fspath)) { + return callback(`File does not exist: ${fspath}`); + } + // 3. 读取内容并验证 + try { + const content = fs.readFileSync(fspath, "utf-8"); + + // 对于 JavaScript 脚本,使用 eval 进行简单验证 + if (filePath.endsWith(".js")) { + // 包装在函数中以避免变量污染 + const wrapper = `(function() { ${content} })`; try { - // 对于 JavaScript 脚本,使用 eval 进行简单验证 - if (filePath.endsWith(".js")) { - // 包装在函数中以避免变量污染 - 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 }); + new Function(wrapper); // 使用 Function 构造器比 direct eval稍微安全一点点,虽在这个场景下差别不大 + } catch (syntaxErr) { + return callback(null, { valid: false, message: syntaxErr.message }); } - }); - }); + } + // 对于 TypeScript,暂不进行复杂编译检查,仅确保文件可读 + + callback(null, { valid: true, message: "Script syntax is valid" }); + } catch (err) { + callback(null, { valid: false, message: `Read Error: ${err.message}` }); + } }, // 暴露给 MCP 或面板的 API 封装 messages: { "open-test-panel"() { Editor.Panel.open("mcp-bridge"); }, - "get-server-state"(event) { - event.reply(null, { config: serverConfig, logs: logBuffer }); - }, + "toggle-server"(event, port) { if (serverConfig.active) this.stopServer(); else this.startServer(port); @@ -1419,42 +1560,129 @@ export default class NewScript extends cc.Component { this.getProfile().save(); addLog("info", `Auto-start set to: ${value}`); }, - }, - // 验证脚本 - validateScript(args, callback) { - 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; - } + "inspect-apis"() { + addLog("info", "[API Inspector] Starting DEEP inspection..."); + // Helper to get function arguments + const getArgs = (func) => { try { - // 对于 JavaScript 脚本,使用 eval 进行简单验证 - if (filePath.endsWith('.js')) { - // 包装在函数中以避免变量污染 - const wrapper = `(function() { ${content} })`; - eval(wrapper); + const str = func.toString(); + const match = str.match(/function\s.*?\(([^)]*)\)/) || str.match(/.*?\(([^)]*)\)/); + if (match) { + return match[1].split(",").map(arg => arg.trim()).filter(a => a).join(", "); } - // 对于 TypeScript 脚本,这里可以添加更复杂的验证逻辑 + return `${func.length} args`; + } catch (e) { + return "?"; + } + }; - callback(null, { valid: true, message: 'Script syntax is valid' }); - } catch (err) { - callback(null, { valid: false, message: err.message }); + // Helper to inspect an object + const inspectObj = (name, obj) => { + if (!obj) return { name, exists: false }; + const props = {}; + const proto = Object.getPrototypeOf(obj); + + // 组合自身属性和原型属性 + const allKeys = new Set([...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertyNames(proto || {})]); + + allKeys.forEach(key => { + if (key.startsWith("_")) return; // 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) { const { query, extensions, includeSubpackages } = args; diff --git a/panel/index.html b/panel/index.html index 84a8e76..1a873a5 100644 --- a/panel/index.html +++ b/panel/index.html @@ -48,6 +48,7 @@ 测试工具 刷新列表 清空结果 + 探查 API
diff --git a/panel/index.js b/panel/index.js index 9a1255f..0dd077b 100644 --- a/panel/index.js +++ b/panel/index.js @@ -86,6 +86,14 @@ Editor.Panel.extend({ els.result.value = ""; }); 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. 【修复】拖拽逻辑 if (els.resizer && els.left) { @@ -141,10 +149,10 @@ Editor.Panel.extend({ els.toolDescription.textContent = "选择工具查看说明"; return; } - + let description = tool.description || "无描述"; let inputSchema = tool.inputSchema; - + let details = []; if (inputSchema && inputSchema.properties) { details.push("参数说明:"); @@ -159,7 +167,7 @@ Editor.Panel.extend({ details.push(propDesc); } } - + els.toolDescription.innerHTML = `${description}

${details.join('
')}`; }, diff --git a/scene-script.js b/scene-script.js index f3d5d46..a473b74 100644 --- a/scene-script.js +++ b/scene-script.js @@ -68,23 +68,34 @@ module.exports = { "update-node-transform": function (event, 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); if (node) { - if (x !== undefined) node.x = x; - if (y !== undefined) node.y = y; - if (scaleX !== undefined) node.scaleX = scaleX; - if (scaleY !== undefined) node.scaleY = scaleY; + Editor.log(`[scene-script] Node found: ${node.name}, Current Pos: (${node.x}, ${node.y})`); + + if (x !== undefined) { + 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) { - // color 格式如 "#FF0000" node.color = new cc.Color().fromHEX(color); } Editor.Ipc.sendToMain("scene:dirty"); 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"); } else { + Editor.error(`[scene-script] Node not found: ${id}`); if (event.reply) event.reply(new Error("Node not found")); } }, @@ -308,7 +319,7 @@ module.exports = { }, "instantiate-prefab": function (event, args) { - const { prefabPath, parentId } = args; + const { prefabUuid, parentId } = args; const scene = cc.director.getScene(); if (!scene) { @@ -316,8 +327,14 @@ module.exports = { return; } - // 加载预制体资源 - cc.loader.loadRes(prefabPath.replace("db://assets/", "").replace(".prefab", ""), cc.Prefab, (err, prefab) => { + if (!prefabUuid) { + 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 (event.reply) event.reply(new Error(`Failed to load prefab: ${err.message}`)); return; diff --git a/test/run_tests.js b/test/run_tests.js index 900f415..e3ae07f 100644 --- a/test/run_tests.js +++ b/test/run_tests.js @@ -229,53 +229,143 @@ const tests = { log('success', `编辑器选中状态已更新为节点 ${nodeId}`); }, - async testAssetManagement() { - log('group', '资源管理测试'); - const scriptPath = 'db://assets/temp_auto_test.js'; + async testScriptOperations() { + log('group', '脚本读写与验证测试 (FS Mode)'); + const scriptPath = 'db://assets/auto_test_script.js'; + const initialContent = 'cc.log("Initial Content");'; + const updatedContent = 'cc.log("Updated Content");'; // 1. 创建脚本 try { + log('info', `创建脚本: ${scriptPath}`); await callTool('manage_script', { action: 'create', path: scriptPath, - content: 'cc.log("Test Script");' + content: initialContent }); - log('success', `已创建临时资源: ${scriptPath}`); + log('success', `脚本已创建`); } catch (e) { if (e.message.includes('exists')) { - log('warn', `资源已存在,正在尝试先删除...`); + log('warn', `脚本已存在,尝试删除重建...`); await callTool('manage_asset', { action: 'delete', path: scriptPath }); - // 重试创建 - await callTool('manage_script', { action: 'create', path: scriptPath, content: 'cc.log("Test Script");' }); + await callTool('manage_script', { action: 'create', path: scriptPath, content: initialContent }); } else { throw e; } } - // 2. 获取信息 - // 等待 AssetDB 刷新 (导入需要时间) - log('info', '等待 3 秒以进行资源导入...'); - await new Promise(r => setTimeout(r, 3000)); + // 等待资源导入 + await new Promise(r => setTimeout(r, 2000)); - log('info', `获取资源信息: ${scriptPath}`); - const info = await callTool('manage_asset', { action: 'get_info', path: scriptPath }); - log('info', `资源信息: ${JSON.stringify(info)}`); + // 2. 验证读取 (FS Read) + log('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, "无法获取资源信息"); - log('success', `已验证资源信息`); + // 3. 验证写入 (FS Write + Refresh) + 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 }); + log('success', `清理临时脚本`); + }, - // 验证删除 (get_info 应该失败或返回 null/报错,但我们检查工具响应) + async testPrefabOperations(sourceNodeId) { + log('group', '预制体管理测试 (UUID Mode)'); + const prefabPath = 'db://assets/AutoTestPrefab.prefab'; + + // 确保清理旧的 try { - const infoDeleted = await callTool('manage_asset', { action: 'get_info', path: scriptPath }); - // 如果返回了信息且 exists 为 true,说明没删掉 - assert(!(infoDeleted && infoDeleted.exists), "资源本应被删除,但仍然存在"); - } catch (e) { - // 如果报错(如 Asset not found),则符合预期 - log('success', `已验证资源删除`); + await callTool('manage_asset', { action: 'delete', path: prefabPath }); + } catch (e) { } + + // 1. 创建预制体 + log('info', `从节点 ${sourceNodeId} 创建预制体: ${prefabPath}`); + 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.testAssetManagement(); + await tests.testEditorSelection(nodeId); + + await tests.testScriptOperations(); + + await tests.testPrefabOperations(nodeId); + + await tests.testResources(); // 清理:我们在测试中已经尽可能清理了,但保留节点可能有助于观察结果 // 这里只是打印完成消息