Repository navigation
Releases
v1.4.0 —— 等待超时可配置 + 可选超时后放行
新增
等待超时可配置,并且可以选「超时后放行」
| 设置 | 默认 | 说明 |
|---|---|---|
confirmTimeoutSeconds |
300 |
没人回答就等这么多秒。0 = 不超时,一直等你回答。范围 0–86400。 |
confirmAllowOnTimeout |
false |
超时后是否放行。默认 false = 超时视为不同意。 |
两个设置都跟着 confirmInput 一起在面板上禁用(没开闸门时它们没有意义),
而「超时后放行」在等待秒数为 0 时额外禁用(永远不会超时,它就没有意义)。
确认弹窗里写明了超时会怎么处理
允许吗?(300 秒内不回答按「不允许」处理)
或者,等待秒数为 0 时:
允许吗?(会一直等你回答)
要不要忽略这个弹窗,取决于忽略之后会发生什么 —— 不说清楚就等于让人盲签。
安全说明:confirmAllowOnTimeout: true 是 fail-open
它的含义就是:你不回话,智能体就自己动你的键鼠。
我把它做成显式的、默认关闭的,并且面板上给的是红底警告(其他提示都是黄底):
「超时后放行」已打开:超过等待时间没人回答时,智能体会直接执行这次输入,
而你不会收到任何提示。你不在的时候,它可以一直这样动你的键鼠。
另外两条边界:
- 超时放行绝不记会话授权。 没有任何人同意过,不该留下长期许可;它只作用于这一次调用。
- 调用方中止(工具超时 / 你取消调用)不等于「等待超时」。 如果把两者混为一谈,
那么在配了「超时后放行」时,只要工具超时就能拿到放行 —— 工具超时于是成了绕过闸门的后门。
这条有专门的测试盯着(见下 ④)。
超时放行是有痕迹的
超时放行最麻烦的地方在于它是静默的:人不在,事情照样发生,事后什么都看不出来。
所以面板的状态区会显示:
已因超时放行 3 次(你不回话时它自己动的)
这是你回来时唯一的线索。
变更:工具超时的算法
开着闸门时,输入工具的 timeoutMs 从固定 600000 改为
confirmTimeoutSeconds × 1000 + 60000。必须让闸门自己先按配置下结论,
否则工具超时先掐断调用,「超时后放行」永远轮不到执行机会。
等待秒数为 0(不超时)时给 24 小时,相当于「一直等」。
顺带纠正一个早先的误解:工具超时没有全局上限。
dsh-tool-call-timeout-policy 的文档写着「Each tool supplies its own limit;
the package has no configuration」。所以原来的 600000 是我照 run_code 的 schema 抄的,
不是宿主限制。
超时后还会把弹窗撤回(abort 自己持有的 signal)—— 否则界面上会一直挂着一个
早已按超时下了结论的问题,用户再点就成了「迟到的回答」。
测试
| 之前 | 现在 | |
|---|---|---|
| 宿主 | 54 | 61 |
| 客户端 | 28 | 30 |
新增四个真跑时序的超时用例(用真实计时器,不是 mock):
- 超时 + 默认 → 拒绝,并断言真的等满了窗口、问题被撤回(signal 已 abort)、
没留下会话授权。 - 超时 + 放行 → 执行,计数为 1,且绝不记会话授权。
- 等待秒数 0 → 不超时,结论仍来自回答本身。
- 调用方中止不被误当成等待超时 —— 故意在
allowOnTimeout: true下触发调用方中止,
断言它走「问不到人」那条并拒绝。
另新增两个新字段的夹取(0 要保留、越界收口、取整、非法值回落)与
「非布尔必须回落 false」 —— 这个开关绝不能被一个意外值打开。
面板侧:三个新字段与禁用联动、等待策略文案(不超时 / 视为不同意 / 放行)、
超时放行次数可见、fail-open 警告必须是错误色 + role=alert。
升级
需要重启 DSH。重启后插件页标题右侧显示 v1.4.0。
注意:升级后默认等待窗口是 300 秒(此前硬编码是 600 秒),超时仍视为不同意 ——
和你现在的行为一致,只是窗口短了一半。想改就在面板「安全闸门」里调。
完整变更:v1.3.2...v1.4.0
v1.3.2 —— 真机联调测出的两个授权显示问题
这次的 1.3.2 很特别:它是靠真机联调测出来的,不是靠单元测试。
把 confirmInput 打开、在真实 DSH 上把闸门五个分支全走了一遍,才暴露下面两个问题 ——
单元测试全都绿的,因为它们测的是「我的逻辑对不对」,测不到「真实 id 长什么样」。
修复
1. 面板上所有会话授权都显示成 session-…
grantList() 取 id.slice(0, 8),而真实会话 id 形如:
session-85555e38-17f8-4cd3-8a4a-9d13c88315a9
└──────┘
前 8 位正好全是这个每条都一样的固定前缀
所以面板上每条授权都显示「session-… · 时间」,完全分不清哪条是哪条。
新增 shortSessionId():先剥掉 session- 前缀再截 → 85555e38。
顺带发现子智能体的会话 id 没有这个前缀(实测是 83fe953e-3768-…),
所以两种形状都要能处理 —— 只看一种就会在另一种上重犯同样的错。
2. 问不到人时的提示指向了错的会话
子智能体收到的原文是:
会话 83fe953e 还没拿到「允许本次会话所有操作」授权,而子智能体自己弹不出窗问人。
但该去授权的是父会话。照着这句话行动会走错方向 —— 而且其实无解,因为
授权只能由人点,子智能体根本弹不出窗。
新增 describeGrantGap(),改成:
会话 83fe953e 与它的父会话 85555e38 都没有「允许本次会话所有操作」授权,
而子智能体自己弹不出窗问人 —— 需要用户在主会话里授权,子智能体才能跟着用。
顺带在真机上确认了两件之前只能靠推理的事
-
state.consent.service === true—— 宿主的userQuestions确实能从插件的根 ctx
用ctx.get()拿到。这是整个设计里唯一没法离线验证的一环(如果拿不到,开着闸门会让
五个输入工具全部失败)。现在确认了。 -
子智能体的会话头实测为:
{ "id": "83fe953e-3768-41d1-b699-15786868584f", "origin": "subagent", "parentSession": "session-85555e38-17f8-4cd3-8a4a-9d13c88315a9", "delegationDepth": 1 }parentSession正好是主会话 id —— 所以「子智能体继承主会话授权」这条规则在真机上
接得上,不是纸面推导。我原来的单元测试只验了自己的逻辑,验不到 DSH 到底往这个字段里填了什么。
真机测试结果(v1.3.0 host 上的五个分支)
| 场景 | 你的操作 | 结果 |
|---|---|---|
| 第 1 轮 | 点「不允许」 | 拦下,什么都没执行 ✓ |
| 第 2 轮 | 点「允许本次操作」 | 放行,且 grants 保持 0(不留会话授权)✓ |
| 第 3 轮 | 点「允许本次会话所有操作」 | 放行,grants 变成 1 条 ✓ |
| 第 4 轮 | 什么都不点 | 没有弹窗,直接放行 ✓ |
| 第 5 轮 | 撤销授权后再调 | 弹窗回来了 ✓ |
| 子智能体 | 无人类可问 | 在征求同意阶段被拒绝(fail-closed)✓ |
第 4 与第 5 轮构成了一个对照实验:同一个调用、同样的参数,有授权就静默通过,
撤销后就重新开始问 —— 这排除了「第 4 轮只是碰巧没弹窗」的可能。
测试全程用一个零风险哨兵:window_activate 的目标窗口标题是编造的,
所以无论闸门放行还是拦下,都走不到真正的输入代码 —— 期间桌面毫发无损。
测试
宿主断言组 51 → 54:shortSessionId(两种真实 id 形状)、grantList 的短名、
describeGrantGap(子智能体 / 普通会话 / 无会话身份三种情况)。
升级
需要重启 DSH。重启后插件页标题右侧应显示 v1.3.2
(同时也带上了 1.3.1 那个「保存后界面立刻同步」的修复)。
完整变更:v1.3.0...v1.3.2
v1.3.1 —— 修复保存后界面不同步
修复:点了开关显示「已保存」,开关却纹丝不动
Field / BoolField 把 /settings 的响应整个丢掉了,界面只能等下一次轮询
(默认 30 秒)才更新。用户看到的是「已保存」但开关没动,退出重进设置页才显示新值
—— 因为那是重新拉了一次 /state。
文本框是同一个根因,只是症状不同:dirty 一直挂在 true 上,「未保存」不消失。
所有走 /settings 的控件都中招,不只是开关。
修法(两处,缺一不可)
-
/settings回一份完整状态快照(buildState()),不再只回
effective/defaults/ …。派生字段(工作区标题、解析出来的目录、回收站统计、
清理预览)根本不在原来的响应里,所以它们也必须一起回。这样客户端也不必再写
逐字段的合并逻辑 —— 少一处可以漏的地方。 -
面板新增「保存后广播」订阅:保存成功即把响应并回面板状态,并且作废在途的轮询
(否则一个更早发出的/state可能在保存之后才回来,把刚写进去的值盖回旧的
—— 那就变成了「有时候灵有时候不灵」)。
用订阅而不是给每个字段传 prop,是因为「保存后同步界面」这件事每个字段都必须做,
而漏掉一个的症状是静默的。订阅让新增字段自动获得这个行为 —— DangerZone 里嵌套的
那个字段正是传 prop 最容易漏掉的一处。
顺带:保存后显示的是宿主重算过的值
不是本地乐观值。所以非法输入被夹取(pollSeconds 填 99999 → 600)会立刻如实显示,
不用等半分钟;保存失败时开关也不会自己翻过去,而是显示宿主的错误。
测试脚手架修正:迷你 React 改成每个组件一份 hook 状态
以前整棵树共用一个扁平数组,于是「条件渲染掉一个带 hook 的子组件」会让它后面所有组件的
槽位整体前移,读到别的组件的状态 —— 测试里真的崩在 DangerZone 的 typed.trim() 上,
因为 typed 读到了相邻组件的 null。
真实 React 是按组件隔离 hook 的,所以那是脚手架在说谎,不是产品 bug。
但它会挡住正当的测试(比如测 enabled 开关),所以必须修。
测试
| 之前 | 现在 | |
|---|---|---|
| 宿主 | 51 | 51 |
| 客户端 | 25 | 28 |
新增 [J] 保存后界面立即同步(回归):切换开关后界面必须立刻反映新值
(且重渲染不产生额外请求)、保存文本框后要显示宿主夹取后的值且「未保存」消失、
嵌套在 DangerZone 里的字段同样同步、保存失败时开关保持原状并显示宿主错误。
该回归验证过:把
broadcastSettingsSaved(body)注释掉,测试立刻以
「保存后开关必须立刻反映新值」失败。
升级
插件安装后需要重启 DSH 才加载。升级请先「卸载」再装,然后重启。
重启后插件页标题右侧会显示 v1.3.1 —— 没看到就说明新构建还没生效。
完整变更:v1.3.0...v1.3.1
v1.3.0 —— 操作前征求同意(默认关)+ 会话级授权与撤销
新增:操作前征求同意
confirmInput(默认关闭)。打开后,五个输入工具每次调用都先停下来问你一次,三选一:
| 选项 | 效果 |
|---|---|
| 允许本次操作 | 只放行这一次,下次还会问 |
| 允许本次会话所有操作 | 本会话内不再询问;授权只放内存,可在面板撤销,DSH 重启即失效 |
| 不允许 | 不做任何事,工具返回错误 |
受保护的是 window_activate / mouse_click / type_text / key / scroll。
screenshot 与 window_list 是只读的,不在闸门内。
弹窗里会写清楚要做什么
人工确认的前提是人能判断,所以弹窗带着这次调用的真实参数:
智能体要执行一次桌面输入操作:
type_text
向当前焦点窗口输入 24 个字符:
rm -rf ./build && pnpm run build
允许吗?
mouse_click 显示坐标与按键、key 显示组合键、window_activate 显示目标窗口标题。
type_text 超 400 字截断并注明原长度。只给一个工具名等于逼人盲签。
拿不到同意就不执行
闸门的每一条失败路径都是拒绝,没有一条会「算了就放行」:
| 情况 | 结果 |
|---|---|
| 你点了「不允许」 | 拒绝 |
| 答案认不出来(未知选项 / 跳过不答 / 只填了自定义文字) | 拒绝 |
宿主没有 userQuestions 服务 |
拒绝,并说明原因 |
| 弹出去了没人答,直到超时 | 拒绝(上限 10 分钟) |
| 调用方是子智能体,而主会话没授权过 | 拒绝 |
子智能体那条要解释一下:它自己弹不出窗问人 —— DSH 的 userQuestions 只允许 runtime root
发起人类交互,被父智能体拥有的子智能体没有人类应答方,硬问会永远挂住。所以它走继承:
主会话点过「允许本次会话所有操作」就放行,否则拒绝。既不给闸门留绕过路径,
也不至于主会话授权后子智能体还是动不了。
已知边界:继承只往上认一层(
parentSession)。更深层的后代拿不到授权,会被拒绝。
撤销授权
面板「安全闸门」区块显示已授权的会话(短 id + 时间)与「撤销全部授权」按钮。
新增 POST /api/dsh-bundle-screen-view/consent(给 sessionId 则只撤那一个)。
- 改
confirmInput(无论开还是关)都会清空全部授权 —— 关掉再打开不会悄悄继承上一次的许可; - 改其它设置不动授权;
- 停用或卸载插件时授权一并清空;
- 授权只放内存,重启从零开始。
设计说明:为什么不用 DSH 原生的 ctx.approval
DSH 确实有审批接缝,但它的结果词汇表是固定的:
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
export type ApprovalPolicy = 'ask' | 'never';只有「允许这一次」和「拒绝」,没有「允许本会话」。 这不是没实现,而是官方明确列为延期:
「the outcome vocabulary has allowed-once but no allow-always, remembered rule,
revocation, or grant store」。
而且它受会话审批策略约束:策略为 never 时任何审批请求都被确定性拒绝 ——
闸门接在它上面,在 never 下会让五个输入工具全部失效,等于废掉。
ctx.userQuestions 的文档写明适用于权限流程("a tool or permission flow"),
支持任意选项列表,也不受审批策略影响。
代价说清楚:走 userQuestions 就绕过了 approval/asked / approval/decided 审计事件,
决策不进 DSH 的审批日志。本插件用面板上的授权列表与撤销入口做可见性补偿。
修复:面板上的一句假话
风险提示原先写死「本插件没有确认后执行机制 —— 权限允许时不会弹窗征求同意」,
加了这个闸门之后那句话就不成立了。现在文案跟着闸门状态走,分成「没开闸门」与「已开闸门」
两条;并补了一条警告:宿主没有 userQuestions 时开启闸门会让五个工具全部失败。
修复:一个从来没渲染过的版本号
versionChip 样式其实一直都在,但没有任何地方用它,buildState 也不报 version。
宿主把客户端 bundle 缓存在内存里直到重启,没有这个信号就没法确认新构建到底加载了没有。
现在 /state 报 version(读 package.json,不写死),面板标题右侧渲染小标。
其它变更
- 开着闸门时五个输入工具的
timeoutMs顶到 600000(宿主上限)。原来的 10~15 秒会在人还没
看清弹窗时就把调用掐掉。超时仍然等于「没拿到同意」,所以是 fail closed。 - 输入工具描述追加一句:这次调用会先征求同意,被拒绝就是什么都没发生,不要重试。
state.consent新增confirmInput/service/ 受闸门工具 / 当前授权 / 超时上限。
测试
| 之前 | 现在 | |
|---|---|---|
宿主 (test-host.mjs) |
27 | 51 |
客户端 (test-client.mjs) |
20 | 25 |
新增三节:[10] 闸门判定全覆盖(关闭时逐字节透传、三种答案的后果、认不出的回答一律拒绝、
ask() 抛错即拒绝、子智能体继承与「只认一层」边界、describeInputCall 的超长截断)、
[11] 注册态工具确实带闸门(超时顶到上限、描述写明会先问,以及一个零风险哨兵:
用 window_activate + 不存在的窗口标题驱动,断言抛的是「被用户拒绝」而非「找不到窗口」)、
[12] 授权的可见性与撤销。
硬约束:闸门用例只用假的
run驱动「允许」路径,真实注册的工具只在「拒绝」路径上被调用
—— 否则跑一次测试就真的会去动开发者的鼠标键盘。这条写进了测试注释。
安装
插件安装后需要重启 DSH 才加载。升级请先「卸载」再装,然后重启。
重启后插件页的「桌面操控」标题右侧会显示 v1.3.0 —— 没看到这个版本号就说明新构建还没生效。
完整变更:v1.2.1...v1.3.0
v1.2.0 — 改名「桌面操控」+ 设置面板与安全闸门
这个版本做了两件事:改名,以及加上一直缺失的设置面板。
改名:屏幕视野 → 桌面操控
旧名字只描述了插件的一半。它确实能截图、能列窗口,但注册的 7 个工具里有 5 个在「动手」
(window_activate / mouse_click / type_text / key / scroll)——
真的移动你的鼠标、按键、打字。「屏幕视野」读起来像个只读的截图工具,与它实际能做的事不符。
| 位置 | 变化 |
|---|---|
displayName |
屏幕视野 → 桌面操控 |
locale/{zh,en}.json 的 meta.title |
→ 桌面操控 / Desktop Control |
| 两份 README 标题 | → 桌面操控 / Desktop Control |
package.json 的 name、entry id、仓库地址 |
不变(避免破坏既有安装) |
新增:设置面板
之前这个插件从来不读配置 —— apply(ctx) 连 config 参数都没有,cordis.patch.yml 里也没有
config 段。现在补齐了完整的设置系统(含新增的浏览器半侧 client.js),
改动立即生效,无需重启。
三道安全闸门
这是本次最重要的部分 —— 也是这个插件一直缺的东西:
| 设置 | 默认 | 作用 |
|---|---|---|
enabled |
true |
总开关。关闭后一个工具都不注册。 |
allowCapture |
true |
只读组:window_list + screenshot。 |
allowInput |
true |
输入组:那 5 个会动你电脑的工具。 |
只想让智能体「看」就关掉 allowInput —— 那 5 个工具会当场从会话里消失,不用重启。
面板里打开它时会显示醒目的风险提示,明确写出「本插件没有确认后执行机制」。
隐私设置
excludeWindowTitles:逗号分隔、大小写不敏感。命中的窗口不出现在 window_list 里,
也不能被截图或操作。它刻意在两条路径上都生效 —— 否则被排除的窗口会从另一个入口泄露出去。
用来挡住密码管理器、网银这类窗口。
行为设置
captureMaxWidth(截图默认最大宽度 320–8192)、imageCacheSize(图片引用 LRU 4–512)。
配置分层与 dsh-bundle-default-workspace 一致:cordis.patch.yml 写部署默认值,
用户覆盖落到 $DSH_HOME/state/dsh-bundle-screen-view/settings.json 的稀疏覆盖层,
点「恢复默认」是删除那个键、回落部署默认值。
修复
inject补上webServer。Cordis 的ctx是受限代理 —— 读一个没声明在inject里的属性会
抛错而不是返回undefined,漏声明会让整个apply()失败、插件在插件页显示「异常」。- 面板路由的 Host 校验里,裸 IPv6 不再被误剥端口(
::1的尾段不是端口)。
测试
新增 test-host.mjs(27 项断言组)与 test-client.mjs(20 项断言组),npm test 可跑。
其中有两条回归专门针对**「浏览器不会报错、只能靠断言拦住」**的错误类别:
- 字段标签必须可见 —— 面板曾经把
label只用在aria-label上、从不渲染它,
界面上只剩一个孤零零的输入框。 - 用到的 CSS 变量必须真实存在 —— 写一个不存在的
var(--dsw-*)会让整条 CSS 声明被判非法、
静默失效,界面无任何提示。
还覆盖了:改闸门后工具立即重挂(7 个 → 2 个,无需重启)、窗口排除的大小写与空名单、
配置夹取、稀疏覆盖层、路由的 403/405/400、受限代理降级,以及面板在各种状态下的渲染
(开/关输入、非 Windows、停用、排除名单回显)。
安装 / 升级
插件管理器目前不支持自动更新:先卸载,再重新安装,然后重启 DSH。
https://github.com/OraSkyC/dsh-bundle-screen-view
从
1.1.0升级的话,注意插件在列表里的名字会从「屏幕视野」变成「桌面操控」。
要求
仅 Windows · Node >= 22.19.0 · 无 npm 依赖 · 无构建步骤。
v1.1.0 — 屏幕视野(首个公开发布)
首个公开发布版本。
⚠️ 先读这段
这个插件注册的 7 个工具里,有 5 个会真的操作你的电脑:
| 工具 | 会不会动你的机器 |
|---|---|
window_list、screenshot |
不会 —— 只读 |
window_activate、mouse_click、type_text、key、scroll |
会 —— 真的移动光标、真的按键、真的打字 |
被授予这些工具的智能体可以对你的桌面做任何你能做的事。插件本身没有「确认后才执行」的机制 ——
一旦工具可用且权限预设允许,调用 mouse_click 不会弹窗征求同意。请在你自己控制的机器上使用。
这是什么
给 DeepSeek Harness 的 Host 装上眼睛和手:截取本地桌面、列出窗口,并驱动真实的鼠标与键盘。
仅支持 Windows。
安装
设置 → 插件 → 添加插件,填入:
https://github.com/OraSkyC/dsh-bundle-screen-view
或命令行:
dsh plugin --profile desktop add https://github.com/OraSkyC/dsh-bundle-screen-view装完重启 DSH。
工具
只读
window_list—— 列出可见顶层窗口(前台优先):hwnd、标题、屏幕矩形screenshot—— 截取主屏或单个窗口,返回可看图 + 文字摘要;支持标题子串与maxWidth
会操作桌面
window_activate—— 把窗口带到前台(最小化的先还原)mouse_click—— 移动光标到屏幕坐标并点击(左/右/中键,1–3 次)type_text—— 向焦点窗口输入 Unicode 文本(任意语言、可含换行)key—— 按键或组合键(ctrl+shift+t、alt+f4、f5…)scroll—— 在光标处滚动,可先移动光标
实现要点
- 截图:屏幕 DC +
BitBlt为主路径(2560×1440 约 37 ms),逐窗口PrintWindow为兜底。
窗口截图从 DWM 合成后的屏幕 DC 裁剪 —— 这是 GPU 合成窗口(Chromium / Electron)
唯一可靠的来源,因为它们的窗口 DC 表面是一张空白白底。 - koffi 从 DSH Desktop 安装目录加载(带回退),因此没有 npm 依赖、没有编译步骤。
直接复用安装目录里已有的那份,而不是让每个用户各装一份原生二进制。 - PNG 编码器是自写的(zlib + CRC32 + 分块),不依赖任何图像库。
- 图片经有界 LRU(64 张)引用,
render()发出type: 'image'块,模型才真正看见像素。
要求
| 项目 | 要求 |
|---|---|
| 操作系统 | 仅 Windows(user32.dll / gdi32.dll) |
| Node | >= 22.19.0 |
| npm 依赖 | 无 |
| 构建步骤 | 无 |
没有浏览器半侧,所以插件页不会出现配置卡 —— 纯 Host 能力,装上就有工具。
用法、风险控制与实现细节见 README.md。