dsh-builtin-browser 0.1.5 → 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 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
- - 会话生命周期为插件级(模型共享一个会话),尚未做到按 agent 隔离。
113
+ - 会话按任务隔离:每个调用方任务(DSH 会话)拥有独立的浏览器会话(独立标签页与历史),并发任务互不干扰;同一任务的多次调用复用同一会话。登录态(cookie)为共享,可用 `browser_auth` 导出/恢复。
114
+ - 人机验证(CAPTCHA)无法自动解决:快照会标注检测到的挑战(`browser_challenge` 可显式检查),此时应请用户在共享窗口中人工完成,而不是反复重试。
107
115
  - 无痕模式(`privateMode`)未实现:它需要 Electron 的 session 分区能力,属于宿主层,本插件不承诺。
108
116
  - 本插件不含浏览器列 UI——那是宿主外壳的配套,别把"浏览器列"当成插件能力。
109
117
 
@@ -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()` 鈫?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`.
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
  }
@@ -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()` 鈫?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`.
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 鈥?discard the (always-resolved) promise.
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);
@@ -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 鈫?agentId` uses). The id is opaque to the wire; only the provider
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 鈥?the same space the human interacts
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 鈥?DOM-referenced, not screen-coordinate-based:
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
  */
@@ -64,6 +129,8 @@ export interface BrowserExecuteRequest {
64
129
  readonly script: string;
65
130
  /** Optional arguments injected into the script scope as `arguments[0..n]`. */
66
131
  readonly args?: readonly unknown[];
132
+ /** Evaluation budget in ms; a wedged page rejects with a timeout. Default 30000. */
133
+ readonly timeoutMs?: number;
67
134
  }
68
135
  /**
69
136
  * Result of an execute. Mirrors CDP `Runtime.evaluate` semantics: either a
@@ -95,7 +162,7 @@ export interface BrowserSnapshotElement {
95
162
  }
96
163
  /**
97
164
  * AI-friendly page snapshot: a compact, numbered inventory of interactive
98
- * elements plus page facts. Not raw HTML 鈥?the model dialogues with this.
165
+ * elements plus page facts. Not raw HTML — the model dialogues with this.
99
166
  */
100
167
  export interface BrowserSnapshotResult {
101
168
  /** Final page URL. */
@@ -106,6 +173,21 @@ export interface BrowserSnapshotResult {
106
173
  readonly elements: readonly BrowserSnapshotElement[];
107
174
  /** True when the snapshot was truncated (element cap reached). */
108
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;
109
191
  }
110
192
  /**
111
193
  * Content fetch formats. `html` is the raw DOM; `markdown` is a structured
@@ -161,7 +243,7 @@ export interface BrowserProvider {
161
243
  available(): boolean;
162
244
  /**
163
245
  * 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
246
+ * backing surface (a view, a headless page, and so on). Sessions are isolated from
165
247
  * each other; a session must not be visible to any other session's caller.
166
248
  */
167
249
  open(): Promise<BrowserSessionId>;
@@ -169,7 +251,7 @@ export interface BrowserProvider {
169
251
  openUrl(session: BrowserSessionId, request: BrowserOpenRequest, signal?: AbortSignal): Promise<void>;
170
252
  /** List the session's tabs. */
171
253
  listTabs(session: BrowserSessionId): Promise<readonly BrowserTab[]>;
172
- /** Switch to a tab by id. Unknown id 鈫?`BROWSER_TAB_UNKNOWN`. */
254
+ /** Switch to a tab by id. Unknown id -> `BROWSER_TAB_UNKNOWN`. */
173
255
  switchTab(session: BrowserSessionId, tabId: string): Promise<void>;
174
256
  /** Close one tab. Closing the active tab activates the next. */
175
257
  closeTab(session: BrowserSessionId, tabId: string): Promise<void>;
@@ -181,17 +263,50 @@ export interface BrowserProvider {
181
263
  execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
182
264
  /** Produce an AI-friendly snapshot of the active tab. */
183
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>;
184
268
  /** Fetch page content in a requested format. */
185
269
  content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
186
270
  /** Click at viewport coordinates (fallback path; execute is preferred). Honor `signal` for cancellation. */
187
271
  click(session: BrowserSessionId, request: BrowserClickRequest, signal?: AbortSignal): Promise<void>;
188
272
  /** Type into the focused element (fallback path). Honor `signal` for cancellation. */
189
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>;
190
276
  /** Capture the current page. Honor `signal` for cancellation. */
191
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>;
192
290
  /** Close the session and destroy its backing surface. Idempotent. */
193
291
  close(session: BrowserSessionId): Promise<void>;
194
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
+ }
195
310
  /**
196
311
  * Typed browser error with a machine-routable, open-string `code` and chained
197
312
  * `cause`. Consumers must tolerate provider-specific codes. Shared codes cover
@@ -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 鈥? * it operates through the {@link ElectronBrowserViewHost} seam, which the
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
- * Get the shared browser session: the one the host already owns, or a new
115
- * one on first use. Shared-browser semantics (Cherry Studio's
116
- * `getOrCreateWindow` pattern): the host's startup view and every later
117
- * agent call must land on the SAME session and view, so opening again
118
- * returns the existing session instead of creating a second view the human
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. */