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.
1. 概述修复版在保留原版 AstrBot + Pycord Discord 适配器整体架构的前提下,重点处理了:
中文斜杠命令名:Discord CHAT_INPUT 允许 Unicode 字母/数字(含中文),修复版允许注册(须 1–32 位且 2. 初始化与生命周期
相关辅助方法(修复版新增)
3. 会话发送
|
| 项目 | 原版 | 修复版 |
|---|---|---|
改写 session.session_id |
原地 split("_")[1],污染调用方对象 |
仅本地变量解析,不修改传入 session |
多段 _ |
split("_")[1] 只取第二段,易截错 |
rsplit("_", 1)[-1] 取最后一段 |
| 取 channel | 仅 get_channel(缓存) |
先缓存,失败再 fetch_channel(_resolve_channel) |
| 事件构造 | create_event |
同样走 create_event(修复过程中曾内联过,最终恢复统一入口) |
| 发送失败 | 异常直接上浮 | 打带 channel 的错误日志后再抛出 |
4. 消息转换与媒体
4.1 文本与 Mention
| 项目 | 原版 | 修复版 |
|---|---|---|
| User mention 剥离 | 仅开头 bot 的 <@id> / <@!id> |
同样只剥 本 bot;支持循环剥连续 bot mention |
| Role mention 剥离 | bot 角色且开头匹配则剥一个 | 循环:可连续剥 bot user + bot role |
| 误剥其它人 @ | 原版已尽量避免 | 保持「只剥 bot」,不用宽正则剥任意 mention |
get_member |
mention 检测侧有 try | 剥离侧也加 try/except,避免偶发异常 |
4.2 附件与音频
| 项目 | 原版 | 修复版 |
|---|---|---|
content_type is None |
需同时判断 truthy 再 startswith |
ct = (content_type or "").lower(),更稳 |
| 空 url 附件 | 仍可能入链 | 无 url 直接跳过 |
Record |
file + url |
同样 file + url |
| 音频落地 | convert_message 内 MediaResolver 转 wav |
保留;失败 try/except 打日志,保留原 URL |
| 作者字段 | display_name |
display_name → name → "" 兜底 |
说明:中间某一版修复稿曾误删音频解析,当前修复版已与原版对齐并加强异常保护。
4.3 session / group id
| 项目 | 原版 | 修复版 |
|---|---|---|
_get_channel_id |
str(getattr(..., None)) 可能得到 "None" |
无 id 返回 "" |
abm.session_id |
str(message.channel.id) |
安全取 id,无则 "" |
5. 事件处理 handle_msg / create_event
| 项目 | 原版 | 修复版 |
|---|---|---|
create_event |
有独立方法 | 保留,send_by_session 与 handle_msg 共用 |
| 客户端未就绪 | 检查 client.user |
_client_ready(),并补 message.self_id |
| 斜杠优先 | interaction_followup_webhook 则 wake |
相同 |
| User / Role 提及唤醒 | 有 | 相同逻辑 + mentions 空列表与异常防护 |
非 discord.Message |
忽略并 warning | 相同 |
6. 斜杠指令注册与回调
这是修复版与原版差异最大的一块。
6.1 命令收集
| 项目 | 原版 | 修复版 |
|---|---|---|
| 插件是否激活 | star_map[path].activated,缺 key 可能 KeyError |
star_map.get(...),无插件跳过 |
| 重复命令名 | 可重复 add_application_command |
seen_names 去重并 warning |
| 单条注册失败 | 可能中断整批 | 单命令 try/except,失败 continue |
| 参数 Option | 固定 一个可选 params: str |
从 handler 签名 生成多个 Option;无参数时回退 原版式 params |
| Option 数量 | 恒为 1 | 最多 25,超出截断 |
| Option 顺序 | 不适用 | required 在前,符合 Discord 要求 |
*args / **kwargs |
不涉及 | 建 Option 时跳过 VAR_POSITIONAL / VAR_KEYWORD |
| 非法 option 名 | 无 | 校验小写 + 字符集,跳过 |
| 类型注解 | 无 | get_type_hints + Optional 解包;int/float/bool/string |
| 同步范围 | sync_commands() |
配置了 guild_id 时 sync_commands(guild_ids=[...]),便于调试、减轻全局延迟/配额 |
| 日配额 30034 | 有专门处理 | 保留;其它异常也 warning |
6.2 命令名规则(含中文)
| 项目 | 原版 | 修复版 |
|---|---|---|
| 正则 | ^[-_'\w]{1,32}$(无 ASCII flag) |
^[-_\w]{1,32}$(Unicode \w,中文可过) |
| 小写 | cmd_name != cmd_name.lower() 则跳过 |
相同 |
| 日志 | debug「Skipping invalid…」 | warning,说明可用中文/字母/数字/_/-,须小写 1–32 |
| 描述 | 多用 handler_metadata.desc |
优先 docstring 首行,再 desc,再默认;空串/超长兜底(≤100) |
结论:中文斜杠命令名可以注册(例如 /帮助),前提是名称本身满足 Discord 长度与字符规则。大写英文、空格、.、超长等仍会被跳过。
子命令(有 parent_command_names)与 CommandGroupFilter 仍不注册为 slash,与原版一致。
6.3 动态回调
| 项目 | 原版 | 修复版 |
|---|---|---|
| 签名 | (ctx, params: str | None = None) |
按 option 名生成显式参数 + *args/**kwargs 兜底;生成失败回退纯 kwargs |
| defer | wait_for(ctx.defer(), 2.5) |
先看 response.is_done(),再 defer |
| defer 失败 | 打日志 return | 尽量 ephemeral 提示用户后 return |
| 业务异常 | 无统一捕获 | _handle 内 try/except,followup 提示「指令执行出错」 |
| 参数拼命令串 | cmd_name + " " + params |
按 param_names 顺序拼接;含空格/引号用双引号转义,减少简单 split 错位 |
| bool | 随 str |
序列化为 true / false |
| channel 为空 | 有 guild_id 兜底 | 相同,且 channel_id is None 时用 "" |
7. 其它小改动
- 去掉对
shlex的依赖,改为自实现双引号转义(避免 Windows/locale 差异) - 常量:
_DISCORD_MAX_OPTIONS = 25、_DISCORD_DESC_MAX = 100 register_handler、元数据meta()、平台注册装饰器与原版语义一致MediaResolver正常使用(非死 import)
8. 已知限制(架构层,非本次必改)
-
斜杠多参 → 文本命令串
仍拼成命令名 参数1 参数2…交给 AstrBotCommandFilter。若下游只按空格拆且不认引号,含空格参数仍可能错位。 -
有明确签名时不再提供万能
params
handler 已有foo, bar时只有这两个 Option;自由长文本需写在某个 string 参数里。无参 handler 仍回退params。 -
复杂 Discord 类型
未映射 User / Channel / Role / Attachment 等 Option 类型,一律 string/int/float/bool。 -
子命令 / 命令组
仍不注册为 Discord 原生 slash 子结构。 -
IDE / Pylance
工作区若未安装discord/astrbot,会出现导入与@override告警,与运行时逻辑无关。
9. 建议验证清单
上线或替换前建议在真实 Discord 环境覆盖:
- 无 token / 错误 token 日志与退出行为
- 普通消息、@bot、@角色(bot 拥有角色)唤醒
- 图片 / 语音附件(音频是否转成可用路径)
- 会话主动
send_by_session(含冷启动后首次发送) - 无参 slash、多参 slash、参数含空格
- 中文命令名注册与触发(如
/帮助) - 重复命令名、非法命令名日志
-
guild_id调试同步 vs 全局同步 - 正常
terminate:清命令、断连、无挂死 task
10. 变更对照速查
| 类别 | 是否修复/增强 |
|---|---|
client 未初始化崩溃 |
✅ |
session.session_id 原地修改 |
✅ |
| channel 缓存未命中 | ✅ fetch_channel |
"None" 污染 group/session id |
✅ |
| 音频 MediaResolver | ✅ 保留并加固 |
| star_map KeyError | ✅ |
| slash 多 Option + 无参 params 回退 | ✅ |
| 重复 slash 名 | ✅ |
| required option 顺序 / 最多 25 个 | ✅ |
| 中文命令名 | ✅ 明确允许 |
| guild_id 类型与同步范围 | ✅ |
| defer / 执行异常用户提示 | ✅ |
| 收消息 / 单项注册异常隔离 | ✅ |
| mention 只剥 bot、可连续剥 | ✅ |
| 子命令组注册为 slash | ❌ 仍不支持(同原版) |
| User/Channel 等 Option 类型 | ❌ 未做 |
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 变更,所有修复均为防御性、向后兼容。