🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

桌面操控 momasiku/dsh-pilot

DeepSeek Harnessv0.7.0
momasiku7 天前有提交已提供中文文档扫描:含敏感能力

DeepSeek Harness 的桌面自动化:给整台 Windows…

59.1/100
综合分
120
GitHub 星标
5,708
npm 周下载
—/5
用户评分

数据截至 2026/10/28 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add @momasiku/dsh-pilot
npm 源 · @momasiku/dsh-pilot 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · vision
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 7 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包@momasiku/dsh-pilot @ 0.7.0
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 14:12:33

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 28 处,展示前 23 条)
  • 写入本地文件
    lib/index.js:89
    mkdirSync(dir, { recursive: true });
  • 读取本地文件
    lib/index.js:117
    if (!existsSync(script)) throw new Error(`pilot: helper script is missing at ${script}`);
  • 写入本地文件
    lib/index.js:122
    writeFileSync(paramsPath, JSON.stringify({ ...params, cancelFile: cancelPath }), 'utf8');
  • 执行外部命令固定命令
    lib/index.js:125
    const child = spawn(
  • 删除本地文件
    lib/index.js:139
    rmSync(file, { force: true });
  • 写入本地文件
    lib/index.js:155
    writeFileSync(cancelPath, 'stop', 'utf8');
  • 执行外部命令固定命令
    lib/index.js:291
    child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', workerScript()], {
  • 写入本地文件
    lib/index.js:406
    if (typeof payload.cancelFile === 'string') writeFileSync(payload.cancelFile, 'stop', 'utf8');
  • 发起网络请求
    lib/index.js:502
    return await actionWorker.request({ ...params, cancelFile: cancelPath }, options);
  • 写入本地文件
    lib/index.js:530
    mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/index.js:558
    writeFileSync(join(dir, 'show.txt'), cancelPath, 'utf8');
  • 执行外部命令固定命令
    lib/index.js:564
    overlayProcess = spawn(
  • 删除本地文件
    lib/index.js:605
    rmSync(join(dir, 'show.txt'), { force: true });
  • 删除本地文件
    lib/index.js:611
    rmSync(stale, { force: true });
  • 读取本地文件
    lib/index.js:625
    return statSync(join(dir, 'esc.txt')).mtimeMs >= sinceMs;
  • 读取本地文件
    lib/index.js:652
    if (!existsSync(join(dir, 'show.txt'))) return async () => {};
  • 读取本地文件
    lib/index.js:656
    if (existsSync(muted)) break;
  • 删除本地文件
    lib/index.js:664
    rmSync(mute, { force: true });
  • 发起网络请求
    lib/index.js:692
    return await actionWorker.request({ ...params, kind: 'capture' }, options);
  • 读取本地文件
    lib/index.js:725
    const entries = readdirSync(dir)
  • 读取本地文件
    lib/index.js:730
    return { full, mtime: statSync(full).mtimeMs };
  • 删除本地文件
    lib/index.js:739
    rmSync(entry.full, { force: true });
  • 删除本地文件
    lib/index.js:1850
    rmSync(batchCancel, { force: true });
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis@deepseek-ai/dsh-attachment@deepseek-ai/dsh-llm@deepseek-ai/dsh-tools
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 截取桌面真实画面
  • 操控鼠标键盘窗口
  • 一次调用跑完任务

给 DeepSeek Harness 装上眼睛和手:screen_view 把桌面的真实 PNG 交给模型,desktop_control 操控鼠标、键盘和窗口,desktop_sequence 一次调用跑完整个任务。

v0.7.02026-10-27
浮层重做:一圈淡光、一块读得清的提示牌,以及「截图里不该有它」。 · 屏幕四边天蓝渐变光(四条带,无描边,峰值 alpha 110),向内 56px 淡出 · 顶部提示改成蓝色半透明面板:圆角 20、55% 透明、边框高光绕圈跑、Segoe UI / 微软雅黑 SemiBold、Esc 键帽 · 去掉光标光晕(轮询追不上真实指针,慢半拍的影子比没有更糟) · 截图前自动让开:mute.txt → muted.txt(≤180 ms)→ 截图 → 恢复,截图现在是干净的 · 修掉单实例的洞:互斥名按路径字符串算,Administrator 与 ADMINI~1 能同时起两个;加 instance.txt 的 PID 锁 · 刷新 33 ms → 16 ms,呼吸每 8 帧应用一次,分辨率每秒跟随一次 · tick 异常不再静默(以前会停在屏幕上但完全不动)
v0.6.52026-10-25
控制整台 Windows 机器的桌面。screen_view 把实时屏幕的真实 PNG 交给模型;desktop_control 在同一片像素空间里驱动真实的鼠标、键盘和窗口;desktop_sequence 一次调用跑完一整批操作,最后只看一眼。 · 0.6.5 改了什么 接管提示框跟着指针走。覆盖层进程不感知 DPI——powershell.exe 的清单在脚本运行前就定好了感知级别,所以在脚本里申请 per-monitor v2 只会返回 false。在 150% 缩放的显示器上,面板是 2560×1600,而 WPF 按 1707×1067 布局窗口,GetCursorPos 却仍以物理像素作答:光晕画偏了半个指针的距离。脚本现在通过 EnumDisplaySettings 读取真实显示模式(WMI 的 Win32_VideoController 在当前 NVIDIA 驱动上返回 0×0),再用光标位置除以缩放比。标题文字一直没错——按逻辑单位布局,操作系统才会正确缩放它。 隐藏延迟成了可配置项。takeoverLingerMs 默认 8 秒,取代原来写死的 2 秒。单个操作现在有足够时间看清,圆环显示期间按 Esc 仍可取消。 两种方式验证:单独运行覆盖层并冻结光标,日志记录物理分辨率 2560x1600(EnumDisplaySettings)、缩放 1.5,光晕落在逻辑坐标 853,533 = 物理坐标 1280,800;对着真实指针运行,1:1 裁剪显示圆环正对光标(中心 908,740,指针 908,739)。 · 安装 · 从仓库 dsh plugin --profile add github:momasiku/dsh-pilot · 或本版本的预构建 tarball dsh plugin --profile add 之后重启一次桌面应用,再开一个新会话。 要求:Windows、PowerShell 5.1 或 7、Node ^22.19.0 || >=24.0.0、DSH 0.1.5-rc.2 到 0.2.x,以及一条声明支持图像输入的模型路由(deepseek-flash 支持)。 · 资源 | 资源 | 用途 | | --- | --- | | dsh-pilot.tgz | 不带版本号的名字,让 releases/latest/download/dsh-pilot.tgz 始终指向最新构建 | | dsh-pilot-0.6.5.tgz | 同一个 tarball,供固定到本标签的 URL 使用 |
v0.6.42026-10-24
· 接管提示改成按回合显示 · 一个回合只显示一次,最后一次动作后 2 秒隐藏;新动作取消隐藏。 · 修掉启动竞态(首个动作记两次 show)。 · Test-Path 短暂失败不再立即隐藏(连续三次读不到才隐藏)。 · 单实例互斥:DSH 重启留下的孤儿进程会自己退出。 · 新增 tools/overlay-smoke.ps1,四项测量。 已知代价(按你的要求不预热):首次出画约 0.7 秒。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-pilot 让 DSH 看见并高效地操作你的电脑 给 DeepSeek Harness 装上眼睛和手,让它直接操作你的桌面。 awesome · DSH plugin 中文说明 · English 中文说明 给 DeepSeek Harness 装上眼睛和手——装在整个桌面上。 范围:Windows 桌面操控——模型看到的是真实屏幕,驱动的是真实鼠标与键盘。 DSH 原本只会「说」:它能推理、能写代码、能规划,但看不见你的屏幕,也动不了你的鼠标。于是看屏幕的是你、截图的是你、点按钮的是你、出错了汇报的也是你——你成了它的手、它的眼,还兼任它的报错信息。 dsh-pilot 把这个环闭上:模型直接看真实像素,然后自己动手。 实例演示 dsh-pilot 在 AutoCAD 里画一块机械板:模型把 REC、角点坐标、内轮廓、五个圆、中心线和两道线性标注依次输入命令行,最后缩放到范围 模型自己画出来的 CAD 图。 用的就是 AutoCAD 自己的命令行——REC → 0,0 → 240,150、内轮廓、五个圆孔、中心线、两道尺寸,AutoCAD 里什么都没装。
展开完整文档(README 剩余 1 节)
dsh-pilot

让 DSH 看见并高效地操作你的电脑

给 DeepSeek Harness 装上眼睛和手,让它直接操作你的桌面。

awesome · DSH plugin

中文说明 · English

中文说明

给 DeepSeek Harness 装上眼睛和手——装在整个桌面上。

范围:Windows 桌面操控——模型看到的是真实屏幕,驱动的是真实鼠标与键盘。

DSH 原本只会「说」:它能推理、能写代码、能规划,但看不见你的屏幕,也动不了你的鼠标。于是看屏幕的是你、截图的是你、点按钮的是你、出错了汇报的也是你——你成了它的手、它的眼,还兼任它的报错信息。

dsh-pilot 把这个环闭上:模型直接看真实像素,然后自己动手。

实例演示

dsh-pilot 在 AutoCAD 里画一块机械板:模型把 REC、角点坐标、内轮廓、五个圆、中心线和两道线性标注依次输入命令行,最后缩放到范围

模型自己画出来的 CAD 图。 用的就是 AutoCAD 自己的命令行——REC → 0,0 → 240,150、内轮廓、五个圆孔、中心线、两道尺寸,AutoCAD 里什么都没装。

dsh-pilot 在 LibreOffice Calc 里改一列表头:聚焦工作簿、鼠标移到 C1、点击、粘贴「Q3 收入」、回车、Ctrl+S —— 全部在一次 desktop_sequence 调用里完成

一次调用改完一张表。 desktop_sequence 聚焦工作簿、把鼠标移到 C1、点击、粘贴「Q3 收入」、回车、保存——整串动作只花一次往返。

dsh-pilot 在 Blender 里建模:先清空视口,加一个立方体,再用细分曲面把它磨成一个球,最后轨道旋转检查

从零建一个 3D 模型。 清空视口 → 加一个立方体 → Ctrl+1、Ctrl+3 两级细分,方盒子被磨成一个球。真机上的 Blender 4.2,跑在一台没有显卡的机器上——靠 Mesa 的软件 OpenGL,而那套环境也是模型自己配好的。

| 工具 | 作用 |
| screen_view | 抓取当前屏幕,把 PNG 作为真正的图像块交给模型(不是 OCR、不是文字描述),连光标位置、前台窗口、各显示器范围一起给出 |
| desktop_control | 移动鼠标、单击/双击/右键、拖拽、滚轮、打字、按键、聚焦与列出窗口;可以在同一次调用里顺手截一张,让效果立刻可见 |
| desktop_sequence | 一次调用跑完一整串动作,最后只看一眼结果 |

它到底解决了什么

桌面自动化的瓶颈从来不是鼠标点得快不快,而是 「模型和电脑要来回说多少次话」:

| 一个五步任务(聚焦窗口 → 点输入框 → 打字 → 回车 → 看结果) | 只用 DSH | DSH + dsh-pilot |
| 谁在看屏幕 | 你 | 模型,看真实像素 |
| 谁在点击打字 | 你 | 模型 |
| 模型 ↔ 电脑往返次数 | 5 次(每步一次,外加你的) | 1 次 |
| 送给模型的截图 | 4–5 张 | 1 张 |
| 出错时 | 你发现、你描述 | 同一次结果里就写明哪一步、为什么 |

为什么坐标可信(这是「点得准」的关键)

视觉驱动点击最经典的翻车方式是「坐标差一点点」,这里堵掉了三个来源:
1. 两端都是 per-monitor DPI 感知。非 DPI 感知的进程会被 Windows 骗——它把 2560×1600 的屏幕看成 1707×1067,于是按这种截图算出来的点击必然偏。传感器和执行器在做任何事之前都声明 PER_MONITOR_AWARE_V2,所以一个图像像素就是一个物理屏幕像素。
2. 每张图都带坐标刻度尺:上沿与左沿每 200px 一条刻度,标注的是屏幕坐标(不是图像偏移),还有 origin x,y step n 说明。模型直接读刻度,不用数像素;刻度是屏幕坐标,所以即使图片被压缩也依然有效。
3. 信封如实说明交付尺寸。如果宿主把图压小了,模型会收到比例与乘数;坐标契约也变成条件式——只有在「交付尺寸 == 采集尺寸」时才承诺「照用 x、y 即可」,否则明确要求乘多少:

<delivered_scale>delivered 1974x873 = 0.873x capture; multiply image readings by 1.1454 to get screen pixels</delivered_scale>

为什么快

批处理是大头(省的是往返,不是点击);第二个大头是不再每次重启 PowerShell——一个常驻 worker 同时服务动作与截图:

| 请求 | 一次性脚本 | 常驻 worker |
| 动作(移动/点击/打字…) | ~0.9–2.0 秒 | 6–12 毫秒 |
| 截图(主屏) | ~1.2–2.3 秒 | ~0.5 秒 |

(2560×1600、150% 缩放、DSH 界面还在动的情况下实测)。worker 在第一次调用时后台预热,它需要的约 1 秒与第一张截图重叠,不会拖慢第一个动作。

没变化的帧不重复发送:每次截图都带 frameHash,与同会话上一帧逐字节相同时就不重复附图(也不重复存储),结果里写 unchanged: true,模型手上那张图的坐标继续有效;想看就传 forceImage: true。

读控件树

截图是要靠猜的:模型在像素里找到按钮、估个中心、点一下,再截一张图看猜没猜对。
而 Windows 通过 UI Automation 直接把同一个控件交出来——角色、名字、矩形——于是点击就变成「那个矩形的中心」,什么也不用看。

desktop_control({ action: 'controls', title: '记事本' })
// 4 [Button] '最小化' @ 887,1 45x39 // 14 [Button] '新建会话' @ 12,54 256x24 // 23 [Edit] '搜索会话名称' @ 204,238 4x20 offscreen

desktop_control({ action: 'click', target: 14, title: '记事本' })

控件树不是更快的截图,是另一种取舍。

| | 截图 | 控件树 |
| 读一次 | ~150 ms,~1k token | ~130-300 ms,~850 token(过滤后) |
| 点击位置 | 从像素里估 | 控件的精确矩形 |
| 状态 | 「看着像禁用」 | enabled、offscreen、HasKeyboardFocus、IsSelected、值 |
| 可用性 | 永远有 | 不一定有,见下 |

按单次调用看,树更慢或打平;按单个任务看,树快得多——一次读取能定位好几个目标,既不用多截一张图,也不用多跑一轮推理。

它不是总存在。 canvas、绘图面、自绘列表、以及什么都不上报的 Electron 窗口,给出的树要么是空的要么没意义。所以截图这条路还在,而且仍然是兜底:先读树,树为空、或者你需要看「画出来什么」而不是「声明了什么」时,再截一张图。

三个要知道的点。

- 索引只针对「某一次列表」。点击时会重新读一遍再解析,而且暖 worker 记着它发出去的那份列表,所以某一行换了身份会明确报错,而不是按位置点下去: the control at index 4 is not the one that was listed: expected [Button] 'OK', found [Button] 'Cancel'。
- meaningfulOnly(默认 true)只返回人能操作的控件。在一个真实的 673 元素窗口上,这是 77 个元素、约 850 token,而不是 9k——其余全是没名字、没状态、点了也没反应的布局面板。要整棵树就传 false。
- includeValues 默认关闭:结构不是内容。即使打开,密码框也会被打码。

当你要找的东西本身没有树——聊天记录、画布、自绘面板——不要去找它。去找围着它的东西再做减法:输入框是个带矩形的 Edit,窗口有矩形,那么聊天区就是它们之间剩下的那块。
或者干脆不用几何:在树里选中那一条,用 HasKeyboardFocus 确认键盘落点就是你要的地方,打字,回车。最后这一步是截图做不到的——截图能给你一根光标,给不了保证。

为什么安全

给模型装上手,最该讲究的就是这一块:

- 风险先声明、后强制:默认 low 可自由批量;声明 medium/high 的批次、或含 risk: "high" 步骤的批次,在碰桌面之前就被拒绝,除非调用方带 confirm: true(而它只会在你同意之后带)。
- dryRun: true 只校验并列出计划,不执行。
- 默认遇错即停:focus 失败绝不会有「接着往面前那个窗口里打字」的后续。
- 定向输入:步骤里指定了窗口(hwnd)时,注入前会再次把该窗口置前,点击不会落进旁边的窗口。
- 结果不明就绝不静默重试:worker 在请求送达后死掉时,动作按失败上报而不是重放——它可能已经生效了;截图没有副作用,才会自动回退重试。
- 中断是真的能中断:按 Esc 会写取消令牌,动作循环在每一步之间轮询(拖拽中、等待中、截图前都查),拖拽在 finally 里松开按键——中断的回合不会留下按住的鼠标,也不会把剩下的点击跑完。
- 不会假装成功:Windows 禁止向更高完整性级别(提权)的窗口注入输入,工具会如实报错。
- 批量有上限:maxSequenceSteps(默认 24)。

安装

从 npm 装(推荐)
dsh plugin --profile <你的profile> add @momasiku/dsh-pilot

或从仓库装
dsh plugin --profile <你的profile> add github:momasiku/dsh-pilot

或用本仓库 Release 里的预构建包,免构建授权
dsh plugin --profile <你的profile> add https://github.com/momasiku/dsh-pilot/releases/latest/download/dsh-pilot.tgz

或你本地已有的检出
dsh plugin --profile <你的profile> add "file:E:\path\to\dsh-pilot"

下载: npm 包 · 预构建 tarball · 源码

✅ 推荐用 npm 上的 scoped 包名 @momasiku/dsh-pilot。 裸名 dsh-pilot 在 npm 上属于另一款无关的浏览器操控插件,而 dsh plugin add 总是先查 npm,所以别用裸名;用仓库地址或 tarball 也照样可以。

包自带 loader 行:plugin add 会把 @momasiku/dsh-pilot 放进 dsh.profile.bundles,行本身来自包内的 cordis.patch.yml(由 dsh.bundle.patch 声明),所以不会像手写补丁行那样被插件管理器重写时弄丢。装完重启一次 DSH Desktop,然后新开一个会话,三个工具就会出现在工具列表里。

要求 DSH 0.1.5-rc.2 至 0.2.x(即 package.json 里的 peer 范围,也是 dsh plugin add 在安装前的门禁)。环境要求:Windows、PowerShell 5.1 或 7、以及声明了图像输入的模型线路(deepseek-flash 可以)。

最近改动

0.7.0 —— 浮层重做:一圈淡光、一块读得清的提示牌,以及「截图里不该有它」。

- 屏幕四边一圈天蓝渐变光(四条带,各自一条 LinearGradientBrush,无描边,峰值 alpha 110)。发光只到边缘、向内 56px 淡出,所以看起来是光渗进来,不是一个画上去的框。
- 顶部提示变成一块蓝色半透明面板:圆角 20、rgba(18,74,148,140),边框上一段高光绕圈跑(旋转渐变),字体换成 Segoe UI / 微软雅黑 SemiBold,Esc 做成真正的键帽。
- 去掉光标光晕。它只能靠轮询 GetCursorPos 再移动窗口,无论循环多快都会慢一两帧 —— 一个总是慢半拍的影子光标比没有更糟。
- 截图前自动让开:宿主写 mute.txt,等浮层回 muted.txt(≤180 ms)再截,拍完立刻恢复。现在 screen_view 出来的图是干净的,没有一个亮边压在画面上。
- 单实例的洞补上了:互斥名是按 StateDir 字符串算的,C:\Users\Administrator\… 和 C:\Users\ADMINI~1\… 指向同一目录却是两个名字,于是两个浮层会同时画。现在多一个 instance.txt 的 PID 锁,跟路径写法无关。
- 刷新 33 ms → 16 ms(光晕没了之后额外开销很小),呼吸每 8 帧应用一次,分辨率每秒检查一次:阿里云这类会话中途改分辨率,面板不会再被推出屏幕。
- tick 里的异常不再静默:以前抛错会被 dispatcher 吞掉,浮层停在屏幕上但完全不动;现在 catch 住并写进 status.txt。
- overlay-smoke 的首帧门槛从 2000 ms 调到 3000 ms:这台负载机光 PowerShell 启动就要 1.8–2.1 s,0.6.5 原版实测 1874 ms。
0.6.5 —— 接管提示终于看得见:像素对了,停留时间也能调。

- 浮层没声明 DPI 感知,150% 缩放下它以为屏幕是 1707×1067(实际 2560×1600):顶部文字被放大半倍,跟随鼠标的光环画到别处去了。现在在加载 WPF 之前声明 per-monitor v2,按真实像素排版,光环落在指针上。
- 隐藏延迟从写死的 2 秒变成配置项 takeoverLingerMs,默认 8 秒:单步动作也来得及看清是谁在操作,Esc 仍然随时能取消。
- 浮层脚本里“宿主两秒后隐藏”的注释同步改成引用这个配置项。

0.6.4 —— 接管提示改成按回合显示。

- 以前每个动作开关一次,画图这种连续操作就是一路闪。 现在一个回合只显示一次,最后一次动作后 2 秒才隐藏;新动作会取消这个隐藏。
- 首次出画约 0.7 秒(不预热)。比这更短的单步动作仍然可能看不到第一帧 —— 这是不预热的代价。
- 修掉启动竞态:脚本末尾的 Show()/Hide() 会和定时器的第一次真实显示撞车,导致首个动作记两次 show。句柄已经由 EnsureHandle() 建好,那两句是多余的。
- Test-Path 短暂失败不再立即隐藏:宿主每次动作都会截断重写 show.txt,正好撞上读不到就是一次假隐藏。现在连续三次读不到才隐藏。
- 单实例互斥:一个进程独占一个状态目录,DSH 重启留下的孤儿不会再和新进程抢同一块屏幕。
- 新增 tools/overlay-smoke.ps1:首次出画耗时、回合内不重显、结束只隐藏一次、第二份进程必须退出。

0.6.3 —— controls 的两处真问题(issue #1 / #2)。

- 结构化输出里有、模型可见文本里没有:controlText() 只拼了 action / cursor / foreground,把 controls 和 windows 整个丢了 —— 对纯文本模型来说,README 里说的「便宜路径」事实上不可用,只能退回截图。现在渲染成 <controls> / <windows> 块,并带上窗口名与计数。
- meaningfulOnly 的 $name 从未赋值:$null.Length -eq 0 恒为真,于是 ListItem / TreeItem / MenuItem / DataItem 四类无论有没有名字都被丢弃。改成读元素自己的名字,与注释里写的意图一致。
- Pane 不在白名单:原生 Win32 对话框(#32770)的所有子控件经 MSAA 桥接后都报 Pane,于是一个满是按钮的对话框过滤后是空的。命名 Pane 现在算控件,无名 Pane 仍视作布局。
- 过滤不再无声:新增 controlTotal / filteredOut 并渲染进文本。实测「运行」对话框:10 个元素,保留 6 个(确定 / 取消 / 浏览 / 提示文字),丢掉 4 个无名 Pane。
- 回归测试:script-smoke 现在会真按一次 Win+R,断言该对话框在 meaningfulOnly:true 下非空且含「确定/取消」,随后 Esc 关闭。

0.6.2 —— 兼容性声明落到能验证的地方。

- 在 DSH 0.2.1-rc.3 上实测通过:screen_view 抓屏、windows 枚举、controls 控件树、desktop_sequence 三步批次都正常,peer 声明 >=0.2.1-0 <0.3.0-0 本身就覆盖 rc.3。
- check-peers 新增 --asar:以前它只校验开发副本 node_modules(可能落在旧代),现在可以直接读已安装桌面端的 app.asar,按里面真实携带的 dsh-* 版本判定 —— 也就是用户真正在跑的那一代。
- --asar 的两个细节:Electron 的 fs 会把 .asar 当压缩包拦截,读取失败时回退到临时副本;asar 头部按 4 字节对齐填充 NUL,要剥掉尾部才能 JSON.parse。

0.6.1 —— 简介和 README 都用中文先说。

- 仓库简介和 npm 简介换成同一句中文:能做和人一样的事,包括且不仅限于 CAD、WPS、浏览器操作、发微信;安全、快速、可靠、聪明、人性化。
- 中文说明移到 README 顶部,英文段落后置并加上 # English 标题,顶部语言链接同步调换。
- 没有代码改动。

0.5.14 —— 坐标契约不再猜「你看到的那张图有多大」。

- 请求管线会在抓取之后再缩一次,而插件看不到这一步:2560×1600 的屏幕上,帧送到模型时是 1610×1006,契约却写着「交付即抓取尺寸……不要缩放」,于是每个读数都偏 1.59 倍。现在契约用部署公布的图片预算预测投影尺寸,并把尺寸和乘数一起写出来:2560x1600 抓取 → 1609x1006 预览,把每个读数乘 1.5911。面积落在预算内的帧会原样送达,所以抓窗口或抓区域能得到真正的 1:1 —— 1365×960 可以,1024×768 可以;4K 和 5K 则分别写 2.2642 倍和 3.0189 倍。

0.5.12 —— 冷树,以及一个什么都没说的空列表。

- Chromium 的无障碍树是懒加载的,而且由第一个来问的人付账:实测重启后在 1.2 秒时只有 3 个元素,下一次调用 0.3 秒时是 77 个。信任第一个答案的人会认定「这窗口没有树」,白白退回像素。现在 controls 在「worker 还没见过完整树、且结果异常少」时会等 300 ms 重试一次。
- 过滤后确实什么都没匹配到时,现在会明说,而不是返回一个读起来像「这里没有树」的空列表:(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them)。

0.5.11 —— 控件树,外加两个让批量操作「不说谎」的修复。

- controls 读窗口的 UI Automation 树,target 点列表里那个控件的精确矩形,于是有名字的控件不必再去像素里找。它不截图,所以也不付 settle。在 673 个元素的窗口上实测:过滤后 0.65 秒、约 850 token(77 个可操作元素),不过滤约 9k。暖 worker 记着它发出去的那份列表,所以某一行换了身份会报错,而不是点到搬进来的那个。
- 不截图的动作也在付 settle(0.5.9)。settle 本是为了「别在重绘中途取帧」,但 desktop_control 每次调用都付。实测不截图的动作用时 814 ms → 12 ms。
- focus 被拒绝却报成功(0.5.9)。那一步只记一条备注、仍然说 ok,于是批量剩下的步骤全打进了真正在前台的那个窗口。现在改成在尝试之后检查真实前台——看状态,不看 SetForegroundWindow 的返回值——不符就报错并说出两个窗口。tolerant: true 可以恢复旧行为,给「坐标反正都对」的调用方留口子。
- peer 范围开始接受 0.2.1 世代(0.5.10)。0.2.1 这个元组需要它自己的预发布比较符:预发布版本只有在某个比较符与它同属一个 major.minor.patch、且自身带预发布标签时才放行。

0.5.8 —— 两个修复,同时包已发布到 npm:@momasiku/dsh-pilot。

- 显示器几何会变陈旧,而且是静默的。 原本显示器清单来自 System.Windows.Forms.Screen.AllScreens,这是静态缓存,只有进程处理 WM_DISPLAYCHANGE 时才刷新;而暖 worker 是没有消息泵的 -NonInteractive 读取循环,缓存永远不会失效。实测:存活 13 小时的 worker 报 <screens>primary WinDisc 0,0,1600,900</screens>,而同一份结果里的 <image_size> 是 1024x768 —— 同一份结果两个答案,而坐标契约正是从这份帧里读的。现在每次截图都用 EnumDisplayMonitors 重新枚举,虚拟屏改用 GetSystemMetrics,两者都不缓存。
Windows 桌面操控。 模型看到真实屏幕,驱动真实鼠标和键盘。

开箱即用时,DeepSeek Harness 会话能推理你的桌面,却碰不到它:你描述屏幕上有什么,你截图,你点击,你汇报结果。dsh-pilot 补上这一环。 模型看着真实像素,然后动手操作:

演示

dsh-pilot 在 AutoCAD 里画一块机械板:模型往命令行里输入 REC 和角点坐标、内轮廓、五个圆、中心线和两个线性标注,然后缩放到范围

一张 CAD 图,由模型画出来。 它驱动 AutoCAD 自带的命令行 —— REC → 0,0 → 240,150,内轮廓、五个孔、中心线和两个标注 —— 而且不在 AutoCAD 里装任何东西。

dsh-pilot 在 LibreOffice Calc 里重命名一列:聚焦工作簿,把指针移到 C1,点击,粘贴「Q3 收入」,按回车,Ctrl+S —— 全在一次 desktop_sequence 调用里完成

一次调用改完一张表。 desktop_sequence 聚焦工作簿,把指针移到 C1,点击,粘贴 Q3 收入,按回车并保存 —— 整条链路只走一次往返。

- 冷启动每次都重付编译开销。 Win32 桥原本每次进程启动都要 Add-Type 编译一次,而且发生在 worker 能回答 ready 之前 —— 正是暖 worker 存在的意义所在的那笔开销。现在按源码哈希编译一次存成 DLL,之后直接加载。一次性抓帧 2638 ms → 813 ms,暖动作仍是 9 ms。改源码会换哈希、换 DLL,缓存绝不会遮蔽后续修复。

npm 上的包名带 scope,是因为 dsh-pilot 这个名字在 npm 上属于另一款无关的浏览器操控插件。仓库名仍然是 momasiku/dsh-pilot。

使用要点

- 先 desktop_control { action: "windows" } 看清有哪些窗口、哪个在前台,再动手。
- 多步任务优先用 desktop_sequence:把「聚焦 → 点击 → 输入 → 回车」写成一次调用。
- 打字支持多行与中文(走剪贴板一次粘贴,之后恢复剪贴板)。
- 想更省 token:capture: "none" 不附图,或依赖「未变化不重发」;想看得更清:rulers: true(默认开)。
- 常用设置(captureRetention、settleMs、stepSettleMs、maxSequenceSteps、useWorker、rulers)写在配置行里,见上面的 Settings 一节。

常见问题

| 现象 | 原因与处理 |
| 提示找不到匹配窗口 | 窗口缩到托盘了,或标题变了:用 action: "windows" 列出来,或改用 hwnd 指定 |
| 点击总是差一点 | 图片被宿主压小了:看 <delivered_scale> 的乘数并乘上去 |
| 点了没反应 | 目标窗口可能是提权窗口,Windows 不允许跨完整性级别注入输入 |
| 升级后工具不见了 | 重启 DSH Desktop:包的代码在加载时重新导入,profile 的插件行在启动时合成 |
| 批次被拒绝执行 | 声明了 medium/high:经用户同意后带 confirm: true 重发,或先用 dryRun: true 看计划 |

License

MIT © 2026 momasiku
dsh-pilot 在 Blender 中建模:清空视口,添加立方体,细分后变成球体,然后旋转模型检查

从零开始建出的 3D 模型。 它清空视口,添加立方体,再用 Ctrl+1 和 Ctrl+3 细分,把立方体变成球体。真实 Blender 4.2,运行在没有 GPU 的机器上——通过 Mesa 的软件 OpenGL 驱动,这套环境由模型自己搭好。

| 工具 | 作用 |
| screen_view | 截取实时桌面,把 PNG 作为真实图像块返回,模型能直接看到——不是 OCR,不是文字摘要 |
| desktop_control | 移动指针、点击、拖拽、滚动、输入、发送按键、聚焦和列出窗口——还能在同一次调用里截取结果 |
| desktop_sequence | 一次调用跑完整批动作,最后只看一眼 |

为什么需要它

看不见屏幕的模型,只能靠别人告诉它屏幕上有什么。碰不到屏幕的模型,只能靠别人告诉它下一步做什么。于是每个桌面任务都变成一场关于桌面的对话,而不是在桌面上干活——用户成了手、成了眼睛,也成了报错信息。

桌面自动化真正贵的地方从来不是鼠标,而是模型和电脑之间要来回说多少次话:

| 一个五步任务:聚焦窗口、点击输入框、输入、按回车、看结果 | 只用 DSH | DSH + dsh-pilot |
| 谁读屏幕 | 你 | 模型,从真实像素读 |
| 谁点击和输入 | 你 | 模型 |
| 模型 ↔ 电脑往返次数 | 5 次(每步一次,加上你那次) | 1 次 |
| 发给模型的截图 | 4–5 张 | 1 张 |
| 模型的时间花在哪 | 等待、反复描述、反复追问 | 思考任务本身 |
| 某步失败时 | 你发现 | 同一份结果会说明是哪一步、为什么 |

用起来是什么感觉

sequenceDiagram
participant U as You
participant M as Model
participant P as dsh-pilot
participant W as Warm worker (PowerShell)
participant D as Your desktop
U->>M: 把第三列改名成 Q3 收入并保存 M->>P: screen_view(window: "报表") P->>D: capture (per-monitor DPI aware)
D-->>M: real PNG + cursor, foreground window, monitor bounds
M->>P: desktop_sequence([focus, click, type, key, key])
P->>W: 5 requests over one pipe — 6–12 ms each W->>D: SetCursorPos, SendInput, clipboard paste
D-->>M: one end frame (with coordinate rulers) + per-step outcome

为什么坐标可信

视觉驱动的点击,最经典的翻车方式就是坐标差一点。dsh-pilot 堵掉了三条会导致这种情况的路:

1. 两端都按每显示器 DPI 感知。 不感知 DPI 的进程会被 Windows 骗:它把 2560×1600 的面板看成 1707×1067,于是按这种截图算出的每次点击都会落错位置。传感器和执行器各自声明先执行 SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2),再做其他事,这样图像的一个像素就对应屏幕的一个物理像素。

2. 帧自带标尺。 上边缘和左边缘每隔 200 px 有一个刻度,每个刻度都标出它所在的屏幕坐标,另外还有一条 origin x,y step n 注释。模型直接从图像上读出位置,不用数像素;而且因为标签是屏幕坐标,无论附件存储怎么缩放,它们都依然有效。

3. 信封说明实际交付了什么。 如果存储把帧重新编码得更小,模型会被告知比例和乘数,坐标约定也随之变成有条件的——只有当交付的帧就是原始捕获时,它才承诺「x 和 y 原样使用」:

<image_size delivered="1974x873" captured="2261x1000">1974x873</image_size>
<delivered_scale>delivered 1974x873 = 0.873x capture; multiply image readings by 1.1454 to get screen pixels</delivered_scale>

为什么快

批量处理是最大的收益(省的是往返次数,不是点击次数)。第二个收益是 PowerShell 不再反复重启:一个常驻的 worker 进程响应每个动作和每一帧。

| 请求 | 一次性脚本 | 常驻 worker |
| 动作(move、click、type 等) | ~0.9–2.0 s | 6–12 ms |
| 捕获(主显示器) | ~1.2–2.3 s | ~0.5 s |

在 2560×1600 面板、150% 缩放、harness UI 正在播放动画的条件下测得。worker 在第一次工具调用时于后台启动,所以它需要的约 1 s 会和第一次捕获重叠,而不是拖慢第一个动作。

相同的帧不会重复发送。 每次捕获都返回 frameHash;当某一帧与同一会话的上一帧逐字节相同时,图像不会再附加(或存储)一次——结果会给出 unchanged: true,模型手上那一帧的坐标依然适用。forceImage: true 可以覆盖这一行为。

读取控件树

截图靠猜:模型在像素里找到按钮,估计它的中心,点击,再截一帧看猜得对不对。Windows 通过 UI Automation 把同一个控件暴露为角色、名称和矩形,于是点击就变成那个矩形的中心,什么都不用看。

desktop_control({ action: 'controls', title: 'Notepad' })
// 4 [Button] '最小化' @ 887,1 45x39 // 14 [Button] '新建会话' @ 12,54 256x24 // 23 [Edit] '搜索会话名称' @ 204,238 4x20 offscreen

desktop_control({ action: 'click', target: 14, title: 'Notepad' })

控件树不是更快的截图,而是另一种取舍。

| | 截图 | 控件树 |
| 一次读取 | ~150 ms,~1k token | ~130-300 ms,~850 token(已过滤) |
| 点击目标 | 从像素估计 | 控件的精确矩形 |
| 状态 | 「看起来是禁用的」 | enabled、offscreen、HasKeyboardFocus、IsSelected、值 |
| 可用性 | 总是可用 | 并非总是可用——见下文 |
每次调用时,树更慢或持平。按任务算则快得多,因为一次读取就能解析多个目标,不必再取一帧,也不必再多一次往返。

它并不总是存在。 画布、绘图表面、自绘列表,以及不对外发布任何内容的 Electron 窗口,给出的树都是空的或没用的。所以截图路径仍然保留,也仍然是兜底方案:先读树,树为空时再取一帧,或者当你需要看画出来的东西而不是声明出来的东西时,再取一帧。

有三件事值得知道。

- 索引指向某个列表中的某一行。点击时会针对一次新读取来解析它,worker 会记住自己发出的那份列表,所以身份已变的行会明确报错,而不是按位置被点中: the control at index 4 is not the one that was listed: expected [Button] 'OK', found [Button] 'Cancel'。
- meaningfulOnly(默认 true)只返回人能操作的 role。在一个真实的 673 元素窗口上,这样是 77 个元素、约 850 token,而不是 9k——其余都是布局面板,没有名字、没有状态、也没有可点的东西。传 false 可拿到整棵树。
- includeValues 默认关闭:结构不是内容。即使打开,密码字段也会被掩码。

当你要找的东西没有自己的树时——聊天记录、画布、自绘面板——别去找它。去找它周围的东西,再做减法:输入框是一个带矩形的 Edit,窗口有矩形,那么聊天记录就是两者之间剩下的部分。或者跳过几何:在树里选中该项,用 HasKeyboardFocus 确认键盘焦点在你想要的位置,输入,按回车。最后这一步是截图做不到的——截图能显示光标,但不能给出承诺。

为什么它是安全的

给模型一双手,是值得做对的部分:

- 风险先声明,再强制执行。 低风险(默认)可自由批量执行。声明为 medium/high 的批次,或带有标记 risk: "high" 步骤的批次,会在任何东西碰到桌面之前被拒绝,除非调用方传入 confirm: true——而调用方只会在你同意之后才这么做。
- dryRun: true 会校验批次并列出计划,但不执行。
- 首次失败即停止(默认):被拒绝的 focus 之后,绝不会再往碰巧在前台的任何窗口里输入。
- 定向输入。 当某一步指定了窗口(hwnd),effector 会在注入前立即把它重新置为前台,所以点击不会落到相邻窗口。
- 未知结果绝不静默重试。 如果请求已送达后 worker 挂了,该动作会报告为失败,而不是重复执行——它可能已经生效了。截图没有副作用,所以它会重试。
- 中断有效。 按 Esc 会写入取消 token,动作循环在每一步之间轮询它(拖拽期间、等待稳定期间、截图之前),而拖拽在 finally 块中释放按钮,因此中断的回合不会让鼠标保持按下状态,也不会让剩余的点击继续执行。
- 它不会谎报权限。 Windows 拒绝向更高完整性(提权)窗口注入输入;工具会报告该失败,而不是假装成功。
- desktop_sequence 有上限,由 maxSequenceSteps 控制(默认 24)。

安装

from npm (recommended)
dsh plugin --profile <name> add @momasiku/dsh-pilot

or from the repository
dsh plugin --profile <name> add github:momasiku/dsh-pilot

or this repository's prebuilt tarball, which skips the build-approval step
dsh plugin --profile <name> add https://github.com/momasiku/dsh-pilot/releases/latest/download/dsh-pilot.tgz

or a checkout you already have
dsh plugin --profile <name> add "file:E:\path\to\dsh-pilot"

下载: npm 包 ·预构建 tarball ·源码

按带作用域的名称安装。 dsh plugin add 会先询问 npm registry,而 npm 上不带作用域的名称 dsh-pilot 属于一个无关的浏览器自动化插件——所以请按 @momasiku/dsh-pilot 安装,或按上面所示用仓库 / tarball 安装。

包自带 loader 行,因此 plugin add 会把 @momasiku/dsh-pilot 放进 dsh.profile.bundles,该行来自包(dsh.bundle.patch → cordis.patch.yml),而不是手写进你的补丁层。之后重启桌面应用一次,然后开始新会话:三个工具会加入工具列表。

要求 DSH 0.1.5-rc.2 到 0.2.x——即 package.json 中的 peer 范围,也是 dsh plugin add 在安装任何东西之前检查的内容。要求:Windows、PowerShell 5.1 或 7,以及声明图像输入能力的模型路由(deepseek-flash 支持)。

最近变更

0.6.5——接管提示词终于可见了:像素正确,停留时间也看得见。

- 覆盖层没有声明 DPI 感知,所以在 150% 缩放下,它按 1707×1067 屏幕布局,而不是 2560×1600:标题大了半倍,光标光环也画得偏离指针。现在它在 WPF 加载前声明 per-monitor v2,并按真实像素布局,所以光环落在指针上。
- 隐藏延迟现在是设置项(takeoverLingerMs,默认 8 秒),而不是硬编码的两秒:单个动作足够读完,Esc 仍可随时取消。
- 覆盖层脚本中关于宿主两秒停留的注释现在会指明该设置。

0.6.4——接管提示词按轮次保持,而不是按动作。

- 它过去在每个动作时切换,所以一连串小步骤会闪烁。 现在一轮只显示一次,并在最后一个动作两秒后隐藏;新动作会取消该隐藏。
- 首帧大约需要 0.7 秒(按要求不做预热)。比这更短的单次操作仍可能错过第一帧——这就是不预热的代价。
- 修复了启动竞态:脚本末尾的 Show()/Hide() 与定时器第一次真正显示撞在一起,导致第一次操作记录了两条显示日志。句柄已经由 EnsureHandle() 创建好了,所以那两行是多余的。
- Test-Path 的瞬时失败不再隐藏浮层:宿主每次操作都会以截断写入的方式重写 show.txt,检查正好落在这个窗口里就会读到 false。现在要连续三次未命中才算数。
- 单实例:一个状态目录只归一个进程所有,所以宿主重启后遗留的浮层不会和新浮层抢同一块屏幕。
- 新增 tools/overlay-smoke.ps1:检查首帧延迟、一轮之内不重复显示、结尾恰好隐藏一次,以及第二份副本必须退出。

0.6.3 —— 两个真实的 controls bug,都来自 issue #1 和 #2 的反馈。

- 结构化输出里有,模型拿到的文本里没有:controlText() 只写了 action / cursor / foreground,把 controls 和 windows 整个丢掉了——于是 README 里说的「廉价路径」对纯文本模型不可用,只能退回截图。现在两者都渲染成 <controls> / <windows> 块,带上窗口名和数量。
- meaningfulOnly 比较了未赋值的 $name:$null.Length -eq 0 永远为真,所以 ListItem、TreeItem、MenuItem 和 DataItem 行不管有没有名字都被丢掉。现在读的是元素自己的名字,这也正是它上面那行注释一直说的。
- 白名单里漏了 Pane:原生 Win32 对话框(#32770)的每个子元素都经 MSAA 桥接,报告为 Pane,于是一个满是按钮的对话框被过滤得干干净净。现在有名字的 Pane 算作控件;没名字的仍算布局。
- 过滤不再悄无声息:新增 controlTotal 和 filteredOut,并渲染进文本。在「运行」对话框上实测:10 个元素,保留 6 个(确定 / 取消 / 浏览 / 提示文字),丢掉 4 个无名 Pane。
- 回归测试:script-smoke 现在会真的弹出 Win+R 对话框,断言在 meaningfulOnly: true 下结果非空且包含「确定」「取消」,然后用 Esc 关掉。

0.6.2 —— 兼容性声明现在放到了能被验证的地方。

- 在 DSH 0.2.1-rc.3 上验证通过:screen_view、windows、controls 和三步 desktop_sequence 都在上面跑通了,而声明的 peer 范围 >=0.2.1-0 <0.3.0-0 本来就覆盖 rc.3。
- check-peers --asar:这项检查以前只看 node_modules 里的开发副本,它可能比用户实际运行的应用老一代。现在读取已安装桌面应用的 app.asar,按那份构建真正携带的 dsh-* 版本做判断。
- 这个 flag 要处理两个细节:Electron 打过补丁的 fs 会把任何 .asar 路径当成归档文件,拒绝读取字节,所以读取失败时回退到临时副本;另外 asar 头部会用 NUL 填充到四字节边界,所以 JSON.parse 之前必须去掉这些填充。

0.6.1 — 描述和 README 都以中文开头。

- 仓库描述和 npm 描述现在是同一句中文:一个人能做的事——CAD、WPS、浏览器、微信——以及安全、快速、可靠、智能、人性化。
- 中文部分移到 README 顶部;英文部分跟在 # English 标题下,顶部的语言链接也做了对调。
- 没有代码改动。

0.6.0 — 接管线、点亮的指针,以及不再隐藏字符的标题。

- The screen says who is driving. While an action runs, one line of large
translucent type names the operator and the way out — DSH 正在操作你的电脑 · 按 Esc 取消 — and a halo follows the cursor. No border: the first version drew one, and at 2560x1600 it was either invisible against a blue wallpaper or heavy enough to feel like a frame. Sizes come from the virtual desktop, so a 4K wall and a 1080p laptop get the same visual weight.
- Esc cancels the batch, not the step. The overlay stamps the cancel token the
action loop already polled; that token now belongs to the whole desktop_sequence,
so a wait in the middle is interrupted too, and the result says why:
the person pressed Esc; the batch stopped where it was. Measured: a 12 s batch with
Esc at 4.5 s returned after 7.1 s.
- Titles are compared after stripping zero-width characters. Edge's own title has
a U+200B inside Microsoft Edge; focus "Microsoft Edge" failed with "no visible
top-level window matches" on a window the person could read perfectly.
- focus accepts an hwnd on its own, as the description always promised.
- Every helper script must keep its UTF-8 BOM — script-smoke fails if one loses it, because PowerShell 5.1 reads a BOM-less file as ANSI and the Chinese caption renders as mojibake. This caught three other scripts that had already lost it.
- The pointer halo is drawn, never written: no SetCursorPos and no input hook, which
is why the ring never makes a person's own mouse feel laggy.

- A takeover line and a lit pointer. While an action runs, one line of large
translucent type names the operator and the way out — DSH 正在操作你的电脑 · 按 Esc 取消 — and a halo follows the cursor. No border around the screen: the first version drew one, and at 2560x1600 it was either invisible against a blue wallpaper or heavy enough to feel like a frame. The type and the halo say the same thing without taking the desktop hostage. Sizes come from the virtual desktop, so a 4K wall and a 1080p laptop get the same visual weight.
- Esc cancels the batch, not the step. The overlay stamps the same cancel token the
action loop already polled; that token now belongs to the whole desktop_sequence,
so a wait in the middle is interrupted too, and the result says why:
the person pressed Esc; the batch stopped where it was. Measured: a 12 s batch with
Esc at 4.5 s returned after 7.1 s.
- The pointer is lit, not taken over. A halo tracks the cursor while the ring is up,
so it is obvious the movement is not yours. It is drawing, not the pointer: Windows
paints the real cursor above every window, so the halo sits under it and the pointer
reads as larger and lit without being replaced — and without the cursor being written, which is what makes other agents' takeovers feel laggy. The halo lives in its own small window rather than as a child of the ring: a child Grid with a runtime margin never painted on this layered surface, while a window that is simply moved does.
- Fully permissioned sessions are not asked again — the ring is the statement, not a prompt. takeover: false turns it off.

0.5.14 — the coordinate contract stops guessing how big the image you see is.

- The request pipeline resizes again after capture, and this plugin never saw
that step: on a 2560x1600 display the frame reached the model as 1610x1006 while
the contract said "delivered at capture size … do not rescale", so every reading taken off it was 1.59x off. The contract now predicts the projected size from the image budget the deployment publishes, and prints the size and the multiplier: 2560x1600 capture -> 1609x1006 preview, multiply every reading by 1.5911. A frame whose area fits the budget arrives untouched, so framing the window or a region gives a true 1:1 frame — 1365x960 does, 1024x768 does, 4K and 5K say 2.2642x and 3.0189x instead.

0.5.12 — a cold tree, and an empty list that explained nothing.

- Chromium builds its accessibility tree lazily, and the first caller pays for it:
measured on the harness window right after a restart, 3 elements at 1.2 s and 77
elements at 0.3 s on the next call. A caller that trusts the first answer concludes
the window has no tree and falls back to pixels for nothing. controls now retries
once, 300 ms later, when the worker has not yet seen a real tree and the answer is
suspiciously small.
- A filtered listing that legitimately matches nothing now says so, instead of
returning an empty list that reads as "this window has no tree":
(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them).

0.5.11 — the control tree, and two fixes that make a batch honest.

- controls reads the window's UI Automation tree, and target clicks a listed
control's exact rectangle, so a named control no longer has to be found in pixels.
It takes no screenshot, so it pays no settle. Measured on a 673-element window:
0.65 s and ~850 tokens filtered (77 operable elements) against ~9k
unfiltered. The worker remembers the listing it handed out, so an index whose row
changed identity fails instead of clicking whatever took its place.
- The settle was paid even when nothing was captured (0.5.9). It exists so a frame
is not taken mid-repaint; desktop_control applied it on every call, capturing or
not. Measured 814 ms -> 12 ms for a non-capturing action.
- A refused focus was reported as success (0.5.9). The step recorded a note, said
ok, and the rest of the batch went to whichever window was actually in front. It
now checks the foreground window after the attempt — the state, not SetForegroundWindow's return value — and fails, naming both. tolerant: true restores the old behaviour for a caller whose coordinates are right either way.
- The peer range now accepts the 0.2.1 host generation (0.5.10). The 0.2.1 tuple
needed its own prerelease comparator: a prerelease only satisfies a range when a
comparator carries a prerelease tag on the same major.minor.patch.

0.5.8 — two fixes, and the package is now on npm as @momasiku/dsh-pilot.

- The monitor geometry could go stale, and silently. The inventory came from
System.Windows.Forms.Screen.AllScreens, a static cache that only refreshes
when the process pumps WM_DISPLAYCHANGE. The warm worker is a
-NonInteractive read loop with no message pump, so nothing ever invalidated
it. Measured: a worker alive 13 hours reported
<screens>primary WinDisc 0,0,1600,900</screens> while the same result carried
<image_size>1024x768</image_size> — two answers to one question, inside the very frame the coordinate contract is read from. Monitors now come from EnumDisplayMonitors per capture and the virtual screen from GetSystemMetrics; neither caches.
- The cold start paid for the bridge every time. The Win32 bridge was compiled
with Add-Type on every process start, before the worker could answer
ready — exactly the cost the warm worker exists to stop paying twice. It now compiles once into a DLL keyed by a hash of its source, then loads that. One-shot capture 2638 ms → 813 ms; warm actions unchanged at 9 ms. A
source edit changes the hash and therefore the DLL, so a stale cache can never
shadow a later fix.

The npm name is scoped because dsh-pilot on npm belongs to an unrelated
browser-automation plugin. The repository is still momasiku/dsh-pilot.

Tools

screen_view

| parameter | type | meaning |
| screen | string | primary (default), all (whole virtual desktop), or a 0-based monitor index |
| window | string | case-insensitive substring of a window title or process name; frames that window |
| region | string | crop as x,y,width,height in physical screen pixels |
| includeCursor | boolean | draw a crosshair at the pointer (default true) |
| rulers | boolean | draw the coordinate rulers (default: the rulers setting) |
| forceImage | boolean | attach the frame even when it repeats the previous frame of the session |

The result carries the image plus a <screen_capture> envelope: delivered size,
capture size when they differ, physical origin, scale, every monitor's bounds, cursor
position, and the foreground window (title, process, class, bounds) — then the coordinate contract for that frame.

desktop_control

| parameter | type | meaning |
| action | string, required | click, doubleClick, rightClick, middleClick, move, drag, scroll, type, key, focus, windows, controls |
| x, y | integer | target in physical screen pixels; required by the pointer actions and by scroll-at-a-point |
| toX, toY | integer | drag end point, or a nonzero value to make scroll horizontal |
| text | string | text to insert for type |
| key | string | a character, enter/tab/esc/f5, or a chord like ctrl+s, alt+tab, win |
| amount | integer | wheel notches for scroll (default 3, positive scrolls up) |
| button | string | left (default), right, middle |
| title | string | window title or process substring; required by focus, filters windows |
| hwnd | string | lock input to one window handle (0x1094C or decimal) |
| capture | boolean | capture a fresh frame after acting (default true) |
| settleMs | integer | wait before that capture so the screen can repaint (default 750) |
| forceImage | boolean | attach the frame even when it repeats the previous frame of the session |
| target | integer | pointer actions: click the control at this index from a controls listing instead of x/y |
| includeValues | boolean | controls: also return each control's value (default false; passwords masked) |
| meaningfulOnly | boolean | controls: only operable roles (default true) |
| tolerant | boolean | focus: record a note instead of failing when Windows refuses the foreground |

type delivers text as one paste (clipboard set, Ctrl+V, clipboard restored), so
applications see a single insertion — multi-line text and CJK included — instead of a burst of per-character keystrokes. windows returns every visible titled top-level window with its bounds, minimized state, and which is foreground: useful before clicking anything.

controls returns the window's control tree as one line per control — index [Role] 'Name' @ left,top widthxheight plus disabled/offscreen/value — and takes no
screenshot (pass capture: true if you want one anyway). Feed an index back as
target to click that control's exact centre. See Reading the control tree above
for when a tree exists and when a frame is still the right tool.

desktop_sequence

| parameter | type | meaning |
| steps | array, required | ordered steps; each is one desktop_control action without its own screenshot, plus optional settleMs, risk, riskNote; action wait takes ms |
| risk | string | low (default) / medium / high — the risk you declare for the whole batch |
| confirm | boolean | required for a medium/high batch; without it the tool refuses before touching the desktop |
| dryRun | boolean | validate the batch and list the plan without executing anything |
| capture | string | end (default) captures one frame after the last step, none skips it |
| target | string | window (default: the window that was foreground during the batch), screen, or all |
| window | string | frame this window instead (title/process substring), overriding target |
| region | string | crop the end frame as x,y,width,height in physical pixels |
| forceImage | boolean | attach the end frame even when it repeats the previous frame |
| stopOnError | boolean | stop at the first failing step (default true) |
| settleMs | integer | delay before the end capture so the screen can repaint |

Every step comes back with its own action, duration in milliseconds, and outcome — so one call still tells the model exactly where a batch went wrong.

Settings

- insert:
- id: pilot
name: dsh-pilot
config:
captureRetention: 30 # frames kept under .dsh-pilot/
timeoutMs: 20000 # per-call kill deadline for a helper
settleMs: 750 # default delay before the automatic capture
stepSettleMs: 120 # per-step delay inside a desktop_sequence
maxSequenceSteps: 24 # hard cap on the steps of one batch
alwaysSaveFile: true # keep the PNG even when no image block is attached
useWorker: true # keep one PowerShell process warm (6-12 ms actions)
rulers: true # draw coordinate rulers on every frame

How it works

flowchart LR
subgraph JS["lib/index.js (the plugin)"]
T1[screen_view] --> C[capture]
T2[desktop_control] --> A[runAction]
T3[desktop_sequence] --> A
C --> R[frame hash + dedup + envelope]
end
C -->|warm request| W[desktop-worker.ps1]
A -->|warm request| W
C -.->|fallback| PB[desktop-probe.ps1]
A -.->|fallback| AB[desktop-action.ps1]
W --> WC["_dsh-win32.ps1 · _dsh-action.ps1 · _dsh-capture.ps1"]
PB --> WC
AB --> WC

Two halves, one coordinate space: the sensor reports the geometry, the effector
consumes the same physical pixels, and GetCursorPos/SetCursorPos report back, so
the loop is self-checking.

The shared code sits in _dsh-win32.ps1 (the Win32 bridge and DPI awareness,
idempotent), _dsh-action.ps1 (Invoke-DshAction) and _dsh-capture.ps1
(Invoke-DshCapture). The one-shot scripts stay as thin bootstraps — they are what the plugin falls back to when the worker cannot be used. The worker speaks one base64 JSON request per line, answers with one JSON line per request, and keeps serving after a failed action, a failed capture, or a cancelled step.

Captured frames land in <session cwd>/.dsh-pilot/ and are pruned to
captureRetention. Pruning never breaks an image the conversation already carries,
because attachments are stored content-addressed.

Troubleshooting

| symptom | cause and fix |
| no visible top-level window matches "..." | the window is hidden to the tray or the title changed; desktop_control { action: "windows" } lists what is really there, and hwnd targets a window that has no title match |
| clicks land next to the target | the frame was delivered smaller than the capture — check <delivered_scale> and apply the multiplier |
| a click does nothing | the target window may be elevated; Windows refuses input injection across integrity levels |
| the tools are missing after an update | restart the desktop app: a package's code is re-imported on load, and the profile's plugin rows are composed at boot |
| a batch refuses to run | it was declared medium/high (or contains a risk: "high" step): re-send with confirm: true after the user agreed, or dryRun: true to see the plan |

Developing

The smoke tests load the plugin against the real @deepseek-ai/dsh-tools from the
desktop checkout, so they exercise every schema and renderer without a harness restart:

the package resolves @deepseek-ai/* from the desktop app's node_modules
New-Item -ItemType Junction -Path node_modules\@deepseek-ai
-Target "D:\AGAENT\DSH Desktop\resources\app\node_modules\@deepseek-ai"

node tools/smoke.mjs "D:\AGAENT\DSH Desktop\resources\app" # load, schemas, renderers
node tools/render-smoke.mjs "D:\AGAENT\DSH Desktop\resources\app" # coordinates: 1:1 vs shrunk, contract, rulers
node tools/sequence-smoke.mjs "D:\AGAENT\DSH Desktop\resources\app" # end-to-end batch on a throwaway window
powershell -NoProfile -ExecutionPolicy Bypass -File tools\script-smoke.ps1 # PowerShell half + worker protocol

sequence-smoke.mjs drives a real batch against a throwaway window it creates
itself (never one of your applications) and the window writes back what it received —
so "focus, then type, then Enter" is proven to have happened in that order, not
merely attempted. script-smoke.ps1 drives the PowerShell half directly: the one-shot
paths, the worker handshake, warm latency, a failing action, a cancellation, a failing
capture, and that the worker survives each of them. The last two need a full-access
shell: the plugin reads its helpers through pipes, which a restricted sandbox denies
with EPERM.

The helper scripts can be driven by hand while debugging — they take a UTF-8 JSON params file and print one JSON result:

'{"out":"E:\tmp\shot.png","screen":"primary","rulers":true}' | Set-Content params.json -Encoding utf8
powershell -NoProfile -File lib\scripts\desktop-probe.ps1 -ParamsPath params.json
powershell -NoProfile -File lib\scripts\desktop-action.ps1 -ParamsPath params.json

Keep the .ps1 files saved with a UTF-8 BOM: Windows PowerShell reads BOM-less
files as ANSI, which mangles non-ASCII window titles and paths.

Reinstalling after an edit

dsh plugin add file:... installs a copy, and a repeat add of an unchanged
file: spec reports "Already up to date" without re-copying even when the files on
disk changed. After editing, copy the changed files into the installed package
yourself and restart the desktop app:

$src = "E:\path\to\dsh-pilot"
$dst = "$env:USERPROFILE\.dsh\profiles\<profile>\node_modules\dsh-pilot"
Copy-Item "$src\lib\index.js" "$dst\lib\index.js" -Force

Why the smoke test validates result shapes

A tool result carrying a key its closed output schema does not declare is rejected by
the runtime after execute() has already performed the side effect — so the action happens yet the caller sees an error. A schema-only check cannot see that failure mode, so smoke.mjs validates a realistic result object against each tool's compiled output schema, and additionally asserts that the validator itself catches the undeclared ok/actedAt keys that shipped once.

License

MIT © 2026 momasiku
预览分享卡片长什么样 ↓momasiku/dsh-pilot
分享 · DeepSeek Harness Hub
momasiku/dsh-pilot

DeepSeek Harness 的桌面自动化:给整台 Windows…

回到顶部