from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Body, Depends, HTTPException, Query, status from fastapi.responses import FileResponse from app.api.deps import CurrentUserContext, get_current_user, require_admin_user from app.schemas.common import ErrorResponse from app.schemas.knowledge import ( KnowledgeActionResponse, KnowledgeDocumentDetailRead, KnowledgeLibraryRead, KnowledgeOnlyOfficeCallbackRead, KnowledgeOnlyOfficeCallbackWrite, KnowledgeOnlyOfficeConfigRead, ) from app.services.knowledge import KnowledgeService router = APIRouter(prefix="/knowledge") @router.get( "/library", response_model=KnowledgeLibraryRead, summary="查询知识库目录", description="返回固定知识库目录与当前已上传文档列表。", responses={ status.HTTP_401_UNAUTHORIZED: { "model": ErrorResponse, "description": "未提供知识库访问用户头。", } }, ) def get_knowledge_library( _: Annotated[CurrentUserContext, Depends(get_current_user)], ) -> KnowledgeLibraryRead: return KnowledgeService().list_library() @router.get( "/documents/{document_id}", response_model=KnowledgeDocumentDetailRead, summary="读取知识库文档详情", description="返回单个知识库文档的元信息、预览类型和预览内容。", responses={ status.HTTP_401_UNAUTHORIZED: { "model": ErrorResponse, "description": "未提供知识库访问用户头。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "知识库文件不存在。", }, }, ) def get_knowledge_document( document_id: str, _: Annotated[CurrentUserContext, Depends(get_current_user)], ) -> KnowledgeDocumentDetailRead: try: return KnowledgeService().get_document_detail(document_id) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。", ) from exc @router.get( "/documents/{document_id}/onlyoffice-config", response_model=KnowledgeOnlyOfficeConfigRead, summary="读取 ONLYOFFICE 预览配置", description="为支持的 Office 文档生成 ONLYOFFICE 前端配置和临时访问令牌。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "ONLYOFFICE 未启用、配置不完整或文件格式不支持。", }, status.HTTP_401_UNAUTHORIZED: { "model": ErrorResponse, "description": "未提供知识库访问用户头。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "知识库文件不存在。", }, }, ) def get_knowledge_document_onlyoffice_config( document_id: str, current_user: Annotated[CurrentUserContext, Depends(get_current_user)], ) -> KnowledgeOnlyOfficeConfigRead: try: return KnowledgeService().build_onlyoffice_config(document_id, current_user) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。", ) from exc except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc @router.post( "/documents", response_model=KnowledgeDocumentDetailRead, status_code=status.HTTP_201_CREATED, summary="上传知识库文档", description="上传原始文件二进制内容到指定知识库目录。已有同名文件会覆盖并提升版本号。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "目录、文件名或文件内容不合法。", }, status.HTTP_401_UNAUTHORIZED: { "model": ErrorResponse, "description": "未提供知识库访问用户头。", }, status.HTTP_403_FORBIDDEN: { "model": ErrorResponse, "description": "只有管理员可以上传知识库文件。", }, }, ) def upload_knowledge_document( content: Annotated[ bytes, Body( media_type="application/octet-stream", description="待上传的文件二进制内容。", ), ], folder: Annotated[str, Query(min_length=1, description="目标知识库目录名称。")], filename: Annotated[str, Query(min_length=1, description="原始文件名。")], current_user: Annotated[CurrentUserContext, Depends(require_admin_user)], ) -> KnowledgeDocumentDetailRead: try: return KnowledgeService().upload_document(folder, filename, content, current_user) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc @router.delete( "/documents/{document_id}", response_model=KnowledgeActionResponse, summary="删除知识库文档", description="删除知识库文档及其索引记录。", responses={ status.HTTP_401_UNAUTHORIZED: { "model": ErrorResponse, "description": "未提供知识库访问用户头。", }, status.HTTP_403_FORBIDDEN: { "model": ErrorResponse, "description": "只有管理员可以删除知识库文件。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "知识库文件不存在。", }, }, ) def delete_knowledge_document( document_id: str, _: Annotated[CurrentUserContext, Depends(require_admin_user)], ) -> KnowledgeActionResponse: try: KnowledgeService().delete_document(document_id) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。", ) from exc return KnowledgeActionResponse(detail="知识库文件已删除。") @router.get( "/documents/{document_id}/content", response_class=FileResponse, summary="下载或预览知识库原文", description="根据文档 ID 返回原始文件内容,可用于浏览器内联预览或下载。", responses={ status.HTTP_200_OK: { "description": "文件内容。", "content": {"application/octet-stream": {}}, }, status.HTTP_401_UNAUTHORIZED: { "model": ErrorResponse, "description": "未提供知识库访问用户头。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "知识库文件不存在。", }, }, ) def get_knowledge_document_content( document_id: str, disposition: Annotated[ str, Query( pattern="^(inline|attachment)$", description="内容展示方式,支持 `inline` 或 `attachment`。", ), ] = "inline", _: Annotated[CurrentUserContext, Depends(get_current_user)] = None, ) -> FileResponse: try: file_path, media_type, filename = KnowledgeService().get_document_content(document_id) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。", ) from exc _ = disposition return FileResponse(file_path, media_type=media_type, filename=filename) @router.get( "/documents/{document_id}/onlyoffice/content", response_class=FileResponse, summary="读取 ONLYOFFICE 文档源文件", description="供 ONLYOFFICE 服务通过短时访问令牌拉取原始文件内容。", responses={ status.HTTP_200_OK: { "description": "文件内容。", "content": {"application/octet-stream": {}}, }, status.HTTP_401_UNAUTHORIZED: { "model": ErrorResponse, "description": "ONLYOFFICE 访问令牌无效。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "知识库文件不存在。", }, }, ) def get_knowledge_document_onlyoffice_content( document_id: str, access_token: Annotated[ str, Query(min_length=1, description="ONLYOFFICE 临时访问令牌。"), ], ) -> FileResponse: try: service = KnowledgeService() service.validate_onlyoffice_access_token(document_id, access_token) file_path, media_type, filename = service.get_document_content(document_id) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。", ) from exc except ValueError as exc: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc return FileResponse(file_path, media_type=media_type, filename=filename) @router.post( "/documents/{document_id}/onlyoffice/callback", response_model=KnowledgeOnlyOfficeCallbackRead, summary="接收 ONLYOFFICE 回调", description="接收 ONLYOFFICE 文档回写回调,在状态满足要求时更新知识库文件内容。", responses={ status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, "description": "回调载荷不合法。", }, status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "知识库文件不存在。", }, }, ) def handle_knowledge_document_onlyoffice_callback( document_id: str, payload: KnowledgeOnlyOfficeCallbackWrite, ) -> KnowledgeOnlyOfficeCallbackRead: try: KnowledgeService().handle_onlyoffice_callback(document_id, payload.model_dump()) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="知识库文件不存在。", ) from exc except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return KnowledgeOnlyOfficeCallbackRead()