refactor(annotation): 移除对 Label Studio Server 的依赖并切换到内嵌编辑器模式

- 移除 LabelStudioClient 和 SyncService 的导入及使用
- 删除与 Label Studio 项目的创建、删除和同步相关代码
- 修改创建数据集映射功能,改为创建 DataMate 标注项目
- 更新删除映射接口,仅进行软删除不再删除 Label Studio 项目
- 修改同步接口为兼容性保留,实际操作为空操作
- 移除 Label Studio 连接诊断功能
- 更新文档说明以反映内嵌编辑器模式的变化
This commit is contained in:
2026-01-09 12:31:03 +08:00
parent 3aa7f6e3a1
commit a82f4f1bc3
5 changed files with 60 additions and 14 deletions

View File

@@ -11,7 +11,6 @@ npm run mock # 启动后台Mock服务(可选)
``` ```
frontend/ frontend/
├── public/ # 📖 文档中心 ├── public/ # 📖 文档中心
│ ├── huawei-logo.webp/ # logo
│ └── xxx/ # 标注工作台(可分离部署) │ └── xxx/ # 标注工作台(可分离部署)
├── src/ # 🎨 前端应用 ├── src/ # 🎨 前端应用

View File

@@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/huawei-logo.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DataMate</title> <title>DataMate</title>
</head> </head>

View File

@@ -45,6 +45,19 @@
window.parent.postMessage({ type, payload }, ORIGIN); 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() { function destroyLabelStudio() {
try { try {
if (lsInstance && typeof lsInstance.destroy === "function") { if (lsInstance && typeof lsInstance.destroy === "function") {
@@ -166,12 +179,17 @@
: { id: selected?.id || "draft", result: (selected && selected.result) || [] }; : { id: selected?.id || "draft", result: (selected && selected.result) || [] };
// 最小化对齐 Label Studio Server 的字段(DataMate 侧会原样存储) // 最小化对齐 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(); if (!annotationPayload.created_at) annotationPayload.created_at = new Date().toISOString();
annotationPayload.updated_at = new Date().toISOString(); annotationPayload.updated_at = new Date().toISOString();
return { return {
taskId: currentTask?.id || null, taskId,
fileId,
annotation: annotationPayload, annotation: annotationPayload,
}; };
} }

View File

@@ -145,16 +145,16 @@ export default function LabelStudioTextEditor() {
}; };
const saveFromExport = async (payload: any) => { const saveFromExport = async (payload: any) => {
const taskId = payload?.taskId; const fileId = payload?.fileId;
const annotation = payload?.annotation; const annotation = payload?.annotation;
if (!taskId || !annotation) { if (!fileId || !annotation) {
message.error("导出标注失败:缺少 taskId/annotation"); message.error("导出标注失败:缺少 fileId/annotation");
return; return;
} }
setSaving(true); setSaving(true);
try { try {
await upsertEditorAnnotationUsingPut(projectId, String(taskId), { annotation }); await upsertEditorAnnotationUsingPut(projectId, String(fileId), { annotation });
message.success("标注已保存"); message.success("标注已保存");
await loadTasks(true); await loadTasks(true);
} catch (e) { } catch (e) {
@@ -272,7 +272,7 @@ export default function LabelStudioTextEditor() {
</Button> </Button>
<Typography.Title level={4} style={{ margin: 0 }}> <Typography.Title level={4} style={{ margin: 0 }}>
</Typography.Title> </Typography.Title>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -13,6 +13,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import hashlib
import httpx import httpx
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import func, select from sqlalchemy import func, select
@@ -41,6 +42,27 @@ class AnnotationEditorService:
self.db = db self.db = db
self.template_service = AnnotationTemplateService() 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: async def _get_project_or_404(self, project_id: str) -> LabelingProject:
result = await self.db.execute( result = await self.db.execute(
select(LabelingProject).where( select(LabelingProject).where(
@@ -207,8 +229,10 @@ class AnnotationEditorService:
) )
ann = ann_result.scalar_one_or_none() ann = ann_result.scalar_one_or_none()
ls_task_id = self._make_ls_task_id(project_id, file_id)
task: Dict[str, Any] = { task: Dict[str, Any] = {
"id": file_id, "id": ls_task_id,
"data": { "data": {
"text": text_content, "text": text_content,
"file_id": file_id, "file_id": file_id,
@@ -222,7 +246,11 @@ class AnnotationEditorService:
if ann: if ann:
annotation_updated_at = ann.updated_at annotation_updated_at = ann.updated_at
# 直接返回存储的 annotation 原始对象(Label Studio 兼容) # 直接返回存储的 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( return EditorTaskResponse(
task=task, task=task,
@@ -247,6 +275,11 @@ class AnnotationEditorService:
if not isinstance(result, list): if not isinstance(result, list):
raise HTTPException(status_code=400, detail="annotation.result 必须为数组") 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( existing_result = await self.db.execute(
select(AnnotationResult).where( select(AnnotationResult).where(
AnnotationResult.project_id == project_id, AnnotationResult.project_id == project_id,
@@ -262,8 +295,6 @@ class AnnotationEditorService:
if existing.updated_at != request.expected_updated_at.replace(tzinfo=None): if existing.updated_at != request.expected_updated_at.replace(tzinfo=None):
raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试") raise HTTPException(status_code=409, detail="标注已被更新,请刷新后重试")
# 固定 annotation.id 为记录ID,保持稳定
annotation_payload["id"] = existing.id
existing.annotation = annotation_payload # type: ignore[assignment] existing.annotation = annotation_payload # type: ignore[assignment]
existing.updated_at = now # type: ignore[assignment] existing.updated_at = now # type: ignore[assignment]
await self.db.commit() await self.db.commit()
@@ -275,7 +306,6 @@ class AnnotationEditorService:
) )
new_id = str(uuid.uuid4()) new_id = str(uuid.uuid4())
annotation_payload["id"] = new_id
record = AnnotationResult( record = AnnotationResult(
id=new_id, id=new_id,
project_id=project_id, project_id=project_id,