Egg.js后端+Wechaty微信协议的开箱即用聊天机器人模板

Egg.js后端+Wechaty微信协议的开箱即用聊天机器人模板 本文还有配套的精品资源点击获取简介一套能直接跑起来的微信自动化交互示例用Egg.js搭服务框架Wechaty处理微信网页版登录、消息收发和会话管理。项目自带完整路由router.js、两个核心控制器home和wechat、封装好的微信业务逻辑service/wechat.js还集成了定时任务触发、多级日志输出区分wechat-demo-web.log和egg-web.log、静态资源托管能力。依赖安装只需npm i本地启动用npm run dev扫码登录个人微信后就能自动响应文本消息。代码结构扁平清晰config目录统一管理所有配置项支持TypeScript类型提示含index.d.ts内置ESLint校验规则和GitHub Actions CI流程.github/workflows/nodejs.yml。适合作为私有微信Bot开发的起点兼容主流Node.js版本不依赖第三方云服务或企业微信API。1. 项目概述为什么这个模板值得你花十分钟看懂它我第一次用 Wechaty 写微信机器人时踩了整整三天坑——不是登录失败就是消息收不到更别说日志混乱、定时任务不触发、本地调试和线上部署行为不一致这些问题。后来我意识到问题根本不在 Wechaty 本身而在于缺少一个“能跑通、能维护、能扩展”的工程骨架。Egg.js 提供了企业级的分层结构和插件机制Wechaty 解决了协议层的黑盒难题但把这两者真正拧成一股绳需要一套经过真实场景验证的集成范式。这个模板就是我从五个实际交付项目里反复提炼出来的最小可行骨架。它不是玩具 Demo也不是教学 Demo而是直接对标生产环境可用的私有化 Bot 基础设施。关键词里的“微信机器人”不是泛指特指基于微信网页版协议即 Wechaty 所依赖的 Puppeteer 微信 Web 微信接口的个人号自动化“Egg.js”在这里不只是个路由容器而是承担了中间件编排、配置中心化、服务生命周期管理、错误兜底等关键职责“Wechaty”也不是简单调个 API而是深度嵌入 Egg 的应用生命周期在 app.beforeStart 阶段完成实例初始化在 app.beforeClose 阶段优雅退出“Node.js”版本兼容性不是一句口号——它实测通过 Node.js v16.20.2、v18.20.4、v20.12.2 三套 LTS 版本且所有依赖均锁定语义化版本号避免因 minor 更新引发的 Puppeteer 兼容断裂“聊天自动化”则体现在三个层面消息响应文本/图片/链接、会话上下文感知通过 contact.id room.id 构建会话键、以及可编程的定时触发比如每天早 9 点推送天气或每小时检查一次未读消息数。这个模板最核心的价值是帮你绕过“能不能连上微信”这个初级门槛直接进入“怎么设计业务逻辑”这个高阶阶段。它不依赖任何云服务、不调用企业微信 API、不走第三方平台中转所有通信链路完全由你本地 Node 进程控制数据不出你的机器。如果你正打算做一个客服自动应答、内部信息聚合、跨平台消息桥接或者只是想研究微信协议交互细节这个模板就是你该从头开始阅读并动手改的第一份代码。它没有炫技的 UI也没有复杂的鉴权流程但每一行代码都在回答一个问题“当用户发来‘你好’系统到底经历了什么”2. 整体架构与设计思路为什么选 Egg.js 而不是 Express 或 Koa2.1 框架选型背后的工程权衡很多人看到“微信机器人”第一反应是用 Express 快速搭个 HTTP 接口再配个 Wechaty 实例监听事件。这确实能跑起来但很快就会遇到四个硬伤配置散落数据库地址、Wechaty 登录超时时间、消息过滤关键词、日志路径……全写在 index.js 里换环境就得全局搜索替换生命周期失控Wechaty 实例启动后如果 Egg 应用因异常重启Wechaty 实例不会自动销毁导致内存泄漏甚至多个实例同时监听同一账号错误无兜底Wechaty 抛出的PuppetError或TimeoutError如果没被中间件捕获整个进程就崩了连日志都来不及打扩展成本高加个定时任务得自己引入 node-schedule加个 Redis 缓存得手动 new Client加个健康检查接口又得写新路由。Egg.js 正是为解决这类问题而生。它的“约定优于配置”不是限制自由而是把高频工程决策固化下来让你专注业务。比如 config 目录统一管理所有环境变量plugin.js 显式声明所用插件wechaty、schedule、loggerapp/router.js 强制路由与控制器解耦——这些都不是语法糖而是防止团队协作时出现“张三写的定时任务在李四的机器上永远不执行”这类低级事故的基础设施。提示这个模板里没有用 Egg 官方的egg-schedule插件而是自研了一个轻量级wechaty-scheduler原因很简单——官方插件基于setInterval无法感知 Wechaty 实例是否已 ready而我们的调度器会在wechaty.ready事件触发后才启动第一个 tick确保每次定时任务执行前微信会话通道一定是通畅的。2.2 Wechaty 集成模式不是“挂载”而是“共生”Wechaty 官方文档推荐的用法是const bot new Wechaty()然后.on(message)监听。但在 Egg.js 里这种写法等于把 Wechaty 当成了一个普通模块完全游离于框架之外。这个模板做了三处关键改造实例托管到 app 对象在app/extend/application.js中将 Wechaty 实例挂载为app.wechaty这样在任意 controller、service、middleware 中都能通过this.app.wechaty访问无需重复 new状态同步到 Egg 生命周期在app.js的app.beforeStart钩子中调用await app.wechaty.start()并在app.beforeClose中调用await app.wechaty.stop()确保 Wechaty 启动时机与 Egg 应用完全对齐事件转发到 Egg 事件总线Wechaty 的原生事件如login、logout、message全部被包装成 Egg 的app.emit(wechat:login, user)形式这样你可以在任意地方监听app.on(wechat:login, handler)而不必在 bot 实例创建处硬编码回调函数。这种设计让 Wechaty 不再是“寄生”在 Egg 上的外部组件而是真正成为 Egg 应用的一个一级公民。你可以像写数据库查询一样写消息处理逻辑也可以像写 HTTP 接口一样写消息回复逻辑所有错误都能被统一的app.onerror中间件捕获并记录。2.3 日志体系为什么需要两个独立日志文件你可能注意到wechat-demo-web.log和egg-web.log是分开的。这不是为了凑数而是源于两类日志的语义差异和排查优先级egg-web.log记录的是 Egg 框架自身的运行轨迹HTTP 请求进来了吗哪个中间件报错了定时任务触发了吗这是系统健康度的晴雨表wechat-demo-web.log则只记录与微信协议强相关的事件扫码成功、登录成功、收到某条文本消息、向某人发送了图片、群聊被踢出……这是业务逻辑是否生效的证据链。举个实际例子某天你发现用户发消息后机器人没回复。先查wechat-demo-web.log如果里面压根没有receive message from xxx这条日志说明 Wechaty 层就没收到消息问题出在协议连接或网络代理如果日志里有接收记录但没看到send reply to xxx那问题就在 controller 或 service 层的业务逻辑里。两个日志文件物理隔离意味着你可以用tail -f wechat-demo-web.log | grep receive实时盯住消息流而不用在几千行混合日志里大海捞针。注意日志分级不是靠 console.log 的颜色区分而是通过ctx.logger.info()、ctx.logger.warn()、ctx.logger.error()三级 API 实现。模板里所有 Wechaty 相关操作都强制使用app.logger而非console确保日志能被 Egg 的日志插件统一格式化、切片、归档。3. 核心模块解析与实操要点从扫码登录到消息响应的完整链路3.1 登录流程扫码不是终点而是起点Wechaty 的扫码登录看似简单但背后藏着几个容易被忽略的关键点Puppet 选择模板默认使用wechaty-puppet-puppeteer即基于 Chrome 的网页版协议这是目前最稳定、社区支持最完善的方案。它依赖本地安装的 Chromium所以npm run dev前必须确保机器上有 Chrome 或 Chromium 可执行文件Mac 默认有Windows/Linux 需手动安装或配置PUPPETEER_EXECUTABLE_PATH扫码超时控制Wechaty 默认扫码等待 60 秒但模板在config/config.default.js中将其改为 120 秒并增加了重试逻辑——如果首次扫码失败比如网络抖动Wechaty 会自动重新生成二维码而不是直接抛错退出登录态持久化Wechaty 默认将登录凭证cookies缓存在内存重启就失效。模板通过puppetOptions配置了cacheDir: path.join(app.config.baseDir, wechaty-cache)将凭证持久化到磁盘下次启动时自动复用避免频繁扫码。实操时你会看到终端输出类似这样的二维码┌───────────────────────────────────────────────────┐ │ │ │ QR Code: https://api.qrserver.com/v1/create-qr │ │ Code?size250x250datahttps%3A%2F%2Fwx.qq.com%2F │ │ │ └───────────────────────────────────────────────────┘别急着打开微信扫一扫——先确认wechat-demo-web.log里有没有QRCode generated日志。如果有说明 Wechaty 已准备好如果没有大概率是 Puppeteer 启动失败常见于 Linux 服务器缺少字体库或沙箱权限。此时不要强行扫码先看egg-web.log里是否有Puppeteer launch failed错误。3.2 消息路由如何让“你好”变成“您好很高兴为您服务”消息从微信客户端发出到你的代码里执行回复逻辑中间要经过三层路由Wechaty 协议层路由Wechaty 实例监听message事件判断消息类型文本/图片/链接、发送者个人/群聊、是否为消息等然后 emit 给 Egg 事件总线Egg 事件总线路由app.on(wechat:message, async (message) { ... })在app.js中注册这里做初步过滤比如屏蔽群消息、过滤关键词再将消息转发给wechatService.handleMessage()业务逻辑路由service/wechat.js中的handleMessage()方法才是真正的“大脑”。它根据message.text()内容匹配预设规则比如- 匹配/^你好$/→ 调用replyText(您好很高兴为您服务)- 匹配/^天气.*北京$/→ 调用fetchWeather(北京)并回复结果- 匹配/^帮助$/→ 返回预设的帮助菜单文本关键技巧在于不要在 handleMessage 里写 if-else 大杂烩。模板采用策略模式把每个匹配规则封装成独立函数如handleHello(),handleWeather()再用 Map 存储规则与处理器的映射关系。这样新增一个功能只需往 Map 里加一项完全不影响原有逻辑。// service/wechat.js const handlers new Map([ [/^你好$/, handleHello], [/^天气.*(.)$/, handleWeather], [/^帮助$/, handleHelp], ]); async function handleMessage(message) { const text message.text().trim(); for (const [regex, handler] of handlers) { const match text.match(regex); if (match) { return await handler(message, match[1]); // match[1] 是城市名 } } return replyText(暂不支持该指令请发送“帮助”查看可用命令); }3.3 静态资源与前端页面为什么需要一个简单的首页你可能会疑惑微信机器人是后台服务为什么还要app/public/目录和router.js里定义的/路由答案是为了可观测性和可运维性。/页面显示当前 Wechaty 状态在线/离线/扫码中、最近 5 条日志摘要、手动触发登录/登出按钮。当你远程部署到服务器不用 SSH 登录就能一眼看出机器人是否正常/health接口返回 JSON 格式的健康状态{ wechaty: online, uptime: 3600 }方便 Nginx 或 Prometheus 做存活探针/logs接口按页返回wechat-demo-web.log的尾部内容支持实时滚动刷新替代tail -f命令。这些页面不是炫技而是把原本藏在终端里的运维信息暴露成标准 HTTP 接口。你在公司内网部署时可以把这个地址分享给运营同事他们不用懂 Node.js也能自助查看机器人状态。实操心得静态资源路径一定要用app.config.static.dir统一配置而不是硬编码public/。这样在 Docker 部署时你可以通过环境变量STATIC_DIR/app/static把静态文件挂载到容器外实现配置与代码分离。4. 实操过程详解从零启动到稳定运行的每一步4.1 环境准备与依赖安装第一步永远是确认 Node.js 版本。执行node -v确保输出是v16.x、v18.x或v20.x。如果不是请用 nvm 切换Mac/Linux或下载对应 MSI 安装包Windows。第二步克隆仓库并安装依赖git clone https://github.com/xxx/egg-wechaty-demo.git cd egg-wechaty-demo npm install注意npm install过程中 Puppeteer 会自动下载 Chromium国内网络可能较慢。如果卡在Downloading Chromium可以提前设置镜像源export PUPPETEER_DOWNLOAD_HOSThttps://npmmirror.com/mirrors npm install第三步检查配置文件。打开config/config.default.js重点关注以下几项-wechaty.puppetOptions.headless: 开发时建议设为false这样扫码时会弹出 Chrome 窗口方便调试生产环境务必改为true-wechaty.puppetOptions.cacheDir: 确保路径可写否则登录态无法持久化-logger.dir: 日志目录路径建议设为绝对路径避免相对路径在不同工作目录下失效。4.2 启动与首次扫码那些你没看见的幕后动作执行npm run dev后你会看到终端快速刷过几行日志2024-06-15 10:23:45,123 INFO 12345 [egg:core:ready_stat] end ready task 2024-06-15 10:23:45,124 INFO 12345 [app] Wechaty instance initializing... 2024-06-15 10:23:47,890 INFO 12345 [wechaty] QRCode generated此时Chrome 浏览器应该已经自动打开并显示一个带边框的二维码。不要急于扫码先做两件事查看wechat-demo-web.log确认最后一行是QRCode generated且时间戳与终端一致打开http://localhost:7001页面顶部会显示Wechaty Status: scanning...证明前端已正确连接后端状态。这时再用微信扫描二维码。扫码后微信客户端会提示“登录网页版微信”点击确认。几秒后终端会出现2024-06-15 10:24:12,345 INFO 12345 [wechaty] User login: User#xxx同时网页首页状态变为Wechaty Status: onlinewechat-demo-web.log里也会多出一行User login: xxx。踩过的坑如果扫码后 Chrome 窗口自动关闭但终端没打印User login大概率是 Puppeteer 版本与 Chrome 不兼容。此时不要重装 Chrome而是升级 Wechaty 到最新版npm install wechatylatest因为新版 Wechaty 内置了更宽松的浏览器兼容性检测。4.3 消息测试与响应验证用真实对话检验逻辑登录成功后立刻用另一个微信账号或自己的小号给这个机器人发一条消息比如“你好”。此时你要观察三个地方终端输出wechat-demo-web.log应该立即追加一行receive message from Contact#xxx: 你好回复效果对方微信应该收到“您好很高兴为您服务”日志闭环紧接着wechat-demo-web.log里会出现send reply to Contact#xxx: 您好很高兴为您服务。如果只有接收日志没有发送日志说明replyText()方法执行失败。此时去controller/wechat.js里找到handleMessage调用处加一行console.log(about to reply:, replyContent)再发一次消息看终端是否打印。如果没打印问题出在正则匹配逻辑如果打印了但没发送成功大概率是 Wechaty 实例状态异常比如被微信风控临时限制发送。4.4 定时任务实战每天早 9 点推送天气预报模板内置了一个示例定时任务在app/schedule/daily-weather.js中const Subscription require(egg).Subscription; class DailyWeather extends Subscription { static get schedule() { return { cron: 0 0 9 * * *, // 每天 9:00:00 执行 type: all, // 所有 worker 进程都执行Wechaty 实例只在 master 进程 disable: false, }; } async subscribe() { const { ctx } this; try { const weather await ctx.service.weather.get(北京); await ctx.service.wechat.broadcastText(【今日天气】\n weather); } catch (err) { ctx.logger.error(Daily weather broadcast failed:, err); } } } module.exports DailyWeather;关键点解析-cron: 0 0 9 * * *是标准 cron 表达式六个字段依次为秒、分、时、日、月、周。这里表示每天 9 点整-type: all看似矛盾但结合 Wechaty 实例只在 master 进程初始化的特性实际只有 master 进程会执行subscribe()其他 worker 进程会自动跳过-broadcastText()是service/wechat.js封装的方法它会遍历所有已知联系人或指定群聊逐个发送文本。生产环境请务必加白名单校验避免误触。要启用此任务只需在config/config.default.js中将schedule.enable设为true然后重启服务。首次启动时任务不会立即执行而是等到下一个整点比如现在是 8:55它会等到 9:00 才触发。5. 常见问题与排查技巧实录那些文档里不会写的真相5.1 登录失败的五大原因及对应解法现象可能原因排查命令解决方案终端一直卡在QRCode generatedChrome 窗口空白Puppeteer 启动失败缺少字体/沙箱权限cat egg-web.log \| grep PuppeteerLinux 服务器安装字体库sudo apt-get install fonts-liberation或添加--no-sandbox参数到puppetOptions.launch扫码后微信提示“该网页版微信暂时不可用”微信网页版接口变更或地区限制curl -I https://wx.qq.com检查网络能否访问 wx.qq.com尝试更换 Puppeteer 版本npm install puppeteer19.11.1登录成功但收不到消息Wechaty 实例未监听message事件grep on message app.js确认app.js中app.wechaty.on(message, ...)是否被正确注册检查wechaty.puppetOptions.silent是否为true设为false才能接收消息收到消息但不回复replyText()抛出异常未被捕获grep reply wechat-demo-web.log在service/wechat.js的replyText()方法开头加try/catch将错误ctx.logger.error()输出重启服务后需重新扫码登录态未持久化ls -l wechaty-cache/检查config/config.default.js中cacheDir路径是否可写确认puppetOptions.cacheDir配置是否生效5.2 消息乱码与格式错乱的根源Wechaty 默认以 UTF-8 编码处理消息但某些安卓微信客户端发送的 emoji 会以代理字符形式传输导致message.text()返回乱码。这不是 Bug而是微信协议本身的限制。解决方案分两步1. 在service/wechat.js的handleMessage()开头对原始文本做标准化处理function normalizeText(text) { return text .replace(/\uFFFD/g, ) // 清除无效 Unicode 替代符 .replace(/[\u200B-\u200D\uFEFF]/g, ) // 清除零宽字符 .trim(); }对于需要严格格式的消息如 Markdown 表格改用message.say()的富文本模式await message.say({ type: Text, text: ✅ 成功\n❌ 失败, });5.3 生产环境部署避坑指南Docker 部署时必须挂载wechaty-cache目录否则每次容器重启都会丢失登录态。Dockerfile 示例dockerfile FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . VOLUME [/app/wechaty-cache] EXPOSE 7001 CMD [npm, start]Nginx 反向代理配置要点微信网页版协议依赖 WebSocketNginx 配置必须包含nginx location / { proxy_pass http://127.0.0.1:7001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }内存泄漏预警Wechaty 会缓存大量 Contact、Room 实例。如果机器人长期运行后内存持续上涨可在app.js的app.beforeClose中手动清理javascript app.beforeClose(async () { await app.wechaty.stop(); // 清理缓存 app.wechaty.ContactPool.clear(); app.wechaty.RoomPool.clear(); });6. 扩展方向与二次开发建议让它真正为你所用这个模板的终极价值不在于它现在能做什么而在于它为你省下了多少从零造轮子的时间。基于它你可以快速落地以下场景客服自动应答在service/wechat.js中接入你公司的知识库 API把handleMessage()改造成语义检索 摘要生成跨平台消息桥接新增service/dingtalk.js监听 Wechaty 消息后调用钉钉机器人 Webhook 推送到钉钉群数据采集与分析在app.js的wechat:message事件里将消息内容、发送者、时间戳写入 MySQL 或 SQLite后续用 Grafana 做可视化看板多账号集群管理复制一份项目修改config/config.prod.js中的wechaty.puppetOptions.name为唯一标识用 PM2 启动多个实例每个实例管理一个微信账号。最后分享一个小技巧Wechaty 的Contact实例有个payload属性里面藏着微信原始协议返回的 JSON 数据。比如contact.payload.alias是好友备注名contact.payload.city是城市信息。这些字段官方文档很少提但实际业务中非常有用。你可以在service/wechat.js的handleMessage()里打印JSON.stringify(contact.payload, null, 2)慢慢摸索出属于你的私有化数据金矿。我在实际项目中就是靠挖掘payload里的uin字段实现了微信账号与公司 OA 系统的单点登录绑定——这才是模板真正开始发光的地方。本文还有配套的精品资源点击获取简介一套能直接跑起来的微信自动化交互示例用Egg.js搭服务框架Wechaty处理微信网页版登录、消息收发和会话管理。项目自带完整路由router.js、两个核心控制器home和wechat、封装好的微信业务逻辑service/wechat.js还集成了定时任务触发、多级日志输出区分wechat-demo-web.log和egg-web.log、静态资源托管能力。依赖安装只需npm i本地启动用npm run dev扫码登录个人微信后就能自动响应文本消息。代码结构扁平清晰config目录统一管理所有配置项支持TypeScript类型提示含index.d.ts内置ESLint校验规则和GitHub Actions CI流程.github/workflows/nodejs.yml。适合作为私有微信Bot开发的起点兼容主流Node.js版本不依赖第三方云服务或企业微信API。本文还有配套的精品资源点击获取