From b1f2aacfad710dc06fd1de0daf8759f6d90d51c3 Mon Sep 17 00:00:00 2001 From: songmd <619472112@qq.com> Date: Thu, 7 Aug 2025 20:34:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E5=85=83=E7=B4=A0?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=AD=98=E5=82=A8=EF=BC=8C=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E9=80=9A=E8=BF=87HTTP=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E5=85=83=E7=B4=A0=E7=9A=84=E5=88=9B=E5=BB=BA=E3=80=81?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=92=8C=E5=88=A0=E9=99=A4=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E3=80=82=E6=9B=B4=E6=96=B0=E4=BA=86=E5=89=8D=E7=AB=AF=E4=B8=8E?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E7=9A=84=E5=90=8C=E6=AD=A5=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E4=BA=86=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=92=8C=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E6=93=8D=E4=BD=9C=E7=9A=84=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 58 +------------ src/index.js | 190 +++++++++++++++++++++++++------------------ src/server.js | 52 ++++++------ 3 files changed, 136 insertions(+), 164 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ee42fbe..262ecdd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -99,7 +99,7 @@ function App() { if (result.success && result.elements && result.elements.length > 0) { const cleanedElements = result.elements.map(cleanElementForExcalidraw) - const convertedElements = convertToExcalidrawElements(cleanedElements) + const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false }) excalidrawAPI.updateScene({ elements: convertedElements }) } } catch (error) { @@ -149,37 +149,6 @@ function App() { } } - // 调试函数:记录删除操作详情 - const debugDeleteOperation = (operation, data, additionalInfo = {}) => { - const timestamp = new Date().toISOString() - const debugInfo = { - operation, - timestamp, - data, - elementCount: excalidrawAPI?.getSceneElements()?.length || 0, - ...additionalInfo - } - - console.group(`🔍 DELETE DEBUG: ${operation}`) - console.log('详细信息:', debugInfo) - - if (operation === 'websocket_received' && data.type === 'element_deleted') { - const currentElements = excalidrawAPI?.getSceneElements() || [] - const targetElement = currentElements.find(el => el.id === data.elementId) - - console.log('目标元素存在:', !!targetElement) - console.log('当前元素ID列表:', currentElements.map(el => el.id)) - console.log('要删除的ID:', data.elementId) - console.log('ID类型匹配检查:', currentElements.map(el => ({ - id: el.id, - type: typeof el.id, - matches: el.id === data.elementId, - strictEquals: el.id === data.elementId - }))) - } - - console.groupEnd() - } const handleWebSocketMessage = (data) => { if (!excalidrawAPI) { @@ -200,8 +169,6 @@ function App() { elements: convertedElements, captureUpdate: CaptureUpdateAction.NEVER }) - console.log('Loaded initial elements with validated bindings:', convertedElements) - debugElementBindings(convertedElements) } break @@ -228,20 +195,11 @@ function App() { break case 'element_deleted': - debugDeleteOperation('websocket_received', data) const filteredElements = currentElements.filter(el => el.id !== data.elementId) - debugDeleteOperation('after_filter', data, { - originalCount: currentElements.length, - filteredCount: filteredElements.length, - actuallyRemoved: currentElements.length - filteredElements.length - }) excalidrawAPI.updateScene({ elements: filteredElements, captureUpdate: CaptureUpdateAction.NEVER }) - debugDeleteOperation('after_update_scene', data, { - finalElementCount: excalidrawAPI.getSceneElements().length - }) break case 'elements_batch_created': @@ -339,20 +297,6 @@ function App() { } } - // Debug function to check element bindings - const debugElementBindings = (elements) => { - console.group('🔍 Element Binding Debug'); - elements.forEach(element => { - if (element.boundElements || element.containerId) { - console.log(`Element ${element.id} (${element.type}):`, { - boundElements: element.boundElements, - containerId: element.containerId, - text: element.text || 'N/A' - }); - } - }); - console.groupEnd(); - } const clearCanvas = async () => { if (excalidrawAPI) { diff --git a/src/index.js b/src/index.js index aea23ee..15d2d15 100755 --- a/src/index.js +++ b/src/index.js @@ -16,8 +16,6 @@ import { z } from 'zod'; import dotenv from 'dotenv'; import logger from './utils/logger.js'; import { - elements, - validateElement, generateId, EXCALIDRAW_ELEMENT_TYPES } from './types.js'; @@ -439,12 +437,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { // Convert text to label format for Excalidraw const excalidrawElement = convertTextToLabel(element); - // Store the converted element locally (MCP server storage) - elements.set(id, excalidrawElement); - - // Sync to canvas (Express server + WebSocket broadcast) + // Create element directly on HTTP server (no local storage) const canvasElement = await createElementOnCanvas(excalidrawElement); + if (!canvasElement) { + throw new Error('Failed to create element: HTTP server unavailable'); + } + logger.info('Element created via MCP and synced to canvas', { id: excalidrawElement.id, type: excalidrawElement.type, @@ -454,7 +453,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: 'text', - text: `Element created successfully!\n\n${JSON.stringify(excalidrawElement, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still created locally)'}` + text: `Element created successfully!\n\n${JSON.stringify(canvasElement, null, 2)}\n\n✅ Synced to canvas` }] }; } @@ -465,28 +464,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!id) throw new Error('Element ID is required'); - const existingElement = elements.get(id); - if (!existingElement) throw new Error(`Element with ID ${id} not found`); - - // Validate the updated element - ElementSchema.parse({ ...existingElement, ...updates }); - - const updatedElement = { - ...existingElement, + // Build update payload with timestamp and version increment + const updatePayload = { + id, ...updates, - updatedAt: new Date().toISOString(), - version: existingElement.version + 1 + updatedAt: new Date().toISOString() }; // Convert text to label format for Excalidraw - const excalidrawElement = convertTextToLabel(updatedElement); + const excalidrawElement = convertTextToLabel(updatePayload); - // Store the converted element locally (MCP server storage) - elements.set(id, excalidrawElement); - - // Sync to canvas (Express server + WebSocket broadcast) + // Update element directly on HTTP server (no local storage) const canvasElement = await updateElementOnCanvas(excalidrawElement); + if (!canvasElement) { + throw new Error('Failed to update element: HTTP server unavailable or element not found'); + } + logger.info('Element updated via MCP and synced to canvas', { id: excalidrawElement.id, synced: !!canvasElement @@ -495,7 +489,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: 'text', - text: `Element updated successfully!\n\n${JSON.stringify(excalidrawElement, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still updated locally)'}` + text: `Element updated successfully!\n\n${JSON.stringify(canvasElement, null, 2)}\n\n✅ Synced to canvas` }] }; } @@ -504,21 +498,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const params = ElementIdSchema.parse(args); const { id } = params; - if (!elements.has(id)) throw new Error(`Element with ID ${id} not found`); - - // Delete locally (MCP server storage) - elements.delete(id); - - // Sync to canvas (Express server + WebSocket broadcast) + // Delete element directly on HTTP server (no local storage) const canvasResult = await deleteElementOnCanvas(id); - const result = { id, deleted: true, syncedToCanvas: !!canvasResult }; + if (!canvasResult) { + throw new Error('Failed to delete element: HTTP server unavailable or element not found'); + } + + const result = { id, deleted: true, syncedToCanvas: true }; logger.info('Element deleted via MCP and synced to canvas', result); return { content: [{ type: 'text', - text: `Element deleted successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasResult ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still deleted locally)'}` + text: `Element deleted successfully!\n\n${JSON.stringify(result, null, 2)}\n\n✅ Synced to canvas` }] }; } @@ -527,23 +520,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const params = QuerySchema.parse(args || {}); const { type, filter } = params; - let results = Array.from(elements.values()); - - if (type) { - results = results.filter(element => element.type === type); - } - - if (filter) { - results = results.filter(element => { - return Object.entries(filter).every(([key, value]) => { - return element[key] === value; + try { + // Build query parameters + const queryParams = new URLSearchParams(); + if (type) queryParams.set('type', type); + if (filter) { + Object.entries(filter).forEach(([key, value]) => { + queryParams.set(key, value); }); - }); + } + + // Query elements from HTTP server + const url = `${EXPRESS_SERVER_URL}/api/elements/search?${queryParams}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP server error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const results = data.elements || []; + + return { + content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] + }; + } catch (error) { + throw new Error(`Failed to query elements: ${error.message}`); } - - return { - content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] - }; } case 'get_resource': { @@ -561,20 +564,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; break; case 'library': - result = { - elements: Array.from(elements.values()) - }; + case 'elements': + try { + // Get elements from HTTP server + const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements`); + if (!response.ok) { + throw new Error(`HTTP server error: ${response.status} ${response.statusText}`); + } + const data = await response.json(); + result = { + elements: data.elements || [] + }; + } catch (error) { + throw new Error(`Failed to get elements: ${error.message}`); + } break; case 'theme': result = { theme: sceneState.theme }; break; - case 'elements': - result = { - elements: Array.from(elements.values()) - }; - break; default: throw new Error(`Unknown resource: ${resource}`); } @@ -644,34 +653,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const params = ElementIdsSchema.parse(args); const { elementIds } = params; - elementIds.forEach(id => { - const element = elements.get(id); - if (element) { - element.locked = true; + try { + // Lock elements through HTTP API updates + const updatePromises = elementIds.map(async (id) => { + return await updateElementOnCanvas({ id, locked: true }); + }); + + const results = await Promise.all(updatePromises); + const successCount = results.filter(result => result).length; + + if (successCount === 0) { + throw new Error('Failed to lock any elements: HTTP server unavailable'); } - }); - - const result = { locked: true, elementIds }; - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] - }; + + const result = { locked: true, elementIds, successCount }; + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error) { + throw new Error(`Failed to lock elements: ${error.message}`); + } } case 'unlock_elements': { const params = ElementIdsSchema.parse(args); const { elementIds } = params; - elementIds.forEach(id => { - const element = elements.get(id); - if (element) { - element.locked = false; + try { + // Unlock elements through HTTP API updates + const updatePromises = elementIds.map(async (id) => { + return await updateElementOnCanvas({ id, locked: false }); + }); + + const results = await Promise.all(updatePromises); + const successCount = results.filter(result => result).length; + + if (successCount === 0) { + throw new Error('Failed to unlock any elements: HTTP server unavailable'); } - }); - - const result = { unlocked: true, elementIds }; - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] - }; + + const result = { unlocked: true, elementIds, successCount }; + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } catch (error) { + throw new Error(`Failed to unlock elements: ${error.message}`); + } } case 'batch_create_elements': { @@ -693,20 +720,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { // Convert text to label format for Excalidraw const excalidrawElement = convertTextToLabel(element); - - // Store the converted element locally (MCP server storage) - elements.set(id, excalidrawElement); createdElements.push(excalidrawElement); } - // Sync all elements to canvas at once (Express server + WebSocket broadcast) + // Create all elements directly on HTTP server (no local storage) const canvasElements = await batchCreateElementsOnCanvas(createdElements); + if (!canvasElements) { + throw new Error('Failed to batch create elements: HTTP server unavailable'); + } + const result = { success: true, - elements: createdElements, - count: createdElements.length, - syncedToCanvas: !!canvasElements + elements: canvasElements, + count: canvasElements.length, + syncedToCanvas: true }; logger.info('Batch elements created via MCP and synced to canvas', { diff --git a/src/server.js b/src/server.js index 11f6c8b..46b0a6d 100644 --- a/src/server.js +++ b/src/server.js @@ -248,32 +248,6 @@ app.delete('/api/elements/:id', (req, res) => { } }); -// Get element by ID -app.get('/api/elements/:id', (req, res) => { - try { - const { id } = req.params; - const element = elements.get(id); - - if (!element) { - return res.status(404).json({ - success: false, - error: `Element with ID ${id} not found` - }); - } - - res.json({ - success: true, - element: element - }); - } catch (error) { - logger.error('Error fetching element:', error); - res.status(500).json({ - success: false, - error: error.message - }); - } -}); - // Query elements with filters app.get('/api/elements/search', (req, res) => { try { @@ -308,6 +282,32 @@ app.get('/api/elements/search', (req, res) => { } }); +// Get element by ID +app.get('/api/elements/:id', (req, res) => { + try { + const { id } = req.params; + const element = elements.get(id); + + if (!element) { + return res.status(404).json({ + success: false, + error: `Element with ID ${id} not found` + }); + } + + res.json({ + success: true, + element: element + }); + } catch (error) { + logger.error('Error fetching element:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + // Batch create elements app.post('/api/elements/batch', (req, res) => { try {