fix: 优化元素处理逻辑,移除本地存储,直接通过HTTP服务器进行元素的创建、更新和删除操作。更新了前端与后端的同步机制,增强了错误处理和日志记录,确保操作的成功反馈。
This commit is contained in:
+1
-57
@@ -99,7 +99,7 @@ function App() {
|
|||||||
|
|
||||||
if (result.success && result.elements && result.elements.length > 0) {
|
if (result.success && result.elements && result.elements.length > 0) {
|
||||||
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
|
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
|
||||||
const convertedElements = convertToExcalidrawElements(cleanedElements)
|
const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false })
|
||||||
excalidrawAPI.updateScene({ elements: convertedElements })
|
excalidrawAPI.updateScene({ elements: convertedElements })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) => {
|
const handleWebSocketMessage = (data) => {
|
||||||
if (!excalidrawAPI) {
|
if (!excalidrawAPI) {
|
||||||
@@ -200,8 +169,6 @@ function App() {
|
|||||||
elements: convertedElements,
|
elements: convertedElements,
|
||||||
captureUpdate: CaptureUpdateAction.NEVER
|
captureUpdate: CaptureUpdateAction.NEVER
|
||||||
})
|
})
|
||||||
console.log('Loaded initial elements with validated bindings:', convertedElements)
|
|
||||||
debugElementBindings(convertedElements)
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -228,20 +195,11 @@ function App() {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case 'element_deleted':
|
case 'element_deleted':
|
||||||
debugDeleteOperation('websocket_received', data)
|
|
||||||
const filteredElements = currentElements.filter(el => el.id !== data.elementId)
|
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({
|
excalidrawAPI.updateScene({
|
||||||
elements: filteredElements,
|
elements: filteredElements,
|
||||||
captureUpdate: CaptureUpdateAction.NEVER
|
captureUpdate: CaptureUpdateAction.NEVER
|
||||||
})
|
})
|
||||||
debugDeleteOperation('after_update_scene', data, {
|
|
||||||
finalElementCount: excalidrawAPI.getSceneElements().length
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'elements_batch_created':
|
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 () => {
|
const clearCanvas = async () => {
|
||||||
if (excalidrawAPI) {
|
if (excalidrawAPI) {
|
||||||
|
|||||||
+109
-81
@@ -16,8 +16,6 @@ import { z } from 'zod';
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import logger from './utils/logger.js';
|
import logger from './utils/logger.js';
|
||||||
import {
|
import {
|
||||||
elements,
|
|
||||||
validateElement,
|
|
||||||
generateId,
|
generateId,
|
||||||
EXCALIDRAW_ELEMENT_TYPES
|
EXCALIDRAW_ELEMENT_TYPES
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
@@ -439,12 +437,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
// Convert text to label format for Excalidraw
|
// Convert text to label format for Excalidraw
|
||||||
const excalidrawElement = convertTextToLabel(element);
|
const excalidrawElement = convertTextToLabel(element);
|
||||||
|
|
||||||
// Store the converted element locally (MCP server storage)
|
// Create element directly on HTTP server (no local storage)
|
||||||
elements.set(id, excalidrawElement);
|
|
||||||
|
|
||||||
// Sync to canvas (Express server + WebSocket broadcast)
|
|
||||||
const canvasElement = await createElementOnCanvas(excalidrawElement);
|
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', {
|
logger.info('Element created via MCP and synced to canvas', {
|
||||||
id: excalidrawElement.id,
|
id: excalidrawElement.id,
|
||||||
type: excalidrawElement.type,
|
type: excalidrawElement.type,
|
||||||
@@ -454,7 +453,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
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');
|
if (!id) throw new Error('Element ID is required');
|
||||||
|
|
||||||
const existingElement = elements.get(id);
|
// Build update payload with timestamp and version increment
|
||||||
if (!existingElement) throw new Error(`Element with ID ${id} not found`);
|
const updatePayload = {
|
||||||
|
id,
|
||||||
// Validate the updated element
|
|
||||||
ElementSchema.parse({ ...existingElement, ...updates });
|
|
||||||
|
|
||||||
const updatedElement = {
|
|
||||||
...existingElement,
|
|
||||||
...updates,
|
...updates,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString()
|
||||||
version: existingElement.version + 1
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert text to label format for Excalidraw
|
// Convert text to label format for Excalidraw
|
||||||
const excalidrawElement = convertTextToLabel(updatedElement);
|
const excalidrawElement = convertTextToLabel(updatePayload);
|
||||||
|
|
||||||
// Store the converted element locally (MCP server storage)
|
// Update element directly on HTTP server (no local storage)
|
||||||
elements.set(id, excalidrawElement);
|
|
||||||
|
|
||||||
// Sync to canvas (Express server + WebSocket broadcast)
|
|
||||||
const canvasElement = await updateElementOnCanvas(excalidrawElement);
|
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', {
|
logger.info('Element updated via MCP and synced to canvas', {
|
||||||
id: excalidrawElement.id,
|
id: excalidrawElement.id,
|
||||||
synced: !!canvasElement
|
synced: !!canvasElement
|
||||||
@@ -495,7 +489,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
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 params = ElementIdSchema.parse(args);
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
if (!elements.has(id)) throw new Error(`Element with ID ${id} not found`);
|
// Delete element directly on HTTP server (no local storage)
|
||||||
|
|
||||||
// Delete locally (MCP server storage)
|
|
||||||
elements.delete(id);
|
|
||||||
|
|
||||||
// Sync to canvas (Express server + WebSocket broadcast)
|
|
||||||
const canvasResult = await deleteElementOnCanvas(id);
|
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);
|
logger.info('Element deleted via MCP and synced to canvas', result);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
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 params = QuerySchema.parse(args || {});
|
||||||
const { type, filter } = params;
|
const { type, filter } = params;
|
||||||
|
|
||||||
let results = Array.from(elements.values());
|
try {
|
||||||
|
// Build query parameters
|
||||||
if (type) {
|
const queryParams = new URLSearchParams();
|
||||||
results = results.filter(element => element.type === type);
|
if (type) queryParams.set('type', type);
|
||||||
}
|
if (filter) {
|
||||||
|
Object.entries(filter).forEach(([key, value]) => {
|
||||||
if (filter) {
|
queryParams.set(key, value);
|
||||||
results = results.filter(element => {
|
|
||||||
return Object.entries(filter).every(([key, value]) => {
|
|
||||||
return element[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': {
|
case 'get_resource': {
|
||||||
@@ -561,20 +564,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'library':
|
case 'library':
|
||||||
result = {
|
case 'elements':
|
||||||
elements: Array.from(elements.values())
|
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;
|
break;
|
||||||
case 'theme':
|
case 'theme':
|
||||||
result = {
|
result = {
|
||||||
theme: sceneState.theme
|
theme: sceneState.theme
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'elements':
|
|
||||||
result = {
|
|
||||||
elements: Array.from(elements.values())
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown resource: ${resource}`);
|
throw new Error(`Unknown resource: ${resource}`);
|
||||||
}
|
}
|
||||||
@@ -644,34 +653,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
const params = ElementIdsSchema.parse(args);
|
const params = ElementIdsSchema.parse(args);
|
||||||
const { elementIds } = params;
|
const { elementIds } = params;
|
||||||
|
|
||||||
elementIds.forEach(id => {
|
try {
|
||||||
const element = elements.get(id);
|
// Lock elements through HTTP API updates
|
||||||
if (element) {
|
const updatePromises = elementIds.map(async (id) => {
|
||||||
element.locked = true;
|
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, successCount };
|
||||||
const result = { locked: true, elementIds };
|
return {
|
||||||
return {
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
||||||
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
};
|
||||||
};
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to lock elements: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'unlock_elements': {
|
case 'unlock_elements': {
|
||||||
const params = ElementIdsSchema.parse(args);
|
const params = ElementIdsSchema.parse(args);
|
||||||
const { elementIds } = params;
|
const { elementIds } = params;
|
||||||
|
|
||||||
elementIds.forEach(id => {
|
try {
|
||||||
const element = elements.get(id);
|
// Unlock elements through HTTP API updates
|
||||||
if (element) {
|
const updatePromises = elementIds.map(async (id) => {
|
||||||
element.locked = false;
|
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, successCount };
|
||||||
const result = { unlocked: true, elementIds };
|
return {
|
||||||
return {
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
||||||
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
};
|
||||||
};
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to unlock elements: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'batch_create_elements': {
|
case 'batch_create_elements': {
|
||||||
@@ -693,20 +720,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
|
|
||||||
// Convert text to label format for Excalidraw
|
// Convert text to label format for Excalidraw
|
||||||
const excalidrawElement = convertTextToLabel(element);
|
const excalidrawElement = convertTextToLabel(element);
|
||||||
|
|
||||||
// Store the converted element locally (MCP server storage)
|
|
||||||
elements.set(id, excalidrawElement);
|
|
||||||
createdElements.push(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);
|
const canvasElements = await batchCreateElementsOnCanvas(createdElements);
|
||||||
|
|
||||||
|
if (!canvasElements) {
|
||||||
|
throw new Error('Failed to batch create elements: HTTP server unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
success: true,
|
success: true,
|
||||||
elements: createdElements,
|
elements: canvasElements,
|
||||||
count: createdElements.length,
|
count: canvasElements.length,
|
||||||
syncedToCanvas: !!canvasElements
|
syncedToCanvas: true
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info('Batch elements created via MCP and synced to canvas', {
|
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
|
// Query elements with filters
|
||||||
app.get('/api/elements/search', (req, res) => {
|
app.get('/api/elements/search', (req, res) => {
|
||||||
try {
|
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
|
// Batch create elements
|
||||||
app.post('/api/elements/batch', (req, res) => {
|
app.post('/api/elements/batch', (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user