桌面自动化工程实践(一)项目整体架构|C# .NET OpenCvSharp

我用 C#/.NET 做了一个 "看得见的" 桌面自动化助手(一):整体架构

合规与安全声明(请先阅读)
本文介绍的是一个

桌面自动化技术的个人学习项目

:所有输入都来自

屏幕截图

,所有输出都走

SendInput 键鼠模拟

—— 不读内存、不注入进程、不碰网络封包,是一个 "屏幕看得见、操作摸得着" 的桌面机器人。
项目明确

不包含任何战斗、瞄准、击杀相关的对抗性功能

,只处理界面导航这类重复性流程。任何对游戏 / 软件的自动化都可能违反其用户协议并导致账号封禁,本文不提供开箱即用的成品,也不鼓励任何人高频使用。技术内容仅供学习交流,风险与责任由使用者自行承担。

0. 这是什么

事情的起因很简单:某 FPS 游戏在正式开局前,有一套又长又固定的大厅流程—— 选模式、选地图、确认配装、等匹配、选角色…… 每局都要点,点了几个月之后,我的手指和耐心总有一个先崩溃。

于是我决定用代码 "雇" 一个桌面机器人,专门替我走这些纯界面、无对抗、高重复的流程。

它和通常意义上的 "外挂" 有本质区别:

传统作弊工具 本项目
信息来源 读内存 / 封包,获得 "上帝视角" 只看屏幕截图,和人眼看到的一模一样
操作方式 直接改游戏状态 SendInput 模拟真人键鼠
能力边界 瞄准、透视、击杀 只做界面导航,不碰战斗
识别失败时 照样执行 暂停,等人来确认

一句话概括:它不是一个 "更强的玩家",而是一双 "会看屏幕、会动鼠标、但随时可以被你打断的手"。

目前项目版本 v0.5.0,技术栈是 C# /.NET 10 + WinForms + OpenCvSharp + Windows 自带 OCR,进场链路已经在真机上端到端跑通。

📷

配图位 1:建议放一张 GUI 控制面板的截图,或 "一键开始 → 链路自动执行" 的 GIF。

1. 整体架构

项目按 "入口 → 命令层 → 核心服务层 → 数据层" 分层:

Program.cs(入口:定位根目录 + 初始化日志/拟人参数 + 命令路由;无参数 → GUI)

   │

   ├── Cli/Commands/\*.cs   命令层:每个 CLI 动词一个命令(诊断后门)

   ├── Gui/\*.cs            图形控制面板(主窗口 + 托盘 + 日志窗)

   │

   ├── Capture/   截屏层:窗口定位 + GDI 抓屏 + 坐标换算

   ├── Vision/    视觉层:OCR + 模板匹配 + 界面识别 + 帧差

   ├── Input/     输入层:SendInput 键鼠模拟 + 拟人路径规划

   ├── Overlay/   可观测层:滚动日志 + 游戏画面悬浮状态面板

   │

   └── Data/      数据层:JSON 模型与加载器

          │

          ├── data/\*.json        运行时数据(界面表/元素表/标定参数…)

          ├── workflows/\*.json   流程定义(状态机)

          └── assets/templates/  模板匹配用的小图

依赖非常克制:只有 OpenCvSharp4(图像处理)和 Vanara.PInvoke.Gdi32/User32(Win32 API 的薄封装),其余全部用 .NET 自带能力(Windows.Media.Ocr、GDI、WinForms)。

下面介绍支撑这个架构的五个核心设计决策,它们也是这个系列后续文章的目录。

2. 核心设计一:数据驱动 —— 加界面不用改代码

这是整个项目最重要的思想:流程、界面、元素、坐标全部是 JSON 数据,代码只负责 "解释执行"。

以 "界面识别表" 为例,机器人认识哪些界面,不写在 C# 的 if-else 里,而是写在 screens.json:

"plaza\_ready": {

  "note": "备战界面-常态(右下有 配装|出发)",

  "markers": \[

    { "type": "ocr", "keywords": \["配装", "出发"], "require\_all": true,

      "region\_name": "zone\_bottom\_right" },

    { "type": "template", "template": "assets/templates/depart\_button.png",

      "threshold": 0.85 }

  ],

  "actions": \["点击 出发 → 匹配"]

}

它表达的含义是:当屏幕右下角同时(require_all)出现 "配装" 和 "出发" 两个词时,当前就是备战界面。

这里有个真实踩过的坑值得一说:配装界面里也有 "配装" 两个字("确认配装"),如果只拿 "配装" 一个词当特征,机器人就会把两个界面搞混 —— 这就是 require_all 防串台机制存在的原因。

数据驱动带来的直接收益是:

  • 加一个新界面 = 加一段 JSON;

  • 加一个新按钮 = 元素表里加一条;

  • 换一种识别方式(比如 OCR 换成模板)= 改 strategy 字段,流程代码一行不动。

目前表中已经用真机标定了 12 个界面,从特勤处、模式选择一直到结算画面。

3. 核心设计二:三层识别策略 ——OCR 为主,逐级兜底

让机器人 "看懂屏幕",我没有一上来就上 YOLO 这类重型方案,而是设计了一个三层兜底的识别策略,按可靠性从高到低:

第一层:OCR 严格命中(主力)

   识别屏幕文字,与词表做"严格包含"匹配 → 命中才行动

        │ 未命中

        ▼

第二层:模板匹配(兜底)

   OpenCvSharp 拿预先裁好的小图(图标/按钮)在画面上做像素级匹配

        │ 仍未命中

        ▼

第三层:固定坐标(最后手段)

   在 1920×1080 设计分辨率上标定的位置,运行时按窗口实际尺寸缩放

        │ 全部失败

        ▼

暂停 + 截图留证 + 人工确认(绝不盲点)

三个关键原则:

  1. 严格命中才行动,"沾边" 的候选只告警。 比如 OCR 读出一个似是而非的词,机器人不会赌一把,只会记录下来。

  2. 美术字交给模板。 游戏里大量标题是艺术字体,OCR 会稳定误读(实测把 "开始游戏" 提示条读成 "0 开 她 窮 观"),这种地方老老实实用像素模板。

  3. 所有元素统一在 1920×1080 设计分辨率上标定,运行时把抓取到的画面归一化到基准尺寸再识别,窗口大小变化不影响结果。

这个策略还有个意外的性能优化故事:初版链路每轮要对同一帧做 16 次 OCR,后来加了同帧词表缓存(同一帧只 OCR 一次,后续都查缓存),降到约 7 次;再配合流程结构优化,整条链路从 112 步压缩到约 64 步。

🔜 OCR 与模板匹配的完整实现细节,将在本系列第三、四篇展开。

4. 核心设计三:流程即状态机 —— 任意界面都能启动

"进房" 这件事看起来是一条直线,但真实世界充满意外:中途弹出广告、仓库升级完成提示、上一局的结算画面没关、网络卡顿停在某个界面……

如果把流程写死成 "第一步点 A、第二步点 B",任何意外都会让它卡死。所以流程被定义成一个带界面分流和中断恢复能力的状态机(workflows/enter_match.json):

{ "id": "detect\_start", "op": "switch\_screen", "max\_loops": 3,

  "default": "unknown\_tab",

  "branches": {

    "plaza\_first": "prep",

    "plaza\_ready": "do\_loadout",

    "space\_continue": "skip\_overlay",

    "mode\_select": "pick\_mode",

    "map\_select": "start\_action",

    "loadout": "check\_loadout",

    "matching": "wait\_match",

    "char\_select": "pick\_operator"

  }

}

主循环只做一件事:截一张图、识别当前在哪个界面,然后查表跳到对应的处理分支;分支处理完再回到主循环。

这带来了两个很舒服的特性:

  • 任意界面启动:不管机器人是在模式选择界面被启动,还是在匹配中途被打断后重启,它都能 "看一眼屏幕" 就知道自己在哪、该干什么,自动接续。

  • 弹层自动消化:广告、结算、任务领取这类 "空格继续" 弹层,在状态机里是统一信号 —— 识别到就自动按空格关掉,主线流程再也不会被非主线弹窗卡死。

此外,每次点击之后都有 expect_screen 轮询验证:点了 "开始行动",就必须在超时时间内看到下一个界面;没看到就判定点击失败,截图留证、软跳转、请人工确认,而不是傻乎乎地继续往下点。

5. 核心设计四:拟人输入 —— 给鼠标一条 "不直" 的路径

机器人操作鼠标最容易暴露机械感的地方,就是瞬间移动 + 笔直路径 + 匀速运动。真人的鼠标轨迹完全不是这样。

项目的 MousePathPlanner 是一个纯逻辑、可单测的路径规划器,给每次移动叠加了四层拟人要素:

// 贝塞尔控制点 = 中点 + 法向随机偏移(弧线方向/幅度每次随机,不重样)

double mx = (start.X + end.X) / 2.0, my = (start.Y + end.Y) / 2.0;

double nx = -dy / len, ny = dx / len;

double curve = (rng.NextDouble() \* 2 - 1) \* Math.Min(cfg.CurvatureMaxPx, dist \* 0.35);

double cx = mx + nx \* curve, cy = my + ny \* curve;

for (int i = 1; i <= steps; i++)

{

&#x20;   double t = (double)i / steps;

&#x20;   double e = t \* t \* (3 - 2 \* t); // smoothstep:起步慢→中途快→收尾慢

&#x20;   double x = (1-e)\*(1-e)\*start.X + 2\*(1-e)\*e\*cx + e\*e\*end.X;

&#x20;   double y = (1-e)\*(1-e)\*start.Y + 2\*(1-e)\*e\*cy + e\*e\*end.Y;

&#x20;   // ...途中逐点随机抖动;末点严格等于目标

}

// 随机概率"过冲":沿末段方向冲出一点,再收回(人甩鼠标的常见微调)

即:随机贝塞尔弧线(路径不直)+ smoothstep 缓动(速度不均)+ 逐点抖动 + 随机过冲再收回。

还有一个很多人不知道的知识点:最终落点使用 SendInput 的绝对坐标归一化(0–65535),整条路径不受系统 "提高指针精确度"(鼠标加速)的影响,落点可以保证像素级精确;而拟人化只作用在 "移动过程"。

项目里立了一条铁律:所有拟人参数必须保持随机范围(min/max 或概率),禁止收敛成固定值。 因为固定的 "拟人轨迹" 跑上一百次,本身就成了最明显的机器特征。

⚠️ 当然要清醒:拟人化只能降低机械感,

不等于免疫风控

。这也是项目坚持半自动、低频使用的原因。

6. 核心设计五:安全网 —— 默认 "不操作",而不是 "赌一把"

自动化系统最危险的时刻,是它 "以为自己看懂了" 的时候。这个项目在安全上的整体取向是 fail-open:不确定就停下来,把决定权交还给人。

具体有四道防线:

  1. 点击落点校验:点击后读回光标实际位置,偏差超过容差(默认 6px)→ 直接钉正一次 → 仍偏就放弃点击。点击永远只发生在校验通过的位置。

  2. 三路同源急停:GUI 急停按钮、Ctrl+C、暂停面板的 "中止",走的是同一个协作式标志位,任何路径触发都会释放所有仍按住的按键,不会留下一个 "一直按着 W 往前冲" 的机器人。

  3. 异常界面暂停:识别不出的界面,自动截图 + OCR 词表存证(mark_unknown),然后暂停等待人工确认。

  4. 半自动默认:默认每个异常点都要人工放行;全自动模式需要显式开启。

再加上 DPI 感知(Per-Monitor V2)和窗口客户区坐标换算,保证窗口化、缩放比例变化时点击位置依然准确。

7. 工程化:93 个测试 + 诊断型 CLI + GUI

一个能长期迭代的自动化项目,不能靠 "肉眼玄学" 调试。项目在工程化上做了三件事:

(1)93 个 xUnit 单元测试,覆盖视觉(帧归一化、帧差、模板自匹配、界面识别)、输入(键名映射、路径规划回归、坐标换算)和数据层(JSON schema 冻结)。尤其路径规划器是纯逻辑,"终点精确、短距单步、过冲收回、步数随距离增长" 都有断言兜底。

(2)一套诊断型 CLI,把每个感知环节拆成可单独验证的命令:

dotnet run -- capture              # 截屏冒烟

dotnet run -- ocr <截图> \[关键词]   # OCR 诊断:词表是否命中

dotnet run -- tplprobe <截图> <模板> # 模板匹配诊断:置信度/位置

dotnet run -- mousetest \<x> \<y>     # 拟人轨迹肉眼验收(不点击)

dotnet run -- annotate <元素>       # 在截图上画出识别标记

任何一个环节出问题,都能单独定位,而不是在整条链路里猜。

(3)BetterGI 式 GUI 控制面板(WinForms):工作流选择、一键开始 / 急停、半自动模式下的 "继续 / 中止" 面板、实时日志窗、系统托盘(收托盘、链路结束气泡通知),以及游戏画面上一个可拖动、鼠标穿透、不抢焦点的悬浮状态面板。GUI 和 CLI 共用全部核心服务,CLI 保留为诊断后门。

📷

配图位 2:建议放悬浮状态面板(叠在游戏画面上的半透明小窗)的截图。

8. 现状与后续计划

  • ✅ 进场链路端到端真机验证通过(选模式 → 选图 → 配装 → 匹配 → 选角色)

  • ✅ 12 个界面真机标定;GUI 主窗口、托盘、暂停面板落地

  • 🚧 干员选择、自动配装已接通链路,待更多真机实测

  • 📋 后续:跑图导航第一跳、撤离后处理、全局急停热键、GUI 状态可视化打磨

系列预告

这个项目我打算按模块持续写下去:

  1. (本文)整体架构

  2. 截屏与窗口定位:GDI 抓屏、DPI 与窗口化坐标换算

  3. OCR 实战:在 .NET 中调用 Windows.Media.Ocr 与词表匹配

  4. 三层识别策略:OCR + OpenCvSharp 模板匹配 + 坐标兜底

  5. 拟人鼠标:贝塞尔弧线、缓动、过冲与抖动的实现

  6. JSON 状态机:把流程写成数据,以及中断恢复设计

  7. 安全设计:急停、落点校验与 fail-open

  8. WinForms 控制面板:托盘、日志窗与穿透式悬浮面板

  9. 工程化:xUnit 测试体系与诊断型 CLI 设计

  10. 复盘:Python 原型迁移到 C#/.NET 的得与失


以上所有内容均为个人技术学习记录。再次提醒:屏幕截图 + 键鼠模拟的方案虽然 "看得见、摸得着",但任何自动化都存在违反用户协议的风险,请敬畏规则、低频使用、风险自担。

如果文中有你感兴趣的部分,欢迎评论区交流,我们下篇见。

posted @ 2026-09-26 03:38  OmniDex  阅读(13)  评论(0)    收藏  举报