主要功能: - 图片上传时 OCR 内容去重(3个上传端点统一使用公共函数 _check_ocr_duplicate) - 图片管理 Tab:展示所有图片、手动删除、一键去重 - 搜索结果详情弹窗增加删除按钮(带确认弹窗) - 图片管理卡片点击查看详情(复用 showOcrDetailModal) - 搜索限制和 LLM 批量判断数量可通过网站设置 - MiniMax API 调用添加 reasoning_split=True - 企业微信机器人:WebSocket 长连接、图片搜索、配置化搜索数量 - 版本号升级至 1.2.0
172 lines
6.8 KiB
Python
172 lines
6.8 KiB
Python
"""
|
||
FastAPI 应用入口
|
||
挂载 v1 API 路由、CORS 中间件、生命周期管理
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
from contextlib import asynccontextmanager
|
||
from pathlib import Path
|
||
from typing import AsyncGenerator
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||
|
||
from app.config import settings
|
||
from app.database import close_db, init_db
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
||
"""
|
||
应用生命周期管理:
|
||
- 启动时:初始化数据目录、验证数据库连接、预热嵌入服务、初始化 BM25 索引
|
||
- 关闭时:释放数据库连接
|
||
"""
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
# ── 启动 ──
|
||
settings.ensure_dirs()
|
||
await init_db()
|
||
|
||
# 预热嵌入服务(懒加载,首次调用时初始化)
|
||
try:
|
||
from app.services.embedding_service import EmbeddingService
|
||
EmbeddingService.get_instance()
|
||
except Exception as exc:
|
||
_logger.warning("嵌入服务预热失败(将在首次使用时重试): %s", exc)
|
||
|
||
# ── 初始化 BM25 索引 ──
|
||
try:
|
||
from app.services.search_engine import BM25Index
|
||
import app.api.v1.images as images_module
|
||
|
||
_images_bm25 = BM25Index()
|
||
|
||
# 加载数据库中已有的 OCR 记录到索引
|
||
from app.database import async_session_factory
|
||
from app.models.base import OCRImage
|
||
from sqlalchemy import select
|
||
|
||
async with async_session_factory() as session:
|
||
result = await session.execute(
|
||
select(OCRImage).where(OCRImage.status == "completed")
|
||
)
|
||
records = result.scalars().all()
|
||
for record in records:
|
||
if record.ocr_text:
|
||
index_text = record.ocr_text
|
||
if record.tags:
|
||
try:
|
||
tags = json.loads(record.tags)
|
||
if tags:
|
||
index_text += "\n" + " ".join(tags)
|
||
except Exception:
|
||
pass
|
||
if record.story_summary:
|
||
index_text += "\n" + record.story_summary
|
||
_images_bm25.add_document(
|
||
doc_id=record.id,
|
||
text=index_text,
|
||
metadata={
|
||
"file_path": record.file_path,
|
||
"tags": json.loads(record.tags) if record.tags else [],
|
||
"story_summary": record.story_summary or "",
|
||
"confidence": record.confidence,
|
||
"provider": record.provider,
|
||
"created_at": str(record.created_at),
|
||
},
|
||
)
|
||
|
||
images_module.bm25_index = _images_bm25
|
||
_logger.info("BM25 索引加载完成: %d 条记录", _images_bm25.size)
|
||
except Exception as exc:
|
||
_logger.warning("BM25 索引初始化失败: %s", exc)
|
||
|
||
yield
|
||
|
||
# ── 关闭 ──
|
||
await close_db()
|
||
|
||
|
||
# ──────────────────────────── 创建应用 ────────────────────────────
|
||
|
||
app = FastAPI(
|
||
title="EduBrain - 中文直播教育知识库",
|
||
description="基于向量检索的中文直播教育知识库系统,支持直播文字稿导入、OCR 截图识别、语义搜索等功能。",
|
||
version="1.2.0",
|
||
lifespan=lifespan,
|
||
)
|
||
|
||
# ──────────────────────────── CORS 中间件 ────────────────────────────
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=settings.CORS_ORIGINS,
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
|
||
# ──────────────────────────── 全局异常处理 ────────────────────────────
|
||
|
||
@app.exception_handler(Exception)
|
||
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||
"""全局未捕获异常处理"""
|
||
logger = logging.getLogger(__name__)
|
||
logger.error("未处理异常: %s", exc, exc_info=True)
|
||
return JSONResponse(
|
||
status_code=500,
|
||
content={"detail": "服务器内部错误", "message": str(exc) if settings.DEBUG else "请稍后重试"},
|
||
)
|
||
|
||
|
||
# ──────────────────────────── 注册路由 ────────────────────────────
|
||
|
||
from app.api.v1.pages import router as pages_router
|
||
from app.api.v1.search import router as search_router
|
||
from app.api.v1.import_export import router as import_export_router
|
||
from app.api.v1.images import router as images_router
|
||
from app.api.v1.settings import router as settings_router
|
||
|
||
app.include_router(pages_router, prefix="/api/v1/pages", tags=["知识页面"])
|
||
app.include_router(search_router, prefix="/api/v1/search", tags=["语义搜索"])
|
||
app.include_router(import_export_router, prefix="/api/v1/import", tags=["导入导出"])
|
||
app.include_router(images_router, prefix="/api/v1/images", tags=["图片OCR"])
|
||
app.include_router(settings_router, prefix="/api/v1/settings", tags=["系统设置"])
|
||
|
||
|
||
# ──────────────────────────── 健康检查 ────────────────────────────
|
||
|
||
@app.get("/health", tags=["系统"])
|
||
async def health_check():
|
||
"""健康检查接口"""
|
||
return {"status": "ok", "version": "1.2.0"}
|
||
|
||
|
||
# ──────────────────────────── 图片文件访问 ────────────────────────────
|
||
|
||
@app.get("/data/images/{image_name}", tags=["前端"])
|
||
async def serve_image(image_name: str):
|
||
"""访问 images 目录下的图片文件(用于前端预览)"""
|
||
img_path = settings.images_dir / image_name
|
||
if not img_path.exists():
|
||
return JSONResponse(status_code=404, content={"detail": "图片不存在"})
|
||
return FileResponse(str(img_path))
|
||
|
||
|
||
# ──────────────────────────── 前端页面 ────────────────────────────
|
||
|
||
@app.get("/", tags=["前端"])
|
||
async def index():
|
||
"""返回前端管理页面"""
|
||
static_dir = Path(__file__).parent.parent / "static"
|
||
index_path = static_dir / "index.html"
|
||
if index_path.exists():
|
||
return FileResponse(str(index_path))
|
||
return RedirectResponse(url="/docs")
|