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

Discord Bot e621自动发图报错:InteractionAlreadyReplied

问题:Discord Bot斜杠命令运行数次后崩溃,报错InteractionAlreadyReplied

我的Discord Bot斜杠命令原本应每隔一分钟自动从e621平台发送图片,但运行几次后就崩溃,抛出以下错误:

Error [InteractionAlreadyReplied]: The reply to this interaction has already been sent or deferred.
    at ChatInputCommandInteraction.reply (/home/******/Desktop/discorde6Bot/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:102:46)
    at Timeout.spamLoop [as _onTimeout] (/home/******/Desktop/discorde6Bot/commands/e621spam.js:117:29)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'InteractionAlreadyReplied'
}

相关代码如下:

const { SlashCommandBuilder } = require("@discordjs/builders");
const axios = require('axios');
const { EmbedBuilder } = require('discord.js');
const { MongoClient } = require('mongodb');

const activeSpams = new Map(); // Map to store active spams by guild ID and channel ID
const imageCache = new Map(); // Map to store cached images by guild ID and channel ID

// MongoDB configuration
const uri = process.env.MONGODB_URI; // Replace with your MongoDB connection URI
const mongoClient = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });

async function getRandomImages(tags) {
  const e621ApiUrl = `https://e621.net/posts.json?tags=order:random+${tags}&limit=100`; // Fetch 100 posts

  try {
    const response = await axios.get(e621ApiUrl, {
      headers: { "User-Agent": "Discord Bot (********** on 621)" },
      maxContentLength: 8 * 1024 * 1024, // Limiting max content length to 8MB
    });

    return response.data.posts.map(post => ({
      imageUrl: post.file.url,
      artist: post.tags.artist.length !== 0 ? post.tags.artist : "Unknown",
    }));
  } catch (error) {
    console.error("Error fetching random images from e621:", error);
    throw new Error("Failed to fetch random images from e621");
  }
}

module.exports = {
  data: new SlashCommandBuilder()
    .setName('e621spam')
    .setDescription('Start or stop sending random images from e621 with the given tags')
    .addStringOption(option =>
      option
        .setName('action')
        .setDescription('Start or stop spamming')
        .setRequired(true)
        .addChoices(
          { name: 'Start', value: 'start' },
          { name: 'Stop', value: 'stop' }
        ))
    .addStringOption(option =>
      option
        .setName('tags')
        .setDescription('Tags to search for')
        .setRequired(false))
    .addIntegerOption(option =>
      option
        .setName('interval')
        .setDescription('Interval between sending images (in minutes)')
        .setRequired(false)),

  async execute(interaction) {
    const action = interaction.options.getString('action');
    const tags = interaction.options.getString('tags')?.split(',') || [];
    let interval = interaction.options.getInteger('interval');

    // Validate interval to be between 1 and 20
    if (interval && (interval < 1 || interval > 20)) {
      await interaction.reply({ content: "Interval must be between 1 and 20 minutes.", ephemeral: true });
      return;
    }

    const guildId = interaction.guildId;
    const channelId = interaction.channelId;
    const spamKey = `${guildId}-${channelId}`;

    if (action === 'start') {
      if (activeSpams.has(spamKey)) {
        await interaction.reply({ content: "There's already an active e621spam in this channel.", ephemeral: true });
        return;
      }

      const spamLoop = async () => {
        try {
          if (!interaction.channel.nsfw) {
            await interaction.reply("This command can only be used in NSFW channels.");
            clearInterval(activeSpams.get(spamKey));
            activeSpams.delete(spamKey);
            return;
          }

          let images = imageCache.get(spamKey);

          console.log("Images in cache:", images); // Add this line to check the content of the image cache

          // If image cache is empty, fetch new images
          if (!images || images.length === 0) {
            images = await getRandomImages(tags);
            imageCache.set(spamKey, images);
          }

          // Send the last image in the list
          const { imageUrl, artist } = images.pop();

          // Create an embed message
          const embed = new EmbedBuilder()
            .setTitle('Random Image from e621')
            .setDescription(`[View Image](${imageUrl})`)
            .setImage(imageUrl)
            .setColor('#0099ff')
            .setFooter({ text: `Artist: ${artist}` });

          // Send the embed message
          await interaction.channel.send({ embeds: [embed] });

          // If image cache is empty after sending, fetch new images
          if (images.length === 0) {
            images = await getRandomImages(tags);
            imageCache.set(spamKey, images);
          }
        } catch (error) {
          console.error("Error in e621spam execution:", error);
          await interaction.reply("Something went wrong. Sorry");
          clearInterval(activeSpams.get(spamKey));
          activeSpams.delete(spamKey);
        }
      };

      // Start the spam loop only if the interval is valid
      if (interval) {
        const spamInterval = setInterval(spamLoop, interval * 60 * 1000); // Convert minutes to milliseconds

        // Store active spams in MongoDB
        try {
          await mongoClient.connect();
          const database = mongoClient.db('bot_db'); // Replace with your MongoDB database name
          const collection = database.collection('activespams');

          await collection.insertOne({ spamKey, guildId, channelId, interval });
        } catch (error) {
          console.error("Error storing active spam in MongoDB:", error);
        } finally {
          await mongoClient.close();
        }

        activeSpams.set(spamKey, spamInterval);
        await interaction.reply({ content: "e621spam started!", ephemeral: true });
      } else {
        await interaction.reply({ content: "Interval must be provided and be between 1 and 20 minutes.", ephemeral: true });
      }

    } else if (action === 'stop') {
      if (!activeSpams.has(spamKey)) {
        await interaction.reply({ content: "There's no active e621spam in this channel.", ephemeral: true });
        return;
      }

      // Remove active spam from MongoDB
      try {
        await mongoClient.connect();
        const database = mongoClient.db('bot_db'); // Replace with your MongoDB database name
        const collection = database.collection('activespams');

        await collection.deleteOne({ spamKey });
      } catch (error) {
        console.error("Error removing active spam from MongoDB:", error);
      } finally {
        await mongoClient.close();
      }

      clearInterval(activeSpams.get(spamKey));
      activeSpams.delete(spamKey);
      await interaction.reply({ content: "e621spam stopped!", ephemeral: true });
    }
  },
};

问题根源

你在定时循环的spamLoop函数里重复使用初始的interaction对象调用reply方法,但每个Discord交互只能被回复一次。第一次循环触发错误或NSFW检查时调用了interaction.reply,后续循环再调用就会触发InteractionAlreadyReplied错误。

修复方案

  1. 将NSFW检查移到启动循环前:频道的NSFW状态不会在循环中频繁变化,没必要每次循环都检查,直接在启动前验证,用初始交互回复提示即可。
  2. 循环内错误提示改用频道发送:循环中出现错误时,不要调用interaction.reply,而是用interaction.channel.send发送普通消息,避免重复回复同一个交互。
  3. 仅在初始交互时回复一次:启动和停止命令的回复只在execute函数主流程中调用,循环内只负责发送图片和普通消息。

修复后的完整代码如下:

const { SlashCommandBuilder } = require("@discordjs/builders");
const axios = require('axios');
const { EmbedBuilder } = require('discord.js');
const { MongoClient } = require('mongodb');

const activeSpams = new Map(); // Map to store active spams by guild ID and channel ID
const imageCache = new Map(); // Map to store cached images by guild ID and channel ID

// MongoDB configuration
const uri = process.env.MONGODB_URI; // Replace with your MongoDB connection URI
const mongoClient = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });

async function getRandomImages(tags) {
  const e621ApiUrl = `https://e621.net/posts.json?tags=order:random+${tags}&limit=100`; // Fetch 100 posts

  try {
    const response = await axios.get(e621ApiUrl, {
      headers: { "User-Agent": "Discord Bot (********** on 621)" },
      maxContentLength: 8 * 1024 * 1024, // Limiting max content length to 8MB
    });

    return response.data.posts.map(post => ({
      imageUrl: post.file.url,
      artist: post.tags.artist.length !== 0 ? post.tags.artist : "Unknown",
    }));
  } catch (error) {
    console.error("Error fetching random images from e621:", error);
    throw new Error("Failed to fetch random images from e621");
  }
}

module.exports = {
  data: new SlashCommandBuilder()
    .setName('e621spam')
    .setDescription('Start or stop sending random images from e621 with the given tags')
    .addStringOption(option =>
      option
        .setName('action')
        .setDescription('Start or stop spamming')
        .setRequired(true)
        .addChoices(
          { name: 'Start', value: 'start' },
          { name: 'Stop', value: 'stop' }
        ))
    .addStringOption(option =>
      option
        .setName('tags')
        .setDescription('Tags to search for')
        .setRequired(false))
    .addIntegerOption(option =>
      option
        .setName('interval')
        .setDescription('Interval between sending images (in minutes)')
        .setRequired(false)),

  async execute(interaction) {
    const action = interaction.options.getString('action');
    const tags = interaction.options.getString('tags')?.split(',') || [];
    let interval = interaction.options.getInteger('interval');

    // Validate interval to be between 1 and 20
    if (interval && (interval < 1 || interval > 20)) {
      await interaction.reply({ content: "Interval must be between 1 and 20 minutes.", ephemeral: true });
      return;
    }

    const guildId = interaction.guildId;
    const channelId = interaction.channelId;
    const spamKey = `${guildId}-${channelId}`;

    if (action === 'start') {
      if (activeSpams.has(spamKey)) {
        await interaction.reply({ content: "There's already an active e621spam in this channel.", ephemeral: true });
        return;
      }

      // 提前检查NSFW频道,避免循环内重复交互回复
      if (!interaction.channel.nsfw) {
        await interaction.reply("This command can only be used in NSFW channels.");
        return;
      }

      const spamLoop = async () => {
        try {
          let images = imageCache.get(spamKey);

          console.log("Images in cache:", images);

          // If image cache is empty, fetch new images
          if (!images || images.length === 0) {
            images = await getRandomImages(tags);
            imageCache.set(spamKey, images);
          }

          // Send the last image in the list
          const { imageUrl, artist } = images.pop();

          // Create an embed message
          const embed = new EmbedBuilder()
            .setTitle('Random Image from e621')
            .setDescription(`[View Image](${imageUrl})`)
            .setImage(imageUrl)
            .setColor('#0099ff')
            .setFooter({ text: `Artist: ${artist}` });

          // Send the embed message
          await interaction.channel.send({ embeds: [embed] });

          // If image cache is empty after sending, fetch new images
          if (images.length === 0) {
            images = await getRandomImages(tags);
            imageCache.set(spamKey, images);
          }
        } catch (error) {
          console.error("Error in e621spam execution:", error);
          // 用channel.send代替interaction.reply发送错误消息
          await interaction.channel.send("Something went wrong. Sorry");
          clearInterval(activeSpams.get(spamKey));
          activeSpams.delete(spamKey);
        }
      };

      // Start the spam loop only if the interval is valid
      if (interval) {
        const spamInterval = setInterval(spamLoop, interval * 60 * 1000); // Convert minutes to milliseconds

        // Store active spams in MongoDB
        try {
          await mongoClient.connect();
          const database = mongoClient.db('bot_db'); // Replace with your MongoDB database name
          const collection = database.collection('activespams');

          await collection.insertOne({ spamKey, guildId, channelId, interval });
        } catch (error) {
          console.error("Error storing active spam in MongoDB:", error);
        } finally {
          await mongoClient.close();
        }

        activeSpams.set(spamKey, spamInterval);
        await interaction.reply({ content: "e621spam started!", ephemeral: true });
      } else {
        await interaction.reply({ content: "Interval must be provided and be between 1 and 20 minutes.", ephemeral: true });
      }

    } else if (action === 'stop') {
      if (!activeSpams.has(spamKey)) {
        await interaction.reply({ content: "There's no active e621spam in this channel.", ephemeral: true });
        return;
      }

      // Remove active spam from MongoDB
      try {
        await mongoClient.connect();
        const database = mongoClient.db('bot_db'); // Replace with your MongoDB database name
        const collection = database.collection('activespams');

        await collection.deleteOne({ spamKey });
      } catch (error) {
        console.error("Error removing active spam from MongoDB:", error);
      } finally {
        await mongoClient.close();
      }

      clearInterval(activeSpams.get(spamKey));
      activeSpams.delete(spamKey);
      await interaction.reply({ content: "e621spam stopped!", ephemeral: true });
    }
  },
};

修改后Bot就能持续发送图片直到执行停止命令,不会再因重复回复交互而崩溃。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 07:14:50