dsh-builtin-browser 0.1.4 → 0.1.6
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/lib/browser/types.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface BrowserExecuteRequest {
|
|
|
64
64
|
readonly script: string;
|
|
65
65
|
/** Optional arguments injected into the script scope as `arguments[0..n]`. */
|
|
66
66
|
readonly args?: readonly unknown[];
|
|
67
|
+
/** Evaluation budget in ms; a wedged page rejects with a timeout. Default 30000. */
|
|
68
|
+
readonly timeoutMs?: number;
|
|
67
69
|
}
|
|
68
70
|
/**
|
|
69
71
|
* Result of an execute. Mirrors CDP `Runtime.evaluate` semantics: either a
|
|
@@ -149,7 +149,13 @@ export class ElectronBrowserProvider {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
signal?.throwIfAborted();
|
|
152
|
-
await handle.sendCommand(CDP_PAGE_NAVIGATE, { url });
|
|
152
|
+
const result = await handle.sendCommand(CDP_PAGE_NAVIGATE, { url });
|
|
153
|
+
// Page.navigate resolves even when the navigation fails; surface the
|
|
154
|
+
// failure instead of leaving a silent white screen.
|
|
155
|
+
const errorText = result.errorText;
|
|
156
|
+
if (typeof errorText === 'string' && errorText !== '') {
|
|
157
|
+
throw new BrowserError(`browser: navigation to "${url}" failed: ${errorText}`, 'BROWSER_NAVIGATION_FAILED');
|
|
158
|
+
}
|
|
153
159
|
}
|
|
154
160
|
/** Execute JS in the active tab's page context. */
|
|
155
161
|
async execute(session, request, signal) {
|
|
@@ -171,11 +177,16 @@ export class ElectronBrowserProvider {
|
|
|
171
177
|
const expression = hasArgs
|
|
172
178
|
? `(function(){ const __dshArgs = ${JSON.stringify(request.args)}; return Function(${JSON.stringify(body)}).apply(null, __dshArgs) })()`
|
|
173
179
|
: `(function(){ return Function(${JSON.stringify(body)})() })()`;
|
|
174
|
-
|
|
180
|
+
// CDP Runtime.evaluate can hang indefinitely on a not-yet-loaded page
|
|
181
|
+
// (navigate returned but the renderer has not committed). Bound it so a
|
|
182
|
+
// stuck call surfaces as BROWSER_EXECUTE_TIMEOUT instead of wedging the
|
|
183
|
+
// whole tool call. The caller's signal wins when it fires first.
|
|
184
|
+
const timeoutMs = request.timeoutMs ?? 30_000;
|
|
185
|
+
const result = await withTimeout(handle.sendCommand(CDP_RUNTIME_EVALUATE, {
|
|
175
186
|
expression,
|
|
176
187
|
returnByValue: true,
|
|
177
188
|
awaitPromise: true,
|
|
178
|
-
});
|
|
189
|
+
}), timeoutMs, signal, `browser: execute timed out after ${timeoutMs}ms`);
|
|
179
190
|
if (result.exceptionDetails !== undefined) {
|
|
180
191
|
const detail = result.exceptionDetails;
|
|
181
192
|
return { ok: false, exception: detail.exception?.description ?? detail.text ?? 'unknown exception' };
|
|
@@ -220,7 +231,10 @@ export class ElectronBrowserProvider {
|
|
|
220
231
|
}
|
|
221
232
|
return { url, title, elements: out, truncated: out.length >= cap }
|
|
222
233
|
})()`;
|
|
223
|
-
|
|
234
|
+
// Same hang guard as execute: a renderer that has not committed after
|
|
235
|
+
// navigate would otherwise block snapshot forever.
|
|
236
|
+
const timeoutMs = 30_000;
|
|
237
|
+
const result = await withTimeout(handleSendEvaluate(tab.handle, script), timeoutMs, signal, `browser: snapshot timed out after ${timeoutMs}ms`);
|
|
224
238
|
if (!result.ok)
|
|
225
239
|
throw new BrowserError(`browser: snapshot evaluation failed: ${result.exception}`, 'BROWSER_SNAPSHOT_FAILED');
|
|
226
240
|
const value = result.value;
|
|
@@ -351,6 +365,46 @@ export class ElectronBrowserProvider {
|
|
|
351
365
|
return result.ok && typeof result.value === 'string' ? result.value : '';
|
|
352
366
|
}
|
|
353
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Bound a promise so a wedged CDP call surfaces as an error instead of
|
|
370
|
+
* hanging the tool call forever. The caller's signal, when provided, wins
|
|
371
|
+
* over the timeout if it fires first.
|
|
372
|
+
* @param promise - the operation to bound.
|
|
373
|
+
* @param ms - the timeout budget.
|
|
374
|
+
* @param signal - optional caller signal.
|
|
375
|
+
* @param message - the timeout error message.
|
|
376
|
+
* @returns the promise's value, or a rejected promise on timeout/abort.
|
|
377
|
+
*/
|
|
378
|
+
function withTimeout(promise, ms, signal, message) {
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
let done = false;
|
|
381
|
+
const timer = setTimeout(() => {
|
|
382
|
+
if (done)
|
|
383
|
+
return;
|
|
384
|
+
done = true;
|
|
385
|
+
reject(new Error(message));
|
|
386
|
+
}, ms);
|
|
387
|
+
const finish = (fn) => {
|
|
388
|
+
if (done)
|
|
389
|
+
return;
|
|
390
|
+
done = true;
|
|
391
|
+
clearTimeout(timer);
|
|
392
|
+
if (signal !== undefined)
|
|
393
|
+
signal.removeEventListener('abort', onAbort);
|
|
394
|
+
fn();
|
|
395
|
+
};
|
|
396
|
+
const onAbort = () => {
|
|
397
|
+
if (done)
|
|
398
|
+
return;
|
|
399
|
+
done = true;
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error('aborted'));
|
|
402
|
+
};
|
|
403
|
+
if (signal !== undefined)
|
|
404
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
405
|
+
promise.then(value => finish(() => resolve(value)), error => finish(() => reject(error)));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
354
408
|
/**
|
|
355
409
|
* Run a `Runtime.evaluate` through a view handle and normalize the result.
|
|
356
410
|
* Shared by execute, snapshot, content, and internal URL reads.
|
|
@@ -358,12 +358,17 @@ export class RemoteElectronViewHost {
|
|
|
358
358
|
class DeferredRemoteView {
|
|
359
359
|
id;
|
|
360
360
|
materialize;
|
|
361
|
+
materialized;
|
|
361
362
|
constructor(id, materialize) {
|
|
362
363
|
this.id = id;
|
|
363
364
|
this.materialize = materialize;
|
|
364
365
|
}
|
|
365
366
|
async sendCommand(method, params) {
|
|
366
|
-
|
|
367
|
+
// Materialize once and cache: every sendCommand on the same handle must
|
|
368
|
+
// target the SAME child view. Re-materializing would re-run createView
|
|
369
|
+
// (duplicate window) and drop the established view.
|
|
370
|
+
this.materialized ??= this.materialize();
|
|
371
|
+
const view = await this.materialized;
|
|
367
372
|
return view.sendCommand(method, params);
|
|
368
373
|
}
|
|
369
374
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-builtin-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Shared real browser capability for DeepSeek Harness: a visible browser the human watches and can take over, driven by the agent over CDP (snapshot/execute/content/tab management); host-side seam, provider, and browser_* tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|