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 字段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.
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 标志防止重复清理isCleanedUp 标志useEffect(() => {
// ...
return () => {
cleanup();
};
}, [id, loading, error, currentTask, currentProject]); // 添加 'id'
改进点:
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);
}
改进点:
修复后,以下场景都能正常工作:
添加了详细的控制台日志:
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.tsxuseLayoutEffect 进行同步清理通过改进数据提取逻辑和编辑器清理流程,现在系统能够: