简介:这份资源是一套基于 Wechaty 框架开发的智能微信群聊机器人助手源码,面向具备一定 Node.js 基础、希望快速搭建群管理工具的开发者与运维人员。它针对群消息繁杂、重要信息易被撤回、多群难以统一管理等痛点,提供自动回复、防撤回记录、疫情数据查询、天气获取、新闻推送、娱乐互动、多群管理与定时提醒等能力,并附带消息记忆存档机制。压缩包共 23 个文件,约 127KB,以 11 个 js 业务脚本为核心,配合 6 个 json 配置与记忆卡文件、1 个 Dockerfile 部署文件、1 个 env 环境变量文件及说明文档,结构紧凑、便于二次开发。目前已有 173 人学习下载。读者可从中获得完整的机器人事件处理逻辑、多群消息转发与记忆卡持久化思路、容器化部署方案及配套说明文档,适合作为微信生态自动化项目的实践参考。
1. 从一条被撤回的消息说起:wechaty 机器人到底能接住多少活
凌晨一点,群里有人发了一张截图又秒撤回,第二天早上运营同事翻遍聊天记录只看到「消息已撤回」四个字。这种场景做社群的人都不陌生——关键信息被撤回、重要通知没人看到、新成员进群没人接待、每天重复回答「今天天气怎么样」「疫情数据在哪看」。这些琐事单拎出来都不难,难的是它们同时发生在一个几十个群的微信生态里,靠人工根本盯不过来。
基于 wechaty 框架开发的智能微信群聊机器人助手,本质上就是把这些高频、重复、有时效性的群聊操作自动化:自动回复聊天对话、防消息撤回、疫情实时数据查询、天气信息获取、新闻资讯推送、娱乐互动游戏、多群管理、定时任务提醒、消息记录归档。它适合谁?适合手里管着多个微信群、又不想被群消息绑架的运营、社区负责人和独立开发者。这篇文章不讲空概念,直接拆开讲每个模块怎么落地、参数怎么调、哪里容易翻车。
2. wechaty 选型与最小可运行骨架:为什么不是 itchat 也不是企业微信 API
2.1 选 wechaty 而不是其他方案的三个硬理由
做微信群机器人,绕不开几个候选:itchat(已基本停更)、企业微信机器人(受限于企业内部群)、以及 wechaty。我最终选 wechaty,核心是三点。
第一,协议适配层做得干净。wechaty 把底层通信抽象成 puppet,你写业务逻辑时不用关心消息是怎么收发的,换 puppet 就像换数据库驱动。第二,事件模型完整。message、room-join、room-leave、friendship、login、logout这些事件覆盖了群聊机器人的绝大多数触发场景,防撤回本质上就是监听message事件时缓存消息体,等room-topic或撤回事件到来时比对。第三,TypeScript 原生支持,类型提示能帮你在写多群管理逻辑时少踩很多字段拼错的坑。
常见做法是先用wechaty-puppet-wechat在本地跑通,再根据实际并发量决定要不要换付费 puppet。我一般会建议新手先用免费 puppet 把业务逻辑跑顺,别一上来就纠结协议稳定性。
2.2 最小可运行骨架:20 行代码让机器人先开口
import { WechatyBuilder } from 'wechaty' const bot = WechatyBuilder.build({ name: 'wx-assistant', puppet: 'wechaty-puppet-wechat', }) bot.on('scan', (qrcode, status) => { console.log(`扫码登录,状态: ${status}`) // 实际部署时这里生成二维码图片供扫描 }) bot.on('login', (user) => { console.log(`用户 ${user} 已登录`) }) bot.on('message', async (msg) => { // 忽略自己发送的消息,避免死循环 if (msg.self()) return const text = msg.text() const room = msg.room() // 只处理群聊消息 if (room && text === 'ping') { await room.say('pong') } }) bot.start() .then(() => console.log('机器人启动成功')) .catch((e) => console.error('启动失败', e))这段代码的逻辑很直白:WechatyBuilder.build创建实例,scan事件负责登录二维码,login确认登录成功,message是核心事件入口。参数上,name是本地会话缓存名,换名字等于换一套登录态;puppet指定协议实现,本地调试用wechaty-puppet-wechat就够。
关键点在msg.self()这个判断。如果不加,机器人回复的消息会再次触发message事件,形成无限循环,这是新手最常见的翻车点之一。另一个参数是room,只有群聊消息才有值,私聊消息这里为null,所以多群管理逻辑必须建立在room判断之上。
2.3 多群管理的核心:room 对象与群白名单
多群管理不是简单地「对所有群生效」,而是要有白名单机制。我一般会维护一个allowedRooms集合,存群 ID 或群名称,消息进来先判断room.id是否在集合里。
const allowedRooms = new Set<string>() // 启动后拉取机器人已加入的群 bot.on('login', async () => { const rooms = await bot.Room.findAll() rooms.forEach((r) => allowedRooms.add(r.id)) console.log(`已加载 ${allowedRooms.size} 个群`) }) bot.on('message', async (msg) => { const room = msg.room() if (!room) return if (!allowedRooms.has(room.id)) return // 非白名单群直接忽略 // 后续业务逻辑 })Room.findAll()返回机器人当前加入的所有群,room.id是稳定标识,比群名称可靠——群名称可以被任意成员修改,用名称做白名单迟早出事。这一步做完,后面所有功能模块都挂在这个白名单判断之后,避免机器人跑到不该去的群里乱说话。
3. 自动回复与防撤回:消息缓存、匹配策略和三个必调参数
3.1 自动回复的匹配策略:精确、前缀还是正则
自动回复最容易写成「关键词命中就回复」,但实际跑起来会发现两个问题:一是关键词太死,用户换个说法就匹配不上;二是关键词太泛,随便一句话都触发回复,群里显得很吵。
我的做法是分三层匹配。第一层精确匹配,用于「天气」「新闻」这类固定指令;第二层前缀匹配,用于「查询 北京疫情」这种带参数的指令;第三层正则匹配,用于「今天.*天气」这种模糊表达。三层按顺序执行,命中即返回,避免重复回复。
const exactRules: Record<string, string> = { '天气': '请发送「天气 城市名」查询天气', '新闻': '正在获取最新新闻...', } const prefixRules: Array<{ prefix: string; handler: (arg: string) => string }> = [ { prefix: '天气 ', handler: (city) => `正在查询 ${city} 的天气` }, { prefix: '查询 ', handler: (kw) => `正在查询 ${kw}` }, ] function matchReply(text: string): string | null { if (exactRules[text]) return exactRules[text] for (const rule of prefixRules) { if (text.startsWith(rule.prefix)) { return rule.handler(text.slice(rule.prefix.length).trim()) } } return null }exactRules用对象存精确指令,prefixRules用数组存前缀规则,matchReply返回null表示不回复。参数上,前缀规则里的空格很重要——「天气 北京」和「天气北京」应该走不同分支,前者带参数,后者是纯指令。这个细节不处理好,用户输入「天气北京」时会得到「请发送天气 城市名」的提示,体验很差。
3.2 防消息撤回:缓存窗口设多大才合理
防撤回的原理不复杂:每条消息进来时缓存一份,当收到撤回通知时,把缓存的原消息重新发出来。但 wechaty 的撤回事件在不同 puppet 下表现不一致,有的 puppet 会触发message事件并带type为Recalled,有的则通过room-topic之类的间接方式。稳妥做法是双保险:既监听撤回类型消息,也维护一个定时清理的消息缓存。
interface CachedMsg { talker: string text: string timestamp: number } const msgCache = new Map<string, CachedMsg>() const CACHE_TTL = 5 * 60 * 1000 // 缓存 5 分钟 bot.on('message', async (msg) => { const room = msg.room() if (!room) return // 撤回消息处理 if (msg.type() === bot.Message.Type.Recalled) { const recalled = msg as any const originId = recalled.id // 部分 puppet 可拿到原消息 id const cached = msgCache.get(originId) if (cached) { await room.say(`[防撤回] ${cached.talker}: ${cached.text}`) msgCache.delete(originId) } return } // 正常消息缓存 const talker = msg.talker() msgCache.set(msg.id, { talker: talker?.name() || '未知', text: msg.text(), timestamp: Date.now(), }) }) // 定时清理过期缓存,防止内存泄漏 setInterval(() => { const now = Date.now() for (const [id, cached] of msgCache) { if (now - cached.timestamp > CACHE_TTL) msgCache.delete(id) } }, 60 * 1000)CACHE_TTL设 5 分钟是权衡结果:太短,用户撤回慢一点就抓不到;太长,内存占用大,而且撤回很久之前的消息再发出来反而打扰群成员。setInterval每分钟清理一次,这是必须的,否则机器人跑几天内存就爆了。msg.id作为缓存键,在支持撤回事件带原 id 的 puppet 下能精确匹配,不支持的 puppet 下只能靠时间窗口近似匹配,这是协议层的限制,不是代码能完全解决的。
3.3 三个必调参数:回复延迟、频率限制、缓存 TTL
自动回复和防撤回跑起来后,有三个参数直接决定体验好坏。
回复延迟:不要收到消息立刻回复,加 500 到 1500 毫秒随机延迟。原因有两个,一是避免被判定为机器行为,二是给用户一种「有人在看」的感觉。我一般用setTimeout包一层。
频率限制:同一个用户 10 秒内最多触发 3 次自动回复,超过就静默。否则有人连续发「天气天气天气」,机器人会刷屏。
缓存 TTL:前面说的 5 分钟是防撤回的,自动回复的上下文缓存可以短一些,2 分钟足够,因为对话通常不会跨太久。
这三个参数没有标准答案,但有一个判断标准:把机器人放进一个 200 人的活跃群跑一天,如果没人抱怨「机器人好烦」,参数就基本对了。
4. 疫情数据、天气与新闻推送:外部接口调用与定时任务编排
4.1 疫情实时数据查询:接口选型与数据格式处理
疫情数据查询这个需求,核心不是技术难度,而是数据源的稳定性和格式统一。常见做法是接一个公开的疫情数据接口,返回 JSON 后解析成文本。这里要注意的是,接口返回的字段名各平台不一样,有的用confirm,有的用confirmed,有的把省份和城市嵌套在不同层级。
import axios from 'axios' interface EpidemicData { province: string city: string confirmed: number recovered: number deaths: number } async function queryEpidemic(keyword: string): Promise<string> { try { const res = await axios.get('https://example-api/epidemic', { params: { keyword }, timeout: 5000, }) const list: EpidemicData[] = res.data.data || [] if (list.length === 0) return `未找到 ${keyword} 的疫情数据` return list .slice(0, 5) .map((d) => `${d.province}${d.city} 确诊${d.confirmed} 治愈${d.recovered} 死亡${d.deaths}`) .join('\n') } catch (e) { console.error('疫情接口请求失败', e) return '疫情数据暂时获取失败,请稍后再试' } }timeout设 5000 毫秒是必须的,外部接口不可控,不设超时机器人会卡住。slice(0, 5)限制返回条数,避免一条消息太长被微信截断。错误处理里返回友好提示而不是抛异常,因为群聊场景下用户不需要看到堆栈信息。
4.2 天气信息获取:城市名解析和缓存策略
天气查询的坑主要在 city 参数上。用户输入「天气 北京」没问题,但输入「天气 朝阳」就可能查不到,因为朝阳是区不是市。我的做法是维护一个常用城市别名表,把「朝阳」「浦东」这类区名映射到所属城市。
const cityAlias: Record<string, string> = { '朝阳': '北京', '浦东': '上海', '天河': '广州', '南山': '深圳', } async function queryWeather(city: string): Promise<string> { const normalized = cityAlias[city] || city const cacheKey = `weather:${normalized}` const cached = weatherCache.get(cacheKey) if (cached && Date.now() - cached.time < 30 * 60 * 1000) { return cached.text } // 调用天气接口... const text = `正在查询 ${normalized} 天气` weatherCache.set(cacheKey, { text, time: Date.now() }) return text }天气数据 30 分钟内不会大变,缓存能显著减少接口调用。cityAlias表不用全,覆盖群里成员常提到的几个区就够,后续按实际查询失败日志补充。
4.3 新闻资讯推送与定时任务:cron 表达式怎么写才不翻车
新闻推送和定时提醒都依赖定时任务。Node.js 里常用node-cron,表达式是标准五段式:分 时 日 月 周。
import cron from 'node-cron' // 每天早上 8 点推送新闻 cron.schedule('0 8 * * *', async () => { const news = await fetchNews() for (const roomId of allowedRooms) { const room = await bot.Room.find({ id: roomId }) await room?.say(`今日新闻:\n${news}`) } }) // 每周一上午 9 点提醒周会 cron.schedule('0 9 * * 1', async () => { // 提醒逻辑 })0 8 * * *是每天 8 点整,0 9 * * 1是每周一 9 点。这里最容易翻车的是时区——服务器如果是 UTC 时间,8 点会变成北京时间 16 点。部署时要么把服务器时区设为Asia/Shanghai,要么在 cron 表达式里手动偏移。我一般直接在 Dockerfile 里设TZ=Asia/Shanghai,省得后面算来算去。
另一个坑是定时任务里的异常没捕获。如果fetchNews抛错,整个 cron 回调会中断,后续群都收不到推送。所以每个定时任务内部都要 try-catch,失败时记录日志并继续下一个群。
5. 避坑与排查:消息撤回抓不到、定时任务不执行、多群串消息
5.1 撤回消息抓不到,缓存里是空的
现象:群里有人撤回消息,机器人没反应,日志里也看不到撤回事件。
原因:不同 puppet 对撤回事件的支持程度不一样。免费 puppet 有时根本不触发Recalled类型消息,或者触发时拿不到原消息 id。
解决:先打印msg.type()确认撤回事件是否触发。如果触发了但拿不到原 id,就退化成时间窗口匹配——缓存最近 30 秒内该群的所有消息,撤回时把最近一条发出来。如果连事件都不触发,说明当前 puppet 不支持,只能换 puppet 或放弃这个功能。这是协议层限制,代码层面无解。
5.2 定时任务到点不执行
现象:cron 表达式写好了,但到时间没动静。
原因:三种可能。一是服务器时区不对,前面说过;二是进程被重启后 cron 没重新注册;三是node-cron的schedule返回的任务对象没被持有,被垃圾回收了。
解决:时区问题在部署层解决。进程重启问题用pm2或systemd守护,确保机器人常驻。任务对象问题,把cron.schedule的返回值存到一个数组里,别让它被回收。
5.3 多群消息串了,A 群的回复发到 B 群
现象:用户在 A 群问天气,机器人把回复发到了 B 群。
原因:异步逻辑里room变量被覆盖。比如在循环里用await处理多个群,但room是外层变量,异步执行时已经变了。
解决:每个异步操作里把room作为参数传进去,不要在回调里引用外层可变变量。这是 JavaScript 闭包的经典坑,写多群逻辑时尤其要注意。
5.4 消息记录归档把磁盘写满
现象:机器人跑了一周,服务器磁盘告警。
原因:消息记录没做滚动清理,所有群所有消息都往一个文件里追加。
解决:按天分文件,保留最近 7 天,用fs.appendFile写入,每天凌晨清理过期文件。如果消息量大,考虑写 SQLite 而不是纯文本,方便查询和清理。
5.5 机器人被群成员举报后掉线
现象:机器人突然掉线,重新登录后很快又掉。
原因:回复频率太高、内容重复度高,被判定为骚扰行为。
解决:降低回复频率,增加随机延迟,避免对同一句话反复回复。如果已经被限制,停用一段时间再试。这个没有技术上的后悔药,只能从行为模式上规避。
6. 进阶技巧:用消息队列削峰,让机器人在 50 个群里也不卡
机器人管几个群时,直接在message事件里处理业务没问题。但群一多,消息并发上来,同步处理会堵。我一般会引入一个简单的内存队列,把消息先入队,再由固定数量的 worker 消费。
import PQueue from 'p-queue' // 并发度设为 3,避免同时调用太多外部接口 const queue = new PQueue({ concurrency: 3 }) bot.on('message', async (msg) => { if (msg.self()) return const room = msg.room() if (!room || !allowedRooms.has(room.id)) return // 消息入队,不阻塞事件循环 queue.add(async () => { try { await handleMessage(msg) } catch (e) { console.error('消息处理失败', e) } }) })concurrency: 3是经验值,太小处理慢,太大外部接口扛不住。queue.add把每条消息包成一个任务,handleMessage里做实际的匹配和回复。这样即使某个群的消息处理卡住,也不会影响其他群。
验证队列是否生效,可以打日志看队列长度:queue.size和queue.pending。如果size持续增长,说明消费速度跟不上,要么提高并发度,要么优化handleMessage里的耗时操作。
另一个技巧是给不同功能设不同优先级。自动回复优先级高,新闻推送优先级低。p-queue支持priority参数,把自动回复设为 1,推送设为 10,队列会优先处理数字小的。
最后说一个我自己的习惯:每次加新功能前,先在测试群里跑 24 小时,观察内存占用和消息延迟。机器人这东西,功能加得越多,出问题的面越大,宁可慢一点,也别让它在正式群里翻车。希望帮到你。
本文还有配套的精品资源,点击获取