Files
DataMate/runtime/datamate-python/app/module/annotation/interface/editor.py
Jerry Yan 6a4c4ae3d7 feat(auth): 为数据管理和RAG服务增加资源访问控制
- 在DatasetApplicationService中注入ResourceAccessService并添加所有权验证
- 在KnowledgeSetApplicationService中注入ResourceAccessService并添加所有权验证
- 修改DatasetRepository接口和实现类,增加按创建者过滤的方法
- 修改KnowledgeSetRepository接口和实现类,增加按创建者过滤的方法
- 在RAG索引器服务中添加知识库访问权限检查和作用域过滤
- 更新实体元对象处理器以使用请求用户上下文获取当前用户
- 在前端设置页面添加用户权限管理功能和角色权限控制
- 为Python标注服务增加用户上下文和数据集访问权限验证
2026-02-06 14:58:46 +08:00

170 lines
6.2 KiB
Python

"""
Label Studio Editor(前端嵌入式)接口
说明:
- 不依赖 Label Studio Server;仅复用其“编辑器”前端库
- DataMate 负责提供 tasks/annotations 数据与保存能力
- 当前支持 dataset_type=TEXT/IMAGE 的项目
"""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, Query, Path
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.logging import get_logger
from app.db.session import get_db
from app.module.annotation.schema.editor import (
EditorProjectInfo,
EditorTaskListResponse,
EditorTaskSegmentResponse,
EditorTaskResponse,
FileVersionCheckResponse,
UseNewVersionResponse,
UpsertAnnotationRequest,
UpsertAnnotationResponse,
)
from app.module.annotation.service.editor import AnnotationEditorService
from app.module.annotation.security import (
RequestUserContext,
get_request_user_context,
)
from app.module.shared.schema import StandardResponse
logger = get_logger(__name__)
router = APIRouter(
prefix="/editor",
tags=["annotation/editor"],
)
@router.get(
"/projects/{project_id}",
response_model=StandardResponse[EditorProjectInfo],
)
async def get_editor_project_info(
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
db: AsyncSession = Depends(get_db),
user_context: RequestUserContext = Depends(get_request_user_context),
):
service = AnnotationEditorService(db, user_context)
info = await service.get_project_info(project_id)
return StandardResponse(code=200, message="success", data=info)
@router.get(
"/projects/{project_id}/tasks",
response_model=StandardResponse[EditorTaskListResponse],
)
async def list_editor_tasks(
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
page: int = Query(0, ge=0, description="页码(从0开始)"),
size: int = Query(50, ge=1, le=200, description="每页大小"),
exclude_source_documents: Optional[bool] = Query(
None,
alias="excludeSourceDocuments",
description="是否排除已被转换为TXT的源文档文件(PDF/DOC/DOCX,仅文本数据集生效)",
),
db: AsyncSession = Depends(get_db),
user_context: RequestUserContext = Depends(get_request_user_context),
):
service = AnnotationEditorService(db, user_context)
result = await service.list_tasks(
project_id,
page=page,
size=size,
exclude_source_documents=exclude_source_documents,
)
return StandardResponse(code=200, message="success", data=result)
@router.get(
"/projects/{project_id}/tasks/{file_id}",
response_model=StandardResponse[EditorTaskResponse],
)
async def get_editor_task(
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
segment_index: Optional[int] = Query(
None, alias="segmentIndex", description="段落索引(分段模式下使用)"
),
db: AsyncSession = Depends(get_db),
user_context: RequestUserContext = Depends(get_request_user_context),
):
service = AnnotationEditorService(db, user_context)
task = await service.get_task(project_id, file_id, segment_index=segment_index)
return StandardResponse(code=200, message="success", data=task)
@router.get(
"/projects/{project_id}/tasks/{file_id}/segments",
response_model=StandardResponse[EditorTaskSegmentResponse],
)
async def get_editor_task_segment(
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
segment_index: int = Query(
..., ge=0, alias="segmentIndex", description="段落索引(从0开始)"
),
db: AsyncSession = Depends(get_db),
user_context: RequestUserContext = Depends(get_request_user_context),
):
service = AnnotationEditorService(db, user_context)
result = await service.get_task_segment(project_id, file_id, segment_index)
return StandardResponse(code=200, message="success", data=result)
@router.put(
"/projects/{project_id}/tasks/{file_id}/annotation",
response_model=StandardResponse[UpsertAnnotationResponse],
)
async def upsert_editor_annotation(
request: UpsertAnnotationRequest,
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
db: AsyncSession = Depends(get_db),
user_context: RequestUserContext = Depends(get_request_user_context),
):
service = AnnotationEditorService(db, user_context)
result = await service.upsert_annotation(project_id, file_id, request)
return StandardResponse(code=200, message="success", data=result)
@router.get(
"/projects/{project_id}/files/{file_id}/version",
response_model=StandardResponse[FileVersionCheckResponse],
)
async def check_file_version(
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
db: AsyncSession = Depends(get_db),
user_context: RequestUserContext = Depends(get_request_user_context),
):
"""
检查文件是否有新版本
"""
service = AnnotationEditorService(db, user_context)
result = await service.check_file_version(project_id, file_id)
return StandardResponse(code=200, message="success", data=result)
@router.post(
"/projects/{project_id}/files/{file_id}/use-new-version",
response_model=StandardResponse[UseNewVersionResponse],
)
async def use_new_version(
project_id: str = Path(..., description="标注项目ID(t_dm_labeling_projects.id)"),
file_id: str = Path(..., description="文件ID(t_dm_dataset_files.id)"),
db: AsyncSession = Depends(get_db),
user_context: RequestUserContext = Depends(get_request_user_context),
):
"""
使用文件新版本并清空标注
"""
service = AnnotationEditorService(db, user_context)
result = await service.use_new_version(project_id, file_id)
return StandardResponse(code=200, message="success", data=result)