fix issue

This commit is contained in:
EduBrain Dev
2026-04-14 21:25:41 +08:00
parent 23eabca58d
commit 4d5665d369
5 changed files with 145 additions and 137 deletions

View File

@@ -21,7 +21,7 @@ from sqlalchemy import select, func, text, delete
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings from app.config import settings
from app.database import get_db from app.database import get_db, async_session_factory
from app.models.base import OCRImage from app.models.base import OCRImage
from app.services.ocr_service import OCRService from app.services.ocr_service import OCRService
from app.services.llm_service import LLMService from app.services.llm_service import LLMService
@@ -299,15 +299,11 @@ async def search_images(
keyword: str, keyword: str,
limit: int = Query(5, ge=1, le=100), limit: int = Query(5, ge=1, le=100),
sort: str = Query("time_desc"), sort: str = Query("time_desc"),
db: AsyncSession = Depends(get_db),
): ):
""" """
AI 搜索图片SSE 流式返回)。 AI 搜索图片SSE 流式返回)。
1. 从 DB 取所有图片,按指定排序 注意:不使用 Depends(get_db),因为 StreamingResponse 的生命周期
2. 每批 5 条发给 Qwen3-8B 判断 超出 request scope需要在生成器内部自行管理 session。
3. 并发池最多 10 个请求
4. 匹配结果实时 SSE 推送
5. 找够 limit 个或遍历完 DB 后结束
""" """
async def event_stream(): async def event_stream():
@@ -320,6 +316,8 @@ async def search_images(
# 发送开始事件 # 发送开始事件
yield f"data: {json.dumps({'type': 'start', 'keyword': keyword, 'limit': limit}, ensure_ascii=False)}\n\n" yield f"data: {json.dumps({'type': 'start', 'keyword': keyword, 'limit': limit}, ensure_ascii=False)}\n\n"
# 在生成器内部自行管理 session避免 get_db 提前关闭
async with async_session_factory() as db:
# 先获取总记录数 # 先获取总记录数
count_result = await db.execute( count_result = await db.execute(
select(func.count(OCRImage.id)).where(OCRImage.status == "completed") select(func.count(OCRImage.id)).where(OCRImage.status == "completed")

View File

@@ -8,6 +8,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import logging import logging
import os
import threading import threading
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
@@ -193,7 +194,11 @@ async def health_check():
@app.get("/data/images/{image_name}", tags=["前端"]) @app.get("/data/images/{image_name}", tags=["前端"])
async def serve_image(image_name: str): async def serve_image(image_name: str):
"""访问 images 目录下的图片文件(用于前端预览)""" """访问 images 目录下的图片文件(用于前端预览)"""
img_path = settings.images_dir / image_name # 防止路径遍历攻击
safe_name = os.path.basename(image_name)
if safe_name != image_name or ".." in image_name:
return JSONResponse(status_code=400, content={"detail": "非法文件名"})
img_path = settings.images_dir / safe_name
if not img_path.exists(): if not img_path.exists():
return JSONResponse(status_code=404, content={"detail": "图片不存在"}) return JSONResponse(status_code=404, content={"detail": "图片不存在"})
return FileResponse(str(img_path)) return FileResponse(str(img_path))

View File

@@ -81,6 +81,7 @@ class OCRImage(Base, TimestampMixin):
String(20), default="pending", comment="状态: pending/processing/completed/failed" String(20), default="pending", comment="状态: pending/processing/completed/failed"
) )
blocks: Mapped[str | None] = mapped_column(Text, nullable=True, comment="OCR 文本块JSON") blocks: Mapped[str | None] = mapped_column(Text, nullable=True, comment="OCR 文本块JSON")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment="错误信息")
@property @property
def blocks_list(self) -> list[dict]: def blocks_list(self) -> list[dict]:

View File

@@ -286,10 +286,12 @@ class LLMService:
try: try:
from openai import AsyncOpenAI from openai import AsyncOpenAI
http_client = httpx.AsyncClient(timeout=60.0)
try:
client = AsyncOpenAI( client = AsyncOpenAI(
api_key=settings.MINIMAX_API_KEY, api_key=settings.MINIMAX_API_KEY,
base_url=settings.MINIMAX_BASE_URL, base_url=settings.MINIMAX_BASE_URL,
http_client=httpx.AsyncClient(timeout=60.0), http_client=http_client,
) )
response = await client.chat.completions.create( response = await client.chat.completions.create(
model=settings.MINIMAX_CHAT_MODEL, model=settings.MINIMAX_CHAT_MODEL,
@@ -317,6 +319,8 @@ class LLMService:
matched_ids.append(articles[num - 1]["id"]) matched_ids.append(articles[num - 1]["id"])
return matched_ids return matched_ids
finally:
await http_client.aclose()
except Exception as exc: except Exception as exc:
logger.warning("LLM 批量判断失败: %s", exc) logger.warning("LLM 批量判断失败: %s", exc)
return [] return []

View File

@@ -102,7 +102,7 @@ class PaddleOCRProvider(OCRProviderBase):
async def recognize(self, image_path: str) -> OCRResult: async def recognize(self, image_path: str) -> OCRResult:
"""使用 PaddleOCR 识别图片""" """使用 PaddleOCR 识别图片"""
self._init_ocr() self._init_ocr()
loop = asyncio.get_event_loop() loop = asyncio.get_running_loop()
def _run_ocr(): def _run_ocr():
result = self._ocr.ocr(image_path, cls=True) result = self._ocr.ocr(image_path, cls=True)
@@ -164,7 +164,7 @@ class AliyunOCRProvider(OCRProviderBase):
import asyncio import asyncio
loop = asyncio.get_event_loop() loop = asyncio.get_running_loop()
def _call_api(): def _call_api():
from alibabacloud_tea_openapi.models import Config from alibabacloud_tea_openapi.models import Config
@@ -249,7 +249,7 @@ class TencentOCRProvider(OCRProviderBase):
import base64 import base64
import json import json
loop = asyncio.get_event_loop() loop = asyncio.get_running_loop()
def _call_api(): def _call_api():
from tencentcloud.common import credential from tencentcloud.common import credential