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

@@ -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,