Discord.py 动态命令选项:无需重启实时更新数据库内容

花韻仙語
发布: 2025-12-09 11:55:17
原创
564人浏览过

Discord.py 动态命令选项:无需重启实时更新数据库内容

本教程旨在解决 discord.py 机器人中动态命令选项无法实时更新的问题。文章将深入探讨 `app_commands.choices` 的局限性,并详细介绍如何利用 `app_commands.transformer` 结合异步数据库操作和高效缓存机制,实现命令选项的实时、动态更新。通过示例代码,您将学会如何构建一个响应迅速、数据一致的 discord 机器人,确保用户始终能访问到最新的数据。

引言:Discord.py 动态命令选项的挑战

在开发 Discord 机器人时,我们经常需要为斜杠命令提供一系列选项供用户选择。例如,一个管理课程的机器人可能需要用户选择一个课程标题。当这些选项来源于一个频繁更新的数据库时,传统的 @app_commands.choices 装饰器就显得力不从心。因为它在机器人启动时只评估一次,导致数据库中的新数据无法实时反映到命令选项中,除非重启机器人。这对于需要高实时性的应用来说是不可接受的。

静态选择的局限性

让我们首先审视 @app_commands.choices 的工作方式。以下是一个典型的使用场景:

import discord
from discord import app_commands, Interaction
from typing import List

# 假设 LessonRepository 是一个与数据库交互的类
class LessonRepository:
    @staticmethod
    def get_all_lessons():
        # 模拟从数据库获取所有课程
        # 实际上这里会进行数据库查询
        class LessonDTO:
            def __init__(self, title):
                self.title = title
        return [LessonDTO("数学"), LessonDTO("物理"), LessonDTO("化学")]

def lesson_choices() -> List[app_commands.Choice[str]]:
    """从数据库获取课程列表并转换为 Choices"""
    return [
        app_commands.Choice(name=lesson.title, value=lesson.title)
        for lesson in LessonRepository.get_all_lessons()
    ]

class SomeCog(discord.ext.commands.Cog):
    def __init__(self, bot: discord.ext.commands.Bot):
        self.bot = bot

    @app_commands.command(name="create_or_update_mark")
    @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: app_commands.Choice[str],
                                    logiks: app_commands.Range[int, 0, 8]):
        # ... 处理逻辑 ...
        await interaction.response.send_message(f"已更新 {student.display_name} 的 {lesson.value} 成绩。", ephemeral=True)

# 机器人启动代码
# bot = commands.Bot(command_prefix="!", intents=discord.Intents.all())
# async def setup():
#     await bot.add_cog(SomeCog(bot))
#     await bot.tree.sync()
# bot.run("YOUR_TOKEN")
登录后复制

如上述代码所示,@app_commands.choices(lesson=lesson_choices()) 这一行在机器人启动时,会调用 lesson_choices() 函数一次,并将其返回的结果作为该命令参数的固定选项。如果 LessonRepository.get_all_lessons() 返回的数据在机器人运行期间发生变化(例如,添加了新课程),命令选项列表不会自动更新。

初探自动补全 (Autocomplete) 及其不足

为了解决上述问题,很多开发者会尝试使用 Discord.py 提供的 autocomplete 功能。autocomplete 允许机器人根据用户的输入实时提供建议。以下是一个初步尝试的 autocomplete 实现:

网易人工智能
网易人工智能

网易数帆多媒体智能生产力平台

网易人工智能 233
查看详情 网易人工智能
# ... (LessonRepository 和 LessonDTO 定义同上) ...

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()
    ]

class SomeCog(discord.ext.commands.Cog):
    # ... (init 方法同上) ...

    @app_commands.command(name="create_or_update_mark")
    @app_commands.default_permissions(administrator=True)
    @app_commands.autocomplete(lesson=lesson_autocomplete) # 使用自动补全
    async def create_or_update_mark(self, interaction: Interaction,
                                    student: discord.Member,
                                    lesson: str, # 注意这里 lesson 的类型变为 str
                                    logiks: app_commands.Range[int, 0, 8]):
        # ... 处理逻辑,lesson 现在是用户输入的字符串 ...
        await interaction.response.send_message(f"已更新 {student.display_name} 的 {lesson} 成绩。", ephemeral=True)
登录后复制

尽管 autocomplete 看起来是正确的方向,但上述实现存在几个关键问题:

  1. 阻塞式数据库调用:LessonRepository.get_all_lessons() 很可能是一个同步函数(即它会阻塞事件循环,直到数据库查询完成)。autocomplete 方法会被频繁调用(用户每输入一个字符都会触发),同步的数据库调用会导致机器人响应变慢甚至无响应。
  2. 性能低下:每次 autocomplete 被调用时,都会重新查询整个数据库以获取所有课程。这在用户快速输入时会产生大量的数据库请求,对数据库造成不必要的负担。
  3. 过滤逻辑不完善:current.lower() in lesson.lower() 这种过滤方式,只有当用户输入的字符串是课程标题的完整子串时才会匹配。它缺乏模糊匹配能力,用户体验不佳。
  4. 缺乏数据验证:lesson 参数现在是一个简单的 str。用户可以输入任何文本,而不仅仅是有效的课程标题。在命令执行时,我们还需要手动验证这个字符串是否对应一个实际的课程。

终极解决方案:使用 app_commands.Transformer 实现动态选择

discord.app_commands.Transformer 是一个强大的工具,它允许我们定义复杂的参数转换逻辑和自动补全行为。通过结合 Transformer、异步数据库操作和数据缓存,我们可以构建一个既高效又健壮的动态命令选项系统。

核心策略

  1. 异步数据库操作:将所有与数据库相关的操作改造为异步模式。如果您的数据库库不支持异步,可以考虑使用 run_in_executor 将同步操作放到单独的线程池中执行,但这并非最佳实践,推荐直接切换到异步数据库库。
  2. 数据缓存机制:在机器人启动时,将数据库中的所有课程数据加载到内存中的缓存 (self.lessons_cache)。当数据库数据更新时,也应有机制来更新这个缓存。autocomplete 方法将直接从缓存中获取数据,避免频繁的数据库查询。
  3. 高效自动补全逻辑:利用 Python 标准库 difflib 进行模糊匹配,提供更智能的建议。同时,根据用户已填写的其他参数(例如 student),可以进一步优化建议列表。
  4. Transformer 的 autocomplete 和 transform 方法
    • autocomplete 方法负责根据用户输入提供建议列表(app_commands.Choice)。
    • transform 方法负责在用户最终选择或输入后,将输入值转换为我们需要的实际对象(例如 Lesson 对象),并进行最终的验证。

示例代码:使用 LessonTransformer

首先,我们定义一个 Lesson 类来封装从数据库获取的课程信息,并在机器人中实现一个缓存机制。

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

# 类型别名,增加可读性
GUILD_ID = int
MEMBER_ID = int
LESSON_ID = int

# 假设 Lesson 类代表数据库中的课程数据
class Lesson:
    def __init__(self, id: int, title: str):
        self.id = id
        self.title = title

    def __repr__(self):
        return f"<Lesson id={self.id} title='{self.title}'>"

# 模拟异步数据库操作
class AsyncLessonRepository:
    @staticmethod
    async def get_all_lessons():
        # 模拟异步数据库查询
        await discord.utils.sleep_until(discord.utils.utcnow() + discord.timedelta(milliseconds=50))
        return [
            Lesson(1, "数学"),
            Lesson(2, "物理"),
            Lesson(3, "化学"),
            Lesson(4, "生物"),
            Lesson(5, "历史"),
            Lesson(6, "地理"),
            Lesson(7, "计算机科学"),
        ]

class MyBot(commands.Bot):
    # 类型提示,方便 IDE 识别
    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_object}}}
        # 这里的缓存设计可以根据实际需求调整,例如只缓存所有课程,不按学生区分
        self.lessons_cache: Dict[GUILD_ID, Dict[MEMBER_ID, Dict[LESSON_ID, Lesson]]] = {}
        self.all_lessons_flat_cache: Dict[LESSON_ID, Lesson] = {} # 用于全局课程查找

    async def setup_hook(self):
        """机器人启动时调用,用于加载缓存和同步命令"""
        print("Bot setup_hook called.")
        await self.tree.sync() # 同步斜杠命令
        await self._load_lessons_cache() # 加载课程缓存
        print("Commands synced and cache loaded.")

    async def _load_lessons_cache(self):
        """从数据库加载所有课程到缓存"""
        print("Loading lessons cache...")
        all_lessons = await AsyncLessonRepository.get_all_lessons()
        self.all_lessons_flat_cache = {lesson.id: lesson for lesson in all_lessons}
        # 模拟为每个学生/公会填充缓存,实际中可能需要更复杂的逻辑
        # 这里仅为演示,假设所有课程对所有学生都可用
        for guild_id in self.guilds: # 假设已经加入了公会
            self.lessons_cache[guild_id.id] = {}
            # 简化处理:假设每个学生都能看到所有课程
            # 真实场景下,可能需要根据学生ID从数据库加载其专属课程
            # for student_id in some_student_ids:
            #     self.lessons_cache[guild_id.id][student_id] = self.all_lessons_flat_cache.copy()
        print(f"Lessons cache loaded: {len(self.all_lessons_flat_cache)} lessons.")

    async def update_lesson_cache(self):
        """在数据库更新后,手动调用此函数以刷新缓存"""
        print("Updating lessons cache...")
        await self._load_lessons_cache()
        print("Lessons cache updated.")


class LessonTransformer(app_commands.Transformer):
    async def find_similar_lesson_titles(self, lessons: Dict[LESSON_ID, Lesson], title: str) -> Dict[LESSON_ID, Lesson]:
        """使用 difflib 查找相似的课程标题"""
        # map(lambda l: l.title, lessons.values()) 提取所有课程的标题
        all_titles = [lesson.title for lesson in lessons.values()]
        # difflib.get_close_matches 查找与输入标题相似的标题
        similar_titles = difflib.get_close_matches(title, all_titles, n=15, cutoff=0.6) # cutoff 可调整匹配严格度
        return {lesson.id: lesson for lesson in lessons.values() if lesson.title in similar_titles}

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

        # 从机器人的全局缓存中获取所有课程
        all_lessons_in_guild = interaction.client.all_lessons_flat_cache

        # 检查用户是否已填写 "student" 参数
        student: Optional[discord.Member] = interaction.namespace.get('student')

        target_lessons: Dict[LESSON_ID, Lesson]
        if student is None:
            # 如果没有指定学生,则显示所有可用课程
            target_lessons = all_lessons_in_guild
        else:
            # 如果指定了学生,可以根据学生ID进一步过滤课程
            # 这里的逻辑需要根据您的实际缓存结构和业务需求调整
            # 示例中,我们假设所有课程对所有学生都可用,所以这里仍然使用全部课程
            # 如果您的缓存是 {guild_id: {student_id: {lesson_id: Lesson}}}
            # 则这里会是 interaction.client.lessons_cache.get(interaction.guild.id, {}).get(student.id, {})
            target_lessons = all_lessons_in_guild

        # 查找与用户输入相似的课程
        similar_lessons = await self.find_similar_lesson_titles(target_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"""
        assert interaction.guild is not None

        # 自动补全建议的 value 是课程 ID (字符串形式),用户也可能手动输入
        if not value.isdigit():
            # 如果用户输入的不是数字(即不是课程 ID),可以尝试根据标题查找
            # 或者直接抛出错误,取决于您的业务逻辑
            # 这里简单处理为抛出错误,要求用户必须从建议中选择或输入有效ID
            raise app_commands.AppCommandError("无效的课程ID或标题。请从建议中选择。")

        lesson_id = int(value)

        # 再次检查学生参数,以便更精确地验证
        student: Optional[discord.Member] = interaction.namespace.get('student')

        # 从缓存中查找课程
        lesson = interaction.client.all_lessons_flat_cache.get(lesson_id)

        if lesson is None:
            raise app_commands.AppCommandError("未找到匹配的课程。")

        # 如果需要,可以在这里添加更复杂的验证,例如检查该课程是否属于指定学生
        # if student is not None and lesson_id not in interaction.client.lessons_cache.get(interaction.guild.id, {}).get(student.id, {}):
        #     raise app_commands.AppCommandError("该课程不属于指定学生。")

        return lesson # 返回 Lesson 对象,方便命令回调函数直接使用

class SomeCog(commands.Cog):
    def __init__(self, bot: MyBot):
        self.bot = bot

    @app_commands.command(name="create_or_update_mark_dynamic")
    @app_commands.guild_only() # 确保只在公会中运行
    @app_commands.default_permissions(administrator=True)
    async def create_or_update_mark_dynamic(
        self,
        interaction: discord.Interaction[MyBot],
        student: discord.Member,
        lesson: app_commands.Transform[Lesson, LessonTransformer], # 使用 Transformer
        logiks: app_commands.Range[int, 0, 8],
    ):
        """动态更新学生成绩的命令"""
        assert interaction.guild is not None

        # lesson 现在已经是经过 Transformer 转换后的 Lesson 对象
        # 我们可以直接访问其属性,例如 lesson.id, lesson.title

        # 假设这里是与数据库交互的逻辑
        # with MarkRepository(student_id=student.id, lesson_title=lesson.title) as lr:
        #     lr.create_or_update(mark_dto=MarkCreateDTO(logiks=logiks))

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

# 机器人启动示例
# intents = discord.Intents.default()
# intents.members = True # 如果需要访问成员信息
# bot = MyBot(command_prefix="!", intents=intents)

# @bot.event
# async def on_ready():
#     print(f'Logged in as {bot.user} (ID: {bot.user.id})')
#     print('------')

# async def main():
#     async with bot:
#         await bot.add_cog(SomeCog(bot))
#         await bot.start("YOUR_BOT_TOKEN")

# if __name__ == "__main__":
#     import asyncio
#     asyncio.run(main())
登录后复制

代码解析

  1. Lesson 类: 这是一个简单的 Python 类,用于表示数据库中的课程记录。在 transform 方法中,我们会将用户的输入最终转换为这个对象。
  2. MyBot 类:
    • 继承 commands.Bot,并添加了一个 lessons_cache 字典来存储从数据库加载的课程数据。
    • setup_hook 方法在机器人启动后被调用

以上就是Discord.py 动态命令选项:无需重启实时更新数据库内容的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

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

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