diff --git a/README.md b/README.md index af26bb6..030256f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ - **端口**: 可以自定义 HTTP 服务监听的端口,默认为 3456 - **自动启动**: 可以设置编辑器启动时自动开启服务 +- **多实例支持**: 如果默认端口 (3456) 被占用,插件会自动尝试端口+1 (如 3457),直到找到可用端口。 +- **配置隔离**: 插件配置(是否自动启动、上次使用的端口)现已存储在项目目录 (`settings/mcp-bridge.json`) 中,不同项目的配置互不干扰。 ## 连接 AI 编辑器 diff --git a/dist/IpcUi.js b/dist/IpcUi.js index e635f2f..322c0c3 100644 --- a/dist/IpcUi.js +++ b/dist/IpcUi.js @@ -1,233 +1,353 @@ "use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; +var __awaiter = + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; +var __generator = + (this && this.__generator) || + function (thisArg, body) { + var _ = { + label: 0, + sent: function () { + if (t[0] & 1) throw t[1]; + return t[1]; + }, + trys: [], + ops: [], + }, + f, + y, + t, + g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return ( + (g.next = verb(0)), + (g["throw"] = verb(1)), + (g["return"] = verb(2)), + typeof Symbol === "function" && + (g[Symbol.iterator] = function () { + return this; + }), + g + ); + function verb(n) { + return function (v) { + return step([n, v]); + }; + } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while ((g && ((g = 0), op[0] && (_ = 0)), _)) + try { + if ( + ((f = 1), + y && + (t = + op[0] & 2 + ? y["return"] + : op[0] + ? y["throw"] || ((t = y["return"]) && t.call(y), 0) + : y.next) && + !(t = t.call(y, op[1])).done) + ) + return t; + if (((y = 0), t)) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if ( + !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && + (op[0] === 6 || op[0] === 2) + ) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; + } + op = body.call(thisArg, _); + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IpcUi = void 0; // @ts-ignore var Editor = window.Editor; var IpcUi = /** @class */ (function () { - function IpcUi(root) { - this.logArea = null; - this.ipcList = null; - this.allMessages = []; - this.filterSelect = null; - this.paramInput = null; - this.root = root; - this.bindEvents(); - } - IpcUi.prototype.bindEvents = function () { - var _this = this; - var btnScan = this.root.querySelector("#btnScanIpc"); - var btnTest = this.root.querySelector("#btnTestIpc"); - var cbSelectAll = this.root.querySelector("#cbSelectAllIpc"); - this.logArea = this.root.querySelector("#ipcLog"); - this.ipcList = this.root.querySelector("#ipcList"); - this.filterSelect = this.root.querySelector("#ipcFilter"); - this.paramInput = this.root.querySelector("#ipcParams"); - if (btnScan) { - btnScan.addEventListener("confirm", function () { return _this.scanMessages(); }); - } - if (btnTest) { - btnTest.addEventListener("confirm", function () { return _this.testSelected(); }); - } - if (cbSelectAll) { - cbSelectAll.addEventListener("change", function (e) { return _this.toggleAll(e.detail ? e.detail.value : (e.target.value === 'true' || e.target.checked)); }); - } - if (this.filterSelect) { - this.filterSelect.addEventListener("change", function () { return _this.filterMessages(); }); - } - }; - IpcUi.prototype.scanMessages = function () { - var _this = this; - this.log("Scanning IPC messages..."); - // @ts-ignore - Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", function (err, msgs) { - if (err) { - _this.log("Scan Error: ".concat(err)); - return; - } - if (msgs) { - _this.allMessages = msgs; - _this.filterMessages(); - _this.log("Found ".concat(msgs.length, " messages.")); - } - else { - _this.log("No messages found."); - } - }); - }; - IpcUi.prototype.filterMessages = function () { - if (!this.allMessages) - return; - var filter = this.filterSelect ? this.filterSelect.value : "all"; - var filtered = this.allMessages; - if (filter === "available") { - filtered = this.allMessages.filter(function (m) { return m.status === "可用"; }); - } - else if (filter === "unavailable") { - filtered = this.allMessages.filter(function (m) { return m.status && m.status.includes("不可用"); }); - } - else if (filter === "untested") { - filtered = this.allMessages.filter(function (m) { return !m.status || m.status === "未测试"; }); - } - this.renderList(filtered); - }; - IpcUi.prototype.renderList = function (msgs) { - var _this = this; - if (!this.ipcList) - return; - this.ipcList.innerHTML = ""; - msgs.forEach(function (msg) { - var item = document.createElement("div"); - item.className = "ipc-item"; - item.style.padding = "4px"; - item.style.borderBottom = "1px solid #333"; - item.style.display = "flex"; - item.style.alignItems = "center"; - // Checkbox - var checkbox = document.createElement("ui-checkbox"); - // @ts-ignore - checkbox.value = false; - checkbox.setAttribute("data-name", msg.name); - checkbox.style.marginRight = "8px"; - // Content - var content = document.createElement("div"); - content.style.flex = "1"; - content.style.fontSize = "11px"; - var statusColor = "#888"; // Untested - if (msg.status === "可用") - statusColor = "#4CAF50"; // Green - else if (msg.status && msg.status.includes("不可用")) - statusColor = "#F44336"; // Red - content.innerHTML = "\n
\n ".concat(msg.name, "\n ").concat(msg.status || "未测试", "\n
\n
").concat(msg.description || "No desc", "
\n
Params: ").concat(msg.params || "None", "
\n "); - // Action Button - var btnRun = document.createElement("ui-button"); - btnRun.innerText = "Run"; - btnRun.className = "tiny"; - btnRun.style.height = "20px"; - btnRun.style.lineHeight = "20px"; - btnRun.addEventListener("confirm", function () { - _this.runTest(msg.name); - }); - item.appendChild(checkbox); - item.appendChild(content); - item.appendChild(btnRun); - _this.ipcList.appendChild(item); - }); - }; - IpcUi.prototype.testSelected = function () { - return __awaiter(this, void 0, void 0, function () { - var checkboxes, toTest, _i, toTest_1, name_1; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); - toTest = []; - checkboxes.forEach(function (cb) { - // In Cocos 2.x, ui-checkbox value is boolean - if (cb.checked || cb.value === true) { - toTest.push(cb.getAttribute("data-name")); - } - }); - if (toTest.length === 0) { - this.log("No messages selected."); - return [2 /*return*/]; - } - this.log("Starting batch test for ".concat(toTest.length, " messages...")); - _i = 0, toTest_1 = toTest; - _a.label = 1; - case 1: - if (!(_i < toTest_1.length)) return [3 /*break*/, 4]; - name_1 = toTest_1[_i]; - return [4 /*yield*/, this.runTest(name_1)]; - case 2: - _a.sent(); - _a.label = 3; - case 3: - _i++; - return [3 /*break*/, 1]; - case 4: - this.log("Batch test completed."); - return [2 /*return*/]; - } - }); - }); - }; - IpcUi.prototype.runTest = function (name) { - var _this = this; - return new Promise(function (resolve) { - var params = null; - if (_this.paramInput && _this.paramInput.value.trim()) { - try { - params = JSON.parse(_this.paramInput.value.trim()); - } - catch (e) { - _this.log("[Error] Invalid JSON Params: ".concat(e)); - resolve(); - return; - } - } - _this.log("Testing: ".concat(name, " with params: ").concat(JSON.stringify(params), "...")); - // @ts-ignore - Editor.Ipc.sendToMain("mcp-bridge:test-ipc-message", { name: name, params: params }, function (err, result) { - if (err) { - _this.log("[".concat(name, "] Failed: ").concat(err)); - } - else { - _this.log("[".concat(name, "] Success: ").concat(JSON.stringify(result))); - } - resolve(); - }); - }); - }; - IpcUi.prototype.toggleAll = function (checked) { - var checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); - checkboxes.forEach(function (cb) { - cb.value = checked; - }); - }; - IpcUi.prototype.log = function (msg) { - if (this.logArea) { - // @ts-ignore - var time = new Date().toLocaleTimeString(); - this.logArea.value += "[".concat(time, "] ").concat(msg, "\n"); - this.logArea.scrollTop = this.logArea.scrollHeight; - } - }; - return IpcUi; -}()); + function IpcUi(root) { + this.logArea = null; + this.ipcList = null; + this.allMessages = []; + this.filterSelect = null; + this.paramInput = null; + this.root = root; + this.bindEvents(); + } + IpcUi.prototype.bindEvents = function () { + var _this = this; + var btnScan = this.root.querySelector("#btnScanIpc"); + var btnTest = this.root.querySelector("#btnTestIpc"); + var cbSelectAll = this.root.querySelector("#cbSelectAllIpc"); + this.logArea = this.root.querySelector("#ipcLog"); + this.ipcList = this.root.querySelector("#ipcList"); + this.filterSelect = this.root.querySelector("#ipcFilter"); + this.paramInput = this.root.querySelector("#ipcParams"); + if (btnScan) { + btnScan.addEventListener("confirm", function () { + return _this.scanMessages(); + }); + } + if (btnTest) { + btnTest.addEventListener("confirm", function () { + return _this.testSelected(); + }); + } + if (cbSelectAll) { + cbSelectAll.addEventListener("change", function (e) { + return _this.toggleAll(e.detail ? e.detail.value : e.target.value === "true" || e.target.checked); + }); + } + if (this.filterSelect) { + this.filterSelect.addEventListener("change", function () { + return _this.filterMessages(); + }); + } + }; + IpcUi.prototype.scanMessages = function () { + var _this = this; + this.log("正在扫描 IPC 消息..."); + // @ts-ignore + Editor.Ipc.sendToMain("mcp-bridge:scan-ipc-messages", function (err, msgs) { + if (err) { + _this.log("扫描错误: ".concat(err)); + return; + } + if (msgs) { + _this.allMessages = msgs; + _this.filterMessages(); + _this.log("找到 ".concat(msgs.length, " 条消息。")); + } else { + _this.log("未找到任何消息。"); + } + }); + }; + IpcUi.prototype.filterMessages = function () { + if (!this.allMessages) return; + var filter = this.filterSelect ? this.filterSelect.value : "all"; + var filtered = this.allMessages; + if (filter === "available") { + filtered = this.allMessages.filter(function (m) { + return m.status === "可用"; + }); + } else if (filter === "unavailable") { + filtered = this.allMessages.filter(function (m) { + return m.status && m.status.includes("不可用"); + }); + } else if (filter === "untested") { + filtered = this.allMessages.filter(function (m) { + return !m.status || m.status === "未测试"; + }); + } + this.renderList(filtered); + }; + IpcUi.prototype.renderList = function (msgs) { + var _this = this; + if (!this.ipcList) return; + this.ipcList.innerHTML = ""; + msgs.forEach(function (msg) { + var item = document.createElement("div"); + item.className = "ipc-item"; + item.style.padding = "4px"; + item.style.borderBottom = "1px solid #333"; + item.style.display = "flex"; + item.style.alignItems = "center"; + // Checkbox + var checkbox = document.createElement("ui-checkbox"); + // @ts-ignore + checkbox.value = false; + checkbox.setAttribute("data-name", msg.name); + checkbox.style.marginRight = "8px"; + // Content + var content = document.createElement("div"); + content.style.flex = "1"; + content.style.fontSize = "11px"; + var statusColor = "#888"; // Untested + if (msg.status === "可用") + statusColor = "#4CAF50"; // Green + else if (msg.status && msg.status.includes("不可用")) statusColor = "#F44336"; // Red + content.innerHTML = + '\n
\n ' + .concat(msg.name, '\n ') + .concat( + msg.status || "未测试", + '\n
\n
', + ) + .concat( + msg.description || "无描述", + '
\n
参数: ', + ) + .concat(msg.params || "无", "
\n "); + // Action Button + var btnRun = document.createElement("ui-button"); + btnRun.innerText = "执行"; + btnRun.className = "tiny"; + btnRun.style.height = "20px"; + btnRun.style.lineHeight = "20px"; + btnRun.addEventListener("confirm", function () { + _this.runTest(msg.name); + }); + item.appendChild(checkbox); + item.appendChild(content); + item.appendChild(btnRun); + _this.ipcList.appendChild(item); + }); + }; + IpcUi.prototype.testSelected = function () { + return __awaiter(this, void 0, void 0, function () { + var checkboxes, toTest, _i, toTest_1, name_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); + toTest = []; + checkboxes.forEach(function (cb) { + // In Cocos 2.x, ui-checkbox value is boolean + if (cb.checked || cb.value === true) { + toTest.push(cb.getAttribute("data-name")); + } + }); + if (toTest.length === 0) { + this.log("未选择任何消息。"); + return [2 /*return*/]; + } + this.log("开始批量测试 ".concat(toTest.length, " 条消息...")); + ((_i = 0), (toTest_1 = toTest)); + _a.label = 1; + case 1: + if (!(_i < toTest_1.length)) return [3 /*break*/, 4]; + name_1 = toTest_1[_i]; + return [4 /*yield*/, this.runTest(name_1)]; + case 2: + _a.sent(); + _a.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: + this.log("批量测试完成。"); + return [2 /*return*/]; + } + }); + }); + }; + IpcUi.prototype.runTest = function (name) { + var _this = this; + return new Promise(function (resolve) { + var params = null; + if (_this.paramInput && _this.paramInput.value.trim()) { + try { + params = JSON.parse(_this.paramInput.value.trim()); + } catch (e) { + _this.log("[错误] 无效的 JSON 参数: ".concat(e)); + resolve(); + return; + } + } + _this.log("正在测试: ".concat(name, ",参数: ").concat(JSON.stringify(params), "...")); + // @ts-ignore + Editor.Ipc.sendToMain( + "mcp-bridge:test-ipc-message", + { name: name, params: params }, + function (err, result) { + if (err) { + _this.log("[".concat(name, "] 失败: ").concat(err)); + } else { + _this.log("[".concat(name, "] 成功: ").concat(JSON.stringify(result))); + } + resolve(); + }, + ); + }); + }; + IpcUi.prototype.toggleAll = function (checked) { + var checkboxes = this.root.querySelectorAll("#ipcList ui-checkbox"); + checkboxes.forEach(function (cb) { + cb.value = checked; + }); + }; + IpcUi.prototype.log = function (msg) { + if (this.logArea) { + // @ts-ignore + var time = new Date().toLocaleTimeString(); + this.logArea.value += "[".concat(time, "] ").concat(msg, "\n"); + this.logArea.scrollTop = this.logArea.scrollHeight; + } + }; + return IpcUi; +})(); exports.IpcUi = IpcUi; diff --git a/main.js b/main.js index df25c80..0694cef 100644 --- a/main.js +++ b/main.js @@ -704,8 +704,8 @@ module.exports = { * @returns {Object} Editor.Profile 实例 */ getProfile() { - // 'local' 表示存储在项目本地(local/mcp-bridge.json) - return Editor.Profile.load("profile://local/mcp-bridge.json", "mcp-bridge"); + // 'project' 表示存储在项目本地(settings/mcp-bridge.json),实现配置隔离 + return Editor.Profile.load("profile://project/mcp-bridge.json", "mcp-bridge"); }, /** @@ -721,139 +721,161 @@ module.exports = { startServer(port) { if (mcpServer) this.stopServer(); - try { - mcpServer = http.createServer((req, res) => { - res.setHeader("Content-Type", "application/json"); - res.setHeader("Access-Control-Allow-Origin", "*"); + const tryStart = (currentPort, retries) => { + if (retries <= 0) { + addLog("error", `Failed to find an available port after multiple attempts.`); + return; + } - let body = ""; - req.on("data", (chunk) => { - body += chunk; + try { + mcpServer = http.createServer((req, res) => { + this._handleRequest(req, res); }); - req.on("end", () => { - const url = req.url; - if (url === "/list-tools") { - const tools = getToolsList(); - addLog("info", `AI Client requested tool list`); - // 明确返回成功结构 - 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") { + + mcpServer.on("error", (e) => { + if (e.code === "EADDRINUSE") { + addLog("warn", `Port ${currentPort} is in use, trying ${currentPort + 1}...`); 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) { + mcpServer.close(); + } catch (err) { + // align + } + mcpServer = null; + // Delay slightly to ensure cleanup + setTimeout(() => { + tryStart(currentPort + 1, retries - 1); + }, 100); + } else { + addLog("error", `Server Error: ${e.message}`); + } + }); + + mcpServer.listen(currentPort, () => { + serverConfig.active = true; + serverConfig.port = currentPort; + addLog("success", `MCP Server running at http://127.0.0.1:${currentPort}`); + Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig); + + // Important: Do NOT save the auto-assigned port to profile to avoid pollution + }); + } catch (e) { + addLog("error", `Failed to start server: ${e.message}`); + } + }; + + // Start trying from the configured port, retry 10 times + tryStart(port, 10); + }, + + _handleRequest(req, res) { + res.setHeader("Content-Type", "application/json"); + res.setHeader("Access-Control-Allow-Origin", "*"); + + let body = ""; + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + const url = req.url; + if (url === "/list-tools") { + const tools = getToolsList(); + addLog("info", `AI Client requested tool list`); + 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); - res.end(JSON.stringify({ error: e.message })); + return res.end(JSON.stringify({ error: err })); } - return; - } - if (url === "/call-tool") { - try { - const { name, arguments: args } = JSON.parse(body || "{}"); - addLog("mcp", `REQ -> [${name}] (队列长度: ${commandQueue.length})`); + addLog("success", `读取成功: ${uri}`); + res.writeHead(200); + 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 { + const { name, arguments: args } = JSON.parse(body || "{}"); + addLog("mcp", `REQ -> [${name}] (队列长度: ${commandQueue.length})`); - // 【关键修复】所有 MCP 指令通过队列串行化执行, - // 防止 AssetDB.refresh 等异步操作被并发请求打断导致编辑器卡死 - enqueueCommand((done) => { - this.handleMcpCall(name, args, (err, result) => { - const response = { - content: [ - { - type: "text", - text: err - ? `Error: ${err}` - : typeof result === "object" - ? JSON.stringify(result, null, 2) - : result, - }, - ], - }; - if (err) { - addLog("error", `RES <- [${name}] 失败: ${err}`); - } else { - // 成功时尝试捕获简单的结果预览(如果是字符串或简短对象) - let preview = ""; - if (typeof result === "string") { - preview = result.length > 100 ? result.substring(0, 100) + "..." : result; - } else if (typeof result === "object") { - try { - const jsonStr = JSON.stringify(result); - preview = - jsonStr.length > 100 ? jsonStr.substring(0, 100) + "..." : jsonStr; - } catch (e) { - preview = "Object (Circular/Unserializable)"; - } - } - addLog("success", `RES <- [${name}] 成功 : ${preview}`); - } - res.writeHead(200); - res.end(JSON.stringify(response)); - done(); // 当前指令完成,释放队列给下一个指令 - }); - }); - } catch (e) { - if (e instanceof SyntaxError) { - addLog("error", `JSON Parse Error: ${e.message}`); - res.writeHead(400); - res.end(JSON.stringify({ error: "Invalid JSON" })); + enqueueCommand((done) => { + this.handleMcpCall(name, args, (err, result) => { + const response = { + content: [ + { + type: "text", + text: err + ? `Error: ${err}` + : typeof result === "object" + ? JSON.stringify(result, null, 2) + : result, + }, + ], + }; + if (err) { + addLog("error", `RES <- [${name}] 失败: ${err}`); } else { - addLog("error", `Internal Server Error: ${e.message}`); - res.writeHead(500); - res.end(JSON.stringify({ error: e.message })); + let preview = ""; + if (typeof result === "string") { + preview = result.length > 100 ? result.substring(0, 100) + "..." : result; + } else if (typeof result === "object") { + try { + const jsonStr = JSON.stringify(result); + preview = jsonStr.length > 100 ? jsonStr.substring(0, 100) + "..." : jsonStr; + } catch (e) { + preview = "Object (Circular/Unserializable)"; + } + } + addLog("success", `RES <- [${name}] 成功 : ${preview}`); } - } - return; + res.writeHead(200); + res.end(JSON.stringify(response)); + done(); + }); + }); + } catch (e) { + if (e instanceof SyntaxError) { + addLog("error", `JSON Parse Error: ${e.message}`); + res.writeHead(400); + res.end(JSON.stringify({ error: "Invalid JSON" })); + } else { + addLog("error", `Internal Server Error: ${e.message}`); + res.writeHead(500); + res.end(JSON.stringify({ error: e.message })); } + } + return; + } - // --- 兜底处理 (404) --- - res.writeHead(404); - res.end(JSON.stringify({ error: "Not Found", url: url })); - }); - }); - - mcpServer.on("error", (e) => { - addLog("error", `Server Error: ${e.message}`); - }); - mcpServer.listen(port, () => { - serverConfig.active = true; - addLog("success", `MCP Server running at http://127.0.0.1:${port}`); - Editor.Ipc.sendToPanel("mcp-bridge", "mcp-bridge:state-changed", serverConfig); - }); - // 启动成功后顺便存一下端口 - this.getProfile().set("last-port", port); - this.getProfile().save(); - } catch (e) { - addLog("error", `Failed to start server: ${e.message}`); - } + res.writeHead(404); + res.end(JSON.stringify({ error: "Not Found", url: url })); + }); }, /** @@ -2273,7 +2295,12 @@ CCProgram fs %{ "toggle-server"(event, port) { if (serverConfig.active) this.stopServer(); - else this.startServer(port); + else { + // 用户手动启动时,保存偏好端口 + this.getProfile().set("last-port", port); + this.getProfile().save(); + this.startServer(port); + } }, "clear-logs"() { logBuffer = []; diff --git a/panel/index.html b/panel/index.html index 1e85d56..00c26d2 100644 --- a/panel/index.html +++ b/panel/index.html @@ -1,316 +1,340 @@
-
- Main - Tool Test - IPC Test -
+
+ 主页 + 工具测试 + IPC 测试 +
-
-
-
- Port: - - Start -
-
- Auto Start -
-
- Clear - Copy All -
-
-
+
+
+
+ 端口: + + 启动 +
+
+ 自动启动 +
+
+ 清空日志 + 复制全部 +
+
+
-
- -
-
-
- - -
- -
-
+
+ +
+
+
+ + +
+ +
+
- -
+ +
-
-
- -
选择工具查看说明
-
-
- - -
-
- 测试工具 - 刷新列表 - 清空结果 - 探查 API -
-
- - -
-
-
-
+
+
+ +
选择工具查看说明
+
+
+ + +
+
+ 测试工具 + 刷新列表 + 清空结果 + 探查 API +
+
+ + +
+
+
+
-
-
- Scan Messages - -
- Test Selected -
-
-
- Select All/None -
-
-
- - -
-
- - -
-
-
+
+
+ 扫描消息 + +
+ 测试选中项 +
+
+
+ 全选/反选 +
+
+
+ + +
+
+ + +
+
+
\ No newline at end of file + .description-box { + background: #222; + color: #ccc; + border: 1px solid #444; + padding: 8px; + font-size: 11px; + line-height: 1.4; + min-height: 60px; + max-height: 120px; + overflow-y: auto; + border-radius: 2px; + } + diff --git a/panel/index.js b/panel/index.js index 664802a..14d4b4a 100644 --- a/panel/index.js +++ b/panel/index.js @@ -39,6 +39,11 @@ Editor.Panel.extend({ */ "mcp-bridge:state-changed"(event, config) { this.updateUI(config.active); + // 如果服务器已启动,更新面板显示的端口为实际运行端口 + if (config.active && config.port) { + const portInput = this.shadowRoot.querySelector("#portInput"); + if (portInput) portInput.value = config.port; + } }, },