@agent360/browser-mcp 1.25.0 → 1.28.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 +95 -21
- package/bin/cli.js +102 -44
- package/extension/background.js +934 -109
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +2 -2
- package/extension/offscreen.js +142 -10
- package/extension/popup.html +36 -2
- package/extension/popup.js +37 -21
- package/index.js +686 -54
- package/package.json +17 -3
- package/tools.js +95 -8
- package/vagt.js +57 -0
package/extension/background.js
CHANGED
|
@@ -36,12 +36,25 @@ function restoreSessions() {
|
|
|
36
36
|
}
|
|
37
37
|
if (validTabIds.size > 0) {
|
|
38
38
|
const activeTabId = data.activeTabId && validTabIds.has(data.activeTabId) ? data.activeTabId : null;
|
|
39
|
+
// Samme kollisionsfejl som i getSession, og derfor samme rettelse: `size + 1`
|
|
40
|
+
// genbruger et nummer der allerede er i brug. Her betyder det at to gendannede
|
|
41
|
+
// sessioner kan komme op med samme navn efter en genstart af service-workeren.
|
|
42
|
+
const brugte = new Set([...sessions.values()].map((x) => x.nummer).filter((n) => typeof n === 'number'));
|
|
43
|
+
let nummer = typeof data.nummer === 'number' ? data.nummer : 1;
|
|
44
|
+
while (brugte.has(nummer)) nummer++;
|
|
39
45
|
sessions.set(Number(port), {
|
|
40
46
|
tabIds: validTabIds,
|
|
41
47
|
activeTabId,
|
|
42
48
|
groupId: data.groupId || null,
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
nummer,
|
|
50
|
+
// MAALT 22/8: her stod `data.color || …` og `data.label || …`. Bumpede
|
|
51
|
+
// kollisionsloekken nummeret, fulgte navn og farve IKKE med — de blev
|
|
52
|
+
// gendannet ordret fra lageret. To sessioner kunne saa have hvert sit
|
|
53
|
+
// nummer og stadig begge hedde "Claude 1" i samme farve. Og navnet er
|
|
54
|
+
// praecis dét brugeren ser paa fanegruppen.
|
|
55
|
+
color: SESSION_COLORS[(nummer - 1) % SESSION_COLORS.length],
|
|
56
|
+
label: `Claude ${nummer}`,
|
|
57
|
+
pid: typeof data.pid === 'number' ? data.pid : null,
|
|
45
58
|
});
|
|
46
59
|
}
|
|
47
60
|
}
|
|
@@ -49,18 +62,114 @@ function restoreSessions() {
|
|
|
49
62
|
return restorePromise;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
function getSession(port) {
|
|
65
|
+
function getSession(port, pid) {
|
|
53
66
|
if (!sessions.has(port)) {
|
|
54
|
-
|
|
67
|
+
// ── Hvorfor det laveste LEDIGE nummer, og ikke sessions.size + 1 (MAALT 22/8) ──
|
|
68
|
+
// Med `size + 1` genbruges et nummer der allerede er i brug, saa snart en chat
|
|
69
|
+
// lukker: tre chats hedder 1, 2, 3 · chat 1 lukker · size er nu 2 · naeste chat
|
|
70
|
+
// faar "Claude 3" — som chat 3 stadig hedder. To chats deler navn OG farve, og
|
|
71
|
+
// brugeren kan ikke se hvilken fanegruppe der hoerer til hvad.
|
|
72
|
+
//
|
|
73
|
+
// Det er en TREDJE mekanisme bag "alt hedder Claude 1", uafhaengig af de to andre
|
|
74
|
+
// (to udvidelser om samme socket, og adoption uden live-port-gate). Den her
|
|
75
|
+
// rammer ogsaa naar alt andet er rigtigt — man skal bare lukke en chat.
|
|
76
|
+
//
|
|
77
|
+
// Laveste ledige nummer genbruger frigivne pladser uden at kollidere, saa numrene
|
|
78
|
+
// bliver ved med at vaere smaa og laesbare. Farven foelger nummeret, saa to
|
|
79
|
+
// samtidige sessioner heller ikke kan faa samme farve.
|
|
80
|
+
const brugte = new Set([...sessions.values()].map((s) => s.nummer).filter((n) => typeof n === 'number'));
|
|
81
|
+
let nummer = 1;
|
|
82
|
+
while (brugte.has(nummer)) nummer++;
|
|
83
|
+
|
|
55
84
|
sessions.set(port, {
|
|
56
85
|
tabIds: new Set(),
|
|
57
86
|
activeTabId: null,
|
|
58
87
|
groupId: null,
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
nummer,
|
|
89
|
+
color: SESSION_COLORS[(nummer - 1) % SESSION_COLORS.length],
|
|
90
|
+
label: `Claude ${nummer}`,
|
|
91
|
+
pid: typeof pid === 'number' ? pid : null,
|
|
61
92
|
});
|
|
62
93
|
}
|
|
63
|
-
|
|
94
|
+
const s = sessions.get(port);
|
|
95
|
+
// 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;
|
|
97
|
+
return s;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Adopt tabs from a session whose MCP connection is gone (FIX-18: reconnect orphaning).
|
|
101
|
+
//
|
|
102
|
+
// Sessions are keyed on the MCP port. A clean shutdown sends session_disconnect and the
|
|
103
|
+
// tabs are closed. But an UNCLEAN drop (server restart, network blip, laptop sleep) leaves
|
|
104
|
+
// the session in storage while the client reconnects on a NEW port — so getSession() hands
|
|
105
|
+
// back an empty session. Observed damage: navigate() returns tab X while click() fails on
|
|
106
|
+
// tab Y, list_tabs() comes back empty, a fresh about:blank spawns per call, and the session
|
|
107
|
+
// label walks Claude 1 → 2 → 3 → 4. Every navigate→click pair breaks.
|
|
108
|
+
//
|
|
109
|
+
// Fix: before serving an unknown port, hand over the tabs of the largest session whose port
|
|
110
|
+
// is no longer in mcpPorts (the live-connection list the offscreen bridge keeps in storage).
|
|
111
|
+
// Keeps label/color so the user sees continuity instead of a renumbered session.
|
|
112
|
+
async function adoptOrphanedSession(port, pid) {
|
|
113
|
+
// Only ever fires for a session that OWNS NOTHING — a session with tabs is left alone.
|
|
114
|
+
if (sessions.has(port) && sessions.get(port).tabIds.size) return null;
|
|
115
|
+
|
|
116
|
+
// ── Hvorfor pid'en er afgoerende (regression fundet 16/8) ────────────────────
|
|
117
|
+
// Den oprindelige version adopterede den STOERSTE session uanset hvem den tilhoerte,
|
|
118
|
+
// med den begrundelse at Claude Code spreder kald over flere forbindelser. Men en
|
|
119
|
+
// helt NY chat ejer ogsaa ingenting — saa hver ny chat stjal den aktive chats faner
|
|
120
|
+
// OG dens identitet (linje: sessions.delete(best.port)). Donorens port forsvandt fra
|
|
121
|
+
// kortet, saa dens naeste kald adopterede tilbage. To chats byttede den samme ene
|
|
122
|
+
// session frem og tilbage: alt hed "Claude 1", og kun én ting kunne koere ad gangen.
|
|
123
|
+
//
|
|
124
|
+
// Det aegte behov er smallere: naar EN chats MCP-server genstarter, faar den en ny
|
|
125
|
+
// port og skal genfinde sine egne faner. Den situation kan skelnes praecist, fordi
|
|
126
|
+
// begge porte hoerer til den samme Claude Code-proces. Derfor: adoptér kun fra en
|
|
127
|
+
// session med samme pid. Mangler pid'en (gammel server mod ny udvidelse), adopteres
|
|
128
|
+
// slet ikke — hellere en frisk session end en stjaalet.
|
|
129
|
+
if (typeof pid !== 'number') return null;
|
|
130
|
+
|
|
131
|
+
// ── Anden halvdel af gaten (MAALT 21/8) ─────────────────────────────────────
|
|
132
|
+
// Pid'en alene raekker ikke. Foraelder-processen er IKKE en unik identitet: starter
|
|
133
|
+
// en klient flere MCP-servere fra den samme proces, har de alle samme pid — og saa
|
|
134
|
+
// adopterede de hinandens faner paa stribe. Maalt med fire samtidige sessioner:
|
|
135
|
+
// alle fik navnet "Claude 3", de tre aeldste mistede deres fane, og en session
|
|
136
|
+
// kunne skifte til en andens. Altsaa "alt hedder Claude 1", i sin rene form, med
|
|
137
|
+
// kun én udvidelse indlaest.
|
|
138
|
+
//
|
|
139
|
+
// Det manglende tjek staar allerede beskrevet oeverst i denne funktion: adoptér kun
|
|
140
|
+
// fra en session hvis port IKKE laengere er forbundet. Er donorens port stadig i
|
|
141
|
+
// live, er det en anden chat der arbejder lige nu — ikke en genstartet server.
|
|
142
|
+
// Listen vedligeholdes af broen (ws_status → mcpPorts) ved hver til- og frakobling.
|
|
143
|
+
const { mcpPorts = [] } = await chrome.storage.local.get({ mcpPorts: [] });
|
|
144
|
+
const levendePorte = new Set(mcpPorts.map(Number));
|
|
145
|
+
|
|
146
|
+
let best = null;
|
|
147
|
+
for (const [p, session] of sessions) {
|
|
148
|
+
if (p === port || !session.tabIds.size) continue;
|
|
149
|
+
if (session.pid !== pid) continue; // en anden chat — lad den vaere
|
|
150
|
+
if (levendePorte.has(Number(p))) continue; // donoren arbejder stadig — hænderne væk
|
|
151
|
+
if (!best || session.tabIds.size > best.session.tabIds.size) best = { port: p, session };
|
|
152
|
+
}
|
|
153
|
+
if (!best) return null;
|
|
154
|
+
|
|
155
|
+
// Verify at least one tab survives — an orphan whose tabs the user already closed is
|
|
156
|
+
// worthless, and adopting it would mask a genuinely fresh start.
|
|
157
|
+
const alive = new Set();
|
|
158
|
+
for (const tabId of best.session.tabIds) {
|
|
159
|
+
try { await chrome.tabs.get(tabId); alive.add(tabId); } catch {}
|
|
160
|
+
}
|
|
161
|
+
if (!alive.size) {
|
|
162
|
+
sessions.delete(best.port);
|
|
163
|
+
persistSessions();
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
best.session.tabIds = alive;
|
|
168
|
+
if (!alive.has(best.session.activeTabId)) best.session.activeTabId = null;
|
|
169
|
+
sessions.delete(best.port);
|
|
170
|
+
sessions.set(port, best.session);
|
|
171
|
+
persistSessions();
|
|
172
|
+
return best.session;
|
|
64
173
|
}
|
|
65
174
|
|
|
66
175
|
// LRU eviction cap: hver session må højst have N åbne tabs samtidigt.
|
|
@@ -69,7 +178,13 @@ function getSession(port) {
|
|
|
69
178
|
// Begrundelse: Claude Code-flows kan åbne 20+ navigate(new_tab=true) per session
|
|
70
179
|
// over en længere conversation. Uden eviction akkumulerer disse i Chrome som
|
|
71
180
|
// orphan-tabs der spiser RAM + giver "extension localhost 19+" tab-noise.
|
|
72
|
-
|
|
181
|
+
//
|
|
182
|
+
// Hævet 10 → 20 (21/8). Ti var for lavt til reelle flows: en jagt der åbner en
|
|
183
|
+
// fane pr. udbyder ramte loftet midtvejs, og evictionen lukkede de faner arbejdet
|
|
184
|
+
// stadig byggede på — tavst, for eviction rapporterer ikke noget. Tyve matcher
|
|
185
|
+
// portspændet (9876-9895), så en session kan holde lige så mange faner som der
|
|
186
|
+
// kan køre samtidige sessioner.
|
|
187
|
+
const MAX_TABS_PER_SESSION = 20;
|
|
73
188
|
|
|
74
189
|
async function evictOldestTabs(session, justAddedTabId) {
|
|
75
190
|
// Drop dead tab-ids først (user manually closed dem)
|
|
@@ -154,6 +269,8 @@ function persistSessions() {
|
|
|
154
269
|
groupId: session.groupId,
|
|
155
270
|
color: session.color,
|
|
156
271
|
label: session.label,
|
|
272
|
+
nummer: session.nummer ?? null, // uden denne mister en gendannet session sin plads og kan kollidere
|
|
273
|
+
pid: session.pid ?? null, // uden denne adopterer en genstartet service worker paa tvaers af chats igen
|
|
157
274
|
};
|
|
158
275
|
}
|
|
159
276
|
chrome.storage.local.set({ sessions: data });
|
|
@@ -384,12 +501,49 @@ async function cdpSend(tabId, method, params = {}) {
|
|
|
384
501
|
}
|
|
385
502
|
|
|
386
503
|
// Clean up debugger + session refs when tabs close
|
|
504
|
+
// Faner agenten selv lukkede via close_tab. En tom session betyder kun "arbejdet er slut"
|
|
505
|
+
// hvis det var BRUGEREN der lukkede den sidste fane.
|
|
506
|
+
const agentLukkedeFaner = new Set();
|
|
507
|
+
|
|
508
|
+
// ── Armerede dialog-haandterere, pr. fane ──────────────────────────────────────
|
|
509
|
+
// MAALT 22/8 af flowtesten: handle_dialog var ubrugelig som den var skrevet. Den
|
|
510
|
+
// BLOKEREDE i op til 10 sekunder mens den ventede paa en dialog — men en alert(),
|
|
511
|
+
// confirm() eller prompt() dukker foerst op naar man klikker paa noget, og klikket
|
|
512
|
+
// kan ikke ske mens kaldet blokerer. Man kunne altsaa hverken arme den foerst eller
|
|
513
|
+
// kalde den bagefter: naar dialogen foerst staar der, er hele fanen laast, og
|
|
514
|
+
// klik-vaerktoejet naar ikke frem. Vaerktoejet kunne kun lykkes hvis en ANDEN aabnede
|
|
515
|
+
// dialogen paa praecis det rigtige tidspunkt.
|
|
516
|
+
//
|
|
517
|
+
// Nu armerer den og vender tilbage med det samme. Lytteren bliver siddende og tager
|
|
518
|
+
// den naeste dialog paa fanen. `wait: true` giver den gamle blokerende adfaerd for
|
|
519
|
+
// de tilfaelde hvor dialogen allerede er undervejs.
|
|
520
|
+
const armeredeDialoger = new Map();
|
|
521
|
+
// Loeftet for den senest armerede dialog pr. fane. Ligger UDEN for armeredeDialoger,
|
|
522
|
+
// fordi lytteren afvaebner i samme oejeblik dialogen aabner — og klikket skal kunne
|
|
523
|
+
// vente paa svaret BAGEFTER.
|
|
524
|
+
const dialogLoefter = new Map(); // tabId -> Promise // tabId → { listener, timer, action }
|
|
525
|
+
|
|
526
|
+
function afvaebnDialog(tabId, grund) {
|
|
527
|
+
const a = armeredeDialoger.get(tabId);
|
|
528
|
+
if (!a) return;
|
|
529
|
+
try { chrome.debugger.onEvent.removeListener(a.listener); } catch {}
|
|
530
|
+
clearTimeout(a.timer);
|
|
531
|
+
armeredeDialoger.delete(tabId);
|
|
532
|
+
// MAALT 22/8: her stoppede funktionen. Loeftet blev ALDRIG opfyldt, saa en kalder
|
|
533
|
+
// med `wait: true` haengte for evigt ad to helt almindelige veje — fanen blev
|
|
534
|
+
// lukket, eller et andet handle_dialog armerede paa samme fane. Ingen fejl, intet
|
|
535
|
+
// svar, bare stilhed. Nu faar kalderen altid et svar.
|
|
536
|
+
if (grund && a.opfyld) a.opfyld({ ok: false, error: grund });
|
|
537
|
+
}
|
|
538
|
+
|
|
387
539
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
540
|
+
afvaebnDialog(tabId, 'fanen blev lukket foer der kom en dialog');
|
|
541
|
+
const lukketAfAgenten = agentLukkedeFaner.delete(tabId);
|
|
388
542
|
debuggerAttached.delete(tabId);
|
|
389
543
|
for (const [port, session] of sessions) {
|
|
390
544
|
if (!session.tabIds.has(tabId)) continue;
|
|
391
545
|
session.tabIds.delete(tabId);
|
|
392
|
-
if (session.tabIds.size === 0) {
|
|
546
|
+
if (session.tabIds.size === 0 && !lukketAfAgenten) {
|
|
393
547
|
// Last tab closed — tell offscreen to terminate the MCP server.
|
|
394
548
|
// Resulting WS-close triggers the existing session_disconnect → releaseSession path.
|
|
395
549
|
chrome.runtime.sendMessage({ type: 'terminate_mcp_session', port }).catch(() => {});
|
|
@@ -460,6 +614,51 @@ async function debuggerType(tabId, text) {
|
|
|
460
614
|
}
|
|
461
615
|
}
|
|
462
616
|
|
|
617
|
+
// ── Naar en dialog kan aabne midt i klikket (MAALT 24/8) ─────────────────────
|
|
618
|
+
//
|
|
619
|
+
// Et confirm() fryser rendereren, og Chrome svarer ALDRIG paa den CDP-kommando der
|
|
620
|
+
// udloeste det. Maalt: browser_click haengte i 25,9 sekunder og gav aldrig svar —
|
|
621
|
+
// mens dialogen faktisk BLEV besvaret af den armerede haandtering (siden rapporterede
|
|
622
|
+
// window.__svar === true). Klikket var altsaa leveret; kun svaret var vaek.
|
|
623
|
+
//
|
|
624
|
+
// Er der armeret en dialog paa fanen, venter vi derfor kun kort. Kommer der intet
|
|
625
|
+
// svar, ER klikket leveret — det er jo netop dét der aabnede dialogen.
|
|
626
|
+
async function dispatchTaalmodigt(tabId, params) {
|
|
627
|
+
if (!armeredeDialoger.has(tabId)) {
|
|
628
|
+
return cdpSend(tabId, 'Input.dispatchMouseEvent', params);
|
|
629
|
+
}
|
|
630
|
+
let faerdig = false;
|
|
631
|
+
const kald = cdpSend(tabId, 'Input.dispatchMouseEvent', params)
|
|
632
|
+
.then((r) => { faerdig = true; return r; })
|
|
633
|
+
.catch(() => { faerdig = true; });
|
|
634
|
+
await Promise.race([kald, new Promise((r) => setTimeout(r, 1200))]);
|
|
635
|
+
return faerdig ? kald : { __dialogBlokerede: true };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// MAALT 28/8: `dispatchTaalmodigt` ovenfor lukkede deadlocken paa museklikket, men
|
|
639
|
+
// settle-opslaget i step 3 er OGSAA et renderer-kald — og maalingen viste at det er
|
|
640
|
+
// PRAECIS der klikket haenger (HAENGER@step3-settle). Deadlocken var kun lukket paa
|
|
641
|
+
// hovedstien.
|
|
642
|
+
//
|
|
643
|
+
// Foerste forsoeg gjorde kuren betinget af at vi kunne SE en dialog (armeret, eller en
|
|
644
|
+
// dispatch der blokerede). Den betingelse holder ikke: dispatchen kan naa at blive
|
|
645
|
+
// kvitteret, og lytteren kan naa at afvaebne, FOER rendereren gaar i staa. Saa faldt vi
|
|
646
|
+
// tilbage i det ubeskyttede kald og hang alligevel — maalt.
|
|
647
|
+
//
|
|
648
|
+
// Derfor er fristen nu betingelsesloes. Opslaget er et par linjers synkron JS: svarer
|
|
649
|
+
// rendereren, er den tilbage paa millisekunder. Bruger den over tre sekunder, er den
|
|
650
|
+
// blokeret — ikke langsom. Saa svarer vi aerligt i stedet for at vente i 30.
|
|
651
|
+
// Et klik der aabnede en dialog ER landet, saa framework-fallbacken skal ikke fyre oveni.
|
|
652
|
+
async function evaluerTaalmodigt(tabId, params, ms = 3000) {
|
|
653
|
+
let faerdig = false;
|
|
654
|
+
const kald = cdpSend(tabId, 'Runtime.evaluate', params)
|
|
655
|
+
.then((r) => { faerdig = true; return r; })
|
|
656
|
+
.catch(() => { faerdig = true; });
|
|
657
|
+
await Promise.race([kald, new Promise((r) => setTimeout(r, ms))]);
|
|
658
|
+
if (faerdig) return kald;
|
|
659
|
+
return { result: { value: { landed: true, fallbackFired: false, rendererSvarede: false } } };
|
|
660
|
+
}
|
|
661
|
+
|
|
463
662
|
async function debuggerClick(tabId, x, y) {
|
|
464
663
|
await debuggerAttach(tabId);
|
|
465
664
|
try {
|
|
@@ -497,7 +696,7 @@ async function debuggerClick(tabId, x, y) {
|
|
|
497
696
|
})()`,
|
|
498
697
|
});
|
|
499
698
|
// 1. mouseMoved first (triggers hover state, required by some frameworks)
|
|
500
|
-
await
|
|
699
|
+
await dispatchTaalmodigt(tabId, {
|
|
501
700
|
type: 'mouseMoved', x, y,
|
|
502
701
|
});
|
|
503
702
|
await new Promise(r => setTimeout(r, 30));
|
|
@@ -505,11 +704,11 @@ async function debuggerClick(tabId, x, y) {
|
|
|
505
704
|
// 0 on release) plus a small press→release gap are REQUIRED for Chrome to
|
|
506
705
|
// synthesize a *trusted* 'click' from the pair. Without them, web-components
|
|
507
706
|
// that gate on the trusted click event (Google Ads, Material Web) never fire.
|
|
508
|
-
await
|
|
707
|
+
await dispatchTaalmodigt(tabId, {
|
|
509
708
|
type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 1,
|
|
510
709
|
});
|
|
511
710
|
await new Promise(r => setTimeout(r, 30));
|
|
512
|
-
await
|
|
711
|
+
await dispatchTaalmodigt(tabId, {
|
|
513
712
|
type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 1,
|
|
514
713
|
});
|
|
515
714
|
// 3. Framework fallback — only if the captured target is STILL connected (i.e.
|
|
@@ -517,14 +716,15 @@ async function debuggerClick(tabId, x, y) {
|
|
|
517
716
|
// SPA re-renders (Google Ads) detach the element first. Fires a full pointer
|
|
518
717
|
// + mouse sequence on the shadow-pierced target, then React/Angular handlers.
|
|
519
718
|
await new Promise(r => setTimeout(r, 120));
|
|
520
|
-
await
|
|
719
|
+
const settle = await evaluerTaalmodigt(tabId, {
|
|
720
|
+
returnByValue: true,
|
|
521
721
|
expression: `(() => {
|
|
522
722
|
const el = window.__bmcpClickTarget;
|
|
523
723
|
const landed = window.__bmcpClicked === true;
|
|
524
724
|
try { window.__bmcpClickListener && document.removeEventListener('click', window.__bmcpClickListener, true); } catch (e) {}
|
|
525
725
|
try { delete window.__bmcpClickTarget; delete window.__bmcpClicked; delete window.__bmcpClickListener; } catch (e) {}
|
|
526
|
-
if (landed) return;
|
|
527
|
-
if (!el || !el.isConnected) return; // already navigated/handled — don't double-fire
|
|
726
|
+
if (landed) return { landed: true, fallbackFired: false }; // FIX-13: trusted click already landed — do NOT double-fire
|
|
727
|
+
if (!el || !el.isConnected) return { landed: false, fallbackFired: false, detached: true }; // already navigated/handled — don't double-fire
|
|
528
728
|
const opts = { bubbles: true, cancelable: true, composed: true, view: window, clientX: ${x}, clientY: ${y} };
|
|
529
729
|
try { el.dispatchEvent(new PointerEvent('pointerdown', opts)); } catch (e) {}
|
|
530
730
|
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
@@ -549,8 +749,24 @@ async function debuggerClick(tabId, x, y) {
|
|
|
549
749
|
const matRipple = el.closest && el.closest('[mat-button], [mat-raised-button], [mat-icon-button], [mat-fab], mat-checkbox, mat-slide-toggle, mat-radio-button');
|
|
550
750
|
if (matRipple) matRipple.dispatchEvent(new MouseEvent('click', opts));
|
|
551
751
|
}
|
|
752
|
+
return { landed: false, fallbackFired: true };
|
|
552
753
|
})()`,
|
|
553
754
|
});
|
|
755
|
+
const vaerdi = settle?.result?.value ?? null;
|
|
756
|
+
|
|
757
|
+
// MAALT 28/8: uden det her svarede klikket 5,5 sek FOER dialogen var besvaret, saa
|
|
758
|
+
// den NAESTE kommando ramte en stadig frossen side og ventede 15 sek forgaeves.
|
|
759
|
+
// Svarede rendereren ikke, ER der en dialog i vejen — saa vent til den er ude af
|
|
760
|
+
// verden, foer vi melder klikket faerdigt. Sidebonus: kalderen faar en side der
|
|
761
|
+
// rent faktisk er klar til naeste skridt.
|
|
762
|
+
if (vaerdi && vaerdi.rendererSvarede === false) {
|
|
763
|
+
const loefte = dialogLoefter.get(tabId);
|
|
764
|
+
if (loefte) {
|
|
765
|
+
await Promise.race([loefte, new Promise((r) => setTimeout(r, 8000))]);
|
|
766
|
+
dialogLoefter.delete(tabId);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return vaerdi;
|
|
554
770
|
} finally {
|
|
555
771
|
await debuggerDetach(tabId);
|
|
556
772
|
}
|
|
@@ -829,6 +1045,13 @@ async function resolveElement(tabId, selectorStr) {
|
|
|
829
1045
|
if (!el) return null;
|
|
830
1046
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
831
1047
|
const r = el.getBoundingClientRect();
|
|
1048
|
+
// MAALT 21/8: et skjult element har rect 0x0 ved (0,0), saa midtpunktet blev (0,0)
|
|
1049
|
+
// og debuggerClick sendte et AEGTE museklik i sidens oeverste venstre hjoerne —
|
|
1050
|
+
// paa hvad der nu laa der (logo, menu, link) — og svarede ok:true. Det er ikke en
|
|
1051
|
+
// rapporteringsfejl men en handlingsfejl: vi klikker et andet sted end der blev bedt om.
|
|
1052
|
+
if (r.width <= 0 || r.height <= 0) {
|
|
1053
|
+
return { found: false, hidden: true, tag: el.tagName, rect: { w: r.width, h: r.height } };
|
|
1054
|
+
}
|
|
832
1055
|
return { x: r.x + r.width / 2, y: r.y + r.height / 2, tag: el.tagName, found: true };
|
|
833
1056
|
};
|
|
834
1057
|
|
|
@@ -850,6 +1073,9 @@ async function resolveElement(tabId, selectorStr) {
|
|
|
850
1073
|
if (!el) return null;
|
|
851
1074
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
852
1075
|
const r = el.getBoundingClientRect();
|
|
1076
|
+
if (r.width <= 0 || r.height <= 0) {
|
|
1077
|
+
return { found: false, hidden: true, tag: el.tagName, rect: { w: r.width, h: r.height } };
|
|
1078
|
+
}
|
|
853
1079
|
return { x: r.x + r.width/2, y: r.y + r.height/2, tag: el.tagName, found: true };
|
|
854
1080
|
})()
|
|
855
1081
|
`);
|
|
@@ -866,6 +1092,9 @@ async function resolveElement(tabId, selectorStr) {
|
|
|
866
1092
|
if (!el) return null;
|
|
867
1093
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
868
1094
|
const r = el.getBoundingClientRect();
|
|
1095
|
+
if (r.width <= 0 || r.height <= 0) {
|
|
1096
|
+
return { found: false, hidden: true, tag: el.tagName, text: el.textContent?.trim().slice(0, 80), rect: { w: r.width, h: r.height } };
|
|
1097
|
+
}
|
|
869
1098
|
return { x: r.x + r.width/2, y: r.y + r.height/2, tag: el.tagName, text: el.textContent?.trim().slice(0, 80), found: true };
|
|
870
1099
|
})()
|
|
871
1100
|
`);
|
|
@@ -874,15 +1103,121 @@ async function resolveElement(tabId, selectorStr) {
|
|
|
874
1103
|
|
|
875
1104
|
// ── Offscreen Document Setup ───────────────────────────────────────────────
|
|
876
1105
|
|
|
1106
|
+
// MAALT 21/8: her stod kun `if (!await chrome.offscreen.hasDocument()) createDocument()`.
|
|
1107
|
+
// Den spurgte om dokumentet FANDTES — aldrig om det SVAREDE. Et dokument hvis script
|
|
1108
|
+
// aldrig blev indlaest (en CSP-afvisning raekker) taeller stadig som eksisterende, saa
|
|
1109
|
+
// hjerteslaget hvert minut gjorde ingenting, for evigt. Udvidelsen saa levende ud i
|
|
1110
|
+
// chrome://extensions, men havde ingen WebSocket og kunne ikke naas af noget — heller
|
|
1111
|
+
// ikke af reload_extension, som netop kraever den forbindelse den mangler. Eneste vej
|
|
1112
|
+
// ud var ↻ i haanden.
|
|
1113
|
+
//
|
|
1114
|
+
// Nu spoerges dokumentet om det er der. Svarer det ikke, erstattes det.
|
|
1115
|
+
async function offscreenSvarer() {
|
|
1116
|
+
try {
|
|
1117
|
+
const svar = await Promise.race([
|
|
1118
|
+
chrome.runtime.sendMessage({ type: 'bmcp_ping' }),
|
|
1119
|
+
new Promise((_, afvis) => setTimeout(() => afvis(new Error('intet svar')), 1500)),
|
|
1120
|
+
]);
|
|
1121
|
+
if (svar?.ok !== true) return false;
|
|
1122
|
+
// "Svarer den?" er ikke nok — den skal ogsaa vaere den udgave vi koerer nu.
|
|
1123
|
+
// En bro fra en aeldre udgave svarer lige saa villigt, og saa blev den aldrig
|
|
1124
|
+
// udskiftet. Oplyser den ingen version, er den fra foer 1.27.1 og altsaa gammel.
|
|
1125
|
+
let vores = null;
|
|
1126
|
+
try { vores = chrome.runtime.getManifest().version; } catch {}
|
|
1127
|
+
if (!vores) return true; // kan vi ikke sammenligne, saa lad den vaere
|
|
1128
|
+
return svar.version === vores;
|
|
1129
|
+
} catch {
|
|
1130
|
+
return false; // ingen modtager, eller den svarede ikke i tide
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Genskabelsen er BEGRAENSET, og det er ikke pynt.
|
|
1135
|
+
//
|
|
1136
|
+
// "Svarer ikke" betyder ikke altid "doed". En AELDRE offscreen.js — fra foer
|
|
1137
|
+
// ping-lytteren fandtes — svarer heller ikke, og Chrome kan servere den fra cache
|
|
1138
|
+
// hen over en genindlaesning (maalt 21/8). Uden en graense ville hjerteslaget saa
|
|
1139
|
+
// lukke og genskabe en fuldt fungerende bro hvert minut, for evigt, og rive
|
|
1140
|
+
// WebSocket-forbindelsen ned hver gang. Kuren ville vaere vaerre end sygdommen.
|
|
1141
|
+
//
|
|
1142
|
+
// Derfor: hoejst tre forsoeg i traek. Er dokumentet aegte doedt, er ét nok. Er det
|
|
1143
|
+
// bare gammelt, koster det tre korte afbrydelser og saa faar det fred et stykke tid.
|
|
1144
|
+
//
|
|
1145
|
+
// MAALT 22/8 — og det var en fejl i denne blok: graensen var PERMANENT. Naaede
|
|
1146
|
+
// taelleren tre, blev der aldrig forsoegt igen, og taelleren nulstilles kun naar en
|
|
1147
|
+
// ping lykkes — hvilket en doed bro pr. definition aldrig goer. Resultatet var en
|
|
1148
|
+
// doed bro der laa doed for evigt, hvor symptomet for brugeren er "browser-mcp
|
|
1149
|
+
// virker ikke", og hvor den eneste udvej var at genindlaese udvidelsen i haanden.
|
|
1150
|
+
// Praecis den tilstand vaernet skulle forhindre.
|
|
1151
|
+
//
|
|
1152
|
+
// Rettelsen: graensen er nu tidsbestemt, ikke endelig. Efter tre forsoeg holder vi
|
|
1153
|
+
// pause — og naar pausen er ovre, proever vi igen. En gammel-men-fungerende bro
|
|
1154
|
+
// faar altsaa ro i pausen i stedet for at blive revet ned hvert minut, og en aegte
|
|
1155
|
+
// doed bro er hoejst én pause fra at blive erstattet. Begge hensyn er i behold.
|
|
1156
|
+
const MAX_OFFSCREEN_GENSKAB = 3;
|
|
1157
|
+
const OFFSCREEN_PAUSE_MS = 10 * 60 * 1000;
|
|
1158
|
+
|
|
877
1159
|
async function ensureOffscreen() {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
1160
|
+
const findes = await chrome.offscreen.hasDocument();
|
|
1161
|
+
|
|
1162
|
+
if (findes) {
|
|
1163
|
+
if (await offscreenSvarer()) {
|
|
1164
|
+
await chrome.storage.local.set({ offscreenGenskabt: 0 }); // levende — nulstil
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
// Taelleren skal ligge i storage, ikke i en modul-variabel: service-workeren
|
|
1168
|
+
// genstartes hele tiden, og en variabel ville nulstilles ved hver genstart —
|
|
1169
|
+
// altsaa ingen graense i praksis.
|
|
1170
|
+
let { offscreenGenskabt = 0 } =
|
|
1171
|
+
await chrome.storage.local.get({ offscreenGenskabt: 0, offscreenPauseTil: 0 });
|
|
1172
|
+
const { offscreenPauseTil = 0 } =
|
|
1173
|
+
await chrome.storage.local.get({ offscreenPauseTil: 0 });
|
|
1174
|
+
|
|
1175
|
+
const nu = Date.now();
|
|
1176
|
+
if (offscreenPauseTil > nu) return; // midt i pausen — lad broen vaere
|
|
1177
|
+
|
|
1178
|
+
if (offscreenPauseTil > 0) {
|
|
1179
|
+
// Pausen er udloebet. Taelleren SKAL nulstilles her, foer graensen tjekkes —
|
|
1180
|
+
// ellers rammer vi graensen igen med det samme, saetter endnu en pause, og
|
|
1181
|
+
// graensen er i praksis permanent alligevel, bare med et ekstra skridt.
|
|
1182
|
+
// (Maalt 22/8: det var praecis den fejl den foerste udgave af rettelsen havde.
|
|
1183
|
+
// Testen "naar pausen er ovre, proeves der igen" fangede den.)
|
|
1184
|
+
offscreenGenskabt = 0;
|
|
1185
|
+
await chrome.storage.local.set({ offscreenGenskabt: 0, offscreenPauseTil: 0 });
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (offscreenGenskabt >= MAX_OFFSCREEN_GENSKAB) {
|
|
1189
|
+
// Pause i stedet for at give op. Taelleren nulstilles samtidig, saa naeste
|
|
1190
|
+
// runde faar sine egne tre forsoeg — ellers ville graensen vaere permanent
|
|
1191
|
+
// alligevel, bare med et ekstra skridt.
|
|
1192
|
+
console.warn(`[BG] offscreen-dokumentet svarer stadig ikke efter ${MAX_OFFSCREEN_GENSKAB} ` +
|
|
1193
|
+
`forsoeg — holder pause i ${OFFSCREEN_PAUSE_MS / 60000} min og proever saa igen. ` +
|
|
1194
|
+
'Haster det: chrome://extensions → slaa udvidelsen fra og til.');
|
|
1195
|
+
await chrome.storage.local.set({ offscreenGenskabt: 0, offscreenPauseTil: nu + OFFSCREEN_PAUSE_MS });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
console.warn(`[BG] offscreen-dokumentet svarer ikke — erstatter det (forsoeg ${offscreenGenskabt + 1}/${MAX_OFFSCREEN_GENSKAB})`);
|
|
1199
|
+
await chrome.storage.local.set({ offscreenGenskabt: offscreenGenskabt + 1 });
|
|
1200
|
+
try { await chrome.offscreen.closeDocument(); } catch (e) {
|
|
1201
|
+
console.warn('[BG] kunne ikke lukke det doede dokument:', e?.message || e);
|
|
1202
|
+
return; // proev igen ved naeste hjerteslag
|
|
1203
|
+
}
|
|
885
1204
|
}
|
|
1205
|
+
|
|
1206
|
+
// Versionen foelger med i URL'en. Et offscreen-dokument har IKKE
|
|
1207
|
+
// chrome.runtime.getManifest() — kaldet kaster "is not a function", saa dokumentet
|
|
1208
|
+
// kunne aldrig oplyse sin version, `offscreenSvarer()` sammenlignede null mod vores
|
|
1209
|
+
// og fik altid falsk, og broen blev revet ned tre gange hvert tiende minut for evigt.
|
|
1210
|
+
// (Symptomet var "Offscreen document closed before fully loading" — nedrivningen
|
|
1211
|
+
// ramte dokumentet mens det stadig startede op.)
|
|
1212
|
+
// URL'en er baaret af dokumentet selv: et dokument oprettet af en aeldre udgave
|
|
1213
|
+
// baerer den aeldre version, hvilket er praecis den skelnen tjekket skal bruge.
|
|
1214
|
+
let minVersion = '';
|
|
1215
|
+
try { minVersion = chrome.runtime.getManifest().version; } catch {}
|
|
1216
|
+
await chrome.offscreen.createDocument({
|
|
1217
|
+
url: 'offscreen.html' + (minVersion ? '?v=' + encodeURIComponent(minVersion) : ''),
|
|
1218
|
+
reasons: ['WORKERS'],
|
|
1219
|
+
justification: 'Maintain persistent WebSocket connection to local MCP server',
|
|
1220
|
+
});
|
|
886
1221
|
}
|
|
887
1222
|
|
|
888
1223
|
// ── Action Logging ─────────────────────────────────────────────────────────
|
|
@@ -913,12 +1248,19 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
913
1248
|
if (msg.type === 'mcp_command') {
|
|
914
1249
|
const port = msg.port;
|
|
915
1250
|
logAction(port, msg.method, msg.params);
|
|
916
|
-
// Restore sessions from storage (service worker may have restarted)
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1251
|
+
// Restore sessions from storage (service worker may have restarted), then take over any
|
|
1252
|
+
// session orphaned by an unclean disconnect so navigate→click keeps hitting the same tab.
|
|
1253
|
+
restoreSessions()
|
|
1254
|
+
.then(() => adoptOrphanedSession(port, msg.pid).catch(() => null)) // adoption is best-effort, never fatal
|
|
1255
|
+
.then(() => {
|
|
1256
|
+
// Stempl pid'en ÉT sted, foer dispatch. De fem nedstroems getSession(port)-kald
|
|
1257
|
+
// arver den derfra, saa ingen af dem behoever at kende til pid-begrebet.
|
|
1258
|
+
getSession(port, msg.pid);
|
|
1259
|
+
dispatch(port, msg.method, msg.params)
|
|
1260
|
+
.then(result => sendResponse(result))
|
|
1261
|
+
.catch(err => sendResponse({ __error: err.message || String(err) }));
|
|
1262
|
+
})
|
|
1263
|
+
.catch(err => sendResponse({ __error: err.message || String(err) })); // else a storage-restore reject hangs the caller
|
|
922
1264
|
return true; // async response
|
|
923
1265
|
}
|
|
924
1266
|
|
|
@@ -928,10 +1270,28 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
928
1270
|
}
|
|
929
1271
|
|
|
930
1272
|
if (msg.type === 'reconnect') {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1273
|
+
// FEJL RETTET 19/8: der var ingen fangst her. Lykkedes closeDocument()
|
|
1274
|
+
// men fejlede ensureOffscreen() — fx fordi dokumentet stadig var ved at
|
|
1275
|
+
// lukke — blev afvisningen slugt, og extensionen stod tilbage UDEN
|
|
1276
|
+
// offscreen-dokument. Ingen WebSocket, ingen genopretning, og kun en
|
|
1277
|
+
// manuel genindlaesning kunne redde den.
|
|
1278
|
+
(async () => {
|
|
1279
|
+
try {
|
|
1280
|
+
if (await chrome.offscreen.hasDocument()) {
|
|
1281
|
+
await chrome.offscreen.closeDocument();
|
|
1282
|
+
}
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
console.warn('[BG] kunne ikke lukke offscreen:', e?.message || e);
|
|
1285
|
+
}
|
|
1286
|
+
// Proev at genskabe. Fejler det, tager hjerteslags-alarmen den
|
|
1287
|
+
// inden for et minut — men kun fordi vi IKKE lader fejlen forsvinde.
|
|
1288
|
+
try {
|
|
1289
|
+
await ensureOffscreen();
|
|
1290
|
+
} catch (e) {
|
|
1291
|
+
console.error('[BG] kunne ikke genskabe offscreen:', e?.message || e);
|
|
1292
|
+
setTimeout(() => ensureOffscreen().catch(console.error), 2000);
|
|
1293
|
+
}
|
|
1294
|
+
})();
|
|
935
1295
|
return;
|
|
936
1296
|
}
|
|
937
1297
|
|
|
@@ -1422,26 +1782,73 @@ async function dismissOverlays(tabId, scope = 'non_critical', maxPasses = 3) {
|
|
|
1422
1782
|
];
|
|
1423
1783
|
// "Ambiguous" texts MAY revert partial form data ("Cancel" usually reverts state)
|
|
1424
1784
|
// — only used when overlay has no editable form fields, or in aggressive scope
|
|
1785
|
+
// ── VETO-LISTE (MAALT 23/8 — det dyreste fund i hele auditten) ────────────
|
|
1786
|
+
//
|
|
1787
|
+
// Koert med den ORDRETTE kode mod knapper i en rigtig browser trykkede
|
|
1788
|
+
// dismiss_overlays paa "Close account", "Close and delete everything",
|
|
1789
|
+
// "Cancel subscription" og "Afvis betalingen permanent" — og paa enhver knap
|
|
1790
|
+
// med aria-label="Close account" eller "Luk kontoen". Det skete i DEFAULT-scope,
|
|
1791
|
+
// ikke kun aggressive.
|
|
1792
|
+
//
|
|
1793
|
+
// Aarsagen: "close" og "luk" er lovlige luk-ord, og matchningen havde ingen
|
|
1794
|
+
// ord-graense. "Close account" indeholder "close". Og instruktionerne beder
|
|
1795
|
+
// agenten kalde dismiss_overlays FOER hvert stoerre skridt, saa det ville ske
|
|
1796
|
+
// paa hver eneste side hvor saadan en knap findes.
|
|
1797
|
+
//
|
|
1798
|
+
// Et luk-ord er derfor ikke laengere nok: findes ET af disse ord i teksten eller
|
|
1799
|
+
// aria-labelen, klikkes der ALDRIG — uanset hvor godt resten matcher. Det er en
|
|
1800
|
+
// veto, ikke en vaegtning. Et overlay der ikke bliver lukket koster et ekstra
|
|
1801
|
+
// skridt; en lukket konto koster brugeren penge eller adgang.
|
|
1802
|
+
const VETO = [
|
|
1803
|
+
'account', 'konto', 'subscription', 'abonnement', 'membership', 'medlemskab',
|
|
1804
|
+
'payment', 'betaling', 'kort', 'card', 'billing', 'faktura', 'invoice',
|
|
1805
|
+
'delete', 'slet', 'remove', 'fjern', 'erase', 'wipe', 'destroy',
|
|
1806
|
+
'permanent', 'permanently', 'forever', 'for evigt', 'irreversibl',
|
|
1807
|
+
'unsubscribe', 'opsig', 'afmeld', 'terminate', 'opheav',
|
|
1808
|
+
'deactivate', 'deaktiver', 'disable', 'deaktivér',
|
|
1809
|
+
'sign out', 'log out', 'log ud', 'logout', 'sign-out',
|
|
1810
|
+
'order', 'ordre', 'purchase', 'koeb', 'køb', 'refund', 'refunder',
|
|
1811
|
+
];
|
|
1812
|
+
const erFarlig = (tekst) => {
|
|
1813
|
+
const t = (tekst || '').toLowerCase();
|
|
1814
|
+
return VETO.some((v) => t.includes(v));
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1425
1817
|
const ambiguousTexts = [
|
|
1426
1818
|
"skip", "cancel", "afvis", "spring over",
|
|
1427
1819
|
];
|
|
1428
1820
|
const xChars = ['×', '✕', '✖', '⨯'];
|
|
1429
1821
|
|
|
1822
|
+
// MAALT 21/8: her stod `if (!el || !el.offsetParent && el.tagName !== 'BODY') return false`.
|
|
1823
|
+
// offsetParent er ALTID null for et position:fixed-element — det er ikke en fejl i
|
|
1824
|
+
// browseren, det er definitionen. Saa hele overlay-fjerneren var blind for praecis
|
|
1825
|
+
// den slags elementer som cookie-bannere, samtykke-bjaelker og modaler ER. Et
|
|
1826
|
+
// synligt fixed-banner med <button aria-label="Close"> blev hverken fundet som
|
|
1827
|
+
// overlay eller som luk-knap: dismissed:[], skipped:[], count:0 — tavst intet.
|
|
1828
|
+
//
|
|
1829
|
+
// Rigtig synlighed laeses af layout og stil, ikke af offsetParent.
|
|
1430
1830
|
const isVisible = (el) => {
|
|
1431
|
-
if (!el
|
|
1831
|
+
if (!el) return false;
|
|
1432
1832
|
const rect = el.getBoundingClientRect();
|
|
1433
|
-
|
|
1833
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
1834
|
+
const st = getComputedStyle(el);
|
|
1835
|
+
if (st.display === 'none' || st.visibility === 'hidden' || st.visibility === 'collapse') return false;
|
|
1836
|
+
if (parseFloat(st.opacity) === 0) return false;
|
|
1837
|
+
return true;
|
|
1434
1838
|
};
|
|
1435
1839
|
|
|
1436
1840
|
const findCloseAffordance = (overlay, allowAmbiguous) => {
|
|
1437
1841
|
const all = [...overlay.querySelectorAll('button, [role="button"], a[href="#"], [aria-label]')];
|
|
1438
1842
|
const allTexts = allowAmbiguous ? [...safeTexts, ...ambiguousTexts] : safeTexts;
|
|
1439
1843
|
|
|
1440
|
-
// Priority 1: aria-label match
|
|
1844
|
+
// Priority 1: aria-label match. MAALT 23/8: "always safe" var FORKERT — aria-label
|
|
1845
|
+
// "Close account" indeholder "close", saa knappen blev trykket. Vetoet nedenfor
|
|
1846
|
+
// er derfor det foerste der koeres, foer nogen match overhovedet forsoeges.
|
|
1441
1847
|
for (const c of all) {
|
|
1442
1848
|
if (!isVisible(c)) continue;
|
|
1443
1849
|
const label = (c.getAttribute('aria-label') || '').toLowerCase();
|
|
1444
1850
|
if (!label) continue;
|
|
1851
|
+
if (erFarlig(label) || erFarlig(c.textContent)) continue; // veto — se listen ovenfor
|
|
1445
1852
|
if (label.includes('close') || label.includes('dismiss') || label.includes('luk')) {
|
|
1446
1853
|
return { el: c, method: 'aria-label', label };
|
|
1447
1854
|
}
|
|
@@ -1455,6 +1862,7 @@ async function dismissOverlays(tabId, scope = 'non_critical', maxPasses = 3) {
|
|
|
1455
1862
|
if (!isVisible(c)) continue;
|
|
1456
1863
|
const text = (c.textContent || '').trim().toLowerCase();
|
|
1457
1864
|
if (!text || text.length > 30) continue;
|
|
1865
|
+
if (erFarlig(text) || erFarlig(c.getAttribute('aria-label'))) continue; // veto
|
|
1458
1866
|
if (allTexts.some(t => text === t || text === t + '!' || text === t + '.')) {
|
|
1459
1867
|
return { el: c, method: 'text-exact', label: text };
|
|
1460
1868
|
}
|
|
@@ -1464,7 +1872,11 @@ async function dismissOverlays(tabId, scope = 'non_critical', maxPasses = 3) {
|
|
|
1464
1872
|
if (!isVisible(c)) continue;
|
|
1465
1873
|
const text = (c.textContent || '').trim().toLowerCase();
|
|
1466
1874
|
if (!text || text.length > 40) continue;
|
|
1467
|
-
|
|
1875
|
+
// MAALT 22/8: contains-passet gjorde "ok" til en delstreng-traeffer, saa
|
|
1876
|
+
// "Book a demo", "Unlock account" og "Cookie settings" blev klikbare — i
|
|
1877
|
+
// DEFAULT-scope. Korte ord maa kun matche eksakt (prioritet 2 ovenfor).
|
|
1878
|
+
if (erFarlig(text) || erFarlig(c.getAttribute('aria-label'))) continue; // veto
|
|
1879
|
+
if (allTexts.filter(t => t.length >= 5).some(t => text.includes(t))) {
|
|
1468
1880
|
return { el: c, method: 'text-contains', label: text };
|
|
1469
1881
|
}
|
|
1470
1882
|
}
|
|
@@ -1473,6 +1885,7 @@ async function dismissOverlays(tabId, scope = 'non_critical', maxPasses = 3) {
|
|
|
1473
1885
|
for (const c of all) {
|
|
1474
1886
|
if (!isVisible(c)) continue;
|
|
1475
1887
|
const text = (c.textContent || '').trim();
|
|
1888
|
+
if (erFarlig(c.getAttribute('aria-label'))) continue; // et × med farlig aria-label
|
|
1476
1889
|
if (xChars.includes(text)) {
|
|
1477
1890
|
return { el: c, method: 'x-char', label: text };
|
|
1478
1891
|
}
|
|
@@ -1494,6 +1907,21 @@ async function dismissOverlays(tabId, scope = 'non_critical', maxPasses = 3) {
|
|
|
1494
1907
|
'[class*="banner" i]:not([class*="-hidden"]):not(input):not(button)',
|
|
1495
1908
|
'[data-testid*="dialog" i]',
|
|
1496
1909
|
'[data-testid*="modal" i]',
|
|
1910
|
+
// MAALT 22/8 af flowtesten: listen matchede kun paa class, aldrig paa id. Et
|
|
1911
|
+
// helt almindeligt <div id="banner"> blev derfor ALDRIG set som et overlay —
|
|
1912
|
+
// og cookie-bannere skrives lige saa ofte med id som med class. Hullet var
|
|
1913
|
+
// skjult indtil i dag, fordi det strukturelle fixed/sticky-spor fangede dem
|
|
1914
|
+
// alligevel; det spor blev skaaret efter sikkerhedsreview, og saa stod hullet
|
|
1915
|
+
// bart. De samme fem ord, samme regler — bare paa id.
|
|
1916
|
+
//
|
|
1917
|
+
// Det her er IKKE det skaarne spor i ny form: her har sideforfatteren selv
|
|
1918
|
+
// kaldt elementet en dialog/modal/banner. Det er en eksplicit erklaering, ikke
|
|
1919
|
+
// et gaet ud fra placering, saa faren ved delstrengs-matchning gaelder ikke.
|
|
1920
|
+
'[id*="modal" i]:not(input):not(button)',
|
|
1921
|
+
'[id*="overlay" i]:not(input):not(button)',
|
|
1922
|
+
'[id*="popover" i]:not(input):not(button)',
|
|
1923
|
+
'[id*="banner" i]:not(input):not(button)',
|
|
1924
|
+
'[id*="dialog" i]:not(input):not(button)',
|
|
1497
1925
|
];
|
|
1498
1926
|
for (const sel of selectors) {
|
|
1499
1927
|
try {
|
|
@@ -1505,6 +1933,15 @@ async function dismissOverlays(tabId, scope = 'non_critical', maxPasses = 3) {
|
|
|
1505
1933
|
} catch {}
|
|
1506
1934
|
}
|
|
1507
1935
|
|
|
1936
|
+
// Et strukturelt spor (alt fixed/sticky over 40x20px som overlay-kandidat) blev
|
|
1937
|
+
// proevet 21/8 og SKAARET 22/8 efter sikkerhedsreview: findCloseAffordance matcher
|
|
1938
|
+
// paa delstrenge, saa "Cancel subscription", "Close account" og "Book now" (via "ok")
|
|
1939
|
+
// alle blev klikkbare — paa hver eneste side, og instruktionerne beder agenten kalde
|
|
1940
|
+
// dismiss_overlays foer hvert stoerre skridt. Den maalte fejl var offsetParent-
|
|
1941
|
+
// blindheden i isVisible ovenfor; den er rettet. Sporet var ny adfaerd uden bevist
|
|
1942
|
+
// behov. Genindfoeres kun med eksakt tekstmatch og et krav om luk-affordance som
|
|
1943
|
+
// direkte barn.
|
|
1944
|
+
|
|
1508
1945
|
for (const overlay of overlays) {
|
|
1509
1946
|
const role = overlay.getAttribute('role') || (overlay.className || '').split(' ')[0] || 'unknown';
|
|
1510
1947
|
|
|
@@ -1761,13 +2198,86 @@ async function dropFileOnTarget(tabId, selector, files) {
|
|
|
1761
2198
|
}
|
|
1762
2199
|
}
|
|
1763
2200
|
|
|
2201
|
+
// Strategy 2 (v1.27): intercept the NATIVE OS file chooser.
|
|
2202
|
+
//
|
|
2203
|
+
// Sites like Google Ads never put an <input type="file"> in the DOM — clicking
|
|
2204
|
+
// their "choose a file" control opens the OS dialog directly, which no browser
|
|
2205
|
+
// automation can reach. Page.setInterceptFileChooserDialog makes Chrome fire
|
|
2206
|
+
// Page.fileChooserOpened instead of showing that dialog, and the event carries
|
|
2207
|
+
// the backendNodeId of the element that requested it. DOM.setFileInputFiles
|
|
2208
|
+
// accepts a backendNodeId, so we can satisfy the request programmatically.
|
|
2209
|
+
//
|
|
2210
|
+
// Order matters: interception must be armed BEFORE the click that opens the
|
|
2211
|
+
// chooser, otherwise the OS dialog is already up and the event never fires.
|
|
2212
|
+
const chooserResult = await interceptFileChooser(tabId, selector, fileList);
|
|
2213
|
+
if (chooserResult) return chooserResult;
|
|
2214
|
+
|
|
1764
2215
|
return {
|
|
1765
2216
|
ok: false,
|
|
1766
2217
|
error: 'no-file-input-found',
|
|
1767
|
-
hint: 'No <input type="file"> found
|
|
2218
|
+
hint: 'No <input type="file"> found, and the native file-chooser interception did not fire. The trigger element may not open a file dialog at all — check the selector.',
|
|
1768
2219
|
};
|
|
1769
2220
|
}
|
|
1770
2221
|
|
|
2222
|
+
// Arms Page.fileChooserOpened, clicks the trigger, and fulfils the chooser with
|
|
2223
|
+
// the given files. Returns null if no chooser opened (so the caller can fall
|
|
2224
|
+
// through to its own error), or a result object on success/explicit failure.
|
|
2225
|
+
async function interceptFileChooser(tabId, selector, fileList) {
|
|
2226
|
+
try {
|
|
2227
|
+
await debuggerAttach(tabId);
|
|
2228
|
+
await cdpSend(tabId, 'Page.enable', {});
|
|
2229
|
+
await cdpSend(tabId, 'DOM.enable', {});
|
|
2230
|
+
await cdpSend(tabId, 'Page.setInterceptFileChooserDialog', { enabled: true });
|
|
2231
|
+
} catch (e) {
|
|
2232
|
+
return null; // interception unavailable — let caller report the original error
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
try {
|
|
2236
|
+
return await new Promise((resolve) => {
|
|
2237
|
+
let settled = false;
|
|
2238
|
+
const finish = (value) => {
|
|
2239
|
+
if (settled) return;
|
|
2240
|
+
settled = true;
|
|
2241
|
+
chrome.debugger.onEvent.removeListener(listener);
|
|
2242
|
+
clearTimeout(timer);
|
|
2243
|
+
resolve(value);
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
const listener = (source, method, eventParams) => {
|
|
2247
|
+
if (source.tabId !== tabId || method !== 'Page.fileChooserOpened') return;
|
|
2248
|
+
const backendNodeId = eventParams?.backendNodeId;
|
|
2249
|
+
if (!backendNodeId) {
|
|
2250
|
+
finish({ ok: false, error: 'file-chooser-without-node', detail: 'Chrome fired fileChooserOpened but supplied no backendNodeId.' });
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
cdpSend(tabId, 'DOM.setFileInputFiles', { backendNodeId, files: fileList })
|
|
2254
|
+
.then(() => finish({
|
|
2255
|
+
ok: true,
|
|
2256
|
+
method: 'native-chooser-intercepted',
|
|
2257
|
+
files: fileList,
|
|
2258
|
+
mode: eventParams.mode,
|
|
2259
|
+
}))
|
|
2260
|
+
.catch(e => finish({ ok: false, error: 'setFileInputFiles-failed', detail: e?.message || String(e) }));
|
|
2261
|
+
};
|
|
2262
|
+
chrome.debugger.onEvent.addListener(listener);
|
|
2263
|
+
|
|
2264
|
+
// 8s: the click is local, so the chooser opens within a frame or two.
|
|
2265
|
+
// A longer wait would just stall the caller when the element opens no dialog.
|
|
2266
|
+
const timer = setTimeout(() => finish(null), 8000);
|
|
2267
|
+
|
|
2268
|
+
// Click the trigger so the page asks for the chooser. Real mouse events —
|
|
2269
|
+
// a synthetic .click() does not always reach the file-picker code path.
|
|
2270
|
+
(async () => {
|
|
2271
|
+
const el = await resolveElement(tabId, selector);
|
|
2272
|
+
if (!el) { finish({ ok: false, error: 'trigger-not-found', detail: selector }); return; }
|
|
2273
|
+
await debuggerClick(tabId, el.x, el.y);
|
|
2274
|
+
})().catch(e => finish({ ok: false, error: 'trigger-click-failed', detail: e?.message || String(e) }));
|
|
2275
|
+
});
|
|
2276
|
+
} finally {
|
|
2277
|
+
try { await cdpSend(tabId, 'Page.setInterceptFileChooserDialog', { enabled: false }); } catch {}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1771
2281
|
// ── Command Dispatcher ──────────────────────────────────────────────────────
|
|
1772
2282
|
|
|
1773
2283
|
async function dispatch(port, method, params) {
|
|
@@ -1825,11 +2335,84 @@ async function dispatch(port, method, params) {
|
|
|
1825
2335
|
return { content, url: tab.url, title: tab.title, method: 'debugger' };
|
|
1826
2336
|
}
|
|
1827
2337
|
|
|
2338
|
+
// Read EVERY row of a virtualised list by scrolling its container until the set stops
|
|
2339
|
+
// growing. Outlook, Gmail and most mail/table UIs keep only ~7 rows in the DOM, so a
|
|
2340
|
+
// single get_page_content sees a sliver — this walks the whole list instead.
|
|
2341
|
+
case 'extract_list': {
|
|
2342
|
+
const tab = await getSessionTab(port);
|
|
2343
|
+
if (tab.url.startsWith('chrome://')) throw new Error('Cannot access chrome:// pages');
|
|
2344
|
+
const rowSel = params.selector;
|
|
2345
|
+
if (!rowSel) throw new Error('extract_list requires `selector` (the repeating row element)');
|
|
2346
|
+
const containerSel = params.container || null;
|
|
2347
|
+
const maxRows = Math.min(params.max_rows || 500, 5000);
|
|
2348
|
+
const stableNeeded = params.stable_rounds || 3;
|
|
2349
|
+
const waitMs = params.wait_ms || 350;
|
|
2350
|
+
|
|
2351
|
+
const seen = new Set();
|
|
2352
|
+
let stable = 0, rounds = 0, atEnd = false;
|
|
2353
|
+
const MAX_ROUNDS = 300; // backstop: a list that never stabilises must not spin forever
|
|
2354
|
+
|
|
2355
|
+
while (seen.size < maxRows && stable < stableNeeded && rounds < MAX_ROUNDS) {
|
|
2356
|
+
rounds++;
|
|
2357
|
+
const r = await safeExecuteScript(tab.id, (rs, cs, keep) => {
|
|
2358
|
+
const rows = Array.from(document.querySelectorAll(rs));
|
|
2359
|
+
const texts = rows
|
|
2360
|
+
.map(el => (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim())
|
|
2361
|
+
.filter(t => t.length > 0);
|
|
2362
|
+
|
|
2363
|
+
// Scroll the row's own scrollable ancestor — scrolling window does nothing when the
|
|
2364
|
+
// list lives in an inner overflow container (the normal case in webmail).
|
|
2365
|
+
let c = cs ? document.querySelector(cs) : null;
|
|
2366
|
+
if (!c && rows.length) {
|
|
2367
|
+
let p = rows[0].parentElement;
|
|
2368
|
+
while (p && p !== document.documentElement) {
|
|
2369
|
+
const s = getComputedStyle(p);
|
|
2370
|
+
if (/(auto|scroll)/.test(s.overflowY) && p.scrollHeight > p.clientHeight + 20) { c = p; break; }
|
|
2371
|
+
p = p.parentElement;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
const step = keep || (c ? c.clientHeight : window.innerHeight) * 0.85;
|
|
2375
|
+
const before = c ? c.scrollTop : window.scrollY;
|
|
2376
|
+
if (c) c.scrollTop = before + step; else window.scrollBy(0, step);
|
|
2377
|
+
const after = c ? c.scrollTop : window.scrollY;
|
|
2378
|
+
const done = c
|
|
2379
|
+
? c.scrollTop + c.clientHeight >= c.scrollHeight - 4
|
|
2380
|
+
: window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 4;
|
|
2381
|
+
return { texts, moved: after - before, done, container: !!c };
|
|
2382
|
+
}, [rowSel, containerSel, params.scroll_step || 0]);
|
|
2383
|
+
|
|
2384
|
+
if (r.cspBlocked) throw new Error('extract_list: this page blocks script injection — use screenshots instead');
|
|
2385
|
+
const data = r.result || { texts: [] };
|
|
2386
|
+
const before = seen.size;
|
|
2387
|
+
for (const t of data.texts) seen.add(t);
|
|
2388
|
+
// Two independent stop signals: nothing new appeared, or the container hit its end.
|
|
2389
|
+
if (seen.size === before) stable++; else stable = 0;
|
|
2390
|
+
if (data.done) { atEnd = true; stable++; }
|
|
2391
|
+
if (!data.texts.length && rounds > 2) break; // selector matches nothing — fail fast
|
|
2392
|
+
await new Promise(res => setTimeout(res, waitMs));
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
return {
|
|
2396
|
+
rows: [...seen],
|
|
2397
|
+
count: seen.size,
|
|
2398
|
+
rounds,
|
|
2399
|
+
reached_end: atEnd,
|
|
2400
|
+
truncated: seen.size >= maxRows,
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
|
|
1828
2404
|
case 'screenshot': {
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
-
|
|
2405
|
+
// EKSPERIMENT 21/8: fanen aktiveres IKKE laengere.
|
|
2406
|
+
//
|
|
2407
|
+
// getSessionTab(…, true) stjal ikke VINDUES-fokus (FIX-1), men den kaldte stadig
|
|
2408
|
+
// chrome.tabs.update({active:true}) — altsaa et fane-skift INDE i vinduet. Sidder
|
|
2409
|
+
// brugeren i det samme Chrome-vindue paa sin egen fane, bliver den revet vaek hver
|
|
2410
|
+
// eneste gang agenten tager et billede. Og billeder tages konstant.
|
|
2411
|
+
//
|
|
2412
|
+
// Aktiveringen er formentlig unoedvendig: CDP Page.captureScreenshot nedenfor
|
|
2413
|
+
// fotograferer en fane der ikke er forrest. Den okkluderede sidste-udvej loefter
|
|
2414
|
+
// stadig vinduet hvis CDP virkelig ikke kan producere en frame.
|
|
2415
|
+
const tab = await getSessionTab(port, false);
|
|
1833
2416
|
if (tab.url.startsWith('chrome://') || tab.url.startsWith('about:')) {
|
|
1834
2417
|
throw new Error(`Cannot screenshot ${tab.url.split(':')[0]}: pages — navigate to a real page first`);
|
|
1835
2418
|
}
|
|
@@ -1887,6 +2470,12 @@ async function dispatch(port, method, params) {
|
|
|
1887
2470
|
|
|
1888
2471
|
case 'execute_script': {
|
|
1889
2472
|
// v1.22.2 (DIAGNOSTIC): Try scripting paths but log all errors so we can see WHY they fail
|
|
2473
|
+
// v1.26: accept `script` as alias for `code` — the historic param-name mismatch caused
|
|
2474
|
+
// silent "unserializable"/undefined failures that read as "execute_script is broken".
|
|
2475
|
+
if (params.code == null && typeof params.script === 'string') params.code = params.script;
|
|
2476
|
+
if (typeof params.code !== 'string' || !params.code.trim()) {
|
|
2477
|
+
return { ok: false, error: 'Missing code. Pass a JavaScript EXPRESSION in `code` (e.g. an IIFE: (() => {...; return x;})()). `return ...` at top level is invalid — the handler wraps code in parentheses.' };
|
|
2478
|
+
}
|
|
1890
2479
|
const tab = await getSessionTab(port);
|
|
1891
2480
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot execute scripts on chrome:// pages');
|
|
1892
2481
|
|
|
@@ -2003,14 +2592,48 @@ async function dispatch(port, method, params) {
|
|
|
2003
2592
|
try {
|
|
2004
2593
|
const el = await resolveElement(tab.id, params.selector);
|
|
2005
2594
|
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
2595
|
+
// Elementet findes, men har ingen udstraekning — at klikke ville ramme (0,0),
|
|
2596
|
+
// altsaa et HELT andet element end det der blev bedt om. Sig det i stedet.
|
|
2597
|
+
if (el.hidden) {
|
|
2598
|
+
return {
|
|
2599
|
+
ok: false,
|
|
2600
|
+
error: 'Element fundet men ikke synligt (0x0) — klik ville ramme sidens hjoerne: ' + params.selector,
|
|
2601
|
+
hidden: true,
|
|
2602
|
+
tag: el.tag,
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2006
2605
|
|
|
2007
2606
|
// Primary path: debugger mouse events (isTrusted=true, works on React/Angular SPAs)
|
|
2008
|
-
await debuggerClick(tab.id, el.x, el.y);
|
|
2009
|
-
return {
|
|
2607
|
+
const clickResult = await debuggerClick(tab.id, el.x, el.y);
|
|
2608
|
+
return {
|
|
2609
|
+
ok: true,
|
|
2610
|
+
method: el.method || 'debugger',
|
|
2611
|
+
tag: el.tag,
|
|
2612
|
+
text: el.text,
|
|
2613
|
+
// MAALT 21/8: `landed` blev allerede beregnet inde i debuggerClick og smidt vaek,
|
|
2614
|
+
// saa `click` svarede ok:true selv naar siden slet ikke reagerede. Nu foelger den med:
|
|
2615
|
+
// landed=false betyder "eventet blev sendt, men intet handler tog imod det".
|
|
2616
|
+
...(clickResult || {}),
|
|
2617
|
+
};
|
|
2010
2618
|
} catch (e) {
|
|
2011
2619
|
// Fallback: synthetic click via chrome.scripting for anti-automation sites
|
|
2012
2620
|
// (Apple ASC etc.) OR user-blocked-debugger scenarios.
|
|
2013
|
-
|
|
2621
|
+
//
|
|
2622
|
+
// MÅLT 29/7: betingelsen var kun /Debugger detached/, men den HYPPIGSTE fejl hedder
|
|
2623
|
+
// "Debugger attach failed after 3 attempts" (kastes l.298) — altså når Chrome nægter
|
|
2624
|
+
// at koble debuggeren på overhovedet. De to strenge ligner hinanden og betyder næsten
|
|
2625
|
+
// det samme, men regexet ramte kun den ene, så fallbacken fyrede aldrig i det tilfælde
|
|
2626
|
+
// den var skrevet til: "user-blocked-debugger scenarios" står ordret i kommentaren
|
|
2627
|
+
// ovenfor, og det var netop dét den ikke dækkede.
|
|
2628
|
+
//
|
|
2629
|
+
// Konsekvens i praksis: klikker brugeren Cancel på Chromes debugger-banner ÉN gang,
|
|
2630
|
+
// husker Chrome det på tværs af extension-reloads, og hvert eneste klik fejler
|
|
2631
|
+
// permanent — selvom scriptingClick ville have virket hele tiden. Den bruger `func:`
|
|
2632
|
+
// og ikke en kode-streng, så den rammes ikke af sidens CSP.
|
|
2633
|
+
//
|
|
2634
|
+
// Prisen ved fallbacken er at klikket mister isTrusted=true. Det tjekker de færreste
|
|
2635
|
+
// sider, og et klik der virker på 95% af nettet slår et klik der aldrig virker.
|
|
2636
|
+
if (/Debugger detached|Debugger attach failed|not attached/i.test(e?.message || '')) {
|
|
2014
2637
|
const r = await scriptingClick(tab.id, params.selector);
|
|
2015
2638
|
if (r.ok) return { ok: true, method: 'scripting-fallback', tag: r.tag };
|
|
2016
2639
|
}
|
|
@@ -2148,6 +2771,11 @@ async function dispatch(port, method, params) {
|
|
|
2148
2771
|
const r = await setCombobox(tab.id, params.selector, values, {
|
|
2149
2772
|
multi: !!params.multi,
|
|
2150
2773
|
query_chars: params.query_chars,
|
|
2774
|
+
// MAALT 22/8: wait_ms blev tavst kasseret her. Skemaet lover parameteren, og
|
|
2775
|
+
// setCombobox laeser opts.wait_ms — men handleren videregav den ikke, saa
|
|
2776
|
+
// ventetiden stod altid paa standarden 3000 ms. En agent der bad om laengere
|
|
2777
|
+
// tid til en langsom liste fik den ikke, og fik ingen besked om det.
|
|
2778
|
+
wait_ms: params.wait_ms,
|
|
2151
2779
|
});
|
|
2152
2780
|
const visibleErrors = r.ok ? [] : await collectVisibleErrors(tab.id, params.selector);
|
|
2153
2781
|
return r.ok ? r : { ...r, visible_errors: visibleErrors };
|
|
@@ -2156,7 +2784,8 @@ async function dispatch(port, method, params) {
|
|
|
2156
2784
|
case 'drop_file': {
|
|
2157
2785
|
const tab = await getSessionTab(port);
|
|
2158
2786
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
2159
|
-
const files = Array.isArray(params.files) ? params.files
|
|
2787
|
+
const files = Array.isArray(params.files) ? params.files
|
|
2788
|
+
: [params.files || params.file || params.file_path].filter(Boolean);
|
|
2160
2789
|
if (!files[0]) return { ok: false, error: 'files or file required' };
|
|
2161
2790
|
return await dropFileOnTarget(tab.id, params.selector || 'body', files);
|
|
2162
2791
|
}
|
|
@@ -2187,8 +2816,9 @@ async function dispatch(port, method, params) {
|
|
|
2187
2816
|
}
|
|
2188
2817
|
|
|
2189
2818
|
case 'press_key': {
|
|
2190
|
-
//
|
|
2191
|
-
|
|
2819
|
+
// EKSPERIMENT 21/8: aktiverer IKKE fanen. Kommentaren her sagde at Chrome ellers
|
|
2820
|
+
// sender tastetrykket til den aktive fane — det maales nu i stedet for at antages.
|
|
2821
|
+
const tab = await getSessionTab(port, false);
|
|
2192
2822
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
2193
2823
|
const key = params.key; // e.g. "Enter", "Tab", "Escape", "ArrowDown"
|
|
2194
2824
|
const modifiers = (params.ctrl ? 2 : 0) | (params.alt ? 1 : 0) | (params.shift ? 8 : 0) | (params.meta ? 4 : 0);
|
|
@@ -2267,6 +2897,65 @@ async function dispatch(port, method, params) {
|
|
|
2267
2897
|
return { ok: true, scrolled: { x: dx, y: dy }, method: 'mouseWheel-stepped' };
|
|
2268
2898
|
}
|
|
2269
2899
|
|
|
2900
|
+
// ── v1.26 "superior" tools ──────────────────────────────────────────────
|
|
2901
|
+
// to the MCP server — only lengths and shape booleans. Born from the 2026-07-27
|
|
2902
|
+
// Azure-secret night: the agent must be able to move a credential from page to
|
|
2903
|
+
// field without the value ever entering the LLM context or transcript.
|
|
2904
|
+
|
|
2905
|
+
case 'double_click': {
|
|
2906
|
+
const tab = await getSessionTab(port);
|
|
2907
|
+
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
2908
|
+
const el = await resolveElement(tab.id, params.selector);
|
|
2909
|
+
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
2910
|
+
await debuggerAttach(tab.id);
|
|
2911
|
+
const { x, y } = el;
|
|
2912
|
+
await cdpSend(tab.id, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
2913
|
+
await new Promise(r => setTimeout(r, 30));
|
|
2914
|
+
// Proper dblclick: two press/release pairs with escalating clickCount.
|
|
2915
|
+
await dispatchTaalmodigt(tab.id, { type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 1 });
|
|
2916
|
+
await dispatchTaalmodigt(tab.id, { type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 1 });
|
|
2917
|
+
await new Promise(r => setTimeout(r, 40));
|
|
2918
|
+
await dispatchTaalmodigt(tab.id, { type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 2 });
|
|
2919
|
+
await dispatchTaalmodigt(tab.id, { type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 2 });
|
|
2920
|
+
return { ok: true, double_clicked: true, tag: el.tag, text: el.text };
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
case 'right_click': {
|
|
2924
|
+
const tab = await getSessionTab(port);
|
|
2925
|
+
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
2926
|
+
const el = await resolveElement(tab.id, params.selector);
|
|
2927
|
+
if (!el) return { ok: false, error: 'Element not found: ' + params.selector };
|
|
2928
|
+
await debuggerAttach(tab.id);
|
|
2929
|
+
const { x, y } = el;
|
|
2930
|
+
await cdpSend(tab.id, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
2931
|
+
await new Promise(r => setTimeout(r, 30));
|
|
2932
|
+
await dispatchTaalmodigt(tab.id, { type: 'mousePressed', x, y, button: 'right', buttons: 2, clickCount: 1 });
|
|
2933
|
+
await dispatchTaalmodigt(tab.id, { type: 'mouseReleased', x, y, button: 'right', buttons: 0, clickCount: 1 });
|
|
2934
|
+
return { ok: true, right_clicked: true, tag: el.tag, text: el.text, note: 'contextmenu event fired; native Chrome menu does not open via CDP — page-level menus (OWA, web apps) do' };
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
case 'click_xy': {
|
|
2938
|
+
// Raw coordinate click — the escape hatch for custom widgets whose buttons
|
|
2939
|
+
// resist every selector strategy (Azure portal dialogs, KO-bound divs).
|
|
2940
|
+
// Coordinates come from the caller's own screenshot analysis.
|
|
2941
|
+
const tab = await getSessionTab(port);
|
|
2942
|
+
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
2943
|
+
if (typeof params.x !== 'number' || typeof params.y !== 'number') {
|
|
2944
|
+
return { ok: false, error: 'x and y (numbers, CSS pixels in viewport) are required' };
|
|
2945
|
+
}
|
|
2946
|
+
await debuggerClick(tab.id, params.x, params.y);
|
|
2947
|
+
return { ok: true, clicked_at: { x: params.x, y: params.y } };
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
case 'reattach_debugger': {
|
|
2951
|
+
// Ghost-attach recovery without full extension reload: force detach + fresh attach.
|
|
2952
|
+
const tab = await getSessionTab(port);
|
|
2953
|
+
try { await chrome.debugger.detach({ tabId: tab.id }); } catch {}
|
|
2954
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2955
|
+
await debuggerAttach(tab.id);
|
|
2956
|
+
return { ok: true, reattached: true, tab_id: tab.id };
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2270
2959
|
case 'hover': {
|
|
2271
2960
|
const tab = await getSessionTab(port);
|
|
2272
2961
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
@@ -2297,21 +2986,45 @@ async function dispatch(port, method, params) {
|
|
|
2297
2986
|
})()
|
|
2298
2987
|
`);
|
|
2299
2988
|
|
|
2989
|
+
// `value` og `label` accepteres som alias for `option`. Uden dem gav et forkert
|
|
2990
|
+
// navn "undefined" som soegetekst — og vaerktoejet svarede alligevel ok:true.
|
|
2991
|
+
const oensket = params.option ?? params.value ?? params.label;
|
|
2992
|
+
if (typeof oensket !== 'string' || !oensket) {
|
|
2993
|
+
return { ok: false, error: 'Manglende `option` (teksten eller vaerdien paa den mulighed der skal vaelges).' };
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2300
2996
|
if (isNativeSelect) {
|
|
2301
|
-
//
|
|
2302
|
-
|
|
2997
|
+
// MAALT 21/8: her blev resultatet af evalueringen — `return !!opt` — kastet vaek,
|
|
2998
|
+
// og handleren svarede ubetinget ok:true. Blev muligheden ikke fundet, skete der
|
|
2999
|
+
// INTET, og svaret sagde stadig at det var lykkedes. Samme fejlklasse som klikket
|
|
3000
|
+
// der svarede ok:true uden at siden reagerede. Nu laeses svaret, og der laeses
|
|
3001
|
+
// TILBAGE fra feltet bagefter, saa "valgt" betyder at vaerdien faktisk staar der.
|
|
3002
|
+
const valg = await debuggerEval(tab.id, `
|
|
2303
3003
|
(function() {
|
|
2304
3004
|
const sel = document.querySelector(${JSON.stringify(params.selector)});
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
3005
|
+
if (!sel) return JSON.stringify({ found: false, error: 'select ikke fundet' });
|
|
3006
|
+
const oensket = ${JSON.stringify(oensket)};
|
|
3007
|
+
const opt = Array.from(sel.options).find(o => o.value === oensket)
|
|
3008
|
+
|| Array.from(sel.options).find(o => o.text.trim() === oensket)
|
|
3009
|
+
|| Array.from(sel.options).find(o => o.text.includes(oensket));
|
|
3010
|
+
if (!opt) {
|
|
3011
|
+
return JSON.stringify({ found: false, error: 'Ingen mulighed matchede: ' + oensket,
|
|
3012
|
+
available: Array.from(sel.options).map(o => o.text.trim()).slice(0, 25) });
|
|
2310
3013
|
}
|
|
2311
|
-
|
|
3014
|
+
sel.value = opt.value;
|
|
3015
|
+
sel.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3016
|
+
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3017
|
+
return JSON.stringify({ found: true, wanted: opt.value, actual: sel.value, text: opt.text.trim() });
|
|
2312
3018
|
})()
|
|
2313
3019
|
`);
|
|
2314
|
-
|
|
3020
|
+
let r; try { r = JSON.parse(valg); } catch { r = null; }
|
|
3021
|
+
if (!r) return { ok: false, type: 'native_select', error: 'Kunne ikke laese resultatet af valget' };
|
|
3022
|
+
if (!r.found) return { ok: false, type: 'native_select', error: r.error, available: r.available };
|
|
3023
|
+
if (r.actual !== r.wanted) {
|
|
3024
|
+
// Reagerede siden ved at rulle valget tilbage (React-styret select), skal det siges.
|
|
3025
|
+
return { ok: false, type: 'native_select', error: `Valget blev rullet tilbage: satte "${r.wanted}", feltet staar paa "${r.actual}"` };
|
|
3026
|
+
}
|
|
3027
|
+
return { ok: true, type: 'native_select', selected: r.text, value: r.actual };
|
|
2315
3028
|
}
|
|
2316
3029
|
|
|
2317
3030
|
// Custom dropdown (Angular Material, React Select, etc.)
|
|
@@ -2324,57 +3037,75 @@ async function dispatch(port, method, params) {
|
|
|
2324
3037
|
await new Promise(r => setTimeout(r, params.wait || 300));
|
|
2325
3038
|
|
|
2326
3039
|
// Step 3: Find and click the option by text
|
|
2327
|
-
const option = await resolveElement(tab.id, `text=${
|
|
2328
|
-
if (!option) return { ok: false, error: 'Option not found: ' +
|
|
2329
|
-
await debuggerClick(tab.id, option.x, option.y);
|
|
2330
|
-
|
|
2331
|
-
|
|
3040
|
+
const option = await resolveElement(tab.id, `text=${oensket}`);
|
|
3041
|
+
if (!option) return { ok: false, error: 'Option not found: ' + oensket };
|
|
3042
|
+
const valgKlik = await debuggerClick(tab.id, option.x, option.y);
|
|
3043
|
+
|
|
3044
|
+
// MAALT 22/8 ved review: aerlighedsfixet blev kun anvendt paa native-grenen
|
|
3045
|
+
// ovenfor. Her stod stadig `return { ok: true }` ubetinget, selv om resultatet
|
|
3046
|
+
// af klikket var beregnet og smidt vaek — praecis den fejl der blev lukket to
|
|
3047
|
+
// gange andre steder samme dag. En brugerdefineret dropdown hvor klikket ikke
|
|
3048
|
+
// blev taget imod, meldte altsaa stadig succes.
|
|
3049
|
+
// MAALT 22/8 (tredje gang samme fejlklasse): `ok: true` stod hardkodet, og `landed`
|
|
3050
|
+
// blev blot spredt ind ved siden af. En dropdown hvor klikket ikke blev taget imod
|
|
3051
|
+
// svarede altsaa {ok:true, landed:false} — og agenten laeser ok. Beskrivelsen lover
|
|
3052
|
+
// ordret "it never reports success without the field actually changing".
|
|
3053
|
+
return {
|
|
3054
|
+
...(valgKlik || {}),
|
|
3055
|
+
ok: valgKlik?.landed !== false,
|
|
3056
|
+
type: 'custom_dropdown',
|
|
3057
|
+
selected: oensket,
|
|
3058
|
+
...(valgKlik?.landed === false
|
|
3059
|
+
? { error: 'Klikket paa muligheden blev ikke taget imod af siden: ' + oensket }
|
|
3060
|
+
: {}),
|
|
3061
|
+
};
|
|
2332
3062
|
}
|
|
2333
3063
|
|
|
2334
3064
|
case 'handle_dialog': {
|
|
2335
|
-
// Auto-handle JS alert/confirm/prompt dialogs
|
|
2336
|
-
// Must be set up BEFORE the dialog appears
|
|
2337
3065
|
const tab = await getSessionTab(port);
|
|
2338
3066
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
2339
|
-
const action = params.action
|
|
3067
|
+
const action = params.action === 'dismiss' ? 'dismiss' : 'accept';
|
|
2340
3068
|
const promptText = params.text || '';
|
|
3069
|
+
const vent = params.wait === true; // gammel, blokerende adfaerd
|
|
3070
|
+
const levetid = params.timeout || 60000;
|
|
2341
3071
|
|
|
2342
3072
|
await debuggerAttach(tab.id);
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
3073
|
+
await cdpSend(tab.id, 'Page.enable', {});
|
|
3074
|
+
afvaebnDialog(tab.id, 'en ny armering overtog denne fane'); // kun én ad gangen
|
|
3075
|
+
|
|
3076
|
+
let opfyld;
|
|
3077
|
+
const svar = new Promise((resolve) => { opfyld = resolve; });
|
|
3078
|
+
|
|
3079
|
+
const listener = (source, method, eventParams) => {
|
|
3080
|
+
if (source.tabId !== tab.id || method !== 'Page.javascriptDialogOpening') return;
|
|
3081
|
+
afvaebnDialog(tab.id); // uden grund: vi svarer selv lige nedenfor
|
|
3082
|
+
cdpSend(tab.id, 'Page.handleJavaScriptDialog', {
|
|
3083
|
+
accept: action === 'accept',
|
|
3084
|
+
promptText,
|
|
3085
|
+
})
|
|
3086
|
+
.then(() => opfyld({ ok: true, dialog_type: eventParams.type, message: eventParams.message, action }))
|
|
3087
|
+
.catch((e) => opfyld({ ok: false, error: e.message }));
|
|
3088
|
+
};
|
|
2346
3089
|
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
resolve({ ok: false, error: 'No dialog appeared within timeout' });
|
|
2352
|
-
}, params.timeout || 10000);
|
|
3090
|
+
const timer = setTimeout(() => {
|
|
3091
|
+
afvaebnDialog(tab.id);
|
|
3092
|
+
opfyld({ ok: false, error: `Ingen dialog dukkede op inden for ${levetid} ms` });
|
|
3093
|
+
}, levetid);
|
|
2353
3094
|
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
clearTimeout(timeout);
|
|
2358
|
-
|
|
2359
|
-
cdpSend(tab.id, 'Page.handleJavaScriptDialog', {
|
|
2360
|
-
accept: action === 'accept',
|
|
2361
|
-
promptText: promptText,
|
|
2362
|
-
}).then(() => {
|
|
2363
|
-
resolve({
|
|
2364
|
-
ok: true,
|
|
2365
|
-
dialog_type: eventParams.type,
|
|
2366
|
-
message: eventParams.message,
|
|
2367
|
-
action,
|
|
2368
|
-
});
|
|
2369
|
-
}).catch(e => resolve({ ok: false, error: e.message }));
|
|
2370
|
-
};
|
|
2371
|
-
chrome.debugger.onEvent.addListener(listener);
|
|
2372
|
-
});
|
|
3095
|
+
armeredeDialoger.set(tab.id, { listener, timer, action, opfyld });
|
|
3096
|
+
dialogLoefter.set(tab.id, svar);
|
|
3097
|
+
chrome.debugger.onEvent.addListener(listener);
|
|
2373
3098
|
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
3099
|
+
if (vent) return await svar;
|
|
3100
|
+
|
|
3101
|
+
// Armeret. Debuggeren bliver siddende — frakobler vi her, doer lytteren med den.
|
|
3102
|
+
return {
|
|
3103
|
+
ok: true,
|
|
3104
|
+
armed: true,
|
|
3105
|
+
action,
|
|
3106
|
+
expires_in_ms: levetid,
|
|
3107
|
+
note: 'Naeste dialog paa denne fane haandteres automatisk. Klik nu paa det der aabner den.',
|
|
3108
|
+
};
|
|
2378
3109
|
}
|
|
2379
3110
|
|
|
2380
3111
|
case 'wait_for_network': {
|
|
@@ -2700,7 +3431,24 @@ async function dispatch(port, method, params) {
|
|
|
2700
3431
|
setTimeout(() => { if (document.getElementById('a360-overlay')) { overlay.remove(); resolve({ acknowledged: false, action: 'timeout', values: {} }); } }, timeout);
|
|
2701
3432
|
});
|
|
2702
3433
|
},
|
|
2703
|
-
|
|
3434
|
+
// MAALT 21/8: her stod `params.title` raat. Skemaet siger at title er VALGFRI
|
|
3435
|
+
// med standarden "Agent360 — Action Required", men udelades den, er vaerdien
|
|
3436
|
+
// undefined — og chrome.scripting.executeScript afviser hele kaldet med
|
|
3437
|
+
// "Error at property 'args': Error at index 1: Value is unserializable".
|
|
3438
|
+
// Altsaa styrtede human-in-the-loop-vaerktoejet hver gang en agent fulgte sit
|
|
3439
|
+
// eget skema. Det blev aldrig fanget, fordi ask_user stod som "springes over"
|
|
3440
|
+
// i flowtesten — den eneste der kunne have set det.
|
|
3441
|
+
//
|
|
3442
|
+
// Alle argumenter tvinges nu til serialiserbare vaerdier, og standarden
|
|
3443
|
+
// anvendes der hvor den er lovet.
|
|
3444
|
+
args: [
|
|
3445
|
+
String(params.message ?? ''),
|
|
3446
|
+
String(params.title ?? 'Agent360 — Action Required'),
|
|
3447
|
+
Array.isArray(fields) ? fields : [],
|
|
3448
|
+
Boolean(hasFields),
|
|
3449
|
+
Number(timeout) || 120000,
|
|
3450
|
+
String(session.label ?? 'Claude'),
|
|
3451
|
+
],
|
|
2704
3452
|
world: 'MAIN',
|
|
2705
3453
|
});
|
|
2706
3454
|
|
|
@@ -2721,13 +3469,35 @@ async function dispatch(port, method, params) {
|
|
|
2721
3469
|
return { error: `Frame ${frameIndex} not found. Available: ${frames?.length || 0} frames`, frames: frames?.map((f, i) => ({ index: i, url: f.url })) };
|
|
2722
3470
|
}
|
|
2723
3471
|
const frameId = frames[frameIndex].frameId;
|
|
3472
|
+
// MAALT 21/8: her stod `func: new Function('return (' + code + ')')`. Den byggede
|
|
3473
|
+
// funktionen i SERVICE-WORKEREN, hvor udvidelsens egen CSP forbyder eval — saa
|
|
3474
|
+
// vaerktoejet fejlede paa hver eneste side, ogsaa med koden '1+1':
|
|
3475
|
+
// "Evaluating a string as JavaScript violates ... 'unsafe-eval' is not allowed".
|
|
3476
|
+
// execute_script loeser det samme problem korrekt: send koden med som ARGUMENT og
|
|
3477
|
+
// byg funktionen INDE i den injicerede func, hvor sidens egen CSP gaelder. Samme
|
|
3478
|
+
// vej her.
|
|
3479
|
+
//
|
|
3480
|
+
// Og som i execute_script (v1.26): accepter `script` som alias for `code`. Samme
|
|
3481
|
+
// navne-uoverensstemmelse har foer faaet vaerktoejer til at se brudte ud i tavshed.
|
|
3482
|
+
if (params.code == null && typeof params.script === 'string') params.code = params.script;
|
|
2724
3483
|
const code = params.code || 'document.body.innerText.slice(0, 5000)';
|
|
2725
3484
|
const [result] = await chrome.scripting.executeScript({
|
|
2726
3485
|
target: { tabId: tab.id, frameIds: [frameId] },
|
|
2727
|
-
func: new Function('return (' + code + ')'),
|
|
2728
3486
|
world: 'MAIN',
|
|
3487
|
+
args: [code],
|
|
3488
|
+
func: (codeStr) => {
|
|
3489
|
+
try {
|
|
3490
|
+
return { __ok: true, value: new Function('return (' + codeStr + ')')() };
|
|
3491
|
+
} catch (e) {
|
|
3492
|
+
return { __scriptingError: true, message: String(e?.message || e) };
|
|
3493
|
+
}
|
|
3494
|
+
},
|
|
2729
3495
|
});
|
|
2730
|
-
|
|
3496
|
+
const r = result?.result;
|
|
3497
|
+
if (r && r.__scriptingError) {
|
|
3498
|
+
return { ok: false, error: r.message, frame_url: frames[frameIndex].url };
|
|
3499
|
+
}
|
|
3500
|
+
return { result: r?.value, frame_url: frames[frameIndex].url };
|
|
2731
3501
|
}
|
|
2732
3502
|
|
|
2733
3503
|
case 'list_frames': {
|
|
@@ -2765,6 +3535,9 @@ async function dispatch(port, method, params) {
|
|
|
2765
3535
|
if (!session.tabIds.has(tabId)) {
|
|
2766
3536
|
throw new Error(`Tab ${tabId} does not belong to this session (${session.label})`);
|
|
2767
3537
|
}
|
|
3538
|
+
// Maerk lukningen som agentens egen. Ellers laeser onRemoved den tomme session som
|
|
3539
|
+
// "brugeren er faerdig" og lukker serveren ned midt i samtalen (MAALT 22/8).
|
|
3540
|
+
agentLukkedeFaner.add(tabId);
|
|
2768
3541
|
await chrome.tabs.remove(tabId);
|
|
2769
3542
|
session.tabIds.delete(tabId);
|
|
2770
3543
|
if (session.activeTabId === tabId) session.activeTabId = null;
|
|
@@ -2828,8 +3601,19 @@ async function dispatch(port, method, params) {
|
|
|
2828
3601
|
return info;
|
|
2829
3602
|
}
|
|
2830
3603
|
|
|
2831
|
-
// Get the DOM node ID for the file input
|
|
2832
|
-
|
|
3604
|
+
// Get the DOM node ID for the file input.
|
|
3605
|
+
//
|
|
3606
|
+
// MAALT 21/8: her stod `const { result: docResult } = await cdpSend(...)`.
|
|
3607
|
+
// Runtime.evaluate ovenfor svarer {result:{...}}, men DOM.getDocument svarer
|
|
3608
|
+
// {root:{...}} — moenstret var kopieret fra det ene kald til det andet. Saa
|
|
3609
|
+
// docResult var undefined, og vaerktoejet doede paa
|
|
3610
|
+
// "Cannot read properties of undefined (reading 'root')" ved HVERT eneste kald.
|
|
3611
|
+
// browser_upload_file kunne ikke uploade en fil paa nogen side overhovedet.
|
|
3612
|
+
const docResult = await cdpSend(tab.id, 'DOM.getDocument', {});
|
|
3613
|
+
if (!docResult?.root?.nodeId) {
|
|
3614
|
+
await debuggerDetach(tab.id);
|
|
3615
|
+
return { ok: false, error: 'DOM.getDocument gav intet rod-element' };
|
|
3616
|
+
}
|
|
2833
3617
|
const { nodeId } = await cdpSend(tab.id, 'DOM.querySelector', {
|
|
2834
3618
|
nodeId: docResult.root.nodeId,
|
|
2835
3619
|
selector: selector,
|
|
@@ -2840,8 +3624,16 @@ async function dispatch(port, method, params) {
|
|
|
2840
3624
|
return { found: false, error: 'Could not get DOM node for file input' };
|
|
2841
3625
|
}
|
|
2842
3626
|
|
|
2843
|
-
// Set files on the input using CDP
|
|
2844
|
-
|
|
3627
|
+
// Set files on the input using CDP.
|
|
3628
|
+
// `file_path` accepteres som alias for `file`/`files` — praecis samme navne-faelde
|
|
3629
|
+
// som execute_script fik lukket i v1.26. Et forkert navn gav [undefined] og en
|
|
3630
|
+
// upload der saa ud til at lykkes.
|
|
3631
|
+
const files = Array.isArray(params.files) ? params.files
|
|
3632
|
+
: [params.files || params.file || params.file_path].filter(Boolean);
|
|
3633
|
+
if (!files.length) {
|
|
3634
|
+
await debuggerDetach(tab.id);
|
|
3635
|
+
return { ok: false, error: 'Ingen fil angivet. Brug `files` (array) eller `file` (enkelt sti).' };
|
|
3636
|
+
}
|
|
2845
3637
|
await cdpSend(tab.id, 'DOM.setFileInputFiles', {
|
|
2846
3638
|
nodeId: nodeId,
|
|
2847
3639
|
files: files,
|
|
@@ -2975,10 +3767,10 @@ async function clickRecaptchaCheckbox(tabId) {
|
|
|
2975
3767
|
type: 'mouseMoved', x: pos.x, y: pos.y,
|
|
2976
3768
|
});
|
|
2977
3769
|
await new Promise(r => setTimeout(r, 100 + Math.random() * 200));
|
|
2978
|
-
await
|
|
3770
|
+
await dispatchTaalmodigt(tabId, {
|
|
2979
3771
|
type: 'mousePressed', x: pos.x, y: pos.y, button: 'left', clickCount: 1,
|
|
2980
3772
|
});
|
|
2981
|
-
await
|
|
3773
|
+
await dispatchTaalmodigt(tabId, {
|
|
2982
3774
|
type: 'mouseReleased', x: pos.x, y: pos.y, button: 'left', clickCount: 1,
|
|
2983
3775
|
});
|
|
2984
3776
|
await debuggerDetach(tabId);
|
|
@@ -3039,10 +3831,10 @@ async function clickCaptchaGridCells(tabId, cells) {
|
|
|
3039
3831
|
type: 'mouseMoved', x: ox, y: oy,
|
|
3040
3832
|
});
|
|
3041
3833
|
await new Promise(r => setTimeout(r, 150 + Math.random() * 300));
|
|
3042
|
-
await
|
|
3834
|
+
await dispatchTaalmodigt(tabId, {
|
|
3043
3835
|
type: 'mousePressed', x: ox, y: oy, button: 'left', clickCount: 1,
|
|
3044
3836
|
});
|
|
3045
|
-
await
|
|
3837
|
+
await dispatchTaalmodigt(tabId, {
|
|
3046
3838
|
type: 'mouseReleased', x: ox, y: oy, button: 'left', clickCount: 1,
|
|
3047
3839
|
});
|
|
3048
3840
|
await new Promise(r => setTimeout(r, 200 + Math.random() * 400));
|
|
@@ -3066,9 +3858,42 @@ async function clickCaptchaGridCells(tabId, cells) {
|
|
|
3066
3858
|
ensureOffscreen().catch(console.error);
|
|
3067
3859
|
|
|
3068
3860
|
chrome.runtime.onStartup.addListener(() => ensureOffscreen().catch(console.error));
|
|
3069
|
-
|
|
3861
|
+
// onInstalled fyrer ved installation, opdatering OG ved "Genindlaes" paa
|
|
3862
|
+
// chrome://extensions. I alle tre tilfaelde er koden aendret pr. definition, saa en
|
|
3863
|
+
// overlevende bro er per definition forældet — uanset hvor villigt den svarer paa ping.
|
|
3864
|
+
// MAALT 22/8: uden tvangen slog en genindlaesning aldrig igennem til broen, og
|
|
3865
|
+
// udvikling krævede en fuld genstart af Chrome hver gang.
|
|
3866
|
+
chrome.runtime.onInstalled.addListener(async () => {
|
|
3867
|
+
try {
|
|
3868
|
+
if (await chrome.offscreen.hasDocument()) await chrome.offscreen.closeDocument();
|
|
3869
|
+
} catch (e) {
|
|
3870
|
+
console.warn('[BG] kunne ikke lukke broen ved genindlaesning:', e?.message || e);
|
|
3871
|
+
}
|
|
3872
|
+
await chrome.storage.local.set({ offscreenGenskabt: 0, offscreenPauseTil: 0 });
|
|
3873
|
+
ensureOffscreen().catch(console.error);
|
|
3874
|
+
});
|
|
3875
|
+
|
|
3876
|
+
// Hjerteslag der genskaber offscreen-dokumentet hvis Chrome har ryddet det.
|
|
3877
|
+
//
|
|
3878
|
+
// FEJL RETTET 19/8: alarmen blev oprettet paa oeverste niveau ved HVER
|
|
3879
|
+
// service-worker-opstart. chrome.alarms.create() med et navn der allerede
|
|
3880
|
+
// findes NULSTILLER nedtaellingen — saa hvis workeren vaagnede oftere end
|
|
3881
|
+
// hvert minut (hvilket den goer ved tab-events, beskeder, navigation),
|
|
3882
|
+
// naaede alarmen aldrig at fyre. Resultat: offscreen-dokumentet doede, intet
|
|
3883
|
+
// genskabte det, og forbindelsen til MCP-serveren kom aldrig tilbage foer
|
|
3884
|
+
// nogen genindlaeste extensionen i haanden.
|
|
3885
|
+
//
|
|
3886
|
+
// Nu oprettes den kun hvis den ikke findes, saa nedtaellingen faar lov at
|
|
3887
|
+
// loebe faerdig.
|
|
3888
|
+
chrome.alarms.get('ensure-offscreen', (eksisterende) => {
|
|
3889
|
+
if (!eksisterende) {
|
|
3890
|
+
chrome.alarms.create('ensure-offscreen', {
|
|
3891
|
+
periodInMinutes: 1,
|
|
3892
|
+
delayInMinutes: 1,
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
});
|
|
3070
3896
|
|
|
3071
|
-
chrome.alarms.create('ensure-offscreen', { periodInMinutes: 1 });
|
|
3072
3897
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
3073
3898
|
if (alarm.name === 'ensure-offscreen') {
|
|
3074
3899
|
ensureOffscreen().catch(console.error);
|