Discord.js击败Boss后重发命令触发DiscordAPIError[40060]问题
Discord.js 按钮交互错误:
Interaction has already been acknowledged 问题现象
使用discord.js开发Boss讨伐功能,用户通过按钮选择攻击招式。击败Boss后重新运行/dungeon命令,Embed能正常发送,但点击按钮会触发DiscordAPIError[40060]错误,页面还会回退到之前的Embed数据。
错误根源
- 重复绑定收集器事件:代码中两次调用
collector.on('collect', ...),同一按钮交互会触发两个回调,导致重复调用i.deferUpdate()或i.editReply(),触发"交互已确认"错误。 - 全局频道收集器:用
interaction.channel.createMessageComponentCollector()创建的是频道级全局收集器,旧会话的收集器未销毁,新会话的按钮点击会被旧收集器捕获,引发逻辑混乱。 - 共享数据引用:
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
相关产品推荐
相关产品推荐

