分类: Discord 机器人开发
Discord 机器人开发#12管理命令冷却时间(Cooldown)与权限(Permission)!(Python 示例)
本文译自韩语原文。
目录12 项
大家好!我是 Mojangcam 😎
上一篇 #11 我们用 Flask 做了一个查看机器人状态的网页仪表盘。现在机器人的功能已经相当齐全了,可真正放进服务器里用的时候,一定会遇到这样的情况。
比如有人每秒狂按 10 次 /뽑기,把频道刷屏;或者随便谁都能执行 /청소,把别人的聊天记录一下子全删光。😱
所以这一篇我们来学习命令冷却时间(Cooldown)和权限(Permission)·身份组(Role)检查。只要加上这两样,机器人立刻就有了"正经运营的机器人"的感觉!

🧠 为什么需要冷却时间和权限?
- 防止刷屏:狂按导致 API 请求扎堆,就会触发 Discord 的速率限制(Rate Limit),机器人会整个卡死。
- 平衡性:签到、抽签、经验值这类有奖励的命令,没有冷却时间就失去意义了。
- 防止事故:
/청소、/밴这样的命令要是谁都能用,服务器一天之内就会垮掉。 - 错误处理:检查没通过时,如果机器人一声不吭,用户就会以为"机器人坏了?"然后继续狂按。一定要有提示消息。
📌 准备工作
- Python 3.8 及以上(推荐 3.10+)
- discord.py 2.x —— 本文以最新版本 2.7.1 为准
- Discord 机器人 Token(还没有的话,先去看 #1 彻底掌握 Slash Command!)
- 测试用的 Discord 服务器(可以随意创建身份组的地方)
pip install -U discord.py这样确认版本。
python -c "import discord; print(discord.__version__)"
# 2.7.1⚠️ 1.x 版本不支持斜杠命令。 如果版本号开头不是
2.,一定要用-U选项升级。
📌 第 1 步 - 加上冷却时间(Cooldown)
给斜杠命令加冷却时间,只要一行装饰器就搞定。
import discord
from discord import app_commands
from discord.ext import commands
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.tree.command(name="출석", description="每天签到一次!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
await interaction.response.send_message(
f"{interaction.user.mention},签到成功!✅"
)我们逐个拆开来看 💡
app_commands.checks.cooldown(rate, per, *, key):斜杠命令专用的冷却装饰器。- rate:在
per秒内最多能用几次。上面的代码是1,也就是 1 次。 - per:冷却时间的长度(秒)。
60.0就是 60 秒 1 次。 - key:决定"按谁来计数"的函数。返回值只要是可哈希的值就行。
key=lambda i: (i.guild_id, i.user.id):按服务器 + 用户的组合计算,所以同一个人在不同服务器的冷却时间是分开计算的。
💡 如果完全省略
key,默认是按用户 ID 计算(所有服务器共用)。反过来,传入key=None就会变成所有人共享的全局冷却。非常适合全服务器的活动命令!
还有,装饰器的顺序很重要。@bot.tree.command() 必须始终放在最上面。放到下面的话,就不会被注册成命令。
📌 第 2 步 - 触发冷却时给出提示
只运行第 1 步的代码的话,触发冷却时机器人没有任何反应。在用户看来,这就是个坏掉的机器人。我们来加上错误处理器吧。
@attend.error
async def attend_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
await interaction.response.send_message(
f"⏳ 太快啦!请在 **{error.retry_after:.1f} 秒**后再试。",
ephemeral=True,
)我们逐个拆开来看 💡
@명령어이름.error:注册该命令专用的错误处理器。参数必须是(interaction, error)2 个。app_commands.CommandOnCooldown:触发冷却时抛出的异常。error.retry_after:告诉你还要再等几秒的 float 值。用:.1f只保留一位小数,看起来更清爽。ephemeral=True:只有执行命令的本人才能看到的消息。可以防止频道被提示消息刷屏。
💡 顺带一提,用
error.cooldown还能取出原本的设置值(rate、per)。想加上"这个命令 60 秒只能用 1 次"这类提示时很有用。
📌 第 3 步 - 检查权限(Permission)
这次来拦住危险的命令。删除消息的命令,应该只有拥有管理消息权限的人才能用。
@bot.tree.command(name="청소", description="一次性删除频道消息")
@app_commands.describe(amount="要删除的消息数量(1~100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
await interaction.response.defer(ephemeral=True)
deleted = await interaction.channel.purge(limit=amount)
await interaction.followup.send(f"🧹 已删除 {len(deleted)} 条消息!", ephemeral=True)我们逐个拆开来看 💡
has_permissions(manage_messages=True):检查用户是否拥有该权限。没有的话会抛出MissingPermissions异常。bot_has_permissions(manage_messages=True):检查机器人是否拥有该权限。没有的话是BotMissingPermissions。漏掉这个,删到一半就会报错!default_permissions(manage_messages=True):对没有权限的人,直接把命令从列表里隐藏。guild_only():禁止在私信(DM)中使用。app_commands.Range[int, 1, 100]:把输入值限制在 1~100。Discord 会自动过滤。
⚠️ 不能只依赖
default_permissions! 官方文档里也明确写着它"只是一个提示(serves as a hint)"。服务器管理员可以在 Discord 设置里改掉这个值。 所以真想拦住的话,一定要同时加上has_permissions。另外,default_permissions在子命令中会被忽略,而且因为是服务器端处理,也不会调用错误处理器。
📌 第 4 步 - 用身份组(Role)来限制
很多时候,我们不想按权限,而是想用"运营团队"这种在服务器里自己创建的身份组来限制。
@bot.tree.command(name="공지", description="发送服务器公告")
@app_commands.describe(message="公告内容")
@app_commands.checks.has_any_role("관리자", "운영진")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
embed = discord.Embed(title="📢 公告", description=message, color=0x5865F2)
embed.set_footer(text=f"发布者:{interaction.user.display_name}")
await interaction.response.send_message(embed=embed)我们逐个拆开来看 💡
has_role("이름"):要求拥有某一个特定身份组。没有的话是MissingRole。has_any_role("A", "B"):列出的身份组中只要有一个就能通过。没有的话是MissingAnyRole。- 身份组除了用名称(字符串),也可以用 ID(整数)指定。名称容易打错或被改名,正式上线的服务推荐用 ID。
@app_commands.checks.has_any_role(123456789012345678, 987654321098765432)💡 附加内容 - 让 VIP 免除冷却时间(dynamic_cooldown)
"赞助者身份组的冷却时间短一点"这种需求也能做到。用 dynamic_cooldown 就行。
from typing import Optional
VIP_ROLE_ID = 123456789012345678 # 免除冷却时间的身份组 ID
def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
user = interaction.user
if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
return None # 返回 None 就没有冷却时间!
return app_commands.Cooldown(1, 30.0) # 普通用户 30 秒 1 次
@bot.tree.command(name="뽑기", description="来一次随机抽签")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
await interaction.response.send_message(f"🎲 结果:**{random.randint(1, 100)} 分**!")关键在于:返回 None,这个用户的冷却时间就会被完全免除。根据条件自己创建 Cooldown(rate, per) 对象并返回就可以了。
📌 完整代码(可直接复制)
# discord.py 2.7.1 / Python 3.8+
import random
import traceback
from typing import Optional
import discord
from discord import app_commands
from discord.ext import commands
TOKEN = "在这里输入机器人 Token"
VIP_ROLE_ID = 123456789012345678 # 免除冷却时间的身份组 ID
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
await bot.tree.sync()
print(f"✅ 登录成功:{bot.user}")
# ── 1) 冷却时间:按服务器·用户,60 秒 1 次 ──────────────────────
@bot.tree.command(name="출석", description="每天签到一次!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
await interaction.response.send_message(
f"{interaction.user.mention},签到成功!✅"
)
# ── 2) 权限检查:只有拥有管理消息权限的人 ──────────────────
@bot.tree.command(name="청소", description="一次性删除频道消息")
@app_commands.describe(amount="要删除的消息数量(1~100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
await interaction.response.defer(ephemeral=True)
deleted = await interaction.channel.purge(limit=amount)
await interaction.followup.send(f"🧹 已删除 {len(deleted)} 条消息!", ephemeral=True)
# ── 3) 身份组检查:只有 '관리자'(管理员)或 '운영진'(运营团队) ───────────────────
@bot.tree.command(name="공지", description="发送服务器公告")
@app_commands.describe(message="公告内容")
@app_commands.checks.has_any_role("관리자", "운영진")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
embed = discord.Embed(title="📢 公告", description=message, color=0x5865F2)
embed.set_footer(text=f"发布者:{interaction.user.display_name}")
await interaction.response.send_message(embed=embed)
# ── 4) 附加内容:VIP 免除冷却时间 ────────────────────────────
def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
user = interaction.user
if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
return None
return app_commands.Cooldown(1, 30.0)
@bot.tree.command(name="뽑기", description="来一次随机抽签")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
await interaction.response.send_message(f"🎲 结果:**{random.randint(1, 100)} 分**!")
# ── 5) 全局错误处理器(所有命令通用) ───────────────────
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
msg = f"⏳ 太快啦!请在 **{error.retry_after:.1f} 秒**后再试。"
elif isinstance(error, app_commands.MissingPermissions):
msg = "🚫 你没有使用这个命令的权限。"
elif isinstance(error, app_commands.BotMissingPermissions):
msg = "🤖 机器人没有权限!请在 服务器设置 → 身份组 中检查机器人的权限。"
elif isinstance(error, app_commands.MissingAnyRole):
msg = "🎭 这个命令需要管理员或运营团队身份组。"
elif isinstance(error, app_commands.CheckFailure):
msg = "❌ 不满足执行条件。"
else:
msg = "⚠️ 发生了未知错误,请稍后再试。"
traceback.print_exception(type(error), error, error.__traceback__)
if interaction.response.is_done():
await interaction.followup.send(msg, ephemeral=True)
else:
await interaction.response.send_message(msg, ephemeral=True)
bot.run(TOKEN)💡 完整代码里特意去掉了第 2 步的
@attend.error。原因会在下面"常见报错"第 3 条里说明!
📌 运行结果
启动机器人,在服务器里逐个试一下,效果是这样的。
先输入 /출석,会显示签到成功的消息。那紧接着再输入一次呢?冷却提示只有自己能看到。

提示消息下面附有"只有你能看到这条消息(이 메시지는 본인만 볼 수 있어요)"对吧?这就是 ephemeral=True 的效果。频道里不会留下任何痕迹。
权限的差别在命令列表里一眼就能看出来。同一个服务器、同一个机器人,根据账号权限的不同,会变成这样。

没有管理消息权限的账号根本看不到 /청소。因为 default_permissions 在 Discord 服务器端就把它过滤掉了。而且,就算服务器管理员放开了权限设置让命令显示出来,has_permissions 也会再拦一道,所以会显示 "🚫 你没有权限"。
同样地,没有管理员或运营团队身份组的人使用 /공지 会被 "🎭 需要身份组" 拒绝;而用带 VIP 身份组的账号狂按 /뽑기,则可以不受冷却限制一直抽下去。
📌 常见报错
1. TypeError: error handler must have 2 parameters
- 原因:错误处理器函数的参数数量不是 2 个。加了
self或者只接收了一个参数时就会出现。 - 解决:一定要写成
async def handler(interaction, error):,正好 2 个。如果在 Cog 里使用,加上self就变成 3 个了,所以在 Cog 中请重写cog_app_command_error。
2. 触发了冷却,机器人却一句话都不说
- 原因:没有加错误处理器。即使抛出了
CommandOnCooldown,如果没有地方接住,它只会留在日志里,用户什么也收不到。 - 解决:一定要注册
@bot.tree.error全局处理器。(完整代码第 5 块)
3. 提示消息出现两次,或者报 InteractionResponded 错误
- 原因:因为同时注册了单独处理器(
@명령어.error)和全局处理器(@bot.tree.error)。discord.py 在执行完单独处理器之后,仍然一定会调用全局处理器。于是它会试图对已经响应过的交互再响应一次,结果就抛出InteractionResponded。 - 解决:只用其中一个。推荐像完整代码那样统一用全局处理器。如果非要两个都用,就在全局处理器里用
if interaction.response.is_done(): return直接返回。
4. 管理员也遇到 MissingPermissions
- 原因:
has_permissions看的是按频道计算出来的实际权限。如果频道的权限覆盖(Override)里拒绝了该权限,即使是服务器管理员也可能被拦住。 - 解决:到频道设置 → 权限里,确认该身份组的这项权限没有被设成 ❌ 关闭。
5. /청소 执行到一半停住了
- 原因:机器人没有管理消息权限,或者试图删除超过 14 天的消息。Discord 禁止批量删除 14 天以前的消息。
- 解决:先用
bot_has_permissions过滤掉,在老频道里测试时把数量调小一点。
💬 结语
今天我们给 Discord 机器人加上了冷却时间和权限·身份组检查。总结一下 🙌
checks.cooldown(rate, per, key=...)→ 防止刷屏,用key自由设定计数标准checks.has_permissions/bot_has_permissions→ 检查用户·机器人的权限checks.has_any_role→ 按服务器身份组限制(推荐用 ID)default_permissions→ 从列表中隐藏,但它只是提示,禁止单独使用@bot.tree.error→ 提示消息集中在一个地方统一管理
下一篇我们来讲Discord 机器人数据存储 - JSON vs SQLite。想让签到记录、积分在机器人关闭后也能保留下来,这部分内容必不可少。敬请期待!🙌
有疑问或者卡住的地方,欢迎在评论区留言 💬
📚 Discord 机器人开发系列
连载 · 共 32 篇中的第 12 篇
Discord 机器人开发
- #1Discord 机器人开发 - Slash Command 完全攻略!
- #2为 Slash Command 添加选项(Arguments)!
- #3在 Slash Command 中添加子命令(Subcommand)!
- #4用 Slash Command 发送 Embed 嵌入消息!
- #5实现按钮(Button)交互!
- #6实现下拉菜单(Select Menu)!
- #7用模态框(Modal)接收用户输入(Python 示例)
- #8用嵌入消息(Embed)的图片和字段美化消息(Python 示例)
- #9接入 AI(Claude·ChatGPT API)!(Python 示例)
- #10支持多语言(i18n)!(Python 示例)
- #11用 Flask 制作网页仪表盘!(Python 示例)
- #12管理命令冷却时间(Cooldown)与权限(Permission)!(Python 示例)当前文章
- #13用身份组·频道自动化管理服务器!(Python 示例)
- #14保存数据!JSON vs SQLite(Python 示例)
- #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제) (韩语)
- #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제) (韩语)
- #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제) (韩语)
- #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제) (韩语)
- #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제) (韩语)
- #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제) (韩语)
- #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제) (韩语)
- #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제) (韩语)
- #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제) (韩语)
- #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제) (韩语)
- #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제) (韩语)
- #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제) (韩语)
- #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제) (韩语)
- #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제) (韩语)
- #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제) (韩语)
- #30打造投票·问卷(Poll)机器人!原生投票 + 按钮实时统计(Python 示例)🗳️
- #32打造服务器日志·审核日志(Audit Log)频道!自动记录消息删除、编辑、成员进出和踢出(Python 示例)🕵️
- #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (韩语)
