ANNOTATION_SAVE_FIX.md 5.9 KB

标注保存和编辑器清理问题修复

问题 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() 函数中添加了数据提取和验证逻辑:

// Extract only the result field if it exists
let resultData: Record<string, any> = {};

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. 改进清理逻辑

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 到依赖数组

useEffect(() => {
  // ...
  return () => {
    cleanup();
  };
}, [id, loading, error, currentTask, currentProject]); // 添加 'id'

改进点

  • ✅ 任务 ID 变化时自动清理旧编辑器
  • ✅ 确保每次加载新任务时都有干净的编辑器实例

3. 在回调中检查清理状态

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. ✅ 刷新页面后重新加载编辑器

调试日志

添加了详细的控制台日志:

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 状态树
  • ✅ 提供详细的调试日志