ANNOTATION_LOADING_FINAL_FIX.md 5.8 KB

标注加载最终修复总结

问题回顾

  1. 标注结果无法加载 - 已修复
  2. 任务状态不更新 - 已修复
  3. MobX 警告 - 已修复

最终解决方案

1. 使用 LabelStudio 的标准方式加载标注

关键改变:在初始化 LabelStudio 时,直接在 task 对象中传递 annotations 数组,而不是在 onStorageInitialized 中手动创建。

// 准备标注数据
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 中选择已加载的标注

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 存储加载的标注

const loadedAnnotationRef = useRef<any>(null); // 存储从 API 加载的标注
const annotationResultRef = useRef<any>(null); // 存储当前编辑器的标注

这样可以避免在清理时丢失已加载的数据。

4. 优化清理逻辑,避免 MobX 警告

问题:在组件卸载时,snapshot 监听器还在尝试访问已销毁的 MobX 对象。

解决方案

  1. 在 snapshot 回调中添加 try-catch 捕获错误
  2. 检查 isCleanedUplsfInstanceRef.current 确保实例还存在
  3. 先销毁 snapshot 监听器,再销毁 LabelStudio 实例

    // 在 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(推荐)✅

    task: {
     data: {...},
     annotations: [{ id: 'xxx', result: [...] }]
    }
    
  2. 通过 createAnnotation()(不推荐用于加载已有标注)❌

    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 警告问题

现在标注系统可以正常工作,用户可以:

  • 创建新标注
  • 保存标注
  • 重新打开并继续编辑
  • 无错误和警告