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

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) {

问题原因分析
  1. 方法调用大小写错误:代码第30行调用了buttonInteraction.Reply()(首字母大写),但Discord.js的正确方法是buttonInteraction.reply()(首字母小写)。这个错误导致用户无角色时,交互没有被正确确认,Discord会一直等待响应,二次触发按钮时旧的未确认交互仍然存在,从而抛出「交互已被确认」的错误。
  2. 全局事件监听逻辑混乱:虽然使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 21:47:08