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:
EduBrain Dev
2026-04-13 22:25:08 +08:00
commit b17786b57b
56 changed files with 9300 additions and 0 deletions

171
app/main.py Normal file
View 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")