discord.js如何检测已存在交互 解决RPS指令重复触发崩溃问题
Discord RPS指令重复触发崩溃修复方案
问题现象
- 触发石头剪刀布(rock, paper, scissors,简称RPS)指令后,未完成当前对局就重复触发指令,机器人会直接崩溃
- 终端抛出错误码为40060的
DiscordAPIError,报错信息为Interaction has already been acknowledged(交互已被确认)
原逻辑缺陷
原实现流程:
- 执行rps指令后,机器人发送带3个选择按钮的游戏嵌入消息
- 临时注册
interactionCreate事件监听按钮交互 - 收到按钮点击后判定胜负、禁用所有按钮、编辑消息展示结果
- 移除当前临时事件监听
缺陷点:
- 每次触发指令都会新增一个独立的事件监听器,旧监听器仅在对应对局收到按钮点击后才会被移除,重复触发指令会导致多个监听器同时存在
- 按钮自定义ID未绑定对局、触发用户标识,所有监听器都会响应任意RPS按钮的点击事件
- 第一个监听器完成交互确认后,其余监听器重复调用
deferUpdate()方法就会触发重复确认报错 - 未设置交互超时,用户长期不点击按钮会导致监听器永久残留
修复方案
- 为每个对局的按钮添加唯一标识:将按钮自定义ID格式改为
rps_<对局消息ID>_<触发用户ID>_<出拳类型>,每个监听器仅响应属于自己对局、由指令触发者发起的交互 - 注册新监听器前兜底移除残留的同闭包监听器,避免冗余监听堆积
- 增加120秒交互超时机制,超时后自动禁用按钮、移除监听器
- 调用交互响应方法前先校验交互状态,如果交互已被延迟/已回复则直接跳过,兜底拦截报错
修复后代码
module.exports = { name: 'rps', cooldown: 0, aliases: [], permissions: [], description: 'Rock Paper Scissors.', async execute(message, args, client, Discord) { const hand = [{ txt: 'Rock', emoji: '✊', index: 0 }, { txt: 'Paper', emoji: '🤚', index: 1 }, { txt: 'Scissors', emoji: '✌️', index: 2 }]; const botMove = hand[Math.floor(Math.random() * 3)]; const gameOwnerId = message.author.id; const rpsMsg = await message.channel.send({ embeds: [ new Discord.MessageEmbed() .setColor('RANDOM') .setTitle('Rock Paper Scissors') .setDescription('Choose a handsign, you have 120 seconds to respond') ], components: [ new Discord.MessageActionRow() .addComponents( new Discord.MessageButton() .setCustomId(`rps_${rpsMsg.id}_${gameOwnerId}_rock`) .setLabel("✊ Rock") .setStyle('PRIMARY'), new Discord.MessageButton() .setCustomId(`rps_${rpsMsg.id}_${gameOwnerId}_paper`) .setLabel("🤚 Paper") .setStyle('PRIMARY'), new Discord.MessageButton() .setCustomId(`rps_${rpsMsg.id}_${gameOwnerId}_scissors`) .setLabel("✌️ Scissors") .setStyle('PRIMARY') ) ] }); let win = 0; let userMove; // 超时自动结束对局 const timeout = setTimeout(async () => { client.off('interactionCreate', handleInteraction); const timeoutEmbed = rpsMsg.embeds[0]; timeoutEmbed.description = 'Game timed out, no response received'; const disabledComponents = rpsMsg.components; disabledComponents[0].components.forEach(comp => comp.disabled = true); await rpsMsg.edit({ embeds: [timeoutEmbed], components: disabledComponents }); }, 120000); async function handleInteraction(interaction) { if (!interaction.isButton()) return; // 只匹配当前对局、当前玩家的按钮交互 if (!interaction.customId.startsWith(`rps_${rpsMsg.id}_${gameOwnerId}`)) return; // 兜底判断交互是否已被处理 if (interaction.deferred || interaction.replied) return; await interaction.deferUpdate(); clearTimeout(timeout); const move = interaction.customId.split('_')[3]; userMove = hand.find(v => v.txt.toLowerCase() == move); switch (move) { case 'rock': win = botMove.index == 0 ? 1 : (botMove.index == 1 ? 0 : 2); break; case 'paper': win = botMove.index == 0 ? 2 : (botMove.index == 1 ? 1 : 0); break; case 'scissors': win = botMove.index == 0 ? 0 : (botMove.index == 1 ? 2 : 1); break; } const resultEmbed = rpsMsg.embeds[0]; resultEmbed.description = `I chose ${botMove.txt}! ${win == 0 ? 'You lost!' : (win == 1 ? 'We tied!' : 'You win!')} (${userMove.emoji} ${win == 0 ? '<' : (win == 1 ? '=' : '>')} ${botMove.emoji})`; const components = rpsMsg.components; components[0].components.forEach(comp => { if (comp.customId == interaction.customId) { comp.disabled = true; comp.style = 'SECONDARY'; } else { comp.disabled = true; } }); await interaction.message.edit({ embeds: [resultEmbed], components: components }); // 移除当前监听器 client.off('interactionCreate', handleInteraction); } // 注册监听器前先兜底移除可能存在的残留监听 client.off('interactionCreate', handleInteraction); client.on('interactionCreate', handleInteraction); } }
注意:如果机器人已经在全局
interactionCreate事件中统一处理按钮交互,不需要在指令内重复注册临时监听,直接在全局事件中通过customId规则匹配对应对局逻辑即可,从根源上避免重复监听问题。
内容的提问来源于stack exchange,提问作者happymancer
相关产品推荐
相关产品推荐

