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

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错误。

另外原代码中还有两个潜在问题:

  1. 拼接e621请求URL时,直接用+ tags会把数组转为逗号分隔字符串,不符合e621的标签格式要求
  2. 检查contentLength时,取的是e621接口响应的长度,而非目标图片的实际大小

修复方案

修改思路

  1. 替换所有interaction.followUp()为直接通过频道对象发送消息,频道对象是持久有效的,不依赖交互的临时Webhook
  2. 保存频道ID,通过客户端实例获取频道,避免对象引用失效问题
  3. 修正e621请求URL的拼接逻辑
  4. 调整错误处理逻辑,避免单次错误直接终止任务

修改后的代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 10:25:55