dsh-builtin-browser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +89 -0
- package/README.md +89 -0
- package/cordis.patch.yml +19 -0
- package/lib/browser/runtime.d.ts +84 -0
- package/lib/browser/runtime.js +138 -0
- package/lib/browser/types.d.ts +203 -0
- package/lib/browser/types.js +18 -0
- package/lib/browser-electron/entry.d.ts +30 -0
- package/lib/browser-electron/entry.js +23 -0
- package/lib/browser-electron/provider.d.ts +169 -0
- package/lib/browser-electron/provider.js +373 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +13 -0
- package/lib/tool-browser/index.d.ts +33 -0
- package/lib/tool-browser/index.js +359 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-browser contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# dsh-builtin-browser (built-in shared real browser plugin for DeepSeek Harness)
|
|
2
|
+
|
|
3
|
+
中文 | [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
A shared real browser capability plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): a visible browser the human watches and can take over, driven by the agent over CDP.
|
|
6
|
+
|
|
7
|
+
This plugin provides the **host-side capability** (browser seam, Electron CDP provider, `browser_*` model tools). The browser's **native view itself** is supplied by the host (a desktop shell) through `ctx.electronViewHost`; the plugin contains no browser UI.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Real view, not a relay.** The browser is a native view (`WebContentsView`) the human can see and operate directly; the agent drives the very same page. The view is provided by the host shell; the plugin drives it.
|
|
12
|
+
- **DOM-referenced, not coordinate-guessing.** `browser_snapshot` returns numbered interactive elements; `browser_execute` runs JS in the page (native setters for framework inputs), so interaction works on React/Vue pages.
|
|
13
|
+
- **Multi-tab sessions.** Open URLs in parallel tabs, list/switch/close/reset, all keeping state.
|
|
14
|
+
- **Multi-format content.** Fetch pages as html / markdown / txt / json, scoped by selector, capped by length and timeout.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- DeepSeek Harness (dsh) with the `web` profile
|
|
19
|
+
- A host that provides `ctx.electronViewHost` (a machine holding real Electron `WebContentsView`s, e.g. dsh's desktop shell). Without one, the plugin mounts the seam but the provider and tools stay disabled — plain `dsh web` is unaffected.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
dsh plugin --profile web add dsh-builtin-browser # from npm once published
|
|
25
|
+
# or from a checkout (one plugin, one repository):
|
|
26
|
+
dsh plugin --profile web add <path-to-this-repo>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This links the plugin, adds `dsh-builtin-browser` to the profile's bundle layer, and mounts:
|
|
30
|
+
|
|
31
|
+
| Row | Subpath | Role |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `browser` | `dsh-builtin-browser/browser` | `ctx.browser` capability seam (always mounted) |
|
|
34
|
+
| `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider (needs `electronViewHost`) |
|
|
35
|
+
| `tool-browser` | `dsh-builtin-browser/tool-browser` | `browser_*` model-facing tools |
|
|
36
|
+
|
|
37
|
+
The provider and tools are gated on `ctx.get('electronViewHost')` being present, so a composition without a desktop shell simply keeps the seam and nothing else.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
The plugin mounts through `cordis.patch.yml`; per-row config:
|
|
42
|
+
|
|
43
|
+
| Row | Key | Type | Default | Description |
|
|
44
|
+
|---|---|---|---|---|
|
|
45
|
+
| `browser-electron` | `viewHost` | object | required | `ElectronBrowserViewHost` supplied by the host shell (typically `!!js ctx.get('electronViewHost')`) |
|
|
46
|
+
| `browser-electron` | `httpOnly` | boolean | `true` | Allow HTTP(S) navigation only; other protocols (e.g. `file:`/`data:`) are rejected (`BROWSER_NAVIGATION_BLOCKED`) |
|
|
47
|
+
| `browser-electron` | `snapshotMaxElements` | number | `60` | Max snapshot elements before truncation |
|
|
48
|
+
| `browser-electron` | `contentMaxChars` | number | `100000` | Default content character cap |
|
|
49
|
+
| `tool-browser` | `timeoutMs` | number | `60000` | Cooperative tool timeout (ms) |
|
|
50
|
+
| `tool-browser` | `tabTools` | boolean | `true` | Register tab-management tools (`browser_list_tabs` etc.) |
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
| Tool | Purpose |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `browser_open` | Open a URL (optionally a new tab); returns a snapshot |
|
|
57
|
+
| `browser_snapshot` | Numbered inventory of interactive elements (inputs/buttons/links) |
|
|
58
|
+
| `browser_execute` | Run JS in the page; args arrive as `arguments[0..n]` |
|
|
59
|
+
| `browser_content` | Fetch the page as html / markdown / txt / json (selector, maxChars, timeoutMs) |
|
|
60
|
+
| `browser_screenshot` | PNG capture, optional `fullPage` |
|
|
61
|
+
| `browser_list_tabs` / `browser_switch_tab` / `browser_close_tab` / `browser_reset` | Multi-tab session management |
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
agent (browser_* tools)
|
|
67
|
+
→ ctx.browser (seam, dsh-builtin-browser/browser)
|
|
68
|
+
→ dsh-builtin-browser/browser-electron (provider)
|
|
69
|
+
→ ElectronBrowserViewHost (supplied by the host shell)
|
|
70
|
+
→ WebContentsView + webContents.debugger (CDP)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The provider is Electron-agnostic by construction: it operates through the `ElectronBrowserViewHost` seam (create/destroy/show views, `sendCommand`), which a real shell implements with Electron objects. The same seam lets a future relay provider (headless Chromium screenshot stream) serve remote deployments without touching the tools.
|
|
74
|
+
|
|
75
|
+
## Division of labor with the desktop shell
|
|
76
|
+
|
|
77
|
+
The browser's **visible view**, the **browser column layout**, and the **column-to-view alignment** belong to the host shell (e.g. dsh's `apps/desktop`), not this plugin. This plugin only consumes the shell-provided `electronViewHost` and owns the seam, provider, and tools. Installing the plugin without a matching shell leaves the capability disabled.
|
|
78
|
+
|
|
79
|
+
## Known limitations
|
|
80
|
+
|
|
81
|
+
- Screenshots are PNG-only (CDP JPEG hangs on Electron 43); JPEG awaits a non-CDP conversion path.
|
|
82
|
+
- `fullPage` capture is flaky under software compositing on some hosts.
|
|
83
|
+
- Session lifecycle is plugin-level (one session shared by the model), not per-agent yet.
|
|
84
|
+
- Private mode (`privateMode`) is not implemented: it needs Electron session partitioning, which is host-layer territory; this plugin does not promise it.
|
|
85
|
+
- This plugin contains no browser-column UI — that is host-shell territory; do not treat "browser column" as a plugin feature.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# dsh-builtin-browser(DeepSeek Harness 内置共享真实浏览器插件)
|
|
2
|
+
|
|
3
|
+
[English](README.en.md) | 中文
|
|
4
|
+
|
|
5
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的共享真实浏览器能力插件:一个用户可见、可随时接管的浏览器,由 agent 通过 CDP 驱动。
|
|
6
|
+
|
|
7
|
+
本插件提供 **host 侧能力**(浏览器 seam、Electron CDP provider、`browser_*` 模型工具)。浏览器本身的**原生视图**由宿主(桌面外壳)通过 `ctx.electronViewHost` 提供;插件不包含浏览器界面 UI。
|
|
8
|
+
|
|
9
|
+
## 功能
|
|
10
|
+
|
|
11
|
+
- **真实视图,而非转播。** 浏览器是原生视图(`WebContentsView`),用户可直接看到并操作;agent 驱动的是同一个页面。视图由宿主外壳提供,插件负责驱动。
|
|
12
|
+
- **DOM 引用,而非猜坐标。** `browser_snapshot` 返回带编号的交互元素;`browser_execute` 在页面里执行 JS(框架输入用原生 setter),在 React/Vue 页面上也能可靠交互。
|
|
13
|
+
- **多标签会话。** 并行打开 URL、查看/切换/关闭/重置标签,状态保持。
|
|
14
|
+
- **多格式内容。** 以 html / markdown / txt / json 抓取页面,支持 selector 限定、长度与超时上限。
|
|
15
|
+
|
|
16
|
+
## 环境要求
|
|
17
|
+
|
|
18
|
+
- DeepSeek Harness(dsh)且安装了 `web` profile
|
|
19
|
+
- 提供 `ctx.electronViewHost` 的宿主(持有真实 Electron `WebContentsView` 的主机,如 dsh 的桌面外壳)。没有它时,插件只挂载 seam,provider 与工具保持禁用——纯 `dsh web` 不受影响。
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
dsh plugin --profile web add dsh-builtin-browser # 发布到 npm 后
|
|
25
|
+
# 或从源码目录(独立仓库,一插件一仓库):
|
|
26
|
+
dsh plugin --profile web add <本仓库路径>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
这会链接插件、把 `dsh-builtin-browser` 加入 profile 的 bundle 层,并挂载:
|
|
30
|
+
|
|
31
|
+
| 行 | 子路径 | 角色 |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `browser` | `dsh-builtin-browser/browser` | `ctx.browser` 能力 seam(始终挂载) |
|
|
34
|
+
| `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider(需要 `electronViewHost`) |
|
|
35
|
+
| `tool-browser` | `dsh-builtin-browser/tool-browser` | `browser_*` 模型侧工具 |
|
|
36
|
+
|
|
37
|
+
provider 与工具以 `ctx.get('electronViewHost')` 是否存在为门控,因此没有桌面外壳的组合只会保留 seam,其余不启用。
|
|
38
|
+
|
|
39
|
+
## 配置
|
|
40
|
+
|
|
41
|
+
插件通过 `cordis.patch.yml` 挂载,各行的配置:
|
|
42
|
+
|
|
43
|
+
| 行 | 配置项 | 类型 | 默认 | 说明 |
|
|
44
|
+
|---|---|---|---|---|
|
|
45
|
+
| `browser-electron` | `viewHost` | 对象 | 必填 | 宿主提供的 `ElectronBrowserViewHost` 实例(通常 `!!js ctx.get('electronViewHost')`) |
|
|
46
|
+
| `browser-electron` | `httpOnly` | 布尔 | `true` | 仅允许 HTTP(S) 导航;其余协议(如 `file:`/`data:`)拒绝(`BROWSER_NAVIGATION_BLOCKED`) |
|
|
47
|
+
| `browser-electron` | `snapshotMaxElements` | 数字 | `60` | 快照最多收录的交互元素数,超出截断 |
|
|
48
|
+
| `browser-electron` | `contentMaxChars` | 数字 | `100000` | 内容抓取默认字符上限 |
|
|
49
|
+
| `tool-browser` | `timeoutMs` | 数字 | `60000` | 工具协作超时(ms) |
|
|
50
|
+
| `tool-browser` | `tabTools` | 布尔 | `true` | 是否注册标签管理工具(`browser_list_tabs` 等) |
|
|
51
|
+
|
|
52
|
+
## 工具
|
|
53
|
+
|
|
54
|
+
| 工具 | 用途 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `browser_open` | 打开 URL(可选新标签);返回快照 |
|
|
57
|
+
| `browser_snapshot` | 交互元素(输入框/按钮/链接)带编号清单 |
|
|
58
|
+
| `browser_execute` | 在页面执行 JS;参数以 `arguments[0..n]` 传入 |
|
|
59
|
+
| `browser_content` | 以 html / markdown / txt / json 抓取页面(selector、maxChars、timeoutMs) |
|
|
60
|
+
| `browser_screenshot` | PNG 截图,可选 `fullPage` |
|
|
61
|
+
| `browser_list_tabs` / `browser_switch_tab` / `browser_close_tab` / `browser_reset` | 多标签会话管理 |
|
|
62
|
+
|
|
63
|
+
## 工作原理
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
agent (browser_* 工具)
|
|
67
|
+
→ ctx.browser (seam, dsh-builtin-browser/browser)
|
|
68
|
+
→ dsh-builtin-browser/browser-electron (provider)
|
|
69
|
+
→ ElectronBrowserViewHost (由宿主外壳提供)
|
|
70
|
+
→ WebContentsView + webContents.debugger (CDP)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
provider 按构造与 Electron 解耦:它通过 `ElectronBrowserViewHost` 接缝操作(创建/销毁/显示视图、`sendCommand`),由真实外壳用 Electron 对象实现。同一接缝也让未来的转播 provider(无头 Chromium 截图流)服务远程部署,而无需改动工具。
|
|
74
|
+
|
|
75
|
+
## 与桌面外壳的分工
|
|
76
|
+
|
|
77
|
+
浏览器**可见视图**、**浏览器列布局**、**列与视图的对齐**都属于宿主外壳(如 dsh 的 `apps/desktop`),不在本插件内。本插件只消费外壳提供的 `electronViewHost`,负责 seam、provider 与工具。若你只装插件而没有配套外壳,插件功能保持禁用。
|
|
78
|
+
|
|
79
|
+
## 已知限制
|
|
80
|
+
|
|
81
|
+
- 截图仅 PNG(CDP JPEG 在 Electron 43 上挂起);JPEG 等待非 CDP 转换路径。
|
|
82
|
+
- 部分主机在软件合成下 `fullPage` 截图不稳定。
|
|
83
|
+
- 会话生命周期为插件级(模型共享一个会话),尚未做到按 agent 隔离。
|
|
84
|
+
- 无痕模式(`privateMode`)未实现:它需要 Electron 的 session 分区能力,属于宿主层,本插件不承诺。
|
|
85
|
+
- 本插件不含浏览器列 UI——那是宿主外壳的配套,别把"浏览器列"当成插件能力。
|
|
86
|
+
|
|
87
|
+
## 许可证
|
|
88
|
+
|
|
89
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# dsh-builtin-browser bundle patch: mounts the shared-browser capability into a
|
|
2
|
+
# profile composition. The browser seam (ctx.browser) is always mounted; the
|
|
3
|
+
# Electron provider activates only where the host provides `electronViewHost`
|
|
4
|
+
# (the desktop shell or any host that supplies a view host through
|
|
5
|
+
# `ctx.get('electronViewHost')`), and the model-facing tools ride the same
|
|
6
|
+
# gate so plain `dsh web` (no native view) stays unaffected.
|
|
7
|
+
- insert:
|
|
8
|
+
- id: browser
|
|
9
|
+
name: dsh-builtin-browser/browser
|
|
10
|
+
|
|
11
|
+
- id: browser-electron
|
|
12
|
+
name: dsh-builtin-browser/browser-electron
|
|
13
|
+
disabled: !!js ctx.get('electronViewHost') === undefined
|
|
14
|
+
config:
|
|
15
|
+
viewHost: !!js ctx.get('electronViewHost')
|
|
16
|
+
|
|
17
|
+
- id: tool-browser
|
|
18
|
+
name: dsh-builtin-browser/tool-browser
|
|
19
|
+
disabled: !!js ctx.get('electronViewHost') === undefined
|
|
@@ -0,0 +1,84 @@
|
|
|
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/browser
|
|
8
|
+
*/
|
|
9
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
|
+
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
import type { BrowserClickRequest, BrowserContentRequest, BrowserContentResult, BrowserExecuteRequest, BrowserExecuteResult, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest } from './types.ts';
|
|
12
|
+
export { BrowserError, } from './types.ts';
|
|
13
|
+
export type { BrowserClickRequest, BrowserContentFormat, BrowserContentRequest, BrowserContentResult, BrowserExecuteRequest, BrowserExecuteResult, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotElement, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest, } 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(): 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<void>;
|
|
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
|
+
/** Execute JS in the session's page context through the selected provider. */
|
|
70
|
+
execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
|
|
71
|
+
/** Produce an AI-friendly snapshot of the session's page. */
|
|
72
|
+
snapshot(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserSnapshotResult>;
|
|
73
|
+
/** Fetch page content in a requested format. */
|
|
74
|
+
content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
|
|
75
|
+
/** Click at viewport coordinates through the selected provider. */
|
|
76
|
+
click(session: BrowserSessionId, request: BrowserClickRequest, signal?: AbortSignal): Promise<void>;
|
|
77
|
+
/** Type into the focused element through the selected provider. */
|
|
78
|
+
type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
|
|
79
|
+
/** Capture the current page through the selected provider. */
|
|
80
|
+
screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult>;
|
|
81
|
+
/** Close the session through the selected provider. Idempotent. */
|
|
82
|
+
close(session: BrowserSessionId): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
export default BrowserRuntime;
|
|
@@ -0,0 +1,138 @@
|
|
|
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/browser
|
|
8
|
+
*/
|
|
9
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
10
|
+
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
import { BrowserError } from "./types.js";
|
|
12
|
+
export { BrowserError, } from "./types.js";
|
|
13
|
+
/**
|
|
14
|
+
* The browser access service. Registered as `ctx.browser` (one instance per
|
|
15
|
+
* context).
|
|
16
|
+
*
|
|
17
|
+
* Selection semantics (resolved at execution time, never order-dependent):
|
|
18
|
+
* - A configured id that is registered and `available()` 鈫?that provider.
|
|
19
|
+
* - A configured id not registered 鈫?`BROWSER_PROVIDER_CONFIGURED_MISSING`.
|
|
20
|
+
* - A configured id registered but unavailable 鈫? * `BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
21
|
+
* - No id configured, exactly one registered usable provider 鈫?that provider.
|
|
22
|
+
* - No id configured, multiple usable providers 鈫?`BROWSER_PROVIDER_AMBIGUOUS`.
|
|
23
|
+
* - No id configured, no usable provider 鈫?`BROWSER_PROVIDER_UNAVAILABLE`.
|
|
24
|
+
*/
|
|
25
|
+
export class BrowserRuntime extends Service {
|
|
26
|
+
/** Provider selection config. */
|
|
27
|
+
static Config = z.object({
|
|
28
|
+
browserProvider: z.string(),
|
|
29
|
+
});
|
|
30
|
+
providers = new Map();
|
|
31
|
+
providerId;
|
|
32
|
+
constructor(ctx, config = {}) {
|
|
33
|
+
super(ctx, 'browser');
|
|
34
|
+
this.providerId = config.browserProvider;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register a browser provider. Throws {@link BrowserError}
|
|
38
|
+
* `BROWSER_DUPLICATE_PROVIDER` if its id is already registered. Returns a
|
|
39
|
+
* disposer; disposed with the calling fiber.
|
|
40
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
41
|
+
* @returns the disposer that unregisters the provider.
|
|
42
|
+
*/
|
|
43
|
+
registerBrowserProvider(provider) {
|
|
44
|
+
if (this.providers.has(provider.id)) {
|
|
45
|
+
throw new BrowserError(`a browser provider with id "${provider.id}" is already registered`, 'BROWSER_DUPLICATE_PROVIDER');
|
|
46
|
+
}
|
|
47
|
+
// Bind the generator to this instance instead of aliasing `this` to a
|
|
48
|
+
// local (no-this-alias): the effect body mutates the instance registry.
|
|
49
|
+
const dispose = this.ctx.effect(function* () {
|
|
50
|
+
this.providers.set(provider.id, provider);
|
|
51
|
+
yield () => this.providers.delete(provider.id);
|
|
52
|
+
}.bind(this), 'browser.registerProvider()');
|
|
53
|
+
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
|
54
|
+
// synchronous fire-and-forget 鈥?discard the (always-resolved) promise.
|
|
55
|
+
return () => void dispose();
|
|
56
|
+
}
|
|
57
|
+
/** Resolve the selected provider or throw the matching {@link BrowserError}. */
|
|
58
|
+
resolveProvider() {
|
|
59
|
+
const { providerId, providers } = this;
|
|
60
|
+
if (providerId !== undefined) {
|
|
61
|
+
const provider = providers.get(providerId);
|
|
62
|
+
if (!provider) {
|
|
63
|
+
throw new BrowserError(`configured browser provider "${providerId}" is not registered`, 'BROWSER_PROVIDER_CONFIGURED_MISSING');
|
|
64
|
+
}
|
|
65
|
+
if (!provider.available()) {
|
|
66
|
+
throw new BrowserError(`configured browser provider "${providerId}" is registered but unavailable`, 'BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE');
|
|
67
|
+
}
|
|
68
|
+
return provider;
|
|
69
|
+
}
|
|
70
|
+
const usable = [...providers.values()].filter(provider => provider.available());
|
|
71
|
+
const [single] = usable;
|
|
72
|
+
if (single === undefined) {
|
|
73
|
+
throw new BrowserError('no usable browser provider is registered', 'BROWSER_PROVIDER_UNAVAILABLE');
|
|
74
|
+
}
|
|
75
|
+
if (usable.length > 1) {
|
|
76
|
+
const ids = usable.map(provider => provider.id).join(', ');
|
|
77
|
+
throw new BrowserError(`multiple usable browser providers are registered (${ids}); configure one explicitly`, 'BROWSER_PROVIDER_AMBIGUOUS');
|
|
78
|
+
}
|
|
79
|
+
return single;
|
|
80
|
+
}
|
|
81
|
+
/** Open a new browser session through the selected provider. */
|
|
82
|
+
async open() {
|
|
83
|
+
return this.resolveProvider().open();
|
|
84
|
+
}
|
|
85
|
+
/** Open a URL through the selected provider, optionally in a new tab. */
|
|
86
|
+
async openUrl(session, request, signal) {
|
|
87
|
+
return this.resolveProvider().openUrl(session, request, signal);
|
|
88
|
+
}
|
|
89
|
+
/** List the session's tabs through the selected provider. */
|
|
90
|
+
async listTabs(session) {
|
|
91
|
+
return this.resolveProvider().listTabs(session);
|
|
92
|
+
}
|
|
93
|
+
/** Switch to a tab through the selected provider. */
|
|
94
|
+
async switchTab(session, tabId) {
|
|
95
|
+
return this.resolveProvider().switchTab(session, tabId);
|
|
96
|
+
}
|
|
97
|
+
/** Close one tab through the selected provider. */
|
|
98
|
+
async closeTab(session, tabId) {
|
|
99
|
+
return this.resolveProvider().closeTab(session, tabId);
|
|
100
|
+
}
|
|
101
|
+
/** Close every tab and reset the session through the selected provider. */
|
|
102
|
+
async reset(session) {
|
|
103
|
+
return this.resolveProvider().reset(session);
|
|
104
|
+
}
|
|
105
|
+
/** Navigate the session's page through the selected provider. */
|
|
106
|
+
async navigate(session, request, signal) {
|
|
107
|
+
return this.resolveProvider().navigate(session, request, signal);
|
|
108
|
+
}
|
|
109
|
+
/** Execute JS in the session's page context through the selected provider. */
|
|
110
|
+
async execute(session, request, signal) {
|
|
111
|
+
return this.resolveProvider().execute(session, request, signal);
|
|
112
|
+
}
|
|
113
|
+
/** Produce an AI-friendly snapshot of the session's page. */
|
|
114
|
+
async snapshot(session, signal) {
|
|
115
|
+
return this.resolveProvider().snapshot(session, signal);
|
|
116
|
+
}
|
|
117
|
+
/** Fetch page content in a requested format. */
|
|
118
|
+
async content(session, request, signal) {
|
|
119
|
+
return this.resolveProvider().content(session, request, signal);
|
|
120
|
+
}
|
|
121
|
+
/** Click at viewport coordinates through the selected provider. */
|
|
122
|
+
async click(session, request, signal) {
|
|
123
|
+
return this.resolveProvider().click(session, request, signal);
|
|
124
|
+
}
|
|
125
|
+
/** Type into the focused element through the selected provider. */
|
|
126
|
+
async type(session, request, signal) {
|
|
127
|
+
return this.resolveProvider().type(session, request, signal);
|
|
128
|
+
}
|
|
129
|
+
/** Capture the current page through the selected provider. */
|
|
130
|
+
async screenshot(session, request, signal) {
|
|
131
|
+
return this.resolveProvider().screenshot(session, request, signal);
|
|
132
|
+
}
|
|
133
|
+
/** Close the session through the selected provider. Idempotent. */
|
|
134
|
+
async close(session) {
|
|
135
|
+
return this.resolveProvider().close(session);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export default BrowserRuntime;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary for the browser capability seam (`ctx.browser`). One seam owns
|
|
3
|
+
* provider registration, session lifecycle, cancellation, errors, and product
|
|
4
|
+
* configuration; providers differ only in what backs a session (an Electron
|
|
5
|
+
* `WebContentsView` in the desktop shell, a headless Chromium relay for
|
|
6
|
+
* remote deployments, 鈥?.
|
|
7
|
+
* @module dsh-browser/browser/types
|
|
8
|
+
*/
|
|
9
|
+
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
10
|
+
/**
|
|
11
|
+
* Stable identity of one browser session, resolved by the Host through a
|
|
12
|
+
* Typert `LookupMap` to the live session object (the same mechanism
|
|
13
|
+
* `Agent 鈫?agentId` uses). The id is opaque to the wire; only the provider
|
|
14
|
+
* and its host binding can interpret it.
|
|
15
|
+
*/
|
|
16
|
+
export type BrowserSessionId = string;
|
|
17
|
+
/**
|
|
18
|
+
* What one browser-capable backend is asked to do. Navigation is the minimal
|
|
19
|
+
* operation every provider shares; the seam deliberately keeps the request
|
|
20
|
+
* minimal so a provider swap never changes how the model asks.
|
|
21
|
+
*/
|
|
22
|
+
export interface BrowserNavigateRequest {
|
|
23
|
+
/** The URL to open. Admission (HTTP(S) only, no credentials/private targets) is provider-owned. */
|
|
24
|
+
readonly url: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Click at viewport coordinates. Coordinates are viewport-relative pixels in
|
|
28
|
+
* the session's own coordinate space 鈥?the same space the human interacts
|
|
29
|
+
* with, so a real view and a relay agree without scaling.
|
|
30
|
+
*/
|
|
31
|
+
export interface BrowserClickRequest {
|
|
32
|
+
/** Viewport-relative x in CSS pixels. */
|
|
33
|
+
readonly x: number;
|
|
34
|
+
/** Viewport-relative y in CSS pixels. */
|
|
35
|
+
readonly y: number;
|
|
36
|
+
}
|
|
37
|
+
/** Type text into the focused element. */
|
|
38
|
+
export interface BrowserTypeRequest {
|
|
39
|
+
/** The text to insert. */
|
|
40
|
+
readonly text: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A screenshot of the current page state. `dataUrl` is a base64 data URL the
|
|
44
|
+
* tool layer renders directly; the live view itself never crosses the wire
|
|
45
|
+
* (the human watches the real view in the desktop shape).
|
|
46
|
+
*/
|
|
47
|
+
export interface BrowserScreenshotResult {
|
|
48
|
+
/** Base64 data URL of the capture (PNG or JPEG per the request). */
|
|
49
|
+
readonly dataUrl: string;
|
|
50
|
+
}
|
|
51
|
+
/** Screenshot capture options. PNG only (CDP JPEG hangs on Electron 43). */
|
|
52
|
+
export interface BrowserScreenshotRequest {
|
|
53
|
+
/** Capture the full scrollable page instead of the viewport. Default false. */
|
|
54
|
+
readonly fullPage?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Execute arbitrary JavaScript in the session's page context. This is the
|
|
58
|
+
* primary interaction path 鈥?DOM-referenced, not screen-coordinate-based:
|
|
59
|
+
* focus/input/click go through the page's own JS (native setters for
|
|
60
|
+
* framework-controlled inputs), matching how a human-driven agent operates.
|
|
61
|
+
*/
|
|
62
|
+
export interface BrowserExecuteRequest {
|
|
63
|
+
/** The JavaScript expression to evaluate in the page context. */
|
|
64
|
+
readonly script: string;
|
|
65
|
+
/** Optional arguments injected into the script scope as `arguments[0..n]`. */
|
|
66
|
+
readonly args?: readonly unknown[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Result of an execute. Mirrors CDP `Runtime.evaluate` semantics: either a
|
|
70
|
+
* value (by value, not by reference) or the exception thrown.
|
|
71
|
+
*/
|
|
72
|
+
export type BrowserExecuteResult = {
|
|
73
|
+
readonly ok: true;
|
|
74
|
+
readonly value: unknown;
|
|
75
|
+
} | {
|
|
76
|
+
readonly ok: false;
|
|
77
|
+
readonly exception: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* One interactive element in a snapshot, with a stable reference number the
|
|
81
|
+
* tool layer (and the model) can cite to target the element.
|
|
82
|
+
*/
|
|
83
|
+
export interface BrowserSnapshotElement {
|
|
84
|
+
/** 1-based reference number, stable for the lifetime of one snapshot. */
|
|
85
|
+
readonly ref: number;
|
|
86
|
+
/** Element kind: 'input' | 'button' | 'link' | 'textarea' | 'select' | 'checkbox' | 'other'. */
|
|
87
|
+
readonly kind: string;
|
|
88
|
+
/** Text content or placeholder/label, truncated for the snapshot. */
|
|
89
|
+
readonly label: string;
|
|
90
|
+
/** Best-effort CSS selector; may be empty when none is derivable. */
|
|
91
|
+
readonly selector: string;
|
|
92
|
+
/** Viewport-relative center, for coordinate fallbacks. */
|
|
93
|
+
readonly x: number;
|
|
94
|
+
readonly y: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* AI-friendly page snapshot: a compact, numbered inventory of interactive
|
|
98
|
+
* elements plus page facts. Not raw HTML 鈥?the model dialogues with this.
|
|
99
|
+
*/
|
|
100
|
+
export interface BrowserSnapshotResult {
|
|
101
|
+
/** Final page URL. */
|
|
102
|
+
readonly url: string;
|
|
103
|
+
/** Page title, if any. */
|
|
104
|
+
readonly title?: string;
|
|
105
|
+
/** Numbered interactive elements. */
|
|
106
|
+
readonly elements: readonly BrowserSnapshotElement[];
|
|
107
|
+
/** True when the snapshot was truncated (element cap reached). */
|
|
108
|
+
readonly truncated: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Content fetch formats. `html` is the raw DOM; `markdown` is a structured
|
|
112
|
+
* reading view; `txt` is plain text (smallest); `json` is structured data.
|
|
113
|
+
*/
|
|
114
|
+
export type BrowserContentFormat = 'html' | 'markdown' | 'txt' | 'json';
|
|
115
|
+
/** Content fetch request: format, optional selector scoping, and caps. */
|
|
116
|
+
export interface BrowserContentRequest {
|
|
117
|
+
/** Desired format. */
|
|
118
|
+
readonly format: BrowserContentFormat;
|
|
119
|
+
/** Optional CSS selector limiting the fetch to one region (e.g. `#main`). */
|
|
120
|
+
readonly selector?: string;
|
|
121
|
+
/** Maximum characters of the returned content. */
|
|
122
|
+
readonly maxChars?: number;
|
|
123
|
+
/** Evaluation timeout in ms; the fetch aborts after this. Default 30000. */
|
|
124
|
+
readonly timeoutMs?: number;
|
|
125
|
+
}
|
|
126
|
+
/** A fetched page content in the requested format. */
|
|
127
|
+
export interface BrowserContentResult {
|
|
128
|
+
/** The content in the requested format, capped at `maxChars`. */
|
|
129
|
+
readonly content: string;
|
|
130
|
+
/** True when the content was truncated. */
|
|
131
|
+
readonly truncated: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A tab inside a browser session. Tabs share the session's profile/cookies
|
|
135
|
+
* and are switchable without losing state.
|
|
136
|
+
*/
|
|
137
|
+
export interface BrowserTab {
|
|
138
|
+
/** Stable tab id within the session. */
|
|
139
|
+
readonly id: string;
|
|
140
|
+
/** Current URL, or empty for a blank tab. */
|
|
141
|
+
readonly url: string;
|
|
142
|
+
/** Page title, if any. */
|
|
143
|
+
readonly title?: string;
|
|
144
|
+
/** Whether this tab is the active one. */
|
|
145
|
+
readonly active: boolean;
|
|
146
|
+
}
|
|
147
|
+
/** Request to open a URL: into the active tab or a new tab. */
|
|
148
|
+
export interface BrowserOpenRequest {
|
|
149
|
+
/** The URL to open. */
|
|
150
|
+
readonly url: string;
|
|
151
|
+
/** Open in a new tab (parallel). Default false (reuse the active tab). */
|
|
152
|
+
readonly newTab?: boolean;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A browser-capable backend. Registered with `ctx.browser.registerBrowserProvider`.
|
|
156
|
+
* `id` is a stable string, unique across the seam.
|
|
157
|
+
*/
|
|
158
|
+
export interface BrowserProvider {
|
|
159
|
+
readonly id: string;
|
|
160
|
+
/** Cheap local usability check; must not make network calls. */
|
|
161
|
+
available(): boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Open a new session. The provider mints the session id and prepares its
|
|
164
|
+
* backing surface (a view, a headless page, 鈥?. Sessions are isolated from
|
|
165
|
+
* each other; a session must not be visible to any other session's caller.
|
|
166
|
+
*/
|
|
167
|
+
open(): Promise<BrowserSessionId>;
|
|
168
|
+
/** Open a URL, optionally in a new tab. Honor `signal` for cancellation. */
|
|
169
|
+
openUrl(session: BrowserSessionId, request: BrowserOpenRequest, signal?: AbortSignal): Promise<void>;
|
|
170
|
+
/** List the session's tabs. */
|
|
171
|
+
listTabs(session: BrowserSessionId): Promise<readonly BrowserTab[]>;
|
|
172
|
+
/** Switch to a tab by id. Unknown id 鈫?`BROWSER_TAB_UNKNOWN`. */
|
|
173
|
+
switchTab(session: BrowserSessionId, tabId: string): Promise<void>;
|
|
174
|
+
/** Close one tab. Closing the active tab activates the next. */
|
|
175
|
+
closeTab(session: BrowserSessionId, tabId: string): Promise<void>;
|
|
176
|
+
/** Close every tab and reset the session's state. */
|
|
177
|
+
reset(session: BrowserSessionId): Promise<void>;
|
|
178
|
+
/** Navigate the active tab. Honor `signal` for cancellation. */
|
|
179
|
+
navigate(session: BrowserSessionId, request: BrowserNavigateRequest, signal?: AbortSignal): Promise<void>;
|
|
180
|
+
/** Execute JS in the active tab's page context. */
|
|
181
|
+
execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
|
|
182
|
+
/** Produce an AI-friendly snapshot of the active tab. */
|
|
183
|
+
snapshot(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserSnapshotResult>;
|
|
184
|
+
/** Fetch page content in a requested format. */
|
|
185
|
+
content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
|
|
186
|
+
/** Click at viewport coordinates (fallback path; execute is preferred). Honor `signal` for cancellation. */
|
|
187
|
+
click(session: BrowserSessionId, request: BrowserClickRequest, signal?: AbortSignal): Promise<void>;
|
|
188
|
+
/** Type into the focused element (fallback path). Honor `signal` for cancellation. */
|
|
189
|
+
type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
|
|
190
|
+
/** Capture the current page. Honor `signal` for cancellation. */
|
|
191
|
+
screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult>;
|
|
192
|
+
/** Close the session and destroy its backing surface. Idempotent. */
|
|
193
|
+
close(session: BrowserSessionId): Promise<void>;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Typed browser error with a machine-routable, open-string `code` and chained
|
|
197
|
+
* `cause`. Consumers must tolerate provider-specific codes. Shared codes cover
|
|
198
|
+
* unavailable, missing, unusable, ambiguous, or duplicate providers, unknown
|
|
199
|
+
* or closed sessions, cancellation, and provider failure; a provider adds its
|
|
200
|
+
* own (e.g. `BROWSER_CDP_ATTACH_FAILED`).
|
|
201
|
+
*/
|
|
202
|
+
export declare class BrowserError extends HarnessError {
|
|
203
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary for the browser capability seam (`ctx.browser`). One seam owns
|
|
3
|
+
* provider registration, session lifecycle, cancellation, errors, and product
|
|
4
|
+
* configuration; providers differ only in what backs a session (an Electron
|
|
5
|
+
* `WebContentsView` in the desktop shell, a headless Chromium relay for
|
|
6
|
+
* remote deployments, 鈥?.
|
|
7
|
+
* @module dsh-browser/browser/types
|
|
8
|
+
*/
|
|
9
|
+
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
10
|
+
/**
|
|
11
|
+
* Typed browser error with a machine-routable, open-string `code` and chained
|
|
12
|
+
* `cause`. Consumers must tolerate provider-specific codes. Shared codes cover
|
|
13
|
+
* unavailable, missing, unusable, ambiguous, or duplicate providers, unknown
|
|
14
|
+
* or closed sessions, cancellation, and provider failure; a provider adds its
|
|
15
|
+
* own (e.g. `BROWSER_CDP_ATTACH_FAILED`).
|
|
16
|
+
*/
|
|
17
|
+
export class BrowserError extends HarnessError {
|
|
18
|
+
}
|