dsh-builtin-browser 0.4.6 → 0.4.8
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/CHANGELOG.md +76 -0
- package/README.en.md +62 -491
- package/README.md +44 -470
- package/client.js +10 -10
- package/cordis.patch.yml +4 -4
- package/desktop-bridge/plugin-browser-bridge.js +1 -1
- package/docs/README.md +8 -4
- package/docs/architecture.md +82 -11
- package/docs/development.md +38 -0
- package/docs/limitations.md +49 -0
- package/docs/tool-reference.md +42 -42
- package/docs/user-guide.md +93 -37
- package/docs/why-browser.md +3 -3
- package/lib/browser/types.d.ts +2 -2
- package/lib/browser-electron/bridge-connection.d.ts +7 -1
- package/lib/browser-electron/bridge-connection.js +58 -3
- package/lib/browser-electron/desktop-bridge-host.d.ts +11 -0
- package/lib/browser-electron/desktop-bridge-host.js +21 -1
- package/lib/browser-electron/entry.d.ts +2 -2
- package/lib/browser-electron/entry.js +186 -103
- package/lib/browser-electron/history-store.d.ts +1 -1
- package/lib/browser-electron/history-store.js +1 -1
- package/lib/browser-electron/host-main.js +3 -3
- package/lib/browser-electron/provider.d.ts +32 -8
- package/lib/browser-electron/provider.js +97 -16
- package/lib/browser-electron/remote-host.js +1 -1
- package/lib/browser-electron/settings-store.d.ts +14 -6
- package/lib/browser-electron/settings-store.js +29 -9
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/tool-browser/element-fields.d.ts +4 -4
- package/lib/tool-browser/element-fields.js +6 -6
- package/lib/tool-browser/index.d.ts +6 -6
- package/lib/tool-browser/index.js +196 -139
- package/package.json +2 -2
- package/scripts/verify-host-compat.mjs +6 -6
- package/src/browser/types.ts +2 -2
- package/src/browser-electron/bridge-connection.ts +61 -3
- package/src/browser-electron/desktop-bridge-host.ts +22 -1
- package/src/browser-electron/entry.ts +101 -9
- package/src/browser-electron/history-store.ts +1 -1
- package/src/browser-electron/host-main.ts +3 -3
- package/src/browser-electron/provider.ts +135 -31
- package/src/browser-electron/remote-host.ts +1 -1
- package/src/browser-electron/settings-store.ts +32 -13
- package/src/index.ts +1 -1
- package/src/tool-browser/element-fields.ts +6 -6
- package/src/tool-browser/index.ts +197 -140
package/docs/user-guide.md
CHANGED
|
@@ -24,9 +24,9 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
24
24
|
| `dsh-builtin-browser` | 包名本身 | **惰性根行**:不注册任何东西(空 `apply()`),只用来声明包名 —— 宿主的客户端插件扫描靠精确包名读到 `dsh.client`,少了这行设置栏不会出现 |
|
|
25
25
|
| `browser` | `dsh-builtin-browser/browser` | `ctx.browser` 能力 seam(始终挂载) |
|
|
26
26
|
| `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider |
|
|
27
|
-
| `tool-browser` | `dsh-builtin-browser/tool-browser` | `
|
|
27
|
+
| `tool-browser` | `dsh-builtin-browser/tool-browser` | `dsh_browser_*` 模型侧工具 |
|
|
28
28
|
|
|
29
|
-
> 没有桌面外壳时插件**自托管**:自己拉起一个标题为 `dsh-browser` 的 Electron 窗口,`
|
|
29
|
+
> 没有桌面外壳时插件**自托管**:自己拉起一个标题为 `dsh-browser` 的 Electron 窗口,`dsh_browser_*` 工具照常可用。
|
|
30
30
|
|
|
31
31
|
## 配置
|
|
32
32
|
|
|
@@ -36,20 +36,20 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
36
36
|
| `browser-electron` | `httpOnly` | 布尔 | `true` | 仅允许 HTTP(S) 导航;`file:`/`data:` 等拒绝 |
|
|
37
37
|
| `browser-electron` | `snapshotMaxElements` | 数字 | `60` | 快照最多收录的交互元素数 |
|
|
38
38
|
| `browser-electron` | `contentMaxChars` | 数字 | 未设置(按格式分档) | 内容抓取的字符上限。**不设**时按格式取分档默认(html 50 000、json 50 000、txt 20 000、markdown 20 000);**设了**就是操作者的值,覆盖分档默认。单次调用的 `maxChars` 优先级最高 |
|
|
39
|
-
| `browser-electron` | `downloadDir` | 字符串 | 系统下载目录(自动识别 `Downloads`/`下载`/`下載`,或 `XDG_DOWNLOAD_DIR`) | 限定 `
|
|
39
|
+
| `browser-electron` | `downloadDir` | 字符串 | 系统下载目录(自动识别 `Downloads`/`下载`/`下載`,或 `XDG_DOWNLOAD_DIR`) | 限定 `dsh_browser_download` 与 `dsh_browser_screenshot` 保存路径必须位于该目录内且不覆盖已有文件;默认收敛到系统下载目录,可改沙箱目录 |
|
|
40
40
|
| `tool-browser` | `timeoutMs` | 数字 | `60000` | 工具协作超时(ms) |
|
|
41
41
|
| `tool-browser` | `tabTools` | 布尔 | `true` | 是否注册标签管理工具 |
|
|
42
|
-
| `tool-browser` | `allowedActions` | 字符串数组 | 不限制 | **部署级**动作白名单,对该部署里的每个任务生效(单个任务仍可用 `
|
|
42
|
+
| `tool-browser` | `allowedActions` | 字符串数组 | 不限制 | **部署级**动作白名单,对该部署里的每个任务生效(单个任务仍可用 `dsh_browser_restrict` 再收窄);`READ_ONLY_TOOLS` 与 `dsh_browser_restrict` 无论列表怎么写都不拦。`cordis.patch.yml` 默认给的是空 `config: {}`(等于不限制) |
|
|
43
43
|
|
|
44
44
|
## 快速上手(给 agent 的提示词示例)
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
1.
|
|
48
|
-
2. 慢站点先
|
|
49
|
-
3. 需要填表时用
|
|
50
|
-
4. 需要截图确认时用
|
|
51
|
-
5. 需要滚动/回退/按键时用
|
|
52
|
-
6. 遇到验证码(
|
|
47
|
+
1. dsh_browser_open 打开 https://example.com
|
|
48
|
+
2. 慢站点先 dsh_browser_wait(url=…) 等页面就绪,再 dsh_browser_snapshot 查看可交互元素
|
|
49
|
+
3. 需要填表时用 dsh_browser_fill(按 name/label/placeholder 匹配,一次填多个字段)
|
|
50
|
+
4. 需要截图确认时用 dsh_browser_screenshot(可 savePath 存文件,大页面用 maxWidth 缩小)
|
|
51
|
+
5. 需要滚动/回退/按键时用 dsh_browser_scroll / dsh_browser_back / dsh_browser_forward / dsh_browser_key
|
|
52
|
+
6. 遇到验证码(dsh_browser_challenge 或快照标注 CHALLENGE)时,停下请用户处理
|
|
53
53
|
7. 每次操作后告知用户你在页面上做了什么
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -64,58 +64,58 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
64
64
|
|
|
65
65
|
**定位:先用语义,再考虑坐标**
|
|
66
66
|
|
|
67
|
-
- **`
|
|
67
|
+
- **`dsh_browser_a11y` 是理解页面的首选**,它给出每个交互节点的**角色**(button/textbox/…)与**可访问名称**,比读 DOM 更接近"这页面上有什么";`dsh_browser_snapshot` 给出可点击元素的**引用与坐标**。
|
|
68
68
|
- **两者都带 `selector`**:无 id 且无 name 时是空串。**有 `selector` 就优先用它**,它比坐标稳定 —— 页面滚动、布局变化都不会让它失效,而坐标会。
|
|
69
69
|
- **优先用 DOM 语义而非坐标**:表单提交优先 `form.requestSubmit()`;点击优先 `element.click()`;坐标点击是最后手段。
|
|
70
|
-
- **`
|
|
71
|
-
- **选中正确的元素**:页面常有隐藏副本(如移动端按钮),用 `
|
|
70
|
+
- **`dsh_browser_click` 的 `target`(css/text)本质上就是语义定位**,能用它就别自己算坐标 —— 它会把元素滚进视野并在中心点击,坐标抖动、像素比、iframe 偏移都由它处理。
|
|
71
|
+
- **选中正确的元素**:页面常有隐藏副本(如移动端按钮),用 `dsh_browser_execute` 过滤可见元素(`getBoundingClientRect()` 宽高 > 0、`getComputedStyle` 非 `display:none`),再取坐标。
|
|
72
72
|
- **取坐标后立即点击**:中间不要插入其他操作(填表、滚动会移动元素,旧坐标立即失效)。
|
|
73
73
|
- **点击前验证命中**:`document.elementFromPoint(x, y)` 确认该坐标确实是目标元素,再执行真实点击。
|
|
74
74
|
- **DPR 注意**:CDP 输入使用 CSS 像素;高 DPI 屏上若点击落空,用 `elementFromPoint` 校准,不要盲试坐标。
|
|
75
75
|
|
|
76
76
|
**截图与内容:先问"我要的是判断还是像素"**
|
|
77
77
|
|
|
78
|
-
- **能用文本就用文本。** `
|
|
79
|
-
- **`
|
|
80
|
-
- **`
|
|
78
|
+
- **能用文本就用文本。** `dsh_browser_snapshot` / `dsh_browser_a11y` / `dsh_browser_content` 给出同一页面的文本表示;**没有图像输入能力的模型看截图什么也得不到**,只会花钱。截图留给布局、图表、设计核对这类**必须看像素**的事。
|
|
79
|
+
- **`dsh_browser_screenshot` 带 `maxWidth`/`maxHeight` 时,结果里会返回 `width`/`height`**(实际像素尺寸)。需要控制成本时用它确认降采样生效了,而不是自己解码 data URL。
|
|
80
|
+
- **`dsh_browser_content` 的 `maxChars` 是按格式分档的**(html/json 默认 5 万,其余 2 万),单次传 `maxChars` 优先于设置里的 `contentMaxChars`。
|
|
81
81
|
|
|
82
82
|
**动作开关:三个门是操作者(人)的,不是你的**
|
|
83
83
|
|
|
84
|
-
设置页里的**执行页面脚本**、**下载文件到磁盘**、**写入登录态** 由人控制,**任何 `
|
|
84
|
+
设置页里的**执行页面脚本**、**下载文件到磁盘**、**写入登录态** 由人控制,**任何 `dsh_browser_*` 工具都改不了它们**。关掉时对应调用会明确报错,一眼能认出:
|
|
85
85
|
|
|
86
86
|
| 谁被关 | 报的错误码 | 哪些工具会遇到 |
|
|
87
87
|
| --- | --- | --- |
|
|
88
|
-
| 执行页面脚本 | `BROWSER_EXECUTE_DISABLED` | `
|
|
89
|
-
| 下载文件到磁盘 | `BROWSER_DOWNLOAD_DISABLED` | `
|
|
90
|
-
| 写入登录态 | `BROWSER_AUTH_WRITE_DISABLED` | `
|
|
91
|
-
| 读取 cookie | `BROWSER_AUTH_DISABLED` | `
|
|
88
|
+
| 执行页面脚本 | `BROWSER_EXECUTE_DISABLED` | `dsh_browser_execute`、以及需要页内脚本的工具 |
|
|
89
|
+
| 下载文件到磁盘 | `BROWSER_DOWNLOAD_DISABLED` | `dsh_browser_download` |
|
|
90
|
+
| 写入登录态 | `BROWSER_AUTH_WRITE_DISABLED` | `dsh_browser_auth` 的 `restore` |
|
|
91
|
+
| 读取 cookie | `BROWSER_AUTH_DISABLED` | `dsh_browser_auth` 的 `flush` |
|
|
92
92
|
|
|
93
93
|
**看到这些码不要重试、不要绕路** —— **它是人的决定,不是可以克服的故障**,直接告诉人"需要他在设置里打开"。
|
|
94
94
|
|
|
95
95
|
另外几个容易认错来源的码:`BROWSER_DOWNLOAD_BLOCKED` / `BROWSER_SCREENSHOT_BLOCKED` 是**保存路径被沙箱拒绝**(不是开关问题,换路径而不是找开关);`BROWSER_DOWNLOAD_UNSUPPORTED` 是**该载体没有这个能力**(比如非自托管浏览器)。
|
|
96
96
|
|
|
97
|
-
`
|
|
97
|
+
`dsh_browser_restrict` **不一样** —— 那是**你自己**设的临时白名单,你有权设也有权解除;它不该被用来绕过上面三个门。
|
|
98
98
|
|
|
99
99
|
**失败怎么读**
|
|
100
100
|
|
|
101
101
|
- **`CDP error: ...` 是浏览器答了但拒绝**(比如元素已经不在)—— 多半是页面变了,重新快照再试,不要盲目重复同一动作。
|
|
102
102
|
- **超时 / 连接类错误**才是浏览器可能有问题;这类错误会触发一次自动探测,通常下一条命令就能恢复。
|
|
103
|
-
- **`BROWSER_SESSION_UNKNOWN`** 说明会话没了(浏览器被替换过);`
|
|
104
|
-
- **同一个动作连续失败两次就换策略**:换 `selector`、换语义定位、或者用 `
|
|
103
|
+
- **`BROWSER_SESSION_UNKNOWN`** 说明会话没了(浏览器被替换过);`dsh_browser_reset_session` 重建本任务的会话。
|
|
104
|
+
- **同一个动作连续失败两次就换策略**:换 `selector`、换语义定位、或者用 `dsh_browser_snapshot` 重新看页面 —— **不要第三次用同样的坐标**。
|
|
105
105
|
|
|
106
|
-
**`
|
|
106
|
+
**`dsh_browser_history` 与动作集**
|
|
107
107
|
|
|
108
|
-
- `
|
|
109
|
-
- **可重放的动作有六种**:`navigate` / `click` / `type` / `scroll` / `key` / `execute`。不是每种历史记录都能 `
|
|
108
|
+
- `dsh_browser_history` **默认只给 20 条**,要更多自己传 `limit`。
|
|
109
|
+
- **可重放的动作有六种**:`navigate` / `click` / `type` / `scroll` / `key` / `execute`。不是每种历史记录都能 `dsh_browser_replay`,遇到不可重放的会明确报错。
|
|
110
110
|
|
|
111
111
|
## 多任务并行
|
|
112
112
|
|
|
113
113
|
每个 DSH 会话(任务)拥有独立的浏览器会话(独立标签页与历史),并发任务互不干扰:
|
|
114
114
|
|
|
115
|
-
- `
|
|
116
|
-
- `
|
|
115
|
+
- `dsh_browser_session` 查看本任务的会话与标签;
|
|
116
|
+
- `dsh_browser_reset_session` 关闭并重建本任务的会话(崩溃或卡死后用它恢复)。
|
|
117
117
|
|
|
118
|
-
登录态(cookie)为所有任务共享;可用 `
|
|
118
|
+
登录态(cookie)为所有任务共享;可用 `dsh_browser_auth` 导出/恢复,重启后不丢。注意 **`flush` 只导出当前页所属站点的 cookie**(按本会话所在页面的 URL 收敛),页面 URL 未知时**直接拒绝**而不是扩大范围去读整台机器的 cookie 罐;`restore` 会向任意域写 cookie,所以 `dsh_browser_auth` **同样受 `dsh_browser_restrict` 白名单约束**,不属于只读豁免。
|
|
119
119
|
|
|
120
120
|
## FAQ
|
|
121
121
|
|
|
@@ -131,7 +131,7 @@ DSH Desktop 上②命中即可用(0.1.18+ 插件自带 electron 包;44+ 二进
|
|
|
131
131
|
确保 Electron ≥ 40(33.x 有合成器缺陷)。自托管截图优先走原生 `capturePage`,多视图/窗口未激活时自动兜底到 CDP。
|
|
132
132
|
|
|
133
133
|
**Q:浏览器窗口不见了?**
|
|
134
|
-
窗口标题为 `dsh-browser`(自托管)。若子进程崩溃(或宿主 DSH 重启)会自动重启;崩溃前已打开的会话在**下一次调用时自动重建**——仅页面状态丢失,无需手动 `
|
|
134
|
+
窗口标题为 `dsh-browser`(自托管)。若子进程崩溃(或宿主 DSH 重启)会自动重启;崩溃前已打开的会话在**下一次调用时自动重建**——仅页面状态丢失,无需手动 `dsh_browser_reset_session`。`dsh_browser_reset_session` 仍可用于主动重置。
|
|
135
135
|
|
|
136
136
|
**Q:设置里的 `contentMaxChars` 改了没反应?**
|
|
137
137
|
先确认改的是哪一层。优先级从高到低是:**单次调用的 `maxChars`** > **配置里的 `contentMaxChars`** > **按格式的分档默认**(html/json 50 000,txt/markdown 20 000)。`contentMaxChars` 的作用就是给这个部署换一个默认值:不设时才用分档,设了就盖过分档。若改了没反应,通常是那一次调用自己传了 `maxChars`。
|
|
@@ -140,18 +140,74 @@ DSH Desktop 上②命中即可用(0.1.18+ 插件自带 electron 包;44+ 二进
|
|
|
140
140
|
能。该载体观察进程退出并清掉**已失效的会话映射**,下一次调用会**重新拉起**浏览器并继续工作;重新拉起前会先 kill 旧进程并**最多等 3 秒**它真正退出,且**只认本次启动写下的 `DevToolsActivePort`**(被杀掉的浏览器没机会清理那个文件,照读会连到别的 Chromium 或 `node --inspect` 上)。反过来,页面只是**慢**不会让插件杀掉你的窗口:命令超时只把连接标记为存疑,下一次调用先用 `Browser.getVersion` 探一次再决定是否丢弃,只有 socket 已关闭才重启。
|
|
141
141
|
|
|
142
142
|
**Q:下载报 CORS 错误?**
|
|
143
|
-
`
|
|
143
|
+
`dsh_browser_download` 在页面上下文内 `fetch`,受同源/CORS 约束;跨域文件请先在同源页面内操作,或直接请求用户提供。仅支持 HTTP(S) URL;`savePath` 必须为绝对路径且位于 `downloadDir` 内(默认系统下载目录,自动识别 `Downloads`/`下载`/`下載`),不覆盖已有文件;`dsh_browser_screenshot` 的 `savePath` 受同一套限制。
|
|
144
144
|
|
|
145
145
|
**Q:如何禁止 agent 乱点?**
|
|
146
|
-
`
|
|
146
|
+
`dsh_browser_restrict` 设置白名单(如只允许 `dsh_browser_snapshot`/`dsh_browser_content`);传空列表解除。注意它是防误操作的**软护栏**,模型可自行解除,不是安全边界。
|
|
147
147
|
|
|
148
|
-
要一个**模型自己解不掉**的限制,用设置页「Agent 能做什么」的三个开关(执行页面脚本 / 下载文件到磁盘 / 写入登录状态):它们写在设置文档里,而没有任何工具能写那个文档,所以模型只能被拒 —— 这与 `
|
|
148
|
+
要一个**模型自己解不掉**的限制,用设置页「Agent 能做什么」的三个开关(执行页面脚本 / 下载文件到磁盘 / 写入登录状态):它们写在设置文档里,而没有任何工具能写那个文档,所以模型只能被拒 —— 这与 `dsh_browser_restrict` 的区别不是"谁更强",而是"谁能改"。部署级(对所有人生效)还可以用 `tool-browser` 的 `allowedActions` 配置,见 `cordis.patch.yml` 的注释。
|
|
149
149
|
|
|
150
150
|
## 故障排查
|
|
151
151
|
|
|
152
152
|
| 现象 | 可能原因 | 处理 |
|
|
153
153
|
| --- | --- | --- |
|
|
154
|
-
| `BROWSER_SESSION_UNKNOWN` | 会话已被关闭/重建(如 `
|
|
155
|
-
| 工具超时 | 页面卡死/未渲染完成 | 稍后重试;`
|
|
154
|
+
| `BROWSER_SESSION_UNKNOWN` | 会话已被关闭/重建(如 `dsh_browser_reset_session` 后仍引用旧会话) | `dsh_browser_session` 查看现状;或 `dsh_browser_reset_session` 重建 |
|
|
155
|
+
| 工具超时 | 页面卡死/未渲染完成 | 稍后重试;`dsh_browser_reset` 重置标签 |
|
|
156
156
|
| 导航被拒 | 非 HTTP(S) 协议 | 检查 URL;`httpOnly` 配置 |
|
|
157
|
-
| 快照为空 | 页面尚未加载 | 等待后重试 `
|
|
157
|
+
| 快照为空 | 页面尚未加载 | 等待后重试 `dsh_browser_snapshot` |
|
|
158
|
+
|
|
159
|
+
## 验证过的版本
|
|
160
|
+
|
|
161
|
+
| 组件 | 版本 |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| DeepSeek Harness(dsh) | `0.2.1-alpha.1`(peer 声明 `>=0.1.1-rc.2 <0.3.0`;更早的 `0.2.0-rc.2`、`0.1.x` 亦有实测记录) |
|
|
164
|
+
| Electron | `44.0.0`(推荐 ≥ 40;33.x 存在合成器缺陷) |
|
|
165
|
+
| Node.js | `22.20.0` |
|
|
166
|
+
| 本机 Chrome / Edge(可选载体) | `154.0.8037.58` / `154.0.4258.37` |
|
|
167
|
+
| 操作系统 | Windows 10 (10.0.26200) |
|
|
168
|
+
|
|
169
|
+
> 插件声明 `electron >= 30`。**核心链路在 Windows 上完整实测**;系统浏览器的查找已适配 Linux 与 macOS(先查 `PATH`,再查各平台的惯例安装位置,均可用 `DSH_BROWSER_CHROME_PATH` / `DSH_BROWSER_EDGE_PATH` 覆盖),但这两个平台上的**端到端链路尚未实测**,暂不承诺。
|
|
170
|
+
|
|
171
|
+
> **宿主兼容性可以随时复验**:`node scripts/verify-host-compat.mjs` 会用某个 profile 里**真实安装**的 `@deepseek-ai/*` 包组装一个宿主,加载本插件的编译产物并真调两个工具。详见[开发](development.md)。
|
|
172
|
+
|
|
173
|
+
## 更新方式(两端不同)
|
|
174
|
+
|
|
175
|
+
插件在两种宿主里各有一份安装,更新路径也不同 —— **更新其中一端不会连带更新另一端**。
|
|
176
|
+
|
|
177
|
+
**桌面端(DSH Desktop)**
|
|
178
|
+
|
|
179
|
+
- 插件是桌面端 profile 里的依赖(profile 目录通常是 `$DSH_HOME/profiles/desktop`)。更新它 = 把该 profile 里的依赖更新到新版本,然后**重启 DSH Desktop**,客户端设置栏与工具才会换成新代码。
|
|
180
|
+
- **桌面端还多一步,而 Web 端没有**:让插件驱动侧栏的那条 bridge 装在**桌面端自己的安装目录**里(`resources/app/`),插件更新**不会**带上它。桌面端升级会替换该目录、bridge 随之消失,所以请重跑一次:
|
|
181
|
+
```bash
|
|
182
|
+
node desktop-bridge/install.mjs # 幂等;已装则只刷新模块
|
|
183
|
+
node desktop-bridge/install.mjs --revert # 回滚
|
|
184
|
+
```
|
|
185
|
+
bridge 不在时插件自动退回自托管(多出一个独立窗口),功能不中断。
|
|
186
|
+
- 浏览器内核默认由桌面端自带的 Electron 提供,插件不会再下载一份 Electron;也可以在设置里改用它自己装的 Chrome / Edge。
|
|
187
|
+
- **升级桌面端本身不会带上插件**,按上面的方法单独更新它。
|
|
188
|
+
|
|
189
|
+
**Web 端(`dsh web`)**
|
|
190
|
+
|
|
191
|
+
- 插件是 web profile 里的依赖(`$DSH_HOME/profiles/web`),更新后**重启 `dsh web`** 生效。
|
|
192
|
+
- Web 端**没有侧栏、也没有 bridge**:共享浏览器由插件自托管拉起。首次安装可能需要 Electron 二进制;若包管理器的构建白名单拦下了它(pnpm v10+ 会拦 `electron` 的 postinstall),执行一次 `npx install-electron` 补上即可。
|
|
193
|
+
- 同样可以在设置里改用本机 Chrome / Edge —— 这是两端**行为一致**的选项。
|
|
194
|
+
- 更新方式与首次安装它时一致(按 npm 包名 `dsh-builtin-browser`、按 GitHub 仓库 `wqty123/dsh-browser`,或本地目录)。
|
|
195
|
+
|
|
196
|
+
**两端一致的体验**
|
|
197
|
+
|
|
198
|
+
- 工具集、设置页里的「浏览器」栏、浏览历史与 cookie 的持久化行为完全相同;差别只在页面由谁承载(桌面端=官方侧栏,Web 端=插件自托管窗口,或你在设置里指定的本机浏览器)。
|
|
199
|
+
- 升级不会丢数据:浏览历史与设置都在 `$DSH_HOME/dsh-builtin-browser-host/`(`history.jsonl`、`settings.json`),登录状态在同一 profile 目录里 —— 包括使用本机浏览器时的 `<chrome|edge>-profile`。
|
|
200
|
+
- 升级后如果历史记录不符合预期,先去「设置 → 浏览器」确认这些开关:历史默认**开**、侧栏自动展开默认**开**、会话结束时自动关闭浏览器默认**关**、载体默认**内置**。
|
|
201
|
+
|
|
202
|
+
## 从 0.4.7 升级到 0.4.8
|
|
203
|
+
|
|
204
|
+
**这是一个改名版本:34 个工具从 `browser_*` 移入 `dsh_browser_*`,功能一字未改。**
|
|
205
|
+
|
|
206
|
+
| | |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| **只装这一个浏览器插件** | **不必更新。** 旧版继续正常工作。 |
|
|
209
|
+
| **还装了别的浏览器插件**(尤其腾讯 BrowserSkill) | **建议更新**,否则对方那套工具注册不上。 |
|
|
210
|
+
|
|
211
|
+
改名原因见 [README 的工具名空间一节](../README.md#工具名空间)。更新后 `browser_open` → `dsh_browser_open`(其余同理),并**重启 DSH** 使其生效。
|
|
212
|
+
|
|
213
|
+
> **0.4.8 起,一个名字被占不再毁掉整套工具。** 此前 34 个工具是顺序直接注册的,一个重名抛错会中断余下的注册,表现成"插件装了但没反应"。现在每个工具独立注册,碰撞只产生一条点名报错的日志(`could not register dsh_browser_xxx`),其余工具照常可用。
|
package/docs/why-browser.md
CHANGED
|
@@ -26,13 +26,13 @@ LLM agent 需要"上网"。传统做法是给 agent 一个**无头浏览器**(Pl
|
|
|
26
26
|
| 真实页面渲染 | ✅ | 部分 | ✅ |
|
|
27
27
|
| 复杂交互(填表/点击/拖拽) | ✅ | ❌ | ✅ |
|
|
28
28
|
| 用户可见、可接管 | ❌ | ❌ | ✅ |
|
|
29
|
-
| 保留登录态 | 需手动管理 | ❌ | ✅(`
|
|
29
|
+
| 保留登录态 | 需手动管理 | ❌ | ✅(`dsh_browser_auth` + cookie 落盘) |
|
|
30
30
|
| 人工处理验证码 | ❌ 卡死 | ❌ | ✅ 请用户点一下即可 |
|
|
31
31
|
| 多任务并行 | 需多实例 | – | ✅ 任务级会话隔离 |
|
|
32
32
|
|
|
33
33
|
## 设计取舍
|
|
34
34
|
|
|
35
|
-
- **登录态共享**:所有任务共享同一份 cookie(符合"用户登录一次,agent 到处可用");需要搬运时用 `
|
|
35
|
+
- **登录态共享**:所有任务共享同一份 cookie(符合"用户登录一次,agent 到处可用");需要搬运时用 `dsh_browser_auth` 手动导出/恢复 —— 它的 `flush` **只导出当前页所属站点的 cookie**(按本会话所在页面的 URL 收敛,页面 URL 未知时拒绝而不是放宽),所以"导出"是逐站点的一步,不是一次性打包整个 cookie 罐。
|
|
36
36
|
- **任务级会话隔离**:每个 DSH 会话(任务)拥有独立的浏览器会话(标签页与历史),并发任务互不抢页面;自托管载体下**每个会话连窗口都各自一份**(窗口按会话 id 分组,窗口标题带任务标识),侧栏与本机浏览器载体则是同一个宿主里切换当前可见视图。
|
|
37
37
|
- **装好即用**:不依赖桌面外壳。有外壳时嵌入外壳视图;纯 `dsh web` 时插件自托管——自己拉起 Electron 窗口,通过本机 TCP JSON-RPC 驱动。
|
|
38
38
|
- **边界清晰**:可见视图、浏览器列布局属于宿主外壳;插件只负责 seam、provider 与工具,不含任何 UI。
|
|
@@ -41,5 +41,5 @@ LLM agent 需要"上网"。传统做法是给 agent 一个**无头浏览器**(Pl
|
|
|
41
41
|
## 什么场景不适合
|
|
42
42
|
|
|
43
43
|
- 需要**无头批量抓取**(成千上万页面):请用专门的抓取工具/服务,不必开窗口。
|
|
44
|
-
- 需要**每个任务完全独立的登录态**(互不可见):本插件默认共享 cookie;如需强隔离,可用多个 DSH 实例或 `
|
|
44
|
+
- 需要**每个任务完全独立的登录态**(互不可见):本插件默认共享 cookie;如需强隔离,可用多个 DSH 实例或 `dsh_browser_auth` 手动管理。
|
|
45
45
|
- 需要**浏览器列 UI / 布局管理**:那是宿主外壳的配套,本插件不包含。
|
package/lib/browser/types.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface BrowserScrollRequest {
|
|
|
58
58
|
}
|
|
59
59
|
/** Press one named key (Enter, Tab, arrows, …). */
|
|
60
60
|
export interface BrowserKeyRequest {
|
|
61
|
-
/** Key name from the supported set (see the
|
|
61
|
+
/** Key name from the supported set (see the dsh_browser_key tool). */
|
|
62
62
|
readonly key: string;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
@@ -333,7 +333,7 @@ export interface BrowserSnapshotElement {
|
|
|
333
333
|
/**
|
|
334
334
|
* True when the element lives inside an (same-origin) iframe: coordinates
|
|
335
335
|
* are still top-document, but DOM selectors are frame-scoped — reach the
|
|
336
|
-
* element via `iframe.contentDocument` in
|
|
336
|
+
* element via `iframe.contentDocument` in dsh_browser_execute.
|
|
337
337
|
*/
|
|
338
338
|
readonly frame?: boolean;
|
|
339
339
|
}
|
|
@@ -23,6 +23,7 @@ export interface BridgeEndpoint {
|
|
|
23
23
|
export declare class BridgeConnection {
|
|
24
24
|
private endpoint;
|
|
25
25
|
private readonly endpointPath?;
|
|
26
|
+
private readonly onLost?;
|
|
26
27
|
private socket;
|
|
27
28
|
private buffer;
|
|
28
29
|
/** Outstanding requests by id. */
|
|
@@ -34,11 +35,14 @@ export declare class BridgeConnection {
|
|
|
34
35
|
private idLessBridge;
|
|
35
36
|
/** True once this connection may only have one request in flight at a time. */
|
|
36
37
|
private serialisable;
|
|
38
|
+
/** True once the shell behind this connection has been reported gone. */
|
|
39
|
+
private lostNotified;
|
|
37
40
|
/**
|
|
38
41
|
* @param endpoint - the shell's published bridge endpoint.
|
|
39
42
|
* @param endpointPath - where that endpoint is republished, so a moved port can be followed.
|
|
43
|
+
* @param onLost - called once when this connection concludes the shell behind it is gone.
|
|
40
44
|
*/
|
|
41
|
-
constructor(endpoint: BridgeEndpoint, endpointPath?: string | undefined);
|
|
45
|
+
constructor(endpoint: BridgeEndpoint, endpointPath?: string | undefined, onLost?: (() => void) | undefined);
|
|
42
46
|
/**
|
|
43
47
|
* Re-read the published endpoint, and adopt it if the shell has moved.
|
|
44
48
|
*
|
|
@@ -53,6 +57,8 @@ export declare class BridgeConnection {
|
|
|
53
57
|
* @returns true when a different endpoint was adopted.
|
|
54
58
|
*/
|
|
55
59
|
private refreshEndpoint;
|
|
60
|
+
/** Report once that the shell is gone, so the surface can fall back instead of dialling it. */
|
|
61
|
+
private notifyLost;
|
|
56
62
|
/**
|
|
57
63
|
* Send one request, reusing the socket if it is still healthy.
|
|
58
64
|
*
|
|
@@ -33,6 +33,15 @@ const MAX_BUFFER_BYTES = 128 * 1024 * 1024;
|
|
|
33
33
|
* client then falls back to arrival order, which is what the protocol guaranteed before.
|
|
34
34
|
*/
|
|
35
35
|
const REQUEST_ID_FIELD = 'bridgeRequestId';
|
|
36
|
+
/**
|
|
37
|
+
* How long the published endpoint may go un-refreshed before its shell is presumed gone.
|
|
38
|
+
*
|
|
39
|
+
* A live shell rewrites the file every fifteen seconds (its keep-alive), so four missed refreshes
|
|
40
|
+
* is not a slow shell — it is a shell that exited and left its last write behind. The distinction
|
|
41
|
+
* matters because the file is the ONLY thing that says where the bridge is: a tombstone names a
|
|
42
|
+
* port nobody owns, and a client that trusts it dials that port for the rest of its life.
|
|
43
|
+
*/
|
|
44
|
+
const ENDPOINT_TTL_MS = 60_000;
|
|
36
45
|
/**
|
|
37
46
|
* A reusable connection to the bridge.
|
|
38
47
|
*
|
|
@@ -51,6 +60,7 @@ const REQUEST_ID_FIELD = 'bridgeRequestId';
|
|
|
51
60
|
export class BridgeConnection {
|
|
52
61
|
endpoint;
|
|
53
62
|
endpointPath;
|
|
63
|
+
onLost;
|
|
54
64
|
socket;
|
|
55
65
|
buffer = '';
|
|
56
66
|
/** Outstanding requests by id. */
|
|
@@ -62,13 +72,17 @@ export class BridgeConnection {
|
|
|
62
72
|
idLessBridge = false;
|
|
63
73
|
/** True once this connection may only have one request in flight at a time. */
|
|
64
74
|
serialisable = false;
|
|
75
|
+
/** True once the shell behind this connection has been reported gone. */
|
|
76
|
+
lostNotified = false;
|
|
65
77
|
/**
|
|
66
78
|
* @param endpoint - the shell's published bridge endpoint.
|
|
67
79
|
* @param endpointPath - where that endpoint is republished, so a moved port can be followed.
|
|
80
|
+
* @param onLost - called once when this connection concludes the shell behind it is gone.
|
|
68
81
|
*/
|
|
69
|
-
constructor(endpoint, endpointPath) {
|
|
82
|
+
constructor(endpoint, endpointPath, onLost) {
|
|
70
83
|
this.endpoint = endpoint;
|
|
71
84
|
this.endpointPath = endpointPath;
|
|
85
|
+
this.onLost = onLost;
|
|
72
86
|
}
|
|
73
87
|
/**
|
|
74
88
|
* Re-read the published endpoint, and adopt it if the shell has moved.
|
|
@@ -90,6 +104,13 @@ export class BridgeConnection {
|
|
|
90
104
|
const raw = JSON.parse(readFileSync(this.endpointPath, 'utf8'));
|
|
91
105
|
if (typeof raw.port !== 'number' || typeof raw.token !== 'string')
|
|
92
106
|
return false;
|
|
107
|
+
// A shell that exited stops refreshing the file, and what it left names a port nobody owns.
|
|
108
|
+
// Trusting it is how a host ends up dialling a dead port for the rest of the process's life
|
|
109
|
+
// — reported as "the browser stopped working and only restarting DSH brought it back".
|
|
110
|
+
if (endpointExpired(raw.updatedAt)) {
|
|
111
|
+
this.notifyLost();
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
93
114
|
if (raw.port === this.endpoint.port && raw.token === this.endpoint.token)
|
|
94
115
|
return false;
|
|
95
116
|
this.endpoint = {
|
|
@@ -105,6 +126,18 @@ export class BridgeConnection {
|
|
|
105
126
|
return false;
|
|
106
127
|
}
|
|
107
128
|
}
|
|
129
|
+
/** Report once that the shell is gone, so the surface can fall back instead of dialling it. */
|
|
130
|
+
notifyLost() {
|
|
131
|
+
if (this.lostNotified)
|
|
132
|
+
return;
|
|
133
|
+
this.lostNotified = true;
|
|
134
|
+
try {
|
|
135
|
+
this.onLost?.();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// A listener must never break the transport: the caller's error is the one that matters.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
108
141
|
/**
|
|
109
142
|
* Send one request, reusing the socket if it is still healthy.
|
|
110
143
|
*
|
|
@@ -126,6 +159,14 @@ export class BridgeConnection {
|
|
|
126
159
|
// when the condition could not clear, spun until the process ran out of heap.
|
|
127
160
|
// Follow the shell if its bridge moved to a new port (see refreshEndpoint).
|
|
128
161
|
this.refreshEndpoint();
|
|
162
|
+
// The shell behind this connection is gone, and the endpoint file says so. Refusing here is
|
|
163
|
+
// what turns a permanent ECONNREFUSED into one named error the surface can act on — by
|
|
164
|
+
// falling back to the browser it hosts itself, instead of dialling a dead port until DSH is
|
|
165
|
+
// restarted.
|
|
166
|
+
if (this.lostNotified) {
|
|
167
|
+
throw new Error('dsh-builtin-browser: the desktop shell that backed this browser has exited '
|
|
168
|
+
+ '(its published endpoint stopped being refreshed)');
|
|
169
|
+
}
|
|
129
170
|
let socket = this.ensureSocket();
|
|
130
171
|
if (this.serialisable && this.queue.length > 0) {
|
|
131
172
|
this.reset(new Error('dsh-builtin-browser: bridge connection reopened so replies stay attributable'));
|
|
@@ -281,8 +322,22 @@ export class BridgeConnection {
|
|
|
281
322
|
entry.reject(error);
|
|
282
323
|
}
|
|
283
324
|
}
|
|
284
|
-
/**
|
|
285
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Whether a published endpoint has gone un-refreshed long enough to be a tombstone.
|
|
327
|
+
*
|
|
328
|
+
* A missing or unparseable stamp is NOT expired: an older bridge that never wrote one must keep
|
|
329
|
+
* working, and guessing "stale" from absence would break every shell that predates the field.
|
|
330
|
+
* @param updatedAt - the file's own refresh stamp, when it carries one.
|
|
331
|
+
*/
|
|
332
|
+
function endpointExpired(updatedAt) {
|
|
333
|
+
if (typeof updatedAt !== 'string')
|
|
334
|
+
return false;
|
|
335
|
+
const at = Date.parse(updatedAt);
|
|
336
|
+
if (!Number.isFinite(at))
|
|
337
|
+
return false;
|
|
338
|
+
return Date.now() - at > ENDPOINT_TTL_MS;
|
|
339
|
+
}
|
|
340
|
+
/** The request id a raw answer line echoes back, when it carries one. */ function echoOf(line) {
|
|
286
341
|
try {
|
|
287
342
|
const answer = JSON.parse(line);
|
|
288
343
|
const value = answer[REQUEST_ID_FIELD];
|
|
@@ -94,10 +94,21 @@ export declare class DesktopBridgeViewHost implements ElectronBrowserViewHost {
|
|
|
94
94
|
* the desktop sidebar is the carrier — by design, not by omission.
|
|
95
95
|
*/
|
|
96
96
|
private userActionHandler;
|
|
97
|
+
/** Told when the shell behind this bridge exits; see `onLost`. */
|
|
98
|
+
private lostListener;
|
|
97
99
|
/**
|
|
98
100
|
* @param endpoint - the shell's published bridge endpoint.
|
|
99
101
|
*/
|
|
100
102
|
private constructor();
|
|
103
|
+
/**
|
|
104
|
+
* Called by the transport once the shell behind it is gone.
|
|
105
|
+
*
|
|
106
|
+
* Forwarded rather than acted on here: only the entry point owns the swap back to the self-hosted
|
|
107
|
+
* browser, and a host that tried to replace itself would be reaching outside its own contract.
|
|
108
|
+
*/
|
|
109
|
+
private reportLost;
|
|
110
|
+
/** Register the "this carrier is gone" listener (see the interface's `onLost`). */
|
|
111
|
+
onLost(listener: () => void): void;
|
|
101
112
|
/**
|
|
102
113
|
* Find a usable desktop bridge, if this surface has one.
|
|
103
114
|
*
|
|
@@ -111,12 +111,32 @@ export class DesktopBridgeViewHost {
|
|
|
111
111
|
* the desktop sidebar is the carrier — by design, not by omission.
|
|
112
112
|
*/
|
|
113
113
|
userActionHandler;
|
|
114
|
+
/** Told when the shell behind this bridge exits; see `onLost`. */
|
|
115
|
+
lostListener;
|
|
114
116
|
/**
|
|
115
117
|
* @param endpoint - the shell's published bridge endpoint.
|
|
116
118
|
*/
|
|
117
119
|
constructor(endpoint, sessionId) {
|
|
118
120
|
this.owner = sessionId !== undefined && sessionId !== String.fromCharCode(39, 39) ? sessionId : randomUUID();
|
|
119
|
-
this.connection = new BridgeConnection(endpoint, bridgeEndpointPath());
|
|
121
|
+
this.connection = new BridgeConnection(endpoint, bridgeEndpointPath(), () => this.reportLost());
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Called by the transport once the shell behind it is gone.
|
|
125
|
+
*
|
|
126
|
+
* Forwarded rather than acted on here: only the entry point owns the swap back to the self-hosted
|
|
127
|
+
* browser, and a host that tried to replace itself would be reaching outside its own contract.
|
|
128
|
+
*/
|
|
129
|
+
reportLost() {
|
|
130
|
+
try {
|
|
131
|
+
this.lostListener?.();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// The listener's failure is not the transport's problem.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Register the "this carrier is gone" listener (see the interface's `onLost`). */
|
|
138
|
+
onLost(listener) {
|
|
139
|
+
this.lostListener = listener;
|
|
120
140
|
}
|
|
121
141
|
/**
|
|
122
142
|
* Find a usable desktop bridge, if this surface has one.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `ctx.electronViewHost`, that host is used (embedded, human-machine shared
|
|
6
6
|
* view). Otherwise the plugin self-hosts: it spawns its own Electron child
|
|
7
7
|
* (`host-main.js`) and drives it over a local TCP JSON-RPC socket, so
|
|
8
|
-
* installing the plugin is enough for `
|
|
8
|
+
* installing the plugin is enough for `dsh_browser_*` tools to work on any
|
|
9
9
|
* surface.
|
|
10
10
|
* @module dsh-browser/browser-electron
|
|
11
11
|
*/
|
|
@@ -27,7 +27,7 @@ export interface Config {
|
|
|
27
27
|
/** Allow navigation only to HTTP(S) URLs. Default true. */
|
|
28
28
|
readonly httpOnly?: boolean;
|
|
29
29
|
/**
|
|
30
|
-
* Directory `
|
|
30
|
+
* Directory `dsh_browser_download` save paths must resolve inside (prevents a
|
|
31
31
|
* prompt-injected agent from writing arbitrary machine paths). Default:
|
|
32
32
|
* the user's Downloads folder; override to confine downloads elsewhere.
|
|
33
33
|
*/
|