discord.js createReactionCollector事件重复触发两次问题排查
Discord.js 反应分页 collect 事件单次触发两次问题修复
问题表现
- 参考discord.js-pagination实现的表情反应分页功能基本可用,但用户每次点击表情,collect事件会连续触发2次
- 已排查确认
sendPaginationMessage方法仅执行一次,不存在重复初始化收集器的问题
原实现代码:
public sendPaginationMessage = async ( message: Message, pages: MessageEmbed[], emojiList: (GuildEmoji | string)[] = ['⏪', '⏩'], timeout = 120000 ) => { const channelId = message?.channelId if (!message || !channelId) { console.error('Channel is inaccessible.') return } if (!pages) { console.error('Pages are not given.') return } if (emojiList.length !== 2) { console.error('Need two emojis.') return } pages[0].footer = {text: `Page 1 / ${pages.length}`} const channel = await this.getChannelById(channelId) as TextChannel if (!channel) { return console.error( `Channel ${channelId} introuvable dans: `, JSON.stringify(this.discordClient.channels.cache, null, 4) ) } const currentPage: Message = await channel.send({embeds: [pages[0]]} as MessageOptions) for (const emoji of emojiList) await currentPage.react(emoji) const filter = (reaction: MessageReaction, user: User) => { const emojiName = reaction.emoji.name return !!emojiName && message.author.id === user.id && emojiList.includes(emojiName) && !user.bot } let page = 0 const collector = currentPage.createReactionCollector({filter, time: timeout}) collector.on('collect', reaction => { reaction.users.remove(message.author) // Log 2 times ¯\_(ツ)_/¯ console.log('reaction', reaction) const emojiName = reaction.emoji.name const previous = emojiName === emojiList[0] if (previous && page <= 0 || !previous && page + 1 >= pages.length) return page = previous ? --page : ++page pages[page].footer = {text: `Page ${page + 1} / ${pages.length}`} currentPage.edit({embeds: [pages[page]]} as MessageOptions) }) collector.on('end', () => { currentPage.reactions.removeAll().catch() }) return currentPage }
根因
- collect回调中执行
reaction.users.remove()是异步操作,discord.js部分版本在更新reaction用户缓存时,会重复触发一次collect事件 - 开启
REACTION/MESSAGE类型Partials时,收集器会同时接收网关实时事件和缓存补发的同一条反应事件,造成重复触发 - 原filter中
emojiList.includes(emojiName)的判断不兼容自定义GuildEmoji场景,会出现匹配逻辑异常
修复方案
- 加处理锁避免重复执行:在收集器外层加布尔锁,同一时间只处理一次翻页逻辑,同时把异步的移除反应、编辑消息操作加上await,避免状态错乱
- 优化表情匹配逻辑:兼容默认unicode表情和自定义服务器表情
- 调整收集器配置:关闭dispose选项,避免反应移除事件干扰收集逻辑
修复后的核心代码段:
// ...省略前面的参数校验、发消息、加初始反应逻辑 const filter = (reaction: MessageReaction, user: User) => { // 同时匹配自定义表情ID和默认表情名称 const emojiMatched = emojiList.some(emoji => { return typeof emoji === 'string' ? emoji === reaction.emoji.name : emoji.id === reaction.emoji.id }) return emojiMatched && message.author.id === user.id && !user.bot } let page = 0 // 加处理锁 let isProcessing = false const collector = currentPage.createReactionCollector({ filter, time: timeout, dispose: false // 关闭反应移除事件的收集 }) collector.on('collect', async (reaction, user) => { // 重复触发直接拦截 if (isProcessing) return isProcessing = true try { // await等待移除反应完成 await reaction.users.remove(user.id) console.log('reaction', reaction) const emojiName = reaction.emoji.name const previous = emojiName === emojiList[0] if ((previous && page <= 0) || (!previous && page + 1 >= pages.length)) { return } page = previous ? --page : ++page pages[page].footer = {text: `Page ${page + 1} / ${pages.length}`} await currentPage.edit({embeds: [pages[page]]} as MessageOptions) } finally { // 无论成功失败都释放锁 isProcessing = false } }) collector.on('end', () => { currentPage.reactions.removeAll().catch() }) // ...
内容的提问来源于stack exchange,提问作者Dyslopia
相关产品推荐
相关产品推荐

