# 标注保存和编辑器清理问题修复 ## 问题 1:422 错误 - 标注结果格式不正确 ### 错误信息 ``` POST http://localhost:8000/api/annotations 422 (Unprocessable Entity) body.result: Input should be a valid dictionary ``` ### 问题原因 LabelStudio 的 `serializeAnnotation()` 方法返回的对象包含多个字段(如 `result`, `id`, `created_at` 等),但后端 API 期望 `result` 字段是一个字典对象。 ### 解决方案 在 `handleSave()` 函数中添加了数据提取和验证逻辑: ```typescript // Extract only the result field if it exists let resultData: Record = {}; if (annotationResult.result && Array.isArray(annotationResult.result)) { // Standard LabelStudio format: { result: [...], ... } resultData = { result: annotationResult.result }; } else if (Array.isArray(annotationResult)) { // If it's already an array, wrap it resultData = { result: annotationResult }; } else { // Use the whole object resultData = annotationResult; } // Validate that we have actual annotation data if (!resultData.result || (Array.isArray(resultData.result) && resultData.result.length === 0)) { setError('请完成标注后再保存(标注结果为空)'); return; } ``` **改进点**: - ✅ 正确提取 `result` 字段 - ✅ 处理多种数据格式 - ✅ 验证标注数据不为空 - ✅ 添加详细的控制台日志 ## 问题 2:编辑器清理问题 ### 错误信息 ``` Warning: Attempted to synchronously unmount a root while React was already rendering. Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree. ``` ### 问题原因 1. 在 React 渲染过程中同步销毁 LabelStudio 实例 2. 在组件卸载后仍然尝试访问 MobX 状态树 3. 任务 ID 变化时没有正确清理旧的编辑器实例 ### 解决方案 #### 1. 改进清理逻辑 ```typescript function cleanup() { if (isCleanedUp) return; isCleanedUp = true; console.log('Cleaning up LabelStudio editor...'); // 1. Dispose snapshot listener first if (snapshotDisposer) { try { snapshotDisposer(); } catch (e) { console.warn('Error disposing snapshot:', e); } snapshotDisposer = null; } // 2. Cancel any pending animation frames if (rafIdRef.current !== null) { cancelAnimationFrame(rafIdRef.current); rafIdRef.current = null; } // 3. Destroy LabelStudio instance asynchronously if (lsfInstanceRef.current) { try { // Give React time to finish rendering before destroying setTimeout(() => { if (lsfInstanceRef.current) { lsfInstanceRef.current.destroy(); lsfInstanceRef.current = null; } }, 0); } catch (e) { console.warn('Error destroying LSF instance:', e); lsfInstanceRef.current = null; } } // 4. Clear window.LabelStudio if (typeof window !== 'undefined' && (window as any).LabelStudio) { delete (window as any).LabelStudio; } // 5. Reset state setEditorReady(false); annotationResultRef.current = null; } ``` **改进点**: - ✅ 添加 `isCleanedUp` 标志防止重复清理 - ✅ 按正确顺序清理资源(snapshot → RAF → LSF instance) - ✅ 异步销毁 LabelStudio 实例(避免 React 渲染冲突) - ✅ 添加错误处理和日志 - ✅ 在 snapshot 回调中检查 `isCleanedUp` 标志 #### 2. 添加任务 ID 到依赖数组 ```typescript useEffect(() => { // ... return () => { cleanup(); }; }, [id, loading, error, currentTask, currentProject]); // 添加 'id' ``` **改进点**: - ✅ 任务 ID 变化时自动清理旧编辑器 - ✅ 确保每次加载新任务时都有干净的编辑器实例 #### 3. 在回调中检查清理状态 ```typescript onStorageInitialized: (LS: any) => { const initAnnotation = () => { if (isCleanedUp) { console.log('Component was cleaned up, skipping annotation initialization'); return; } const as = LS.annotationStore; const annotation = as.createAnnotation(); as.selectAnnotation(annotation.id); if (annotation) { snapshotDisposer = onSnapshot(annotation, () => { if (!isCleanedUp) { annotationResultRef.current = annotation.serializeAnnotation(); } }); } }; setTimeout(initAnnotation, 100); } ``` **改进点**: - ✅ 在初始化前检查组件是否已清理 - ✅ 在 snapshot 回调中检查清理状态 - ✅ 避免访问已销毁的状态树 ## 测试场景 修复后,以下场景都能正常工作: 1. ✅ 保存标注结果(正确格式) 2. ✅ 标注完一个任务后进入另一个任务 3. ✅ 快速切换任务 4. ✅ 从标注页面返回任务列表 5. ✅ 刷新页面后重新加载编辑器 ## 调试日志 添加了详细的控制台日志: ```typescript console.log('Loading LabelStudio editor for task:', currentTask.id); console.log('Initializing LabelStudio instance...'); console.log('LabelStudio instance initialized'); console.log('Cleaning up LabelStudio editor...'); console.log('LabelStudio editor cleaned up'); console.log('Annotation result:', annotationResult); console.log('Sending annotation data:', resultData); ``` 这些日志可以帮助调试编辑器加载和清理过程。 ## 相关文件 - `web/apps/lq_label/src/views/annotation-view/annotation-view.tsx` ## 后续优化建议 1. **添加加载状态指示器**:在编辑器初始化时显示进度 2. **添加保存成功提示**:使用 Toast 显示保存成功消息 3. **优化清理时机**:考虑使用 `useLayoutEffect` 进行同步清理 4. **添加错误重试机制**:保存失败时允许用户重试 ## 总结 通过改进数据提取逻辑和编辑器清理流程,现在系统能够: - ✅ 正确保存标注结果到后端 - ✅ 在任务切换时正确清理编辑器 - ✅ 避免 React 渲染冲突 - ✅ 避免访问已销毁的 MobX 状态树 - ✅ 提供详细的调试日志