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

Discord.js v14:同频道多消息按钮交互识别异常问题

按钮交互角色分配Bug:第二条消息按钮触发第一条的逻辑

我写了一段Discord机器人代码,通过按钮交互实现角色分配,但遇到了问题:发送第二条带按钮的消息后,点击这条消息的按钮,Bot会忽略当前按钮,错误执行第一条消息的按钮逻辑。之前用索引设置按钮ID,后来改成nanoid生成唯一ID,问题仍然存在,Bot始终只响应第一条消息的按钮逻辑。

点击test-6按钮触发test-1角色分配

问题代码

function buttonBuilder(buttonName, role) {
  return new ButtonBuilder()
    .setCustomId(buttonName)
    .setLabel(role.name)
    .setStyle(ButtonStyle.Primary);
}

const roles = Array.from({ length: 5 }, (_, i) => i + 1).map((i) => {
  const id = nanoid(7).toLocaleLowerCase();
  return {
    name: `role${id}`,
    buttonName: `button${id}`,
  };
});

function getData() {
  const command = new SlashCommandBuilder()
    .setName("react-role")
    .setDescription("Reaction Role Messege")
    .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator)
    .addStringOption((option) =>
      option
        .setName("title")
        .setDescription("Title.")
        .setRequired(true)
    )
    .addStringOption((option) =>
      option
        .setName("description")
        .setDescription("Description.")
        .setRequired(true)
    );

  for (const [index, role] of roles.entries()) {
    command.addRoleOption((option) =>
      option
        .setName(role.name)
        .setDescription(`Role ${role.name} (Optional)`)
        .setRequired(index < 2)
    );
  }

  return command;
}

export default {
  data: getData(),

  run: async (client, interaction) => {
    const title = interaction.options.getString("title");
    const description = interaction.options.getString("description");

    const button = new ActionRowBuilder();

    for (const role of roles) {
      const roleOption = interaction.options.getRole(role.name);
      if (roleOption) {
        button.addComponents(buttonBuilder(role.buttonName, roleOption));
      }
    }

    const styledDescription = description.split("|").join("\n");

    const embed = new EmbedBuilder()
      .setColor("#4e4072")
      .setTitle(title)
      .setDescription(styledDescription);

    await interaction.reply({
      embeds: [embed],
      components: [button],
      fetchReply: true,
    });

    const collector =
      await interaction.channel.createMessageComponentCollector();

    collector.on("collect", async (i) => {
      const member = i.member;

      const role = roles.find((r) => {
        return r.buttonName === i.customId;
      });

      if (role) {
        const roleStat = interaction.options.getRole(role.name);
        await roleChecker(member, roleStat, i);
        await interaction.editReply({
          embeds: [embed],
          components: [button],
          fetchReply: true,
        });
      }
    });
  },
};

async function roleChecker(member, roleStat, i) {
  if (member.roles.cache.has(roleStat.id)) {
    await i
      .reply({
        embeds: [
          {
            title: `${roleStat.name} role is deleted!`,
            description: `${member.user.username}, ${roleStat.name} role is deleted!`,
          },
        ],
        ephemeral: true,
      })
      .then(() => setTimeout(() => i.deleteReply(), 5000));
    return member.roles.remove(roleStat);
  } else {
    await i
      .reply({
        embeds: [
          {
            title: `You have got ${roleStat.name} role!`,
            description: `${member.user.username}, you have got ${roleStat.name} role!`,
          },
        ],
        ephemeral: true,
        fetchReply: true,
      })
      .then(() => setTimeout(() => i.deleteReply(), 5000));
    return member.roles.add(roleStat);
  }
}

问题根源

  1. 全局roles变量冲突:roles是全局变量,机器人启动时只生成一次,后续所有命令调用都复用这组ID和选项名。第二条消息的按钮ID看似唯一,但实际和第一条消息的按钮绑定到同一套roles数据,匹配时会拿到第一次命令的角色选项,导致逻辑错误。
  2. 频道级Collector干扰:用interaction.channel.createMessageComponentCollector()创建的是频道级监听,会捕获频道内所有按钮交互,且每次执行命令都会新建一个Collector,第一个Collector会优先响应,导致后续消息的按钮被错误处理。
  3. 动态命令选项不合法:Discord的Slash命令选项名必须固定,不能动态生成。你用role${id}作为选项名,机器人重启后才会更新选项,导致命令选项和实际按钮不匹配。

修复方案

修正后的代码

function buttonBuilder(roleId, roleName) {
  return new ButtonBuilder()
    .setCustomId(`assign-role-${roleId}`)
    .setLabel(roleName)
    .setStyle(ButtonStyle.Primary);
}

function getData() {
  const command = new SlashCommandBuilder()
    .setName("react-role")
    .setDescription("角色分配消息")
    .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator)
    .addStringOption(option =>
      option.setName("title")
        .setDescription("消息标题")
        .setRequired(true)
    )
    .addStringOption(option =>
      option.setName("description")
        .setDescription("消息描述,用|分隔换行")
        .setRequired(true)
    );

  // 固定选项名,避免动态生成导致的Discord命令规则问题
  for (let i = 1; i <= 5; i++) {
    command.addRoleOption(option =>
      option.setName(`role${i}`)
        .setDescription(`第${i}个角色(前2个必填)`)
        .setRequired(i < 3)
    );
  }

  return command;
}

export default {
  data: getData(),

  run: async (client, interaction) => {
    const title = interaction.options.getString("title");
    const description = interaction.options.getString("description");

    const buttonRow = new ActionRowBuilder();
    // 遍历固定选项,生成按钮时直接将角色ID嵌入customId
    for (let i = 1; i <= 5; i++) {
      const roleOption = interaction.options.getRole(`role${i}`);
      if (roleOption) {
        buttonRow.addComponents(buttonBuilder(roleOption.id, roleOption.name));
      }
    }

    const styledDescription = description.split("|").join("\n");

    const embed = new EmbedBuilder()
      .setColor("#4e4072")
      .setTitle(title)
      .setDescription(styledDescription);

    // 获取发送后的消息对象,用于绑定专属Collector
    const sentMessage = await interaction.reply({
      embeds: [embed],
      components: [buttonRow],
      fetchReply: true,
    });

    // 给当前消息创建专属Collector,仅监听这条消息的按钮交互
    const collector = sentMessage.createMessageComponentCollector({
      time: 86400000 // 设置24小时超时,避免资源浪费
    });

    collector.on("collect", async (i) => {
      // 从customId中提取角色ID
      const roleId = i.customId.replace("assign-role-", "");
      const role = i.guild.roles.cache.get(roleId);
      
      if (!role) {
        return i.reply({ content: "该角色已不存在!", ephemeral: true });
      }

      const member = i.member;
      await roleChecker(member, role, i);
    });

    // 超时后禁用按钮,提示用户交互已结束
    collector.on("end", () => {
      buttonRow.components.forEach(btn => btn.setDisabled(true));
      interaction.editReply({ components: [buttonRow] }).catch(() => {});
    });
  },
};

async function roleChecker(member, roleStat, i) {
  if (member.roles.cache.has(roleStat.id)) {
    await i.reply({
      embeds: [{
        title: `${roleStat.name} 角色已移除`,
        description: `${member.user.username},你已失去${roleStat.name}角色`,
      }],
      ephemeral: true,
    });
    setTimeout(() => i.deleteReply().catch(() => {}), 5000);
    return member.roles.remove(roleStat);
  } else {
    await i.reply({
      embeds: [{
        title: `你已获得${roleStat.name}角色`,
        description: `${member.user.username},成功添加${roleStat.name}角色`,
      }],
      ephemeral: true,
    });
    setTimeout(() => i.deleteReply().catch(() => {}), 5000);
    return member.roles.add(roleStat);
  }
}

核心修复说明

  • 移除全局依赖:不再用全局roles变量,直接将角色ID嵌入按钮customId,让按钮和角色直接绑定,彻底避免混淆。
  • 消息专属Collector:用sentMessage.createMessageComponentCollector()替代频道级监听,确保每个消息的按钮交互只被自身的Collector处理。
  • 固定命令选项:遵循Discord命令规则,使用固定的选项名role1到role5,避免动态选项导致的不兼容问题。
  • 超时处理:给Collector设置超时时间,超时后禁用按钮,提升用户体验同时避免资源浪费。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 22:14:55