zlwu před 11 měsíci
rodič
revize
be4d27a254

+ 5 - 0
backend/ENV_CONFIG.md

@@ -4,4 +4,9 @@
 | `MYSQL_READ_TIMEOUT` | `10` | ⚠️ 当前未使用:`aiomysql` 不支持此参数,已从连接池配置中移除。保留配置项以便未来兼容。 |
 | `MYSQL_READ_TIMEOUT` | `10` | ⚠️ 当前未使用:`aiomysql` 不支持此参数,已从连接池配置中移除。保留配置项以便未来兼容。 |
 | `MYSQL_POOL_RECYCLE` | `7200` | MySQL 连接池自动回收间隔(秒),保持长连接,小于服务器 `wait_timeout` 可避免闲置断连。 |
 | `MYSQL_POOL_RECYCLE` | `7200` | MySQL 连接池自动回收间隔(秒),保持长连接,小于服务器 `wait_timeout` 可避免闲置断连。 |
 | `MYSQL_HEALTHCHECK_INTERVAL` | `600` | MySQL 连接池健康检查间隔(秒),定期 `PING` 保持连接活跃,仅在连接断开时重建。 |
 | `MYSQL_HEALTHCHECK_INTERVAL` | `600` | MySQL 连接池健康检查间隔(秒),定期 `PING` 保持连接活跃,仅在连接断开时重建。 |
+| `REDIS_HEALTHCHECK_INTERVAL` | `300` | Redis 连接健康检查间隔(秒),定期 `PING` 保持连接活跃,自动检测并重建断开的连接。 |
+| `HTTPX_MAX_KEEPALIVE_CONNECTIONS` | `20` | HTTP 客户端 keepalive 连接池大小,用于 DashScope API 连接复用。 |
+| `HTTPX_MAX_CONNECTIONS` | `50` | HTTP 客户端最大连接数,支持并发请求。 |
+| `HTTPX_KEEPALIVE_EXPIRY` | `60.0` | HTTP 客户端 keepalive 连接过期时间(秒),保持连接更长时间以减少重建开销。 |
+| `LLM_HEALTHCHECK_INTERVAL` | `300` | LLM 连接健康检查间隔(秒),长时间未使用时自动检查连接状态,失效时重建。 |
 
 

+ 76 - 3
backend/ai_service.py

@@ -60,9 +60,9 @@ class AIService:
             trust_env=False,  # 不信任环境变量中的代理设置
             trust_env=False,  # 不信任环境变量中的代理设置
             # 连接池优化:复用 TCP 连接,但每次 API 调用都是独立请求
             # 连接池优化:复用 TCP 连接,但每次 API 调用都是独立请求
             limits=httpx.Limits(
             limits=httpx.Limits(
-                max_keepalive_connections=20,  # 增加keepalive连接数(默认10)
-                max_connections=50,  # 增加总连接数(默认20),支持并发请求
-                keepalive_expiry=30.0,  # keepalive过期时间30秒
+                max_keepalive_connections=settings.HTTPX_MAX_KEEPALIVE_CONNECTIONS,  # 从配置读取
+                max_connections=settings.HTTPX_MAX_CONNECTIONS,  # 从配置读取
+                keepalive_expiry=settings.HTTPX_KEEPALIVE_EXPIRY,  # 从配置读取
             ),
             ),
             # HTTP/2支持 - 多路复用,减少连接开销,支持并发请求
             # HTTP/2支持 - 多路复用,减少连接开销,支持并发请求
             http2=True,
             http2=True,
@@ -74,6 +74,10 @@ class AIService:
             http_client=http_client,  # 使用自定义 HTTP 客户端
             http_client=http_client,  # 使用自定义 HTTP 客户端
         )
         )
         
         
+        # 🔥 连接健康检查标记
+        self._last_llm_healthcheck: float = 0.0
+        self._llm_healthcheck_interval = settings.LLM_HEALTHCHECK_INTERVAL  # 从配置读取
+        
         # 🔥 TTS优化:预热TTS连接(后台执行,不阻塞启动)
         # 🔥 TTS优化:预热TTS连接(后台执行,不阻塞启动)
         import threading
         import threading
         
         
@@ -149,6 +153,9 @@ class AIService:
             import time
             import time
             llm_api_start = time.time()
             llm_api_start = time.time()
             
             
+            # 🔥 连接健康检查(长时间未使用时)
+            await self._ensure_llm_connection()
+            
             try:
             try:
                 response = self.openai_client.chat.completions.create(
                 response = self.openai_client.chat.completions.create(
                     model=settings.llm_model,
                     model=settings.llm_model,
@@ -286,6 +293,9 @@ class AIService:
             # 构建System Prompt
             # 构建System Prompt
             system_prompt = self._build_system_prompt()
             system_prompt = self._build_system_prompt()
             
             
+            # 🔥 连接健康检查(长时间未使用时)
+            await self._ensure_llm_connection()
+            
             # 调用LLM
             # 调用LLM
             response = self.openai_client.chat.completions.create(
             response = self.openai_client.chat.completions.create(
                 model=settings.llm_model,
                 model=settings.llm_model,
@@ -587,6 +597,66 @@ class AIService:
             )
             )
             raise
             raise
     
     
+    async def _ensure_llm_connection(self):
+        """确保LLM连接可用(长时间未使用时进行健康检查)"""
+        import time
+        now = time.time()
+        
+        # 如果距离上次检查超过间隔,执行健康检查
+        if now - self._last_llm_healthcheck < self._llm_healthcheck_interval:
+            return
+        
+        try:
+            # 🔥 执行简单的健康检查:发送一个极短的测试请求
+            # 注意:使用asyncio.to_thread包装同步调用,避免阻塞事件循环
+            def health_check():
+                return self.openai_client.chat.completions.create(
+                    model=settings.llm_model,
+                    messages=[{"role": "user", "content": "Hi"}],
+                    max_tokens=1,
+                    timeout=5.0,  # 5秒超时
+                )
+            
+            test_response = await asyncio.to_thread(health_check)
+            self._last_llm_healthcheck = now
+            logger.debug("✅ LLM连接健康检查成功")
+        except Exception as e:
+            # 健康检查失败,重建HTTP客户端
+            logger.warning(f"⚠️ LLM连接健康检查失败,重建连接: {e}")
+            self._recreate_http_client()
+            self._last_llm_healthcheck = now
+    
+    def _recreate_http_client(self):
+        """重建HTTP客户端(连接失效时)"""
+        import httpx
+        try:
+            # 关闭旧客户端
+            if hasattr(self, 'openai_client') and self.openai_client:
+                if hasattr(self.openai_client, '_client') and self.openai_client._client:
+                    self.openai_client._client.close()
+        except:
+            pass
+        
+        # 创建新客户端(使用配置值)
+        http_client = httpx.Client(
+            proxy=None,
+            timeout=60.0,
+            trust_env=False,
+            limits=httpx.Limits(
+                max_keepalive_connections=settings.HTTPX_MAX_KEEPALIVE_CONNECTIONS,
+                max_connections=settings.HTTPX_MAX_CONNECTIONS,
+                keepalive_expiry=settings.HTTPX_KEEPALIVE_EXPIRY,
+            ),
+            http2=True,
+        )
+        
+        self.openai_client = OpenAI(
+            api_key=settings.active_dashscope_api_key,
+            base_url=get_dashscope_http_base_url(),
+            http_client=http_client,
+        )
+        logger.info("✅ HTTP客户端已重建")
+    
     def _get_voice_for_emotion(self, emotion: EmotionType) -> str:
     def _get_voice_for_emotion(self, emotion: EmotionType) -> str:
         """根据情感选择音色"""
         """根据情感选择音色"""
         # 所有情感统一使用配置文件中的音色
         # 所有情感统一使用配置文件中的音色
@@ -745,6 +815,9 @@ Critical:
             总结文本
             总结文本
         """
         """
         try:
         try:
+            # 🔥 连接健康检查(长时间未使用时)
+            await self._ensure_llm_connection()
+            
             response = self.openai_client.chat.completions.create(
             response = self.openai_client.chat.completions.create(
                 model=settings.llm_model,
                 model=settings.llm_model,
                 messages=[
                 messages=[

+ 9 - 0
backend/config.py

@@ -110,6 +110,15 @@ class Settings(BaseSettings):
     REDIS_SSL: bool = os.getenv("REDIS_SSL", "false").lower() == "true"
     REDIS_SSL: bool = os.getenv("REDIS_SSL", "false").lower() == "true"
     REDIS_SESSION_TTL: int = int(os.getenv("REDIS_SESSION_TTL", "86400"))
     REDIS_SESSION_TTL: int = int(os.getenv("REDIS_SESSION_TTL", "86400"))
     REDIS_MAX_CONNECTIONS: int = int(os.getenv("REDIS_MAX_CONNECTIONS", "50"))
     REDIS_MAX_CONNECTIONS: int = int(os.getenv("REDIS_MAX_CONNECTIONS", "50"))
+    REDIS_HEALTHCHECK_INTERVAL: int = int(os.getenv("REDIS_HEALTHCHECK_INTERVAL", "300"))  # 🔥 Redis健康检查间隔(秒),5分钟检查一次
+    
+    # HTTP客户端配置(用于DashScope API)
+    HTTPX_MAX_KEEPALIVE_CONNECTIONS: int = int(os.getenv("HTTPX_MAX_KEEPALIVE_CONNECTIONS", "20"))  # 🔥 keepalive连接数
+    HTTPX_MAX_CONNECTIONS: int = int(os.getenv("HTTPX_MAX_CONNECTIONS", "50"))  # 🔥 最大连接数
+    HTTPX_KEEPALIVE_EXPIRY: float = float(os.getenv("HTTPX_KEEPALIVE_EXPIRY", "60.0"))  # 🔥 keepalive过期时间(秒)
+    
+    # LLM连接健康检查配置
+    LLM_HEALTHCHECK_INTERVAL: int = int(os.getenv("LLM_HEALTHCHECK_INTERVAL", "300"))  # 🔥 LLM连接健康检查间隔(秒),5分钟检查一次
     
     
     # CORS配置
     # CORS配置
     # 支持localhost和任意IP地址访问(端口3000为Vite默认HTTPS端口)
     # 支持localhost和任意IP地址访问(端口3000为Vite默认HTTPS端口)

+ 68 - 2
backend/database.py

@@ -33,12 +33,17 @@ class DatabaseManager:
         self._mysql_lock = asyncio.Lock()
         self._mysql_lock = asyncio.Lock()
         self._redis_lock = asyncio.Lock()
         self._redis_lock = asyncio.Lock()
         self._last_mysql_healthcheck: float = 0.0
         self._last_mysql_healthcheck: float = 0.0
+        self._last_redis_healthcheck: float = 0.0
+        self._healthcheck_task: Optional[asyncio.Task] = None
+        self._healthcheck_running = False
     
     
     async def connect(self):
     async def connect(self):
         """初始化数据库连接"""
         """初始化数据库连接"""
         try:
         try:
             await self._create_mysql_pool()
             await self._create_mysql_pool()
             await self._create_redis_client()
             await self._create_redis_client()
+            # 🔥 启动后台健康检查任务
+            self._start_healthcheck_task()
         except Exception as e:
         except Exception as e:
             logger.error(f"❌ 数据库连接失败: {e}")
             logger.error(f"❌ 数据库连接失败: {e}")
             raise
             raise
@@ -101,6 +106,9 @@ class DatabaseManager:
     async def disconnect(self):
     async def disconnect(self):
         """关闭数据库连接"""
         """关闭数据库连接"""
         try:
         try:
+            # 🔥 停止健康检查任务
+            self._stop_healthcheck_task()
+            
             if self.mysql_pool:
             if self.mysql_pool:
                 self.mysql_pool.close()
                 self.mysql_pool.close()
                 await self.mysql_pool.wait_closed()
                 await self.mysql_pool.wait_closed()
@@ -139,6 +147,63 @@ class DatabaseManager:
             await self._create_mysql_pool()
             await self._create_mysql_pool()
             self._last_mysql_healthcheck = loop.time()
             self._last_mysql_healthcheck = loop.time()
     
     
+    async def _ensure_redis_client(self):
+        """确保Redis连接可用(定期健康检查,自动重连)"""
+        if not self.redis_client:
+            await self._create_redis_client()
+            return
+        
+        loop = asyncio.get_running_loop()
+        now = loop.time()
+        # 🔥 Redis健康检查间隔(从配置读取)
+        if now - self._last_redis_healthcheck < settings.REDIS_HEALTHCHECK_INTERVAL:
+            return
+        
+        # 🔥 执行健康检查
+        try:
+            await self.redis_client.ping()
+            self._last_redis_healthcheck = now
+            # 健康检查成功,连接正常
+        except Exception as exc:
+            # 🔥 健康检查失败,重建连接
+            logger.warning(f"⚠️ Redis健康检查失败,连接可能已断开,准备重建: {exc}")
+            await self._create_redis_client()
+            self._last_redis_healthcheck = loop.time()
+    
+    def _start_healthcheck_task(self):
+        """启动后台健康检查任务"""
+        if self._healthcheck_running:
+            return
+        
+        self._healthcheck_running = True
+        
+        async def healthcheck_loop():
+            """定期健康检查循环"""
+            while self._healthcheck_running:
+                try:
+                    await asyncio.sleep(300)  # 每5分钟检查一次
+                    if not self._healthcheck_running:
+                        break
+                    
+                    # 检查MySQL
+                    await self._ensure_mysql_pool()
+                    
+                    # 检查Redis
+                    await self._ensure_redis_client()
+                    
+                except Exception as e:
+                    logger.error(f"❌ 健康检查任务异常: {e}")
+        
+        self._healthcheck_task = asyncio.create_task(healthcheck_loop())
+        logger.info("✅ 数据库健康检查任务已启动")
+    
+    def _stop_healthcheck_task(self):
+        """停止后台健康检查任务"""
+        self._healthcheck_running = False
+        if self._healthcheck_task and not self._healthcheck_task.done():
+            self._healthcheck_task.cancel()
+            logger.info("✅ 数据库健康检查任务已停止")
+    
     async def _run_with_retry(self, operation: Callable[[], Awaitable], description: str):
     async def _run_with_retry(self, operation: Callable[[], Awaitable], description: str):
         """执行MySQL操作并在连接异常时重试一次"""
         """执行MySQL操作并在连接异常时重试一次"""
         for attempt in range(2):
         for attempt in range(2):
@@ -196,8 +261,9 @@ class DatabaseManager:
         
         
         return await self._run_with_retry(_operation, "执行更新")
         return await self._run_with_retry(_operation, "执行更新")
     
     
-    def get_redis(self) -> aioredis.Redis:
-        """获取Redis客户端"""
+    async def get_redis(self) -> aioredis.Redis:
+        """获取Redis客户端(自动健康检查)"""
+        await self._ensure_redis_client()
         if not self.redis_client:
         if not self.redis_client:
             raise RuntimeError("Redis客户端未初始化")
             raise RuntimeError("Redis客户端未初始化")
         return self.redis_client
         return self.redis_client

+ 7 - 21
backend/main.py

@@ -25,40 +25,26 @@ logging.basicConfig(
 @asynccontextmanager
 @asynccontextmanager
 async def lifespan(app: FastAPI):
 async def lifespan(app: FastAPI):
     """应用生命周期管理"""
     """应用生命周期管理"""
-    # 🔥 启动时初始化Redis
+    # 启动时初始化数据库(包含MySQL和Redis,自动启动健康检查任务)
     try:
     try:
-        redis_client = redis.Redis(
-            host=settings.REDIS_HOST,
-            port=settings.REDIS_PORT,
-            db=settings.REDIS_DB,
-            password=settings.REDIS_PASSWORD if settings.REDIS_PASSWORD else None,
-            decode_responses=False,  # 保持二进制,手动decode
-            max_connections=settings.REDIS_MAX_CONNECTIONS,
-        )
+        await init_database()
         
         
-        # 测试连接
-        await redis_client.ping()
+        # 获取Redis客户端(已配置健康检查)
+        from database import db_manager
+        redis_client = await db_manager.get_redis()
         
         
         # 创建Redis缓存服务
         # 创建Redis缓存服务
         app.state.redis_cache = RedisCacheService(redis_client)
         app.state.redis_cache = RedisCacheService(redis_client)
         app.state.redis_client = redis_client
         app.state.redis_client = redis_client
         
         
     except Exception as e:
     except Exception as e:
+        logger.error(f"❌ 数据库初始化失败: {e}")
         app.state.redis_cache = None
         app.state.redis_cache = None
         app.state.redis_client = None
         app.state.redis_client = None
     
     
-    # 启动时初始化数据库
-    try:
-        await init_database()
-    except Exception as e:
-        pass
-    
     yield
     yield
     
     
-    # 关闭时清理资源
-    if hasattr(app.state, 'redis_client') and app.state.redis_client:
-        await app.state.redis_client.close()
-    
+    # 关闭时清理资源(database.py会自动处理)
     await close_database()
     await close_database()
 
 
 
 

+ 12 - 3
backend/services/context_service.py

@@ -21,12 +21,18 @@ class ContextService:
         self.db = db_manager
         self.db = db_manager
         self.redis = None
         self.redis = None
         if db_manager:
         if db_manager:
-            self.redis = db_manager.get_redis()
+            # 🔥 延迟获取Redis(在首次使用时)
+            pass
 
 
-    def set_db_manager(self, db_manager):
+    async def set_db_manager(self, db_manager):
         """设置数据库管理器(用于延迟初始化)"""
         """设置数据库管理器(用于延迟初始化)"""
         self.db = db_manager
         self.db = db_manager
-        self.redis = db_manager.get_redis()
+        self.redis = await db_manager.get_redis()
+    
+    async def _ensure_redis(self):
+        """确保Redis连接可用"""
+        if not self.redis and self.db:
+            self.redis = await self.db.get_redis()
     
     
     async def get_context_for_round(
     async def get_context_for_round(
         self, 
         self, 
@@ -55,6 +61,9 @@ class ContextService:
         """
         """
         
         
         try:
         try:
+            # 🔥 确保Redis连接可用
+            await self._ensure_redis()
+            
             if current_round <= 15:
             if current_round <= 15:
                 # 前15轮:直接返回所有历史消息
                 # 前15轮:直接返回所有历史消息
                 messages = await self._get_messages_from_cache_or_db(
                 messages = await self._get_messages_from_cache_or_db(

+ 12 - 3
backend/services/message_service.py

@@ -24,12 +24,18 @@ class MessageService:
         self.db = db_manager
         self.db = db_manager
         self.redis = None
         self.redis = None
         if db_manager:
         if db_manager:
-            self.redis = db_manager.get_redis()
+            # 🔥 延迟获取Redis(在首次使用时)
+            pass
 
 
-    def set_db_manager(self, db_manager):
+    async def set_db_manager(self, db_manager):
         """设置数据库管理器(用于延迟初始化)"""
         """设置数据库管理器(用于延迟初始化)"""
         self.db = db_manager
         self.db = db_manager
-        self.redis = db_manager.get_redis()
+        self.redis = await db_manager.get_redis()
+    
+    async def _ensure_redis(self):
+        """确保Redis连接可用"""
+        if not self.redis and self.db:
+            self.redis = await self.db.get_redis()
     
     
     async def save_message(
     async def save_message(
         self,
         self,
@@ -54,6 +60,9 @@ class MessageService:
         """
         """
         
         
         try:
         try:
+            # 🔥 确保Redis连接可用
+            await self._ensure_redis()
+            
             logger.info("🚨 [TEST] save_message 方法被调用了!")
             logger.info("🚨 [TEST] save_message 方法被调用了!")
             logger.info(f"📥 [保存消息] 开始: user={user_id}, role={role}, content_length={len(content)}")
             logger.info(f"📥 [保存消息] 开始: user={user_id}, role={role}, content_length={len(content)}")
             
             

+ 9 - 1
backend/services/summary_service.py

@@ -19,7 +19,12 @@ class SummaryService:
             db_manager: 数据库管理器实例
             db_manager: 数据库管理器实例
         """
         """
         self.db = db_manager
         self.db = db_manager
-        self.redis = db_manager.get_redis()
+        self.redis = None  # 🔥 延迟获取Redis
+    
+    async def _ensure_redis(self):
+        """确保Redis连接可用"""
+        if not self.redis and self.db:
+            self.redis = await self.db.get_redis()
     
     
     async def generate_summary(
     async def generate_summary(
         self,
         self,
@@ -41,6 +46,9 @@ class SummaryService:
         """
         """
         
         
         try:
         try:
+            # 🔥 确保Redis连接可用
+            await self._ensure_redis()
+            
             logger.info(f"🔄 开始生成总结: user={user_id}, rounds={start_round}-{end_round}")
             logger.info(f"🔄 开始生成总结: user={user_id}, rounds={start_round}-{end_round}")
             
             
             # 0. 参数验证
             # 0. 参数验证

+ 2 - 2
backend/websocket_handler.py

@@ -73,8 +73,8 @@ class WebSocketHandler:
             # 使用延迟初始化的全局实例
             # 使用延迟初始化的全局实例
             from services.message_service import message_service
             from services.message_service import message_service
             from services.context_service import context_service
             from services.context_service import context_service
-            message_service.set_db_manager(db_manager)
-            context_service.set_db_manager(db_manager)
+            await message_service.set_db_manager(db_manager)
+            await context_service.set_db_manager(db_manager)
             self.message_service = message_service
             self.message_service = message_service
             self.context_service = context_service
             self.context_service = context_service
         except Exception as e:
         except Exception as e:

+ 1 - 0
frontend/SSL证书配置.md

@@ -117,5 +117,6 @@ ssl/
 
 
 
 
 
 
+