You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Discord.js斜杠命令自动注册功能故障排查求助

Discord.js Bot 斜杠命令自动注册故障排查

我正在开发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可能还没加载完所有模块,或者加载的模块因格式问题未被识别,导致收集到的命令为空。


修复建议

  1. 移除重复的Ready监听:删除registerCommands.js里的client.once('ready'),将命令注册逻辑直接放在模块导出的函数中,确保在index.js调用时立即执行。

  2. 统一模块加载逻辑:修改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);
        }
    };
    
  3. 统一API版本:将registerCommands.js中的discord-api-types/v9替换为v10:

    const { Routes } = require('discord-api-types/v10');
    
  4. 调整命令注册流程:在index.js的ready事件中,加载完所有模块后,调用registerCommands.js的注册逻辑,直接使用client.commands收集到的命令进行注册,避免重复读取文件。


内容的提问来源于stack exchange,提问作者Elser Pilot Car

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 10:38:09