Fix MCP delete/update methods and add frontend sync functionality

- Fix MCP server update_element method parameter parsing issue
- Add comprehensive delete operation debugging and validation
- Implement frontend sync button with real-time status feedback
- Add elements sync API endpoint (/api/elements/sync) for manual synchronization
- Enhance WebSocket message handling with proper element validation
- Add binding validation and cleanup for Excalidraw elements
- Improve error handling and logging throughout the sync process
- Add sync status tracking and user feedback in the UI
This commit is contained in:
songmd
2025-08-06 19:55:25 +08:00
parent 1831a621ac
commit 60c8af9ca8
4 changed files with 400 additions and 11 deletions
+44
View File
@@ -231,6 +231,50 @@
.loading-content div:first-child {
margin-bottom: 10px;
}
/* Sync Controls Styles */
.sync-controls {
display: flex;
align-items: center;
gap: 10px;
}
.btn-loading {
position: relative;
}
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #ffffff40;
border-top: 2px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 5px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.sync-status {
font-size: 12px;
min-width: 100px;
}
.sync-success {
color: #4caf50;
}
.sync-error {
color: #f44336;
}
.sync-time {
color: #666;
}
</style>
</head>
<body>
+247 -9
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'
import { Excalidraw, convertToExcalidrawElements } from '@excalidraw/excalidraw'
import { Excalidraw, convertToExcalidrawElements, CaptureUpdateAction } from '@excalidraw/excalidraw'
import '@excalidraw/excalidraw/index.css'
// Helper function to clean elements for Excalidraw
@@ -13,10 +13,62 @@ const cleanElementForExcalidraw = (element) => {
return cleanElement;
}
// Helper function to validate and fix element binding data
const validateAndFixBindings = (elements) => {
const elementMap = new Map(elements.map(el => [el.id, el]));
return elements.map(element => {
const fixedElement = { ...element };
// Validate and fix boundElements
if (fixedElement.boundElements) {
if (Array.isArray(fixedElement.boundElements)) {
fixedElement.boundElements = fixedElement.boundElements.filter(binding => {
// Ensure binding has required properties
if (!binding || typeof binding !== 'object') return false;
if (!binding.id || !binding.type) return false;
// Ensure the referenced element exists
const referencedElement = elementMap.get(binding.id);
if (!referencedElement) return false;
// Validate binding type
if (!['text', 'arrow'].includes(binding.type)) return false;
return true;
});
// Remove boundElements if empty
if (fixedElement.boundElements.length === 0) {
fixedElement.boundElements = null;
}
} else {
// Invalid boundElements format, set to null
fixedElement.boundElements = null;
}
}
// Validate and fix containerId
if (fixedElement.containerId) {
const containerElement = elementMap.get(fixedElement.containerId);
if (!containerElement) {
// Container doesn't exist, remove containerId
fixedElement.containerId = null;
}
}
return fixedElement;
});
}
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null)
const [isConnected, setIsConnected] = useState(false)
const websocketRef = useRef(null)
// Sync state management
const [syncStatus, setSyncStatus] = useState('idle') // idle, syncing, success, error
const [lastSyncTime, setLastSyncTime] = useState(null)
// WebSocket connection
useEffect(() => {
@@ -97,6 +149,38 @@ 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) {
return
@@ -104,13 +188,20 @@ function App() {
try {
const currentElements = excalidrawAPI.getSceneElements()
console.log('当前元素:', currentElements);
switch (data.type) {
case 'initial_elements':
if (data.elements && data.elements.length > 0) {
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
const convertedElements = convertToExcalidrawElements(cleanedElements)
excalidrawAPI.updateScene({ elements: convertedElements })
const validatedElements = validateAndFixBindings(cleanedElements)
const convertedElements = convertToExcalidrawElements(validatedElements)
excalidrawAPI.updateScene({
elements: convertedElements,
captureUpdate: CaptureUpdateAction.NEVER
})
console.log('Loaded initial elements with validated bindings:', convertedElements)
debugElementBindings(convertedElements)
}
break
@@ -118,7 +209,10 @@ function App() {
const cleanedNewElement = cleanElementForExcalidraw(data.element)
const newElement = convertToExcalidrawElements([cleanedNewElement])
const updatedElementsAfterCreate = [...currentElements, ...newElement]
excalidrawAPI.updateScene({ elements: updatedElementsAfterCreate })
excalidrawAPI.updateScene({
elements: updatedElementsAfterCreate,
captureUpdate: CaptureUpdateAction.NEVER
})
break
case 'element_updated':
@@ -127,19 +221,46 @@ function App() {
const updatedElements = currentElements.map(el =>
el.id === data.element.id ? convertedUpdatedElement : el
)
excalidrawAPI.updateScene({ elements: updatedElements })
excalidrawAPI.updateScene({
elements: updatedElements,
captureUpdate: CaptureUpdateAction.NEVER
})
break
case 'element_deleted':
debugDeleteOperation('websocket_received', data)
const filteredElements = currentElements.filter(el => el.id !== data.elementId)
excalidrawAPI.updateScene({ elements: filteredElements })
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':
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
const batchElements = convertToExcalidrawElements(cleanedBatchElements)
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
excalidrawAPI.updateScene({ elements: updatedElementsAfterBatch })
excalidrawAPI.updateScene({
elements: updatedElementsAfterBatch,
captureUpdate: CaptureUpdateAction.NEVER
})
break
case 'elements_synced':
console.log(`Sync confirmed by server: ${data.count} elements`)
// Sync confirmation already handled by HTTP response
break
case 'sync_status':
console.log(`Server sync status: ${data.elementCount} elements`)
break
default:
@@ -150,6 +271,89 @@ function App() {
}
}
// Data format conversion for backend
const convertToBackendFormat = (element) => {
return {
...element
}
}
// Format sync time display
const formatSyncTime = (time) => {
if (!time) return ''
return time.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// Main sync function
const syncToBackend = async () => {
if (!excalidrawAPI) {
console.warn('Excalidraw API not available')
return
}
setSyncStatus('syncing')
try {
// 1. Get current elements
const currentElements = excalidrawAPI.getSceneElements()
console.log(`Syncing ${currentElements.length} elements to backend`)
// 2. Filter out deleted elements
const activeElements = currentElements.filter(el => !el.isDeleted)
// 3. Convert to backend format
const backendElements = activeElements.map(convertToBackendFormat)
// 4. Send to backend
const response = await fetch('/api/elements/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
elements: backendElements,
timestamp: new Date().toISOString()
})
})
if (response.ok) {
const result = await response.json()
setSyncStatus('success')
setLastSyncTime(new Date())
console.log(`Sync successful: ${result.count} elements synced`)
// Reset status after 2 seconds
setTimeout(() => setSyncStatus('idle'), 2000)
} else {
const error = await response.json()
setSyncStatus('error')
console.error('Sync failed:', error.error)
}
} catch (error) {
setSyncStatus('error')
console.error('Sync error:', error)
}
}
// 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) {
try {
@@ -165,11 +369,17 @@ function App() {
}
// Clear the frontend canvas
excalidrawAPI.updateScene({ elements: [] })
excalidrawAPI.updateScene({
elements: [],
captureUpdate: CaptureUpdateAction.IMMEDIATELY
})
} catch (error) {
console.error('Error clearing canvas:', error)
// Still clear frontend even if backend fails
excalidrawAPI.updateScene({ elements: [] })
excalidrawAPI.updateScene({
elements: [],
captureUpdate: CaptureUpdateAction.IMMEDIATELY
})
}
}
}
@@ -184,6 +394,34 @@ function App() {
<div className={`status-dot ${isConnected ? 'status-connected' : 'status-disconnected'}`}></div>
<span>{isConnected ? 'Connected' : 'Disconnected'}</span>
</div>
{/* Sync Controls */}
<div className="sync-controls">
<button
className={`btn-primary ${syncStatus === 'syncing' ? 'btn-loading' : ''}`}
onClick={syncToBackend}
disabled={syncStatus === 'syncing' || !excalidrawAPI}
>
{syncStatus === 'syncing' && <span className="spinner"></span>}
{syncStatus === 'syncing' ? 'Syncing...' : 'Sync to Backend'}
</button>
{/* Sync Status */}
<div className="sync-status">
{syncStatus === 'success' && (
<span className="sync-success"> Synced</span>
)}
{syncStatus === 'error' && (
<span className="sync-error"> Sync Failed</span>
)}
{lastSyncTime && syncStatus === 'idle' && (
<span className="sync-time">
Last sync: {formatSyncTime(lastSyncTime)}
</span>
)}
</div>
</div>
<button className="btn-secondary" onClick={clearCanvas}>Clear Canvas</button>
</div>
</div>