fix: 优化元素处理逻辑,移除本地存储,直接通过HTTP服务器进行元素的创建、更新和删除操作。更新了前端与后端的同步机制,增强了错误处理和日志记录,确保操作的成功反馈。

This commit is contained in:
songmd
2025-08-07 20:34:51 +08:00
parent 60c8af9ca8
commit b1f2aacfad
3 changed files with 136 additions and 164 deletions
+1 -57
View File
@@ -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) {
+109 -81
View File
@@ -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', {
+26 -26
View File
@@ -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 {