fix: 优化元素处理逻辑,移除本地存储,直接通过HTTP服务器进行元素的创建、更新和删除操作。更新了前端与后端的同步机制,增强了错误处理和日志记录,确保操作的成功反馈。
This commit is contained in:
+1
-57
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user