简介:针对无需登录微信即可完成常用自动化操作的诉求,这款基于C#的微信自动化模拟工具源码包,面向有一定C#基础、希望深入理解微信桌面端交互逻辑或二次开发的工程师。压缩包共55个文件,除DLL依赖库、CS核心逻辑、JSON配置、EXE可执行文件与PDB调试文件外,还包含Cache、VSIDX等编译索引和项目状态文件,整体仅2.21MB,组织结构清晰。已有676人学习下载,表明该方案在微信自动化方向具备实用参考价值。源码内含完整的Visual Studio解决方案、窗体界面、程序入口及资源配置,并附带说明文档与许可证,可直接打开编译调试。通过对照核心窗体与入口程序代码,可梳理自动回复、联系人管理模块的消息模拟与调用流程,也能基于现有结构扩展日程提醒、群发管理等自定义功能。
1. 微信自动化模拟:C# 能做什么,不能做什么
我们说的"C#微信自动化模拟工具"(源码包里叫 WeChatAuto),说白了就是一套用 C# 写的 PC 微信客户端自动化驱动库。它不碰协议、不抓包、不逆向加密逻辑,而是站在微信窗口外面,模拟人的操作——找到窗口、定位输入框、粘贴文本、按回车、点按钮。这么做的优点是稳定:只要微信界面上某个按钮还在,这套代码就能继续用;缺点是有限制:微信大量使用了自绘控件,标准 UI 自动化的读取能力经常失灵,所以很多实现得配合坐标、剪贴板和键盘钩子。那它能干什么?能替运营人员定时在群里发通知、能帮测试工程师把"给 100 个好友发同一条消息"变成 10 分钟自动完成,也能在你维护的 C# 上位机项目里嵌入一个"控制微信弹消息"的模块。它不能干什么?不能绕过微信的安全限制、不能低成本批量养号、更不能在登录状态异常时强行稳定运行。适合谁?适合有 C# 基础、想用 Windows 原生消息机制解决重复操作的开发者,也适合需要一套可二次开发的微信自动化模板的小团队。这套源码的核心思路才是值钱的地方:窗口句柄、消息注入、输入模拟这三板斧,走到别的 Win32 客户端自动化项目上同样成立。
2. 选型与工程骨架:三种技术路线和我的最终选择
做微信自动化,第一件事不是写代码,而是选路。路线选错,后面每个功能都在打补丁。我拆完 WeChatAuto 的源码包后,把它的技术方案和业界常见做法对比了一遍,整理出三条路线:纯 UI Automation、Windows 消息模拟、协议级 Hook。这三条路各有各的适用边界。
2.1 三条路线对比:为什么 UI Automation 经常失灵
纯 UI Automation 是微软官方的无障碍访问框架,用System.Windows.Automation命名空间就可以拿到窗口树里的按钮、输入框、文本控件。理论上最干净——不涉及坐标、不涉及 P/Invoke,但微信客户端是 MFC + DirectUI 自绘的混合体,很多控件没有暴露标准ControlType,你拿到的可能是一个Custom类型,里面既没有Name也没有Value。常见的表现是:FindFirst 能找到控件对象,但获取不到文本和位置,或者点击目标后触发的是空白区域。
Windows 消息模拟是另一种思路:用FindWindow拿到主窗口句柄,再用FindWindowEx或EnumChildWindows找子窗口,最后通过SendMessage/PostMessage把鼠标点击、滚动、按键事件发给指定句柄。这条路的好处是不依赖界面文本,缺点是消息类型有限,比如微信自绘的输入框不是标准EDIT控件,发WM_SETTEXT对方根本不接收。
协议级 Hook 是高风险路线,需要注入 DLL 到微信进程,HOOK 掉send/recv网络函数,或者直接挂钩WeChatWin.dll里的导出函数。这条路最接近"微信机器人"的效果,能收发消息、能拿好友列表,但新版微信加了完整性校验,注入稍有不慎,微信直接闪退,而且这类代码有账号风险。WeChatAuto 源码包没有选这条路,这说明作者优先考虑的是可维护性和存活率。
2.2 最终选型:Windows 消息 + 剪贴板注入 + 坐标辅助
我打开源码包里的WeChatAuto.sln,发现核心依赖就是user32.dll和kernel32.dll,几乎全部是 P/Invoke。推荐的做法是:窗口定位用FindWindow+ 枚举子窗口;文本输入用剪贴板注入(把文本放到系统剪贴板,然后向窗口发Ctrl+V);点击操作优先发WM_LBUTTONDOWN/WM_LBUTTONUP给控件句柄,如果控件句柄拿不到位置,再退化到屏幕坐标模拟。这套方案的容错点在于"先句柄后坐标"——句柄能定位就不猜坐标,句柄失效才用坐标兜底。
工程骨架是 .NET Framework 4.7.2 控制台应用 + 一个工具类库。为什么不用 .NET 6+?因为微信是 32/64 位混合进程,而我们要调用的 Win32 API 本身是原生的,.NET Framework 的 P/Invoke 调度在这类场景里更成熟;另外很多读者手里的上位机项目还是 Framework 系,兼容性更友好。如果用 .NET Core 3.1 以上也可以,但要注意 RuntimeIdentifier 打包的架构问题,下面会遇到。
// P/Invoke 声明:windows 核心操作 [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);逻辑说明:FindWindow接受类名和窗口标题,微信主窗口的类名在不同版本里变化过,早年是WeChatMainWndForPC,新版出现过ChatWnd、MMChatMain等,所以只看标题"微信"更保险。FindWindowEx用来找子控件,如果子控件类名不确定,要结合枚举回调。PostMessage是异步投递,不会等目标处理完,适合按键类消息;需要等处理结果时用SendMessage,但 SendMessage 容易卡死,因为目标窗口没处理完你的调用不会返回。
参数说明:Msg是消息号,比如WM_LBUTTONDOWN是 0x201,WM_LBUTTONUP是 0x202,WM_KEYDOWN是 0x100。wParam和lParam携带附加信息,比如鼠标消息里wParam低位是 x 坐标,lParam低位是 y 坐标,如果是发给控件句柄的,坐标系是相对控件客户区的。
我一般会封装一个Win32Helper静态类,把这些声明集中放,避免散落在业务代码里。实际拆包时发现 WeChatAuto 源码里已经做好了这层封装,直接调用即可。
3. 核心模块实现:窗口定位、消息模拟与输入注入
这一章是源码里最值得抄的部分。微信自动化的操作闭环可以拆成四步:定位主窗口、定位输入框、注入文本、触发发送。每一步都有对应的问题和对应解法,下面按顺序写清。
3.1 定位微信主窗口:从 FindWindow 到多开场景
最朴素的定位就是一次FindWindow(null, "微信")。但真实环境中微信可能没登陆、最小化到了托盘、或者开了多开。托盘状态时主窗口句柄还在,但IsWindowVisible返回 false,你给它发消息它未必处理。多开场景下,每个微信进程有一个独立的主窗口句柄,FindWindow只能拿到其中一个。
public static IntPtr FindWeChatMainWindow(int targetProcessId = 0) { IntPtr found = IntPtr.Zero; EnumWindows((hWnd, lParam) => { if (!IsWindowVisible(hWnd)) return true; GetWindowThreadProcessId(hWnd, out int pid); if (targetProcessId > 0 && pid != targetProcessId) return true; StringBuilder sb = new StringBuilder(256); GetWindowText(hWnd, sb, 256); if (sb.Length == 0) return true; IntPtr classBuf = Marshal.AllocHGlobal(256); GetClassName(hWnd, classBuf, 256); string cls = Marshal.PtrToStringAuto(classBuf); Marshal.FreeHGlobal(classBuf); // 优先匹配类名,再匹配窗口标题 if (cls.Contains("WeChat") || cls.Contains("ChatWnd") || sb.ToString().Contains("微信")) { found = hWnd; return false; } return true; }, IntPtr.Zero); return found; }逻辑说明:这段用EnumWindows遍历所有顶级窗口,通过进程 ID 过滤和类名/标题双重判断。GetWindowThreadProcessId拿到了窗口所属的进程 ID,这样在多个微信窗口里你能精确选中指定的那个。判断条件里先看类名,再看标题,顺序不能反了——微信的窗口标题在某些系统语言下不是"微信"两个字,但类名WeChatMainWndForPC是稳定的。
参数说明:targetProcessId为 0 时表示不限制进程,返回第一个匹配的窗口。这在多开时需要小心,因为EnumWindows枚举顺序不保证是启动顺序。如果你想操作最新打开的实例,可以通过Process.StartTime来得到进程列表后再调用此函数。
3.2 向微信输入框注入文本:剪贴板法比 SendMessage 可靠
微信的输入框不是标准EDIT控件。你用 Spy++ 去抓的时候,能看到一个ChatEditCtrl类名,但这个控件内部是自绘的,直接发WM_SETTEXT不会生效,甚至SendMessage(WM_CHAR)也不能逐字输入。这里现成的可靠方案是:先把文本放进剪贴板,然后向聊天窗口发Ctrl+V粘贴,等粘贴完成后发回车。
public static void SendTextToWeChat(IntPtr mainWindow, IntPtr editHandle, string text) { // 1. 准备剪贴板文本 if (!OpenClipboard(mainWindow)) throw new Win32Exception(Marshal.GetLastWin32Error()); EmptyClipboard(); IntPtr hGlobal = Marshal.StringToHGlobalUni(text); SetClipboardData(13, hGlobal); // CF_UNICODETEXT = 13 CloseClipboard(); // 2. 保证剪贴板已就绪 Thread.Sleep(50); // 3. 激活主窗口,确保键盘焦点在微信进程 SetForegroundWindow(mainWindow); Thread.Sleep(100); // 4. 向输入框句柄发送 Ctrl+V PostMessage(editHandle, WM_KEYDOWN, (IntPtr)VK_CONTROL, IntPtr.Zero); PostMessage(editHandle, WM_CHAR, (IntPtr)'v', IntPtr.Zero); PostMessage(editHandle, WM_KEYUP, (IntPtr)VK_CONTROL, IntPtr.Zero); // 5. 等粘贴完成 Thread.Sleep(200); // 6. 发送回车 PostMessage(editHandle, WM_KEYDOWN, (IntPtr)VK_RETURN, IntPtr.Zero); PostMessage(editHandle, WM_KEYUP, (IntPtr)VK_RETURN, IntPtr.Zero); }逻辑说明:剪贴板操作是核心。OpenClipboard需要传一个窗口句柄作为所有者,传mainWindow可以避免剪贴板被其他进程锁定。SetClipboardData里的13是CF_UNICODETEXT,对应Marshal.StringToHGlobalUni生成的全局内存块。这里有个细节:粘贴消息发给输入框句柄editHandle,但Ctrl+V需要键盘焦点正确——所以先SetForegroundWindow(mainWindow)让微信置前。有些系统对PostMessage发送热键组合不响应,更稳妥的是用SendInput直接模拟键盘,但那就是最后手段了,因为SendInput把虚拟键送到当前前台窗口,容易送偏。
参数说明:WM_CHAR的参数是字符码,'v'的 ASCII 是 118,但这里我们用的是WM_CHAR,wParam传(IntPtr)'v'就代表字符 v。真正决定粘贴动作的其实是Ctrl按下时收到Ctrl+V键盘消息或者SendInput的组合键,PostMessage 的时序偶发失灵,血泪经验是在PostMessage(WM_KEYDOWN, VK_CONTROL)后加 20ms 延时,再发WM_CHAR,否则微信会丢掉v。
3.3 鼠标点击:句柄优先,坐标兜底
有些按钮(比如"发送"按钮、聊天列表里某个联系人)没有固定窗口句柄,或者句柄拿不到有效位置。这时候需要模拟鼠标点击。首选是用SendMessage向按钮句柄发点击消息,因为坐标系统不用算;如果句柄无效,退而用SetCursorPos+mouse_event模拟物理点击。
public static void ClickControl(IntPtr hWnd, uint x, uint y) { // 方法一:向控件句柄发送点击消息 IntPtr lParam = (IntPtr)((y << 16) | (x & 0xFFFF)); SendMessage(hWnd, WM_LBUTTONDOWN, (IntPtr)1, lParam); SendMessage(hWnd, WM_LBUTTONUP, (IntPtr)0, lParam); } public static void ClickScreenPoint(int screenX, int screenY) { // 方法二:屏幕坐标物理点击 SetCursorPos(screenX, screenY); mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, UIntPtr.Zero); mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, UIntPtr.Zero); }逻辑说明:第一种方法把鼠标消息直接投递给目标控件,lParam的低位是 x 坐标、高位是 y 坐标,坐标系是目标窗口客户区。注意WM_LBUTTONDOWN的wParam要传 1(左键按下状态),WM_LBUTTONUP传 0。第二种方法用物理鼠标事件,适合点击屏幕坐标已知、且目标不是普通窗口控件的场景。如果在高 DPI 显示器下,第二种方法要用SetProcessDPIAware()先声明进程感知 DPI,否则系统会把物理坐标换算成虚拟坐标,导致点击偏移。
参数说明:x、y的范围取决于SendMessage目标。如果hWnd是微信主窗口客户区,那坐标是相对主窗口客户区的;如果是某个子控件的句柄,坐标就是相对该控件客户区的,这很容易搞混。我一般在调用前先GetWindowRect拿到目标窗口屏幕矩形,再计算相对坐标,避免偏移。
4. 业务封装:好友定位、群发模板与消息接收回调
定位和输入是地基,真正的业务价值在于把原始操作组合成"搜索联系人→发送消息→接收回复"这样的完整闭环。WeChatAuto 源码包里没有做太重的业务,但接口设计上已经留好了扩展位,我在这章把最常见的三个业务模块讲透。
4.1 好友定位:用搜索框代替读取控件文本
微信聊天列表是自绘的,你很难用UI Automation拿到"张三"这个文本对应的控件位置。但微信 PC 版有一个所有版本都保留的交互——顶部搜索框。用Ctrl+F聚焦搜索框,输入微信 ID 或备注名,搜索结果第一项通常就是对应会话。这个方法绕过了控件读取难题,而且非常稳定。
public static bool LocateContact(IntPtr mainWindow, IntPtr searchEdit, string contactName) { // 假设 searchEdit 是搜索框句柄,可以按 3.2 的剪贴板法输入 SendTextToSearchBox(mainWindow, searchEdit, contactName); Thread.Sleep(300); // 搜索结果出来后,按下箭头选第一条 + 回车进入会话 PostMessage(searchEdit, WM_KEYDOWN, (IntPtr)VK_DOWN, IntPtr.Zero); PostMessage(searchEdit, WM_KEYUP, (IntPtr)VK_DOWN, IntPtr.Zero); Thread.Sleep(200); PostMessage(searchEdit, WM_KEYDOWN, (IntPtr)VK_RETURN, IntPtr.Zero); PostMessage(searchEdit, WM_KEYUP, (IntPtr)VK_RETURN, IntPtr.Zero); return true; }逻辑说明:SendTextToSearchBox跟前面的剪贴板粘贴类似,只是目标句柄不同。输入完名字后,微信会自动弹出搜索结果列表。如果这个联系人是你最近聊过的,第一项就是会话;如果不在最近列表,第一项也可能是联系人本身的搜索结果。所以严格一点的做法是输入姓名后,再按一下Enter直接跳转到聊天窗口——微信支持在搜索框直接回车打开第一个匹配结果。这里的VK_DOWN是为了预防微信版本差异导致焦点不在第一条上。
参数说明:contactName最好优先用微信号或手机号,因为备注重名的概率很大。搜索框句柄的获取方式因微信版本而异,Spy++抓到的类名一般是SearchBoxEdit,但也有变体。我会把这个句柄作为参数暴露给上层调用者,实现解耦。
4.2 群发模板:循环 + 随机延时 + 防重复
群发功能是运营最刚需的,也是最容易触发风控的操作。我在源码里看到的是这个模式:一个联系人列表,一个文本模板,循环执行"搜索 → 输入内容 → 发送 → 随机停顿"。
public void BatchSend(List<string> contacts, string messageTemplate, Random random) { // 预先把模板里占位符替换掉 foreach (string contact in contacts) { string message = messageTemplate.Replace("{name}", contact); LocateContact(_mainWindow, _searchEdit, contact); Thread.Sleep(random.Next(800, 1500)); // 等会话切换完成 SendTextToWeChat(_mainWindow, _editHandle, message); int delay = random.Next(1500, 4000); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 已发送至 {contact},等待 {delay}ms"); Thread.Sleep(delay); } }逻辑说明:Random延时是关键。 固定 3 秒一次的操作频率在微信风控模型里是典型的机器特征。 我一般会把停顿范围拉开到 1.5 到 4 秒,并且在每 5 条之后额外加一个 10 秒的长停,模拟人回复消息后再继续。 另外,发消息前可以随机在文本前后加一些变化后缀,比如换个标点符号,减少模板判重概率。
参数说明:{name}占位符替换是实现模板个性化的最低成本方式。 如果你要发的内容完全一致,微信很容易在当前聊天会话中检测到重复内容。 还有一个值得做的动作:在发送之前,先检查这个联系人是不是已经是当前打开的会话,如果是,就跳过搜索定位,避免无谓操作。
4.3 消息接收回调:剪贴板捕获 + 事件通知
接收消息比发送消息难得多,因为微信没有暴露"新消息到达"的窗口消息。 我在源码包里看到作者用了轮询加剪贴板的方式:每隔 1.5 秒,聚焦到最近会话第一条,模拟双击选中最新消息文本,然后Ctrl+C复制,再从剪贴板里取文本。 这种方式虽然不够优雅,但对纯 Windows 消息模拟方案来说是最容易落地的。
public class MessageReceiver { public event EventHandler<string> MessageReceived; public void StartPolling(IntPtr mainWindow, IntPtr chatListHandle, int intervalMs = 1500) { Task.Run(async () => { string lastText = string.Empty; while (!_stopRequested) { // 模拟点击会话列表第一项 ClickControl(chatListHandle, 10, 10); await Task.Delay(200); // 全选并复制当前会话最新消息 SendKeys("^a"); await Task.Delay(100); SendKeys("^c"); await Task.Delay(150); string text = Clipboard.GetText(); if (!string.IsNullOrEmpty(text) && text != lastText) { lastText = text; MessageReceived?.Invoke(this, text); } await Task.Delay(intervalMs); } }); } }逻辑说明:这个实现里面最核心的是event关键字——C# 委托和事件在这里是一个典型场景,UI 线程可以订阅MessageReceived来实时刷新界面。SendKeys是系统自带的模拟按键类,"^a"表示Ctrl+A,"^c"表示Ctrl+C。 点击会话列表第一项的坐标(10, 10)是一个经验值,在大多数分辨率下这是第一行会话的中点。 你可能会问:点击第一项之后,复制出来的是整个会话窗口的选中内容,而不是最新一条。 所以还需要在复制之前先按End键把光标移到末尾,或者双击一条消息选中它。 这里为了示例简洁没有写完整,实际使用时要针对微信的键盘操作做微调。
参数说明:intervalMs设得太短会导致微信界面频繁闪烁,设得太长会错过消息。 1.5 秒是比较平衡的值,对单会话监控够了。 另外,Clipboard.GetText()在剪贴板被其他程序占用时会抛异常,所以务必要放在try-catch里,跳过异常轮次。
5. 避坑与常见问题:五条血泪经验,每一条都让项目翻过车
微信自动化最大的敌人不是写不出代码,而是环境变化。 我在拆解这个源码包并自己复现的过程中,踩了不止十个坑,挑五个最典型的记录下来,按"现象 → 原因 → 解决"写成笔记,希望你能跳过这些坑。
5.1 主窗口句柄拿到了,但发消息对方没反应
现象:FindWindow正常返回了一个句柄,SendMessage发送消息也没有报错,但微信界面纹丝不动。
原因:微信在启动时会有多个窗口,比如登录窗口的类名是WeChatLoginWndForPC,主窗口是WeChatMainWndForPC,但FindWindow(null, "微信")可能匹配到的是一个小窗口(比如设置页、预览窗口)。另一个原因是窗口最小化到托盘后,消息不会触发界面刷新。
解决:用EnumWindows遍历所有窗口时,附加条件IsWindowVisible和GetWindowRect判断窗口尺寸,主窗口矩形的宽高应该大于 400x600。另外,发消息之前先ShowWindow(hWnd, SW_RESTORE)把所有最小化的微信窗口还原。
5.2 剪贴板粘贴总是插入到错误位置
现象: 用 3.2 的代码向聊天输入框粘贴文本,结果文本跑到了搜索框里,或者插到了聊天记录里。
原因: 焦点并没有真正落在目标输入框上。 虽然你调用了SetForegroundWindow(mainWindow),但微信主窗口内部有多个输入控件,键盘焦点默认可能停留在搜索框或者左侧列表。
解决: 在粘贴之前,用PostMessage(editHandle, WM_LBUTTONDOWN, ...)点击输入框内部一次,让焦点切换过去。 点击坐标可以取输入框客户区中点,先GetClientRect(editHandle),再发点击。 然后再剪贴板粘贴。 还有一个常见做法是发送Ctrl+L或其他快捷键聚焦输入框,不同微信版本快捷键不同,我用过Ctrl+Enter快捷键设置里可以自定义,推荐直接改成Ctrl+L聚焦输入框。
5.3 模拟点击的坐标偏移一百多个像素
现象:SetCursorPos指定的坐标和实际点击位置不一致,在 2K 或 4K 高 DPI 屏幕上尤其明显。
原因: 进程没有声明 DPI 感知,Windows 桌面缩放(比如 150%)会把物理坐标映射成虚拟坐标。 你定位的坐标是按物理像素算的,但SetCursorPos使用虚拟化坐标,两者相差缩放倍率。
解决: 进程启动时马上调用SetProcessDPIAware(),并且用GetSystemMetrics(SM_CXSCREEN)验证坐标范围。 如果还不行,就用GetWindowRect获取窗口的物理矩形,再换算成SetCursorPos可用的坐标。 在源码包的工具类里我看到作者留了一个DpiHelper,里面用GetDpiForWindow(hWnd)做动态缩放,这是最彻底的方案。
5.4 微信多开时打开了错误的会话窗口
现象: 电脑开了两个微信,想操作第二个微信,结果所有消息都发到了第一个微信上。
原因:FindWindow返回的句柄属于第一个进程。 微信主窗口句柄和进程 ID 相关联,你发消息时没有指定进程 ID,操作系统把消息投递给了第一个窗口。
解决: 使用Process.GetProcessesByName("WeChat")拿到所有微信进程,再为每个进程调用FindWeChatMainWindow(pid)。 发送消息之前确认GetWindowThreadProcessId(hWnd, out pid)等于你期望的进程 ID。 为了区分哪个进程对应哪个微信号,可以比较进程启动时间——后启动的通常是新登录的。
5.5 高频操作后回不了消息、被限制功能
现象: 批量发送 80 条消息之后,微信弹出安全提示,或者干脆不再接收新消息。
原因: 不是窗口操作问题,而是行为频率和账号质量的风控。 不管用什么 UI 自动化方案,操作行为特征都会被记录——固定间隔、固定顺序、无鼠标轨迹、无阅读动作,这些都会被判定为模拟行为。
解决: 把批处理的节奏做得像真人。 每次发送之前随机滚动一下会话列表、打开两三个聊天窗口看看、再回到目标窗口,模拟"人正在翻看微信"。 每发送 20 条,就随机休息 30 到 90 秒,期间可以做一些无关操作。 如果是公司业务群发,建议用企业微信官方接口,不要用 PC 版硬刷。 到了这一步,已经不是代码问题,而是运营策略问题,源码工具只能保证前置动作不犯错。
6. 验证与进阶:把模拟工具变成可回归的自动化测试脚本
源码跑通之后,你会发现最大的麻烦不是功能实现,而是“今天能用、明天可能就不能用”。 微信一升级,类名变了、坐标偏了、消息时序抖了。 所以我强烈建议你在这个仓库基础上加一层“回归验证”包装,把每个核心动作做前置检查和后置断言,失败时留下快照。 这样才能保证改一行代码之后,你能立刻知道哪条链路断了。
我会在Program.cs入口处定义一个WeChatTestCase基类,里面封装几个验证方法:AssertWindowVisible、AssertTextBoxEditable、AssertLastMessageSend。 每次跑完一个动作,把截图存到DebugOutput目录,并把日志写到控制台和log.txt两份。 然后写一个Watchdog后台线程,每 30 秒向微信聊天窗口发送一个探针字符串“ping”,如果两分钟内没收到回执,就自动重启脚本。
进阶技巧方面,有两个点很实用。 一是用 C# 委托和事件把接收到的消息发布到多个订阅者——比如一个订阅者弹 Windows 通知,另一个订阅者写数据库。 这样自动化消息接收就变成了一个宿主服务,而不是一次性脚本。 二是用 WMI 查询 CPU ID 作为当前实例标识,在跑群发任务时把任务状态以 JSON 写到本地,下次启动时通过异步Task并发恢复未完成的部分,避免重复发送。
收尾前我贴一段验证窗口可见性的辅助代码,这也是我自己的惯性写法:每步操作前先验证当前窗口状态,确认无误再走下一步。
public static void AssertWindowReady(IntPtr hWnd, string actionName) { if (hWnd == IntPtr.Zero) { Log($"[断言失败] {actionName} 窗口句柄为空,停止操作"); Environment.Exit(-1); } if (!IsWindowVisible(hWnd)) { ShowWindow(hWnd, SW_RESTORE); Thread.Sleep(200); } bool isEditable = false; var editWnd = FindWindowEx(hWnd, IntPtr.Zero, "ChatEditCtrl", null); if (editWnd != IntPtr.Zero) { isEditable = IsWindowEnabled(editWnd); } if (!isEditable) { Log($"[警告] {actionName} 输入框不可编辑,可能处于防打扰状态"); } Log($"[验证通过] {actionName} 窗口状态正常"); }逻辑说明:FindWindowEx用类名ChatEditCtrl查找输入框,不同微信版本这个类名可能变化,所以isEditable为 false 时不强制退出,但给出警告。 窗口不可见时先调ShowWindow还原,这是最常用的后悔药——千万别直接继续发消息,那样很容易把消息发到未知窗口。
参数说明:actionName是操作描述,比如"打开会话窗口"或"发送文本"。 整个脚本跑完后,你会在日志里看到每个动作的验证结果。 我把这个验证方法放在了所有业务动作的入口处——从那以后我每次改版微信后,都强制先跑一遍全链路测试,再打开批量发送开关,再也没有出现过半夜发消息发飞的情况。 希望这段经验能帮你少走几条弯路,祝你拆包顺利。
本文还有配套的精品资源,点击获取