dsh-codex-style-computer-use 1.3.0 → 1.3.1
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.md +24 -15
- package/README.zh.md +23 -15
- package/icon.svg +29 -0
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +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/compile-worker.ps1 +5 -1
- package/lib/core/index.js +98 -11
- package/lib/core/native-runtime.js +6 -1
- package/lib/core/winmd/Windows.winmd +0 -0
- package/lib/core/worker.cs +653 -155
- package/lib/index.js +17 -0
- package/lib/policy.js +23 -3
- package/lib/tools.js +40 -2
- package/package.json +4 -2
- package/scripts/render-promo.py +81 -0
- package/skills/computer-use/SKILL.md +17 -2
- package/skills/computer-use/references/lifecycle.md +2 -2
- package/skills/computer-use/references/zero-uia-apps.md +54 -0
package/README.md
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Codex-style Computer Use
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Bring a Codex-style desktop workflow to DeepSeek Harness.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Describe the goal. Your agent observes the screen, locates controls, clicks and types, then checks the result. Work with native Windows apps from the conversation.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
native Windows applications through prebuilt C# workers included in the package. No PowerShell 7,
|
|
9
|
-
SDK or install-time builds are needed.
|
|
7
|
+
English | [中文](README.zh.md) · [Install](#install-and-update-in-dsh-market) · [Changelog](CHANGELOG.md)
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+

|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
- **See the target. Take the action.** Combine screenshots, UI Automation and numbered targets with mouse, keyboard, drag, scroll and window tools.
|
|
12
|
+
- **Take over. Let the agent wait. Hand back and continue.** Version 1.3.0 adds a 1.5-second notice before input. Use your computer normally outside actual actuation; after handback, the agent re-observes and resumes the original task.
|
|
13
|
+
- **Fewer round trips. More efficient token use.** Incremental control observations, cropped or downsampled screenshots and batches of known actions reduce repeated context and model calls.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+

|
|
16
16
|
|
|
17
|
-
|
|
17
|
+

|
|
18
18
|
|
|
19
|
-
**1.
|
|
19
|
+
These are **1.3.0 feature illustrations**. Actual token use depends on the task, model and observation strategy. Approvals remain enabled by default; explicit pause, cancellation and exit remain authoritative.
|
|
20
20
|
|
|
21
|
-
**
|
|
21
|
+
**Ready to install.** 18 desktop tools, with native helpers included. No SDK or install-time compilation. Requires Windows, Node.js ≥ 22 and the .NET Framework 4.x included with Windows. An independent community plugin; Codex describes the interaction style.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
<details>
|
|
24
|
+
<summary>Current release and compatibility</summary>
|
|
25
|
+
|
|
26
|
+
**1.3.1** is the current release on npm's `latest` channel: `dsh-codex-style-computer-use@1.3.1`. It adds worker-side OCR with per-word coordinates, change/caret-aware waits, UAC and lock-screen awareness with automatic continuation, and IME-safe clipboard text. Follow the host's restart indication when updating a loaded version. See [1.3.1](docs/RELEASE-1.3.1.md) and [GitHub Releases](https://github.com/ltmroberthk915/dsh-computer-use/releases).
|
|
27
|
+
|
|
28
|
+
Version 1.3.1 reads text from the screen by name, notices the Windows secure desktop and resumes on its own, and keeps every colour on the same DPI-derived edge rail with a visible breathing floor. Version 1.3.0 releases desktop control when work ends while the chat continues.
|
|
29
|
+
|
|
30
|
+
Earlier fixes: [1.2.3 approval attribution](docs/RELEASE-1.2.3.md) · [1.2.1 native attention](docs/RELEASE-1.2.1.md) · [standalone host patch](docs/RELEASE-1.2.2.md).
|
|
31
|
+
|
|
32
|
+
</details>
|
|
24
33
|
|
|
25
34
|
## What it does
|
|
26
35
|
|
|
@@ -41,9 +50,9 @@ under `$DSH_HOME/data/computer-use/`.
|
|
|
41
50
|
|
|
42
51
|
## Install and update in dsh-market
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
Search **computer-use** or **Codex-style** in the market. Look for repository **ltmroberthk915/dsh-computer-use** and npm package **dsh-codex-style-computer-use** (maintainer: **ltmroberthk**), then click **Install**. Use **Update** or **Update all** for stable-channel updates. Both native helpers are included; no PowerShell 7 or build permission is needed.
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
Alternatively, enter **`dsh-codex-style-computer-use`** in **Settings → Plugins → Add plugin** for the current stable channel, or **`dsh-codex-style-computer-use@1.3.0`** for the version illustrated here. Enter the npm package spec directly. Your host may apply a waiting period to fresh versions; follow its indication. A newly added bundle can load live on supported hosts; check that its tools and skill appear. Restart from the tray when replacing a loaded version, when the client remains stale, or when DSH reports **restart-required**.
|
|
47
56
|
|
|
48
57
|
### Releases less than 24 hours old
|
|
49
58
|
|
package/README.zh.md
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Codex 风格 Computer Use
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**更接近 Codex 的电脑操控体验,带到 DeepSeek Harness。**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
你描述目标,AI 看屏幕、找控件、点鼠标、敲键盘,再检查结果。让原生 Windows 应用里的工作,也能在对话中完成。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
观察并驱动原生 Windows 应用。发布包已包含编译好的原生组件,无需安装 PowerShell 7、SDK 或执行编译命令。
|
|
7
|
+
[English](README.md) | 中文 · [安装](#在-dsh-market-安装和更新) · [版本记录](CHANGELOG.md)
|
|
9
8
|
|
|
10
|
-
[
|
|
9
|
+

|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- **看得准,也点得动。** 截图、控件树与编号定位配合使用,支持点击、输入、拖拽、滚动和窗口操作。
|
|
12
|
+
- **你接手,AI 等待,再接着做。** 1.3.0 提供 1.5 秒操作预告;非实际操控期间正常使用电脑,交回后重新观察并接续原任务。
|
|
13
|
+
- **更少往返,更省 Token。** 控件增量观察、区域与降采样截图,加上连续动作批量执行,减少重复观察和模型往返。
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+

|
|
15
16
|
|
|
16
|
-
|
|
17
|
+

|
|
17
18
|
|
|
18
|
-
**1.
|
|
19
|
+
上图为 **1.3.0 功能示意**。实际 Token 用量取决于任务、模型和观察方式。默认保留操作审批;明确暂停、取消和退出始终有效。
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
**安装即用。** 18 个桌面工具,原生组件随包提供,无需 SDK 或安装时编译。支持 Windows、Node.js ≥ 22 和系统自带的 .NET Framework 4.x。独立社区插件;Codex 用于说明交互风格。
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
<details>
|
|
24
|
+
<summary>当前版本与兼容性</summary>
|
|
23
25
|
|
|
24
|
-
**1.
|
|
26
|
+
**1.3.1** 是 npm `latest` 频道的当前版本:`dsh-codex-style-computer-use@1.3.1`。新增 worker 侧 OCR(逐词坐标)、可定位变化与识别光标闪烁的等待、UAC/锁屏感知与自动续接、以及绕过 IME 的剪贴板文本通道。更新已加载的旧版本后,按宿主提示重启。详见 [1.3.1](docs/RELEASE-1.3.1.md) 与 [GitHub Releases](https://github.com/ltmroberthk915/dsh-computer-use/releases)。
|
|
27
|
+
|
|
28
|
+
1.3.1 可直接从屏幕读字定位控件,察觉 Windows 安全桌面并在其结束后自动续接;所有颜色共用同一条按 DPI 换算的边缘光环,呼吸谷值保持可见。1.3.0 起工具用完可主动释放,聊天继续;正常收尾自动退出。
|
|
29
|
+
|
|
30
|
+
历史兼容修复:[1.2.3 审批归因](docs/RELEASE-1.2.3.md) · [1.2.1 原生提醒](docs/RELEASE-1.2.1.md) · [独立宿主补丁](docs/RELEASE-1.2.2.md)。
|
|
31
|
+
|
|
32
|
+
</details>
|
|
25
33
|
|
|
26
34
|
## 它能做什么
|
|
27
35
|
|
|
@@ -39,9 +47,9 @@
|
|
|
39
47
|
|
|
40
48
|
## 在 dsh-market 安装和更新
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
在市场搜索 **computer-use** 或 **Codex-style**,认准仓库 **ltmroberthk915/dsh-computer-use**、npm 包 **dsh-codex-style-computer-use**(维护者 **ltmroberthk**),点击**安装**。以后通过市场的**更新**或**全部更新**获取正式频道版本。两份原生组件都已随包提供,无需 PowerShell 7、SDK 或构建白名单。
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
也可在 **设置 → 插件 → 添加插件** 输入 **`dsh-codex-style-computer-use`**,安装当前正式频道;要体验本页展示的 1.3.0,输入 **`dsh-codex-style-computer-use@1.3.0`**。这里填 npm 包名即可。宿主可能对新版本设置等待期,以界面提示为准。首次新增 bundle 可在支持的宿主中热加载;确认工具与 skill 已出现即可使用。替换已经加载的版本、客户端界面未刷新或界面提示 **restart-required / 需要重启** 时,从托盘完整退出 DSH 并重新打开。
|
|
45
53
|
|
|
46
54
|
### 发布未满 24 小时
|
|
47
55
|
|
package/icon.svg
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="tile" x1="4" y1="0" x2="28" y2="32" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#17243F"/>
|
|
5
|
+
<stop offset="1" stop-color="#05080F"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<radialGradient id="halo" cx="11" cy="27" r="20" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#8B5CF6" stop-opacity=".2"/>
|
|
9
|
+
<stop offset="1" stop-color="#8B5CF6" stop-opacity="0"/>
|
|
10
|
+
</radialGradient>
|
|
11
|
+
<linearGradient id="rail" x1="5" y1="6" x2="27" y2="24" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop stop-color="#5EEAD4"/>
|
|
13
|
+
<stop offset=".5" stop-color="#38BDF8"/>
|
|
14
|
+
<stop offset="1" stop-color="#A78BFA"/>
|
|
15
|
+
</linearGradient>
|
|
16
|
+
<radialGradient id="glow" cx="18.2" cy="19.4" r="7.6" gradientUnits="userSpaceOnUse">
|
|
17
|
+
<stop stop-color="#38BDF8" stop-opacity=".32"/>
|
|
18
|
+
<stop offset="1" stop-color="#38BDF8" stop-opacity="0"/>
|
|
19
|
+
</radialGradient>
|
|
20
|
+
</defs>
|
|
21
|
+
<rect width="32" height="32" rx="7.5" fill="url(#tile)"/>
|
|
22
|
+
<rect width="32" height="32" rx="7.5" fill="url(#halo)"/>
|
|
23
|
+
<rect x="5.6" y="5.9" width="20.8" height="16" rx="3" fill="#FFFFFF" fill-opacity=".06" stroke="url(#rail)" stroke-width="1.9"/>
|
|
24
|
+
<path d="M5.6 10.1 H26.4" stroke="url(#rail)" stroke-width="1.05" stroke-opacity=".5"/>
|
|
25
|
+
<circle cx="8.5" cy="8" r=".9" fill="#5EEAD4"/>
|
|
26
|
+
<circle cx="11.2" cy="8" r=".9" fill="#A78BFA" fill-opacity=".85"/>
|
|
27
|
+
<circle cx="18.2" cy="19.4" r="7.6" fill="url(#glow)"/>
|
|
28
|
+
<path d="M15.6 12.4 L15.6 21.2 L17.8 19 L19.3 22.4 L20.8 21.7 L19.3 18.5 L22.3 18.5 Z" fill="#F8FAFC" stroke="#070B14" stroke-width=".85" stroke-linejoin="round"/>
|
|
29
|
+
</svg>
|
|
Binary file
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"platform": "win32",
|
|
4
4
|
"architecture": "x64",
|
|
5
5
|
"sourceSha256": "6e17308fc1cb0de1ece6f3164866b6de03ecdf74a42a6e78c0a8e6d02912c3bf",
|
|
6
|
-
"binarySha256": "
|
|
6
|
+
"binarySha256": "f39e79148d0f1c8fa527f55607eb4e7440e82c3ab5b33162bde773528418fe5a"
|
|
7
7
|
}
|
|
Binary file
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"format": 1,
|
|
3
3
|
"platform": "win32",
|
|
4
4
|
"architecture": "x64",
|
|
5
|
-
"sourceSha256": "
|
|
6
|
-
"binarySha256": "
|
|
5
|
+
"sourceSha256": "8427a18e1f161959043f638cab821e701714f3d83138911dc9c4fb2cbeb152bf",
|
|
6
|
+
"binarySha256": "0e9212f628ebd73655de1235b3e7bb4ce176c4b60da16c336f2ebae7397084a2"
|
|
7
7
|
}
|
|
@@ -47,7 +47,11 @@ $refs = @(
|
|
|
47
47
|
'/r:System.Web.Extensions.dll', '/r:System.Windows.Forms.dll',
|
|
48
48
|
"/r:$gac\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll",
|
|
49
49
|
"/r:$gac\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll",
|
|
50
|
-
"/r:$gac\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll"
|
|
50
|
+
"/r:$gac\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll",
|
|
51
|
+
# Inbox OCR (2026-11-04): union winmd ships in the package next to this script.
|
|
52
|
+
"/r:$env:windir\Microsoft.NET\Framework64\v4.0.30319\System.Runtime.dll",
|
|
53
|
+
"/r:$gac\System.Runtime.WindowsRuntime\v4.0_4.0.0.0__b77a5c561934e089\System.Runtime.WindowsRuntime.dll",
|
|
54
|
+
"/r:$PSScriptRoot\winmd\Windows.winmd"
|
|
51
55
|
)
|
|
52
56
|
|
|
53
57
|
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
package/lib/core/index.js
CHANGED
|
@@ -22,6 +22,10 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
|
22
22
|
|
|
23
23
|
const DEFAULT_TIMEOUT_MS = 15_000
|
|
24
24
|
const CAPTURE_TIMEOUT_MS = 30_000
|
|
25
|
+
// A UIA tree walk can block the resident's synchronous COM thread. Bound the
|
|
26
|
+
// caller's wait separately from recovery of a worker that never returns.
|
|
27
|
+
const UIA_READS = new Set(['uia', 'uiaFromPoint', 'uiaFocused'])
|
|
28
|
+
const URGENT_CONTROL = new Set(['cancelInput', 'abort', 'panic', 'exit', 'endAsk'])
|
|
25
29
|
const INPUT_OPS = new Set(['click', 'shiftClick', 'move', 'drag', 'scroll', 'selectRange',
|
|
26
30
|
'type', 'key', 'uiaAct', 'windowOp', 'activate', 'clipWrite', 'calibrate', 'selftest'])
|
|
27
31
|
const isInputOp = (op, args = {}) => INPUT_OPS.has(op) || (op === 'focus' && args.refocus === true)
|
|
@@ -148,6 +152,7 @@ export class ComputerUse extends EventEmitter {
|
|
|
148
152
|
// bare `Error: spawn UNKNOWN` — no path, no syscall, no Windows code — because NOTHING listened
|
|
149
153
|
// for this child's 'error' event: an unhandled 'error' on an EventEmitter is thrown, so the rich
|
|
150
154
|
// error object (which names the file Windows refused) was discarded on the way out.
|
|
155
|
+
const child = this.proc
|
|
151
156
|
this.proc.on('error', (e) => {
|
|
152
157
|
let file = '?'
|
|
153
158
|
try { file = `${existsSync(this.workerExe)} exists, ${statSync(this.workerExe).size} bytes` } catch { file = 'stat failed' }
|
|
@@ -165,11 +170,11 @@ export class ComputerUse extends EventEmitter {
|
|
|
165
170
|
const line = buf.slice(0, nl).trim()
|
|
166
171
|
buf = buf.slice(nl + 1)
|
|
167
172
|
if (!line) continue
|
|
168
|
-
this.onLine(line)
|
|
173
|
+
if (this.proc === child) this.onLine(line)
|
|
169
174
|
}
|
|
170
175
|
})
|
|
171
176
|
this.proc.stderr.on('data', (d) => this.log(`[worker:stderr] ${d}`))
|
|
172
|
-
this.proc.on('exit', this.onWorkerExit)
|
|
177
|
+
this.proc.on('exit', (code) => { if (this.proc === child) this.onWorkerExit(code) })
|
|
173
178
|
return new Promise((resolve, reject) => {
|
|
174
179
|
const t = setTimeout(() => reject(new Error('worker ready timeout')), 10_000)
|
|
175
180
|
// surface the real reason instead of letting the raw emitter error escape
|
|
@@ -185,6 +190,7 @@ export class ComputerUse extends EventEmitter {
|
|
|
185
190
|
onWorkerExit (code) {
|
|
186
191
|
this.ready = false
|
|
187
192
|
this.proc = null
|
|
193
|
+
this.finishUiaRead(false)
|
|
188
194
|
for (const [id, p] of this.pending) { clearTimeout(p.timer); p.cleanup?.(); p.reject(workerCallError('worker exited', 'worker-exited', p.op, id)) }
|
|
189
195
|
this.pending.clear()
|
|
190
196
|
this.emit('dead', code)
|
|
@@ -211,6 +217,14 @@ export class ComputerUse extends EventEmitter {
|
|
|
211
217
|
this.syncCooperation(msg.cooperation)
|
|
212
218
|
return
|
|
213
219
|
}
|
|
220
|
+
// Secure-desktop transitions (UAC prompt / lock screen, human request 2026-11-04): the
|
|
221
|
+
// worker refuses to dispatch while it stands and clears it the moment the normal desktop
|
|
222
|
+
// returns. Mirror the state so callers can show it, and let the plugin notify the human.
|
|
223
|
+
if (msg.event === 'secure') {
|
|
224
|
+
this.secureDesktop = msg.why === 'active'
|
|
225
|
+
this.emit('secure', msg)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
214
228
|
// Durable brake events are separate from automatic physical-input handoff.
|
|
215
229
|
// panic — ESC: the human ABORTED. The machine is stopped; wrap up briefly, then wait.
|
|
216
230
|
// resume — Ctrl+Alt+R: work may continue (a PAUSE ends; it never ends the cycle).
|
|
@@ -236,6 +250,7 @@ export class ComputerUse extends EventEmitter {
|
|
|
236
250
|
this.emit(msg.event, msg)
|
|
237
251
|
return
|
|
238
252
|
}
|
|
253
|
+
if (this.uiaRead?.id === msg.id) this.finishUiaRead(true)
|
|
239
254
|
const p = this.pending.get(msg.id)
|
|
240
255
|
if (!p) return
|
|
241
256
|
this.pending.delete(msg.id)
|
|
@@ -246,6 +261,7 @@ export class ComputerUse extends EventEmitter {
|
|
|
246
261
|
else {
|
|
247
262
|
const error = workerCallError(msg.error || 'worker error', msg.code || 'worker-refusal-or-error', p.op, msg.id,
|
|
248
263
|
msg.outcome === 'not-dispatched' ? 'not-dispatched' : 'unknown')
|
|
264
|
+
if (msg.glowHealth) error.glowHealth = msg.glowHealth
|
|
249
265
|
p.reject(error.code === 'HUMAN_REOBSERVE' ? humanReobserveError(error) : error)
|
|
250
266
|
}
|
|
251
267
|
}
|
|
@@ -266,6 +282,14 @@ export class ComputerUse extends EventEmitter {
|
|
|
266
282
|
context?.assertOwnership?.()
|
|
267
283
|
try { await this.ensureWorker() }
|
|
268
284
|
catch (error) { throw workerCallError(error.message || String(error), 'worker-unavailable', op, null, 'not-dispatched') }
|
|
285
|
+
const child = this.proc
|
|
286
|
+
if (!child) throw workerCallError('worker exited before dispatch', 'worker-exited', op, null, 'not-dispatched')
|
|
287
|
+
const queueDeadline = Date.now() + timeoutMs
|
|
288
|
+
while (this.uiaRead && !URGENT_CONTROL.has(op)) {
|
|
289
|
+
await this.waitForUiaRead(op, input, signal, queueDeadline)
|
|
290
|
+
if (this.proc !== child) throw workerCallError('worker exited before dispatch', 'worker-exited', op, null, 'not-dispatched')
|
|
291
|
+
if (Date.now() >= queueDeadline) throw workerCallError('deadline expired waiting for the UIA read', 'worker-uia-busy', op, null, 'not-dispatched')
|
|
292
|
+
}
|
|
269
293
|
if (signal?.aborted) throw workerCallError('owning tool cancelled', 'INPUT_CANCELLED', op, null, 'not-dispatched')
|
|
270
294
|
beforeDispatch?.()
|
|
271
295
|
context?.assertOwnership?.()
|
|
@@ -282,6 +306,7 @@ export class ComputerUse extends EventEmitter {
|
|
|
282
306
|
if (input) signed.__inputSeries = this.inputContext.getStore()?.inputSeries || randomUUID()
|
|
283
307
|
return new Promise((resolve, reject) => {
|
|
284
308
|
let written = false
|
|
309
|
+
const read = UIA_READS.has(op) && killOnTimeout ? (this.uiaRead = { id, child, timedOut: false }) : null
|
|
285
310
|
const cleanup = () => signal?.removeEventListener('abort', abort)
|
|
286
311
|
const abort = () => {
|
|
287
312
|
if (!this.pending.delete(id)) return
|
|
@@ -293,6 +318,21 @@ export class ComputerUse extends EventEmitter {
|
|
|
293
318
|
const timer = setTimeout(() => {
|
|
294
319
|
this.pending.delete(id)
|
|
295
320
|
cleanup()
|
|
321
|
+
if (read && this.uiaRead === read) {
|
|
322
|
+
read.timedOut = true
|
|
323
|
+
this.emit('uia-read-settled', false)
|
|
324
|
+
// Drop the response by id when it eventually arrives. A later reply for
|
|
325
|
+
// this id cancels only this watchdog, never a newer worker's deadline.
|
|
326
|
+
const grace = Math.min(timeoutMs, DEFAULT_TIMEOUT_MS)
|
|
327
|
+
read.watchdog = setTimeout(() => {
|
|
328
|
+
if (this.proc !== child || this.uiaRead !== read) return
|
|
329
|
+
this.log(`[core] UIA read ${id} (${op}) still blocked after ${grace}ms recovery grace; stopping worker pid=${child.pid}`)
|
|
330
|
+
try { child.kill() } catch { /* already gone */ }
|
|
331
|
+
}, grace)
|
|
332
|
+
reject(workerCallError(`UIA observation timed out: ${op} (${timeoutMs}ms); worker still finishing this read. Narrow the tree or use a point query once it returns.`,
|
|
333
|
+
'worker-timeout', op, id, 'not-dispatched'))
|
|
334
|
+
return
|
|
335
|
+
}
|
|
296
336
|
// A CALL THAT RELEASES THE BRAKE MUST NOT BE KILLED FOR BEING SLOW. `proc.kill()` deletes no
|
|
297
337
|
// STOP file (there is no ProcessExit handler in worker.cs; `abort` is Environment.Exit(0) at
|
|
298
338
|
// worker.cs:3045), so killing here would end a question with the brake still up — the exact
|
|
@@ -303,18 +343,51 @@ export class ComputerUse extends EventEmitter {
|
|
|
303
343
|
// UIA/COM provider call blocks its single-threaded loop forever), so kill it and let the NEXT
|
|
304
344
|
// call respawn fresh instead of queueing behind a corpse; pending calls are rejected via
|
|
305
345
|
// onWorkerExit.
|
|
306
|
-
if (killOnTimeout) { try { this.proc
|
|
346
|
+
if (killOnTimeout) { try { if (this.proc === child) child.kill() } catch { /* already gone */ } }
|
|
307
347
|
reject(workerCallError(`worker timeout: ${op} (${timeoutMs}ms)` +
|
|
308
348
|
(killOnTimeout
|
|
309
349
|
? ' — worker killed, next call respawns'
|
|
310
350
|
: ' — worker left alive so the brake can still be released'),
|
|
311
351
|
'worker-timeout', op, id))
|
|
312
|
-
},
|
|
352
|
+
}, Math.max(0, queueDeadline - Date.now()))
|
|
313
353
|
this.pending.set(id, { resolve, reject, timer, op, input, cleanup, abort })
|
|
314
354
|
signal?.addEventListener('abort', abort, { once: true })
|
|
315
355
|
if (signal?.aborted) { abort(); return }
|
|
316
356
|
written = true
|
|
317
|
-
|
|
357
|
+
child.stdin.write(JSON.stringify({ id, op, args: signed }) + '\r\n')
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
finishUiaRead (completed) {
|
|
362
|
+
const read = this.uiaRead
|
|
363
|
+
if (!read) return
|
|
364
|
+
this.uiaRead = null
|
|
365
|
+
clearTimeout(read.watchdog)
|
|
366
|
+
this.emit('uia-read-settled', completed && !read.timedOut)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Only the stdin reader's cancellation command is out of band. Preserve
|
|
370
|
+
// emergency/release requests, but never queue ordinary work behind a slow
|
|
371
|
+
// observation after its caller has given up. Do not replay any request.
|
|
372
|
+
waitForUiaRead (op, input, signal, deadline) {
|
|
373
|
+
const busy = () => workerCallError('native UIA observation still busy; this request was not sent',
|
|
374
|
+
'worker-uia-busy', op, null, 'not-dispatched')
|
|
375
|
+
if (this.uiaRead.timedOut || Date.now() >= deadline) return Promise.reject(busy())
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
const finish = (error) => {
|
|
378
|
+
clearTimeout(timer)
|
|
379
|
+
this.off('uia-read-settled', settled)
|
|
380
|
+
this.off('input-cancel', cancel)
|
|
381
|
+
signal?.removeEventListener('abort', cancel)
|
|
382
|
+
if (error) reject(error); else resolve()
|
|
383
|
+
}
|
|
384
|
+
const settled = (completed) => finish(completed ? null : busy())
|
|
385
|
+
const cancel = () => finish(workerCallError('cancelled before dispatch', 'INPUT_CANCELLED', op, null, 'not-dispatched'))
|
|
386
|
+
const timer = setTimeout(() => finish(busy()), Math.max(0, deadline - Date.now()))
|
|
387
|
+
this.once('uia-read-settled', settled)
|
|
388
|
+
if (input) this.once('input-cancel', cancel)
|
|
389
|
+
signal?.addEventListener('abort', cancel, { once: true })
|
|
390
|
+
if (signal?.aborted) cancel()
|
|
318
391
|
})
|
|
319
392
|
}
|
|
320
393
|
|
|
@@ -643,15 +716,21 @@ export class ComputerUse extends EventEmitter {
|
|
|
643
716
|
async landmarks (opts = {}) {
|
|
644
717
|
const step = opts.step ?? 16
|
|
645
718
|
const insetPct = opts.insetPct ?? 10
|
|
719
|
+
// UIA-ONLY MODE (test report 2026-11-04): an animated neighbour (DSH Pet) keeps flipping the
|
|
720
|
+
// sampled pixel signature, so every refresh came back STALE_MARK. uiaOnly pins identity by
|
|
721
|
+
// window handle / process / geometry alone — the sampled signature is read (for the report)
|
|
722
|
+
// but never compared; dispatch-time occlusion checking still guards the actual click.
|
|
723
|
+
const uiaOnly = opts.uiaOnly === true
|
|
646
724
|
const fs = await this.frameSig({ hwnd: opts.hwnd, step, insetPct })
|
|
725
|
+
if (uiaOnly) fs.sig = null
|
|
647
726
|
const hwnd = fs.hwnd
|
|
648
|
-
if (!hwnd || !fs.sig) throw staleMark('window snapshot unavailable')
|
|
727
|
+
if (!hwnd || (!fs.sig && !uiaOnly)) throw staleMark('window snapshot unavailable')
|
|
649
728
|
const cached = this.maps.get(String(hwnd))
|
|
650
|
-
const key = JSON.stringify([opts.depth ?? 14, opts.maxNodes ?? 1500, opts.maxMarks ?? 80, opts.minSize ?? 8, opts.role, opts.nameContains])
|
|
729
|
+
const key = JSON.stringify([opts.depth ?? 14, opts.maxNodes ?? 1500, opts.maxMarks ?? 80, opts.minSize ?? 8, opts.role, opts.nameContains, uiaOnly])
|
|
651
730
|
if (cached && sameMarkFrame(cached.frame, fs) && cached.key === key && !opts.refresh) {
|
|
652
731
|
this.lastMarks = cached.marks
|
|
653
732
|
this.lastMarkFrame = { ...fs, step, insetPct }
|
|
654
|
-
return { hwnd, marks: cached.marks, count: cached.marks.length, reused: true, ageMs: Date.now() - cached.ts, sig: fs.sig }
|
|
733
|
+
return { hwnd, marks: cached.marks, count: cached.marks.length, reused: true, ageMs: Date.now() - cached.ts, sig: fs.sig, uiaOnly }
|
|
655
734
|
}
|
|
656
735
|
const uia = await this.call('uia', {
|
|
657
736
|
hwnd: hwnd || undefined,
|
|
@@ -662,6 +741,7 @@ export class ComputerUse extends EventEmitter {
|
|
|
662
741
|
})
|
|
663
742
|
const marks = buildMarks(uia.flat || [], opts.maxMarks ?? 80, opts.minSize ?? 8)
|
|
664
743
|
const after = await this.frameSig({ hwnd, step, insetPct })
|
|
744
|
+
if (uiaOnly) after.sig = null
|
|
665
745
|
if (!sameMarkFrame(fs, after)) {
|
|
666
746
|
const changed = ['hwnd', 'pid'].filter(k => fs[k] !== after[k])
|
|
667
747
|
for (const k of ['x', 'y', 'width', 'height']) if (fs.region?.[k] !== after.region?.[k]) changed.push(`region.${k}`)
|
|
@@ -674,7 +754,7 @@ export class ComputerUse extends EventEmitter {
|
|
|
674
754
|
this.lastMarkFrame = { ...after, step, insetPct }
|
|
675
755
|
this.maps.set(String(hwnd), { marks, frame: after, sig: after.sig, key, ts: Date.now() })
|
|
676
756
|
if (this.maps.size > 16) this.maps.delete(this.maps.keys().next().value)
|
|
677
|
-
return { hwnd, marks, count: marks.length, reused: false, sig: fs ? fs.sig : null, elements: (uia.flat || []).length }
|
|
757
|
+
return { hwnd, marks, count: marks.length, reused: false, sig: fs ? fs.sig : null, uiaOnly, elements: (uia.flat || []).length }
|
|
678
758
|
}
|
|
679
759
|
|
|
680
760
|
// The human's emergency brake, reachable from the model side too (ESC / Ctrl+Alt+Q are
|
|
@@ -748,6 +828,8 @@ export class ComputerUse extends EventEmitter {
|
|
|
748
828
|
}
|
|
749
829
|
|
|
750
830
|
async clipRead () { return this.call('clipRead', {}) }
|
|
831
|
+
/** Worker-side OCR: text + per-word boxes in ABSOLUTE screen pixels (2026-11-04). */
|
|
832
|
+
async ocr (region) { return this.call('ocr', region ? { ...region } : {}) }
|
|
751
833
|
async clipWrite (text) { return this.actuate('clipWrite', { text }) }
|
|
752
834
|
|
|
753
835
|
async activateWindow (sel) { return this.actuate('activate', sel) }
|
|
@@ -837,8 +919,13 @@ function staleMark (reason) {
|
|
|
837
919
|
}
|
|
838
920
|
|
|
839
921
|
function sameMarkFrame (a, b) {
|
|
840
|
-
|
|
841
|
-
['x', 'y', 'width', 'height'].every(k => Number.isFinite(a.region?.[k]) && a.region[k] === b.region
|
|
922
|
+
const identity = a?.hwnd === b?.hwnd && a?.pid === b?.pid &&
|
|
923
|
+
['x', 'y', 'width', 'height'].every(k => Number.isFinite(a.region?.[k]) && a.region[k] === b.region[k])
|
|
924
|
+
if (!identity) return false
|
|
925
|
+
// A uiaOnly landmark frame (sig === null, test report 2026-11-04) deliberately carries no
|
|
926
|
+
// pixel signature: identity is geometry alone, and the click path's occlusion check remains.
|
|
927
|
+
if (a.sig === null) return true
|
|
928
|
+
return !!a?.sig && a.sig === b?.sig
|
|
842
929
|
}
|
|
843
930
|
|
|
844
931
|
/** Keep secrets out of the audit log. */
|
|
@@ -13,7 +13,12 @@ const framework = env => path.join(env.SystemRoot || env.windir || 'C:\\Windows'
|
|
|
13
13
|
export function workerReferences (env = process.env) {
|
|
14
14
|
const gac = path.join(env.SystemRoot || env.windir || 'C:\\Windows', 'Microsoft.NET', 'assembly', 'GAC_MSIL')
|
|
15
15
|
return ['System.dll', 'System.Core.dll', 'System.Drawing.dll', 'System.Web.Extensions.dll', 'System.Windows.Forms.dll',
|
|
16
|
-
...['UIAutomationClient', 'UIAutomationTypes', 'WindowsBase'].map(name => path.join(gac, name, 'v4.0_4.0.0.0__31bf3856ad364e35', `${name}.dll`))
|
|
16
|
+
...['UIAutomationClient', 'UIAutomationTypes', 'WindowsBase'].map(name => path.join(gac, name, 'v4.0_4.0.0.0__31bf3856ad364e35', `${name}.dll`)),
|
|
17
|
+
// Inbox OCR (2026-11-04): the legacy csc cannot compose the split WinMetadata winmds, so the
|
|
18
|
+
// union Windows.winmd ships inside the package and the WinRT interop pieces come from the OS.
|
|
19
|
+
path.join(framework(env), 'System.Runtime.dll'),
|
|
20
|
+
path.join(gac, 'System.Runtime.WindowsRuntime', 'v4.0_4.0.0.0__b77a5c561934e089', 'System.Runtime.WindowsRuntime.dll'),
|
|
21
|
+
path.join(import.meta.dirname, 'winmd', 'Windows.winmd')]
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export function compileNativeBinary (source, output, references, { env = process.env } = {}) {
|
|
Binary file
|