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

Discord.js设置命令交互报错排查及实现方案咨询

解决Discord机器人Settings命令的交互错误与模式选择问题

一、核心错误原因与修复方案

1. 重复创建Collector导致的冲突

当前代码在InteractionCreate事件中,每次触发任何交互都会创建新的Collector,这会导致多个Collector同时监听同一组件,引发重复响应、40060(交互已被确认)和InteractionNotReplied错误。

修复: 将Collector的创建逻辑移至Settings命令的execute函数中,仅在用户触发命令并收到初始回复后创建Collector,绑定到当前会话:

async execute(client, interaction) {
    // 原有创建选择菜单、按钮和初始Embed的代码保持不变
    const selectMenu = new StringSelectMenuBuilder()
        .setCustomId('settings')
        .setPlaceholder('Выберите подходящую настройку')
        .addOptions(
            new StringSelectMenuOptionBuilder()
                .setLabel('Цвета')
                .setEmoji('🎨')
                .setDescription('Изменить цвет эмбедов')
                .setValue('color'),
        );

    const deleteButton = new ButtonBuilder()
        .setCustomId('delete-message')
        .setLabel('Закрыть')
        .setStyle(ButtonStyle.Danger);

    const row = new ActionRowBuilder().addComponents(selectMenu);
    const buttonsRow = new ActionRowBuilder().addComponents(deleteButton);

    const startEmbed = new EmbedBuilder()
        .setTitle("Ожидается выбор конфигурации")
        .setDescription(`> Выберите нужную вам конфигурацию через меню выбора ниже\n`)
        .addFields({
            name: "Текущая конфигурация:",
            value: `Цвет сообщений: \`${(await client.getColor(interaction.guildId)).toString(16)}\``
        })
        .setColor(await client.getColor(interaction.guildId));

    // 发送初始回复
    await interaction.reply({embeds: [startEmbed], components: [row, buttonsRow]});

    // 创建绑定当前会话的Collector
    const filter = i => i.user.id === interaction.user.id;
    const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 });

    collector.on('collect', async i => {
        try {
            if (i.isStringSelectMenu()) {
                if (i.values[0] === 'color') {
                    // 加载服务器配置并创建颜色设置界面组件
                    const guildConfig = await guild.findOne({id: interaction.guildId});
                    const colorButtons = new ActionRowBuilder()
                        .addComponents(
                            new ButtonBuilder().setCustomId('back').setLabel('Назад').setStyle(ButtonStyle.Secondary),
                            new ButtonBuilder().setCustomId('change-color').setLabel('Изменить цвет').setStyle(ButtonStyle.Secondary),
                            new ButtonBuilder().setCustomId('reset-color').setLabel('Сбросить').setStyle(ButtonStyle.Danger).setDisabled(!guildConfig?.embed_color)
                        );

                    const colorEmbed = new EmbedBuilder()
                        .setTitle('Настройка цвета сообщений бота')
                        .setDescription('> Воспользуйтесь кнопками ниже для взаимодействия с меню.\n> Изменение цвета сообщений подразумевает изменение цвета, который отображается слева во всех эмбед сообщениях.\n\n' +
                            'Вы также можете воспользоваться утилитой для подбора HEX цвета!\n\n' +
                            'Чтобы изменить цвет, нажмите кнопку "Изменить цвет", затем введите HEX-код цвета в модальном окне и подтвердите.\n\n' +
                            '> Примечание: цвет должен быть в формате HEX (например, #FF0000 для красного цвета).')
                        .addFields({
                            name: "Текущий цвет сообщений:",
                            value: `\`#${(await client.getColor(interaction.guildId)).toString(16)}\``
                        })
                        .setColor(await client.getColor(interaction.guildId));

                    await i.update({embeds: [colorEmbed], components: [colorButtons]});
                }
            }

            if (i.customId === 'delete-message') {
                await i.message.delete();
                collector.stop("Сообщение удалено");
                return;
            }

            if (i.customId === 'back') {
                const guildConfig = await guild.findOne({id: interaction.guildId});
                const backEmbed = new EmbedBuilder()
                    .setTitle("Ожидается выбор конфигурации")
                    .setDescription(`> Выберите нужную вам конфигурацию через меню выбора ниже\n`)
                    .addFields({
                        name: "Текущая конфигурация:",
                        value: `Цвет сообщений: ${guildConfig?.embed_color || 'стандартный'}`
                    })
                    .setColor(guildConfig?.embed_color || '#4860fa');

                await i.update({embeds: [backEmbed], components: [row, buttonsRow]});
            }

            if (i.customId === 'reset-color') {
                await guild.findOneAndDelete({id: interaction.guildId});
                const defaultColor = await client.getColor(interaction.guildId);
                // 更新Embed和按钮状态
                const updatedEmbed = EmbedBuilder.from(i.message.embeds[0])
                    .setFields({name: "Текущий цвет сообщений:", value: `\`стандартный (#${defaultColor.toString(16)})\``})
                    .setColor(defaultColor);
                const updatedButtons = ActionRowBuilder.from(i.message.components[0])
                    .setComponents(i.message.components[0].components.map(btn => 
                        btn.customId === 'reset-color' ? btn.setDisabled(true) : btn
                    ));

                await i.update({embeds: [updatedEmbed], components: [updatedButtons]});
            }

            if (i.customId === 'change-color') {
                const modalId = `color-modal-${interaction.user.id}`;
                await i.showModal(
                    new ModalBuilder()
                        .setCustomId(modalId)
                        .setTitle("Напишите hex код цвета")
                        .addComponents(new ActionRowBuilder()
                            .addComponents(
                                new TextInputBuilder()
                                    .setCustomId("color-input")
                                    .setLabel("HEX")
                                    .setStyle(1)
                                    .setMinLength(7)
                                    .setMaxLength(7)
                                    .setPlaceholder("#FFFFFF")
                                    .setRequired(true)
                            )
                        )
                );

                try {
                    const modalInteraction = await i.awaitModalSubmit({
                        filter: m => m.customId === modalId && m.user.id === interaction.user.id,
                        time: 60000
                    });

                    const newColor = modalInteraction.fields.getTextInputValue("color-input").replaceAll("#", "");
                    const isValidHex = /^[0-9A-F]{6}$/i.test(newColor);
                    if (!isValidHex) {
                        await modalInteraction.reply({ content: `\`${newColor}\` не является правильным HEX кодом`, ephemeral: true });
                        return;
                    }

                    await guild.updateOne({id: interaction.guildId}, {$set: {embed_color: newColor}}, {upsert: true});
                    const parsedColor = parseInt(newColor, 16);

                    // 更新主消息
                    const updatedEmbed = EmbedBuilder.from(i.message.embeds[0])
                        .setFields({name: "Текущий цвет сообщений:", value: `\`#${newColor}\``})
                        .setColor(parsedColor);
                    const updatedButtons = ActionRowBuilder.from(i.message.components[0])
                        .setComponents(i.message.components[0].components.map(btn => 
                            btn.customId === 'reset-color' ? btn.setDisabled(false) : btn
                        ));

                    await i.update({embeds: [updatedEmbed], components: [updatedButtons]});
                    await modalInteraction.reply({ content: "Цвет успешно обновлен", ephemeral: true });
                } catch (err) {
                    if (!i.replied) await i.reply({ content: "Время ввода истекло", ephemeral: true });
                }
            }
        } catch (error) {
            console.error("Ошибка обработки компонента:", error);
            // 避免重复回复
            if (!i.replied && !i.deferred) {
                await i.reply({ content: "Произошла ошибка при обработке запроса", ephemeral: true });
            }
        }
    });

    collector.on('end', async () => {
        try {
            const originalMessage = await interaction.fetchReply();
            await originalMessage.edit({components: []});
        } catch (err) {
            console.log("Сообщение уже удалено");
        }
    });
}

2. 交互响应逻辑错误修复

  • 使用i.update()替代i.editReply():组件交互需要用update来更新自身对应的消息,editReply仅针对原始命令交互。
  • 模态框处理改用await而非.then():避免未处理的Promise rejection,同时确保只回复一次。
  • 异常处理中检查交互状态:在catch块中判断交互是否已被响应,防止重复回复引发40060错误。

3. 追踪命令发起用户

  • Collector模式:直接在filter中使用interaction.user.id(原始命令发起者的ID),限制只有发起者能操作组件。
  • 事件模式:在组件的customId中嵌入用户ID(如settings-back-123456789),在InteractionCreate事件中解析customId并验证用户身份。

二、Collector vs 事件模式的选择

Collector模式(推荐当前场景)

  • 适用场景:临时会话式交互(如Settings菜单,有超时限制)。
  • 优点:自动绑定到特定会话,超时后自动清理,逻辑集中,无需全局监听所有交互。
  • 缺点:无法处理持久化组件(如服务器固定消息中的按钮)。

事件模式

  • 适用场景:持久化组件(如常驻消息中的功能按钮)。
  • 优点:可以处理长期存在的组件交互。
  • 缺点:需要手动管理用户验证和会话生命周期,容易出现冲突。

对于Settings命令这种临时、一次性的交互,Collector模式逻辑更清晰,也更容易避免错误。

三、其他优化点

  • 提前获取数据库数据,避免在Collector回调中重复查询,提升性能。
  • 组件customId语义化命名,方便后续扩展功能。
  • 处理模态框超时情况,避免未捕获的异常。

内容的提问来源于stack exchange,提问作者Snak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 19:09:50