Skip to content

Releases

v1.4.0 —— 等待超时可配置 + 可选超时后放行

Choose a tag to compare

@OraSkyC OraSkyC released this 08 Oct 18:26

新增

等待超时可配置,并且可以选「超时后放行」

设置 默认 说明
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):

  1. 超时 + 默认 → 拒绝,并断言真的等满了窗口、问题被撤回(signal 已 abort)、
    没留下会话授权。
  2. 超时 + 放行 → 执行,计数为 1,且绝不记会话授权。
  3. 等待秒数 0 → 不超时,结论仍来自回答本身。
  4. 调用方中止不被误当成等待超时 —— 故意在 allowOnTimeout: true 下触发调用方中止,
    断言它走「问不到人」那条并拒绝。

另新增两个新字段的夹取(0 要保留、越界收口、取整、非法值回落)与
「非布尔必须回落 false」 —— 这个开关绝不能被一个意外值打开。

面板侧:三个新字段与禁用联动、等待策略文案(不超时 / 视为不同意 / 放行)、
超时放行次数可见、fail-open 警告必须是错误色 + role=alert。

升级

需要重启 DSH。重启后插件页标题右侧显示 v1.4.0。

注意:升级后默认等待窗口是 300 秒(此前硬编码是 600 秒),超时仍视为不同意 ——
和你现在的行为一致,只是窗口短了一半。想改就在面板「安全闸门」里调。

完整变更:v1.3.2...v1.4.0

v1.3.2 —— 真机联调测出的两个授权显示问题

Choose a tag to compare

@OraSkyC OraSkyC released this 08 Oct 18:20

这次的 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 都没有「允许本次会话所有操作」授权,
而子智能体自己弹不出窗问人 —— 需要用户在主会话里授权,子智能体才能跟着用。

顺带在真机上确认了两件之前只能靠推理的事

  1. state.consent.service === true —— 宿主的 userQuestions 确实能从插件的根 ctx
    用 ctx.get() 拿到。这是整个设计里唯一没法离线验证的一环(如果拿不到,开着闸门会让
    五个输入工具全部失败)。现在确认了。

  2. 子智能体的会话头实测为:

    {
      "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 —— 修复保存后界面不同步

Choose a tag to compare

@OraSkyC OraSkyC released this 08 Oct 18:07

修复:点了开关显示「已保存」,开关却纹丝不动

Field / BoolField 把 /settings 的响应整个丢掉了,界面只能等下一次轮询
(默认 30 秒)才更新。用户看到的是「已保存」但开关没动,退出重进设置页才显示新值
—— 因为那是重新拉了一次 /state。

文本框是同一个根因,只是症状不同:dirty 一直挂在 true 上,「未保存」不消失。
所有走 /settings 的控件都中招,不只是开关。

修法(两处,缺一不可)

  1. /settings 回一份完整状态快照(buildState()),不再只回
    effective / defaults / …。派生字段(工作区标题、解析出来的目录、回收站统计、
    清理预览)根本不在原来的响应里,所以它们也必须一起回。这样客户端也不必再写
    逐字段的合并逻辑 —— 少一处可以漏的地方。

  2. 面板新增「保存后广播」订阅:保存成功即把响应并回面板状态,并且作废在途的轮询
    (否则一个更早发出的 /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 —— 操作前征求同意(默认关)+ 会话级授权与撤销

Choose a tag to compare

@OraSkyC OraSkyC released this 08 Oct 17:38

新增:操作前征求同意

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 — 改名「桌面操控」+ 设置面板与安全闸门

Choose a tag to compare

@OraSkyC OraSkyC released this 08 Oct 16:32

这个版本做了两件事:改名,以及加上一直缺失的设置面板。

改名:屏幕视野 → 桌面操控

旧名字只描述了插件的一半。它确实能截图、能列窗口,但注册的 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 — 屏幕视野(首个公开发布)

Choose a tag to compare

@OraSkyC OraSkyC released this 08 Oct 16:06

首个公开发布版本。

⚠️ 先读这段

这个插件注册的 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。