dsh-builtin-browser 0.1.21 → 0.1.23
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 +22 -10
- package/README.md +22 -10
- package/docs/README.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/tool-reference.md +2 -2
- package/docs/user-guide.md +6 -6
- package/lib/browser/runtime.js +9 -1
- package/lib/browser-electron/host-main.d.ts +1 -0
- package/lib/browser-electron/host-main.js +157 -15
- package/lib/browser-electron/provider.d.ts +107 -12
- package/lib/browser-electron/provider.js +340 -50
- package/lib/browser-electron/remote-host.d.ts +46 -6
- package/lib/browser-electron/remote-host.js +147 -33
- package/lib/tool-browser/index.js +2 -2
- package/package.json +8 -6
- package/src/browser/runtime.ts +15 -1
- package/src/browser-electron/host-main.ts +163 -9
- package/src/browser-electron/provider.ts +385 -55
- package/src/browser-electron/remote-host.ts +149 -31
- package/src/tool-browser/index.ts +2 -2
|
@@ -83,6 +83,12 @@ declare function resolveElectronPathImpl(inputs: ElectronPathInputs): string;
|
|
|
83
83
|
* resources dir at all) likewise cannot run a script argument.
|
|
84
84
|
*/
|
|
85
85
|
declare function isBareElectron(exe: string): boolean;
|
|
86
|
+
/** Candidate executable layouts used by the Electron npm package on one platform. */
|
|
87
|
+
declare function electronDistCandidates(pkgRoot: string, platform?: NodeJS.Platform): string[];
|
|
88
|
+
/** From an electron package entry file, find the dist executable beside it. */
|
|
89
|
+
declare function electronExeBeside(entry: string, platform?: NodeJS.Platform): string | undefined;
|
|
90
|
+
/** From an electron package root, find its dist executable. */
|
|
91
|
+
declare function electronDistExe(pkgRoot: string, platform?: NodeJS.Platform): string | undefined;
|
|
86
92
|
/**
|
|
87
93
|
* Self-hosted view host: spawns the plugin's Electron child on first use and
|
|
88
94
|
* keeps it alive until dispose(). Fallback when no desktop shell provides
|
|
@@ -93,14 +99,20 @@ export declare class RemoteElectronViewHost implements ElectronBrowserViewHost {
|
|
|
93
99
|
/** Test seam: the executable to spawn instead of the resolved Electron
|
|
94
100
|
* binary. Absent -> resolveElectronPath() (production behavior). */
|
|
95
101
|
private readonly spawnExecutable?;
|
|
102
|
+
/** Test seam: the binary probe behind `available()`. Absent ->
|
|
103
|
+
* resolveElectronPath(). */
|
|
104
|
+
private readonly probe;
|
|
96
105
|
private client;
|
|
97
106
|
private server;
|
|
98
107
|
private pendingSocket;
|
|
99
108
|
private readonly views;
|
|
100
109
|
private readyPromise;
|
|
101
110
|
private disposed;
|
|
102
|
-
/**
|
|
111
|
+
/** Last probe result. A success is kept for the host's lifetime; a failure
|
|
112
|
+
* is retried after `PROBE_RETRY_MS` (see `available()`). */
|
|
103
113
|
private electronAvailable;
|
|
114
|
+
/** Earliest time the next probe may run after a negative one. */
|
|
115
|
+
private nextProbeAt;
|
|
104
116
|
/** Window groups (windowId per view), re-sent on every materialization so
|
|
105
117
|
* a restarted child still places views in the right windows. */
|
|
106
118
|
private readonly groups;
|
|
@@ -109,13 +121,22 @@ export declare class RemoteElectronViewHost implements ElectronBrowserViewHost {
|
|
|
109
121
|
constructor(hostMainPath: string,
|
|
110
122
|
/** Test seam: the executable to spawn instead of the resolved Electron
|
|
111
123
|
* binary. Absent -> resolveElectronPath() (production behavior). */
|
|
112
|
-
spawnExecutable?: string | undefined
|
|
124
|
+
spawnExecutable?: string | undefined,
|
|
125
|
+
/** Test seam: the binary probe behind `available()`. Absent ->
|
|
126
|
+
* resolveElectronPath(). */
|
|
127
|
+
probe?: () => void);
|
|
113
128
|
/**
|
|
114
129
|
* Cheap usability probe: can we find an Electron binary to spawn? The scan
|
|
115
|
-
* is filesystem-only (no network), per the seam's contract,
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
130
|
+
* is filesystem-only (no network), per the seam's contract, so a missing
|
|
131
|
+
* binary surfaces as `BROWSER_PROVIDER_UNAVAILABLE` at provider selection
|
|
132
|
+
* instead of a confusing spawn failure on first use.
|
|
133
|
+
*
|
|
134
|
+
* A success is cached for the host's lifetime. A failure is NOT: an install
|
|
135
|
+
* (`npm i electron`, or the postinstall that fetches the binary) can land
|
|
136
|
+
* long after DSH started, and provider selection happens once per process —
|
|
137
|
+
* caching "no" permanently reported "no usable browser provider is
|
|
138
|
+
* registered" until DSH restarted, with no way back short of that. Retrying
|
|
139
|
+
* on a cooldown heals it by itself while keeping the scan off the hot path.
|
|
119
140
|
*/
|
|
120
141
|
available(): boolean;
|
|
121
142
|
/** Ensure the child is up and ready (lazy on first use; restarts after a crash). */
|
|
@@ -126,6 +147,22 @@ export declare class RemoteElectronViewHost implements ElectronBrowserViewHost {
|
|
|
126
147
|
createView(): ElectronViewHandle;
|
|
127
148
|
private ensureView;
|
|
128
149
|
showView(handle: ElectronViewHandle, label?: string): void;
|
|
150
|
+
/**
|
|
151
|
+
* (Re)present a view and WAIT for the child to confirm it, so the provider
|
|
152
|
+
* can dispatch CDP `Input.*` knowing the view has a display surface.
|
|
153
|
+
*
|
|
154
|
+
* Two things this fixes, both silent before:
|
|
155
|
+
* - showing a view that does not exist yet is a no-op in the child, so the
|
|
156
|
+
* view must be materialized first (a fresh tab's createView races the
|
|
157
|
+
* provider's showActive);
|
|
158
|
+
* - a view whose renderer was replaced by a navigation is \"already\n * visible\" and therefore skipped by the child's flicker-avoidance fast
|
|
159
|
+
* path, leaving the new renderer without a surface.
|
|
160
|
+
*
|
|
161
|
+
* The barrier ping is meaningful because the child handles messages strictly
|
|
162
|
+
* in order: its reply is queued behind the showView and so only arrives once
|
|
163
|
+
* the visibility change has been applied.
|
|
164
|
+
*/
|
|
165
|
+
presentView(handle: ElectronViewHandle, label?: string): Promise<void>;
|
|
129
166
|
/** Route a view into its session's own window (one window per session). */
|
|
130
167
|
groupView(handle: ElectronViewHandle, windowId: string, label?: string): void;
|
|
131
168
|
/** Register the provider's handler for user-initiated toolbar actions. */
|
|
@@ -144,6 +181,9 @@ export declare function defaultHostMainPath(): string;
|
|
|
144
181
|
* steps; `resolveElectronPath` is the full resolution order.
|
|
145
182
|
*/
|
|
146
183
|
export declare const internals: {
|
|
184
|
+
electronDistCandidates: typeof electronDistCandidates;
|
|
185
|
+
electronDistExe: typeof electronDistExe;
|
|
186
|
+
electronExeBeside: typeof electronExeBeside;
|
|
147
187
|
isBareElectron: typeof isBareElectron;
|
|
148
188
|
resolveElectronPath: typeof resolveElectronPath;
|
|
149
189
|
resolveElectronPathImpl: typeof resolveElectronPathImpl;
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* @module dsh-browser/browser-electron/remote-host
|
|
26
26
|
*/
|
|
27
27
|
import { execFileSync, spawn } from 'node:child_process';
|
|
28
|
+
import { appendFileSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
|
|
28
29
|
import { randomBytes } from 'node:crypto';
|
|
29
30
|
import { createRequire } from 'node:module';
|
|
30
31
|
import { existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
@@ -33,6 +34,8 @@ import { createServer } from 'node:net';
|
|
|
33
34
|
import { fileURLToPath } from 'node:url';
|
|
34
35
|
/** How long to wait for the child to signal readiness before failing. */
|
|
35
36
|
const READY_TIMEOUT_MS = 20_000;
|
|
37
|
+
/** Budget for the present/barrier round-trips (see presentView). */
|
|
38
|
+
const PRESENT_TIMEOUT_MS = 10_000;
|
|
36
39
|
/** Safety cap on a single RPC reply line (base64 downloads are the big ones). */
|
|
37
40
|
const MAX_RPC_BUFFER_BYTES = 512 * 1024 * 1024;
|
|
38
41
|
function resolveElectronPath() {
|
|
@@ -316,23 +319,31 @@ function compareVersions(a, b) {
|
|
|
316
319
|
}
|
|
317
320
|
return 0;
|
|
318
321
|
}
|
|
319
|
-
/**
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
join(
|
|
325
|
-
|
|
322
|
+
/** Candidate executable layouts used by the Electron npm package on one platform. */
|
|
323
|
+
function electronDistCandidates(pkgRoot, platform = process.platform) {
|
|
324
|
+
// macOS keeps the runnable binary inside the Electron.app bundle; unlike
|
|
325
|
+
// Windows/Linux there is no dist/electron(.exe) sibling to probe.
|
|
326
|
+
if (platform === 'darwin') {
|
|
327
|
+
return [join(pkgRoot, 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')];
|
|
328
|
+
}
|
|
329
|
+
return [
|
|
330
|
+
join(pkgRoot, 'dist', 'electron.exe'),
|
|
331
|
+
join(pkgRoot, 'dist', 'electron'),
|
|
326
332
|
];
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
}
|
|
334
|
+
/** From an electron package entry file, find the dist executable beside it. */
|
|
335
|
+
function electronExeBeside(entry, platform = process.platform) {
|
|
336
|
+
const packageRoots = [dirname(entry), join(dirname(entry), '..')];
|
|
337
|
+
for (const packageRoot of packageRoots) {
|
|
338
|
+
const candidate = electronDistExe(packageRoot, platform);
|
|
339
|
+
if (candidate !== undefined)
|
|
329
340
|
return candidate;
|
|
330
341
|
}
|
|
331
342
|
return undefined;
|
|
332
343
|
}
|
|
333
344
|
/** From an electron package root, find its dist executable. */
|
|
334
|
-
function electronDistExe(pkgRoot) {
|
|
335
|
-
for (const candidate of
|
|
345
|
+
function electronDistExe(pkgRoot, platform = process.platform) {
|
|
346
|
+
for (const candidate of electronDistCandidates(pkgRoot, platform)) {
|
|
336
347
|
if (existsSync(candidate))
|
|
337
348
|
return candidate;
|
|
338
349
|
}
|
|
@@ -411,8 +422,13 @@ class ElectronChildClient {
|
|
|
411
422
|
});
|
|
412
423
|
this.child.stderr.setEncoding('utf8');
|
|
413
424
|
this.child.stderr.on('data', chunk => {
|
|
414
|
-
// Diagnostics only; never parse stderr as protocol.
|
|
415
|
-
|
|
425
|
+
// Diagnostics only; never parse stderr as protocol. Mirror to a log file
|
|
426
|
+
// as well: under a plain `dsh web` self-host these lines only ever reach
|
|
427
|
+
// the DSH process's stderr, so a host that crashes in a restart loop left
|
|
428
|
+
// the user with a string of timeouts and no stack to act on.
|
|
429
|
+
const text = String(chunk);
|
|
430
|
+
process.stderr.write(`[dsh-browser host] ${text}`);
|
|
431
|
+
appendHostLog(text);
|
|
416
432
|
});
|
|
417
433
|
// Send the spawn token over stdin (first line). The child reads it from
|
|
418
434
|
// stdin instead of argv so the token is never visible in the process
|
|
@@ -423,11 +439,17 @@ class ElectronChildClient {
|
|
|
423
439
|
// A failed spawn (bad/corrupt binary) emits 'error' — without a listener
|
|
424
440
|
// that would crash the whole DSH process.
|
|
425
441
|
this.child.on('error', error => {
|
|
426
|
-
|
|
442
|
+
const line = `spawn error: ${String(error)}`;
|
|
443
|
+
process.stderr.write(`[dsh-browser host] ${line}\n`);
|
|
444
|
+
appendHostLog(`${line}\n`);
|
|
427
445
|
this.fail(new Error(`dsh-builtin-browser: browser host failed to start: ${String(error)}`));
|
|
428
446
|
});
|
|
429
447
|
this.child.on('exit', (code, signal) => {
|
|
430
|
-
|
|
448
|
+
// The exit code/signal is the other half of a crash report; without it a
|
|
449
|
+
// restart loop is indistinguishable from a clean stop.
|
|
450
|
+
const line = `browser host exited (code=${String(code)} signal=${String(signal)})`;
|
|
451
|
+
appendHostLog(`${line}\n`);
|
|
452
|
+
this.fail(new Error(`dsh-builtin-browser: ${line}`));
|
|
431
453
|
});
|
|
432
454
|
}
|
|
433
455
|
/** Reject everything in flight, mark the client dead, and notify the host. */
|
|
@@ -602,6 +624,10 @@ class RemoteView {
|
|
|
602
624
|
params: params ?? {},
|
|
603
625
|
});
|
|
604
626
|
}
|
|
627
|
+
/** Give this view web focus in the child, so keyboard input reaches the page. */
|
|
628
|
+
focus() {
|
|
629
|
+
return this.client.call('focus', { viewId: this.id });
|
|
630
|
+
}
|
|
605
631
|
/** Ask the child to download a URL to a local file (keeps cookies/login). */
|
|
606
632
|
async download(url, savePath) {
|
|
607
633
|
// The child writes the file itself (temp + rename); only a small
|
|
@@ -622,6 +648,14 @@ class RemoteView {
|
|
|
622
648
|
return this.client.call('restoreAuth', { viewId: this.id, cookies }).then(r => r.restored);
|
|
623
649
|
}
|
|
624
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* How long a FAILED Electron-binary probe is remembered before the binary
|
|
653
|
+
* search runs again (`available()`). A success is kept for the host's lifetime.
|
|
654
|
+
*/
|
|
655
|
+
const PROBE_RETRY_MS = (() => {
|
|
656
|
+
const raw = Number(process.env.DSH_BROWSER_PROBE_RETRY_MS);
|
|
657
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : 30000;
|
|
658
|
+
})();
|
|
625
659
|
/**
|
|
626
660
|
* Self-hosted view host: spawns the plugin's Electron child on first use and
|
|
627
661
|
* keeps it alive until dispose(). Fallback when no desktop shell provides
|
|
@@ -630,14 +664,18 @@ class RemoteView {
|
|
|
630
664
|
export class RemoteElectronViewHost {
|
|
631
665
|
hostMainPath;
|
|
632
666
|
spawnExecutable;
|
|
667
|
+
probe;
|
|
633
668
|
client;
|
|
634
669
|
server;
|
|
635
670
|
pendingSocket;
|
|
636
671
|
views = new Map();
|
|
637
672
|
readyPromise;
|
|
638
673
|
disposed = false;
|
|
639
|
-
/**
|
|
674
|
+
/** Last probe result. A success is kept for the host's lifetime; a failure
|
|
675
|
+
* is retried after `PROBE_RETRY_MS` (see `available()`). */
|
|
640
676
|
electronAvailable;
|
|
677
|
+
/** Earliest time the next probe may run after a negative one. */
|
|
678
|
+
nextProbeAt = 0;
|
|
641
679
|
/** Window groups (windowId per view), re-sent on every materialization so
|
|
642
680
|
* a restarted child still places views in the right windows. */
|
|
643
681
|
groups = new Map();
|
|
@@ -646,30 +684,43 @@ export class RemoteElectronViewHost {
|
|
|
646
684
|
constructor(hostMainPath,
|
|
647
685
|
/** Test seam: the executable to spawn instead of the resolved Electron
|
|
648
686
|
* binary. Absent -> resolveElectronPath() (production behavior). */
|
|
649
|
-
spawnExecutable
|
|
687
|
+
spawnExecutable,
|
|
688
|
+
/** Test seam: the binary probe behind `available()`. Absent ->
|
|
689
|
+
* resolveElectronPath(). */
|
|
690
|
+
probe = resolveElectronPath) {
|
|
650
691
|
this.hostMainPath = hostMainPath;
|
|
651
692
|
this.spawnExecutable = spawnExecutable;
|
|
693
|
+
this.probe = probe;
|
|
652
694
|
}
|
|
653
695
|
/**
|
|
654
696
|
* Cheap usability probe: can we find an Electron binary to spawn? The scan
|
|
655
|
-
* is filesystem-only (no network), per the seam's contract,
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
697
|
+
* is filesystem-only (no network), per the seam's contract, so a missing
|
|
698
|
+
* binary surfaces as `BROWSER_PROVIDER_UNAVAILABLE` at provider selection
|
|
699
|
+
* instead of a confusing spawn failure on first use.
|
|
700
|
+
*
|
|
701
|
+
* A success is cached for the host's lifetime. A failure is NOT: an install
|
|
702
|
+
* (`npm i electron`, or the postinstall that fetches the binary) can land
|
|
703
|
+
* long after DSH started, and provider selection happens once per process —
|
|
704
|
+
* caching "no" permanently reported "no usable browser provider is
|
|
705
|
+
* registered" until DSH restarted, with no way back short of that. Retrying
|
|
706
|
+
* on a cooldown heals it by itself while keeping the scan off the hot path.
|
|
659
707
|
*/
|
|
660
708
|
available() {
|
|
661
|
-
if (this.electronAvailable ===
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
709
|
+
if (this.electronAvailable === true)
|
|
710
|
+
return true;
|
|
711
|
+
if (Date.now() < this.nextProbeAt)
|
|
712
|
+
return false;
|
|
713
|
+
this.nextProbeAt = Date.now() + PROBE_RETRY_MS;
|
|
714
|
+
try {
|
|
715
|
+
this.probe();
|
|
716
|
+
this.electronAvailable = true;
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
this.electronAvailable = false;
|
|
720
|
+
// The provider will be reported unavailable, so the detailed
|
|
721
|
+
// resolution error (install-electron / ELECTRON_PATH guidance) would
|
|
722
|
+
// otherwise never reach the user — surface it on stderr.
|
|
723
|
+
process.stderr.write(`[dsh-browser host] electron unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
673
724
|
}
|
|
674
725
|
return this.electronAvailable;
|
|
675
726
|
}
|
|
@@ -823,6 +874,35 @@ export class RemoteElectronViewHost {
|
|
|
823
874
|
.then(() => this.client?.call('showView', { viewId: handle.id, ...label !== undefined ? { label } : {} }))
|
|
824
875
|
.catch(() => { });
|
|
825
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* (Re)present a view and WAIT for the child to confirm it, so the provider
|
|
879
|
+
* can dispatch CDP `Input.*` knowing the view has a display surface.
|
|
880
|
+
*
|
|
881
|
+
* Two things this fixes, both silent before:
|
|
882
|
+
* - showing a view that does not exist yet is a no-op in the child, so the
|
|
883
|
+
* view must be materialized first (a fresh tab's createView races the
|
|
884
|
+
* provider's showActive);
|
|
885
|
+
* - a view whose renderer was replaced by a navigation is \"already\n * visible\" and therefore skipped by the child's flicker-avoidance fast
|
|
886
|
+
* path, leaving the new renderer without a surface.
|
|
887
|
+
*
|
|
888
|
+
* The barrier ping is meaningful because the child handles messages strictly
|
|
889
|
+
* in order: its reply is queued behind the showView and so only arrives once
|
|
890
|
+
* the visibility change has been applied.
|
|
891
|
+
*/
|
|
892
|
+
async presentView(handle, label) {
|
|
893
|
+
await this.ready();
|
|
894
|
+
const client = this.client;
|
|
895
|
+
if (client === undefined)
|
|
896
|
+
throw new HostGoneError('browser host unavailable');
|
|
897
|
+
// A view created after the last createView does not exist in the child yet.
|
|
898
|
+
// ensureMaterialized also rebuilds it against a fresh child after a host
|
|
899
|
+
// death, which is the path that used to leave sessions timing out forever.
|
|
900
|
+
if (handle instanceof DeferredRemoteView) {
|
|
901
|
+
await withTimeout(handle.ensureMaterialized(), PRESENT_TIMEOUT_MS, 'browser host did not materialize the view');
|
|
902
|
+
}
|
|
903
|
+
await withTimeout(client.call('showView', { viewId: handle.id, ...label !== undefined ? { label } : {} }), PRESENT_TIMEOUT_MS, 'browser host did not confirm the view presentation');
|
|
904
|
+
await withTimeout(client.call('ping'), PRESENT_TIMEOUT_MS, 'browser host stopped answering');
|
|
905
|
+
}
|
|
826
906
|
/** Route a view into its session's own window (one window per session). */
|
|
827
907
|
groupView(handle, windowId, label) {
|
|
828
908
|
this.groups.set(handle.id, { windowId, ...label !== undefined ? { label } : {} });
|
|
@@ -927,12 +1007,43 @@ class DeferredRemoteView {
|
|
|
927
1007
|
async capture(opts) {
|
|
928
1008
|
return this.withRecovery(view => view.capture(opts));
|
|
929
1009
|
}
|
|
1010
|
+
async focus() {
|
|
1011
|
+
return this.withRecovery(view => view.focus());
|
|
1012
|
+
}
|
|
930
1013
|
async flushAuth() {
|
|
931
1014
|
return this.withRecovery(view => view.flushAuth());
|
|
932
1015
|
}
|
|
933
1016
|
async restoreAuth(cookies) {
|
|
934
1017
|
return this.withRecovery(view => view.restoreAuth(cookies));
|
|
935
1018
|
}
|
|
1019
|
+
/** Materialize this view in the child (no-op when already there). */
|
|
1020
|
+
async ensureMaterialized() {
|
|
1021
|
+
await this.withRecovery(async () => undefined);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Append one host-diagnostics chunk to `$DSH_HOME/logs/dsh-builtin-browser-host.log`.
|
|
1026
|
+
* Best-effort and self-limiting: diagnostics must never break the host or grow
|
|
1027
|
+
* without bound, so failures are swallowed and the file is truncated when it
|
|
1028
|
+
* gets large (a crash loop writes quickly).
|
|
1029
|
+
*/
|
|
1030
|
+
const HOST_LOG_MAX_BYTES = 2 * 1024 * 1024;
|
|
1031
|
+
function appendHostLog(text) {
|
|
1032
|
+
const home = process.env.DSH_HOME;
|
|
1033
|
+
if (home === undefined || home === '')
|
|
1034
|
+
return;
|
|
1035
|
+
try {
|
|
1036
|
+
const dir = join(home, 'logs');
|
|
1037
|
+
const file = join(dir, 'dsh-builtin-browser-host.log');
|
|
1038
|
+
try {
|
|
1039
|
+
if (statSync(file).size > HOST_LOG_MAX_BYTES)
|
|
1040
|
+
writeFileSync(file, '');
|
|
1041
|
+
}
|
|
1042
|
+
catch { /* first write, or unreadable: just append */ }
|
|
1043
|
+
mkdirSync(dir, { recursive: true });
|
|
1044
|
+
appendFileSync(file, text);
|
|
1045
|
+
}
|
|
1046
|
+
catch { /* diagnostics are best-effort only */ }
|
|
936
1047
|
}
|
|
937
1048
|
/** Reject a promise if it does not settle within the budget. */
|
|
938
1049
|
function withTimeout(promise, ms, message) {
|
|
@@ -951,6 +1062,9 @@ export function defaultHostMainPath() {
|
|
|
951
1062
|
* steps; `resolveElectronPath` is the full resolution order.
|
|
952
1063
|
*/
|
|
953
1064
|
export const internals = {
|
|
1065
|
+
electronDistCandidates,
|
|
1066
|
+
electronDistExe,
|
|
1067
|
+
electronExeBeside,
|
|
954
1068
|
isBareElectron,
|
|
955
1069
|
resolveElectronPath,
|
|
956
1070
|
resolveElectronPathImpl,
|
|
@@ -1042,7 +1042,7 @@ export function apply(ctx, config = {}) {
|
|
|
1042
1042
|
description: 'Capture the current shared-browser page as a screenshot (PNG default, JPEG optional). Use for visual confirmation of layout, charts, designs, or CAPTCHAs, or to feed a vision tool (read_image) that locates elements visually. Supports full-page capture, save-to-file, JPEG encoding, and downscaling (maxWidth/maxHeight) to cut vision-tool token cost. JPEG is only available on the self-hosted native path; the desktop-shell path returns PNG.',
|
|
1043
1043
|
parameters: {
|
|
1044
1044
|
fullPage: { type: 'boolean', description: 'Capture the full scrollable page instead of the viewport (default false).' },
|
|
1045
|
-
savePath: { type: 'string', description: 'Absolute file path to also save the image to (e.g. for read_image vision location).' },
|
|
1045
|
+
savePath: { type: 'string', description: 'Absolute file path to also save the image to (e.g. for read_image vision location). Must resolve inside the configured downloadDir (default: the system Downloads folder, localized names such as ~/下载 included); an existing file is never overwritten.' },
|
|
1046
1046
|
format: { type: 'string', enum: ['png', 'jpeg'], description: 'Image format (default png; jpeg is self-hosted native path only).' },
|
|
1047
1047
|
quality: { type: 'number', description: 'JPEG quality 1-100 (default 80); ignored for PNG.' },
|
|
1048
1048
|
maxWidth: { type: 'number', description: 'Downscale to fit within this width (aspect preserved).' },
|
|
@@ -1312,7 +1312,7 @@ export function apply(ctx, config = {}) {
|
|
|
1312
1312
|
description: 'Download a URL to a local file, keeping the browser session\'s cookies and login state. Use for fetching files behind authentication or from the current page context. Available on the self-hosted browser; the desktop shell delegates downloads to the real browser UI.',
|
|
1313
1313
|
parameters: {
|
|
1314
1314
|
url: { type: 'string', required: true, description: 'The URL to download.' },
|
|
1315
|
-
savePath: { type: 'string', required: true, description: 'Absolute path of the file to write.' },
|
|
1315
|
+
savePath: { type: 'string', required: true, description: 'Absolute path of the file to write. Must resolve inside the configured downloadDir (default: the system Downloads folder, localized names such as ~/下载 included); an existing file is never overwritten.' },
|
|
1316
1316
|
},
|
|
1317
1317
|
output: {
|
|
1318
1318
|
schema: { type: 'object', additionalProperties: false, properties: { path: { type: 'string', required: true } } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-builtin-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
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",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
68
|
-
"@deepseek-ai/dsh-llm": "
|
|
69
|
-
"@deepseek-ai/dsh-tools": "
|
|
70
|
-
"@deepseek-ai/dsh-system-prompt": "
|
|
68
|
+
"@deepseek-ai/dsh-llm": ">=0.1.1-rc.2 <0.3.0",
|
|
69
|
+
"@deepseek-ai/dsh-tools": ">=0.1.1-rc.2 <0.3.0",
|
|
70
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.1.1-rc.2 <0.3.0",
|
|
71
71
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"patch": "./cordis.patch.yml"
|
|
80
80
|
},
|
|
81
81
|
"compatibility": {
|
|
82
|
-
"dsh": ">=0.1.1-rc.1 <0.
|
|
82
|
+
"dsh": ">=0.1.1-rc.1 <0.3.0",
|
|
83
83
|
"profiles": [
|
|
84
84
|
"web",
|
|
85
85
|
"desktop"
|
|
@@ -87,7 +87,9 @@
|
|
|
87
87
|
"dshReleases": {
|
|
88
88
|
"0.1.0-rc.8": "unknown",
|
|
89
89
|
"0.1.1-rc.1": "compatible",
|
|
90
|
-
"0.1.1-rc.2": "compatible"
|
|
90
|
+
"0.1.1-rc.2": "compatible",
|
|
91
|
+
"0.2.0-rc.1": "compatible",
|
|
92
|
+
"0.2.0-rc.2": "compatible"
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
}
|
package/src/browser/runtime.ts
CHANGED
|
@@ -177,7 +177,21 @@ export class BrowserRuntime extends Service {
|
|
|
177
177
|
const usable = [...providers.values()].filter(provider => provider.available())
|
|
178
178
|
const [single] = usable
|
|
179
179
|
if (single === undefined) {
|
|
180
|
-
|
|
180
|
+
// Two very different causes used to read identically here — nothing is
|
|
181
|
+
// registered, or something is registered but cannot run — and only the
|
|
182
|
+
// second one has a fix the user can apply without reinstalling.
|
|
183
|
+
const registered = [...providers.values()].map(provider => provider.id)
|
|
184
|
+
if (registered.length === 0) {
|
|
185
|
+
throw new BrowserError(
|
|
186
|
+
'no browser provider is registered — install a browser plugin (e.g. dsh-builtin-browser) and enable it in this profile',
|
|
187
|
+
'BROWSER_PROVIDER_UNAVAILABLE',
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
throw new BrowserError(
|
|
191
|
+
`browser provider(s) ${registered.join(', ')} are registered but report themselves unavailable — `
|
|
192
|
+
+ 'for dsh-builtin-browser this means no spawnable Electron binary was found; run `npx install-electron` in the active profile or set ELECTRON_PATH',
|
|
193
|
+
'BROWSER_PROVIDER_UNAVAILABLE',
|
|
194
|
+
)
|
|
181
195
|
}
|
|
182
196
|
if (usable.length > 1) {
|
|
183
197
|
const ids = usable.map(provider => provider.id).join(', ')
|