桌面自动化工程实践(一)项目整体架构|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 设计分辨率上标定的位置,运行时按窗口实际尺寸缩放
  │ 全部失败
  ▼
暂停 + 截图留证 + 人工确认(绝不盲点)
三个关键原则:
-
严格命中才行动,"沾边" 的候选只告警。 比如 OCR 读出一个似是而非的词,机器人不会赌一把,只会记录下来。
-
美术字交给模板。 游戏里大量标题是艺术字体,OCR 会稳定误读(实测把 "开始游戏" 提示条读成 "0 开 她 窮 观"),这种地方老老实实用像素模板。
-
所有元素统一在 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++)
{
  double t = (double)i / steps;
  double e = t \* t \* (3 - 2 \* t); // smoothstep:起步慢→中途快→收尾慢
  double x = (1-e)\*(1-e)\*start.X + 2\*(1-e)\*e\*cx + e\*e\*end.X;
  double y = (1-e)\*(1-e)\*start.Y + 2\*(1-e)\*e\*cy + e\*e\*end.Y;
  // ...途中逐点随机抖动;末点严格等于目标
}
// 随机概率"过冲":沿末段方向冲出一点,再收回(人甩鼠标的常见微调)
即:随机贝塞尔弧线(路径不直)+ smoothstep 缓动(速度不均)+ 逐点抖动 + 随机过冲再收回。
还有一个很多人不知道的知识点:最终落点使用 SendInput 的绝对坐标归一化(0–65535),整条路径不受系统 "提高指针精确度"(鼠标加速)的影响,落点可以保证像素级精确;而拟人化只作用在 "移动过程"。
项目里立了一条铁律:所有拟人参数必须保持随机范围(min/max 或概率),禁止收敛成固定值。 因为固定的 "拟人轨迹" 跑上一百次,本身就成了最明显的机器特征。
⚠️ 当然要清醒:拟人化只能降低机械感,
不等于免疫风控
。这也是项目坚持半自动、低频使用的原因。
6. 核心设计五:安全网 —— 默认 "不操作",而不是 "赌一把"
自动化系统最危险的时刻,是它 "以为自己看懂了" 的时候。这个项目在安全上的整体取向是 fail-open:不确定就停下来,把决定权交还给人。
具体有四道防线:
-
点击落点校验:点击后读回光标实际位置,偏差超过容差(默认 6px)→ 直接钉正一次 → 仍偏就放弃点击。点击永远只发生在校验通过的位置。
-
三路同源急停:GUI 急停按钮、Ctrl+C、暂停面板的 "中止",走的是同一个协作式标志位,任何路径触发都会释放所有仍按住的按键,不会留下一个 "一直按着 W 往前冲" 的机器人。
-
异常界面暂停:识别不出的界面,自动截图 + OCR 词表存证(
mark_unknown),然后暂停等待人工确认。 -
半自动默认:默认每个异常点都要人工放行;全自动模式需要显式开启。
再加上 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 状态可视化打磨
系列预告
这个项目我打算按模块持续写下去:
-
(本文)整体架构
-
截屏与窗口定位:GDI 抓屏、DPI 与窗口化坐标换算
-
OCR 实战:在 .NET 中调用 Windows.Media.Ocr 与词表匹配
-
三层识别策略:OCR + OpenCvSharp 模板匹配 + 坐标兜底
-
拟人鼠标:贝塞尔弧线、缓动、过冲与抖动的实现
-
JSON 状态机:把流程写成数据,以及中断恢复设计
-
安全设计:急停、落点校验与 fail-open
-
WinForms 控制面板:托盘、日志窗与穿透式悬浮面板
-
工程化:xUnit 测试体系与诊断型 CLI 设计
-
复盘:Python 原型迁移到 C#/.NET 的得与失
以上所有内容均为个人技术学习记录。再次提醒:屏幕截图 + 键鼠模拟的方案虽然 "看得见、摸得着",但任何自动化都存在违反用户协议的风险,请敬畏规则、低频使用、风险自担。
如果文中有你感兴趣的部分,欢迎评论区交流,我们下篇见。

浙公网安备 33010602011771号