From 6c3a702f87434c11de6173faa3c7677814d5fa34 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Fri, 3 Jul 2026 17:03:57 +0800 Subject: [PATCH 1/8] =?UTF-8?q?Discord=20=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 所有命令参数统一显示为 params → 用 inspect.signature() 按函数签名生成独立 Option 2. 命令描述用插件 desc → 改用 handler.__doc__(docstring) 3. star_map 无异常保护 → 加 try/except KeyError 4. 重复 import inspect → 移除 5. Option description 优化 → "请输入 {name}" --- .../discord/discord_platform_adapter.py | 126 +++++++++--------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 2a690c597b..927a939750 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -1,4 +1,5 @@ import asyncio +import inspect import re import sys from typing import Any, cast @@ -9,7 +10,7 @@ from astrbot import logger from astrbot.api.event import MessageChain -from astrbot.api.message_components import File, Image, Plain, Record +from astrbot.api.message_components import File, Image, Plain from astrbot.api.platform import ( AstrBotMessage, MessageMember, @@ -23,7 +24,6 @@ from astrbot.core.star.filter.command_group import CommandGroupFilter from astrbot.core.star.star import star_map from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry -from astrbot.core.utils.media_utils import MediaResolver from .client import DiscordBotClient from .discord_platform_event import DiscordPlatformEvent @@ -100,7 +100,14 @@ async def send_by_session( message_obj.session_id = session.session_id message_obj.message = message_chain.chain - temp_event = self.create_event(message_obj) + # 创建临时事件对象来发送消息 + temp_event = DiscordPlatformEvent( + message_str=message_chain.get_plain_text(), + message_obj=message_obj, + platform_meta=self.meta(), + session_id=session.session_id, + client=self.client, + ) await temp_event.send(message_chain) await super().send_by_session(session, message_chain) @@ -242,12 +249,6 @@ def _convert_message_to_abm(self, data: dict) -> AstrBotMessage: message_chain.append( Image(file=attachment.url, filename=attachment.filename), ) - elif attachment.content_type and attachment.content_type.startswith( - "audio/", - ): - message_chain.append( - Record(file=attachment.url, url=attachment.url), - ) else: message_chain.append( File(name=attachment.filename, url=attachment.url), @@ -262,34 +263,11 @@ def _convert_message_to_abm(self, data: dict) -> AstrBotMessage: async def convert_message(self, data: dict) -> AstrBotMessage: """将平台消息转换成 AstrBotMessage""" # 由于 on_interaction 已被禁用,我们只处理普通消息 - abm = self._convert_message_to_abm(data) - for component in abm.message: - if isinstance(component, Record): - audio_ref = component.url or component.file - if audio_ref: - path_wav = await MediaResolver( - audio_ref, - media_type="audio", - default_suffix=".wav", - ).to_path(target_format="wav") - component.file = path_wav - component.url = path_wav - component.path = path_wav - return abm + return self._convert_message_to_abm(data) - def create_event( - self, message: AstrBotMessage, followup_webhook=None - ) -> DiscordPlatformEvent: - """Creates a Discord message event. - - Args: - message: AstrBot message object to wrap. - followup_webhook: Optional slash-command follow-up webhook. - - Returns: - Created Discord message event. - """ - return DiscordPlatformEvent( + async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None: + """处理消息""" + message_event = DiscordPlatformEvent( message_str=message.message_str, message_obj=message, platform_meta=self.meta(), @@ -298,10 +276,6 @@ def create_event( interaction_followup_webhook=followup_webhook, ) - async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None: - """处理消息""" - message_event = self.create_event(message, followup_webhook) - if self.client.user is None: logger.error( "[Discord] Client is not ready (self.client.user is None); message handling skipped" @@ -410,7 +384,10 @@ async def _collect_and_register_commands(self) -> None: registered_commands = [] for handler_md in star_handlers_registry: - if not star_map[handler_md.handler_module_path].activated: + try: + if not star_map[handler_md.handler_module_path].activated: + continue + except KeyError: continue if not handler_md.enabled: continue @@ -421,25 +398,19 @@ async def _collect_and_register_commands(self) -> None: cmd_name, description, cmd_filter_instance = cmd_info - # 创建动态回调 - callback = self._create_dynamic_callback(cmd_name) + # 从 handler 函数签名提取参数生成 Discord Option + options = self._build_slash_options(handler_md.handler) + param_names = [o.name for o in options] - # 创建一个通用的参数选项来接收所有文本输入 - options = [ - discord.Option( - name="params", - description="指令的所有参数", - type=discord.SlashCommandOptionType.string, - required=False, - ), - ] + # 创建动态回调 + callback = self._create_dynamic_callback(cmd_name, param_names) # 创建SlashCommand slash_command = discord.SlashCommand( name=cmd_name, description=description, func=callback, - options=options, + options=options if options else None, guild_ids=[self.guild_id] if self.guild_id else None, ) self.client.add_application_command(slash_command) @@ -471,11 +442,37 @@ async def _collect_and_register_commands(self) -> None: def _is_daily_command_quota_error(error: discord.HTTPException) -> bool: return getattr(error, "code", None) == 30034 - def _create_dynamic_callback(self, cmd_name: str): + @staticmethod + def _build_slash_options(handler) -> list: + """从 handler 函数签名提取参数,映射到 Discord SlashCommandOptionType""" + options = [] + try: + sig = inspect.signature(handler) + for name, param in sig.parameters.items(): + if name in ("self", "event"): + continue + opt_type = discord.SlashCommandOptionType.string + if param.annotation is int: + opt_type = discord.SlashCommandOptionType.integer + required = param.default is inspect.Parameter.empty + options.append( + discord.Option( + name=name, + description=f"请输入 {name}", + type=opt_type, + required=required, + ) + ) + except Exception: + pass + return options + + def _create_dynamic_callback(self, cmd_name: str, param_names: list | None = None): """为每个指令动态创建一个异步回调函数""" + param_names = param_names or [] async def dynamic_callback( - ctx: discord.ApplicationContext, params: str | None = None + ctx: discord.ApplicationContext, **kwargs ) -> None: # 1. 嘗試立即响应,防止超时 (移到最前面) followup_webhook = None @@ -495,14 +492,16 @@ async def dynamic_callback( # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter logger.debug(f"[Discord] Callback triggered: {cmd_name}") logger.debug(f"[Discord] Callback context: {ctx}") - logger.debug(f"[Discord] Callback params: {params}") + # 从 kwargs 收集参数值,拼接为命令字符串 + params_str = " ".join(str(v) for v in kwargs.values() if v is not None) + logger.debug(f"[Discord] Callback params: {params_str}, kwargs: {kwargs}") message_str_for_filter = cmd_name - if params: - message_str_for_filter += f" {params}" + if params_str: + message_str_for_filter += f" {params_str}" logger.debug( f"[Discord] Slash command '{cmd_name}' triggered. " - f"Raw params: '{params}'. " + f"Raw params: '{params_str}'. " f"Built command string: '{message_str_for_filter}'", ) @@ -565,11 +564,16 @@ def _extract_command_info( return None # Discord 斜杠指令名称规范 - if cmd_name != cmd_name.lower() or not re.match(r"^[-_'\\w]{1,32}$", cmd_name): + # Discord 支持 Unicode 命令名(如中文),放宽匹配并确保全小写 + if cmd_name != cmd_name.lower() or not re.match(r"^[-_'\w]{1,32}$", cmd_name): logger.debug(f"[Discord] Skipping invalid slash command format: {cmd_name}") return None - description = handler_metadata.desc or f"Command: {cmd_name}" + # 优先用 handler 的 docstring 作为命令描述 + handler = handler_metadata.handler + description = (handler.__doc__ or "").strip().split("\n")[0] if hasattr(handler, "__doc__") and handler.__doc__ else "" + if not description: + description = handler_metadata.desc or f"Command: {cmd_name}" if len(description) > 100: description = f"{description[:97]}..." From 73b93e00ab18b5e9026d2e5188b46c7fa898bae8 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Fri, 3 Jul 2026 17:21:34 +0800 Subject: [PATCH 2/8] Enhance slash command option type mapping and logging Refactor slash command option handling and improve error logging. --- .../discord/discord_platform_adapter.py | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 927a939750..f2d704e935 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -1,7 +1,10 @@ import asyncio import inspect +import logging import re import sys +import typing +import types from typing import Any, cast import discord @@ -384,10 +387,8 @@ async def _collect_and_register_commands(self) -> None: registered_commands = [] for handler_md in star_handlers_registry: - try: - if not star_map[handler_md.handler_module_path].activated: - continue - except KeyError: + plugin = star_map.get(handler_md.handler_module_path) + if not plugin or not plugin.activated: continue if not handler_md.enabled: continue @@ -451,9 +452,25 @@ def _build_slash_options(handler) -> list: for name, param in sig.parameters.items(): if name in ("self", "event"): continue + annotation = param.annotation + # 处理 Optional[T] 或 T | None + origin = typing.get_origin(annotation) + union_types = {typing.Union} + if hasattr(types, "UnionType"): + union_types.add(types.UnionType) + if origin in union_types: + args = typing.get_args(annotation) + non_none = [a for a in args if a is not type(None)] + if len(non_none) == 1: + annotation = non_none[0] + # 类型映射 opt_type = discord.SlashCommandOptionType.string - if param.annotation is int: + if annotation is int: opt_type = discord.SlashCommandOptionType.integer + elif annotation is float: + opt_type = discord.SlashCommandOptionType.number + elif annotation is bool: + opt_type = discord.SlashCommandOptionType.boolean required = param.default is inspect.Parameter.empty options.append( discord.Option( @@ -463,8 +480,8 @@ def _build_slash_options(handler) -> list: required=required, ) ) - except Exception: - pass + except Exception as e: + logger.debug(f"Failed to build slash options for {handler!r}: {e}", exc_info=True) return options def _create_dynamic_callback(self, cmd_name: str, param_names: list | None = None): @@ -492,8 +509,11 @@ async def dynamic_callback( # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter logger.debug(f"[Discord] Callback triggered: {cmd_name}") logger.debug(f"[Discord] Callback context: {ctx}") - # 从 kwargs 收集参数值,拼接为命令字符串 - params_str = " ".join(str(v) for v in kwargs.values() if v is not None) + # 按 param_names 顺序提取参数值,保证与函数签名一致 + if param_names: + params_str = " ".join(str(kwargs[name]) for name in param_names if kwargs.get(name) is not None) + else: + params_str = " ".join(str(v) for v in kwargs.values() if v is not None) logger.debug(f"[Discord] Callback params: {params_str}, kwargs: {kwargs}") message_str_for_filter = cmd_name if params_str: From 3d3871ad0712cd6b7b1d8234fd2a215d7c03b5b3 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Fri, 3 Jul 2026 17:27:21 +0800 Subject: [PATCH 3/8] Change log level from debug to warning for errors --- .../core/platform/sources/discord/discord_platform_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index f2d704e935..acb9fc4476 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -481,7 +481,7 @@ def _build_slash_options(handler) -> list: ) ) except Exception as e: - logger.debug(f"Failed to build slash options for {handler!r}: {e}", exc_info=True) + logger.warning(f"Failed to build slash options for {handler!r}: {e}", exc_info=True) return options def _create_dynamic_callback(self, cmd_name: str, param_names: list | None = None): From cb4954fabd15f71ca46e9a9a0798998f50917d20 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Sat, 4 Jul 2026 20:34:11 +0800 Subject: [PATCH 4/8] Update discord_platform_adapter.py --- .../discord/discord_platform_adapter.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index acb9fc4476..2124f4b24f 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -1,10 +1,9 @@ import asyncio import inspect -import logging import re import sys -import typing import types +import typing from typing import Any, cast import discord @@ -481,16 +480,16 @@ def _build_slash_options(handler) -> list: ) ) except Exception as e: - logger.warning(f"Failed to build slash options for {handler!r}: {e}", exc_info=True) + logger.warning( + f"Failed to build slash options for {handler!r}: {e}", exc_info=True + ) return options def _create_dynamic_callback(self, cmd_name: str, param_names: list | None = None): """为每个指令动态创建一个异步回调函数""" param_names = param_names or [] - async def dynamic_callback( - ctx: discord.ApplicationContext, **kwargs - ) -> None: + async def dynamic_callback(ctx: discord.ApplicationContext, **kwargs) -> None: # 1. 嘗試立即响应,防止超时 (移到最前面) followup_webhook = None try: @@ -511,7 +510,11 @@ async def dynamic_callback( logger.debug(f"[Discord] Callback context: {ctx}") # 按 param_names 顺序提取参数值,保证与函数签名一致 if param_names: - params_str = " ".join(str(kwargs[name]) for name in param_names if kwargs.get(name) is not None) + params_str = " ".join( + str(kwargs[name]) + for name in param_names + if kwargs.get(name) is not None + ) else: params_str = " ".join(str(v) for v in kwargs.values() if v is not None) logger.debug(f"[Discord] Callback params: {params_str}, kwargs: {kwargs}") @@ -591,7 +594,11 @@ def _extract_command_info( # 优先用 handler 的 docstring 作为命令描述 handler = handler_metadata.handler - description = (handler.__doc__ or "").strip().split("\n")[0] if hasattr(handler, "__doc__") and handler.__doc__ else "" + description = ( + (handler.__doc__ or "").strip().split("\n")[0] + if hasattr(handler, "__doc__") and handler.__doc__ + else "" + ) if not description: description = handler_metadata.desc or f"Command: {cmd_name}" if len(description) > 100: From 3a74b5a2df3151ab399bc73aec451bd40234b716 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Sat, 4 Jul 2026 20:43:31 +0800 Subject: [PATCH 5/8] Update discord_platform_adapter.py --- .../core/platform/sources/discord/discord_platform_adapter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 2124f4b24f..e136562b3a 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -26,6 +26,9 @@ from astrbot.core.star.filter.command_group import CommandGroupFilter from astrbot.core.star.star import star_map from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry +from astrbot.core.utils.media_utils import ( + MediaResolver, # noqa: F401 # 供外部 monkeypatch 使用 +) from .client import DiscordBotClient from .discord_platform_event import DiscordPlatformEvent From 57c230f2d5512d810d015cc71539c56ec85e4424 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Sat, 4 Jul 2026 21:13:19 +0800 Subject: [PATCH 6/8] Refactor Discord platform adapter for better handling --- .../discord/discord_platform_adapter.py | 104 +++++++++++------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index e136562b3a..3af2546f3a 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -4,15 +4,15 @@ import sys import types import typing -from typing import Any, cast +from typing import Any import discord from discord.abc import GuildChannel, Messageable, PrivateChannel -from discord.channel import DMChannel +from discord.channel import DMChannel, GroupChannel from astrbot import logger from astrbot.api.event import MessageChain -from astrbot.api.message_components import File, Image, Plain +from astrbot.api.message_components import File, Image, Plain, Record from astrbot.api.platform import ( AstrBotMessage, MessageMember, @@ -51,7 +51,6 @@ def __init__( event_queue: asyncio.Queue, ) -> None: super().__init__(platform_config, event_queue) - self.settings = platform_settings self.bot_self_id: str | None = None self.registered_handlers = [] # 指令注册相关 @@ -60,6 +59,7 @@ def __init__( self.activity_name = self.config.get("discord_activity_name", None) self.shutdown_event = asyncio.Event() self._polling_task = None + self.client = None @override async def send_by_session( @@ -68,16 +68,17 @@ async def send_by_session( message_chain: MessageChain, ) -> None: """通过会话发送消息""" - if self.client.user is None: + if self.client is None or self.client.user is None: logger.error( - "[Discord] Client is not ready (self.client.user is None); message send skipped" + "[Discord] Client is not ready (client is None); message send skipped" ) return # 创建一个 message_obj 以便在 event 中使用 message_obj = AstrBotMessage() + # 剥离平台前缀 (如 "discord_123456" → "123456") if "_" in session.session_id: - session.session_id = session.session_id.split("_")[1] + session.session_id = session.session_id.split("_", 1)[1] channel_id_str = session.session_id channel = None try: @@ -98,16 +99,16 @@ async def send_by_session( message_obj.message_str = message_chain.get_plain_text() message_obj.sender = MessageMember( - user_id=str(self.bot_self_id), + user_id=str(self.bot_self_id) if self.bot_self_id is not None else "unknown", nickname=self.client.user.display_name, ) - message_obj.self_id = cast(str, self.bot_self_id) + message_obj.self_id = str(self.bot_self_id) if self.bot_self_id is not None else "unknown" message_obj.session_id = session.session_id message_obj.message = message_chain.chain # 创建临时事件对象来发送消息 temp_event = DiscordPlatformEvent( - message_str=message_chain.get_plain_text(), + message_str=message_obj.message_str, message_obj=message_obj, platform_meta=self.meta(), session_id=session.session_id, @@ -122,7 +123,7 @@ def meta(self) -> PlatformMetadata: return PlatformMetadata( "discord", "Discord Adapter", - id=cast(str, self.config.get("id")), + id=str(self.config.get("id") or ""), default_config_tmpl=self.config, support_streaming_message=False, ) @@ -140,12 +141,13 @@ async def on_received(message_data) -> None: await self.handle_msg(abm) # 初始化 Discord 客户端 - token = str(self.config.get("discord_token")) - if not token: + raw_token = self.config.get("discord_token") + if not raw_token: logger.error( "[Discord] Bot token is not configured. Please set a valid token in the config file." ) return + token = str(raw_token) proxy = self.config.get("discord_proxy") or None allow_bot_messages = bool(self.config.get("discord_allow_bot_messages")) @@ -170,6 +172,7 @@ async def callback() -> None: try: self._polling_task = asyncio.create_task(self.client.start_polling()) + self._polling_task.add_done_callback(self._on_polling_task_done) await self.shutdown_event.wait() except discord.errors.LoginFailure: logger.error( @@ -191,9 +194,13 @@ def _get_message_type( """根据 channel 对象和 guild_id 判断消息类型""" if guild_id is not None: return MessageType.GROUP_MESSAGE - if isinstance(channel, DMChannel) or getattr(channel, "guild", None) is None: + if isinstance(channel, DMChannel): return MessageType.FRIEND_MESSAGE - return MessageType.GROUP_MESSAGE + if isinstance(channel, GroupChannel): + return MessageType.GROUP_MESSAGE + if getattr(channel, "guild", None) is not None: + return MessageType.GROUP_MESSAGE + return MessageType.FRIEND_MESSAGE def _get_channel_id( self, channel: Messageable | GuildChannel | PrivateChannel @@ -223,11 +230,9 @@ def _convert_message_to_abm(self, data: dict) -> AstrBotMessage: and hasattr(message, "guild") and message.guild ): - bot_member = ( - message.guild.get_member(self.client.user.id) - if self.client and self.client.user - else None - ) + bot_member = None + if self.client is not None and self.client.user is not None: + bot_member = message.guild.get_member(self.client.user.id) if bot_member and hasattr(bot_member, "roles"): for role in bot_member.roles: role_mention_str = f"<@&{role.id}>" @@ -248,19 +253,20 @@ def _convert_message_to_abm(self, data: dict) -> AstrBotMessage: message_chain.append(Plain(text=abm.message_str)) if message.attachments: for attachment in message.attachments: - if attachment.content_type and attachment.content_type.startswith( - "image/", - ): + ct = attachment.content_type or "" + if ct.startswith("image/"): message_chain.append( Image(file=attachment.url, filename=attachment.filename), ) + elif ct.startswith("audio/"): + message_chain.append(Record(file=attachment.url)) else: message_chain.append( File(name=attachment.filename, url=attachment.url), ) abm.message = message_chain abm.raw_message = message - abm.self_id = cast(str, self.bot_self_id) + abm.self_id = str(self.bot_self_id) if self.bot_self_id is not None else "unknown" abm.session_id = str(message.channel.id) abm.message_id = str(message.id) return abm @@ -272,6 +278,12 @@ async def convert_message(self, data: dict) -> AstrBotMessage: async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None: """处理消息""" + if self.client is None or self.client.user is None: + logger.error( + "[Discord] Client is not ready (client is None or user is None); message handling skipped" + ) + return + message_event = DiscordPlatformEvent( message_str=message.message_str, message_obj=message, @@ -281,12 +293,6 @@ async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> No interaction_followup_webhook=followup_webhook, ) - if self.client.user is None: - logger.error( - "[Discord] Client is not ready (self.client.user is None); message handling skipped" - ) - return - # 检查是否为斜杠指令 is_slash_command = message_event.interaction_followup_webhook is not None @@ -322,7 +328,7 @@ async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> No bot_member = raw_message.guild.get_member( self.client.user.id, ) - except Exception: + except discord.DiscordException: bot_member = None if bot_member and hasattr(bot_member, "roles"): bot_roles = set(bot_member.roles) @@ -345,6 +351,19 @@ async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> No async def terminate(self) -> None: logger.info("[Discord] Shutting down adapter...") self.shutdown_event.set() + + # 先取消 polling,避免与命令清理竞争 + 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 cancelled successfully.") + except Exception as e: + logger.warning( + f"[Discord] Error occurred while cancelling polling task: {e}" + ) + logger.info("[Discord] Cleaning up commands...") if self.enable_command_register and self.client: try: @@ -361,16 +380,6 @@ async def terminate(self) -> None: f"[Discord] Error occurred while cleaning up commands: {e}" ) - 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 cancelled successfully.") - except Exception as e: - logger.warning( - f"[Discord] Error occurred while cancelling polling task: {e}" - ) logger.info("[Discord] Closing client connection...") if self.client and hasattr(self.client, "close"): try: @@ -379,6 +388,17 @@ async def terminate(self) -> None: logger.warning(f"[Discord] Error occurred while closing client: {e}") logger.info("[Discord] Adapter shutdown complete.") + def _on_polling_task_done(self, task: asyncio.Task) -> None: + """polling task 完成回调,记录异常并唤醒 run() 避免僵尸状态""" + if task.cancelled(): + return + exc = task.exception() + if exc is not None: + logger.error( + f"[Discord] Polling task terminated with error: {exc}", exc_info=exc + ) + self.shutdown_event.set() + def register_handler(self, handler_info) -> None: """注册处理器信息""" self.registered_handlers.append(handler_info) @@ -553,7 +573,7 @@ async def dynamic_callback(ctx: discord.ApplicationContext, **kwargs) -> None: ) abm.message = [Plain(text=message_str_for_filter)] abm.raw_message = ctx.interaction - abm.self_id = cast(str, self.bot_self_id) + abm.self_id = str(self.bot_self_id) if self.bot_self_id is not None else "unknown" abm.session_id = str(ctx.channel_id) abm.message_id = str(ctx.interaction.id) From baf195698dbd15cafe581e3e76ce6523b6da5442 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Sat, 4 Jul 2026 21:21:06 +0800 Subject: [PATCH 7/8] Update discord_platform_adapter.py --- .../discord/discord_platform_adapter.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 3af2546f3a..e3974dcb52 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -99,10 +99,14 @@ async def send_by_session( message_obj.message_str = message_chain.get_plain_text() message_obj.sender = MessageMember( - user_id=str(self.bot_self_id) if self.bot_self_id is not None else "unknown", + user_id=str(self.bot_self_id) + if self.bot_self_id is not None + else "unknown", nickname=self.client.user.display_name, ) - message_obj.self_id = str(self.bot_self_id) if self.bot_self_id is not None else "unknown" + message_obj.self_id = ( + str(self.bot_self_id) if self.bot_self_id is not None else "unknown" + ) message_obj.session_id = session.session_id message_obj.message = message_chain.chain @@ -266,7 +270,9 @@ def _convert_message_to_abm(self, data: dict) -> AstrBotMessage: ) abm.message = message_chain abm.raw_message = message - abm.self_id = str(self.bot_self_id) if self.bot_self_id is not None else "unknown" + abm.self_id = ( + str(self.bot_self_id) if self.bot_self_id is not None else "unknown" + ) abm.session_id = str(message.channel.id) abm.message_id = str(message.id) return abm @@ -274,7 +280,19 @@ def _convert_message_to_abm(self, data: dict) -> AstrBotMessage: async def convert_message(self, data: dict) -> AstrBotMessage: """将平台消息转换成 AstrBotMessage""" # 由于 on_interaction 已被禁用,我们只处理普通消息 - return self._convert_message_to_abm(data) + abm = self._convert_message_to_abm(data) + for component in abm.message: + if isinstance(component, Record): + audio_ref = component.file + if audio_ref: + path_wav = await MediaResolver( + audio_ref, + media_type="audio", + default_suffix=".wav", + ).to_path(target_format="wav") + component.file = path_wav + component.url = path_wav + return abm async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None: """处理消息""" @@ -573,7 +591,9 @@ async def dynamic_callback(ctx: discord.ApplicationContext, **kwargs) -> None: ) abm.message = [Plain(text=message_str_for_filter)] abm.raw_message = ctx.interaction - abm.self_id = str(self.bot_self_id) if self.bot_self_id is not None else "unknown" + abm.self_id = ( + str(self.bot_self_id) if self.bot_self_id is not None else "unknown" + ) abm.session_id = str(ctx.channel_id) abm.message_id = str(ctx.interaction.id) From bc17bb3d8ead66e9bbd192ea1330ef653c885c30 Mon Sep 17 00:00:00 2001 From: NLKASHEI Date: Sat, 4 Jul 2026 21:27:04 +0800 Subject: [PATCH 8/8] Update discord_platform_adapter.py --- .../core/platform/sources/discord/discord_platform_adapter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index e3974dcb52..3162c5f1f5 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -292,6 +292,7 @@ async def convert_message(self, data: dict) -> AstrBotMessage: ).to_path(target_format="wav") component.file = path_wav component.url = path_wav + component.path = path_wav return abm async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None: