基于Node.js的Telegram群管机器人自动化:敏感词过滤与定时任务实践
2026/9/16 20:03:54 网站建设 项目流程

简介:基于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.jsnode-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.jssensitive_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% 以上。注意deleteMessagesendMessage都加了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,true

cron使用标准五段 cron 表达式(分 时 日 月 周),chat_id是目标群组的 ID,message是消息正文,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参数为文件路径、streamBuffer,项目中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字段都会记录,最终效果是:正常任务到点即发,失败任务有迹可循,限流情况下不会加剧报错。这样一套组合下来,群管机器人才真正具备可维护性。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询