Discord 适配器增强功能#9125
Conversation
1. 所有命令参数统一显示为 params → 用 inspect.signature() 按函数签名生成独立 Option
2. 命令描述用插件 desc → 改用 handler.__doc__(docstring)
3. star_map 无异常保护 → 加 try/except KeyError
4. 重复 import inspect → 移除
5. Option description 优化 → "请输入 {name}"
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
send_by_sessionandhandle_msgyou now constructDiscordPlatformEventin 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, buildingparams_strfromkwargs.values()makes the command string depend on the kwargs dict order; using theparam_nameslist to iterate in a defined order (and to skip unexpected keys) would make argument reconstruction more predictable and stable. - In
_build_slash_optionsyou swallow all exceptions with a bareexcept 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
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.
| # 从 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}" |
There was a problem hiding this comment.
这里有两个需要注意的问题:
- 未使用的变量:传入
_create_dynamic_callback的param_names列表在函数体内完全没有被使用。 - 参数顺序与安全性:直接遍历
kwargs.values()拼接参数可能会受到不属于命令选项的额外 kwargs 的干扰。建议使用param_names来按函数签名的确切顺序提取参数值。
⚠️ 重要提示:由于通用的CommandFilter内部使用简单的.split(" ")来解析参数,如果用户在 Discord 斜杠指令中输入的某个参数值包含空格(例如message="hello world"),拼接为params_str后会被CommandFilter错误地拆分为多个参数,从而导致解析失败。这是目前文本命令过滤器和结构化斜杠指令对接时的固有局限性,建议在后续版本中考虑让CommandFilter支持直接接收已解析的参数字典。
| # 从 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}" |
Refactor slash command option handling and improve error logging.
There was a problem hiding this comment.
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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Discord 平台适配器 Bug 修复
概述
共修复 18 个 Bug,覆盖空值安全、逻辑错误、异常处理、资源生命周期和代码质量。
修复清单
一、空值安全(6 项)
__init__self.client未初始化,terminate()或send_by_session()过早调用时抛AttributeErrorself.client = Nonesend_by_session/handle_msgself.client为None时直接访问.user属性崩溃if self.client is None or self.client.user is None守卫handle_msgDiscordPlatformEvent创建之后,白创建对象_convert_message/send_by_session/_create_dynamic_callbackcast(str, self.bot_self_id)—bot_self_id为None时,self_id仍是None,下游is_mentioned()中int(None)崩溃str(...) if ... is not None else "unknown"meta()cast(str, self.config.get("id"))— 配置缺"id"时返回Nonestr(... or "")send_by_sessionstr(self.bot_self_id)—bot_self_id为None时发送者 ID 变成字符串"None""unknown"兜底二、逻辑错误(4 项)
_get_message_typeGroupChannel(群组私聊)被误判为FRIEND_MESSAGE(原逻辑:isinstance(DMChannel) or guild is None)from discord.channel import GroupChannel,显式isinstance(channel, GroupChannel)判断_convert_messageself.client.user.id if self.client and self.client.user else None— Python 先求值真值分支再检查条件,属性不存在时仍崩溃if/else块run()str(self.config.get("discord_token"))—None变"None"字符串,绕过if not token:空值检查,导致用无效 token 连接 Discordstr()send_by_sessionsession_id.split("_")[1]对多下划线 ID(如discord_123_456)会截断split("_", 1)[1]三、异常处理与鲁棒性(4 项)
handle_msgexcept Exception裸捕获,可能吞掉KeyboardInterrupt等except discord.DiscordExceptionrun()LoginFailure)被静默吞掉,适配器进入僵尸状态直到terminate()才暴露_on_polling_task_done回调:记录错误日志 + 设置shutdown_event唤醒run()terminate()convert_messageMediaResolver转换为.wav,导致 CI 测试失败MediaResolver→to_path(target_format="wav")转换,同时设置file/url/path四、代码质量(4 项)
__init__self.settings = platform_settings— 全文无引用,死代码send_by_sessionmessage_chain.get_plain_text()连续调用两次message_obj.message_strimportcast导入已无调用点(所有cast(str, ...)已被替换)send_by_session"discord_123456" → "123456"注释文件变更
discord_platform_adapter.py:576 行(原始)→ ~640 行(修复后)无破坏性 API 变更,所有修复均为防御性、向后兼容。