Discord Bot定时发送e621图片数小时后触发Invalid Webhook Token错误
Discord斜杠命令机器人运行数小时后因Invalid Webhook Token崩溃
问题描述
开发了一个Discord斜杠命令机器人,功能是每隔指定分钟自动从e621发送一张图片。机器人能正常运行1-2小时,但之后会抛出错误并崩溃。
错误信息
/home/****/Desktop/discorde6Bot/node_modules/discord.js/node_modules/@discordjs/rest/dist/index.js:687 throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData); ^ DiscordAPIError[50027]: Invalid Webhook Token at handleErrors (/home//****//Desktop/discorde6Bot/node_modules/discord.js/node_modules/@discordjs/rest/dist/index.js:687:13) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async SequentialHandler.runRequest (/home/****/Desktop/discorde6Bot/node_modules/discord.js/node_modules/@discordjs/rest/dist/index.js:1072:23) at async SequentialHandler.queueRequest (/home/****/Desktop/discorde6Bot/node_modules/discord.js/node_modules/@discordjs/rest/dist/index.js:913:14) at async _REST.request (/home/****/Desktop/discorde6Bot/node_modules/discord.js/node_modules/@discordjs/rest/dist/index.js:1218:22) at async InteractionWebhook.send (/home/****/Desktop/discorde6Bot/node_modules/discord.js/src/structures/Webhook.js:222:15) at async Timeout.spamLoop [as _onTimeout] (/home/****/Desktop/discorde6Bot/commands/e621spam.js:120:11) { requestBody: { files: [], json: { content: 'Something went wrong. Sorry', tts: false, nonce: undefined, embeds: undefined, components: undefined, username: undefined, avatar_url: undefined, allowed_mentions: undefined, flags: undefined, message_reference: undefined, attachments: undefined, sticker_ids: undefined, thread_name: undefined } }, rawError: { message: 'Invalid Webhook Token', code: 50027 }, code: 50027, status: 401, method: 'POST', url: 'https://discord.com/api/v10/webhooks/1082024195938062437/aW50ZXJhY3Rpb246MTIzNjQzODg0Njk0MjQ3ODQxNzp4QmVsa09QWUVZN0VrSkRUWHpHRVZQSzZZd2k1cUt5dzVDdENrT0JPQThkTUdlWTgza2hXdkhwalJIQWdkVHlaRkNsNlhaVjdDd2FzU0l3dmE1SHN0cURJdFdhSTFaTlAxMnFpc2JiTWsycFlRUWM4ZXVZd21XOEpyckdVSTRsdA?wait=true' }
相关代码
const { SlashCommandBuilder } = require("@discordjs/builders"); const axios = require('axios'); const { EmbedBuilder } = require('discord.js'); const activeSpams = new Map(); // Map to store active spams function randomPicUrl(response, usedIndices) { let randomIndex = Math.floor(Math.random() * response.data.posts.length); let post; if (usedIndices.includes(randomIndex)) { return randomPicUrl(response, usedIndices); } else { post = response.data.posts[randomIndex]; usedIndices.push(randomIndex); return post.file.url; } } function containsBlacklistedTags(tags, blacklist) { for (const tag of tags) { let checkTagElem = tag.split(' '); for (const e of checkTagElem) { if (blacklist.includes(e.toLowerCase())) { return true; } } } return false; } 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)) .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(',') || []; const interval = interaction.options.getInteger('interval'); 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 () => { const blacklist = ['de*****', 'go***', 'tent****', 'fa***', 'sc***', 'animated', 'type:mp4', 'type:webm']; const e621ApiUrl = "https://e621.net/posts.json?tags=order:random " + tags; console.log(`Constructed URL: ${e621ApiUrl}`); try { if (!interaction.channel.nsfw) { await interaction.followUp({ content: "This command can only be used in NSFW channels." }); clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); return; } if (containsBlacklistedTags(tags, blacklist)) { await interaction.followUp({ content: "Content contains blacklisted tags." }); clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); return; } const response = await axios.get(e621ApiUrl, { headers: { "User-Agent": "Discord Bot (************ on 621)" }, maxContentLength: 8 * 1024 * 1024, // Limiting max content length to 8MB }); console.log(response.data.posts.length); const usedIndices = []; const imageUrl = randomPicUrl(response, usedIndices); if (imageUrl === null) { return; } // Check file size before sending const contentLength = response.headers['content-length']; if (contentLength && parseInt(contentLength) > 8 * 1024 * 1024) { console.log("File size is too big! Retrying..."); return; } // Create an embed message const embed = new EmbedBuilder() .setTitle('Random Image from e621') .setDescription(`[View Image](${imageUrl})`) .setImage(imageUrl) .setColor('#0099ff'); // Send the embed message await interaction.channel.send({ embeds: [embed] }); } catch (e) { await interaction.followUp("Something went wrong. Sorry"); console.log(e); clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); } }; // Start the spam loop const spamInterval = setInterval(spamLoop, interval * 60 * 1000); // Convert minutes to milliseconds activeSpams.set(spamKey, spamInterval); await interaction.reply({ content: "e621spam started!", 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; } clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); await interaction.reply({ content: "e621spam stopped!", ephemeral: true }); } }, };
已尝试过重试机制和生成新Webhook Token,均无效。期望机器人能持续运行直到执行停止命令。
问题原因
核心问题是**interaction.followUp()的错误使用**:
Discord交互的Webhook令牌是临时有效(通常15分钟左右),而你的定时循环spamLoop会持续运行1-2小时,此时初始交互的Webhook令牌早已过期,调用followUp()就会触发Invalid Webhook Token错误。
另外原代码中还有两个潜在问题:
- 拼接e621请求URL时,直接用
+ tags会把数组转为逗号分隔字符串,不符合e621的标签格式要求 - 检查
contentLength时,取的是e621接口响应的长度,而非目标图片的实际大小
修复方案
修改思路
- 替换所有
interaction.followUp()为直接通过频道对象发送消息,频道对象是持久有效的,不依赖交互的临时Webhook - 保存频道ID,通过客户端实例获取频道,避免对象引用失效问题
- 修正e621请求URL的拼接逻辑
- 调整错误处理逻辑,避免单次错误直接终止任务
修改后的代码
const { SlashCommandBuilder } = require("@discordjs/builders"); const axios = require('axios'); const { EmbedBuilder } = require('discord.js'); const activeSpams = new Map(); let client; // 机器人启动时传入客户端实例 module.exports.setClient = (botClient) => { client = botClient; }; function randomPicUrl(response, usedIndices) { let randomIndex = Math.floor(Math.random() * response.data.posts.length); let post; if (usedIndices.includes(randomIndex)) { return randomPicUrl(response, usedIndices); } else { post = response.data.posts[randomIndex]; usedIndices.push(randomIndex); return post.file.url; } } function containsBlacklistedTags(tags, blacklist) { for (const tag of tags) { let checkTagElem = tag.split(' '); for (const e of checkTagElem) { if (blacklist.includes(e.toLowerCase())) { return true; } } } return false; } 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)) .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(',') || []; const interval = interaction.options.getInteger('interval'); const guildId = interaction.guildId; const channelId = interaction.channelId; const spamKey = `${guildId}-${channelId}`; if (action === 'start') { if (activeSpams.has(spamKey)) { await interaction.reply({ content: "当前频道已有活跃的e621spam任务。", ephemeral: true }); return; } const spamLoop = async () => { const blacklist = ['de*****', 'go***', 'tent****', 'fa***', 'sc***', 'animated', 'type:mp4', 'type:webm']; const e621ApiUrl = `https://e621.net/posts.json?tags=order:random ${tags.join(' ')}`; console.log(`Constructed URL: ${e621ApiUrl}`); try { const channel = client.channels.cache.get(channelId); if (!channel) { console.log("无法找到目标频道,停止任务"); clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); return; } if (!channel.nsfw) { await channel.send("此命令仅能在NSFW频道使用。"); clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); return; } if (containsBlacklistedTags(tags, blacklist)) { await channel.send("内容包含黑名单标签。"); clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); return; } const response = await axios.get(e621ApiUrl, { headers: { "User-Agent": "Discord Bot (************ on 621)" }, maxContentLength: 8 * 1024 * 1024, }); console.log(response.data.posts.length); if (response.data.posts.length === 0) { await channel.send("未找到符合条件的图片。"); return; } const usedIndices = []; const imageUrl = randomPicUrl(response, usedIndices); if (!imageUrl) { await channel.send("无法获取有效图片链接。"); return; } // 检查图片实际大小(可选) // const imageHead = await axios.head(imageUrl); // const contentLength = imageHead.headers['content-length']; // if (contentLength && parseInt(contentLength) > 8 * 1024 * 1024) { // console.log("图片过大,跳过本次"); // return; // } const embed = new EmbedBuilder() .setTitle('Random Image from e621') .setDescription(`[查看图片](${imageUrl})`) .setImage(imageUrl) .setColor('#0099ff'); await channel.send({ embeds: [embed] }); } catch (e) { const channel = client.channels.cache.get(channelId); if (channel) { await channel.send("出错了,抱歉。"); } console.log(e); // 可选:保留任务继续执行,而非直接终止 // clearInterval(activeSpams.get(spamKey)); // activeSpams.delete(spamKey); } }; const spamInterval = setInterval(spamLoop, interval * 60 * 1000); activeSpams.set(spamKey, spamInterval); await interaction.reply({ content: "e621spam已启动!", ephemeral: true }); } else if (action === 'stop') { if (!activeSpams.has(spamKey)) { await interaction.reply({ content: "当前频道没有活跃的e621spam任务。", ephemeral: true }); return; } clearInterval(activeSpams.get(spamKey)); activeSpams.delete(spamKey); await interaction.reply({ content: "e621spam已停止!", ephemeral: true }); } }, };
额外配置
在机器人主文件中需要添加一行代码,传入客户端实例:
const e621Command = require('./commands/e621spam'); e621Command.setClient(client);
内容的提问来源于stack exchange,提问作者Sapphire Shork
相关产品推荐
相关产品推荐

