dsh-builtin-browser 0.1.5 → 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
|
|
@@ -177,11 +177,16 @@ export class ElectronBrowserProvider {
|
|
|
177
177
|
const expression = hasArgs
|
|
178
178
|
? `(function(){ const __dshArgs = ${JSON.stringify(request.args)}; return Function(${JSON.stringify(body)}).apply(null, __dshArgs) })()`
|
|
179
179
|
: `(function(){ return Function(${JSON.stringify(body)})() })()`;
|
|
180
|
-
|
|
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, {
|
|
181
186
|
expression,
|
|
182
187
|
returnByValue: true,
|
|
183
188
|
awaitPromise: true,
|
|
184
|
-
});
|
|
189
|
+
}), timeoutMs, signal, `browser: execute timed out after ${timeoutMs}ms`);
|
|
185
190
|
if (result.exceptionDetails !== undefined) {
|
|
186
191
|
const detail = result.exceptionDetails;
|
|
187
192
|
return { ok: false, exception: detail.exception?.description ?? detail.text ?? 'unknown exception' };
|
|
@@ -226,7 +231,10 @@ export class ElectronBrowserProvider {
|
|
|
226
231
|
}
|
|
227
232
|
return { url, title, elements: out, truncated: out.length >= cap }
|
|
228
233
|
})()`;
|
|
229
|
-
|
|
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`);
|
|
230
238
|
if (!result.ok)
|
|
231
239
|
throw new BrowserError(`browser: snapshot evaluation failed: ${result.exception}`, 'BROWSER_SNAPSHOT_FAILED');
|
|
232
240
|
const value = result.value;
|
|
@@ -357,6 +365,46 @@ export class ElectronBrowserProvider {
|
|
|
357
365
|
return result.ok && typeof result.value === 'string' ? result.value : '';
|
|
358
366
|
}
|
|
359
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
|
+
}
|
|
360
408
|
/**
|
|
361
409
|
* Run a `Runtime.evaluate` through a view handle and normalize the result.
|
|
362
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",
|