dsh-browser-plus 0.5.0 → 0.5.1
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/CHANGELOG.md +131 -144
- package/README.en.md +27 -8
- package/README.md +27 -8
- package/client/index.js +185 -0
- package/cordis.patch.yml +24 -0
- package/docs/README.md +1 -1
- package/docs/SOAK-CHECKLIST.md +1 -1
- package/docs/tool-reference.md +5 -3
- package/docs/user-guide.md +20 -4
- package/lib/browser/runtime.d.ts +14 -1
- package/lib/browser/runtime.js +28 -0
- package/lib/browser/types.d.ts +96 -6
- package/lib/browser-electron/chrome-state.d.ts +24 -0
- package/lib/browser-electron/entry.d.ts +12 -5
- package/lib/browser-electron/entry.js +5 -2
- package/lib/browser-electron/host-main.d.ts +2 -1
- package/lib/browser-electron/host-main.js +205 -8
- package/lib/browser-electron/page-chrome.js +249 -14
- package/lib/browser-electron/provider.d.ts +60 -2
- package/lib/browser-electron/provider.js +300 -50
- package/lib/browser-electron/remote-host.d.ts +3 -1
- package/lib/browser-electron/remote-host.js +42 -1
- package/lib/client.js +185 -0
- package/lib/command-browser/index.d.ts +20 -0
- package/lib/command-browser/index.js +35 -0
- package/lib/http-browser/index.d.ts +28 -0
- package/lib/http-browser/index.js +110 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +11 -0
- package/lib/task-todos/index.d.ts +25 -0
- package/lib/task-todos/index.js +100 -0
- package/lib/tool-browser/index.js +190 -95
- package/package.json +27 -2
- package/scripts/build-client.mjs +20 -0
- package/scripts/smoke-browser-tools.mjs +84 -1
- package/scripts/smoke-chrome-world.mjs +6 -4
- package/scripts/test-orb-drag.mjs +83 -0
- package/src/browser/runtime.ts +36 -0
- package/src/browser/types.ts +98 -6
- package/src/browser-electron/chrome-state.ts +18 -0
- package/src/browser-electron/entry.ts +17 -7
- package/src/browser-electron/host-main.ts +207 -11
- package/src/browser-electron/page-chrome.ts +249 -14
- package/src/browser-electron/provider.ts +328 -50
- package/src/browser-electron/remote-host.ts +49 -2
- package/src/command-browser/index.ts +61 -0
- package/src/http-browser/index.ts +139 -0
- package/src/index.ts +13 -0
- package/src/task-todos/index.ts +114 -0
- package/src/tool-browser/index.ts +193 -96
|
@@ -233,6 +233,86 @@ function formatSnapshot(snapshot) {
|
|
|
233
233
|
: '';
|
|
234
234
|
return `${header}\n\n${body}${tail}${banner}${user}`;
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* The snapshot shape both `browser_open` and `browser_snapshot` return.
|
|
238
|
+
*
|
|
239
|
+
* Shared on purpose: a tool output schema rejects unknown properties, so a
|
|
240
|
+
* field declared on only one of the two schemas is silently dropped before the
|
|
241
|
+
* model ever sees it — which is exactly what happened to `frames` and the
|
|
242
|
+
* element `frame` index when iframe support landed (declared on browser_open,
|
|
243
|
+
* missing from browser_snapshot, and dropped from browser_open's own mapper).
|
|
244
|
+
*/
|
|
245
|
+
const SNAPSHOT_OUTPUT_SCHEMA = {
|
|
246
|
+
type: 'object',
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
properties: {
|
|
249
|
+
snapshotId: { type: 'string', required: true },
|
|
250
|
+
url: { type: 'string', required: true },
|
|
251
|
+
title: { type: 'string' },
|
|
252
|
+
truncated: { type: 'boolean' },
|
|
253
|
+
frames: {
|
|
254
|
+
type: 'array',
|
|
255
|
+
description: 'Every iframe on the page; readable:false marks the cross-origin ones whose contents cannot be read.',
|
|
256
|
+
items: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
additionalProperties: false,
|
|
259
|
+
properties: {
|
|
260
|
+
index: { type: 'number', required: true },
|
|
261
|
+
url: { type: 'string', required: true },
|
|
262
|
+
readable: { type: 'boolean', required: true },
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
elements: {
|
|
267
|
+
type: 'array',
|
|
268
|
+
required: true,
|
|
269
|
+
items: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
additionalProperties: false,
|
|
272
|
+
properties: {
|
|
273
|
+
ref: { type: 'number', required: true },
|
|
274
|
+
kind: { type: 'string', required: true },
|
|
275
|
+
label: { type: 'string', required: true },
|
|
276
|
+
x: { type: 'number', required: true },
|
|
277
|
+
y: { type: 'number', required: true },
|
|
278
|
+
loc: { type: 'string', required: true },
|
|
279
|
+
frame: { type: 'number', description: 'Index into frames when this element lives inside an iframe.' },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
challenge: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
additionalProperties: false,
|
|
286
|
+
properties: {
|
|
287
|
+
blocked: { type: 'boolean', required: true },
|
|
288
|
+
kind: { type: 'string' },
|
|
289
|
+
reason: { type: 'string' },
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
userControlling: { type: 'boolean' },
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
/** Project a provider snapshot onto that shared model-facing shape. */
|
|
296
|
+
function snapshotOutput(snapshot) {
|
|
297
|
+
return {
|
|
298
|
+
snapshotId: snapshot.snapshotId,
|
|
299
|
+
url: snapshot.url,
|
|
300
|
+
...snapshot.title !== undefined ? { title: snapshot.title } : {},
|
|
301
|
+
...snapshot.frames !== undefined ? { frames: snapshot.frames.map(frame => ({ index: frame.index, url: frame.url, readable: frame.readable })) } : {},
|
|
302
|
+
elements: snapshot.elements.map(el => ({
|
|
303
|
+
ref: el.ref,
|
|
304
|
+
kind: el.kind,
|
|
305
|
+
label: el.label,
|
|
306
|
+
x: el.x,
|
|
307
|
+
y: el.y,
|
|
308
|
+
loc: el.loc,
|
|
309
|
+
...el.frame !== undefined ? { frame: el.frame } : {},
|
|
310
|
+
})),
|
|
311
|
+
truncated: snapshot.truncated,
|
|
312
|
+
...snapshot.challenge !== undefined ? { challenge: snapshot.challenge } : {},
|
|
313
|
+
...snapshot.userControlling !== undefined ? { userControlling: snapshot.userControlling } : {},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
236
316
|
/** Register all browser tools with `ctx.tools`. */
|
|
237
317
|
export function apply(ctx, config = {}) {
|
|
238
318
|
const timeoutMs = config.timeoutMs ?? 60_000;
|
|
@@ -261,42 +341,7 @@ export function apply(ctx, config = {}) {
|
|
|
261
341
|
space: { type: 'string', description: 'Optional browser-task label shown in the task manager and active window title.' },
|
|
262
342
|
},
|
|
263
343
|
output: {
|
|
264
|
-
schema:
|
|
265
|
-
type: 'object',
|
|
266
|
-
additionalProperties: false,
|
|
267
|
-
properties: {
|
|
268
|
-
snapshotId: { type: 'string', required: true },
|
|
269
|
-
url: { type: 'string', required: true },
|
|
270
|
-
title: { type: 'string' },
|
|
271
|
-
truncated: { type: 'boolean' },
|
|
272
|
-
elements: {
|
|
273
|
-
type: 'array',
|
|
274
|
-
required: true,
|
|
275
|
-
items: {
|
|
276
|
-
type: 'object',
|
|
277
|
-
additionalProperties: false,
|
|
278
|
-
properties: {
|
|
279
|
-
ref: { type: 'number', required: true },
|
|
280
|
-
kind: { type: 'string', required: true },
|
|
281
|
-
label: { type: 'string', required: true },
|
|
282
|
-
x: { type: 'number', required: true },
|
|
283
|
-
y: { type: 'number', required: true },
|
|
284
|
-
loc: { type: 'string', required: true },
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
challenge: {
|
|
289
|
-
type: 'object',
|
|
290
|
-
additionalProperties: false,
|
|
291
|
-
properties: {
|
|
292
|
-
blocked: { type: 'boolean', required: true },
|
|
293
|
-
kind: { type: 'string' },
|
|
294
|
-
reason: { type: 'string' },
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
userControlling: { type: 'boolean' },
|
|
298
|
-
},
|
|
299
|
-
},
|
|
344
|
+
schema: SNAPSHOT_OUTPUT_SCHEMA,
|
|
300
345
|
render: (_args, value) => [{ type: 'text', text: formatSnapshot(value) }],
|
|
301
346
|
},
|
|
302
347
|
timeoutMs,
|
|
@@ -313,15 +358,7 @@ export function apply(ctx, config = {}) {
|
|
|
313
358
|
...args.newTab === true ? { newTab: true } : {},
|
|
314
359
|
}, exec.signal);
|
|
315
360
|
const snapshot = await browser.snapshot(session, {}, exec.signal);
|
|
316
|
-
return
|
|
317
|
-
snapshotId: snapshot.snapshotId,
|
|
318
|
-
url: snapshot.url,
|
|
319
|
-
...snapshot.title !== undefined ? { title: snapshot.title } : {},
|
|
320
|
-
elements: snapshot.elements.map(el => ({ ref: el.ref, kind: el.kind, label: el.label, x: el.x, y: el.y, loc: el.loc })),
|
|
321
|
-
truncated: snapshot.truncated,
|
|
322
|
-
...snapshot.challenge !== undefined ? { challenge: snapshot.challenge } : {},
|
|
323
|
-
...snapshot.userControlling !== undefined ? { userControlling: snapshot.userControlling } : {},
|
|
324
|
-
};
|
|
361
|
+
return snapshotOutput(snapshot);
|
|
325
362
|
}, args.space);
|
|
326
363
|
},
|
|
327
364
|
}));
|
|
@@ -546,42 +583,7 @@ export function apply(ctx, config = {}) {
|
|
|
546
583
|
limit: { type: 'number', description: 'Maximum number of elements to return (1-1000; default 60).' },
|
|
547
584
|
},
|
|
548
585
|
output: {
|
|
549
|
-
schema:
|
|
550
|
-
type: 'object',
|
|
551
|
-
additionalProperties: false,
|
|
552
|
-
properties: {
|
|
553
|
-
snapshotId: { type: 'string', required: true },
|
|
554
|
-
url: { type: 'string', required: true },
|
|
555
|
-
title: { type: 'string' },
|
|
556
|
-
truncated: { type: 'boolean' },
|
|
557
|
-
elements: {
|
|
558
|
-
type: 'array',
|
|
559
|
-
required: true,
|
|
560
|
-
items: {
|
|
561
|
-
type: 'object',
|
|
562
|
-
additionalProperties: false,
|
|
563
|
-
properties: {
|
|
564
|
-
ref: { type: 'number', required: true },
|
|
565
|
-
kind: { type: 'string', required: true },
|
|
566
|
-
label: { type: 'string', required: true },
|
|
567
|
-
x: { type: 'number', required: true },
|
|
568
|
-
y: { type: 'number', required: true },
|
|
569
|
-
loc: { type: 'string', required: true },
|
|
570
|
-
},
|
|
571
|
-
},
|
|
572
|
-
},
|
|
573
|
-
challenge: {
|
|
574
|
-
type: 'object',
|
|
575
|
-
additionalProperties: false,
|
|
576
|
-
properties: {
|
|
577
|
-
blocked: { type: 'boolean', required: true },
|
|
578
|
-
kind: { type: 'string' },
|
|
579
|
-
reason: { type: 'string' },
|
|
580
|
-
},
|
|
581
|
-
},
|
|
582
|
-
userControlling: { type: 'boolean' },
|
|
583
|
-
},
|
|
584
|
-
},
|
|
586
|
+
schema: SNAPSHOT_OUTPUT_SCHEMA,
|
|
585
587
|
render: (_args, value) => [{ type: 'text', text: formatSnapshot(value) }],
|
|
586
588
|
},
|
|
587
589
|
timeoutMs,
|
|
@@ -595,15 +597,7 @@ export function apply(ctx, config = {}) {
|
|
|
595
597
|
...args.query !== undefined ? { query: args.query } : {},
|
|
596
598
|
...args.limit !== undefined ? { limit: args.limit } : {},
|
|
597
599
|
}, exec.signal));
|
|
598
|
-
return
|
|
599
|
-
snapshotId: snapshot.snapshotId,
|
|
600
|
-
url: snapshot.url,
|
|
601
|
-
...snapshot.title !== undefined ? { title: snapshot.title } : {},
|
|
602
|
-
elements: snapshot.elements.map(el => ({ ref: el.ref, kind: el.kind, label: el.label, x: el.x, y: el.y, loc: el.loc })),
|
|
603
|
-
truncated: snapshot.truncated,
|
|
604
|
-
...snapshot.challenge !== undefined ? { challenge: snapshot.challenge } : {},
|
|
605
|
-
...snapshot.userControlling !== undefined ? { userControlling: snapshot.userControlling } : {},
|
|
606
|
-
};
|
|
600
|
+
return snapshotOutput(snapshot);
|
|
607
601
|
},
|
|
608
602
|
}));
|
|
609
603
|
ctx.tools.register(defineTool({
|
|
@@ -1171,9 +1165,11 @@ export function apply(ctx, config = {}) {
|
|
|
1171
1165
|
}));
|
|
1172
1166
|
ctx.tools.register(defineTool({
|
|
1173
1167
|
name: 'browser_wait_for',
|
|
1174
|
-
description: 'Wait until
|
|
1168
|
+
description: 'Wait until a selector, some text, or both reaches a state, polling every 250ms. `state` is visible (default), attached, hidden or detached — hidden/detached are how you wait for something to go away. Use before clicking or reading, instead of sleeping.',
|
|
1175
1169
|
parameters: {
|
|
1176
|
-
selector: { type: 'string',
|
|
1170
|
+
selector: { type: 'string', description: 'CSS selector to wait for. Omit to watch the document text (give `text`).' },
|
|
1171
|
+
text: { type: 'string', description: 'Text that must be present: inside the matched element when a selector is given, otherwise anywhere in the document.' },
|
|
1172
|
+
state: { type: 'string', enum: ['visible', 'attached', 'hidden', 'detached'], description: 'Which state to wait for. visible (default) needs a 4x4 px element that is not display:none / visibility:hidden; attached only needs it to exist; hidden and detached wait for it to go away.' },
|
|
1177
1173
|
timeoutMs: { type: 'number', description: `Total budget in ms (default 15000), capped below this tool's ${String(timeoutMs)}ms budget.` },
|
|
1178
1174
|
visible: { type: 'boolean', description: 'Require visibility (>4x4 px, not display:none). Default true.' },
|
|
1179
1175
|
},
|
|
@@ -1183,12 +1179,18 @@ export function apply(ctx, config = {}) {
|
|
|
1183
1179
|
additionalProperties: false,
|
|
1184
1180
|
properties: {
|
|
1185
1181
|
found: { type: 'boolean', required: true },
|
|
1182
|
+
state: { type: 'string', required: true },
|
|
1186
1183
|
selector: { type: 'string', required: true },
|
|
1187
1184
|
tag: { type: 'string', required: true },
|
|
1188
1185
|
text: { type: 'string' },
|
|
1189
1186
|
},
|
|
1190
1187
|
},
|
|
1191
|
-
render: (_args, value) => [{
|
|
1188
|
+
render: (_args, value) => [{
|
|
1189
|
+
type: 'text',
|
|
1190
|
+
text: value.selector === ''
|
|
1191
|
+
? `Found text "${(value.text ?? '').slice(0, 80)}" (state "${value.state}").`
|
|
1192
|
+
: `Found <${value.tag}> ${value.selector}${value.text !== undefined ? ` — "${value.text.slice(0, 80)}"` : ''}.`,
|
|
1193
|
+
}],
|
|
1192
1194
|
},
|
|
1193
1195
|
timeoutMs,
|
|
1194
1196
|
isConcurrencySafe: () => true,
|
|
@@ -1198,11 +1200,13 @@ export function apply(ctx, config = {}) {
|
|
|
1198
1200
|
if (browser === undefined)
|
|
1199
1201
|
throw new Error('tool-browser: browser service unavailable');
|
|
1200
1202
|
const result = await withTaskAction(browser, taskKey(exec), 'wait for element', exec, session => browser.waitForElement(session, {
|
|
1201
|
-
selector: args.selector,
|
|
1203
|
+
...args.selector !== undefined ? { selector: args.selector } : {},
|
|
1204
|
+
...args.text !== undefined ? { text: args.text } : {},
|
|
1205
|
+
...args.state !== undefined ? { state: args.state } : {},
|
|
1202
1206
|
timeoutMs: withinToolBudget(args.timeoutMs, 15_000),
|
|
1203
1207
|
...args.visible !== undefined ? { visible: args.visible } : {},
|
|
1204
1208
|
}, exec.signal));
|
|
1205
|
-
return { found: true, selector: result.selector, tag: result.tag, text: result.text };
|
|
1209
|
+
return { found: true, state: result.state, selector: result.selector, tag: result.tag, text: result.text };
|
|
1206
1210
|
},
|
|
1207
1211
|
}));
|
|
1208
1212
|
ctx.tools.register(defineTool({
|
|
@@ -1441,6 +1445,97 @@ export function apply(ctx, config = {}) {
|
|
|
1441
1445
|
};
|
|
1442
1446
|
},
|
|
1443
1447
|
}));
|
|
1448
|
+
ctx.tools.register(defineTool({
|
|
1449
|
+
name: 'browser_pdf',
|
|
1450
|
+
description: 'Print the active tab to a PDF file — Chrome\'s "Save as PDF". The path must be inside the browser write roots. Returns the path and byte size.',
|
|
1451
|
+
parameters: {
|
|
1452
|
+
savePath: { type: 'string', required: true, description: 'Absolute path of the .pdf to write. Must be inside the browser write roots.' },
|
|
1453
|
+
landscape: { type: 'boolean', description: 'Landscape orientation. Default portrait.' },
|
|
1454
|
+
printBackground: { type: 'boolean', description: 'Include background colours and images. Default true — dark pages would otherwise print as white paper.' },
|
|
1455
|
+
paperWidth: { type: 'number', description: 'Paper width in inches. Default 8.5.' },
|
|
1456
|
+
paperHeight: { type: 'number', description: 'Paper height in inches. Default 11.' },
|
|
1457
|
+
},
|
|
1458
|
+
output: {
|
|
1459
|
+
schema: {
|
|
1460
|
+
type: 'object',
|
|
1461
|
+
additionalProperties: false,
|
|
1462
|
+
properties: {
|
|
1463
|
+
path: { type: 'string', required: true },
|
|
1464
|
+
bytes: { type: 'number', required: true },
|
|
1465
|
+
},
|
|
1466
|
+
},
|
|
1467
|
+
render: (_args, value) => [{ type: 'text', text: `Wrote ${value.bytes} bytes to ${value.path}` }],
|
|
1468
|
+
},
|
|
1469
|
+
timeoutMs,
|
|
1470
|
+
isConcurrencySafe: () => true,
|
|
1471
|
+
async execute(args, exec) {
|
|
1472
|
+
assertAllowed('browser_pdf', exec);
|
|
1473
|
+
const browser = ctx.get('browser');
|
|
1474
|
+
if (browser === undefined)
|
|
1475
|
+
throw new Error('tool-browser: browser service unavailable');
|
|
1476
|
+
const result = await withTaskAction(browser, taskKey(exec), 'print to pdf', exec, session => browser.pdf(session, {
|
|
1477
|
+
savePath: args.savePath,
|
|
1478
|
+
...args.landscape !== undefined ? { landscape: args.landscape } : {},
|
|
1479
|
+
...args.printBackground !== undefined ? { printBackground: args.printBackground } : {},
|
|
1480
|
+
...args.paperWidth !== undefined ? { paperWidth: args.paperWidth } : {},
|
|
1481
|
+
...args.paperHeight !== undefined ? { paperHeight: args.paperHeight } : {},
|
|
1482
|
+
}, exec.signal));
|
|
1483
|
+
return { path: result.path, bytes: result.bytes };
|
|
1484
|
+
},
|
|
1485
|
+
}));
|
|
1486
|
+
ctx.tools.register(defineTool({
|
|
1487
|
+
name: 'browser_highlight',
|
|
1488
|
+
description: 'Draw the DevTools-style highlight box over the first element matching a CSS selector, so a human watching the window can see what the Agent is about to touch. It goes through the CDP overlay, so the page DOM is never modified. Pass clear: true to remove it.',
|
|
1489
|
+
parameters: {
|
|
1490
|
+
selector: { type: 'string', description: 'CSS selector whose first match to highlight. Required unless clear is true.' },
|
|
1491
|
+
clear: { type: 'boolean', description: 'Remove the highlight instead of drawing one.' },
|
|
1492
|
+
},
|
|
1493
|
+
output: {
|
|
1494
|
+
schema: {
|
|
1495
|
+
type: 'object',
|
|
1496
|
+
additionalProperties: false,
|
|
1497
|
+
properties: {
|
|
1498
|
+
matched: { type: 'boolean', required: true },
|
|
1499
|
+
cleared: { type: 'boolean', required: true },
|
|
1500
|
+
nodeId: { type: 'number' },
|
|
1501
|
+
box: {
|
|
1502
|
+
type: 'object',
|
|
1503
|
+
additionalProperties: false,
|
|
1504
|
+
properties: {
|
|
1505
|
+
x: { type: 'number', required: true },
|
|
1506
|
+
y: { type: 'number', required: true },
|
|
1507
|
+
width: { type: 'number', required: true },
|
|
1508
|
+
height: { type: 'number', required: true },
|
|
1509
|
+
},
|
|
1510
|
+
},
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
render: (_args, value) => [{
|
|
1514
|
+
type: 'text',
|
|
1515
|
+
text: value.matched
|
|
1516
|
+
? `Highlighted node ${String(value.nodeId)} at ${Math.round(value.box?.x ?? 0)},${Math.round(value.box?.y ?? 0)} (${Math.round(value.box?.width ?? 0)}x${Math.round(value.box?.height ?? 0)})`
|
|
1517
|
+
: value.cleared ? 'Highlight cleared' : 'No element matched that selector',
|
|
1518
|
+
}],
|
|
1519
|
+
},
|
|
1520
|
+
timeoutMs,
|
|
1521
|
+
isConcurrencySafe: () => true,
|
|
1522
|
+
async execute(args, exec) {
|
|
1523
|
+
assertAllowed('browser_highlight', exec);
|
|
1524
|
+
const browser = ctx.get('browser');
|
|
1525
|
+
if (browser === undefined)
|
|
1526
|
+
throw new Error('tool-browser: browser service unavailable');
|
|
1527
|
+
const result = await withTaskAction(browser, taskKey(exec), 'highlight element', exec, session => browser.highlight(session, {
|
|
1528
|
+
...args.selector !== undefined ? { selector: args.selector } : {},
|
|
1529
|
+
...args.clear !== undefined ? { clear: args.clear } : {},
|
|
1530
|
+
}, exec.signal));
|
|
1531
|
+
return {
|
|
1532
|
+
matched: result.matched,
|
|
1533
|
+
cleared: result.cleared,
|
|
1534
|
+
...result.nodeId !== undefined ? { nodeId: result.nodeId } : {},
|
|
1535
|
+
...result.box !== undefined ? { box: result.box } : {},
|
|
1536
|
+
};
|
|
1537
|
+
},
|
|
1538
|
+
}));
|
|
1444
1539
|
if (config.tabTools !== false) {
|
|
1445
1540
|
ctx.tools.register(defineTool({
|
|
1446
1541
|
name: 'browser_list_tabs',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-plus",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Enhanced shared browser for DeepSeek Harness: one visible window with isolated task views, a page task manager, non-stealing background tasks, built-in toolbar and operation trail, and pinned Electron 42.9.3 compositing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -22,6 +22,21 @@
|
|
|
22
22
|
"types": "./lib/tool-browser/index.d.ts",
|
|
23
23
|
"default": "./lib/tool-browser/index.js"
|
|
24
24
|
},
|
|
25
|
+
"./command-browser": {
|
|
26
|
+
"types": "./lib/command-browser/index.d.ts",
|
|
27
|
+
"default": "./lib/command-browser/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./http-browser": {
|
|
30
|
+
"types": "./lib/http-browser/index.d.ts",
|
|
31
|
+
"default": "./lib/http-browser/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./task-todos": {
|
|
34
|
+
"types": "./lib/task-todos/index.d.ts",
|
|
35
|
+
"default": "./lib/task-todos/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./client": {
|
|
38
|
+
"default": "./lib/client.js"
|
|
39
|
+
},
|
|
25
40
|
"./host-main": {
|
|
26
41
|
"default": "./lib/browser-electron/host-main.js"
|
|
27
42
|
},
|
|
@@ -31,6 +46,7 @@
|
|
|
31
46
|
"files": [
|
|
32
47
|
"lib",
|
|
33
48
|
"src",
|
|
49
|
+
"client",
|
|
34
50
|
"docs",
|
|
35
51
|
"assets",
|
|
36
52
|
"scripts",
|
|
@@ -43,8 +59,9 @@
|
|
|
43
59
|
"NOTICE.md"
|
|
44
60
|
],
|
|
45
61
|
"scripts": {
|
|
46
|
-
"build": "npm run build:icons && tsc -p tsconfig.json",
|
|
62
|
+
"build": "npm run build:icons && tsc -p tsconfig.json && npm run build:client",
|
|
47
63
|
"build:icons": "node scripts/build-icons.mjs",
|
|
64
|
+
"build:client": "node scripts/build-client.mjs",
|
|
48
65
|
"pretest": "tsc -p tsconfig.json",
|
|
49
66
|
"test": "node --test test/*.test.mjs",
|
|
50
67
|
"test:only": "node --test test/*.test.mjs",
|
|
@@ -52,6 +69,7 @@
|
|
|
52
69
|
"smoke:electron-host": "node scripts/smoke-electron-host.mjs",
|
|
53
70
|
"smoke:browser-tools": "node scripts/smoke-browser-tools.mjs",
|
|
54
71
|
"smoke:chrome-world": "node scripts/smoke-chrome-world.mjs",
|
|
72
|
+
"test:orb-drag": "node scripts/test-orb-drag.mjs",
|
|
55
73
|
"prepack": "npm run build"
|
|
56
74
|
},
|
|
57
75
|
"keywords": [
|
|
@@ -89,6 +107,13 @@
|
|
|
89
107
|
"dsh": {
|
|
90
108
|
"bundle": {
|
|
91
109
|
"patch": "./cordis.patch.yml"
|
|
110
|
+
},
|
|
111
|
+
"client": {
|
|
112
|
+
"inject": [
|
|
113
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
114
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right"
|
|
115
|
+
],
|
|
116
|
+
"platform": "web"
|
|
92
117
|
}
|
|
93
118
|
},
|
|
94
119
|
"optionalDependencies": {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the browser bundle: copy the single-file client source to lib/client.js.
|
|
3
|
+
*
|
|
4
|
+
* The client-modules loader serves the package's \`./client\` export, and the
|
|
5
|
+
* bundle is one file that only requires platform modules (React), so there is
|
|
6
|
+
* nothing to bundle or minify — the copy keeps the shipped artifact identical to
|
|
7
|
+
* the reviewed source. Run by \`npm run build\` and by \`prepack\`.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
10
|
+
|
|
11
|
+
const source = new URL('../client/index.js', import.meta.url)
|
|
12
|
+
const output = new URL('../lib/client.js', import.meta.url)
|
|
13
|
+
|
|
14
|
+
const body = await readFile(source, 'utf8')
|
|
15
|
+
if (!body.includes('__ModuleLoader__.load')) {
|
|
16
|
+
throw new Error('client/index.js must register through window.__ModuleLoader__.load')
|
|
17
|
+
}
|
|
18
|
+
await mkdir(new URL('../lib/', import.meta.url), { recursive: true })
|
|
19
|
+
await writeFile(output, body)
|
|
20
|
+
process.stdout.write('built lib/client.js (' + String(body.length) + ' bytes)\n')
|
|
@@ -35,9 +35,14 @@ const check = async (name, fn) => {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// 这套检查**探 chrome 的内部状态**(window.__dshTabs / __dshChromeMotion / __dshBookmarks…),
|
|
39
|
+
// 而默认的 isolated 世界把这些放在 chrome 自己的世界里 —— 从页面世界读到的全是 undefined。
|
|
40
|
+
// 所以行为套件显式跑 main;「隔离是否真的隔离、chrome 在隔离世界里能不能挂起来」由
|
|
41
|
+
// `npm run smoke:chrome-world` 负责(它同时跑两种模式做 A/B)。
|
|
42
|
+
const host = new RemoteElectronViewHost(defaultHostMainPath(), { chromeWorld: 'main' })
|
|
39
43
|
const provider = new ElectronBrowserProvider(host, { writeRoots: [outDir], readRoots: [outDir] })
|
|
40
44
|
|
|
45
|
+
|
|
41
46
|
try {
|
|
42
47
|
const session = await provider.open()
|
|
43
48
|
console.log('session', session)
|
|
@@ -63,6 +68,21 @@ try {
|
|
|
63
68
|
return 'no error'
|
|
64
69
|
})
|
|
65
70
|
await check('waitForElement', async () => (await provider.waitForElement(session, { selector: 'body' })).tag)
|
|
71
|
+
// 纯文字等待:没有选择器可量,状态必须由「文字在不在」决定。
|
|
72
|
+
// 这一条曾经永远超时(脚本仍按 visibleNow(null) === false 判定)。
|
|
73
|
+
await check('waitForElement by text', async () => (await provider.waitForElement(session, { text: 'This domain is for use' })).state)
|
|
74
|
+
// 真机打印:Electron 的 debugger 没有 CDP Page.printToPDF,
|
|
75
|
+
// 所以这条只有走宿主的 webContents.printToPDF 才可能过。
|
|
76
|
+
await check('pdf', async () => {
|
|
77
|
+
const out = join(outDir, 'page.pdf')
|
|
78
|
+
const result = await provider.pdf(session, { savePath: out })
|
|
79
|
+
const bytes = readFileSync(out)
|
|
80
|
+
if (bytes.subarray(0, 5).toString() !== '%PDF-') throw new Error('not a PDF: ' + bytes.subarray(0, 8).toString())
|
|
81
|
+
if (bytes.length !== result.bytes) throw new Error('byte count mismatch: ' + String(bytes.length) + ' vs ' + String(result.bytes))
|
|
82
|
+
const pages = (bytes.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []).length
|
|
83
|
+
if (pages < 1) throw new Error('the printed document has no page objects')
|
|
84
|
+
return { bytes: bytes.length, pages }
|
|
85
|
+
})
|
|
66
86
|
await check('scrape concurrent', async () => {
|
|
67
87
|
const out = join(outDir, 'rows.jsonl')
|
|
68
88
|
const started = await provider.startScrape(session, {
|
|
@@ -1801,6 +1821,69 @@ await check('a confirm dialog can be dismissed, not only accepted', async () =>
|
|
|
1801
1821
|
await new Promise(resolve => setTimeout(resolve, 800))
|
|
1802
1822
|
}
|
|
1803
1823
|
})
|
|
1824
|
+
// 真机:同源子框架里的控件要能看到(元素带 frame 序号、坐标加上框架的矩形);
|
|
1825
|
+
// 跨源框架的 contentDocument 读不到,必须明确记成 readable:false —— 不假装能读。
|
|
1826
|
+
await check('the snapshot sees into same-origin frames and names the cross-origin ones', async () => {
|
|
1827
|
+
const size = await laidOut()
|
|
1828
|
+
if (size === null) return { skipped: 'the view is not laid out in this run' }
|
|
1829
|
+
const { createServer } = await import('node:http')
|
|
1830
|
+
const page = (port) => '<!doctype html><title>frames</title>'
|
|
1831
|
+
+ '<button id="outer">Outer Button</button>'
|
|
1832
|
+
+ '<iframe id="same" src="/inner" style="width:220px;height:60px"></iframe>'
|
|
1833
|
+
+ '<iframe id="cross" src="http://127.0.0.1:' + String(port) + '/inner" style="width:220px;height:60px"></iframe>'
|
|
1834
|
+
const innerPage = '<!doctype html><title>inner</title><button id="inside">Inner Button</button>'
|
|
1835
|
+
const inner = createServer((_request, response) => {
|
|
1836
|
+
response.writeHead(200, { 'content-type': 'text/html' })
|
|
1837
|
+
response.end(innerPage)
|
|
1838
|
+
})
|
|
1839
|
+
await new Promise(resolve => inner.listen(0, '127.0.0.1', resolve))
|
|
1840
|
+
// 路径要路由:主页面里的 src="/inner" 必须拿到内部页面,
|
|
1841
|
+
// 否则框架里装的就是主页面自己(第一版就是这样,量出来才发现)。
|
|
1842
|
+
const outerServer = createServer((request, response) => {
|
|
1843
|
+
response.writeHead(200, { 'content-type': 'text/html' })
|
|
1844
|
+
if (request.url === '/inner') {
|
|
1845
|
+
response.end(innerPage)
|
|
1846
|
+
return
|
|
1847
|
+
}
|
|
1848
|
+
response.end(page(inner.address().port))
|
|
1849
|
+
})
|
|
1850
|
+
await new Promise(resolve => outerServer.listen(0, '127.0.0.1', resolve))
|
|
1851
|
+
try {
|
|
1852
|
+
await provider.navigate(session, { url: 'http://127.0.0.1:' + String(outerServer.address().port) + '/' })
|
|
1853
|
+
let snapshot = null
|
|
1854
|
+
for (let i = 0; i < 25; i += 1) {
|
|
1855
|
+
await new Promise(resolve => setTimeout(resolve, 150))
|
|
1856
|
+
snapshot = await provider.snapshot(session, { limit: 50 })
|
|
1857
|
+
if ((snapshot.frames ?? []).length >= 2) break
|
|
1858
|
+
}
|
|
1859
|
+
const frames = snapshot.frames ?? []
|
|
1860
|
+
if (frames.length < 2) throw new Error('both iframes should be reported: ' + JSON.stringify(frames))
|
|
1861
|
+
const inside = snapshot.elements.find(element => element.label === 'Inner Button')
|
|
1862
|
+
if (inside === undefined) throw new Error('the same-origin frame content was not read: ' + JSON.stringify(snapshot.elements.map(e => e.label)))
|
|
1863
|
+
if (inside.frame !== 0) throw new Error('the frame element did not carry its index: ' + String(inside.frame))
|
|
1864
|
+
const outside = snapshot.elements.find(element => element.label === 'Outer Button')
|
|
1865
|
+
if (outside === undefined || outside.frame !== undefined) throw new Error('a top-level element must not claim a frame')
|
|
1866
|
+
const cross = frames.find(frame => frame.readable === false)
|
|
1867
|
+
if (cross === undefined) throw new Error('the cross-origin frame must be reported unreadable: ' + JSON.stringify(frames))
|
|
1868
|
+
const same = frames.find(frame => frame.readable === true)
|
|
1869
|
+
if (same === undefined) throw new Error('the same-origin frame should be readable: ' + JSON.stringify(frames))
|
|
1870
|
+
// 工具层也必须把这两样透出去。provider 有、工具层丢,才是这个 bug 的
|
|
1871
|
+
// 真正形态:只测 provider 的冒烟一直是绿的。
|
|
1872
|
+
// exec 不带 agent -> taskKey() 是 'default',正好复用上面这个会话。
|
|
1873
|
+
const viaTool = await call('browser_snapshot', { limit: 50 }, {})
|
|
1874
|
+
const toolFrames = viaTool.frames ?? []
|
|
1875
|
+
if (toolFrames.length < 2) throw new Error('the tool layer dropped frames: ' + JSON.stringify(viaTool.frames))
|
|
1876
|
+
const toolInside = (viaTool.elements ?? []).find(element => element.label === 'Inner Button')
|
|
1877
|
+
if (toolInside === undefined || toolInside.frame !== 0) {
|
|
1878
|
+
throw new Error('the tool layer dropped the element frame index: ' + JSON.stringify(toolInside))
|
|
1879
|
+
}
|
|
1880
|
+
return { frames: frames.length, inside: inside.frame, cross: String(cross.url).slice(0, 34) }
|
|
1881
|
+
} finally {
|
|
1882
|
+
outerServer.close()
|
|
1883
|
+
inner.close()
|
|
1884
|
+
}
|
|
1885
|
+
})
|
|
1886
|
+
|
|
1804
1887
|
// 真机:页面自己的 console 输出和网络请求必须能被读到(这是「为什么没成功」的唯一线索)。
|
|
1805
1888
|
await check('console and network capture what the page did', async () => {
|
|
1806
1889
|
const size = await laidOut()
|
|
@@ -28,7 +28,7 @@ const probe = async (label, options) => {
|
|
|
28
28
|
await view.sendCommand('Page.navigate', { url: 'https://example.com/' })
|
|
29
29
|
await wait(2500)
|
|
30
30
|
const reply = await view.sendCommand('Runtime.evaluate', {
|
|
31
|
-
expression: 'JSON.stringify({ chromeHost: document.getElementById("__dsh_browser_chrome_host__") !== null, binding: typeof window.__dshBrowserTaskAction, tasks: typeof window.__dshTasks, trail: typeof window.__dshTrail })',
|
|
31
|
+
expression: 'JSON.stringify({ chromeHost: document.getElementById("__dsh_browser_chrome_host__") !== null, painted: (() => { const h = document.getElementById("__dsh_browser_chrome_host__"); if (h === null) return 0; return Math.round(h.getBoundingClientRect().width) })(), binding: typeof window.__dshBrowserTaskAction, tasks: typeof window.__dshTasks, trail: typeof window.__dshTrail })',
|
|
32
32
|
returnByValue: true,
|
|
33
33
|
})
|
|
34
34
|
const value = JSON.parse(reply?.result?.value ?? '{}')
|
|
@@ -42,8 +42,10 @@ const probe = async (label, options) => {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// 两种模式都**显式**指定:默认值会变(2026-10-04 起默认 isolated),
|
|
46
|
+
// 用 `{}` 表示「main」会让这条检查在默认值一变就静默失效。
|
|
47
|
+
const isolated = await probe('isolated (default)', {})
|
|
48
|
+
const main = await probe('main (opt-in)', { chromeWorld: 'main' })
|
|
47
49
|
|
|
48
50
|
// Assert only what chromeWorld claims. window.__dshBrowserTaskAction is reported
|
|
49
51
|
// but not asserted: it appears asynchronously after load, so a fixed wait
|
|
@@ -60,4 +62,4 @@ if (failures.length > 0) {
|
|
|
60
62
|
console.log(JSON.stringify({ ok: false, failures }))
|
|
61
63
|
process.exit(1)
|
|
62
64
|
}
|
|
63
|
-
console.log(JSON.stringify({ ok: true, note: 'the chrome mounts in both modes; only the
|
|
65
|
+
console.log(JSON.stringify({ ok: true, note: 'the chrome mounts in both modes; only main (the opt-in bisect path) leaks its globals to the page' }))
|