feat: global exception handler, concurrent VLM, and docker security

- Add global FastAPI exception handler for unhandled errors
- Add get_current_user_obj() dependency for cleaner auth patterns
- Switch VLM image description from serial to asyncio.gather concurrency
- Extract score conversion to shared score_utils module
- Docker: use env vars for passwords, remove hardcoded API key default
- Add .gitattributes and update .gitignore for tar.gz and tsbuildinfo

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 15:53:18 +08:00
parent 37c0364e7e
commit b8253b86a0
11 changed files with 196 additions and 131 deletions
+35 -34
View File
@@ -4,14 +4,17 @@ FastAPI应用入口
"""
import os
import sys
import logging
from pathlib import Path
from datetime import datetime
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
import uvicorn
logger = logging.getLogger(__name__)
# 设置系统时区为北京时间
os.environ['TZ'] = 'Asia/Shanghai'
@@ -52,9 +55,9 @@ def startup_knowledge_base():
try:
from src.services.file_watcher_service import start_file_watcher
start_file_watcher()
print("知识库文件监控服务启动成功")
logger.info("知识库文件监控服务启动成功")
except Exception as e:
print(f"启动知识库文件监控服务失败: {str(e)}")
logger.error(f"启动知识库文件监控服务失败: {str(e)}")
def seed_forum_categories():
@@ -72,17 +75,17 @@ def seed_forum_categories():
try:
existing_count = db.query(ForumCategory).count()
if existing_count > 0:
print(f"论坛分类已存在({existing_count} 个),跳过初始化")
logger.info(f"论坛分类已存在({existing_count} 个),跳过初始化")
return
for cat_data in DEFAULT_CATEGORIES:
category = ForumCategory(**cat_data)
db.add(category)
db.commit()
print(f"已创建 {len(DEFAULT_CATEGORIES)} 个论坛分类")
logger.info(f"已创建 {len(DEFAULT_CATEGORIES)} 个论坛分类")
except Exception as e:
db.rollback()
print(f"初始化论坛分类失败: {e}")
logger.error(f"初始化论坛分类失败: {e}")
finally:
db.close()
@@ -94,10 +97,9 @@ async def startup_event():
import asyncio
import sys
print("=" * 50, file=sys.stderr)
print("应用启动事件开始", file=sys.stderr)
print("=" * 50, file=sys.stderr)
sys.stderr.flush()
logger.info("=" * 50)
logger.info("应用启动事件开始")
logger.info("=" * 50)
# 等待数据库可用(重试机制,处理 DNS 解析延迟)
max_retries = 10
@@ -105,28 +107,23 @@ async def startup_event():
for attempt in range(max_retries):
try:
print(f"尝试连接数据库(尝试 {attempt + 1}/{max_retries}...", file=sys.stderr)
sys.stderr.flush()
logger.info(f"尝试连接数据库(尝试 {attempt + 1}/{max_retries}...")
# 尝试创建数据库表
create_tables()
print("数据库表创建成功", file=sys.stderr)
sys.stderr.flush()
logger.info("数据库表创建成功")
break
except Exception as e:
if attempt < max_retries - 1:
print(f"数据库连接失败(尝试 {attempt + 1}/{max_retries}: {e}", file=sys.stderr)
print(f"等待 {retry_delay} 秒后重试...", file=sys.stderr)
sys.stderr.flush()
logger.warning(f"数据库连接失败(尝试 {attempt + 1}/{max_retries}: {e}")
await asyncio.sleep(retry_delay)
else:
print(f"数据库连接失败,已达到最大重试次数: {e}", file=sys.stderr)
sys.stderr.flush()
logger.error(f"数据库连接失败,已达到最大重试次数: {e}")
# 不抛出异常,让应用继续启动,但数据库操作会失败
try:
startup_knowledge_base()
except Exception as e:
print(f"启动知识库服务失败: {e}", file=sys.stderr)
logger.error(f"启动知识库服务失败: {e}")
# 确保系统知识库与目录同步
try:
@@ -137,17 +134,15 @@ async def startup_event():
kb_service.ensure_system_knowledge_bases()
db.close()
except Exception as e:
print(f"同步系统知识库失败: {e}", file=sys.stderr)
logger.error(f"同步系统知识库失败: {e}")
# 初始化论坛分类
try:
seed_forum_categories()
except Exception as e:
print(f"初始化论坛分类失败: {e}", file=sys.stderr)
sys.stderr.flush()
print("应用启动事件完成", file=sys.stderr)
sys.stderr.flush()
logger.error(f"初始化论坛分类失败: {e}")
logger.info("应用启动事件完成")
# 应用关闭事件
@app.on_event("shutdown")
@@ -156,9 +151,18 @@ async def shutdown_event():
try:
from src.services.file_watcher_service import stop_file_watcher
stop_file_watcher()
print("知识库文件监控服务已停止")
logger.info("知识库文件监控服务已停止")
except Exception as e:
print(f"停止知识库文件监控服务失败: {str(e)}")
logger.error(f"停止知识库文件监控服务失败: {str(e)}")
# 全局异常处理器
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
logger.error(f"未处理的异常: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "服务器内部错误,请稍后重试"}
)
# 注册路由
app.include_router(auth.router)
@@ -315,11 +319,8 @@ async def list_services():
if __name__ == "__main__":
import sys
print("=" * 50, file=sys.stderr)
print("启动 Uvicorn 服务器...", file=sys.stderr)
print(f"Host: {settings.host}, Port: {settings.port}", file=sys.stderr)
print("=" * 50, file=sys.stderr)
sys.stderr.flush()
logger.info("启动 Uvicorn 服务器...")
logger.info(f"Host: {settings.host}, Port: {settings.port}")
uvicorn.run(
"main:app",