```
feat(mcp-bridge): 实现MCP服务器功能增强和日志系统 - 添加日志缓冲区和封装日志函数,支持多种日志类型(info, success, warn, error, mcp) - 实现MCP服务器启动/停止功能,支持端口配置和状态管理 - 添加配置文件管理(auto-start, last-port),支持持久化设置 - 实现完整的工具API接口(get_selected_node, set_node_name, save_scene等) - 统一处理MCP调用逻辑,便于日志记录和错误处理 - 更新面板界面,添加端口输入、自动启动开关、日志查看等功能 - 优化错误处理和响应格式,符合MCP标准规范 ```
This commit is contained in:
378
main.js
378
main.js
@@ -3,6 +3,29 @@
|
|||||||
const http = require("http");
|
const http = require("http");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
let logBuffer = []; // 存储所有日志
|
||||||
|
let mcpServer = null;
|
||||||
|
let serverConfig = {
|
||||||
|
port: 3456,
|
||||||
|
active: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 封装日志函数,同时发送给面板和编辑器控制台
|
||||||
|
function addLog(type, message) {
|
||||||
|
const logEntry = {
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
type: type,
|
||||||
|
content: message,
|
||||||
|
};
|
||||||
|
logBuffer.push(logEntry);
|
||||||
|
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:on-log", logEntry);
|
||||||
|
// 【修改】移除 Editor.log,保持编辑器控制台干净
|
||||||
|
// 仅在非常严重的系统错误时才输出到编辑器
|
||||||
|
if (type === "error") {
|
||||||
|
Editor.error(`[MCP] ${message}`); // 如果你完全不想在编辑器看,可以注释掉
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getNewSceneTemplate = () => {
|
const getNewSceneTemplate = () => {
|
||||||
// 尝试获取 UUID 生成函数
|
// 尝试获取 UUID 生成函数
|
||||||
let newId = "";
|
let newId = "";
|
||||||
@@ -43,46 +66,34 @@ const getNewSceneTemplate = () => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"scene-script": "scene-script.js",
|
"scene-script": "scene-script.js",
|
||||||
load() {
|
load() {
|
||||||
// 插件加载时启动一个微型服务器供 MCP 使用 (默认端口 3000)
|
addLog("info", "MCP Bridge Plugin Loaded");
|
||||||
this.startMcpServer();
|
// 读取配置
|
||||||
|
let profile = this.getProfile();
|
||||||
|
serverConfig.port = profile.get("last-port") || 3456;
|
||||||
|
let autoStart = profile.get("auto-start");
|
||||||
|
|
||||||
|
if (autoStart) {
|
||||||
|
addLog("info", "Auto-start is enabled. Initializing server...");
|
||||||
|
// 延迟一点启动,确保编辑器环境完全就绪
|
||||||
|
setTimeout(() => {
|
||||||
|
this.startServer(serverConfig.port);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取配置文件的辅助函数
|
||||||
|
getProfile() {
|
||||||
|
// 'local' 表示存储在项目本地(local/mcp-bridge.json)
|
||||||
|
return Editor.Profile.load("profile://local/mcp-bridge.json", "mcp-bridge");
|
||||||
},
|
},
|
||||||
|
|
||||||
unload() {
|
unload() {
|
||||||
if (this.server) this.server.close();
|
this.stopServer();
|
||||||
},
|
},
|
||||||
|
startServer(port) {
|
||||||
|
if (mcpServer) this.stopServer();
|
||||||
|
|
||||||
// 暴露给 MCP 或面板的 API 封装
|
try {
|
||||||
messages: {
|
mcpServer = http.createServer((req, res) => {
|
||||||
"open-test-panel"() {
|
|
||||||
Editor.Panel.open("mcp-bridge");
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取当前选中节点信息
|
|
||||||
"get-selected-info"(event) {
|
|
||||||
let selection = Editor.Selection.curSelection("node");
|
|
||||||
if (event) event.reply(null, selection);
|
|
||||||
return selection;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 修改场景中的节点(需要通过 scene-script)
|
|
||||||
"set-node-property"(event, args) {
|
|
||||||
Editor.log("Calling scene script with:", args); // 打印日志确认 main 进程收到了面板的消息
|
|
||||||
|
|
||||||
// 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致
|
|
||||||
Editor.Scene.callSceneScript("mcp-bridge", "set-property", args, (err, result) => {
|
|
||||||
if (err) {
|
|
||||||
Editor.error("Scene Script Error:", err);
|
|
||||||
}
|
|
||||||
if (event && event.reply) {
|
|
||||||
event.reply(err, result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// 简易 MCP 桥接服务器
|
|
||||||
startMcpServer() {
|
|
||||||
this.server = http.createServer((req, res) => {
|
|
||||||
// 设置 CORS 方便调试
|
// 设置 CORS 方便调试
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
@@ -207,137 +218,39 @@ module.exports = {
|
|||||||
return res.end(JSON.stringify({ tools }));
|
return res.end(JSON.stringify({ tools }));
|
||||||
}
|
}
|
||||||
if (req.url === "/call-tool" && req.method === "POST") {
|
if (req.url === "/call-tool" && req.method === "POST") {
|
||||||
// 2. 执行工具逻辑
|
try {
|
||||||
const { name, arguments: args } = JSON.parse(body);
|
const { name, arguments: args } = JSON.parse(body);
|
||||||
|
|
||||||
if (name === "get_selected_node") {
|
addLog("mcp", `REQ -> [${name}] ${JSON.stringify(args)}`);
|
||||||
let ids = Editor.Selection.curSelection("node");
|
|
||||||
res.end(JSON.stringify({ content: [{ type: "text", text: JSON.stringify(ids) }] }));
|
this.handleMcpCall(name, args, (err, result) => {
|
||||||
} else if (name === "set_node_name") {
|
// 3. 构建 MCP 标准响应格式
|
||||||
Editor.Scene.callSceneScript(
|
const response = {
|
||||||
"mcp-bridge",
|
content: [
|
||||||
"set-property",
|
|
||||||
{
|
{
|
||||||
id: args.id,
|
type: "text",
|
||||||
path: "name",
|
text: err
|
||||||
value: args.newName,
|
? `Error: ${err}`
|
||||||
|
: typeof result === "object"
|
||||||
|
? JSON.stringify(result, null, 2)
|
||||||
|
: result,
|
||||||
},
|
},
|
||||||
(err, result) => {
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: err ? `Error: ${err}` : `Success: ${result}` },
|
|
||||||
],
|
],
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
},
|
// 4. 记录返回日志
|
||||||
);
|
|
||||||
} else if (name === "save_scene") {
|
|
||||||
// 触发编辑器保存指令
|
|
||||||
Editor.Ipc.sendToMain("scene:save-scene");
|
|
||||||
res.end(JSON.stringify({ content: [{ type: "text", text: "Scene saved successfully" }] }));
|
|
||||||
} else if (name === "get_scene_hierarchy") {
|
|
||||||
Editor.Scene.callSceneScript("mcp-bridge", "get-hierarchy", (err, hierarchy) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
res.end(
|
addLog("error", `RES <- [${name}] Failed: ${err}`);
|
||||||
JSON.stringify({
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: "Error fetching hierarchy: " + err.message },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
res.end(
|
// 日志里只显示简短的返回值,防止长 JSON(如 hierarchy)刷屏
|
||||||
JSON.stringify({
|
const logRes = typeof result === "object" ? "[Object Data]" : result;
|
||||||
content: [{ type: "text", text: JSON.stringify(hierarchy, null, 2) }],
|
addLog("success", `RES <- [${name}] Success: ${logRes}`);
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
});
|
});
|
||||||
} else if (name === "update_node_transform") {
|
} catch (e) {
|
||||||
Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", args, (err, result) => {
|
addLog("error", `Parse Error: ${e.message}`);
|
||||||
res.end(
|
res.end(JSON.stringify({ content: [{ type: "text", text: `Error: ${e.message}` }] }));
|
||||||
JSON.stringify({
|
|
||||||
content: [{ type: "text", text: err ? `Error: ${err}` : result }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else if (name === "create_scene") {
|
|
||||||
const url = `db://assets/${args.sceneName}.fire`;
|
|
||||||
if (Editor.assetdb.exists(url)) {
|
|
||||||
return res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [{ type: "text", text: "Error: Scene already exists" }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成标准场景内容
|
|
||||||
const sceneJson = getNewSceneTemplate();
|
|
||||||
|
|
||||||
Editor.assetdb.create(url, sceneJson, (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [{ type: "text", text: "Error creating scene: " + err }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [{ type: "text", text: `Standard Scene created at ${url}` }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (name === "create_prefab") {
|
|
||||||
const url = `db://assets/${args.prefabName}.prefab`;
|
|
||||||
// 2.4.x 创建预制体的 IPC 消息
|
|
||||||
Editor.Ipc.sendToMain("scene:create-prefab", args.nodeId, url);
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: `Command sent: Creating prefab '${args.prefabName}'` },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (name === "open_scene") {
|
|
||||||
const url = args.url;
|
|
||||||
// 1. 将 db:// 路径转换为 UUID
|
|
||||||
const uuid = Editor.assetdb.urlToUuid(url);
|
|
||||||
|
|
||||||
if (uuid) {
|
|
||||||
// 2. 发送核心 IPC 消息给主进程
|
|
||||||
// scene:open-by-uuid 是编辑器内置的场景打开逻辑
|
|
||||||
Editor.Ipc.sendToMain("scene:open-by-uuid", uuid);
|
|
||||||
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: `Success: Opening scene ${url} (UUID: ${uuid})` },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: `Error: Could not find asset with URL ${url}` },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (name === "create_node") {
|
|
||||||
// 转发给场景脚本处理
|
|
||||||
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => {
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: err ? `Error: ${err}` : `Node created: ${result}` },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
@@ -350,7 +263,154 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.server.listen(3456);
|
mcpServer.listen(port, () => {
|
||||||
Editor.log("MCP Server standard interface listening on http://localhost:3456");
|
serverConfig.active = true;
|
||||||
|
addLog("success", `Server started on port ${port}`);
|
||||||
|
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
mcpServer.on("error", (err) => {
|
||||||
|
addLog("error", `Server Error: ${err.message}`);
|
||||||
|
this.stopServer();
|
||||||
|
});
|
||||||
|
// 启动成功后顺便存一下端口
|
||||||
|
this.getProfile().set("last-port", port);
|
||||||
|
this.getProfile().save();
|
||||||
|
} catch (e) {
|
||||||
|
addLog("error", `Failed to start server: ${e.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopServer() {
|
||||||
|
if (mcpServer) {
|
||||||
|
mcpServer.close();
|
||||||
|
mcpServer = null;
|
||||||
|
serverConfig.active = false;
|
||||||
|
addLog("warn", "MCP Server stopped");
|
||||||
|
Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 统一处理逻辑,方便日志记录
|
||||||
|
handleMcpCall(name, args, callback) {
|
||||||
|
switch (name) {
|
||||||
|
case "get_selected_node":
|
||||||
|
const ids = Editor.Selection.curSelection("node");
|
||||||
|
callback(null, ids);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "set_node_name":
|
||||||
|
Editor.Scene.callSceneScript(
|
||||||
|
"mcp-bridge",
|
||||||
|
"set-property",
|
||||||
|
{
|
||||||
|
id: args.id,
|
||||||
|
path: "name",
|
||||||
|
value: args.newName,
|
||||||
|
},
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "save_scene":
|
||||||
|
Editor.Ipc.sendToMain("scene:save-scene");
|
||||||
|
callback(null, "Scene saved successfully");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "get_scene_hierarchy":
|
||||||
|
Editor.Scene.callSceneScript("mcp-bridge", "get-hierarchy", callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "update_node_transform":
|
||||||
|
Editor.Scene.callSceneScript("mcp-bridge", "update-node-transform", args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "create_scene":
|
||||||
|
const sceneUrl = `db://assets/${args.sceneName}.fire`;
|
||||||
|
if (Editor.assetdb.exists(sceneUrl)) {
|
||||||
|
return callback("Scene already exists");
|
||||||
|
}
|
||||||
|
Editor.assetdb.create(sceneUrl, getNewSceneTemplate(), (err) => {
|
||||||
|
callback(err, err ? null : `Standard Scene created at ${sceneUrl}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "create_prefab":
|
||||||
|
const prefabUrl = `db://assets/${args.prefabName}.prefab`;
|
||||||
|
Editor.Ipc.sendToMain("scene:create-prefab", args.nodeId, prefabUrl);
|
||||||
|
callback(null, `Command sent: Creating prefab '${args.prefabName}'`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "open_scene":
|
||||||
|
const openUuid = Editor.assetdb.urlToUuid(args.url);
|
||||||
|
if (openUuid) {
|
||||||
|
Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid);
|
||||||
|
callback(null, `Success: Opening scene ${args.url}`);
|
||||||
|
} else {
|
||||||
|
callback(`Could not find asset with URL ${args.url}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "create_node":
|
||||||
|
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callback(`Unknown tool: ${name}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 暴露给 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);
|
||||||
|
},
|
||||||
|
"clear-logs"() {
|
||||||
|
logBuffer = [];
|
||||||
|
addLog("info", "Logs cleared");
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改场景中的节点(需要通过 scene-script)
|
||||||
|
"set-node-property"(event, args) {
|
||||||
|
addLog("mcp", `Creating node: ${args.name} (${args.type})`);
|
||||||
|
// 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致
|
||||||
|
Editor.Scene.callSceneScript("mcp-bridge", "set-property", args, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
Editor.error("Scene Script Error:", err);
|
||||||
|
}
|
||||||
|
if (event && event.reply) {
|
||||||
|
event.reply(err, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"create-node"(event, args) {
|
||||||
|
addLog("mcp", `Creating node: ${args.name} (${args.type})`);
|
||||||
|
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => {
|
||||||
|
if (err) addLog("error", `CreateNode Failed: ${err}`);
|
||||||
|
else addLog("success", `Node Created: ${result}`);
|
||||||
|
event.reply(err, result);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"get-server-state"(event) {
|
||||||
|
let profile = this.getProfile();
|
||||||
|
event.reply(null, {
|
||||||
|
config: serverConfig,
|
||||||
|
logs: logBuffer,
|
||||||
|
autoStart: profile.get("auto-start"), // 返回自动启动状态
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
"set-auto-start"(event, value) {
|
||||||
|
this.getProfile().set("auto-start", value);
|
||||||
|
this.getProfile().save();
|
||||||
|
addLog("info", `Auto-start set to: ${value}`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,5 +16,11 @@
|
|||||||
"title": "MCP Test Panel",
|
"title": "MCP Test Panel",
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 300
|
"height": 300
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"local": {
|
||||||
|
"auto-start": false,
|
||||||
|
"last-port": 3456
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
panel/index.html
114
panel/index.html
@@ -1,17 +1,105 @@
|
|||||||
<div style="padding: 10px;">
|
<div class="mcp-container">
|
||||||
<ui-prop name="Node ID">
|
<div class="toolbar">
|
||||||
<ui-input id="nodeId" placeholder="Click 'Get' to fetch ID"></ui-input>
|
<div class="ctrl-group">
|
||||||
</ui-prop>
|
<span>Port:</span>
|
||||||
<ui-prop name="New Name">
|
<ui-input id="portInput" value="3456"></ui-input>
|
||||||
<ui-input id="newName" placeholder="Enter new name"></ui-input>
|
<ui-button id="btnToggle" class="green">Start</ui-button>
|
||||||
</ui-prop>
|
|
||||||
|
|
||||||
<div style="margin-top: 10px; display: flex; flex-direction: column; gap: 5px;">
|
|
||||||
<ui-button id="btn-get" class="green">获取选中节点 ID</ui-button>
|
|
||||||
<ui-button id="btn-set" class="blue">修改节点名称</ui-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="log" style="margin-top: 15px; font-size: 12px; color: #66ccff; border-top: 1px solid #555; padding-top: 5px;">
|
<!-- 新增的自动启动勾选框 -->
|
||||||
Status: Ready
|
<div class="ctrl-group" style="margin-left: 15px">
|
||||||
|
<ui-checkbox id="autoStartCheck">Auto Start</ui-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<ui-button id="btnClear" class="transparent">Clear</ui-button>
|
||||||
|
<ui-button id="btnCopy" class="transparent">Copy All</ui-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志区域 -->
|
||||||
|
<div id="logConsole" class="log-view"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
}
|
||||||
|
.mcp-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
flex-shrink: 0; /* 禁止头部压缩 */
|
||||||
|
}
|
||||||
|
.ctrl-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-view {
|
||||||
|
flex: 1; /* 自动撑满剩余空间 */
|
||||||
|
background: #1a1a1a;
|
||||||
|
margin-top: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: "Consolas", "Monaco", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
/* 【关键】允许鼠标选中文字 */
|
||||||
|
-webkit-user-select: text;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border-left: 4px solid #444;
|
||||||
|
padding-left: 8px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 颜色修正 */
|
||||||
|
.time {
|
||||||
|
color: #5c6370;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
border-left-color: #61afef;
|
||||||
|
color: #abb2bf;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
border-left-color: #98c379;
|
||||||
|
color: #98c379;
|
||||||
|
}
|
||||||
|
.warn {
|
||||||
|
border-left-color: #e5c07b;
|
||||||
|
color: #e5c07b;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
border-left-color: #e06c75;
|
||||||
|
color: #e06c75;
|
||||||
|
}
|
||||||
|
.mcp {
|
||||||
|
border-left-color: #c678dd;
|
||||||
|
color: #d19a66;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
106
panel/index.js
106
panel/index.js
@@ -1,59 +1,89 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
Editor.Panel.extend({
|
Editor.Panel.extend({
|
||||||
// 读取样式和模板
|
|
||||||
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
style: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
template: fs.readFileSync(Editor.url("packages://mcp-bridge/panel/index.html"), "utf-8"),
|
||||||
|
|
||||||
// 面板渲染成功后的回调
|
messages: {
|
||||||
|
"mcp-bridge:on-log"(event, log) {
|
||||||
|
this.renderLog(log);
|
||||||
|
},
|
||||||
|
"mcp-bridge:state-changed"(event, config) {
|
||||||
|
this.updateUI(config.active);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
ready() {
|
ready() {
|
||||||
// 使用 querySelector 确保能拿到元素,避免依赖可能为 undefined 的 this.$
|
const portInput = this.shadowRoot.querySelector("#portInput");
|
||||||
const btnGet = this.shadowRoot.querySelector("#btn-get");
|
const btnToggle = this.shadowRoot.querySelector("#btnToggle");
|
||||||
const btnSet = this.shadowRoot.querySelector("#btn-set");
|
const autoStartCheck = this.shadowRoot.querySelector("#autoStartCheck");
|
||||||
const nodeIdInput = this.shadowRoot.querySelector("#nodeId");
|
const btnClear = this.shadowRoot.querySelector("#btnClear");
|
||||||
const newNameInput = this.shadowRoot.querySelector("#newName");
|
const btnCopy = this.shadowRoot.querySelector("#btnCopy");
|
||||||
const logDiv = this.shadowRoot.querySelector("#log");
|
const logView = this.shadowRoot.querySelector("#logConsole");
|
||||||
|
|
||||||
if (!btnGet || !btnSet) {
|
// 初始化
|
||||||
Editor.error("Failed to find UI elements. Check if IDs in HTML match.");
|
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||||
return;
|
if (data) {
|
||||||
}
|
portInput.value = data.config.port;
|
||||||
|
this.updateUI(data.config.active);
|
||||||
// 测试获取信息
|
data.logs.forEach((log) => this.renderLog(log));
|
||||||
btnGet.addEventListener("confirm", () => {
|
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:get-selected-info", (err, ids) => {
|
|
||||||
if (ids && ids.length > 0) {
|
|
||||||
nodeIdInput.value = ids[0];
|
|
||||||
logDiv.innerText = "Status: Selected Node " + ids[0];
|
|
||||||
} else {
|
|
||||||
logDiv.innerText = "Status: No node selected";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnToggle.addEventListener("confirm", () => {
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(portInput.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 测试修改信息
|
btnClear.addEventListener("confirm", () => {
|
||||||
btnSet.addEventListener("confirm", () => {
|
logView.innerHTML = "";
|
||||||
let data = {
|
Editor.Ipc.sendToMain("mcp-bridge:clear-logs");
|
||||||
id: nodeIdInput.value,
|
});
|
||||||
path: "name",
|
|
||||||
value: newNameInput.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!data.id) {
|
btnCopy.addEventListener("confirm", () => {
|
||||||
logDiv.innerText = "Error: Please get Node ID first";
|
require("electron").clipboard.writeText(logView.innerText);
|
||||||
return;
|
Editor.success("All logs copied!");
|
||||||
}
|
});
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
|
||||||
|
if (data) {
|
||||||
|
portInput.value = data.config.port;
|
||||||
|
this.updateUI(data.config.active);
|
||||||
|
|
||||||
Editor.Ipc.sendToMain("mcp-bridge:set-node-property", data, (err, res) => {
|
// 设置自动启动复选框状态
|
||||||
if (err) {
|
autoStartCheck.value = data.autoStart;
|
||||||
logDiv.innerText = "Error: " + err;
|
|
||||||
} else {
|
data.logs.forEach((log) => this.renderLog(log));
|
||||||
logDiv.innerText = "Success: " + res;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
autoStartCheck.addEventListener("change", (event) => {
|
||||||
|
// event.target.value 在 ui-checkbox 中是布尔值
|
||||||
|
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", event.target.value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderLog(log) {
|
||||||
|
const logView = this.shadowRoot.querySelector("#logConsole");
|
||||||
|
if (!logView) return;
|
||||||
|
|
||||||
|
// 记录当前滚动条位置
|
||||||
|
const isAtBottom = logView.scrollHeight - logView.scrollTop <= logView.clientHeight + 50;
|
||||||
|
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = `log-item ${log.type}`;
|
||||||
|
el.innerHTML = `<span class="time">${log.time}</span><span class="msg">${log.content}</span>`;
|
||||||
|
logView.appendChild(el);
|
||||||
|
|
||||||
|
// 如果用户正在向上翻看,不自动滚动;否则自动滚到底部
|
||||||
|
if (isAtBottom) {
|
||||||
|
logView.scrollTop = logView.scrollHeight;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUI(isActive) {
|
||||||
|
const btnToggle = this.shadowRoot.querySelector("#btnToggle");
|
||||||
|
if (!btnToggle) return;
|
||||||
|
btnToggle.innerText = isActive ? "Stop" : "Start";
|
||||||
|
btnToggle.style.backgroundColor = isActive ? "#aa4444" : "#44aa44";
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user