dsh-codex-style-computer-use 1.3.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ar.md +234 -0
- package/README.de.md +234 -0
- package/README.es.md +264 -0
- package/README.fa.md +234 -0
- package/README.fr.md +243 -0
- package/README.md +72 -17
- package/README.pt.md +234 -0
- package/README.ru.md +259 -0
- package/README.zh.md +66 -16
- package/icon.svg +20 -4
- package/lib/audit.js +7 -0
- package/lib/batch.js +14 -19
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +1 -1
- package/lib/compact-feedback.js +175 -0
- package/lib/continuation.js +1 -1
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/index.js +403 -136
- package/lib/core/native-runtime.js +17 -0
- package/lib/core/worker.cs +2160 -229
- package/lib/feedback-routes.json +11 -0
- package/lib/feedback.js +49 -16
- package/lib/image-output.js +87 -17
- package/lib/index.js +25 -7
- package/lib/key-command.js +37 -0
- package/lib/launch.js +67 -0
- package/lib/media-contract.js +43 -0
- package/lib/observations.js +267 -13
- package/lib/policy.js +29 -9
- package/lib/tool-contract.js +56 -0
- package/lib/tools.js +203 -52
- package/lib/verification-region.js +17 -0
- package/mcp/package.json +2 -1
- package/mcp/src/execution.js +8 -5
- package/mcp/src/index.js +16 -267
- package/mcp/src/tools.js +271 -0
- package/package.json +6 -1
- package/scripts/check-behavior.mjs +83 -0
- package/scripts/measure-tool-tokens.mjs +21 -0
- package/scripts/verify-package.mjs +9 -0
- package/skills/computer-use/SKILL.md +55 -43
- package/skills/computer-use/references/lifecycle.md +11 -7
- package/skills/computer-use/references/maintenance.md +9 -5
- package/skills/computer-use/references/precision.md +39 -14
- package/skills/computer-use/references/progressive-control.md +10 -4
package/README.zh.md
CHANGED
|
@@ -1,29 +1,54 @@
|
|
|
1
1
|
# Codex 风格 Computer Use
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**全市场最精准的 Windows 桌面操控,也是单个任务成本最低的。**
|
|
4
4
|
|
|
5
|
-
你描述目标,AI
|
|
5
|
+
你描述目标,AI 读准控件身份、按真实控件执行、回读结果——一段已知流程一轮模型往返就跑完,而不是每个动作各花一轮。
|
|
6
6
|
|
|
7
|
-
[English](README.md) | 中文 · [安装](#在-dsh-market-安装和更新) · [版本记录](CHANGELOG.md)
|
|
7
|
+
[English](README.md) | 中文 | [Français](README.fr.md) | [Deutsch](README.de.md) | [Español](README.es.md) | [Português](README.pt.md) | [العربية](README.ar.md) | [Русский](README.ru.md) | [فارسی](README.fa.md) · [安装](#在-dsh-market-安装和更新) · [版本记录](CHANGELOG.md) · [2.0.0 变更](docs/RELEASE-2.0.0.md)
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
11
|
+
## 为什么它更准
|
|
12
|
+
|
|
13
|
+
- **认身份,不猜坐标。** UI Automation 给出真实控件树与精确 `AutomationId`,动作可以走控件自身的 UI Automation 模式,而不是盲点坐标。
|
|
14
|
+
- **标记先校验,再执行。** 默认情况下,编号标记在派发输入前按窗口身份与采样像素校验,过期标记会被拒绝,不会点错位置。动画邻居导致像素签名失效时,`uiaOnly` 用窗口句柄与几何固定身份,派发时的遮挡检查仍然生效。
|
|
15
|
+
- **查询有边界。** UIA 查询默认深度 6、120 节点、1200 毫秒软预算;深度超过 6 或节点超过 300 时必须提供已测量的 `anchor`,不会悄悄扫错区域。
|
|
16
|
+
- **看不见就读字。** `computer_ocr` 返回绝对屏幕像素的行与词框,零 UIA 应用也能按名字定位。
|
|
17
|
+
|
|
18
|
+
## 为什么它更省
|
|
19
|
+
|
|
20
|
+
- **一段已知流程只花一轮往返。** 一次 `computer_batch` 串行执行整串动作、遇错即停,只花一轮模型往返,而不是每个动作各花一轮。
|
|
21
|
+
- **只回增量。** 把上一次观察 ID 作为 `since` 传回,只拿到新增、变化、删除与顺序,而不是整棵树重发。
|
|
22
|
+
- **2.0.0 默认精简回执。** 动作后的文字回执通常最多再加 6 KiB;需要细节时仍可用 `feedback: full`。
|
|
23
|
+
- **证据不进对话。** 完整点查证据可留在有界本地缓存(内存 4 MiB、磁盘 32 MiB、64 条、24 小时过期),按需读取。
|
|
24
|
+
- **本地的事留在本地。** UIA、屏幕 OCR 与像素校验都在预置原生 worker 里完成,不额外产生每一步的视觉账单。
|
|
25
|
+
|
|
26
|
+
## 它不会抢走你的桌面
|
|
27
|
+
|
|
28
|
+
- **会等你。** 操作前 1 秒提示(3 分钟内再次操作 0.1 秒)后即时交出桌面;你操作完,AI 重新观察并续接原任务。
|
|
29
|
+
- **刹车能传进模型。** `Ctrl+Esc` 暂停、`Ctrl+Alt+R` 恢复、`Ctrl+Alt+Q` 退出,停止事件被推入正在运行的会话。默认保留操作审批,密码框默认拒绝。
|
|
30
|
+
- **做不到的会说出来。** 检测到 Windows 安全桌面(UAC 或锁屏)会报告并在结束后自动续接,而不是盲目重试。
|
|
14
31
|
|
|
15
32
|

|
|
16
33
|
|
|
17
34
|

|
|
18
35
|
|
|
19
|
-
上图为
|
|
36
|
+
上图为 1.3.0 引入的交互模型;2.0.0 的风筝光标、有界查询与精简回执见上。实际 Token 用量取决于任务、模型和观察方式。默认保留操作审批;明确暂停、取消和退出始终有效。
|
|
37
|
+
|
|
38
|
+
**安装即用。** 19 个桌面工具,原生组件随包提供,无需 SDK 或安装时编译。支持 Windows、Node.js ≥ 22 和系统自带的 .NET Framework 4.x。独立社区插件;Codex 用于说明交互风格。
|
|
39
|
+
|
|
40
|
+
## 2.0.0 新特性
|
|
20
41
|
|
|
21
|
-
|
|
42
|
+
- **默认精简回执**——动作后的文字回执通常最多再加 6 KiB;需要细节可用 `feedback: full`,不需要可用 `feedback: none`。
|
|
43
|
+
- **有界、带锚点的 UIA 查询**——默认深度 6、120 节点、1200 毫秒软预算;深度超过 6 或节点超过 300 的查询必须提供已测量的 `anchor`。
|
|
44
|
+
- **证据按需读取**——内存 4 MiB + 磁盘 32 MiB、64 条记录、24 小时过期;磁盘失败回退到有界内存。
|
|
45
|
+
- **自适应操作预告**——首次 1 秒,3 分钟滚动窗口内再次操作 0.1 秒;倾斜的风筝标记点击点。
|
|
46
|
+
- **加固的输入路径**——窗口管理快捷键守卫、worker 背压/恢复、过期目标检查与取消清理。
|
|
22
47
|
|
|
23
48
|
<details>
|
|
24
49
|
<summary>当前版本与兼容性</summary>
|
|
25
50
|
|
|
26
|
-
**
|
|
51
|
+
**2.0.0** 为本次大版本,先发布至 npm `next`,通过仓库规定的 24 小时成熟期后再升稳定频道。详见 [2.0.0 变更与迁移](docs/RELEASE-2.0.0.md),稳定版以 registry 标签为准。
|
|
27
52
|
|
|
28
53
|
1.3.1 可直接从屏幕读字定位控件,察觉 Windows 安全桌面并在其结束后自动续接;所有颜色共用同一条按 DPI 换算的边缘光环,呼吸谷值保持可见。1.3.0 起工具用完可主动释放,聊天继续;正常收尾自动退出。
|
|
29
54
|
|
|
@@ -33,6 +58,12 @@
|
|
|
33
58
|
|
|
34
59
|
## 它能做什么
|
|
35
60
|
|
|
61
|
+
2.0.0在 AI 操作和保留现场时使用小巧的蓝色风筝光标:倾斜的尖端就是点击位置,文本、缩放等提示收在风筝中央。呼吸状态表示仍需保持页面与鼠标位置;放开现场后恢复应用原来的鼠标。Windows 鼠标主题保持原样。
|
|
62
|
+
|
|
63
|
+
2.0.0进一步收紧 UIA 查询范围,并默认精简动作后的文字反馈;视觉模型仍保留原有图片检查。调用可用 `feedback: compact|full|none`,点击可用 `verificationRegion: "x,y,w,h"` 指定真正的结果区域。正常精简反馈在原动作回执之外最多增加 6 KiB 文字。完整点查证据可留在有容量与期限限制的本地缓存,按需读快照。磁盘失败先回退有界内存;如果无法保留可回读的快照,则直接完整返回已有证据,此时不受精简文字预算限制,不重复查询或操作。
|
|
64
|
+
|
|
65
|
+
带过滤条件的 UIA 查询必须指定 `hwnd`,默认深度 6、最多 120 节点和 1200 毫秒软预算。深度大于 6 或节点上限大于 300 时,须提供该窗口内已测量的 `anchor:"x,y"`;精确 `id` 也不豁免。单次提供器调用可能超过软预算,部分结果不能证明目标不存在或唯一。窗口管理快捷键后,先用 `computer_state {windows:true}` 查看窗口,再成功激活目标并重新观察,随后才能继续键盘输入。
|
|
66
|
+
|
|
36
67
|
- **观察** —— 窗口列表、截图(区域 / JPEG / PNG / 降采样)、可点元素的 Set-of-Marks 编号图,
|
|
37
68
|
以及带精确 `AutomationId` 的 UI Automation 树。
|
|
38
69
|
- **操作** —— 点击、移动、拖拽、滚动、选中文本区间、键入、按键,并可走控件**自身的 UI
|
|
@@ -42,14 +73,13 @@
|
|
|
42
73
|
- **能传到模型的人为刹车** —— 停止事件被推入正在运行的会话,agent 当场知道自己被打断,
|
|
43
74
|
而不是等到下一轮才发现。
|
|
44
75
|
|
|
45
|
-
|
|
46
|
-
(`%LOCALAPPDATA%\dsh-computer-use\worker\`)、审计日志与截图(`$DSH_HOME/data/computer-use/`)。
|
|
76
|
+
工具观察桌面,不遍历用户文档。插件管理原生组件(`%LOCALAPPDATA%\dsh-computer-use\worker\`),以及 `$DSH_HOME/data/computer-use/` 下的审计日志、截图和自有 `observation-cache` 有界缓存。`computer_ctrl {action:"clear_cache"}` 只清理本 Agent 的观察缓存,不删除截图、附件或用户文档。
|
|
47
77
|
|
|
48
78
|
## 在 dsh-market 安装和更新
|
|
49
79
|
|
|
50
80
|
在市场搜索 **computer-use** 或 **Codex-style**,认准仓库 **ltmroberthk915/dsh-computer-use**、npm 包 **dsh-codex-style-computer-use**(维护者 **ltmroberthk**),点击**安装**。以后通过市场的**更新**或**全部更新**获取正式频道版本。两份原生组件都已随包提供,无需 PowerShell 7、SDK 或构建白名单。
|
|
51
81
|
|
|
52
|
-
也可在 **设置 → 插件 → 添加插件** 输入 **`dsh-codex-style-computer-use
|
|
82
|
+
也可在 **设置 → 插件 → 添加插件** 输入 **`dsh-codex-style-computer-use`**,安装当前正式频道;要体验本页描述的 2.0.0,输入 **`dsh-codex-style-computer-use@2.0.0`**。这里填 npm 包名即可。宿主可能对新版本设置等待期,以界面提示为准。首次新增 bundle 可在支持的宿主中热加载;确认工具与 skill 已出现即可使用。替换已经加载的版本、客户端界面未刷新或界面提示 **restart-required / 需要重启** 时,从托盘完整退出 DSH 并重新打开。
|
|
53
83
|
|
|
54
84
|
### 发布未满 24 小时
|
|
55
85
|
|
|
@@ -108,9 +138,9 @@ $CuDshInstall = Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness'
|
|
|
108
138
|
|
|
109
139
|
## 1.1 的按需控制
|
|
110
140
|
|
|
111
|
-
- **按 Agent 加载:**未激活时暴露 `computer_use_activate`、`computer_ctrl`、`computer_ask`。成功读取 computer-use skill、确认规程或调用激活入口后,该 Agent
|
|
141
|
+
- **按 Agent 加载:**未激活时暴露 `computer_use_activate`、`computer_ctrl`、`computer_ask`。成功读取 computer-use skill、确认规程或调用激活入口后,该 Agent 获得 19 个桌面工具,加入口共 20 个。其他 Agent 和子 Agent 独立启用,支持原生调用和 Node `run_code`。激活本身不启动控制,也不释放人为刹车。
|
|
112
142
|
- **已观察的 UIA 目标:**指定窗口和 name/id 的聚焦查询默认生成 target 句柄。控件重建时,显式 `rebind:true` 才允许在同一窗口内进行唯一身份或语义匹配。上下文变化、歧义、扫描不完整时拒绝动作。宽查询需 `targets:true` 才生成恢复句柄,避免普通观察承担额外开销。
|
|
113
|
-
- **增量观察:**重复相同查询并传入上一 observation 的 `since
|
|
143
|
+
- **增量观察:**重复相同查询并传入上一 observation 的 `since`,返回新增、变化、删除及顺序信息。快照按 Agent 隔离,每个 store 总计最多索引 64 份观察、在内存保留 4 MiB JSON。`snapshot` 可取回仍保留的历史结果,`offset` 和 `limit` 可分页读取数组。缺少基线、范围变化、证据不完整或增量更大时自动回退全量。
|
|
114
144
|
|
|
115
145
|
保留的是提供器实际返回的完整字段,沿用原有提供器限制;历史快照不代替动作后的新验证。缓存上限和恢复边界见[详细协议](skills/computer-use/references/progressive-control.md)。
|
|
116
146
|
|
|
@@ -119,7 +149,7 @@ $CuDshInstall = Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness'
|
|
|
119
149
|
| 分组 | 工具 |
|
|
120
150
|
|---|---|
|
|
121
151
|
| 激活 | `computer_use_activate` |
|
|
122
|
-
| 观察 | `computer_state` `computer_shot` `computer_marks` `computer_uia` |
|
|
152
|
+
| 观察 | `computer_state` `computer_shot` `computer_ocr` `computer_marks` `computer_uia` |
|
|
123
153
|
| 操作 | `computer_click` `computer_move` `computer_drag` `computer_scroll` `computer_select` `computer_key` `computer_type` `computer_uia_act` `computer_window` `computer_clip` |
|
|
124
154
|
| 流程 | `computer_wait` `computer_batch` |
|
|
125
155
|
| 元 | `computer_ask` `computer_ctrl` |
|
|
@@ -157,7 +187,9 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
157
187
|
| 键 | 默认 | 含义 |
|
|
158
188
|
|---|---:|---|
|
|
159
189
|
| `automationMode` | `standard` | 审批模式(见上) |
|
|
160
|
-
| `progressiveTools` | `true` | 按 Agent 激活;设为 `false`
|
|
190
|
+
| `progressiveTools` | `true` | 按 Agent 激活;设为 `false` 全局暴露 19 个桌面工具 |
|
|
191
|
+
| `actionFeedback` | `true` | 直接动作及批处理后自动观察 |
|
|
192
|
+
| `actionFeedbackDetail` | `compact` | `compact`、`full` 或 `none`;单次调用的 `feedback` 可覆盖默认值;无法缓存时完整内联返回证据 |
|
|
161
193
|
| `dryRun` | `false` | 只模拟操作并记录 |
|
|
162
194
|
| `maxActionsPerMinute` | `60` | 核心层操作限速 |
|
|
163
195
|
| `annotateMarks` | `true` | 在截图上绘制 Set-of-Marks 方框 |
|
|
@@ -169,9 +201,23 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
169
201
|
`mcp/` 是建立在同一核心之上的零依赖 MCP stdio 服务,供只讲 MCP、不讲 Cordis 的客户端使用:
|
|
170
202
|
|
|
171
203
|
```sh
|
|
204
|
+
# 已发布包(2.0.1 起可用),无需克隆仓库
|
|
205
|
+
npx -y dsh-codex-style-computer-use
|
|
206
|
+
|
|
207
|
+
# 从仓库检出运行
|
|
172
208
|
node mcp/src/index.js
|
|
173
209
|
```
|
|
174
210
|
|
|
211
|
+
该入口随根包发布,运行时报告根包版本;默认只能观察桌面。启动它的宿主在负责桌面操作授权时,
|
|
212
|
+
可以设置 `DSH_CU_ALLOW_INPUT=1`。模型调用工具不能开启输入:兼容工具 `cu_approve` 只说明授权来源,
|
|
213
|
+
不会授予同意。DSH 的 `automationMode` 属于 Cordis 入口,不是 MCP 的审批服务。
|
|
214
|
+
`DSH_CU_DRY_RUN=1` 只模拟已由宿主启用的输入。
|
|
215
|
+
|
|
216
|
+
两个入口共享按键规范化、批次失败与接管后续行语义,以及图像坐标换算。MCP 保留截图时间、光标、
|
|
217
|
+
稳定性和区域元数据,只附最后一张 PNG/JPEG,不把图像字节重复塞进 JSON。`cu_hold` / `cu_free`
|
|
218
|
+
用于操作间保护或放开现场,`cu_release` 结束桌面周期;`cu_drag` 支持 `expectCursor` 与 `hwnd` 校验。
|
|
219
|
+
MCP 工具范围较小,DSH 的人类提问和宿主生命周期集成仍由 Cordis 入口提供。
|
|
220
|
+
|
|
175
221
|
## 开发
|
|
176
222
|
|
|
177
223
|
发布者检查默认查询 npm 官方源。若本地无法访问,可用 `CU_REGISTRY` 为只读的 `check` 命令指定镜像;JSON 输出会注明 `registry` 和 `authoritative`。PowerShell 示例(只影响当前终端):
|
|
@@ -195,6 +241,10 @@ node build/verify-tools-schema.mjs # 工具 sc
|
|
|
195
241
|
工具定义走真实的 `@deepseek-ai/dsh-tools` DSL 编译,因此 schema 写错会在这里失败,
|
|
196
242
|
而不是在宿主重启时把整棵插件树带崩。部分守卫会编译原生 worker,仅 Windows 可跑。
|
|
197
243
|
|
|
244
|
+
发版前先跑 `npm run verify:package`:它校验打包身份、预编译 helper 的哈希,以及每一个 `bin`
|
|
245
|
+
条目(目标文件存在、带 node shebang)。它刻意不是发布钩子——本包不带任何生命周期脚本——
|
|
246
|
+
所以请在 `npm publish` 之前自己手动执行一次。
|
|
247
|
+
|
|
198
248
|
## 许可
|
|
199
249
|
|
|
200
250
|
MIT —— 见 [LICENSE](LICENSE)。
|
package/icon.svg
CHANGED
|
@@ -13,10 +13,14 @@
|
|
|
13
13
|
<stop offset=".5" stop-color="#38BDF8"/>
|
|
14
14
|
<stop offset="1" stop-color="#A78BFA"/>
|
|
15
15
|
</linearGradient>
|
|
16
|
-
<radialGradient id="glow" cx="
|
|
17
|
-
<stop stop-color="#38BDF8" stop-opacity=".
|
|
16
|
+
<radialGradient id="glow" cx="17.4" cy="16.8" r="7.4" gradientUnits="userSpaceOnUse">
|
|
17
|
+
<stop stop-color="#38BDF8" stop-opacity=".30"/>
|
|
18
18
|
<stop offset="1" stop-color="#38BDF8" stop-opacity="0"/>
|
|
19
19
|
</radialGradient>
|
|
20
|
+
<linearGradient id="kite" x1="14.1" y1="10.6" x2="19.9" y2="23.0" gradientUnits="userSpaceOnUse">
|
|
21
|
+
<stop stop-color="#55ADFF"/>
|
|
22
|
+
<stop offset="1" stop-color="#163A99"/>
|
|
23
|
+
</linearGradient>
|
|
20
24
|
</defs>
|
|
21
25
|
<rect width="32" height="32" rx="7.5" fill="url(#tile)"/>
|
|
22
26
|
<rect width="32" height="32" rx="7.5" fill="url(#halo)"/>
|
|
@@ -24,6 +28,18 @@
|
|
|
24
28
|
<path d="M5.6 10.1 H26.4" stroke="url(#rail)" stroke-width="1.05" stroke-opacity=".5"/>
|
|
25
29
|
<circle cx="8.5" cy="8" r=".9" fill="#5EEAD4"/>
|
|
26
30
|
<circle cx="11.2" cy="8" r=".9" fill="#A78BFA" fill-opacity=".85"/>
|
|
27
|
-
<circle cx="
|
|
28
|
-
|
|
31
|
+
<circle cx="17.4" cy="16.8" r="7.4" fill="url(#glow)"/>
|
|
32
|
+
|
|
33
|
+
<!-- mouse-line tail of the agent kite pointer -->
|
|
34
|
+
<path d="M19.87 22.96 C 19.2 25.3, 16.4 24.1, 14.6 26.3" stroke="#3B82F6" stroke-opacity=".92" stroke-width=".74" stroke-linecap="round" fill="none"/>
|
|
35
|
+
|
|
36
|
+
<!-- tilted kite pointer, simplified from the runtime KitePointerPainter -->
|
|
37
|
+
<polygon points="14.13,10.64 22.07,14.43 19.87,22.96 11.93,19.17"
|
|
38
|
+
fill="none" stroke="#3684FF" stroke-opacity=".35" stroke-width="1.5" stroke-linejoin="round"/>
|
|
39
|
+
<polygon points="14.13,10.64 22.07,14.43 19.87,22.96 11.93,19.17" fill="url(#kite)"/>
|
|
40
|
+
<polygon points="14.13,10.64 22.07,14.43 17.00,16.80" fill="#A9E0FF" fill-opacity=".30"/>
|
|
41
|
+
<polygon points="17.00,16.80 22.07,14.43 19.87,22.96" fill="#06184F" fill-opacity=".28"/>
|
|
42
|
+
<polygon points="14.13,10.64 22.07,14.43 19.87,22.96 11.93,19.17"
|
|
43
|
+
fill="none" stroke="#CFEFFF" stroke-opacity=".95" stroke-width=".33" stroke-linejoin="round"/>
|
|
44
|
+
<line x1="14.13" y1="10.64" x2="19.87" y2="22.96" stroke="#D5F2FF" stroke-opacity=".30" stroke-width=".25"/>
|
|
29
45
|
</svg>
|
package/lib/audit.js
CHANGED
|
@@ -28,3 +28,10 @@ export function appendAudit (dataDir, rec) {
|
|
|
28
28
|
appendFileSync(join(dataDir.root, 'audit.jsonl'), JSON.stringify(rec) + '\n')
|
|
29
29
|
} catch { /* audit must never break the action */ }
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
export function installAudit (cu, dataDir) {
|
|
33
|
+
const events = ['action', 'action:settled', 'handoff-wait']
|
|
34
|
+
const write = record => appendAudit(dataDir, record)
|
|
35
|
+
for (const event of events) cu.on(event, write)
|
|
36
|
+
return () => { for (const event of events) cu.off(event, write) }
|
|
37
|
+
}
|
package/lib/batch.js
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
// Serial execution and observation selection; no desktop or lifecycle side effects here.
|
|
2
2
|
import { handoffResult } from './continuation.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const isReadback = (step) => ['computer_state', 'computer_uia', 'computer_marks', 'computer_shot'].includes(step.tool) ||
|
|
6
|
-
(step.tool === 'computer_clip' && step.args?.text === undefined)
|
|
7
|
-
|
|
8
|
-
export function actionRefusal (step, result) {
|
|
9
|
-
if (result?.ok === false || result?.success === false) return result.error || 'action reported failure'
|
|
10
|
-
if (step.tool === 'computer_window' && (step.args?.op || 'activate') === 'activate' && result?.activated !== true) {
|
|
11
|
-
return 'target window activation was not confirmed; subsequent actions were not dispatched'
|
|
12
|
-
}
|
|
13
|
-
if (step.tool === 'computer_ctrl' && step.args?.action === 'recover' && result?.recovered !== true) return result.reason || 'temporary pause recovery refused'
|
|
14
|
-
if (step.tool === 'computer_ctrl' && step.args?.action === 'release' && !result?.released && !result?.alreadyReleased) return result.reason || 'cycle release refused'
|
|
15
|
-
return null
|
|
16
|
-
}
|
|
3
|
+
import { actionRefusal, actionError, isReadStep as isRead, isReadbackStep as isReadback, endsBatch } from './tool-contract.js'
|
|
4
|
+
export { actionRefusal } from './tool-contract.js'
|
|
17
5
|
|
|
18
6
|
export async function runBatch ({ steps, handlers, signal, stopped, screenshot, shot = 'auto', exec, assertOwnership }) {
|
|
19
7
|
if (!Array.isArray(steps) || !['auto', 'always', 'never'].includes(shot)) throw new Error('invalid batch actions or shot mode')
|
|
20
8
|
const actions = []
|
|
21
9
|
let failed = false
|
|
22
10
|
let continuation = null
|
|
11
|
+
let endedBy = null
|
|
23
12
|
for (const step of steps) {
|
|
24
13
|
const name = step?.tool
|
|
25
14
|
if (failed || signal?.aborted) {
|
|
26
15
|
actions.push({ tool: name || '', ok: false, outcome: 'not-dispatched',
|
|
27
16
|
...(continuation && !signal?.aborted
|
|
28
17
|
? { status: 'deferred', message: 'Resume this unfinished step after fresh target evidence; the original task must continue.' }
|
|
29
|
-
: { error: signal?.aborted ? 'cancelled' : 'earlier action failed' }) })
|
|
18
|
+
: { error: signal?.aborted ? 'cancelled' : endedBy ? 'batch ended by control action' : 'earlier action failed' }) })
|
|
30
19
|
failed = true
|
|
31
20
|
continue
|
|
32
21
|
}
|
|
@@ -38,22 +27,28 @@ export async function runBatch ({ steps, handlers, signal, stopped, screenshot,
|
|
|
38
27
|
const result = await fn(step.args || {}, exec)
|
|
39
28
|
const refusal = actionRefusal(step, result)
|
|
40
29
|
actions.push({ tool: name, ok: !refusal, result, ...(refusal ? { error: refusal } : {}) })
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
if (result?.taskStatus === 'continuing' && result.continuation?.required && !signal?.aborted && !stopped()) {
|
|
31
|
+
const nestedPath = result.continuation.resumeActionPath || [result.continuation.resumeFromActionIndex]
|
|
32
|
+
continuation = { ...result.continuation, resumeFromActionIndex: actions.length - 1,
|
|
33
|
+
resumeActionPath: [actions.length - 1, ...nestedPath.filter(Number.isInteger)] }
|
|
34
|
+
}
|
|
35
|
+
if (endsBatch(step)) endedBy = { tool: name, action: step.args?.action || name.replace(/^cu_/, '') }
|
|
36
|
+
else if (result?.endedBy) endedBy = result.endedBy
|
|
37
|
+
failed = !!refusal || !!continuation || (!stoppedBefore && stopped()) || !!endedBy
|
|
43
38
|
} catch (error) {
|
|
44
39
|
const checkpoint = !signal?.aborted && !stopped() ? handoffResult(error) : null
|
|
45
40
|
if (checkpoint) {
|
|
46
41
|
continuation = { ...checkpoint.continuation, resumeFromActionIndex: actions.length }
|
|
47
42
|
actions.push({ tool: name, ...checkpoint, continuation })
|
|
48
43
|
} else {
|
|
49
|
-
actions.push({ tool: name,
|
|
50
|
-
code: error.code || 'action-error', outcome: error.outcome || 'unknown' })
|
|
44
|
+
actions.push({ tool: name, ...actionError(error) })
|
|
51
45
|
}
|
|
52
46
|
failed = true
|
|
53
47
|
}
|
|
54
48
|
}
|
|
55
49
|
const needsShot = shot === 'always' || (shot === 'auto' && steps.some(s => !isRead(s)) && !isReadback(steps.at(-1) || {}))
|
|
56
50
|
const result = { actions, attachedShot: null }
|
|
51
|
+
if (endedBy) result.endedBy = endedBy
|
|
57
52
|
if (continuation) Object.assign(result, {
|
|
58
53
|
status: 'continuation-required', taskStatus: 'continuing', transient: true, continuation,
|
|
59
54
|
})
|
|
Binary file
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"platform": "win32",
|
|
4
4
|
"architecture": "x64",
|
|
5
5
|
"sourceSha256": "6e17308fc1cb0de1ece6f3164866b6de03ecdf74a42a6e78c0a8e6d02912c3bf",
|
|
6
|
-
"binarySha256": "
|
|
6
|
+
"binarySha256": "ad99b350925f8b373a93b80ee73b2632d23f8f6e8bab913a2a83becfa7b016af"
|
|
7
7
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Small post-action evidence, shared by host/MCP adapters. Never dispatches input,
|
|
2
|
+
// retries a read, inventories a UIA tree, or changes the original action receipt.
|
|
3
|
+
export const COMPACT_FEEDBACK_BYTES = 6144
|
|
4
|
+
export const COMPACT_OBSERVATION_MS = 6000
|
|
5
|
+
export const COMPACT_POINT_MS = 5000
|
|
6
|
+
|
|
7
|
+
const finite = value => typeof value === 'number' && Number.isFinite(value)
|
|
8
|
+
const bytes = value => Buffer.byteLength(JSON.stringify(value), 'utf8')
|
|
9
|
+
const rectKeys = ['x', 'y', 'width', 'height']
|
|
10
|
+
const windowKeys = ['hwnd', 'pid', 'process', 'title', 'class', 'active', 'minimized']
|
|
11
|
+
const elementKeys = ['name', 'role', 'controlType', 'automationId', 'className', 'value', 'text', 'enabled', 'isEnabled', 'offscreen', 'isOffscreen', 'editable', 'focused', 'hasKeyboardFocus', 'selected', 'checked', 'toggleState', 'expanded', 'expandCollapseState', 'hwnd', 'pid']
|
|
12
|
+
|
|
13
|
+
function point (value) {
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
const parts = value.trim().split(/[,\s]+/)
|
|
16
|
+
if (parts.length !== 2 || parts.some(p => !p)) return null
|
|
17
|
+
value = { x: Number(parts[0]), y: Number(parts[1]) }
|
|
18
|
+
}
|
|
19
|
+
return value && finite(value.x) && finite(value.y) ? { x: Math.round(value.x), y: Math.round(value.y) } : null
|
|
20
|
+
}
|
|
21
|
+
export function feedbackRegion (value) {
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
const parts = value.trim().split(/[,\s]+/)
|
|
24
|
+
if (parts.length !== 4 || parts.some(p => !p)) return null
|
|
25
|
+
value = Object.fromEntries(rectKeys.map((k, i) => [k, Number(parts[i])]))
|
|
26
|
+
}
|
|
27
|
+
if (!value || !rectKeys.every(k => finite(value[k])) || value.width <= 0 || value.height <= 0) return null
|
|
28
|
+
return Object.fromEntries(rectKeys.map(k => [k, value[k]]))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A native default verification crop is NOT authorization to narrow vision.
|
|
32
|
+
// Only an explicitly requested region changes the automatic image viewport.
|
|
33
|
+
export function feedbackViewport (args = {}, receipt = {}) {
|
|
34
|
+
args ||= {}; receipt ||= {}
|
|
35
|
+
const requested = (receipt.verificationSource === 'requested-region' ? feedbackRegion(receipt.verificationRegion) : null) ||
|
|
36
|
+
feedbackRegion(args.verificationRegion)
|
|
37
|
+
if (requested) return { region: rectKeys.map(k => requested[k]).join(','),
|
|
38
|
+
point: { x: Math.floor(requested.x + requested.width / 2), y: Math.floor(requested.y + requested.height / 2) }, source: 'requested-region' }
|
|
39
|
+
const actual = point(receipt)
|
|
40
|
+
if (actual) return { point: actual, source: 'action-receipt' }
|
|
41
|
+
const target = point(args.to) || point(args.at)
|
|
42
|
+
if (target) return { point: { x: target.x + (finite(args.dx) ? args.dx : 0), y: target.y + (finite(args.dy) ? args.dy : 0) }, source: 'requested-point' }
|
|
43
|
+
return { source: 'no-point' }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clipped (value, limit, cuts, path) {
|
|
47
|
+
const text = String(value), raw = Buffer.from(text, 'utf8')
|
|
48
|
+
if (raw.length <= limit) return text
|
|
49
|
+
let end = limit - 3
|
|
50
|
+
while (end > 0 && (raw[end] & 0xc0) === 0x80) end--
|
|
51
|
+
cuts.push(path)
|
|
52
|
+
return raw.subarray(0, end).toString('utf8') + '…'
|
|
53
|
+
}
|
|
54
|
+
function brief (value, keys, cuts, path, limit = 768) {
|
|
55
|
+
if (!value || typeof value !== 'object') return null
|
|
56
|
+
const result = {}
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
const item = value[key]
|
|
59
|
+
if (typeof item === 'string') result[key] = clipped(item, limit, cuts, path + '.' + key)
|
|
60
|
+
else if (typeof item === 'boolean' || finite(item) || item === null) result[key] = item
|
|
61
|
+
}
|
|
62
|
+
const rect = feedbackRegion(value.rect || value.bounds)
|
|
63
|
+
if (rect) result.rect = rect
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Budget applies to the ADDED observation envelope, never to an input receipt.
|
|
68
|
+
// Whole fields are omitted if needed; a truncated field is never silently complete.
|
|
69
|
+
export function limitCompactFeedback (envelope, maxBytes = COMPACT_FEEDBACK_BYTES) {
|
|
70
|
+
const limit = Number.isInteger(maxBytes) && maxBytes >= 1024 ? Math.min(maxBytes, COMPACT_FEEDBACK_BYTES) : COMPACT_FEEDBACK_BYTES
|
|
71
|
+
const result = JSON.parse(JSON.stringify(envelope))
|
|
72
|
+
const omitted = []
|
|
73
|
+
const existing = result.observation?.truncatedFields || []
|
|
74
|
+
const budget = result.budget = { maxBytes: limit, returnedBytes: 0, truncated: existing.length > 0, omittedFields: omitted }
|
|
75
|
+
const fit = () => {
|
|
76
|
+
// returnedBytes is part of its own JSON; stabilize decimal width.
|
|
77
|
+
for (let i = 0; i < 4; i++) budget.returnedBytes = bytes(result)
|
|
78
|
+
return budget.returnedBytes <= limit
|
|
79
|
+
}
|
|
80
|
+
if (fit()) return result
|
|
81
|
+
for (const field of ['ancestors', 'focused', 'element', 'receiver', 'window', 'contextFromAction']) {
|
|
82
|
+
if (result.observation && Object.hasOwn(result.observation, field)) {
|
|
83
|
+
delete result.observation[field]; omitted.push('observation.' + field); budget.truncated = true
|
|
84
|
+
if (fit()) return result
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Unknown adapter metadata is not allowed to evade the byte contract. Retain
|
|
88
|
+
// the truth that this observation could not fit, rather than cut JSON or paths.
|
|
89
|
+
result.observation = { scope: 'compact', incomplete: true, reason: 'observation exceeded compact byte budget; request feedback full for complete readback' }
|
|
90
|
+
omitted.push('observation'); budget.truncated = true
|
|
91
|
+
if (result.feedback) result.feedback = { kind: result.feedback.kind, detail: 'compact', reason: 'bounded post-action observation' }
|
|
92
|
+
if (!fit()) {
|
|
93
|
+
for (const key of Object.keys(result)) if (!['observation', 'feedback', 'verification', 'budget'].includes(key)) delete result[key]
|
|
94
|
+
if (typeof result.verification === 'string') result.verification = clipped(result.verification, 512, omitted, 'verification')
|
|
95
|
+
fit()
|
|
96
|
+
}
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function collectCompactState (cu, { args = {}, receipt = {}, checkAllowed = () => {}, archive, now = Date.now } = {}) {
|
|
101
|
+
args ||= {}; receipt ||= {}
|
|
102
|
+
const began = now(), deadline = began + COMPACT_OBSERVATION_MS, cuts = [], failures = []
|
|
103
|
+
const observation = { scope: 'local-post-action', ts: began, elementAvailable: false }
|
|
104
|
+
const raw = { scope: 'local-post-action', ts: began }
|
|
105
|
+
let viewport = feedbackViewport(args, receipt), calls = 0
|
|
106
|
+
const read = async (op, params, cap) => {
|
|
107
|
+
checkAllowed()
|
|
108
|
+
const remaining = Math.floor(deadline - now())
|
|
109
|
+
if (remaining <= 0) throw new Error('compact observation time budget exhausted')
|
|
110
|
+
calls++
|
|
111
|
+
return cu.call(op, params, Math.min(cap, remaining))
|
|
112
|
+
}
|
|
113
|
+
if (!viewport.point) {
|
|
114
|
+
try {
|
|
115
|
+
raw.cursor = await read('cursor', {}, 1000)
|
|
116
|
+
const current = point(raw.cursor)
|
|
117
|
+
if (current) viewport = { point: current, source: 'current-cursor' }
|
|
118
|
+
else failures.push({ channel: 'cursor', error: 'cursor position unavailable' })
|
|
119
|
+
} catch (error) { failures.push({ channel: 'cursor', error: clipped(error.message || error, 320, cuts, 'errors.cursor') }) }
|
|
120
|
+
}
|
|
121
|
+
if (viewport.point) {
|
|
122
|
+
observation.point = { ...viewport.point, source: viewport.source }
|
|
123
|
+
raw.point = observation.point
|
|
124
|
+
if (viewport.source === 'action-receipt' || viewport.source === 'current-cursor') observation.cursor = { ...viewport.point, source: viewport.source }
|
|
125
|
+
try {
|
|
126
|
+
const probe = await read('uiaFromPoint', viewport.point, COMPACT_POINT_MS)
|
|
127
|
+
raw.probe = probe
|
|
128
|
+
observation.window = brief(probe.root, windowKeys, cuts, 'window', 320)
|
|
129
|
+
observation.receiver = brief(probe.receives, windowKeys, cuts, 'receiver', 320)
|
|
130
|
+
observation.element = brief(probe.element, elementKeys, cuts, 'element')
|
|
131
|
+
observation.focused = brief(probe.focused, elementKeys, cuts, 'focused', 320)
|
|
132
|
+
if (typeof probe.typingWouldReach === 'boolean') observation.typingWouldReach = probe.typingWouldReach
|
|
133
|
+
const ancestors = probe.ancestors || probe.element?.ancestors || []
|
|
134
|
+
observation.ancestors = Array.isArray(ancestors) ? ancestors.slice(0, 3).map((a, i) => brief(a, elementKeys, cuts, `ancestors.${i}`, 160)) : []
|
|
135
|
+
if (Array.isArray(ancestors) && ancestors.length > 3) cuts.push('ancestors')
|
|
136
|
+
observation.elementAvailable = !!probe.element
|
|
137
|
+
if (!probe.element) failures.push({ channel: 'uiaFromPoint', error: 'no UIA element at this point; absence does not prove task failure' })
|
|
138
|
+
} catch (error) { failures.push({ channel: 'uiaFromPoint', error: clipped(error.message || error, 320, cuts, 'errors.uiaFromPoint') }) }
|
|
139
|
+
}
|
|
140
|
+
if (!viewport.point) observation.reason = 'No action, witness-region or current-cursor point was available; no UIA tree was read.'
|
|
141
|
+
if (receipt.receiver || receipt.focus || receipt.cursor) observation.contextFromAction = {
|
|
142
|
+
source: 'action-receipt, not an independent post-action read',
|
|
143
|
+
receiver: brief(receipt.receiver, windowKeys, cuts, 'context.receiver', 160),
|
|
144
|
+
focus: brief(receipt.focus, windowKeys, cuts, 'context.focus', 160),
|
|
145
|
+
cursor: point(receipt.cursor),
|
|
146
|
+
}
|
|
147
|
+
if (failures.length) observation.errors = failures
|
|
148
|
+
if (archive && (raw.probe || raw.cursor)) {
|
|
149
|
+
try {
|
|
150
|
+
const archived = await archive({ ...raw, ...(failures.length ? { errors: failures } : {}) })
|
|
151
|
+
const snapshot = typeof archived === 'string' ? archived : archived?.snapshot || archived?.observation
|
|
152
|
+
if (typeof snapshot === 'string' && /^O[\w-]{1,127}$/.test(snapshot)) observation.snapshot = snapshot
|
|
153
|
+
const cache = archived?.cache || archived?.archive
|
|
154
|
+
if (cache && (cache.status === 'memory-only' || cache.reason || cache.fallback)) {
|
|
155
|
+
observation.cache = brief(cache, ['status', 'reason', 'fallback'], cuts, 'cache', 160)
|
|
156
|
+
}
|
|
157
|
+
if (!observation.snapshot) {
|
|
158
|
+
// Once neither disk nor memory retains the evidence, truncation would
|
|
159
|
+
// make the omitted fields irretrievable. Return the existing raw value
|
|
160
|
+
// inline without another probe, input replay, clone or disk attempt.
|
|
161
|
+
observation.cache = { status: 'uncached', reason: cache?.reason || 'cache-unavailable', fallback: 'inline' }
|
|
162
|
+
observation.inlineEvidence = raw
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
observation.archiveError = clipped(error.message || error, 240, cuts, 'archiveError')
|
|
166
|
+
observation.cache = { status: 'uncached', reason: 'cache-unavailable', fallback: 'inline' }
|
|
167
|
+
observation.inlineEvidence = raw
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (cuts.length) observation.truncatedFields = [...new Set(cuts)]
|
|
171
|
+
observation.reads = calls
|
|
172
|
+
observation.elapsedMs = Math.max(0, now() - began)
|
|
173
|
+
observation.limitations = 'A local point/focus read may miss remote or visual changes. It does not verify the task effect; use explicit UIA/visual or application readback when needed.'
|
|
174
|
+
return observation
|
|
175
|
+
}
|
package/lib/continuation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Temporary ownership handoff is a task checkpoint, not a failed/cancelled task.
|
|
2
2
|
export function handoffResult (error) {
|
|
3
|
-
if (error?.code
|
|
3
|
+
if (!['HUMAN_REOBSERVE', 'WORKER_REOBSERVE'].includes(error?.code) || !error.continuation?.required) return null
|
|
4
4
|
return {
|
|
5
5
|
ok: false,
|
|
6
6
|
status: 'continuation-required',
|
|
Binary file
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"format": 1,
|
|
3
3
|
"platform": "win32",
|
|
4
4
|
"architecture": "x64",
|
|
5
|
-
"sourceSha256": "
|
|
6
|
-
"binarySha256": "
|
|
5
|
+
"sourceSha256": "b448b6a665147bde3698471c0a36897188fd26688338a919b45ac2d96b175a95",
|
|
6
|
+
"binarySha256": "f8042acb806f07240ee5ba70eda3b235fc8f7e668318cc44e29b889a82b0b647"
|
|
7
7
|
}
|