dsh-appstate-F1x
作者 F1x · Telegram @f8xx9
给 agent 的"看软件状态"工具集。不截桌面,不抢焦点,不动鼠标。
五个工具:前四个只读,不调用 SetForegroundWindow / SetFocus / SendInput;第五个负责启动程序时同样不抢前台。你在用电脑的时候,agent 也能查软件状态、开新窗口而不打断你。
appstate_list 有哪些窗口
appstate_text 窗口里的文字和控件状态
appstate_shot 只抓这一个窗口的像素
appstate_wait 等状态变成某个样子
appstate_launch 启动程序,窗口不在最上层出现
为什么不是截屏
全屏截图的三个硬伤:
- 抓的是整个桌面——你在干嘛都进图了,隐私和体积双重浪费。
- 窗口被盖住就抓错——截屏拿到的是"屏幕现在显示什么",不是"这个软件现在是什么状态"。
- 为了抓全要抢焦点——
SetForegroundWindow把窗口顶到前面,你的输入被打断、光标被挪走。
这套工具把"看状态"拆成三条只读通道,各自解决一种场景,最后才落到像素。
安装
本机已完成,重装步骤:
# 1. 包装到 profile(junction,不是复制,避免两份代码漂移)
cmd //c "mklink /J D:\\dsh\\.dsh\\profiles\\desktop\\node_modules\\@local\\dsh-appstate D:\\nx8\\dsh-appstate"
# 2. 登记(package.json)
# dependencies: "@local/dsh-appstate": "link:D:/nx8/dsh-appstate"
# dsh.profile.bundles: 追加 "@local/dsh-appstate"
# 3. 重载:设置里开关一次插件,或 touch profile 的 cordis.patch.yml
注意:ln -s 在 Windows 上会退化成分目录复制,用 mklink /J。
工具
appstate_list
列顶层窗口。查"目标在不在、pid 是多少、是不是最小化"。
| 参数 | 类型 | 说明 |
|---|---|---|
name |
string | 标题正则过滤 |
includeHidden |
bool | 连隐藏/托盘/零尺寸窗口一起列,默认 false |
返回 { count, windows[] },每项:
{ "hwnd": 525190, "hex": "0x80386", "pid": 18836, "process": "Mojin-nolock",
"class": "wailsWindow", "title": "摸金校尉", "width": 1280, "height": 840,
"minimized": false, "foreground": false }
实测输出:
0x301E0 pid=884 AyuGram 1936x1056 FG hermes1 @ F1x (5640312)
0x1B004A6 pid=18048 Mojin-nolock 1280x 840 摸金校尉
0x1406BE pid=12700 explorer 160x 28 MIN D:\Mojin-...\Mojin-0.1.0-beta.19
appstate_text
读窗口的 UI 树文本。这是默认该用的工具:窗口标题、输入框内容、列表项、聊天记录、构建日志、按钮勾选态,都在这里。
| 参数 | 类型 | 说明 |
|---|---|---|
target |
string(必需) | pid / 0x句柄 / 标题正则 |
maxChars |
int | 返回字符上限,默认 6000 |
maxNodes |
int | 遍历控件数上限,默认 1500 |
返回 { window, chars, text }。text 每行形如:
[Document] 页面标题 @0,0 1920x1040
[TextField] 搜索 | value=deepseek
[CheckBox] 自动更新 | toggle=On
[Button] 保存 | disabled
实测:
- Firefox 整页正文 ≈ 1.9s(1773 个节点,CacheRequest 批量取属性;逐节点读要 6s+)
- DSH(Electron)窗口 7948 字符,第二次调用即满(Chromium 的无障碍树是懒加载的)
- 摸金校尉(Wails/WebView2)只有 93 字符——只有外壳,DOM 没进无障碍树
appstate_shot
抓一个窗口的像素存 PNG。被别的窗口盖住、不是前台,照样抓得到。
| 参数 | 类型 | 说明 |
|---|---|---|
target |
string(必需) | pid / 0x句柄 / 标题正则 |
out |
string | 输出路径,默认 <outDir>/shot-<ts>.png |
stable |
bool | 连抓到相邻两帧像素一致才采信,避免动画/滚动的撕裂帧 |
clientOnly |
bool | 只抓客户区,去掉标题栏和边框 |
返回 { path, width, height, bytes, blank, stable?, window?, pid? }。
appstate_shot target=6200 stable=true
# saved D:\nx8\appstate\out\shot-1794001670544.png (1936x1056, 183552 bytes)
实测:Firefox 被最大化的 DSH 完全盖住时抓取 → 1936×1056、183KB、图里没有 DSH 一个像素。
blank: true 表示该窗口返回不出像素(纯 GPU 合成)。不要重试,改读该软件自己的接口/日志/状态文件。
minimized: true 表示抓的是最小化窗口——Windows 仍保留一个 160×28 的任务栏缩略矩形,图里没有内容。工具会显式警告,看到就不要把它当成"窗口状态"。
CLI 的 -Json 输出是 PowerShell 原始字段名(Hwnd/Pid/Width…),插件在 normalizeWindow 里统一转成 camelCase;调用工具时看到的是 camelCase。
appstate_wait
轮询 UI 文本直到正则命中。替代"反复截图看变了没有"。
| 参数 | 类型 | 说明 |
|---|---|---|
target |
string(必需) | 同上 |
pattern |
string(必需) | 要等的正则 |
timeoutSec |
number | 超时秒数,默认 60 |
intervalSec |
number | 轮询间隔,默认 2 |
返回 { matched, elapsedSec, lines[], window }。
matched after 3.6s:
移植 dsh-F1x 到 Hermes
[TreeItem] 移植 dsh-F1x 到 Hermes 2分钟 @8,444 264x32
appstate_launch
启动程序,但不让新窗口抢前台。默认把窗口压到 z-order 最底层。
| 参数 | 类型 | 说明 |
|---|---|---|
command |
string(必需) | 完整命令行,如 notepad.exe |
workDir |
string | 工作目录,默认当前目录 |
minimized |
bool | 最小化启动(SW_SHOWMINNOACTIVE)而不是"显示但不激活" |
waitWindowMs |
int | 等窗口出现以便压底,默认 4000 |
noBehind |
bool | 只保证不激活,不动 z-order |
appstate_launch command="notepad.exe"
# launched pid=6040 (notepad) window 0x160AA2 "无标题 - 记事本" 1424x802, pushed behind without focus
启动分四步,目的是让窗口从来没有以可见状态出现在顶层:
CreateProcessW+STARTF_USESHOWWINDOW+SW_HIDE(0)—— 窗口创建出来就是隐藏的- 轮询(60ms)找到该进程的新窗口
SetWindowPos(HWND_BOTTOM, SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE)—— 先在底层就位ShowWindow(SW_SHOWNOACTIVATE)—— 就位之后再显示
早期版本用 SW_SHOWNOACTIVATE 直接启动,窗口会先出现在顶层、随后才被压下去,中间那一帧就是看得见的"弹一下"。
现在改成"隐藏创建 → 底层就位 → 显示",记事本这类尊重 wShowWindow 的程序完全不会露脸:
{ "behind": true, "visibleAtDiscovery": false, "foreground": false }
visibleAtDiscovery: true 表示窗口在被找到之前已经自己显示过了(UWP / ApplicationFrameHost 是典型),这类窗口仍可能闪一帧——宿主框架自己调用了显示,启动参数管不到。
实测:记事本启动后落在 z-order 最后一行(EnumWindows 顺序即从上到下),前台始终是原来的 DSH 窗口。
替代什么:start、cmd /c start、Start-Process——这些都会激活新窗口并抢走你的输入焦点。
命令行版本
同一套 PowerShell 脚本的 bash 包装,不进 DSH 也能用:
/d/nx8/appstate/appstate list
/d/nx8/appstate/appstate text 6200 -MaxChars 4000
/d/nx8/appstate/appstate ui 6200 -Json # 结构化节点
/d/nx8/appstate/appstate shot 6200 -Out out/x.png -Stable
/d/nx8/appstate/appstate state 6200 -Shot # 文本+图一步
/d/nx8/appstate/appstate watch 4620 -Until "构建完成"
/d/nx8/appstate/appstate launch "notepad.exe" -Json # 启动但不抢前台
/d/nx8/appstate/appstate demote 4620 # 把已有窗口压到底层,不激活
appstate 脚本指向本包的 scripts/,与插件共用同一份实现。
原理
| 层 | 手段 | 为什么这样选 |
|---|---|---|
| 窗口枚举 | EnumWindows |
覆盖所有进程,包括 UIA 看不见的 Qt / Wails / 控制台窗口 |
| UI 读取 | UI Automation CacheRequest + FindAll |
一次性批量取属性,1773 节点 420ms;逐属性跨进程调用要 6s+ |
| 像素 | PrintWindow(hwnd, hdc, PW_RENDERFULLCONTENT) |
要的是 DWM 里该窗口的合成结果,所以被遮挡也无所谓;不需要窗口成为前台 |
| 稳定帧 | 连续两帧 PNG 的 MD5 相同 | 静止画面连抓 10 张哈希全同,说明没有压缩噪声,两图不同就是真的变了 |
三个"不做":不 SetForegroundWindow、不 SetFocus、不 SendInput。这是"无感"的来源。
性能
每次工具调用都会起一个 powershell.exe,冷启动约占 1.5s,其余是实际工作:
| 调用 | 耗时 | 构成 |
|---|---|---|
appstate_list(20 个窗口) |
~3.8s | 1.5s 启动 + 0.45s 加载 UIA 程序集 + 0.3s Add-Type + 枚举 |
appstate_text(Firefox 整页) |
~1.9s 起 | 主要耗时在 UIA 跨进程遍历 |
appstate_shot(1936×1056) |
~3.3s | 启动 + PrintWindow 单帧约 74ms |
启动开销可通过常驻 PowerShell worker 消掉(同一进程内复用类型与程序集),本轮未做。
边界(实测,不是推测)
| 窗口类型 | appstate_text |
appstate_shot |
|---|---|---|
| Win32 / WinForms / WPF / UWP | ✅ | ✅ |
| Firefox / Chrome / Edge | ✅ 整页正文 | ✅ |
| Electron(DSH、VSCode) | ✅ 第二次调用即满 | ✅ |
| WebView2 / Wails | ⚠️ 只有外壳 | ✅ |
| Qt(AyuGram、PixPin) | ❌ 没有无障碍树 | ✅ |
| 最小化窗口 | ❌ | ❌ rect 塌成 1×1,命令明确报错 |
| 游戏 / 视频 / GPU 合成 | ❌ | ⚠️ Blank: true |
兜底顺序:text 有内容就用 → 空则 shot → 还 Blank 或最小化就换专用通道(CDP、qBittorrent/aria2 API、SMTC、日志、SQLite),或把 agent 搬进第二个 Windows 会话。
最小化是唯一的系统性缺口:要抓就得先恢复,而恢复就打扰你。正确解法是读状态文件,不是画面。
配置
cordis.patch.yml:
- insert:
- id: appstate
name: '@local/dsh-appstate'
config:
outDir: 'D:/nx8/appstate/out' # shot 默认输出目录
timeoutMs: 30000 # 单次 PowerShell 调用上限
appstate_wait 不受 timeoutMs 限制(它是多次短调用轮询),但每次内部读取仍受该上限约束。
故障排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 工具没出现 | 插件 fiber 失败 | 设置里看插件状态;node -e "import('file:///D:/nx8/dsh-appstate-F1x/index.js')" 先验语法 |
unsupported JSON schema |
schema 用了属性内 required: true |
DSH 只认标准 JSON Schema,required 必须是对象级数组 |
字段全 undefined |
PowerShell 输出是 PascalCase | 由 normalizeWindow 映射,改 schema 时别绕过它 |
no window matched |
pid 变了(目标软件重启) | 先 appstate_list 拿新 pid;用标题正则更稳 |
no top-level window with handle |
窗口已关闭 | 同上 |
UI Automation cannot see hwnd |
目标进程完整性级别更高 | 管理员身份运行 DSH |
| Chromium 窗口 text 很薄 | 无障碍树懒加载 | 再调一次 |
文件
dsh-appstate-F1x/
├── index.js # 插件宿主:4 个工具注册与调用
├── cordis.patch.yml # bundle patch(insert 一行 + 配置)
├── package.json # name/version/exports/dsh.bundle
├── scripts/
│ ├── Native.ps1 # Win32:EnumWindows / PrintWindow / RECT
│ ├── AppStateLib.ps1 # 三个读取器 + 稳定帧判定
│ └── appstate-cli.ps1 # CLI:list/text/ui/shot/state/watch/-Json
└── README.md
卸载
删 package.json 里 @local/dsh-appstate 的两处登记(dependencies、dsh.profile.bundles),删 junction,重启 DSH。
No comments yet. Be the first to write one.