Discord机器人删除角色按钮二次触发报Interaction已确认错误
问题:Discord角色删除功能二次触发报错「Interaction has already been acknowledged」
我做了一个带角色设置的菜单,其中包含角色删除功能。首次运行正常,但二次触发时出现错误。
实现代码
const { ButtonBuilder, ActionRowBuilder, StringSelectMenuBuilder, ButtonStyle, } = require('discord.js'); const database = require('../../database'); async function removeRoleButton(buttonInteraction) { const guildId = buttonInteraction.guild.id; const userId = buttonInteraction.user.id; // Get personal roles from the database const personalRoleIds = await database.getPersonalRoleFromDB2(guildId, userId); const personalRoles = personalRoleIds && personalRoleIds.length > 0 ? (await Promise.all(personalRoleIds.map(roleId => buttonInteraction.guild.roles.fetch(roleId).catch(() => null)))).filter(role => role) : []; if (personalRoles.length === 0) { await buttonInteraction.Reply({ content: 'У вас нет личной роли для удаления.', ephemeral: true, }); } else { // Create role selector menu const roleSelector = generateRoleSelector(personalRoles); const roleSelectActionRow = new ActionRowBuilder().addComponents(roleSelector); const removeButton = new ButtonBuilder() .setCustomId('removeRole') .setLabel('Удалить роль') .setStyle(ButtonStyle.Danger); const removeActionRow = new ActionRowBuilder().addComponents(removeButton); await buttonInteraction.reply({ content: 'Выберите личную роль для удаления:', components: [roleSelectActionRow, removeActionRow], ephemeral: true, }); function generateRoleSelector(personalRoles) { const roleSelector = new StringSelectMenuBuilder() .setCustomId('roleSelector') .setPlaceholder('Выберите личную роль для удаления') .setMaxValues(1); const options = personalRoles.map((role) => ({ label: role.name, value: role.id, description: `ID роли: ${role.id}`, })); roleSelector.addOptions(options); return roleSelector; } async function handleRoleSelectorInteraction(selectInteraction) { // Check if the user who caused the interaction is the same as the one who clicked the remove button if (selectInteraction.user.id !== buttonInteraction.user.id) return; const selectedRoleId = selectInteraction.values[0]; const selectedRole = selectInteraction.guild.roles.cache.get(selectedRoleId); // Remove role const removeRoleSelector = new StringSelectMenuBuilder() .setCustomId('removeRoleSelector') .setPlaceholder('Выберите, что делать с участниками этой роли') .addOptions([ { label: 'Удалить роль у всех участников', value: 'remove', description: 'Это действие удалит выбранную личную роль у всех участников, у которых она есть.', emoji: '🗑️', }, { label: 'Отменить удаление', value: 'cancel', description: 'Отменить удаление личной роли.', emoji: '❌', }, ]); const removeRoleActionRow = new ActionRowBuilder().addComponents(removeRoleSelector); await selectInteraction.update({ content: `Выбрана личная роль "${selectedRole.name}" для удаления. Выберите, что делать с участниками этой роли.`, components: [removeRoleActionRow], }); // Remove the listener to avoid duplicate handling buttonInteraction.client.removeListener('interactionCreate', handleRoleSelectorInteraction); async function handleConfirmationInteraction(confirmInteraction) { // Check if the user who caused the interaction is the same as the one who clicked the remove button if (confirmInteraction.user.id !== buttonInteraction.user.id) return; const action = confirmInteraction.values[0]; if (action === 'remove') { // Remove the role from all members await Promise.all( selectedRole.members.map((member) => member.roles.remove(selectedRole).catch(() => null)) ); await selectedRole.delete(); // Remove the role from the database await database.removePersonalRoleFromDB2(guildId, userId, selectedRoleId); await confirmInteraction.update({ content: `Личная роль "${selectedRole.name}" успешно удалена.`, components: [], }); } else { await confirmInteraction.update({ content: `Удаление личной роли "${selectedRole.name}" отменено.`, components: [], }); } } buttonInteraction.client.once('interactionCreate', async (confirmInteraction) => { if (!confirmInteraction.isStringSelectMenu() || confirmInteraction.customId !== 'removeRoleSelector') return; await handleConfirmationInteraction(confirmInteraction); }); } buttonInteraction.client.once('interactionCreate', async (selectInteraction) => { if (!selectInteraction.isStringSelectMenu() || selectInteraction.customId !== 'roleSelector') return; await handleRoleSelectorInteraction(selectInteraction); }); } } module.exports = { execute: removeRoleButton };
报错信息
DiscordAPIError[40060]: Interaction has already been acknowledged. at handleErrors (C:\Users\User\Desktop\bot discord\node_modules@discordjs\rest\dist\index.js:640:13) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async BurstHandler.runRequest (C:\Users\User\Desktop\bot discord\node_modules@discordjs\rest\dist\index.js:736:23) at async REST.request (C:\Users\User\Desktop\bot discord\node_modules@discordjs\rest\dist\index.js:1387:22) at async ButtonInteraction.reply (C:\Users\User\Desktop\bot discord\node_modules\discord.js\src\structures\interfaces\InteractionResponses.js:111:5) at async Object.removeRoleButton [as execute] (C:\Users\User\Desktop\bot discord\buttons\role\roleDelete.js:36:5) {
问题原因分析
- 方法调用大小写错误:代码第30行调用了
buttonInteraction.Reply()(首字母大写),但Discord.js的正确方法是buttonInteraction.reply()(首字母小写)。这个错误导致用户无角色时,交互没有被正确确认,Discord会一直等待响应,二次触发按钮时旧的未确认交互仍然存在,从而抛出「交互已被确认」的错误。 - 全局事件监听逻辑混乱:虽然使用
once注册监听器,但尝试移除handleRoleSelectorInteraction监听器的操作无效——once注册的监听器触发后会自动移除,且此处removeListener找不到对应引用,可能导致后续触发时出现冲突。
解决方案
1. 修复方法调用大小写错误
将第30行的buttonInteraction.Reply()改为buttonInteraction.reply(),确保无角色场景下交互能被正确确认。
2. 改用awaitMessageComponent替代全局事件监听
Discord.js的Interaction对象提供了awaitMessageComponent方法,可更安全地等待后续交互,避免全局事件监听带来的冲突。重构后的完整代码如下:
const { ButtonBuilder, ActionRowBuilder, StringSelectMenuBuilder, ButtonStyle, } = require('discord.js'); const database = require('../../database'); async function removeRoleButton(buttonInteraction) { const guildId = buttonInteraction.guild.id; const userId = buttonInteraction.user.id; // 获取数据库中的个人角色 const personalRoleIds = await database.getPersonalRoleFromDB2(guildId, userId); const personalRoles = personalRoleIds && personalRoleIds.length > 0 ? (await Promise.all(personalRoleIds.map(roleId => buttonInteraction.guild.roles.fetch(roleId).catch(() => null)))).filter(role => role) : []; if (personalRoles.length === 0) { // 修复大小写错误 await buttonInteraction.reply({ content: 'У вас нет личной роли для удаления.', ephemeral: true, }); return; // 提前返回,避免执行后续逻辑 } // 创建角色选择菜单 function generateRoleSelector(personalRoles) { const roleSelector = new StringSelectMenuBuilder() .setCustomId('roleSelector') .setPlaceholder('Выберите личную роль для удаления') .setMaxValues(1); const options = personalRoles.map((role) => ({ label: role.name, value: role.id, description: `ID роли: ${role.id}`, })); roleSelector.addOptions(options); return roleSelector; } const roleSelector = generateRoleSelector(personalRoles); const roleSelectActionRow = new ActionRowBuilder().addComponents(roleSelector); await buttonInteraction.reply({ content: 'Выберите личную роль для удаления:', components: [roleSelectActionRow], ephemeral: true, }); try { // 等待用户选择角色 const selectInteraction = await buttonInteraction.awaitMessageComponent({ filter: i => i.customId === 'roleSelector' && i.user.id === userId, time: 60000 // 60秒超时 }); const selectedRoleId = selectInteraction.values[0]; const selectedRole = selectInteraction.guild.roles.cache.get(selectedRoleId); // 创建确认选择菜单 const removeRoleSelector = new StringSelectMenuBuilder() .setCustomId('removeRoleSelector') .setPlaceholder('Выберите, что делать с участниками этой роли') .addOptions([ { label: 'Удалить роль у всех участников', value: 'remove', description: 'Это действие удалит выбранную личную роль у всех участников, у которых она есть.', emoji: '🗑️', }, { label: 'Отменить удаление', value: 'cancel', description: 'Отменить удаление личной роли.', emoji: '❌', }, ]); const removeRoleActionRow = new ActionRowBuilder().addComponents(removeRoleSelector); await selectInteraction.update({ content: `Выбрана личная роль "${selectedRole.name}" для удаления. Выберите, что делать с участниками этой роли.`, components: [removeRoleActionRow], }); // 等待用户确认操作 const confirmInteraction = await selectInteraction.awaitMessageComponent({ filter: i => i.customId === 'removeRoleSelector' && i.user.id === userId, time: 60000 }); const action = confirmInteraction.values[0]; if (action === 'remove') { // 移除所有成员的角色 await Promise.all( selectedRole.members.map((member) => member.roles.remove(selectedRole).catch(() => null)) ); await selectedRole.delete(); await database.removePersonalRoleFromDB2(guildId, userId, selectedRoleId); await confirmInteraction.update({ content: `Личная роль "${selectedRole.name}" успешно удалена.`, components: [], }); } else { await confirmInteraction.update({ content: `Удаление личной роли "${selectedRole.name}" отменено.`, components: [], }); } } catch (error) { // 处理超时或其他错误 if (!buttonInteraction.replied) { await buttonInteraction.reply({ content: '操作超时或发生错误.', ephemeral: true, }); } else { await buttonInteraction.editReply({ content: '操作超时或发生错误.', components: [], }); } } } module.exports = { execute: removeRoleButton };
关键改进点
- 修复了
Reply的大小写错误,确保交互被正确确认。 - 用
awaitMessageComponent替代全局once事件监听,每个交互流程独立,避免二次触发时的冲突。 - 添加了超时处理,防止交互一直挂起。
- 移除了冗余的
removeRole按钮(原代码中该按钮未绑定逻辑)。
内容的提问来源于stack exchange,提问作者Flipdanger
相关产品推荐
相关产品推荐

