FlaUI.UIA3避坑指南:微信自动化消息发送中的5个常见错误

使用C# + FlaUI Windows自动化框架实现打开记事本并输入“Hello” 阅读详情

FlaUI.UIA3微信自动化实战:从原理到避坑的深度解析

如果你正在用C#和FlaUI.UIA3捣鼓微信自动化,大概率已经踩过不少坑了。微信PC版的UI结构不算复杂,但用自动化工具操作时,各种意想不到的问题会接踵而至——进程附加失败、元素定位不准、消息发送不出去,这些我都经历过。今天这篇文章,我想从一个实际开发者的角度,分享我在微信自动化项目中积累的经验,特别是那些容易掉进去的“坑”,以及如何系统性地解决它们。

微信自动化不是简单的模拟点击,它涉及到Windows UI自动化技术的深入理解、微信客户端UI结构的准确分析,以及健壮的错误处理机制。很多人照着网上的代码抄一遍,运行时却发现完全不是那么回事。这是因为微信的UI结构在不同版本间会有微调,而且自动化操作对时机和状态的依赖非常敏感。

1. 环境搭建与基础配置:避开第一个雷区

开始之前,得先把环境弄对。很多人第一步就栽了跟头——FlaUI的版本选择和引用方式。

1.1 项目配置与依赖管理

我强烈建议不要直接从NuGet安装FlaUI的预编译包,特别是当你需要调试或深入定制时。微信的UI结构比较特殊,某些情况下需要修改FlaUI的源码来适配。直接从GitHub克隆源码,自己编译引用,这样最稳妥。

<!-- 项目文件中的引用示例 -->
<Reference Include="FlaUI.Core">
  <HintPath>..\lib\FlaUI.Core.dll</HintPath>
</Reference>
<Reference Include="FlaUI.UIA3">
  <HintPath>..\lib\FlaUI.UIA3.dll</HintPath>
</Reference>

编译时要注意目标框架版本。微信PC版目前还是基于传统的Win32技术,所以你的项目最好选择.NET Framework 4.6.1或更高版本,确保对Windows API有完整的支持。

注意:如果你遇到“无法加载DLL‘UIAutomationCore.dll’”之类的错误,大概率是系统缺少必要的运行时组件。确保安装了最新版本的.NET Framework运行时,并且系统是Windows 10或更高版本。

1.2 微信进程的可靠附加

附加到微信进程是第一步,但这里就有好几个坑等着你。

using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.UIA3;

// 错误示例:直接获取第一个进程
var processes = Process.GetProcessesByName("WeChat");
if (processes.Length == 0)
{
    Console.WriteLine("微信未启动");
    return;
}

// 这样写有问题:如果用户开了多个微信实例呢?
using var app = Application.Attach(processes[0].Id);

更健壮的做法是让用户选择,或者处理多实例的情况:

public static Application GetWeChatApplication()
{
    var processes = Process.GetProcessesByName("WeChat");
    
    if (processes.Length == 0)
    {
        // 尝试启动微信
        var wechatPath = @"C:\Program Files (x86)\Tencent\WeChat\WeChat.exe";
        if (File.Exists(wechatPath))
        {
            return Application.Launch(wechatPath);
        }
        throw new InvalidOperationException("微信未运行且无法自动启动");
    }
    
    if (processes.Length == 1)
    {
        return Application.Attach(processes[0].Id);
    }
    
    // 多实例处理:让用户选择或者使用特定逻辑
    Console.WriteLine("检测到多个微信实例,请选择:");
    for (int i = 0; i < processes.Length; i++)
    {
        Console.WriteLine($"{i}: PID={processes[i].Id}, 启动时间={processes[i].StartTime}");
    }
    
    // 这里可以添加更复杂的逻辑,比如根据窗口标题选择
    return Application.Attach(processes[0].Id);
}

1.3 自动化实例的创建与管理

创建UIA3Automation实例时,很多人忽略了一个重要细节:一定要使用using语句或者确保正确释放。UI自动化会占用系统资源,不释放的话可能导致内存泄漏。

// 正确做法:使用using确保资源释放
using var automation = new UIA3Automation();
var mainWindow = app.GetMainWindow(automation);

// 或者,如果你需要长时间保持自动化实例
var automation = new UIA3Automation();
try
{
    var mainWindow = app.GetMainWindow(automation);
    // 执行操作...
}
finally
{
    automation.Dispose();
}

2. 微信UI元素定位:精准找到目标控件

微信的UI结构不算复杂,但元素定位的准确性直接决定了自动化脚本的稳定性。我见过太多因为定位不准而失败的案例。

2.1 理解微信的UI层次结构

先用FlaUInspect工具分析微信的UI结构,这是必不可少的一步。微信主窗口的基本结构通常是这样的:

Window (微信主窗口)
├── Pane (左侧导航栏)
│   ├── Button (聊天)
│   ├── Button (通讯录)
│   └── Button (朋友圈)
├── Pane (主内容区)
│   ├── List (会话列表/联系人列表)
│   └── Edit (搜索框)
└── Pane (聊天区域)
    ├── Edit (消息输入框)
    └── Button (发送按钮)

但要注意,不同版本的微信,这个结构可能有细微差别。我建议在代码中加入版本检测逻辑:

public class WeChatUIMapper
{
    private readonly Window _mainWindow;
    
    // 微信版本检测
    public string DetectWeChatVersion()
    {
        var versionInfo = _mainWindow.FindFirstDescendant(
            cf => cf.ByControlType(ControlType.Text).And(cf.ByName.Contains("微信"))
        );
        
        if (versionInfo != null)
        {
            // 从窗口标题或其他地方提取版本信息
            return ExtractVersionFromText(versionInfo.Name);
        }
        return "未知版本";
    }
    
    // 根据版本调整元素定位策略
    public AutomationElement GetSearchBox()
    {
        var version = DetectWeChatVersion();
        
        if (version.StartsWith("3."))
        {
            // 微信3.x版本的搜索框定位方式
            return _mainWindow.FindFirstDescendant(
                cf => cf.ByControlType(ControlType.Edit).And(cf.ByName("搜索"))
            );
        }
        else
        {
            // 较新版本的定位方式
            return _mainWindow.FindFirstDescendant(
                cf => cf.ByControlType(ControlType.Edit).And(
                    cf.ByAutomationId("SearchEditBox")
                )
            );
        }
    }
}

2.2 元素定位的多种策略

不要只依赖一种定位方式。我总结了几种策略,按优先级使用:

定位策略优点缺点适用场景
ByName最直观,微信很多元素有明确名称名称可能变化,多语言版本不同聊天、通讯录、朋友圈等导航按钮
ByAutomationId最稳定,ID通常不变微信很多元素没有设置AutomationId输入框、按钮等标准控件
ByControlType通用性强可能匹配到多个元素需要结合其他条件使用
FindAllDescendants + 条件过滤最灵活性能较差复杂或动态变化的UI
XPath精准定位依赖特定结构,易受UI变化影响已知固定结构的元素

实际代码中,我通常会写一个包装方法,尝试多种方式:

public static AutomationElement FindWeChatElement(
    Window mainWindow, 
    string elementName, 
    ControlType? expectedType = null)
{
    // 策略1:按名称查找
    var element = mainWindow.FindFirstDescendant(cf => cf.ByName(elementName));
    if (element != null && (expectedType == null || element.ControlType == expectedType))
    {
        return element;
    }
    
    // 策略2:按类型和名称组合查找
    if (expectedType != null)
    {
        element = mainWindow.FindFirstDescendant(
            cf => cf.ByControlType(expectedType.Value).And(cf.ByName(elementName))
        );
        if (element != null) return element;
    }
    
    // 策略3:查找所有后代然后过滤
    var allElements = mainWindow.FindAllDescendants();
    element = allElements.FirstOrDefault(e => 
        e.Name == elementName && 
        (expectedType == null || e.ControlType == expectedType)
    );
    
    if (element != null) return element;
    
    // 策略4:使用XPath作为后备方案
    // 这里需要根据实际情况编写XPath
    throw new ElementNotFoundException($"未找到元素: {elementName}");
}

2.3 处理动态加载和延迟渲染

微信的UI元素不是一次性全部加载的,特别是聊天列表、联系人列表这种可能很长的列表。直接查找可能找不到,需要等待。

public static AutomationElement WaitForElement(
    Window mainWindow, 
    Func<AutomationElement> findFunc, 
    int timeoutMs = 5000, 
    int intervalMs = 200)
{
    var startTime = DateTime.Now;
    
    while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
    {
        var element = findFunc();
        if (element != null && element.IsAvailable)
        {
            // 额外检查:元素是否可见且可交互
            if (element.BoundingRectangle.Width > 0 && 
                element.BoundingRectangle.Height > 0)
            {
                return element;
            }
        }
        
        Thread.Sleep(intervalMs);
    }
    
    throw new TimeoutException($"等待元素超时: {timeoutMs}ms");
}

// 使用示例
var chatButton = WaitForElement(
    mainWindow, 
    () => mainWindow.FindFirstDescendant(cf => cf.ByName("聊天"))
);

3. 消息发送流程的完整实现与优化

发送消息看起来简单,但每个环节都可能出问题。我从最简单的发送到文件传输助手开始,逐步构建健壮的发送逻辑。

3.1 基础消息发送流程

先看一个基础的发送流程,这里有很多需要注意的细节:

public class WeChatMessageSender
{
    private readonly Window _mainWindow;
    private readonly UIA3Automation _automation;
    
    public bool SendMessageToContact(string contactName, string message)
    {
        try
        {
            // 1. 确保微信窗口激活
            ActivateWeChatWindow();
            
            // 2. 切换到搜索模式
            SwitchToSearchMode();
            
            // 3. 搜索联系人
            SearchContact(contactName);
            
            // 4. 等待搜索结果并选择
            SelectContactFromResults(contactName);
            
            // 5. 输入消息
            InputMessage(message);
            
            // 6. 发送
            return SendMessage();
        }
        catch (Exception ex)
        {
            LogError($"发送消息失败: {ex.Message}");
            return false;
        }
    }
    
    private void ActivateWeChatWindow()
    {
        // 使用Windows API确保窗口激活
        var handle = new IntPtr(_mainWindow.Properties.NativeWindowHandle);
        if (handle != IntPtr.Zero)
        {
            // 先恢复窗口(如果最小化)
            User32.ShowWindow(handle, User32.SW_RESTORE);
            
            // 激活窗口
            User32.SetForegroundWindow(handle);
            
            // 等待窗口完全激活
            Thread.Sleep(300);
        }
    }
}

3.2 搜索联系人的正确姿势

搜索功能最容易出问题,特别是当微信有大量联系人时:

private void SearchContact(string contactName)
{
    // 查找搜索框
    var searchBox = _mainWindow.FindFirstDescendant(
        cf => cf.ByControlType(ControlType.Edit).And(
            cf.ByName("搜索").Or(cf.ByName("搜索联系人"))
        )
    );
    
    if (searchBox == null)
    {
        // 尝试点击搜索图标
        var searchIcon = _mainWindow.FindFirstDescendant(
            cf => cf.ByControlType(ControlType.Button).And(cf.ByName("搜索"))
        );
        if (searchIcon != null)
        {
            searchIcon.Click();
            Thread.Sleep(500); // 等待搜索框出现
            searchBox = _mainWindow.FindFirstDescendant(
                cf => cf.ByControlType(ControlType.Edit)
            );
        }
    }
    
    if (searchBox == null)
        throw new ElementNotFoundException("找不到搜索框");
    
    // 清空搜索框(重要!)
    searchBox.AsTextBox().Text = "";
    
    // 输入联系人名称
    Keyboard.Type(contactName);
    
    // 等待搜索结果加载
    Thread.Sleep(800); // 这个等待时间很关键
    
    // 按回车确认搜索
    Keyboard.Type(VirtualKeyShort.RETURN);
    
    // 额外等待搜索结果渲染
    Thread.Sleep(1000);
}

提示:搜索后的等待时间需要根据联系人数量调整。如果联系人很多,可能需要更长的等待时间。我通常会在代码中加入动态等待逻辑,检测搜索结果是否已经出现。

3.3 处理搜索结果的选择

搜索结果可能包含多个匹配项,需要准确选择:

private void SelectContactFromResults(string contactName)
{
    // 等待搜索结果容器出现
    var resultsContainer = WaitForElement(
        _mainWindow,
        () => _mainWindow.FindFirstDescendant(
            cf => cf.ByName("搜索结果").Or(cf.ByName("搜索"))
        ),
        timeoutMs: 3000
    );
    
    if (resultsContainer == null)
    {
        // 可能没有单独的搜索结果容器,直接在主窗口查找
        var contactItems = _mainWindow.FindAllDescendants()
            .Where(e => e.ControlType == ControlType.ListItem &&
                       e.Name.Contains(contactName))
            .ToList();
        
        if (contactItems.Count == 0)
            throw new ContactNotFoundException($"未找到联系人: {contactName}");
        
        // 选择第一个匹配的联系人
        contactItems[0].Click();
    }
    else
    {
        // 在搜索结果容器中查找
        var contactItem = resultsContainer.FindFirstDescendant(
            cf => cf.ByControlType(ControlType.ListItem).And(cf.ByName(contactName))
        );
        
        if (contactItem == null)
            throw new ContactNotFoundException($"在搜索结果中未找到联系人: {contactName}");
        
        contactItem.Click();
    }
    
    // 等待聊天界面加载完成
    Thread.Sleep(1000);
}

3.4 消息输入与发送的细节处理

输入消息时,直接设置文本框文本可能不如模拟键盘输入稳定:

private void InputMessage(string message)
{
    // 查找消息输入框
    var messageInput = WaitForElement(
        _mainWindow,
        () => _mainWindow.FindFirstDescendant(
            cf => cf.ByControlType(ControlType.Edit).And(
                cf.ByName("输入").Or(cf.ByAutomationId("ChatInput"))
            )
        ),
        timeoutMs: 2000
    );
    
    if (messageInput == null)
    {
        // 备用方案:查找任何可编辑的文本框
        messageInput = _mainWindow.FindFirstDescendant(
            cf => cf.ByControlType(ControlType.Edit).And(
                cf.ByClassName("Edit").Or(cf.ByAutomationId("Edit"))
            )
        );
    }
    
    if (messageInput == null)
        throw new ElementNotFoundException("找不到消息输入框");
    
    // 点击输入框获取焦点
    messageInput.Click();
    Thread.Sleep(200);
    
    // 清空可能存在的旧内容
    var textBox = messageInput.AsTextBox();
    if (!string.IsNullOrEmpty(textBox.Text))
    {
        // 全选删除
        Keyboard.TypeSimultaneously(VirtualKeyShort.CONTROL, VirtualKeyShort.KEY_A);
        Thread.Sleep(100);
        Keyboard.Type(VirtualKeyShort.DELETE);
        Thread.Sleep(100);
    }
    
    // 输入消息
    if (message.Length > 100)
    {
        // 长文本使用剪贴板,更稳定
        System.Windows.Forms.Clipboard.SetText(message);
        Keyboard.TypeSimultaneously(VirtualKeyShort.CONTROL, VirtualKeyShort.KEY_V);
    }
    else
    {
        // 短文本直接键盘输入
        Keyboard.Type(message);
    }
    
    // 等待输入完成
    Thread.Sleep(300);
}

private bool SendMessage()
{
    // 查找发送按钮
    var sendButton = _mainWindow.FindFirstDescendant(
        cf => cf.ByControlType(ControlType.Button).And(
            cf.ByName("发送").Or(cf.ByName("Send")).Or(cf.ByName("发送(S)"))
        )
    );
    
    if (sendButton == null)
    {
        // 备用方案:按Ctrl+Enter发送
        Keyboard.TypeSimultaneously(VirtualKeyShort.CONTROL, VirtualKeyShort.RETURN);
        return true;
    }
    
    // 检查按钮是否可用
    if (sendButton.IsEnabled)
    {
        sendButton.Click();
        return true;
    }
    
    return false;
}

4. 高级功能与异常处理

基础功能实现后,我们需要考虑更复杂的场景和健壮性。

4.1 批量消息发送的实现

批量发送不是简单的循环,需要考虑速率限制和错误恢复:

public class BatchMessageSender
{
    private readonly WeChatMessageSender _sender;
    private readonly int _delayBetweenMessages;
    
    public BatchMessageSender(int delayMs = 1000)
    {
        _sender = new WeChatMessageSender();
        _delayBetweenMessages = delayMs;
    }
    
    public BatchSendResult SendToMultipleContacts(
        List<string> contacts, 
        string message,
        Action<string, bool> progressCallback = null)
    {
        var result = new BatchSendResult();
        
        foreach (var contact in contacts)
        {
            try
            {
                var success = _sender.SendMessageToContact(contact, message);
                
                result.TotalAttempts++;
                if (success)
                {
                    result.SuccessCount++;
                    progressCallback?.Invoke($"{contact}: 发送成功", true);
                }
                else
                {
                    result.FailedContacts.Add(contact);
                    progressCallback?.Invoke($"{contact}: 发送失败", false);
                }
                
                // 避免发送过快被限制
                Thread.Sleep(_delayBetweenMessages);
                
                // 每发送10条消息,额外休息一下
                if (result.SuccessCount % 10 == 0)
                {
                    Thread.Sleep(3000);
                }
            }
            catch (Exception ex)
            {
                result.FailedContacts.Add(contact);
                result.Errors.Add($"{contact}: {ex.Message}");
                progressCallback?.Invoke($"{contact}: 异常 - {ex.Message}", false);
                
                // 严重错误时暂停
                if (ex is ElementNotFoundException || ex is TimeoutException)
                {
                    Thread.Sleep(5000);
                }
            }
        }
        
        return result;
    }
}

public class BatchSendResult
{
    public int TotalAttempts { get; set; }
    public int SuccessCount { get; set; }
    public List<string> FailedContacts { get; } = new();
    public List<string> Errors { get; } = new();
    
    public double SuccessRate => TotalAttempts > 0 ? 
        (double)SuccessCount / TotalAttempts * 100 : 0;
}

4.2 处理微信的多标签页和弹出窗口

微信有时会有各种弹出窗口(登录验证、新消息提示等),需要处理:

public class WeChatWindowManager
{
    private readonly Application _app;
    private readonly UIA3Automation _automation;
    
    public bool ClosePopupWindows()
    {
        var allWindows = _app.GetAllTopLevelWindows(_automation);
        var closedCount = 0;
        
        foreach (var window in allWindows)
        {
            var title = window.Title;
            
            // 识别并关闭常见的弹出窗口
            if (IsPopupWindow(title))
            {
                try
                {
                    // 查找关闭按钮
                    var closeButton = window.FindFirstDescendant(
                        cf => cf.ByControlType(ControlType.Button).And(
                            cf.ByName("关闭").Or(cf.ByName("取消")).Or(cf.ByName("确定"))
                        )
                    );
                    
                    if (closeButton != null)
                    {
                        closeButton.Click();
                        closedCount++;
                        Thread.Sleep(500);
                    }
                }
                catch
                {
                    // 忽略关闭失败的情况
                }
            }
        }
        
        return closedCount > 0;
    }
    
    private bool IsPopupWindow(string title)
    {
        var popupKeywords = new[]
        {
            "提示", "确认", "警告", "错误", "登录", "验证",
            "安全", "升级", "更新", "网络"
        };
        
        return popupKeywords.Any(keyword => 
            title.Contains(keyword, StringComparison.OrdinalIgnoreCase));
    }
    
    public void EnsureMainWindowActive()
    {
        var mainWindow = _app.GetMainWindow(_automation);
        if (mainWindow != null)
        {
            // 切换到主窗口
            mainWindow.Focus();
            
            // 关闭可能的弹出窗口
            ClosePopupWindows();
            
            // 再次激活主窗口
            mainWindow.Focus();
            Thread.Sleep(300);
        }
    }
}

4.3 监控与日志系统

完善的日志系统对于调试和问题排查至关重要:

public class WeChatAutomationLogger
{
    private readonly string _logFilePath;
    private readonly bool _enableScreenshots;
    
    public WeChatAutomationLogger(string logDir = null, bool enableScreenshots = true)
    {
        _enableScreenshots = enableScreenshots;
        
        if (string.IsNullOrEmpty(logDir))
        {
            logDir = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "WeChatAutomation",
                "Logs"
            );
        }
        
        Directory.CreateDirectory(logDir);
        _logFilePath = Path.Combine(logDir, $"wechat_auto_{DateTime.Now:yyyyMMdd_HHmmss}.log");
    }
    
    public void LogOperation(string operation, bool success, string details = null)
    {
        var logEntry = $"[{DateTime.Now:HH:mm:ss}] {operation} - {(success ? "成功" : "失败")}";
        
        if (!string.IsNullOrEmpty(details))
        {
            logEntry += $" | {details}";
        }
        
        File.AppendAllText(_logFilePath, logEntry + Environment.NewLine);
        Console.WriteLine(logEntry);
        
        // 失败时截图
        if (!success && _enableScreenshots)
        {
            CaptureScreenshot(operation);
        }
    }
    
    private void CaptureScreenshot(string operationName)
    {
        try
        {
            var screenshotDir = Path.Combine(
                Path.GetDirectoryName(_logFilePath),
                "Screenshots"
            );
            Directory.CreateDirectory(screenshotDir);
            
            var screenshotPath = Path.Combine(
                screenshotDir,
                $"{DateTime.Now:yyyyMMdd_HHmmss}_{operationName}.png"
            );
            
            // 使用FlaUI的截图功能
            var mainWindow = GetWeChatMainWindow();
            if (mainWindow != null)
            {
                var screenshot = mainWindow.Capture();
                screenshot.ToFile(screenshotPath);
            }
        }
        catch (Exception ex)
        {
            File.AppendAllText(_logFilePath, 
                $"[{DateTime.Now:HH:mm:ss}] 截图失败: {ex.Message}{Environment.NewLine}");
        }
    }
}

5. 性能优化与最佳实践

当自动化脚本需要长时间运行时,性能优化就变得很重要。

5.1 减少不必要的元素查找

元素查找是性能瓶颈,要尽量减少查找次数:

public class WeChatUICache
{
    private readonly Dictionary<string, AutomationElement> _elementCache = new();
    private readonly Window _mainWindow;
    private DateTime _lastCacheRefresh = DateTime.MinValue;
    
    public AutomationElement GetCachedElement(string elementKey, Func<AutomationElement> findFunc)
    {
        // 缓存有效期:5分钟
        if (_elementCache.ContainsKey(elementKey) && 
            (DateTime.Now - _lastCacheRefresh).TotalMinutes < 5)
        {
            var element = _elementCache[elementKey];
            if (element != null && element.IsAvailable)
            {
                return element;
            }
        }
        
        // 缓存失效或元素不可用,重新查找
        var newElement = findFunc();
        if (newElement != null)
        {
            _elementCache[elementKey] = newElement;
            _lastCacheRefresh = DateTime.Now;
        }
        
        return newElement;
    }
    
    public void InvalidateCache()
    {
        _elementCache.Clear();
        _lastCacheRefresh = DateTime.MinValue;
    }
    
    // 常用元素的缓存访问方法
    public AutomationElement GetSearchBox() => 
        GetCachedElement("SearchBox", () => 
            _mainWindow.FindFirstDescendant(cf => cf.ByName("搜索")));
    
    public AutomationElement GetChatList() => 
        GetCachedElement("ChatList", () => 
            _mainWindow.FindFirstDescendant(cf => cf.ByName("会话")));
}

5.2 异步操作与超时控制

避免脚本卡死,必须设置合理的超时:

public static async Task<AutomationElement> FindElementWithTimeoutAsync(
    Window mainWindow,
    Func<AutomationElement> findFunc,
    int timeoutMs = 5000,
    CancellationToken cancellationToken = default)
{
    var startTime = DateTime.Now;
    
    while (!cancellationToken.IsCancellationRequested)
    {
        var element = findFunc();
        if (element != null && element.IsAvailable)
        {
            return element;
        }
        
        if ((DateTime.Now - startTime).TotalMilliseconds > timeoutMs)
        {
            break;
        }
        
        await Task.Delay(200, cancellationToken);
    }
    
    return null;
}

public static async Task<bool> WaitForConditionAsync(
    Func<bool> condition,
    int timeoutMs = 5000,
    int checkIntervalMs = 200,
    CancellationToken cancellationToken = default)
{
    var startTime = DateTime.Now;
    
    while (!cancellationToken.IsCancellationRequested)
    {
        if (condition())
        {
            return true;
        }
        
        if ((DateTime.Now - startTime).TotalMilliseconds > timeoutMs)
        {
            return false;
        }
        
        await Task.Delay(checkIntervalMs, cancellationToken);
    }
    
    return false;
}

5.3 错误恢复与重试机制

自动化脚本必须有自我恢复能力:

public class RetryExecutor
{
    private readonly int _maxRetries;
    private readonly int _retryDelayMs;
    
    public RetryExecutor(int maxRetries = 3, int retryDelayMs = 1000)
    {
        _maxRetries = maxRetries;
        _retryDelayMs = retryDelayMs;
    }
    
    public async Task<T> ExecuteWithRetryAsync<T>(
        Func<Task<T>> operation,
        Func<Exception, bool> shouldRetry = null,
        Action<int, Exception> onRetry = null)
    {
        var lastException = new Exception();
        
        for (int attempt = 1; attempt <= _maxRetries; attempt++)
        {
            try
            {
                return await operation();
            }
            catch (Exception ex)
            {
                lastException = ex;
                
                // 检查是否应该重试
                if (shouldRetry != null && !shouldRetry(ex))
                {
                    throw;
                }
                
                onRetry?.Invoke(attempt, ex);
                
                if (attempt < _maxRetries)
                {
                    await Task.Delay(_retryDelayMs * attempt); // 指数退避
                }
            }
        }
        
        throw new AggregateException(
            $"操作在{_maxRetries}次重试后失败", 
            lastException);
    }
    
    public bool ShouldRetryOnCommonErrors(Exception ex)
    {
        // 这些错误通常可以重试
        return ex is ElementNotFoundException ||
               ex is TimeoutException ||
               ex.Message.Contains("无法访问") ||
               ex.Message.Contains("超时") ||
               ex.Message.Contains("不可用");
    }
}

6. 实际项目中的架构设计

对于需要长期维护的微信自动化项目,良好的架构设计能让代码更易维护。

6.1 分层架构设计

我推荐的分层架构:

WeChatAutomation.Core (核心层)
├── Interfaces (接口定义)
├── Models (数据模型)
├── Services (核心服务)
└── Utilities (工具类)

WeChatAutomation.Implementation (实现层)
├── UIA3 (FlaUI具体实现)
├── ElementLocators (元素定位器)
└── Operations (具体操作)

WeChatAutomation.Client (客户端)
├── ConsoleApp (控制台应用)
├── WinFormsApp (窗体应用)
└── Service (Windows服务)

6.2 配置化管理

把容易变化的配置提取出来:

{
  "WeChatAutomation": {
    "Timeouts": {
      "ElementFind": 5000,
      "PageLoad": 3000,
      "Operation": 10000
    },
    "RetrySettings": {
      "MaxRetries": 3,
      "RetryDelay": 1000
    },
    "ElementSelectors": {
      "SearchBox": {
        "Strategies": [
          { "Type": "ByName", "Value": "搜索" },
          { "Type": "ByAutomationId", "Value": "SearchEditBox" },
          { "Type": "ByControlType", "Value": "Edit" }
        ]
      },
      "SendButton": {
        "Strategies": [
          { "Type": "ByName", "Value": "发送(S)" },
          { "Type": "ByName", "Value": "发送" },
          { "Type": "ByAutomationId", "Value": "SendButton" }
        ]
      }
    },
    "WeChatPaths": {
      "Default": "C:\\Program Files (x86)\\Tencent\\WeChat\\WeChat.exe",
      "Alternatives": [
        "D:\\WeChat\\WeChat.exe",
        "C:\\Users\\%USERNAME%\\AppData\\Local\\Tencent\\WeChat\\WeChat.exe"
      ]
    }
  }
}

6.3 插件化设计

支持不同的微信版本和功能扩展:

public interface IWeChatAutomationPlugin
{
    string PluginName { get; }
    Version SupportedWeChatVersion { get; }
    
    bool CanHandle(Window mainWindow);
    void Initialize(Window mainWindow, UIA3Automation automation);
    
    // 插件提供的功能
    IEnumerable<string> GetAvailableContacts();
    bool SendMessage(string contact, string message);
    // ... 其他功能
}

public class WeChatAutomationHost
{
    private readonly List<IWeChatAutomationPlugin> _plugins = new();
    private IWeChatAutomationPlugin _activePlugin;
    
    public void RegisterPlugin(IWeChatAutomationPlugin plugin)
    {
        _plugins.Add(plugin);
    }
    
    public bool Initialize(Window mainWindow, UIA3Automation automation)
    {
        foreach (var plugin in _plugins.OrderByDescending(p => p.SupportedWeChatVersion))
        {
            if (plugin.CanHandle(mainWindow))
            {
                _activePlugin = plugin;
                plugin.Initialize(mainWindow, automation);
                return true;
            }
        }
        
        return false;
    }
    
    // 通过活动插件执行操作
    public bool SendMessage(string contact, string message)
    {
        if (_activePlugin == null)
            throw new InvalidOperationException("没有可用的插件");
        
        return _activePlugin.SendMessage(contact, message);
    }
}

微信自动化确实有很多坑,但一旦掌握了正确的方法,就能构建出稳定可靠的自动化脚本。关键是要理解UI自动化的原理,而不是盲目复制代码。每个微信版本、每台电脑的环境都可能不同,所以你的代码需要有足够的灵活性和容错能力。

我在实际项目中发现,最稳定的方案往往是最简单直接的方案。不要过度设计,但要确保每个操作都有错误处理和恢复机制。多使用等待和重试,少依赖固定的延迟时间。最重要的是,要有一套完善的日志系统,这样当出现问题时,你能快速定位原因。

自动化工具应该让工作更轻松,而不是制造更多问题。花时间构建一个健壮的基础框架,后续的开发会顺利很多。记住,好的自动化脚本是经过大量测试和调整的产物,不要指望一次就能写完美。

Flaui使用说明 FlaUI 是一个 .NET 库,可帮助对 Windows 应用程序(Win32、WinForms、WPF、Store Apps 等)进行自动化 UI 测试。它基于 Microsoft 的本机 UI 自动化库,因此是它们的包装器。FlaUI 包装了 UI Automation 库中的几乎所有内容,但也提供了本机对象,以防有人有 FlaUI 尚未涵盖的特殊需求。FlaUI官方介绍讲分两个版本UIA2:原生 UI 自动化 API 的托管库。 阅读详情

相关推荐

使用C#进行UI自动化:UIA2与UIA3及FlaUI的介绍

UI Automation (UIA) 是微软提供的一个框架,用于使Windows应用程序对辅助技术和自动化测试工具可访问。它允许开发人员和测试人员编写程序来控制和查询用户界面元素。随着技术的发展,UIA也经历了不同的版本迭代,以适应新的需求和技术进步。本文将介绍UIA2和UIA3的主要区别,并探讨如何使用FlaUI这个第三方库来简化UI自动化的实现。FlaUI是一个开源的.NET库,它构建在UI Automation之上,旨在简化UI自动化的过程。

听我俩天的博客 2574

(1)开启探索微信自动化之路-微信UI自动化(.Net+C#)

微信自动化

SOF学习拒绝刷屏 1万+

告别手动操作:用FlaUI.UIA3轻松搞定微信消息自动化(C#实战篇)

本文详细介绍了如何利用C#和FlaUI.UIA3库构建稳健的微信消息自动化方案。通过环境搭建、核心代码解析和实战场景扩展,指导开发者实现自动发送消息、批量通知及定时任务,并分享了应对UI变化、提升稳定性的高级技巧与避坑指南,帮助告别重复手动操作。

weixin_29268637的博客 229

C# .NET基于FlaUIAutomation开发PC端微信机器人#1

Hello 朋友们大家好,我是J3ME,最近正在开发一个微信机器人项目,在开发之前尝试找了许多资料,结果发现针对C# .NET开发微信机器人的项目,不论是开源还是闭源的都算是比较少,(甚至是其他语言,目前开源能用的也是比较少),目前主流的两种开发思路:一是HOOK,二是Web协议(包括统信OS)。这两种开发思路缺点都比较明显,那么今天我来给大家分享一种比较特别的开发思路,基于UIAutomation(UI自动化)来实现PC端微信机器人的开发。准备好了吗,让我们开始吧!

martinzuo的博客 4031

微信3.9.6.33自动化实战:用C#和FlaUI.UIA4实现消息自动发送(附避坑指南)

本文详细介绍了如何针对微信3.9.6.33版本,使用C#和FlaUI.UIA4框架实现消息自动发送功能。内容涵盖环境搭建、新版微信UI结构解析、核心自动化流程实现,并提供了关键的稳定性优化与避坑指南,帮助开发者构建健壮的桌面自动化脚本。

weixin_29300931的博客 622

Python FlaUI自动化测试:告别屏幕阅读器,精准操控新版微信

UI自动化测试是软件质量保障的关键环节,其核心原理在于通过程序模拟用户操作,与应用程序的UI元素进行交互。传统的屏幕阅读器或图像识别方案虽然通用,但存在稳定性差、维护成本高、执行效率低等痛点。现代UI自动化框架通过直接访问应用程序底层的UI树结构,实现了对控件的精准定位与操作,从而在测试稳定性、执行速度和可维护性上带来了显著的技术价值。这一技术尤其适用于Windows桌面应用,如微信、QQ等客户端软件的自动化测试与批量操作。本文以新版微信(4.1.5.16)为例,深入解析如何利用Python FlaUI库,

weixin_28826521的博客 292

RPA之基于FlaUI的微信发送消息给某人

RPA之基于C# FlaUI的微信发送消息给某人 目的 一直想实现微信的群发功能,但是,没有实现,原因有一条是怕违法,记得某某公司因为破解了微信的接口巴拉巴拉,然后,被告了。罚了N多钱。 这个时候,我想,如果我基于RPA技术,那么,就不会对微信有任何影响啊,毕竟,我只是模拟我的电脑操作,微信公司也识别不到我的行为是非法的。 那么,这个行为可能就是基于安全的方式的技术了。 所以,我就尝试了一下,同时也借鉴了网络上的资料,给我了一些启发 一、FlaUI是什么? FlaUI 是一个基于微软 UIAutomat

蓝创精英团队 4401

C#实现微信消息自动发送

FlaUI.UIA3实现微信或者QQ消息的自动群发

weixin_41374915的博客 3945

C#中使用FlaUI进行Windows应用程序自动化及自动测试

C#中使用FlaUI进行Windows应用程序自动化及自动测试 FlaUI 是Roemer 开发的自动化框架,前身是TestStack.White,为了提供一个非常干净和现代的代码库,重新写了FlaUI以便于轻松进行协作和进一步开发。 FlaUI是一个.NET库,可帮助自动测试Windows应用程序(Win32,WinForms,WPF,Store Apps等)。它基于Microsoft的本机UI自动化库,因此是围绕它们的包装。FlaUI几乎包装了UI自动化库中的所有内容,还提供了本机对象,以防万.

1万+

【微信自动化】第二节:控制微信导航栏

本章节我们通过FlaUI获取微信窗体的导航栏按钮,实现控制导航栏切换的效果

搬砖赚钱养家 1502

Winform基于FlaUI实现微信自动化

基于FlaUI实现微信自动化,定时任务(定时给好友发送消息、定时发送群聊消息、定时采集朋友圈消息等)、自动回复好友及群聊消息、群聊机器人

ftfmatlab的博客 1320

C# .NET基于FlaUIAutomation开发PC端微信机器人#2

Hello 朋友们大家好,我们接着上一节:C# .NET基于FlaUIAutomation开发PC端微信机器人#1继续我们的微信机器人开发,本节要实现的内容是获取当前聊天对象的聊天记录。 首先,必不可少的安装相关库以及初始化FlaUIAutomation本节将不在重复,不清楚的朋友烦请移步上一节文章,话不多说,直接上代码: 运行代码,查看效果: 代码的的具体作用,我已经注释的非常清楚,最后需要再次强调:基于UI自动化库开发的重点是要对自动化目标程序的元素树结

martinzuo的博客 1700

C#FlaUI.UIA实现发送微信消息原理

C#FlaUI.UIA实现发送微信消息原理

基于FlaUI自动化+chatGPT实现微信自动回复

3.当刷新到新的消息时,模拟鼠标点击到对应的会话人,此时判断是群聊还是人,如果是群聊则不回复。5.获取chatGPT的内容后将内容输入到微信聊天框,并模拟鼠标点击发送按钮。注:ChatAchieve是 IChat的实现,是对chatGPT的实现。4.获取消息后转发给chatGPT,同时等待chatGPT回复内容。1.将文件传输助手置顶,模拟鼠标点击文件传输助手;6.模拟鼠标点击文件传输助手,等待其它消息。2.一直刷新会话列表,有新的消息就需要回复内容;

ftfmatlab的博客 2013

FlaUISamples.rar

使用FlaUI实现微信自动发送消息

WSL 相关命令时遇到了 “无法从raw.githubusercontent.com提取分发列表、操作超时

核心问题:国内网络屏蔽raw.githubusercontent.com,导致WSL无法获取发行版列表;最优方案:优先用方案1(手动指定发行版安装),无需改配置,1分钟搞定;永久修复:用方案2(修改hosts),适合需要管理多个WSL发行版的场景;兜底方案:用方案3(手动下载安装包),适配所有网络环境。按以上步骤操作后,你不仅能正常安装WSL,等命令也能正常执行,后续可正常使用WSL搭配Docker Desktop(如切换WSL 2后端),无需再担心网络超时问题。

weixin_39766667的博客 2140

获取微信聊天记录

【代码】获取微信聊天记录。

saga661的博客 4132

基于LangGraph的智能报告生成平台项目分析

不知道你是否知道或者了解OpenAI and Gemini Deep Research。他们是一种能够根据输入问题进行规划、结合网络搜索获取信息并最终呈现结果的研究工具或技术。那这样research是如何实现的呢?最近刚好看到一个实现类似功能的开源项目: open_deep_search。本文将基于该项目进行代码层面剖析。

u011160113的博客 923
上一篇: FastReport.NET报表打印避坑指南:C#开发者必知的5个实用技巧
下一篇: 昇腾CANN训练营实战:30分钟搞定你的第一个Ascend C算子(附完整代码)
python9snake
博客等级 码龄1天 0粉丝 1724原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值