feat: 实现 open_prefab 工具,优化预制体创建稳定性,并完成全量源码 (JS/TS)、文档与配置的汉化合规审计

This commit is contained in:
火焰库拉
2026-02-13 13:52:27 +08:00
parent 8df6f5a415
commit 24bc7b7b1f
11 changed files with 2024 additions and 1547 deletions

View File

@@ -1,23 +1,53 @@
"use strict";
/**
* MCP Bridge 插件面板脚本
* 负责处理面板 UI 交互、与主进程通信以及提供测试工具界面。
*/
const fs = require("fs");
const { IpcUi } = require("../dist/IpcUi");
Editor.Panel.extend({
/**
* 面板 CSS 样式
*/
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
/**
* 面板 HTML 模板
*/
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
/**
* 监听来自主进程的消息
*/
messages: {
/**
* 接收并渲染日志
* @param {Object} event IPC 事件对象
* @param {Object} log 日志数据
*/
"mcp-bridge:on-log"(event, log) {
this.renderLog(log);
},
/**
* 服务器状态变更通知
* @param {Object} event IPC 事件对象
* @param {Object} config 服务器配置
*/
"mcp-bridge:state-changed"(event, config) {
this.updateUI(config.active);
},
},
/**
* 面板就绪回调,进行 DOM 绑定与事件初始化
*/
ready() {
const root = this.shadowRoot;
// 获取 DOM 元素
// 获取 DOM 元素映射
const els = {
port: root.querySelector("#portInput"),
btnToggle: root.querySelector("#btnToggle"),
@@ -41,7 +71,7 @@ Editor.Panel.extend({
resizer: root.querySelector("#testResizer"),
};
// 1. 初始化状态
// 1. 初始化服务器状态与配置
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
if (data) {
els.port.value = data.config.port;
@@ -52,10 +82,10 @@ Editor.Panel.extend({
}
});
// 初始化 IPC UI
// 初始化 IPC 调试专用 UI (由 TypeScript 编写并编译到 dist)
new IpcUi(root);
// 2. 标签切换
// 2. 标签切换逻辑
els.tabMain.addEventListener("confirm", () => {
els.tabMain.classList.add("active");
els.tabTest.classList.remove("active");
@@ -64,6 +94,7 @@ Editor.Panel.extend({
els.panelTest.classList.remove("active");
els.panelIpc.classList.remove("active");
});
els.tabTest.addEventListener("confirm", () => {
els.tabTest.classList.add("active");
els.tabMain.classList.remove("active");
@@ -71,8 +102,9 @@ Editor.Panel.extend({
els.panelTest.classList.add("active");
els.panelMain.classList.remove("active");
els.panelIpc.classList.remove("active");
this.fetchTools(els);
this.fetchTools(els); // 切换到测试页时自动拉取工具列表
});
els.tabIpc.addEventListener("confirm", () => {
els.tabIpc.classList.add("active");
els.tabMain.classList.remove("active");
@@ -82,39 +114,42 @@ Editor.Panel.extend({
els.panelTest.classList.remove("active");
});
// 3. 基础功能
// 3. 基础控制按钮逻辑
els.btnToggle.addEventListener("confirm", () => {
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(els.port.value));
});
root.querySelector("#btnClear").addEventListener("confirm", () => {
els.logView.innerHTML = "";
Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
});
root.querySelector("#btnCopy").addEventListener("confirm", () => {
require("electron").clipboard.writeText(els.logView.innerText);
Editor.success("日志已复制");
Editor.success("日志已复制到剪贴板");
});
els.autoStart.addEventListener("change", (e) => {
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", e.target.value);
});
// 4. 测试页功能
// 4. API 测试页交互逻辑
els.listBtn.addEventListener("confirm", () => this.fetchTools(els));
els.clearBtn.addEventListener("confirm", () => {
els.result.value = "";
});
els.testBtn.addEventListener("confirm", () => this.runTest(els));
els.testBtn.addEventListener("confirm", () => this.runTest(els));
// 添加 API 探查功能
// API 探查功能 (辅助开发者发现可用内部 IPC)
const probeBtn = root.querySelector("#probeApisBtn");
if (probeBtn) {
probeBtn.addEventListener("confirm", () => {
Editor.Ipc.sendToMain("mcp-bridge:inspect-apis");
els.result.value = "探查指令已发送。请查看编辑器控制台日志。";
els.result.value = "API 探查指令已发送。请查看编辑器控制台 (Console) 获取详细报告。";
});
}
// 5. 【修复】拖拽逻辑
// 5. 测试页分栏拖拽缩放逻辑
if (els.resizer && els.left) {
els.resizer.addEventListener("mousedown", (e) => {
e.preventDefault();
@@ -135,8 +170,13 @@ Editor.Panel.extend({
}
},
/**
* 从本地服务器获取 MCP 工具列表并渲染
* @param {Object} els DOM 元素映射
*/
fetchTools(els) {
const url = `http://localhost:${els.port.value}/list-tools`;
els.result.value = "正在获取工具列表...";
fetch(url)
.then((r) => r.json())
.then((data) => {
@@ -154,64 +194,86 @@ Editor.Panel.extend({
};
els.toolsList.appendChild(item);
});
// 保存工具映射表,以便后续检索
this.toolsMap = toolsMap;
els.result.value = `成功加载 ${data.tools.length} 个工具。`;
els.result.value = `成功加载 ${data.tools.length} 个工具。`;
})
.catch((e) => {
els.result.value = "Error: " + e.message;
els.result.value = "获取失败: " + e.message;
});
},
/**
* 在面板中展示工具的详细描述与参数定义
* @param {Object} els DOM 元素映射
* @param {Object} tool 工具定义对象
*/
showToolDescription(els, tool) {
if (!tool) {
els.toolDescription.textContent = "选择工具查看说明";
els.toolDescription.textContent = "选择工具查看说明";
return;
}
let description = tool.description || "无描述";
let description = tool.description || "无描述";
let inputSchema = tool.inputSchema;
let details = [];
if (inputSchema && inputSchema.properties) {
details.push("参数说明:");
details.push("<b>参数说明:</b>");
for (const [key, prop] of Object.entries(inputSchema.properties)) {
let propDesc = `- ${key}`;
let propDesc = `- <code>${key}</code>`;
if (prop.description) {
propDesc += `: ${prop.description}`;
}
if (prop.required || (inputSchema.required && inputSchema.required.includes(key))) {
propDesc += " (必填)";
propDesc += " <span style='color:#f44'>(必填)</span>";
}
details.push(propDesc);
}
}
els.toolDescription.innerHTML = `${description}<br><br>${details.join('<br>')}`;
els.toolDescription.innerHTML = `${description}<br><br>${details.join("<br>")}`;
},
/**
* 执行工具测试请求
* @param {Object} els DOM 元素映射
*/
runTest(els) {
const url = `http://localhost:${els.port.value}/call-tool`;
const body = { name: els.toolName.value, arguments: JSON.parse(els.toolParams.value || "{}") };
els.result.value = "正在测试...";
let args;
try {
args = JSON.parse(els.toolParams.value || "{}");
} catch (e) {
els.result.value = "JSON 格式错误: " + e.message;
return;
}
const body = { name: els.toolName.value, arguments: args };
els.result.value = "正在发送请求...";
fetch(url, { method: "POST", body: JSON.stringify(body) })
.then((r) => r.json())
.then((d) => {
els.result.value = JSON.stringify(d, null, 2);
})
.catch((e) => {
els.result.value = "Error: " + e.message;
els.result.value = "测试异常: " + e.message;
});
},
/**
* 获取指定工具的示例参数
* @param {string} name 工具名称
* @returns {Object} 示例参数对象
*/
getExample(name) {
const examples = {
set_node_name: { id: "UUID", newName: "Hello" },
update_node_transform: { id: "UUID", x: 0, y: 0, color: "#FF0000" },
create_node: { name: "Node", type: "sprite", parentId: "" },
set_node_name: { id: "节点-UUID", newName: "新名称" },
update_node_transform: { id: "节点-UUID", x: 0, y: 0, color: "#FF0000" },
create_node: { name: "新节点", type: "sprite", parentId: "" },
open_scene: { url: "db://assets/Scene.fire" },
open_prefab: { url: "db://assets/MyPrefab.prefab" },
manage_editor: { action: "get_selection" },
find_gameobjects: { conditions: { name: "Node", active: true }, recursive: true },
find_gameobjects: { conditions: { name: "MyNode", active: true }, recursive: true },
manage_material: {
action: "create",
path: "db://assets/materials/NewMaterial.mat",
@@ -225,7 +287,7 @@ Editor.Panel.extend({
execute_menu_item: { menuPath: "Assets/Create/Folder" },
apply_text_edits: {
filePath: "db://assets/scripts/TestScript.ts",
edits: [{ type: "insert", position: 0, text: "// Test comment\n" }],
edits: [{ type: "insert", position: 0, text: "// 测试注释\n" }],
},
read_console: { limit: 10, type: "log" },
validate_script: { filePath: "db://assets/scripts/TestScript.ts" },
@@ -233,6 +295,10 @@ Editor.Panel.extend({
return examples[name] || {};
},
/**
* 将日志条目渲染至面板控制台
* @param {Object} log 日志对象
*/
renderLog(log) {
const view = this.shadowRoot.querySelector("#logConsole");
if (!view) return;
@@ -244,6 +310,10 @@ Editor.Panel.extend({
if (atBottom) view.scrollTop = view.scrollHeight;
},
/**
* 根据服务器运行状态更新 UI 按钮文字与样式
* @param {boolean} active 服务器是否处于激活状态
*/
updateUI(active) {
const btn = this.shadowRoot.querySelector("#btnToggle");
if (!btn) return;