资讯详情

资讯详情

插件化Java微信机器人:基于WeChatFerry的架构与实战

简介一份基于WeChatFerry-Java-Client打造的插件化Java微信机器人项目适合有一定Java基础、希望研究微信自动化消息收发与联系人管理的开发者。资源包共108个文件约2.68MB核心代码以57个Java源文件为主覆盖消息收发、联系人管理、SQL执行等模块同时包含12个C文件、6个JAR包及若干proto、yaml、yml配置文件对应底层SDK封装、依赖库与运行配置目录结构清晰。目前已有435人学习下载。项目采用插件化设计将核心功能与扩展功能解耦可依据自身需求编写插件实现个性化回复、群管理等功能内容预览显示其中包含send_msg、receive_msg、contact_mgmt、exec_sql等关键实现并附有license_check.bat等辅助脚本便于快速部署和二次开发。对关注Java应用与微信机器人实践的开发者来说是一份有参考价值的完整示例项目。1. 微信机器人不止发消息WeChatFerry-JavaBot 把插件化玩明白了做微信机器人的路子很多但大多数方案把逻辑写死在主程序里改一个功能就要重新编译、重启进程维护成本高得离谱。这个基于 WeChatFerry-Java-Client 的插件化 Java 微信机器人思路是把每个功能做成独立插件运行时动态加载主框架只负责消息分发和插件管理。我第一次拆它的源码时最意外的一点是消息从微信进程到 Java 插件中间只隔了一个 WebSocket链路比想象中短得多。如果你已经熟悉 Spring Boot 那一套想给自己的微信账号加自动回复、群管理、定时推送又不想每次改功能都动主程序这个资源值得花一个下午拆一遍。新手能按步骤跑通第一个插件熟手能直接看它的事件分发和类加载器设计。2. 插件化架构与消息流转理解 WCF 的 Java 落地2.1 从微信钩子到 Java 事件WCF 的核心链路WeChatFerry简称 WCF并不是一个 Java 项目它是运行在 Windows 上的微信 Hook 库通过注入 DLL 拦截微信内部消息再以 WebSocket 方式对外提供接口。这个 JavaBot 项目做的事是把 WCF 的 WebSocket 接口封装成 Java 能直接调用的客户端 SDK并在 SDK 之上加了一层插件容器。完整链路是这样的微信进程 → wcf.dll 钩子层 → WCF SDK → WebSocket默认 127.0.0.1:7600→ Java 客户端 → 消息分发器 → 插件实例这条链路上Java 项目本身不直接碰微信内部结构所有交互都走 WebSocket 的 JSON 协议。好处是微信升级导致 Hook API 变化时只要 WCF 服务端还能跑Java 侧代码基本不用动坏处是 WebSocket 是长连接断线重连、消息积压、回调阻塞这些问题都得自己兜住。// 连接 WCF 服务端的核心代码端口必须和 wcf 启动配置一致 WcfClient client new WcfClient(ws://127.0.0.1:7600); client.connect(); // 建立长连接内部含握手和心跳 client.registerMsgCallback(msg - { // 全量消息都会进这个回调文本、图片、文件、系统通知 System.out.println(收到消息: msg.getContent()); });这段就是最小可用的连接样例。WcfClient 封装了 WebSocket 握手、心跳维持和 JSON 序列化connect 之后不需要额外写保活线程。registerMsgCallback 注册的是全局回调所有消息先到这里再由框架层做类型分发。拿到 WcfMsg 后常用的 getter 有 getContent()消息正文、getSender()发送人 wxid、getRoomId()群聊 id非群聊为空、getType()消息类型。提示connect 失败时先确认 WCF 服务端是否在监听 7600Windows 下直接netstat -ano | findstr 7600看不到监听就回去检查微信版本对不对。2.2 插件的加载与生命周期注册、卸载、热更新插件容器是整个项目最值得看的部分。它不是在 main 方法里 new 一堆对象而是约定一个插件接口由容器统一扫描、加载、卸载。每个插件一个 JAR 包放 plugins 目录启动时加载运行时可热插拔。public interface WcfPlugin { String getName(); // 插件全局唯一标识 void onLoad(WcfContext context); // 加载时注册指令和定时任务 boolean onMessage(WcfMsg msg, WcfContext context); // 返回 true 表示已消费 void onUnload(); // 卸载时释放资源 }接口只有四个方法设计意图很明确。getName 是插件在容器里的唯一标识重名插件会被拒绝加载。onLoad 里拿到的 WcfContext 是核心对象发消息、查联系人、管理白名单的 API 都挂在它上面。onMessage 的返回值值得单独说——返回 true 表示这条消息被当前插件消费后续插件不再收到返回 false 则继续往后传。写插件时第一件事就是判断消息类型是不是自己关心的再决定消费还是放行不然两个插件会互相抢消息。// 插件管理器核心逻辑每个 JAR 独立 URLClassLoader避免类冲突 public void loadPlugin(String jarPath) throws Exception { URLClassLoader loader new URLClassLoader( new URL[] { new File(jarPath).toURI().toURL() }, getClass().getClassLoader() ); // 约定实现类名或通过 ServiceLoader 发现 Class? clazz loader.loadClass(com.bot.plugins.MyPlugin); WcfPlugin plugin (WcfPlugin) clazz.getDeclaredConstructor().newInstance(); plugin.onLoad(context); plugins.put(plugin.getName(), plugin); }这是插件加载的最小实现思路实际项目里通常还会加磁盘目录监听plugins 目录新增 JAR 自动加载删除 JAR 自动卸载。这里先记住一个结论同一个类名被两个不同的类加载器加载后它们并不是同一个类instanceof 会返回 false。热更新相关的坑第 5 章专门讲。2.3 消息类型与分发策略WCF 的消息类型定义在常量表里常见的有文本、图片、文件、语音、视频、系统通知这几类。分发器拿到消息后不是直接调插件而是先解析出消息类型和会话维度单聊/群聊再按插件声明的兴趣范围做过滤分发。// 分发器伪代码先按类型过滤再逐个流转 if (msg.getType() WcfMsgType.TEXT) { for (WcfPlugin p : plugins.values()) { if (p.interest(msg)) { boolean consumed p.onMessage(msg, context); if (consumed) break; // 消费即停不再往后续传 } } }这样设计的好处是每个插件不用重复写 if 判断也避免了一个插件收到所有消息导致日志噪音。实际项目里兴趣判断一般做成注解声明比如 MsgFilter(type TEXT, rooms {群A})由容器在加载时解析注解生成过滤表。2.4 断线重连与消息确认WebSocket 长连接在 Windows 桌面环境下三天两头掉线微信崩溃、网络切换、休眠唤醒都会断。框架里默认的重连策略是固定间隔但固定间隔有个问题——服务端还没起来时客户端每 5 秒重连一次日志刷得飞快。我一般改成指数退避第一次失败等 1 秒第二次 2 秒第三次 4 秒封顶 30 秒服务端恢复后能较快感知日志也干净。消息确认这块WCF 的 WebSocket 协议是推送不确认模型服务端推了就算完。这意味着客户端处理消息的速度必须跟上推送速度处理不过来的话消息就在 TCP 缓冲区里排队积压多了直接丢。所以队列容量的监视比写业务逻辑更重要我习惯在框架层统计入队数和出队数差值超过阈值就输出警告日志。3. 快速上手环境准备与第一个插件3.1 环境要求与依赖引入WCF 的定位是 Windows 桌面端 Hook环境要求很具体先列一张表组件要求说明操作系统Windows 10/11 x64不支持 Linux / macOS别想着跑容器微信与 WCF 匹配的固定版本版本敏感自动更新后会失效JDK8 或 11标准 Java不需要新语法特性构建工具Maven 3.6依赖管理和打包插件 JAR依赖引入就一行坐标里的 groupId 和 artifactId 以你解压出来的 SDK 模块实际 pom 为准这里只写占位写法dependency groupIdcom.example/groupId artifactIdwcf-java-client/artifactId version${wcf.version}/version /dependency如果你拿到的资源里没有把 SDK 装进本地仓库可以在 SDK 模块目录执行mvn install -DskipTests把产物装进本地 Maven 仓库再在机器人模块里按上面的坐标引用。环境准备里最容易翻车的是微信版本。WCF 通常针对某个具体微信版本做偏移量适配微信一旦自动升级Hook 注入就会失败。我一般会把微信的自动更新关掉并在启动脚本里加一步版本校验版本对不上直接退出别带病运行。3.2 写第一个插件自动回复写插件只需要做两件事实现 WcfPlugin 接口打成 JAR 包丢进 plugins 目录。这里做一个最简单的文本自动回复插件public class EchoPlugin implements WcfPlugin { private WcfContext ctx; Override public String getName() { return echo; } Override public void onLoad(WcfContext context) { this.ctx context; System.out.println([echo] 插件已加载); } Override public boolean onMessage(WcfMsg msg, WcfContext context) { // 只处理文本消息非文本直接放行 if (msg.getType() ! WcfMsgType.TEXT) { return false; } // sendText 第一个参数是目标 wxid 或群 id ctx.sendText(msg.getSender(), 收到: msg.getContent()); return true; } Override public void onUnload() { System.out.println([echo] 插件已卸载); } }EchoPlugin 的逻辑很直白onLoad 里保存 contextonMessage 里判断类型是文本就原样回一句然后返回 true 表示这条消息处理完了。sendText 的第一个参数传 msg.getSender()单聊场景没问题群聊场景要传群 id 而不是发送人 wxid这个区别新手经常搞混发错地方容易造成骚扰。编译和打包mvn clean package -DskipTests # 产物在 target/ 目录下拷到机器人根目录的 plugins/ 文件夹 cp target/echo-plugin-1.0.jar ../bot/plugins/打包时注意 pom 里不要引入 spring-boot 那种 fat jar 插件插件 JAR 只需要包含自己的业务类框架类由主程序提供。打出来一个几百 KB 的小 JAR 就对了几十 MB 的肯定有问题。3.3 配置文件与参数说明机器人根目录的 config 文件控制框架行为核心参数这几项参数默认值作用wcf.ws.urlws://127.0.0.1:7600WCF 服务端地址wcf.reconnect.interval5000断线重连间隔毫秒wcf.plugins.dirplugins插件扫描目录wcf.whitelist.enablefalse是否启用联系人白名单wcf.msg.queue.size10000消息队列容量wcf: ws: url: ws://127.0.0.1:7600 reconnect: interval: 5000 max-retries: -1 # -1 表示无限重连 plugins: dir: plugins whitelist: enable: true list: - wxid_xxxreconnect.interval 默认 5 秒如果服务端重启频繁可以调大避免日志刷屏。whitelist 打开后白名单外的联系人消息会被框架直接丢弃不会进入任何插件这对消息回错人这种事故是最后一道保险。4. 功能扩展实战群管、定时任务与异步处理4.1 群聊消息管理与关键词回复实际场景里机器人在群里待得最多。群消息的判断靠 roomId代码写起来是这样Override public boolean onMessage(WcfMsg msg, WcfContext context) { String roomId msg.getRoomId(); if (roomId null || roomId.isEmpty()) { return false; // 不是群消息放行给其他插件 } String content msg.getContent(); if (content.startsWith(/mute)) { // 禁言指令只有管理员才能触发 if (isAdmin(msg.getSender())) { context.sendText(roomId, 本群已开启机器人管理模式); } } return false; // 继续让后面的插件有机会处理 }注意最后返回的是 false这让群管理插件只做自己那一小件事不垄断消息。isAdmin 可以做成从配置或数据库读管理员 wxid 列表判断逻辑单独抽一个方法别写在消息回调里。关键词回复也是同一套路子把关键词和回复内容的映射做成配置表插件只负责查表业务变更不用重新打包。4.2 定时任务与异步处理消息回调线程被长时间占用是很多机器人越跑越慢的根源。WCF 的消息回调跑在框架的线程池里你如果在回调里做 HTTP 请求或写数据库后面排队消息就全卡住了。正确姿势是把重活丢进自己的线程池public class TimerPlugin implements WcfPlugin { private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); private final ExecutorService workers Executors.newFixedThreadPool(4); private WcfContext ctx; Override public void onLoad(WcfContext context) { this.ctx context; scheduler.scheduleAtFixedRate(() - { // 每天早上九点给指定群推一条摘要 String report fetchReport(); ctx.sendText(群wxid, report); }, 0, 24, TimeUnit.HOURS); } Override public boolean onMessage(WcfMsg msg, WcfContext context) { if (!msg.getContent().startsWith(/report)) { return false; } workers.submit(() - { String report buildReport(); // 耗时的数据汇总 context.sendText(msg.getRoomId(), report); }); return true; } Override public void onUnload() { scheduler.shutdownNow(); workers.shutdownNow(); } }两个要点。第一定时任务和异步任务的线程池必须在 onUnload 里 shutdownNow否则插件卸载后线程还在跑属于典型的内存泄漏。第二scheduleAtFixedRate 的起始延迟设 0 只是演示真实场景建议先计算下个整点用delay nextHour - now作为首次延迟避免每次都在启动时刻触发。fetchReport 和 buildReport 是占位业务逻辑实际可以接 HTTP API但记得把请求超时设短默认的 30 秒会让线程池很容易被打满。4.3 发送图片、文件与会话感知文本之外的能力才是机器人的实用价值。发送图片用 sendImage参数是本地文件路径框架会转成微信能识别的格式boolean ok ctx.sendImage(D:\\bot\\data\\qrcode.png, roomId); if (!ok) { // 常见原因路径不存在、文件被占用、微信窗口被最小化到托盘 log.warn(图片发送失败, path{}, path); }sendFile 类似但文件发送在某些微信版本上有大小限制超过上限会静默失败没有异常抛出所以发送前先检查文件大小。会话感知是指同一个机器人被拉进多个群回复要回到正确的会话。框架的 WcfMsg 里已经带 roomId你只要保证从哪条消息来回哪条消息去不要自己维护一个全局当前群变量多群并发时那个变量一定是乱的。5. 常见问题与避坑消息丢、线程炸、Hook 失效运行微信机器人是个体力活最大的不可控因素全在 Windows 桌面环境。这一章把高频踩坑逐条记录每条都是现象、原因、解决三段式。5.1 微信升级后机器人完全没反应现象某天开始机器人完全没反应日志停更WCF 服务端起不来。原因微信客户端自动升级了。WCF 针对固定微信版本做内存偏移量适配版本一变 Hook 就失效DLL 注入不进去WebSocket 服务根本没起来。解决固定微信版本并关闭自动更新。Windows 下微信升级策略可以在设置里关掉但更稳妥的做法是下载对应版本的安装包备份升级检测被触发时直接覆盖安装回去。启动脚本里加一段版本号校验读微信安装目录的版本文件跟配置的期望版本比对不一致就直接退出。5.2 消息偶尔收不到重启后恢复现象消息偶尔丢尤其消息多的时候重启机器人后又能正常一阵。原因消息回调里做了耗时操作阻塞了框架线程池或者消息量超过队列容量队列拒绝写入后消息被丢弃。这是最隐蔽的问题因为日志里没有任何报错。解决回调只做判断和派发把 IO 操作全部丢到自己的线程池。队列容量调大只是缓解根治是让回调快进快出。我在生产环境里习惯给消息队列加监控计数队列积压超过阈值就告警比自己盯着日志强。5.3 热更新插件后报 ClassCastException现象热更新插件 JAR 后框架强转插件实例时报 ClassCastException或者插件里的单例状态丢失。原因插件 JAR 被 URLClassLoader 加载过卸载后旧类加载器没有真正释放新 JAR 又是新的类加载器。两个加载器加载同一个插件类它们不是同一个类强转会失败。解决加载器必须和插件生命周期绑定卸载时把插件、线程池、类加载器一并置空并显式调用 loader.close()JDK 8 支持。如果插件的静态变量持有 context 引用类加载器永远回收不掉。排查时用 jmap 看类加载器数量最直观数量只增不减就是泄漏了。5.4 发送频率一高就被限制现象消息能收到但发送一快就被限制发送接口返回成功但对方收不到。原因发送太快是玄学但基本能确认短时间大量同内容消息会触发限制表现为发送接口返回成功却无实际效果或者直接被要求验证。解决发送侧加限速。我用一个简单的令牌桶每发一条扣一个令牌令牌不足就 sleep 随机 1-3 秒。对同内容回复做去重同一句话在短时间内不重复发送用内容 hash 做缓存。这些策略没有标准答案以自己实测为准。5.5 跑几个小时后 JVM 崩溃或微信白屏现象运行几个小时后 JVM 异常退出或者微信界面白屏无响应。原因DLL 注入的微信进程不稳定加上 Java 侧线程反复创建销毁资源耗尽。常见诱因是定时任务用 new Thread 起线程且不回收。解决所有线程池统一在插件 onLoad 创建、onUnload 关闭微信侧定期清理消息记录避免内存膨胀到微信自己崩。如果崩溃高频出现换一台干净的 Windows 环境把输入法、安全软件这些可能注入进程的软件全卸载Hook 类工具对环境洁癖要求很高。6. 进阶多实例部署与消息落库6.1 一对多多微信号隔离部署一台机器跑多个机器人核心是端口隔离。WCF 服务端启动时可以通过参数指定不同端口Java 侧在配置里改 wcf.ws.url 指向对应端口即可。每个实例拥有独立的 plugins 目录和数据目录进程之间互不干扰。部署时我习惯把每个实例的日志输出到独立文件排查问题的时候不用在日志海洋里捞。6.2 消息落库把对话变成可检索数据机器人的价值在数据。每条经过框架的消息在分发前写一份到 SQLite后面要统计活跃度、找历史上下文都方便private void persist(WcfMsg msg) { try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement( INSERT INTO msg_log(room_id, sender, content, type, ts) VALUES(?,?,?,?,?))) { ps.setString(1, msg.getRoomId()); ps.setString(2, msg.getSender()); ps.setString(3, msg.getContent()); ps.setInt(4, msg.getType()); ps.setLong(5, System.currentTimeMillis()); ps.executeUpdate(); } catch (SQLException e) { log.error(消息落库失败, e); } }落库操作要放在异步线程不能直接写在回调里原因第 4 章讲过。SQLite 单文件、零运维最合适这种单机场景并发量再上去再考虑换 MySQL初期别过度设计。做这块最深的体会是微信群机器人最大的风险从来不是技术而是你拿它干了什么。从那以后我每次部署前都强制走一遍自检清单确认消息不越权、不泄露、不骚扰时间长了这成了一种习惯。这套插件化框架的边界、参数和坑希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →