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
相关产品推荐
相关产品推荐

