dsh-builtin-browser 0.1.10 → 0.1.11
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.en.md +7 -1
- package/README.md +5 -0
- package/lib/browser-electron/host-main.js +101 -0
- package/lib/browser-electron/provider.d.ts +2 -0
- package/lib/browser-electron/provider.js +28 -11
- package/lib/browser-electron/remote-host.js +62 -40
- package/package.json +1 -1
- package/src/browser-electron/host-main.ts +80 -0
- package/src/browser-electron/provider.ts +34 -11
- package/src/browser-electron/remote-host.ts +61 -36
- package/src/types/electron-shim.d.ts +13 -0
package/README.en.md
CHANGED
|
@@ -120,9 +120,15 @@ The browser's **visible view**, the **browser column layout**, and the **column-
|
|
|
120
120
|
## Known limitations
|
|
121
121
|
|
|
122
122
|
- Screenshots are PNG-only (CDP JPEG hangs on Electron 43); JPEG awaits a non-CDP conversion path.
|
|
123
|
+
- Self-hosted captures prefer Electron's native `capturePage` (CDP `captureScreenshot` can hang with multiple views in the window); the target tab is raised before capturing.
|
|
124
|
+
- **Electron >= 40 is recommended**: 33.x has a compositor defect that intermittently breaks capture ("display surface not available"). The plugin automatically picks the newest Electron in the environment (peer dependency > `ELECTRON_PATH` > newest among anchors/pnpm store).
|
|
123
125
|
- `fullPage` capture is flaky under software compositing on some hosts.
|
|
124
|
-
-
|
|
126
|
+
- Sessions are isolated per task (each DSH session gets its own browser session with own tabs/history; concurrent tasks never interfere; calls within one task reuse the same session). Logins (cookies) are shared and can be exported/restored with `browser_auth`.
|
|
127
|
+
- CAPTCHA cannot be solved automatically: snapshots flag detected challenges (`browser_challenge` checks explicitly); ask the human to complete it in the shared window instead of retrying.
|
|
125
128
|
- Private mode (`privateMode`) is not implemented: it needs Electron session partitioning, which is host-layer territory; this plugin does not promise it.
|
|
129
|
+
- `browser_download` fetches in the page context (keeps logins) and is subject to same-origin/CORS constraints; single files are capped at 256 MB.
|
|
130
|
+
- The `browser_auth` cookie round-trip does not preserve `hostOnly`/`sameSite` (host-only cookies come back as domain cookies); it is available on the self-hosted browser only.
|
|
131
|
+
- After a self-hosted child crash the browser host restarts automatically, but sessions opened before the crash are gone — call `browser_reset_session` to rebuild.
|
|
126
132
|
- This plugin contains no browser-column UI — that is host-shell territory; do not treat "browser column" as a plugin feature.
|
|
127
133
|
|
|
128
134
|
## License
|
package/README.md
CHANGED
|
@@ -127,10 +127,15 @@ provider 按构造与 Electron 解耦:它通过 `ElectronBrowserViewHost` 接缝
|
|
|
127
127
|
## 已知限制
|
|
128
128
|
|
|
129
129
|
- 截图仅 PNG(CDP JPEG 在 Electron 43 上挂起);JPEG 等待非 CDP 转换路径。
|
|
130
|
+
- 自托管截图优先走 Electron 原生 `capturePage`(CDP `captureScreenshot` 在多视图下会挂起);截图前自动把目标标签置顶。
|
|
131
|
+
- **Electron 版本建议 ≥ 40**:33.x 存在合成器缺陷,会间歇性导致截图失败("display surface not available")。插件会**自动选择环境中最新版本的 Electron**(peer 依赖 > `ELECTRON_PATH` > 锚点/pnpm store 中最新版)。
|
|
130
132
|
- 部分主机在软件合成下 `fullPage` 截图不稳定。
|
|
131
133
|
- 会话按任务隔离:每个调用方任务(DSH 会话)拥有独立的浏览器会话(独立标签页与历史),并发任务互不干扰;同一任务的多次调用复用同一会话。登录态(cookie)为共享,可用 `browser_auth` 导出/恢复。
|
|
132
134
|
- 人机验证(CAPTCHA)无法自动解决:快照会标注检测到的挑战(`browser_challenge` 可显式检查),此时应请用户在共享窗口中人工完成,而不是反复重试。
|
|
133
135
|
- 无痕模式(`privateMode`)未实现:它需要 Electron 的 session 分区能力,属于宿主层,本插件不承诺。
|
|
136
|
+
- `browser_download` 在页面上下文内 `fetch`(带登录态),受同源/CORS 约束;单文件上限 256MB。
|
|
137
|
+
- `browser_auth` 的 cookie 往返不保留 `hostOnly`/`sameSite` 字段(host-only cookie 恢复后变成 domain cookie);仅自托管浏览器可用。
|
|
138
|
+
- 自托管浏览器子进程崩溃后会自动重启,但崩溃前已打开的会话视图已失效,调用 `browser_reset_session` 重建即可。
|
|
134
139
|
- 本插件不含浏览器列 UI——那是宿主外壳的配套,别把"浏览器列"当成插件能力。
|
|
135
140
|
|
|
136
141
|
## 许可证
|
|
@@ -16,6 +16,19 @@
|
|
|
16
16
|
import { app, BrowserWindow, WebContentsView } from 'electron';
|
|
17
17
|
import { createInterface } from 'node:readline';
|
|
18
18
|
import { createConnection } from 'node:net';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
// Isolate this host's profile from the DSH app's default Electron userData:
|
|
21
|
+
// several Electron instances sharing Roaming\Electron fight over the GPU
|
|
22
|
+
// cache/session locks, which can leave the window without a display surface
|
|
23
|
+
// (capturePage then fails). A dedicated userData also persists cookies across
|
|
24
|
+
// host restarts (on top of browser_auth). Must run before app is ready.
|
|
25
|
+
try {
|
|
26
|
+
const base = process.env.DSH_HOME ?? app.getPath('appData');
|
|
27
|
+
app.setPath('userData', join(base, 'dsh-builtin-browser-host'));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
process.stderr.write(`[dsh-browser host] userData setup failed: ${String(error)}\n`);
|
|
31
|
+
}
|
|
19
32
|
/** CDP protocol version attached to every view's debugger. */
|
|
20
33
|
const CDP_VERSION = '1.3';
|
|
21
34
|
/** Download cap: the body is shipped base64 as one JSON line; bound the memory. */
|
|
@@ -121,6 +134,94 @@ async function handle(op, msg) {
|
|
|
121
134
|
reply(msg.id, { ok: true, result });
|
|
122
135
|
return;
|
|
123
136
|
}
|
|
137
|
+
case 'capture': {
|
|
138
|
+
const viewId = msg.viewId;
|
|
139
|
+
if (viewId === undefined)
|
|
140
|
+
throw new Error('capture missing viewId');
|
|
141
|
+
const entry = views.get(viewId);
|
|
142
|
+
if (entry === undefined)
|
|
143
|
+
throw new Error(`capture: unknown view ${viewId}`);
|
|
144
|
+
// Two complementary paths, because each has a failure mode:
|
|
145
|
+
// - capturePage: fast and reliable with several WebContentsViews in
|
|
146
|
+
// the window, but needs a live display surface (fails when the
|
|
147
|
+
// window is minimized/occluded/unpainted).
|
|
148
|
+
// - CDP Page.captureScreenshot: works without a display surface, but
|
|
149
|
+
// can hang when another hidden WebContentsView exists in the window.
|
|
150
|
+
// Try capturePage first (show/focus/restore + one retry), then CDP.
|
|
151
|
+
if (window !== undefined) {
|
|
152
|
+
try {
|
|
153
|
+
if (!window.isVisible())
|
|
154
|
+
window.show();
|
|
155
|
+
}
|
|
156
|
+
catch { /* closing */ }
|
|
157
|
+
try {
|
|
158
|
+
window.restore();
|
|
159
|
+
}
|
|
160
|
+
catch { /* not minimized */ }
|
|
161
|
+
window.focus();
|
|
162
|
+
}
|
|
163
|
+
let base64 = '';
|
|
164
|
+
try {
|
|
165
|
+
let image;
|
|
166
|
+
try {
|
|
167
|
+
image = await entry.webContentsView.webContents.capturePage();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
process.stderr.write(`[dsh-browser host] capturePage failed: ${String(error)}\n`);
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
172
|
+
image = await entry.webContentsView.webContents.capturePage();
|
|
173
|
+
}
|
|
174
|
+
const png = image.toPNG();
|
|
175
|
+
if (png.length > 0)
|
|
176
|
+
base64 = png.toString('base64');
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const state = window !== undefined
|
|
180
|
+
? JSON.stringify({
|
|
181
|
+
win: { visible: window.isVisible(), minimized: window.isMinimized(), focused: window.isFocused() },
|
|
182
|
+
})
|
|
183
|
+
: 'no-window';
|
|
184
|
+
process.stderr.write(`[dsh-browser host] capturePage retry failed: ${String(error)} state=${state}\n`);
|
|
185
|
+
base64 = '';
|
|
186
|
+
}
|
|
187
|
+
if (base64 === '') {
|
|
188
|
+
// CDP fallback. Page.captureScreenshot can hang when OTHER views
|
|
189
|
+
// (especially hidden attach-first ones) are in the window, so
|
|
190
|
+
// temporarily detach the siblings, capture in single-view state,
|
|
191
|
+
// then restore them (target stays on top).
|
|
192
|
+
const siblings = [...views.values()].filter(v => v !== entry);
|
|
193
|
+
for (const v of siblings) {
|
|
194
|
+
try {
|
|
195
|
+
window?.contentView.removeChildView(v.webContentsView);
|
|
196
|
+
}
|
|
197
|
+
catch { /* already gone */ }
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const shot = await entry.webContentsView.webContents.debugger.sendCommand('Page.captureScreenshot', {});
|
|
201
|
+
const data = shot.data;
|
|
202
|
+
if (typeof data === 'string' && data.length > 0)
|
|
203
|
+
base64 = data;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
for (const v of siblings) {
|
|
207
|
+
try {
|
|
208
|
+
window?.contentView.addChildView(v.webContentsView);
|
|
209
|
+
}
|
|
210
|
+
catch { /* destroyed */ }
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
window?.contentView.removeChildView(entry.webContentsView);
|
|
214
|
+
window?.contentView.addChildView(entry.webContentsView);
|
|
215
|
+
}
|
|
216
|
+
catch { /* closing */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (base64 === '') {
|
|
220
|
+
throw new Error('capture produced no image (view not painted)');
|
|
221
|
+
}
|
|
222
|
+
reply(msg.id, { ok: true, result: { base64, width: 0, height: 0 } });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
124
225
|
case 'download': {
|
|
125
226
|
const viewId = msg.viewId;
|
|
126
227
|
const url = msg.url;
|
|
@@ -183,6 +183,8 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
183
183
|
readonly dataUrl: string;
|
|
184
184
|
readonly path?: string;
|
|
185
185
|
}>;
|
|
186
|
+
/** Build the data URL and optionally write the PNG to disk. */
|
|
187
|
+
private saveScreenshot;
|
|
186
188
|
/** Append one operation to the session's history. */
|
|
187
189
|
private record;
|
|
188
190
|
/** Return the session's chronological operation log (newest last). */
|
|
@@ -569,36 +569,53 @@ export class ElectronBrowserProvider {
|
|
|
569
569
|
}
|
|
570
570
|
/** Capture the current page, optionally full-page. PNG only (CDP JPEG hangs on Electron 43). */
|
|
571
571
|
async screenshot(session, request, signal) {
|
|
572
|
-
const
|
|
572
|
+
const s = this.session(session);
|
|
573
|
+
const { handle } = this.activeTab(s);
|
|
573
574
|
signal?.throwIfAborted();
|
|
575
|
+
// Native capturePage path (self-hosted): CDP Page.captureScreenshot can
|
|
576
|
+
// hang indefinitely on a view once another (hidden) WebContentsView exists
|
|
577
|
+
// in the window; capturePage is fast for visible views and resolves
|
|
578
|
+
// immediately (empty) for hidden ones.
|
|
579
|
+
const capturable = handle;
|
|
580
|
+
if (request?.fullPage !== true && typeof capturable.capture === 'function') {
|
|
581
|
+
// Ensure the target view is the visible one before capturing.
|
|
582
|
+
this.showActive(s);
|
|
583
|
+
const timeoutMs = 30_000;
|
|
584
|
+
const shot = await withTimeout(capturable.capture(), timeoutMs, signal, `browser: screenshot timed out after ${timeoutMs}ms`);
|
|
585
|
+
if (shot.base64 === '') {
|
|
586
|
+
throw new BrowserError('browser: capture returned an empty image (view not painted); retry shortly', 'BROWSER_SCREENSHOT_FAILED');
|
|
587
|
+
}
|
|
588
|
+
return this.saveScreenshot(shot.base64, request?.savePath);
|
|
589
|
+
}
|
|
590
|
+
// Fallback: a desktop-shell handle (no capture()) or a full-page capture
|
|
591
|
+
// uses CDP; full-page needs `captureBeyondViewport` which capturePage lacks.
|
|
574
592
|
const params = {};
|
|
575
593
|
if (request?.fullPage === true) {
|
|
576
594
|
// `captureBeyondViewport` captures the full scrollable content; without
|
|
577
595
|
// a clip this yields the full-page image (CDP default is the viewport).
|
|
578
596
|
params.captureBeyondViewport = true;
|
|
579
597
|
}
|
|
580
|
-
// Bound the capture: a view that has not painted can wedge
|
|
581
|
-
// Page.captureScreenshot indefinitely (observed after view re-showing).
|
|
582
598
|
const timeoutMs = 30_000;
|
|
583
599
|
const result = await withTimeout(handle.sendCommand(CDP_PAGE_CAPTURE_SCREENSHOT, params), timeoutMs, signal, `browser: screenshot timed out after ${timeoutMs}ms`);
|
|
584
600
|
const data = result.data;
|
|
585
601
|
if (typeof data !== 'string') {
|
|
586
602
|
throw new BrowserError('browser: screenshot returned no image data', 'BROWSER_SCREENSHOT_FAILED');
|
|
587
603
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
604
|
+
return this.saveScreenshot(data, request?.savePath);
|
|
605
|
+
}
|
|
606
|
+
/** Build the data URL and optionally write the PNG to disk. */
|
|
607
|
+
saveScreenshot(base64, savePath) {
|
|
608
|
+
if (savePath !== undefined) {
|
|
592
609
|
try {
|
|
593
|
-
writeFileSync(
|
|
594
|
-
return { dataUrl: `data:image/png;base64,${
|
|
610
|
+
writeFileSync(savePath, Buffer.from(base64, 'base64'));
|
|
611
|
+
return { dataUrl: `data:image/png;base64,${base64}`, path: savePath };
|
|
595
612
|
}
|
|
596
613
|
catch (error) {
|
|
597
614
|
// Report the write problem but keep the capture usable.
|
|
598
|
-
throw new BrowserError(`browser: screenshot save to "${
|
|
615
|
+
throw new BrowserError(`browser: screenshot save to "${savePath}" failed: ${String(error)}`, 'BROWSER_SCREENSHOT_SAVE_FAILED', { cause: error });
|
|
599
616
|
}
|
|
600
617
|
}
|
|
601
|
-
return { dataUrl: `data:image/png;base64,${
|
|
618
|
+
return { dataUrl: `data:image/png;base64,${base64}` };
|
|
602
619
|
}
|
|
603
620
|
/** Append one operation to the session's history. */
|
|
604
621
|
record(s, action, params, ok, detail) {
|
|
@@ -26,12 +26,13 @@ const READY_TIMEOUT_MS = 20_000;
|
|
|
26
26
|
/** Safety cap on a single RPC reply line (base64 downloads are the big ones). */
|
|
27
27
|
const MAX_RPC_BUFFER_BYTES = 512 * 1024 * 1024;
|
|
28
28
|
/**
|
|
29
|
-
* Locate the Electron binary
|
|
29
|
+
* Locate the Electron binary:
|
|
30
30
|
* 1. require('electron') from this module (optional peer dependency),
|
|
31
|
-
* 2.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* 2. ELECTRON_PATH (explicit override),
|
|
32
|
+
* 3. every DSH install anchor + pnpm virtual store candidate, choosing the
|
|
33
|
+
* NEWEST version found. Older Electron releases (e.g. 33.x) have a
|
|
34
|
+
* compositor defect that intermittently breaks page capture, so prefer
|
|
35
|
+
* the newest binary available in the environment.
|
|
35
36
|
* @returns the path to the Electron executable.
|
|
36
37
|
*/
|
|
37
38
|
function resolveElectronPath() {
|
|
@@ -45,8 +46,16 @@ function resolveElectronPath() {
|
|
|
45
46
|
catch {
|
|
46
47
|
// continue probing
|
|
47
48
|
}
|
|
48
|
-
// 2.
|
|
49
|
-
|
|
49
|
+
// 2. Explicit override (user intent wins).
|
|
50
|
+
const override = process.env.ELECTRON_PATH;
|
|
51
|
+
if (typeof override === 'string' && override.length > 0 && existsSync(override))
|
|
52
|
+
return override;
|
|
53
|
+
// 3. Collect every candidate (anchors + pnpm stores), then pick the newest.
|
|
54
|
+
const candidates = [];
|
|
55
|
+
const add = (version, path) => {
|
|
56
|
+
if (path !== undefined)
|
|
57
|
+
candidates.push({ version, path });
|
|
58
|
+
};
|
|
50
59
|
const anchors = [];
|
|
51
60
|
const globalPrefix = process.env.npm_config_prefix ?? process.env.PREFIX;
|
|
52
61
|
if (globalPrefix !== undefined) {
|
|
@@ -60,21 +69,13 @@ function resolveElectronPath() {
|
|
|
60
69
|
try {
|
|
61
70
|
const resolved = require.resolve('electron', { paths: [anchor] });
|
|
62
71
|
if (typeof resolved === 'string' && resolved.length > 0) {
|
|
63
|
-
|
|
64
|
-
// next to the package root.
|
|
65
|
-
const exe = electronExeBeside(resolved);
|
|
66
|
-
if (exe !== undefined)
|
|
67
|
-
return exe;
|
|
72
|
+
add(versionOf(resolved), electronExeBeside(resolved));
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
catch {
|
|
71
76
|
// continue probing
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
|
-
// 3. pnpm virtual store, walking up from several candidate roots: this
|
|
75
|
-
// module (its real path may differ from where the profile resolves it when
|
|
76
|
-
// installed via a junction/link), the process CWD, and the Node executable
|
|
77
|
-
// dir (DSH CLI typically runs from the workspace root).
|
|
78
79
|
const roots = new Set([
|
|
79
80
|
fileURLToPath(new URL('.', import.meta.url)),
|
|
80
81
|
process.cwd(),
|
|
@@ -85,9 +86,12 @@ function resolveElectronPath() {
|
|
|
85
86
|
for (let depth = 0; depth < 8; depth++) {
|
|
86
87
|
const store = join(dir, 'node_modules', '.pnpm');
|
|
87
88
|
if (existsSync(store)) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
for (const entry of readdirSync(store)) {
|
|
90
|
+
if (!entry.startsWith('electron@'))
|
|
91
|
+
continue;
|
|
92
|
+
const exe = electronDistExe(join(store, entry, 'node_modules', 'electron'));
|
|
93
|
+
add(entry.slice('electron@'.length), exe);
|
|
94
|
+
}
|
|
91
95
|
}
|
|
92
96
|
const parent = join(dir, '..');
|
|
93
97
|
if (parent === dir)
|
|
@@ -95,13 +99,31 @@ function resolveElectronPath() {
|
|
|
95
99
|
dir = parent;
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
if (candidates.length > 0) {
|
|
103
|
+
candidates.sort((a, b) => compareVersions(b.version, a.version));
|
|
104
|
+
const best = candidates[0];
|
|
105
|
+
if (best !== undefined)
|
|
106
|
+
return best.path;
|
|
107
|
+
}
|
|
102
108
|
throw new Error('dsh-builtin-browser: cannot locate the Electron binary. Install electron in the host profile ' +
|
|
103
109
|
'(e.g. `dsh plugin --profile web add electron`) or set ELECTRON_PATH to the electron executable.');
|
|
104
110
|
}
|
|
111
|
+
/** Extract an electron version like "43.4.0" from a path containing it. */
|
|
112
|
+
function versionOf(path) {
|
|
113
|
+
const match = /electron@(\d+\.\d+\.\d+)/.exec(path);
|
|
114
|
+
return match?.[1] ?? '0.0.0';
|
|
115
|
+
}
|
|
116
|
+
/** Compare dotted numeric versions; higher wins. */
|
|
117
|
+
function compareVersions(a, b) {
|
|
118
|
+
const pa = a.split('.').map(n => Number(n) || 0);
|
|
119
|
+
const pb = b.split('.').map(n => Number(n) || 0);
|
|
120
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
121
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
122
|
+
if (d !== 0)
|
|
123
|
+
return d;
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
105
127
|
/** From an electron package entry file, find the dist executable beside it. */
|
|
106
128
|
function electronExeBeside(entry) {
|
|
107
129
|
const candidates = [
|
|
@@ -116,6 +138,14 @@ function electronExeBeside(entry) {
|
|
|
116
138
|
}
|
|
117
139
|
return undefined;
|
|
118
140
|
}
|
|
141
|
+
/** From an electron package root, find its dist executable. */
|
|
142
|
+
function electronDistExe(pkgRoot) {
|
|
143
|
+
for (const candidate of [join(pkgRoot, 'dist', 'electron.exe'), join(pkgRoot, 'dist', 'electron')]) {
|
|
144
|
+
if (existsSync(candidate))
|
|
145
|
+
return candidate;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
119
149
|
/** dirname without importing node:path's dirname separately. */
|
|
120
150
|
function dirname(p) {
|
|
121
151
|
const i = p.lastIndexOf('/');
|
|
@@ -123,23 +153,6 @@ function dirname(p) {
|
|
|
123
153
|
const k = Math.max(i, j);
|
|
124
154
|
return k < 0 ? p : p.slice(0, k);
|
|
125
155
|
}
|
|
126
|
-
/** Search a pnpm virtual store for an Electron dist binary. */
|
|
127
|
-
function findElectronExe(store) {
|
|
128
|
-
const entries = existsSync(store) ? readdirSync(store) : [];
|
|
129
|
-
for (const entry of entries) {
|
|
130
|
-
if (!entry.startsWith('electron@'))
|
|
131
|
-
continue;
|
|
132
|
-
const candidates = [
|
|
133
|
-
join(store, entry, 'node_modules', 'electron', 'dist', 'electron.exe'),
|
|
134
|
-
join(store, entry, 'node_modules', 'electron', 'dist', 'electron'),
|
|
135
|
-
];
|
|
136
|
-
for (const candidate of candidates) {
|
|
137
|
-
if (existsSync(candidate))
|
|
138
|
-
return candidate;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
156
|
/**
|
|
144
157
|
* Line-delimited JSON-RPC client over a local TCP socket. Electron's main
|
|
145
158
|
* process on Windows does not receive piped stdin, so the parent listens on a
|
|
@@ -164,6 +177,7 @@ class ElectronChildClient {
|
|
|
164
177
|
this.port = port;
|
|
165
178
|
this.onExit = onExit;
|
|
166
179
|
const electron = resolveElectronPath();
|
|
180
|
+
process.stderr.write(`[dsh-browser host] spawning electron: ${electron}\n`);
|
|
167
181
|
// ELECTRON_RUN_AS_NODE (even an empty string) makes Electron run as plain
|
|
168
182
|
// Node, breaking require('electron'); NODE_OPTIONS can inject flags that
|
|
169
183
|
// break the child. Rebuild the env without either.
|
|
@@ -310,6 +324,10 @@ class RemoteView {
|
|
|
310
324
|
const result = await this.client.call('download', { viewId: this.id, url, savePath });
|
|
311
325
|
writeFileSync(savePath, Buffer.from(result.base64, 'base64'));
|
|
312
326
|
}
|
|
327
|
+
/** Native capturePage snapshot of the view (PNG base64 + size). */
|
|
328
|
+
capture() {
|
|
329
|
+
return this.client.call('capture', { viewId: this.id });
|
|
330
|
+
}
|
|
313
331
|
/** Export the session's cookies (login state). */
|
|
314
332
|
flushAuth() {
|
|
315
333
|
return this.client.call('flushAuth', { viewId: this.id }).then(r => r.cookies);
|
|
@@ -480,6 +498,10 @@ class DeferredRemoteView {
|
|
|
480
498
|
const view = await this.materializeOnce();
|
|
481
499
|
return view.download(url, savePath);
|
|
482
500
|
}
|
|
501
|
+
async capture() {
|
|
502
|
+
const view = await this.materializeOnce();
|
|
503
|
+
return view.capture();
|
|
504
|
+
}
|
|
483
505
|
async flushAuth() {
|
|
484
506
|
const view = await this.materializeOnce();
|
|
485
507
|
return view.flushAuth();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-builtin-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Shared real browser plugin for DeepSeek Harness: install-and-use — a visible native browser the human can take over, driven by the agent over CDP. DOM-level interaction (React/Vue safe), per-task session isolation, cookie persistence (browser_auth), CAPTCHA detection, batch form fill (browser_fill), authenticated download, history/replay, action restriction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -17,6 +17,19 @@
|
|
|
17
17
|
import { app, BrowserWindow, WebContentsView } from 'electron'
|
|
18
18
|
import { createInterface } from 'node:readline'
|
|
19
19
|
import { createConnection } from 'node:net'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
// Isolate this host's profile from the DSH app's default Electron userData:
|
|
23
|
+
// several Electron instances sharing Roaming\Electron fight over the GPU
|
|
24
|
+
// cache/session locks, which can leave the window without a display surface
|
|
25
|
+
// (capturePage then fails). A dedicated userData also persists cookies across
|
|
26
|
+
// host restarts (on top of browser_auth). Must run before app is ready.
|
|
27
|
+
try {
|
|
28
|
+
const base = process.env.DSH_HOME ?? app.getPath('appData')
|
|
29
|
+
app.setPath('userData', join(base, 'dsh-builtin-browser-host'))
|
|
30
|
+
} catch (error) {
|
|
31
|
+
process.stderr.write(`[dsh-browser host] userData setup failed: ${String(error)}\n`)
|
|
32
|
+
}
|
|
20
33
|
|
|
21
34
|
/** CDP protocol version attached to every view's debugger. */
|
|
22
35
|
const CDP_VERSION = '1.3'
|
|
@@ -120,6 +133,73 @@ async function handle(op: string, msg: { id: number; viewId?: string; method?: s
|
|
|
120
133
|
reply(msg.id, { ok: true, result })
|
|
121
134
|
return
|
|
122
135
|
}
|
|
136
|
+
case 'capture': {
|
|
137
|
+
const viewId = msg.viewId
|
|
138
|
+
if (viewId === undefined) throw new Error('capture missing viewId')
|
|
139
|
+
const entry = views.get(viewId)
|
|
140
|
+
if (entry === undefined) throw new Error(`capture: unknown view ${viewId}`)
|
|
141
|
+
// Two complementary paths, because each has a failure mode:
|
|
142
|
+
// - capturePage: fast and reliable with several WebContentsViews in
|
|
143
|
+
// the window, but needs a live display surface (fails when the
|
|
144
|
+
// window is minimized/occluded/unpainted).
|
|
145
|
+
// - CDP Page.captureScreenshot: works without a display surface, but
|
|
146
|
+
// can hang when another hidden WebContentsView exists in the window.
|
|
147
|
+
// Try capturePage first (show/focus/restore + one retry), then CDP.
|
|
148
|
+
if (window !== undefined) {
|
|
149
|
+
try { if (!window.isVisible()) window.show() } catch { /* closing */ }
|
|
150
|
+
try { window.restore() } catch { /* not minimized */ }
|
|
151
|
+
window.focus()
|
|
152
|
+
}
|
|
153
|
+
let base64 = ''
|
|
154
|
+
try {
|
|
155
|
+
let image
|
|
156
|
+
try {
|
|
157
|
+
image = await entry.webContentsView.webContents.capturePage()
|
|
158
|
+
} catch (error) {
|
|
159
|
+
process.stderr.write(`[dsh-browser host] capturePage failed: ${String(error)}\n`)
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
161
|
+
image = await entry.webContentsView.webContents.capturePage()
|
|
162
|
+
}
|
|
163
|
+
const png = image.toPNG()
|
|
164
|
+
if (png.length > 0) base64 = png.toString('base64')
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const state = window !== undefined
|
|
167
|
+
? JSON.stringify({
|
|
168
|
+
win: { visible: window.isVisible(), minimized: window.isMinimized(), focused: window.isFocused() },
|
|
169
|
+
})
|
|
170
|
+
: 'no-window'
|
|
171
|
+
process.stderr.write(`[dsh-browser host] capturePage retry failed: ${String(error)} state=${state}\n`)
|
|
172
|
+
base64 = ''
|
|
173
|
+
}
|
|
174
|
+
if (base64 === '') {
|
|
175
|
+
// CDP fallback. Page.captureScreenshot can hang when OTHER views
|
|
176
|
+
// (especially hidden attach-first ones) are in the window, so
|
|
177
|
+
// temporarily detach the siblings, capture in single-view state,
|
|
178
|
+
// then restore them (target stays on top).
|
|
179
|
+
const siblings = [...views.values()].filter(v => v !== entry)
|
|
180
|
+
for (const v of siblings) {
|
|
181
|
+
try { window?.contentView.removeChildView(v.webContentsView) } catch { /* already gone */ }
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const shot = await entry.webContentsView.webContents.debugger.sendCommand('Page.captureScreenshot', {})
|
|
185
|
+
const data = (shot as { data?: unknown }).data
|
|
186
|
+
if (typeof data === 'string' && data.length > 0) base64 = data
|
|
187
|
+
} finally {
|
|
188
|
+
for (const v of siblings) {
|
|
189
|
+
try { window?.contentView.addChildView(v.webContentsView) } catch { /* destroyed */ }
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
window?.contentView.removeChildView(entry.webContentsView)
|
|
193
|
+
window?.contentView.addChildView(entry.webContentsView)
|
|
194
|
+
} catch { /* closing */ }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (base64 === '') {
|
|
198
|
+
throw new Error('capture produced no image (view not painted)')
|
|
199
|
+
}
|
|
200
|
+
reply(msg.id, { ok: true, result: { base64, width: 0, height: 0 } })
|
|
201
|
+
return
|
|
202
|
+
}
|
|
123
203
|
case 'download': {
|
|
124
204
|
const viewId = msg.viewId
|
|
125
205
|
const url = msg.url
|
|
@@ -743,16 +743,37 @@ export class ElectronBrowserProvider implements BrowserProvider {
|
|
|
743
743
|
request?: { readonly fullPage?: boolean; readonly savePath?: string },
|
|
744
744
|
signal?: AbortSignal,
|
|
745
745
|
): Promise<{ readonly dataUrl: string; readonly path?: string }> {
|
|
746
|
-
const
|
|
746
|
+
const s = this.session(session)
|
|
747
|
+
const { handle } = this.activeTab(s)
|
|
747
748
|
signal?.throwIfAborted()
|
|
749
|
+
// Native capturePage path (self-hosted): CDP Page.captureScreenshot can
|
|
750
|
+
// hang indefinitely on a view once another (hidden) WebContentsView exists
|
|
751
|
+
// in the window; capturePage is fast for visible views and resolves
|
|
752
|
+
// immediately (empty) for hidden ones.
|
|
753
|
+
const capturable = handle as { capture?(): Promise<{ base64: string; width: number; height: number }> }
|
|
754
|
+
if (request?.fullPage !== true && typeof capturable.capture === 'function') {
|
|
755
|
+
// Ensure the target view is the visible one before capturing.
|
|
756
|
+
this.showActive(s)
|
|
757
|
+
const timeoutMs = 30_000
|
|
758
|
+
const shot = await withTimeout(
|
|
759
|
+
capturable.capture(),
|
|
760
|
+
timeoutMs,
|
|
761
|
+
signal,
|
|
762
|
+
`browser: screenshot timed out after ${timeoutMs}ms`,
|
|
763
|
+
)
|
|
764
|
+
if (shot.base64 === '') {
|
|
765
|
+
throw new BrowserError('browser: capture returned an empty image (view not painted); retry shortly', 'BROWSER_SCREENSHOT_FAILED')
|
|
766
|
+
}
|
|
767
|
+
return this.saveScreenshot(shot.base64, request?.savePath)
|
|
768
|
+
}
|
|
769
|
+
// Fallback: a desktop-shell handle (no capture()) or a full-page capture
|
|
770
|
+
// uses CDP; full-page needs `captureBeyondViewport` which capturePage lacks.
|
|
748
771
|
const params: Record<string, unknown> = {}
|
|
749
772
|
if (request?.fullPage === true) {
|
|
750
773
|
// `captureBeyondViewport` captures the full scrollable content; without
|
|
751
774
|
// a clip this yields the full-page image (CDP default is the viewport).
|
|
752
775
|
params.captureBeyondViewport = true
|
|
753
776
|
}
|
|
754
|
-
// Bound the capture: a view that has not painted can wedge
|
|
755
|
-
// Page.captureScreenshot indefinitely (observed after view re-showing).
|
|
756
777
|
const timeoutMs = 30_000
|
|
757
778
|
const result = await withTimeout(
|
|
758
779
|
handle.sendCommand(CDP_PAGE_CAPTURE_SCREENSHOT, params),
|
|
@@ -764,19 +785,21 @@ export class ElectronBrowserProvider implements BrowserProvider {
|
|
|
764
785
|
if (typeof data !== 'string') {
|
|
765
786
|
throw new BrowserError('browser: screenshot returned no image data', 'BROWSER_SCREENSHOT_FAILED')
|
|
766
787
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
788
|
+
return this.saveScreenshot(data, request?.savePath)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Build the data URL and optionally write the PNG to disk. */
|
|
792
|
+
private saveScreenshot(base64: string, savePath?: string): { dataUrl: string; path?: string } {
|
|
793
|
+
if (savePath !== undefined) {
|
|
771
794
|
try {
|
|
772
|
-
writeFileSync(
|
|
773
|
-
return { dataUrl: `data:image/png;base64,${
|
|
795
|
+
writeFileSync(savePath, Buffer.from(base64, 'base64'))
|
|
796
|
+
return { dataUrl: `data:image/png;base64,${base64}`, path: savePath }
|
|
774
797
|
} catch (error) {
|
|
775
798
|
// Report the write problem but keep the capture usable.
|
|
776
|
-
throw new BrowserError(`browser: screenshot save to "${
|
|
799
|
+
throw new BrowserError(`browser: screenshot save to "${savePath}" failed: ${String(error)}`, 'BROWSER_SCREENSHOT_SAVE_FAILED', { cause: error })
|
|
777
800
|
}
|
|
778
801
|
}
|
|
779
|
-
return { dataUrl: `data:image/png;base64,${
|
|
802
|
+
return { dataUrl: `data:image/png;base64,${base64}` }
|
|
780
803
|
}
|
|
781
804
|
|
|
782
805
|
/** Append one operation to the session's history. */
|
|
@@ -32,12 +32,13 @@ const READY_TIMEOUT_MS = 20_000
|
|
|
32
32
|
const MAX_RPC_BUFFER_BYTES = 512 * 1024 * 1024
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Locate the Electron binary
|
|
35
|
+
* Locate the Electron binary:
|
|
36
36
|
* 1. require('electron') from this module (optional peer dependency),
|
|
37
|
-
* 2.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* 2. ELECTRON_PATH (explicit override),
|
|
38
|
+
* 3. every DSH install anchor + pnpm virtual store candidate, choosing the
|
|
39
|
+
* NEWEST version found. Older Electron releases (e.g. 33.x) have a
|
|
40
|
+
* compositor defect that intermittently breaks page capture, so prefer
|
|
41
|
+
* the newest binary available in the environment.
|
|
41
42
|
* @returns the path to the Electron executable.
|
|
42
43
|
*/
|
|
43
44
|
function resolveElectronPath(): string {
|
|
@@ -49,8 +50,14 @@ function resolveElectronPath(): string {
|
|
|
49
50
|
} catch {
|
|
50
51
|
// continue probing
|
|
51
52
|
}
|
|
52
|
-
// 2.
|
|
53
|
-
|
|
53
|
+
// 2. Explicit override (user intent wins).
|
|
54
|
+
const override = process.env.ELECTRON_PATH
|
|
55
|
+
if (typeof override === 'string' && override.length > 0 && existsSync(override)) return override
|
|
56
|
+
// 3. Collect every candidate (anchors + pnpm stores), then pick the newest.
|
|
57
|
+
const candidates: Array<{ version: string; path: string }> = []
|
|
58
|
+
const add = (version: string, path: string | undefined): void => {
|
|
59
|
+
if (path !== undefined) candidates.push({ version, path })
|
|
60
|
+
}
|
|
54
61
|
const anchors: string[] = []
|
|
55
62
|
const globalPrefix = process.env.npm_config_prefix ?? process.env.PREFIX
|
|
56
63
|
if (globalPrefix !== undefined) {
|
|
@@ -64,19 +71,12 @@ function resolveElectronPath(): string {
|
|
|
64
71
|
try {
|
|
65
72
|
const resolved: unknown = require.resolve('electron', { paths: [anchor] })
|
|
66
73
|
if (typeof resolved === 'string' && resolved.length > 0) {
|
|
67
|
-
|
|
68
|
-
// next to the package root.
|
|
69
|
-
const exe = electronExeBeside(resolved)
|
|
70
|
-
if (exe !== undefined) return exe
|
|
74
|
+
add(versionOf(resolved), electronExeBeside(resolved))
|
|
71
75
|
}
|
|
72
76
|
} catch {
|
|
73
77
|
// continue probing
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
|
-
// 3. pnpm virtual store, walking up from several candidate roots: this
|
|
77
|
-
// module (its real path may differ from where the profile resolves it when
|
|
78
|
-
// installed via a junction/link), the process CWD, and the Node executable
|
|
79
|
-
// dir (DSH CLI typically runs from the workspace root).
|
|
80
80
|
const roots = new Set<string>([
|
|
81
81
|
fileURLToPath(new URL('.', import.meta.url)),
|
|
82
82
|
process.cwd(),
|
|
@@ -87,23 +87,45 @@ function resolveElectronPath(): string {
|
|
|
87
87
|
for (let depth = 0; depth < 8; depth++) {
|
|
88
88
|
const store = join(dir, 'node_modules', '.pnpm')
|
|
89
89
|
if (existsSync(store)) {
|
|
90
|
-
const
|
|
91
|
-
|
|
90
|
+
for (const entry of readdirSync(store)) {
|
|
91
|
+
if (!entry.startsWith('electron@')) continue
|
|
92
|
+
const exe = electronDistExe(join(store, entry, 'node_modules', 'electron'))
|
|
93
|
+
add(entry.slice('electron@'.length), exe)
|
|
94
|
+
}
|
|
92
95
|
}
|
|
93
96
|
const parent = join(dir, '..')
|
|
94
97
|
if (parent === dir) break
|
|
95
98
|
dir = parent
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
if (candidates.length > 0) {
|
|
102
|
+
candidates.sort((a, b) => compareVersions(b.version, a.version))
|
|
103
|
+
const best = candidates[0]
|
|
104
|
+
if (best !== undefined) return best.path
|
|
105
|
+
}
|
|
101
106
|
throw new Error(
|
|
102
107
|
'dsh-builtin-browser: cannot locate the Electron binary. Install electron in the host profile ' +
|
|
103
108
|
'(e.g. `dsh plugin --profile web add electron`) or set ELECTRON_PATH to the electron executable.',
|
|
104
109
|
)
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
/** Extract an electron version like "43.4.0" from a path containing it. */
|
|
113
|
+
function versionOf(path: string): string {
|
|
114
|
+
const match = /electron@(\d+\.\d+\.\d+)/.exec(path)
|
|
115
|
+
return match?.[1] ?? '0.0.0'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Compare dotted numeric versions; higher wins. */
|
|
119
|
+
function compareVersions(a: string, b: string): number {
|
|
120
|
+
const pa = a.split('.').map(n => Number(n) || 0)
|
|
121
|
+
const pb = b.split('.').map(n => Number(n) || 0)
|
|
122
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
123
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0)
|
|
124
|
+
if (d !== 0) return d
|
|
125
|
+
}
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
/** From an electron package entry file, find the dist executable beside it. */
|
|
108
130
|
function electronExeBeside(entry: string): string | undefined {
|
|
109
131
|
const candidates = [
|
|
@@ -118,6 +140,14 @@ function electronExeBeside(entry: string): string | undefined {
|
|
|
118
140
|
return undefined
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
/** From an electron package root, find its dist executable. */
|
|
144
|
+
function electronDistExe(pkgRoot: string): string | undefined {
|
|
145
|
+
for (const candidate of [join(pkgRoot, 'dist', 'electron.exe'), join(pkgRoot, 'dist', 'electron')]) {
|
|
146
|
+
if (existsSync(candidate)) return candidate
|
|
147
|
+
}
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
/** dirname without importing node:path's dirname separately. */
|
|
122
152
|
function dirname(p: string): string {
|
|
123
153
|
const i = p.lastIndexOf('/')
|
|
@@ -126,22 +156,6 @@ function dirname(p: string): string {
|
|
|
126
156
|
return k < 0 ? p : p.slice(0, k)
|
|
127
157
|
}
|
|
128
158
|
|
|
129
|
-
/** Search a pnpm virtual store for an Electron dist binary. */
|
|
130
|
-
function findElectronExe(store: string): string | undefined {
|
|
131
|
-
const entries = existsSync(store) ? readdirSync(store) : []
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
if (!entry.startsWith('electron@')) continue
|
|
134
|
-
const candidates = [
|
|
135
|
-
join(store, entry, 'node_modules', 'electron', 'dist', 'electron.exe'),
|
|
136
|
-
join(store, entry, 'node_modules', 'electron', 'dist', 'electron'),
|
|
137
|
-
]
|
|
138
|
-
for (const candidate of candidates) {
|
|
139
|
-
if (existsSync(candidate)) return candidate
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return undefined
|
|
143
|
-
}
|
|
144
|
-
|
|
145
159
|
/** One RPC round-trip with the child. */
|
|
146
160
|
interface Pending {
|
|
147
161
|
resolve(result: unknown): void
|
|
@@ -171,6 +185,7 @@ class ElectronChildClient {
|
|
|
171
185
|
private readonly onExit?: () => void,
|
|
172
186
|
) {
|
|
173
187
|
const electron = resolveElectronPath()
|
|
188
|
+
process.stderr.write(`[dsh-browser host] spawning electron: ${electron}\n`)
|
|
174
189
|
// ELECTRON_RUN_AS_NODE (even an empty string) makes Electron run as plain
|
|
175
190
|
// Node, breaking require('electron'); NODE_OPTIONS can inject flags that
|
|
176
191
|
// break the child. Rebuild the env without either.
|
|
@@ -308,6 +323,11 @@ class RemoteView implements ElectronViewHandle {
|
|
|
308
323
|
writeFileSync(savePath, Buffer.from(result.base64, 'base64'))
|
|
309
324
|
}
|
|
310
325
|
|
|
326
|
+
/** Native capturePage snapshot of the view (PNG base64 + size). */
|
|
327
|
+
capture(): Promise<{ base64: string; width: number; height: number }> {
|
|
328
|
+
return this.client.call<{ base64: string; width: number; height: number }>('capture', { viewId: this.id })
|
|
329
|
+
}
|
|
330
|
+
|
|
311
331
|
/** Export the session's cookies (login state). */
|
|
312
332
|
flushAuth(): Promise<ExportedCookie[]> {
|
|
313
333
|
return this.client.call<{ cookies: ExportedCookie[] }>('flushAuth', { viewId: this.id }).then(r => r.cookies)
|
|
@@ -483,6 +503,11 @@ class DeferredRemoteView implements ElectronViewHandle {
|
|
|
483
503
|
return view.download(url, savePath)
|
|
484
504
|
}
|
|
485
505
|
|
|
506
|
+
async capture(): Promise<{ base64: string; width: number; height: number }> {
|
|
507
|
+
const view = await this.materializeOnce()
|
|
508
|
+
return view.capture()
|
|
509
|
+
}
|
|
510
|
+
|
|
486
511
|
async flushAuth(): Promise<ExportedCookie[]> {
|
|
487
512
|
const view = await this.materializeOnce()
|
|
488
513
|
return view.flushAuth()
|
|
@@ -41,12 +41,17 @@ declare module 'electron' {
|
|
|
41
41
|
set(details: CookieSetter): Promise<void>
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
export interface NativeImage {
|
|
45
|
+
toPNG(): Buffer
|
|
46
|
+
getSize(): { width: number; height: number }
|
|
47
|
+
}
|
|
44
48
|
export interface WebContents {
|
|
45
49
|
readonly id: number
|
|
46
50
|
readonly debugger: WebContentsDebugger
|
|
47
51
|
readonly session: Session
|
|
48
52
|
close(): void
|
|
49
53
|
downloadURL(url: string): void
|
|
54
|
+
capturePage(): Promise<NativeImage>
|
|
50
55
|
}
|
|
51
56
|
export interface WebContentsView {
|
|
52
57
|
readonly webContents: WebContents
|
|
@@ -62,6 +67,12 @@ declare module 'electron' {
|
|
|
62
67
|
readonly children: WebContentsView[]
|
|
63
68
|
}
|
|
64
69
|
getContentSize(): [number, number]
|
|
70
|
+
isVisible(): boolean
|
|
71
|
+
isMinimized(): boolean
|
|
72
|
+
isFocused(): boolean
|
|
73
|
+
show(): void
|
|
74
|
+
restore(): void
|
|
75
|
+
focus(): void
|
|
65
76
|
on(event: 'closed', listener: () => void): this
|
|
66
77
|
on(event: 'resize', listener: () => void): this
|
|
67
78
|
off(event: 'closed', listener: () => void): this
|
|
@@ -70,6 +81,8 @@ declare module 'electron' {
|
|
|
70
81
|
export const app: {
|
|
71
82
|
whenReady(): Promise<void>
|
|
72
83
|
exit(code?: number): void
|
|
84
|
+
getPath(name: string): string
|
|
85
|
+
setPath(name: string, path: string): void
|
|
73
86
|
}
|
|
74
87
|
export interface BrowserWindowConstructor {
|
|
75
88
|
new(options?: Record<string, unknown>): BrowserWindow
|