discord.js消息删除追踪完全指南:缓存、审计日志与数据库兜底方案
做Discord机器人开发的兄弟应该都遇到过一个看着简单、真做起来全是坑的需求用户在群里把消息一删你后台想知道到底是哪个人删的、删的内容是什么。很多人第一反应是监听messageDelete事件结果写完发现messageDelete拿到的消息对象里author经常是null要么就是根本查不到删除者是谁。这个问题我在 discord.js 下折腾过好几轮踩过缓存、Partial、审计日志的各种坑今天干脆把整套追踪方案捋一遍从事件机制讲起到缓存方案、审计日志方案、数据库兜底方案一条龙讲清楚。无论你是刚开始写 bot 的新手还是已经维护过几个生产机器人的老手这篇文章都能让你少走弯路。1. 先搞清楚discord.js的消息删除事件机制在写追踪逻辑之前我建议你先花几分钟把messageDelete这个事件的底层机制看明白不然代码写出来大概率是“看起来能跑实际一测就挂”。这一节不涉及复杂理论只讲清楚这个事件能给你什么、不能给你什么。1.1 messageDelete事件能拿到什么参数discord.js 中监听消息删除最常见的写法是这样const { Client, GatewayIntentBits } require(discord.js); const client new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, ], }); client.on(messageDelete, (message) { console.log(被删除的消息ID:, message.id); console.log(消息内容:, message.content); console.log(作者ID:, message.author?.id); console.log(频道ID:, message.channelId); }); client.login(你的BotToken);看起来很简单对吧messageDelete回调里拿到一个message对象里面有content、channelId、id理论上还有author。但问题在于messageDelete事件触发时这条消息可能已经被 Discord 从服务器上抹掉了discord.js 之所以还能把这个对象交给你是因为它的内部缓存里还留着这条消息的“尸体”。打个比方这就像快递柜取件你收到通知说某个快递被取走了删除事件触发了但是你想查是谁取走的快递柜的屏幕上只剩下一个取件记录消息ID上面没有取件人的脸。除非快递柜本身装了监控审计日志否则你很难知道取件人身份。具体到messageDelete的message对象里面字段的可用性取决于这条消息是否还在 discord.js 的缓存里。如果缓存还在message.author就能正常返回一个User对象如果缓存已经被清理或者因为消息太早、没被缓存过message.author就是null。这是整篇文章最关键的一个坑。1.2 为什么“追踪删除用户”会翻车理解了事件机制之后你会发现messageDelete本身能给你的是“哪些消息被删了”而不是“谁删的”。这在单聊场景下无所谓——因为私信基本只有发送者本人能删。但在服务器Guild里删除消息的人可能是消息的作者本人他自己删了管理员或版主通过权限删除bot 自己比如按命令清理消息Discord 官方系统例如违规内容被平台移除。messageDelete事件本身区分不了这几种情况它只有一个消息对象没有deletedBy这类字段。那网上有人说“用messageDelete就能知道删除者”是怎么回事其实他们是把“消息作者”和“删除者”混为一谈了。message.author返回的是这条消息的原始作者而不是删消息的操作者。如果你的需求只是“知道谁的消息被删了”这没问题但如果你要的是“知道谁执行的删除操作”你就得走 Discord 的审计日志Audit Logs方案我会在第三章展开。还有第二种翻车情况就是忽略缓存的存在。discord.js 默认只缓存最近的消息一旦消息被清理或者长期没人读messageDelete触发时message.author就是nullmessage.content也是null。这时候你写message.author.id直接就会抛异常很多新手在这一步卡死。2. 方案一从事件缓存里掏出作者信息既然我们已经知道messageDelete能拿到缓存中的消息对象那么最简单的实现思路就是“在删除事件发生前就提前把消息作者记下来”。这个方案的核心思路是平时通过messageCreate事件记住每条消息的作者和ID删除时回过头来查记录。它适合部署成本比较低、不需要知道“谁删的”而只想知道“谁的消息被删了”的场景。2.1 基础实现让bot记住消息作者最直接的做法是维护一个全局缓存在messageCreate里写入消息ID到作者的映射然后在messageDelete里查这个映射const { Client, GatewayIntentBits } require(discord.js); const client new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, ], }); // 用 Map 做内存缓存消息ID - { 作者ID, 频道ID, 内容 } const messageCacheMap new Map(); const CACHE_LIMIT 10000; // 防止内存无限增长 client.on(messageCreate, (message) { // 忽略bot自己的消息按需调整 if (message.author.bot) return; messageCacheMap.set(message.id, { authorId: message.author.id, authorTag: message.author.tag, channelId: message.channel.id, content: message.content.slice(0, 200), deletedAt: null, }); // 控制缓存大小超过限制时删除最早一条 if (messageCacheMap.size CACHE_LIMIT) { const oldestKey messageCacheMap.keys().next().value; messageCacheMap.delete(oldestKey); } }); client.on(messageDelete, (message) { const record messageCacheMap.get(message.id); if (!record) { console.log(消息ID ${message.id} 没有被缓存无法获取作者); return; } // 更新删除时间 record.deletedAt new Date().toISOString(); console.log(被删除的消息ID:, message.id); console.log(作者:, record.authorTag); console.log(内容:, record.content); console.log(频道:, record.channelId); console.log(删除时间:, record.deletedAt); }); client.login(你的BotToken);这个方案的优点是逻辑直观不需要申请额外权限只需要配置GuildMessages意图。缺点也很明显它依赖 bot 从消息发送开始就一直保持在线监听到删除那一刻如果 bot 中途重启或者断线期间的messageCreate就没记录删除时就查不到。这是很多临时审计方案的通病你必须心里有数。2.2 用Partial结构应对缓存丢失刚才那个纯messageCreate方案有个隐患即便 bot 一直在线messageDelete触发时discord.js 传给回调函数的message对象也可能不是完整的。比如消息太老、消息不在缓存里、或者涉及服务器未缓存的频道时你拿到的message是Partial状态访问message.author就可能得到null。这种情况的应对方式是启用Partials让你的 bot 在事件触发时尝试通过 API 重新拉取消息数据。但注意messageDelete的场景下消息已经删了你没法用fetch()重新获取完整消息所以Partials.Message能做的只是让message.author从“直接报错”变成“返回 null”你还是要提前有兜底逻辑。正确用法是在初始化Client的时候声明支持部分结构const { Client, GatewayIntentBits, Partials } require(discord.js); const client new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, ], partials: [ Partials.Message, Partials.Channel, ], }); client.on(messageDelete, async (message) { // 如果是 partial 消息很多字段不可用直接降级处理 if (message.partial) { console.log(收到一个 partial 消息删除事件author 可能为 null); return; } console.log(作者:, message.author?.id ?? 未知); }); client.login(你的BotToken);很多网上教程只告诉你加partials不告诉你messageDelete里其实救不回来。这里有个实操心得partials主要帮的是messageCreate之外的其他消息事件比如messageReactionAdd、messageUpdate在这些场景下部分消息还能通过fetch()补全。但删除事件中消息已经没了fetch()只会抛DiscordAPIError。所以你在messageDelete里判断message.partial为 true 时最佳策略不是尝试拉取而是走你自己的数据库或缓存记录。2.3 方案一的边界和适用场景方案一能覆盖的场景是bot 常驻服务器、消息从发出到删除都在 bot 的监控窗口内、你只关心“哪条消息、谁发的、内容是什么”。它是轻量、快速、零门槛的适合绝大多数社区管理机器人。它的边界非常明确没法知道删除者是谁也没法处理 bot 离线期间产生的消息。如果服务器运营者真正想要的是精确到人的“删消息操作者”你需要用到第三章的审计日志方案。我在实际项目里的做法是先用方案一做内容留存再用方案二做操作者追踪两者互补。这里有个容易忽略的角色权限细节即便你是机器人如果 bot 没有被授予VIEW_AUDIT_LOG权限第三章的审计日志方案也会直接拿不到数据。所以在服务器里给 bot 分配角色时至少要勾选“查看审计日志”这个权限否则后面排查半天都找不到原因。3. 方案二通过审计日志锁定删除者如果产品的核心诉求是“查出是谁删了消息”那messageDelete事件本身满足不了必须借助 Discord 的审计日志Audit Logs能力。这是 Discord 官方提供的操作留痕机制服务器上有权限的成员执行敏感操作删消息、踢人、封号等都会被记录。discord.js 通过fetchAuditLogs方法可以读取这些记录。3.1 审计日志的工作原理Discord 的审计日志可以类比成小区物业的监控系统。大门保安bot知道有人进出了消息被删了但这人是谁、刷的哪张卡得去物业办公室调监控审计日志。监控不是一直把所有画面都存着它只保留最近一小段时间而且你去调的时候得确认“调监控的人”有没有权限。在 Discord 里审计日志的记录条目非常丰富其中MESSAGE_DELETE类型就记录着“谁删了哪条消息”。不过这里有个限制审计日志不会记录每一条消息删除的详细顺序它更像一个摘要。当有人在短时间内批量删除大量消息时Discord 往往会合并成较少的审计日志条目这时候你靠日志只能知道“这批消息大概是谁删的”而不是逐条精确匹配。用 discord.js 读取审计日志的 API 大概是这样的const { AuditLogEvent } require(discord.js); const fetchedLogs await message.guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MessageDelete, }); const deletionLog fetchedLogs.entries.first(); if (!deletionLog) { // 没有找到对应的审计日志 } // deletionLog.executor 是执行删除的人 // deletionLog.target 是这条删除记录涉及的目标通常是被删除消息的作者3.2 完整实现与执行过程接下来我给出一个在实际项目中可以直接用的完整示例。逻辑是先监听messageDelete拿到消息 ID 和频道 ID然后调用fetchAuditLogs获取最近一条MESSAGE_DELETE记录接着验证这条记录是否真的对应我们正在处理的这条消息。为什么要验证因为服务器里删除消息这件事随时在发生你取到的“最近一条”很可能是别人删的不校验就直接拿executor结果肯定错乱。const { Client, GatewayIntentBits, AuditLogEvent } require(discord.js); const client new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, ], }); client.on(messageDelete, async (message) { // 私信消息没有审计日志直接跳过 if (!message.guild) return; try { // 拉取最近一条消息删除审计记录 const fetchedLogs await message.guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MessageDelete, }); const deletionLog fetchedLogs.entries.first(); // 没有日志说明可能权限不足或不是人工删除 if (!deletionLog) { console.log(消息 ${message.id} 被删除但无法获取审计日志); return; } // 关键校验判断这条日志是不是当前删除事件 const { executor, target, createdTimestamp } deletionLog; // 日志中记录的目标必须是被删消息的作者 const isTargetMatch target?.id message.author?.id; // 日志产生时间与删除事件时间相差不能太大3000ms内 const isRecent Date.now() - createdTimestamp 3000; // 如果目标不匹配或者时间差太多说明取到的是历史日志不能采用 if (!isTargetMatch || !isRecent) { console.log(消息 ${message.id} 被删除但审计日志无法确认删除者); return; } console.log(被删除的消息ID:, message.id); console.log(删除者:, executor?.tag ?? 未知); console.log(消息作者:, target?.tag ?? 未知); // 这里可以继续写你的业务逻辑比如发日志到指定频道 } catch (error) { // 常见错误缺少权限、API超时等 console.error(获取审计日志失败:, error.message); } }); client.login(你的BotToken);注意我用的两个校验条件一是target.id message.author?.id二是时间差Date.now() - createdTimestamp 3000。前者防张冠李戴后者防时间跨度过大导致匹配到历史记录。如果你发现日志总是匹配不上优先怀疑这两个条件是不是太严格比如系统删除消息时createdTimestamp可能跟事件时间差超过 3 秒那就要适当放宽。3.3 这个方案必须注意的几个坑审计日志方案虽然能拿到删除者但坑非常多。我一个个说每条都是实际操作中验证过的。第一个坑机器人删自己消息时不一定有审计日志。Discord 对 bot 自身的操作留痕规则和人工操作不同某些情况下 bot 通过 API 批量删除消息审计日志里可能根本没有对应条目或者只记录一条聚合日志。我遇到过测试脚本批量清理消息时fetchAuditLogs返回null的情况。解决方案是当拿不到日志时不要硬猜删除者而是记录为“无法确定”。第二个坑权限不足时fetchAuditLogs会直接抛错。bot 必须拥有查看审计日志的权限VIEW_AUDIT_LOG否则 API 会返回 403。这个错在错误消息里不那么显眼新手经常忽略。你可以在初始化时给 bot 角色勾上权限同时在代码里catch到 403 时额外提示运营者。第三个坑审计日志不是实时同步的。实践里我发现 Discord 的审计日志存在一定延迟有时候消息删除后立刻拉取拿到的最近一条不是刚发生的动作而是几秒前的另一个操作。所以我在代码里留了 3 秒窗口实际上如果你在超高频操作的服务器里最好把窗口缩短到 1 秒甚至直接改成“批量对比最近 5 条审计日志找到 target 精确匹配的那条”。第四个坑审核日志保留时间有限。Discord 的审计日志有保留期限免费情况下一般只能查最近的记录时间太久远通常超过 30-90 天具体取决于服务器等级就查不到了。如果需要长期追踪必须叠加数据库方案。4. 常见问题排查与进阶兜底方案方案一、方案二都讲完了但实际开发中你大概率会遇到一些排列组合式的问题比如数据时有时无、缓存失效、日志对不上等等。这一章我把最常见的几个问题抽出来给出排查思路最后再分享一个我一直在用的数据库兜底方案。4.1 author为null / 缓存失效怎么排查遇到message.author是null按顺序排查这几个点确认 bot 是否配置了Partials.Message。没有配置时访问一个不完整消息的author可能直接抛TypeError而不是返回null。配置了之后能优雅降级。确认消息是否在 bot 在线期间发送。如果 bot 重启过重启之前发送的消息不在缓存里删除时就是空。确认频道类型。messageDelete在私信、群聊、服务器文本频道都会触发但私信里message.guild是null如果你代码里没有判空一进来就报错。排查方法其实很朴素在messageDelete回调第一行加日志把message.id、message.partial、message.author?.id全打出来连续观察几条真实删除记录基本能定位是哪一环丢了。4.2 审计日志获取失败的原因代码没报错但deletionLog是空的最常见原因有三个bot 没有VIEW_AUDIT_LOG权限删除动作不是“人”做的比如是 Discord 官方移除违规内容、或者 bot 自己通过 API 删除删除发生得太早审计日志已经滚动出保留区。如果是权限问题你可以在服务器设置里检查 bot 角色权限。如果是类型问题建议在messageDelete里把message.author?.bot作为参考如果被删消息的作者是 bot大概率删除者也是脚本操作审计日志可能不完整。如果是滚动过期只能接受现实换数据库方案做长期记录。4.3 终极兜底模块化消息存储方案如果你在一个讲究审计合规的服务器里做运营工具只靠事件缓存和审计日志都不够稳我建议直接上数据库方案。核心思路很简单messageCreate时把消息完整元数据写进数据库messageDelete时先查数据库拿到消息作者和历史记录再结合审计日志写入删除者。我常用的是 SQLite 或 MySQL表结构大致这样CREATE TABLE message_logs ( id VARCHAR(64) PRIMARY KEY, author_id VARCHAR(32) NOT NULL, channel_id VARCHAR(32) NOT NULL, guild_id VARCHAR(32), content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL, deleted_by VARCHAR(32) NULL ); CREATE INDEX idx_author ON message_logs(author_id); CREATE INDEX idx_deleted ON message_logs(deleted_at);配合运行逻辑// 在 messageCreate 里执行 INSERT // 在 messageDelete 里执行 UPDATE把 deleted_at 和 deleted_by 填上实际写的时候deleted_by的来源需要两层判断先查审计日志如果审计日志拿不到就置为 NULL表示“未知删除者”。数据库方案比纯内存方案强在bot 重启不丢数据能追查历史记录也能做统计报表。缺点是要额外维护数据库对于只想做个 500 人小群管理 bot 的开发者来说可能过重。我在生产环境里的最终选型是内存缓存 审计日志 数据库三层结合。内存缓存响应速度最快用于即时日志审计日志补充删除者信息数据库做长期留存和回溯查询。三层各有分工基本覆盖了所有追踪场景。最后分享一个小技巧如果你已经按上面的方案跑起来了建议再加一个“删除者最近操作缓存”用一个小 Map 记录“删除者ID - 最近删除时间”。当messageDelete触发时先查审计日志如果日志有延迟拿不到就看看这个 Map 里最近 500ms 内有没有对应的删除者记录。这个技巧在处理高频批量删除时特别管用能比纯审计日志更稳地捕捉到操作者。当然如果拿不到就不要强行猜审计系统最忌讳伪造数据。我做这类追踪功能时的原则是宁可记录“未知”也不要猜测一个可能错误的删除者毕竟审计数据的可信度比数据完整度更重要。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →