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,