"""资料导入 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 )