"""Define API routes for documents.""" from __future__ import annotations from io import BytesIO from urllib.parse import quote from fastapi import APIRouter, File, Form, HTTPException, UploadFile from fastapi.responses import StreamingResponse from loguru import logger from app.api.models import DocumentUploadResponse from app.application.documents import DocumentProcessResult from app.shared.bootstrap import get_document_command_service, get_document_query_service # Keep route handlers close to their transport-layer wiring for easier auditing. router = APIRouter(prefix="/documents", tags=["documents"]) def _document_response(result: DocumentProcessResult) -> DocumentUploadResponse: """Handle document response for this module.""" return DocumentUploadResponse( doc_id=result.doc_id, doc_name=result.doc_name, status=result.status, message=result.message, num_chunks=result.num_chunks, summary=result.summary, summary_latency_ms=result.summary_latency_ms, ) @router.post("/upload", response_model=DocumentUploadResponse) async def upload_document( file: UploadFile = File(..., description="上传的文档文件"), doc_name: str | None = Form(None, description="文档名称"), regulation_type: str | None = Form(None, description="法规类型"), version: str | None = Form(None, description="文档版本"), generate_summary: bool = Form(False, description="是否生成摘要"), ): """Handle upload document.""" content = await file.read() if not file.filename: raise HTTPException(status_code=400, detail="文件名不能为空") if not content: raise HTTPException(status_code=400, detail="上传文件为空") try: result = get_document_command_service().upload_and_process( file_name=file.filename, content=content, content_type=file.content_type or "application/octet-stream", doc_name=doc_name, regulation_type=regulation_type or "", version=version or "", generate_summary=generate_summary, ) if result.status == "failed": raise HTTPException(status_code=500, detail=result.message) return _document_response(result) except HTTPException: raise except Exception as exc: logger.exception("文档上传失败") raise HTTPException(status_code=500, detail=str(exc)) @router.get("/status/{doc_id}", response_model=DocumentUploadResponse) async def get_document_status(doc_id: str): """Return document status.""" document = get_document_query_service().get(doc_id) if not document: raise HTTPException(status_code=404, detail="文档不存在") return DocumentUploadResponse( doc_id=document.doc_id, doc_name=document.doc_name, status=document.status.value, message=document.error_message or "查询成功", num_chunks=document.chunk_count, summary=document.summary, summary_latency_ms=document.summary_latency_ms, regulation_type=document.regulation_type, version=document.version, ) @router.get("/download/{doc_id}") async def download_document(doc_id: str): """Handle download document.""" try: document, file_data = get_document_query_service().download(doc_id) encoded_name = quote(document.file_name) return StreamingResponse( BytesIO(file_data), media_type=document.content_type or "application/octet-stream", headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_name}"}, ) except FileNotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) except Exception as exc: logger.exception("文档下载失败") raise HTTPException(status_code=500, detail=str(exc)) @router.get("/list") async def list_documents(): """List documents.""" documents = get_document_query_service().list_documents() return { "documents": [ { "doc_id": item.doc_id, "doc_name": item.doc_name, "status": item.status.value, "chunk_count": item.chunk_count, "updated_at": item.updated_at.isoformat(), } for item in documents ], "total": len(documents), } @router.get("/management-list") async def get_document_management_list(): """Return document management list.""" documents = get_document_query_service().list_documents() return { "documents": [ { "doc_id": item.doc_id, "doc_name": item.doc_name, "status": item.status.value, "chunk_count": item.chunk_count, "size_bytes": item.size_bytes, "summary": item.summary, "updated_at": item.updated_at.isoformat(), "regulation_type": item.regulation_type, "version": item.version, } for item in documents ], "total": len(documents), } @router.delete("/{doc_id}") async def delete_document(doc_id: str): """Delete a document and its associated data.""" deleted = get_document_command_service().delete(doc_id) if not deleted: raise HTTPException(status_code=404, detail="文档不存在") return {"doc_id": doc_id, "deleted": True} @router.post("/{doc_id}/retry", response_model=DocumentUploadResponse) async def retry_document(doc_id: str): """Re-process a failed document.""" try: result = get_document_command_service().retry(doc_id) if result.status == "failed": raise HTTPException(status_code=500, detail=result.message) return _document_response(result) except HTTPException: raise except Exception as exc: logger.exception("文档重试失败") raise HTTPException(status_code=500, detail=str(exc))