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

Discord.js击败Boss后重发命令触发DiscordAPIError[40060]问题

Discord.js 按钮交互错误:Interaction has already been acknowledged

问题现象

使用discord.js开发Boss讨伐功能,用户通过按钮选择攻击招式。击败Boss后重新运行/dungeon命令,Embed能正常发送,但点击按钮会触发DiscordAPIError[40060]错误,页面还会回退到之前的Embed数据。

错误根源

  1. 重复绑定收集器事件:代码中两次调用collector.on('collect', ...),同一按钮交互会触发两个回调,导致重复调用i.deferUpdate()或i.editReply(),触发"交互已确认"错误。
  2. 全局频道收集器:用interaction.channel.createMessageComponentCollector()创建的是频道级全局收集器,旧会话的收集器未销毁,新会话的按钮点击会被旧收集器捕获,引发逻辑混乱。
  3. 共享数据引用:embeds和row为会话内共享变量,多个会话会覆盖同一引用,导致新会话UI被旧数据污染。

修复方案

1. 合并收集器事件回调

将两个collect事件合并为一个,通过customId分支处理不同按钮逻辑,避免重复触发。

2. 使用消息级收集器

替换频道收集器为当前回复消息的收集器,确保只监听当前会话的按钮,会话结束后自动销毁:

const reply = await interaction.reply({ ... });
const collector = reply.createMessageComponentCollector({ time: 120000 }); // 2分钟超时

3. 隔离会话数据

每次会话创建独立的Embed和ActionRow实例,避免跨会话数据污染;使用EmbedBuilder构建Embed,保证实例独立性。

4. 收集器结束时清理状态

在collector.on('end')中禁用按钮或移除组件,避免无效点击。

修复后的完整代码

const { ApplicationCommandType, EmbedBuilder } = require('discord.js');
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const wait = require('node:timers/promises').setTimeout;

const User = require("../../models/user");
const planetsJson = require("../../json/planetsv2.json")
const bossesJson = require("../../json/bosses.json")

module.exports = {
  name: 'dungeon',
  description: "讨伐Boss推进关卡",
  type: ApplicationCommandType.ChatInput,
  run: async (client, interaction) => {
    const userid = interaction.user.id;
    let user = await User.findOne({ ID: userid });

    const eWeapon = user.Equipped.Weapon;
    let userHealth = user.Health;

    const planets = planetsJson;
    const boss = bossesJson;

    const planetName = planets.planets[user.Planet];
    const bossName = planets[planetName].bosses[user.City];
    let bossHealth = boss[bossName].health;
    const bossDamageArray = boss[bossName].damage;

    // 生成按钮行的工具函数
    const createRow = (disabled = false) => new ActionRowBuilder()
      .addComponents(
        new ButtonBuilder()
          .setCustomId('attack1')
          .setLabel(user.Inventory[eWeapon].Attacks[0].name)
          .setStyle(ButtonStyle.Primary)
          .setDisabled(disabled),
        new ButtonBuilder()
          .setCustomId('attack2')
          .setLabel(user.Inventory[eWeapon].Attacks[1].name)
          .setStyle(ButtonStyle.Primary)
          .setDisabled(disabled),
      );

    // 生成Embed的工具函数
    const createEmbed = (bossHp, userHp, extraFields = []) => new EmbedBuilder()
      .setColor(0x0099FF)
      .setTitle(`${interaction.user.tag} vs ${bossName}`)
      .addFields(
        { name: `${bossName}'s Health`, value: `${bossHp}`, inline: true },
        { name: `${interaction.user.tag}`, value: `${userHp}`, inline: true },
        ...extraFields
      );

    // 发送初始消息并创建消息级收集器
    const initialEmbed = createEmbed(bossHealth, userHealth, [{ name: '\u200B', value: 'What will you do?' }]);
    const reply = await interaction.reply({ embeds: [initialEmbed], components: [createRow(false)], ephemeral: true });
    const collector = reply.createMessageComponentCollector({ time: 120000 });

    collector.on('collect', async i => {
      // 仅允许发起命令的用户操作
      if (i.user.id !== interaction.user.id) {
        return i.reply({ content: '这不是你的讨伐会话!', ephemeral: true });
      }

      let attackName, userDamage;
      const disabledRow = createRow(true);

      // 处理不同攻击按钮
      if (i.customId === 'attack1') {
        const attack = user.Inventory[eWeapon].Attacks[0];
        attackName = attack.name;
        userDamage = attack.damage[Math.floor(Math.random() * attack.damage.length)];
      } else if (i.customId === 'attack2') {
        const attack = user.Inventory[eWeapon].Attacks[1];
        attackName = attack.name;
        userDamage = attack.damage[Math.floor(Math.random() * attack.damage.length)];
      } else {
        return i.deferUpdate();
      }

      // 扣除Boss血量并更新消息
      bossHealth -= userDamage;
      let embed = createEmbed(bossHealth, userHealth, [
        { name: `\u200B`, value: ` ` },
        { name: `你对${bossName}造成了${userDamage}点伤害!`, value: `${bossName}正在反击...` }
      ]);
      await i.update({ embeds: [embed], components: [disabledRow] });

      // 判断Boss是否被击败
      if (bossHealth <= 0) {
        // 更新用户关卡进度
        user.City += 1;
        if (user.City === 5) {
          user.Planet += 1;
          user.City = 0;
        }
        await user.save();

        embed = new EmbedBuilder()
          .setColor(0x00FF00)
          .setTitle('讨伐成功!')
          .setDescription(`你击败了${bossName},已进入下一关!`);
        await i.editReply({ embeds: [embed], components: [] });
        collector.stop('boss_defeated');
        return;
      }

      // Boss反击延迟
      await wait(3000);

      // 扣除用户血量
      const bossDamage = bossDamageArray[Math.floor(Math.random() * bossDamageArray.length)];
      userHealth -= bossDamage;

      // 判断用户是否死亡
      if (userHealth <= 0) {
        embed = new EmbedBuilder()
          .setColor(0xFF0000)
          .setTitle('讨伐失败')
          .setDescription(`你被${bossName}击败了!`);
        await i.editReply({ embeds: [embed], components: [] });
        collector.stop('user_dead');
        return;
      }

      // 恢复按钮,允许继续攻击
      embed = createEmbed(bossHealth, userHealth, [
        { name: `\u200B`, value: ` ` },
        { name: `${bossName}对你造成了${bossDamage}点伤害!`, value: '轮到你攻击了!' }
      ]);
      await i.editReply({ embeds: [embed], components: [createRow(false)] });
    });

    collector.on('end', (collected, reason) => {
      console.log(`收集器结束:${reason},共收集${collected.size}次交互`);
      // 超时后禁用按钮
      if (reason === 'time') {
        interaction.editReply({ components: [createRow(true)] }).catch(() => {});
      }
    });
  }
};

额外优化点

  • 添加用户权限校验,防止无关用户操作按钮
  • 使用工具函数封装按钮和Embed创建,减少重复代码
  • 增加收集器超时时间,避免会话永久挂起
  • 所有异步操作添加await,保证逻辑执行顺序正确

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 20:47:37