# 标注加载最终修复总结 ## 问题回顾 1. ✅ **标注结果无法加载** - 已修复 2. ✅ **任务状态不更新** - 已修复 3. ✅ **MobX 警告** - 已修复 ## 最终解决方案 ### 1. 使用 LabelStudio 的标准方式加载标注 **关键改变**:在初始化 LabelStudio 时,直接在 `task` 对象中传递 `annotations` 数组,而不是在 `onStorageInitialized` 中手动创建。 ```typescript // 准备标注数据 const annotations = []; const existingResult = loadedAnnotationRef.current; if (existingResult && existingResult.result && Array.isArray(existingResult.result)) { annotations.push({ id: 'existing', result: existingResult.result, }); } // 初始化 LabelStudio lsfInstanceRef.current = new LabelStudio(editorContainerRef.current, { config: currentProject.config, task: { id: ..., data: currentTask.data, annotations: annotations.length > 0 ? annotations : undefined, // 关键! }, // ... }); ``` ### 2. 在 onStorageInitialized 中选择已加载的标注 ```typescript onStorageInitialized: (LS: any) => { const initAnnotation = () => { const as = LS.annotationStore; // 如果有已加载的标注,选择第一个 if (as.annotations && as.annotations.length > 0) { as.selectAnnotation(as.annotations[0].id); // 设置 snapshot 监听器 } else { // 创建新的空白标注 const annotation = as.createAnnotation(); as.selectAnnotation(annotation.id); } }; setTimeout(initAnnotation, 100); } ``` ### 3. 使用独立的 ref 存储加载的标注 ```typescript const loadedAnnotationRef = useRef(null); // 存储从 API 加载的标注 const annotationResultRef = useRef(null); // 存储当前编辑器的标注 ``` 这样可以避免在清理时丢失已加载的数据。 ### 4. 优化清理逻辑,避免 MobX 警告 **问题**:在组件卸载时,snapshot 监听器还在尝试访问已销毁的 MobX 对象。 **解决方案**: 1. 在 snapshot 回调中添加 try-catch 捕获错误 2. 检查 `isCleanedUp` 和 `lsfInstanceRef.current` 确保实例还存在 3. 先销毁 snapshot 监听器,再销毁 LabelStudio 实例 ```typescript // 在 snapshot 回调中 snapshotDisposer = onSnapshot(annotation, () => { if (!isCleanedUp && lsfInstanceRef.current) { try { annotationResultRef.current = annotation.serializeAnnotation(); } catch (e) { // Ignore errors during cleanup console.warn('Error serializing annotation:', e); } } }); // 清理顺序 function cleanup() { // 1. 先销毁 snapshot 监听器 if (snapshotDisposer) { snapshotDisposer(); snapshotDisposer = null; } // 2. 再销毁 LabelStudio 实例 if (lsfInstanceRef.current) { lsfInstanceRef.current.destroy(); lsfInstanceRef.current = null; } } ``` ## 修复效果 ### ✅ 标注加载 - 打开已标注的任务时,标注结果正确显示 - 多边形、标签等所有标注元素都正确渲染 - 控制台显示 "✅ Selecting existing annotation" ### ✅ 任务状态更新 - 保存标注后,任务状态更新为"已完成" - 任务列表中正确显示完成状态 - 不会创建重复的标注记录 ### ✅ 无 MobX 警告 - 组件卸载时不再出现 MobX 警告 - 清理逻辑正确执行 - 控制台干净无错误 ## 测试验证 ### 场景 1:首次标注 1. 打开待处理任务 2. 完成标注 3. 保存 4. ✅ 任务状态变为"已完成" ### 场景 2:重新打开已标注任务 1. 打开已完成任务 2. ✅ 标注结果正确显示 3. ✅ 无 MobX 警告 ### 场景 3:修改已有标注 1. 修改标注内容 2. 保存 3. 重新打开 4. ✅ 显示最新修改 ### 场景 4:导航离开 1. 打开标注界面 2. 点击返回 3. ✅ 无 MobX 警告 4. ✅ 清理日志正常 ## 技术要点 ### LabelStudio 标注加载的正确方式 LabelStudio 支持两种方式加载标注: 1. **通过 task.annotations**(推荐)✅ ```typescript task: { data: {...}, annotations: [{ id: 'xxx', result: [...] }] } ``` 2. **通过 createAnnotation()**(不推荐用于加载已有标注)❌ ```typescript as.createAnnotation({ result: [...] }) ``` 我们使用第一种方式,因为: - 这是 LabelStudio 的标准方式 - 自动处理标注的初始化 - 避免手动管理标注状态 ### MobX State Tree 清理 MobX State Tree 要求: 1. 在访问节点前检查它是否还在树中 2. 在销毁树前先移除所有监听器 3. 使用 try-catch 捕获清理时的错误 ### React useEffect 清理 正确的清理顺序: 1. 设置 `isCleanedUp = true` 标志 2. 取消所有异步操作(animation frames) 3. 移除所有事件监听器(snapshot disposers) 4. 销毁外部实例(LabelStudio) 5. 清理全局状态(window.LabelStudio) ## 相关文件 - `web/apps/lq_label/src/views/annotation-view/annotation-view.tsx` - 标注视图(主要修改) - `web/apps/lq_label/src/services/api.ts` - API 服务 - `backend/routers/annotation.py` - 标注 API 路由 - `backend/routers/task.py` - 任务 API 路由 ## 后续优化建议 1. **移除调试日志** - 生产环境中移除详细的 console.log - 保留关键的错误日志 2. **添加加载状态** - 显示"加载标注中..."提示 - 处理加载失败的情况 3. **性能优化** - 缓存标注数据,避免重复请求 - 使用 React.memo 优化组件渲染 4. **用户体验** - 添加"标注已加载"的提示 - 支持撤销/重做功能 - 添加自动保存功能 ## 总结 通过使用 LabelStudio 的标准 API 和正确的清理逻辑,我们成功解决了: - ✅ 标注结果加载问题 - ✅ 任务状态更新问题 - ✅ MobX 警告问题 现在标注系统可以正常工作,用户可以: - 创建新标注 - 保存标注 - 重新打开并继续编辑 - 无错误和警告