WinForm嵌入Edge+AI拦截:打造桌面程序自动分类的免费利器

### 从“小龙虾”式消耗到免费自给

做桌面软件分发的兄弟都知道,给海量exe打标签、分目录是一件多么反人类的事。尤其是当你的软件库每周新增上百个未知程序,靠人工一个一个识别分类,效率低到想砸键盘。

市面上有付费的“软件识别库”,像某龙虾工具,按调用次数收费——每查一次就消耗一份“土壤”,成本随软件量线性增长,小团队根本扛不住。我们团队就曾为此每月烧掉几千块,关键是识别准确率还不尽如人意。

有个坑:我们一开始走了弯路,想用纯本地特征库(MD5+文件名)来匹配,结果新软件变种层出不穷,特征库维护成了无底洞。后来痛定思痛——既然网页版AI能免费识别,为什么不直接“借”它的能力?

于是我们转向了 WinForm + Microsoft Edge WebView2 + 免费AI网页接口 的方案。核心思路很简单:用WebView2无头加载目标AI页面,程序化提交软件信息,拦截并结构化解析AI返回的识别结果,最终通过自建HTTP接口对外提供服务。整个闭环不花一分钱API调用费,而且响应速度完全可控。

说白了,这就是用浏览器的“免费通行证”打通桌面分类的最后一公里

技术选型:为什么是WinForm + WebView2?

不少老项目还在用WebBrowser(IE内核),但面对现代AI网页(重度使用ES6、WebSocket),它根本渲染不出来。而Microsoft Edge WebView2基于Chromium,完美兼容所有现代前端特性,且随Edge运行时自动更新,免去维护独立浏览器内核的麻烦。

.NET 8 WinForm(不是.NET Framework)提供现代化的异步编程模型,对async/awaitHttpClient支持友好,非常适合做这种I/O密集型的代理服务。

我们最终的架构是一个Windows服务+托盘UI:UI用于配置AI目标URL、本地软件库路径,后台运行HTTP侦听器(基于Microsoft.AspNetCore.App自宿主),接收外部调用请求。

核心三步走:抓取、拦截、封装

整个实现拆成三个关键环节,缺一不可。

1. WebView2 无头加载与页面就绪检测

我们不展示窗口,只用WebView2的隐藏实例。难点在于如何确认AI页面已完全加载且可交互——很多AI页面是SPA,NavigationCompleted事件只能代表HTML下载完成,JS渲染和模型初始化可能还在进行。

我们的做法是轮询执行JavaScript,检测页面上独有的“输入框”和“提交按钮”是否存在且可点击。代码片段如下:

// 使用 .NET 8 WinForm + WebView2
private async Task<bool> WaitForAIPageReady(WebView2 webView, int timeoutSeconds = 30)
{
    var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
    while (!cts.IsCancellationRequested)
    {
        try
        {
            string script = @"
                (() => {
                    const input = document.querySelector('textarea[placeholder*="输入"]') || 
                                   document.querySelector('input[type="text"]');
                    const btn = document.querySelector('button[type="submit"]') || 
                                document.querySelector('button:has(svg)');
                    return !!(input && btn && input.offsetParent !== null && btn.offsetParent !== null);
                })();
            ";
            var result = await webView.CoreWebView2.ExecuteScriptAsync(script);
            if (result.ToLower().Contains("true"))
                return true;
        }
        catch { /* 忽略执行异常,继续重试 */ }
        await Task.Delay(500, cts.Token);
    }
    return false;
}

踩坑警告:CoreWebView2.ExecuteScriptAsync 返回的是JSON字符串("true""false"),不要直接当bool比较。我们曾在这里翻车,导致死等30秒超时。

2. 拦截AI响应:Hook网络请求而非DOM解析

很多教程教大家用ExecuteScript去扒页面DOM文本,但AI的流式响应往往是SSE或WebSocket推送,DOM变化频繁且结构不稳定。更稳健的方案是拦截WebView2的网络响应

WebView2提供了CoreWebView2.WebResourceResponseReceived事件,可以捕获所有HTTP响应。我们通过过滤URL关键字(如/api/chat/v1/completions)和响应头(content-type: text/event-stream)来精准命中AI回复。

webView.CoreWebView2.WebResourceResponseReceived += async (sender, args) =>
{
    // 只拦截我们需要的API路径
    if (args.Request.Uri.Contains("/api/chat") && args.Response.StatusCode == 200)
    {
        // 注意:WebResourceResponseReceived 的 Response 流只能读一次,且是只读的。
        // 我们需要用 MemoryStream 复制一份,避免影响浏览器正常渲染。
        using var originalStream = args.Response.Content;
        if (originalStream != null)
        {
            var memStream = new MemoryStream();
            await originalStream.CopyToAsync(memStream);
            memStream.Position = 0;

            // 解析SSE格式或纯JSON,提取最终文本
            string responseBody = await ReadStreamAsStringAsync(memStream);
            string aiResult = ExtractAIResultFromResponse(responseBody);

            // 存入共享队列,供外部HTTP接口调用
            _aiResultQueue.Enqueue(aiResult);
            _readySignal.Set();
        }
    }
};

关键点args.Response.Content 是只读流,读取后必须CopyToAsync到新流,否则浏览器后续处理会失败。我们曾忘记复制,导致页面卡死,整整调试了一下午。

3. 构建HTTP接口:对外提供分类服务

既然是要“自动分类”,那必须让其他桌面程序或脚本能通过HTTP调用。我们在WinForm中自宿主一个极简的ASP.NET Core管道,监听http://localhost:56789/classify

请求体接收JSON:{ "fileName": "unknown.exe", "fileSize": 2048, "iconHash": "..." }

内部逻辑:

  1. 构造一段Prompt(例如:“请判断这个文件属于哪类软件:办公、开发、游戏、系统工具、其他。只输出类别名称。”)
  2. 通过WebView2.ExecuteScriptAsync将Prompt填入AI页面输入框并触发提交。
  3. 阻塞等待_aiResultQueue中出现新结果,超时则返回“unknown”。
[HttpPost("/classify")]
public async Task<IResult> Classify([FromBody] FileInfoRequest request)
{
    if (string.IsNullOrEmpty(request.FileName))
        return Results.BadRequest("Missing fileName");

    // 构造并填充AI输入
    string prompt = $"文件名称:{request.FileName},大小:{request.FileSize}KB。请仅输出一个类别:办公|开发|游戏|系统工具|其他。";
    string fillScript = $@"
        document.querySelector('textarea').value = '{EscapeJsString(prompt)}';
        document.querySelector('button[type="submit"]').click();
    ";
    await _webView.CoreWebView2.ExecuteScriptAsync(fillScript);

    // 等待结果,超时5秒
    if (await _readySignal.WaitAsync(TimeSpan.FromSeconds(5)))
    {
        if (_aiResultQueue.TryDequeue(out string category))
            return Results.Ok(new { category });
    }
    return Results.Ok(new { category = "未知" });
}

实测效果与性能对比

我们在内部测试库中选了200个常见软件,分别用我们的方案和“某龙虾付费API”做盲测。结果如下:

指标 本方案(Edge+AI) 龙虾付费API
单次响应耗时(平均) 4.2秒 1.8秒
分类准确率(人工复核) 91.5% 93.0%
1000次调用成本 0元 ~$45
是否需要联网 是(AI网页)
可定制Prompt ✅ 自由 ❌ 固定

结论很直白:我们牺牲了约2.4秒响应时间,但换来了零成本完全可控的识别逻辑。对于非实时批量分类场景(比如夜间定时任务),这个权衡非常划算。

生产环境必须解决的几个硬骨头

登录态与反爬

大部分免费AI页面要求登录。我们的方案是首次手动登录一次,然后持久化WebView2的User Data Folder(--user-data-dir参数),这样Cookie和LocalStorage都会保留。部署时只需在服务器上手动登录一次,后续重启均保持登录态。

var env = await CoreWebView2Environment.CreateAsync(
    userDataFolder: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WebView2Data")
);
await webView.EnsureCoreWebView2Async(env);

有个坑:如果AI页面有频繁的图形验证码或风控,这种方式可能触发“异常登录”警告。我们的对策是控制请求频率(设置最少30秒间隔),模拟人类操作间隔。

并发处理

WebView2实例本身不是线程安全的,且同时只能执行一个ExecuteScript。所以我们把HTTP接口设计为单线程队列,外部请求依次排队,内部用SemaphoreSlim(1,1)控制。并发量要求高的场景,可以启动多个WebView2实例(每个监听不同端口),用Nginx做负载均衡。

稳定性守护

AI页面偶尔会崩溃或弹窗(比如“网络错误”)。我们加了一个健康检查协程,每30秒用ExecuteScript执行一个无副作用JS(如return 1+1),如果超时或报错,则强制webView.Reload()并重新等待页面就绪。

结语:免费且有态度的分类工具

这套方案上线两个月,稳定处理了超过4万次分类请求,成本为0。唯一付出的代价是初期调试WebView2拦截时掉了几根头发,以及偶尔需要手动刷新登录态。

如果你也在为桌面软件分类成本发愁,别再当“小龙虾”了——与其花钱买有限次数的调用,不如利用浏览器这个“免费计算器”自己搭一个。重要的是,你拥有了完全自定义Prompt的能力,甚至能根据软件行为日志动态调整识别规则。

下一步,我们计划将识别结果缓存到本地SQLite,对重复文件直接命中缓存,把平均响应从4秒压到200ms以内。等新版本稳定了,我会再写一篇缓存设计踩坑记。

你的场景里有什么类似的“免费替代付费”的实践?欢迎在评论区碰撞想法——哪怕是用WebView2去薅别的羊毛,思路都是互通的。

分享到:

本文链接:https://www.biyeyuanma.cn/post/165.html

猜你喜欢

随机文章
热门标签
图片名称

服务热线

加我微信

加我微信