Discord.js Bot interaction.reply未等待异步代码执行完成问题
问题根因
你遇到的问题不是async/await语法失效,是两个实际逻辑问题导致的异常表现:
- Puppeteer的
page.goto()方法默认仅等待页面load事件触发就返回,多数内容类站点的列表是在初始load事件后通过JS异步渲染的,你在goto执行完后立即抓取DOM,此时目标.round-number下的h3元素还没挂载到页面上,最终healthArray为空,取随机值时得到undefined,看起来就像回复逻辑提前执行。 - Discord斜杠命令有3秒首次响应强制限制,Puppeteer冷启动完整Chromium实例+页面加载的耗时很容易超过这个阈值,一旦超时交互token会直接失效,后续再调用
interaction.reply()会直接抛出交互已处理的错误,和异步等待逻辑无关。 - 你写的Cheerio节点遍历逻辑本身是同步执行的,不存在漏等异步操作的问题。
修复方案
按以下步骤调整代码即可解决:
- 爬取逻辑启动前先调用
interaction.deferReply()延迟响应,该方法会立即给Discord返回“请求已接收”的占位状态,将响应有效期延长至15分钟,彻底规避3秒超时问题。注意延迟响应后不能再调用reply(),需要用editReply()发送最终内容。 - 给
page.goto()增加加载等待配置,同时主动等待目标选择器渲染完成后再抓取DOM,避免拿到未渲染完成的空页面。 - 增加异常捕获和兜底逻辑,避免Puppeteer运行报错时进程残留、或者爬取空内容时返回undefined。
- Puppeteer启动时加无沙箱参数,减少服务器环境下的启动耗时和权限报错。
修复后的完整可运行代码:
const { SlashCommandBuilder } = require('discord.js'); const puppeteer = require('puppeteer'); const cheerio = require('cheerio'); module.exports = { data: new SlashCommandBuilder() .setName("tips") .setDescription("获取随机健康常识"), async execute(interaction) { // 先延迟响应,规避Discord 3秒交互超时 await interaction.deferReply(); let browser; try { const healthArray = []; browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // 等待网络空闲状态再判定页面加载完成,适配异步渲染内容 await page.goto("https://www.example.com/health-facts/", { waitUntil: 'networkidle2', timeout: 15000 }); // 强制等待目标元素挂载,最多等5秒 await page.waitForSelector('.round-number h3', { timeout: 5000 }); const pageHtml = await page.evaluate(() => document.documentElement.innerHTML); const $ = cheerio.load(pageHtml); $(".round-number h3").each((i, el) => { const row = $(el).text() .replace(/\s+/g, " ") .replace(/^[0-9]+\.\s*/g, "") .trim(); healthArray.push(row); }); await browser.close(); browser = null; // 兜底处理,避免空数组报错 const randomFact = healthArray.length ? healthArray[Math.floor(Math.random() * healthArray.length)] : "暂时无法获取健康常识,请稍后再试"; await interaction.editReply(randomFact); } catch (err) { console.error("健康常识爬取失败:", err); if (browser) await browser.close(); // 区分交互状态返回错误提示 if (interaction.deferred) { await interaction.editReply("获取健康常识失败,请稍后重试"); } else { await interaction.reply("获取健康常识失败,请稍后重试"); } } }, };
优化建议
- 不需要每次用户调用命令都启动一次Puppeteer爬取,可以加定时任务,比如每1小时爬取一次所有健康常识存入内存缓存,用户调用命令时直接从缓存取随机内容返回,响应速度可以达到毫秒级,也不会给目标站点造成频繁请求压力。
- 如果部署环境资源有限,可以把Puppeteer换成更轻量的HTTP请求库(比如axios)直接拉取页面源码,只要站点内容不是强依赖客户端JS渲染,爬取耗时可以压缩到几百毫秒,完全不需要担心超时问题。
内容的提问来源于stack exchange,提问作者kaazue
相关产品推荐
相关产品推荐

