本文详细介绍了在 `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 类:
# python
# git
# 编码
# app
# 回调函数
# ai
# 常见问题
# 标准库
相关文章:
宝塔建站后网页无法访问如何解决?
如何选择适合PHP云建站的开源框架?
如何在Ubuntu系统下快速搭建WordPress个人网站?
如何在阿里云虚拟主机上快速搭建个人网站?
建站VPS选购需注意哪些关键参数?
大连 网站制作,大连天途有线官网?
公司网站制作价格怎么算,公司办个官网需要多少钱?
青岛网站建设如何选择本地服务器?
如何基于云服务器快速搭建网站及云盘系统?
制作网站外包平台,自动化接单网站有哪些?
深圳网站制作设计招聘,关于服装设计的流行趋势,哪里的资料比较全面?
武清网站制作公司,天津武清个人营业执照注销查询系统网站?
高性能网站服务器配置指南:安全稳定与高效建站核心方案
如何高效配置IIS服务器搭建网站?
宿州网站制作公司兴策,安徽省低保查询网站?
建站一年半SEO优化实战指南:核心词挖掘与长尾流量提升策略
香港服务器WordPress建站指南:SEO优化与高效部署策略
大连网站制作公司哪家好一点,大连买房网站哪个好?
如何选择建站程序?包含哪些必备功能与类型?
如何通过WDCP绑定主域名及创建子域名站点?
高性能网站服务器部署指南:稳定运行与安全配置优化方案
小说建站VPS选用指南:性能对比、配置优化与建站方案解析
如何批量查询域名的建站时间记录?
建站之星在线客服如何快速接入解答?
北京制作网站的公司排名,北京三快科技有限公司是做什么?北京三快科技?
企业网站制作费用多少,企业网站空间一般需要多大,费用是多少?
微信小程序 五星评分(包括半颗星评分)实例代码
太平洋网站制作公司,网络用语太平洋是什么意思?
专业制作网站的公司哪家好,建立一个公司网站的费用.有哪些部分,分别要多少钱?
建站主机功能解析:服务器选择与快速搭建指南
建站之星IIS配置教程:代码生成技巧与站点搭建指南
建站之星客服服务时间及联系方式如何?
建站之星如何实现五合一智能建站与营销推广?
个人摄影网站制作流程,摄影爱好者都去什么网站?
如何通过远程VPS快速搭建个人网站?
娃派WAP自助建站:免费模板+移动优化,快速打造专业网站
制作网站公司那家好,网络公司是做什么的?
建站之星各版本价格是多少?
魔方云NAT建站如何实现端口转发?
西安制作网站公司有哪些,西安货运司机用的最多的app或者网站是什么?
如何配置FTP站点权限与安全设置?
文字头像制作网站推荐软件,醒图能自动配文字吗?
导航网站建站方案与优化指南:一站式高效搭建技巧解析
如何选择服务器才能高效搭建专属网站?
公司网站制作费用多少,为公司建立一个网站需要哪些费用?
内部网站制作流程,如何建立公司内部网站?
学校免费自助建站系统:智能生成+拖拽设计+多端适配
建站主机选哪种环境更利于SEO优化?
宠物网站制作html代码,有没有专门介绍宠物如何养的网站啊?
阿里云高弹*务器配置方案|支持分布式架构与多节点部署
*请认真填写需求信息,我们会在24小时内与您取得联系。