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

discord.js如何检测已存在交互 解决RPS指令重复触发崩溃问题

Discord RPS指令重复触发崩溃修复方案

问题现象

  • 触发石头剪刀布(rock, paper, scissors,简称RPS)指令后,未完成当前对局就重复触发指令,机器人会直接崩溃
  • 终端抛出错误码为40060的DiscordAPIError,报错信息为Interaction has already been acknowledged(交互已被确认)

原逻辑缺陷

原实现流程:

  1. 执行rps指令后,机器人发送带3个选择按钮的游戏嵌入消息
  2. 临时注册interactionCreate事件监听按钮交互
  3. 收到按钮点击后判定胜负、禁用所有按钮、编辑消息展示结果
  4. 移除当前临时事件监听

缺陷点:

  • 每次触发指令都会新增一个独立的事件监听器,旧监听器仅在对应对局收到按钮点击后才会被移除,重复触发指令会导致多个监听器同时存在
  • 按钮自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 13:45:34