不少人在写 Discord 机器人时会想到一个需求:机器人一上线,就自动把当前可用的 Slash 命令清单或对应的帮助内容发到某个频道,方便群成员第一时间了解能用哪些指令。这个需求看起来简单,但实际实现时涉及事件钩子的选择、命令对象的读取方式以及频道权限的获取,稍不注意就会踩坑。本文以 Python 的 discord.py 为主、discord.js 为辅,完整讲解实现思路。

一、核心思路:在 on_ready 中读取并推送命令
discord.py 提供了 setup_hook 和 on_ready 两个可以用来做启动逻辑的钩子。两者的区别在于触发时机:setup_hook 在机器人登录完成、但还没有连接到网关之前触发,此时不能进行任何需要网关连接的操作;而 on_ready 在与 Discord 网关建立连接后触发,此时可以正常获取频道、发送消息。因此发送消息的操作要放在 on_ready 或者 setup_hook 中通过 await bot.wait_until_ready() 等待就绪后再执行。
另一个需要注意的点是 on_ready 并不保证只触发一次。网络波动导致重连时,这个事件会被再次调用,如果不做防护,机器人会把命令列表重复发送多次。常见的处理办法是用一个模块级布尔变量做标记,或者在第一次发送后取消监听。
二、用 discord.py 获取 Slash 命令并自动发送
discord.py 从 2.0 版本开始原生支持应用命令,通过 bot.tree 可以访问已注册的斜杠命令。注意 bot.tree.get_commands() 拿到的是本地缓存的命令对象,如果命令尚未同步到 Discord 服务器,本地列表和实际列表可能不一致。更稳妥的做法是调用 await bot.tree.fetch_commands(),它会向 Discord 发起真实请求,返回全局命令;如果命令注册在特定服务器(guild),则需要传入 guild 参数。
下面的示例演示了完整流程:机器人就绪后,拉取已同步的 Slash 命令,把命令名和描述拼成一条消息,发送到指定频道。频道获取可以通过 ID 也可以通过名称加服务器对象。
import discord
from discord import app_commands
from discord.ext import commands
TOKEN = "你的机器人令牌"
CHANNEL_ID = 123456789012345678 # 要推送的频道ID
class MyBot(commands.Bot):
def __init__(self):
super().__init__(command_prefix="!", intents=discord.Intents.default())
async def setup_hook(self):
# 同步斜杠命令到当前服务器(开发阶段建议绑定单个guild,同步更快)
guild = discord.Object(id=987654321098765432)
self.tree.copy_global_to(guild=guild)
await self.tree.sync(guild=guild)
bot = MyBot()
@bot.tree.command(name="ping", description="测试延迟")
async def ping(interaction: discord.Interaction):
await interaction.response.send_message("pong!")
_sent = False
@bot.event
async def on_ready():
global _sent
if _sent: # 防止重连时重复发送
return
_sent = True
# 从 Discord 服务器拉取真实已注册的命令
cmds = await bot.tree.fetch_commands()
lines = [f"/{c.name} —— {c.description or '暂无描述'}" for c in cmds]
embed = discord.Embed(
title="我已上线,以下是当前可用的命令",
description="\n".join(lines) if lines else "暂无命令",
color=discord.Color.green()
)
channel = bot.get_channel(CHANNEL_ID)
if channel is None:
channel = await bot.fetch_channel(CHANNEL_ID)
await channel.send(embed=embed)
bot.run(TOKEN)代码里有几个细节值得说明。第一,copy_global_to 加 sync(guild=...) 的组合适合开发阶段,命令会立刻在目标服务器生效;上线后再改成全局同步即可。第二,fetch_commands 返回的对象类型是 app_commands.AppCommand,它和装饰器注册时的 Command 对象不同,前者是 Discord 返回的原始数据,包含命令 ID、描述、选项等字段,适合用来生成帮助文本。第三,发送前最好判断频道类型,避免机器人被拉进语音频道类目时报错。
三、用 discord.js 实现同样的效果
如果技术栈是 Node.js,思路完全一致:在 Client 的 ready 事件中调用 client.application.commands.fetch() 拿到已注册命令,再通过 client.channels.fetch 定位频道发送。discord.js 的事件注册从 v14 开始推荐使用 once 参数,天然解决了重复触发问题。
const { Client, GatewayIntentBits, EmbedBuilder, Events } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, async (c) => {
// 拉取全局已注册的斜杠命令
const cmds = await c.application.commands.fetch();
const lines = cmds.map(cmd => `/${cmd.name} —— ${cmd.description || '暂无描述'}`);
const embed = new EmbedBuilder()
.setTitle('我已上线,以下是当前可用的命令')
.setDescription(lines.length ? lines.join('\n') : '暂无命令')
.setColor(0x57F287);
const channel = await c.channels.fetch('123456789012345678');
if (channel && channel.isTextBased()) {
await channel.send({ embeds: [embed] });
}
});
client.login('你的机器人令牌');这里用 client.once 代替 client.on,事件只触发一次,不需要额外做防重标记。如果命令是按服务器注册的,则要改成通过 guild.commands.fetch() 获取对应服务器的命令列表。
四、常见坑点与优化建议
首先是消息权限问题。机器人向频道发送消息需要在该频道拥有 Send Messages 和 Embed Links 权限,缺了后者 Embed 会被当作空消息发送失败。上线前建议在服务器设置里给机器人的角色补齐这两个权限,或在代码中捕获 Forbidden 异常并记录日志。
其次是命令同步延迟。全局命令同步有时需要几分钟才生效,如果你发现 fetch_commands 拿到的列表和预期不符,多半是同步还没完成。开发阶段绑定单一 guild 可以把同步时间压缩到秒级,这是官方文档明确推荐的调试方式。
最后可以考虑把推送逻辑做得更通用一些,比如把频道 ID 存到配置文件或数据库,支持多个服务器各自配置推送频道;或者给每条命令加上参数说明和用法示例,让推送出来的内容本身就是一份使用手册。这样机器人每次上线或命令更新后重新推送,群成员始终能看到最新的命令说明,体验会好很多。
Discord机器人Slash命令discord.py修改时间:2026-09-16 00:04:39