DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

luckyf1oat /

luckyf1oat/dsh-appstate-F1x

Verified

DSH plugin: read Windows app state without screen captures, focus stealing or cursor movement. Telegram: @f8xx9

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: master@5586e5c5

dsh-appstate-F1x

作者 F1x · Telegram @f8xx9

给 agent 的"看软件状态"工具集。不截桌面,不抢焦点,不动鼠标。

五个工具:前四个只读,不调用 SetForegroundWindow / SetFocus / SendInput;第五个负责启动程序时同样不抢前台。你在用电脑的时候,agent 也能查软件状态、开新窗口而不打断你。

appstate_list    有哪些窗口
appstate_text    窗口里的文字和控件状态
appstate_shot    只抓这一个窗口的像素
appstate_wait    等状态变成某个样子
appstate_launch  启动程序,窗口不在最上层出现

为什么不是截屏

全屏截图的三个硬伤:

  1. 抓的是整个桌面——你在干嘛都进图了,隐私和体积双重浪费。
  2. 窗口被盖住就抓错——截屏拿到的是"屏幕现在显示什么",不是"这个软件现在是什么状态"。
  3. 为了抓全要抢焦点——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

启动分四步,目的是让窗口从来没有以可见状态出现在顶层:

  1. CreateProcessW + STARTF_USESHOWWINDOW + SW_HIDE(0) —— 窗口创建出来就是隐藏的
  2. 轮询(60ms)找到该进程的新窗口
  3. SetWindowPos(HWND_BOTTOM, SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE) —— 先在底层就位
  4. 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。

—/ 5

No ratings yet

Verified DSH bundle

Commit 5586e5c5962d

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout