快速开始
$W = ".\scripts\win-harness.exe"
& $W windows -Filter "Notepad" # 1. 找窗口
& $W tree -Window "Notepad" -Depth 2 # 2. 看元素树
& $W click -Window "Notepad" -AutomationId "..." # 3. 精确定位并点击
& $W read -Window "Notepad" -Type Document # 4. 读值验证
输出统一为 UTF-8 JSON:{"ok":true,"data":...} 或 {"ok":false,"error":"..."}。中文转义为 \uXXXX(JSON 解析后无损)。
命令速查
| 命令 | 用途 |
|---|---|
| windows [-Filter 标题] [-Pid n] | 枚举顶层窗口(hwnd/title/pid/rect) |
| tree -Window <定位> [-Depth 3] [-Filter 文本] [-Backend uia\|msaa\|auto] | 元素树(扁平 JSON) |
| find -Window <定位> [-Name <>] [-Type <>] [-AutomationId <>] [-Exact] | 查找元素(最多 20 个,含坐标/可点击点/值) |
| click -Window <定位> (-AutomationId <>\|-Name <>\|-X n -Y n) [-Index 0] [-Double] [-Right] [-Scroll] [-Via auto\|click\|select\|toggle\|invoke\|expand] [-ForceClick] | 点击(默认按控件语义选通道,失败回退鼠标) |
| type -Window <定位> (-Text <>\|-TextB64 <>) [-AutoFocus] [-Focus <>] [-NoAutoFocus] | 输入文本(剪贴板法,支持中文)|
| setvalue -Window <定位> [-Name <>] [-Type Edit] (-Text <>) | 直接设值(UIA 原生,最可靠) |
| key -Window <定位> -Keys "ENTER" [-AutoFocus] [-Focus <>] | 按键(如 CTRL+S / ALT+F4) |
| screenshot [-Window <定位>] [-Monitor n] [-Out <路径>] | 截图(默认整个虚拟桌面) |
| wait -Window <定位> -Name <> [-Timeout 10] [-Gone] | 等待元素出现/消失 |
| focus -Window <定位> | 激活窗口 |
| setwindow -Window <定位> [-X -Y -Width -Height] [-State ...] [-MoveToMonitor n] [-Center] | 窗口状态/位置/尺寸/跨屏/居中 |
| launch -Path <exe> [-Args <>] [-Workdir <>] [-WaitWindow <>] [-Timeout 15] [-Max] | 启动程序(可等窗口) |
| kill (-Window \| -Pid \| -Path) [-Force] | 结束进程 |
| hover [-Window <定位>] -X -Y | 移动光标 + 报告光标下元素 |
| drag [-Window <定位>] (-FromX -FromY -ToX -ToY \| -From x,y -To y,y) [-Duration 500] [-Button left\|right] | 拖拽(分步插值,非瞬移) |
| scroll [-Window <定位>] -X -Y [-Delta -120] [-Times 1] | 真滚轮(非 UIA 滚动) |
| clipboard [-Get] \| [-Set -Text <>] | 读/写剪贴板 |
| read -Window <定位> [-Name <>\|-AutomationId <>] [-Index n] | 读元素文本(Value→Text→Name) |
| getprop -Window <定位> [-Name <>\|-AutomationId <>] [-Index n] | 读属性与支持的 Pattern |
| pixel -X -Y | 取屏幕像素颜色 |
| do -Ops <子命令1 ; 子命令2 ; ...> | 批处理,一次进程执行多步 |
| b64 -Text <> | 文本转 base64(备用) |
窗口定位:标题(模糊包含)|pid:1234|hwnd:12345
核心工作流
- 找窗口 →
windows -Filter "关键词"(拿 hwnd / title / pid) - 看结构 →
tree -Window <定位> -Depth 3(了解元素、名称、automationId) - 精确定位 → 优先用
-AutomationId(最稳定),其次-Name -Exact,最后-Name+-Index - 操作 →
click/setvalue/type/key - 验证 →
read/find/screenshot/pixel
元素定位策略(四级降级)
-AutomationId(首选):稳定,不受语言/版本影响。用tree/find先发现它-Name -Exact:名称精确匹配-Name+-Type+-Class:组合缩小范围-X -Y坐标:最后手段。配合hover先探测该点是什么元素
⚠ 歧义即报错:匹配到多个元素且未显式给
-Index时,命令会报错并列出候选,绝不静默取第一个。 同理,同标题多窗口需-WindowIndex <n>,否则报错列出候选。
点击通道(-Via)—— 语义远比"能点就行"重要
click 的默认通道 auto 按控件类型对齐鼠标点击的真实语义:
| 控件类型 | 鼠标点击的含义 | 使用的通道 |
|---|---|---|
| ListItem / TreeItem / TabItem / DataItem | 选中 | SelectionItemPattern.Select() |
| CheckBox / ToggleButton | 切换 | TogglePattern.Toggle() |
| Button / Hyperlink / MenuItem | 激活默认动作 | InvokePattern.Invoke() |
| 其他 | — | 真实鼠标点击 |
⚠
Invoke被刻意排在最后。对列表项而言InvokePattern.Invoke()的语义是「执行默认动作」 (≈ 双击 → 打开),而不是「点击 → 选中」。若把它排在前面,一次本意是"选中文件"的click会把文件打开 —— 破坏性副作用,而响应里只显示via=invoke,用户无从察觉。
需要显式激活/打开时,写明 -Via invoke:
& $W click -Window $hw -Type ListItem -NameB64 <b64> -Exact -Via select # 只选中
& $W click -Window $hw -Type ListItem -NameB64 <b64> -Exact -Via invoke # 打开
& $W click -Window $hw -AutomationId btnOk -Via click # 强制真实鼠标点击
-ForceClick 等价于 -Via click(保留兼容)。响应中的 via 字段取值:
select / toggle / expand / invoke / click(显式要求鼠标)/ click-fallback(auto 下无 Pattern 可用)/ point(坐标模式)。
键盘焦点(-AutoFocus)
type 与 key 默认不干预键盘焦点 —— 直接向当前前台窗口发送输入。
需要置焦点时显式加 -AutoFocus(或用 -Focus <名称> 指定目标)。其实现走 UIA SetFocus():
⚠ 历史实现是「找到编辑区 → 合成点击一下」来抢焦点。这一下点击除置焦点外,还会 清空列表等其他控件的选中状态。现已改为 UIA 原生
SetFocus()(不产生鼠标事件)。SetFocus失败时不会退回点击,而是明确报错 —— 静默的副作用比报错代价更高。
响应中会附带 focus 对象(via=uia-setfocus、element、hasKeyboardFocus,未真正获得焦点时给出 warning)。
多候选时同样遵守歧义规则(报错列候选),需用 -FocusIndex <n> 或 -Focus <名称> 收敛。
坐标系与可靠性(重要)
- 进程启动时自动声明 DPI 感知(优先 per-monitor-v2),统一 UIA 与 GDI 的坐标空间
- 负数坐标完全支持。左/上副屏位于负坐标区域(如
-X -1281 -Y 277),可直接使用。 解析器能区分「选项名」与「负数值」——-1281会作为-X的值,不会被当成开关。- 若某个值本身以
-开头且不是数字(如-Args -Dfoo),用等号形式:-Args=-Dfoo
- 若某个值本身以
- 坐标无效会直接报「坐标无效」,不会退化成「匹配到 N 个元素(无过滤条件)」的元素歧义错误
screenshot返回实际捕获矩形{x,y,w,h}—— 用它把图片坐标换算回屏幕坐标- 若 UIA 与 GetWindowRect 的窗口矩形读数不一致,
screenshot会附带warning字段 hover/drag/scroll是纯坐标操作,作用于屏幕最顶层窗口。被遮挡时请传-Window <定位>先把目标置前台(否则会操作到错误的窗口上且无报错)setwindow的响应字段(不再自相矛盾):applied—— 已执行的动作名列表(如["rect","moveToMonitor"]),不含几何结果requested_rect—— 请求的几何(仅当请求了几何时出现)rect—— 权威结果,执行后实际几何,一切以它为准geometry_note—— 请求与实得不一致时的解释(应用最小尺寸 / 跨 DPI 换算)monitor_index—— 窗口当前所在显示器序号- ⚠ 窗口移动或改尺寸后,之前记录的屏幕坐标全部失效,需重新读
rect或重新截图
MSAA 回退通道
Chromium 系应用(Edge / Electron / WebView2 / Tauri)的 UIA 树极浅,因为 Chromium 的无障碍树是惰性启用的。
-Backend auto(默认)在 UIA 元素数 < 15 时自动改走 MSAA 通道,取元素更多者,并标注 "backend":"msaa-fallback"。
实测效果(depth=4):Edge 7 → 17,VS Code 6 → 9。 但要注意:MSAA 只多挖出浏览器外壳(窗口按钮、标签栏),不触及网页正文。网页内部操作请走 CDP。
支持程度分级(实测)
| 类型 | 等级 | 说明 |
|---|---|---|
| Win32 经典控件(记事本旧版/资源管理器/对话框) | ★★★★★ | 纯元素驱动,AutomationId 稳定,零截图 |
| WinForms / WPF | ★★★★★ | 同上;WPF 虚拟化列表需 -Scroll |
| UWP / WinUI(计算器/设置) | ★★★★☆ | 元素暴露好;但组合键可能不响应,改用 setvalue |
| Qt(如 Navicat) | ★★★☆☆ | 部分控件可用,QML 应用暴露差 |
| 自绘 UI(微信 4.0、CAD、国产软件) | ★★☆☆☆ | UIA 几乎为空(微信实测仅 3 个元素),需截图 + pixel + 坐标 + hover 探测 |
| Chromium 系(Edge/Electron/WebView2) | ★☆☆☆☆ | UIA 树 6-13 个元素,MSAA 仅补外壳 → 请用 CDP |
工具路由(三堵墙,别硬啃)
| 场景 | 用什么 | 为什么 | |---|---|---| | 浏览器页面内操作(登录态、JS 渲染、DOM) | browser-harness(CDP) | UIA/MSAA 都摸不到网页正文 | | Office 文档正文(Word/Excel 内容) | COM / VBA | UIA 只能摸到 Ribbon 和对话框,正文需要 Office 对象模型 | | DirectX 全屏游戏 | 不支持 | 独占全屏下 GDI 截图为黑屏,需 DXGI Desktop Duplication;且 SendInput 会被 raw input 忽略 | | 桌面窗口/控件/对话框/启动程序 | win-harness | 本位面 |
已知局限与对策
| 场景 | 现象 | 对策 |
|---|---|---|
| WinUI 应用(Win11 记事本)组合键 | CTRL+A/CTRL+C/CTRL+V 无效 | 用 setvalue 直接设值;read 直接读文档 |
| 组合键在部分应用失效 | 无反应 | 改用分步单键,或 setvalue |
| 目标被其他窗口遮挡 | 纯坐标操作打错窗口且无报错 | hover/drag/scroll 传 -Window 先置前台 |
| 剪贴板被其他程序占用 | type 报错 | 内置 5 次重试;仍失败稍后重试 |
| 最小化窗口 | UIA 树不可用 | focus 自动还原(SW_RESTORE)后再操作 |
| 元素名称含空格/特殊字符 | -Name 不匹配 | 用 tree 查看精确名称;改用 -AutomationId |
| 多显示器 + 缩放 | 截图截错区域(历史故障) | 已修:显式 DPI 声明 + 虚拟桌面截图;留意 warning 字段 |
已修复的高危问题(避免回归)
| 问题 | 曾经的后果 | 现在的行为 |
|---|---|---|
| 列表项点击走 Invoke | 本意"选中文件"变成"打开文件"(破坏性且无提示) | Select 排前、Invoke 排最后;via 字段如实汇报通道 |
| autofocus 用合成点击 | type/key 抢焦点时清空列表选中状态 | 改用 UIA SetFocus();type/key 默认不再干预焦点 |
| launch -WaitWindow 取首个同标题窗口 | 已有同名窗口时返回错误的 hwnd/pid | 优先等本次启动进程自己的窗口;退回时附 warning |
| kill -Path 取首个同名进程 | 可能杀掉错误的进程 | 多进程时报错列候选,要求 -Pid |
| 元素/窗口多匹配静默取首个 | 操作到错误元素/窗口且无报错 | 一律报错列候选,要求 -Index / -WindowIndex |
| 负数坐标解析成"无值开关" | 左/上副屏上 click/hover/drag/scroll/pixel 全部不可用 | 解析器区分选项与负数;click 不再用 >= 0 判断是否走坐标 |
| 坐标丢失伪装成元素歧义 | 报「匹配到 N 个元素(无过滤条件)」,排障方向被带偏 | 坐标无效/缺失时直接报坐标错误;无任何条件时明确要求给坐标或元素条件 |
| setwindow 的 applied 与 rect 自相矛盾 | 同一响应两个矛盾矩形(跨 DPI 移动后 1.5x 差异),易误读 | applied 只列动作;新增 requested_rect + geometry_note + monitor_index |
排障
- 未找到窗口:
windows不带过滤列出全部;注意标题关键词(部分匹配即可) - 窗口歧义报错:按提示用
-WindowIndex n,或直接用hwnd: - 元素歧义报错:按候选列表用
-AutomationId/-Exact/-Index收敛 - 点击无效:
getprop看isOffscreen/isEnabled;屏外元素加-Scroll;或-ForceClick强制走鼠标 - 重新编译:修改
scripts/src/*.cs后运行scripts/build.ps1(用 .NET Framework 自带 csc.exe,无需安装工具链)
实现说明(维护参考)
scripts/win-harness.exe— 编译产物(单文件,运行时不依赖 PowerShell)scripts/src/Native.cs— P/Invoke:窗口/鼠标键盘/DPl/剪贴板/MSAAscripts/src/Uia.cs— UIA 封装 +FindSpec/ResolveOne(歧义断言)+ MSAA 遍历scripts/src/InputSim.cs— SendInput 鼠标键盘/剪贴板/截图/显示器几何scripts/src/WinHarness.cs— CLI 主程序 + 命令分发 + 批处理 + MiniJsonscripts/build.ps1— 编译脚本(UTF-8 BOM 转换 + 退出码检查)scripts/win-harness.exe.bak— 升级前基线备份- 为何用 exe 而非 PowerShell 脚本:脚本内嵌 C# P/Invoke 会触发 AMSI 启发式误拦(ScriptContainedMaliciousContent);编译为 exe 后二进制不经脚本扫描,稳定可靠
- 为何 JSON 输出 \uXXXX 转义:规避控制台/管道编码链(GBK/UTF-8 混杂)造成的中文损坏,任何链路下无损
Scan to join WeChat group