From c6f037a947e324b96dc7df7d4ede1757a1b08919 Mon Sep 17 00:00:00 2001 From: baby-2016 <2185823427@qq.com> Date: Sat, 28 Feb 2026 21:17:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(plugins):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=A4=A9=E6=B0=94=E5=9B=BE=E7=89=87=E6=B8=B2=E6=9F=93=E5=B0=BA?= =?UTF-8?q?=E5=AF=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/managers/image_manager.py | 4 ++-- plugins/weather.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/managers/image_manager.py b/core/managers/image_manager.py index 3d92944..cb557d9 100644 --- a/core/managers/image_manager.py +++ b/core/managers/image_manager.py @@ -112,11 +112,11 @@ class ImageManager(Singleton): logger.exception(f"渲染模板 {template_name} 失败: {e}") return None - async def render_template_to_base64(self, template_name: str, data: Dict[str, Any], output_name: str = "output.png", quality: int = 80, image_type: str = "png") -> Optional[str]: + async def render_template_to_base64(self, template_name: str, data: Dict[str, Any], output_name: str = "output.png", quality: int = 80, image_type: str = "png", width: int = 1920, height: int = 1080) -> Optional[str]: """ 渲染模板并返回 Base64 编码的图片字符串 """ - file_path = await self.render_template(template_name, data, output_name, quality, image_type) + file_path = await self.render_template(template_name, data, output_name, quality, image_type, width=width, height=height) if not file_path: return None diff --git a/plugins/weather.py b/plugins/weather.py index 950ed73..445b1f6 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -186,7 +186,7 @@ async def handle_weather(bot, event: MessageEvent, args: List[str]): try: # 渲染HTML模板为图片 base64_image = await image_manager.render_template_to_base64( - "weather.html", weather_info, output_name="weather.png" + "weather.html", weather_info, output_name="weather.png", width=1080 ) if base64_image: From 0baf07a716ed147373d38f7c7ef6f19e6fcca668 Mon Sep 17 00:00:00 2001 From: K2cr2O1 <2221577113@qq.com> Date: Sat, 28 Feb 2026 21:20:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(=E5=8F=8D=E5=90=91WS=E7=AE=A1=E7=90=86?= =?UTF-8?q?):=20=E5=AE=9E=E7=8E=B0=E5=A4=9A=E5=89=8D=E7=AB=AF=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8E=E5=AE=8C=E5=96=84=E6=B8=85=E7=90=86=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为每个前端创建独立的Bot实例,防止状态混乱 - 分离消息锁和时间戳存储,修复清理逻辑错误 - 完善客户端断开时的清理逻辑,包括负载计数和健康状态 - 添加文档说明多前端支持的功能和解决方案 --- MULTI_FRONTEND_SUPPORT.md | 175 ++++++++++++++++++++++++++++ core/managers/reverse_ws_manager.py | 32 ++++- 2 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 MULTI_FRONTEND_SUPPORT.md diff --git a/MULTI_FRONTEND_SUPPORT.md b/MULTI_FRONTEND_SUPPORT.md new file mode 100644 index 0000000..c999cd2 --- /dev/null +++ b/MULTI_FRONTEND_SUPPORT.md @@ -0,0 +1,175 @@ +# 多前端支持问题分析和解决方案 + +## 已实现的功能 + +### 1. 负载均衡 +- ✅ 自动选择负载最低的健康客户端 +- ✅ 健康检查(30秒内有活动) +- ✅ 负载计数(消息处理次数) +- ✅ API调用时自动切换客户端 + +### 2. 防重复发送 +- ✅ 事件ID检查(`id`/`post_id`/`time`) +- ✅ 消息锁机制(异步锁) +- ✅ 双重检查(锁内外各一次) +- ✅ 自动清理过期数据 + +### 3. 多前端支持 +- ✅ 每个前端独立的Bot实例 +- ✅ 客户端连接/断开管理 +- ✅ 完整的清理机制 + +## 已修复的问题 + +### 1. 清理不完整 +**问题**:客户端断开连接时没有清理负载计数和健康状态 + +**解决方案**: +```python +async def _disconnect_client(self, client_id: str) -> None: + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_self_ids: + del self.client_self_ids[client_id] + if client_id in self._client_load: + del self._client_load[client_id] + if client_id in self._client_health: + del self._client_health[client_id] + if client_id in self.bots: + del self.bots[client_id] +``` + +### 2. Bot实例共享 +**问题**:多个前端共享同一个Bot实例,可能导致状态混乱 + +**解决方案**:为每个前端创建独立的Bot实例 +```python +# Bot实例字典(每个前端独立的Bot实例) +self.bots: Dict[str, Bot] = {} + +# 为每个前端创建独立的Bot实例 +if client_id not in self.bots: + temp_ws = WS() + temp_ws.self_id = event.self_id if hasattr(event, 'self_id') else 0 + self.bots[client_id] = Bot(temp_ws) + +event.bot = self.bots[client_id] +``` + +### 3. 清理逻辑错误 +**问题**:清理过期数据时,将Lock对象当作时间戳来处理 + +**解决方案**:分离存储Lock对象和时间戳 +```python +# 分离存储 +self._message_locks: Dict[str, asyncio.Lock] = {} # 存储Lock对象 +self._message_lock_times: Dict[str, datetime] = {} # 存储Lock的创建时间 + +# 清理时使用时间戳字典 +expired_locks = [ + lock_key for lock_key, timestamp in self._message_lock_times.items() + if (current_time - timestamp).total_seconds() > self._lock_ttl +] +``` + +## 可能存在的问题 + +### 1. API响应混淆 +**问题描述**:当多个前端同时响应时,`_pending_requests`是全局共享的,可能导致响应匹配错误 + +**当前解决方案**:使用echo_id匹配响应,任何前端的响应都会被正确匹配 +```python +echo_id = data.get("echo") +if echo_id and echo_id in self._pending_requests: + future = self._pending_requests.pop(echo_id) + if not future.done(): + future.set_result(data) +``` + +**潜在风险**:如果多个前端同时响应同一个API请求,只有第一个响应会被处理 + +**建议优化**:可以考虑在API调用时指定客户端ID,确保响应来自正确的客户端 + +### 2. 负载均衡不准确 +**问题描述**:负载计数可能不准确,因为多个前端可能同时处理相同的消息 + +**当前解决方案**:负载计数只是参考,系统会优先选择健康的客户端 + +**建议优化**:可以考虑使用更复杂的负载均衡策略,如加权轮询 + +### 3. Bot实例状态 +**问题描述**:每个前端有独立的Bot实例,可能导致状态不一致 + +**当前解决方案**:Bot实例是无状态的,只依赖于WS实例和self_id + +**建议优化**:如果需要共享状态,可以使用Redis等外部存储 + +## 最佳实践 + +### 1. 部署建议 +- 部署2-3个前端实例进行负载均衡 +- 确保前端实例之间的网络连接稳定 +- 定期检查前端连接状态 + +### 2. 监控建议 +- 关注重复事件日志,排查网络问题 +- 监控客户端健康状态 +- 监控API调用成功率 + +### 3. 调试建议 +- 使用`get_healthy_clients()`查看健康客户端 +- 使用`get_client_with_least_load()`查看负载最低的客户端 +- 查看日志了解API调用情况 + +## 性能优化建议 + +### 1. API响应过滤 +可以在API调用时记录客户端ID,确保响应来自正确的客户端: +```python +# 记录API请求的客户端ID +self._api_requests[echo_id] = { + 'client_id': client_id, + 'timestamp': datetime.now() +} + +# 处理响应时验证客户端ID +if echo_id in self._api_requests: + request_info = self._api_requests[echo_id] + if request_info['client_id'] == client_id: + # 处理响应 +``` + +### 2. 负载均衡策略 +可以考虑使用更复杂的负载均衡策略: +- 加权轮询:根据客户端性能分配权重 +- 最少连接:选择连接数最少的客户端 +- 响应时间:选择响应时间最短的客户端 + +### 3. 缓存优化 +可以考虑使用Redis缓存Bot实例的状态: +```python +# 缓存Bot实例的状态 +await redis_manager.set(f"neobot:bot:{client_id}:status", status_data, ex=300) +``` + +## 故障排查 + +### 问题:消息重复处理 +**原因**:网络延迟导致前端重复发送 + +**解决**:系统已自动处理,检查事件ID是否正确设置 + +### 问题:API调用超时 +**原因**:选择的客户端不健康或网络问题 + +**解决**:系统会自动切换到其他健康客户端 + +### 问题:所有客户端都不健康 +**原因**:前端断开连接或网络问题 + +**解决**:检查前端连接状态和网络连接 + +### 问题:Bot实例初始化失败 +**原因**:WS实例缺失或self_id未设置 + +**解决**:确保在事件处理时正确设置self_id diff --git a/core/managers/reverse_ws_manager.py b/core/managers/reverse_ws_manager.py index d506573..a0d270c 100644 --- a/core/managers/reverse_ws_manager.py +++ b/core/managers/reverse_ws_manager.py @@ -20,6 +20,7 @@ from ..utils.error_codes import ErrorCode, create_error_response from .command_manager import matcher from models.events.factory import EventFactory from .redis_manager import redis_manager +from ..bot import Bot class ReverseWSManager: @@ -48,11 +49,15 @@ class ReverseWSManager: self._processed_events: Dict[str, datetime] = {} # 已处理的事件ID和时间 self._event_ttl = 60 # 事件ID保留时间(秒) self._message_locks: Dict[str, asyncio.Lock] = {} # 消息处理锁 + self._message_lock_times: Dict[str, datetime] = {} # 消息锁创建时间 self._lock_ttl = 300 # 锁保留时间(秒) # 启动清理任务 self._cleanup_task = None + # Bot实例字典(每个前端独立的Bot实例) + self.bots: Dict[str, Bot] = {} + async def start(self, host: str = "0.0.0.0", port: int = 3002) -> None: """ 启动反向 WebSocket 服务端。 @@ -162,11 +167,14 @@ class ReverseWSManager: # 清理过期的消息锁 expired_locks = [ - lock_key for lock_key, timestamp in self._message_locks.items() + lock_key for lock_key, timestamp in self._message_lock_times.items() if (current_time - timestamp).total_seconds() > self._lock_ttl ] for lock_key in expired_locks: - del self._message_locks[lock_key] + if lock_key in self._message_locks: + del self._message_locks[lock_key] + if lock_key in self._message_lock_times: + del self._message_lock_times[lock_key] except asyncio.CancelledError: break @@ -184,6 +192,14 @@ class ReverseWSManager: del self.clients[client_id] if client_id in self.client_self_ids: del self.client_self_ids[client_id] + if client_id in self._client_load: + del self._client_load[client_id] + if client_id in self._client_health: + del self._client_health[client_id] + if client_id in self.bots: + del self.bots[client_id] + + self.logger.info(f"客户端已断开并清理: {client_id}") async def _on_event(self, client_id: str, event_data: Dict[str, Any]) -> None: """ @@ -199,7 +215,16 @@ class ReverseWSManager: if hasattr(event, 'self_id'): self.client_self_ids[client_id] = event.self_id - event.bot = None + # 为事件注入Bot实例 + from ..ws import WS + + # 为每个前端创建独立的Bot实例 + if client_id not in self.bots: + temp_ws = WS() + temp_ws.self_id = event.self_id if hasattr(event, 'self_id') else 0 + self.bots[client_id] = Bot(temp_ws) + + event.bot = self.bots[client_id] # 记录客户端健康状态 self._client_health[client_id] = datetime.now() @@ -392,6 +417,7 @@ class ReverseWSManager: """ if key not in self._message_locks: self._message_locks[key] = asyncio.Lock() + self._message_lock_times[key] = datetime.now() return self._message_locks[key] def _update_client_load(self, client_id: str) -> None: