From cebcd6925a47e754dda7d027a1e22672bf3c23a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9B=9E=E5=BD=92=E5=A4=A9=E7=A9=BA?= Date: Wed, 25 Jun 2025 11:46:49 +0800 Subject: [PATCH] =?UTF-8?q?[fix]=20(discord=5Fplatform=5Fadapter)=20?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E4=BA=86=20=E2=80=9CDiscord=20=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=97=A0=E6=B3=95=E4=BC=98=E9=9B=85=E9=87=8D=E8=BD=BD?= =?UTF-8?q?=E2=80=9D=20=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 问题现象(AI总结) - 在通过 Web 面板或配置变更热重载 Discord 平台时,适配器的 terminate() 方法会被调用,但经常出现“卡死”或长时间无响应,导致 Discord 平台无法优雅重载。 - 日志显示停留在“正在清理已注册的斜杠指令...”等步骤,甚至出现超时或异常。 #### 2. 原因分析 - 适配器的 terminate() 方法中,涉及多个异步操作(如取消 polling 任务、清理斜杠指令、关闭客户端)。 - 某些 await 操作(如 await self.client.sync_commands() 或 await self.client.close())在网络异常、事件循环被取消等情况下,可能会阻塞或抛出 CancelledError,导致整个重载流程卡住。 - 之前的实现没有对这些 await 操作加超时保护,也没有分步日志,难以定位具体卡点。 #### 3. 修复措施 - 分步日志:在 terminate() 的每个关键步骤前后都加了详细日志,便于定位卡点。 - 超时保护:对所有关键 await 操作(如 polling 任务取消、指令清理、客户端关闭)都加了 asyncio.wait_for(..., timeout=10),防止无限阻塞。 - 健壮性提升:先 cancel polling 任务,再清理指令,最后关闭客户端。每一步都捕获异常并输出日志,保证即使某一步失败也能继续后续清理。 - 避免重复终止:移除了 run() 方法中的 finally: await self.terminate(),只允许外部统一调度,防止重复调用导致资源冲突或日志重复。 #### 4. 修复效果 - 现在 Discord 平台适配器在热重载或终止时,能优雅地依次完成所有清理步骤,不会因某一步阻塞导致整个流程卡死。 --- .../discord/discord_platform_adapter.py | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index d605ee621..2ba833d65 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -46,6 +46,8 @@ class DiscordPlatformAdapter(Platform): self.enable_command_register = self.config.get("discord_command_register", True) self.guild_id = self.config.get("discord_guild_id_for_debug", None) self.activity_name = self.config.get("discord_activity_name", None) + self.shutdown_event = asyncio.Event() + self._polling_task = None @override async def send_by_session( @@ -106,7 +108,6 @@ class DiscordPlatformAdapter(Platform): @override async def run(self): """主要运行逻辑""" - # 初始化回调函数 async def on_received(message_data): logger.debug(f"[Discord] 收到消息: {message_data}") @@ -137,13 +138,15 @@ class DiscordPlatformAdapter(Platform): self.client.on_ready_once_callback = callback try: - await self.client.start_polling() + self._polling_task = asyncio.create_task(self.client.start_polling()) + await self.shutdown_event.wait() except discord.errors.LoginFailure: logger.error("[Discord] 登录失败。请检查你的 Bot Token 是否正确。") except discord.errors.ConnectionClosed: logger.warning("[Discord] 与 Discord 的连接已关闭。") except Exception as e: logger.error(f"[Discord] 适配器运行时发生意外错误: {e}", exc_info=True) + # finally: 不再自动调用 self.terminate(),只允许外部统一调度 def _get_message_type( self, channel: Messageable, guild_id: int | None = None @@ -166,7 +169,6 @@ class DiscordPlatformAdapter(Platform): content = message.content - # 如果机器人被@,移除@部分 # 剥离 User Mention (<@id>, <@!id>) if self.client and self.client.user: @@ -262,27 +264,39 @@ class DiscordPlatformAdapter(Platform): self.commit_event(message_event) - @override async def terminate(self): """终止适配器""" - logger.info("[Discord] 正在终止适配器...") - + logger.info("[Discord] 正在终止适配器... (step 1: cancel polling task)") + self.shutdown_event.set() + # 优先 cancel polling_task + if self._polling_task: + self._polling_task.cancel() + try: + await asyncio.wait_for(self._polling_task, timeout=10) + except asyncio.CancelledError: + logger.info("[Discord] polling_task 已取消。") + except Exception as e: + logger.warning(f"[Discord] polling_task 取消异常: {e}") + logger.info("[Discord] 正在清理已注册的斜杠指令... (step 2)") # 清理指令 if self.enable_command_register and self.client: - logger.info("[Discord] 正在清理已注册的斜杠指令...") try: - # 传入空的列表来清除所有全局指令 - # 如果指定了 guild_id,则只清除该服务器的指令 - await self.client.sync_commands( - commands=[], guild_ids=[self.guild_id] if self.guild_id else None + await asyncio.wait_for( + self.client.sync_commands( + commands=[], guild_ids=[self.guild_id] if self.guild_id else None + ), + timeout=10 ) logger.info("[Discord] 指令清理完成。") except Exception as e: logger.error(f"[Discord] 清理指令时发生错误: {e}", exc_info=True) - + logger.info("[Discord] 正在关闭 Discord 客户端... (step 3)") if self.client and hasattr(self.client, "close"): - await self.client.close() + try: + await asyncio.wait_for(self.client.close(), timeout=10) + except Exception as e: + logger.warning(f"[Discord] 客户端关闭异常: {e}") logger.info("[Discord] 适配器已终止。") def register_handler(self, handler_info):