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错误。
修复方案
- 将NSFW检查移到启动循环前:频道的NSFW状态不会在循环中频繁变化,没必要每次循环都检查,直接在启动前验证,用初始交互回复提示即可。
- 循环内错误提示改用频道发送:循环中出现错误时,不要调用
interaction.reply,而是用interaction.channel.send发送普通消息,避免重复回复同一个交互。 - 仅在初始交互时回复一次:启动和停止命令的回复只在
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
相关产品推荐
相关产品推荐

