feat: 完善知识库、策略预览与OnlyOffice集成,增强后端启动依赖检查

This commit is contained in:
caoxiaozhu
2026-05-09 05:59:46 +00:00
parent 1d3ac5c2e0
commit d9133193e8
31 changed files with 5534 additions and 5343 deletions

View File

@@ -1,62 +1,62 @@
from collections.abc import Generator
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, Header, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_session_factory
def get_db() -> Generator[Session, None, None]:
db = get_session_factory()()
try:
yield db
finally:
db.close()
@dataclass(slots=True)
class CurrentUserContext:
username: str
name: str
role_codes: list[str]
is_admin: bool
def get_current_user(
x_auth_username: Annotated[str | None, Header()] = None,
x_auth_name: Annotated[str | None, Header()] = None,
x_auth_role_codes: Annotated[str | None, Header()] = None,
x_auth_is_admin: Annotated[str | None, Header()] = None,
) -> CurrentUserContext:
role_codes = [item.strip() for item in (x_auth_role_codes or "").split(",") if item.strip()]
is_admin = str(x_auth_is_admin or "").strip().lower() in {"1", "true", "yes", "on"}
username = (x_auth_username or "").strip()
name = (x_auth_name or username).strip()
if not username and not name:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录后再访问知识库。",
)
return CurrentUserContext(
username=username or name,
name=name or username,
role_codes=role_codes,
is_admin=is_admin,
)
def require_admin_user(
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
) -> CurrentUserContext:
if current_user.is_admin or "manager" in current_user.role_codes:
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以上传、删除或修改知识库文件。",
)
from collections.abc import Generator
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, Header, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_session_factory
def get_db() -> Generator[Session, None, None]:
db = get_session_factory()()
try:
yield db
finally:
db.close()
@dataclass(slots=True)
class CurrentUserContext:
username: str
name: str
role_codes: list[str]
is_admin: bool
def get_current_user(
x_auth_username: Annotated[str | None, Header()] = None,
x_auth_name: Annotated[str | None, Header()] = None,
x_auth_role_codes: Annotated[str | None, Header()] = None,
x_auth_is_admin: Annotated[str | None, Header()] = None,
) -> CurrentUserContext:
role_codes = [item.strip() for item in (x_auth_role_codes or "").split(",") if item.strip()]
is_admin = str(x_auth_is_admin or "").strip().lower() in {"1", "true", "yes", "on"}
username = (x_auth_username or "").strip()
name = (x_auth_name or username).strip()
if not username and not name:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录后再访问知识库。",
)
return CurrentUserContext(
username=username or name,
name=name or username,
role_codes=role_codes,
is_admin=is_admin,
)
def require_admin_user(
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
) -> CurrentUserContext:
if current_user.is_admin or "manager" in current_user.role_codes:
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以上传、删除或修改知识库文件。",
)

View File

@@ -1,124 +1,124 @@
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
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)
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
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 KnowledgeOnlyOfficeCallbackRead()
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
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)
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
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 KnowledgeOnlyOfficeCallbackRead()

View File

@@ -1,18 +1,18 @@
from fastapi import APIRouter
from app.api.v1.endpoints.auth import router as auth_router
from app.api.v1.endpoints.bootstrap import router as bootstrap_router
from app.api.v1.endpoints.employees import router as employees_router
from app.api.v1.endpoints.health import router as health_router
from app.api.v1.endpoints.knowledge import router as knowledge_router
from app.api.v1.endpoints.reimbursements import router as reimbursements_router
from app.api.v1.endpoints.settings import router as settings_router
router = APIRouter()
router.include_router(health_router, tags=["health"])
router.include_router(bootstrap_router, tags=["bootstrap"])
router.include_router(auth_router, tags=["auth"])
router.include_router(knowledge_router, tags=["knowledge"])
router.include_router(employees_router, prefix="/employees", tags=["employees"])
router.include_router(reimbursements_router, prefix="/reimbursements", tags=["reimbursements"])
router.include_router(settings_router, tags=["settings"])
from fastapi import APIRouter
from app.api.v1.endpoints.auth import router as auth_router
from app.api.v1.endpoints.bootstrap import router as bootstrap_router
from app.api.v1.endpoints.employees import router as employees_router
from app.api.v1.endpoints.health import router as health_router
from app.api.v1.endpoints.knowledge import router as knowledge_router
from app.api.v1.endpoints.reimbursements import router as reimbursements_router
from app.api.v1.endpoints.settings import router as settings_router
router = APIRouter()
router.include_router(health_router, tags=["health"])
router.include_router(bootstrap_router, tags=["bootstrap"])
router.include_router(auth_router, tags=["auth"])
router.include_router(knowledge_router, tags=["knowledge"])
router.include_router(employees_router, prefix="/employees", tags=["employees"])
router.include_router(reimbursements_router, prefix="/reimbursements", tags=["reimbursements"])
router.include_router(settings_router, tags=["settings"])