简介针对无需登录微信即可完成常用自动化操作的诉求这款基于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 枚举子窗口文本输入用剪贴板注入把文本放到系统剪贴板然后向窗口发CtrlV点击操作优先发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是 0x201WM_LBUTTONUP是 0x202WM_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)也不能逐字输入。这里现成的可靠方案是先把文本放进剪贴板然后向聊天窗口发CtrlV粘贴等粘贴完成后发回车。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. 向输入框句柄发送 CtrlV 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但CtrlV需要键盘焦点正确——所以先SetForegroundWindow(mainWindow)让微信置前。有些系统对PostMessage发送热键组合不响应更稳妥的是用SendInput直接模拟键盘但那就是最后手段了因为SendInput把虚拟键送到当前前台窗口容易送偏。参数说明WM_CHAR的参数是字符码v的 ASCII 是 118但这里我们用的是WM_CHARwParam传(IntPtr)v就代表字符 v。真正决定粘贴动作的其实是Ctrl按下时收到CtrlV键盘消息或者SendInput的组合键PostMessage 的时序偶发失灵血泪经验是在PostMessage(WM_KEYDOWN, VK_CONTROL)后加 20ms 延时再发WM_CHAR否则微信会丢掉v。3.3 鼠标点击句柄优先坐标兜底有些按钮比如发送按钮、聊天列表里某个联系人没有固定窗口句柄或者句柄拿不到有效位置。这时候需要模拟鼠标点击。首选是用SendMessage向按钮句柄发点击消息因为坐标系统不用算如果句柄无效退而用SetCursorPosmouse_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 版有一个所有版本都保留的交互——顶部搜索框。用CtrlF聚焦搜索框输入微信 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(Liststring 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 秒聚焦到最近会话第一条模拟双击选中最新消息文本然后CtrlC复制再从剪贴板里取文本。 这种方式虽然不够优雅但对纯 Windows 消息模拟方案来说是最容易落地的。public class MessageReceiver { public event EventHandlerstring 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表示CtrlA^c表示CtrlC。 点击会话列表第一项的坐标(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)再发点击。 然后再剪贴板粘贴。 还有一个常见做法是发送CtrlL或其他快捷键聚焦输入框不同微信版本快捷键不同我用过CtrlEnter快捷键设置里可以自定义推荐直接改成CtrlL聚焦输入框。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是操作描述比如打开会话窗口或发送文本。 整个脚本跑完后你会在日志里看到每个动作的验证结果。 我把这个验证方法放在了所有业务动作的入口处——从那以后我每次改版微信后都强制先跑一遍全链路测试再打开批量发送开关再也没有出现过半夜发消息发飞的情况。 希望这段经验能帮你少走几条弯路祝你拆包顺利。本文还有配套的精品资源点击获取