Discord.py 动态命令选项:无需重启更新数据库驱动的交互式选择


discord.py 动态命令选项:无需重启更新数据库驱动的交互式选择

本文详细介绍了在 `discord.py` 机器人中,如何实现基于数据库动态更新的命令选择项,避免因数据库变更而需要重启机器人。通过利用 `app_commands.autocomplete` 结合 `app_commands.Transformer` 和本地缓存机制,我们能够构建高效、响应迅速且上下文感知的交互式选项,同时强调异步数据库操作和智能模糊匹配的重要性。

在 discord.py 开发中,当机器人命令的选项(choices)依赖于频繁更新的数据库内容时,直接使用 @app_commands.choices 装饰器会遇到一个常见问题:这些选项在机器人启动时被静态加载,无法在运行时动态更新。这意味着如果数据库中的数据发生变化,除非重启机器人,否则命令选项将不会反映这些新更改。为了解决这一问题,discord.py 提供了 app_commands.autocomplete 和 app_commands.Transformer 机制,允许我们构建高度动态且响应迅速的命令选项。

静态选择项的问题

考虑以下场景,一个机器人需要提供课程标题作为命令选项,而这些课程标题存储在数据库中:

def lesson_choices() -> list[Choice[str]]:
    # 假设 LessonRepository.get_all_lessons() 从数据库获取所有课程
    return [
        Choice(name=lesson.title, value=lesson.title)
        for lesson in LessonRepository.get_all_lessons()
    ]

@app_commands.command()
@app_commands.default_permissions(administrator=True)
@app_commands.choices(lesson=lesson_choices()) # 这里的 choices 是静态的
async def create_or_update_mark(self, interaction: Interaction,
                                student: discord.Member,
                                lesson: Choice[str],
                                logiks: app_commands.Range[int, 0, 8]):
    # ... 命令逻辑 ...

这种方法的问题在于 lesson_choices() 函数在机器人启动时只被调用一次,其返回的选项列表被硬编码到命令定义中。如果 LessonRepository 中的数据在机器人运行期间更新,lesson 参数的可用选项将不会随之改变。

引入动态选择:autocomplete 的初步尝试

为了实现动态更新,discord.py 提供了 autocomplete 回调函数。当用户开始输入命令参数时,autocomplete 函数会被调用,并返回一个建议列表。然而,初次尝试时可能会遇到性能和逻辑上的挑战:

async def lesson_autocomplete(interaction: Interaction, current: str) -> list[app_commands.Choice[str]]:
    # 每次用户输入时都查询数据库,效率低下
    lessons = [lesson_dto.title for lesson_dto in LessonRepository.get_all_lessons()]
    # 简单的模糊匹配,可能不够智能
    return [
        app_commands.Choice(name=lesson, value=lesson)
        for lesson in lessons if current.lower() in lesson.lower()
    ]

@app_commands.command()
@app_commands.default_permissions(administrator=True)
@app_commands.autocomplete(lesson=lesson_autocomplete) # 使用 autocomplete
async def create_or_update_mark(self, interaction: Interaction,
                                student: discord.Member,
                                lesson: str, # 注意这里 lesson 的类型变为 str
                                logiks: app_commands.Range[int, 0, 8]):
    # ... 命令逻辑 ...

尽管 autocomplete 实现了动态性,但上述实现存在以下问题:

Explainpaper Explainpaper

阅读学术论文的更好方法,你的学术论文阅读助手。

Explainpaper 89 查看详情 Explainpaper
  1. 数据库查询频率过高: lesson_autocomplete 每次用户输入字符时都会触发,频繁地查询数据库会严重影响机器人性能,甚至可能阻塞事件循环(如果数据库操作是同步的)。
  2. 模糊匹配效率低: current.lower() in lesson.lower() 这种匹配方式只有当用户输入的内容是课程标题的子字符串时才有效,对于拼写错误或不完整的输入,建议效果不佳。
  3. 缺乏上下文感知: 无法根据命令中其他已输入的参数(例如 student)来过滤建议。

最佳实践:结合 Transformer 和缓存机制

为了彻底解决上述问题,推荐使用 discord.py 的 app_commands.Transformer 结合本地缓存和异步数据库操作。Transformer 允许我们封装复杂的参数处理逻辑,包括 autocomplete 和最终参数值的转换。

核心思想

  1. 异步数据库操作: 确保所有数据库交互都使用异步库,避免阻塞机器人的事件循环。
  2. 本地缓存: 在机器人启动时加载数据库数据到内存缓存中,并在数据更新时同步更新缓存。autocomplete 回调函数直接从缓存中获取数据,而不是频繁查询数据库。
  3. 智能模糊匹配: 使用如 difflib 等标准库提供更智能的字符串匹配,为用户提供更准确的建议。
  4. 上下文感知: 在 autocomplete 中利用 interaction.namespace 获取命令中其他已输入的参数,从而提供更精确的建议。
  5. Transformer 封装: 将 autocomplete 和参数转换逻辑封装在一个 Transformer 类中,使代码更模块化和可维护。

示例代码

以下是一个完整的示例,展示如何使用 Transformer 实现动态、高效的命令选项:

import discord
from discord.ext import commands
from discord import app_commands
import difflib
from typing import TYPE_CHECKING, Dict, List, Any, Optional, TypeAlias, Union

# 定义类型别名,提高可读性
GUILD_ID: TypeAlias = int
MEMBER_ID: TypeAlias = int
LESSON_ID: TypeAlias = int

# 假设的课程数据模型
class Lesson:
    id: int
    title: str
    # 可以添加其他课程相关数据

    def __init__(self, id: int, title: str):
        self.id = id
        self.title = title

# 模拟数据库操作层
class LessonRepository:
    _lessons_db: Dict[int, Lesson] = {} # 模拟数据库存储

    @staticmethod
    async def get_all_lessons() -> List[Lesson]:
        # 模拟异步数据库查询
        await discord.utils.sleep_until_next_event_loop_tick() # 模拟IO等待
        return list(LessonRepository._lessons_db.values())

    @staticmethod
    async def get_lesson_by_id(lesson_id: int) -> Optional[Lesson]:
        await discord.utils.sleep_until_next_event_loop_tick()
        return LessonRepository._lessons_db.get(lesson_id)

    @staticmethod
    async def add_lesson(lesson: Lesson):
        await discord.utils.sleep_until_next_event_loop_tick()
        LessonRepository._lessons_db[lesson.id] = lesson

    @staticmethod
    async def update_lesson_title(lesson_id: int, new_title: str):
        await discord.utils.sleep_until_next_event_loop_tick()
        if lesson_id in LessonRepository._lessons_db:
            LessonRepository._lessons_db[lesson_id].title = new_title

# 机器人主类
class MyBot(commands.Bot):
    # 类型检查时用于提示存在此函数
    if TYPE_CHECKING:
        some_function_for_loading_the_lessons_cache: Any

    def __init__(self, *args: Any, **kwargs: Any):
        super().__init__(*args, **kwargs)

        # 缓存结构:{guild_id: {student_id: {lesson_id: Lesson}}}
        # 实际应用中,如果课程不与特定学生或服务器绑定,可以简化缓存结构
        self.lessons_cache: Dict[GUILD_ID, Dict[MEMBER_ID, Dict[LESSON_ID, Lesson]]] = {}

    async def setup_hook(self):
        """
        此异步函数在机器人启动时调用一次,用于加载缓存和同步应用命令。
        """
        print("Bot setup_hook called.")
        # 同步所有应用命令到 Discord
        await self.tree.sync()
        print("Application commands synced.")
        # 首次加载缓存数据
        await self._load_lessons_cache()
        print("Lessons cache loaded.")

    async def _load_lessons_cache(self):
        """
        从数据库加载所有课程到缓存中。
        实际应用中,可能需要根据 guild_id 和 member_id 进行更细致的加载。
        这里为了演示,假设所有课程对所有学生和服务器都可用。
        """
        all_lessons = await LessonRepository.get_all_lessons()
        # 简化缓存逻辑,假设所有课程适用于所有服务器和学生
        # 实际中,你可能需要根据用户或服务器ID来过滤课程
        # 这里只是一个演示如何填充缓存的通用方法

        # 假设一个默认的 guild_id 和 student_id 来存储所有课程
        # 在实际应用中,这部分逻辑需要根据你的业务需求进行调整
        # 例如,可以从数据库中获取所有有效的 guild_id 和 student_id

        # 为演示目的,我们假设一个虚拟的 guild_id 和 student_id
        virtual_guild_id = 1 # 替换为你的实际 guild_id
        virtual_student_id = 1 # 替换为你的实际 student_id

        if virtual_guild_id not in self.lessons_cache:
            self.lessons_cache[virtual_guild_id] = {}
        if virtual_student_id not in self.lessons_cache[virtual_guild_id]:
            self.lessons_cache[virtual_guild_id][virtual_student_id] = {}

        for lesson in all_lessons:
            self.lessons_cache[virtual_guild_id][virtual_student_id][lesson.id] = lesson

        print(f"Cache content after load: {self.lessons_cache}")

    async def update_lesson_in_cache(self, lesson_id: int, new_title: str):
        """
        更新缓存中的课程信息,模拟数据库更新后的缓存同步。
        """
        # 遍历所有缓存层级,更新对应的课程
        for guild_id in self.lessons_cache:
            for student_id in self.lessons_cache[guild_id]:
                if lesson_id in self.lessons_cache[guild_id][student_id]:
                    self.lessons_cache[guild_id][student_id][lesson_id].title = new_title
                    print(f"Cache updated for lesson ID {lesson_id} with new title: {new_title}")
                    return

# 课程 Transformer
class LessonTransformer(app_commands.Transformer):
    async def find_similar_lesson_titles(self, lessons: Dict[LESSON_ID, Lesson], title: str) -> Dict[LESSON_ID, Lesson]:
        """
        使用 difflib 查找相似的课程标题。
        """
        lesson_titles = [lesson.title for lesson in lessons.values()]
        # 获取与输入标题最接近的15个匹配项,cutoff=0.6表示相似度阈值
        similar_titles = difflib.get_close_matches(title, lesson_titles, n=15, cutoff=0.6)

        # 根据相似标题构建返回字典
        result_lessons = {}
        for lesson_id, lesson_obj in lessons.items():
            if lesson_obj.title in similar_titles:
                result_lessons[lesson_id] = lesson_obj
        return result_lessons

    async def autocomplete(self, interaction: discord.Interaction[MyBot], value: str, /) -> List[app_commands.Choice[str]]:
        """
        提供课程名称的自动补全建议。
        """
        # 前提:此命令只能在服务器(guild)中调用
        assert interaction.guild is not None

        # 检查用户是否已填写“student”参数,以便进行更精确的过滤
        student: Optional[discord.Member] = interaction.namespace.get('student')

        # 获取当前服务器的所有课程
        guild_lessons_cache: Dict[MEMBER_ID, Dict[LESSON_ID, Lesson]] = interaction.client.lessons_cache.get(
            interaction.guild.id, {}
        )

        if student is None:
            # 如果没有指定学生,则显示所有学生的课程(扁平化处理)
            flat_lessons: Dict[LESSON_ID, Lesson] = {}
            for student_lessons in guild_lessons_cache.values():
                flat_lessons.update(student_lessons)

            similar_lessons = await self.find_similar_lesson_titles(flat_lessons, value)
        else:
            # 如果指定了学生,则只显示该学生的课程
            student_lessons: Dict[LESSON_ID, Lesson] = guild_lessons_cache.get(student.id, {})
            similar_lessons = await self.find_similar_lesson_titles(student_lessons, value)

        # 返回自动补全选项,value 存储课程ID
        return [
            app_commands.Choice(name=lesson.title, value=str(lesson_id))
            for lesson_id, lesson in similar_lessons.items()
        ]

    async def transform(self, interaction: discord.Interaction[MyBot], value: str, /) -> Union[Lesson, LESSON_ID]:
        """
        将用户输入的字符串值转换为 Lesson 对象或课程ID。
        """
        # 前提:此命令只能在服务器(guild)中调用
        assert interaction.guild is not None

        # 自动补全只是建议,最终用户可能输入任意值,需要进行验证
        if not value.isdigit():
            # 如果不是数字,说明用户没有选择自动补全的建议(其value是ID),而是手动输入了文本
            # 此时可以尝试根据文本查找,或者抛出错误
            raise app_commands.AppCommandError("无效的课程ID或名称。请从建议列表中选择。")

        lesson_id = int(value)
        student: Optional[discord.Member] = interaction.namespace.get('student')

        # 从缓存中查找课程
        guild_lessons_cache = interaction.client.lessons_cache.get(interaction.guild.id, {})

        if student is None:
            # 如果没有指定学生,需要遍历所有学生来查找课程
            for student_lessons in guild_lessons_cache.values():
                lesson = student_lessons.get(lesson_id)
                if lesson:
                    return lesson # 找到课程对象
            # 如果遍历所有学生都没找到,则返回ID,让后续逻辑处理验证
            return lesson_id 
        else:
            # 如果指定了学生,直接从该学生的课程中查找
            student_lessons = guild_lessons_cache.get(student.id, {})
            lesson = student_lessons.get(lesson_id)
            if lesson is None:
                raise app_commands.AppCommandError("无效的课程ID或该学生没有此课程。")
            return lesson


# 命令 Cog
class MarkCog(commands.Cog):
    def __init__(self, bot: MyBot):
        self.bot = bot

    @app_commands.command(name="update_mark", description="创建或更新学生成绩")
    @app_commands.guild_only() # 确保只在服务器中使用
    @app_commands.default_permissions(administrator=True)
    async def create_or_update_mark(
        self,
        interaction: discord.Interaction[MyBot],
        student: discord.Member,
        # 使用 Transform 装饰器,将 lesson 参数的类型处理委托给 LessonTransformer
        lesson: app_commands.Transform[Union[Lesson, LESSON_ID], LessonTransformer],
        logiks: app_commands.Range[int, 0, 8],
    ):
        assert interaction.guild is not None

        # Transformer 返回的 lesson 可能是 Lesson 对象或 LESSON_ID (int)
        # 需要在这里进行最终的类型检查和验证
        if isinstance(lesson, int):
            # 如果 Transformer 返回的是 ID,说明在 transform 阶段没有找到具体的 Lesson 对象
            # 此时需要再次从缓存中查找并验证,或者抛出错误
            potential_lesson = None
            guild_lessons_cache = interaction.client.lessons_cache.get(interaction.guild.id, {})
            student_lessons = guild_lessons_cache.get(student.id, {})
            potential_lesson = student_lessons.get(lesson)

            if potential_lesson is None:
                await interaction.response.send_message("无法找到指定的课程或该学生没有此课程。", ephemeral=True)
                return
            lesson = potential_lesson

        # 至此,lesson 变量保证是一个 Lesson 对象
        # 在这里执行你的业务逻辑,例如更新数据库
        # 假设 MarkRepository 是异步的
        # with MarkRepository(student_id=student.id, lesson_title=lesson.title) as lr:
        #     lr.create_or_update(mark_dto=MarkCreateDTO(logiks=logiks))

        # 模拟更新数据库和缓存
        # await LessonRepository.update_lesson_title(lesson.id, f"{lesson.title}_updated")
        # await self.bot.update_lesson_in_cache(lesson.id, f"{lesson.title}_updated")


        await interaction.response.send_message(
            f"学生 {student.display_name} 的课程 '{lesson.title}' 成绩已更新为 {logiks}。",
            ephemeral=True
        )

# 机器人启动和添加 Cog
async def main():
    # 模拟初始化一些课程数据
    await LessonRepository.add_lesson(Lesson(id=101, title="数学基础"))
    await LessonRepository.add_lesson(Lesson(id=102, title="物理概论"))
    await LessonRepository.add_lesson(Lesson(id=103, title="化学实验"))
    await LessonRepository.add_lesson(Lesson(id=104, title="生物探索"))
    await LessonRepository.add_lesson(Lesson(id=201, title="高级编程"))
    await LessonRepository.add_lesson(Lesson(id=202, title="数据结构"))

    intents = discord.Intents.default()
    intents.members = True # 如果需要获取成员信息,请开启此意图
    intents.message_content = True # 如果需要处理消息内容,请开启此意图

    bot = MyBot(command_prefix="!", intents=intents)
    await bot.add_cog(MarkCog(bot))

    # 替换为你的 Bot Token
    await bot.start("YOUR_BOT_TOKEN")

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

代码详解

  1. Lesson 类和 LessonRepository:

    • Lesson 是一个简单的数据传输对象(DTO),代表数据库中的课程信息。
    • LessonRepository 模拟了异步的数据库操作,get_all_lessons() 等方法都使用 await discord.utils.sleep_until_next_event_loop_tick() 来模拟异步 I/O,强调了使用异步数据库库的重要性。
  2. MyBot 类:

    • self.lessons_cache:这是一个嵌套字典,用于存储缓存的课程数据。其结构设计为 {guild_id: {student_id: {lesson_id: Lesson}}},允许根据服务器和学生进行精细化缓存。实际应用中,你需要根据你的业务逻辑调整缓存结构。
    • setup_hook():在机器人启动时被调用,用于同步命令 (await self.tree.sync()) 和调用 _load_lessons_cache() 来加载初始数据到缓存。
    • _load_lessons_cache():负责从数据库加载数据并填充 self.lessons_cache。此函数应该在机器人启动时执行一次,并在数据库数据发生重大更新时(例如通过管理命令)重新执行。
    • update_lesson_in_cache():一个示例函数,用于在数据库数据更新后,同步更新缓存。
  3. LessonTransformer 类:

    • find_similar_lesson_titles(): 这是一个辅助函数,利用 Python 标准库 difflib.get_close_matches 来实现智能的模糊匹配。它能够根据用户输入,从所有可用课程中找出最相似的标题,并返回对应的 Lesson 对象字典。n 参数控制返回的最大匹配数,cutoff 参数设置相似度阈值。
    • autocomplete(self, interaction, value, /):
      • 当用户在 Discord 客户端中输入 lesson 参数时,此方法会被调用。
      • interaction.namespace.get('student') 用于获取命令中其他已输入的参数值。这使得 autocomplete 能够实现上下文感知,例如,如果用户已经选择了某个学生,建议列表将只显示该学生相关的课程。
      • 根据是否有 student 参数,从缓存中获取相应的课程列表(flat_lessons 或 student_lessons)。
      • 调用 find_similar_lesson_titles 进行模糊匹配。
      • 返回一个 app_commands.Choice 列表,

以上就是Discord.py 动态命令选项:无需重启更新数据库驱动的交互式选择的详细内容,更多请关注其它相关文章!


# 遍历  # ip项目的营销推广方案设计  # seo基本步骤  # 医疗网站建设定做  # 自驾旅游营销推广文案  # 宿州网站建设工作室  # 白云区律师网站建设培训  # 惠阳家具网站建设报价  # 建设集团网站设计推荐  # 许昌网站排名优化怎么选  # 通化模板网站建设价格  # 实际应用  # 在这里  # 他已  # python  # 数据库中  # 是一个  # 重启  # 启动时  # 回调  # 加载  # 标准库  # 常见问题  # ai  # 回调函数  # app  # 编码  # git 


相关栏目: 【 Google疑问12 】 【 Facebook疑问10 】 【 优化推广96088 】 【 技术知识133117 】 【 IDC资讯59369 】 【 网络运营7196 】 【 IT资讯61894


相关推荐: mysql离线安装后如何启动_mysql离线安装完成后启动服务的方法  AI图层蒙版怎么用_AI图层蒙版应用技巧与设计实例  《三角洲行动》战斗步枪与机枪类改装代码分享  解决J*aScript动态图片上传中ID重复问题:在同一页面显示多张独立图片  lol小红书怎么|直播|?lol小红书|直播|是什么意思?  暴风影音官网正式版_暴风影音手机版官网下载安卓  解决SQLAlchemy模型跨文件关联的Linter兼容性指南  胃动力不足?试试这5个调理方法  申通快递查询 申通物流快递单实时查询入口  PySimpleGUI中实现键盘按键与按钮事件绑定教程  CSS绝对定位与溢出控制:实现背景元素局部显示不触发滚动条  J*a中为什么强调组合优于继承_组合模式带来的灵活性与可维护性解析  精通VS Code多光标编辑以实现闪电般快速的修改  b站如何剪辑视频_b站必剪app使用教程  rabbitmq 持久化有什么缺点?  顺丰快递在线查询系统 顺丰快递官方查单入口  微信朋友圈怎么设置三天可见 微信朋友圈设置指定天数可见步骤【教程】  Python自动化抓取GBGB赛狗比赛结果:日期范围与赛道筛选教程  Win10如何查看已安装的更新补丁 Win10卸载指定更新教程【教程】  Lar*el Eloquent中通过Join查询关联数据表:解决多行子查询问题  Win10显卡驱动安装失败怎么办 Win10使用DDU彻底卸载驱动【解决】  天天漫画2025最新入口 天天漫画永久有效登录入口  店铺如何做视频号推广?做视频号推广有用吗?  Win10共享文件夹设置方法 Win10局域网文件共享全攻略【教程】  Firefox OS应用开发:解决XMLHttpRequest跨域请求阻塞问题  解决Pandas DataFrame高度碎片化警告:高效创建多列的策略  抖音小程序怎么开通?小程序开通条件是什么?  口腔诊所管理软件推荐  创客贴登录页面入口 创客贴网页版最新网址链接  win11讲述人怎么关闭 Win11屏幕朗读辅助功能禁用方法【技巧】  《三国:谋定天下》平民全阶段通用阵容  《绿竹漫游》关闭消息通知方法  汽水音乐车机版官网5.0 汽水音乐车机版5.0版本下载入口  PHP odbc_fetch_array 返回值处理:如何正确访问嵌套数组元素  苹果iPhone14ProMax如何新建AppleID_iPhone14ProMax新建AppleID具体流程  vivo手机视频通话美颜怎么设置_vivo视频通话美颜开启方法  《兴业银行》注册登录方法  邮政快递寄件查询入口 邮政快递收件查询入口  《海底捞》点外卖方法  什么是Satis,如何用它搭建一个私有的composer仓库?  Win10如何彻底关闭OneDrive Win10禁用云同步功能【纯净】  XPath动态元素定位:如何精准选择文本内容变化的元素  PHP魔术方法__set与__isset:设计考量、性能权衡与静态分析的视角  12306夜间购票失败? | 查看官方公布的暂停服务公告与应对方案  sublime如何处理超大文件不卡顿 _sublime打开大日志文件技巧  三星A55应用闪退排查步骤_Samsung A55稳定性优化技巧  《绝区零》2.3前瞻|直播|内容介绍  西瓜视频怎么查看访客记录_西瓜视频访客记录查看方法  《雷电模拟器》自动点击设置方法  《360浏览器》自动保存账号密码设置方法 

 2025-12-02

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.