dsh-browser-plus 0.0.0-stage → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -0
- package/LICENSE +22 -0
- package/NOTICE.md +7 -0
- package/README.en.md +119 -0
- package/README.md +118 -2
- package/assets/dsh-browser-plus-256.png +0 -0
- package/assets/dsh-browser-plus-512.png +0 -0
- package/assets/dsh-browser-plus-small.svg +9 -0
- package/assets/dsh-browser-plus.ico +0 -0
- package/assets/dsh-browser-plus.svg +11 -0
- package/assets/readme-workspace.png +0 -0
- package/client/index.js +185 -0
- package/cordis.patch.yml +41 -0
- package/docs/MIGRATION.md +48 -0
- package/docs/README.md +22 -0
- package/docs/SOAK-CHECKLIST.md +98 -0
- package/docs/architecture.md +88 -0
- package/docs/tool-reference.md +126 -0
- package/docs/user-guide.md +137 -0
- package/docs/why-browser.md +45 -0
- package/lib/browser/runtime.d.ts +238 -0
- package/lib/browser/runtime.js +330 -0
- package/lib/browser/types.d.ts +758 -0
- package/lib/browser/types.js +18 -0
- package/lib/browser-electron/auth-cookies.d.ts +54 -0
- package/lib/browser-electron/auth-cookies.js +83 -0
- package/lib/browser-electron/chrome-state.d.ts +211 -0
- package/lib/browser-electron/chrome-state.js +12 -0
- package/lib/browser-electron/entry.d.ts +73 -0
- package/lib/browser-electron/entry.js +65 -0
- package/lib/browser-electron/fingerprint.d.ts +29 -0
- package/lib/browser-electron/fingerprint.js +42 -0
- package/lib/browser-electron/host-main.d.ts +19 -0
- package/lib/browser-electron/host-main.js +2691 -0
- package/lib/browser-electron/icon.d.ts +11 -0
- package/lib/browser-electron/icon.js +23 -0
- package/lib/browser-electron/page-chrome.d.ts +21 -0
- package/lib/browser-electron/page-chrome.js +2269 -0
- package/lib/browser-electron/provider.d.ts +767 -0
- package/lib/browser-electron/provider.js +2825 -0
- package/lib/browser-electron/remote-host.d.ts +145 -0
- package/lib/browser-electron/remote-host.js +993 -0
- package/lib/browser-electron/task-summary.d.ts +2 -0
- package/lib/browser-electron/task-summary.js +12 -0
- package/lib/browser-electron/task-thumbnail.d.ts +11 -0
- package/lib/browser-electron/task-thumbnail.js +9 -0
- package/lib/browser-electron/write-guard.d.ts +41 -0
- package/lib/browser-electron/write-guard.js +123 -0
- package/lib/client.js +185 -0
- package/lib/command-browser/index.d.ts +20 -0
- package/lib/command-browser/index.js +35 -0
- package/lib/http-browser/index.d.ts +28 -0
- package/lib/http-browser/index.js +110 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +25 -0
- package/lib/task-todos/index.d.ts +25 -0
- package/lib/task-todos/index.js +100 -0
- package/lib/tool-browser/index.d.ts +31 -0
- package/lib/tool-browser/index.js +2026 -0
- package/package.json +120 -4
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +20 -0
- package/scripts/build-icons.mjs +80 -0
- package/scripts/capture-window.ps1 +79 -0
- package/scripts/crop-image.ps1 +20 -0
- package/scripts/smoke-browser-tools.mjs +2051 -0
- package/scripts/smoke-chrome-world.mjs +65 -0
- package/scripts/smoke-electron-host.mjs +50 -0
- package/scripts/test-orb-drag.mjs +83 -0
- package/src/browser/runtime.ts +506 -0
- package/src/browser/types.ts +741 -0
- package/src/browser-electron/auth-cookies.ts +125 -0
- package/src/browser-electron/chrome-state.ts +192 -0
- package/src/browser-electron/entry.ts +125 -0
- package/src/browser-electron/fingerprint.ts +45 -0
- package/src/browser-electron/host-main.ts +2526 -0
- package/src/browser-electron/icon.ts +26 -0
- package/src/browser-electron/page-chrome.ts +2281 -0
- package/src/browser-electron/provider.ts +3366 -0
- package/src/browser-electron/remote-host.ts +1051 -0
- package/src/browser-electron/task-summary.ts +10 -0
- package/src/browser-electron/task-thumbnail.ts +17 -0
- package/src/browser-electron/write-guard.ts +134 -0
- package/src/command-browser/index.ts +61 -0
- package/src/http-browser/index.ts +139 -0
- package/src/index.ts +65 -0
- package/src/task-todos/index.ts +114 -0
- package/src/tool-browser/index.ts +2071 -0
- package/src/types/electron-shim.d.ts +143 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# 用户指南
|
|
2
|
+
|
|
3
|
+
## 环境要求
|
|
4
|
+
|
|
5
|
+
- DeepSeek Harness(dsh)且安装了 `web` profile
|
|
6
|
+
- **Electron 运行时**(可选 package dependency):插件固定 `42.9.3` 并优先使用自身安装的 binary;纯 `dsh web` 下找不到该版本会明确失败,避免 43.x compositor 故障。
|
|
7
|
+
|
|
8
|
+
## 安装
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
# 从 npm 安装
|
|
12
|
+
# dsh plugin --profile web add dsh-browser-plus
|
|
13
|
+
|
|
14
|
+
# 或从 GitHub 安装(未发布新版本时用这条)
|
|
15
|
+
dsh plugin --profile web add github:ParticleLight/dsh-browser-plus
|
|
16
|
+
|
|
17
|
+
# 或从源码目录(独立仓库,一插件一仓库)
|
|
18
|
+
dsh plugin --profile web add <本仓库路径>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
安装会链接插件、把 `dsh-browser-plus` 加入 profile 的 bundle 层,并挂载 **7 行**:
|
|
22
|
+
|
|
23
|
+
| 行 | 子路径 | 角色 |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `browser` | `dsh-browser-plus/browser` | `ctx.browser` 能力 seam(始终挂载) |
|
|
26
|
+
| `browser-electron` | `dsh-browser-plus/browser-electron` | Electron CDP provider |
|
|
27
|
+
| `tool-browser` | `dsh-browser-plus/tool-browser` | `browser_*` 模型侧工具 |
|
|
28
|
+
| `browser-plus`(根行) | `dsh-browser-plus` | **空行为**的根行:只为让客户端半边(右侧栏面板)被 client-modules 扫到 |
|
|
29
|
+
| `browser-command` | `dsh-browser-plus/command-browser` | `/browser` 斜杠命令(需要 `commands`) |
|
|
30
|
+
| `browser-http` | `dsh-browser-plus/http-browser` | 面板用的 HTTP 路由(需要 `webServer`) |
|
|
31
|
+
| `browser-task-todos` | `dsh-browser-plus/task-todos` | 把 Agent 的 `todo_write` 计划推给悬浮球(依赖可选的 `sessionProjections`) |
|
|
32
|
+
|
|
33
|
+
> 没有桌面外壳时插件**自托管**:自己拉起一个标题为 `dsh-browser-plus` 的 Electron 窗口,`browser_*` 工具照常可用。
|
|
34
|
+
|
|
35
|
+
## 配置
|
|
36
|
+
|
|
37
|
+
| 行 | 配置项 | 类型 | 默认 | 说明 |
|
|
38
|
+
| --- | --- | --- | --- | --- |
|
|
39
|
+
| `browser-electron` | `viewHost` | 对象 | 必填 | 宿主提供的 `ElectronBrowserViewHost`(通常 `!!js ctx.get('electronViewHost')`) |
|
|
40
|
+
| `browser-electron` | `httpOnly` | 布尔 | `true` | 仅允许 HTTP(S) 导航;`file:`/`data:` 等拒绝 |
|
|
41
|
+
| `browser-electron` | `writeRoots` | 字符串数组 | `[工作目录, 系统临时目录]` | `browser_screenshot`/`browser_download` 允许写入的绝对目录;越界拒绝 |
|
|
42
|
+
| `browser-electron` | `readRoots` | 字符串数组 | 同 `writeRoots` | `browser_upload_file` 与 `browser_auth action=restore file=…` 允许读取的绝对目录;越界拒绝 |
|
|
43
|
+
| `browser-electron` | `chromeWorld` | `main` / `isolated` | `main` | 注入 chrome 所在的 JS 世界。`isolated` 让页面读不到任务状态与 binding token,但每个文档多一个 CDP context——**需先在真实窗口验证工具栏**(见 SOAK 第 8 节) |
|
|
44
|
+
| `browser-electron` | `snapshotMaxElements` | 数字 | `60` | 快照最多收录的交互元素数 |
|
|
45
|
+
| `browser-electron` | `contentMaxChars` | 数字 | `100000` | 内容抓取默认字符上限 |
|
|
46
|
+
| `tool-browser` | `timeoutMs` | 数字 | `60000` | 工具协作超时(ms) |
|
|
47
|
+
| `tool-browser` | `allowedActions` | 字符串数组 | 无 | 插件级初始动作白名单;每个任务可用 `browser_restrict` 为自己覆盖或解除 |
|
|
48
|
+
| `tool-browser` | `tabTools` | 布尔 | `true` | 是否注册标签管理工具 |
|
|
49
|
+
|
|
50
|
+
## 快速上手(给 agent 的提示词示例)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
1. browser_open 打开 https://example.com
|
|
54
|
+
2. browser_snapshot 查看页面有哪些可交互元素、编号和 snapshotId
|
|
55
|
+
3. 优先 browser_click_ref(snapshotId, ref) 或 browser_scroll_into_view(snapshotId, ref),页面变化后重新快照
|
|
56
|
+
4. 需要填表时用 browser_fill(按 name/label/placeholder 匹配,一次填多个字段)
|
|
57
|
+
5. 后退、前进、刷新、停止和滚动使用 browser_back/browser_forward/browser_reload/browser_stop/browser_scroll
|
|
58
|
+
6. 遇到验证码(browser_challenge 或快照标注 CHALLENGE)时,调用 browser_handoff state=waiting-user,停下等待用户交还任务
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 打开浏览器窗口
|
|
62
|
+
|
|
63
|
+
窗口默认只在 Agent 第一次调用浏览器工具时出现。人也可以自己打开它:
|
|
64
|
+
|
|
65
|
+
- **`/browser` 命令** —— 在输入框敲 `/`(或点 `+`)选「浏览器」,回车即打开或前置窗口。它**不产生模型消息**,只是把窗口带到眼前。
|
|
66
|
+
- **右侧栏「DSH-Browser-Plus」** —— 在右侧栏的「添加」列表里选它(名字和图标都是这个插件自己的,避免和 DSH 自带的「浏览器」撞名),面板打开的同时窗口就被带出来了;面板里还有一个按钮可以随时再前置一次,并显示当前有几个浏览器任务。
|
|
67
|
+
|
|
68
|
+
> 右侧栏里 DSH 自带的「浏览器」是另一个东西:DSH 自己的沙箱 iframe 浏览器,与这个自托管窗口无关。
|
|
69
|
+
|
|
70
|
+
## 操作纪律
|
|
71
|
+
|
|
72
|
+
- **优先用快照引用**:先取得 `snapshotId`,再用 `browser_click_ref` 或 `browser_scroll_into_view`;引用过期时重新快照,而不是猜测同名控件。
|
|
73
|
+
- **常用浏览操作不用写脚本**:后退、前进、刷新、停止和滚动优先使用对应 `browser_*` 工具。
|
|
74
|
+
- **表单优先批量填写**:React/Vue 页面用 `browser_fill`;坐标点击只保留给 canvas、图标等没有语义节点的控件。
|
|
75
|
+
- **browser_execute 是最后手段**:只在新工具无法表达的页面特有操作中使用。
|
|
76
|
+
- **DPR 注意**:CDP 输入使用 CSS 像素;高 DPI 屏上若点击落空,用 `elementFromPoint` 校准,不要盲试坐标。
|
|
77
|
+
|
|
78
|
+
## 多任务并行
|
|
79
|
+
|
|
80
|
+
每个 DSH 会话(任务)拥有独立的浏览器会话(独立标签页与历史),并发任务互不干扰:
|
|
81
|
+
|
|
82
|
+
- `browser_session` 查看本任务的会话与标签;
|
|
83
|
+
- `browser_reset_session` 关闭并重建本任务的会话(崩溃或卡死后用它恢复)。
|
|
84
|
+
|
|
85
|
+
当前版本使用**一个共享可见浏览器窗口**,每个任务仍有隔离的任务视图、标签与历史。页面任务管理器切换可见任务;后台任务操作只更新自己的视图,不会抢走当前页面。`browser_space label="..."` 为本浏览器任务命名,`browser_space`(无参)列出全部浏览器任务。
|
|
86
|
+
|
|
87
|
+
工具栏是**常驻顶栏**:和标签栏一起占视图的高度(84px,开书签栏时 118px),页面**不需要为它让位**,也不会滑出或收回。任务按钮打开左侧工作区面板,操作轨迹按钮在桌面端打开右侧工作区面板。顶部工具栏最右侧常驻“接管 / 交还 Agent”控件,不必先打开任务面板;任务卡继续显示执行中、等待用户、用户接管、失败和空闲状态。接管期间新的 Agent 页面操作会停止,快照和内容读取仍可用于确认状态。
|
|
88
|
+
|
|
89
|
+
用户直接点击页面、编辑表单或使用非滚动键盘操作时,会自动切换为用户控制;滚轮、触摸拖动、滚动条操作,以及页面非编辑区的上下翻页键不会触发接管。Agent 自己的 CDP 鼠标和键盘输入带有短暂抑制标记,不会误交还控制权。
|
|
90
|
+
|
|
91
|
+
任务与轨迹状态采用版本化增量更新:普通操作只更新受影响的任务卡和一条轨迹。缩略图仅在工作区打开时按需刷新当前可见任务,后台任务保留最后图像。
|
|
92
|
+
|
|
93
|
+
页面原生 `alert/confirm/prompt` **默认**会被立刻接受(页面永不卡死),内容记录在 `browser_history`(`dialog` 条目)中。要驱动「确认删除」这类页面,先用 `browser_dialog` 设好**下一个**对话框怎么答(`accept`/`dismiss`,`prompt()` 可配 `promptText`)再触发它;`inspect` 报告上一次。
|
|
94
|
+
|
|
95
|
+
按键、双击、悬停、文件上传、等待元素、快照引用和原生导航:见 `browser_press_key` / `browser_double_click` / `browser_hover` / `browser_upload_file` / `browser_wait_for` / `browser_click_ref` / `browser_back` 等工具(完整参考见 [工具参考](tool-reference.md))。
|
|
96
|
+
|
|
97
|
+
登录态(cookie)为所有任务共享;可用 `browser_auth` 导出/恢复,重启后不丢。
|
|
98
|
+
|
|
99
|
+
某些站点(带 WAF 挑战的站点)会用**轮换名称**续期挑战 Cookie,旧代不会自动消失;两代共存时站点可能直接返回 400/412。遇到这种情况用 `browser_auth action="clear" domain="example.com"`(可再加 `name` 只删一个)清掉旧代,不必清空整个 profile;清除只影响该域及其子域,其他站点登录态保留。
|
|
100
|
+
|
|
101
|
+
## FAQ
|
|
102
|
+
|
|
103
|
+
**Q:纯 `dsh web` 能用吗?**
|
|
104
|
+
能。插件自托管:自己拉起 Electron 窗口,无需桌面外壳。
|
|
105
|
+
|
|
106
|
+
**Q:找不到 Electron?**
|
|
107
|
+
插件只接受 Electron `42.9.3`:优先自身 optional dependency,其次校验 `ELECTRON_PATH`、DSH 锚点与 pnpm store 候选。找不到时重新安装插件依赖,或把 `ELECTRON_PATH` 指向一个经 package metadata 验证为 `42.9.3` 的 binary。
|
|
108
|
+
|
|
109
|
+
**Q:截图失败或挂起?**
|
|
110
|
+
确认运行时是 Electron `42.9.3`,不要用 43.x。自托管截图优先走原生 `capturePage`,共享窗口内存在多个视图且目标未激活时自动兜底到 CDP。
|
|
111
|
+
|
|
112
|
+
**Q:浏览器窗口不见了?**
|
|
113
|
+
想主动把它叫回来:输入框里敲 `/browser`(或点 `+` 选「浏览器」),或者在右侧栏的「添加」列表里选「DSH-Browser-Plus」—— 两者都会打开或前置窗口,且**不产生模型消息**。窗口标题为 `dsh-browser-plus`(显示当前任务标签时为 `dsh-browser-plus — <名>`);所有任务共享这一可见窗口,通过页面任务管理器切换各自隔离视图。若子进程崩溃会自动重启;重启后旧会话失效,调用 `browser_reset_session` 重建。
|
|
114
|
+
|
|
115
|
+
**Q:下载报 CORS 错误?**
|
|
116
|
+
`browser_download` 在页面上下文内 `fetch`,受同源/CORS 约束;跨域文件请先在同源页面内操作,或直接请求用户提供。
|
|
117
|
+
|
|
118
|
+
**Q:如何禁止 agent 乱点?**
|
|
119
|
+
`browser_restrict` 设置白名单(如只允许 `browser_snapshot`/`browser_content`);传空列表解除。**规则按任务隔离**:一个任务设的白名单不会影响其它并行任务;`tool-browser.allowedActions` 配置作为所有任务的默认值。
|
|
120
|
+
|
|
121
|
+
**Q:能直接导入 Edge/Chrome 的登录状态吗?**
|
|
122
|
+
**不能自动导入**,这是浏览器的安全机制而非本插件的限制:Chrome/Edge 127+ 用 **App-Bound Encryption** 加密 cookie 值(实测本机 Chrome 的 cookie 全部是 `v20` 前缀),密钥绑定浏览器自身可执行文件身份,**复制 profile 也解不开**——实测把 `Local State` + `Default/Network/Cookies` 复制到临时目录再启动 Chrome,`Storage.getCookies` 返回 0 条。两条可行路径:
|
|
123
|
+
|
|
124
|
+
1. **在本插件自己的浏览器里登录一次(推荐)**:profile 是持久的(`<DSH_HOME>/dsh-browser-plus-host`),点页面工具栏的「接管」手动登录,之后 agent 的任务就一直带着这份登录态。
|
|
125
|
+
2. **导入用户导出的 cookie 文件**:用扩展或 DevTools 导出成 JSON,然后 `browser_auth { action: "restore", file: "<路径>" }`。文件须在 `browser-electron.readRoots` 内(默认:工作目录与系统临时目录);接受裸数组或 `{"cookies": [...]}` 两种形状,**两种字段风格都认**:① 本插件导出的 `url` 风格;② **浏览器扩展(Cookie-Editor / EditThisCookie)与 Edge 自带导出的 `domain` + `path` 风格 —— 没有 `url` 字段,插件会自行推导**(这正是「从浏览器导出再导入」的实际用法)。`sameSite` 同时接受 Chromium 拼写(`no_restriction`)与 Playwright 拼写(`None`/`Lax`/`Strict`)。格式不合法的条目会被跳过并在 `failed` 里计数。
|
|
126
|
+
|
|
127
|
+
## 故障排查
|
|
128
|
+
|
|
129
|
+
| 现象 | 可能原因 | 处理 |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| `BROWSER_SESSION_UNKNOWN` | 子进程重启后旧会话失效 | `browser_reset_session` |
|
|
132
|
+
| 工具超时 | 页面卡死/未渲染完成 | 稍后重试;`browser_reset` 重置标签 |
|
|
133
|
+
| 导航被拒 | 非 HTTP(S) 协议,或 URL 内嵌凭据 | 检查 URL;`httpOnly` 配置 |
|
|
134
|
+
| `BROWSER_WRITE_PATH_DENIED` | 落盘路径不在允许根内 | 改存工作目录/临时目录,或在 `browser-electron.writeRoots` 追加该目录 |
|
|
135
|
+
| `BROWSER_READ_PATH_DENIED` | 上传的文件不存在,或不在允许根内 | 改传工作目录/临时目录内的文件,或配置 `browser-electron.readRoots` |
|
|
136
|
+
| 下载被拒 | 非 HTTP(S),或 URL 内嵌凭据 | 下载与导航共用 `admitUrl` 准入;凭据请走页面登录态 |
|
|
137
|
+
| 快照为空 | 页面尚未加载 | 等待后重试 `browser_snapshot` |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 为什么做共享真实浏览器
|
|
2
|
+
|
|
3
|
+
## 问题
|
|
4
|
+
|
|
5
|
+
LLM agent 需要"上网"。传统做法是给 agent 一个**无头浏览器**(Playwright/Puppeteer 等)或**网页抓取工具**:
|
|
6
|
+
|
|
7
|
+
- 无头浏览器:agent 在看不见的页面里操作,用户无法确认它在做什么,也无法中途接管;遇到登录、验证码、人机检测时寸步难行。
|
|
8
|
+
- 网页抓取:只能读不能交互,且大量现代网站依赖 JS 渲染,抓到的内容失真。
|
|
9
|
+
|
|
10
|
+
这两条路都绕开了"用户"——浏览器成了 agent 的私有工具,而不是用户和 agent 共用的工具。
|
|
11
|
+
|
|
12
|
+
## 方案:共享真实浏览器
|
|
13
|
+
|
|
14
|
+
`dsh-browser-plus` 的思路很朴素:**让 agent 驱动用户眼前那个真实的浏览器窗口**。
|
|
15
|
+
|
|
16
|
+
- 浏览器是原生 `WebContentsView`,用户直接看到 agent 的每一步操作(打开页面、填写表单、点击、滚动);
|
|
17
|
+
- 用户**随时可以上手接管**:输入、点击、登录、过验证码——做完之后 agent 继续;
|
|
18
|
+
- agent 通过 CDP 在同一个页面里执行 JS,拿到的是与用户所见完全一致的真实 DOM。
|
|
19
|
+
|
|
20
|
+
一句话:浏览器不是 agent 的"黑盒工具",而是**用户与 agent 之间的共享工作台**。
|
|
21
|
+
|
|
22
|
+
## 与无头方案的能力对比
|
|
23
|
+
|
|
24
|
+
| 能力 | 无头浏览器 | 网页抓取 | 本插件(共享真实浏览器) |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| 真实页面渲染 | ✅ | 部分 | ✅ |
|
|
27
|
+
| 复杂交互(填表/点击/拖拽) | ✅ | ❌ | ✅ |
|
|
28
|
+
| 用户可见、可接管 | ❌ | ❌ | ✅ |
|
|
29
|
+
| 保留登录态 | 需手动管理 | ❌ | ✅(`browser_auth` + cookie 落盘) |
|
|
30
|
+
| 人工处理验证码 | ❌ 卡死 | ❌ | ✅ 请用户点一下即可 |
|
|
31
|
+
| 多任务并行 | 需多实例 | – | ✅ 任务级会话隔离 |
|
|
32
|
+
|
|
33
|
+
## 设计取舍
|
|
34
|
+
|
|
35
|
+
- **登录态共享**:所有任务共享同一份 cookie(符合"用户登录一次,agent 到处可用");需要隔离时用 `browser_auth` 手动导出/恢复。
|
|
36
|
+
- **任务级会话隔离**:每个 DSH 会话(任务)拥有独立的浏览器会话(标签页与历史),并发任务互不抢页面;但窗口只有一个,当前活动的任务视图可见。
|
|
37
|
+
- **装好即用**:不依赖桌面外壳。有外壳时嵌入外壳视图;纯 `dsh web` 时插件自托管——自己拉起 Electron 窗口,通过本机 TCP JSON-RPC 驱动。
|
|
38
|
+
- **边界清晰**:可见视图、浏览器列布局属于宿主外壳;插件只负责 seam、provider 与工具,不含任何 UI。
|
|
39
|
+
- **人机验证不硬刚**:检测到 Cloudflare/reCAPTCHA/hCaptcha/Turnstile 时停下,请用户在共享窗口人工完成,而不是盲目重试。
|
|
40
|
+
|
|
41
|
+
## 什么场景不适合
|
|
42
|
+
|
|
43
|
+
- 需要**无头批量抓取**(成千上万页面):请用专门的抓取工具/服务,不必开窗口。
|
|
44
|
+
- 需要**每个任务完全独立的登录态**(互不可见):本插件默认共享 cookie;如需强隔离,可用多个 DSH 实例或 `browser_auth` 手动管理。
|
|
45
|
+
- 需要**浏览器列 UI / 布局管理**:那是宿主外壳的配套,本插件不包含。
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the browser capability seam (`ctx.browser`): the
|
|
3
|
+
* provider registry and provider-selecting execution for browser sessions.
|
|
4
|
+
* Duplicate ids are rejected. At execution time, a configured provider must
|
|
5
|
+
* exist and be usable; without one, exactly one usable provider is required,
|
|
6
|
+
* so selection never depends on registration order.
|
|
7
|
+
* @module dsh-browser-plus/browser
|
|
8
|
+
*/
|
|
9
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
|
+
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
import type { BrowserContentRequest, BrowserHandoffState, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillRequest, BrowserFillResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenOptions, BrowserOpenRequest, BrowserDragRequest, BrowserDragResult, BrowserPointerResult, BrowserPointerTarget, BrowserPressKeyRequest, BrowserClearAuthRequest, BrowserScrapeRequest, BrowserScrapeStatus, BrowserClearAuthResult, BrowserProvider, BrowserRefRequest, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserScrollIntoViewRequest, BrowserScrollRequest, BrowserScrollResult, BrowserSessionId, BrowserSnapshotResult, BrowserSpaceInfo, BrowserTaskInfo, BrowserTaskUpdate, BrowserTab, BrowserTypeRequest, BrowserUploadFileRequest, BrowserUploadFileResult, BrowserPdfRequest, BrowserPdfResult, BrowserHighlightRequest, BrowserHighlightResult, BrowserWaitForRequest, BrowserWaitForResult, BrowserChallenge, BrowserTaskTodo, ExportedCookie } from './types.ts';
|
|
12
|
+
export { BrowserError, } from './types.ts';
|
|
13
|
+
export type { BrowserChallenge, BrowserContentFormat, BrowserControlOwner, BrowserContentRequest, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillField, BrowserFillRequest, BrowserFillResult, BrowserHandoffState, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenOptions, BrowserOpenRequest, BrowserDragRequest, BrowserDragResult, BrowserPointerResult, BrowserPointerTarget, BrowserPressKeyRequest, BrowserClearAuthRequest, BrowserScrapeRequest, BrowserScrapeStatus, BrowserClearAuthResult, BrowserProvider, BrowserRefRequest, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserScrollIntoViewRequest, BrowserScrollRequest, BrowserScrollResult, BrowserSessionId, BrowserSnapshotElement, BrowserSnapshotResult, BrowserSpaceInfo, BrowserTaskInfo, BrowserTaskStatus, BrowserTaskUpdate, BrowserTab, BrowserTypeRequest, BrowserUploadFileRequest, BrowserUploadFileResult, BrowserWaitForRequest, BrowserWaitForResult, ExportedCookie, } from './types.ts';
|
|
14
|
+
declare module '@deepseek-ai/cordis' {
|
|
15
|
+
interface Context {
|
|
16
|
+
browser: BrowserRuntime;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Config for the browser seam. `browserProvider` pins which provider wins;
|
|
21
|
+
* it is optional (a single registered usable provider auto-selects).
|
|
22
|
+
*/
|
|
23
|
+
export interface BrowserRuntimeConfig {
|
|
24
|
+
/** Explicit browser provider id. Omitted = auto-select when exactly one usable. */
|
|
25
|
+
readonly browserProvider?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The browser access service. Registered as `ctx.browser` (one instance per
|
|
29
|
+
* context).
|
|
30
|
+
*
|
|
31
|
+
* Selection semantics (resolved at execution time, never order-dependent):
|
|
32
|
+
* - A configured id that is registered and `available()` → that provider.
|
|
33
|
+
* - A configured id not registered → `BROWSER_PROVIDER_CONFIGURED_MISSING`.
|
|
34
|
+
* - A configured id registered but unavailable → `BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
35
|
+
* - No id configured, exactly one registered usable provider → that provider.
|
|
36
|
+
* - No id configured, multiple usable providers → `BROWSER_PROVIDER_AMBIGUOUS`.
|
|
37
|
+
* - No id configured, no usable provider → `BROWSER_PROVIDER_UNAVAILABLE`.
|
|
38
|
+
*/
|
|
39
|
+
export declare class BrowserRuntime extends Service {
|
|
40
|
+
/** Provider selection config. */
|
|
41
|
+
static Config: z<BrowserRuntimeConfig>;
|
|
42
|
+
private providers;
|
|
43
|
+
private readonly providerId;
|
|
44
|
+
constructor(ctx: Context, config?: BrowserRuntimeConfig);
|
|
45
|
+
/**
|
|
46
|
+
* Register a browser provider. Throws {@link BrowserError}
|
|
47
|
+
* `BROWSER_DUPLICATE_PROVIDER` if its id is already registered. Returns a
|
|
48
|
+
* disposer; disposed with the calling fiber.
|
|
49
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
50
|
+
* @returns the disposer that unregisters the provider.
|
|
51
|
+
*/
|
|
52
|
+
registerBrowserProvider(provider: BrowserProvider): () => void;
|
|
53
|
+
/** Resolve the selected provider or throw the matching {@link BrowserError}. */
|
|
54
|
+
private resolveProvider;
|
|
55
|
+
/** Open a new browser session through the selected provider. */
|
|
56
|
+
open(options?: BrowserOpenOptions): Promise<BrowserSessionId>;
|
|
57
|
+
/** Open a URL through the selected provider, optionally in a new tab. */
|
|
58
|
+
openUrl(session: BrowserSessionId, request: BrowserOpenRequest, signal?: AbortSignal): Promise<void>;
|
|
59
|
+
/** List the session's tabs through the selected provider. */
|
|
60
|
+
listTabs(session: BrowserSessionId): Promise<readonly BrowserTab[]>;
|
|
61
|
+
/** Switch to a tab through the selected provider. */
|
|
62
|
+
switchTab(session: BrowserSessionId, tabId: string): Promise<void>;
|
|
63
|
+
/** Close one tab through the selected provider. */
|
|
64
|
+
closeTab(session: BrowserSessionId, tabId: string): Promise<boolean>;
|
|
65
|
+
/** Close every tab and reset the session through the selected provider. */
|
|
66
|
+
reset(session: BrowserSessionId): Promise<void>;
|
|
67
|
+
/** Navigate the session's page through the selected provider. */
|
|
68
|
+
navigate(session: BrowserSessionId, request: BrowserNavigateRequest, signal?: AbortSignal): Promise<void>;
|
|
69
|
+
/** Navigate to the previous history entry through the selected provider. */
|
|
70
|
+
back(session: BrowserSessionId, signal?: AbortSignal): Promise<boolean>;
|
|
71
|
+
/** Navigate to the next history entry through the selected provider. */
|
|
72
|
+
forward(session: BrowserSessionId, signal?: AbortSignal): Promise<boolean>;
|
|
73
|
+
/** Reload the active page through the selected provider. */
|
|
74
|
+
reload(session: BrowserSessionId, signal?: AbortSignal): Promise<void>;
|
|
75
|
+
/** Stop loading the active page through the selected provider. */
|
|
76
|
+
stopLoading(session: BrowserSessionId, signal?: AbortSignal): Promise<void>;
|
|
77
|
+
/** Execute JS in the session's page context through the selected provider. */
|
|
78
|
+
execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
|
|
79
|
+
/** Produce an AI-friendly snapshot of the session's page. */
|
|
80
|
+
snapshot(session: BrowserSessionId, options?: {
|
|
81
|
+
query?: string;
|
|
82
|
+
limit?: number;
|
|
83
|
+
}, signal?: AbortSignal): Promise<BrowserSnapshotResult>;
|
|
84
|
+
/** Apply device/viewport/media emulation to the session's active tab. */
|
|
85
|
+
emulate(session: BrowserSessionId, options?: {
|
|
86
|
+
width?: number;
|
|
87
|
+
height?: number;
|
|
88
|
+
deviceScaleFactor?: number;
|
|
89
|
+
mobile?: boolean;
|
|
90
|
+
userAgent?: string;
|
|
91
|
+
colorScheme?: 'light' | 'dark' | 'no-preference';
|
|
92
|
+
clear?: boolean;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
applied: string[];
|
|
95
|
+
}>;
|
|
96
|
+
/** Console messages the host captured for the session's active tab. */
|
|
97
|
+
consoleMessages(session: BrowserSessionId, options?: {
|
|
98
|
+
limit?: number;
|
|
99
|
+
level?: string;
|
|
100
|
+
clear?: boolean;
|
|
101
|
+
}): Promise<{
|
|
102
|
+
messages: Array<{
|
|
103
|
+
level: string;
|
|
104
|
+
text: string;
|
|
105
|
+
at: string;
|
|
106
|
+
}>;
|
|
107
|
+
}>;
|
|
108
|
+
/** Network requests the host captured for the session's active tab. */
|
|
109
|
+
networkRequests(session: BrowserSessionId, options?: {
|
|
110
|
+
limit?: number;
|
|
111
|
+
failedOnly?: boolean;
|
|
112
|
+
urlContains?: string;
|
|
113
|
+
clear?: boolean;
|
|
114
|
+
}): Promise<{
|
|
115
|
+
requests: Array<{
|
|
116
|
+
method: string;
|
|
117
|
+
url: string;
|
|
118
|
+
status?: number;
|
|
119
|
+
mime?: string;
|
|
120
|
+
kind?: string;
|
|
121
|
+
failed?: string;
|
|
122
|
+
ms?: number;
|
|
123
|
+
at: string;
|
|
124
|
+
}>;
|
|
125
|
+
}>;
|
|
126
|
+
/** Set how the host answers the next JS dialog, and report the state. */
|
|
127
|
+
setDialogPolicy(session: BrowserSessionId, policy: {
|
|
128
|
+
behavior: 'accept' | 'dismiss';
|
|
129
|
+
promptText?: string;
|
|
130
|
+
}): Promise<{
|
|
131
|
+
dialog: unknown;
|
|
132
|
+
policy: {
|
|
133
|
+
behavior: 'accept' | 'dismiss';
|
|
134
|
+
promptText?: string;
|
|
135
|
+
};
|
|
136
|
+
}>;
|
|
137
|
+
/** Drain any pending dialog, then report the last one and the current policy. */
|
|
138
|
+
inspectDialog(session: BrowserSessionId): Promise<{
|
|
139
|
+
dialog: unknown;
|
|
140
|
+
policy: {
|
|
141
|
+
behavior: 'accept' | 'dismiss';
|
|
142
|
+
promptText?: string;
|
|
143
|
+
};
|
|
144
|
+
}>;
|
|
145
|
+
/** The last JS dialog the host reported, plus the current policy. */
|
|
146
|
+
dialogState(session: BrowserSessionId): {
|
|
147
|
+
dialog: unknown;
|
|
148
|
+
policy: {
|
|
149
|
+
behavior: 'accept' | 'dismiss';
|
|
150
|
+
promptText?: string;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
/** Click one element referenced by an exact snapshot. */
|
|
154
|
+
clickRef(session: BrowserSessionId, request: BrowserRefRequest, signal?: AbortSignal): Promise<void>;
|
|
155
|
+
/** Scroll one element referenced by an exact snapshot into view. */
|
|
156
|
+
scrollIntoView(session: BrowserSessionId, request: BrowserScrollIntoViewRequest, signal?: AbortSignal): Promise<BrowserScrollResult>;
|
|
157
|
+
/** Fetch page content in a requested format. */
|
|
158
|
+
content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
|
|
159
|
+
/** Click at viewport coordinates through the selected provider. */
|
|
160
|
+
click(session: BrowserSessionId, request: BrowserPointerTarget, signal?: AbortSignal): Promise<BrowserPointerResult>;
|
|
161
|
+
/** Double-click at viewport coordinates through the selected provider. */
|
|
162
|
+
doubleClick(session: BrowserSessionId, request: BrowserPointerTarget, signal?: AbortSignal): Promise<BrowserPointerResult>;
|
|
163
|
+
/** Press on one target, move to another, release, through the selected provider. */
|
|
164
|
+
drag(session: BrowserSessionId, request: BrowserDragRequest, signal?: AbortSignal): Promise<BrowserDragResult>;
|
|
165
|
+
hover(session: BrowserSessionId, request: BrowserPointerTarget, signal?: AbortSignal): Promise<BrowserPointerResult>;
|
|
166
|
+
/** Scroll the active page through the selected provider. */
|
|
167
|
+
scroll(session: BrowserSessionId, request: BrowserScrollRequest, signal?: AbortSignal): Promise<BrowserScrollResult>;
|
|
168
|
+
/** Attach a local file to a file input through the selected provider. */
|
|
169
|
+
uploadFile(session: BrowserSessionId, request: BrowserUploadFileRequest, signal?: AbortSignal): Promise<BrowserUploadFileResult>;
|
|
170
|
+
/** Wait for an element through the selected provider (bounded polling). */
|
|
171
|
+
highlight(session: BrowserSessionId, request: BrowserHighlightRequest, signal?: AbortSignal): Promise<BrowserHighlightResult>;
|
|
172
|
+
pdf(session: BrowserSessionId, request: BrowserPdfRequest, signal?: AbortSignal): Promise<BrowserPdfResult>;
|
|
173
|
+
waitForElement(session: BrowserSessionId, request: BrowserWaitForRequest, signal?: AbortSignal): Promise<BrowserWaitForResult>;
|
|
174
|
+
/** Type into the focused element through the selected provider. */
|
|
175
|
+
type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
|
|
176
|
+
/** Press a key into the session's page through the selected provider. */
|
|
177
|
+
pressKey(session: BrowserSessionId, request: BrowserPressKeyRequest, signal?: AbortSignal): Promise<void>;
|
|
178
|
+
/** Fill a form's fields in one batch through the selected provider. */
|
|
179
|
+
fillForm(session: BrowserSessionId, request: BrowserFillRequest, signal?: AbortSignal): Promise<BrowserFillResult>;
|
|
180
|
+
/** Capture the current page through the selected provider. */
|
|
181
|
+
screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult>;
|
|
182
|
+
/** Check for a human-verification challenge on the active tab. */
|
|
183
|
+
detectChallenge(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserChallenge>;
|
|
184
|
+
/** Return the session's chronological operation log through the provider. */
|
|
185
|
+
history(session: BrowserSessionId): Promise<readonly BrowserHistoryEntry[]>;
|
|
186
|
+
/** Replay one recorded operation by sequence number through the provider. */
|
|
187
|
+
replay(session: BrowserSessionId, seq: number): Promise<void>;
|
|
188
|
+
/** Download a URL to a local file through the provider. */
|
|
189
|
+
download(session: BrowserSessionId, request: BrowserDownloadRequest, signal?: AbortSignal): Promise<{
|
|
190
|
+
readonly path: string;
|
|
191
|
+
}>;
|
|
192
|
+
/** Export the session's cookies through the provider. */
|
|
193
|
+
flushAuth(session: BrowserSessionId): Promise<readonly ExportedCookie[]>;
|
|
194
|
+
/** Import cookies into the session through the provider. */
|
|
195
|
+
restoreAuth(session: BrowserSessionId, cookies: readonly ExportedCookie[]): Promise<number>;
|
|
196
|
+
/** Import cookies from a JSON export on disk through the provider. */
|
|
197
|
+
importAuth(session: BrowserSessionId, path: string): Promise<{
|
|
198
|
+
restored: number;
|
|
199
|
+
failed: number;
|
|
200
|
+
}>;
|
|
201
|
+
/** Start a background scrape batch through the provider. */
|
|
202
|
+
startScrape(session: BrowserSessionId, request: BrowserScrapeRequest): Promise<BrowserScrapeStatus>;
|
|
203
|
+
/** Progress of one scrape batch through the provider. */
|
|
204
|
+
scrapeStatus(id: string): Promise<BrowserScrapeStatus>;
|
|
205
|
+
/** Ask a running scrape batch to stop through the provider. */
|
|
206
|
+
stopScrape(id: string): Promise<BrowserScrapeStatus>;
|
|
207
|
+
/** Every scrape batch the provider knows about. */
|
|
208
|
+
listScrapes(): Promise<readonly BrowserScrapeStatus[]>;
|
|
209
|
+
/** Remove cookies for one site scope through the provider. */
|
|
210
|
+
clearAuth(session: BrowserSessionId, request: BrowserClearAuthRequest): Promise<BrowserClearAuthResult>;
|
|
211
|
+
/** Set the session's browser task label through the selected provider. */
|
|
212
|
+
setSpace(session: BrowserSessionId, label: string): Promise<void>;
|
|
213
|
+
/** List browser tasks (legacy spaces) with labels through the selected provider. */
|
|
214
|
+
listSpaces(): Promise<readonly BrowserSpaceInfo[]>;
|
|
215
|
+
/** List browser tasks with live collaboration status through the provider. */
|
|
216
|
+
listTasks(): Promise<readonly BrowserTaskInfo[]>;
|
|
217
|
+
/** Read one session's task collaboration state through the provider. */
|
|
218
|
+
getTask(session: BrowserSessionId): Promise<BrowserTaskInfo>;
|
|
219
|
+
/** Update one session's visible task state through the provider. */
|
|
220
|
+
updateTask(session: BrowserSessionId, update: BrowserTaskUpdate): Promise<BrowserTaskInfo>;
|
|
221
|
+
/** Mark one session as waiting for the user or returned to Agent control. */
|
|
222
|
+
setHandoff(session: BrowserSessionId, state: BrowserHandoffState): Promise<BrowserTaskInfo>;
|
|
223
|
+
/**
|
|
224
|
+
* Bring the shared browser window to the front through the selected
|
|
225
|
+
* provider, opening it when nothing is open yet.
|
|
226
|
+
*/
|
|
227
|
+
ensureWindowVisible(): Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* Mirror one task's Agent todo list into the shared window (the floating orb
|
|
230
|
+
* reads it there). A provider without a window has nowhere to put it, so this
|
|
231
|
+
* is a no-op rather than an error.
|
|
232
|
+
*/
|
|
233
|
+
pushTaskTodos(taskKey: string, todos: readonly BrowserTaskTodo[]): Promise<void>;
|
|
234
|
+
/** Close the session through the selected provider. Idempotent; a missing
|
|
235
|
+
* provider is treated as already-closed so teardown paths stay no-ops. */
|
|
236
|
+
close(session: BrowserSessionId): Promise<void>;
|
|
237
|
+
}
|
|
238
|
+
export default BrowserRuntime;
|