DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

xiaobaga281 /

xiaobaga281/dsh-native-uia

Verified

御屏 · DSH 桌面操控插件:以 Windows UI Automation 控件树为主通道,纯文本模型即可操控桌面,后台投递动作、批量执行、逐步校验与配方回放

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@e93335e3

dsh-native-uia — DeepSeek Harness 原生桌面操控插件

给官方桌面端(@deepseek-ai/dsh-desktop,本包在 0.2.0-rc.2 上验证)加 computer-use 能力。 主观测通道是 Windows UI Automation 控件树,不是截图:纯文本模型就能操控桌面, 截图只是 Canvas / 自绘界面的兜底支路。

为什么不是"配了多模态模型才给用"

通道 每步成本 失效方式
像素回环(Anthropic CU / Operator / Copilot Studio) ~4.8k token/步,2–5 s 主题、字号、窗口移动、布局重排
控件树回环(本插件) 一次 snapshot 约 1.4k token,与屏幕分辨率无关 只在看不到树的应用上失效(Canvas、游戏、部分 Electron)

所以多模态是支路而不是闸门。截图片落盘成 PNG 并返回路径,由宿主自带的 read_image 决定是否进模型——它已经有正确的路由能力判定(不在支持 image 的路由上 拒绝写入历史),本插件不复制这套判定,也就不承担把图片塞进纯文本历史的后果。

安装 / 卸载

拿到这个包的人怎么装

  1. 解压到一个稳定的目录,不要放进 DeepSeek Harness 的安装目录(resources\app.asar 旁边)——应用更新或重装会把那里清掉。推荐:

    • Windows:C:\Users\<你>\.dsh\plugins\dsh-native-uia
    • 或任意路径,如 D:\plugins\dsh-native-uia
  2. 在该目录下执行:

    node scripts/install.mjs install
    

    这个脚本只改 profile package.json 的两个键(dsh.profile.bundles 与 dependencies), 写入的是 link:<本目录绝对路径>,并在改动前留一份 .bak-<时间戳> 备份。 它从脚本自身位置推导安装路径,因此放在哪都能装,不依赖用户名或盘符。

  3. 重启 DeepSeek Harness。重启后 127.0.0.1:3099 起控制页服务,会话视图出现「电脑操控」标签。

需要 node(≥18)。若 DSH_PROFILE 指向非默认 profile,可显式指定:

node scripts/install.mjs install      # 默认 ~/.dsh/profiles/desktop
DSH_PROFILE=my-profile node scripts/install.mjs install   # 只写 profile 名也可以

本机维护

node scripts/install.mjs install     # 写入 profile 的 bundles + dependencies,自动备份
node scripts/install.mjs status
node scripts/install.mjs remove      # 只删自己那两条,其它条目不动

安装后重启 DeepSeek Harness。重启后插件在 127.0.0.1:3099 起一个控制页服务, 会话视图里出现「电脑操控」标签(iframe 嵌本地服务,与 LSE 插件同构)。

端口被占用时插件拒绝激活(这个绑定同时是本机单实例锁);宿主日志会给出失败原因。

模型看到的工具

工具 作用
computer_windows 列出可见顶层窗口,拿 windowId
computer_snapshot 读控件树 → 短 ref(e1…)+ revision;新快照作废旧 ref
computer_find_text 按文本找元素,不必拉整棵树:命中项带 ref + 可点中心坐标 + by 描述符,按"能不能点"排序
computer_act 单动作:invoke / set_value / toggle / select / expand / scroll / focus
computer_run 一次调用批量执行,逐步本地校验,失败即停并报 performedCount
computer_wait 等条件而不是猜 sleep
computer_resolve_window 按 pid/title/class/titleRegex 解析或等待窗口,拿当前 windowId(窗口重开、改标题后的重新定位)
computer_capture PNG 截图:scope="window"(默认,PrintWindow + PW_RENDERFULLCONTENT)或 scope="screen"(整个桌面,含所有显示器)
computer_launch / computer_close 启动并按需关闭本插件自己开的窗口
computer_replay 回放成功批次的配方;元素按描述符重绑定,布局漂移后仍能跑

find_text 的排序:按"能不能点"

2026-10-18 真机实测(打开 DSH 设置→花费并截图)暴露:搜"设置"时,agent 自己的对话正文 (Text 节点,全部 offscreen)抢占了前 5 名,真正的 MenuItem "设置" 排第 6;搜"花费"时, 侧栏会话标题(TreeItem)压在 Button "花费" 前面。原因是旧的排序只区分"容器/非容器"。

现在按固定优先级排(同一档内保持扫描顺序):

档 条件 理由
0 Button / MenuItem / Tab / CheckBox … 且有可用 pattern 真正的命令控件
1 其它带可操作 pattern 的类型 少见但能点
2 其余(如纯 Text) 可读,不可点
3 TreeItem / ListItem / DataItem 侧栏会话行:是导航,不是目标
4 Pane / Document / Window / Group 点它只是聚焦窗口
5 isOffscreen: true 不滚动就点不到,绝不能排在可见元素前面

Text 不算可操作——它带 Text pattern,若算作可操作,聊天记录里每一段都会和真控件抢位。

同时每个命中都签发 ref:此前 find_text 只给坐标和 by 描述符,而 computer_act 只收 ref, 导致搜索结果只能退化成物理 computer_click(移动真实鼠标)。现在 computer_act ref=eN 可直接接。

截图:单窗口 vs 整屏

早先只能截单个窗口,屏幕上的其它东西(第二个应用、别的进程的对话框、任务栏、副屏)在模型眼里 根本不存在——这就是"设备屏幕截不全"。现在两条路:

scope 截什么 实现
window(默认) 一扇窗口,可在后台、不被遮挡 PrintWindow(PW_RENDERFULLCONTENT),失败退 PrintWindow(0),再退屏幕 BitBlt
screen 整个虚拟桌面(多显示器、负原点都算) StretchBlt 从虚拟屏原点整块拷贝,HALFTONE 缩放着色让缩小后的文字仍可读

两者都会回传 coordinateMap(screenX = <原点> + imgX * <scale>),可直接喂 computer_click。

maxDim 是"长边上限",不是目标宽度。它按比例同时缩宽高:1941×1243 的窗口传 maxDim=1600 得到 1600×1024(scale 1.2131),比例不变、不裁剪。默认值:

scope 默认 maxDim 结果
window 0(原生 1:1) 窗口真实像素,scale = 1,坐标直接等于屏幕坐标
screen 0(原生 1:1) 桌面真实像素(本机 2560×1600),坐标可直接点

两个 scope 都默认原生:缩小后的截图会静默损害可读性(小字糊掉)并引入缩放系数——"看起来被裁掉" 往往就是缩放造成的错觉。要省 token 时显式传 maxDim(如 1600)。

整屏截图会临时隐藏御屏自己的浮层

指示层是always-on-top 的 WPF 窗口,所以桌面 GDI 抓屏会正确地把御屏自己录进去:顶部 "御屏·正在操控"胶囊 + 四周紫色光晕。这既是污染证据,在截图预览打开时还会递归拍到"截图的截图"。

现在 scope="screen" 走 ActivityWindow.withoutIndicator():先把指示层切到 hidden,等一个 WPF 调度轮次(50 ms)让 HWND 真正不可见,再采样桌面,最后立刻恢复原来的 phase(acting 或 stopped)。实测同一块屏,有浮层 1,068,169 字节 / 无浮层 966,504 字节,像素确有差异, 恢复后 visible=true,整个隐藏→采样→恢复耗时约 330 ms。

scope="window" 不做这个切换:PrintWindow(hwnd) 只渲染该窗口,本来就不含浮层,为它隐藏 只会让人看到一次无谓的闪烁。

屏幕尺寸一律实测,绝不硬编码:GetSystemMetrics(SM_XVIRTUALSCREEN..SM_CYVIRTUALSCREEN) 取虚拟屏 原点与尺寸,失败退主屏 metrics,再失败退 Screen.PrimaryScreen.Bounds,三者都测不到就报 screen_unknown 而不是猜一个 1920x1080——猜错会让每次坐标归一化都静默偏到别处。capture_screen 的返回里带上实测的 screenRect,模型能直接看到真实分辨率。

速度与准确性(2026-10-17 实测调优)

先说结论:坐标是准的,错在"选了谁"

同一台机器上实测坐标链路误差 0 像素:SendInput 绝对坐标请求 (530,392),光标读回 (530,392);五个测试点(含 2400,1500 屏幕角落)全部 0 偏差。150% DPI 下 worker 走 PerMonitorV2 + MOUSEEVENTF_ABSOLUTE|VIRTUALDESK 归一化,这条换算是对的。

所以"agent 老是点不准"的真因在语义:模型拿到的是对的坐标,但选错了元素。三个具体陷阱:

陷阱 现象 修法
容器排第一 搜"插件"时,窗口自身的 Pane/Document(1941x1243)先命中,点它只是聚焦窗口 find_text 按可操作性排序,容器排最后并标注 ←容器,click 只会聚焦窗口
同名不可分 窗口里有 2 个 Button "新建会话",by 只带 name+type,FindFirst 永远给第一个 by 携带 nth(第几个)/nearPoint(离哪最近),同名元素各自可寻址
静默无效 invoke 一个没有 InvokePattern 的 Pane 返回成功,模型以为点中了 before===after 时明确回话"没有观察到任何状态变化",并指向 find_text

nth 实测有效:同为 by={name:"新建会话",controlType:"Button"},nth=1 解析到 _2H3hWW_brand(侧栏顶部),nth=2 解析到 _2H3hWW_newSession(新建按钮),是两个不同控件。

速度

snapshot 是热路径,成本几乎全在跨进程 COM 往返上,不在数据量:一个节点的 $el.Current 之后每个属性访问仍是一次往返,加上 GetSupportedPatterns 一次,实测 13.6 ms/节点。四处改动把它降到 7.4–8.1 ms/节点(同窗口、同 200 节点):

  1. 属性只读一次——Name/ClassName/ControlType/IsOffscreen/AutomationId/IsEnabled 先落到局部变量再组节点;旧代码把 AutomationId/IsEnabled/IsOffscreen 各读了两次。
  2. 模式集合按签名缓存——同一控件类型的模式集合高度重复,命中后跳过字符串正则与去重。
  3. 默认节点预算 300 → 200——尾部多半是模型从不操作的窗口装饰;截断时会明说,并指出 三条出路(收紧过滤、用 find_text、提高 maxNodes)。
  4. 渲染只留可操作模式——ScrollItem,Transform,ItemContainer 这类对"能不能点"无影响, 从渲染里去掉让树小约 28%(300 节点:7075 → 5107 token)。

准确性上多了一行可操作摘要:快照头部先列 ref "名字"(实测 200 节点里 66 个),模型常常 直接挑中目标去 computer_act,不必在整棵树里扫。compact: true 进一步压缩名字与值。

顺带修掉一个既存崩溃:Evict-ForReservation 与 PatternsOf 会用 $null 去索引 Hashtable, 抛 Key cannot be null,表现是整个 snapshot 偶发 internal_error——只在触发淘汰或遇到 "无名字也无 Id 的模式"时出现,所以随手跑测试永远看不见。两处都改成 ContainsKey 先行。

架构

cordis apply()  ──►  src/tools.js   模型面工具 + defineTool
                    src/guard.js    唯一的策略判定点(worker 不含策略)
                    src/worker-client.js  stdio JSON-lines,ASCII 转义
                    src/dashboard.js     127.0.0.1 控制页 API/SSE
                    worker/uia-worker.ps1  PowerShell 5.1 + System.Windows.Automation

worker 是独立进程:UIA 调用能阻塞,进程内跑会拖死宿主事件循环;官方 dsh-host-directory-picker-native 出于同一理由也把 Win32/COM 调用放进子进程。 零新增依赖(无 koffi、无 npm 安装、无编译),所以不需要 allowBuilds 审批。

批量动作走两阶段:prepare_batch 只解析描述符不做任何事,Node 侧逐条过策略, 全绿才 commit_batch(带 revision,不匹配就 0 动作拒收)。控制页的 PAUSE/KILL 不靠约定,卡在 worker.call() 这个所有输入的必经点上。

屏幕指示层(顶部居中胶囊)

指示层由 worker/indicator-server.ps1 绘制,src/activity-window.js 驱动,只有一个所有者 (index.js;工具层不碰它——这条不变量由 verify.mjs 的闸门守着)。只有两个可见状态:

相位 什么时候 胶囊文字
acting 一个会改动桌面的动作开始 御屏 · 正在操控(淡紫)
stopped 控制页终止 / Esc / computer_done 御屏 · 已停止(红),1200 ms 后收起(DSH_INDICATOR_STOPPED_MS 可调)

曾经有一个"暂停"态(waiting:御屏 · 等待确认,琥珀)——控制页的 PAUSE 按钮早已移除, 后端调用点也随之不存在,于是整个暂停能力被删掉了,包括 store.paused、pause/resume 两个命令和渲染端的等待相位。停手由终止和 Esc 承担:二者都把 killed 闩住,下一个 试图落盘的动作会在 worker.call() 咽喉点被拦下并回话(这一次拦截同时清除闩锁); 再下一个动作会自动重启 worker (这是既有设计,见 verify.mjs 的相应闸门)。要彻底关掉,用宿主"插件"页的组件开关。

指示层进程崩溃不需要人工干预:Node 侧懒启动会在下一次指示层命令(下一次动作的 claim、心跳或收尾)自动重新派生进程,退出事件以 warn 级写进面板日志。(实测过一次 WPF 合成线程 COM 崩溃:WER 1001,DUCE.Channel.SyncFlush,Node 只看到 exit 2。)

租约与心跳分工不同(各由 verify.mjs 闸门钉住):租约(默认 300 秒,DSH_NATIVE_UIA_LEASE_MS 可覆盖)只被真实工具调用续——动作 claim、只读观测(snapshot/windows/capture/parse/ find_text)observe;它到期就是"模型忘了调 computer_done"的自动收尾兜底:静默的 run 必在 租约内以红色「已停止」熄灭,不会永生。曾有的设计让 30 秒心跳顺手续租,胶囊就再也停不下来了。 心跳只喂渲染层(防 think gap 期间指示层的 idleHideMs 看门狗先收 capsule),不续租;一个命令 真正在飞时(busy)过期自动推迟而不是硬收。只读会话本身不会点亮指示层:observe() 只对 已经在跑的 run 生效。

空闲自熄(默认 20 秒,DSH_NATIVE_UIA_IDLE_MS 可覆盖)是租约之内更快的一层:18 个工具全部 经 registerTools 里同一个包装器 touch(),所以"模型还在不在动桌面"只判定一次。模型停止调用 工具后倒计时开始,到点即以 finishRun 收尾——这正是"agent 用完御屏不会主动关闭"的修复。它 不续租(与心跳同理,由闸门钉住),并且只在 store.running === true 时武装,只读会话不会 为一个不存在的 run 安排收尾。显式 computer_done 会解除计时器并把 target 清成 null,所以 "完成"之后面板不会再挂着上一轮的目标窗口。

Esc 由指示层进程感知(它常驻且空闲,毫秒级应答),worker 只在指示层不可用时兜底。两个通道 都读 GetAsyncKeyState 的低位("自上次查询以来按过")而不只是高位("此刻是否按住"): 轮询间隔 40 ms 时,一次短于间隔的轻点也不会被漏掉——这是"点 ESC 停止很慢"的修复。

策略边界(按"开发自用"设定)

硬拒、且不可批准:系统关机/重启/注销、退出程序、破坏性删除。 被拒的步骤在 commit_batch 之前就被拦下,performedCount=0。 computer_close 只允许关闭本插件启动的窗口。

窗口的关闭按钮(关闭 / Close / ✕ / ×)与 Alt+F4 同级:invoke 它等于结束整个应用, 所以归入 exit 一并拦住。这条是补上的漏洞——早先的模式只认"退出程序/关闭程序"这种长短语,而真实 UIA 树里窗口关闭按钮就叫"关闭",于是最顺手的绕过方式一直躺在策略边界之内。不误伤 取消、确定、 最小化/最大化 以及"关闭对话框"这类正常操作(闸门同时钉住"必须拦住"和"不得误伤"两侧)。

已知未覆盖的口子,不假装:模型仍可让目标应用自己触发删除(例如选中后按键盘), 也可通过 launch 起一个能关机的程序再用输入操作它——shutdown.exe/logoff/ taskkill 这几个名字已进拒收表,但这是名字表,不是能力沙箱。要真边界请把它 当"防手滑"而不是"防对抗"。

验证

node scripts/verify.mjs       # 静态闸门:解析、策略、契约、两态、心跳不续租、Esc 走指示层、工具与 worker 能力对齐、SSE 扇出防护
node scripts/check-lease.mjs  # 指示层行为:租约/心跳/观察续租/终止重启/探针静默/静默自熄(不需要桌面)
node scripts/smoke.mjs        # 只读真机:spawn worker、树、find_text、指示层 escape_pressed、截图、负测
node scripts/verify-live.mjs  # 重启宿主后:向运行中的插件进程要 /api/state 并断言(含「契约横幅」= 加载的是哪一代代码)

D:\1\2\probe\act-e2e.mjs 通过真实工具定义跑输入路径。离线跑用的是 defineTool 的结构化桩, 因此参数 DSL 的真校验只有在宿主里加载插件才算数。

发布包

npm pack      # → dsh-native-uia-0.2.0.tgz(package/ 为根)

包内即 files 列表 + README.md + LICENSE:src/、worker/(含 bin/ocr.exe)、web/、client/、scripts/、locale/、icon.svg、cordis.patch.yml。

源码仓库:https://github.com/xiaobaga281/dsh-native-uia

变更记录

0.2.0

围绕"点不准 / 截不全"的一轮实测修复(同机实测数据见各节):

  • find_text 结果按可点击性排序:命令控件 → 其它可操作 → 文本 → 侧栏导航行 → 容器 → 屏外。 之前搜"设置"会被 agent 自己的对话正文(Text、屏外)淹没,真正的 MenuItem 排到第 6。
  • find_text 每个命中签发 ref:此前只给坐标和 by 描述符,而 computer_act 只收 ref, 搜索结果只能退化成物理点击(移动真实鼠标)。
  • 同名元素可单独寻址:by 支持 nth(第几个)与 nearPoint(离哪最近)。窗口里两个 Button "新建会话" 之前永远只能命中第一个。
  • computer_act 的无效动作判定改为能力表:以前用 before === after,而按钮没有 Value, 导致每个正常按钮都被误报"没有状态变化"。
  • 整屏截图 scope="screen":此前只能截单窗口,桌面其余部分对模型不存在。
  • 整屏截图时临时隐藏御屏浮层:指示层是 always-on-top,GDI 抓屏会把它自己录进去 (顶部胶囊 + 紫色光晕),预览打开时还会递归拍到"截图的截图"。现在隐藏 → 采样 → 立即恢复。
  • 截图默认原生 1:1:maxDim 是"长边上限"而非目标宽度;默认不再静默缩放。
  • prepare_batch 校验动作名:fly 之前能通过 dry run,直到 commit 才报错;大小写也统一。
  • computer_wait 缺目标时立即报错:以前要空等整个超时,再回一个误导性的 element_gone。
  • 修既存崩溃:淘汰与模式缓存用 $null 索引 Hashtable,会让整个 snapshot 偶发 internal_error。
  • 安装脚本可移植:安装路径从脚本自身位置推导,不再写死某个用户目录;DSH_PROFILE 支持 profile 名或绝对路径两种写法。

闸门:node scripts/check-lease.mjs / node scripts/verify.mjs(53 项)/ node scripts/smoke.mjs(11 项)全绿。

0.1.0

首个可用版本:UIA 控件树为主通道、后台投递动作、批量执行与逐步校验、配方回放、 截图兜底、ESC 中断、静默自动收尾。

—/ 5

No ratings yet

Verified DSH bundle

Commit e93335e35fec

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