Discord.js斜杠命令自动注册功能故障排查求助
我正在开发Discord.js Bot,index.js已实现模块自动加载、重载及错误模块禁用功能,运行正常。但其中的registerCommands.js模块无法自动注册所有自动加载模块里的斜杠命令,调试多日无果,求分析问题原因。
相关代码
Index.js
const fs = require('fs'); const path = require('path'); const chokidar = require('chokidar'); const { Client, GatewayIntentBits, ActivityType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, PermissionsBitField, ChannelType, Events, Partials, StringSelectMenuOptionBuilder } = require('discord.js'); let config = require('./config.js'); let guildConfig = require('./guildConfig.js'); // New addition const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildBans, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildIntegrations, GatewayIntentBits.GuildWebhooks, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildMessageTyping, GatewayIntentBits.DirectMessages, GatewayIntentBits.DirectMessageReactions, GatewayIntentBits.DirectMessageTyping, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildScheduledEvents ], partials: [Partials.Channel] }); const ADMIN_CHANNEL_ID = '0000'; // Replace with your specific channel ID in the admin guild let logChannel = null; const modules = new Map(); const getTimeStamp = () => { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${hours}:${minutes}:${seconds}`; }; const logMessage = async (message) => { const timestampedMessage = `[${getTimeStamp()}] ${message}`; originalConsoleLog(timestampedMessage); // Log to console if (logChannel) { try { await logChannel.send(`\`\`\`js ${timestampedMessage} \`\`\``); } catch (error) { originalConsoleLog(`[${getTimeStamp()}] Error sending log message to Discord:`, error); } } else { originalConsoleLog(`[${getTimeStamp()}] Log channel not found. Unable to send log: ${message}`); } }; // Override console.log to use logMessage function const originalConsoleLog = console.log; console.log = async (...args) => { const message = args.join(' '); await logMessage(message); }; const initializeLogChannel = async () => { try { const guild = await client.guilds.fetch(config.adminGuild); logChannel = await guild.channels.fetch(ADMIN_CHANNEL_ID); if (!logChannel) { await logMessage(`Channel with ID ${ADMIN_CHANNEL_ID} not found in guild ${config.adminGuild}`); } } catch (error) { await logMessage(`Error fetching guild or channel: ${error.message}`); } }; const loadModule = async (filePath) => { try { // Attempt to unload the module if it exists await unloadModule(filePath); delete require.cache[require.resolve(filePath)]; const module = require(filePath); if (typeof module === 'function') { module(client, { config, guildConfig }); // Pass config and guildConfig to the module await logMessage(`Loaded module: ${filePath}`); modules.set(filePath, module); } } catch (error) { await logMessage(`Error loading module ${filePath}: ${error.message}`); await logMessage(`Unloaded module due to error: ${error.message}`); } }; const unloadModule = async (filePath, error = null) => { if (modules.has(filePath)) { try { const module = modules.get(filePath); if (module.unload) { module.unload(client); } delete require.cache[require.resolve(filePath)]; modules.delete(filePath); let message = `Unloaded module: ${filePath}`; if (error) { message += ` due to error: ${error.message}`; } await logMessage(message); } catch (unloadError) { await logMessage(`Error unloading module ${filePath}: ${unloadError.message}`); } } }; const loadModules = async (dir) => { try { const files = fs.readdirSync(dir); for (const file of files) { const filePath = path.join(dir, file); if (fs.statSync(filePath).isDirectory()) { await loadModules(filePath); } else if (file.endsWith('.js')) { await loadModule(filePath); } } } catch (error) { await logMessage(`Error loading modules from directory ${dir}: ${error.message}`); } }; client.once('ready', async () => { await logMessage(`Logged in as ${client.user.tag}!`); client.user.setActivity('Being built!', { type: ActivityType.Watching }); await initializeLogChannel(); // Load all modules from the ./bot-modules/ directory await loadModules(path.join(__dirname, 'bot-modules')); // Load and execute the register commands module require('./bot-modules/registerCommands')(client); // Start the watcher after loading all modules const watcher = chokidar.watch(path.join(__dirname, 'bot-modules'), { ignoreInitial: true }); watcher.on('all', (event, filePath) => { if (filePath.endsWith('.js')) { if (event === 'change' || event === 'add') { console.log(`Reloading module: ${filePath}`); loadModule(filePath); } else if (event === 'unlink') { console.log(`Removing module: ${filePath}`); unloadModule(filePath); } } }); // Watch the config.js and guildConfig.js files for changes const configWatcher = chokidar.watch([path.join(__dirname, 'config.js'), path.join(__dirname, 'guildConfig.js')], { ignoreInitial: true }); const reloadConfigs = async (filePath) => { const configName = path.basename(filePath); console.log(`Reloading ${configName}`); delete require.cache[require.resolve(filePath)]; if (configName === 'config.js') { config = require('./config.js'); await logMessage(`Configuration reloaded: ${configName}`); } else if (configName === 'guildConfig.js') { guildConfig = require('./guildConfig.js'); await logMessage(`Configuration reloaded: ${configName}`); } }; configWatcher.on('change', reloadConfigs); }); client.login(config.token);
registerCommands.js
const { REST } = require('@discordjs/rest'); const { Routes } = require('discord-api-types/v9'); const fs = require('fs'); const path = require('path'); module.exports = async (client) => { const commands = []; client.commands = new Map(); try { // Load command files const commandFiles = fs.readdirSync(path.join(__dirname)).filter(file => file.endsWith('.js') && file !== 'registerCommands.js'); for (const file of commandFiles) { console.log(`Loading command file: ${file}`); const command = require(path.join(__dirname, file)); if (command.data && command.execute) { commands.push(command.data.toJSON()); client.commands.set(command.data.name, command); console.log(`Loaded command: ${command.data.name}`); } else { console.warn(`Skipping file: ${file} (missing data or execute)`); } } } catch (error) { console.error('Error loading commands:', error); } const rest = new REST({ version: '10' }).setToken(client.token); client.once('ready', async () => { try { console.log('Started refreshing application (/) commands.'); // Register commands for the guilds specified in the config const { adminGuild, memberGuild } = require('./config.js'); const guildIds = [adminGuild, memberGuild]; for (const guildId of guildIds) { // Register commands in the guild await rest.put( Routes.applicationGuildCommands(client.user.id, guildId), { body: commands } ); console.log(`Successfully reloaded application (/) commands for guild ${guildId}.`); // Log the registered commands console.log('Registered commands:'); commands.forEach(cmd => console.log(`- ${cmd.name}`)); } } catch (error) { console.error('Error registering application (/) commands:', error); } console.log(`Logged in as ${client.user.tag}!`); }); client.on('interactionCreate', async (interaction) => { if (!interaction.isCommand()) return; const command = client.commands.get(interaction.commandName); if (!command) return; try { await command.execute(interaction); } catch (error) { console.error(error); await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); } }); };
斜杠命令示例模块
const { SlashCommandBuilder } = require('discord.js'); console.log('Loading ping command...'); module.exports = { data: new SlashCommandBuilder() .setName('ping') .setDescription('Replies with Pong!'), async execute(interaction) { console.log('Executing ping command...'); await interaction.reply('Pong!'); }, };
config.js
const guildConfig = require('./guildConfig.js'); module.exports = { token: '000000', openAIApiKey: '000000', adminGuild: guildConfig.adminGuild.id, memberGuild: guildConfig.memberGuild.id, guildConfig };
注:公会ID已成功获取,故未附上guildConfig.js文件。
问题原因分析
1. Ready事件监听时机错误
index.js的ready事件触发后,才调用registerCommands.js模块,而registerCommands.js内部又绑定了client.once('ready')。由于ready事件已经触发过,这个监听永远不会执行,导致命令注册的核心逻辑完全没运行。
2. 模块加载逻辑不兼容
index.js的loadModule函数仅加载导出为函数的模块,但斜杠命令模块导出的是包含data和execute的对象,不会被loadModule识别加载。同时registerCommands.js自己读取文件加载命令,但和index.js的模块体系脱节,且因为时机问题没执行注册。
3. API版本不匹配
registerCommands.js使用discord-api-types/v9的Routes,但REST实例指定的是version: '10',版本不一致会导致API调用失败。
4. 命令注册时机错位
即使registerCommands.js的ready监听能触发,它的命令收集逻辑是在模块初始化时执行的,此时index.js可能还没加载完所有模块,或者加载的模块因格式问题未被识别,导致收集到的命令为空。
修复建议
移除重复的Ready监听:删除
registerCommands.js里的client.once('ready'),将命令注册逻辑直接放在模块导出的函数中,确保在index.js调用时立即执行。统一模块加载逻辑:修改
index.js的loadModule函数,支持识别命令对象模块,将其添加到client.commands中:const loadModule = async (filePath) => { try { await unloadModule(filePath); delete require.cache[require.resolve(filePath)]; const module = require(filePath); if (typeof module === 'function') { module(client, { config, guildConfig }); await logMessage(`Loaded module: ${filePath}`); modules.set(filePath, module); } else if (module.data && module.execute) { // 处理命令对象模块 client.commands.set(module.data.name, module); await logMessage(`Loaded command: ${module.data.name}`); modules.set(filePath, module); } } catch (error) { await logMessage(`Error loading module ${filePath}: ${error.message}`); await unloadModule(filePath, error); } };统一API版本:将
registerCommands.js中的discord-api-types/v9替换为v10:const { Routes } = require('discord-api-types/v10');调整命令注册流程:在
index.js的ready事件中,加载完所有模块后,调用registerCommands.js的注册逻辑,直接使用client.commands收集到的命令进行注册,避免重复读取文件。
内容的提问来源于stack exchange,提问作者Elser Pilot Car

