dsh-builtin-browser 0.1.10 → 0.1.12
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 +199 -83
- package/README.md +197 -89
- package/lib/browser-electron/host-main.js +101 -0
- package/lib/browser-electron/provider.d.ts +2 -0
- package/lib/browser-electron/provider.js +28 -11
- package/lib/browser-electron/remote-host.js +62 -40
- package/package.json +1 -1
- package/src/browser-electron/host-main.ts +80 -0
- package/src/browser-electron/provider.ts +34 -11
- package/src/browser-electron/remote-host.ts +61 -36
- package/src/types/electron-shim.d.ts +13 -0
package/README.md
CHANGED
|
@@ -1,80 +1,162 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/github/stars/wqty123/dsh-browser?style=flat&label=%E2%98%85&color=08C" alt="GitHub stars">
|
|
3
|
+
<img src="https://img.shields.io/npm/v/dsh-builtin-browser?style=flat&label=npm&color=CB3837" alt="npm version">
|
|
4
|
+
<img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License">
|
|
5
|
+
<img src="https://img.shields.io/badge/DSH-Plugin-47848F?style=flat" alt="DeepSeek Harness plugin">
|
|
6
|
+
<img src="https://img.shields.io/badge/Windows%20%7C%20macOS%20%7C%20Linux-4493F8?style=flat-square" alt="Platforms">
|
|
7
|
+
</p>
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
<p align="center"><sub>中文 · <a href="README.en.md">English</a></sub></p>
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
<h3 align="center">为 DeepSeek Harness 生态打造的<b>共享真实浏览器</b>插件(装好即用,人机同页)</h3>
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
<h4 align="center">agent 驱动一个真实、可见、可随时人工接管的浏览器——人与 agent 操作的是<b>同一个页面</b>。</h4>
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
- **真实可见、人工可接管**:不是无头截屏或转播,用户随时能看到页面并直接接手操作。
|
|
11
|
-
- **DOM 级驱动,框架友好**:React/Vue 受控组件也能可靠填写。
|
|
12
|
-
- **并发安全**:任务级浏览器会话隔离,多个任务并行互不抢页面、不互相污染。
|
|
13
|
-
- **面向真实世界**:人机验证识别、登录态持久化、批量填表、带登录态下载、操作回放、动作限制,一应俱全。
|
|
15
|
+
## 这是什么
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
`dsh-builtin-browser` 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 提供浏览器能力:
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
- **真实视图,而非转播**:浏览器是原生 `WebContentsView`,用户直接看到 agent 在做什么,随时可以上手接管;
|
|
20
|
+
- **装好即用**:有桌面外壳时嵌入外壳视图;纯 `dsh web` 也能**自托管**——插件自己拉起一个 Electron 窗口,不需要任何额外配置;
|
|
21
|
+
- **一插件即一套工具**:安装后 agent 自动获得 20 个 `browser_*` 工具(打开、查看、操作、填表、截图、下载、登录态管理……)。
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
- **DOM 引用,而非猜坐标。** `browser_snapshot` 返回带编号的交互元素;`browser_execute` 在页面里执行 JS(框架输入用原生 setter),在 React/Vue 页面上也能可靠交互。
|
|
21
|
-
- **多标签会话。** 并行打开 URL、查看/切换/关闭/重置标签,状态保持。
|
|
22
|
-
- **多格式内容。** 以 html / markdown / txt / json 抓取页面,支持 selector 限定、长度与超时上限。
|
|
23
|
+
一句话:**安装插件 = 获得一个与用户共享、可被 agent 驱动的真实浏览器。**
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- **按任务隔离。** 每个 DSH 任务(会话)拥有独立的浏览器会话(独立标签页与历史),并发任务互不干扰;同一任务内多次调用复用同一会话(`browser_session` / `browser_reset_session`)。
|
|
27
|
-
- **登录态持久化。** `browser_auth` 导出/恢复 cookie,重启后登录态不丢。
|
|
28
|
-
- **人机验证识别。** 自动检测 Cloudflare / reCAPTCHA / hCaptcha / Turnstile 等挑战(`browser_challenge`,快照也会标注),提示人工在共享窗口完成,不再盲目重试。
|
|
29
|
-
- **批量表单填充。** `browser_fill` 一次填写多个字段:按选择器/名称/标签匹配,支持受控输入、下拉、单选/复选,可选提交。
|
|
30
|
-
- **操作历史与回放。** `browser_history` 记录操作日志,`browser_replay` 可回放某一步。
|
|
31
|
-
- **带登录态下载。** `browser_download` 用会话 cookie 把文件取到本地,登录后内容可直接落盘。
|
|
32
|
-
- **安全限制。** `browser_restrict` 限制允许的浏览器动作,防误点/误导航。
|
|
33
|
-
|
|
34
|
-
## 环境要求
|
|
35
|
-
|
|
36
|
-
- DeepSeek Harness(dsh)且安装了 `web` profile
|
|
37
|
-
- **Electron 运行时**(可选 peer 依赖):桌面外壳自带;纯 `dsh web` 下需要能定位到 Electron 二进制(见下)
|
|
38
|
-
- 有桌面外壳(`ctx.electronViewHost`)时用外壳嵌入视图;没有时插件**自托管**:自己拉起一个 Electron 窗口,`browser_*` 工具照常可用
|
|
39
|
-
|
|
40
|
-
**Electron 定位顺序**:① `require('electron')`(peer 依赖已装)→ ② DSH 安装锚点 → ③ `node_modules/.pnpm` 虚拟仓库 → ④ `ELECTRON_PATH` 环境变量。找不到时工具会报清晰的错误提示。
|
|
41
|
-
|
|
42
|
-
### 验证过的版本
|
|
43
|
-
|
|
44
|
-
| 组件 | 版本 |
|
|
45
|
-
|---|---|
|
|
46
|
-
| DeepSeek Harness(dsh) | `0.1.0-rc.5` |
|
|
47
|
-
| Electron | `43.4.0` |
|
|
48
|
-
| Node.js | `22.20.0` |
|
|
49
|
-
| dsh-builtin-browser | `0.1.7` |
|
|
50
|
-
| 操作系统 | Windows 10 (10.0.26200) |
|
|
51
|
-
|
|
52
|
-
> 插件声明 `electron >= 30`;其他平台(如 macOS/Linux)按同一协议运行,但仅在上表 Windows 环境实测。
|
|
53
|
-
|
|
54
|
-
## 安装
|
|
25
|
+
## 快速开始
|
|
55
26
|
|
|
56
27
|
```sh
|
|
57
|
-
|
|
58
|
-
|
|
28
|
+
# 方式一:从 npm 安装(已发布)
|
|
29
|
+
dsh plugin --profile web add dsh-builtin-browser
|
|
30
|
+
|
|
31
|
+
# 方式二:从源码目录安装(独立仓库,一插件一仓库)
|
|
59
32
|
dsh plugin --profile web add <本仓库路径>
|
|
60
33
|
```
|
|
61
34
|
|
|
62
|
-
|
|
35
|
+
安装后,agent 即可使用浏览器工具,例如:
|
|
36
|
+
|
|
37
|
+
| 想做什么 | 用哪个工具 | 说明 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| 打开页面 | `browser_open` | 打开 URL,返回带编号元素的快照 |
|
|
40
|
+
| 了解页面 | `browser_snapshot` | 输入框/按钮/链接的编号清单,可据此定位 |
|
|
41
|
+
| 操作页面 | `browser_execute` | 在页面里执行 JS(原生 setter,框架友好) |
|
|
42
|
+
| 填写表单 | `browser_fill` | 一次填写多个字段,可选提交 |
|
|
43
|
+
| 看到页面 | `browser_screenshot` | PNG 截图,可存文件交给视觉模型 |
|
|
44
|
+
|
|
45
|
+
完整清单见[工具参考](#工具参考)。
|
|
46
|
+
|
|
47
|
+
## 主要功能
|
|
48
|
+
|
|
49
|
+
<table>
|
|
50
|
+
<tr>
|
|
51
|
+
<td width="50%" valign="top">
|
|
52
|
+
<h3>共享真实浏览器</h3>
|
|
53
|
+
<p>原生视图而非无头截屏。用户与 agent 操作同一个页面:用户能看到每一步,随时接管;agent 驱动的就是用户眼前那个窗口。</p>
|
|
54
|
+
</td>
|
|
55
|
+
<td width="50%" valign="top">
|
|
56
|
+
<h3>DOM 级驱动,框架友好</h3>
|
|
57
|
+
<p><code>browser_snapshot</code> 返回带编号的交互元素;<code>browser_execute</code> 在页面内执行 JS(受控输入用原生 setter + input/change 事件),React/Vue 页面也能可靠交互。</p>
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<tr>
|
|
61
|
+
<td width="50%" valign="top">
|
|
62
|
+
<h3>多标签会话</h3>
|
|
63
|
+
<p>并行打开 URL,查看/切换/关闭/重置标签,每个会话的状态独立保持。</p>
|
|
64
|
+
</td>
|
|
65
|
+
<td width="50%" valign="top">
|
|
66
|
+
<h3>多格式内容</h3>
|
|
67
|
+
<p>以 html / markdown / txt / json 抓取页面,支持 CSS selector 限定、字符上限与超时控制。</p>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<tr>
|
|
71
|
+
<td width="50%" valign="top">
|
|
72
|
+
<h3>任务级会话隔离</h3>
|
|
73
|
+
<p>每个 DSH 任务(会话)拥有独立的浏览器会话(独立标签页与历史),并发任务互不抢页面、互不污染;同一任务内多次调用复用同一会话。</p>
|
|
74
|
+
</td>
|
|
75
|
+
<td width="50%" valign="top">
|
|
76
|
+
<h3>登录态持久化</h3>
|
|
77
|
+
<p><code>browser_auth</code> 导出/恢复 cookie,重启后登录态不丢;自托管实例的 cookie 本身也落盘持久。</p>
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
<tr>
|
|
81
|
+
<td width="50%" valign="top">
|
|
82
|
+
<h3>人机验证识别</h3>
|
|
83
|
+
<p>自动检测 Cloudflare / reCAPTCHA / hCaptcha / Turnstile 等挑战(<code>browser_challenge</code>,快照也会标注),提示人工在共享窗口完成,不再盲目重试。</p>
|
|
84
|
+
</td>
|
|
85
|
+
<td width="50%" valign="top">
|
|
86
|
+
<h3>批量表单填充</h3>
|
|
87
|
+
<p><code>browser_fill</code> 一次填写多个字段:按选择器/名称/标签匹配,支持受控输入、下拉、单选/复选,可选提交;单个字段失败不影响其余字段。</p>
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
90
|
+
<tr>
|
|
91
|
+
<td width="50%" valign="top">
|
|
92
|
+
<h3>操作历史与回放</h3>
|
|
93
|
+
<p><code>browser_history</code> 记录操作日志(打开/执行/点击/输入/填表/下载/登录),<code>browser_replay</code> 可按序号回放某一步。</p>
|
|
94
|
+
</td>
|
|
95
|
+
<td width="50%" valign="top">
|
|
96
|
+
<h3>带登录态下载</h3>
|
|
97
|
+
<p><code>browser_download</code> 在页面上下文内携带会话 cookie 拉取文件并落盘,登录后才能访问的内容也能直接下载。</p>
|
|
98
|
+
</td>
|
|
99
|
+
</tr>
|
|
100
|
+
<tr>
|
|
101
|
+
<td width="50%" valign="top">
|
|
102
|
+
<h3>安全限制</h3>
|
|
103
|
+
<p><code>browser_restrict</code> 限制允许的浏览器动作(白名单),防止 agent 误点、误导航;只读工具(snapshot/content/screenshot)不受限。</p>
|
|
104
|
+
</td>
|
|
105
|
+
<td width="50%" valign="top">
|
|
106
|
+
<h3>截图即存即读</h3>
|
|
107
|
+
<p><code>browser_screenshot</code> 支持 <code>savePath</code> 直接落盘 PNG,交给视觉模型(modlens 等)做基于视觉的元素定位。</p>
|
|
108
|
+
</td>
|
|
109
|
+
</tr>
|
|
110
|
+
</table>
|
|
111
|
+
|
|
112
|
+
## 为什么选它
|
|
113
|
+
|
|
114
|
+
- **装好即用,零配置**:不需要桌面外壳、不需要额外启动步骤;纯 `dsh web` 环境自托管拉起 Electron 窗口,`browser_*` 工具照常可用。
|
|
115
|
+
- **人机协同,互不干扰**:用户能看到并接管 agent 的每一个动作;任务级会话隔离让多个并行任务各自拥有独立的标签页与历史。
|
|
116
|
+
- **面向真实世界的自动化**:人机验证识别、登录态持久化、批量填表、带登录态下载、操作回放、动作限制——把"真实浏览器"变成可靠的 agent 能力。
|
|
117
|
+
- **可测试、可替换的架构**:provider 与 Electron 通过 `ElectronBrowserViewHost` 接缝解耦,同一套工具层未来可对接无头转播 provider,无需改动模型侧。
|
|
118
|
+
|
|
119
|
+
## 工具参考
|
|
120
|
+
|
|
121
|
+
| 工具 | 用途 | 守卫 |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `browser_open` | 打开 URL(可选新标签),返回页面快照 | ✅ |
|
|
124
|
+
| `browser_snapshot` | 交互元素(输入框/按钮/链接)带编号清单 | – |
|
|
125
|
+
| `browser_execute` | 在页面执行 JS;参数以 `arguments[0..n]` 传入 | ✅ |
|
|
126
|
+
| `browser_content` | 以 html / markdown / txt / json 抓取页面(selector、maxChars、timeoutMs) | – |
|
|
127
|
+
| `browser_click` | 按视口坐标点击(配合截图做视觉定位) | ✅ |
|
|
128
|
+
| `browser_type` | 向聚焦元素输入文本(CDP `Input.insertText`) | ✅ |
|
|
129
|
+
| `browser_fill` | 批量填充表单(选择器/名称/标签匹配,受控输入、下拉、单选/复选,可选提交) | ✅ |
|
|
130
|
+
| `browser_screenshot` | PNG 截图,可选 `fullPage` 与 `savePath` | – |
|
|
131
|
+
| `browser_list_tabs` | 当前会话的标签列表 | – |
|
|
132
|
+
| `browser_switch_tab` | 按 id 切换标签(自托管下同步切换可见视图) | ✅ |
|
|
133
|
+
| `browser_close_tab` | 按 id 关闭标签;关闭活动标签后激活下一个 | – |
|
|
134
|
+
| `browser_reset` | 关闭本任务所有标签,回到一个空白标签 | ✅ |
|
|
135
|
+
| `browser_session` | 查看本任务的浏览器会话与标签 | – |
|
|
136
|
+
| `browser_reset_session` | 关闭并重建本任务的浏览器会话 | ✅ |
|
|
137
|
+
| `browser_history` | 操作日志(最新在后),含成功/失败与结果摘要 | – |
|
|
138
|
+
| `browser_replay` | 按序号回放某一步(navigate/execute/click/type) | ✅ |
|
|
139
|
+
| `browser_download` | 带会话 cookie 下载 URL 到本地文件(上限 256MB) | ✅ |
|
|
140
|
+
| `browser_auth` | 导出/恢复 cookie(登录态持久化,自托管可用) | ✅ |
|
|
141
|
+
| `browser_challenge` | 检测人机验证(CAPTCHA / Cloudflare / reCAPTCHA / hCaptcha / Turnstile) | – |
|
|
142
|
+
| `browser_restrict` | 限制允许的浏览器动作(白名单;空列表解除) | – |
|
|
143
|
+
|
|
144
|
+
> 「守卫」列:打 ✅ 的动作受 `browser_restrict` 白名单约束;只读工具(snapshot/content/screenshot/list_tabs/session/challenge/history)永不拦截。
|
|
63
145
|
|
|
64
|
-
|
|
65
|
-
|---|---|---|
|
|
66
|
-
| `browser` | `dsh-builtin-browser/browser` | `ctx.browser` 能力 seam(始终挂载) |
|
|
67
|
-
| `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider(需要 `electronViewHost`) |
|
|
68
|
-
| `tool-browser` | `dsh-builtin-browser/tool-browser` | `browser_*` 模型侧工具 |
|
|
146
|
+
### 操作纪律(点击/填表)
|
|
69
147
|
|
|
70
|
-
|
|
148
|
+
- **优先用 DOM 语义而非坐标**:表单提交优先 `form.requestSubmit()`;点击优先 `element.click()`;坐标点击是最后手段。
|
|
149
|
+
- **选中正确的元素**:页面常有隐藏副本(如移动端按钮),用 `browser_execute` 过滤可见元素(`getBoundingClientRect()` 宽高 > 0、`getComputedStyle` 非 `display:none`),再取坐标。
|
|
150
|
+
- **取坐标后立即点击**:中间不要插入其他操作(填表、滚动会移动元素,旧坐标立即失效)。
|
|
151
|
+
- **点击前验证命中**:`document.elementFromPoint(x, y)` 确认该坐标确实是目标元素(按钮/链接),再执行真实点击。
|
|
152
|
+
- **DPR 注意**:CDP 输入使用 CSS 像素;高 DPI 屏上若点击落空,用 `elementFromPoint` 校准,不要盲试坐标。
|
|
71
153
|
|
|
72
154
|
## 配置
|
|
73
155
|
|
|
74
|
-
插件通过 `cordis.patch.yml`
|
|
156
|
+
插件通过 `cordis.patch.yml` 挂载三行,各行配置:
|
|
75
157
|
|
|
76
158
|
| 行 | 配置项 | 类型 | 默认 | 说明 |
|
|
77
|
-
|
|
159
|
+
| --- | --- | --- | --- | --- |
|
|
78
160
|
| `browser-electron` | `viewHost` | 对象 | 必填 | 宿主提供的 `ElectronBrowserViewHost` 实例(通常 `!!js ctx.get('electronViewHost')`) |
|
|
79
161
|
| `browser-electron` | `httpOnly` | 布尔 | `true` | 仅允许 HTTP(S) 导航;其余协议(如 `file:`/`data:`)拒绝(`BROWSER_NAVIGATION_BLOCKED`) |
|
|
80
162
|
| `browser-electron` | `snapshotMaxElements` | 数字 | `60` | 快照最多收录的交互元素数,超出截断 |
|
|
@@ -82,32 +164,6 @@ provider 与工具以 `ctx.get('electronViewHost')` 是否存在为门控,因此
|
|
|
82
164
|
| `tool-browser` | `timeoutMs` | 数字 | `60000` | 工具协作超时(ms) |
|
|
83
165
|
| `tool-browser` | `tabTools` | 布尔 | `true` | 是否注册标签管理工具(`browser_list_tabs` 等) |
|
|
84
166
|
|
|
85
|
-
## 工具
|
|
86
|
-
|
|
87
|
-
| 工具 | 用途 |
|
|
88
|
-
|---|---|
|
|
89
|
-
| `browser_open` | 打开 URL(可选新标签);返回快照 |
|
|
90
|
-
| `browser_snapshot` | 交互元素(输入框/按钮/链接)带编号清单 |
|
|
91
|
-
| `browser_execute` | 在页面执行 JS;参数以 `arguments[0..n]` 传入 |
|
|
92
|
-
| `browser_content` | 以 html / markdown / txt / json 抓取页面(selector、maxChars、timeoutMs) |
|
|
93
|
-
| `browser_screenshot` | PNG 截图,可选 `fullPage` |
|
|
94
|
-
| `browser_list_tabs` / `browser_switch_tab` / `browser_close_tab` / `browser_reset` | 多标签会话管理 |
|
|
95
|
-
| `browser_session` / `browser_reset_session` | 查看/重置当前任务的浏览器会话(按任务隔离) |
|
|
96
|
-
| `browser_history` / `browser_replay` | 查看操作记录并回放某一步 |
|
|
97
|
-
| `browser_download` | 带会话 cookie 下载 URL 到本地文件 |
|
|
98
|
-
| `browser_restrict` | 限制允许的浏览器动作,防误点/误导航 |
|
|
99
|
-
| `browser_auth` | 导出/恢复 cookie,持久化登录态 |
|
|
100
|
-
| `browser_challenge` | 检测人机验证(CAPTCHA / Cloudflare / reCAPTCHA / hCaptcha / Turnstile)是否拦截当前页 |
|
|
101
|
-
| `browser_fill` | 批量填充表单(选择器/名称/标签匹配,支持受控输入、下拉、单选/复选,可选提交) |
|
|
102
|
-
|
|
103
|
-
### 操作纪律(点击/填表)
|
|
104
|
-
|
|
105
|
-
- **优先用 DOM 语义而非坐标**:表单提交优先 `form.requestSubmit()`;点击优先 `element.click()`;坐标点击是最后手段。
|
|
106
|
-
- **选中正确的元素**:页面常有隐藏副本(如移动端按钮),用 `browser_execute` 过滤可见元素(`getBoundingClientRect()` 宽高 > 0、`getComputedStyle` 非 `display:none`),再取坐标。
|
|
107
|
-
- **取坐标后立即点击**:中间不要插入其他操作(填表、滚动会移动元素,旧坐标立即失效)。
|
|
108
|
-
- **点击前验证命中**:`document.elementFromPoint(x, y)` 确认该坐标确实是目标元素(按钮/链接),再执行真实点击。
|
|
109
|
-
- **DPR 注意**:CDP 输入使用 CSS 像素;高 DPI 屏上若点击落空,用 `elementFromPoint` 校准,不要盲试坐标。
|
|
110
|
-
|
|
111
167
|
## 工作原理
|
|
112
168
|
|
|
113
169
|
```
|
|
@@ -118,21 +174,73 @@ agent (browser_* 工具)
|
|
|
118
174
|
→ WebContentsView + webContents.debugger (CDP)
|
|
119
175
|
```
|
|
120
176
|
|
|
121
|
-
|
|
177
|
+
- **seam 层**(`browser` 行)提供 `ctx.browser` 服务:provider 注册、会话生命周期、错误码,与具体实现解耦;
|
|
178
|
+
- **provider 层**(`browser-electron` 行)通过 `ElectronBrowserViewHost` 接缝操作视图(创建/销毁/显示/`sendCommand`),由真实外壳用 Electron 对象实现;
|
|
179
|
+
- **工具层**(`tool-browser` 行)提供模型侧的 20 个 `browser_*` 工具,按调用方任务(DSH 会话)维护独立的浏览器会话。
|
|
180
|
+
|
|
181
|
+
**自托管模式**:没有桌面外壳时,插件自己拉起一个 Electron 子进程(`host-main.js`),通过本机 TCP JSON-RPC 驱动,窗口标题 `dsh-browser`。子进程崩溃会自动重启;截图优先走 Electron 原生 `capturePage`(CDP 截图在多视图下会挂起),并自动选择环境中**最新版本**的 Electron(33.x 有合成器缺陷,建议 ≥ 40)。
|
|
182
|
+
|
|
183
|
+
**Electron 定位顺序**:① `require('electron')`(peer 依赖)→ ② `ELECTRON_PATH`(显式覆盖)→ ③ DSH 安装锚点与 pnpm 虚拟仓库中**版本最新**者。找不到时工具会报清晰的错误提示。
|
|
122
184
|
|
|
123
185
|
## 与桌面外壳的分工
|
|
124
186
|
|
|
125
|
-
浏览器**可见视图**、**浏览器列布局**、**列与视图的对齐**都属于宿主外壳(如 dsh 的 `apps/desktop`),不在本插件内。本插件只消费外壳提供的 `electronViewHost`,负责 seam、provider
|
|
187
|
+
浏览器**可见视图**、**浏览器列布局**、**列与视图的对齐**都属于宿主外壳(如 dsh 的 `apps/desktop`),不在本插件内。本插件只消费外壳提供的 `electronViewHost`,负责 seam、provider 与工具。没有配套外壳时插件**自托管**,功能照常可用。
|
|
188
|
+
|
|
189
|
+
## 环境要求
|
|
190
|
+
|
|
191
|
+
- DeepSeek Harness(dsh)且安装了 `web` profile
|
|
192
|
+
- **Electron 运行时**(可选 peer 依赖):桌面外壳自带;纯 `dsh web` 下插件自动定位 Electron 二进制(见上,建议 ≥ 40)
|
|
193
|
+
|
|
194
|
+
### 验证过的版本
|
|
195
|
+
|
|
196
|
+
| 组件 | 版本 |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| DeepSeek Harness(dsh) | `0.1.0-rc.5` |
|
|
199
|
+
| Electron | `43.4.0`(推荐 ≥ 40;33.x 存在合成器缺陷) |
|
|
200
|
+
| Node.js | `22.20.0` |
|
|
201
|
+
| dsh-builtin-browser | `0.1.11` |
|
|
202
|
+
| 操作系统 | Windows 10 (10.0.26200) |
|
|
203
|
+
|
|
204
|
+
> 插件声明 `electron >= 30`;其他平台(macOS/Linux)按同一协议运行,但仅在上表 Windows 环境实测。
|
|
126
205
|
|
|
127
206
|
## 已知限制
|
|
128
207
|
|
|
129
208
|
- 截图仅 PNG(CDP JPEG 在 Electron 43 上挂起);JPEG 等待非 CDP 转换路径。
|
|
209
|
+
- 自托管截图优先走 Electron 原生 `capturePage`(CDP `captureScreenshot` 在多视图下会挂起);截图前自动把目标标签置顶。
|
|
130
210
|
- 部分主机在软件合成下 `fullPage` 截图不稳定。
|
|
131
|
-
-
|
|
132
|
-
- 人机验证(CAPTCHA)无法自动解决:快照会标注检测到的挑战(`browser_challenge` 可显式检查),此时应请用户在共享窗口中人工完成,而不是反复重试。
|
|
211
|
+
- 人机验证(CAPTCHA)无法自动解决:快照会标注检测到的挑战,此时应请用户在共享窗口中人工完成,而不是反复重试。
|
|
133
212
|
- 无痕模式(`privateMode`)未实现:它需要 Electron 的 session 分区能力,属于宿主层,本插件不承诺。
|
|
213
|
+
- `browser_download` 在页面上下文内 `fetch`(带登录态),受同源/CORS 约束;单文件上限 256MB。
|
|
214
|
+
- `browser_auth` 的 cookie 往返不保留 `hostOnly`/`sameSite` 字段(host-only cookie 恢复后变成 domain cookie);仅自托管浏览器可用。
|
|
215
|
+
- 自托管浏览器子进程崩溃后会自动重启,但崩溃前已打开的会话视图已失效,调用 `browser_reset_session` 重建即可。
|
|
134
216
|
- 本插件不含浏览器列 UI——那是宿主外壳的配套,别把"浏览器列"当成插件能力。
|
|
135
217
|
|
|
136
|
-
##
|
|
218
|
+
## 开发
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
# 类型检查 + 构建(lib/)
|
|
222
|
+
pnpm run build
|
|
223
|
+
|
|
224
|
+
# 功能测试:启动本地页面服务器 + Electron probe(见仓库测试脚本)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
代码结构:
|
|
228
|
+
|
|
229
|
+
| 目录 | 职责 |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `src/browser/` | `ctx.browser` seam 与全部请求/结果类型 |
|
|
232
|
+
| `src/browser-electron/` | Electron CDP provider、自托管子进程(`host-main.ts`)与 RPC 层 |
|
|
233
|
+
| `src/tool-browser/` | 模型侧 `browser_*` 工具 |
|
|
234
|
+
| `src/types/` | electron 环境类型(shim,避免强制依赖 electron 类型) |
|
|
235
|
+
|
|
236
|
+
## 特别感谢
|
|
237
|
+
|
|
238
|
+
特别感谢 [DeepSeek Harness 原始仓库](https://github.com/deepseek-ai/deepseek-harness) 与 DeepSeek AI 团队:本插件的 seam、工具运行时与插件体系都构建在这个项目之上。
|
|
239
|
+
|
|
240
|
+
同时感谢 [Cordis](https://github.com/cordiverse/cordis) 提供的插件化基础,以及所有参与讨论、测试、反馈和插件开发的社区成员。
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
本项目遵循 [MIT License](LICENSE)。
|
|
137
245
|
|
|
138
|
-
|
|
246
|
+
> 本项目是 DeepSeek Harness 的社区插件,并非 DeepSeek 官方产品。
|
|
@@ -16,6 +16,19 @@
|
|
|
16
16
|
import { app, BrowserWindow, WebContentsView } from 'electron';
|
|
17
17
|
import { createInterface } from 'node:readline';
|
|
18
18
|
import { createConnection } from 'node:net';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
// Isolate this host's profile from the DSH app's default Electron userData:
|
|
21
|
+
// several Electron instances sharing Roaming\Electron fight over the GPU
|
|
22
|
+
// cache/session locks, which can leave the window without a display surface
|
|
23
|
+
// (capturePage then fails). A dedicated userData also persists cookies across
|
|
24
|
+
// host restarts (on top of browser_auth). Must run before app is ready.
|
|
25
|
+
try {
|
|
26
|
+
const base = process.env.DSH_HOME ?? app.getPath('appData');
|
|
27
|
+
app.setPath('userData', join(base, 'dsh-builtin-browser-host'));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
process.stderr.write(`[dsh-browser host] userData setup failed: ${String(error)}\n`);
|
|
31
|
+
}
|
|
19
32
|
/** CDP protocol version attached to every view's debugger. */
|
|
20
33
|
const CDP_VERSION = '1.3';
|
|
21
34
|
/** Download cap: the body is shipped base64 as one JSON line; bound the memory. */
|
|
@@ -121,6 +134,94 @@ async function handle(op, msg) {
|
|
|
121
134
|
reply(msg.id, { ok: true, result });
|
|
122
135
|
return;
|
|
123
136
|
}
|
|
137
|
+
case 'capture': {
|
|
138
|
+
const viewId = msg.viewId;
|
|
139
|
+
if (viewId === undefined)
|
|
140
|
+
throw new Error('capture missing viewId');
|
|
141
|
+
const entry = views.get(viewId);
|
|
142
|
+
if (entry === undefined)
|
|
143
|
+
throw new Error(`capture: unknown view ${viewId}`);
|
|
144
|
+
// Two complementary paths, because each has a failure mode:
|
|
145
|
+
// - capturePage: fast and reliable with several WebContentsViews in
|
|
146
|
+
// the window, but needs a live display surface (fails when the
|
|
147
|
+
// window is minimized/occluded/unpainted).
|
|
148
|
+
// - CDP Page.captureScreenshot: works without a display surface, but
|
|
149
|
+
// can hang when another hidden WebContentsView exists in the window.
|
|
150
|
+
// Try capturePage first (show/focus/restore + one retry), then CDP.
|
|
151
|
+
if (window !== undefined) {
|
|
152
|
+
try {
|
|
153
|
+
if (!window.isVisible())
|
|
154
|
+
window.show();
|
|
155
|
+
}
|
|
156
|
+
catch { /* closing */ }
|
|
157
|
+
try {
|
|
158
|
+
window.restore();
|
|
159
|
+
}
|
|
160
|
+
catch { /* not minimized */ }
|
|
161
|
+
window.focus();
|
|
162
|
+
}
|
|
163
|
+
let base64 = '';
|
|
164
|
+
try {
|
|
165
|
+
let image;
|
|
166
|
+
try {
|
|
167
|
+
image = await entry.webContentsView.webContents.capturePage();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
process.stderr.write(`[dsh-browser host] capturePage failed: ${String(error)}\n`);
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
172
|
+
image = await entry.webContentsView.webContents.capturePage();
|
|
173
|
+
}
|
|
174
|
+
const png = image.toPNG();
|
|
175
|
+
if (png.length > 0)
|
|
176
|
+
base64 = png.toString('base64');
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const state = window !== undefined
|
|
180
|
+
? JSON.stringify({
|
|
181
|
+
win: { visible: window.isVisible(), minimized: window.isMinimized(), focused: window.isFocused() },
|
|
182
|
+
})
|
|
183
|
+
: 'no-window';
|
|
184
|
+
process.stderr.write(`[dsh-browser host] capturePage retry failed: ${String(error)} state=${state}\n`);
|
|
185
|
+
base64 = '';
|
|
186
|
+
}
|
|
187
|
+
if (base64 === '') {
|
|
188
|
+
// CDP fallback. Page.captureScreenshot can hang when OTHER views
|
|
189
|
+
// (especially hidden attach-first ones) are in the window, so
|
|
190
|
+
// temporarily detach the siblings, capture in single-view state,
|
|
191
|
+
// then restore them (target stays on top).
|
|
192
|
+
const siblings = [...views.values()].filter(v => v !== entry);
|
|
193
|
+
for (const v of siblings) {
|
|
194
|
+
try {
|
|
195
|
+
window?.contentView.removeChildView(v.webContentsView);
|
|
196
|
+
}
|
|
197
|
+
catch { /* already gone */ }
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const shot = await entry.webContentsView.webContents.debugger.sendCommand('Page.captureScreenshot', {});
|
|
201
|
+
const data = shot.data;
|
|
202
|
+
if (typeof data === 'string' && data.length > 0)
|
|
203
|
+
base64 = data;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
for (const v of siblings) {
|
|
207
|
+
try {
|
|
208
|
+
window?.contentView.addChildView(v.webContentsView);
|
|
209
|
+
}
|
|
210
|
+
catch { /* destroyed */ }
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
window?.contentView.removeChildView(entry.webContentsView);
|
|
214
|
+
window?.contentView.addChildView(entry.webContentsView);
|
|
215
|
+
}
|
|
216
|
+
catch { /* closing */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (base64 === '') {
|
|
220
|
+
throw new Error('capture produced no image (view not painted)');
|
|
221
|
+
}
|
|
222
|
+
reply(msg.id, { ok: true, result: { base64, width: 0, height: 0 } });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
124
225
|
case 'download': {
|
|
125
226
|
const viewId = msg.viewId;
|
|
126
227
|
const url = msg.url;
|
|
@@ -183,6 +183,8 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
183
183
|
readonly dataUrl: string;
|
|
184
184
|
readonly path?: string;
|
|
185
185
|
}>;
|
|
186
|
+
/** Build the data URL and optionally write the PNG to disk. */
|
|
187
|
+
private saveScreenshot;
|
|
186
188
|
/** Append one operation to the session's history. */
|
|
187
189
|
private record;
|
|
188
190
|
/** Return the session's chronological operation log (newest last). */
|
|
@@ -569,36 +569,53 @@ export class ElectronBrowserProvider {
|
|
|
569
569
|
}
|
|
570
570
|
/** Capture the current page, optionally full-page. PNG only (CDP JPEG hangs on Electron 43). */
|
|
571
571
|
async screenshot(session, request, signal) {
|
|
572
|
-
const
|
|
572
|
+
const s = this.session(session);
|
|
573
|
+
const { handle } = this.activeTab(s);
|
|
573
574
|
signal?.throwIfAborted();
|
|
575
|
+
// Native capturePage path (self-hosted): CDP Page.captureScreenshot can
|
|
576
|
+
// hang indefinitely on a view once another (hidden) WebContentsView exists
|
|
577
|
+
// in the window; capturePage is fast for visible views and resolves
|
|
578
|
+
// immediately (empty) for hidden ones.
|
|
579
|
+
const capturable = handle;
|
|
580
|
+
if (request?.fullPage !== true && typeof capturable.capture === 'function') {
|
|
581
|
+
// Ensure the target view is the visible one before capturing.
|
|
582
|
+
this.showActive(s);
|
|
583
|
+
const timeoutMs = 30_000;
|
|
584
|
+
const shot = await withTimeout(capturable.capture(), timeoutMs, signal, `browser: screenshot timed out after ${timeoutMs}ms`);
|
|
585
|
+
if (shot.base64 === '') {
|
|
586
|
+
throw new BrowserError('browser: capture returned an empty image (view not painted); retry shortly', 'BROWSER_SCREENSHOT_FAILED');
|
|
587
|
+
}
|
|
588
|
+
return this.saveScreenshot(shot.base64, request?.savePath);
|
|
589
|
+
}
|
|
590
|
+
// Fallback: a desktop-shell handle (no capture()) or a full-page capture
|
|
591
|
+
// uses CDP; full-page needs `captureBeyondViewport` which capturePage lacks.
|
|
574
592
|
const params = {};
|
|
575
593
|
if (request?.fullPage === true) {
|
|
576
594
|
// `captureBeyondViewport` captures the full scrollable content; without
|
|
577
595
|
// a clip this yields the full-page image (CDP default is the viewport).
|
|
578
596
|
params.captureBeyondViewport = true;
|
|
579
597
|
}
|
|
580
|
-
// Bound the capture: a view that has not painted can wedge
|
|
581
|
-
// Page.captureScreenshot indefinitely (observed after view re-showing).
|
|
582
598
|
const timeoutMs = 30_000;
|
|
583
599
|
const result = await withTimeout(handle.sendCommand(CDP_PAGE_CAPTURE_SCREENSHOT, params), timeoutMs, signal, `browser: screenshot timed out after ${timeoutMs}ms`);
|
|
584
600
|
const data = result.data;
|
|
585
601
|
if (typeof data !== 'string') {
|
|
586
602
|
throw new BrowserError('browser: screenshot returned no image data', 'BROWSER_SCREENSHOT_FAILED');
|
|
587
603
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
604
|
+
return this.saveScreenshot(data, request?.savePath);
|
|
605
|
+
}
|
|
606
|
+
/** Build the data URL and optionally write the PNG to disk. */
|
|
607
|
+
saveScreenshot(base64, savePath) {
|
|
608
|
+
if (savePath !== undefined) {
|
|
592
609
|
try {
|
|
593
|
-
writeFileSync(
|
|
594
|
-
return { dataUrl: `data:image/png;base64,${
|
|
610
|
+
writeFileSync(savePath, Buffer.from(base64, 'base64'));
|
|
611
|
+
return { dataUrl: `data:image/png;base64,${base64}`, path: savePath };
|
|
595
612
|
}
|
|
596
613
|
catch (error) {
|
|
597
614
|
// Report the write problem but keep the capture usable.
|
|
598
|
-
throw new BrowserError(`browser: screenshot save to "${
|
|
615
|
+
throw new BrowserError(`browser: screenshot save to "${savePath}" failed: ${String(error)}`, 'BROWSER_SCREENSHOT_SAVE_FAILED', { cause: error });
|
|
599
616
|
}
|
|
600
617
|
}
|
|
601
|
-
return { dataUrl: `data:image/png;base64,${
|
|
618
|
+
return { dataUrl: `data:image/png;base64,${base64}` };
|
|
602
619
|
}
|
|
603
620
|
/** Append one operation to the session's history. */
|
|
604
621
|
record(s, action, params, ok, detail) {
|