feat: v1.2.0 - 图片去重与管理、微信机器人优化、搜索设置可配置
主要功能: - 图片上传时 OCR 内容去重(3个上传端点统一使用公共函数 _check_ocr_duplicate) - 图片管理 Tab:展示所有图片、手动删除、一键去重 - 搜索结果详情弹窗增加删除按钮(带确认弹窗) - 图片管理卡片点击查看详情(复用 showOcrDetailModal) - 搜索限制和 LLM 批量判断数量可通过网站设置 - MiniMax API 调用添加 reasoning_split=True - 企业微信机器人:WebSocket 长连接、图片搜索、配置化搜索数量 - 版本号升级至 1.2.0
This commit is contained in:
171
app/main.py
Normal file
171
app/main.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user