@agent360/browser-mcp 1.29.0 → 1.29.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/README.md +161 -46
- package/bin/cli.js +99 -21
- package/extension/background.js +1210 -192
- package/extension/manifest.json +1 -1
- package/extension/offscreen.js +62 -5
- package/extension/popup.html +9 -4
- package/extension/popup.js +5 -5
- package/index.js +251 -118
- package/package.json +3 -3
- package/tools.js +16 -16
- package/vagt.js +3 -3
package/extension/background.js
CHANGED
|
@@ -79,7 +79,15 @@ function getSession(port, pid) {
|
|
|
79
79
|
// samtidige sessioner heller ikke kan faa samme farve.
|
|
80
80
|
const brugte = new Set([...sessions.values()].map((s) => s.nummer).filter((n) => typeof n === 'number'));
|
|
81
81
|
let nummer = 1;
|
|
82
|
-
|
|
82
|
+
// Havde denne chat en plads foer den slap sin port, og er den stadig ledig, saa faar
|
|
83
|
+
// den sin egen tilbage — se pladsPrPid. Ellers laveste ledige, som foer.
|
|
84
|
+
const husket = typeof pid === 'number' ? pladsPrPid.get(pid) : undefined;
|
|
85
|
+
if (typeof husket === 'number' && !brugte.has(husket)) {
|
|
86
|
+
nummer = husket;
|
|
87
|
+
} else {
|
|
88
|
+
while (brugte.has(nummer)) nummer++;
|
|
89
|
+
}
|
|
90
|
+
husketPlads(pid, nummer);
|
|
83
91
|
|
|
84
92
|
sessions.set(port, {
|
|
85
93
|
tabIds: new Set(),
|
|
@@ -93,7 +101,7 @@ function getSession(port, pid) {
|
|
|
93
101
|
}
|
|
94
102
|
const s = sessions.get(port);
|
|
95
103
|
// Foerste kald fra en genstartet server kan baere pid'en foer sessionen har den.
|
|
96
|
-
if (s.pid == null && typeof pid === 'number') s.pid = pid;
|
|
104
|
+
if (s.pid == null && typeof pid === 'number') { s.pid = pid; husketPlads(pid, s.nummer); }
|
|
97
105
|
return s;
|
|
98
106
|
}
|
|
99
107
|
|
|
@@ -260,6 +268,9 @@ async function releaseSession(port) {
|
|
|
260
268
|
//
|
|
261
269
|
// En UVENTET afbrydelse (chatten er vaek) skal stadig lukke fanerne — derfor
|
|
262
270
|
// skelnes der, i stedet for bare at tjekke om sessionen er tom.
|
|
271
|
+
// #12: sessionen forsvinder her — dens frist skal med, uanset hvilken vej vi gaar ud.
|
|
272
|
+
chrome.alarms.clear(`frigiv-${port}`).catch(() => {});
|
|
273
|
+
|
|
263
274
|
if (frivilligtFrigivet.delete(port)) {
|
|
264
275
|
if (session.tabIds.size) {
|
|
265
276
|
// Sessionen arbejder igen. Behold den, saa naeste binding kan adoptere den
|
|
@@ -273,12 +284,35 @@ async function releaseSession(port) {
|
|
|
273
284
|
}
|
|
274
285
|
|
|
275
286
|
// Detach debugger + close all session tabs
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
//
|
|
288
|
+
// #13: fejlene blev slugt her, og `sessions.delete(port)` koerte alligevel. En fane der
|
|
289
|
+
// ikke KUNNE lukkes blev dermed foraeldreloes: stadig aaben, stadig i en farvet gruppe,
|
|
290
|
+
// men uden for enhver session — usynlig for list_tabs og aldrig ryddet op. Praeeksisterende,
|
|
291
|
+
// men v1.29 slipper porte langt oftere, saa stien koeres langt hyppigere end foer.
|
|
292
|
+
const stadigAabne = new Set();
|
|
293
|
+
for (const tabId of [...session.tabIds]) {
|
|
278
294
|
debuggerForceDetach(tabId);
|
|
279
295
|
try {
|
|
280
296
|
await chrome.tabs.remove(tabId);
|
|
281
|
-
|
|
297
|
+
session.tabIds.delete(tabId);
|
|
298
|
+
} catch {
|
|
299
|
+
// Fanen kan vaere lukket i forvejen — saa er den vaek, og det er fint. Findes den
|
|
300
|
+
// stadig, beholder vi den: bedre en session der lever lidt for laenge end en fane
|
|
301
|
+
// ingen ejer.
|
|
302
|
+
try {
|
|
303
|
+
await chrome.tabs.get(tabId);
|
|
304
|
+
stadigAabne.add(tabId);
|
|
305
|
+
} catch {
|
|
306
|
+
session.tabIds.delete(tabId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (stadigAabne.size) {
|
|
312
|
+
console.warn('[BG] kunne ikke lukke', [...stadigAabne], '— sessionen beholdes saa fanerne ikke strander');
|
|
313
|
+
session.tabIds = stadigAabne;
|
|
314
|
+
persistSessions();
|
|
315
|
+
return;
|
|
282
316
|
}
|
|
283
317
|
|
|
284
318
|
sessions.delete(port);
|
|
@@ -476,13 +510,26 @@ chrome.debugger.onDetach.addListener((source, reason) => {
|
|
|
476
510
|
// Side-effectful methods (Input.*, DOM.setFileInputFiles) must NEVER auto-retry:
|
|
477
511
|
// Chrome may detach AFTER processing the input (e.g., keystroke triggered navigation),
|
|
478
512
|
// and a blind retry would double-type or double-click.
|
|
513
|
+
// MAALT 9/9-2026 (fundet af Astra, reproduceret her): `Runtime.evaluate` stod paa listen,
|
|
514
|
+
// fordi de fleste kald er laesninger. Men vi kan ikke afgoere paa metodenavnet om et
|
|
515
|
+
// udtryk MUTERER — og flere af dem goer:
|
|
516
|
+
// * settle-udtrykket i debuggerClick FYRER reserveloesnings-klikket
|
|
517
|
+
// * scroll'ens reserveloesning kalder window.scrollBy
|
|
518
|
+
// * fill skriver i feltet gennem evalAttached
|
|
519
|
+
// Reproduktion: cdpSend(1,'Runtime.evaluate',{expression:'window.tael++'}) med en
|
|
520
|
+
// detach-fejl koerte udtrykket FIRE gange. Paa en SPA hvor debuggeren falder af, kunne
|
|
521
|
+
// det altsaa lande fire klik — paa en knap der maaske bestiller noget.
|
|
522
|
+
//
|
|
523
|
+
// Vi kan ikke skelne, saa standarden skal vaere sikker. Falder debuggeren af midt i en
|
|
524
|
+
// evaluering, faar kalderen fejlen og kan selv beslutte om det er forsvarligt at gentage.
|
|
525
|
+
// Prisen er en tabt gentagelse paa anti-automatiserings-sider; alternativet er et
|
|
526
|
+
// dobbeltklik, og de to ting er ikke lige slemme.
|
|
479
527
|
const RETRYABLE_CDP_METHODS = new Set([
|
|
480
528
|
'DOM.getDocument',
|
|
481
529
|
'DOM.querySelector',
|
|
482
530
|
'DOM.querySelectorAll',
|
|
483
531
|
'DOM.focus',
|
|
484
532
|
'DOM.describeNode',
|
|
485
|
-
'Runtime.evaluate',
|
|
486
533
|
'Runtime.enable',
|
|
487
534
|
'Page.captureScreenshot',
|
|
488
535
|
'Page.enable',
|
|
@@ -494,6 +541,86 @@ const RETRYABLE_CDP_METHODS = new Set([
|
|
|
494
541
|
// CDP wrapper with auto-recovery: re-attaches on detach errors.
|
|
495
542
|
// For read-only methods (whitelist above), retries once after re-attach.
|
|
496
543
|
// For side-effectful methods, only re-attaches and throws — caller must decide.
|
|
544
|
+
// MAALT 8/9-2026: `Input.dispatchMouseEvent` med mouseWheel indfrier ALDRIG sit loefte.
|
|
545
|
+
// `browser_scroll` med pixels ramte derfor serverens 30-sekunders-loft 6 kald ud af 6,
|
|
546
|
+
// paa baade en kort og en lang side — og den `window.scrollBy` der er skrevet til netop
|
|
547
|
+
// det tilfaelde, ligger i et `catch` og kunne aldrig naas. En haenger er ikke en exception.
|
|
548
|
+
// Fristen findes for at reserveloesningen kan naas. Alt der HAR en reserveloesning skal
|
|
549
|
+
// kunne naas via en frist, ikke kun via en fejl.
|
|
550
|
+
// Foerste udgave satte 1.500 ms paa ALT. Astra fandt 9/9 at det indfoerte en ny fejlklasse:
|
|
551
|
+
// dialog-ventetider bruger 3.000 ms, og et Page.captureScreenshot paa en tung side kan
|
|
552
|
+
// lovligt tage laengere. Fristen skar dem over, og skaermbilledets reserveloesning blev
|
|
553
|
+
// dermed naaet i almindelig drift — se rettelse B nedenfor.
|
|
554
|
+
//
|
|
555
|
+
// Den hang der blev MAALT var Input.dispatchMouseEvent med mouseWheel, som aldrig indfrier
|
|
556
|
+
// sit loefte. Laesekald har i forvejen deres egne ydre frister (evaluerTaalmodigt).
|
|
557
|
+
// Derfor: kort frist paa input-kald, og en rundhaandet bagstopper paa resten — stadig under
|
|
558
|
+
// serverens 30 s, saa kalderens reserveloesning kan naas.
|
|
559
|
+
const CDP_FRIST_INPUT_MS = 1500;
|
|
560
|
+
const CDP_FRIST_MS = 8000;
|
|
561
|
+
// MAALT 9/9 af reviewet: ét loft paa alt braekker tre ting. Et skaermbillede paa en tung
|
|
562
|
+
// side bruger lovligt mere end 8 s; `execute_script` med awaitPromise venter paa BRUGERENS
|
|
563
|
+
// egen kode, som serveren giver 30 s; og et enkelt tastetryk paa en side med validering
|
|
564
|
+
// pr. anslag kan lovligt overskride input-fristen. Fristen skal derfor kende kaldet.
|
|
565
|
+
const CDP_FRIST_TUNG_MS = 20000; // under serverens 30 s, men over alt lovligt
|
|
566
|
+
|
|
567
|
+
function cdpFrist(method, params) {
|
|
568
|
+
const m = String(method);
|
|
569
|
+
// Brugerens egen kode maa vente: awaitPromise betyder "vent paa dette loefte".
|
|
570
|
+
if (m === 'Runtime.evaluate' && params && params.awaitPromise) return CDP_FRIST_TUNG_MS;
|
|
571
|
+
if (m === 'Page.captureScreenshot' || m === 'Network.getResponseBody') return CDP_FRIST_TUNG_MS;
|
|
572
|
+
return m.startsWith('Input.') ? CDP_FRIST_INPUT_MS : CDP_FRIST_MS;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Skaermbilledet har et budget for HELE kaeden, ikke kun pr. kald.
|
|
576
|
+
// MAALT 11/9 af Astra (sign-off, R5 F8): standardoptagelsen hang og koblede foerst fra efter 19 s. cdpSend gentog den,
|
|
577
|
+
// fordi den staar som sikker at gentage, og billedet kom efter ca. 38 s - serveren havde opgivet ved 30 s. I 1.29.0
|
|
578
|
+
// sendte en frist paa 8 s kaldet videre til fromSurface:false, som svarede paa et halvt sekund.
|
|
579
|
+
// Standardoptagelsen faar derfor 10 s. Reserven faar resten af budgettet, saa svaret naar frem foer serverens frist.
|
|
580
|
+
// MAALT 11/9 af Fable (e2e-review): haenger BEGGE optagelser, haevede 1.29.0 vinduet og leverede et billede efter 16,7 s.
|
|
581
|
+
// En frist afskar den sidste udvej her, saa der kom intet billede. Budgettet reserverer derfor haevMs til den haevede runde.
|
|
582
|
+
//
|
|
583
|
+
// MAALT 12/9 af Astra (tredje runde paa samme sted): en gentilslutning paa 12,5 s betoed at kandidaten opgav efter 26,9 s,
|
|
584
|
+
// hvor 1.29.0 leverede et billede efter 29,2 s. Aarsagen var ikke logikken, men TALLET: 26 s var valgt frit, ikke udledt.
|
|
585
|
+
// Hver runde fandt et nyt scenarie i de 4 sekunder vi gav bort. Budgettet er derfor nu udledt af det eneste tal der
|
|
586
|
+
// betyder noget - serverens egen frist pr. kald (sendToExtension i mcp-server/index.js) - minus den tid svaret skal
|
|
587
|
+
// bruge paa at komme tilbage gennem broen. Aendrer serverens frist sig, foelger budgettet med.
|
|
588
|
+
const SERVER_FRIST_MS = 30000; // sendToExtension(..., timeoutMs = 30000) i mcp-server/index.js
|
|
589
|
+
// MAALT 12/9 af Astra: hjemrejsen for en skaermbillede-stor nyttelast over den lokale WebSocket er 84-154 ms
|
|
590
|
+
// (0,5 / 2 / 8 MB, tre maalinger hver). 1500 ms var 10x det - altsaa endnu et frit valgt tal i den regel der lige var
|
|
591
|
+
// lukket. 500 ms er tre gange det maalte og krymper baandet, hvor 1.29.0 leverer og vi ikke goer, fra ~1,35 s til ~0,35 s.
|
|
592
|
+
const SVARETS_HJEMREJSE_MS = 500; // udvidelse -> offscreen -> WebSocket -> server
|
|
593
|
+
function skaermbilledeFrister() {
|
|
594
|
+
return { foersteMs: 10000, samletMs: SERVER_FRIST_MS - SVARETS_HJEMREJSE_MS, haevMs: 8000 };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// wait_for_network har samme loft hos serveren (30 s). MAALT 11/9 af Astra (e2e-review): svaret kom efter 13 s, body-kaldet
|
|
598
|
+
// fik CDP_FRIST_TUNG_MS (20 s) oveni, og serveren opgav ved 30 s, hvor 1.29.0 svarede efter 21 s med body:null.
|
|
599
|
+
// Hele vaerktoejet har derfor ét budget, og body-kaldet faar kun det der er tilbage.
|
|
600
|
+
// Astra (efterproevning af c826f63): budgettet skar en body over der kom efter 27 s + 2 s, som 1.29.0 leverede efter 29 s.
|
|
601
|
+
// Body-kaldet faar derfor aldrig kortere tid end 1.29.0's frist (CDP_FRIST_MS) og aldrig mere end CDP_FRIST_TUNG_MS.
|
|
602
|
+
// MAALT 12/9 af Astra: her stod 28000 haardkodet - det SAMME frie tal som skaermbilledets budget lige var sluppet af med.
|
|
603
|
+
// Begge budgetter udledes nu af serverens egen frist, saa reglen gaelder hele vejen og ikke kun dér hvor den blev fundet.
|
|
604
|
+
function netvaerkFrister() {
|
|
605
|
+
return { budgetMs: SERVER_FRIST_MS - SVARETS_HJEMREJSE_MS, bodyMinMs: CDP_FRIST_MS, bodyMaxMs: CDP_FRIST_TUNG_MS };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function cdpMedFrist(tabId, method, params) {
|
|
609
|
+
let ur;
|
|
610
|
+
const frist = cdpFrist(method, params);
|
|
611
|
+
return Promise.race([
|
|
612
|
+
chrome.debugger.sendCommand({ tabId }, method, params),
|
|
613
|
+
new Promise((_, afvis) => {
|
|
614
|
+
// MAALT 11/9 (raa CDP-proeve i Chrome for Testing): Chrome leverer ikke Input.* til en fane i baggrunden.
|
|
615
|
+
// Kaldet haenger, og efter aktivering svarer det paa millisekunder. Fejlen siger det, saa agenten ved hvad den goer.
|
|
616
|
+
const hint = String(method).startsWith('Input.')
|
|
617
|
+
? ' (fanen er sandsynligvis i baggrunden - Chrome leverer ikke mus og taster til en fane der ikke er aktiv; kald browser_switch_tab og proev igen)'
|
|
618
|
+
: '';
|
|
619
|
+
ur = setTimeout(() => afvis(new Error(`CDP svarede ikke inden ${frist} ms: ${method}${hint}`)), frist);
|
|
620
|
+
}),
|
|
621
|
+
]).finally(() => clearTimeout(ur));
|
|
622
|
+
}
|
|
623
|
+
|
|
497
624
|
async function cdpSend(tabId, method, params = {}) {
|
|
498
625
|
await debuggerAttach(tabId);
|
|
499
626
|
let lastMsg = '';
|
|
@@ -502,7 +629,7 @@ async function cdpSend(tabId, method, params = {}) {
|
|
|
502
629
|
// where Chrome re-detaches between attach and command execution.
|
|
503
630
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
504
631
|
try {
|
|
505
|
-
return await
|
|
632
|
+
return await cdpMedFrist(tabId, method, params);
|
|
506
633
|
} catch (e) {
|
|
507
634
|
const msg = e?.message || String(e);
|
|
508
635
|
const isDetachError =
|
|
@@ -539,6 +666,23 @@ const agentLukkedeFaner = new Set();
|
|
|
539
666
|
// Skelnen betyder alt i releaseSession: en frivillig frigivelse maa aldrig lukke
|
|
540
667
|
// faner, mens en uventet afbrydelse (chatten er vaek) netop skal rydde op.
|
|
541
668
|
const frivilligtFrigivet = new Set();
|
|
669
|
+
// Hvilken plads en chat sidst havde, husket paa dens pid — ikke paa porten.
|
|
670
|
+
//
|
|
671
|
+
// MAALT 8/9 (#17): siden porten slippes naar en session er faerdig, slettes sessionen.
|
|
672
|
+
// Kommer chatten tilbage, faar den det laveste LEDIGE nummer — saa en chat der var
|
|
673
|
+
// "Claude 3" vender tilbage som "Claude 1" i en anden farve, og brugeren kan ikke
|
|
674
|
+
// genkende sin egen fanegruppe. Identiteten hoerer til chatten, ikke til porten.
|
|
675
|
+
//
|
|
676
|
+
// Garantien er praecis "dit gamle nummer HVIS det er ledigt". Er det taget, vinder den
|
|
677
|
+
// nulevende session — ellers ville vi genindfoere den navnekollision som lavest-ledige-
|
|
678
|
+
// nummer blev indfoert for at loese.
|
|
679
|
+
const pladsPrPid = new Map();
|
|
680
|
+
function husketPlads(pid, nummer) {
|
|
681
|
+
if (typeof pid !== 'number') return;
|
|
682
|
+
pladsPrPid.set(pid, nummer);
|
|
683
|
+
// Kortet maa ikke vokse i det uendelige paa en langtlevende worker.
|
|
684
|
+
if (pladsPrPid.size > 60) pladsPrPid.delete(pladsPrPid.keys().next().value);
|
|
685
|
+
}
|
|
542
686
|
|
|
543
687
|
// ── Armerede dialog-haandterere, pr. fane ──────────────────────────────────────
|
|
544
688
|
// MAALT 22/8 af flowtesten: handle_dialog var ubrugelig som den var skrevet. Den
|
|
@@ -571,10 +715,41 @@ function afvaebnDialog(tabId, grund) {
|
|
|
571
715
|
if (grund && a.opfyld) a.opfyld({ ok: false, error: grund });
|
|
572
716
|
}
|
|
573
717
|
|
|
718
|
+
// Fylder sessions-kortet fra lageret UDEN at filtrere paa om fanerne stadig findes.
|
|
719
|
+
//
|
|
720
|
+
// `restoreSessions()` dropper sessioner uden GYLDIGE faner (`validTabIds.size > 0`), og i
|
|
721
|
+
// `tabs.onRemoved` er den fane vi lige har mistet netop den der ville goere sessionen
|
|
722
|
+
// ugyldig. Bruger man restoreSessions dér, forsvinder praecis den session man skal handle
|
|
723
|
+
// paa. Samme faelde som i alarm-lytteren, og derfor samme svar: laes lageret raat.
|
|
724
|
+
async function hydrerSessionerRaat() {
|
|
725
|
+
if (sessions.size) return;
|
|
726
|
+
const { sessions: gemte } = await chrome.storage.local.get({ sessions: {} });
|
|
727
|
+
for (const [port, d] of Object.entries(gemte)) {
|
|
728
|
+
const p = Number(port);
|
|
729
|
+
if (sessions.has(p)) continue;
|
|
730
|
+
sessions.set(p, {
|
|
731
|
+
tabIds: new Set(d.tabIds || []),
|
|
732
|
+
activeTabId: d.activeTabId ?? null,
|
|
733
|
+
groupId: d.groupId ?? null,
|
|
734
|
+
color: d.color,
|
|
735
|
+
label: d.label,
|
|
736
|
+
nummer: d.nummer ?? null,
|
|
737
|
+
pid: d.pid ?? null,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
574
742
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
575
743
|
afvaebnDialog(tabId, 'fanen blev lukket foer der kom en dialog');
|
|
576
744
|
const lukketAfAgenten = agentLukkedeFaner.delete(tabId);
|
|
577
745
|
debuggerAttached.delete(tabId);
|
|
746
|
+
// MAALT 8/9 (#15): loekken herunder loeb SYNKRONT paa `sessions`. Vaekker eventet en
|
|
747
|
+
// suspenderet service-worker, er kortet tomt, loekken koerer nul gange, og hverken
|
|
748
|
+
// nedlukningen eller fristen bliver sat — porten holdes saa til 4-timers-tomgangen.
|
|
749
|
+
// Agentens egen close_tab ramte det ikke (den kommer som mcp_command, der vaekker og
|
|
750
|
+
// gendanner foerst). Det var specifikt MENNESKET der lukkede den sidste fane, senere.
|
|
751
|
+
(async () => {
|
|
752
|
+
await hydrerSessionerRaat();
|
|
578
753
|
for (const [port, session] of sessions) {
|
|
579
754
|
if (!session.tabIds.has(tabId)) continue;
|
|
580
755
|
session.tabIds.delete(tabId);
|
|
@@ -604,6 +779,7 @@ chrome.tabs.onRemoved.addListener((tabId) => {
|
|
|
604
779
|
persistSessions();
|
|
605
780
|
}
|
|
606
781
|
}
|
|
782
|
+
})().catch(() => {});
|
|
607
783
|
});
|
|
608
784
|
|
|
609
785
|
// Physical-key `code` for a character, US layout. We used to build this as
|
|
@@ -638,18 +814,9 @@ async function typeCharsAttached(tabId, text) {
|
|
|
638
814
|
for (let i = 0; i < text.length; i++) {
|
|
639
815
|
const char = text[i];
|
|
640
816
|
const code = cdpCodeForChar(char);
|
|
641
|
-
await
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
key: char,
|
|
645
|
-
...(code ? { code } : {}),
|
|
646
|
-
unmodifiedText: char,
|
|
647
|
-
});
|
|
648
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
649
|
-
type: 'keyUp',
|
|
650
|
-
key: char,
|
|
651
|
-
...(code ? { code } : {}),
|
|
652
|
-
});
|
|
817
|
+
await tastParAttached(tabId,
|
|
818
|
+
{ text: char, key: char, ...(code ? { code } : {}), unmodifiedText: char },
|
|
819
|
+
{ key: char, ...(code ? { code } : {}) });
|
|
653
820
|
// Human-like typing: random 30-120ms, occasional longer pause
|
|
654
821
|
const pause = (i > 0 && i % (7 + Math.floor(Math.random() * 5)) === 0)
|
|
655
822
|
? 150 + Math.random() * 200 // thinking pause every ~10 chars
|
|
@@ -706,15 +873,59 @@ async function evaluerTaalmodigt(tabId, params, ms = 3000) {
|
|
|
706
873
|
let faerdig = false;
|
|
707
874
|
const kald = cdpSend(tabId, 'Runtime.evaluate', params)
|
|
708
875
|
.then((r) => { faerdig = true; return r; })
|
|
709
|
-
.catch(() => { faerdig = true; });
|
|
876
|
+
.catch((e) => { faerdig = true; return { __fejl: String(e?.message || e) }; });
|
|
710
877
|
await Promise.race([kald, new Promise((r) => setTimeout(r, ms))]);
|
|
711
878
|
if (faerdig) return kald;
|
|
712
|
-
return { result: { value: { landed: true, fallbackFired: false, rendererSvarede: false } } };
|
|
879
|
+
return { result: { value: { landed: true, fallbackFired: false, rendererSvarede: false, observeret: false } } };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Én regel for om et klik landede - brugt af click, click_xy og select_option. Astra, tredje runde:
|
|
883
|
+
// select_option havde stadig den gamle ("ikke falsk" = landet), saa et uvist klik blev til ok:true.
|
|
884
|
+
function klikLandede(r) {
|
|
885
|
+
return r?.landed === true || r?.detached === true;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Teksten der foelger med et uvist klik. Staar ét sted, fordi den skal vaere ens for click, click_xy og select_option -
|
|
889
|
+
// tre kaldesteder med den samme regel har foer drevet fra hinanden (select_option havde den gamle i to udgaver).
|
|
890
|
+
const UVIST_NOTE = 'Klikket blev sendt, og siden aendrede sig paa mousedown - men ikke af selve klikket. Det kan vaere en ripple, og det kan vaere en menu der aabner paa mousedown. Tjek tilstanden foer du klikker igen: et klik nummer to lukker en menu der allerede er aaben.';
|
|
891
|
+
const UVERIFICERET_NOTE = 'Museknappen blev sendt, men siden kunne ikke laeses bagefter (opslaget fejlede, og siden navigerede ikke). ' +
|
|
892
|
+
'Klikket kan vaere landet. Tjek tilstanden foer du klikker igen.';
|
|
893
|
+
|
|
894
|
+
// MAALT 12/9 af Astra: der er TO uvisheds-kanaler - `uvist` (kun mousedown aendrede noget) og `uverificeret`
|
|
895
|
+
// (settle-opslaget fejlede uden navigation, men museknappen ER sendt). Rettelsen roerte kun den foerste, saa den anden
|
|
896
|
+
// gav stadig et bart ok:false hvor 1.29.0 gav ok:true - og et bart nej faar agenten til at klikke igen.
|
|
897
|
+
// Betingelsen spoerger derfor paa landed === null, ikke paa ét flag: en fremtidig tredje kanal er daekket fra dag ét.
|
|
898
|
+
function uvisVurdering(r) {
|
|
899
|
+
if (!r || r.landed !== null) return null;
|
|
900
|
+
if (r.uvist) return { maaske_landet: true, note: UVIST_NOTE };
|
|
901
|
+
if (r.uverificeret) return { maaske_landet: true, note: UVERIFICERET_NOTE };
|
|
902
|
+
return { maaske_landet: true, note: 'Handlingen blev sendt, men virkningen kunne ikke bekraeftes. Tjek tilstanden foer du gentager den.' };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// MAALT 10/9 af Astra (anden runde): et settle-opslag der FEJLEDE gav null, og null blev til ok:true.
|
|
906
|
+
// Men den hyppigste grund til at opslaget fejler er at klikket navigerede - det er en virkning, og agenten
|
|
907
|
+
// maa ikke faa at vide at den skal klikke igen.
|
|
908
|
+
// Tredje runde: "detached", "closed" og "Cannot find" er IKKE bevis for navigation - afkobling sker ogsaa
|
|
909
|
+
// naar nogen aabner DevTools. Bevis er nu: fanen er lukket, adressen er skiftet, eller JavaScript-konteksten
|
|
910
|
+
// blev revet ned (det sker kun naar dokumentet blev udskiftet). Alt andet er uvist og siges som uvist.
|
|
911
|
+
async function tolkManglendeSettle(tabId, settle, urlFoer) {
|
|
912
|
+
const fejl = settle?.__fejl || 'settle-opslaget gav intet svar';
|
|
913
|
+
const fane = await chrome.tabs.get(tabId).catch(() => null);
|
|
914
|
+
const urlEfter = fane?.url ?? null;
|
|
915
|
+
const kontekstVaek = /Execution context was destroyed|Cannot find context with specified id|Inspected target navigated/i.test(fejl);
|
|
916
|
+
if (!fane || (urlFoer && urlEfter && urlEfter !== urlFoer) || kontekstVaek) {
|
|
917
|
+
return { landed: false, fallbackFired: false, detached: true, navigerede: true };
|
|
918
|
+
}
|
|
919
|
+
return { landed: null, fallbackFired: false, uverificeret: true, fejl };
|
|
713
920
|
}
|
|
714
921
|
|
|
715
922
|
async function debuggerClick(tabId, x, y) {
|
|
716
923
|
await debuggerAttach(tabId);
|
|
924
|
+
// Er museknappen sendt ned, kan klikket vaere landet - saa maa en fejl bagefter ikke fore til et klik til.
|
|
925
|
+
let trykSendt = false;
|
|
717
926
|
try {
|
|
927
|
+
// Adressen foer klikket - et skift bagefter er bevis for en virkning (se tolkManglendeSettle).
|
|
928
|
+
const urlFoer = (await chrome.tabs.get(tabId).catch(() => null))?.url ?? null;
|
|
718
929
|
// 0. Capture the DEEPEST target element under the point BEFORE dispatching.
|
|
719
930
|
// Web-components (Google Ads <button-panel>, Material Web) keep their real
|
|
720
931
|
// <button> inside an (open) shadow root, so we pierce shadow roots to reach
|
|
@@ -757,52 +968,135 @@ async function debuggerClick(tabId, x, y) {
|
|
|
757
968
|
// 0 on release) plus a small press→release gap are REQUIRED for Chrome to
|
|
758
969
|
// synthesize a *trusted* 'click' from the pair. Without them, web-components
|
|
759
970
|
// that gate on the trusted click event (Google Ads, Material Web) never fire.
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
971
|
+
trykSendt = true;
|
|
972
|
+
// Astra, tredje runde: fejlede mousePressed EFTER at vaere leveret, blev mouseReleased aldrig sendt,
|
|
973
|
+
// og siden stod tilbage med knappen nede (drag-tilstand). Knappen slippes nu altid.
|
|
974
|
+
let trykFejl = null;
|
|
975
|
+
try {
|
|
976
|
+
await dispatchTaalmodigt(tabId, {
|
|
977
|
+
type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 1,
|
|
978
|
+
});
|
|
979
|
+
await new Promise(r => setTimeout(r, 30));
|
|
980
|
+
} catch (e) { trykFejl = e; }
|
|
981
|
+
try {
|
|
982
|
+
await dispatchTaalmodigt(tabId, {
|
|
983
|
+
type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 1,
|
|
984
|
+
});
|
|
985
|
+
} catch (e) { trykFejl = trykFejl || e; }
|
|
986
|
+
if (trykFejl) throw trykFejl;
|
|
767
987
|
// 3. Framework fallback — only if the captured target is STILL connected (i.e.
|
|
768
988
|
// the trusted click in step 2 did not already handle it). Settle delay lets
|
|
769
989
|
// SPA re-renders (Google Ads) detach the element first. Fires a full pointer
|
|
770
990
|
// + mouse sequence on the shadow-pierced target, then React/Angular handlers.
|
|
771
991
|
await new Promise(r => setTimeout(r, 120));
|
|
992
|
+
// MAALT 9/9-2026: oprydningen (removeEventListener + delete) laa FOER reserveloesningen
|
|
993
|
+
// fyrede. Derfor kunne intet observere om det syntetiske klik virkede, og udtrykket
|
|
994
|
+
// svarede landed:false som et GAET. Paa en div-baseret dropdown betoed det
|
|
995
|
+
// {ok:true, landed:false} og en menu der aldrig aabnede — issue #19.
|
|
996
|
+
// Nu staar lytteren stadig paa document mens reserveloesningen fyrer, og laeses bagefter:
|
|
997
|
+
// det er forskellen paa en maaling og en antagelse. Der ryddes op paa hver udgang.
|
|
998
|
+
// NB: ingen backticks i udtrykket herunder — det ER et template literal.
|
|
772
999
|
const settle = await evaluerTaalmodigt(tabId, {
|
|
773
1000
|
returnByValue: true,
|
|
774
1001
|
expression: `(() => {
|
|
775
1002
|
const el = window.__bmcpClickTarget;
|
|
776
1003
|
const landed = window.__bmcpClicked === true;
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1004
|
+
const ryd = () => {
|
|
1005
|
+
try { window.__bmcpClickListener && document.removeEventListener('click', window.__bmcpClickListener, true); } catch (e) {}
|
|
1006
|
+
try { delete window.__bmcpClickTarget; delete window.__bmcpClicked; delete window.__bmcpClickListener; } catch (e) {}
|
|
1007
|
+
};
|
|
1008
|
+
// Et billigt fingeraftryk af det et klik plejer at aendre: antal noder, synlig tekst,
|
|
1009
|
+
// adressen, og om noget er aabnet/valgt. Bevidst groft — det skal kunne tages to gange
|
|
1010
|
+
// paa faa millisekunder, ikke beskrive siden.
|
|
1011
|
+
// MAALT 11/9 af Astra (R5 F6): tekst og feltvaerdier blev talt i LAENGDE, saa AAAA -> BBBB var
|
|
1012
|
+
// usynlig, og et klik der virkede blev meldt som fejl. Nu hashes indholdet (FNV-1a, 32 bit).
|
|
1013
|
+
const hash = (s) => {
|
|
1014
|
+
let x = 2166136261;
|
|
1015
|
+
for (let i = 0; i < s.length; i++) { x ^= s.charCodeAt(i); x = Math.imul(x, 16777619); }
|
|
1016
|
+
return (x >>> 0).toString(36);
|
|
1017
|
+
};
|
|
1018
|
+
const aftryk = () => {
|
|
1019
|
+
try {
|
|
1020
|
+
return document.querySelectorAll('*').length + '|' +
|
|
1021
|
+
hash(document.body ? String(document.body.innerText) : '') + '|' +
|
|
1022
|
+
location.href + '|' +
|
|
1023
|
+
document.querySelectorAll('[aria-expanded="true"],[aria-selected="true"],[open],.open,.active').length + '|' +
|
|
1024
|
+
// Astra, anden runde: en afkrydsning eller en feltvaerdi aendrer hverken noder, tekst eller
|
|
1025
|
+
// adresse - saa et klik der VIRKEDE blev meldt som fejl, og et nyt klik ville fortryde det.
|
|
1026
|
+
document.querySelectorAll('input:checked,option:checked').length + '|' +
|
|
1027
|
+
hash(Array.from(document.querySelectorAll('input,textarea,select')).map((e) => String(e.value || '')).join(''));
|
|
1028
|
+
} catch (e) { return 'aftryk-fejlede'; }
|
|
1029
|
+
};
|
|
1030
|
+
if (landed) { ryd(); return { landed: true, fallbackFired: false }; } // FIX-13: trusted click already landed — do NOT double-fire
|
|
1031
|
+
if (el === null) { ryd(); return { landed: false, fallbackFired: false, intetMaal: true }; } // intet element under punktet (fx uden for vinduet) - ingen virkning
|
|
1032
|
+
if (!el || !el.isConnected) { ryd(); return { landed: false, fallbackFired: false, detached: true }; } // already navigated/handled — don't double-fire
|
|
1033
|
+
const foerAftryk = aftryk();
|
|
781
1034
|
const opts = { bubbles: true, cancelable: true, composed: true, view: window, clientX: ${x}, clientY: ${y} };
|
|
782
|
-
|
|
1035
|
+
// Samme moenster som script-klikket (Astra 11/9): en PointerEvent uden pointerType er ikke en mus.
|
|
1036
|
+
const mus = { ...opts, pointerType: 'mouse', isPrimary: true, pointerId: 1, button: 0 };
|
|
1037
|
+
try { el.dispatchEvent(new PointerEvent('pointerdown', { ...mus, buttons: 1 })); } catch (e) {}
|
|
783
1038
|
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
784
|
-
try { el.dispatchEvent(new PointerEvent('pointerup',
|
|
1039
|
+
try { el.dispatchEvent(new PointerEvent('pointerup', { ...mus, buttons: 0 })); } catch (e) {}
|
|
785
1040
|
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
786
|
-
|
|
1041
|
+
// MAALT 10/9 i en rigtig browser: her stod BEGGE — dispatchEvent('click') OG el.click().
|
|
1042
|
+
// Det er to klik-haendelser. Paa alt hvad der skifter tilstand (dropdowns, faneblade,
|
|
1043
|
+
// afkrydsningsfelter, harmonikaer) aabner det foerste og det andet lukker igen, saa
|
|
1044
|
+
// resultatet er intet. Det er aarsagen til at issue #19's div-dropdown "aldrig aabnede":
|
|
1045
|
+
// den aabnede og lukkede inde i den samme reserveloesning.
|
|
1046
|
+
// ét klik: aftryk 11|53|…|0 -> 11|69|…|1 (menuen aaben)
|
|
1047
|
+
// to klik: aftryk 11|53|…|0 -> 11|53|…|0 (tilbage ved start)
|
|
1048
|
+
// el.click() foretraekkes, fordi den ogsaa udloeser elementets aktiverings-adfaerd
|
|
1049
|
+
// (foelg link, skift afkrydsning) — det goer en syntetisk MouseEvent ikke paalideligt.
|
|
1050
|
+
// MAALT 11/9 af Astra (R5 F5): aftrykket til afgoerelsen herunder var foerAftryk fra FOER mousedown.
|
|
1051
|
+
// En ripple-node fra mousedown lignede derfor en virkning af el.click(), React blev sprunget over,
|
|
1052
|
+
// og svaret blev landed:true med nul handling. Afgoerelsen maaler nu kun hvad el.click() gjorde.
|
|
1053
|
+
const foerKlik = aftryk();
|
|
787
1054
|
if (typeof el.click === 'function') el.click();
|
|
1055
|
+
else el.dispatchEvent(new MouseEvent('click', opts));
|
|
1056
|
+
|
|
1057
|
+
// MAALT 10/9 af Astra (anden runde): React-fiberens onClick blev kaldt UBETINGET efter el.click() -
|
|
1058
|
+
// men el.click() udloeser allerede Reacts handler. To koersler, og en toggle endte hvor den startede.
|
|
1059
|
+
// Framework-vejene er til elementer hvor det native klik INGEN virkning gav.
|
|
1060
|
+
if (aftryk() === foerKlik) {
|
|
1061
|
+
// React fiber fallback — find and call onClick handler directly
|
|
1062
|
+
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
|
|
1063
|
+
if (fiberKey) {
|
|
1064
|
+
let fiber = el[fiberKey];
|
|
1065
|
+
for (let i = 0; i < 10 && fiber; i++) {
|
|
1066
|
+
if (fiber.memoizedProps?.onClick) { fiber.memoizedProps.onClick(new MouseEvent('click', {bubbles:true})); break; }
|
|
1067
|
+
fiber = fiber.return;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
788
1070
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (fiber.memoizedProps?.onClick) { fiber.memoizedProps.onClick(new MouseEvent('click', {bubbles:true})); break; }
|
|
795
|
-
fiber = fiber.return;
|
|
1071
|
+
// Angular Material fallback — ripple + internal handlers
|
|
1072
|
+
const ngKey = Object.keys(el).find(k => k.startsWith('__ng'));
|
|
1073
|
+
if (ngKey || el.getAttribute('ng-click') || el.getAttribute('(click)')) {
|
|
1074
|
+
const matRipple = el.closest && el.closest('[mat-button], [mat-raised-button], [mat-icon-button], [mat-fab], mat-checkbox, mat-slide-toggle, mat-radio-button');
|
|
1075
|
+
if (matRipple) matRipple.dispatchEvent(new MouseEvent('click', opts));
|
|
796
1076
|
}
|
|
797
1077
|
}
|
|
798
1078
|
|
|
799
|
-
//
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1079
|
+
// Lytteren kan IKKE bruges her. Den udloeses af enhver dispatch paa maalet, og
|
|
1080
|
+
// reserveloesningen dispatcher netop paa maalet — saa flaget ville vaere sandt fordi
|
|
1081
|
+
// VI sendte noget, ikke fordi siden reagerede. Reproduceret 9/9 mod et <div> uden
|
|
1082
|
+
// nogen handler: foer=false, efter=true.
|
|
1083
|
+
// Derfor maales sidens REAKTION i stedet, med samme aftryks-greb som select_option
|
|
1084
|
+
// allerede bruger: aendrede noget sig af det et klik plejer at aendre?
|
|
1085
|
+
// MAALT 12/9 af Fable (e2e runde 3): her sammenlignedes med aftrykket fra FOER mousedown.
|
|
1086
|
+
// Rettelsen af R5 F5 ovenfor gjaldt kun HVILKEN vej der blev proevet, ikke hvad vi SVAREDE. Uden en framework-vej
|
|
1087
|
+
// at falde tilbage paa gav en ripple derfor stadig landed:true med nul handling.
|
|
1088
|
+
// Tre udfald, ikke to: aendrede klikket (eller framework-vejen) noget, er det landet. Aendrede kun mousedown
|
|
1089
|
+
// noget, ved vi det ikke - det kan vaere en ripple, men ogsaa en menu der aabner paa mousedown. Saa siges der
|
|
1090
|
+
// hverken ja eller nej: landed null bliver til maaske_landet hos kalderen, med den maalte tilstand.
|
|
1091
|
+
const efterAftryk = aftryk();
|
|
1092
|
+
const klikketVirkede = efterAftryk !== foerKlik;
|
|
1093
|
+
const kunMousedown = !klikketVirkede && foerKlik !== foerAftryk;
|
|
1094
|
+
ryd();
|
|
1095
|
+
return {
|
|
1096
|
+
landed: kunMousedown ? null : klikketVirkede,
|
|
1097
|
+
...(kunMousedown ? { uvist: true } : {}),
|
|
1098
|
+
fallbackFired: true, aftrykFoer: foerAftryk, aftrykEfter: efterAftryk,
|
|
1099
|
+
};
|
|
806
1100
|
})()`,
|
|
807
1101
|
});
|
|
808
1102
|
const vaerdi = settle?.result?.value ?? null;
|
|
@@ -819,7 +1113,10 @@ async function debuggerClick(tabId, x, y) {
|
|
|
819
1113
|
dialogLoefter.delete(tabId);
|
|
820
1114
|
}
|
|
821
1115
|
}
|
|
822
|
-
return vaerdi;
|
|
1116
|
+
return vaerdi ?? await tolkManglendeSettle(tabId, settle, urlFoer);
|
|
1117
|
+
} catch (e) {
|
|
1118
|
+
if (trykSendt && e && typeof e === 'object') e.trykSendt = true;
|
|
1119
|
+
throw e;
|
|
823
1120
|
} finally {
|
|
824
1121
|
await debuggerDetach(tabId);
|
|
825
1122
|
}
|
|
@@ -851,20 +1148,21 @@ async function evalAttached(tabId, expression) {
|
|
|
851
1148
|
return result.result?.value;
|
|
852
1149
|
}
|
|
853
1150
|
|
|
1151
|
+
// En tast ned og op igen - og op igen UANSET hvad. MAALT 10/9 af Astra (anden runde): press_key fik
|
|
1152
|
+
// keyUp-altid i foerste runde, men de tre hjaelpere der ogsaa sender taster gjorde ikke. Timede et
|
|
1153
|
+
// keyDown ud efter at det VAR landet, forlod hjaelperen funktionen foer sit keyUp, og tasten sad fast
|
|
1154
|
+
// for siden. Et fastsiddende Cmd goer det naeste tastetryk til en genvej.
|
|
1155
|
+
async function tastParAttached(tabId, ned, op) {
|
|
1156
|
+
let fejl = null;
|
|
1157
|
+
try { await cdpSend(tabId, 'Input.dispatchKeyEvent', { type: 'keyDown', ...ned }); } catch (e) { fejl = e; }
|
|
1158
|
+
try { await cdpSend(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', ...op }); } catch (e) { fejl = fejl || e; }
|
|
1159
|
+
if (fejl) throw fejl;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
854
1162
|
// Select-all + Backspace. Assumes the debugger is already attached.
|
|
855
1163
|
async function clearFieldAttached(tabId) {
|
|
856
|
-
await
|
|
857
|
-
|
|
858
|
-
});
|
|
859
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
860
|
-
type: 'keyUp', key: 'a', code: 'KeyA',
|
|
861
|
-
});
|
|
862
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
863
|
-
type: 'keyDown', key: 'Backspace', code: 'Backspace',
|
|
864
|
-
});
|
|
865
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
866
|
-
type: 'keyUp', key: 'Backspace', code: 'Backspace',
|
|
867
|
-
});
|
|
1164
|
+
await tastParAttached(tabId, { key: 'a', code: 'KeyA', modifiers: SELECT_ALL_MODS }, { key: 'a', code: 'KeyA' });
|
|
1165
|
+
await tastParAttached(tabId, { key: 'Backspace', code: 'Backspace' }, { key: 'Backspace', code: 'Backspace' });
|
|
868
1166
|
}
|
|
869
1167
|
|
|
870
1168
|
async function debuggerFill(tabId, selector, value) {
|
|
@@ -900,6 +1198,37 @@ async function debuggerFill(tabId, selector, value) {
|
|
|
900
1198
|
try {
|
|
901
1199
|
await clearFieldAttached(tabId);
|
|
902
1200
|
|
|
1201
|
+
// ── Blev feltet FAKTISK tomt? (MAALT 8/9) ────────────────────────────────
|
|
1202
|
+
//
|
|
1203
|
+
// `clearFieldAttached` sender Cmd/Ctrl+A og Backspace som aegte tastetryk. Paa et
|
|
1204
|
+
// React-styret felt tommer det ikke: maalt paa forbrugeragenten.dk/penge-tilbage gav
|
|
1205
|
+
// to fill-kald efter hinanden vaerdien "test@example.dkanden@example.dk" — begge kald
|
|
1206
|
+
// svarede ok:true. Vaerktoejet meldte succes og gjorde noget andet end det lovede.
|
|
1207
|
+
//
|
|
1208
|
+
// Et fill paa et TOMT felt var rent i samme maaling, saa fejlen sidder alene her.
|
|
1209
|
+
// Derfor: laes tilbage, og ryd med den vej der virker paa styrede felter hvis
|
|
1210
|
+
// tastetrykkene ikke slog igennem. Vi gaetter ikke paa hvorfor — vi tjekker.
|
|
1211
|
+
const restVaerdi = await evalAttached(tabId, `
|
|
1212
|
+
(function() {
|
|
1213
|
+
const el = document.activeElement;
|
|
1214
|
+
return el && 'value' in el ? el.value : '';
|
|
1215
|
+
})()
|
|
1216
|
+
`);
|
|
1217
|
+
if (restVaerdi) {
|
|
1218
|
+
await evalAttached(tabId, `
|
|
1219
|
+
(function() {
|
|
1220
|
+
const el = document.activeElement;
|
|
1221
|
+
if (!el || !('value' in el)) return false;
|
|
1222
|
+
const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
1223
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
1224
|
+
if (setter) setter.call(el, ''); else el.value = '';
|
|
1225
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1226
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1227
|
+
return true;
|
|
1228
|
+
})()
|
|
1229
|
+
`);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
903
1232
|
// Fast path: one trusted InputEvent instead of N key events. This is the same
|
|
904
1233
|
// primitive set_combobox and set_date already rely on, it avoids per-key `code`
|
|
905
1234
|
// mapping entirely, and it turns a 40-character value from ~3 seconds of
|
|
@@ -971,11 +1300,41 @@ async function scriptingClick(tabId, selector) {
|
|
|
971
1300
|
}
|
|
972
1301
|
if (!el) return { ok: false, reason: 'not_found' };
|
|
973
1302
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
974
|
-
|
|
1303
|
+
// Sign-off 11/9 (Astra og Fable, begge reproduceret): paa en baggrundsfane blev et klik der krævede isTrusted,
|
|
1304
|
+
// eller kun lyttede paa pointerdown, meldt ok:true uden nogen virkning. Klikket maaler nu sidens reaktion med
|
|
1305
|
+
// samme aftryk og samme regel som debuggerens reserve (debuggerClick) - ingen backticks herinde.
|
|
1306
|
+
const hash = (s) => {
|
|
1307
|
+
let x = 2166136261;
|
|
1308
|
+
for (let i = 0; i < s.length; i++) { x ^= s.charCodeAt(i); x = Math.imul(x, 16777619); }
|
|
1309
|
+
return (x >>> 0).toString(36);
|
|
1310
|
+
};
|
|
1311
|
+
const aftryk = () => {
|
|
1312
|
+
try {
|
|
1313
|
+
return document.querySelectorAll('*').length + '|' +
|
|
1314
|
+
hash(document.body ? String(document.body.innerText) : '') + '|' +
|
|
1315
|
+
location.href + '|' +
|
|
1316
|
+
document.querySelectorAll('[aria-expanded="true"],[aria-selected="true"],[open],.open,.active').length + '|' +
|
|
1317
|
+
document.querySelectorAll('input:checked,option:checked').length + '|' +
|
|
1318
|
+
hash(Array.from(document.querySelectorAll('input,textarea,select')).map((e) => String(e.value || '')).join(' '));
|
|
1319
|
+
} catch (e) { return 'aftryk-fejlede'; }
|
|
1320
|
+
};
|
|
1321
|
+
const opts = { bubbles: true, cancelable: true, composed: true, view: window };
|
|
1322
|
+
// Et rigtigt klik sender pointerdown foer mousedown; Radix/shadcn aabner paa pointerdown.
|
|
1323
|
+
// MAALT 11/9 af Astra (efterproevning af 9814636): uden pointerType ("") handlede en side der reagerer paa ikke-mus-
|
|
1324
|
+
// pointere OG paa click to gange. En rigtig mus sender pointerType "mouse".
|
|
1325
|
+
const mus = { ...opts, pointerType: 'mouse', isPrimary: true, pointerId: 1, button: 0 };
|
|
1326
|
+
try { el.dispatchEvent(new PointerEvent('pointerdown', { ...mus, buttons: 1 })); } catch (e) {}
|
|
975
1327
|
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
1328
|
+
try { el.dispatchEvent(new PointerEvent('pointerup', { ...mus, buttons: 0 })); } catch (e) {}
|
|
976
1329
|
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
1330
|
+
// Aftrykket tages lige foer el.click(). MAALT 11/9 af Astra (e2e-review): taget foer mousedown blev en ripple klikbevis
|
|
1331
|
+
// (ok:true, nul handlinger; 1.29.0: fejl). En side der reagerer paa pointerdown, bliver derfor aerligt uvist.
|
|
1332
|
+
const foer = aftryk();
|
|
977
1333
|
el.click();
|
|
978
|
-
|
|
1334
|
+
// Maales i samme oejeblik som klikket. Astra (efterproevning af c1496d4): en anden maaling 200 ms senere gjorde et
|
|
1335
|
+
// uafhaengigt ur til klikbevis og et klik der navigerede i ventetiden til en fejl. En sen React-opdatering giver
|
|
1336
|
+
// derfor landed:false og maaske_landet - aerligt, aldrig en falsk succes.
|
|
1337
|
+
return { ok: true, tag: el.tagName, landed: aftryk() !== foer, detached: el.isConnected === false };
|
|
979
1338
|
},
|
|
980
1339
|
args: [selector],
|
|
981
1340
|
});
|
|
@@ -1209,6 +1568,20 @@ async function offscreenSvarer() {
|
|
|
1209
1568
|
const MAX_OFFSCREEN_GENSKAB = 3;
|
|
1210
1569
|
const OFFSCREEN_PAUSE_MS = 10 * 60 * 1000;
|
|
1211
1570
|
|
|
1571
|
+
// Et omraade paa mere end dette ville faa HVER brugers browser til at probe tusindvis af porte hvert andet sekund.
|
|
1572
|
+
const PORTE_MAX_SPAEND = 200;
|
|
1573
|
+
/** Laeser et alternativt portomraade fra chrome.storage.local. Ugyldigt = standarden (9876-9895) staar. */
|
|
1574
|
+
async function portOmraadeFraLager() {
|
|
1575
|
+
try {
|
|
1576
|
+
const { bmcpPorte } = await chrome.storage.local.get({ bmcpPorte: null });
|
|
1577
|
+
const m = /^(\d{4,5})-(\d{4,5})$/.exec(String(bmcpPorte ?? ''));
|
|
1578
|
+
if (!m) return null;
|
|
1579
|
+
const fra = Number(m[1]), til = Number(m[2]);
|
|
1580
|
+
if (fra < 1024 || til > 65535 || til < fra || til - fra >= PORTE_MAX_SPAEND) return null;
|
|
1581
|
+
return fra + '-' + til;
|
|
1582
|
+
} catch { return null; }
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1212
1585
|
async function ensureOffscreen() {
|
|
1213
1586
|
const findes = await chrome.offscreen.hasDocument();
|
|
1214
1587
|
|
|
@@ -1266,8 +1639,15 @@ async function ensureOffscreen() {
|
|
|
1266
1639
|
// baerer den aeldre version, hvilket er praecis den skelnen tjekket skal bruge.
|
|
1267
1640
|
let minVersion = '';
|
|
1268
1641
|
try { minVersion = chrome.runtime.getManifest().version; } catch {}
|
|
1642
|
+
// Portomraadet foelger samme vej som versionen. MAALT 12/9 af Astra: serveren sender kun til den NYESTE forbundne
|
|
1643
|
+
// udvidelse, saa en testbrowser paa de samme porte bliver "nyeste" for hver eneste koerende chats server. Et
|
|
1644
|
+
// omraade i chrome.storage.local lader én profil koere isoleret - og fordi det er en VAERDI og ikke en kodeaendring,
|
|
1645
|
+
// er repoets filer byte-identiske, og udgivelsens kode-aftryk er uroert. (Mit foerste forslag, at flytte portene
|
|
1646
|
+
// til manifestet, ville have fjernet dem fra aftrykket - Astra maalte at de ligger i offscreen.js, som hashes.)
|
|
1647
|
+
const porteParam = await portOmraadeFraLager();
|
|
1269
1648
|
await chrome.offscreen.createDocument({
|
|
1270
|
-
url: 'offscreen.html' + (minVersion ? '?v=' + encodeURIComponent(minVersion) : '')
|
|
1649
|
+
url: 'offscreen.html' + (minVersion ? '?v=' + encodeURIComponent(minVersion) : '')
|
|
1650
|
+
+ (porteParam ? (minVersion ? '&' : '?') + 'porte=' + porteParam : ''),
|
|
1271
1651
|
reasons: ['WORKERS'],
|
|
1272
1652
|
justification: 'Maintain persistent WebSocket connection to local MCP server',
|
|
1273
1653
|
});
|
|
@@ -1277,22 +1657,42 @@ async function ensureOffscreen() {
|
|
|
1277
1657
|
|
|
1278
1658
|
const SENSITIVE = new Set(['get_cookies', 'get_local_storage', 'execute_script', 'extract_token']);
|
|
1279
1659
|
|
|
1280
|
-
|
|
1660
|
+
// MAALT 11/9 (Astra, efterproevet): her stod `params: JSON.stringify(params).slice(0, 200)`.
|
|
1661
|
+
// De foerste 200 tegn af ALLE parametre — ogsaa vaerdien til fill (adgangskoder), cookie-
|
|
1662
|
+
// vaerdier og adresser med login-tokens — laa i klartekst i chrome.storage.local. Popuppen
|
|
1663
|
+
// viser kun tid, vaerktoej og session, saa parametrene tjente intet. De gemmes ikke laengere,
|
|
1664
|
+
// og poster gemt af aeldre udgaver renses ved opdatering (rensHandlingslog i onInstalled).
|
|
1665
|
+
async function logAction(port, method) {
|
|
1281
1666
|
const category = SENSITIVE.has(method) ? 'sensitive' : 'safe';
|
|
1282
1667
|
const session = sessions.get(port);
|
|
1283
1668
|
const entry = {
|
|
1284
1669
|
time: Date.now(),
|
|
1285
1670
|
method,
|
|
1286
|
-
params: JSON.stringify(params).slice(0, 200),
|
|
1287
1671
|
category,
|
|
1288
1672
|
session: session?.label || `Port ${port}`,
|
|
1289
1673
|
color: session?.color || 'grey',
|
|
1290
1674
|
};
|
|
1291
|
-
|
|
1292
|
-
actionLog.
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1675
|
+
try {
|
|
1676
|
+
const { actionLog = [] } = await chrome.storage.local.get({ actionLog: [] });
|
|
1677
|
+
// MAALT 11/9 af Astra (e2e-review): et logAction der koerte samtidig med rensHandlingslog i onInstalled, havde laest den
|
|
1678
|
+
// gamle log foer oprydningen skrev - og skrev saa den gamle adgangskode tilbage. Hver skrivning renser derfor selv.
|
|
1679
|
+
const renset = (Array.isArray(actionLog) ? actionLog : []).filter(Boolean).map(({ params, ...resten }) => resten);
|
|
1680
|
+
renset.unshift(entry);
|
|
1681
|
+
if (renset.length > 50) renset.length = 50;
|
|
1682
|
+
await chrome.storage.local.set({ actionLog: renset });
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
console.warn('[BG] handlingslog kunne ikke skrives:', e?.message || e);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
async function rensHandlingslog() {
|
|
1689
|
+
try {
|
|
1690
|
+
const { actionLog } = await chrome.storage.local.get({ actionLog: [] });
|
|
1691
|
+
if (!Array.isArray(actionLog) || !actionLog.some((p) => p && 'params' in p)) return;
|
|
1692
|
+
await chrome.storage.local.set({ actionLog: actionLog.map(({ params, ...resten }) => resten) });
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
console.warn('[BG] gammel handlingslog kunne ikke renses:', e?.message || e);
|
|
1695
|
+
}
|
|
1296
1696
|
}
|
|
1297
1697
|
|
|
1298
1698
|
// ── Message Handler — receives commands from offscreen.js ──────────────────
|
|
@@ -1300,7 +1700,7 @@ function logAction(port, method, params) {
|
|
|
1300
1700
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
1301
1701
|
if (msg.type === 'mcp_command') {
|
|
1302
1702
|
const port = msg.port;
|
|
1303
|
-
logAction(port, msg.method
|
|
1703
|
+
logAction(port, msg.method);
|
|
1304
1704
|
// Restore sessions from storage (service worker may have restarted), then take over any
|
|
1305
1705
|
// session orphaned by an unclean disconnect so navigate→click keeps hitting the same tab.
|
|
1306
1706
|
restoreSessions()
|
|
@@ -1367,10 +1767,35 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
1367
1767
|
|
|
1368
1768
|
const OAUTH_DOMAINS = ['accounts.google.com', 'login.microsoftonline.com', 'github.com/login/oauth', 'slack.com/oauth', 'app.hubspot.com/oauth'];
|
|
1369
1769
|
|
|
1770
|
+
// MAALT 10/9, reproduceret i selen: her stod kun `lastCreatedTabId = tab.id` — sat paa
|
|
1771
|
+
// HVER onCreated, ogsaa naar brugeren selv trykker Cmd+T eller aabner sin netbank.
|
|
1772
|
+
// `get_new_tab` adopterede den saa ind i agentens session, hvorefter skaermbillede,
|
|
1773
|
+
// sidetekst og localStorage af BRUGERENS fane var lovligt. Reproduktionen: bruger aabner
|
|
1774
|
+
// brugerens-netbank.example -> get_new_tab svarer med den -> fanen staar i tabIds.
|
|
1775
|
+
//
|
|
1776
|
+
// En ny fane hoerer til en session naar den er aabnet FRA en af dens faner (klik paa et
|
|
1777
|
+
// link med target=_blank, en OAuth-popup). Det staar i openerTabId. Er der ingen opener,
|
|
1778
|
+
// var det brugeren, og saa er den ikke vores.
|
|
1370
1779
|
let lastCreatedTabId = null;
|
|
1780
|
+
// Aabneren huskes PR. FANE. En global "seneste aabner" blev 10/9 (Astra, reproduceret) laant af
|
|
1781
|
+
// den forkerte fane, naar to faner blev aabnet mens get_new_tab ventede paa tabs.get.
|
|
1782
|
+
const openerForFane = new Map();
|
|
1783
|
+
// MAALT 11/9 af Astra (R5 F9), reproduceret: fane 1 aabner en popup og lukkes derefter (typisk
|
|
1784
|
+
// "log ind i nyt vindue"). get_new_tab slog aabneren op i sessionens faner NU, hvor 1 var vaek,
|
|
1785
|
+
// og svarede not-ours paa sessionens egen popup. Ejeren huskes derfor i det oejeblik fanen
|
|
1786
|
+
// oprettes, hvor aabneren stadig staar i sin session.
|
|
1787
|
+
const ejerForFane = new Map();
|
|
1371
1788
|
|
|
1372
1789
|
chrome.tabs.onCreated.addListener(async (tab) => {
|
|
1373
1790
|
lastCreatedTabId = tab.id;
|
|
1791
|
+
openerForFane.set(tab.id, tab.openerTabId ?? null);
|
|
1792
|
+
if (openerForFane.size > 500) openerForFane.delete(openerForFane.keys().next().value);
|
|
1793
|
+
let ejer = null;
|
|
1794
|
+
if (tab.openerTabId != null) {
|
|
1795
|
+
for (const [p, s] of sessions) { if (s.tabIds.has(tab.openerTabId)) { ejer = p; break; } }
|
|
1796
|
+
}
|
|
1797
|
+
ejerForFane.set(tab.id, ejer);
|
|
1798
|
+
if (ejerForFane.size > 500) ejerForFane.delete(ejerForFane.keys().next().value);
|
|
1374
1799
|
|
|
1375
1800
|
// Auto-claim OAuth popups for the session that opened them
|
|
1376
1801
|
if (tab.pendingUrl || tab.url) {
|
|
@@ -1428,7 +1853,9 @@ function parsePlaceholderFormat(placeholder) {
|
|
|
1428
1853
|
const order = parts.map(p => p.includes('Y') ? 'Y' : p.includes('M') ? 'M' : p.includes('D') ? 'D' : null);
|
|
1429
1854
|
if (order.includes(null) || new Set(order).size !== 3) return null;
|
|
1430
1855
|
const padded = parts.map(p => p.length >= 2);
|
|
1431
|
-
|
|
1856
|
+
// Fjerde runde: YY og YYYY skal kunne skelnes, ellers skrives "2026" i et felt der kun tager to cifre.
|
|
1857
|
+
const lengths = parts.map(p => p.length);
|
|
1858
|
+
return { sep, order, padded, lengths };
|
|
1432
1859
|
}
|
|
1433
1860
|
|
|
1434
1861
|
function isoToFormat(iso, fmt) {
|
|
@@ -1436,7 +1863,7 @@ function isoToFormat(iso, fmt) {
|
|
|
1436
1863
|
if (!m) throw new Error('Invalid ISO date: ' + iso);
|
|
1437
1864
|
const [, y, mo, d] = m;
|
|
1438
1865
|
return fmt.order.map((slot, i) => {
|
|
1439
|
-
if (slot === 'Y') return y;
|
|
1866
|
+
if (slot === 'Y') return fmt.lengths && fmt.lengths[i] === 2 ? y.slice(2) : y;
|
|
1440
1867
|
if (slot === 'M') return fmt.padded[i] ? mo : String(parseInt(mo, 10));
|
|
1441
1868
|
if (slot === 'D') return fmt.padded[i] ? d : String(parseInt(d, 10));
|
|
1442
1869
|
}).join(fmt.sep);
|
|
@@ -1459,17 +1886,53 @@ function parseMonthYearText(text) {
|
|
|
1459
1886
|
return null;
|
|
1460
1887
|
}
|
|
1461
1888
|
|
|
1462
|
-
function valueLooksLikeIso(
|
|
1463
|
-
if (!
|
|
1889
|
+
function valueLooksLikeIso(raaVaerdi, iso, fmt) {
|
|
1890
|
+
if (!raaVaerdi || !iso) return false;
|
|
1464
1891
|
const [y, m, d] = iso.split('-');
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1892
|
+
const Y = Number(y), M = Number(m), D = Number(d);
|
|
1893
|
+
// Fjerde runde: en aflaesning der BEGYNDER med den oenskede ISO-dato (fx "2026-01-02T12:00:00") er den dato,
|
|
1894
|
+
// uanset hvilket format placeholderen lover - ellers blev en korrekt dato afvist og kalenderen proevet oveni.
|
|
1895
|
+
if (raaVaerdi.trim().startsWith(iso) && !/\d/.test(raaVaerdi.trim().charAt(iso.length))) return true;
|
|
1896
|
+
// MAALT 11/9 af Astra (R5): klokkeslaet og tidszone er ikke en del af datoen. F2: "02/01 20:26" gav timen 20
|
|
1897
|
+
// som aaret 2020 og ok:true. F3: "02/01/2026 12:00 GMT" blev afvist af bogstavkontrollen paa "GMT", og
|
|
1898
|
+
// kalenderen blev proevet oveni. Begge fjernes foer datoen laeses.
|
|
1899
|
+
const value = raaVaerdi
|
|
1900
|
+
.replace(/\b\d{1,2}:\d{2}(:\d{2}(\.\d+)?)?(\s*[ap]\.?m\.?)?(?![\d:])/gi, ' ')
|
|
1901
|
+
.replace(/\b(GMT|UTC|UT|CET|CEST|EET|EEST|WET|WEST|BST|EST|EDT|CST|CDT|MST|MDT|PST|PDT)\b([+-]\d{1,2}(:?\d{2})?)?/g, ' ')
|
|
1902
|
+
.replace(/\s+/g, ' ')
|
|
1903
|
+
.trim();
|
|
1904
|
+
if (!value) return false;
|
|
1905
|
+
// Astra, anden runde: tre `value.includes(...)` hver for sig godkendte "20/12/2026" som 2026-01-02.
|
|
1906
|
+
// Tredje runde: tre `digits.includes(...)` koerte stadig FOER kontrollen af hele tal, saa
|
|
1907
|
+
// "2026-1-1 02:00" blev 2026-11-02. Nu sammenlignes cifre som én streng KUN naar vaerdien udelukkende
|
|
1908
|
+
// ER otte cifre - og med kendt format kun i formatets raekkefoelge ("01/12/2026" under DD/MM/YYYY er
|
|
1909
|
+
// 1. december, ikke 12. januar).
|
|
1910
|
+
const ordener = fmt?.order ? [fmt.order] : [['Y', 'M', 'D'], ['D', 'M', 'Y'], ['M', 'D', 'Y']];
|
|
1911
|
+
const del = { Y: y, M: m, D: d };
|
|
1912
|
+
if (/^\s*\d{8}\s*$/.test(value)) {
|
|
1913
|
+
const cifre = value.trim();
|
|
1914
|
+
return ordener.some((o) => o.map((slot) => del[slot]).join('') === cifre);
|
|
1915
|
+
}
|
|
1916
|
+
// Hele tal fra vaerdiens START - et klokkeslaet bagefter maa ikke levere dag eller maaned.
|
|
1917
|
+
const tok = value.match(/\d+/g) || [];
|
|
1918
|
+
const passer = (slot, s) => {
|
|
1919
|
+
if (s === undefined) return false;
|
|
1920
|
+
if (slot === 'Y') return (s.length === 4 && Number(s) === Y) || (s.length === 2 && Number(s) === Y % 100);
|
|
1921
|
+
return s.length <= 2 && Number(s) === (slot === 'M' ? M : D);
|
|
1922
|
+
};
|
|
1923
|
+
// Fjerde runde: rene tal kun naar vaerdien ingen bogstaver har - i "2 Jan 26 05:00" blev timen ellers et aarstal.
|
|
1924
|
+
if (!/\p{L}/u.test(value) && ordener.some((o) => o.every((slot, i) => passer(slot, tok[i])))) return true;
|
|
1925
|
+
// Maanedsnavn ("2 Jan 2026", "2. maj 2026", "Jan 2, 2026"): dagen skal staa lige foer eller lige efter navnet.
|
|
1926
|
+
const navne = [/jan/, /feb/, /mar/, /apr/, /ma[iyj]/, /jun/, /jul/, /aug/, /sep/, /o[ck]t/, /nov/, /de[cz]/];
|
|
1927
|
+
const navn = navne[M - 1];
|
|
1928
|
+
if (navn) {
|
|
1929
|
+
const lav = value.toLowerCase();
|
|
1930
|
+
const n = navn.source;
|
|
1931
|
+
const foer = new RegExp('^\\s*(\\d{1,2})\\.?\\s+' + n + '[a-zæøå]*\\.?,?\\s+(\\d{4})(?!\\d)').exec(lav);
|
|
1932
|
+
const efter = new RegExp('^\\s*' + n + '[a-zæøå]*\\.?\\s+(\\d{1,2}),?\\s+(\\d{4})(?!\\d)').exec(lav);
|
|
1933
|
+
for (const r of [foer, efter]) if (r && Number(r[1]) === D && Number(r[2]) === Y) return true;
|
|
1934
|
+
}
|
|
1935
|
+
return false;
|
|
1473
1936
|
}
|
|
1474
1937
|
|
|
1475
1938
|
async function getDateInputInfo(tabId, selector) {
|
|
@@ -1538,25 +2001,9 @@ async function setDateMaskedTyping(tabId, selector, iso, format) {
|
|
|
1538
2001
|
await debuggerFocus(tabId, selector);
|
|
1539
2002
|
await debuggerAttach(tabId);
|
|
1540
2003
|
try {
|
|
1541
|
-
await
|
|
1542
|
-
type: 'keyDown', key: 'a', code: 'KeyA', modifiers: SELECT_ALL_MODS,
|
|
1543
|
-
});
|
|
1544
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1545
|
-
type: 'keyUp', key: 'a', code: 'KeyA',
|
|
1546
|
-
});
|
|
1547
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1548
|
-
type: 'keyDown', key: 'Backspace', code: 'Backspace',
|
|
1549
|
-
});
|
|
1550
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1551
|
-
type: 'keyUp', key: 'Backspace', code: 'Backspace',
|
|
1552
|
-
});
|
|
2004
|
+
await clearFieldAttached(tabId);
|
|
1553
2005
|
await cdpSend(tabId, 'Input.insertText', { text: formatted });
|
|
1554
|
-
await
|
|
1555
|
-
type: 'keyDown', key: 'Tab', code: 'Tab',
|
|
1556
|
-
});
|
|
1557
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1558
|
-
type: 'keyUp', key: 'Tab', code: 'Tab',
|
|
1559
|
-
});
|
|
2006
|
+
await tastParAttached(tabId, { key: 'Tab', code: 'Tab' }, { key: 'Tab', code: 'Tab' });
|
|
1560
2007
|
} finally {
|
|
1561
2008
|
await debuggerDetach(tabId);
|
|
1562
2009
|
}
|
|
@@ -1715,12 +2162,7 @@ async function setDatePicker(tabId, selector, iso) {
|
|
|
1715
2162
|
await debuggerAttach(tabId);
|
|
1716
2163
|
try {
|
|
1717
2164
|
const key = delta > 0 ? 'PageDown' : 'PageUp';
|
|
1718
|
-
await
|
|
1719
|
-
type: 'keyDown', key, code: key,
|
|
1720
|
-
});
|
|
1721
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1722
|
-
type: 'keyUp', key, code: key,
|
|
1723
|
-
});
|
|
2165
|
+
await tastParAttached(tabId, { key, code: key }, { key, code: key });
|
|
1724
2166
|
} finally {
|
|
1725
2167
|
await debuggerDetach(tabId);
|
|
1726
2168
|
}
|
|
@@ -2063,6 +2505,26 @@ async function setCombobox(tabId, selector, values, opts = {}) {
|
|
|
2063
2505
|
const waitIterations = Math.max(1, Math.ceil(waitMs / 100));
|
|
2064
2506
|
const results = [];
|
|
2065
2507
|
|
|
2508
|
+
// MAALT 9/9-2026: paa en aegte <select> brugte den her 8,5 sekunder paa at sige nej.
|
|
2509
|
+
// Den klikkede feltet, forsoegte at tomme det med en input-vaerdisaetter (en <select>
|
|
2510
|
+
// ER ikke et input), skrev tekst ind, og pollede saa 30 gange efter en listbox der
|
|
2511
|
+
// aldrig kan opstaa — for saa at svare "no-options-rendered". Kapaciteten fandtes hele
|
|
2512
|
+
// tiden i browser_select_option, som klarer samme felt paa 9 ms. Nu siger den det
|
|
2513
|
+
// med det samme i stedet for at lade agenten vente og gaette.
|
|
2514
|
+
const erNativeSelect = await debuggerEval(tabId, `(() => {
|
|
2515
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2516
|
+
return el?.tagName === 'SELECT';
|
|
2517
|
+
})()`).catch(() => false);
|
|
2518
|
+
if (erNativeSelect) {
|
|
2519
|
+
return {
|
|
2520
|
+
ok: false,
|
|
2521
|
+
error: 'native-select',
|
|
2522
|
+
hint: 'Feltet er en almindelig <select>. Brug browser_select_option i stedet — ' +
|
|
2523
|
+
'set_combobox er til dropdowns bygget af div/li med en listbox.',
|
|
2524
|
+
selector,
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2066
2528
|
for (const val of valueList) {
|
|
2067
2529
|
try {
|
|
2068
2530
|
const inputEl = await resolveElement(tabId, selector);
|
|
@@ -2469,24 +2931,82 @@ async function dispatch(port, method, params) {
|
|
|
2469
2931
|
if (tab.url.startsWith('chrome://') || tab.url.startsWith('about:')) {
|
|
2470
2932
|
throw new Error(`Cannot screenshot ${tab.url.split(':')[0]}: pages — navigate to a real page first`);
|
|
2471
2933
|
}
|
|
2472
|
-
//
|
|
2473
|
-
//
|
|
2474
|
-
|
|
2934
|
+
// MAALT 10/9, anden runde (Astra, reproduceret): reserveloesningen captureVisibleTab
|
|
2935
|
+
// fotograferer den fane der er SYNLIG i vinduet, ikke agentens. Et tjek foer og et efter
|
|
2936
|
+
// kunne ikke udelukke at brugeren skiftede A->B->A imens - og saa blev brugerens side
|
|
2937
|
+
// leveret. To tidspunkter beviser ikke hvad der skete imellem. Reserveloesningen er derfor
|
|
2938
|
+
// fjernet: CDP optager netop `tab.id`, eller der er intet billede.
|
|
2939
|
+
// Samme runde: en frist paa optagelsen startede alligevel en ny runde (haev vinduet, optag
|
|
2940
|
+
// igen), og kaeden kom over serverens 30 s. En frist markeres nu, og saa proeves der ikke igen.
|
|
2941
|
+
// Sign-off 11/9 (Astra, R5 F8): hele kaeden har ét budget (skaermbilledeFrister). Fristen gaelder ogsaa mens
|
|
2942
|
+
// cdpSend gentager et kald efter en afkobling - det var den gentagelse der bar kaeden over 30 s.
|
|
2943
|
+
const { foersteMs, samletMs, haevMs = 0 } = skaermbilledeFrister();
|
|
2944
|
+
const budgetSlut = Date.now() + samletMs;
|
|
2945
|
+
// MAALT 11/9 af Astra (e2e runde 2), tre fejl i den haevede runde: reservationen blev trukket fra IGEN (reserven fik
|
|
2946
|
+
// 0 ms), `debuggerAttach` laa uden for fristloebet (en gentilslutning paa 12,5 s bar kaeden over 30 s), og haevningen
|
|
2947
|
+
// startede nye optagelser og smed de igangvaerende vaek - 1.29.0 fik netop svar fra dem der allerede loeb.
|
|
2948
|
+
const igangvaerende = []; // alle optagelser der stadig kan svare, paa tvaers af runder
|
|
2949
|
+
const tryCapture = async ({ sidsteUdvej = false } = {}) => {
|
|
2950
|
+
const reserveret = sidsteUdvej ? 0 : haevMs; // kun foerste runde holder tid fri til haevningen
|
|
2951
|
+
const denneRunde = [];
|
|
2952
|
+
const optag = (p) => {
|
|
2953
|
+
const kald = cdpSend(tab.id, 'Page.captureScreenshot', p);
|
|
2954
|
+
kald.catch(() => {}); // et svar efter budgettet er ligegyldigt
|
|
2955
|
+
igangvaerende.push(kald);
|
|
2956
|
+
denneRunde.push(kald);
|
|
2957
|
+
return kald;
|
|
2958
|
+
};
|
|
2959
|
+
const medFrist = async (loefte, ms) => {
|
|
2960
|
+
let ur;
|
|
2961
|
+
try {
|
|
2962
|
+
return await Promise.race([
|
|
2963
|
+
loefte,
|
|
2964
|
+
new Promise((_, afvis) => {
|
|
2965
|
+
const frist = Math.max(0, ms);
|
|
2966
|
+
ur = setTimeout(() => afvis(new Error(`CDP svarede ikke inden ${frist} ms: Page.captureScreenshot`)), frist);
|
|
2967
|
+
}),
|
|
2968
|
+
]);
|
|
2969
|
+
} catch (e) {
|
|
2970
|
+
if (e && typeof e === 'object' && /svarede ikke inden/.test(e.message || '')) e.ingenNyRunde = true;
|
|
2971
|
+
throw e;
|
|
2972
|
+
} finally {
|
|
2973
|
+
clearTimeout(ur);
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
// Tilslutningen skal ogsaa ligge inde i budgettet: en gentilslutning kan tage lang tid paa et haevet vindue.
|
|
2977
|
+
await medFrist(debuggerAttach(tab.id), budgetSlut - reserveret - Date.now());
|
|
2978
|
+
const standard = optag({ format: 'png' });
|
|
2475
2979
|
try {
|
|
2476
|
-
await
|
|
2980
|
+
const shot = await medFrist(standard, Math.min(foersteMs, budgetSlut - reserveret - Date.now()));
|
|
2981
|
+
return { image: 'data:image/png;base64,' + shot.data };
|
|
2982
|
+
} catch (foersteFejl) {
|
|
2983
|
+
// fromSurface:false proeves ogsaa efter en frist: i 1.29.0 var det netop fristen der naaede hertil, og den
|
|
2984
|
+
// leverede billedet. Men efter en frist startes ingen runde mere, uanset hvordan reserven fejler.
|
|
2985
|
+
// MAALT 11/9 af Astra (efterproevning af f084d1b): standardoptagelsen lykkedes efter 11 s, reserven fejlede, og
|
|
2986
|
+
// fristen paa 10 s havde kasseret standardbilledet. Standardoptagelsen loeber derfor videre efter sin frist, og
|
|
2987
|
+
// den af de to der lykkes foerst inden for budgettet, vinder.
|
|
2988
|
+
optag({ format: 'png', fromSurface: false, captureBeyondViewport: false });
|
|
2477
2989
|
try {
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2990
|
+
// Den haevede runde er sidste udvej paa en tildaekket skaerm og skal have tid tilbage (haevMs) - ellers naar
|
|
2991
|
+
// 1.29.0's eneste virkende vej aldrig frem (Fable, e2e 11/9).
|
|
2992
|
+
// MAALT 12/9 af Astra: alle igangvaerende optagelser laa i den SAMME Promise.any, saa et gammelt billede fra
|
|
2993
|
+
// foer haevningen kunne svare foerst og vinde - selv om siden havde aendret sig imens. Rundens EGNE optagelser
|
|
2994
|
+
// afgoer nu svaret; de aeldre taeller stadig med, men kun hvis rundens egne ikke naar frem.
|
|
2995
|
+
let shot;
|
|
2996
|
+
try {
|
|
2997
|
+
shot = await medFrist(Promise.any(denneRunde), budgetSlut - reserveret - Date.now());
|
|
2998
|
+
} catch (rundeFejl) {
|
|
2999
|
+
const aeldre = igangvaerende.filter((k) => !denneRunde.includes(k));
|
|
3000
|
+
if (!aeldre.length) throw rundeFejl;
|
|
3001
|
+
// Et aeldre billede der ALLEREDE er kommet, svarer i samme oejeblik - ogsaa naar budgettet er brugt op.
|
|
3002
|
+
shot = await medFrist(Promise.any(aeldre), budgetSlut - Date.now());
|
|
3003
|
+
}
|
|
2484
3004
|
return { image: 'data:image/png;base64,' + shot.data };
|
|
3005
|
+
} catch (andenFejl) {
|
|
3006
|
+
const fejl = andenFejl instanceof AggregateError ? andenFejl.errors[andenFejl.errors.length - 1] : andenFejl;
|
|
3007
|
+
if (foersteFejl?.ingenNyRunde && fejl && typeof fejl === 'object') fejl.ingenNyRunde = true;
|
|
3008
|
+
throw fejl;
|
|
2485
3009
|
}
|
|
2486
|
-
} catch {
|
|
2487
|
-
// CDP failed entirely — native tabs API (needs the tab visible in its window).
|
|
2488
|
-
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
|
|
2489
|
-
return { image: dataUrl };
|
|
2490
3010
|
}
|
|
2491
3011
|
};
|
|
2492
3012
|
|
|
@@ -2494,6 +3014,13 @@ async function dispatch(port, method, params) {
|
|
|
2494
3014
|
try {
|
|
2495
3015
|
return await tryCapture();
|
|
2496
3016
|
} catch (firstErr) {
|
|
3017
|
+
// 10/9 (Astra, R2) stod her: en frist betyder at kompositoren ikke svarede, saa en ny runde fordobler kun
|
|
3018
|
+
// ventetiden. MAALT 11/9 af Fable: paa en TILDAEKKET skaerm haenger begge optagelser netop indtil vinduet haeves -
|
|
3019
|
+
// 1.29.0 leverede dér et billede efter 16,7 s, hvor HEAD gav op. Budgettet (haevMs) holder kaeden under serverens
|
|
3020
|
+
// 30 s, saa den sidste udvej maa proeves uanset om fejlen var en frist.
|
|
3021
|
+
// Er budgettet brugt, er der ikke tid til en runde med haevet vindue foer serverens 30 s. (haevMs ovenfor er netop
|
|
3022
|
+
// den tid der blev holdt fri til den her runde.)
|
|
3023
|
+
if (budgetSlut - Date.now() <= 0) throw firstErr;
|
|
2497
3024
|
// Both methods failed → the window is genuinely OCCLUDED (covered by other windows),
|
|
2498
3025
|
// so Chrome's compositor produced no frames. LAST RESORT ONLY: raise the window to
|
|
2499
3026
|
// de-occlude it, capture, then RESTORE the user's previously-focused window. This
|
|
@@ -2503,7 +3030,7 @@ async function dispatch(port, method, params) {
|
|
|
2503
3030
|
await chrome.windows.update(tab.windowId, { focused: true, state: 'normal' });
|
|
2504
3031
|
await chrome.tabs.update(tab.id, { active: true }).catch(() => {});
|
|
2505
3032
|
await new Promise(r => setTimeout(r, 250)); // let it composite
|
|
2506
|
-
return await tryCapture();
|
|
3033
|
+
return await tryCapture({ sidsteUdvej: true }); // resten af budgettet - der er ikke en runde mere efter denne
|
|
2507
3034
|
} catch (secondErr) {
|
|
2508
3035
|
throw new Error(
|
|
2509
3036
|
`Screenshot failed after focus-neutral AND raised attempts. ` +
|
|
@@ -2534,19 +3061,43 @@ async function dispatch(port, method, params) {
|
|
|
2534
3061
|
|
|
2535
3062
|
const diag = { tried: [] };
|
|
2536
3063
|
|
|
3064
|
+
// MAALT 10/9 af Astra (anden runde), reproduceret med en taeller: kode der udfoerte en
|
|
3065
|
+
// effekt og SAA kastede, blev koert igen via debuggeren - to effekter. `sendt` beskyttede
|
|
3066
|
+
// kun debugger-loekken. Er koden koert, er dens fejl svaret; den koeres ikke igen.
|
|
3067
|
+
// Samme runde: scripting-stierne afventede ikke et Promise ("Promise.resolve(42)" gav {}).
|
|
3068
|
+
// Den injicerede funktion er nu async og afventer resultatet.
|
|
3069
|
+
const koertOgFejlede = (r) => ({
|
|
3070
|
+
ok: false, error: r.message, name: r.name,
|
|
3071
|
+
method: r.world === 'MAIN' ? 'scripting-main' : 'scripting-isolated',
|
|
3072
|
+
note: 'Koden KOERTE og kastede en fejl. Den koeres ikke igen via debuggeren, fordi det den ' +
|
|
3073
|
+
'naaede at goere foer fejlen saa ville ske to gange.',
|
|
3074
|
+
});
|
|
3075
|
+
// MAALT 11/9 af Astra (R5 F4), reproduceret: scriptet sendte en POST og returnerede et
|
|
3076
|
+
// Promise; mens scripting-stien ventede, forsvandt dokumentet, og Chrome afviste med
|
|
3077
|
+
// "Frame with ID 0 was removed." Handleren gik saa videre til debuggeren, som koerte koden
|
|
3078
|
+
// igen - to POST'er. Forsvinder siden EFTER at koden er startet, kan den have koert, og
|
|
3079
|
+
// den koeres ikke igen. Blev den aldrig indsproejtet, maa debuggeren stadig proeve.
|
|
3080
|
+
const sidenForsvandt = (m) => /was removed|execution context was destroyed|document (was )?unloaded/i.test(m);
|
|
3081
|
+
const maaskeKoert = (world, m) => ({
|
|
3082
|
+
ok: false, error: m, maybe_ran: true,
|
|
3083
|
+
method: world === 'MAIN' ? 'scripting-main' : 'scripting-isolated',
|
|
3084
|
+
note: 'Siden skiftede eller lukkede mens koden koerte. Den kan allerede have koert, saa den ' +
|
|
3085
|
+
'koeres ikke igen via debuggeren. Kald igen kun hvis det er sikkert at koere to gange.',
|
|
3086
|
+
});
|
|
2537
3087
|
// Step 1: try ISOLATED world
|
|
2538
3088
|
try {
|
|
2539
3089
|
const [result] = await chrome.scripting.executeScript({
|
|
2540
3090
|
target: { tabId: tab.id },
|
|
2541
3091
|
world: 'ISOLATED',
|
|
2542
3092
|
args: [params.code],
|
|
2543
|
-
func: (codeStr) => {
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
}
|
|
3093
|
+
func: async (codeStr) => {
|
|
3094
|
+
// Oversaettelse og koersel er adskilt: fejler oversaettelsen (CSP, syntaks), koerte
|
|
3095
|
+
// intet, og debuggeren maa proeve. Fejler KOERSLEN, er koden allerede koert.
|
|
3096
|
+
let fn;
|
|
3097
|
+
try { fn = new Function('return (' + codeStr + ')'); }
|
|
3098
|
+
catch (e) { return { __kompilering: true, message: String(e?.message || e), name: e?.name, world: 'ISOLATED' }; }
|
|
3099
|
+
try { return { __ok: true, value: await fn() }; }
|
|
3100
|
+
catch (e) { return { __scriptingError: true, koerte: true, message: String(e?.message || e), name: e?.name, world: 'ISOLATED' }; }
|
|
2550
3101
|
},
|
|
2551
3102
|
});
|
|
2552
3103
|
const r = result?.result;
|
|
@@ -2554,10 +3105,14 @@ async function dispatch(port, method, params) {
|
|
|
2554
3105
|
if (r && typeof r === 'object' && r.__ok) {
|
|
2555
3106
|
return { result: r.value, method: 'scripting-isolated' };
|
|
2556
3107
|
}
|
|
2557
|
-
if (r && typeof r === 'object' && r.__scriptingError)
|
|
3108
|
+
if (r && typeof r === 'object' && r.__scriptingError && r.koerte) return koertOgFejlede(r);
|
|
3109
|
+
if (r && typeof r === 'object' && (r.__scriptingError || r.__kompilering)) {
|
|
2558
3110
|
diag.isolated_error = r.message;
|
|
2559
3111
|
}
|
|
2560
3112
|
} catch (e) {
|
|
3113
|
+
// Ingen maybe_ran her. MAALT 11/9 i Chrome for Testing 153: `new Function` i ISOLATED afvises af udvidelsens
|
|
3114
|
+
// CSP ('unsafe-eval'), saa brugerens kode kan aldrig have koert i denne verden. Sign-off (Astra): en afvisning
|
|
3115
|
+
// med "Frame with ID 0 was removed." foer start gav maybe_ran og nul koersler, hvor 1.29.0 koerte koden via MAIN.
|
|
2561
3116
|
diag.isolated_throw = String(e?.message || e);
|
|
2562
3117
|
}
|
|
2563
3118
|
|
|
@@ -2567,13 +3122,14 @@ async function dispatch(port, method, params) {
|
|
|
2567
3122
|
target: { tabId: tab.id },
|
|
2568
3123
|
world: 'MAIN',
|
|
2569
3124
|
args: [params.code],
|
|
2570
|
-
func: (codeStr) => {
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
}
|
|
3125
|
+
func: async (codeStr) => {
|
|
3126
|
+
// Oversaettelse og koersel er adskilt: fejler oversaettelsen (CSP, syntaks), koerte
|
|
3127
|
+
// intet, og debuggeren maa proeve. Fejler KOERSLEN, er koden allerede koert.
|
|
3128
|
+
let fn;
|
|
3129
|
+
try { fn = new Function('return (' + codeStr + ')'); }
|
|
3130
|
+
catch (e) { return { __kompilering: true, message: String(e?.message || e), name: e?.name, world: 'MAIN' }; }
|
|
3131
|
+
try { return { __ok: true, value: await fn() }; }
|
|
3132
|
+
catch (e) { return { __scriptingError: true, koerte: true, message: String(e?.message || e), name: e?.name, world: 'MAIN' }; }
|
|
2577
3133
|
},
|
|
2578
3134
|
});
|
|
2579
3135
|
const r = result?.result;
|
|
@@ -2581,11 +3137,14 @@ async function dispatch(port, method, params) {
|
|
|
2581
3137
|
if (r && typeof r === 'object' && r.__ok) {
|
|
2582
3138
|
return { result: r.value, method: 'scripting-main' };
|
|
2583
3139
|
}
|
|
2584
|
-
if (r && typeof r === 'object' && r.__scriptingError)
|
|
3140
|
+
if (r && typeof r === 'object' && r.__scriptingError && r.koerte) return koertOgFejlede(r);
|
|
3141
|
+
if (r && typeof r === 'object' && (r.__scriptingError || r.__kompilering)) {
|
|
2585
3142
|
diag.main_error = r.message;
|
|
2586
3143
|
}
|
|
2587
3144
|
} catch (e) {
|
|
2588
|
-
|
|
3145
|
+
const m = String(e?.message || e);
|
|
3146
|
+
if (sidenForsvandt(m)) return maaskeKoert('MAIN', m);
|
|
3147
|
+
diag.main_throw = m;
|
|
2589
3148
|
}
|
|
2590
3149
|
|
|
2591
3150
|
// Step 3: debugger fallback — the ONLY universal path for arbitrary STRING code
|
|
@@ -2597,10 +3156,15 @@ async function dispatch(port, method, params) {
|
|
|
2597
3156
|
// fires and debuggerEval silently returned undefined → the caller saw a bare
|
|
2598
3157
|
// `{method:"debugger"}` with no result. Also surface script exceptions + raw
|
|
2599
3158
|
// diagnostics so a genuine failure is never mistaken for an empty success.
|
|
2600
|
-
let rawDbg, dbgErr = '';
|
|
3159
|
+
let rawDbg, dbgErr = '', sendt = false;
|
|
2601
3160
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
2602
3161
|
try {
|
|
2603
3162
|
await debuggerAttach(tab.id);
|
|
3163
|
+
// MAALT 10/9 af Astra: faldt debuggeren af EFTER at scriptet var sendt, loeb loekken
|
|
3164
|
+
// videre og koerte BRUGERENS kode igen — op til fire gange. Vi kan ikke se om et
|
|
3165
|
+
// vilkaarligt script muterer. Samme regel som for Runtime.evaluate i cdpSend: er det
|
|
3166
|
+
// sendt, gentages det ikke automatisk. Kun en fejl FOER afsendelse proeves igen.
|
|
3167
|
+
sendt = true;
|
|
2604
3168
|
rawDbg = await cdpSend(tab.id, 'Runtime.evaluate', {
|
|
2605
3169
|
expression: '(' + params.code + '\n)',
|
|
2606
3170
|
returnByValue: true,
|
|
@@ -2616,12 +3180,14 @@ async function dispatch(port, method, params) {
|
|
|
2616
3180
|
return { result: rawDbg.result.value, method: 'debugger' };
|
|
2617
3181
|
}
|
|
2618
3182
|
dbgErr = 'empty/undefined CDP response: ' + JSON.stringify(rawDbg);
|
|
3183
|
+
break; // tomt svar efter afsendelse: scriptet kan have koert — gentag ikke
|
|
2619
3184
|
} catch (e) {
|
|
2620
3185
|
const m = String(e?.message || e);
|
|
2621
3186
|
if (m.startsWith('__SCRIPT_EX__')) {
|
|
2622
3187
|
throw new Error(m.slice('__SCRIPT_EX__'.length) + ' | scripting-diag: ' + JSON.stringify(diag));
|
|
2623
3188
|
}
|
|
2624
3189
|
dbgErr = m;
|
|
3190
|
+
if (sendt) break; // sendt = maaske koert — se kommentaren ved afsendelsen
|
|
2625
3191
|
if (!/detach|attach|empty|gone|given id|not attached/i.test(m)) break;
|
|
2626
3192
|
}
|
|
2627
3193
|
await debuggerDetach(tab.id).catch(() => {});
|
|
@@ -2629,6 +3195,8 @@ async function dispatch(port, method, params) {
|
|
|
2629
3195
|
}
|
|
2630
3196
|
throw new Error(
|
|
2631
3197
|
'execute_script failed on all paths. debugger: ' + dbgErr +
|
|
3198
|
+
(sendt ? ' | NB: scriptet blev sendt til siden foer det fejlede og KAN allerede have koert. ' +
|
|
3199
|
+
'Det gentages ikke automatisk — kald igen kun hvis det er sikkert at koere to gange.' : '') +
|
|
2632
3200
|
' | raw: ' + JSON.stringify(rawDbg) +
|
|
2633
3201
|
' | scripting-diag: ' + JSON.stringify(diag)
|
|
2634
3202
|
);
|
|
@@ -2659,7 +3227,6 @@ async function dispatch(port, method, params) {
|
|
|
2659
3227
|
// Primary path: debugger mouse events (isTrusted=true, works on React/Angular SPAs)
|
|
2660
3228
|
const clickResult = await debuggerClick(tab.id, el.x, el.y);
|
|
2661
3229
|
return {
|
|
2662
|
-
ok: true,
|
|
2663
3230
|
method: el.method || 'debugger',
|
|
2664
3231
|
tag: el.tag,
|
|
2665
3232
|
text: el.text,
|
|
@@ -2667,6 +3234,15 @@ async function dispatch(port, method, params) {
|
|
|
2667
3234
|
// saa `click` svarede ok:true selv naar siden slet ikke reagerede. Nu foelger den med:
|
|
2668
3235
|
// landed=false betyder "eventet blev sendt, men intet handler tog imod det".
|
|
2669
3236
|
...(clickResult || {}),
|
|
3237
|
+
// MAALT 9/9 (issue #19, fjerde gang samme fejlklasse efter select_option og fill):
|
|
3238
|
+
// `ok: true` stod hardkodet, og `landed` blev spredt ind bagefter. Klikket svarede
|
|
3239
|
+
// altsaa ja og nej i samme aandedrag, og en agent laeser `ok`.
|
|
3240
|
+
// Et element der forsvandt ER en virkning — derfor tæller `detached` som landet.
|
|
3241
|
+
// MAALT 12/9 af Astra (N1): `ok` stod FOER settle-vaerdien her og i click_xy, hvor select_option var immun -
|
|
3242
|
+
// saa et svar fra siden kunne bestemme vaerktoejets egen dom. Ikke naabart i dag, men det er samme klasse som
|
|
3243
|
+
// lige blev lukket for noten, og de tre steder var indbyrdes uens. Vaerktoejets vurdering staar nu sidst.
|
|
3244
|
+
ok: klikLandede(clickResult),
|
|
3245
|
+
...(uvisVurdering(clickResult) || {}),
|
|
2670
3246
|
};
|
|
2671
3247
|
} catch (e) {
|
|
2672
3248
|
// Fallback: synthetic click via chrome.scripting for anti-automation sites
|
|
@@ -2686,9 +3262,46 @@ async function dispatch(port, method, params) {
|
|
|
2686
3262
|
//
|
|
2687
3263
|
// Prisen ved fallbacken er at klikket mister isTrusted=true. Det tjekker de færreste
|
|
2688
3264
|
// sider, og et klik der virker på 95% af nettet slår et klik der aldrig virker.
|
|
2689
|
-
|
|
3265
|
+
// MAALT 10/9 af Astra (anden runde), reproduceret: museknappen var sendt ned og op, CDP meldte
|
|
3266
|
+
// derefter afkobling - og reserveloesningen klikkede EN GANG TIL. To effekter, ok:true. Er
|
|
3267
|
+
// trykket sendt, kan klikket vaere landet; saa klikkes der ikke igen.
|
|
3268
|
+
if (e?.trykSendt) {
|
|
3269
|
+
return {
|
|
3270
|
+
ok: false, error: e.message, maaske_landet: true, method: 'debugger',
|
|
3271
|
+
note: 'Museklikket blev sendt, men debuggeren koblede fra bagefter. Klikket KAN vaere landet, ' +
|
|
3272
|
+
'saa det gentages ikke. Tjek siden foer du klikker igen.',
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
// MAALT 11/9 i Chrome for Testing: en fane i baggrunden faar ikke Input.* - musebevaegelsen udloeber FOER
|
|
3276
|
+
// trykket er sendt. Intet klik kan vaere landet (trykSendt er falsk, se ovenfor), saa script-klikket er sikkert.
|
|
3277
|
+
const inputFristFoerTryk = /svarede ikke inden \d+ ms: Input\./.test(e?.message || '');
|
|
3278
|
+
if (inputFristFoerTryk || /Debugger detached|Debugger attach failed|not attached/i.test(e?.message || '')) {
|
|
3279
|
+
// Ingen "ny adresse = klikket navigerede"-regel her. Astra (efterproevning af c1496d4): en UAFHAENGIG navigation
|
|
3280
|
+
// fjernede rammen foer scriptet koerte, og reglen svarede ok:true med nul handlinger. En afvisning beviser ikke at
|
|
3281
|
+
// scriptet koerte, og en ny adresse beviser ikke at det var klikket.
|
|
2690
3282
|
const r = await scriptingClick(tab.id, params.selector);
|
|
2691
|
-
if (r.ok)
|
|
3283
|
+
if (r.ok && !inputFristFoerTryk) {
|
|
3284
|
+
// Debuggeren var blokeret eller afkoblet: samme svar som 1.29.0. landed vedlaegges kun naar klikket er bevist - Astra
|
|
3285
|
+
// (efterproevning af c826f63): en menu der aabnede paa mousedown, blev ellers meldt som landed:false.
|
|
3286
|
+
return { ok: true, method: 'scripting-fallback', tag: r.tag, ...(klikLandede(r) ? { landed: true } : {}) };
|
|
3287
|
+
}
|
|
3288
|
+
if (r.ok) {
|
|
3289
|
+
// Sign-off 11/9 (Astra og Fable): paa en baggrundsfane svarede reserven ok:true, ogsaa naar siden intet gjorde
|
|
3290
|
+
// (handler der kraever isTrusted). 1.29.0 svarede med en fejl. Nu kraever ok samme bevis som de andre klikveje.
|
|
3291
|
+
// Viste siden ingen reaktion, er klikket alligevel sendt - saa det meldes som "kan vaere landet", ikke gentag blindt.
|
|
3292
|
+
if (klikLandede(r)) {
|
|
3293
|
+
return {
|
|
3294
|
+
ok: true, method: 'scripting-fallback', tag: r.tag, landed: true,
|
|
3295
|
+
note: 'Fanen var i baggrunden, saa musehaendelser naaede ikke frem. Klikket blev udfoert med et script i stedet.',
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
return {
|
|
3299
|
+
ok: false, method: 'scripting-fallback', tag: r.tag, landed: false, maaske_landet: true, error: e.message,
|
|
3300
|
+
note: 'Fanen var i baggrunden, saa musehaendelser naaede ikke frem. Et script-klik blev sendt, men klikket selv ' +
|
|
3301
|
+
'gav ingen synlig virkning: enten kraever siden et aegte klik, eller klikket virkede uden synlig aendring. Tjek siden, ' +
|
|
3302
|
+
'og kald browser_switch_tab og klik igen kun hvis intet skete.',
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
2692
3305
|
}
|
|
2693
3306
|
throw e;
|
|
2694
3307
|
}
|
|
@@ -2714,6 +3327,27 @@ async function dispatch(port, method, params) {
|
|
|
2714
3327
|
await debuggerFill(tab.id, parsed.selector, params.value);
|
|
2715
3328
|
return { ok: true, method: 'debugger' };
|
|
2716
3329
|
} catch (e) {
|
|
3330
|
+
// MAALT 10/9 af Astra: debugger-vejen timede ud, og reserveloesningen skrev saa hele
|
|
3331
|
+
// vaerdien med den native setter. Men Promise.race afbryder ikke — tastetrykkene fra
|
|
3332
|
+
// debugger-forsoeget kan lande BAGEFTER. Reproduceret: "X" blev til "XX", og kaldet
|
|
3333
|
+
// svarede ok:true. To regler: er vaerdien der allerede efter en frist, er vi faerdige;
|
|
3334
|
+
// og efter reserveloesningen laeses feltet igen, saa en fordobling ses i stedet for
|
|
3335
|
+
// at blive meldt som succes.
|
|
3336
|
+
const laesFelt = async () => {
|
|
3337
|
+
const r = await safeExecuteScript(tab.id, (sel) => {
|
|
3338
|
+
const el = document.querySelector(sel);
|
|
3339
|
+
return el && 'value' in el ? el.value : null;
|
|
3340
|
+
}, [parsed.selector]).catch(() => null);
|
|
3341
|
+
return r && !r.cspBlocked ? r.result : null;
|
|
3342
|
+
};
|
|
3343
|
+
const fristUdloeb = /svarede ikke inden/.test(e?.message || '');
|
|
3344
|
+
if (fristUdloeb) await new Promise((r) => setTimeout(r, 400));
|
|
3345
|
+
// MAALT 11/9 af Astra (R5 F1): feltet laeses FOER reserveloesningen skriver. Det er det der skiller en
|
|
3346
|
+
// side der afviste vaerdien (feltet stod stille) fra en side der formaterede den (feltet aendrede sig).
|
|
3347
|
+
const foer = await laesFelt();
|
|
3348
|
+
if (fristUdloeb && foer === params.value) {
|
|
3349
|
+
return { ok: true, method: 'debugger', note: 'landede trods fristen' };
|
|
3350
|
+
}
|
|
2717
3351
|
// Fallback to executeScript if debugger fails
|
|
2718
3352
|
const scriptResult = await safeExecuteScript(tab.id, (sel, val) => {
|
|
2719
3353
|
const el = document.querySelector(sel);
|
|
@@ -2728,8 +3362,59 @@ async function dispatch(port, method, params) {
|
|
|
2728
3362
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2729
3363
|
return { ok: true };
|
|
2730
3364
|
}, [parsed.selector, params.value]);
|
|
2731
|
-
if (
|
|
2732
|
-
|
|
3365
|
+
if (scriptResult.cspBlocked) return { ok: false, error: e.message, method: 'debugger' };
|
|
3366
|
+
if (!scriptResult.result?.ok) return scriptResult.result;
|
|
3367
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3368
|
+
const endelig = await laesFelt();
|
|
3369
|
+
// Kun to sluttilstande er fejl. FORDOBLET: et forsinket Input.insertText landede efter
|
|
3370
|
+
// setteren ("X" -> "XX", reproduceret). TOEMT: et forsinket Cmd+A/Backspace ryddede feltet
|
|
3371
|
+
// igen. Alt andet — "5" der bliver til "5,00 kr", et telefonnummer med mellemrum — er
|
|
3372
|
+
// feltets egen formatering og maa ikke meldes som fejl.
|
|
3373
|
+
const v = String(params.value ?? '');
|
|
3374
|
+
if (typeof endelig === 'string' && endelig !== v) {
|
|
3375
|
+
const fordoblet = endelig === v + v || (v.length >= 3 && endelig.includes(v + v));
|
|
3376
|
+
if (fordoblet || (v && endelig === '')) {
|
|
3377
|
+
return {
|
|
3378
|
+
ok: false, method: 'fallback', error: fordoblet ? 'feltet-fordoblet' : 'feltet-toemt',
|
|
3379
|
+
forventet: v, faktisk: endelig,
|
|
3380
|
+
note: 'Et forsinket tastetryk fra debugger-forsoeget landede efter reserveloesningen.',
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
// Anden runde: "OLD" efter fill("NEW") blev kaldt formatering. Tredje og fjerde runde: hver regel for
|
|
3384
|
+
// "det er bare formatering" havde huller ("5" -> "15", "1.5" -> "15", Unicode-minus, "+45" -> "+1 45").
|
|
3385
|
+
// Femte runde (R5 F1): at melde ENHVER afvigelse som fejl gjorde korrekt formatering ("1.234,50 kr",
|
|
3386
|
+
// "+45 12 34 56 78") til ok:false, hvor 1.29.0 sagde ok. Skellet maales nu i stedet for at gaettes:
|
|
3387
|
+
// stod feltet stille (foer === efter) -> uvist, og det siges: afviger + uaendret (se nedenfor)
|
|
3388
|
+
// aendrede det sig til noget andet -> ok:true, men afviger:true med den faktiske vaerdi
|
|
3389
|
+
// Kalderen faar altsaa aldrig en tavs succes paa en anden vaerdi end den der blev skrevet.
|
|
3390
|
+
// Sign-off 11/9 (Astra og Fable, begge reproduceret): feltet viste allerede "1.234,50 kr", fill("1234.5")
|
|
3391
|
+
// blev formateret tilbage til praecis det samme, og svaret var "siden tog ikke imod vaerdien" (1.29.0: ok).
|
|
3392
|
+
// Foer = efter kan ikke skelne en afvisning fra en vaerdi der allerede stod der i sidens format.
|
|
3393
|
+
// Kun en toemning der ikke skete er entydig: intet format goer "" til noget andet.
|
|
3394
|
+
if (typeof foer === 'string' && endelig === foer) {
|
|
3395
|
+
if (!v) {
|
|
3396
|
+
return {
|
|
3397
|
+
ok: false, method: 'fallback', error: 'feltet-viser-andet', forventet: v, faktisk: endelig,
|
|
3398
|
+
note: 'Feltet skulle toemmes, men viser det samme som foer.',
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
return {
|
|
3402
|
+
ok: true, method: 'fallback', value: endelig, afviger: true, uaendret: true, forventet: v, faktisk: endelig,
|
|
3403
|
+
note: 'Feltet viste det samme foer og efter skrivningen. Enten stod vaerdien der allerede i sidens eget ' +
|
|
3404
|
+
'format, eller siden tog ikke imod den. Tjek `faktisk` foer du gaar videre.',
|
|
3405
|
+
};
|
|
3406
|
+
}
|
|
3407
|
+
return {
|
|
3408
|
+
ok: true, method: 'fallback', value: endelig, afviger: true, forventet: v, faktisk: endelig,
|
|
3409
|
+
...(typeof foer === 'string' ? {} : { foer_ukendt: true }),
|
|
3410
|
+
note: 'Feltet aendrede sig, men viser en anden tekst end den der blev skrevet - fx formatering ' +
|
|
3411
|
+
'("5,00 kr", "+45 12 34 56 78") eller en afkortning. Tjek `faktisk`, hvis den praecise vaerdi betyder noget.',
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
return {
|
|
3415
|
+
ok: true, method: 'fallback', value: endelig ?? v,
|
|
3416
|
+
...(typeof endelig === 'string' ? {} : { verificeret: false }),
|
|
3417
|
+
};
|
|
2733
3418
|
}
|
|
2734
3419
|
}
|
|
2735
3420
|
|
|
@@ -2764,9 +3449,16 @@ async function dispatch(port, method, params) {
|
|
|
2764
3449
|
await new Promise(r => setTimeout(r, 250));
|
|
2765
3450
|
const v = await readBackValue(tab.id, params.selector);
|
|
2766
3451
|
tried.push({ path: 'masked', format: fmt.order.join(fmt.sep), value: v });
|
|
2767
|
-
if (valueLooksLikeIso(v, iso)) return { ok: true, method: 'masked', value: v, format: fmt.order.join(fmt.sep) };
|
|
3452
|
+
if (valueLooksLikeIso(v, iso, fmt)) return { ok: true, method: 'masked', value: v, format: fmt.order.join(fmt.sep) };
|
|
2768
3453
|
} catch (e) {
|
|
2769
|
-
|
|
3454
|
+
// MAALT 10/9 af Astra: en frist her betyder ikke at intet skete. Tastetrykkene kan
|
|
3455
|
+
// allerede staa i feltet, og saa ville kalender-vejen nedenfor saette datoen EN GANG
|
|
3456
|
+
// TIL. Laes feltet foer vi proever noget andet.
|
|
3457
|
+
const v = await readBackValue(tab.id, params.selector).catch(() => null);
|
|
3458
|
+
tried.push({ path: 'masked', error: e.message, value: v });
|
|
3459
|
+
if (valueLooksLikeIso(v, iso, fmt)) {
|
|
3460
|
+
return { ok: true, method: 'masked', value: v, format: fmt.order.join(fmt.sep), note: 'landede trods fejl i afsendelsen' };
|
|
3461
|
+
}
|
|
2770
3462
|
}
|
|
2771
3463
|
} else {
|
|
2772
3464
|
tried.push({
|
|
@@ -2787,11 +3479,13 @@ async function dispatch(port, method, params) {
|
|
|
2787
3479
|
|
|
2788
3480
|
// Path C: calendar-picker navigation
|
|
2789
3481
|
if (!params.skip_picker) {
|
|
3482
|
+
// Fjerde runde (Astra): kalender-grenen glemte feltets format, saa 01/12/2026 blev godkendt som 12. januar.
|
|
3483
|
+
const kendtFormat = parsePlaceholderFormat(info.placeholder) || parsePlaceholderFormat(info.ariaLabel);
|
|
2790
3484
|
const r = await setDatePicker(tab.id, params.selector, iso);
|
|
2791
3485
|
await new Promise(r2 => setTimeout(r2, 200));
|
|
2792
3486
|
const v = await readBackValue(tab.id, params.selector);
|
|
2793
3487
|
tried.push({ path: 'picker', ...r, value: v });
|
|
2794
|
-
if (r.ok && valueLooksLikeIso(v, iso)) return { ok: true, method: 'picker', value: v, navAttempts: r.navAttempts };
|
|
3488
|
+
if (r.ok && valueLooksLikeIso(v, iso, kendtFormat)) return { ok: true, method: 'picker', value: v, navAttempts: r.navAttempts };
|
|
2795
3489
|
}
|
|
2796
3490
|
|
|
2797
3491
|
const visibleErrors = await collectVisibleErrors(tab.id, params.selector);
|
|
@@ -2888,26 +3582,44 @@ async function dispatch(port, method, params) {
|
|
|
2888
3582
|
const vkCode = VK_CODES[key];
|
|
2889
3583
|
const vkParams = vkCode ? { windowsVirtualKeyCode: vkCode, nativeVirtualKeyCode: vkCode } : {};
|
|
2890
3584
|
|
|
3585
|
+
// MAALT 10/9 af Astra: keyDown og keyUp stod i samme try. Timede keyDown ud — og det
|
|
3586
|
+
// kan det, selv naar tasten LANDEDE (Enter der sender en formular) — blev keyUp aldrig
|
|
3587
|
+
// sendt. En tast der kun er trykket ned, er en tast der haenger. Nu sendes keyUp altid,
|
|
3588
|
+
// og svaret siger om nedtrykket fejlede i stedet for at kaste raat.
|
|
2891
3589
|
await debuggerAttach(tab.id);
|
|
3590
|
+
let tastFejl = null;
|
|
2892
3591
|
try {
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3592
|
+
try {
|
|
3593
|
+
await cdpSend(tab.id, 'Input.dispatchKeyEvent', {
|
|
3594
|
+
type: 'keyDown',
|
|
3595
|
+
key,
|
|
3596
|
+
code: params.code || key,
|
|
3597
|
+
modifiers,
|
|
3598
|
+
text: key.length === 1 ? key : '',
|
|
3599
|
+
...vkParams,
|
|
3600
|
+
});
|
|
3601
|
+
} catch (e) { tastFejl = e; }
|
|
3602
|
+
try {
|
|
3603
|
+
await cdpSend(tab.id, 'Input.dispatchKeyEvent', {
|
|
3604
|
+
type: 'keyUp',
|
|
3605
|
+
key,
|
|
3606
|
+
code: params.code || key,
|
|
3607
|
+
modifiers,
|
|
3608
|
+
...vkParams,
|
|
3609
|
+
});
|
|
3610
|
+
} catch (e) { if (!tastFejl) tastFejl = e; }
|
|
2908
3611
|
} finally {
|
|
2909
3612
|
await debuggerDetach(tab.id);
|
|
2910
3613
|
}
|
|
3614
|
+
if (tastFejl) {
|
|
3615
|
+
const frist = /svarede ikke inden/.test(tastFejl.message || '');
|
|
3616
|
+
return {
|
|
3617
|
+
ok: false, key, error: tastFejl.message,
|
|
3618
|
+
...(frist ? { maaske_landet: true,
|
|
3619
|
+
note: 'Chrome kvitterede ikke inden fristen. Tasten kan alligevel have virket ' +
|
|
3620
|
+
'(fx en formular der blev sendt) — tjek siden foer du trykker igen.' } : {}),
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
2911
3623
|
return { ok: true, key };
|
|
2912
3624
|
}
|
|
2913
3625
|
|
|
@@ -2926,6 +3638,14 @@ async function dispatch(port, method, params) {
|
|
|
2926
3638
|
// FB/Twitter/IG only trigger lazy-load on continuous wheel events, not a single large delta.
|
|
2927
3639
|
const dx = params.x || 0;
|
|
2928
3640
|
const dy = params.y || 0;
|
|
3641
|
+
// Hvor stod siden FOER vi roerte den? Uden det tal kan reserveloesningen ikke vide
|
|
3642
|
+
// hvor meget hjulet naaede, og ender med at rulle for langt.
|
|
3643
|
+
// MAALT 10/9: her stod `.catch(() => ({x:0,y:0}))`. Et opdigtet nulpunkt er vaerre end
|
|
3644
|
+
// ingen: stod siden paa 500 og laesningen fejlede, ville reserveloesningen rulle OP.
|
|
3645
|
+
const start = await debuggerEval(tab.id, '({x: window.scrollX, y: window.scrollY})')
|
|
3646
|
+
.catch(() => null);
|
|
3647
|
+
const startKendt = !!start && typeof start.y === 'number';
|
|
3648
|
+
const startX = startKendt ? start.x : 0, startY = startKendt ? start.y : 0;
|
|
2929
3649
|
try {
|
|
2930
3650
|
await debuggerAttach(tab.id);
|
|
2931
3651
|
const STEP_SIZE = 300; // pixels per wheel-event (matches a typical mouse-wheel notch)
|
|
@@ -2943,9 +3663,70 @@ async function dispatch(port, method, params) {
|
|
|
2943
3663
|
// before any subsequent commands run (caller often scrapes immediately after)
|
|
2944
3664
|
await new Promise(r => setTimeout(r, 600));
|
|
2945
3665
|
} catch (e) {
|
|
2946
|
-
//
|
|
2947
|
-
|
|
2948
|
-
|
|
3666
|
+
// MAALT 9/9 af reviewet: her stod `window.scrollBy(dx, dy)` — altsaa "rul det HELE igen".
|
|
3667
|
+
// Promise.race afbryder ikke det kald den opgiver, saa hjultrin der allerede virkede
|
|
3668
|
+
// bliver liggende. Reproduceret: scroll({y:600}), foerste trin flyttede 300 uden at
|
|
3669
|
+
// kvittere, fallbacken lagde 600 oveni = 900 faktisk, 600 rapporteret.
|
|
3670
|
+
// scrollTo mod en beregnet MAAL-position er idempotent: har hjulet allerede rullet
|
|
3671
|
+
// halvdelen, ruller vi kun resten.
|
|
3672
|
+
// MAALT 10/9 af Astra, i MIN egen rettelse fra fire timer foer: her stod
|
|
3673
|
+
// `.catch(() => null)` og derefter `ok: true` ubetinget. Fejlede ogsaa
|
|
3674
|
+
// reserveloesningen, svarede vaerktoejet succes med nul rullede pixels.
|
|
3675
|
+
// Femte gang samme fejlklasse paa én dag — og den her var min.
|
|
3676
|
+
// MAALT 10/9 af Astra (anden runde), reproduceret: kunne starten ikke laeses, blev der
|
|
3677
|
+
// rullet RELATIVT - og hjultrin der allerede var landet blev lagt oveni (500 -> 1400 ved
|
|
3678
|
+
// y:600). Flaget start_ukendt dokumenterede risikoen uden at forhindre den. Uden kendt
|
|
3679
|
+
// start findes ingen rulning der kan gentages uden at rulle dobbelt, saa der rulles ikke.
|
|
3680
|
+
if (!startKendt) {
|
|
3681
|
+
return {
|
|
3682
|
+
ok: false, method: 'fallback', error: 'scroll-uvist', start_ukendt: true, hjul_fejl: e.message,
|
|
3683
|
+
hint: 'Hjul-kaldet svarede ikke, og startpositionen kunne ikke laeses, saa siden KAN have ' +
|
|
3684
|
+
'rullet. Laes window.scrollY med browser_execute_script, og rul derefter det der mangler.',
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
const landede = await debuggerEval(tab.id, `(() => {
|
|
3688
|
+
const foer = { x: window.scrollX, y: window.scrollY };
|
|
3689
|
+
window.scrollTo(${startX} + ${dx}, ${startY} + ${dy});
|
|
3690
|
+
return { foer, efter: { x: window.scrollX, y: window.scrollY } };
|
|
3691
|
+
})()`).catch((fejl) => ({ fejl: fejl?.message || String(fejl) }));
|
|
3692
|
+
|
|
3693
|
+
if (!landede || landede.fejl) {
|
|
3694
|
+
return {
|
|
3695
|
+
ok: false, method: 'fallback', error: 'scroll-mislykkedes',
|
|
3696
|
+
hjul_fejl: e.message,
|
|
3697
|
+
fallback_fejl: landede?.fejl || 'reserveloesningen svarede ikke',
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
// MAALT 11/9 af Astra (e2e-review): med blød rulning (scroll-behavior: smooth) naar siden foerst maalet over de naeste
|
|
3701
|
+
// billeder. Laest i samme oejeblik blev en rulning der lykkedes meldt som "bunden er maaske naaet" (1.29.0: ok).
|
|
3702
|
+
// Positionen laeses igen hvert 100 ms, til maalet er naaet eller siden staar stille - hoejst ca. 1 s.
|
|
3703
|
+
// Astra (efterproevning af c826f63): bundet af et ANTAL forsoeg kom en side med langsomme opslag over serverens 30 s
|
|
3704
|
+
// (1.29.0: svar efter 3 s). Genlaesningen er bundet af tid.
|
|
3705
|
+
const roSlut = Date.now() + 1000;
|
|
3706
|
+
while (Date.now() < roSlut && !(landede.efter.x === startX + dx && landede.efter.y === startY + dy)) {
|
|
3707
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
3708
|
+
const nu = await debuggerEval(tab.id, '({x: window.scrollX, y: window.scrollY})').catch(() => null);
|
|
3709
|
+
if (!nu || typeof nu.y !== 'number') break;
|
|
3710
|
+
const stille = nu.x === landede.efter.x && nu.y === landede.efter.y;
|
|
3711
|
+
landede.efter = nu;
|
|
3712
|
+
if (stille) break;
|
|
3713
|
+
}
|
|
3714
|
+
const flyttede = landede.efter.x !== landede.foer.x || landede.efter.y !== landede.foer.y;
|
|
3715
|
+
const alleredeFremme = !flyttede && startKendt &&
|
|
3716
|
+
landede.efter.x === startX + dx && landede.efter.y === startY + dy;
|
|
3717
|
+
// Astra (efterproevning af cf5b98a): en animation der gik frem, tilbage og foerst naaede maalet efter 1,6 s, fik
|
|
3718
|
+
// svaret ok:false "siden flyttede sig ikke" (1.29.0: ok:true). En rulning der ER sendt, meldes aldrig som fiasko.
|
|
3719
|
+
// Kan bevaegelsen ikke ses inden for ventetiden, er svaret uvist - samme aerlighed som maaske_landet paa klik.
|
|
3720
|
+
return {
|
|
3721
|
+
ok: true,
|
|
3722
|
+
method: 'fallback', fallback_reason: e.message,
|
|
3723
|
+
position: landede.efter, foer: landede.foer,
|
|
3724
|
+
...(flyttede || alleredeFremme ? {} : {
|
|
3725
|
+
uvist: true,
|
|
3726
|
+
note: 'Rulningen blev sendt, men positionen var uaendret da vi svarede: enten er en blød rulning stadig i gang, ' +
|
|
3727
|
+
'eller bunden er naaet. Laes window.scrollY med browser_execute_script hvis den praecise position betyder noget.',
|
|
3728
|
+
}),
|
|
3729
|
+
};
|
|
2949
3730
|
}
|
|
2950
3731
|
return { ok: true, scrolled: { x: dx, y: dy }, method: 'mouseWheel-stepped' };
|
|
2951
3732
|
}
|
|
@@ -2996,8 +3777,30 @@ async function dispatch(port, method, params) {
|
|
|
2996
3777
|
if (typeof params.x !== 'number' || typeof params.y !== 'number') {
|
|
2997
3778
|
return { ok: false, error: 'x and y (numbers, CSS pixels in viewport) are required' };
|
|
2998
3779
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3780
|
+
// MAALT 10/9: debuggerClick maaler om klikket landede, men svaret blev smidt vaek og
|
|
3781
|
+
// `ok: true` stod hardkodet — samme fejl som `click` havde (issue #19). Samme regel her.
|
|
3782
|
+
let klik;
|
|
3783
|
+
try {
|
|
3784
|
+
klik = await debuggerClick(tab.id, params.x, params.y);
|
|
3785
|
+
} catch (e) {
|
|
3786
|
+
// Samme regel som click (Astra, tredje runde): er museknappen sendt, kan klikket vaere landet -
|
|
3787
|
+
// og en kastet fejl mister markeringen over forbindelsen.
|
|
3788
|
+
if (e?.trykSendt) {
|
|
3789
|
+
return {
|
|
3790
|
+
ok: false, error: e.message, maaske_landet: true, clicked_at: { x: params.x, y: params.y },
|
|
3791
|
+
note: 'Museklikket blev sendt, men debuggeren fejlede bagefter. Klikket KAN vaere landet, ' +
|
|
3792
|
+
'saa det gentages ikke. Tjek siden foer du klikker igen.',
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
throw e;
|
|
3796
|
+
}
|
|
3797
|
+
return {
|
|
3798
|
+
clicked_at: { x: params.x, y: params.y },
|
|
3799
|
+
...(klik || {}),
|
|
3800
|
+
// Samme som click: vaerktoejets egen dom og et uvist klik staar sidst, saa siden ikke kan overskrive dem.
|
|
3801
|
+
ok: klikLandede(klik),
|
|
3802
|
+
...(uvisVurdering(klik) || {}),
|
|
3803
|
+
};
|
|
3001
3804
|
}
|
|
3002
3805
|
|
|
3003
3806
|
case 'reattach_debugger': {
|
|
@@ -3052,6 +3855,20 @@ async function dispatch(port, method, params) {
|
|
|
3052
3855
|
// INTET, og svaret sagde stadig at det var lykkedes. Samme fejlklasse som klikket
|
|
3053
3856
|
// der svarede ok:true uden at siden reagerede. Nu laeses svaret, og der laeses
|
|
3054
3857
|
// TILBAGE fra feltet bagefter, saa "valgt" betyder at vaerdien faktisk staar der.
|
|
3858
|
+
// MAALT 8/9 paa forbrugeragenten.dk/penge-tilbage: her laa en anden fejl af samme
|
|
3859
|
+
// familie. Vagten laeste `sel.value` SYNKRONT lige efter dispatch og kaldte enhver
|
|
3860
|
+
// afvigelse "rullet tilbage". Men et React-styret felt der ARBEJDER ser praecis
|
|
3861
|
+
// saadan ud: onChange koerer, komponenten gemmer valget et andet sted og nulstiller
|
|
3862
|
+
// sin egen `value`. Vi maalte altsaa succes som fiasko — og sagde ok:false om et
|
|
3863
|
+
// valg der faktisk landede (chippen "Norlys Energi ×" stod paa siden bagefter).
|
|
3864
|
+
//
|
|
3865
|
+
// Det er den omvendte udgave af issue #19, og rettelsen er den samme som issuet
|
|
3866
|
+
// beder om: maal EFFEKTEN, ikke feltet. Aendrede resten af siden sig, gjorde
|
|
3867
|
+
// komponenten sit arbejde — uanset hvad feltet staar paa nu.
|
|
3868
|
+
const aftryk = `(function(el){
|
|
3869
|
+
return el.options.length + '|' + (el.form ? el.form.innerText.length : document.body.innerText.length);
|
|
3870
|
+
})(document.querySelector(${JSON.stringify(params.selector)}))`;
|
|
3871
|
+
|
|
3055
3872
|
const valg = await debuggerEval(tab.id, `
|
|
3056
3873
|
(function() {
|
|
3057
3874
|
const sel = document.querySelector(${JSON.stringify(params.selector)});
|
|
@@ -3064,20 +3881,44 @@ async function dispatch(port, method, params) {
|
|
|
3064
3881
|
return JSON.stringify({ found: false, error: 'Ingen mulighed matchede: ' + oensket,
|
|
3065
3882
|
available: Array.from(sel.options).map(o => o.text.trim()).slice(0, 25) });
|
|
3066
3883
|
}
|
|
3884
|
+
const foer = ${aftryk};
|
|
3067
3885
|
sel.value = opt.value;
|
|
3068
3886
|
sel.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3069
3887
|
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3070
|
-
return JSON.stringify({ found: true, wanted: opt.value, actual: sel.value, text: opt.text.trim() });
|
|
3888
|
+
return JSON.stringify({ found: true, wanted: opt.value, actual: sel.value, text: opt.text.trim(), foer });
|
|
3071
3889
|
})()
|
|
3072
3890
|
`);
|
|
3073
3891
|
let r; try { r = JSON.parse(valg); } catch { r = null; }
|
|
3074
3892
|
if (!r) return { ok: false, type: 'native_select', error: 'Kunne ikke laese resultatet af valget' };
|
|
3075
3893
|
if (!r.found) return { ok: false, type: 'native_select', error: r.error, available: r.available };
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3894
|
+
|
|
3895
|
+
if (r.actual === r.wanted) return { ok: true, type: 'native_select', selected: r.text, value: r.actual };
|
|
3896
|
+
|
|
3897
|
+
// Feltet holder ikke vaerdien. Giv rammen tid til at gen-rendere, og se saa efter
|
|
3898
|
+
// om NOGET andet aendrede sig. Gjorde det det, blev valget taget imod.
|
|
3899
|
+
await new Promise((res) => setTimeout(res, 150));
|
|
3900
|
+
const efter = await debuggerEval(tab.id, `
|
|
3901
|
+
(function() {
|
|
3902
|
+
const sel = document.querySelector(${JSON.stringify(params.selector)});
|
|
3903
|
+
if (!sel) return JSON.stringify({ vaerdi: null, aftryk: null });
|
|
3904
|
+
return JSON.stringify({ vaerdi: sel.value, aftryk: ${aftryk} });
|
|
3905
|
+
})()
|
|
3906
|
+
`);
|
|
3907
|
+
let e; try { e = JSON.parse(efter); } catch { e = null; }
|
|
3908
|
+
|
|
3909
|
+
if (e && e.vaerdi === r.wanted) {
|
|
3910
|
+
return { ok: true, type: 'native_select', selected: r.text, value: e.vaerdi };
|
|
3911
|
+
}
|
|
3912
|
+
if (e && e.aftryk && r.foer && e.aftryk !== r.foer) {
|
|
3913
|
+
return {
|
|
3914
|
+
ok: true, type: 'native_select', selected: r.text, value: e.vaerdi,
|
|
3915
|
+
note: `Feltet nulstillede sig selv til "${e.vaerdi}", men siden reagerede — ` +
|
|
3916
|
+
'et styret felt der gemmer valget et andet sted. Valget landede.',
|
|
3917
|
+
};
|
|
3079
3918
|
}
|
|
3080
|
-
return { ok:
|
|
3919
|
+
return { ok: false, type: 'native_select',
|
|
3920
|
+
error: `Valget blev rullet tilbage: satte "${r.wanted}", feltet staar paa "${e ? e.vaerdi : r.actual}", ` +
|
|
3921
|
+
'og intet andet paa siden aendrede sig.' };
|
|
3081
3922
|
}
|
|
3082
3923
|
|
|
3083
3924
|
// Custom dropdown (Angular Material, React Select, etc.)
|
|
@@ -3105,12 +3946,15 @@ async function dispatch(port, method, params) {
|
|
|
3105
3946
|
// ordret "it never reports success without the field actually changing".
|
|
3106
3947
|
return {
|
|
3107
3948
|
...(valgKlik || {}),
|
|
3108
|
-
ok: valgKlik
|
|
3949
|
+
ok: klikLandede(valgKlik),
|
|
3109
3950
|
type: 'custom_dropdown',
|
|
3110
3951
|
selected: oensket,
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3952
|
+
// MAALT 12/9 af Astra: her stod fejlteksten paa ALT der ikke var bevist landet - ogsaa naar koden lige havde
|
|
3953
|
+
// regnet ud at den IKKE ved det. En skarp benaegtelse oven paa en uvished er mindre aerlig end ingen tekst.
|
|
3954
|
+
...(uvisVurdering(valgKlik)
|
|
3955
|
+
|| (!klikLandede(valgKlik)
|
|
3956
|
+
? { error: 'Klikket paa muligheden blev ikke taget imod af siden: ' + oensket }
|
|
3957
|
+
: {})),
|
|
3114
3958
|
};
|
|
3115
3959
|
}
|
|
3116
3960
|
|
|
@@ -3166,6 +4010,8 @@ async function dispatch(port, method, params) {
|
|
|
3166
4010
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
3167
4011
|
const urlPattern = params.url_pattern || '';
|
|
3168
4012
|
const timeout = params.timeout || 15000;
|
|
4013
|
+
const netFrister = netvaerkFrister();
|
|
4014
|
+
const budgetSlut = Date.now() + netFrister.budgetMs;
|
|
3169
4015
|
|
|
3170
4016
|
await debuggerAttach(tab.id);
|
|
3171
4017
|
try {
|
|
@@ -3187,10 +4033,18 @@ async function dispatch(port, method, params) {
|
|
|
3187
4033
|
if (!urlPattern || url.includes(urlPattern)) {
|
|
3188
4034
|
chrome.debugger.onEvent.removeListener(listener);
|
|
3189
4035
|
clearTimeout(timer);
|
|
3190
|
-
// Try to get response body
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
4036
|
+
// Try to get response body - kun inden for vaerktoejets budget (netvaerkBudgetMs). Naar den ikke frem, er
|
|
4037
|
+
// svaret body:null som i 1.29.0, i stedet for at serverens 30 s loeber ud.
|
|
4038
|
+
const bodyKald = cdpSend(tab.id, 'Network.getResponseBody', { requestId: eventParams.requestId });
|
|
4039
|
+
bodyKald.catch(() => {});
|
|
4040
|
+
let bodyUr;
|
|
4041
|
+
Promise.race([
|
|
4042
|
+
bodyKald,
|
|
4043
|
+
new Promise((ok) => {
|
|
4044
|
+
const bodyFrist = Math.min(netFrister.bodyMaxMs, Math.max(netFrister.bodyMinMs, budgetSlut - Date.now()));
|
|
4045
|
+
bodyUr = setTimeout(() => ok(null), bodyFrist);
|
|
4046
|
+
}),
|
|
4047
|
+
]).finally(() => clearTimeout(bodyUr)).then(bodyResult => {
|
|
3194
4048
|
resolve({
|
|
3195
4049
|
ok: true,
|
|
3196
4050
|
url,
|
|
@@ -3254,8 +4108,101 @@ async function dispatch(port, method, params) {
|
|
|
3254
4108
|
}
|
|
3255
4109
|
|
|
3256
4110
|
case 'get_cookies': {
|
|
3257
|
-
|
|
3258
|
-
|
|
4111
|
+
// MAALT 10/9, reproduceret i selen: uden `domain` blev filteret {} — altsaa INTET
|
|
4112
|
+
// filter, altsaa hver eneste cookie i profilen, inklusive httpOnly-sessionscookies
|
|
4113
|
+
// som sidens eget JS ikke maa se. Skemaet siger required: ['domain'], men serveren
|
|
4114
|
+
// videresender argumenter uvalideret, saa skemaet var en henstilling.
|
|
4115
|
+
if (!params.domain || typeof params.domain !== 'string' || !params.domain.trim()) {
|
|
4116
|
+
return {
|
|
4117
|
+
ok: false,
|
|
4118
|
+
error: 'domain-mangler',
|
|
4119
|
+
hint: 'Angiv `domain`. Uden det ville kaldet returnere HVER cookie i profilen — ' +
|
|
4120
|
+
'ogsaa fra sider der intet har med opgaven at goere.',
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
// MAALT 10/9, to runder. Foerste udgave gaettede domaeneslaegtskab ud fra fanernes
|
|
4124
|
+
// vaertsnavne ("a.example.com ligger under com"). Astra omgik det tre veje: en fane paa
|
|
4125
|
+
// https://com/ aabnede hele .com, en file://bank.example/-fane gav bankens cookies uden at
|
|
4126
|
+
// nogen side var aabnet, og et tomt vaertsnavn (about:blank) lod "bank.example." slippe
|
|
4127
|
+
// igennem. Et domaene kan man ikke raesonnere sig til uden public suffix-listen. Saa nu
|
|
4128
|
+
// spoerges Chrome i stedet: hvilke cookies ville du SENDE til de http(s)-sider sessionen har
|
|
4129
|
+
// aabne? Kun dem - og kun dem der passer paa det domaene der blev bedt om.
|
|
4130
|
+
const session = getSession(port);
|
|
4131
|
+
// Astra, tredje runde: en inkognito-fane har sit EGET cookie-lager. Uden storeId blev den
|
|
4132
|
+
// almindelige profils cookies laest for en inkognito-fane.
|
|
4133
|
+
let lagre = null;
|
|
4134
|
+
try { lagre = await chrome.cookies.getAllCookieStores(); } catch {}
|
|
4135
|
+
const lagerFor = (tabId) => (lagre || []).find((l) => (l.tabIds || []).includes(tabId))?.id;
|
|
4136
|
+
const sider = [];
|
|
4137
|
+
let ukendtLager = false;
|
|
4138
|
+
for (const id of session.tabIds) {
|
|
4139
|
+
const t = await chrome.tabs.get(id).catch(() => null);
|
|
4140
|
+
try {
|
|
4141
|
+
const u = new URL(t?.url || '');
|
|
4142
|
+
if ((u.protocol === 'https:' || u.protocol === 'http:') && u.hostname) {
|
|
4143
|
+
const storeId = lagerFor(id);
|
|
4144
|
+
// Astra (sign-off 11/9): fejlede getAllCookieStores for en inkognitofane, blev storeId udeladt, og Chrome
|
|
4145
|
+
// laeste den almindelige profils lager. Kan en inkognitofanes lager ikke findes, laeses intet for den.
|
|
4146
|
+
if (t.incognito && !storeId) { ukendtLager = true; continue; }
|
|
4147
|
+
// Fanens vaertsnavn er altid ASCII (punycode). Et afsluttende punktum er en ANDEN cookie-vaert i
|
|
4148
|
+
// Chromium (Astra, fjerde runde: x.example. fik cookies fra x.example) - saa det bevares.
|
|
4149
|
+
sider.push({ u, vaert: u.hostname.toLowerCase(), storeId });
|
|
4150
|
+
}
|
|
4151
|
+
} catch {}
|
|
4152
|
+
}
|
|
4153
|
+
if (!sider.length && ukendtLager) {
|
|
4154
|
+
return {
|
|
4155
|
+
ok: false, error: 'cookie-lager-ukendt',
|
|
4156
|
+
hint: 'Fanen er et inkognitovindue, og Chrome oplyste ikke dens cookie-lager. Intet blev laest - ellers ville ' +
|
|
4157
|
+
'den almindelige profils cookies blive leveret i stedet.',
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
const vaertsnavne = sider.map((x) => x.vaert);
|
|
4161
|
+
// Argumentet normaliseres som fanens adresse - "bücher.example" ER xn--bcher-kva.example.
|
|
4162
|
+
let d = params.domain.trim().toLowerCase().replace(/^\.+/, '');
|
|
4163
|
+
try { if (d) d = new URL('http://' + d + '/').hostname; } catch {}
|
|
4164
|
+
const slaegt = (a, b) => a === b || a.endsWith('.' + b) || b.endsWith('.' + a);
|
|
4165
|
+
if (!d || !vaertsnavne.some((h) => slaegt(h, d))) {
|
|
4166
|
+
return {
|
|
4167
|
+
ok: false, error: 'domaene-ikke-i-sessionen', domain: d, aabne: vaertsnavne,
|
|
4168
|
+
hint: 'Cookies kan kun laeses for http(s)-sider denne session har aabne. Naviger til ' +
|
|
4169
|
+
'siden foerst - saa kan agenten ikke laese cookies fra noget den ikke arbejder med.',
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
// Noeglen er et JSON-array, saa "a|b" i sti og navn ikke kan laegge to cookies sammen til én.
|
|
4173
|
+
const fundne = new Map();
|
|
4174
|
+
const med = (c, storeId) => {
|
|
4175
|
+
const cd = String(c.domain || '').toLowerCase().replace(/^\./, '');
|
|
4176
|
+
if (slaegt(cd, d)) fundne.set(JSON.stringify([storeId ?? '', c.domain, c.path, c.name]), c);
|
|
4177
|
+
};
|
|
4178
|
+
for (const side of sider) {
|
|
4179
|
+
const lager = side.storeId ? { storeId: side.storeId } : {};
|
|
4180
|
+
// Fable (sign-off 11/9): en session paa http:// fik en Secure-cookie fra overdomaenet. {url} udelader dem selv,
|
|
4181
|
+
// men opslagene paa {domain} kender ikke sidens protokol. Chrome sender aldrig en Secure-cookie til http.
|
|
4182
|
+
// Astra (efterproevning af c1496d4): Chromium regner localhost for sikker og sender Secure-cookies dertil over http.
|
|
4183
|
+
const sikkerVaert = side.u.protocol === 'https:' || side.vaert === 'localhost' || side.vaert.endsWith('.localhost') ||
|
|
4184
|
+
/^127(\.\d{1,3}){3}$/.test(side.vaert) || side.vaert === '[::1]';
|
|
4185
|
+
const sendesOverProtokollen = (c) => !c.secure || sikkerVaert;
|
|
4186
|
+
// De cookies Chrome ville SENDE til siden - inklusive overdomaenets ...
|
|
4187
|
+
for (const c of await chrome.cookies.getAll({ url: side.u.href, ...lager })) med(c, side.storeId);
|
|
4188
|
+
// ... plus sidens EGNE cookies paa alle stier (Path=/api kom ikke med ovenfor). {domain} giver
|
|
4189
|
+
// ogsaa underdomaener og, for et public suffix, hele suffixet - saa kun cookies hvis domaene ER
|
|
4190
|
+
// vaertsnavnet.
|
|
4191
|
+
for (const c of await chrome.cookies.getAll({ domain: side.vaert, ...lager })) {
|
|
4192
|
+
if (String(c.domain || '').toLowerCase().replace(/^\./, '') === side.vaert && sendesOverProtokollen(c)) med(c, side.storeId);
|
|
4193
|
+
}
|
|
4194
|
+
// MAALT 11/9 af Astra (R5 F7): overdomaenets cookie paa en anden sti (Domain=.example.com; Path=/api) kom
|
|
4195
|
+
// med i 1.29.0, men ikke her: {url} giver kun sidens egen sti, og {domain: vaert} holdt kun cookies hvis
|
|
4196
|
+
// domaene ER vaertsnavnet. Chrome spoerges nu ogsaa om det domaene der blev bedt om, og kun cookies som
|
|
4197
|
+
// DENNE vaert ville faa tilsendt paa en eller anden sti beholdes. En host-only-cookie sendes kun til sin
|
|
4198
|
+
// egen vaert, saa den skal passe praecist.
|
|
4199
|
+
for (const c of await chrome.cookies.getAll({ domain: d, ...lager })) {
|
|
4200
|
+
const cd = String(c.domain || '').toLowerCase().replace(/^\./, '');
|
|
4201
|
+
const sendesTilVaerten = c.hostOnly ? side.vaert === cd : (side.vaert === cd || side.vaert.endsWith('.' + cd));
|
|
4202
|
+
if (sendesTilVaerten && sendesOverProtokollen(c)) med(c, side.storeId);
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
return { cookies: [...fundne.values()].map(c => ({ name: c.name, value: c.value, domain: c.domain, path: c.path })) };
|
|
3259
4206
|
}
|
|
3260
4207
|
|
|
3261
4208
|
case 'get_local_storage': {
|
|
@@ -3273,12 +4220,64 @@ async function dispatch(port, method, params) {
|
|
|
3273
4220
|
}
|
|
3274
4221
|
|
|
3275
4222
|
case 'set_cookies': {
|
|
4223
|
+
// MAALT 11/9 af Opus og Fable (e2e-review): laesningen er begraenset til sessionens egne sider, men skrivningen satte
|
|
4224
|
+
// cookies paa ETHVERT domaene - ogsaa sider sessionen aldrig har aabnet (og butikkens begrundelse lovede det modsatte).
|
|
4225
|
+
// Samme regel begge veje: kun de http(s)-vaerter sessionen har aabne.
|
|
4226
|
+
// MAALT 11/9 (e2e runde 2): slaegtskabet blev laest BEGGE veje, saa en cookie til et uaabnet UNDERdomaene slap
|
|
4227
|
+
// igennem (Astra), adressen kom fra kalderen i stedet for sessionens side (Opus), og en inkognitofane skrev i den
|
|
4228
|
+
// almindelige profils lager (Opus). Reglen er nu laesningens: cookiens domaene skal vaere vaerten selv eller et
|
|
4229
|
+
// overdomaene vaerten faar cookies fra, adressen bygges af sidens egen oprindelse (saa Chrome haandhaever bl.a.
|
|
4230
|
+
// public suffix), og lageret foelger fanen.
|
|
4231
|
+
const saetSession = getSession(port);
|
|
4232
|
+
let saetUkendtLager = false;
|
|
4233
|
+
let saetLagre = null;
|
|
4234
|
+
try { saetLagre = await chrome.cookies.getAllCookieStores(); } catch {}
|
|
4235
|
+
const saetSider = [];
|
|
4236
|
+
for (const id of saetSession.tabIds) {
|
|
4237
|
+
const t = await chrome.tabs.get(id).catch(() => null);
|
|
4238
|
+
try {
|
|
4239
|
+
const u = new URL(t?.url || '');
|
|
4240
|
+
if ((u.protocol !== 'https:' && u.protocol !== 'http:') || !u.hostname) continue;
|
|
4241
|
+
const storeId = (saetLagre || []).find((l) => (l.tabIds || []).includes(id))?.id;
|
|
4242
|
+
if (t.incognito && !storeId) { saetUkendtLager = true; continue; }
|
|
4243
|
+
saetSider.push({ u, vaert: u.hostname.toLowerCase(), storeId });
|
|
4244
|
+
} catch {}
|
|
4245
|
+
}
|
|
4246
|
+
if (!saetSider.length && saetUkendtLager) {
|
|
4247
|
+
return {
|
|
4248
|
+
ok: false, error: 'cookie-lager-ukendt',
|
|
4249
|
+
hint: 'Fanen er et inkognitovindue, og Chrome oplyste ikke dens cookie-lager. Intet blev skrevet - ellers ville ' +
|
|
4250
|
+
'cookien lande i den almindelige profil i stedet.',
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
4253
|
+
// Sidens vaert faar cookies fra sig selv og fra sine overdomaener - ikke fra et underdomaene den ikke har aabnet.
|
|
4254
|
+
// MAALT 12/9 af Astra: `find` tog den FOERSTE fane der endte paa domaenet, saa en aaben a.example.com vandt over en
|
|
4255
|
+
// lige saa aaben example.com - og adressen blev derefter bygget af den forkerte vaert. Den noejagtige vaert vinder nu.
|
|
4256
|
+
// Slaegtskabs-stien (sidens vaert faar cookies fra sit overdomaene) kraever at kalderen SELV har sagt `domain`:
|
|
4257
|
+
// uden `domain` er cookien host-only, og saa kan oensket ikke opfyldes fra et underdomaene.
|
|
4258
|
+
const saetSideFor = (cd, eksplicitDomaene) =>
|
|
4259
|
+
saetSider.find((s) => s.vaert === cd)
|
|
4260
|
+
|| (eksplicitDomaene ? saetSider.find((s) => s.vaert.endsWith('.' + cd)) : undefined);
|
|
3276
4261
|
const results = [];
|
|
3277
4262
|
const cookieList = Array.isArray(params.cookies) ? params.cookies : [params];
|
|
3278
4263
|
for (const c of cookieList) {
|
|
4264
|
+
let cd = String(c.domain || '').trim().toLowerCase().replace(/^\.+/, '');
|
|
4265
|
+
if (!cd && c.url) { try { cd = new URL(c.url).hostname.toLowerCase(); } catch {} }
|
|
4266
|
+
try { if (cd) cd = new URL('http://' + cd + '/').hostname; } catch {}
|
|
4267
|
+
const side = cd ? saetSideFor(cd, Boolean(String(c.domain || '').trim())) : null;
|
|
4268
|
+
if (!side) {
|
|
4269
|
+
results.push({
|
|
4270
|
+
ok: false, name: c.name, error: 'domaene-ikke-i-sessionen', domain: cd || null,
|
|
4271
|
+
aabne: saetSider.map((s) => s.vaert),
|
|
4272
|
+
hint: 'Cookies kan kun saettes for http(s)-sider denne session har aabne, og kun for sidens eget domaene ' +
|
|
4273
|
+
'eller - hvis du selv angiver domain - et overdomaene den faar cookies fra. Naviger til siden foerst.',
|
|
4274
|
+
});
|
|
4275
|
+
continue;
|
|
4276
|
+
}
|
|
3279
4277
|
try {
|
|
3280
4278
|
const cookie = await chrome.cookies.set({
|
|
3281
|
-
|
|
4279
|
+
// Adressen bygges af sessionens egen side - saa Chrome selv haandhaever sine domaeneregler (public suffix osv.).
|
|
4280
|
+
url: side.u.origin + (typeof c.path === 'string' && c.path.startsWith('/') ? c.path : '/'),
|
|
3282
4281
|
name: c.name,
|
|
3283
4282
|
value: c.value,
|
|
3284
4283
|
domain: c.domain,
|
|
@@ -3286,6 +4285,7 @@ async function dispatch(port, method, params) {
|
|
|
3286
4285
|
secure: c.secure !== false,
|
|
3287
4286
|
httpOnly: c.httpOnly || false,
|
|
3288
4287
|
sameSite: c.sameSite || 'lax',
|
|
4288
|
+
...(side.storeId ? { storeId: side.storeId } : {}),
|
|
3289
4289
|
});
|
|
3290
4290
|
results.push({ ok: true, name: c.name });
|
|
3291
4291
|
} catch (e) {
|
|
@@ -3370,7 +4370,7 @@ async function dispatch(port, method, params) {
|
|
|
3370
4370
|
chrome.notifications.create(notifId, {
|
|
3371
4371
|
type: 'basic',
|
|
3372
4372
|
iconUrl: 'icons/icon-128.png',
|
|
3373
|
-
title: `${session.label}
|
|
4373
|
+
title: `${session.label} - Action Required`,
|
|
3374
4374
|
message: params.message,
|
|
3375
4375
|
requireInteraction: true,
|
|
3376
4376
|
silent: false,
|
|
@@ -3431,7 +4431,7 @@ async function dispatch(port, method, params) {
|
|
|
3431
4431
|
|
|
3432
4432
|
const h = document.createElement('div');
|
|
3433
4433
|
h.style.cssText = 'font-size:14px;font-weight:600;color:#3b82f6;margin-bottom:4px';
|
|
3434
|
-
h.textContent = title || 'Agent360
|
|
4434
|
+
h.textContent = title || 'Agent360 - Action Required';
|
|
3435
4435
|
card.appendChild(h);
|
|
3436
4436
|
const badge = document.createElement('div');
|
|
3437
4437
|
badge.style.cssText = 'font-size:10px;color:#94a3b8;margin-bottom:12px';
|
|
@@ -3485,7 +4485,7 @@ async function dispatch(port, method, params) {
|
|
|
3485
4485
|
});
|
|
3486
4486
|
},
|
|
3487
4487
|
// MAALT 21/8: her stod `params.title` raat. Skemaet siger at title er VALGFRI
|
|
3488
|
-
// med standarden "Agent360
|
|
4488
|
+
// med standarden "Agent360 - Action Required", men udelades den, er vaerdien
|
|
3489
4489
|
// undefined — og chrome.scripting.executeScript afviser hele kaldet med
|
|
3490
4490
|
// "Error at property 'args': Error at index 1: Value is unserializable".
|
|
3491
4491
|
// Altsaa styrtede human-in-the-loop-vaerktoejet hver gang en agent fulgte sit
|
|
@@ -3496,7 +4496,7 @@ async function dispatch(port, method, params) {
|
|
|
3496
4496
|
// anvendes der hvor den er lovet.
|
|
3497
4497
|
args: [
|
|
3498
4498
|
String(params.message ?? ''),
|
|
3499
|
-
String(params.title ?? 'Agent360
|
|
4499
|
+
String(params.title ?? 'Agent360 - Action Required'),
|
|
3500
4500
|
Array.isArray(fields) ? fields : [],
|
|
3501
4501
|
Boolean(hasFields),
|
|
3502
4502
|
Number(timeout) || 120000,
|
|
@@ -3563,7 +4563,23 @@ async function dispatch(port, method, params) {
|
|
|
3563
4563
|
if (!lastCreatedTabId) return { error: 'No new tab detected' };
|
|
3564
4564
|
try {
|
|
3565
4565
|
const tab = await chrome.tabs.get(lastCreatedTabId);
|
|
3566
|
-
//
|
|
4566
|
+
// Kun faner der er aabnet FRA en af sessionens egne faner. Uden det her overtog
|
|
4567
|
+
// agenten enhver fane brugeren selv havde aabnet — se kommentaren ved onCreated.
|
|
4568
|
+
const session = getSession(port);
|
|
4569
|
+
const opener = tab.openerTabId ?? openerForFane.get(tab.id) ?? null;
|
|
4570
|
+
// Aabneren i sessionen NU, eller sessionen der ejede aabneren da fanen blev oprettet
|
|
4571
|
+
// (aabneren kan vaere lukket siden - se ejerForFane ved onCreated).
|
|
4572
|
+
const voresNu = opener != null && session.tabIds.has(opener);
|
|
4573
|
+
const voresDaDenBlevAabnet = opener != null && ejerForFane.get(tab.id) === port;
|
|
4574
|
+
if (!voresNu && !voresDaDenBlevAabnet) {
|
|
4575
|
+
return {
|
|
4576
|
+
error: 'not-ours',
|
|
4577
|
+
hint: 'Den seneste nye fane blev ikke aabnet fra en af dine egne faner, saa den ' +
|
|
4578
|
+
'tilhoerer brugeren. Brug browser_navigate(new_tab: true) hvis du selv skal ' +
|
|
4579
|
+
'have en ny fane.',
|
|
4580
|
+
tab_id: tab.id,
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
3567
4583
|
await addTabToSession(port, tab.id);
|
|
3568
4584
|
return { id: tab.id, url: tab.url, title: tab.title };
|
|
3569
4585
|
} catch {
|
|
@@ -3658,7 +4674,7 @@ async function dispatch(port, method, params) {
|
|
|
3658
4674
|
const { result: nodeResult } = await cdpSend(tab.id, 'Runtime.evaluate', {
|
|
3659
4675
|
expression: `(() => {
|
|
3660
4676
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
3661
|
-
if (!el) return JSON.stringify({ found: false, error: 'File input not found: ${selector}
|
|
4677
|
+
if (!el) return JSON.stringify({ found: false, error: 'File input not found: ' + ${JSON.stringify(selector)} });
|
|
3662
4678
|
return JSON.stringify({ found: true, tag: el.tagName, type: el.type, accept: el.accept, multiple: el.multiple });
|
|
3663
4679
|
})()`,
|
|
3664
4680
|
returnByValue: true,
|
|
@@ -3932,6 +4948,8 @@ chrome.runtime.onStartup.addListener(() => ensureOffscreen().catch(console.error
|
|
|
3932
4948
|
// MAALT 22/8: uden tvangen slog en genindlaesning aldrig igennem til broen, og
|
|
3933
4949
|
// udvikling krævede en fuld genstart af Chrome hver gang.
|
|
3934
4950
|
chrome.runtime.onInstalled.addListener(async () => {
|
|
4951
|
+
// Foerst: aeldre udgaver gemte parametre (adgangskoder, cookie-vaerdier) i historikken.
|
|
4952
|
+
await rensHandlingslog();
|
|
3935
4953
|
try {
|
|
3936
4954
|
if (await chrome.offscreen.hasDocument()) await chrome.offscreen.closeDocument();
|
|
3937
4955
|
} catch (e) {
|