From a82f4f1bc3cbd6452e7ff0323a1ad95b702669fb Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 9 Jan 2026 12:31:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor(annotation):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=AF=B9=20Label=20Studio=20Server=20=E7=9A=84=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=B9=B6=E5=88=87=E6=8D=A2=E5=88=B0=E5=86=85=E5=B5=8C?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 LabelStudioClient 和 SyncService 的导入及使用 - 删除与 Label Studio 项目的创建、删除和同步相关代码 - 修改创建数据集映射功能,改为创建 DataMate 标注项目 - 更新删除映射接口,仅进行软删除不再删除 Label Studio 项目 - 修改同步接口为兼容性保留,实际操作为空操作 - 移除 Label Studio 连接诊断功能 - 更新文档说明以反映内嵌编辑器模式的变化 --- frontend/README.md | 1 - frontend/index.html | 1 - frontend/public/lsf/lsf.html | 22 +++++++++- .../Annotate/LabelStudioTextEditor.tsx | 10 ++--- .../app/module/annotation/service/editor.py | 40 ++++++++++++++++--- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index b4fcd04..5946da6 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -11,7 +11,6 @@ npm run mock # 启动后台Mock服务(可选) ``` frontend/ ├── public/ # 📖 文档中心 -│ ├── huawei-logo.webp/ # logo │ └── xxx/ # 标注工作台(可分离部署) │ ├── src/ # 🎨 前端应用 diff --git a/frontend/index.html b/frontend/index.html index 278d49b..32605f2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,6 @@ - DataMate diff --git a/frontend/public/lsf/lsf.html b/frontend/public/lsf/lsf.html index ac70325..b7f3d2d 100644 --- a/frontend/public/lsf/lsf.html +++ b/frontend/public/lsf/lsf.html @@ -45,6 +45,19 @@ window.parent.postMessage({ type, payload }, ORIGIN); } + window.addEventListener("error", (event) => { + try { + postToParent("LS_ERROR", { message: event?.message || "iframe 内发生脚本错误" }); + } catch (_) {} + }); + + window.addEventListener("unhandledrejection", (event) => { + try { + const reason = event?.reason; + postToParent("LS_ERROR", { message: reason?.message || String(reason || "iframe 内发生未处理异常") }); + } catch (_) {} + }); + function destroyLabelStudio() { try { if (lsInstance && typeof lsInstance.destroy === "function") { @@ -166,12 +179,17 @@ : { id: selected?.id || "draft", result: (selected && selected.result) || [] }; // 最小化对齐 Label Studio Server 的字段(DataMate 侧会原样存储) - if (!annotationPayload.task) annotationPayload.task = currentTask?.id || null; + const taskId = typeof currentTask?.id === "number" ? currentTask.id : Number(currentTask?.id) || null; + const fileId = currentTask?.data?.file_id || currentTask?.data?.fileId || null; + + annotationPayload.id = typeof annotationPayload.id === "number" ? annotationPayload.id : taskId || 1; + annotationPayload.task = taskId; if (!annotationPayload.created_at) annotationPayload.created_at = new Date().toISOString(); annotationPayload.updated_at = new Date().toISOString(); return { - taskId: currentTask?.id || null, + taskId, + fileId, annotation: annotationPayload, }; } diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index 345bf24..238f481 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -145,16 +145,16 @@ export default function LabelStudioTextEditor() { }; const saveFromExport = async (payload: any) => { - const taskId = payload?.taskId; + const fileId = payload?.fileId; const annotation = payload?.annotation; - if (!taskId || !annotation) { - message.error("导出标注失败:缺少 taskId/annotation"); + if (!fileId || !annotation) { + message.error("导出标注失败:缺少 fileId/annotation"); return; } setSaving(true); try { - await upsertEditorAnnotationUsingPut(projectId, String(taskId), { annotation }); + await upsertEditorAnnotationUsingPut(projectId, String(fileId), { annotation }); message.success("标注已保存"); await loadTasks(true); } catch (e) { @@ -272,7 +272,7 @@ export default function LabelStudioTextEditor() { 返回 - 标注(内嵌编辑器) + 标注
diff --git a/runtime/datamate-python/app/module/annotation/service/editor.py b/runtime/datamate-python/app/module/annotation/service/editor.py index e779789..749ea71 100644 --- a/runtime/datamate-python/app/module/annotation/service/editor.py +++ b/runtime/datamate-python/app/module/annotation/service/editor.py @@ -13,6 +13,7 @@ import uuid from datetime import datetime from typing import Any, Dict, List, Optional, Tuple +import hashlib import httpx from fastapi import HTTPException from sqlalchemy import func, select @@ -41,6 +42,27 @@ class AnnotationEditorService: self.db = db self.template_service = AnnotationTemplateService() + @staticmethod + def _stable_ls_id(seed: str) -> int: + """ + 生成稳定的 Label Studio 风格整数 ID(JS 安全整数范围内)。 + + 说明: + - Label Studio Frontend 的 mobx-state-tree 模型对 task/annotation 的 id 有类型约束(通常为 number)。 + - DataMate 使用 UUID 作为 file_id/project_id,因此需映射为整数供编辑器使用。 + - 取 sha1 的前 13 个 hex(52bit),落在 JS Number 的安全整数范围。 + """ + digest = hashlib.sha1(seed.encode("utf-8")).hexdigest() + value = int(digest[:13], 16) + return value if value > 0 else 1 + + def _make_ls_task_id(self, project_id: str, file_id: str) -> int: + return self._stable_ls_id(f"task:{project_id}:{file_id}") + + def _make_ls_annotation_id(self, project_id: str, file_id: str) -> int: + # 单人单份最终标签:每个 task 只保留一个 annotation,id 直接与 task 绑定即可 + return self._stable_ls_id(f"annotation:{project_id}:{file_id}") + async def _get_project_or_404(self, project_id: str) -> LabelingProject: result = await self.db.execute( select(LabelingProject).where( @@ -207,8 +229,10 @@ class AnnotationEditorService: ) ann = ann_result.scalar_one_or_none() + ls_task_id = self._make_ls_task_id(project_id, file_id) + task: Dict[str, Any] = { - "id": file_id, + "id": ls_task_id, "data": { "text": text_content, "file_id": file_id, @@ -222,7 +246,11 @@ class AnnotationEditorService: if ann: annotation_updated_at = ann.updated_at # 直接返回存储的 annotation 原始对象(Label Studio 兼容) - task["annotations"] = [ann.annotation] + stored = dict(ann.annotation or {}) + stored["task"] = ls_task_id + if not isinstance(stored.get("id"), int): + stored["id"] = self._make_ls_annotation_id(project_id, file_id) + task["annotations"] = [stored] return EditorTaskResponse( task=task, @@ -247,6 +275,11 @@ class AnnotationEditorService: if not isinstance(result, list): raise HTTPException(status_code=400, detail="annotation.result 必须为数组") + ls_task_id = self._make_ls_task_id(project_id, file_id) + annotation_payload["task"] = ls_task_id + if not isinstance(annotation_payload.get("id"), int): + annotation_payload["id"] = self._make_ls_annotation_id(project_id, file_id) + existing_result = await self.db.execute( select(AnnotationResult).where( AnnotationResult.project_id == project_id, @@ -262,8 +295,6 @@ class AnnotationEditorService: if existing.updated_at != request.expected_updated_at.replace(tzinfo=None): raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试") - # 固定 annotation.id 为记录ID,保持稳定 - annotation_payload["id"] = existing.id existing.annotation = annotation_payload # type: ignore[assignment] existing.updated_at = now # type: ignore[assignment] await self.db.commit() @@ -275,7 +306,6 @@ class AnnotationEditorService: ) new_id = str(uuid.uuid4()) - annotation_payload["id"] = new_id record = AnnotationResult( id=new_id, project_id=project_id,