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

DDD
发布: 2025-12-02 14:15:06
原创
149人浏览过

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

Weights.gg
Weights.gg

多功能的AI在线创作与交流平台

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

驱动精灵
驱动精灵

驱动精灵基于驱动之家十余年的专业数据积累,驱动支持度高,已经为数亿用户解决了各种电脑驱动问题、系统故障,是目前有效的驱动软件,有需要的小伙伴快来保存下载体验吧!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号