Files
hunjing/backend/app/api/v1/endpoints/imports.py
T
2026-08-21 16:00:13 +08:00

225 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""资料导入 API(api-spec §8)。
JSON 提交与文件上传分两个端点(FastAPI 无法优雅地在单端点同时吃 JSON body
与 multipart);两者返回结构一致。路径挂在 ``/characters/{character_id}`` 下。
候选抽取为异步后台任务:触发即返回 job 状态,前端轮询 ``GET /imports/{id}``
(extracting_* → reviewing)与候选列表获取结果。
"""
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
from app.api.v1.deps import get_current_user
from app.schemas.auth import CurrentUser
from app.schemas.candidates import (
BatchReviewRequest,
BatchReviewResult,
MemoryCandidateResponse,
ProfileCandidateResponse,
)
from app.schemas.common import Page
from app.schemas.imports import (
CreateImportRequest,
ImportItemResponse,
ImportJobResponse,
)
from app.services.imports import (
batch_review_memory_candidates,
batch_review_profile_candidates,
create_import_from_json,
create_import_from_upload,
get_import,
list_import_items,
list_jobs_page,
list_memory_candidates,
list_profile_candidates,
trigger_memory_extraction,
trigger_personality_extraction,
trigger_profile_extraction,
)
router = APIRouter(tags=["imports"])
@router.post("/characters/{character_id}/imports", response_model=ImportJobResponse)
async def create_import_json(
character_id: str,
payload: CreateImportRequest,
current_user: CurrentUser = Depends(get_current_user),
) -> ImportJobResponse:
return await create_import_from_json(
character_id, payload, owner_scope=current_user.owner_scope
)
@router.post("/characters/{character_id}/imports/upload", response_model=ImportJobResponse)
async def create_import_upload(
character_id: str,
file: UploadFile = File(..., description="上传文件(文本或音频)。"),
title: str | None = Form(default=None),
source_description: str | None = Form(default=None),
speaker_label: str | None = Form(default=None, description="音频说话人标签。"),
consent_status: str = Form(default="unknown"),
usage_scope: str = Form(default="reference_only"),
current_user: CurrentUser = Depends(get_current_user),
) -> ImportJobResponse:
return await create_import_from_upload(
character_id,
file,
title=title,
source_description=source_description,
speaker_label=speaker_label,
consent_status=consent_status,
usage_scope=usage_scope,
owner_scope=current_user.owner_scope,
)
@router.get("/characters/{character_id}/imports", response_model=Page[ImportJobResponse])
async def list_imports(
character_id: str,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
current_user: CurrentUser = Depends(get_current_user),
) -> Page[ImportJobResponse]:
return await list_jobs_page(
character_id, page=page, page_size=page_size, owner_scope=current_user.owner_scope
)
@router.get(
"/characters/{character_id}/imports/{import_id}",
response_model=ImportJobResponse,
)
async def read_import(
character_id: str,
import_id: str,
current_user: CurrentUser = Depends(get_current_user),
) -> ImportJobResponse:
return await get_import(character_id, import_id, owner_scope=current_user.owner_scope)
@router.get(
"/characters/{character_id}/imports/{import_id}/items",
response_model=Page[ImportItemResponse],
)
async def read_import_items(
character_id: str,
import_id: str,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
current_user: CurrentUser = Depends(get_current_user),
) -> Page[ImportItemResponse]:
return await list_import_items(
character_id,
import_id,
page=page,
page_size=page_size,
owner_scope=current_user.owner_scope,
)
# —— 候选抽取(5b,异步后台任务)——
@router.post(
"/characters/{character_id}/imports/{import_id}/profile-candidates",
response_model=ImportJobResponse,
)
async def trigger_profile_candidates(
character_id: str,
import_id: str,
current_user: CurrentUser = Depends(get_current_user),
) -> ImportJobResponse:
return await trigger_profile_extraction(
character_id, import_id, owner_scope=current_user.owner_scope
)
@router.get(
"/characters/{character_id}/imports/{import_id}/profile-candidates",
response_model=list[ProfileCandidateResponse],
)
async def list_profile_candidates_api(
character_id: str,
import_id: str,
status: str | None = Query(default=None, description="按审核状态过滤。"),
current_user: CurrentUser = Depends(get_current_user),
) -> list[ProfileCandidateResponse]:
return await list_profile_candidates(
character_id, import_id, status=status, owner_scope=current_user.owner_scope
)
@router.post(
"/characters/{character_id}/imports/{import_id}/memory-candidates",
response_model=ImportJobResponse,
)
async def trigger_memory_candidates(
character_id: str,
import_id: str,
current_user: CurrentUser = Depends(get_current_user),
) -> ImportJobResponse:
return await trigger_memory_extraction(
character_id, import_id, owner_scope=current_user.owner_scope
)
@router.get(
"/characters/{character_id}/imports/{import_id}/memory-candidates",
response_model=list[MemoryCandidateResponse],
)
async def list_memory_candidates_api(
character_id: str,
import_id: str,
status: str | None = Query(default=None, description="按审核状态过滤。"),
current_user: CurrentUser = Depends(get_current_user),
) -> list[MemoryCandidateResponse]:
return await list_memory_candidates(
character_id, import_id, status=status, owner_scope=current_user.owner_scope
)
@router.post(
"/characters/{character_id}/imports/{import_id}/personality-proposals",
response_model=ImportJobResponse,
)
async def trigger_personality_proposals(
character_id: str,
import_id: str,
current_user: CurrentUser = Depends(get_current_user),
) -> ImportJobResponse:
return await trigger_personality_extraction(
character_id, import_id, owner_scope=current_user.owner_scope
)
# —— 候选审核应用(5c)——
@router.post(
"/characters/{character_id}/profile-candidates/batch-review",
response_model=BatchReviewResult,
)
async def batch_review_profile(
character_id: str,
payload: BatchReviewRequest,
current_user: CurrentUser = Depends(get_current_user),
) -> BatchReviewResult:
return await batch_review_profile_candidates(
character_id, payload, owner_scope=current_user.owner_scope
)
@router.post(
"/characters/{character_id}/memory-candidates/batch-review",
response_model=BatchReviewResult,
)
async def batch_review_memory(
character_id: str,
payload: BatchReviewRequest,
current_user: CurrentUser = Depends(get_current_user),
) -> BatchReviewResult:
return await batch_review_memory_candidates(
character_id, payload, owner_scope=current_user.owner_scope
)