@mehmoodqureshi/chrome-mcp 0.9.2 → 0.9.3
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.md +4 -2
- package/dist/shared/page-fns.js +208 -182
- package/dist/shared/screenshot.d.ts +14 -0
- package/dist/shared/screenshot.js +19 -4
- package/dist/shared/snapshot.d.ts +24 -2
- package/dist/shared/snapshot.js +117 -33
- package/dist/src/bridge/connection.d.ts +13 -0
- package/dist/src/bridge/connection.js +55 -1
- package/dist/src/bridge/server.d.ts +2 -0
- package/dist/src/bridge/server.js +5 -0
- package/dist/src/bridge/workspace.d.ts +1 -1
- package/dist/src/bridge/workspace.js +2 -2
- package/dist/src/executor/cdp-executor.d.ts +3 -2
- package/dist/src/executor/cdp-executor.js +21 -6
- package/dist/src/executor/extension-executor.d.ts +6 -2
- package/dist/src/executor/extension-executor.js +18 -5
- package/dist/src/executor/types.d.ts +25 -2
- package/dist/src/mcp/locate.d.ts +7 -4
- package/dist/src/mcp/locate.js +15 -8
- package/dist/src/mcp/tools.js +16 -9
- package/docs/BLUEPRINT.md +1 -1
- package/extension-dist/background.js +494 -263
- package/extension-dist/manifest.json +3 -2
- package/package.json +1 -1
|
@@ -285,29 +285,31 @@
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
// shared/snapshot.ts
|
|
288
|
-
function collectSnapshot(interactiveOnly = true, max = 200) {
|
|
288
|
+
function collectSnapshot(interactiveOnly = true, max = 200, locator = null) {
|
|
289
289
|
const INTERACTIVE = "a[href],button,input,select,textarea,[role=button],[role=link],[role=tab],[role=checkbox],[role=radio],[role=menuitem],[role=option],[role=switch],[contenteditable=true],[onclick]";
|
|
290
290
|
const LANDMARK = "h1,h2,h3,[role=heading],nav,main,header,footer,[role=navigation]";
|
|
291
291
|
const sel = interactiveOnly ? INTERACTIVE : `${INTERACTIVE},${LANDMARK}`;
|
|
292
292
|
const visible = (el) => {
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
const s = window.getComputedStyle(el);
|
|
296
|
-
if (s.visibility === "hidden" || s.display === "none") return false;
|
|
297
|
-
const cv = el.checkVisibility;
|
|
298
|
-
if (typeof cv === "function") {
|
|
293
|
+
const h = el;
|
|
294
|
+
if (typeof h.checkVisibility === "function") {
|
|
299
295
|
try {
|
|
300
|
-
|
|
296
|
+
if (!h.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true })) return false;
|
|
297
|
+
const r2 = h.getBoundingClientRect();
|
|
298
|
+
return r2.width !== 0 || r2.height !== 0;
|
|
301
299
|
} catch {
|
|
302
300
|
}
|
|
303
301
|
}
|
|
302
|
+
const r = h.getBoundingClientRect();
|
|
303
|
+
if (r.width === 0 && r.height === 0) return false;
|
|
304
|
+
const s = window.getComputedStyle(h);
|
|
305
|
+
if (s.visibility === "hidden" || s.display === "none") return false;
|
|
304
306
|
let p = el.parentElement;
|
|
305
307
|
while (p) {
|
|
306
308
|
const ps = window.getComputedStyle(p);
|
|
307
309
|
if (ps.display === "none" || ps.visibility === "hidden") return false;
|
|
308
310
|
p = p.parentElement;
|
|
309
311
|
}
|
|
310
|
-
if (
|
|
312
|
+
if (h.offsetParent === null && s.position !== "fixed") return false;
|
|
311
313
|
return true;
|
|
312
314
|
};
|
|
313
315
|
const accName = (el) => {
|
|
@@ -350,46 +352,98 @@
|
|
|
350
352
|
if (candidates.length >= max) return;
|
|
351
353
|
let matched;
|
|
352
354
|
try {
|
|
353
|
-
matched =
|
|
355
|
+
matched = root.querySelectorAll(sel);
|
|
354
356
|
} catch {
|
|
355
357
|
matched = [];
|
|
356
358
|
}
|
|
357
|
-
for (
|
|
359
|
+
for (let i = 0; i < matched.length; i++) {
|
|
358
360
|
if (candidates.length >= max) break;
|
|
361
|
+
const el = matched[i];
|
|
359
362
|
if (seen.has(el)) continue;
|
|
360
363
|
seen.add(el);
|
|
361
364
|
candidates.push(el);
|
|
362
365
|
}
|
|
363
366
|
let hosts;
|
|
364
367
|
try {
|
|
365
|
-
hosts =
|
|
368
|
+
hosts = root.querySelectorAll("*");
|
|
366
369
|
} catch {
|
|
367
370
|
hosts = [];
|
|
368
371
|
}
|
|
369
|
-
for (
|
|
372
|
+
for (let i = 0; i < hosts.length; i++) {
|
|
370
373
|
if (candidates.length >= max) break;
|
|
371
|
-
const sr =
|
|
374
|
+
const sr = hosts[i].shadowRoot;
|
|
372
375
|
if (sr) collect(sr);
|
|
373
376
|
}
|
|
374
377
|
};
|
|
375
378
|
collect(document);
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
let n = 0;
|
|
379
|
-
for (const el of els) {
|
|
380
|
-
if (nodes.length >= max) break;
|
|
381
|
-
const ref = `e${++n}`;
|
|
382
|
-
el.setAttribute("data-mcp-ref", ref);
|
|
383
|
-
const node = { ref, role: roleOf(el), name: accName(el), tag: el.tagName.toLowerCase() };
|
|
379
|
+
const read = (el) => {
|
|
380
|
+
const node = { role: roleOf(el), name: accName(el), tag: el.tagName.toLowerCase() };
|
|
384
381
|
const isSecret = el.tagName === "INPUT" && el.type === "password";
|
|
385
382
|
if (isSecret) node.secret = true;
|
|
386
383
|
const v = el.value;
|
|
387
384
|
if (!isSecret && typeof v === "string" && v) node.value = v.slice(0, 200);
|
|
388
385
|
if (el.disabled) node.disabled = true;
|
|
389
386
|
if (el.checked) node.checked = true;
|
|
390
|
-
|
|
387
|
+
return { el, node };
|
|
388
|
+
};
|
|
389
|
+
const els = candidates.filter(visible);
|
|
390
|
+
let picked;
|
|
391
|
+
let truncated = false;
|
|
392
|
+
let nearby;
|
|
393
|
+
let refPrefix = "e";
|
|
394
|
+
if (locator && (locator.role !== void 0 || locator.name !== void 0)) {
|
|
395
|
+
const norm = (x) => x.replace(/\s+/g, " ").trim().toLowerCase();
|
|
396
|
+
const wantRole = locator.role !== void 0 ? norm(locator.role) : null;
|
|
397
|
+
const wantName = locator.name;
|
|
398
|
+
const score = (role, name) => {
|
|
399
|
+
if (wantRole !== null && norm(role) !== wantRole) return 0;
|
|
400
|
+
if (!wantName) return 1;
|
|
401
|
+
const have = norm(name);
|
|
402
|
+
const need = norm(wantName);
|
|
403
|
+
if (!have) return 0;
|
|
404
|
+
if (name.trim() === wantName.trim()) return 4;
|
|
405
|
+
if (have === need) return 3;
|
|
406
|
+
if (have.startsWith(need)) return 2;
|
|
407
|
+
if (have.includes(need)) return 1;
|
|
408
|
+
return 0;
|
|
409
|
+
};
|
|
410
|
+
let best = 0;
|
|
411
|
+
picked = [];
|
|
412
|
+
const sameRole = [];
|
|
413
|
+
for (const el of els) {
|
|
414
|
+
const s = read(el);
|
|
415
|
+
const sc = score(s.node.role, s.node.name);
|
|
416
|
+
if (sc === 0) {
|
|
417
|
+
if ((wantRole === null || norm(s.node.role) === wantRole) && sameRole.length < 8) {
|
|
418
|
+
sameRole.push(`${s.node.role} "${s.node.name}"`);
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (sc > best) {
|
|
423
|
+
best = sc;
|
|
424
|
+
picked = [s];
|
|
425
|
+
} else if (sc === best) {
|
|
426
|
+
picked.push(s);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (picked.length === 0) nearby = sameRole;
|
|
430
|
+
refPrefix = `l${Date.now().toString(36).slice(-4)}-`;
|
|
431
|
+
} else {
|
|
432
|
+
picked = [];
|
|
433
|
+
for (const el of els) {
|
|
434
|
+
if (picked.length >= max) break;
|
|
435
|
+
picked.push(read(el));
|
|
436
|
+
}
|
|
437
|
+
truncated = els.length > picked.length;
|
|
391
438
|
}
|
|
392
|
-
|
|
439
|
+
const nodes = [];
|
|
440
|
+
let n = 0;
|
|
441
|
+
for (const { el, node } of picked) {
|
|
442
|
+
const ref = `${refPrefix}${++n}`;
|
|
443
|
+
el.setAttribute("data-mcp-ref", ref);
|
|
444
|
+
nodes.push({ ref, ...node });
|
|
445
|
+
}
|
|
446
|
+
return { url: location.href, title: document.title, nodes, truncated, ...nearby ? { nearby } : {} };
|
|
393
447
|
}
|
|
394
448
|
|
|
395
449
|
// shared/page-fns.ts
|
|
@@ -440,177 +494,197 @@
|
|
|
440
494
|
return { dx, dy, exact: true, scrollX: win.scrollX, scrollY: win.scrollY };
|
|
441
495
|
};
|
|
442
496
|
const sel = typeof a.selector === "string" && a.selector.length > 0 ? a.selector : null;
|
|
443
|
-
const el = sel ? deepQuery(sel) : null;
|
|
444
|
-
const missing = sel !== null && el === null;
|
|
445
497
|
const setValue = (node, next) => {
|
|
446
498
|
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), "value")?.set;
|
|
447
499
|
if (setter) setter.call(node, next);
|
|
448
500
|
else node.value = next;
|
|
449
501
|
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
450
502
|
};
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (missing) return { found: false };
|
|
461
|
-
const root = el ?? document.documentElement;
|
|
462
|
-
if (!root) return { found: true, html: "" };
|
|
463
|
-
const outer = a.outer === true || !sel;
|
|
464
|
-
return { found: true, html: outer ? root.outerHTML : root.innerHTML };
|
|
465
|
-
}
|
|
466
|
-
case "click": {
|
|
467
|
-
if (!el) return { found: false };
|
|
468
|
-
el.scrollIntoView({ block: "center" });
|
|
469
|
-
el.click();
|
|
470
|
-
return { found: true };
|
|
471
|
-
}
|
|
472
|
-
case "type": {
|
|
473
|
-
const node = el;
|
|
474
|
-
if (!node) return { found: false };
|
|
475
|
-
node.focus();
|
|
476
|
-
const next = (a.clear ? "" : node.value ?? "") + (a.text ?? "");
|
|
477
|
-
setValue(node, next);
|
|
478
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
479
|
-
return { found: true };
|
|
480
|
-
}
|
|
481
|
-
case "focus": {
|
|
482
|
-
const node = el;
|
|
483
|
-
if (!node) return { found: false };
|
|
484
|
-
node.focus();
|
|
485
|
-
if (a.clear) setValue(node, "");
|
|
486
|
-
return { found: true };
|
|
487
|
-
}
|
|
488
|
-
case "point": {
|
|
489
|
-
if (!el) return { found: false };
|
|
490
|
-
el.scrollIntoView({ block: "center", inline: "center" });
|
|
491
|
-
const r = el.getBoundingClientRect();
|
|
492
|
-
const off = frameOffset();
|
|
493
|
-
return {
|
|
494
|
-
found: true,
|
|
495
|
-
x: r.left + r.width / 2 + off.dx,
|
|
496
|
-
y: r.top + r.height / 2 + off.dy,
|
|
497
|
-
// false => this frame's coordinates cannot be mapped to the top viewport.
|
|
498
|
-
exact: off.exact
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
case "hover": {
|
|
502
|
-
if (!el) return { found: false };
|
|
503
|
-
el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
504
|
-
el.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
|
505
|
-
return { found: true };
|
|
506
|
-
}
|
|
507
|
-
case "select": {
|
|
508
|
-
const node = el;
|
|
509
|
-
if (!node || !node.options) return { found: false };
|
|
510
|
-
const want = new Set(a.values ?? []);
|
|
511
|
-
let matched = false;
|
|
512
|
-
for (const opt of Array.from(node.options)) {
|
|
513
|
-
const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
|
|
514
|
-
opt.selected = on;
|
|
515
|
-
if (on) matched = true;
|
|
503
|
+
const perform = (el2) => {
|
|
504
|
+
const missing = sel !== null && el2 === null;
|
|
505
|
+
switch (a.op) {
|
|
506
|
+
case "probe":
|
|
507
|
+
return { found: true, url: location.href, title: document.title };
|
|
508
|
+
case "text": {
|
|
509
|
+
if (missing) return { found: false };
|
|
510
|
+
const root = el2 ?? document.body;
|
|
511
|
+
return { found: true, text: root ? root.innerText ?? "" : "" };
|
|
516
512
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const dims = {
|
|
524
|
-
w: window.innerWidth,
|
|
525
|
-
h: window.innerHeight,
|
|
526
|
-
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
527
|
-
fullH: Math.max(d.scrollHeight, d.clientHeight)
|
|
528
|
-
};
|
|
529
|
-
if (!sel) return { found: true, dims, element: null, missing: false };
|
|
530
|
-
if (!el) return { found: true, dims, element: null, missing: true };
|
|
531
|
-
el.scrollIntoView({ block: "center", inline: "center" });
|
|
532
|
-
const r = el.getBoundingClientRect();
|
|
533
|
-
const off = frameOffset();
|
|
534
|
-
return {
|
|
535
|
-
found: true,
|
|
536
|
-
dims,
|
|
537
|
-
element: {
|
|
538
|
-
x: r.left + off.dx + off.scrollX,
|
|
539
|
-
y: r.top + off.dy + off.scrollY,
|
|
540
|
-
w: r.width,
|
|
541
|
-
h: r.height
|
|
542
|
-
},
|
|
543
|
-
missing: false,
|
|
544
|
-
exact: off.exact
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
case "scroll": {
|
|
548
|
-
if (el) el.scrollIntoView({ block: "center" });
|
|
549
|
-
else if (a.x != null || a.y != null) window.scrollTo(a.x ?? 0, a.y ?? 0);
|
|
550
|
-
else window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
|
|
551
|
-
return { found: !missing };
|
|
552
|
-
}
|
|
553
|
-
case "storage": {
|
|
554
|
-
const store = a.session ? window.sessionStorage : window.localStorage;
|
|
555
|
-
const op = a.storageOp;
|
|
556
|
-
if (op === "set") {
|
|
557
|
-
store.setItem(String(a.key), String(a.value ?? ""));
|
|
558
|
-
return { found: true, ok: true };
|
|
513
|
+
case "html": {
|
|
514
|
+
if (missing) return { found: false };
|
|
515
|
+
const root = el2 ?? document.documentElement;
|
|
516
|
+
if (!root) return { found: true, html: "" };
|
|
517
|
+
const outer = a.outer === true || !sel;
|
|
518
|
+
return { found: true, html: outer ? root.outerHTML : root.innerHTML };
|
|
559
519
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
520
|
+
case "click": {
|
|
521
|
+
if (!el2) return { found: false };
|
|
522
|
+
el2.scrollIntoView({ block: "center" });
|
|
523
|
+
el2.click();
|
|
524
|
+
return { found: true };
|
|
563
525
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
return { found:
|
|
526
|
+
case "type": {
|
|
527
|
+
const node = el2;
|
|
528
|
+
if (!node) return { found: false };
|
|
529
|
+
node.focus();
|
|
530
|
+
const next = (a.clear ? "" : node.value ?? "") + (a.text ?? "");
|
|
531
|
+
setValue(node, next);
|
|
532
|
+
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
533
|
+
return { found: true };
|
|
567
534
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (
|
|
535
|
+
case "focus": {
|
|
536
|
+
const node = el2;
|
|
537
|
+
if (!node) return { found: false };
|
|
538
|
+
node.focus();
|
|
539
|
+
if (a.clear) setValue(node, "");
|
|
540
|
+
return { found: true };
|
|
573
541
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
setTimeout(tick, every);
|
|
542
|
+
case "point": {
|
|
543
|
+
if (!el2) return { found: false };
|
|
544
|
+
el2.scrollIntoView({ block: "center", inline: "center" });
|
|
545
|
+
const r = el2.getBoundingClientRect();
|
|
546
|
+
const off = frameOffset();
|
|
547
|
+
return {
|
|
548
|
+
found: true,
|
|
549
|
+
x: r.left + r.width / 2 + off.dx,
|
|
550
|
+
y: r.top + r.height / 2 + off.dy,
|
|
551
|
+
// false => this frame's coordinates cannot be mapped to the top viewport.
|
|
552
|
+
exact: off.exact
|
|
586
553
|
};
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
554
|
+
}
|
|
555
|
+
case "hover": {
|
|
556
|
+
if (!el2) return { found: false };
|
|
557
|
+
el2.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
558
|
+
el2.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
|
559
|
+
return { found: true };
|
|
560
|
+
}
|
|
561
|
+
case "select": {
|
|
562
|
+
const node = el2;
|
|
563
|
+
if (!node || !node.options) return { found: false };
|
|
564
|
+
const want = new Set(a.values ?? []);
|
|
565
|
+
let matched = false;
|
|
566
|
+
for (const opt of Array.from(node.options)) {
|
|
567
|
+
const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
|
|
568
|
+
opt.selected = on;
|
|
569
|
+
if (on) matched = true;
|
|
570
|
+
}
|
|
571
|
+
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
572
|
+
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
573
|
+
return { found: true, matched };
|
|
574
|
+
}
|
|
575
|
+
case "measure": {
|
|
576
|
+
const d = document.documentElement;
|
|
577
|
+
const dims = {
|
|
578
|
+
w: window.innerWidth,
|
|
579
|
+
h: window.innerHeight,
|
|
580
|
+
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
581
|
+
fullH: Math.max(d.scrollHeight, d.clientHeight),
|
|
582
|
+
dpr: window.devicePixelRatio || 1,
|
|
583
|
+
scrollX: window.scrollX,
|
|
584
|
+
scrollY: window.scrollY
|
|
602
585
|
};
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
586
|
+
if (!sel) return { found: true, dims, element: null, missing: false };
|
|
587
|
+
if (!el2) return { found: true, dims, element: null, missing: true };
|
|
588
|
+
el2.scrollIntoView({ block: "center", inline: "center" });
|
|
589
|
+
const r = el2.getBoundingClientRect();
|
|
590
|
+
const off = frameOffset();
|
|
591
|
+
return {
|
|
592
|
+
found: true,
|
|
593
|
+
dims,
|
|
594
|
+
element: {
|
|
595
|
+
x: r.left + off.dx + off.scrollX,
|
|
596
|
+
y: r.top + off.dy + off.scrollY,
|
|
597
|
+
w: r.width,
|
|
598
|
+
h: r.height
|
|
599
|
+
},
|
|
600
|
+
missing: false,
|
|
601
|
+
exact: off.exact
|
|
607
602
|
};
|
|
608
|
-
|
|
609
|
-
|
|
603
|
+
}
|
|
604
|
+
case "scroll": {
|
|
605
|
+
if (el2) el2.scrollIntoView({ block: "center" });
|
|
606
|
+
else if (a.x != null || a.y != null) window.scrollTo(a.x ?? 0, a.y ?? 0);
|
|
607
|
+
else window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
|
|
608
|
+
return { found: !missing };
|
|
609
|
+
}
|
|
610
|
+
case "storage": {
|
|
611
|
+
const store = a.session ? window.sessionStorage : window.localStorage;
|
|
612
|
+
const op = a.storageOp;
|
|
613
|
+
if (op === "set") {
|
|
614
|
+
store.setItem(String(a.key), String(a.value ?? ""));
|
|
615
|
+
return { found: true, ok: true };
|
|
616
|
+
}
|
|
617
|
+
if (op === "remove") {
|
|
618
|
+
store.removeItem(String(a.key));
|
|
619
|
+
return { found: true, ok: true };
|
|
620
|
+
}
|
|
621
|
+
if (op === "clear") {
|
|
622
|
+
store.clear();
|
|
623
|
+
return { found: true, ok: true };
|
|
624
|
+
}
|
|
625
|
+
if (a.key) return { found: true, ok: true, value: store.getItem(a.key) };
|
|
626
|
+
const entries = {};
|
|
627
|
+
for (let i = 0; i < store.length; i++) {
|
|
628
|
+
const k = store.key(i);
|
|
629
|
+
if (k) entries[k] = store.getItem(k) ?? "";
|
|
630
|
+
}
|
|
631
|
+
return { found: true, ok: true, entries };
|
|
632
|
+
}
|
|
633
|
+
// -- the two polling ops: one injection that resolves in-page, rather than
|
|
634
|
+
// one executeScript round-trip per tick --
|
|
635
|
+
case "waitSelector": {
|
|
636
|
+
const deadline = Date.now() + (a.timeoutMs ?? 5e3);
|
|
637
|
+
const every = a.interval ?? 120;
|
|
638
|
+
return new Promise((resolve) => {
|
|
639
|
+
const tick = () => {
|
|
640
|
+
if (sel && deepQuery(sel)) return resolve({ found: true });
|
|
641
|
+
if (Date.now() > deadline) return resolve({ found: false });
|
|
642
|
+
setTimeout(tick, every);
|
|
643
|
+
};
|
|
644
|
+
tick();
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
case "waitFor": {
|
|
648
|
+
const deadline = Date.now() + (a.timeoutMs ?? 3e4);
|
|
649
|
+
const every = a.interval ?? 150;
|
|
650
|
+
const want = typeof a.textContains === "string" && a.textContains.length > 0 ? a.textContains : null;
|
|
651
|
+
const gone = a.gone === true;
|
|
652
|
+
return new Promise((resolve) => {
|
|
653
|
+
const hit = () => {
|
|
654
|
+
let present;
|
|
655
|
+
if (sel) present = !!deepQuery(sel);
|
|
656
|
+
else if (want) present = (document.body?.innerText ?? "").includes(want);
|
|
657
|
+
else present = true;
|
|
658
|
+
return gone ? !present : present;
|
|
659
|
+
};
|
|
660
|
+
const tick = () => {
|
|
661
|
+
if (hit()) return resolve({ found: true, matched: true });
|
|
662
|
+
if (Date.now() > deadline) return resolve({ found: true, matched: false });
|
|
663
|
+
setTimeout(tick, every);
|
|
664
|
+
};
|
|
665
|
+
tick();
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
default:
|
|
669
|
+
return { found: false, error: `unknown page op: ${String(a.op)}` };
|
|
610
670
|
}
|
|
611
|
-
|
|
612
|
-
|
|
671
|
+
};
|
|
672
|
+
const el = sel ? deepQuery(sel) : null;
|
|
673
|
+
const waits = a.op === "click" || a.op === "type" || a.op === "focus" || a.op === "point" || a.op === "hover" || a.op === "select";
|
|
674
|
+
if (waits && sel && !el && a.timeoutMs && a.timeoutMs > 0) {
|
|
675
|
+
const deadline = Date.now() + a.timeoutMs;
|
|
676
|
+
const every = a.interval ?? 120;
|
|
677
|
+
return new Promise((resolve) => {
|
|
678
|
+
const tick = () => {
|
|
679
|
+
const hit = deepQuery(sel);
|
|
680
|
+
if (hit) return resolve(perform(hit));
|
|
681
|
+
if (Date.now() > deadline) return resolve({ found: false });
|
|
682
|
+
setTimeout(tick, every);
|
|
683
|
+
};
|
|
684
|
+
setTimeout(tick, every);
|
|
685
|
+
});
|
|
613
686
|
}
|
|
687
|
+
return perform(el);
|
|
614
688
|
}
|
|
615
689
|
|
|
616
690
|
// shared/observers.ts
|
|
@@ -676,13 +750,18 @@
|
|
|
676
750
|
}
|
|
677
751
|
|
|
678
752
|
// shared/screenshot.ts
|
|
753
|
+
var DEFAULT_SCREENSHOT_FORMAT = "jpeg";
|
|
754
|
+
var DEFAULT_JPEG_QUALITY = 70;
|
|
755
|
+
var DEFAULT_SCREENSHOT_SCALE = 1;
|
|
679
756
|
var MAX_CAPTURE_PX = 16384;
|
|
680
757
|
function planScreenshot(dims, opts = {}) {
|
|
758
|
+
const dpr = dims.dpr && dims.dpr > 0 ? dims.dpr : 1;
|
|
759
|
+
const scale = (opts.scale && opts.scale > 0 ? opts.scale : DEFAULT_SCREENSHOT_SCALE) / dpr;
|
|
681
760
|
if (opts.element) {
|
|
682
761
|
const realH = Math.max(1, Math.round(opts.element.h));
|
|
683
762
|
const clipH = Math.min(opts.element.h, MAX_CAPTURE_PX);
|
|
684
763
|
return {
|
|
685
|
-
clip: { x: opts.element.x, y: opts.element.y, width: opts.element.w, height: clipH, scale
|
|
764
|
+
clip: { x: opts.element.x, y: opts.element.y, width: opts.element.w, height: clipH, scale },
|
|
686
765
|
captureBeyondViewport: true,
|
|
687
766
|
width: Math.max(1, Math.round(opts.element.w)),
|
|
688
767
|
height: Math.min(realH, MAX_CAPTURE_PX),
|
|
@@ -693,7 +772,7 @@
|
|
|
693
772
|
if (opts.fullPage) {
|
|
694
773
|
const clipH = Math.min(dims.fullH, MAX_CAPTURE_PX);
|
|
695
774
|
return {
|
|
696
|
-
clip: { x: 0, y: 0, width: dims.fullW, height: clipH, scale
|
|
775
|
+
clip: { x: 0, y: 0, width: dims.fullW, height: clipH, scale },
|
|
697
776
|
captureBeyondViewport: true,
|
|
698
777
|
width: dims.fullW,
|
|
699
778
|
height: clipH,
|
|
@@ -702,6 +781,7 @@
|
|
|
702
781
|
};
|
|
703
782
|
}
|
|
704
783
|
return {
|
|
784
|
+
...scale !== 1 ? { clip: { x: dims.scrollX ?? 0, y: dims.scrollY ?? 0, width: dims.w, height: dims.h, scale } } : {},
|
|
705
785
|
captureBeyondViewport: false,
|
|
706
786
|
width: dims.w,
|
|
707
787
|
height: dims.h,
|
|
@@ -794,16 +874,21 @@
|
|
|
794
874
|
async function targetTab(cmd) {
|
|
795
875
|
return cmd.tabId ? parseTabId(cmd.tabId) : currentTabId();
|
|
796
876
|
}
|
|
797
|
-
|
|
877
|
+
var TABLESS = /* @__PURE__ */ new Set(["tabs_list", "tab_new", "ping_probe", "download_file"]);
|
|
878
|
+
async function resolveTab(cmd) {
|
|
879
|
+
if (TABLESS.has(cmd.method)) return null;
|
|
880
|
+
return targetTab(cmd);
|
|
881
|
+
}
|
|
882
|
+
async function urlForCommand(cmd, tab) {
|
|
798
883
|
if (cmd.method === "navigate") {
|
|
799
884
|
const u = cmd.params.url;
|
|
800
885
|
return typeof u === "string" ? u : "";
|
|
801
886
|
}
|
|
802
|
-
return observedTabUrl(cmd);
|
|
887
|
+
return observedTabUrl(cmd, tab);
|
|
803
888
|
}
|
|
804
|
-
async function observedTabUrl(cmd) {
|
|
889
|
+
async function observedTabUrl(cmd, tab) {
|
|
805
890
|
try {
|
|
806
|
-
const tabId = await targetTab(cmd);
|
|
891
|
+
const tabId = tab ?? await targetTab(cmd);
|
|
807
892
|
const t = await chrome.tabs.get(tabId);
|
|
808
893
|
return t.url ?? "";
|
|
809
894
|
} catch {
|
|
@@ -836,14 +921,61 @@
|
|
|
836
921
|
if (outcomes.length === 0) throw new CmdError("TARGET_GONE", "the page returned no result for this command");
|
|
837
922
|
return outcomes.find((o) => o.found === true) ?? outcomes[0];
|
|
838
923
|
}
|
|
839
|
-
|
|
924
|
+
function waitUntilOf(cmd) {
|
|
925
|
+
const w = cmd.params.waitUntil;
|
|
926
|
+
return w === "domcontentloaded" || w === "networkidle" ? w : "load";
|
|
927
|
+
}
|
|
928
|
+
function armNavigationWait(tabId, waitUntil, timeoutMs = 3e4) {
|
|
840
929
|
const start = Date.now();
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
930
|
+
const NETWORK_QUIET_MS = 500;
|
|
931
|
+
let done = () => void 0;
|
|
932
|
+
const finished = new Promise((resolve) => {
|
|
933
|
+
done = resolve;
|
|
934
|
+
});
|
|
935
|
+
const isTop = (d) => d.tabId === tabId && d.frameId === 0 && d.timeStamp >= start - 1;
|
|
936
|
+
const onDom = (d) => {
|
|
937
|
+
if (isTop(d) && waitUntil === "domcontentloaded") done();
|
|
938
|
+
};
|
|
939
|
+
const onCompleted = (d) => {
|
|
940
|
+
if (!isTop(d)) return;
|
|
941
|
+
if (waitUntil === "networkidle") setTimeout(done, NETWORK_QUIET_MS);
|
|
942
|
+
else done();
|
|
943
|
+
};
|
|
944
|
+
const onSameDoc = (d) => {
|
|
945
|
+
if (isTop(d)) done();
|
|
946
|
+
};
|
|
947
|
+
const onError = (d) => {
|
|
948
|
+
if (isTop(d)) done();
|
|
949
|
+
};
|
|
950
|
+
const onUpdated = (id, info) => {
|
|
951
|
+
if (id === tabId && info.status === "complete") done();
|
|
952
|
+
};
|
|
953
|
+
chrome.webNavigation.onDOMContentLoaded.addListener(onDom);
|
|
954
|
+
chrome.webNavigation.onCompleted.addListener(onCompleted);
|
|
955
|
+
chrome.webNavigation.onHistoryStateUpdated.addListener(onSameDoc);
|
|
956
|
+
chrome.webNavigation.onReferenceFragmentUpdated.addListener(onSameDoc);
|
|
957
|
+
chrome.webNavigation.onErrorOccurred.addListener(onError);
|
|
958
|
+
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
959
|
+
const cleanup = () => {
|
|
960
|
+
chrome.webNavigation.onDOMContentLoaded.removeListener(onDom);
|
|
961
|
+
chrome.webNavigation.onCompleted.removeListener(onCompleted);
|
|
962
|
+
chrome.webNavigation.onHistoryStateUpdated.removeListener(onSameDoc);
|
|
963
|
+
chrome.webNavigation.onReferenceFragmentUpdated.removeListener(onSameDoc);
|
|
964
|
+
chrome.webNavigation.onErrorOccurred.removeListener(onError);
|
|
965
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
966
|
+
};
|
|
967
|
+
return {
|
|
968
|
+
async wait() {
|
|
969
|
+
const timer = setTimeout(done, timeoutMs);
|
|
970
|
+
try {
|
|
971
|
+
await finished;
|
|
972
|
+
} finally {
|
|
973
|
+
clearTimeout(timer);
|
|
974
|
+
cleanup();
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
cancel: cleanup
|
|
978
|
+
};
|
|
847
979
|
}
|
|
848
980
|
function resolveSelector(cmd) {
|
|
849
981
|
const s = cmd.params.selector;
|
|
@@ -855,29 +987,73 @@
|
|
|
855
987
|
function selectorOf(cmd) {
|
|
856
988
|
return resolveSelector(cmd);
|
|
857
989
|
}
|
|
858
|
-
|
|
990
|
+
var ACTION_WAIT_MS = 5e3;
|
|
991
|
+
var withWait = (a) => ({ ...a, timeoutMs: ACTION_WAIT_MS, interval: 120 });
|
|
992
|
+
async function waitForSelector(tabId, selector, frameIds, timeoutMs = ACTION_WAIT_MS) {
|
|
859
993
|
const out = await execOp(tabId, { op: "waitSelector", selector, timeoutMs, interval: 120 }, frameIds);
|
|
860
994
|
return out.found === true;
|
|
861
995
|
}
|
|
996
|
+
var DEBUGGER_GRACE_MS = 1500;
|
|
997
|
+
var dbgSessions = /* @__PURE__ */ new Map();
|
|
998
|
+
function forgetDebugger(tabId) {
|
|
999
|
+
const s = dbgSessions.get(tabId);
|
|
1000
|
+
if (s?.detachTimer) clearTimeout(s.detachTimer);
|
|
1001
|
+
dbgSessions.delete(tabId);
|
|
1002
|
+
}
|
|
1003
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
1004
|
+
if (source.tabId !== void 0) forgetDebugger(source.tabId);
|
|
1005
|
+
});
|
|
1006
|
+
chrome.tabs.onRemoved.addListener((tabId) => forgetDebugger(tabId));
|
|
1007
|
+
function scheduleDetach(tabId) {
|
|
1008
|
+
const s = dbgSessions.get(tabId);
|
|
1009
|
+
if (!s) return;
|
|
1010
|
+
if (s.detachTimer) clearTimeout(s.detachTimer);
|
|
1011
|
+
s.detachTimer = setTimeout(() => {
|
|
1012
|
+
dbgSessions.delete(tabId);
|
|
1013
|
+
chrome.debugger.detach({ tabId }).catch(() => void 0);
|
|
1014
|
+
}, DEBUGGER_GRACE_MS);
|
|
1015
|
+
}
|
|
1016
|
+
async function ensureAttached(tabId) {
|
|
1017
|
+
const s = dbgSessions.get(tabId);
|
|
1018
|
+
if (s) {
|
|
1019
|
+
if (s.detachTimer) clearTimeout(s.detachTimer);
|
|
1020
|
+
s.detachTimer = null;
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
1024
|
+
dbgSessions.set(tabId, { detachTimer: null });
|
|
1025
|
+
}
|
|
1026
|
+
var DETACHED_RE = /not attached|no target with given id|detached/i;
|
|
862
1027
|
async function withDebugger(tabId, fn) {
|
|
863
1028
|
return locks.run(`dbg:${tabId}`, async () => {
|
|
864
1029
|
const target = { tabId };
|
|
865
|
-
await
|
|
1030
|
+
await ensureAttached(tabId);
|
|
866
1031
|
try {
|
|
867
1032
|
return await fn(target);
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
if (!DETACHED_RE.test(String(err?.message ?? err))) throw err;
|
|
1035
|
+
forgetDebugger(tabId);
|
|
1036
|
+
await ensureAttached(tabId);
|
|
1037
|
+
return await fn(target);
|
|
868
1038
|
} finally {
|
|
869
|
-
|
|
1039
|
+
scheduleDetach(tabId);
|
|
870
1040
|
}
|
|
871
1041
|
});
|
|
872
1042
|
}
|
|
873
|
-
async function trustedType(tabId, selector, text, clear, frameIds) {
|
|
874
|
-
const focused = await execOp(tabId, { op: "focus", selector, clear }, frameIds);
|
|
1043
|
+
async function trustedType(tabId, selector, text, clear, frameIds, pressEnter = false) {
|
|
1044
|
+
const focused = await execOp(tabId, withWait({ op: "focus", selector, clear }), frameIds);
|
|
875
1045
|
if (!focused.found) return false;
|
|
876
|
-
await withDebugger(tabId, (t) =>
|
|
1046
|
+
await withDebugger(tabId, async (t) => {
|
|
1047
|
+
await chrome.debugger.sendCommand(t, "Input.insertText", { text });
|
|
1048
|
+
if (!pressEnter) return;
|
|
1049
|
+
for (const type of ["keyDown", "keyUp"]) {
|
|
1050
|
+
await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
877
1053
|
return true;
|
|
878
1054
|
}
|
|
879
1055
|
async function trustedClick(tabId, selector, frameIds) {
|
|
880
|
-
const pt = await execOp(tabId, { op: "point", selector }, frameIds);
|
|
1056
|
+
const pt = await execOp(tabId, withWait({ op: "point", selector }), frameIds);
|
|
881
1057
|
if (!pt.found) return "missing";
|
|
882
1058
|
if (pt.exact === false) return "inexact";
|
|
883
1059
|
await withDebugger(tabId, async (t) => {
|
|
@@ -896,12 +1072,28 @@
|
|
|
896
1072
|
missing: found.missing === true
|
|
897
1073
|
};
|
|
898
1074
|
}
|
|
899
|
-
|
|
1075
|
+
function encodingOf(cmd) {
|
|
1076
|
+
const f = cmd.params.format;
|
|
1077
|
+
const q = cmd.params.quality;
|
|
1078
|
+
const sc = cmd.params.scale;
|
|
1079
|
+
return {
|
|
1080
|
+
format: f === "png" || f === "jpeg" ? f : DEFAULT_SCREENSHOT_FORMAT,
|
|
1081
|
+
quality: typeof q === "number" && q >= 1 && q <= 100 ? Math.round(q) : DEFAULT_JPEG_QUALITY,
|
|
1082
|
+
scale: typeof sc === "number" && sc > 0 ? sc : void 0
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function screenshotViaDebugger(tabId, fullPage, enc, selector, frameIds) {
|
|
900
1086
|
const measured = await measurePage(tabId, selector, frameIds);
|
|
901
1087
|
if (!measured) throw new CmdError("CDP_ERROR", "could not read page dimensions");
|
|
902
1088
|
if (selector && measured.missing) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selector}`);
|
|
903
|
-
const plan = planScreenshot(measured.dims, { fullPage, element: measured.element });
|
|
904
|
-
const params = {
|
|
1089
|
+
const plan = planScreenshot(measured.dims, { fullPage, element: measured.element, scale: enc.scale });
|
|
1090
|
+
const params = {
|
|
1091
|
+
format: enc.format,
|
|
1092
|
+
captureBeyondViewport: plan.captureBeyondViewport,
|
|
1093
|
+
// Skip the slow PNG compression pass; bytes-on-the-wire matter less than latency here.
|
|
1094
|
+
optimizeForSpeed: true
|
|
1095
|
+
};
|
|
1096
|
+
if (enc.format === "jpeg") params.quality = enc.quality;
|
|
905
1097
|
if (plan.clip) params.clip = plan.clip;
|
|
906
1098
|
const data = await withDebugger(tabId, async (target) => {
|
|
907
1099
|
const res = await chrome.debugger.sendCommand(target, "Page.captureScreenshot", params);
|
|
@@ -909,14 +1101,14 @@
|
|
|
909
1101
|
});
|
|
910
1102
|
return {
|
|
911
1103
|
dataBase64: data,
|
|
912
|
-
mimeType: "image/png",
|
|
1104
|
+
mimeType: enc.format === "jpeg" ? "image/jpeg" : "image/png",
|
|
913
1105
|
width: plan.width,
|
|
914
1106
|
height: plan.height,
|
|
915
1107
|
truncated: plan.truncated,
|
|
916
1108
|
fullHeight: plan.fullHeight
|
|
917
1109
|
};
|
|
918
1110
|
}
|
|
919
|
-
async function screenshotViaVisibleTab(tabId, fullPage) {
|
|
1111
|
+
async function screenshotViaVisibleTab(tabId, fullPage, enc) {
|
|
920
1112
|
let t = await chrome.tabs.get(tabId);
|
|
921
1113
|
if (!t.active) {
|
|
922
1114
|
await chrome.tabs.update(tabId, { active: true });
|
|
@@ -929,12 +1121,15 @@
|
|
|
929
1121
|
() => ({ w: window.innerWidth, h: window.innerHeight, full: document.documentElement.scrollHeight }),
|
|
930
1122
|
[]
|
|
931
1123
|
);
|
|
932
|
-
const dataUrl = await chrome.tabs.captureVisibleTab(
|
|
1124
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(
|
|
1125
|
+
t.windowId,
|
|
1126
|
+
enc.format === "jpeg" ? { format: "jpeg", quality: enc.quality } : { format: "png" }
|
|
1127
|
+
);
|
|
933
1128
|
const viewportH = dims?.h ?? 0;
|
|
934
1129
|
const fullH = dims?.full ?? viewportH;
|
|
935
1130
|
return {
|
|
936
1131
|
dataBase64: dataUrl.split(",")[1] ?? "",
|
|
937
|
-
mimeType: "image/png",
|
|
1132
|
+
mimeType: enc.format === "jpeg" ? "image/jpeg" : "image/png",
|
|
938
1133
|
width: dims?.w ?? 0,
|
|
939
1134
|
height: viewportH,
|
|
940
1135
|
truncated: fullPage && fullH > viewportH,
|
|
@@ -1036,7 +1231,12 @@
|
|
|
1036
1231
|
}
|
|
1037
1232
|
return allowed;
|
|
1038
1233
|
}
|
|
1039
|
-
|
|
1234
|
+
/**
|
|
1235
|
+
* Execute one command. `tab` is the id `resolveTab` already produced for it
|
|
1236
|
+
* (the router resolves once and shares it); omitted → resolve here.
|
|
1237
|
+
*/
|
|
1238
|
+
async run(cmd, tab = null) {
|
|
1239
|
+
const targetTab2 = async (c) => tab ?? (c.tabId ? parseTabId(c.tabId) : currentTabId());
|
|
1040
1240
|
switch (cmd.method) {
|
|
1041
1241
|
case "ping_probe":
|
|
1042
1242
|
return {};
|
|
@@ -1071,8 +1271,12 @@
|
|
|
1071
1271
|
return { id: created.id, reused: false, needsNav: false };
|
|
1072
1272
|
});
|
|
1073
1273
|
if (claim.needsNav) {
|
|
1074
|
-
|
|
1075
|
-
await
|
|
1274
|
+
const nav = armNavigationWait(claim.id, "load");
|
|
1275
|
+
await chrome.tabs.update(claim.id, { url }).catch((err) => {
|
|
1276
|
+
nav.cancel();
|
|
1277
|
+
throw err;
|
|
1278
|
+
});
|
|
1279
|
+
await nav.wait();
|
|
1076
1280
|
}
|
|
1077
1281
|
if (active) {
|
|
1078
1282
|
const t = await chrome.tabs.get(claim.id);
|
|
@@ -1088,42 +1292,56 @@
|
|
|
1088
1292
|
}
|
|
1089
1293
|
// -- navigation --
|
|
1090
1294
|
case "navigate": {
|
|
1091
|
-
const id = await
|
|
1295
|
+
const id = await targetTab2(cmd);
|
|
1092
1296
|
const url = String(cmd.params.url);
|
|
1093
|
-
|
|
1094
|
-
await
|
|
1297
|
+
const nav = armNavigationWait(id, waitUntilOf(cmd));
|
|
1298
|
+
await chrome.tabs.update(id, { url }).catch((err) => {
|
|
1299
|
+
nav.cancel();
|
|
1300
|
+
throw err;
|
|
1301
|
+
});
|
|
1302
|
+
await nav.wait();
|
|
1095
1303
|
const t = await chrome.tabs.get(id);
|
|
1096
1304
|
return { url: t.url ?? url, title: t.title ?? "" };
|
|
1097
1305
|
}
|
|
1098
1306
|
case "back": {
|
|
1099
|
-
const id = await
|
|
1100
|
-
|
|
1307
|
+
const id = await targetTab2(cmd);
|
|
1308
|
+
const nav = armNavigationWait(id, "load", 15e3);
|
|
1309
|
+
const moved = await chrome.tabs.goBack(id).then(() => true, () => false);
|
|
1310
|
+
if (moved) await nav.wait();
|
|
1311
|
+
else nav.cancel();
|
|
1101
1312
|
const t = await chrome.tabs.get(id);
|
|
1102
1313
|
return { url: t.url ?? "", title: t.title ?? "" };
|
|
1103
1314
|
}
|
|
1104
1315
|
case "forward": {
|
|
1105
|
-
const id = await
|
|
1106
|
-
|
|
1316
|
+
const id = await targetTab2(cmd);
|
|
1317
|
+
const nav = armNavigationWait(id, "load", 15e3);
|
|
1318
|
+
const moved = await chrome.tabs.goForward(id).then(() => true, () => false);
|
|
1319
|
+
if (moved) await nav.wait();
|
|
1320
|
+
else nav.cancel();
|
|
1107
1321
|
const t = await chrome.tabs.get(id);
|
|
1108
1322
|
return { url: t.url ?? "", title: t.title ?? "" };
|
|
1109
1323
|
}
|
|
1110
1324
|
case "reload": {
|
|
1111
|
-
const id = await
|
|
1112
|
-
|
|
1113
|
-
await
|
|
1325
|
+
const id = await targetTab2(cmd);
|
|
1326
|
+
const nav = armNavigationWait(id, waitUntilOf(cmd));
|
|
1327
|
+
await chrome.tabs.reload(id).catch((err) => {
|
|
1328
|
+
nav.cancel();
|
|
1329
|
+
throw err;
|
|
1330
|
+
});
|
|
1331
|
+
await nav.wait();
|
|
1114
1332
|
const t = await chrome.tabs.get(id);
|
|
1115
1333
|
return { url: t.url ?? "", title: t.title ?? "" };
|
|
1116
1334
|
}
|
|
1117
1335
|
// -- reads (isolated world; CSP-safe) --
|
|
1118
1336
|
case "get_text": {
|
|
1119
|
-
const id = await
|
|
1337
|
+
const id = await targetTab2(cmd);
|
|
1120
1338
|
const frameIds = await this.frames(cmd, id);
|
|
1121
1339
|
const out = await execOp(id, { op: "text", selector: selectorOf(cmd) ?? null }, frameIds);
|
|
1122
1340
|
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selectorOf(cmd)}`);
|
|
1123
1341
|
return { text: String(out.text ?? ""), frameId: out.frameId };
|
|
1124
1342
|
}
|
|
1125
1343
|
case "get_html": {
|
|
1126
|
-
const id = await
|
|
1344
|
+
const id = await targetTab2(cmd);
|
|
1127
1345
|
const frameIds = await this.frames(cmd, id);
|
|
1128
1346
|
const out = await execOp(
|
|
1129
1347
|
id,
|
|
@@ -1135,14 +1353,19 @@
|
|
|
1135
1353
|
}
|
|
1136
1354
|
// -- accessibility snapshot (tags elements with data-mcp-ref so refs work) --
|
|
1137
1355
|
case "snapshot": {
|
|
1138
|
-
const id = await
|
|
1356
|
+
const id = await targetTab2(cmd);
|
|
1139
1357
|
const interactiveOnly = cmd.params.interactiveOnly !== false;
|
|
1140
1358
|
const max = typeof cmd.params.max === "number" ? cmd.params.max : 200;
|
|
1359
|
+
const loc = cmd.params.locator;
|
|
1360
|
+
const locator = loc && typeof loc === "object" ? {
|
|
1361
|
+
role: typeof loc.role === "string" ? loc.role : void 0,
|
|
1362
|
+
name: typeof loc.name === "string" ? loc.name : void 0
|
|
1363
|
+
} : null;
|
|
1141
1364
|
const frameIds = await this.frames(cmd, id);
|
|
1142
1365
|
const raw = await execInTab(
|
|
1143
1366
|
id,
|
|
1144
1367
|
collectSnapshot,
|
|
1145
|
-
[interactiveOnly, max],
|
|
1368
|
+
[interactiveOnly, max, locator],
|
|
1146
1369
|
void 0,
|
|
1147
1370
|
frameIds
|
|
1148
1371
|
);
|
|
@@ -1150,19 +1373,18 @@
|
|
|
1150
1373
|
}
|
|
1151
1374
|
// -- <select> option(s) by value or visible label --
|
|
1152
1375
|
case "select_option": {
|
|
1153
|
-
const id = await
|
|
1376
|
+
const id = await targetTab2(cmd);
|
|
1154
1377
|
const sel = requireSelector(cmd);
|
|
1155
1378
|
const frameIds = await this.frames(cmd, id);
|
|
1156
1379
|
const values = Array.isArray(cmd.params.values) ? cmd.params.values.map(String) : [];
|
|
1157
|
-
await
|
|
1158
|
-
const out = await execOp(id, { op: "select", selector: sel, values }, frameIds);
|
|
1380
|
+
const out = await execOp(id, withWait({ op: "select", selector: sel, values }), frameIds);
|
|
1159
1381
|
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1160
1382
|
if (out.matched !== true) throw new CmdError("SELECTOR_NOT_FOUND", `no <select> option matched for ${sel}`);
|
|
1161
1383
|
return { ok: true, frameId: out.frameId };
|
|
1162
1384
|
}
|
|
1163
1385
|
// -- cookies for the tab's URL (chrome.cookies; needs "cookies" permission) --
|
|
1164
1386
|
case "get_cookies": {
|
|
1165
|
-
const id = await
|
|
1387
|
+
const id = await targetTab2(cmd);
|
|
1166
1388
|
const t = await chrome.tabs.get(id);
|
|
1167
1389
|
const url = typeof cmd.params.url === "string" ? cmd.params.url : t.url;
|
|
1168
1390
|
if (!url) throw new CmdError("BAD_ARGS", "no url to read cookies for");
|
|
@@ -1181,7 +1403,7 @@
|
|
|
1181
1403
|
}
|
|
1182
1404
|
// -- localStorage / sessionStorage (isolated world) --
|
|
1183
1405
|
case "storage": {
|
|
1184
|
-
const id = await
|
|
1406
|
+
const id = await targetTab2(cmd);
|
|
1185
1407
|
const op = String(cmd.params.op);
|
|
1186
1408
|
const key = typeof cmd.params.key === "string" ? cmd.params.key : null;
|
|
1187
1409
|
const value = typeof cmd.params.value === "string" ? cmd.params.value : null;
|
|
@@ -1193,49 +1415,36 @@
|
|
|
1193
1415
|
}
|
|
1194
1416
|
// -- interaction (synthetic events in the isolated world) --
|
|
1195
1417
|
case "click": {
|
|
1196
|
-
const id = await
|
|
1418
|
+
const id = await targetTab2(cmd);
|
|
1197
1419
|
const sel = requireSelector(cmd);
|
|
1198
1420
|
const frameIds = await this.frames(cmd, id);
|
|
1199
|
-
if (!await waitForSelector(id, sel, frameIds)) {
|
|
1200
|
-
throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1201
|
-
}
|
|
1202
1421
|
if (cmd.params.trusted === true) {
|
|
1203
1422
|
const verdict = await trustedClick(id, sel, frameIds);
|
|
1204
1423
|
if (verdict === "missing") throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1205
1424
|
if (verdict === "ok") return { ok: true };
|
|
1206
1425
|
}
|
|
1207
|
-
const out = await execOp(id, { op: "click", selector: sel }, frameIds);
|
|
1426
|
+
const out = await execOp(id, withWait({ op: "click", selector: sel }), frameIds);
|
|
1208
1427
|
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1209
1428
|
return { ok: true, frameId: out.frameId, ...cmd.params.trusted === true ? { trusted: false } : {} };
|
|
1210
1429
|
}
|
|
1211
1430
|
case "type": {
|
|
1212
|
-
const id = await
|
|
1431
|
+
const id = await targetTab2(cmd);
|
|
1213
1432
|
const sel = requireSelector(cmd);
|
|
1214
1433
|
const frameIds = await this.frames(cmd, id);
|
|
1215
1434
|
const text = String(cmd.params.text ?? "");
|
|
1216
1435
|
const clear = cmd.params.clear === true;
|
|
1217
|
-
if (!await waitForSelector(id, sel, frameIds)) {
|
|
1218
|
-
throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1219
|
-
}
|
|
1220
1436
|
if (cmd.params.trusted === true) {
|
|
1221
|
-
if (!await trustedType(id, sel, text, clear, frameIds)) {
|
|
1437
|
+
if (!await trustedType(id, sel, text, clear, frameIds, cmd.params.pressEnter === true)) {
|
|
1222
1438
|
throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1223
1439
|
}
|
|
1224
|
-
if (cmd.params.pressEnter === true) {
|
|
1225
|
-
await withDebugger(id, async (t) => {
|
|
1226
|
-
for (const type of ["keyDown", "keyUp"]) {
|
|
1227
|
-
await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
|
|
1228
|
-
}
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
1440
|
return { ok: true };
|
|
1232
1441
|
}
|
|
1233
|
-
const out = await execOp(id, { op: "type", selector: sel, text, clear }, frameIds);
|
|
1442
|
+
const out = await execOp(id, withWait({ op: "type", selector: sel, text, clear }), frameIds);
|
|
1234
1443
|
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1235
1444
|
return { ok: true, frameId: out.frameId };
|
|
1236
1445
|
}
|
|
1237
1446
|
case "press": {
|
|
1238
|
-
const id = await
|
|
1447
|
+
const id = await targetTab2(cmd);
|
|
1239
1448
|
const key = String(cmd.params.key ?? "");
|
|
1240
1449
|
await execInTab(
|
|
1241
1450
|
id,
|
|
@@ -1250,16 +1459,15 @@
|
|
|
1250
1459
|
return { ok: true };
|
|
1251
1460
|
}
|
|
1252
1461
|
case "hover": {
|
|
1253
|
-
const id = await
|
|
1462
|
+
const id = await targetTab2(cmd);
|
|
1254
1463
|
const sel = requireSelector(cmd);
|
|
1255
1464
|
const frameIds = await this.frames(cmd, id);
|
|
1256
|
-
await
|
|
1257
|
-
const out = await execOp(id, { op: "hover", selector: sel }, frameIds);
|
|
1465
|
+
const out = await execOp(id, withWait({ op: "hover", selector: sel }), frameIds);
|
|
1258
1466
|
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1259
1467
|
return { ok: true, frameId: out.frameId };
|
|
1260
1468
|
}
|
|
1261
1469
|
case "scroll": {
|
|
1262
|
-
const id = await
|
|
1470
|
+
const id = await targetTab2(cmd);
|
|
1263
1471
|
const frameIds = await this.frames(cmd, id);
|
|
1264
1472
|
await execOp(
|
|
1265
1473
|
id,
|
|
@@ -1281,20 +1489,21 @@
|
|
|
1281
1489
|
// concurrent batches) and supports true full-page + element capture.
|
|
1282
1490
|
// Falls back to captureVisibleTab only if the debugger can't attach.
|
|
1283
1491
|
case "screenshot": {
|
|
1284
|
-
const id = await
|
|
1492
|
+
const id = await targetTab2(cmd);
|
|
1285
1493
|
const fullPage = cmd.params.fullPage === true;
|
|
1286
1494
|
const selector = selectorOf(cmd);
|
|
1495
|
+
const enc = encodingOf(cmd);
|
|
1287
1496
|
const frameIds = await this.frames(cmd, id);
|
|
1288
1497
|
try {
|
|
1289
|
-
return await screenshotViaDebugger(id, fullPage, selector, frameIds);
|
|
1498
|
+
return await screenshotViaDebugger(id, fullPage, enc, selector, frameIds);
|
|
1290
1499
|
} catch (err) {
|
|
1291
1500
|
if (err instanceof CmdError && err.code === "SELECTOR_NOT_FOUND") throw err;
|
|
1292
|
-
return await screenshotViaVisibleTab(id, fullPage);
|
|
1501
|
+
return await screenshotViaVisibleTab(id, fullPage, enc);
|
|
1293
1502
|
}
|
|
1294
1503
|
}
|
|
1295
1504
|
// -- eval (MAIN world; may be blocked by strict page CSP) --
|
|
1296
1505
|
case "eval": {
|
|
1297
|
-
const id = await
|
|
1506
|
+
const id = await targetTab2(cmd);
|
|
1298
1507
|
const expr = String(cmd.params.expression ?? "");
|
|
1299
1508
|
const result = await execInTab(
|
|
1300
1509
|
id,
|
|
@@ -1316,7 +1525,7 @@
|
|
|
1316
1525
|
// One injection that polls IN-PAGE until the condition holds or the deadline
|
|
1317
1526
|
// passes, rather than one executeScript round-trip per tick.
|
|
1318
1527
|
case "wait_for": {
|
|
1319
|
-
const id = await
|
|
1528
|
+
const id = await targetTab2(cmd);
|
|
1320
1529
|
const frameIds = await this.frames(cmd, id);
|
|
1321
1530
|
const timeout = typeof cmd.params.timeoutMs === "number" ? cmd.params.timeoutMs : 3e4;
|
|
1322
1531
|
const start = Date.now();
|
|
@@ -1357,7 +1566,7 @@
|
|
|
1357
1566
|
}
|
|
1358
1567
|
// -- upload: set local file(s) on a file <input> via CDP DOM.setFileInputFiles --
|
|
1359
1568
|
case "upload_file": {
|
|
1360
|
-
const id = await
|
|
1569
|
+
const id = await targetTab2(cmd);
|
|
1361
1570
|
const sel = requireSelector(cmd);
|
|
1362
1571
|
const files = Array.isArray(cmd.params.files) ? cmd.params.files.map(String) : [];
|
|
1363
1572
|
if (files.length === 0) throw new CmdError("BAD_ARGS", 'upload_file requires a non-empty "files" array');
|
|
@@ -1378,7 +1587,7 @@
|
|
|
1378
1587
|
// Read via a probe injection rather than chrome.webNavigation so no
|
|
1379
1588
|
// extra host-level permission is needed to see them. --
|
|
1380
1589
|
case "frames_list": {
|
|
1381
|
-
const id = await
|
|
1590
|
+
const id = await targetTab2(cmd);
|
|
1382
1591
|
const probes = await chrome.scripting.executeScript({
|
|
1383
1592
|
target: { tabId: id, allFrames: true },
|
|
1384
1593
|
func: pageOp,
|
|
@@ -1400,7 +1609,7 @@
|
|
|
1400
1609
|
// that loaded before registration gets it injected here, and the caller
|
|
1401
1610
|
// is told capture only starts from that moment. --
|
|
1402
1611
|
case "observers": {
|
|
1403
|
-
const id = await
|
|
1612
|
+
const id = await targetTab2(cmd);
|
|
1404
1613
|
const frameIds = await this.frames(cmd, id);
|
|
1405
1614
|
const opts = {
|
|
1406
1615
|
console: cmd.params.console === true,
|
|
@@ -1462,7 +1671,7 @@
|
|
|
1462
1671
|
// -- print_pdf: Page.printToPDF over the same one-op debugger attach the
|
|
1463
1672
|
// screenshot path uses. Returns base64; the server writes the file. --
|
|
1464
1673
|
case "print_pdf": {
|
|
1465
|
-
const id = await
|
|
1674
|
+
const id = await targetTab2(cmd);
|
|
1466
1675
|
const params = {
|
|
1467
1676
|
printBackground: cmd.params.printBackground !== false,
|
|
1468
1677
|
landscape: cmd.params.landscape === true,
|
|
@@ -1510,14 +1719,15 @@
|
|
|
1510
1719
|
"no policy has arrived from the chrome-mcp server yet, so this extension is refusing every command"
|
|
1511
1720
|
);
|
|
1512
1721
|
}
|
|
1722
|
+
const tab = await resolveTab(cmd);
|
|
1513
1723
|
if (policy) {
|
|
1514
|
-
const url = isUrlGated(cmd.method) ? await urlForCommand(cmd) : "";
|
|
1724
|
+
const url = isUrlGated(cmd.method) ? await urlForCommand(cmd, tab) : "";
|
|
1515
1725
|
const verdict = evaluatePolicy(url, cmd.method, policy);
|
|
1516
1726
|
if (!verdict.ok) throw new CmdError("POLICY_DENIED", verdict.reason);
|
|
1517
1727
|
}
|
|
1518
|
-
const data = await this.deps.exec.run(cmd);
|
|
1728
|
+
const data = await this.deps.exec.run(cmd, tab);
|
|
1519
1729
|
const frame = { type: "result", v: PROTOCOL_VERSION, id: cmd.id, ok: true, data };
|
|
1520
|
-
const tabUrl = await observedTabUrl(cmd);
|
|
1730
|
+
const tabUrl = await observedTabUrl(cmd, tab);
|
|
1521
1731
|
if (tabUrl) frame.tabUrl = tabUrl;
|
|
1522
1732
|
this.deps.send(frame);
|
|
1523
1733
|
} catch (err) {
|
|
@@ -1570,10 +1780,29 @@
|
|
|
1570
1780
|
var KEEPALIVE_ALARM = "chrome-mcp-keepalive";
|
|
1571
1781
|
var currentPolicy = null;
|
|
1572
1782
|
var executor = new ChromeExecutor(() => currentPolicy);
|
|
1783
|
+
var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 1e4];
|
|
1784
|
+
var reconnectAttempt = 0;
|
|
1785
|
+
var reconnectTimer = null;
|
|
1786
|
+
function scheduleReconnect() {
|
|
1787
|
+
if (reconnectTimer) return;
|
|
1788
|
+
const delay2 = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
1789
|
+
reconnectAttempt++;
|
|
1790
|
+
reconnectTimer = setTimeout(() => {
|
|
1791
|
+
reconnectTimer = null;
|
|
1792
|
+
void ensureConnected();
|
|
1793
|
+
}, delay2);
|
|
1794
|
+
}
|
|
1795
|
+
function clearReconnect() {
|
|
1796
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1797
|
+
reconnectTimer = null;
|
|
1798
|
+
reconnectAttempt = 0;
|
|
1799
|
+
}
|
|
1573
1800
|
var ws = new WsClient({
|
|
1574
1801
|
onCommand: (cmd) => void router.dispatch(cmd),
|
|
1575
1802
|
onState: (state) => {
|
|
1576
1803
|
void persistState(state);
|
|
1804
|
+
if (state === "connected") clearReconnect();
|
|
1805
|
+
else if (state === "idle") scheduleReconnect();
|
|
1577
1806
|
if (state === "unauthorized") void adoptBundledPairing();
|
|
1578
1807
|
},
|
|
1579
1808
|
onPolicy: (policy) => {
|
|
@@ -1672,11 +1901,13 @@
|
|
|
1672
1901
|
if (area === "local" && (changes.wsPort || changes.token || changes.profile)) {
|
|
1673
1902
|
if (ws.state === "unauthorized") ws.state = "idle";
|
|
1674
1903
|
if (changes.profile) ws.close();
|
|
1904
|
+
clearReconnect();
|
|
1675
1905
|
void ensureConnected();
|
|
1676
1906
|
}
|
|
1677
1907
|
});
|
|
1678
1908
|
chrome.runtime.onMessage.addListener((msg) => {
|
|
1679
1909
|
if (msg?.type === "reconnect") {
|
|
1910
|
+
clearReconnect();
|
|
1680
1911
|
ws.close();
|
|
1681
1912
|
ws.state = "idle";
|
|
1682
1913
|
void ensureConnected();
|