全网整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:400-690-7320

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 实现了动态性,但上述实现存在以下问题:

SCISPACE SCISPACE

AI论文研究助手,探索和解释论文的平台

SCISPACE 65 查看详情 SCISPACE
  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 动态命令选项:无需重启更新数据库驱动的交互式选择的详细内容,更多请关注其它相关文章!


# 遍历  # 三河短视频营销推广  # 唐山营销推广唐山  # 河南seo是什么电话  # 网站站外优化设置在哪里  # 网站推广代理文案怎么写  # 梅州关键词排名软件  # 包河区建设局网站公示  # 荆州网站推广工作招聘网  # 无锡关键词排名怎么解决  # 白山seo公司如何引流  # 在这里  # 如何使用  # 他已  # python  # 数据库中  # 是一个  # 重启  # 启动时  # 回调  # 加载  # 标准库  # 常见问题  # ai  # 回调函数  # app  # 编码  # git 


相关文章: 学习通网页版官方登录 超星学习通电脑端入口指南  J*a如何使用AtomicInteger控制计数_J*a无锁计数器性能分析  蛙漫正版漫画平台入口_蛙漫免费阅读全站漫画资源  抖音未来赚钱的新趋势 2025年值得关注的变现风口分析  Pandas DataFrame:高效添加条件计算列  MinIO大规模对象列表性能瓶颈深度解析与外部元数据管理策略  在Pyomo中实现基于变量的条件约束:Big-M方法详解  Go调试环境为何无法启动_Go调试器启动失败原因与解决策略  Yandex浏览器官方网页版入口 Yandex浏览器最新版官网  字由网在线版登录地址 字由网网页版安全入口  “在文档元素之后找到了标记”是什么错误? 检查并修复XML中多个根元素的3个方法  使用CSS更改登录屏幕输入框中PNG图标颜色的策略与局限性  Python多线程中正确使用sigwait处理SIGALRM信号  c++中的const_cast和reinterpret_cast怎么用_c++四种类型转换  曝R星经典之作开发图 设计简陋但信息密集!  c++如何使用TBB库进行任务并行_c++ Intel线程构建模块  俄罗斯搜索引擎Yandex指南 附2025年免登录官网入口  谷歌浏览器怎么给标签页静音_Chrome标签静音快捷操作  Win11网速慢怎么解决 Win11网络设置优化解除限速  如何让 composer 信任自签名的 HTTPS 证书源?  没有大陆身份证/银行卡如何实名微信? 亲测有效的几种方法分享  抖音网页版平台入口 抖音网页版官网在线访问教程  1688商家版怎样分析买家画像精准供货_1688商家版分析买家画像精准供货【供货策略】  React/Next.js中实现列表项的动态选择与移动  iCloud登录入口网页版 苹果iCloud官网登录  知音漫客正版漫画平台_知音漫客官网账号登录  怎么去除衣服上的口红印_生活小妙招教你用酒精轻松擦除  Tabulator表格日期时间排序问题及自定义解决方案  极速漫画官方主页网址 极速漫画漫画在线浏览官网链接  LINUX怎么设置定时任务_LINUX crontab配置教程  C++的std::mdspan是什么_C++23中用于操作多维数组的非拥有视图  一加 14R 快充无反应_一加 14R 充电优化  QQ邮箱稳定登录入口_QQ邮箱官方网站网页版使用  PyTorch模型训练准确率不提升:诊断与修复常见指标计算错误  抖音网页版快捷访问 抖音网页版网页版入口操作教程  顺丰快件物流信息 官方网站查询入口  Win10磁盘清理工具在哪 Win10打开并使用磁盘清理【教程】  4399免费游戏网址入口 4399小游戏免费入口点开即玩  MAC怎么让Dock栏只显示当前运行的应用_MAC终端命令实现极简Dock栏  Python模块化编程:有效管理依赖与避免循环引用  Python中高效访问嵌套字典与列表中的键值对  小红书怎么解除第三方平台绑定_小红书多平台登录解绑方法介绍  如何在CSS中使用visited与link控制链接颜色_visited link伪类配合  c++项目目录结构应该如何组织_c++工程化项目结构规范  蛙漫限时开放最深处链接_蛙漫全站漫画会员同款秒开地址  在WordPress中通过REST API获取BasicAuth保护的远程文章  淘宝支付提示失败如何解决 淘宝支付流程优化方法  单12V-2×6实现为RTX 5090供电750W!甚至都没敢跑分  漫蛙漫画官方首页 漫蛙2漫画在线阅读入口  J*aScript map 迭代中检测空数组元素的有效方法 

您的项目需求

*请认真填写需求信息,我们会在24小时内与您取得联系。