dsh-plugin-computer-use 2.4.1 → 2.5.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/LICENSE +21 -21
- package/README.md +65 -1
- package/index.js +30 -12
- package/linux.js +41 -0
- package/package.json +8 -3
- package/scripts/wayland.mjs +98 -0
- package/wayland-helper/LICENSE.md +21 -0
- package/wayland-helper/NOTICE.md +17 -0
- package/wayland-helper/winrects@cua/extension.js +1053 -0
- package/wayland-helper/winrects@cua/metadata.json +7 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 dsh-plugin contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-plugin contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,6 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
让模型**看见窗口和桌面并操作鼠标键盘**——v2 起整个后端换装 [Cua Driver](https://cua.ai) 的**进程内 SDK**(`@trycua/cua-driver`,Rust 原生运行时随 npm 包按平台分发,直接加载进 dsh 宿主进程):**无守护进程、无 MCP、无 PowerShell**,装上即用,跨 Windows / macOS / Linux。
|
|
4
4
|
|
|
5
|
+
## v2.5:Ubuntu 26.04 / GNOME Wayland
|
|
6
|
+
|
|
7
|
+
旧版没有开启驱动的原生 Wayland 开关,即使 `DISPLAY=:0` 存在,它也可能只是 XWayland,无法代表整个桌面。v2.5 在加载原生库前检测 `XDG_SESSION_TYPE` / `WAYLAND_DISPLAY`,自动设置 `CUA_DRIVER_RS_ENABLE_WAYLAND=1`;驱动依赖最低升到 npm 已发布的 0.28.3。环境中显式设置的 `0` / `false` 会保留,行配置 `wayland: on` / `off` 可覆盖。
|
|
8
|
+
|
|
9
|
+
GNOME/Mutter 还需要 **WinRects Shell 扩展**提供窗口几何、截图和精确激活;输入通过 Remote Desktop portal/libei 获取系统授权。npm SDK 没带 CLI 发行包中的扩展,因此本插件随包附带上游 WinRects v8(支持 GNOME 45–50;含来源与 MIT 许可证),并提供安装和检查入口。无需另装 Cua 守护进程。
|
|
10
|
+
|
|
11
|
+
**从 npm 安装/更新**(在 Ubuntu 的图形桌面终端运行,替换自己的 profile):
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
dsh plugin --profile web add dsh-plugin-computer-use@2.5.0
|
|
15
|
+
npx --yes --package=dsh-plugin-computer-use@2.5.0 dsh-computer-use-wayland install
|
|
16
|
+
# 注销并重新登录一次后:
|
|
17
|
+
npx --yes --package=dsh-plugin-computer-use@2.5.0 dsh-computer-use-wayland status
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
如果已经使用 `tofu-box`,通过 `dsh plugin --profile web update tofu-box` 更新依赖,再执行上面的扩展安装/检查命令。伞包现有 `^2.4.1` 范围覆盖 2.5.0;无需另外添加单独的 computer-use 插件行。更新与安装扩展后重启 dsh。
|
|
21
|
+
|
|
22
|
+
**从本仓库试用:在 Ubuntu 的图形桌面终端运行**(把路径和 profile 换成自己的):
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
cd /path/to/dsh-plugins
|
|
26
|
+
node dsh-plugin-computer-use/scripts/wayland.mjs install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
安装只写当前用户的扩展目录,保留已启用的其他扩展。之后需要**注销并重新登录一次**,让 GNOME 加载扩展;Wayland 下不能用 Alt+F2 → r 重启 Shell。
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
cd /path/to/dsh-plugins
|
|
33
|
+
node dsh-plugin-computer-use/scripts/wayland.mjs status
|
|
34
|
+
dsh plugin --profile web add "link:$(pwd)/dsh-plugin-computer-use"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
重启 dsh,然后先试 `computer_windows` 和 `computer_screenshot`。第一次需要前台输入时,允许系统弹出的远程桌面/键鼠控制授权。此授权属于驱动运行时,重启后可能再次询问。插件安装不会自动授予它。
|
|
38
|
+
|
|
39
|
+
**用本地 tgz 安装**:下载/复制 `dsh-plugin-computer-use-2.5.0.tgz` 到 Ubuntu 后运行:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
dsh plugin --profile web add ./dsh-plugin-computer-use-2.5.0.tgz
|
|
43
|
+
npx --yes --package=./dsh-plugin-computer-use-2.5.0.tgz dsh-computer-use-wayland install
|
|
44
|
+
# 注销并重新登录后:
|
|
45
|
+
npx --yes --package=./dsh-plugin-computer-use-2.5.0.tgz dsh-computer-use-wayland status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
如果通过 `tofu-box` 安装,应更新伞包中的依赖;不要叠装单独的 computer-use 行。也可直接对该 profile 下已安装包的 `scripts/wayland.mjs` 运行 `install` / `status`。
|
|
49
|
+
|
|
50
|
+
`status` 检查实际加载的扩展版本、Remote Desktop portal 的键鼠能力、无障碍总线与会话变量;缺项时返回非零退出码。Ubuntu 缺少组件时可以安装:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
sudo apt install libglib2.0-bin at-spi2-core xdg-desktop-portal xdg-desktop-portal-gnome
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
请从同一个图形用户会话启动 dsh,并保留 `WAYLAND_DISPLAY`、`XDG_RUNTIME_DIR`、`DBUS_SESSION_BUS_ADDRESS`。不要用 root 或另一条不带桌面环境的 SSH 会话启动。若 GNOME 的 Extensions 总开关关闭,请先在扩展管理器开启,再运行检查。
|
|
57
|
+
|
|
58
|
+
Wayland 的后台能力取决于应用的 AT-SPI 支持,像素/键盘输入可能需要临时前置窗口;不保证与 Windows 一样能操作完全遮挡的窗口。缺少扩展、portal 或权限时,驱动错误和空窗口表会附带安装提示。Sway/KDE 等沿用驱动各自的原生支持范围,WinRects 安装器只用于 GNOME。参见[上游平台支持说明](https://cua.ai/docs/cua-driver/concepts/platform-support)。
|
|
59
|
+
|
|
60
|
+
当前开发机为 Windows;本版已做会话检测、安装器、诊断与插件接入回归,**尚未在 Ubuntu 26.04 实机完成截图/输入验收**。
|
|
61
|
+
|
|
5
62
|
## 打开应用的行为(v2.3 起)
|
|
6
63
|
|
|
7
64
|
`computer_open` 只做一件事:**能复用就复用,确认没在跑才启动**。它按下面的顺序判断,顺序本身就是设计——"有没有进程"比"有没有窗口"可靠得多:
|
|
@@ -78,6 +135,7 @@
|
|
|
78
135
|
askPolicy: once-per-agent # 或 always / never
|
|
79
136
|
telemetry: false # Cua Driver 运行时的无内容产品遥测,默认关
|
|
80
137
|
dpiAware: true # Windows:驱动启动前把宿主进程切到 Per-Monitor-V2 DPI 感知
|
|
138
|
+
wayland: auto # Linux:自动启用原生 Wayland;on/off 显式覆盖环境开关
|
|
81
139
|
restore: true # 允许把已运行应用的最小化窗口唤醒到前台(false = 永不抢焦点)
|
|
82
140
|
activateRunning: false # 应用在跑但没窗口(托盘态)时,是否让它的启动入口去激活(默认否,只报告)
|
|
83
141
|
```
|
|
@@ -90,7 +148,7 @@
|
|
|
90
148
|
|
|
91
149
|
## 平台与限制
|
|
92
150
|
|
|
93
|
-
- **Windows 11 实测**(自测脚本见下);macOS / Linux
|
|
151
|
+
- **Windows 11 实测**(自测脚本见下);macOS / Linux **未实机验证**。Linux Wayland 从 v2.5 起自动开启原生后端,GNOME 的安装与诊断见上;macOS 需给运行 dsh 的宿主进程授予"辅助功能/屏幕录制"(TCC)权限,且驱动的 macOS 前台叠加层在进程内形态不可用。
|
|
94
152
|
- Windows 高 DPI:启动日志会打印 `host process DPI awareness = …`;正常为 `per-monitor-v2`。若显示 `UNAWARE`(宿主 manifest 或他方抢先固定了感知级别),桌面截图/坐标在缩放屏上不可靠,插件会在每张桌面截图摘要里带显式警告——优先改用 `app=` 窗口操作。
|
|
95
153
|
- 桌面截图仅主显示器(窗口级截图不受限);被最小化的窗口**读树可以、截图不行**,插件会先唤醒它再截——这是唯一会短暂占用前台的动作,`restore: false` 可关掉。
|
|
96
154
|
- **窗口枚举可能失灵**:长生命周期的宿主里可能出现 `list_windows` 恒返回 0 窗口(此时桌面截图、光标读取、指定窗口的截图都还正常)。这种情况下按进程判断的部分仍然可靠(不会重复启动应用),但没法定位/点击那个窗口——重启 dsh 可重建枚举。
|
|
@@ -99,6 +157,12 @@
|
|
|
99
157
|
|
|
100
158
|
## 自测
|
|
101
159
|
|
|
160
|
+
无需图形桌面的 Wayland 回归测试(检测、扩展安装与诊断):
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
node --experimental-test-module-mocks --test tests/*.test.mjs
|
|
164
|
+
```
|
|
165
|
+
|
|
102
166
|
```sh
|
|
103
167
|
node selftest/run.mjs
|
|
104
168
|
```
|
package/index.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* v2 rewires the whole backend onto the Cua Driver SDK (@trycua/cua-driver):
|
|
5
5
|
* a Rust in-process computer-use runtime loaded directly into the dsh host.
|
|
6
6
|
* No daemon, no MCP server, no PowerShell scripts — `pnpm` pulls one native
|
|
7
|
-
* package per OS/arch
|
|
8
|
-
*
|
|
7
|
+
* package per OS/arch (Windows tested; macOS / Linux require their desktop
|
|
8
|
+
* permissions). v2.5 enables native Wayland before runtime initialization and
|
|
9
|
+
* bundles the GNOME WinRects helper with an explicit user-session installer.
|
|
9
10
|
*
|
|
10
11
|
* v2.2 fixes the two things that made v2 unusable on a real Windows desktop:
|
|
11
12
|
*
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
import zlib from 'node:zlib'
|
|
84
85
|
import z from '@deepseek-ai/schemastery'
|
|
85
86
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
87
|
+
import { configureLinuxDesktop, probeGnomeHelper, linuxDesktopHint } from './linux.js'
|
|
86
88
|
|
|
87
89
|
export const name = 'computer-use'
|
|
88
90
|
export const inject = ['tools', 'attachments']
|
|
@@ -100,6 +102,8 @@ export const Config = z.object({
|
|
|
100
102
|
* driver starts (strongly recommended; set false only if some other component
|
|
101
103
|
* already owns process DPI awareness and you must not touch it). */
|
|
102
104
|
dpiAware: z.boolean(),
|
|
105
|
+
/** Native Wayland: auto-detect by default; on/off override the driver env flag. */
|
|
106
|
+
wayland: z.union([z.const('auto'), z.const('on'), z.const('off')]),
|
|
103
107
|
})
|
|
104
108
|
|
|
105
109
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -291,6 +295,11 @@ export function apply(ctx, config) {
|
|
|
291
295
|
// that app exposes no window (tray state)? Off by default — see computer_open.
|
|
292
296
|
const activateRunningCfg = !!(config && config.activateRunning === true)
|
|
293
297
|
if (!telemetry) process.env.CUA_DRIVER_RS_TELEMETRY_ENABLED = '0'
|
|
298
|
+
// Must happen BEFORE importing/creating the native runtime. DISPLAY may be
|
|
299
|
+
// XWayland and is deliberately not used to select the desktop backend.
|
|
300
|
+
const linuxDesktop = configureLinuxDesktop({ wayland: config?.wayland ?? 'auto' })
|
|
301
|
+
const desktopHint = linuxDesktopHint(linuxDesktop)
|
|
302
|
+
const driverError = (tool, r, limit = 400) => new Error(tool + ': ' + String(r.text ?? 'failed').slice(0, limit) + desktopHint)
|
|
294
303
|
|
|
295
304
|
// ── DPI bootstrap (Windows) ────────────────────────────────────────────────
|
|
296
305
|
// The driver assumes a Per-Monitor-V2-aware host (its daemon exe ships that
|
|
@@ -372,8 +381,16 @@ export function apply(ctx, config) {
|
|
|
372
381
|
if (driver) return Promise.resolve(driver)
|
|
373
382
|
if (driverDead) throw new Error('Cua Driver runtime was shut down; restart dsh to reactivate computer-use')
|
|
374
383
|
if (!driverPromise) {
|
|
375
|
-
driverPromise = ensureProcessDpiAware().then(() => {
|
|
384
|
+
driverPromise = ensureProcessDpiAware().then(async () => {
|
|
376
385
|
ctx.logger.info('computer-use: host process DPI awareness = ' + dpiState.text)
|
|
386
|
+
if (linuxDesktop.native) {
|
|
387
|
+
if (!process.env.WAYLAND_DISPLAY) throw new Error('Native Wayland needs WAYLAND_DISPLAY from the graphical user session.' + desktopHint)
|
|
388
|
+
ctx.logger.info('computer-use: native Wayland backend enabled (XWayland DISPLAY does not override it)')
|
|
389
|
+
if (linuxDesktop.gnome) {
|
|
390
|
+
const helper = await probeGnomeHelper()
|
|
391
|
+
if (!helper.active) ctx.logger.warn('computer-use: GNOME WinRects helper missing or outdated.' + desktopHint)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
377
394
|
return import('@trycua/cua-driver')
|
|
378
395
|
}).then((m) => {
|
|
379
396
|
const created = m.CuaDriver.create()
|
|
@@ -438,7 +455,7 @@ export function apply(ctx, config) {
|
|
|
438
455
|
/** Same, but throws on isError with the driver's own message. */
|
|
439
456
|
const call = async (tool, args) => {
|
|
440
457
|
const r = await raw(tool, args)
|
|
441
|
-
if (r.isError) throw
|
|
458
|
+
if (r.isError) throw driverError(tool, r)
|
|
442
459
|
return r
|
|
443
460
|
}
|
|
444
461
|
const structured = (r) => {
|
|
@@ -454,7 +471,7 @@ export function apply(ctx, config) {
|
|
|
454
471
|
*/
|
|
455
472
|
const listWindows = async (args) => {
|
|
456
473
|
const r = await raw('list_windows', args)
|
|
457
|
-
if (r.isError) throw
|
|
474
|
+
if (r.isError) throw driverError('list_windows', r, 300)
|
|
458
475
|
return structured(r)
|
|
459
476
|
}
|
|
460
477
|
|
|
@@ -486,7 +503,7 @@ export function apply(ctx, config) {
|
|
|
486
503
|
if (pid === undefined) {
|
|
487
504
|
const needle = String(a.app).toLowerCase()
|
|
488
505
|
const cands = wins.filter((x) => String(x.app_name ?? '').toLowerCase().includes(needle) || String(x.title ?? '').toLowerCase().includes(needle))
|
|
489
|
-
if (cands.length === 0) throw new Error('no window matches app "' + a.app + '" — check computer_windows')
|
|
506
|
+
if (cands.length === 0) throw new Error('no window matches app "' + a.app + '" — check computer_windows' + desktopHint)
|
|
490
507
|
pid = cands[0].pid
|
|
491
508
|
}
|
|
492
509
|
let mine = wins.filter((x) => x.pid === pid)
|
|
@@ -925,11 +942,11 @@ export function apply(ctx, config) {
|
|
|
925
942
|
const r = await raw(tool, base)
|
|
926
943
|
if (r.isError && isEscalationHint(r)) {
|
|
927
944
|
const r2 = await raw(tool, { ...base, delivery_mode: 'foreground' })
|
|
928
|
-
if (r2.isError) throw
|
|
945
|
+
if (r2.isError) throw driverError(tool, r2)
|
|
929
946
|
r2.escalated = true
|
|
930
947
|
return r2
|
|
931
948
|
}
|
|
932
|
-
if (r.isError) throw
|
|
949
|
+
if (r.isError) throw driverError(tool, r)
|
|
933
950
|
return r
|
|
934
951
|
}
|
|
935
952
|
const actionNote = (r) => (r.escalated ? ' [background input was refused; re-delivered with a brief foreground swap]' : /"effect":"suspected_noop"/.test(String(r.rawJson ?? '')) ? ' [driver suspects the target ignored the input — verify on the screenshot]' : '')
|
|
@@ -954,7 +971,7 @@ export function apply(ctx, config) {
|
|
|
954
971
|
await sleep(350)
|
|
955
972
|
r2 = await raw('press_key', { ...targetArg, key, modifiers: mods, delivery_mode: 'foreground' })
|
|
956
973
|
}
|
|
957
|
-
if (r2.isError) throw new Error('hotkey ' + mods.join('+') + '+' + key + ': ' + String(r.text ?? 'failed').slice(0, 260) + ' (foreground fallback too: ' + String(r2.text ?? 'failed').slice(0, 160) + ')')
|
|
974
|
+
if (r2.isError) throw new Error('hotkey ' + mods.join('+') + '+' + key + ': ' + String(r.text ?? 'failed').slice(0, 260) + ' (foreground fallback too: ' + String(r2.text ?? 'failed').slice(0, 160) + ')' + desktopHint)
|
|
958
975
|
r2.escalated = true
|
|
959
976
|
return r2
|
|
960
977
|
}
|
|
@@ -992,7 +1009,8 @@ export function apply(ctx, config) {
|
|
|
992
1009
|
|
|
993
1010
|
const COORD_DESC = 'Coordinate frames: with a window target, x/y are pixels of the LAST computer_screenshot OF THAT WINDOW; desktop scope uses pixels of the last desktop screenshot (physical-desktop px are auto-mapped into the frame).'
|
|
994
1011
|
const KEYB_FIRST = ' KEYBOARD-FIRST: prefer computer_open / computer_key / computer_sequence over pixel clicks whenever a keyboard path exists. ELEMENT-FIRST: if the last window screenshot listed an element for the target, click by its element= token — it works on backgrounded/minimized windows and never touches the user\'s cursor.'
|
|
995
|
-
const
|
|
1012
|
+
const platformNote = linuxDesktop.native ? ' On native Wayland, semantic AT-SPI actions may run in the background; raw input may require a foreground swap and system Remote Desktop consent. GNOME needs the WinRects helper; a missing route returns a setup hint.' : ''
|
|
1013
|
+
const register = (definition) => ctx.effect(() => ctx.tools.register(defineTool({ ...definition, description: definition.description + platformNote })), 'computer-use: tool ' + definition.name)
|
|
996
1014
|
|
|
997
1015
|
/** Shared action wrapper: grant, run, settle, re-capture the acted-on view. */
|
|
998
1016
|
const actAndShot = async (exec, grant, summary, run) => {
|
|
@@ -1089,7 +1107,7 @@ export function apply(ctx, config) {
|
|
|
1089
1107
|
const needle = String(a.app).toLowerCase()
|
|
1090
1108
|
wins = wins.filter((x) => String(x.app_name ?? '').toLowerCase().includes(needle) || String(x.title ?? '').toLowerCase().includes(needle))
|
|
1091
1109
|
}
|
|
1092
|
-
if (wins.length === 0) return { table: 'no windows matched' }
|
|
1110
|
+
if (wins.length === 0) return { table: 'no windows matched' + ((sc.windows ?? []).length === 0 ? desktopHint : '') }
|
|
1093
1111
|
const rows = wins.slice(0, 40).map((w) => 'pid=' + w.pid + ' win=' + (w.window_id ?? w.windowId) + (w.is_on_screen === false ? ' [off-screen]' : '') + (w.minimized ? ' [minimized]' : '') + ' z=' + (w.z_index ?? '?') + ' ' + JSON.stringify(w.app_name ?? '') + ' ' + JSON.stringify(String(w.title ?? '').slice(0, 60)) + ' ' + JSON.stringify(w.bounds ?? {}))
|
|
1094
1112
|
return { table: rows.join('\n') + (wins.length > 40 ? '\n… ' + (wins.length - 40) + ' more (filter with app=)' : '') }
|
|
1095
1113
|
},
|
|
@@ -1583,7 +1601,7 @@ export function apply(ctx, config) {
|
|
|
1583
1601
|
return { kind: 'ask', reason: '允许 computer-use 操作应用?一次批准覆盖同一会话的后续动作(askPolicy: always 可改为每次一询);默认后台注入——不移动你的鼠标、不抢焦点,仅个别拒收后台输入的应用会短暂前置。首个动作: ' + exec.name + '。' }
|
|
1584
1602
|
})
|
|
1585
1603
|
|
|
1586
|
-
ctx.logger.info('computer-use v2.
|
|
1604
|
+
ctx.logger.info('computer-use v2.5: cua-driver backend (session "' + SESSION_LABEL + '", all calls labelled); ' + MUTATING.size + ' mutating tools gated; askPolicy=' + askPolicy + '; telemetry=' + telemetry + '; restoreExisting=' + restoreCfg + '; nativeWayland=' + linuxDesktop.native)
|
|
1587
1605
|
|
|
1588
1606
|
// Best-effort: push the model-frame cap into the driver for window captures
|
|
1589
1607
|
// (the driver resizes window screenshots AND keeps x,y/zoom coordinates
|
package/linux.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export const WAYLAND_ENV = 'CUA_DRIVER_RS_ENABLE_WAYLAND'
|
|
4
|
+
export const HELPER_UUID = 'winrects@cua'
|
|
5
|
+
export const HELPER_VERSION = 8
|
|
6
|
+
|
|
7
|
+
/** DISPLAY is also set by XWayland; it must not hide a native Wayland session. */
|
|
8
|
+
export function configureLinuxDesktop({ platform = process.platform, env = process.env, wayland = 'auto' } = {}) {
|
|
9
|
+
const state = { linux: platform === 'linux', wayland: false, native: false, gnome: false }
|
|
10
|
+
if (!state.linux) return state
|
|
11
|
+
const session = String(env.XDG_SESSION_TYPE ?? '').toLowerCase()
|
|
12
|
+
state.wayland = session === 'wayland' || (session !== 'x11' && !!env.WAYLAND_DISPLAY)
|
|
13
|
+
state.gnome = /gnome|ubuntu/i.test(String(env.XDG_CURRENT_DESKTOP ?? '') + ':' + String(env.DESKTOP_SESSION ?? ''))
|
|
14
|
+
if (wayland === 'on') env[WAYLAND_ENV] = '1'
|
|
15
|
+
else if (wayland === 'off') env[WAYLAND_ENV] = '0'
|
|
16
|
+
else if (state.wayland && env[WAYLAND_ENV] === undefined) env[WAYLAND_ENV] = '1'
|
|
17
|
+
const flag = String(env[WAYLAND_ENV] ?? '').trim()
|
|
18
|
+
state.native = state.wayland && !!flag && !/^(0|false)$/i.test(flag)
|
|
19
|
+
return state
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Bounded, shell-free desktop probes; never request screen/input permission here. */
|
|
23
|
+
export function runDesktopCommand(command, args) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
execFile(command, args, { timeout: 4000, maxBuffer: 256 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
26
|
+
resolve({ ok: !error, stdout: String(stdout ?? '').trim(), error: error ? String(stderr || error.message).trim() : '' })
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function probeGnomeHelper(run = runDesktopCommand) {
|
|
32
|
+
const result = await run('gdbus', ['call', '--session', '--dest', 'org.cua.WinRects', '--object-path', '/org/cua/WinRects', '--method', 'org.cua.WinRects.GetVersion'])
|
|
33
|
+
const version = result.ok ? Number(/(?:uint32\s+)?(\d+)/.exec(result.stdout)?.[1] ?? 0) : 0
|
|
34
|
+
return { active: result.ok && version >= HELPER_VERSION, version, error: result.error }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function linuxDesktopHint(state) {
|
|
38
|
+
if (!state.native) return ''
|
|
39
|
+
if (!state.gnome) return ' Native Wayland is enabled; available capture and input routes depend on the compositor. Run dsh in the graphical user session with WAYLAND_DISPLAY, XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS available.'
|
|
40
|
+
return ' GNOME/Wayland setup: run dsh-computer-use-wayland install (or node <plugin-dir>/scripts/wayland.mjs install), then log out and back in once; run dsh-computer-use-wayland status in the same desktop session. Allow the system Remote Desktop prompt on the first input action. Keep WAYLAND_DISPLAY, XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS available to the dsh process.'
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-computer-use",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"keywords": ["dsh-plugin", "deepseek-harness", "dsh", "computer-use", "desktop", "automation", "screenshot", "input"],
|
|
6
6
|
"publishConfig": { "registry": "https://registry.npmjs.org" },
|
|
7
7
|
"repository": { "type": "git", "url": "git+https://github.com/tofu-zyc/dsh-plugin-tofu-box.git", "directory": "dsh-plugin-computer-use" },
|
|
8
8
|
"bugs": { "url": "https://github.com/tofu-zyc/dsh-plugin-tofu-box/issues" },
|
|
9
9
|
"homepage": "https://github.com/tofu-zyc/dsh-plugin-tofu-box/tree/main/dsh-plugin-computer-use",
|
|
10
|
-
"description": "computer-use v2.
|
|
10
|
+
"description": "computer-use v2.5:Ubuntu/GNOME Wayland 自动检测并启用原生后端;内置 WinRects 扩展安装与诊断命令,驱动最低 0.28.3。基于 Cua Driver 进程内 SDK 的跨平台桌面操作。13 个工具(截图/窗口列表/缩放/光标/移动/点击/拖拽/滚动/输入/按键/打开应用/连续动作/等待)。v2.4.1 适配 dsh 0.2.0:审批探测改走公有 overrideOf/session 覆盖 + 部署默认策略,任何探测形状失效时保守放行,杜绝 danger-full-access 会话因私有 API 变动而自锁全部工具。v2.4 修「宿主闲置后窗口/应用枚举静默变空」:全部驱动调用统一挂命名 session(查询也不例外),session 卡死按 end→start→重试→重建运行时分级自愈,list_windows 报错不再吞成『无窗口』;computer_type 桌面输入遇非 ASCII 自动改走剪贴板粘贴(绕开 IME 对 CJK/emoji 的损坏,事后恢复用户剪贴板);桌面坐标给成物理分辨率时自动映射进截图坐标系;computer_screenshot 新增 save= 同时存盘+进剪贴板。v2.3 系列能力保留:open 先查进程表绝不启动第二实例、识别启动即退出的假成功、托盘态应用默认只报告不代启。Windows 宿主进程自动切 Per-Monitor-V2 DPI 感知,默认后台注入不抢焦点,元素树+element_token 精确操作。无需守护进程/MCP/PowerShell。写操作走原生 tools/pre-execute 审批。",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "index.js",
|
|
13
|
+
"bin": { "dsh-computer-use-wayland": "./scripts/wayland.mjs" },
|
|
14
|
+
"scripts": { "test": "node --experimental-test-module-mocks --test tests/*.test.mjs" },
|
|
13
15
|
"dependencies": {
|
|
14
|
-
"@trycua/cua-driver": "^0.28.
|
|
16
|
+
"@trycua/cua-driver": "^0.28.3",
|
|
15
17
|
"koffi": "^3.2.1"
|
|
16
18
|
},
|
|
17
19
|
"peerDependencies": {
|
|
@@ -33,6 +35,9 @@
|
|
|
33
35
|
},
|
|
34
36
|
"files": [
|
|
35
37
|
"index.js",
|
|
38
|
+
"linux.js",
|
|
39
|
+
"scripts/wayland.mjs",
|
|
40
|
+
"wayland-helper/",
|
|
36
41
|
"cordis.patch.yml",
|
|
37
42
|
"README.md"
|
|
38
43
|
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, mkdir, copyFile, realpath } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { dirname, join, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
import { configureLinuxDesktop, probeGnomeHelper, runDesktopCommand, HELPER_UUID } from '../linux.js'
|
|
7
|
+
|
|
8
|
+
const pluginDir = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
9
|
+
const source = join(pluginDir, 'wayland-helper', HELPER_UUID)
|
|
10
|
+
|
|
11
|
+
// gsettings prints GVariant, not JSON. Extension UUIDs cannot contain quotes
|
|
12
|
+
// or escapes, so accept only its narrow string-array representation.
|
|
13
|
+
export function parseEnabledExtensions(value) {
|
|
14
|
+
const text = value.trim().replace(/^@as\s+/, '')
|
|
15
|
+
if (!/^\[\s*(?:'[^'\\]*'\s*(?:,\s*'[^'\\]*'\s*)*)?\]$/.test(text)) {
|
|
16
|
+
throw new Error('Cannot parse GNOME enabled-extensions; no settings were changed: ' + value)
|
|
17
|
+
}
|
|
18
|
+
return [...text.matchAll(/'([^']*)'/g)].map(match => match[1])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function installHelper({ env = process.env, home = homedir(), run = runDesktopCommand, sourceDir = source } = {}) {
|
|
22
|
+
const version = await run('gnome-shell', ['--version'])
|
|
23
|
+
if (!version.ok) throw new Error('gnome-shell is unavailable; run this command in your GNOME desktop session.')
|
|
24
|
+
const major = /\b(\d+)(?:\.\d+)?\b/.exec(version.stdout)?.[1]
|
|
25
|
+
const metadata = JSON.parse(await readFile(join(sourceDir, 'metadata.json'), 'utf8'))
|
|
26
|
+
if (!metadata['shell-version'].includes(major)) throw new Error('This bundled helper does not support GNOME ' + major + '; supported versions: ' + metadata['shell-version'].join(', '))
|
|
27
|
+
// Read and validate settings BEFORE copying files or changing anything.
|
|
28
|
+
const current = await run('gsettings', ['get', 'org.gnome.shell', 'enabled-extensions'])
|
|
29
|
+
if (!current.ok) throw new Error('Cannot read GNOME settings: ' + current.error)
|
|
30
|
+
const enabled = parseEnabledExtensions(current.stdout)
|
|
31
|
+
const blocked = await run('gsettings', ['get', 'org.gnome.shell', 'disabled-extensions'])
|
|
32
|
+
if (!blocked.ok) throw new Error('Cannot read GNOME disabled-extensions: ' + blocked.error)
|
|
33
|
+
const disabled = parseEnabledExtensions(blocked.stdout)
|
|
34
|
+
const destination = join(env.XDG_DATA_HOME || join(home, '.local', 'share'), 'gnome-shell', 'extensions', HELPER_UUID)
|
|
35
|
+
await mkdir(destination, { recursive: true })
|
|
36
|
+
for (const file of ['extension.js', 'metadata.json']) await copyFile(join(sourceDir, file), join(destination, file))
|
|
37
|
+
if (!enabled.includes(HELPER_UUID)) {
|
|
38
|
+
enabled.push(HELPER_UUID)
|
|
39
|
+
const updated = await run('gsettings', ['set', 'org.gnome.shell', 'enabled-extensions', JSON.stringify(enabled)])
|
|
40
|
+
if (!updated.ok) throw new Error('Helper files were installed, but enabling failed: ' + updated.error)
|
|
41
|
+
}
|
|
42
|
+
// GNOME's forced-disabled list takes precedence over enabled-extensions.
|
|
43
|
+
if (disabled.includes(HELPER_UUID)) {
|
|
44
|
+
const updated = await run('gsettings', ['set', 'org.gnome.shell', 'disabled-extensions', JSON.stringify(disabled.filter(uuid => uuid !== HELPER_UUID))])
|
|
45
|
+
if (!updated.ok) throw new Error('Helper files were installed, but clearing its disabled state failed: ' + updated.error)
|
|
46
|
+
}
|
|
47
|
+
return destination
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function desktopStatus({ env = process.env, platform = process.platform, run = runDesktopCommand } = {}) {
|
|
51
|
+
// Inspect a copy: a status command must not change the caller's environment.
|
|
52
|
+
const state = configureLinuxDesktop({ platform, env: { ...env } })
|
|
53
|
+
if (!state.linux || !state.wayland || !state.gnome) return { ready: false, lines: ['This setup command targets a GNOME/Wayland user session.', 'Detected: ' + platform + ' / ' + (env.XDG_SESSION_TYPE || '?') + ' / ' + (env.XDG_CURRENT_DESKTOP || '?')] }
|
|
54
|
+
const [helper, portal, accessibility, disabled] = await Promise.all([
|
|
55
|
+
probeGnomeHelper(run),
|
|
56
|
+
run('gdbus', ['call', '--session', '--dest', 'org.freedesktop.portal.Desktop', '--object-path', '/org/freedesktop/portal/desktop', '--method', 'org.freedesktop.DBus.Properties.Get', 'org.freedesktop.portal.RemoteDesktop', 'AvailableDeviceTypes']),
|
|
57
|
+
run('gdbus', ['call', '--session', '--dest', 'org.a11y.Bus', '--object-path', '/org/a11y/bus', '--method', 'org.a11y.Bus.GetAddress']),
|
|
58
|
+
run('gsettings', ['get', 'org.gnome.shell', 'disable-user-extensions']),
|
|
59
|
+
])
|
|
60
|
+
const devices = portal.ok ? Number(/uint32\s+(\d+)/.exec(portal.stdout)?.[1] ?? 0) : 0
|
|
61
|
+
const input = (devices & 3) === 3
|
|
62
|
+
const session = !!env.WAYLAND_DISPLAY && !!env.XDG_RUNTIME_DIR && !!env.DBUS_SESSION_BUS_ADDRESS
|
|
63
|
+
return {
|
|
64
|
+
ready: state.native && session && helper.active && input && accessibility.ok && disabled.ok && disabled.stdout === 'false',
|
|
65
|
+
lines: [
|
|
66
|
+
'Wayland environment: ' + (session ? 'OK' : 'MISSING (WAYLAND_DISPLAY / XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS)'),
|
|
67
|
+
'Native Wayland backend: ' + (state.native ? 'automatic' : 'disabled by CUA_DRIVER_RS_ENABLE_WAYLAND; unset it or use config wayland: on'),
|
|
68
|
+
'WinRects helper: ' + (helper.active ? 'ACTIVE (v' + helper.version + ')' : 'MISSING/OUTDATED; run install, then log out and back in once'),
|
|
69
|
+
'GNOME user extensions: ' + (disabled.ok && disabled.stdout === 'false' ? 'enabled' : 'disabled or unreadable; enable Extensions in GNOME first'),
|
|
70
|
+
'Remote Desktop portal (keyboard + pointer): ' + (input ? 'available (consent is requested on first use)' : 'MISSING; install xdg-desktop-portal and xdg-desktop-portal-gnome'),
|
|
71
|
+
'Accessibility bus: ' + (accessibility.ok ? 'available' : 'MISSING; install at-spi2-core'),
|
|
72
|
+
'Run dsh from this same graphical user session; restart it after setup.',
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function main(args = process.argv.slice(2)) {
|
|
78
|
+
const command = args[0] ?? 'status'
|
|
79
|
+
if (!['status', 'install', '--help', '-h'].includes(command) || args.length > 1) throw new Error('Usage: dsh-computer-use-wayland [status|install]')
|
|
80
|
+
if (command === '--help' || command === '-h') { console.log('Usage: dsh-computer-use-wayland [status|install]'); return }
|
|
81
|
+
if (process.platform !== 'linux') throw new Error('Run this command on the Ubuntu/GNOME machine where dsh controls the desktop.')
|
|
82
|
+
if (command === 'install') {
|
|
83
|
+
const state = configureLinuxDesktop({ env: { ...process.env } })
|
|
84
|
+
if (!state.wayland || !state.gnome) throw new Error('Run install from your GNOME/Wayland graphical session.')
|
|
85
|
+
const destination = await installHelper()
|
|
86
|
+
console.log('Installed ' + HELPER_UUID + ' to ' + destination + '.\nLog out and back in once, restart dsh, then run dsh-computer-use-wayland status.\nOn Wayland, restarting GNOME Shell with Alt+F2 r is unavailable.')
|
|
87
|
+
} else {
|
|
88
|
+
const status = await desktopStatus()
|
|
89
|
+
console.log(status.lines.join('\n'))
|
|
90
|
+
if (!status.ready) process.exitCode = 1
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// npm exposes bins as symlinks on Linux; compare resolved files so the command
|
|
95
|
+
// also executes when launched through node_modules/.bin or npx.
|
|
96
|
+
if (process.argv[1] && pathToFileURL(await realpath(resolve(process.argv[1]))).href === import.meta.url) {
|
|
97
|
+
main().catch(error => { console.error(error.message); process.exitCode = 1 })
|
|
98
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cua AI, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
The unmodified `winrects@cua/extension.js` and `metadata.json` are from
|
|
2
|
+
[trycua/cua](https://github.com/trycua/cua/tree/1c6932f4eb62a06a881edd1128adf5f33b09ce92/libs/cua-driver/wayland-helper),
|
|
3
|
+
tag `cua-driver-rs-v0.28.4`, commit `1c6932f4eb62a06a881edd1128adf5f33b09ce92`.
|
|
4
|
+
Copyright (c) 2025 Cua AI, Inc.; MIT license, reproduced in LICENSE.md.
|
|
5
|
+
|
|
6
|
+
Helper API v8 supports GNOME Shell 45–50. This helper version includes the
|
|
7
|
+
GNOME stage screenshot fix and is compatible with the 0.28.3 npm runtime.
|
|
8
|
+
The helper is bundled because the npm SDK distributes native bindings but
|
|
9
|
+
does not include the GNOME Shell extension shipped in the upstream CLI.
|
|
10
|
+
|
|
11
|
+
Install it explicitly with `dsh-computer-use-wayland install` (no root needed),
|
|
12
|
+
then log out and back in once. It supplies compositor geometry, capture and
|
|
13
|
+
activation. Input uses the driver's Remote Desktop portal and its system
|
|
14
|
+
consent dialog. Installation does not grant portal input permission.
|
|
15
|
+
|
|
16
|
+
To remove it, run `gnome-extensions disable winrects@cua`, delete only the
|
|
17
|
+
`winrects@cua` directory reported by the installer, and log out and back in.
|