ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Node.js的Telegram群管机器人自动化:敏感词过滤与定时任务实践

基于Node.js的Telegram群管机器人自动化:敏感词过滤与定时任务实践 简介基于Telegram机器人开发的群组管理自动化工具面向社群运营人员、群组管理员及对机器人开发感兴趣的进阶用户旨在解决日常群组维护中重复性操作多、消息监管难等痛点。工具集成敏感词监控与过滤、定时消息群发、图片发送、多任务配置管理等核心功能可覆盖社群日常运营、定时提醒推送、广告内容自动发布等典型场景。资源包共8个文件包含核心JavaScript脚本、词库与任务配置txt/json/csv以及docx和md说明文档压缩包大小仅37KB结构清晰便于快速上手和二次开发。目前已有70人学习使用。借助该工具使用者能尽快搭建一套完整的Telegram群管机器人灵活配置敏感词规则与定时任务大幅提升社群管理效率同时通过配置管理模块实现多群组差异化运营。1. 群管机器人为什么值得一套可配置的自动化框架当一个人工维护的Telegram群组超过三五百人广告、争吵、水群在凌晨两三点集中爆发时管理员的时间根本不够用。这套基于Telegram机器人开发的群组管理自动化工具把敏感词监控、定时消息、图片群发和多任务配置管理收敛到一组配置文件和脚本里敏感词放在文本文件中可以随时改定时任务用CSV就能排整个脚本体量在几百行左右适合社群运营者和后端开发者直接复用。它的核心不是某个华丽的界面而是用最朴素的方式把高频重复操作变成可追踪的自动化流程。我把代码包里的结构拆了一遍下面是我觉得最值得关注的技术点。2. 项目结构与核心依赖理解 bots 的运行骨架打开压缩包后第一眼看到的是telegramBots-main目录里散落的几个 JS 文件和配置文件。很多人习惯直接运行node broadcast.js看到机器人动了就以为完事了其实这种项目管理的关键在于弄清楚哪些代码负责监听、哪些代码负责调度、哪些文件只是配置。这一章先把骨架讲明白后面调参才不会抓瞎。2.1 从 package.json 读依赖与脚本入口package.json是整个项目的入口说明书。一个典型的包声明长这样{ name: telegram-bots, version: 1.0.0, main: broadcast.js, scripts: { start: node broadcast.js }, dependencies: { node-telegram-bot-api: ^0.61.0, csv-parse: ^5.3.0, node-cron: ^3.0.0 } }main指向broadcast.js说明这是主进程文件而不是sensitive_words.js。node-telegram-bot-api负责和 Telegram Bot API 通信底层是 HTTPS 长轮询csv-parse用来读jobs.csv里的定时任务node-cron提供 cron 表达式调度能力。如果你手里的包里没有node-cron也可以用 Node.js 原生setTimeout做简单轮询但那样对“每天 10:00 发提醒”这种需求很别扭。这里有个选型上的注意点broadcast.js同时承担了监听和发送两个职责说明这个项目刻意保持了单进程的结构。好处是部署简单、一台小机器就能跑坏处是如果群组量很大长轮询回调里的阻塞操作会拖慢消息处理。我一般会保留单进程但把敏感词匹配改成异步批量匹配避免文本过滤阻塞消息收发。2.2 目录职责划分与启动流程项目里的文件数量不多但每个文件的职责边界很清楚文件职责broadcast.js主进程注册机器人监听、加载任务、初始化和启动sensitive_words.js敏感词过滤模块导出匹配函数sensitive_words.txt敏感词词库每行一个词或正则规则jobs.csv定时任务配置管理消息内容和发送时间package.json依赖、脚本和元数据声明启动流程并不复杂通常是在broadcast.js里先实例化TelegramBot然后调用sensitive_words.js的初始化函数加载词库再解析jobs.csv并注册定时任务。这个顺序不能乱因为如果先把机器人启动监听用户立刻发来一句话而此时词库还没加载完敏感词模块就会漏过这条消息。npm install node broadcast.js依赖安装完成后直接运行主进程代码里的长轮询会一直保持连接。调试时建议先用bot.getMe()验证 token 是否有效再继续后续联调。如果看到ETIMEDOUT先检查服务器是否能正常访问 Telegram 的 API 域名这个和机器人本身没有关系。3. 敏感词监控实时拦截从词库到消息失活的完整链路敏感词监控不能只做一次text.includes(word)就完事还要考虑大小写、空格、同音字替换、误杀率和词库热更新。这个项目把词库和匹配逻辑分开本身就是一种可维护的设计。我以sensitive_words.js和sensitive_words.txt为基础拆解实时过滤的完整实现链路。3.1 词库设计纯文本行的规则加载sensitive_words.txt的每一行代表一个敏感词或正则表达式。最简单的格式就是普通文本但生产环境里我建议把规则分成两类一类是精确词比如“代开发票”另一类是正则表达式比如\b广告\b。在 JavaScript 里加载词库时可以通过文件内容的首个字符来区分const fs require(fs); const readline require(readline); async function loadWords(filePath) { const words []; const rules []; const stream fs.createReadStream(filePath); const rl readline.createInterface({ input: stream }); for await (const line of rl) { const trimmed line.trim(); if (!trimmed || trimmed.startsWith(#)) continue; if (trimmed.startsWith(/)) { const match trimmed.match(/^\/(.)\/([a-z]*)$/); if (match) rules.push(new RegExp(match[1], match[2])); } else { words.push(trimmed.toLowerCase()); } } return { words, rules }; }#开头的是注释/.../i格式的会被当成正则解析普通行一律转小写后保存。这样做的理由是Telegram 用户经常会用大写、空格、表情符号绕过简单过滤词库层面统一小写比在匹配时反复转换更省 CPU。加载词库的时间应该放在机器人启动阶段而不是每条消息都读一次文件。3.2 消息监听与实时过滤钩子broadcast.js里通常会注册on(message)事件这是所有文字消息进入处理管道的唯一入口。敏感词过滤应该放在这里并且要在回复任何内容之前执行。典型实现如下const { words, rules } await loadWords(sensitive_words.txt); bot.on(message, async (msg) { if (!msg.text) return; const text msg.text.toLowerCase(); let hitRule null; for (const word of words) { if (text.includes(word)) { hitRule word; break; } } if (!hitRule) { for (const rule of rules) { if (rule.test(text)) { hitRule rule.source; break; } } } if (hitRule) { await bot.deleteMessage(msg.chat.id, msg.message_id).catch(() {}); await bot.sendMessage(msg.chat.id, 消息已被过滤原因包含敏感词: ${hitRule}) .catch(() {}); } });这段代码的逻辑顺序是先匹配精确词再匹配正则规则。为什么不是先正则因为正则性能通常比includes差先用高频的精确词卡住大部分广告再用正则兜底拦截率能到 95% 以上。注意deleteMessage和sendMessage都加了catch(() {})这是因为 Telegram API 对重复删除或消息权限限制会返回错误不能因为一次删除失败就让整个回调抛异常。这里更关键的参数是msg.chat.id。在群组里它通常是负数在私聊里是正数过滤模块不需要特意区分但发送过滤提示时要注意群组是否有禁言权限否则提示消息一样会被群管理员视为噪音。我一般会把提示消息也做成可配置项放在jobs.csv里统一管理。3.3 误杀控制与豁免策略纯关键词匹配最大的问题是误杀。群友说“我要举报这个广告”如果词库里恰好有“举报”或“广告”整句都会被吞掉。常见做法是加入长度阈值当消息长度小于 5 个字符且命中短词时不直接删除而是先通过restrictChatMember把用户设为只读。这个策略在sensitive_words.js里可以被设计成一个独立函数function shouldBlock(text, minLength 5) { const wordsHit words.filter(w text.includes(w)); if (wordsHit.length 0) return false; const longWordHit wordsHit.some(w w.length minLength); return text.length minLength || longWordHit; }当短词只命中一个且消息很短时很可能是误杀应该交给管理员复核而不是机器人直接下结论。用这种策略可以显著降低社群成员的投诉量。另外白名单机制也值得补上允许群管理员把某些用户 ID 加入豁免列表这部分信息可以存在jobs.csv之外单独的 JSON 文件里避免和定时任务混淆。4. 定时消息与图片群发jobs.csv 驱动 broadcast.js 的多任务调度定时消息群发是这个项目最抢眼的部分。jobs.csv不只是存消息内容它承载了多任务配置管理的核心模型每行一个任务通过 cron 表达式决定什么时候发、发给哪个群、发文字还是图片。理解这个模型就能在不用改代码的情况下扩展任何定时提醒场景。4.1 jobs.csv 的任务字段与格式约束一个实际可用的jobs.csv通常包含以下字段id,cron,chat_id,message,image_path,enabled 1,0 9 * * *,-100123456789,早安提醒今日社群早报已发布,, 2,0 18 * * *,-100123456789,晚间公告请勿刷屏,,false 3,30 10 * * 1,-100987654321,每周一产品动态更新,./assets/weekly.png,truecron使用标准五段 cron 表达式分 时 日 月 周chat_id是目标群组的 IDmessage是消息正文image_path是可选图片路径enabled控制该任务是否参与调度。这里有一个容易踩的坑CSV 中的chat_id如果以负号开头某些解析库会把它当作数字并丢弃符号。所以读取时不能直接parseFloat必须保留为字符串后再通过Number()转换。const fs require(fs); const { parse } require(csv-parse); fs.createReadStream(jobs.csv) .pipe(parse({ columns: true, trim: true })) .on(data, (row) { const chatId Number(row.chat_id); if (!Number.isInteger(chatId) || row.enabled ! true) { return; } registerTask(row.id, row.cron, chatId, row.message, row.image_path); });这样处理能确保负群组 ID 不丢失。enabled字段的值统一用字符串true判断避免 CSV 解析成布尔值导致兼容问题。4.2 基于 node-cron 的调度器封装拿到cron表达式后常见的做法是用node-cron注册任务。但直接循环注册会有一个隐患如果脚本崩溃重启所有定时任务都会重新注册已经发出去的那部分消息可能被重复发送。所以通常会在broadcast.js内部维护一个任务 Map用job.id做键const cron require(node-cron); const jobRegistry new Map(); function registerTask(id, cronExpr, chatId, message, imagePath) { if (jobRegistry.has(id)) { console.warn(Task ${id} already registered, skip); return; } const task cron.schedule(cronExpr, async () { try { if (imagePath) { await bot.sendPhoto(chatId, imagePath, { caption: message }); } else { await bot.sendMessage(chatId, message); } } catch (err) { console.error(Failed to send task ${id}:, err.message); } }); jobRegistry.set(id, task); }registerTask在每次进程重启后调用jobRegistry的作用是防止同一 ID 的任务被重复注册。这个设计在开发模式下很有效因为 Node.js 的nodemon会自动重启进程如果没有这个 Map几分钟内的重启就会产生多条重复消息。发送失败时记录日志而不是直接抛出异常是因为 Telegram API 对速率限制很敏感单条消息失败不应该拖垮整个调度循环。4.3 图片发送与多媒体兼容细节图片发送看似只是sendPhoto一个方法实际上参数细节决定了任务的稳定性。sendPhoto接受photo参数为文件路径、stream或Buffer项目中image_path用文件路径最直观。图片上传失败时常见原因不是路径写错而是服务器上的图片超过了 Telegram 的硬性限制当前限制是照片最大 10 MB文档最大 50 MB。我在处理这种资源包时通常会把图片统一压缩到 720p 以下既能保证清晰度又明显降低上传超时概率。bot.sendPhoto(chatId, ./assets/banner.png, { caption: message, disable_notification: true });disable_notification: true是一个容易被忽略的参数。对早安提醒这类低优先级任务关闭通知可以避免打扰用户而真正重要的定时公告则不要把这项打开。具体策略可以放到jobs.csv里加一个silent字段让运营在不动代码的情况下决定每条任务是否强提醒。5. 生产环境优化热更新词库与任务触达验证代码跑起来只是第一步真正长期稳定运行靠的是两个细节词库能不能不改代码就更新以及定时消息发出去之后怎么确认触达有效。针对这两个点我整理了三个可以直接落地的优化做法。5.1 敏感词热更新开发环境里每次都重启进程来加载新词但生产环境不能这么干因为重启会导致长轮询连接断开极端情况下会丢失消息。更稳妥的方案是用fs.watch监听sensitive_words.txt的变化fs.watch(sensitive_words.txt, () { const { words, rules } loadWordsSync(sensitive_words.txt); global.__wordFilter { words, rules }; });把词库对象挂到global上过滤模块每次读取当前快照。文件一改动下一次消息进来时自动使用新规则。这种方式对管理员最友好在服务器上直接用vim改词库不需要碰机器人进程。注意fs.watch在部分 Linux 环境下对文件内容变更不敏感我一般会退一步每 30 秒用fs.stat检查文件修改时间改动时再重新加载。5.2 定时任务发送结果落表broadcast.js的定时任务如果有失败不能只看控制台日志。我建议在进程中维护一个发送结果数组并在任务完成后追加一行 JSON 记录到send_log.jsonl例如echo {taskId:3,chatId:-100123456789,time:2025-01-01 18:00:00,status:success} send_log.jsonl这样做的意义在于当社群运营质疑“今天的提醒为什么没发”时可以直接查看这个文件用grep按任务 ID 筛选立刻定位是 API 超时、群组被解散还是 token 失效。配合定时任务本身这个做法可以让异常恢复时间从小时级别降到分钟级别。5.3 触达失败后的退避重试定时消息触达失败后最常见的错误是429: Too Many Requests。这时不要立刻重发而是记录下失败任务等待一段时间后再试。简单实现是在broadcast.js里维护一个失败队列const retryQueue []; function pushRetry(task, delayMs) { retryQueue.push(setTimeout(() { sendNow(task).catch(() pushRetry(task, delayMs * 2)); }, delayMs)); }退避时间从 5 秒开始每次失败翻倍最多重试 3 次。注意这里一定要指数退避否则失败集中时反而会加剧限流。配合前面的日志文件每次重试的time字段都会记录最终效果是正常任务到点即发失败任务有迹可循限流情况下不会加剧报错。这样一套组合下来群管机器人才真正具备可维护性。本文还有配套的精品资源点击获取
返回列表