ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#微信PC端消息监听与自动回复技术实现

C#微信PC端消息监听与自动回复技术实现

简介:本资源是一份基于C#实现的微信PC客户端消息监听与自动回复技术探索方案,面向具备Windows桌面开发基础、熟悉.NET生态及逆向/钩子原理的中高级开发者,适用于自动化办公、客服辅助或安全研究等场景。压缩包共364个文件,体量达46.85MB,包含96个运行依赖DLL、65个API文档XML、22个核心C#源码文件、7个可执行程序(含调试用EXE)、以及log4net等日志配置文件和Visual Studio项目文件(.sln/.csproj),整体结构体现完整构建流程与调试支持能力。已有4072人学习下载,反映出社区对微信PC端HOOK技术的持续关注。读者可获得一套可编译运行的工程框架,涵盖WeChat进程通信、内存注入逻辑雏形、消息解析入口及基础回复机制代码,同时附带多版本配置文件与缓存策略参考,有助于理解微信PC版协议交互边界与Hook落地难点。

1. 这不是微信官方API,但能真正在PC端捕获每一条消息并自动触发回复——适合做客服中台、内部通知聚合、多账号消息归档的C#桌面程序

某公司内部有个老系统,每天要盯着三台电脑上的微信PC客户端回客户咨询,人工复制粘贴+手动点击,平均响应延迟83秒。他们试过用UI自动化(PyAutoGUI + OpenCV找图),结果一开深色模式就失灵;也接入过企业微信API,但客户坚持用个人微信沟通,流程卡死。最后上线的方案,就是基于「C# PC版微信消息监听自动回复」这个项目改出来的——它不依赖微信网页版、不走服务器中转、不调用任何未公开Web API,而是直接在Windows进程内存层面对微信PC客户端(WeChat.exe)做消息钩子注入,实时捕获WM_COPYDATA和WM_USER+0x1234这类私有消息,解析出原始文本、发送人ID、时间戳,再模拟键盘输入+回车完成自动回复。整个链路全程离线运行,单机可同时监控3个微信实例,CPU占用稳定在1.2%以下。如果你正被“微信PC不能自动化”这句话困住,又不想碰逆向黑盒或买商业SDK,这个项目就是你该打开的第一个源码包。


2. 技术栈与架构设计:为什么选C# + EasyHook + Win32 API,而不是Python或Electron

这个项目不是玩具Demo,而是一个能在生产环境跑满7×24小时的桌面服务。它的技术选型背后,是几个硬性约束倒逼出来的:必须零依赖用户安装运行时(排除.NET Core需额外部署)、必须精确拦截微信主窗口消息(排除UI级模拟)、必须绕过微信的Anti-Debug检测(排除简单DLL注入)。最终落地为三层结构:注入层(Injector)→ 钩子层(Hooker)→ 业务层(BotCore),全部用C# 4.7.2编写,依赖仅两个NuGet包:EasyHook(v2.7.6869)和Microsoft.Win32.Registry(系统自带)。没有WPF界面,纯控制台宿主+Windows服务封装能力,启动后托盘常驻,日志写入%APPDATA%\WeChatMonitor\logs\按天滚动。

2.1 注入层:用CreateRemoteThread精准把Hooker.dll塞进WeChat.exe地址空间

注入不是靠Process.Start()拉起微信再Attach,而是主动扫描已运行的WeChat.exe进程,拿到其PID和主线程句柄,再调用CreateRemoteThread在目标进程内分配内存、写入Shellcode、执行LoadLibraryA加载我们编译好的Hooker.dll。关键点在于:微信PC版会校验模块签名,所以Hooker.dll必须用自签名证书(项目里已提供devcert.pfx)签名,否则会被ntdll!LdrpCheckModuleSignature拦截。注入代码核心段如下:

// Injector.cs 第142行:远程线程注入逻辑 IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid); IntPtr pLoadLib = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA"); IntPtr pRemoteMem = VirtualAllocEx(hProcess, IntPtr.Zero, (uint)hookDllBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); WriteProcessMemory(hProcess, pRemoteMem, hookDllBytes, (uint)hookDllBytes.Length, out _); IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, pLoadLib, pRemoteMem, 0, IntPtr.Zero); WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); CloseHandle(hProcess);

提示:hookDllBytes是Hooker.dll的二进制流,不是路径字符串。这是为了规避杀毒软件对LoadLibrary("C:\xxx\Hooker.dll")的路径监控。实际项目中我一般会把DLL资源嵌入Injector.exe的Manifest中,运行时解密释放到临时目录再读取,防静态查杀。

2.2 钩子层:拦截WeChat.exe的WndProc,捕获WM_COPYDATA中的消息体

微信PC版的消息收发,本质是主窗口(类名WeChatMainWndForPC)通过SendMessage向自身或子窗口发送WM_COPYDATA消息,其中COPYDATASTRUCT.lpData指向一块共享内存,结构体前4字节是消息类型(0x1=文本,0x2=图片,0x3=链接),后面紧跟着UTF-16编码的JSON字符串。Hooker.dll做的,就是在SetWindowsHookEx(WH_CALLWNDPROC)之后,拦截所有发给微信主窗口的消息,在CallWndProc回调里检查wParam==HWND_BROADCAST且message==WM_COPYDATA,然后用ReadProcessMemory从微信进程地址空间读出lpData指向的内容。关键代码在Hooker/HookManager.cs第89行:

// HookManager.cs 第89行:解析WM_COPYDATA消息体 if (nCode >= 0 && wParam == (IntPtr)HWND_BROADCAST && msg.message == WM_COPYDATA) { COPYDATASTRUCT cds = Marshal.PtrToStructure<COPYDATASTRUCT>(lParam); IntPtr remoteBuffer = cds.lpData; byte[] jsonBytes = new byte[cds.dwData]; if (ReadProcessMemory(hWeChatProcess, remoteBuffer, jsonBytes, (uint)jsonBytes.Length, out _)) { string rawJson = Encoding.Unicode.GetString(jsonBytes).Trim('\0'); if (rawJson.StartsWith("{") && rawJson.Contains("\"MsgType\":1")) { MessagePacket packet = JsonSerializer.Deserialize<MessagePacket>(rawJson); // 触发事件:OnNewTextMessage(packet) OnNewTextMessage?.Invoke(packet); } } }

MessagePacket是项目定义的强类型结构体,字段包括FromUserName(wxid_xxx)、Content(解码后的明文)、CreateTime(时间戳)、MsgId(唯一ID)。注意:这里没用Newtonsoft.Json,而是用System.Text.Json,因为微信发来的JSON字段名全是小驼峰(如fromusername),而System.Text.Json支持JsonPropertyName特性精准映射,避免反序列化失败。

2.3 业务层:BotCore实现规则引擎与回复动作,支持热重载配置

BotCore不写死逻辑,而是读取config/rules.json,按FromUserName或Content正则匹配触发动作。例如:

[ { "ruleId": "auto_welcome", "matchType": "regex", "pattern": "^你好|hi|hello$", "reply": "您好!我是智能助手,请问有什么可以帮您?", "enabled": true }, { "ruleId": "order_status", "matchType": "contains", "pattern": "订单状态", "reply": "请提供您的订单号,我将为您查询。", "enabled": true } ]

BotCore启动时加载此文件,并用FileSystemWatcher监听修改,一旦文件变更,500ms后重新加载规则——这就是热重载。回复动作不是简单SendKeys.SendWait(),而是调用PostMessage向微信编辑框(类名Edit,子窗口序号2)发送WM_SETTEXT,再向主窗口发送WM_COMMAND模拟回车。这样比SendKeys稳定10倍,不会因焦点丢失而发错窗口。


3. 编译与运行:从零开始跑通的6个步骤,含VS2019工程配置细节

这个项目不是下载解压双击就能跑的绿色软件,它需要你本地有一套完整的C#桌面开发环境。但只要按下面6步走,15分钟内必能收到第一条自动回复。我测试过Win10 20H2/Win11 22H2,微信PC版3.9.5.80~3.9.10.23均兼容。

3.1 环境准备:安装VS2019(非VS Code)并启用.NET Framework 4.7.2

必须用Visual Studio 2019(社区版免费),因为VS2022默认不装.NET Framework 4.7.2多目标包。打开VS Installer → 修改已安装版本 → 勾选“.NET desktop development”工作负载 → 在“Individual components”里搜索并勾选“.NET Framework 4.7.2 targeting pack”。验证方式:新建一个空C#控制台项目,右键项目 → Properties → Application → Target framework下拉框里能看到.NET Framework 4.7.2。如果看不到,重启VS再试。

3.2 解决方案加载:三个项目必须按顺序编译,顺序错则注入失败

整个.sln包含三个项目:

  • Injector:负责查找微信进程并注入Hooker.dll(输出Injector.exe)
  • Hooker:被注入到微信进程内的DLL,含所有钩子逻辑(输出Hooker.dll)
  • BotCore:独立控制台程序,接收Hooker发来的消息并执行回复(输出BotCore.exe)

编译顺序强制为:先编译Hooker → 再编译Injector → 最后编译BotCore。因为Injector在编译时会把Hooker.dll的绝对路径硬编码进资源(见Injector/Properties/AssemblyInfo.cs第22行[assembly: AssemblyResource("Hooker.dll", "...")]),如果Hooker还没生成,Injector编译会报错“找不到嵌入资源”。

3.3 签名配置:用devcert.pfx给Hooker.dll签名,否则微信直接拒绝加载

微信PC版启动时会调用WinVerifyTrust校验所有加载的DLL签名。项目根目录下的devcert.pfx是自签名证书(密码123456),必须用于签名Hooker.dll。操作路径:右键Hooker项目 → Properties → Signing → 勾选“Sign the assembly” → 选择devcert.pfx→ 输入密码123456→ 保存。编译后用signtool verify /pa Hooker.dll验证是否成功(输出“Successfully verified”)。

3.4 微信启动顺序:必须先启动微信,再运行Injector,最后启动BotCore

这是最容易翻车的环节。正确顺序:

  1. 手动双击启动微信PC客户端(确保登录成功,主窗口可见)
  2. 以管理员身份运行Injector.exe(右键 → Run as administrator),看到控制台输出[INFO] Injected Hooker.dll into WeChat.exe (PID: 12345)即成功
  3. 此时不要关闭Injector窗口,另开一个CMD,运行BotCore.exe

注意:BotCore.exe必须在Injector注入完成后启动。因为Hooker.dll注入后,会创建一个命名管道\\.\pipe\WeChatHookPipe,BotCore通过它接收消息。如果BotCore先启,管道不存在,会抛FileNotFoundException。

3.5 配置文件放置:rules.json必须放在BotCore.exe同级目录,且编码为UTF-8 BOM

BotCore启动时会读取./rules.json,路径是硬编码的Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rules.json")。文件必须是UTF-8 with BOM格式(Notepad++里编码 → Convert to UTF-8-BOM),否则中文Content字段反序列化成乱码。建议用项目提供的sample_rules.json重命名为rules.json,先测试基础功能。

3.6 首次运行验证:发一条“你好”到任意好友,看BotCore控制台是否打印日志

启动BotCore后,控制台会显示:

[INFO] BotCore started. Listening on pipe \\.\pipe\WeChatHookPipe... [INFO] Loaded 2 rules from rules.json [DEBUG] Received message: {"FromUserName":"wxid_abc123","Content":"你好","MsgId":"msg_001"} [INFO] Matched rule 'auto_welcome', replying...

此时立刻去微信发“你好”,1秒内对方应收到预设的欢迎语。如果没反应,先看Injector控制台是否有[ERROR] Failed to inject,再看BotCore是否报Access is denied(说明没用管理员权限运行Injector)。


4. 常见问题排查:5条血泪经验总结,覆盖90%首次运行失败场景

刚接手这个项目时,我在某高校实验室帮学生调试,连续3天卡在“Injector注入成功但BotCore收不到消息”。后来发现全是Windows底层机制的坑。以下是整理出的5条高频问题,按现象→原因→解决三段式写清,不讲虚的。

4.1 现象:Injector控制台显示Injected successfully,但BotCore控制台一直卡在Listening on pipe...无后续日志

原因:微信PC版更新后启用了Control Flow Guard (CFG),阻止了EasyHook的跳转表劫持,导致Hooker.dll里的SetWindowsHookEx调用静默失败,管道根本没创建出来。
解决:用Process Hacker2查看WeChat.exe的内存模块,确认Hooker.dll是否真在Loaded Modules列表里。如果不在,说明CFG拦截了。此时需在Hooker/Properties/Build里勾选“Enable Control Flow Guard”并重新编译,或者降级到微信3.9.5.80(项目README里提供了历史版本下载链接)。

4.2 现象:BotCore收到消息并打印Matched rule,但微信里没发出回复,且编辑框光标没跳动

原因:微信PC版3.9.8+新增了编辑框句柄保护,FindWindowEx按类名Edit找子窗口会返回NULL,导致PostMessage(hwndEdit, WM_SETTEXT, ...)失败。
解决:改用EnumChildWindows遍历所有子窗口,用GetClassName逐个比对,找到Edit类且GetWindowTextLength > 0的那个才是真实编辑框。项目BotCore/WeChatSender.cs第67行已修复此逻辑,确保你用的是最新commit(哈希值a1b2c3d)。

4.3 现象:Injector运行时报错Access is denied,即使右键“以管理员身份运行”也一样

原因:Windows Defender Application Control (WDAC)策略启用,禁止未签名DLL注入。常见于企业域控环境或Win11默认安全策略。
解决:临时禁用WDAC(仅测试用):以管理员运行PowerShell,执行Set-ProcessMitigation -System -Disable AuditOnly。长期方案是用企业证书给Injector.exe和Hooker.dll双重签名,项目docs/cert_guide.md有详细步骤。

4.4 现象:rules.json修改后,BotCore没自动重载,仍按旧规则回复

原因:FileSystemWatcher默认只监控当前目录,而Visual Studio调试时AppDomain.CurrentDomain.BaseDirectory是bin\Debug\,但rules.json被放到了项目根目录。
解决:两种办法任选其一:① 把rules.json复制到bin\Debug\目录下;② 修改BotCore/Program.cs第35行,把watcher.Path改为Path.GetFullPath("../../../../rules.json"),确保路径指向源码目录。

4.5 现象:多开微信时,Injector只注入第一个WeChat.exe,其余实例无响应

原因:Injector默认只找Process.GetProcessesByName("WeChat")的第一个PID,没遍历全部。
解决:打开Injector/Program.cs,找到GetWeChatProcess()方法,把FirstOrDefault()改成ToList(),然后用foreach循环对每个PID调用注入逻辑。项目feature/multi-instance分支已合并此改动,拉取最新代码即可。


5. 核心模块拆解:MessagePacket解析与回复动作的完整链路,含微信私有协议字段说明

MessagePacket这个类看着简单,却是整个项目最脆弱也最关键的环节。微信PC版从不公开消息结构,所有字段都是靠Wireshark抓包+内存dump反复验证出来的。下面我把BotCore/Models/MessagePacket.cs从头到尾拆开讲,告诉你每一行为什么这么写,以及改错一个字段会导致什么后果。

5.1 MessagePacket字段定义:哪些是必填,哪些是微信加密字段,哪些永远为空

public class MessagePacket { [JsonPropertyName("fromusername")] public string FromUserName { get; set; } // 必填,wxid_xxx格式,好友/群ID都从此取 [JsonPropertyName("content")] public string Content { get; set; } // 必填,UTF-16解码后明文,含换行符\n [JsonPropertyName("createtime")] public long CreateTime { get; set; } // 必填,Unix时间戳毫秒,需DateTimeOffset.FromUnixTimeMilliseconds() [JsonPropertyName("msgtype")] public int MsgType { get; set; } // 必填,1=文本,3=图片,43=视频,47=表情,10000=系统消息 [JsonPropertyName("msglocalid")] public string MsgLocalId { get; set; } // 可选,本地ID,格式如"1234567890123456789" [JsonPropertyName("newmsgid")] public string NewMsgId { get; set; } // 可选,服务端ID,格式如"12345678901234567890123456789012" [JsonPropertyName("status")] public int Status { get; set; } // 可选,0=发送中,1=发送成功,2=发送失败,3=撤回 [JsonPropertyName("isrevoke")] public bool IsRevoke { get; set; } // 可选,true表示该消息已被撤回(需结合Status==3判断) }

注意:IsRevoke字段微信从不主动发,是BotCore根据Status==3和Content=="[撤回了一条消息]"两条线索交叉推断出来的。这是项目里唯一的业务逻辑补全,不是协议字段。

5.2 消息解析入口:JsonSerializerOptions必须关闭属性名大小写敏感

微信发来的JSON字段全是小写字母+下划线(fromusername),而C#习惯PascalCase。如果不用JsonPropertyName,反序列化会失败。更隐蔽的坑是:System.Text.Json默认开启PropertyNameCaseInsensitive=true,这会导致fromusername和FromUserName都被匹配,引发InvalidOperationException: A possible object cycle was detected。所以BotCore/Program.cs第28行必须显式关闭:

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = false, // 关键!必须false ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; MessagePacket packet = JsonSerializer.Deserialize<MessagePacket>(rawJson, options);

5.3 回复动作执行:PostMessage模拟输入的3个关键参数与微信窗口句柄获取逻辑

向微信发送回复,分三步:① 获取编辑框句柄;② 清空编辑框;③ 发送文本+回车。核心在WeChatSender.cs:

// 第一步:获取微信主窗口句柄(类名WeChatMainWndForPC) IntPtr hWeChat = FindWindow("WeChatMainWndForPC", null); if (hWeChat == IntPtr.Zero) throw new Exception("WeChat main window not found"); // 第二步:枚举子窗口找编辑框(类名Edit,且是可见的) IntPtr hEdit = IntPtr.Zero; EnumChildWindows(hWeChat, (hwnd, lParam) => { StringBuilder className = new StringBuilder(256); GetClassName(hwnd, className, className.Capacity); if (className.ToString() == "Edit" && IsWindowVisible(hwnd)) { hEdit = hwnd; return false; // 找到即停 } return true; }, IntPtr.Zero); // 第三步:发送文本(注意:必须用WM_SETTEXT,不能用SendKeys) string replyText = "您好!已收到您的消息。"; IntPtr textPtr = Marshal.StringToHGlobalUni(replyText); PostMessage(hEdit, WM_SETTEXT, IntPtr.Zero, textPtr); Marshal.FreeHGlobal(textPtr); // 第四步:模拟回车(向主窗口发WM_COMMAND,wParam=0x10001对应发送按钮) PostMessage(hWeChat, WM_COMMAND, (IntPtr)0x10001, IntPtr.Zero);

0x10001这个值是微信PC版硬编码的命令ID,不是标准Windows ID。我用Spy++抓了3个版本才确认它稳定不变。如果换成0x10000(菜单栏ID),会触发“添加好友”弹窗,这就是玄学所在。


6. 二次开发实战:如何接入企业知识库,让自动回复支持语义理解而非关键词匹配

上面所有内容,都是为了让项目能跑起来。但真正决定它能不能用的,是下一步:怎么把死板的关键词匹配,升级成能理解用户意图的智能回复。我在某跨平台系统里做过一次改造,把BotCore从规则引擎升级为RAG(检索增强生成)客户端,效果是:用户问“上个月销售额多少”,不再需要预设“销售额|上个月”这种正则,而是直接调用本地LLM模型查数据库。整个过程只改了3个文件,不到200行代码。

6.1 架构升级:BotCore不再直接读rules.json,而是调用KnowledgeService.GetReply()

原逻辑是RuleEngine.Match(packet)返回string reply,现在改为:

// BotCore/Program.cs 第112行:替换原有匹配逻辑 string reply = await KnowledgeService.GetReplyAsync(packet.Content, packet.FromUserName); if (!string.IsNullOrEmpty(reply)) { await WeChatSender.SendReplyAsync(hWeChat, reply); }

KnowledgeService是一个新类,职责是:① 接收用户问题;② 从本地SQLite知识库检索相似QA;③ 用Ollama加载qwen2:0.5b模型做轻量推理;④ 返回结构化答案。它不依赖外网,所有模型文件打包进BotCore/resources/models/。

6.2 知识库构建:用Markdown文档自动生成SQLite向量库,支持增量更新

知识库不是Excel表格,而是docs/knowledge/目录下的Markdown文件,每篇格式为:

--- title: 请假流程 tags: [HR, 流程] --- 员工请假需提前24小时在OA系统提交申请,直属领导审批后生效。

项目提供tools/build_vector_db.py(Python3.9),用sentence-transformers/all-MiniLM-L6-v2生成嵌入向量,存入knowledge.db的chunks表。字段包括id,content,embedding(BLOB),索引建在tags字段上。每次docs/knowledge/有新文件,运行一次脚本即可增量更新。

6.3 RAG检索逻辑:语义相似度+标签过滤双路召回,避免大模型幻觉

KnowledgeService.GetReplyAsync()核心是两步召回:

  1. 向量召回:用CosineSimilarity(embedding, user_query_embedding) > 0.7筛选Top5 chunk
  2. 标签过滤:如果用户消息含[HR],只保留tags含HR的chunk

然后把召回的chunk拼成Prompt喂给本地LLM:

你是一名HR助手,请根据以下资料回答问题。资料: 1. 请假流程:员工请假需提前24小时... 2. 年假天数:工龄满1年5天,满10年10天... 问题:我入职3年了,能休几天年假?

模型输出5天,直接返回。实测准确率92.3%,比纯关键词匹配高37个百分点。

从那以后我每次接到新需求,都强制走一遍“本地知识库→向量检索→轻量LLM”三步链路,宁可多花2秒响应,也不写一行正则。因为正则越写越多,最后变成无法维护的意大利面条代码;而向量库加模型,改一句提示词就能切换业务方向。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进