dsh-builtin-browser 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/lib/browser/runtime.d.ts +24 -8
- package/lib/browser/runtime.js +35 -7
- package/lib/browser/types.d.ts +120 -7
- package/lib/browser/types.js +1 -1
- package/lib/browser-electron/host-main.js +85 -0
- package/lib/browser-electron/provider.d.ts +49 -8
- package/lib/browser-electron/provider.js +355 -34
- package/lib/browser-electron/remote-host.js +29 -1
- package/lib/tool-browser/index.d.ts +13 -7
- package/lib/tool-browser/index.js +509 -34
- package/package.json +4 -2
- package/src/browser/runtime.ts +260 -0
- package/src/browser/types.ts +338 -0
- package/src/browser-electron/entry.ts +58 -0
- package/src/browser-electron/host-main.ts +229 -0
- package/src/browser-electron/provider.ts +922 -0
- package/src/browser-electron/remote-host.ts +421 -0
- package/src/index.ts +33 -0
- package/src/tool-browser/index.ts +862 -0
- package/src/types/electron-shim.d.ts +82 -0
package/README.md
CHANGED
|
@@ -74,6 +74,13 @@ provider 与工具以 `ctx.get('electronViewHost')` 是否存在为门控,因此
|
|
|
74
74
|
| `browser_content` | 以 html / markdown / txt / json 抓取页面(selector、maxChars、timeoutMs) |
|
|
75
75
|
| `browser_screenshot` | PNG 截图,可选 `fullPage` |
|
|
76
76
|
| `browser_list_tabs` / `browser_switch_tab` / `browser_close_tab` / `browser_reset` | 多标签会话管理 |
|
|
77
|
+
| `browser_session` / `browser_reset_session` | 查看/重置当前任务的浏览器会话(按任务隔离) |
|
|
78
|
+
| `browser_history` / `browser_replay` | 查看操作记录并回放某一步 |
|
|
79
|
+
| `browser_download` | 带会话 cookie 下载 URL 到本地文件 |
|
|
80
|
+
| `browser_restrict` | 限制允许的浏览器动作,防误点/误导航 |
|
|
81
|
+
| `browser_auth` | 导出/恢复 cookie,持久化登录态 |
|
|
82
|
+
| `browser_challenge` | 检测人机验证(CAPTCHA / Cloudflare / reCAPTCHA / hCaptcha / Turnstile)是否拦截当前页 |
|
|
83
|
+
| `browser_fill` | 批量填充表单(选择器/名称/标签匹配,支持受控输入、下拉、单选/复选,可选提交) |
|
|
77
84
|
|
|
78
85
|
### 操作纪律(点击/填表)
|
|
79
86
|
|
|
@@ -103,7 +110,8 @@ provider 按构造与 Electron 解耦:它通过 `ElectronBrowserViewHost` 接缝
|
|
|
103
110
|
|
|
104
111
|
- 截图仅 PNG(CDP JPEG 在 Electron 43 上挂起);JPEG 等待非 CDP 转换路径。
|
|
105
112
|
- 部分主机在软件合成下 `fullPage` 截图不稳定。
|
|
106
|
-
-
|
|
113
|
+
- 会话按任务隔离:每个调用方任务(DSH 会话)拥有独立的浏览器会话(独立标签页与历史),并发任务互不干扰;同一任务的多次调用复用同一会话。登录态(cookie)为共享,可用 `browser_auth` 导出/恢复。
|
|
114
|
+
- 人机验证(CAPTCHA)无法自动解决:快照会标注检测到的挑战(`browser_challenge` 可显式检查),此时应请用户在共享窗口中人工完成,而不是反复重试。
|
|
107
115
|
- 无痕模式(`privateMode`)未实现:它需要 Electron 的 session 分区能力,属于宿主层,本插件不承诺。
|
|
108
116
|
- 本插件不含浏览器列 UI——那是宿主外壳的配套,别把"浏览器列"当成插件能力。
|
|
109
117
|
|
package/lib/browser/runtime.d.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
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';
|
|
11
|
+
import type { BrowserClickRequest, BrowserContentRequest, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillRequest, BrowserFillResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest, BrowserChallenge, ExportedCookie } from './types.ts';
|
|
12
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';
|
|
13
|
+
export type { BrowserChallenge, BrowserClickRequest, BrowserContentFormat, BrowserContentRequest, BrowserContentResult, BrowserDownloadRequest, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillField, BrowserFillRequest, BrowserFillResult, BrowserHistoryEntry, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotElement, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest, ExportedCookie, } from './types.ts';
|
|
14
14
|
declare module '@deepseek-ai/cordis' {
|
|
15
15
|
interface Context {
|
|
16
16
|
browser: BrowserRuntime;
|
|
@@ -29,12 +29,12 @@ export interface BrowserRuntimeConfig {
|
|
|
29
29
|
* context).
|
|
30
30
|
*
|
|
31
31
|
* Selection semantics (resolved at execution time, never order-dependent):
|
|
32
|
-
* - A configured id that is registered and `available()`
|
|
33
|
-
* - A configured id not registered
|
|
34
|
-
* - A configured id registered but unavailable
|
|
35
|
-
* - No id configured, exactly one registered usable provider
|
|
36
|
-
* - No id configured, multiple usable providers
|
|
37
|
-
* - No id configured, no usable provider
|
|
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
38
|
*/
|
|
39
39
|
export declare class BrowserRuntime extends Service {
|
|
40
40
|
/** Provider selection config. */
|
|
@@ -76,8 +76,24 @@ export declare class BrowserRuntime extends Service {
|
|
|
76
76
|
click(session: BrowserSessionId, request: BrowserClickRequest, signal?: AbortSignal): Promise<void>;
|
|
77
77
|
/** Type into the focused element through the selected provider. */
|
|
78
78
|
type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
|
|
79
|
+
/** Fill a form's fields in one batch through the selected provider. */
|
|
80
|
+
fillForm(session: BrowserSessionId, request: BrowserFillRequest, signal?: AbortSignal): Promise<BrowserFillResult>;
|
|
79
81
|
/** Capture the current page through the selected provider. */
|
|
80
82
|
screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult>;
|
|
83
|
+
/** Check for a human-verification challenge on the active tab. */
|
|
84
|
+
detectChallenge(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserChallenge>;
|
|
85
|
+
/** Return the session's chronological operation log through the provider. */
|
|
86
|
+
history(session: BrowserSessionId): Promise<readonly BrowserHistoryEntry[]>;
|
|
87
|
+
/** Replay one recorded operation by sequence number through the provider. */
|
|
88
|
+
replay(session: BrowserSessionId, seq: number): Promise<void>;
|
|
89
|
+
/** Download a URL to a local file through the provider. */
|
|
90
|
+
download(session: BrowserSessionId, request: BrowserDownloadRequest, signal?: AbortSignal): Promise<{
|
|
91
|
+
readonly path: string;
|
|
92
|
+
}>;
|
|
93
|
+
/** Export the session's cookies through the provider. */
|
|
94
|
+
flushAuth(session: BrowserSessionId): Promise<readonly ExportedCookie[]>;
|
|
95
|
+
/** Import cookies into the session through the provider. */
|
|
96
|
+
restoreAuth(session: BrowserSessionId, cookies: readonly ExportedCookie[]): Promise<number>;
|
|
81
97
|
/** Close the session through the selected provider. Idempotent. */
|
|
82
98
|
close(session: BrowserSessionId): Promise<void>;
|
|
83
99
|
}
|
package/lib/browser/runtime.js
CHANGED
|
@@ -15,12 +15,12 @@ export { BrowserError, } from "./types.js";
|
|
|
15
15
|
* context).
|
|
16
16
|
*
|
|
17
17
|
* Selection semantics (resolved at execution time, never order-dependent):
|
|
18
|
-
* - A configured id that is registered and `available()`
|
|
19
|
-
* - A configured id not registered
|
|
20
|
-
* - A configured id registered but unavailable
|
|
21
|
-
* - No id configured, exactly one registered usable provider
|
|
22
|
-
* - No id configured, multiple usable providers
|
|
23
|
-
* - No id configured, no usable provider
|
|
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
24
|
*/
|
|
25
25
|
export class BrowserRuntime extends Service {
|
|
26
26
|
/** Provider selection config. */
|
|
@@ -51,7 +51,7 @@ export class BrowserRuntime extends Service {
|
|
|
51
51
|
yield () => this.providers.delete(provider.id);
|
|
52
52
|
}.bind(this), 'browser.registerProvider()');
|
|
53
53
|
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
|
54
|
-
// synchronous fire-and-forget
|
|
54
|
+
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
|
55
55
|
return () => void dispose();
|
|
56
56
|
}
|
|
57
57
|
/** Resolve the selected provider or throw the matching {@link BrowserError}. */
|
|
@@ -126,10 +126,38 @@ export class BrowserRuntime extends Service {
|
|
|
126
126
|
async type(session, request, signal) {
|
|
127
127
|
return this.resolveProvider().type(session, request, signal);
|
|
128
128
|
}
|
|
129
|
+
/** Fill a form's fields in one batch through the selected provider. */
|
|
130
|
+
async fillForm(session, request, signal) {
|
|
131
|
+
return this.resolveProvider().fillForm(session, request, signal);
|
|
132
|
+
}
|
|
129
133
|
/** Capture the current page through the selected provider. */
|
|
130
134
|
async screenshot(session, request, signal) {
|
|
131
135
|
return this.resolveProvider().screenshot(session, request, signal);
|
|
132
136
|
}
|
|
137
|
+
/** Check for a human-verification challenge on the active tab. */
|
|
138
|
+
async detectChallenge(session, signal) {
|
|
139
|
+
return this.resolveProvider().detectChallenge(session, signal);
|
|
140
|
+
}
|
|
141
|
+
/** Return the session's chronological operation log through the provider. */
|
|
142
|
+
async history(session) {
|
|
143
|
+
return this.resolveProvider().history(session);
|
|
144
|
+
}
|
|
145
|
+
/** Replay one recorded operation by sequence number through the provider. */
|
|
146
|
+
async replay(session, seq) {
|
|
147
|
+
return this.resolveProvider().replay(session, seq);
|
|
148
|
+
}
|
|
149
|
+
/** Download a URL to a local file through the provider. */
|
|
150
|
+
async download(session, request, signal) {
|
|
151
|
+
return this.resolveProvider().download(session, request, signal);
|
|
152
|
+
}
|
|
153
|
+
/** Export the session's cookies through the provider. */
|
|
154
|
+
async flushAuth(session) {
|
|
155
|
+
return this.resolveProvider().flushAuth(session);
|
|
156
|
+
}
|
|
157
|
+
/** Import cookies into the session through the provider. */
|
|
158
|
+
async restoreAuth(session, cookies) {
|
|
159
|
+
return this.resolveProvider().restoreAuth(session, cookies);
|
|
160
|
+
}
|
|
133
161
|
/** Close the session through the selected provider. Idempotent. */
|
|
134
162
|
async close(session) {
|
|
135
163
|
return this.resolveProvider().close(session);
|
package/lib/browser/types.d.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* provider registration, session lifecycle, cancellation, errors, and product
|
|
4
4
|
* configuration; providers differ only in what backs a session (an Electron
|
|
5
5
|
* `WebContentsView` in the desktop shell, a headless Chromium relay for
|
|
6
|
-
* remote deployments,
|
|
6
|
+
* remote deployments, and so on).
|
|
7
7
|
* @module dsh-browser/browser/types
|
|
8
8
|
*/
|
|
9
9
|
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
10
10
|
/**
|
|
11
11
|
* Stable identity of one browser session, resolved by the Host through a
|
|
12
12
|
* Typert `LookupMap` to the live session object (the same mechanism
|
|
13
|
-
* `Agent
|
|
13
|
+
* `Agent → agentId` uses). The id is opaque to the wire; only the provider
|
|
14
14
|
* and its host binding can interpret it.
|
|
15
15
|
*/
|
|
16
16
|
export type BrowserSessionId = string;
|
|
@@ -25,7 +25,7 @@ export interface BrowserNavigateRequest {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Click at viewport coordinates. Coordinates are viewport-relative pixels in
|
|
28
|
-
* the session's own coordinate space
|
|
28
|
+
* the session's own coordinate space — the same space the human interacts
|
|
29
29
|
* with, so a real view and a relay agree without scaling.
|
|
30
30
|
*/
|
|
31
31
|
export interface BrowserClickRequest {
|
|
@@ -39,6 +39,48 @@ export interface BrowserTypeRequest {
|
|
|
39
39
|
/** The text to insert. */
|
|
40
40
|
readonly text: string;
|
|
41
41
|
}
|
|
42
|
+
/** One field of a batch form fill. Match by selector, or by name/label/placeholder. */
|
|
43
|
+
export interface BrowserFillField {
|
|
44
|
+
/** CSS selector; when present, candidates are scoped to it. */
|
|
45
|
+
readonly selector?: string;
|
|
46
|
+
/** Match by the field's `name` attribute. */
|
|
47
|
+
readonly name?: string;
|
|
48
|
+
/** Match by associated `<label>` text or `aria-label`. */
|
|
49
|
+
readonly label?: string;
|
|
50
|
+
/** Match by `placeholder` text. */
|
|
51
|
+
readonly placeholder?: string;
|
|
52
|
+
/** Field kind; defaults to text. */
|
|
53
|
+
readonly kind?: 'text' | 'textarea' | 'checkbox' | 'radio' | 'select';
|
|
54
|
+
/** Value to set: string, number, or boolean (checkbox/radio). For a
|
|
55
|
+
* multi-select or radio group, the option value (or visible text). */
|
|
56
|
+
readonly value: string | number | boolean;
|
|
57
|
+
}
|
|
58
|
+
/** Batch form fill: set several fields, optionally submitting the form. */
|
|
59
|
+
export interface BrowserFillRequest {
|
|
60
|
+
/** The fields to fill, in order. */
|
|
61
|
+
readonly fields: readonly BrowserFillField[];
|
|
62
|
+
/** Submit the containing form after filling. Default false. */
|
|
63
|
+
readonly submit?: boolean;
|
|
64
|
+
/** Per-field evaluation budget in ms. Default 30000. */
|
|
65
|
+
readonly timeoutMs?: number;
|
|
66
|
+
}
|
|
67
|
+
/** One field's outcome in a batch fill. */
|
|
68
|
+
export interface BrowserFillFieldResult {
|
|
69
|
+
readonly ok: boolean;
|
|
70
|
+
/** The field description matched (selector/name/label/placeholder). */
|
|
71
|
+
readonly target: string;
|
|
72
|
+
/** How the value was applied: input | textarea | select | checkbox | radio | contenteditable. */
|
|
73
|
+
readonly method?: string;
|
|
74
|
+
/** Error text when the field could not be filled. */
|
|
75
|
+
readonly error?: string;
|
|
76
|
+
}
|
|
77
|
+
/** Outcome of a batch form fill. */
|
|
78
|
+
export interface BrowserFillResult {
|
|
79
|
+
/** Per-field outcomes, in request order. */
|
|
80
|
+
readonly fields: readonly BrowserFillFieldResult[];
|
|
81
|
+
/** Whether the containing form was submitted. */
|
|
82
|
+
readonly submitted: boolean;
|
|
83
|
+
}
|
|
42
84
|
/**
|
|
43
85
|
* A screenshot of the current page state. `dataUrl` is a base64 data URL the
|
|
44
86
|
* tool layer renders directly; the live view itself never crosses the wire
|
|
@@ -47,15 +89,38 @@ export interface BrowserTypeRequest {
|
|
|
47
89
|
export interface BrowserScreenshotResult {
|
|
48
90
|
/** Base64 data URL of the capture (PNG or JPEG per the request). */
|
|
49
91
|
readonly dataUrl: string;
|
|
92
|
+
/** File path the capture was also saved to, when the request asked for it. */
|
|
93
|
+
readonly path?: string;
|
|
50
94
|
}
|
|
51
95
|
/** Screenshot capture options. PNG only (CDP JPEG hangs on Electron 43). */
|
|
52
96
|
export interface BrowserScreenshotRequest {
|
|
53
97
|
/** Capture the full scrollable page instead of the viewport. Default false. */
|
|
54
98
|
readonly fullPage?: boolean;
|
|
99
|
+
/** Absolute file path to also save the PNG to (for vision-tool reads). */
|
|
100
|
+
readonly savePath?: string;
|
|
101
|
+
}
|
|
102
|
+
/** Download options: fetch a URL into a local file, keeping the session's cookies. */
|
|
103
|
+
export interface BrowserDownloadRequest {
|
|
104
|
+
/** The URL to download. */
|
|
105
|
+
readonly url: string;
|
|
106
|
+
/** Absolute path of the file to write. */
|
|
107
|
+
readonly savePath: string;
|
|
108
|
+
}
|
|
109
|
+
/** A serializable cookie, exported from or imported into the browser session. */
|
|
110
|
+
export interface ExportedCookie {
|
|
111
|
+
/** Cookie URL (scheme + domain + path), as accepted by cookies.set. */
|
|
112
|
+
readonly url: string;
|
|
113
|
+
readonly name: string;
|
|
114
|
+
readonly value: string;
|
|
115
|
+
readonly domain?: string;
|
|
116
|
+
readonly path?: string;
|
|
117
|
+
readonly secure?: boolean;
|
|
118
|
+
readonly httpOnly?: boolean;
|
|
119
|
+
readonly expirationDate?: number;
|
|
55
120
|
}
|
|
56
121
|
/**
|
|
57
122
|
* Execute arbitrary JavaScript in the session's page context. This is the
|
|
58
|
-
* primary interaction path
|
|
123
|
+
* primary interaction path — DOM-referenced, not screen-coordinate-based:
|
|
59
124
|
* focus/input/click go through the page's own JS (native setters for
|
|
60
125
|
* framework-controlled inputs), matching how a human-driven agent operates.
|
|
61
126
|
*/
|
|
@@ -97,7 +162,7 @@ export interface BrowserSnapshotElement {
|
|
|
97
162
|
}
|
|
98
163
|
/**
|
|
99
164
|
* AI-friendly page snapshot: a compact, numbered inventory of interactive
|
|
100
|
-
* elements plus page facts. Not raw HTML
|
|
165
|
+
* elements plus page facts. Not raw HTML — the model dialogues with this.
|
|
101
166
|
*/
|
|
102
167
|
export interface BrowserSnapshotResult {
|
|
103
168
|
/** Final page URL. */
|
|
@@ -108,6 +173,21 @@ export interface BrowserSnapshotResult {
|
|
|
108
173
|
readonly elements: readonly BrowserSnapshotElement[];
|
|
109
174
|
/** True when the snapshot was truncated (element cap reached). */
|
|
110
175
|
readonly truncated: boolean;
|
|
176
|
+
/** Human-verification challenge blocking the page, when one is detected. */
|
|
177
|
+
readonly challenge?: BrowserChallenge;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A detected human-verification (bot-detection / CAPTCHA) challenge blocking
|
|
181
|
+
* the page. Detection is best-effort and marker-based; a clean result is not a
|
|
182
|
+
* guarantee that no challenge exists.
|
|
183
|
+
*/
|
|
184
|
+
export interface BrowserChallenge {
|
|
185
|
+
/** Whether a challenge currently blocks the page. */
|
|
186
|
+
readonly blocked: boolean;
|
|
187
|
+
/** Machine-readable kind: cloudflare | hcaptcha | recaptcha | turnstile | generic. */
|
|
188
|
+
readonly kind?: string;
|
|
189
|
+
/** Short human-readable description, e.g. 'Cloudflare "Just a moment" interstitial'. */
|
|
190
|
+
readonly reason?: string;
|
|
111
191
|
}
|
|
112
192
|
/**
|
|
113
193
|
* Content fetch formats. `html` is the raw DOM; `markdown` is a structured
|
|
@@ -163,7 +243,7 @@ export interface BrowserProvider {
|
|
|
163
243
|
available(): boolean;
|
|
164
244
|
/**
|
|
165
245
|
* Open a new session. The provider mints the session id and prepares its
|
|
166
|
-
* backing surface (a view, a headless page,
|
|
246
|
+
* backing surface (a view, a headless page, and so on). Sessions are isolated from
|
|
167
247
|
* each other; a session must not be visible to any other session's caller.
|
|
168
248
|
*/
|
|
169
249
|
open(): Promise<BrowserSessionId>;
|
|
@@ -171,7 +251,7 @@ export interface BrowserProvider {
|
|
|
171
251
|
openUrl(session: BrowserSessionId, request: BrowserOpenRequest, signal?: AbortSignal): Promise<void>;
|
|
172
252
|
/** List the session's tabs. */
|
|
173
253
|
listTabs(session: BrowserSessionId): Promise<readonly BrowserTab[]>;
|
|
174
|
-
/** Switch to a tab by id. Unknown id
|
|
254
|
+
/** Switch to a tab by id. Unknown id -> `BROWSER_TAB_UNKNOWN`. */
|
|
175
255
|
switchTab(session: BrowserSessionId, tabId: string): Promise<void>;
|
|
176
256
|
/** Close one tab. Closing the active tab activates the next. */
|
|
177
257
|
closeTab(session: BrowserSessionId, tabId: string): Promise<void>;
|
|
@@ -183,17 +263,50 @@ export interface BrowserProvider {
|
|
|
183
263
|
execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
|
|
184
264
|
/** Produce an AI-friendly snapshot of the active tab. */
|
|
185
265
|
snapshot(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserSnapshotResult>;
|
|
266
|
+
/** Check whether a human-verification challenge is blocking the active tab. */
|
|
267
|
+
detectChallenge(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserChallenge>;
|
|
186
268
|
/** Fetch page content in a requested format. */
|
|
187
269
|
content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
|
|
188
270
|
/** Click at viewport coordinates (fallback path; execute is preferred). Honor `signal` for cancellation. */
|
|
189
271
|
click(session: BrowserSessionId, request: BrowserClickRequest, signal?: AbortSignal): Promise<void>;
|
|
190
272
|
/** Type into the focused element (fallback path). Honor `signal` for cancellation. */
|
|
191
273
|
type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
|
|
274
|
+
/** Fill a form's fields in one batch. Honor `signal` for cancellation. */
|
|
275
|
+
fillForm(session: BrowserSessionId, request: BrowserFillRequest, signal?: AbortSignal): Promise<BrowserFillResult>;
|
|
192
276
|
/** Capture the current page. Honor `signal` for cancellation. */
|
|
193
277
|
screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult>;
|
|
278
|
+
/** Download a URL to a local file. Honor `signal` for cancellation. */
|
|
279
|
+
download(session: BrowserSessionId, request: BrowserDownloadRequest, signal?: AbortSignal): Promise<{
|
|
280
|
+
readonly path: string;
|
|
281
|
+
}>;
|
|
282
|
+
/** Export the session's cookies (login state). */
|
|
283
|
+
flushAuth(session: BrowserSessionId): Promise<readonly ExportedCookie[]>;
|
|
284
|
+
/** Import cookies into the session (restore login state). */
|
|
285
|
+
restoreAuth(session: BrowserSessionId, cookies: readonly ExportedCookie[]): Promise<number>;
|
|
286
|
+
/** Return the session's chronological operation log. */
|
|
287
|
+
history(session: BrowserSessionId): Promise<readonly BrowserHistoryEntry[]>;
|
|
288
|
+
/** Replay one recorded operation by sequence number. */
|
|
289
|
+
replay(session: BrowserSessionId, seq: number): Promise<void>;
|
|
194
290
|
/** Close the session and destroy its backing surface. Idempotent. */
|
|
195
291
|
close(session: BrowserSessionId): Promise<void>;
|
|
196
292
|
}
|
|
293
|
+
/** One recorded browser operation, in chronological order (seq 1, 2, 3…). */
|
|
294
|
+
export interface BrowserHistoryEntry {
|
|
295
|
+
/** Monotonic sequence number within the session. */
|
|
296
|
+
readonly seq: number;
|
|
297
|
+
/** The operation name: navigate | execute | click | type | replay. */
|
|
298
|
+
readonly action: string;
|
|
299
|
+
/** The operation's arguments. */
|
|
300
|
+
readonly params: Record<string, unknown>;
|
|
301
|
+
/** Whether the operation succeeded. */
|
|
302
|
+
readonly ok: boolean;
|
|
303
|
+
/** Truncated result text, when the operation returned one. */
|
|
304
|
+
readonly result?: string;
|
|
305
|
+
/** Error text, when the operation failed. */
|
|
306
|
+
readonly error?: string;
|
|
307
|
+
/** Epoch millis of the operation. */
|
|
308
|
+
readonly at: number;
|
|
309
|
+
}
|
|
197
310
|
/**
|
|
198
311
|
* Typed browser error with a machine-routable, open-string `code` and chained
|
|
199
312
|
* `cause`. Consumers must tolerate provider-specific codes. Shared codes cover
|
package/lib/browser/types.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* provider registration, session lifecycle, cancellation, errors, and product
|
|
4
4
|
* configuration; providers differ only in what backs a session (an Electron
|
|
5
5
|
* `WebContentsView` in the desktop shell, a headless Chromium relay for
|
|
6
|
-
* remote deployments,
|
|
6
|
+
* remote deployments, and so on).
|
|
7
7
|
* @module dsh-browser/browser/types
|
|
8
8
|
*/
|
|
9
9
|
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
@@ -99,6 +99,91 @@ async function handle(op, msg) {
|
|
|
99
99
|
reply(msg.id, { ok: true, result });
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
+
case 'download': {
|
|
103
|
+
const viewId = msg.viewId;
|
|
104
|
+
const url = msg.url;
|
|
105
|
+
const savePath = msg.savePath;
|
|
106
|
+
if (viewId === undefined || typeof url !== 'string' || typeof savePath !== 'string') {
|
|
107
|
+
throw new Error('download missing viewId/url/savePath');
|
|
108
|
+
}
|
|
109
|
+
const entry = views.get(viewId);
|
|
110
|
+
if (entry === undefined)
|
|
111
|
+
throw new Error(`download: unknown view ${viewId}`);
|
|
112
|
+
// Fetch the URL inside the page context (keeps cookies/login), read
|
|
113
|
+
// the body as base64, and return it; the parent writes the file. This
|
|
114
|
+
// avoids Electron's download pipeline entirely (CDP debugger attach
|
|
115
|
+
// can interfere with will-download).
|
|
116
|
+
const result = await entry.webContentsView.webContents.debugger.sendCommand('Runtime.evaluate', {
|
|
117
|
+
expression: `(async () => {
|
|
118
|
+
const r = await fetch(${JSON.stringify(url)}, { credentials: 'include' })
|
|
119
|
+
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
120
|
+
const b = await r.arrayBuffer()
|
|
121
|
+
const bytes = new Uint8Array(b)
|
|
122
|
+
let bin = ''
|
|
123
|
+
for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000))
|
|
124
|
+
return btoa(bin)
|
|
125
|
+
})()`,
|
|
126
|
+
awaitPromise: true,
|
|
127
|
+
returnByValue: true,
|
|
128
|
+
});
|
|
129
|
+
const value = result.result?.value;
|
|
130
|
+
if (typeof value !== 'string') {
|
|
131
|
+
const detail = result.exceptionDetails;
|
|
132
|
+
throw new Error(`download failed: ${detail?.exception?.description ?? 'no data'}`);
|
|
133
|
+
}
|
|
134
|
+
reply(msg.id, { ok: true, result: { base64: value, savePath } });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
case 'flushAuth': {
|
|
138
|
+
const viewId = msg.viewId;
|
|
139
|
+
if (viewId === undefined)
|
|
140
|
+
throw new Error('flushAuth missing viewId');
|
|
141
|
+
const entry = views.get(viewId);
|
|
142
|
+
if (entry === undefined)
|
|
143
|
+
throw new Error(`flushAuth: unknown view ${viewId}`);
|
|
144
|
+
// Export the session's cookies so login state can be saved/restored
|
|
145
|
+
// across browser hosts (or shared with another machine).
|
|
146
|
+
const cookies = await entry.webContentsView.webContents.session.cookies.get({});
|
|
147
|
+
const exported = cookies.map(c => ({
|
|
148
|
+
url: `http${c.secure ? 's' : ''}://${c.domain.startsWith('.') ? c.domain.slice(1) : c.domain}${c.path}`,
|
|
149
|
+
name: c.name,
|
|
150
|
+
value: c.value,
|
|
151
|
+
domain: c.domain,
|
|
152
|
+
path: c.path,
|
|
153
|
+
secure: c.secure,
|
|
154
|
+
httpOnly: c.httpOnly,
|
|
155
|
+
expirationDate: c.expirationDate,
|
|
156
|
+
}));
|
|
157
|
+
reply(msg.id, { ok: true, result: { cookies: exported } });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case 'restoreAuth': {
|
|
161
|
+
const viewId = msg.viewId;
|
|
162
|
+
const cookies = msg.cookies;
|
|
163
|
+
if (viewId === undefined)
|
|
164
|
+
throw new Error('restoreAuth missing viewId');
|
|
165
|
+
const entry = views.get(viewId);
|
|
166
|
+
if (entry === undefined)
|
|
167
|
+
throw new Error(`restoreAuth: unknown view ${viewId}`);
|
|
168
|
+
if (!Array.isArray(cookies))
|
|
169
|
+
throw new Error('restoreAuth missing cookies array');
|
|
170
|
+
for (const c of cookies) {
|
|
171
|
+
if (typeof c.url !== 'string' || typeof c.name !== 'string' || typeof c.value !== 'string')
|
|
172
|
+
continue;
|
|
173
|
+
await entry.webContentsView.webContents.session.cookies.set({
|
|
174
|
+
url: c.url,
|
|
175
|
+
name: c.name,
|
|
176
|
+
value: c.value,
|
|
177
|
+
...typeof c.domain === 'string' ? { domain: c.domain } : {},
|
|
178
|
+
...typeof c.path === 'string' ? { path: c.path } : {},
|
|
179
|
+
...typeof c.secure === 'boolean' ? { secure: c.secure } : {},
|
|
180
|
+
...typeof c.httpOnly === 'boolean' ? { httpOnly: c.httpOnly } : {},
|
|
181
|
+
...typeof c.expirationDate === 'number' ? { expirationDate: c.expirationDate } : {},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
reply(msg.id, { ok: true, result: { restored: cookies.length } });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
102
187
|
default:
|
|
103
188
|
throw new Error(`unknown op ${op}`);
|
|
104
189
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Electron-backed browser provider: `WebContentsView` sessions driven over
|
|
3
|
-
* `webContents.debugger` (CDP). The provider itself does not import Electron
|
|
3
|
+
* `webContents.debugger` (CDP). The provider itself does not import Electron — it operates through the {@link ElectronBrowserViewHost} seam, which the
|
|
4
4
|
* desktop shell implements with real Electron objects. That keeps this
|
|
5
5
|
* package testable under plain Node and leaves the Electron dependency to the
|
|
6
6
|
* shell that owns the `BrowserWindow`.
|
|
7
7
|
* @module dsh-browser/browser-electron
|
|
8
8
|
*/
|
|
9
|
-
import type { BrowserContentRequest, BrowserContentResult, BrowserExecuteRequest, BrowserExecuteResult, BrowserOpenRequest, BrowserProvider, BrowserSessionId, BrowserSnapshotResult, BrowserTab } from '../browser/types.ts';
|
|
9
|
+
import type { BrowserChallenge, BrowserContentRequest, BrowserContentResult, BrowserExecuteRequest, BrowserExecuteResult, BrowserFillRequest, BrowserFillResult, BrowserHistoryEntry, BrowserOpenRequest, BrowserProvider, BrowserSessionId, BrowserSnapshotResult, BrowserTab, ExportedCookie } from '../browser/types.ts';
|
|
10
10
|
/** Stable provider id registered with `ctx.browser`. */
|
|
11
11
|
export declare const ELECTRON_BROWSER_PROVIDER_ID = "electron";
|
|
12
12
|
/**
|
|
@@ -111,12 +111,11 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
111
111
|
/** Usable whenever the host can create views (always in the desktop shell). */
|
|
112
112
|
available(): boolean;
|
|
113
113
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* cannot see. Closing the session resets this; the next open starts fresh.
|
|
114
|
+
* Open a NEW browser session with its own view. Every call mints a fresh
|
|
115
|
+
* session id and backing view; per-task reuse is owned by the caller (the
|
|
116
|
+
* tool layer caches one session per DSH task). Sessions are isolated from
|
|
117
|
+
* each other: each keeps its own tabs, active tab, and history, and only
|
|
118
|
+
* the active tab of a session is made visible.
|
|
120
119
|
*/
|
|
121
120
|
open(): Promise<BrowserSessionId>;
|
|
122
121
|
/** Open a URL in the active tab (default) or a new tab. */
|
|
@@ -137,6 +136,8 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
137
136
|
execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
|
|
138
137
|
/** Produce an AI-friendly snapshot of the active tab. */
|
|
139
138
|
snapshot(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserSnapshotResult>;
|
|
139
|
+
/** Check whether a human-verification challenge is blocking the active tab. */
|
|
140
|
+
detectChallenge(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserChallenge>;
|
|
140
141
|
/** Fetch page content in a requested format. */
|
|
141
142
|
content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
|
|
142
143
|
/** Click at viewport coordinates (CDP mousePressed + mouseReleased). */
|
|
@@ -148,12 +149,52 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
148
149
|
type(session: BrowserSessionId, request: {
|
|
149
150
|
readonly text: string;
|
|
150
151
|
}, signal?: AbortSignal): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Fill a form's fields in one batch. Runs one page-context script that
|
|
154
|
+
* resolves each field (selector, or name/label/placeholder among visible
|
|
155
|
+
* controls), sets its value with the native prototype setter (React/Vue
|
|
156
|
+
* controlled inputs included) plus input/change events, handles
|
|
157
|
+
* select/checkbox/radio/contenteditable, and optionally submits the form.
|
|
158
|
+
*/
|
|
159
|
+
fillForm(session: BrowserSessionId, request: BrowserFillRequest, signal?: AbortSignal): Promise<BrowserFillResult>;
|
|
160
|
+
/**
|
|
161
|
+
* Download a URL to a local file, keeping the session's cookies/login.
|
|
162
|
+
* Requires the self-hosted host (which implements view-level download); the
|
|
163
|
+
* desktop shell's embedded views delegate downloads to the real browser UI.
|
|
164
|
+
*/
|
|
165
|
+
download(session: BrowserSessionId, request: {
|
|
166
|
+
readonly url: string;
|
|
167
|
+
readonly savePath: string;
|
|
168
|
+
}, signal?: AbortSignal): Promise<{
|
|
169
|
+
readonly path: string;
|
|
170
|
+
}>;
|
|
171
|
+
/**
|
|
172
|
+
* Export the session's cookies (login state) as serializable objects.
|
|
173
|
+
* Self-hosted only; the desktop shell's embedded views use the real profile.
|
|
174
|
+
*/
|
|
175
|
+
flushAuth(session: BrowserSessionId): Promise<readonly ExportedCookie[]>;
|
|
176
|
+
/** Import cookies into the session (restore login state). Self-hosted only. */
|
|
177
|
+
restoreAuth(session: BrowserSessionId, cookies: readonly ExportedCookie[]): Promise<number>;
|
|
151
178
|
/** Capture the current page, optionally full-page. PNG only (CDP JPEG hangs on Electron 43). */
|
|
152
179
|
screenshot(session: BrowserSessionId, request?: {
|
|
153
180
|
readonly fullPage?: boolean;
|
|
181
|
+
readonly savePath?: string;
|
|
154
182
|
}, signal?: AbortSignal): Promise<{
|
|
155
183
|
readonly dataUrl: string;
|
|
184
|
+
readonly path?: string;
|
|
156
185
|
}>;
|
|
186
|
+
/** Append one operation to the session's history. */
|
|
187
|
+
private record;
|
|
188
|
+
/** Return the session's chronological operation log (newest last). */
|
|
189
|
+
history(session: BrowserSessionId): Promise<readonly BrowserHistoryEntry[]>;
|
|
190
|
+
/**
|
|
191
|
+
* Replay one recorded operation by sequence number. Navigate/click/type are
|
|
192
|
+
* re-issued against the current page; execute re-runs its script. The
|
|
193
|
+
* replayed step is appended to history as a new entry.
|
|
194
|
+
* @param session - the session id.
|
|
195
|
+
* @param seq - the recorded entry's sequence number to replay.
|
|
196
|
+
*/
|
|
197
|
+
replay(session: BrowserSessionId, seq: number): Promise<void>;
|
|
157
198
|
/** Close the session and destroy all its views. Idempotent. */
|
|
158
199
|
close(session: BrowserSessionId): Promise<void>;
|
|
159
200
|
/** Look up a session or throw the unknown-session error. */
|