feat: 完善后端 API OpenAPI 文档与统一错误响应 schema
This commit is contained in:
@@ -1,124 +1,294 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.api.deps import CurrentUserContext, get_current_user, require_admin_user
|
||||
from app.schemas.knowledge import (
|
||||
KnowledgeActionResponse,
|
||||
KnowledgeDocumentDetailRead,
|
||||
KnowledgeLibraryRead,
|
||||
KnowledgeOnlyOfficeCallbackRead,
|
||||
KnowledgeOnlyOfficeConfigRead,
|
||||
)
|
||||
from app.services.knowledge import KnowledgeService
|
||||
|
||||
router = APIRouter(prefix="/knowledge")
|
||||
|
||||
|
||||
@router.get("/library", response_model=KnowledgeLibraryRead)
|
||||
def get_knowledge_library(
|
||||
_: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
) -> KnowledgeLibraryRead:
|
||||
return KnowledgeService().list_library()
|
||||
|
||||
|
||||
@router.get("/documents/{document_id}", response_model=KnowledgeDocumentDetailRead)
|
||||
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)
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@router.post("/documents", response_model=KnowledgeDocumentDetailRead, status_code=status.HTTP_201_CREATED)
|
||||
async def upload_knowledge_document(
|
||||
request: Request,
|
||||
folder: Annotated[str, Query(min_length=1)],
|
||||
filename: Annotated[str, Query(min_length=1)],
|
||||
current_user: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
) -> KnowledgeDocumentDetailRead:
|
||||
content = await request.body()
|
||||
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)
|
||||
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")
|
||||
def get_knowledge_document_content(
|
||||
document_id: str,
|
||||
disposition: Annotated[str, Query(pattern="^(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")
|
||||
def get_knowledge_document_onlyoffice_content(
|
||||
document_id: str,
|
||||
access_token: Annotated[str, Query(min_length=1)],
|
||||
) -> 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
|
||||
_ = 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)
|
||||
async def handle_knowledge_document_onlyoffice_callback(
|
||||
document_id: str,
|
||||
request: Request,
|
||||
) -> KnowledgeOnlyOfficeCallbackRead:
|
||||
payload = await request.json()
|
||||
try:
|
||||
KnowledgeService().handle_onlyoffice_callback(document_id, payload)
|
||||
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 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()
|
||||
|
||||
Reference in New Issue
Block a user