dsh-builtin-browser 0.1.16 → 0.1.18
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.en.md +10 -4
- package/README.md +11 -5
- package/docs/architecture.md +1 -1
- package/docs/user-guide.md +8 -4
- package/lib/browser-electron/remote-host.js +103 -17
- package/package.json +5 -9
- package/src/browser-electron/remote-host.ts +102 -16
- package/src/types/electron-shim.d.ts +11 -3
package/README.en.md
CHANGED
|
@@ -217,7 +217,7 @@ agent (browser_* tools)
|
|
|
217
217
|
|
|
218
218
|
**The self-hosted browser IS a real browser**: every task (DSH session) gets its **own browser window** with a full toolbar — address bar, back/forward/reload buttons, and a tab strip (new/switch/close tabs). A human can use it exactly like Chrome: type a URL in the address bar (https:// is added automatically), click tabs, open new ones. Keyboard focus follows your clicks — **click the address bar to type, click the page to interact** (Windows focus routing; fixes the case where clicks did not move focus and the address bar could not receive typed URLs). Human and agent actions feed the **same session model** (same tabs, history, and navigation); the window title always shows the task label plus the page title/URL, and views follow the window size on resize. A window closes automatically with its session when the task ends.
|
|
219
219
|
|
|
220
|
-
**Electron lookup order**: ① `require('electron')` (
|
|
220
|
+
**Electron lookup order**: ① the current process IS Electron (DSH Desktop main process) → reuse the host binary directly; ② walk the process ancestry for the host's Electron binary (covers hosts that run the plugin in a child Node process, e.g. DSH Desktop; PowerShell CIM on Windows, last resort only); ③ `require('electron')` (the electron package bundled with the plugin); ④ `ELECTRON_PATH` (explicit override); ⑤ the newest among DSH install anchors and pnpm virtual stores. **Zero extra install on DSH Desktop**; when nothing is found a clear error tells you what to do.
|
|
221
221
|
|
|
222
222
|
## Division of labor with the desktop shell
|
|
223
223
|
|
|
@@ -225,8 +225,10 @@ The browser's **visible view**, the **browser column layout**, and the **column-
|
|
|
225
225
|
|
|
226
226
|
## Requirements
|
|
227
227
|
|
|
228
|
-
- DeepSeek Harness (dsh) with the `web`
|
|
229
|
-
- **Electron runtime** (
|
|
228
|
+
- DeepSeek Harness (dsh) with the matching profile (`web` / `desktop`, etc.)
|
|
229
|
+
- **Electron runtime** (required dependency, installed automatically with the plugin):
|
|
230
|
+
- **DSH Desktop**: the host itself runs on Electron — the plugin reuses the host binary automatically (the bundled electron is just a fallback);
|
|
231
|
+
- **plain `dsh web` self-hosted**: uses the bundled electron package directly (≥ 40 recommended; 44+ downloads its binary on first use, needs network)
|
|
230
232
|
|
|
231
233
|
### Verified versions
|
|
232
234
|
|
|
@@ -235,7 +237,7 @@ The browser's **visible view**, the **browser column layout**, and the **column-
|
|
|
235
237
|
| DeepSeek Harness (dsh) | `0.1.1-rc.2` (peer range `^0.1.1-rc.2`) |
|
|
236
238
|
| Electron | `44.0.0` (≥ 40 recommended; 33.x has a compositor defect) |
|
|
237
239
|
| Node.js | `22.20.0` |
|
|
238
|
-
| dsh-builtin-browser | `0.1.
|
|
240
|
+
| dsh-builtin-browser | `0.1.18` |
|
|
239
241
|
| OS | Windows 10 (10.0.26200) |
|
|
240
242
|
|
|
241
243
|
> The plugin declares `electron >= 30`; it has **only been verified on Windows** (macOS/Linux untested, not yet promised).
|
|
@@ -288,6 +290,10 @@ Code layout:
|
|
|
288
290
|
| 6 | 2026-08 | **Windows handshake & tab lookup**: the Electron GUI process never receives piped stdin → RPC token now flows over **stdin + env var**; `browser_switch_tab`/`browser_close_tab` locate tabs across sessions (`locateTab`), `browser_close_tab` no longer fakes success, unknown ids error with the session's actual tab list |
|
|
289
291
|
| 7 | 2026-08 | **Toolbar interaction (Windows focus routing)**: keyboard input only reaches the focused view and the page view grabbed it, so the address bar could not receive input → added `wireFocusRouting` (clicking a view focuses it) + window refocus restores the last-clicked view; verified with real OS input probes |
|
|
290
292
|
| **0.1.16** | 2026-08-26 | **Release**: all seven rounds ship as **0.1.16** (build clean, 21/21 tests pass, `v0.1.16`) |
|
|
293
|
+
| 8 | 2026-08-27 | **DSH Desktop host-Electron reuse**: running inside an Electron process reuses the host binary directly; when the host runs the plugin in a child Node process, walk the process ancestry to find the host's Electron (PowerShell CIM on Windows, last resort only) — **DSH Desktop works with zero install**; error now hints per active profile; electron shim completed to fix the CI typecheck; docs updated |
|
|
294
|
+
| **0.1.17** | 2026-08-27 | **Release**: round 8 ships as **0.1.17** (build clean, 21/21 tests pass) |
|
|
295
|
+
| 9 | 2026-08-27 | **electron becomes a required dependency**: moved from optional peer into `dependencies`, so installing the plugin brings the electron package automatically (44+ downloads its binary lazily on first use); DSH Desktop still reuses the host binary; docs and error message updated |
|
|
296
|
+
| **0.1.18** | 2026-08-27 | **Release**: round 9 (electron as a required dependency) ships as **0.1.18** (build clean, 21/21 tests pass) |
|
|
291
297
|
|
|
292
298
|
## Acknowledgements
|
|
293
299
|
|
package/README.md
CHANGED
|
@@ -217,7 +217,7 @@ agent (browser_* 工具)
|
|
|
217
217
|
|
|
218
218
|
**自托管浏览器就是一台真正的浏览器**:每个任务(DSH 会话)拥有**独立的浏览器窗口**,窗口自带完整工具栏——地址栏、后退/前进/刷新按钮、标签条(新建/切换/关闭标签)。人可以直接像用 Chrome 一样使用它:在地址栏输入网址(自动补 `https://`)、点标签切换页面、开新标签;键盘焦点跟随点击——**点地址栏即可输入、点页面即可操作**(Windows 焦点路由,修复了点击不转移焦点导致地址栏无法输入的问题)。agent 与人的操作都汇入**同一个会话模型**(同一套标签、历史与导航),窗口标题实时显示当前任务标识与页面标题/URL,窗口缩放时视图自动跟随。任务结束后窗口随会话自动关闭。
|
|
219
219
|
|
|
220
|
-
**Electron 定位顺序**:① `require('electron')`(
|
|
220
|
+
**Electron 定位顺序**:① 插件运行在 Electron 进程内(DSH Desktop 主进程)时直接复用宿主二进制 → ② 进程祖先树中的宿主 Electron 二进制(DSH Desktop 把插件跑在子 Node 进程时的兜底)→ ③ `require('electron')`(随插件安装的 electron 包)→ ④ `ELECTRON_PATH`(显式覆盖)→ ⑤ DSH 安装锚点与 pnpm 虚拟仓库中**版本最新**者。**DSH Desktop 上零额外安装**;找不到时工具会报清晰的错误提示。
|
|
221
221
|
|
|
222
222
|
## 与桌面外壳的分工
|
|
223
223
|
|
|
@@ -225,8 +225,10 @@ agent (browser_* 工具)
|
|
|
225
225
|
|
|
226
226
|
## 环境要求
|
|
227
227
|
|
|
228
|
-
- DeepSeek Harness(dsh)
|
|
229
|
-
- **Electron 运行时**(
|
|
228
|
+
- DeepSeek Harness(dsh),已安装对应 profile(`web` / `desktop` 等)
|
|
229
|
+
- **Electron 运行时**(必装依赖,随插件自动安装):
|
|
230
|
+
- **DSH Desktop**:宿主本身基于 Electron,**插件自动复用宿主二进制**(随包安装的 electron 仅作后备);
|
|
231
|
+
- **纯 `dsh web` 自托管**:直接使用随插件安装的 electron 包(建议 ≥ 40;44+ 首次使用自动下载二进制,需网络)
|
|
230
232
|
|
|
231
233
|
### 验证过的版本
|
|
232
234
|
|
|
@@ -235,7 +237,7 @@ agent (browser_* 工具)
|
|
|
235
237
|
| DeepSeek Harness(dsh) | `0.1.1-rc.2`(peer 声明 `^0.1.1-rc.2`) |
|
|
236
238
|
| Electron | `44.0.0`(推荐 ≥ 40;33.x 存在合成器缺陷) |
|
|
237
239
|
| Node.js | `22.20.0` |
|
|
238
|
-
| dsh-builtin-browser | `0.1.
|
|
240
|
+
| dsh-builtin-browser | `0.1.18` |
|
|
239
241
|
| 操作系统 | Windows 10 (10.0.26200) |
|
|
240
242
|
|
|
241
243
|
> 插件声明 `electron >= 30`;**当前仅在 Windows 环境实测**(macOS/Linux 未验证,暂不承诺)。
|
|
@@ -253,7 +255,7 @@ agent (browser_* 工具)
|
|
|
253
255
|
- 页面弹窗(`window.open` / `target=_blank`)会被重定向到当前标签页内打开,不创建独立窗口,以免破坏标签/会话模型。
|
|
254
256
|
- `browser_auth` 的 cookie 往返不保留 `hostOnly`/`sameSite` 字段(host-only cookie 恢复后变成 domain cookie);仅自托管浏览器可用。
|
|
255
257
|
- 自托管浏览器子进程崩溃后会自动重启,但崩溃前已打开的会话视图已失效,调用 `browser_reset_session` 重建即可。
|
|
256
|
-
- Electron 44+
|
|
258
|
+
- electron 随插件安装;Electron 44+ 首次打开浏览器窗口时自动下载二进制(约 100MB,需网络),之后不再需要。若探测时网络不可用,可预装 `ELECTRON_PATH` 指定的二进制。
|
|
257
259
|
- 本插件不含浏览器列 UI——那是宿主外壳的配套,别把"浏览器列"当成插件能力。
|
|
258
260
|
|
|
259
261
|
## 开发
|
|
@@ -288,6 +290,10 @@ npm run build
|
|
|
288
290
|
| 第六轮 | 2026-08 | **Windows 握手与标签定位**:Electron GUI 进程收不到 piped stdin → RPC token 改 **stdin + 环境变量双通道**;`browser_switch_tab`/`browser_close_tab` 跨会话按 id 定位(`locateTab`),`browser_close_tab` 不再静默假成功,未知 id 报错附带现有标签列表 |
|
|
289
291
|
| 第七轮 | 2026-08 | **工具栏交互(Windows 焦点路由)**:键盘输入只进有焦点的 view,页面 view 抢占焦点导致地址栏无法输入 → 新增 `wireFocusRouting`(点击即聚焦该 view)+ 窗口 refocus 恢复上次点击的 view;真机 OS 输入探针验证 |
|
|
290
292
|
| **0.1.16** | 2026-08-26 | **发布**:以上七轮全部随 **0.1.16** 发布(构建零错误、21 项测试全绿,`v0.1.16`) |
|
|
293
|
+
| 第八轮 | 2026-08-27 | **DSH Desktop 宿主 Electron 复用**:插件运行在 Electron 进程内直接复用宿主二进制;插件跑在宿主子 Node 进程时沿进程祖先树找到宿主 Electron 兜底(Windows 用 PowerShell CIM,仅最后手段)——DSH Desktop **零安装开箱可用**;报错按当前 profile 动态提示;补齐 electron shim 修复 CI 类型检查;文档同步 |
|
|
294
|
+
| **0.1.17** | 2026-08-27 | **发布**:第八轮修复随 **0.1.17** 发布(构建零错误、21 项测试全绿) |
|
|
295
|
+
| 第九轮 | 2026-08-27 | **electron 改为必装依赖**:从 optional peer 移入 `dependencies`,安装插件即自动带上 electron 包(44+ 二进制首次使用懒下载);DSH Desktop 依旧复用宿主二进制;文档与报错同步 |
|
|
296
|
+
| **0.1.18** | 2026-08-27 | **发布**:第九轮「electron 改为必装依赖」随 **0.1.18** 发布(构建零错误、21 项测试全绿) |
|
|
291
297
|
|
|
292
298
|
## 特别感谢
|
|
293
299
|
|
package/docs/architecture.md
CHANGED
|
@@ -60,7 +60,7 @@ RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
|
|
|
60
60
|
|
|
61
61
|
- **协议**:本机 loopback TCP,每行一个 JSON(`{ id, op, ... }` ↔ `{ id, ok, result|err }`);
|
|
62
62
|
- **认证**:每次 spawn 生成随机 token(经 `--rpc-token` 传入),子进程首条消息必须回传该 token(`hello`);服务端只接受**一个**连接,其余连接直接断开——本机其他进程无法伪冒子进程或注入回复;
|
|
63
|
-
- **Electron 定位**:①
|
|
63
|
+
- **Electron 定位**:① 插件运行在 Electron 进程内(DSH Desktop 主进程)时直接复用宿主二进制(`process.execPath`);② 进程祖先树中的宿主 Electron 二进制(DSH Desktop 把插件跑在子 Node 进程时的兜底,Windows 用 PowerShell CIM 查询,仅在其它路径全部落空时才执行)→ ③ 随插件安装的 electron 包 → ④ `ELECTRON_PATH` → ⑤ 锚点与 pnpm store 中**版本最新**者(33.x 有合成器缺陷,建议 ≥ 40);
|
|
64
64
|
- **稳健性**:子进程/套接字都有 `error` 监听(否则未捕获事件会炸掉整个 DSH 进程);子进程退出自动重启;物化失败可重试;下载仅限 HTTP(S)、`savePath` 必须绝对路径(默认限定 `~/Downloads`,可配置 `downloadDir` 覆盖)、流式限流 + Content-Length 提前拒绝、256MB 上限与 60s 超时,文件由**子进程直接落盘**(临时文件 + rename,不再经 RPC 传 base64);cookie 导出/恢复有 30s 超时;
|
|
65
65
|
- **视图可见性**:多标签/多会话时,`showView` 隐藏其他视图并把目标视图置顶(remove+re-add),确保用户看到的是活动标签;目标是当前可见视图时**跳过** remove/re-add(否则每次操作都闪烁);窗口标题实时显示当前可见会话的任务标识 + 页面标题/URL(`showView` 携带会话 label,子进程读 `getTitle`/`getURL`);窗口 resize 时视图 bounds 自动跟随;
|
|
66
66
|
- **孤儿防护**:父进程断开时子进程自动退出,不留僵尸窗口;
|
package/docs/user-guide.md
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## 环境要求
|
|
4
4
|
|
|
5
|
-
- DeepSeek Harness(dsh)
|
|
6
|
-
- **Electron 运行时**(
|
|
5
|
+
- DeepSeek Harness(dsh),已安装对应 profile(`web` / `desktop` 等)
|
|
6
|
+
- **Electron 运行时**(必装依赖,随插件自动安装):
|
|
7
|
+
- **DSH Desktop**:宿主本身基于 Electron,**插件自动复用宿主二进制**(随包安装的 electron 仅作后备);
|
|
8
|
+
- **纯 `dsh web` 自托管**:直接使用随插件安装的 electron 包(建议 ≥ 40,33.x 存在截图合成器缺陷;44+ 首次使用自动下载,需网络)。
|
|
7
9
|
|
|
8
10
|
## 安装
|
|
9
11
|
|
|
10
12
|
```sh
|
|
11
|
-
# 从 npm 安装(已发布)
|
|
13
|
+
# 从 npm 安装(已发布);`--profile` 换成你实际使用的 profile(DSH Desktop 为 `desktop`)
|
|
12
14
|
dsh plugin --profile web add dsh-builtin-browser
|
|
13
15
|
|
|
14
16
|
# 或从源码目录(独立仓库,一插件一仓库)
|
|
@@ -72,7 +74,9 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
72
74
|
能。插件自托管:自己拉起 Electron 窗口,无需桌面外壳。
|
|
73
75
|
|
|
74
76
|
**Q:找不到 Electron?**
|
|
75
|
-
插件按顺序自动定位:①
|
|
77
|
+
插件按顺序自动定位:① 插件运行在 Electron 进程内(DSH Desktop 主进程)时直接复用宿主二进制 → ② 进程祖先树中的宿主 Electron 二进制(DSH Desktop 把插件跑在子 Node 进程时的兜底)→ ③ 随插件安装的 electron 包 `require('electron')`(仅二进制已就绪时)→ ④ `ELECTRON_PATH` 环境变量 → ⑤ DSH 锚点与 pnpm 虚拟仓库中版本最新者。
|
|
78
|
+
|
|
79
|
+
DSH Desktop 上①②任一命中即开箱可用,无需任何安装。纯 `dsh web` 下全部落空时,报错会给出指引:electron 已随插件安装,44+ 二进制首次使用自动下载(需网络);必要时可 `npx install-electron` 或设置 `ELECTRON_PATH`。
|
|
76
80
|
|
|
77
81
|
**Q:截图失败或挂起?**
|
|
78
82
|
确保 Electron ≥ 40(33.x 有合成器缺陷)。自托管截图优先走原生 `capturePage`,多视图/窗口未激活时自动兜底到 CDP。
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
* sees traffic.
|
|
25
25
|
* @module dsh-browser/browser-electron/remote-host
|
|
26
26
|
*/
|
|
27
|
-
import { spawn } from 'node:child_process';
|
|
27
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
28
28
|
import { randomBytes } from 'node:crypto';
|
|
29
29
|
import { createRequire } from 'node:module';
|
|
30
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
31
|
-
import { dirname, join } from 'node:path';
|
|
30
|
+
import { existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
31
|
+
import { basename, dirname, join } from 'node:path';
|
|
32
32
|
import { createServer } from 'node:net';
|
|
33
33
|
import { fileURLToPath } from 'node:url';
|
|
34
34
|
/** How long to wait for the child to signal readiness before failing. */
|
|
@@ -40,12 +40,15 @@ const MAX_RPC_BUFFER_BYTES = 512 * 1024 * 1024;
|
|
|
40
40
|
* 0. the current process IS Electron (DSH Desktop hosts load this plugin
|
|
41
41
|
* inside their own Electron main process) — reuse the host binary, so
|
|
42
42
|
* the plugin works out of the box without `add electron`;
|
|
43
|
-
* 1. require('electron') from this module (
|
|
43
|
+
* 1. require('electron') from this module (bundled dependency — the
|
|
44
|
+
* electron package ships with the plugin),
|
|
44
45
|
* 2. ELECTRON_PATH (explicit override),
|
|
45
46
|
* 3. every DSH install anchor + pnpm virtual store candidate, choosing the
|
|
46
47
|
* NEWEST version found. Older Electron releases (e.g. 33.x) have a
|
|
47
48
|
* compositor defect that intermittently breaks page capture, so prefer
|
|
48
|
-
* the newest binary available in the environment
|
|
49
|
+
* the newest binary available in the environment,
|
|
50
|
+
* 4. last resort: the process ancestry, for hosts that run this plugin in a
|
|
51
|
+
* child Node process of their own Electron main process (DSH Desktop).
|
|
49
52
|
* @returns the path to the Electron executable.
|
|
50
53
|
*/
|
|
51
54
|
function resolveElectronPath() {
|
|
@@ -57,8 +60,8 @@ function resolveElectronPath() {
|
|
|
57
60
|
if (typeof exe === 'string' && exe.length > 0 && existsSync(exe))
|
|
58
61
|
return exe;
|
|
59
62
|
}
|
|
60
|
-
// 1.
|
|
61
|
-
//
|
|
63
|
+
// 1. Bundled dependency — but ONLY when its binary is already on disk.
|
|
64
|
+
// Electron 44+ downloads the binary lazily on the first require()
|
|
62
65
|
// (it has no postinstall anymore), so probing here must never trigger a
|
|
63
66
|
// synchronous network download: available() would block DSH startup for
|
|
64
67
|
// the download (or fail outright offline), surfacing as a broken plugin.
|
|
@@ -137,17 +140,100 @@ function resolveElectronPath() {
|
|
|
137
140
|
dir = parent;
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
// 4. Last resort for Electron hosts. DSH Desktop may run the agent runtime
|
|
144
|
+
// (and thus this plugin) in a plain Node child of its Electron main
|
|
145
|
+
// process — step 0 does not fire there, and there is no electron package
|
|
146
|
+
// anywhere to find. The host's own binary IS an Electron runtime and is
|
|
147
|
+
// guaranteed present (it is running us), so walk the process ancestry
|
|
148
|
+
// and reuse the first Electron executable found. Zero extra install;
|
|
149
|
+
// this is what makes the plugin work on DSH Desktop without `add
|
|
150
|
+
// electron`, no matter how the host loads plugins.
|
|
151
|
+
const hostExe = hostElectronViaProcessTree();
|
|
152
|
+
if (hostExe !== undefined)
|
|
153
|
+
return hostExe;
|
|
146
154
|
throw new Error('dsh-builtin-browser: cannot locate the Electron binary. Options: (1) if you are on DSH Desktop, ' +
|
|
147
|
-
'this should never appear — the plugin reuses the host Electron automatically; (2)
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
155
|
+
'this should never appear — the plugin reuses the host Electron automatically; (2) the electron ' +
|
|
156
|
+
'package ships with the plugin, so on newer installs nothing is needed here — on installs from ' +
|
|
157
|
+
'before that change, add it to the active profile (`dsh plugin --profile <your-profile> add electron`; ' +
|
|
158
|
+
'on DSH Desktop the profile is `desktop`); (3) set ELECTRON_PATH to the electron executable. Note: ' +
|
|
159
|
+
'Electron 44+ downloads its binary on first use, so if the package is installed but the binary is ' +
|
|
160
|
+
'missing, run `npx install-electron` once (needs network) and retry.');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Last-resort Electron discovery: walk this process's ancestry looking for a
|
|
164
|
+
* parent process whose executable is itself an Electron binary. Covers hosts
|
|
165
|
+
* that run the plugin in a child Node process of their own Electron main
|
|
166
|
+
* process (e.g. DSH Desktop): the host binary is already on disk and running,
|
|
167
|
+
* so reusing it needs no extra install. Filesystem-only on POSIX (/proc); on
|
|
168
|
+
* Windows a best-effort PowerShell CIM query is used — this path only runs
|
|
169
|
+
* when every other lookup failed, so its latency never touches the happy
|
|
170
|
+
* path, and a missing/blocked powershell simply yields nothing.
|
|
171
|
+
*/
|
|
172
|
+
function hostElectronViaProcessTree() {
|
|
173
|
+
let pid = process.ppid;
|
|
174
|
+
for (let depth = 0; depth < 6 && pid !== undefined && pid > 1; depth++) {
|
|
175
|
+
const exe = processExecutable(pid);
|
|
176
|
+
if (exe !== undefined && isElectronBinary(exe))
|
|
177
|
+
return exe;
|
|
178
|
+
pid = processParent(pid);
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
/** The executable path of a running process, or undefined when unknown. */
|
|
183
|
+
function processExecutable(pid) {
|
|
184
|
+
try {
|
|
185
|
+
if (process.platform === 'win32') {
|
|
186
|
+
const out = execFileSync('powershell.exe', [
|
|
187
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
188
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").ExecutablePath`,
|
|
189
|
+
], { encoding: 'utf8', windowsHide: true, timeout: 3000 });
|
|
190
|
+
const line = out.trim().split(/\r?\n/)[0] ?? '';
|
|
191
|
+
return line.length > 0 ? line : undefined;
|
|
192
|
+
}
|
|
193
|
+
const exe = readlinkSync(`/proc/${pid}/exe`);
|
|
194
|
+
return exe.length > 0 ? exe : undefined;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** The parent pid of a running process, or undefined when unknown. */
|
|
201
|
+
function processParent(pid) {
|
|
202
|
+
try {
|
|
203
|
+
if (process.platform === 'win32') {
|
|
204
|
+
const out = execFileSync('powershell.exe', [
|
|
205
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
206
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").ParentProcessId`,
|
|
207
|
+
], { encoding: 'utf8', windowsHide: true, timeout: 3000 });
|
|
208
|
+
const n = Number(out.trim().split(/\r?\n/)[0]);
|
|
209
|
+
return Number.isInteger(n) && n > 1 ? n : undefined;
|
|
210
|
+
}
|
|
211
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
212
|
+
// Format: pid (comm) state ppid ... — comm is parenthesized and may
|
|
213
|
+
// itself contain spaces/parens, so parse from the LAST ')'; ppid is the
|
|
214
|
+
// second field after it.
|
|
215
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/);
|
|
216
|
+
const n = Number(fields[1]);
|
|
217
|
+
return Number.isInteger(n) && n > 1 ? n : undefined;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* True when the given executable is an Electron binary. The name match
|
|
225
|
+
* covers dev binaries (electron, electron.exe); the resources marker covers
|
|
226
|
+
* packaged apps whose exe carries the product name (e.g. "DSH Desktop.exe") —
|
|
227
|
+
* every Electron distribution ships `resources/electron.asar`,
|
|
228
|
+
* `resources/app.asar`, or `resources/default_app.asar` beside the binary.
|
|
229
|
+
*/
|
|
230
|
+
function isElectronBinary(exe) {
|
|
231
|
+
if (basename(exe).toLowerCase().includes('electron'))
|
|
232
|
+
return true;
|
|
233
|
+
const resources = join(dirname(exe), 'resources');
|
|
234
|
+
return existsSync(join(resources, 'electron.asar'))
|
|
235
|
+
|| existsSync(join(resources, 'app.asar'))
|
|
236
|
+
|| existsSync(join(resources, 'default_app.asar'));
|
|
151
237
|
}
|
|
152
238
|
/** Extract an electron version like "43.4.0" from a path containing it. */
|
|
153
239
|
function versionOf(path) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-builtin-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Shared real browser plugin for DeepSeek Harness: install-and-use — a visible native browser the human can take over, driven by the agent over CDP. DOM-level interaction (React/Vue safe), per-task session isolation, cookie persistence (browser_auth), CAPTCHA detection, batch form fill (browser_fill), authenticated download, history/replay, action restriction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -60,19 +60,15 @@
|
|
|
60
60
|
"engines": {
|
|
61
61
|
"node": ">=22.19"
|
|
62
62
|
},
|
|
63
|
-
"dependencies": {
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"electron": ">= 30.0.0"
|
|
65
|
+
},
|
|
64
66
|
"peerDependencies": {
|
|
65
67
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
66
68
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
67
69
|
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
68
70
|
"@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
|
|
69
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
70
|
-
"electron": ">= 30.0.0"
|
|
71
|
-
},
|
|
72
|
-
"peerDependenciesMeta": {
|
|
73
|
-
"electron": {
|
|
74
|
-
"optional": true
|
|
75
|
-
}
|
|
71
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
76
72
|
},
|
|
77
73
|
"devDependencies": {
|
|
78
74
|
"@types/node": "^22.0.0",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
* @module dsh-browser/browser-electron/remote-host
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { spawn, type ChildProcessByStdio } from 'node:child_process'
|
|
28
|
+
import { execFileSync, spawn, type ChildProcessByStdio } from 'node:child_process'
|
|
29
29
|
import { randomBytes } from 'node:crypto'
|
|
30
30
|
import { createRequire } from 'node:module'
|
|
31
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
32
|
-
import { dirname, join } from 'node:path'
|
|
31
|
+
import { existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs'
|
|
32
|
+
import { basename, dirname, join } from 'node:path'
|
|
33
33
|
import { createServer, type Server, type Socket } from 'node:net'
|
|
34
34
|
import { fileURLToPath } from 'node:url'
|
|
35
35
|
import type { BrowserUserAction, ElectronBrowserViewHost, ElectronViewHandle, ScreenshotOptions } from './provider.js'
|
|
@@ -46,12 +46,15 @@ const MAX_RPC_BUFFER_BYTES = 512 * 1024 * 1024
|
|
|
46
46
|
* 0. the current process IS Electron (DSH Desktop hosts load this plugin
|
|
47
47
|
* inside their own Electron main process) — reuse the host binary, so
|
|
48
48
|
* the plugin works out of the box without `add electron`;
|
|
49
|
-
* 1. require('electron') from this module (
|
|
49
|
+
* 1. require('electron') from this module (bundled dependency — the
|
|
50
|
+
* electron package ships with the plugin),
|
|
50
51
|
* 2. ELECTRON_PATH (explicit override),
|
|
51
52
|
* 3. every DSH install anchor + pnpm virtual store candidate, choosing the
|
|
52
53
|
* NEWEST version found. Older Electron releases (e.g. 33.x) have a
|
|
53
54
|
* compositor defect that intermittently breaks page capture, so prefer
|
|
54
|
-
* the newest binary available in the environment
|
|
55
|
+
* the newest binary available in the environment,
|
|
56
|
+
* 4. last resort: the process ancestry, for hosts that run this plugin in a
|
|
57
|
+
* child Node process of their own Electron main process (DSH Desktop).
|
|
55
58
|
* @returns the path to the Electron executable.
|
|
56
59
|
*/
|
|
57
60
|
function resolveElectronPath(): string {
|
|
@@ -62,8 +65,8 @@ function resolveElectronPath(): string {
|
|
|
62
65
|
const exe = process.execPath
|
|
63
66
|
if (typeof exe === 'string' && exe.length > 0 && existsSync(exe)) return exe
|
|
64
67
|
}
|
|
65
|
-
// 1.
|
|
66
|
-
//
|
|
68
|
+
// 1. Bundled dependency — but ONLY when its binary is already on disk.
|
|
69
|
+
// Electron 44+ downloads the binary lazily on the first require()
|
|
67
70
|
// (it has no postinstall anymore), so probing here must never trigger a
|
|
68
71
|
// synchronous network download: available() would block DSH startup for
|
|
69
72
|
// the download (or fail outright offline), surfacing as a broken plugin.
|
|
@@ -136,20 +139,103 @@ function resolveElectronPath(): string {
|
|
|
136
139
|
dir = parent
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// 4. Last resort for Electron hosts. DSH Desktop may run the agent runtime
|
|
143
|
+
// (and thus this plugin) in a plain Node child of its Electron main
|
|
144
|
+
// process — step 0 does not fire there, and there is no electron package
|
|
145
|
+
// anywhere to find. The host's own binary IS an Electron runtime and is
|
|
146
|
+
// guaranteed present (it is running us), so walk the process ancestry
|
|
147
|
+
// and reuse the first Electron executable found. Zero extra install;
|
|
148
|
+
// this is what makes the plugin work on DSH Desktop without `add
|
|
149
|
+
// electron`, no matter how the host loads plugins.
|
|
150
|
+
const hostExe = hostElectronViaProcessTree()
|
|
151
|
+
if (hostExe !== undefined) return hostExe
|
|
144
152
|
throw new Error(
|
|
145
153
|
'dsh-builtin-browser: cannot locate the Electron binary. Options: (1) if you are on DSH Desktop, ' +
|
|
146
|
-
'this should never appear — the plugin reuses the host Electron automatically; (2)
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
154
|
+
'this should never appear — the plugin reuses the host Electron automatically; (2) the electron ' +
|
|
155
|
+
'package ships with the plugin, so on newer installs nothing is needed here — on installs from ' +
|
|
156
|
+
'before that change, add it to the active profile (`dsh plugin --profile <your-profile> add electron`; ' +
|
|
157
|
+
'on DSH Desktop the profile is `desktop`); (3) set ELECTRON_PATH to the electron executable. Note: ' +
|
|
158
|
+
'Electron 44+ downloads its binary on first use, so if the package is installed but the binary is ' +
|
|
159
|
+
'missing, run `npx install-electron` once (needs network) and retry.',
|
|
150
160
|
)
|
|
151
161
|
}
|
|
152
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Last-resort Electron discovery: walk this process's ancestry looking for a
|
|
165
|
+
* parent process whose executable is itself an Electron binary. Covers hosts
|
|
166
|
+
* that run the plugin in a child Node process of their own Electron main
|
|
167
|
+
* process (e.g. DSH Desktop): the host binary is already on disk and running,
|
|
168
|
+
* so reusing it needs no extra install. Filesystem-only on POSIX (/proc); on
|
|
169
|
+
* Windows a best-effort PowerShell CIM query is used — this path only runs
|
|
170
|
+
* when every other lookup failed, so its latency never touches the happy
|
|
171
|
+
* path, and a missing/blocked powershell simply yields nothing.
|
|
172
|
+
*/
|
|
173
|
+
function hostElectronViaProcessTree(): string | undefined {
|
|
174
|
+
let pid: number | undefined = process.ppid
|
|
175
|
+
for (let depth = 0; depth < 6 && pid !== undefined && pid > 1; depth++) {
|
|
176
|
+
const exe = processExecutable(pid)
|
|
177
|
+
if (exe !== undefined && isElectronBinary(exe)) return exe
|
|
178
|
+
pid = processParent(pid)
|
|
179
|
+
}
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The executable path of a running process, or undefined when unknown. */
|
|
184
|
+
function processExecutable(pid: number): string | undefined {
|
|
185
|
+
try {
|
|
186
|
+
if (process.platform === 'win32') {
|
|
187
|
+
const out = execFileSync('powershell.exe', [
|
|
188
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
189
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").ExecutablePath`,
|
|
190
|
+
], { encoding: 'utf8', windowsHide: true, timeout: 3000 })
|
|
191
|
+
const line = out.trim().split(/\r?\n/)[0] ?? ''
|
|
192
|
+
return line.length > 0 ? line : undefined
|
|
193
|
+
}
|
|
194
|
+
const exe = readlinkSync(`/proc/${pid}/exe`)
|
|
195
|
+
return exe.length > 0 ? exe : undefined
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The parent pid of a running process, or undefined when unknown. */
|
|
202
|
+
function processParent(pid: number): number | undefined {
|
|
203
|
+
try {
|
|
204
|
+
if (process.platform === 'win32') {
|
|
205
|
+
const out = execFileSync('powershell.exe', [
|
|
206
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
207
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").ParentProcessId`,
|
|
208
|
+
], { encoding: 'utf8', windowsHide: true, timeout: 3000 })
|
|
209
|
+
const n = Number(out.trim().split(/\r?\n/)[0])
|
|
210
|
+
return Number.isInteger(n) && n > 1 ? n : undefined
|
|
211
|
+
}
|
|
212
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
|
|
213
|
+
// Format: pid (comm) state ppid ... — comm is parenthesized and may
|
|
214
|
+
// itself contain spaces/parens, so parse from the LAST ')'; ppid is the
|
|
215
|
+
// second field after it.
|
|
216
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/)
|
|
217
|
+
const n = Number(fields[1])
|
|
218
|
+
return Number.isInteger(n) && n > 1 ? n : undefined
|
|
219
|
+
} catch {
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* True when the given executable is an Electron binary. The name match
|
|
226
|
+
* covers dev binaries (electron, electron.exe); the resources marker covers
|
|
227
|
+
* packaged apps whose exe carries the product name (e.g. "DSH Desktop.exe") —
|
|
228
|
+
* every Electron distribution ships `resources/electron.asar`,
|
|
229
|
+
* `resources/app.asar`, or `resources/default_app.asar` beside the binary.
|
|
230
|
+
*/
|
|
231
|
+
function isElectronBinary(exe: string): boolean {
|
|
232
|
+
if (basename(exe).toLowerCase().includes('electron')) return true
|
|
233
|
+
const resources = join(dirname(exe), 'resources')
|
|
234
|
+
return existsSync(join(resources, 'electron.asar'))
|
|
235
|
+
|| existsSync(join(resources, 'app.asar'))
|
|
236
|
+
|| existsSync(join(resources, 'default_app.asar'))
|
|
237
|
+
}
|
|
238
|
+
|
|
153
239
|
/** Extract an electron version like "43.4.0" from a path containing it. */
|
|
154
240
|
function versionOf(path: string): string {
|
|
155
241
|
const match = /electron@(\d+\.\d+\.\d+)/.exec(path)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ambient declaration for the `electron` module used by the self-hosted
|
|
3
|
-
* browser host (`host-main.ts`). The electron package is
|
|
4
|
-
* dependency
|
|
5
|
-
*
|
|
3
|
+
* browser host (`host-main.ts`). The electron package is a bundled runtime
|
|
4
|
+
* dependency, but its type definitions can be absent when the published
|
|
5
|
+
* `src/` is typechecked standalone; this shim keeps typechecking
|
|
6
|
+
* self-contained.
|
|
6
7
|
* @module dsh-browser/types/electron-shim
|
|
7
8
|
*/
|
|
8
9
|
|
|
@@ -57,10 +58,15 @@ declare module 'electron' {
|
|
|
57
58
|
loadURL(url: string): Promise<void>
|
|
58
59
|
loadFile(path: string): Promise<void>
|
|
59
60
|
send(channel: string, payload: unknown): void
|
|
61
|
+
focus(): void
|
|
60
62
|
setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' } | { action: 'allow' }): void
|
|
61
63
|
getTitle(): string
|
|
62
64
|
getURL(): string
|
|
63
65
|
on(event: 'ipc-message', listener: (event: unknown, channel: string, ...args: unknown[]) => void): this
|
|
66
|
+
// Windows keyboard focus routing: clicks reach any view, but keyboard
|
|
67
|
+
// events go only to the focused webContents (electron#28163). `input` is
|
|
68
|
+
// the raw input event; only its `type` is consumed here.
|
|
69
|
+
on(event: 'input-event', listener: (event: unknown, input: { type: string }) => void): this
|
|
64
70
|
on(event: 'page-title-updated' | 'did-navigate' | 'did-navigate-in-page' | 'dom-ready', listener: (event: unknown, ...args: unknown[]) => void): this
|
|
65
71
|
}
|
|
66
72
|
export interface WebContentsView {
|
|
@@ -88,8 +94,10 @@ declare module 'electron' {
|
|
|
88
94
|
setTitle(title: string): void
|
|
89
95
|
on(event: 'closed', listener: () => void): this
|
|
90
96
|
on(event: 'resize', listener: () => void): this
|
|
97
|
+
on(event: 'focus', listener: () => void): this
|
|
91
98
|
off(event: 'closed', listener: () => void): this
|
|
92
99
|
off(event: 'resize', listener: () => void): this
|
|
100
|
+
off(event: 'focus', listener: () => void): this
|
|
93
101
|
}
|
|
94
102
|
export const app: {
|
|
95
103
|
whenReady(): Promise<void>
|