
本文详细介绍了在 `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 参数的可用选项将不会随之改变。
为了实现动态更新,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 实现了动态性,但上述实现存在以下问题:
为了彻底解决上述问题,推荐使用 discord.py 的 app_commands.Transformer 结合本地缓存和异步数据库操作。Transformer 允许我们封装复杂的参数处理逻辑,包括 autocomplete 和最终参数值的转换。
以下是一个完整的示例,展示如何使用 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())Lesson 类和 LessonRepository:
MyBot 类:
LessonTransformer 类:
以上就是Discord.py 动态命令选项:无需重启更新数据库驱动的交互式选择的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号