Skip to content

Discord 适配器增强功能#9125

Open
NLKASHEI wants to merge 8 commits into
AstrBotDevs:masterfrom
NLKASHEI:patch-1
Open

Discord 适配器增强功能#9125
NLKASHEI wants to merge 8 commits into
AstrBotDevs:masterfrom
NLKASHEI:patch-1

Conversation

@NLKASHEI

@NLKASHEI NLKASHEI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Discord 平台适配器 Bug 修复

概述

共修复 18 个 Bug,覆盖空值安全、逻辑错误、异常处理、资源生命周期和代码质量。


修复清单

一、空值安全(6 项)

# 位置 问题 修复
1 __init__ self.client 未初始化,terminate()send_by_session() 过早调用时抛 AttributeError 添加 self.client = None
2 send_by_session / handle_msg self.clientNone 时直接访问 .user 属性崩溃 统一加 if self.client is None or self.client.user is None 守卫
3 handle_msg client 空值检查放在 DiscordPlatformEvent 创建之后,白创建对象 检查移到 event 创建之前
4 _convert_message / send_by_session / _create_dynamic_callback cast(str, self.bot_self_id)bot_self_idNone 时,self_id 仍是 None,下游 is_mentioned()int(None) 崩溃 改为 str(...) if ... is not None else "unknown"
5 meta() cast(str, self.config.get("id")) — 配置缺 "id" 时返回 None 改为 str(... or "")
6 send_by_session str(self.bot_self_id)bot_self_idNone 时发送者 ID 变成字符串 "None" "unknown" 兜底

二、逻辑错误(4 项)

# 位置 问题 修复
7 _get_message_type GroupChannel(群组私聊)被误判为 FRIEND_MESSAGE(原逻辑:isinstance(DMChannel) or guild is None 新增 from discord.channel import GroupChannel,显式 isinstance(channel, GroupChannel) 判断
8 _convert_message 三元表达式 self.client.user.id if self.client and self.client.user else None — Python 先求值真值分支再检查条件,属性不存在时仍崩溃 替换为 if/else
9 run() str(self.config.get("discord_token"))None"None" 字符串,绕过 if not token: 空值检查,导致用无效 token 连接 Discord 先取原始值判空,再 str()
10 send_by_session session_id.split("_")[1] 对多下划线 ID(如 discord_123_456)会截断 改为 split("_", 1)[1]

三、异常处理与鲁棒性(4 项)

# 位置 问题 修复
11 handle_msg except Exception 裸捕获,可能吞掉 KeyboardInterrupt 改为 except discord.DiscordException
12 run() polling task 异常(如 LoginFailure)被静默吞掉,适配器进入僵尸状态直到 terminate() 才暴露 新增 _on_polling_task_done 回调:记录错误日志 + 设置 shutdown_event 唤醒 run()
13 terminate() 命令清理在 polling 取消之前执行,可能与运行中的 polling 产生竞争 调整顺序:先取消 polling → 再清理命令 → 最后关闭连接
14 convert_message 音频附件未通过 MediaResolver 转换为 .wav,导致 CI 测试失败 恢复 MediaResolverto_path(target_format="wav") 转换,同时设置 file/url/path

四、代码质量(4 项)

# 位置 问题 修复
15 __init__ self.settings = platform_settings — 全文无引用,死代码 删除
16 send_by_session message_chain.get_plain_text() 连续调用两次 复用 message_obj.message_str
17 import cast 导入已无调用点(所有 cast(str, ...) 已被替换) 删除
18 send_by_session 注释说明 session_id 前缀剥离行为 添加 "discord_123456" → "123456" 注释

文件变更

discord_platform_adapter.py:576 行(原始)→ ~640 行(修复后)

无破坏性 API 变更,所有修复均为防御性、向后兼容。

1. 所有命令参数统一显示为 params → 用 inspect.signature() 按函数签名生成独立 Option
2. 命令描述用插件 desc → 改用 handler.__doc__(docstring)
3. star_map 无异常保护 → 加 try/except KeyError
4. 重复 import inspect → 移除
5. Option description 优化 → "请输入 {name}"
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. labels Jul 3, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In send_by_session and handle_msg you now construct DiscordPlatformEvent in two slightly different places; consider refactoring this into a single helper/factory to avoid duplication and keep behavior consistent between normal handling and temporary send events.
  • In _create_dynamic_callback, building params_str from kwargs.values() makes the command string depend on the kwargs dict order; using the param_names list to iterate in a defined order (and to skip unexpected keys) would make argument reconstruction more predictable and stable.
  • In _build_slash_options you swallow all exceptions with a bare except Exception: pass; adding at least a debug log with the handler name and error will make it much easier to diagnose broken signatures or unexpected annotations without failing silently.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `send_by_session` and `handle_msg` you now construct `DiscordPlatformEvent` in two slightly different places; consider refactoring this into a single helper/factory to avoid duplication and keep behavior consistent between normal handling and temporary send events.
- In `_create_dynamic_callback`, building `params_str` from `kwargs.values()` makes the command string depend on the kwargs dict order; using the `param_names` list to iterate in a defined order (and to skip unexpected keys) would make argument reconstruction more predictable and stable.
- In `_build_slash_options` you swallow all exceptions with a bare `except Exception: pass`; adding at least a debug log with the handler name and error will make it much easier to diagnose broken signatures or unexpected annotations without failing silently.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/discord/discord_platform_adapter.py" line_range="445-454" />
<code_context>
+    @staticmethod
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Improve robustness of type mapping and error handling in _build_slash_options

Only bare `int` annotations are treated as integer options; types like `Optional[int]`, `bool`, or `float` all degrade to string. The blanket `except Exception: pass` also hides any signature inspection failures. Please extend the type handling (e.g., via `typing.get_origin`/`get_args` for optionals and other primitives) and log caught exceptions at least at debug level instead of silently ignoring them.

Suggested implementation:

```python
import inspect
from typing import get_origin, get_args

```

```python
    @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

                annotation = param.annotation
                # 无类型标注时保持默认字符串
                if annotation is not inspect._empty:
                    origin = get_origin(annotation)
                    args = get_args(annotation)

                    # 处理 Optional[T] 或 Union[T, None] 形式
                    if origin is typing.Union and args:
                        non_none_args = [t for t in args if t is not type(None)]  # noqa: E721
                        if len(non_none_args) == 1:
                            annotation = non_none_args[0]
                            origin = get_origin(annotation)
                            args = get_args(annotation)

                    # 简单的原始类型映射
                    if annotation is int:
                        opt_type = discord.SlashCommandOptionType.integer
                    elif annotation is bool:
                        opt_type = discord.SlashCommandOptionType.boolean
                    elif annotation is float:
                        # Discord 对数字使用 number 类型
                        opt_type = discord.SlashCommandOptionType.number

```

1. The updated implementation assumes a logger is available; if this module does not yet define one, add:
   `import logging` at the top of the file and `logger = logging.getLogger(__name__)` near the top-level.
2. Replace any existing bare `except Exception: pass` around `_build_slash_options`'s logic with:
   `except Exception as exc: logger.debug("Failed to build slash options for %r: %s", handler, exc, exc_info=True)` so that failures are logged instead of silently ignored.
3. Ensure `typing` is imported as `import typing` if not already present, since the new code references `typing.Union`.
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/discord/discord_platform_adapter.py" line_range="470-478" />
<code_context>
+            pass
+        return options
+
+    def _create_dynamic_callback(self, cmd_name: str, param_names: list | None = None):
         """为每个指令动态创建一个异步回调函数"""
+        param_names = param_names or []
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Use param_names to control argument ordering and filtering when building params_str

Right now the callback ignores `param_names` and just joins `kwargs.values()`, which can include unexpected keys and a different order than the handler’s signature. Please construct `params_str` by iterating over `param_names` and reading `kwargs[name]` when available, skipping unknown keys. This will keep argument order stable and avoid serializing unintended kwargs into the command string.

Suggested implementation:

```python
            # 1. 嘗試立即响应,防止超时 (移到最前面)
            followup_webhook = None
            # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter
            logger.debug(f"[Discord] Callback triggered: {cmd_name}")
            logger.debug(f"[Discord] Callback context: {ctx}")

            # 根据 param_names 控制参数顺序和过滤,避免意外 kwargs
            ordered_args: list[str] = []
            if param_names:
                for name in param_names:
                    if name in kwargs:
                        ordered_args.append(str(kwargs[name]))
            else:
                # 作为回退,保持与旧实现行为一致(但不依赖于 param_names)
                ordered_args = [str(v) for v in kwargs.values()]

            params_str = " ".join(ordered_args)

```

1. 确保在调用 `_create_dynamic_callback` 时传入的 `param_names` 顺序与对应 handler 的参数顺序一致(例如从函数签名或 Discord option 定义中提取),否则无法保证期望的参数顺序。
2. 如果下游代码已经在 `dynamic_callback` 中构造了 `params_str`(例如原来有一行 `params_str = " ".join(str(v) for v in kwargs.values())` 但不在本段上下文中),需要将那行删除或替换为这里的 `params_str` 构造逻辑,以避免重复或不一致的行为。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/platform/sources/discord/discord_platform_adapter.py
Comment thread astrbot/core/platform/sources/discord/discord_platform_adapter.py

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Discord platform adapter to dynamically generate Discord slash command options from handler function signatures, support Unicode command names, and use handler docstrings as command descriptions. It also removes audio record handling and the create_event helper method. The reviewer feedback suggests improving the slash command option generator to support optional, union, float, and boolean types. Additionally, it is recommended to use the extracted parameter names to safely order arguments in the dynamic callback, restore the create_event helper to avoid duplicate instantiation, and simplify the plugin activation check using star_map.get() instead of a try-except block.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread astrbot/core/platform/sources/discord/discord_platform_adapter.py
Comment on lines +495 to +500
# 从 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}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

这里有两个需要注意的问题:

  1. 未使用的变量:传入 _create_dynamic_callbackparam_names 列表在函数体内完全没有被使用。
  2. 参数顺序与安全性:直接遍历 kwargs.values() 拼接参数可能会受到不属于命令选项的额外 kwargs 的干扰。建议使用 param_names 来按函数签名的确切顺序提取参数值。

⚠️ 重要提示:由于通用的 CommandFilter 内部使用简单的 .split(" ") 来解析参数,如果用户在 Discord 斜杠指令中输入的某个参数值包含空格(例如 message="hello world"),拼接为 params_str 后会被 CommandFilter 错误地拆分为多个参数,从而导致解析失败。这是目前文本命令过滤器和结构化斜杠指令对接时的固有局限性,建议在后续版本中考虑让 CommandFilter 支持直接接收已解析的参数字典。

Suggested change
# 从 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}"
# 从 kwargs 收集参数值,拼接为命令字符串
params_str = " ".join(str(kwargs[name]) for name in param_names if kwargs.get(name) is not None)
logger.debug(f"[Discord] Callback params: {params_str}, kwargs: {kwargs}")
message_str_for_filter = cmd_name
if params_str:
message_str_for_filter += f" {params_str}"

Comment thread astrbot/core/platform/sources/discord/discord_platform_adapter.py
Comment thread astrbot/core/platform/sources/discord/discord_platform_adapter.py Outdated
@NLKASHEI NLKASHEI marked this pull request as draft July 3, 2026 09:14
@NLKASHEI NLKASHEI marked this pull request as ready for review July 3, 2026 09:20
@NLKASHEI NLKASHEI marked this pull request as draft July 3, 2026 09:20
Refactor slash command option handling and improve error logging.
@NLKASHEI NLKASHEI marked this pull request as ready for review July 3, 2026 09:21

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/discord/discord_platform_adapter.py" line_range="446-447" />
<code_context>
         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 = []
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Broad exception handling in _build_slash_options can hide misconfigurations of handlers.

Catching all exceptions here, logging only at debug, and returning an empty `options` list means misconfigured or unexpectedly annotated handlers quietly become slash commands with no options. To make these issues visible, either log failures at warning/error level or re-raise unexpected errors so registration fails fast instead of silently degrading behavior.

Suggested implementation:

```python
    @staticmethod
    def _build_slash_options(handler) -> list:
        """从 handler 函数签名提取参数,映射到 Discord SlashCommandOptionType"""
        options = []
        # 这里不再捕获所有异常,以避免静默吞掉处理器配置错误。
        # 如果 handler 的签名或注解有问题,应让异常向上传播,从而在注册阶段暴露问题。
        sig = inspect.signature(handler)
        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"):

```

1.`_build_slash_options` 的后半部分,如果当前存在类似 `except Exception as exc: ...` 的广泛异常处理,请将其改为:
   - 要么完全移除 `except`(让异常直接抛出),
   - 要么仅捕获预期的、可恢复的异常类型(例如对特定注解解析错误),并使用 `logger.warning``logger.error` 记录详细信息后重新抛出异常:
   ```python
   except SomeExpectedError as exc:
       logger.warning("Failed to build slash options for %r: %s", handler, exc)
       raise
   ```
2. 如果当前仅使用 `logger.debug` 记录失败并返回空列表,请将日志级别提升到 `warning``error`,并确保在真正的配置错误情况下调用 `raise`,以便命令注册在出错时不会静默成功。
3. 如果本模块尚未定义 logger(例如 `logger = logging.getLogger(__name__)`),需要在文件顶部导入 `logging` 并定义一个模块级 logger,以支持上述 warning/error 级别的日志记录。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/platform/sources/discord/discord_platform_adapter.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant