@agent360/browser-mcp 1.28.1 → 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 +1309 -194
- 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 +380 -109
- package/package.json +5 -5
- 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
|
|
|
@@ -211,6 +219,8 @@ async function evictOldestTabs(session, justAddedTabId) {
|
|
|
211
219
|
async function addTabToSession(port, tabId) {
|
|
212
220
|
const session = getSession(port);
|
|
213
221
|
session.tabIds.add(tabId);
|
|
222
|
+
// Sessionen lever igen — aflys en eventuel port-frigivelse (se tabs.onRemoved).
|
|
223
|
+
chrome.alarms.clear(`frigiv-${port}`).catch(() => {});
|
|
214
224
|
|
|
215
225
|
// LRU eviction: når sessionen overstiger cap, luk de ældste tabs.
|
|
216
226
|
if (session.tabIds.size > MAX_TABS_PER_SESSION) {
|
|
@@ -247,13 +257,62 @@ async function releaseSession(port) {
|
|
|
247
257
|
const session = sessions.get(port);
|
|
248
258
|
if (!session) return;
|
|
249
259
|
|
|
260
|
+
// ── Bad vi selv om frigivelsen? (FUNDET AF REVIEW 7/9) ────────────────────
|
|
261
|
+
//
|
|
262
|
+
// Vejen fra "sessionen er tom" til denne funktion gaar over mindst tre hop:
|
|
263
|
+
// sendMessage -> offscreen sender terminate og lukker WS -> ws.onclose ->
|
|
264
|
+
// session_disconnect. Aabner agenten en fane i de ~50 ms undervejs, lukkede
|
|
265
|
+
// oprydningen herunder DEN fane — paa grundlag af en beslutning der blev truffet
|
|
266
|
+
// foer fanen fandtes. Det er praecis "agenten holder pause og genoptager"-
|
|
267
|
+
// scenariet frigivelsen er bygget til at understoette.
|
|
268
|
+
//
|
|
269
|
+
// En UVENTET afbrydelse (chatten er vaek) skal stadig lukke fanerne — derfor
|
|
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
|
+
|
|
274
|
+
if (frivilligtFrigivet.delete(port)) {
|
|
275
|
+
if (session.tabIds.size) {
|
|
276
|
+
// Sessionen arbejder igen. Behold den, saa naeste binding kan adoptere den
|
|
277
|
+
// (adoptOrphanedSession finder donorer med samme pid OG faner).
|
|
278
|
+
persistSessions();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
sessions.delete(port);
|
|
282
|
+
persistSessions();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
250
286
|
// Detach debugger + close all session tabs
|
|
251
|
-
|
|
252
|
-
|
|
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]) {
|
|
253
294
|
debuggerForceDetach(tabId);
|
|
254
295
|
try {
|
|
255
296
|
await chrome.tabs.remove(tabId);
|
|
256
|
-
|
|
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;
|
|
257
316
|
}
|
|
258
317
|
|
|
259
318
|
sessions.delete(port);
|
|
@@ -401,7 +460,13 @@ async function debuggerAttach(tabId) {
|
|
|
401
460
|
lastMsg = 'attach resolved but Chrome shows tab not attached (ghost — page likely mid-reload)';
|
|
402
461
|
try { await chrome.debugger.detach({ tabId }); } catch {}
|
|
403
462
|
} catch (e) {
|
|
404
|
-
|
|
463
|
+
// MAALT 31/8 af den nye udvidelses-test: her stod `includes('Already attached')`
|
|
464
|
+
// med stort A. Chromes faktiske besked er "Another debugger is already attached
|
|
465
|
+
// to the tab with id: N" — med lille. Tjekket ramte ALDRIG. Resultat: naar en
|
|
466
|
+
// anden debugger havde fanen (DevTools aabent, en anden udvidelse), blev det
|
|
467
|
+
// behandlet som en fejl, proevet tre gange, og kastet — i stedet for bare at
|
|
468
|
+
// bruge den session der allerede fandtes.
|
|
469
|
+
if (/already attached/i.test(e.message || '')) {
|
|
405
470
|
// Chrome side has session — sync local cache
|
|
406
471
|
debuggerAttached.add(tabId);
|
|
407
472
|
return;
|
|
@@ -445,13 +510,26 @@ chrome.debugger.onDetach.addListener((source, reason) => {
|
|
|
445
510
|
// Side-effectful methods (Input.*, DOM.setFileInputFiles) must NEVER auto-retry:
|
|
446
511
|
// Chrome may detach AFTER processing the input (e.g., keystroke triggered navigation),
|
|
447
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.
|
|
448
527
|
const RETRYABLE_CDP_METHODS = new Set([
|
|
449
528
|
'DOM.getDocument',
|
|
450
529
|
'DOM.querySelector',
|
|
451
530
|
'DOM.querySelectorAll',
|
|
452
531
|
'DOM.focus',
|
|
453
532
|
'DOM.describeNode',
|
|
454
|
-
'Runtime.evaluate',
|
|
455
533
|
'Runtime.enable',
|
|
456
534
|
'Page.captureScreenshot',
|
|
457
535
|
'Page.enable',
|
|
@@ -463,6 +541,86 @@ const RETRYABLE_CDP_METHODS = new Set([
|
|
|
463
541
|
// CDP wrapper with auto-recovery: re-attaches on detach errors.
|
|
464
542
|
// For read-only methods (whitelist above), retries once after re-attach.
|
|
465
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
|
+
|
|
466
624
|
async function cdpSend(tabId, method, params = {}) {
|
|
467
625
|
await debuggerAttach(tabId);
|
|
468
626
|
let lastMsg = '';
|
|
@@ -471,7 +629,7 @@ async function cdpSend(tabId, method, params = {}) {
|
|
|
471
629
|
// where Chrome re-detaches between attach and command execution.
|
|
472
630
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
473
631
|
try {
|
|
474
|
-
return await
|
|
632
|
+
return await cdpMedFrist(tabId, method, params);
|
|
475
633
|
} catch (e) {
|
|
476
634
|
const msg = e?.message || String(e);
|
|
477
635
|
const isDetachError =
|
|
@@ -504,6 +662,27 @@ async function cdpSend(tabId, method, params = {}) {
|
|
|
504
662
|
// Faner agenten selv lukkede via close_tab. En tom session betyder kun "arbejdet er slut"
|
|
505
663
|
// hvis det var BRUGEREN der lukkede den sidste fane.
|
|
506
664
|
const agentLukkedeFaner = new Set();
|
|
665
|
+
// Porte hvor VI selv har bedt serveren slippe porten, fordi sessionen var tom.
|
|
666
|
+
// Skelnen betyder alt i releaseSession: en frivillig frigivelse maa aldrig lukke
|
|
667
|
+
// faner, mens en uventet afbrydelse (chatten er vaek) netop skal rydde op.
|
|
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
|
+
}
|
|
507
686
|
|
|
508
687
|
// ── Armerede dialog-haandterere, pr. fane ──────────────────────────────────────
|
|
509
688
|
// MAALT 22/8 af flowtesten: handle_dialog var ubrugelig som den var skrevet. Den
|
|
@@ -536,21 +715,71 @@ function afvaebnDialog(tabId, grund) {
|
|
|
536
715
|
if (grund && a.opfyld) a.opfyld({ ok: false, error: grund });
|
|
537
716
|
}
|
|
538
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
|
+
|
|
539
742
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
540
743
|
afvaebnDialog(tabId, 'fanen blev lukket foer der kom en dialog');
|
|
541
744
|
const lukketAfAgenten = agentLukkedeFaner.delete(tabId);
|
|
542
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();
|
|
543
753
|
for (const [port, session] of sessions) {
|
|
544
754
|
if (!session.tabIds.has(tabId)) continue;
|
|
545
755
|
session.tabIds.delete(tabId);
|
|
546
756
|
if (session.tabIds.size === 0 && !lukketAfAgenten) {
|
|
547
757
|
// Last tab closed — tell offscreen to terminate the MCP server.
|
|
548
758
|
// Resulting WS-close triggers the existing session_disconnect → releaseSession path.
|
|
759
|
+
frivilligtFrigivet.add(port);
|
|
549
760
|
chrome.runtime.sendMessage({ type: 'terminate_mcp_session', port }).catch(() => {});
|
|
761
|
+
} else if (session.tabIds.size === 0) {
|
|
762
|
+
// ── Agenten lukkede selv sin sidste fane (MAALT 7/9-2026) ────────────────
|
|
763
|
+
// Her stod der intet, og det var med vilje: en agent der lukker en fane midt i
|
|
764
|
+
// et forloeb skal ikke miste browseren. Men serverens EGEN instruks siger til
|
|
765
|
+
// hver eneste agent: "ALWAYS close tabs when done". Hver velopdragen chat endte
|
|
766
|
+
// altsaa med at holde sin port til 4-timers-tomgangen udloeb — den dokumenterede
|
|
767
|
+
// god-praksis slog oprydningen ihjel, og 20 porte kunne staa optaget af chats
|
|
768
|
+
// der for laengst var faerdige.
|
|
769
|
+
//
|
|
770
|
+
// Nu: fem minutters henstand. Kommer der en ny fane inden da, aflyses den
|
|
771
|
+
// (se getSessionTab). Sker der intet, bedes serveren slippe porten — den DOER
|
|
772
|
+
// ikke, saa chatten kan hente browseren tilbage naar som helst.
|
|
773
|
+
//
|
|
774
|
+
// chrome.alarms og ikke setTimeout: en MV3-service-worker suspenderes, og en
|
|
775
|
+
// timer ville forsvinde med den.
|
|
776
|
+
chrome.alarms.create(`frigiv-${port}`, { delayInMinutes: 5 });
|
|
777
|
+
persistSessions();
|
|
550
778
|
} else {
|
|
551
779
|
persistSessions();
|
|
552
780
|
}
|
|
553
781
|
}
|
|
782
|
+
})().catch(() => {});
|
|
554
783
|
});
|
|
555
784
|
|
|
556
785
|
// Physical-key `code` for a character, US layout. We used to build this as
|
|
@@ -585,18 +814,9 @@ async function typeCharsAttached(tabId, text) {
|
|
|
585
814
|
for (let i = 0; i < text.length; i++) {
|
|
586
815
|
const char = text[i];
|
|
587
816
|
const code = cdpCodeForChar(char);
|
|
588
|
-
await
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
key: char,
|
|
592
|
-
...(code ? { code } : {}),
|
|
593
|
-
unmodifiedText: char,
|
|
594
|
-
});
|
|
595
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
596
|
-
type: 'keyUp',
|
|
597
|
-
key: char,
|
|
598
|
-
...(code ? { code } : {}),
|
|
599
|
-
});
|
|
817
|
+
await tastParAttached(tabId,
|
|
818
|
+
{ text: char, key: char, ...(code ? { code } : {}), unmodifiedText: char },
|
|
819
|
+
{ key: char, ...(code ? { code } : {}) });
|
|
600
820
|
// Human-like typing: random 30-120ms, occasional longer pause
|
|
601
821
|
const pause = (i > 0 && i % (7 + Math.floor(Math.random() * 5)) === 0)
|
|
602
822
|
? 150 + Math.random() * 200 // thinking pause every ~10 chars
|
|
@@ -653,15 +873,59 @@ async function evaluerTaalmodigt(tabId, params, ms = 3000) {
|
|
|
653
873
|
let faerdig = false;
|
|
654
874
|
const kald = cdpSend(tabId, 'Runtime.evaluate', params)
|
|
655
875
|
.then((r) => { faerdig = true; return r; })
|
|
656
|
-
.catch(() => { faerdig = true; });
|
|
876
|
+
.catch((e) => { faerdig = true; return { __fejl: String(e?.message || e) }; });
|
|
657
877
|
await Promise.race([kald, new Promise((r) => setTimeout(r, ms))]);
|
|
658
878
|
if (faerdig) return kald;
|
|
659
|
-
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 };
|
|
660
920
|
}
|
|
661
921
|
|
|
662
922
|
async function debuggerClick(tabId, x, y) {
|
|
663
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;
|
|
664
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;
|
|
665
929
|
// 0. Capture the DEEPEST target element under the point BEFORE dispatching.
|
|
666
930
|
// Web-components (Google Ads <button-panel>, Material Web) keep their real
|
|
667
931
|
// <button> inside an (open) shadow root, so we pierce shadow roots to reach
|
|
@@ -704,52 +968,135 @@ async function debuggerClick(tabId, x, y) {
|
|
|
704
968
|
// 0 on release) plus a small press→release gap are REQUIRED for Chrome to
|
|
705
969
|
// synthesize a *trusted* 'click' from the pair. Without them, web-components
|
|
706
970
|
// that gate on the trusted click event (Google Ads, Material Web) never fire.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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;
|
|
714
987
|
// 3. Framework fallback — only if the captured target is STILL connected (i.e.
|
|
715
988
|
// the trusted click in step 2 did not already handle it). Settle delay lets
|
|
716
989
|
// SPA re-renders (Google Ads) detach the element first. Fires a full pointer
|
|
717
990
|
// + mouse sequence on the shadow-pierced target, then React/Angular handlers.
|
|
718
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.
|
|
719
999
|
const settle = await evaluerTaalmodigt(tabId, {
|
|
720
1000
|
returnByValue: true,
|
|
721
1001
|
expression: `(() => {
|
|
722
1002
|
const el = window.__bmcpClickTarget;
|
|
723
1003
|
const landed = window.__bmcpClicked === true;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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();
|
|
728
1034
|
const opts = { bubbles: true, cancelable: true, composed: true, view: window, clientX: ${x}, clientY: ${y} };
|
|
729
|
-
|
|
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) {}
|
|
730
1038
|
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
731
|
-
try { el.dispatchEvent(new PointerEvent('pointerup',
|
|
1039
|
+
try { el.dispatchEvent(new PointerEvent('pointerup', { ...mus, buttons: 0 })); } catch (e) {}
|
|
732
1040
|
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
733
|
-
|
|
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();
|
|
734
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
|
+
}
|
|
735
1070
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (fiber.memoizedProps?.onClick) { fiber.memoizedProps.onClick(new MouseEvent('click', {bubbles:true})); break; }
|
|
742
|
-
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));
|
|
743
1076
|
}
|
|
744
1077
|
}
|
|
745
1078
|
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
};
|
|
753
1100
|
})()`,
|
|
754
1101
|
});
|
|
755
1102
|
const vaerdi = settle?.result?.value ?? null;
|
|
@@ -766,7 +1113,10 @@ async function debuggerClick(tabId, x, y) {
|
|
|
766
1113
|
dialogLoefter.delete(tabId);
|
|
767
1114
|
}
|
|
768
1115
|
}
|
|
769
|
-
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;
|
|
770
1120
|
} finally {
|
|
771
1121
|
await debuggerDetach(tabId);
|
|
772
1122
|
}
|
|
@@ -798,20 +1148,21 @@ async function evalAttached(tabId, expression) {
|
|
|
798
1148
|
return result.result?.value;
|
|
799
1149
|
}
|
|
800
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
|
+
|
|
801
1162
|
// Select-all + Backspace. Assumes the debugger is already attached.
|
|
802
1163
|
async function clearFieldAttached(tabId) {
|
|
803
|
-
await
|
|
804
|
-
|
|
805
|
-
});
|
|
806
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
807
|
-
type: 'keyUp', key: 'a', code: 'KeyA',
|
|
808
|
-
});
|
|
809
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
810
|
-
type: 'keyDown', key: 'Backspace', code: 'Backspace',
|
|
811
|
-
});
|
|
812
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
813
|
-
type: 'keyUp', key: 'Backspace', code: 'Backspace',
|
|
814
|
-
});
|
|
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' });
|
|
815
1166
|
}
|
|
816
1167
|
|
|
817
1168
|
async function debuggerFill(tabId, selector, value) {
|
|
@@ -847,6 +1198,37 @@ async function debuggerFill(tabId, selector, value) {
|
|
|
847
1198
|
try {
|
|
848
1199
|
await clearFieldAttached(tabId);
|
|
849
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
|
+
|
|
850
1232
|
// Fast path: one trusted InputEvent instead of N key events. This is the same
|
|
851
1233
|
// primitive set_combobox and set_date already rely on, it avoids per-key `code`
|
|
852
1234
|
// mapping entirely, and it turns a 40-character value from ~3 seconds of
|
|
@@ -918,11 +1300,41 @@ async function scriptingClick(tabId, selector) {
|
|
|
918
1300
|
}
|
|
919
1301
|
if (!el) return { ok: false, reason: 'not_found' };
|
|
920
1302
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
921
|
-
|
|
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) {}
|
|
922
1327
|
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
1328
|
+
try { el.dispatchEvent(new PointerEvent('pointerup', { ...mus, buttons: 0 })); } catch (e) {}
|
|
923
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();
|
|
924
1333
|
el.click();
|
|
925
|
-
|
|
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 };
|
|
926
1338
|
},
|
|
927
1339
|
args: [selector],
|
|
928
1340
|
});
|
|
@@ -1156,6 +1568,20 @@ async function offscreenSvarer() {
|
|
|
1156
1568
|
const MAX_OFFSCREEN_GENSKAB = 3;
|
|
1157
1569
|
const OFFSCREEN_PAUSE_MS = 10 * 60 * 1000;
|
|
1158
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
|
+
|
|
1159
1585
|
async function ensureOffscreen() {
|
|
1160
1586
|
const findes = await chrome.offscreen.hasDocument();
|
|
1161
1587
|
|
|
@@ -1213,8 +1639,15 @@ async function ensureOffscreen() {
|
|
|
1213
1639
|
// baerer den aeldre version, hvilket er praecis den skelnen tjekket skal bruge.
|
|
1214
1640
|
let minVersion = '';
|
|
1215
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();
|
|
1216
1648
|
await chrome.offscreen.createDocument({
|
|
1217
|
-
url: 'offscreen.html' + (minVersion ? '?v=' + encodeURIComponent(minVersion) : '')
|
|
1649
|
+
url: 'offscreen.html' + (minVersion ? '?v=' + encodeURIComponent(minVersion) : '')
|
|
1650
|
+
+ (porteParam ? (minVersion ? '&' : '?') + 'porte=' + porteParam : ''),
|
|
1218
1651
|
reasons: ['WORKERS'],
|
|
1219
1652
|
justification: 'Maintain persistent WebSocket connection to local MCP server',
|
|
1220
1653
|
});
|
|
@@ -1224,22 +1657,42 @@ async function ensureOffscreen() {
|
|
|
1224
1657
|
|
|
1225
1658
|
const SENSITIVE = new Set(['get_cookies', 'get_local_storage', 'execute_script', 'extract_token']);
|
|
1226
1659
|
|
|
1227
|
-
|
|
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) {
|
|
1228
1666
|
const category = SENSITIVE.has(method) ? 'sensitive' : 'safe';
|
|
1229
1667
|
const session = sessions.get(port);
|
|
1230
1668
|
const entry = {
|
|
1231
1669
|
time: Date.now(),
|
|
1232
1670
|
method,
|
|
1233
|
-
params: JSON.stringify(params).slice(0, 200),
|
|
1234
1671
|
category,
|
|
1235
1672
|
session: session?.label || `Port ${port}`,
|
|
1236
1673
|
color: session?.color || 'grey',
|
|
1237
1674
|
};
|
|
1238
|
-
|
|
1239
|
-
actionLog.
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
+
}
|
|
1243
1696
|
}
|
|
1244
1697
|
|
|
1245
1698
|
// ── Message Handler — receives commands from offscreen.js ──────────────────
|
|
@@ -1247,7 +1700,7 @@ function logAction(port, method, params) {
|
|
|
1247
1700
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
1248
1701
|
if (msg.type === 'mcp_command') {
|
|
1249
1702
|
const port = msg.port;
|
|
1250
|
-
logAction(port, msg.method
|
|
1703
|
+
logAction(port, msg.method);
|
|
1251
1704
|
// Restore sessions from storage (service worker may have restarted), then take over any
|
|
1252
1705
|
// session orphaned by an unclean disconnect so navigate→click keeps hitting the same tab.
|
|
1253
1706
|
restoreSessions()
|
|
@@ -1314,10 +1767,35 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
1314
1767
|
|
|
1315
1768
|
const OAUTH_DOMAINS = ['accounts.google.com', 'login.microsoftonline.com', 'github.com/login/oauth', 'slack.com/oauth', 'app.hubspot.com/oauth'];
|
|
1316
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.
|
|
1317
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();
|
|
1318
1788
|
|
|
1319
1789
|
chrome.tabs.onCreated.addListener(async (tab) => {
|
|
1320
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);
|
|
1321
1799
|
|
|
1322
1800
|
// Auto-claim OAuth popups for the session that opened them
|
|
1323
1801
|
if (tab.pendingUrl || tab.url) {
|
|
@@ -1375,7 +1853,9 @@ function parsePlaceholderFormat(placeholder) {
|
|
|
1375
1853
|
const order = parts.map(p => p.includes('Y') ? 'Y' : p.includes('M') ? 'M' : p.includes('D') ? 'D' : null);
|
|
1376
1854
|
if (order.includes(null) || new Set(order).size !== 3) return null;
|
|
1377
1855
|
const padded = parts.map(p => p.length >= 2);
|
|
1378
|
-
|
|
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 };
|
|
1379
1859
|
}
|
|
1380
1860
|
|
|
1381
1861
|
function isoToFormat(iso, fmt) {
|
|
@@ -1383,7 +1863,7 @@ function isoToFormat(iso, fmt) {
|
|
|
1383
1863
|
if (!m) throw new Error('Invalid ISO date: ' + iso);
|
|
1384
1864
|
const [, y, mo, d] = m;
|
|
1385
1865
|
return fmt.order.map((slot, i) => {
|
|
1386
|
-
if (slot === 'Y') return y;
|
|
1866
|
+
if (slot === 'Y') return fmt.lengths && fmt.lengths[i] === 2 ? y.slice(2) : y;
|
|
1387
1867
|
if (slot === 'M') return fmt.padded[i] ? mo : String(parseInt(mo, 10));
|
|
1388
1868
|
if (slot === 'D') return fmt.padded[i] ? d : String(parseInt(d, 10));
|
|
1389
1869
|
}).join(fmt.sep);
|
|
@@ -1406,17 +1886,53 @@ function parseMonthYearText(text) {
|
|
|
1406
1886
|
return null;
|
|
1407
1887
|
}
|
|
1408
1888
|
|
|
1409
|
-
function valueLooksLikeIso(
|
|
1410
|
-
if (!
|
|
1889
|
+
function valueLooksLikeIso(raaVaerdi, iso, fmt) {
|
|
1890
|
+
if (!raaVaerdi || !iso) return false;
|
|
1411
1891
|
const [y, m, d] = iso.split('-');
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
if (
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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;
|
|
1420
1936
|
}
|
|
1421
1937
|
|
|
1422
1938
|
async function getDateInputInfo(tabId, selector) {
|
|
@@ -1485,25 +2001,9 @@ async function setDateMaskedTyping(tabId, selector, iso, format) {
|
|
|
1485
2001
|
await debuggerFocus(tabId, selector);
|
|
1486
2002
|
await debuggerAttach(tabId);
|
|
1487
2003
|
try {
|
|
1488
|
-
await
|
|
1489
|
-
type: 'keyDown', key: 'a', code: 'KeyA', modifiers: SELECT_ALL_MODS,
|
|
1490
|
-
});
|
|
1491
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1492
|
-
type: 'keyUp', key: 'a', code: 'KeyA',
|
|
1493
|
-
});
|
|
1494
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1495
|
-
type: 'keyDown', key: 'Backspace', code: 'Backspace',
|
|
1496
|
-
});
|
|
1497
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1498
|
-
type: 'keyUp', key: 'Backspace', code: 'Backspace',
|
|
1499
|
-
});
|
|
2004
|
+
await clearFieldAttached(tabId);
|
|
1500
2005
|
await cdpSend(tabId, 'Input.insertText', { text: formatted });
|
|
1501
|
-
await
|
|
1502
|
-
type: 'keyDown', key: 'Tab', code: 'Tab',
|
|
1503
|
-
});
|
|
1504
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1505
|
-
type: 'keyUp', key: 'Tab', code: 'Tab',
|
|
1506
|
-
});
|
|
2006
|
+
await tastParAttached(tabId, { key: 'Tab', code: 'Tab' }, { key: 'Tab', code: 'Tab' });
|
|
1507
2007
|
} finally {
|
|
1508
2008
|
await debuggerDetach(tabId);
|
|
1509
2009
|
}
|
|
@@ -1662,12 +2162,7 @@ async function setDatePicker(tabId, selector, iso) {
|
|
|
1662
2162
|
await debuggerAttach(tabId);
|
|
1663
2163
|
try {
|
|
1664
2164
|
const key = delta > 0 ? 'PageDown' : 'PageUp';
|
|
1665
|
-
await
|
|
1666
|
-
type: 'keyDown', key, code: key,
|
|
1667
|
-
});
|
|
1668
|
-
await cdpSend(tabId, 'Input.dispatchKeyEvent', {
|
|
1669
|
-
type: 'keyUp', key, code: key,
|
|
1670
|
-
});
|
|
2165
|
+
await tastParAttached(tabId, { key, code: key }, { key, code: key });
|
|
1671
2166
|
} finally {
|
|
1672
2167
|
await debuggerDetach(tabId);
|
|
1673
2168
|
}
|
|
@@ -2010,6 +2505,26 @@ async function setCombobox(tabId, selector, values, opts = {}) {
|
|
|
2010
2505
|
const waitIterations = Math.max(1, Math.ceil(waitMs / 100));
|
|
2011
2506
|
const results = [];
|
|
2012
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
|
+
|
|
2013
2528
|
for (const val of valueList) {
|
|
2014
2529
|
try {
|
|
2015
2530
|
const inputEl = await resolveElement(tabId, selector);
|
|
@@ -2416,24 +2931,82 @@ async function dispatch(port, method, params) {
|
|
|
2416
2931
|
if (tab.url.startsWith('chrome://') || tab.url.startsWith('about:')) {
|
|
2417
2932
|
throw new Error(`Cannot screenshot ${tab.url.split(':')[0]}: pages — navigate to a real page first`);
|
|
2418
2933
|
}
|
|
2419
|
-
//
|
|
2420
|
-
//
|
|
2421
|
-
|
|
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' });
|
|
2422
2979
|
try {
|
|
2423
|
-
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 });
|
|
2424
2989
|
try {
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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
|
+
}
|
|
2431
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;
|
|
2432
3009
|
}
|
|
2433
|
-
} catch {
|
|
2434
|
-
// CDP failed entirely — native tabs API (needs the tab visible in its window).
|
|
2435
|
-
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
|
|
2436
|
-
return { image: dataUrl };
|
|
2437
3010
|
}
|
|
2438
3011
|
};
|
|
2439
3012
|
|
|
@@ -2441,6 +3014,13 @@ async function dispatch(port, method, params) {
|
|
|
2441
3014
|
try {
|
|
2442
3015
|
return await tryCapture();
|
|
2443
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;
|
|
2444
3024
|
// Both methods failed → the window is genuinely OCCLUDED (covered by other windows),
|
|
2445
3025
|
// so Chrome's compositor produced no frames. LAST RESORT ONLY: raise the window to
|
|
2446
3026
|
// de-occlude it, capture, then RESTORE the user's previously-focused window. This
|
|
@@ -2450,7 +3030,7 @@ async function dispatch(port, method, params) {
|
|
|
2450
3030
|
await chrome.windows.update(tab.windowId, { focused: true, state: 'normal' });
|
|
2451
3031
|
await chrome.tabs.update(tab.id, { active: true }).catch(() => {});
|
|
2452
3032
|
await new Promise(r => setTimeout(r, 250)); // let it composite
|
|
2453
|
-
return await tryCapture();
|
|
3033
|
+
return await tryCapture({ sidsteUdvej: true }); // resten af budgettet - der er ikke en runde mere efter denne
|
|
2454
3034
|
} catch (secondErr) {
|
|
2455
3035
|
throw new Error(
|
|
2456
3036
|
`Screenshot failed after focus-neutral AND raised attempts. ` +
|
|
@@ -2481,19 +3061,43 @@ async function dispatch(port, method, params) {
|
|
|
2481
3061
|
|
|
2482
3062
|
const diag = { tried: [] };
|
|
2483
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
|
+
});
|
|
2484
3087
|
// Step 1: try ISOLATED world
|
|
2485
3088
|
try {
|
|
2486
3089
|
const [result] = await chrome.scripting.executeScript({
|
|
2487
3090
|
target: { tabId: tab.id },
|
|
2488
3091
|
world: 'ISOLATED',
|
|
2489
3092
|
args: [params.code],
|
|
2490
|
-
func: (codeStr) => {
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
}
|
|
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' }; }
|
|
2497
3101
|
},
|
|
2498
3102
|
});
|
|
2499
3103
|
const r = result?.result;
|
|
@@ -2501,10 +3105,14 @@ async function dispatch(port, method, params) {
|
|
|
2501
3105
|
if (r && typeof r === 'object' && r.__ok) {
|
|
2502
3106
|
return { result: r.value, method: 'scripting-isolated' };
|
|
2503
3107
|
}
|
|
2504
|
-
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)) {
|
|
2505
3110
|
diag.isolated_error = r.message;
|
|
2506
3111
|
}
|
|
2507
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.
|
|
2508
3116
|
diag.isolated_throw = String(e?.message || e);
|
|
2509
3117
|
}
|
|
2510
3118
|
|
|
@@ -2514,13 +3122,14 @@ async function dispatch(port, method, params) {
|
|
|
2514
3122
|
target: { tabId: tab.id },
|
|
2515
3123
|
world: 'MAIN',
|
|
2516
3124
|
args: [params.code],
|
|
2517
|
-
func: (codeStr) => {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
}
|
|
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' }; }
|
|
2524
3133
|
},
|
|
2525
3134
|
});
|
|
2526
3135
|
const r = result?.result;
|
|
@@ -2528,11 +3137,14 @@ async function dispatch(port, method, params) {
|
|
|
2528
3137
|
if (r && typeof r === 'object' && r.__ok) {
|
|
2529
3138
|
return { result: r.value, method: 'scripting-main' };
|
|
2530
3139
|
}
|
|
2531
|
-
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)) {
|
|
2532
3142
|
diag.main_error = r.message;
|
|
2533
3143
|
}
|
|
2534
3144
|
} catch (e) {
|
|
2535
|
-
|
|
3145
|
+
const m = String(e?.message || e);
|
|
3146
|
+
if (sidenForsvandt(m)) return maaskeKoert('MAIN', m);
|
|
3147
|
+
diag.main_throw = m;
|
|
2536
3148
|
}
|
|
2537
3149
|
|
|
2538
3150
|
// Step 3: debugger fallback — the ONLY universal path for arbitrary STRING code
|
|
@@ -2544,10 +3156,15 @@ async function dispatch(port, method, params) {
|
|
|
2544
3156
|
// fires and debuggerEval silently returned undefined → the caller saw a bare
|
|
2545
3157
|
// `{method:"debugger"}` with no result. Also surface script exceptions + raw
|
|
2546
3158
|
// diagnostics so a genuine failure is never mistaken for an empty success.
|
|
2547
|
-
let rawDbg, dbgErr = '';
|
|
3159
|
+
let rawDbg, dbgErr = '', sendt = false;
|
|
2548
3160
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
2549
3161
|
try {
|
|
2550
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;
|
|
2551
3168
|
rawDbg = await cdpSend(tab.id, 'Runtime.evaluate', {
|
|
2552
3169
|
expression: '(' + params.code + '\n)',
|
|
2553
3170
|
returnByValue: true,
|
|
@@ -2563,12 +3180,14 @@ async function dispatch(port, method, params) {
|
|
|
2563
3180
|
return { result: rawDbg.result.value, method: 'debugger' };
|
|
2564
3181
|
}
|
|
2565
3182
|
dbgErr = 'empty/undefined CDP response: ' + JSON.stringify(rawDbg);
|
|
3183
|
+
break; // tomt svar efter afsendelse: scriptet kan have koert — gentag ikke
|
|
2566
3184
|
} catch (e) {
|
|
2567
3185
|
const m = String(e?.message || e);
|
|
2568
3186
|
if (m.startsWith('__SCRIPT_EX__')) {
|
|
2569
3187
|
throw new Error(m.slice('__SCRIPT_EX__'.length) + ' | scripting-diag: ' + JSON.stringify(diag));
|
|
2570
3188
|
}
|
|
2571
3189
|
dbgErr = m;
|
|
3190
|
+
if (sendt) break; // sendt = maaske koert — se kommentaren ved afsendelsen
|
|
2572
3191
|
if (!/detach|attach|empty|gone|given id|not attached/i.test(m)) break;
|
|
2573
3192
|
}
|
|
2574
3193
|
await debuggerDetach(tab.id).catch(() => {});
|
|
@@ -2576,6 +3195,8 @@ async function dispatch(port, method, params) {
|
|
|
2576
3195
|
}
|
|
2577
3196
|
throw new Error(
|
|
2578
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.' : '') +
|
|
2579
3200
|
' | raw: ' + JSON.stringify(rawDbg) +
|
|
2580
3201
|
' | scripting-diag: ' + JSON.stringify(diag)
|
|
2581
3202
|
);
|
|
@@ -2606,7 +3227,6 @@ async function dispatch(port, method, params) {
|
|
|
2606
3227
|
// Primary path: debugger mouse events (isTrusted=true, works on React/Angular SPAs)
|
|
2607
3228
|
const clickResult = await debuggerClick(tab.id, el.x, el.y);
|
|
2608
3229
|
return {
|
|
2609
|
-
ok: true,
|
|
2610
3230
|
method: el.method || 'debugger',
|
|
2611
3231
|
tag: el.tag,
|
|
2612
3232
|
text: el.text,
|
|
@@ -2614,6 +3234,15 @@ async function dispatch(port, method, params) {
|
|
|
2614
3234
|
// saa `click` svarede ok:true selv naar siden slet ikke reagerede. Nu foelger den med:
|
|
2615
3235
|
// landed=false betyder "eventet blev sendt, men intet handler tog imod det".
|
|
2616
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) || {}),
|
|
2617
3246
|
};
|
|
2618
3247
|
} catch (e) {
|
|
2619
3248
|
// Fallback: synthetic click via chrome.scripting for anti-automation sites
|
|
@@ -2633,9 +3262,46 @@ async function dispatch(port, method, params) {
|
|
|
2633
3262
|
//
|
|
2634
3263
|
// Prisen ved fallbacken er at klikket mister isTrusted=true. Det tjekker de færreste
|
|
2635
3264
|
// sider, og et klik der virker på 95% af nettet slår et klik der aldrig virker.
|
|
2636
|
-
|
|
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.
|
|
2637
3282
|
const r = await scriptingClick(tab.id, params.selector);
|
|
2638
|
-
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
|
+
}
|
|
2639
3305
|
}
|
|
2640
3306
|
throw e;
|
|
2641
3307
|
}
|
|
@@ -2661,6 +3327,27 @@ async function dispatch(port, method, params) {
|
|
|
2661
3327
|
await debuggerFill(tab.id, parsed.selector, params.value);
|
|
2662
3328
|
return { ok: true, method: 'debugger' };
|
|
2663
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
|
+
}
|
|
2664
3351
|
// Fallback to executeScript if debugger fails
|
|
2665
3352
|
const scriptResult = await safeExecuteScript(tab.id, (sel, val) => {
|
|
2666
3353
|
const el = document.querySelector(sel);
|
|
@@ -2675,8 +3362,59 @@ async function dispatch(port, method, params) {
|
|
|
2675
3362
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2676
3363
|
return { ok: true };
|
|
2677
3364
|
}, [parsed.selector, params.value]);
|
|
2678
|
-
if (
|
|
2679
|
-
|
|
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
|
+
};
|
|
2680
3418
|
}
|
|
2681
3419
|
}
|
|
2682
3420
|
|
|
@@ -2711,9 +3449,16 @@ async function dispatch(port, method, params) {
|
|
|
2711
3449
|
await new Promise(r => setTimeout(r, 250));
|
|
2712
3450
|
const v = await readBackValue(tab.id, params.selector);
|
|
2713
3451
|
tried.push({ path: 'masked', format: fmt.order.join(fmt.sep), value: v });
|
|
2714
|
-
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) };
|
|
2715
3453
|
} catch (e) {
|
|
2716
|
-
|
|
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
|
+
}
|
|
2717
3462
|
}
|
|
2718
3463
|
} else {
|
|
2719
3464
|
tried.push({
|
|
@@ -2734,11 +3479,13 @@ async function dispatch(port, method, params) {
|
|
|
2734
3479
|
|
|
2735
3480
|
// Path C: calendar-picker navigation
|
|
2736
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);
|
|
2737
3484
|
const r = await setDatePicker(tab.id, params.selector, iso);
|
|
2738
3485
|
await new Promise(r2 => setTimeout(r2, 200));
|
|
2739
3486
|
const v = await readBackValue(tab.id, params.selector);
|
|
2740
3487
|
tried.push({ path: 'picker', ...r, value: v });
|
|
2741
|
-
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 };
|
|
2742
3489
|
}
|
|
2743
3490
|
|
|
2744
3491
|
const visibleErrors = await collectVisibleErrors(tab.id, params.selector);
|
|
@@ -2835,26 +3582,44 @@ async function dispatch(port, method, params) {
|
|
|
2835
3582
|
const vkCode = VK_CODES[key];
|
|
2836
3583
|
const vkParams = vkCode ? { windowsVirtualKeyCode: vkCode, nativeVirtualKeyCode: vkCode } : {};
|
|
2837
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.
|
|
2838
3589
|
await debuggerAttach(tab.id);
|
|
3590
|
+
let tastFejl = null;
|
|
2839
3591
|
try {
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
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; }
|
|
2855
3611
|
} finally {
|
|
2856
3612
|
await debuggerDetach(tab.id);
|
|
2857
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
|
+
}
|
|
2858
3623
|
return { ok: true, key };
|
|
2859
3624
|
}
|
|
2860
3625
|
|
|
@@ -2873,6 +3638,14 @@ async function dispatch(port, method, params) {
|
|
|
2873
3638
|
// FB/Twitter/IG only trigger lazy-load on continuous wheel events, not a single large delta.
|
|
2874
3639
|
const dx = params.x || 0;
|
|
2875
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;
|
|
2876
3649
|
try {
|
|
2877
3650
|
await debuggerAttach(tab.id);
|
|
2878
3651
|
const STEP_SIZE = 300; // pixels per wheel-event (matches a typical mouse-wheel notch)
|
|
@@ -2890,9 +3663,70 @@ async function dispatch(port, method, params) {
|
|
|
2890
3663
|
// before any subsequent commands run (caller often scrapes immediately after)
|
|
2891
3664
|
await new Promise(r => setTimeout(r, 600));
|
|
2892
3665
|
} catch (e) {
|
|
2893
|
-
//
|
|
2894
|
-
|
|
2895
|
-
|
|
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
|
+
};
|
|
2896
3730
|
}
|
|
2897
3731
|
return { ok: true, scrolled: { x: dx, y: dy }, method: 'mouseWheel-stepped' };
|
|
2898
3732
|
}
|
|
@@ -2943,8 +3777,30 @@ async function dispatch(port, method, params) {
|
|
|
2943
3777
|
if (typeof params.x !== 'number' || typeof params.y !== 'number') {
|
|
2944
3778
|
return { ok: false, error: 'x and y (numbers, CSS pixels in viewport) are required' };
|
|
2945
3779
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
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
|
+
};
|
|
2948
3804
|
}
|
|
2949
3805
|
|
|
2950
3806
|
case 'reattach_debugger': {
|
|
@@ -2999,6 +3855,20 @@ async function dispatch(port, method, params) {
|
|
|
2999
3855
|
// INTET, og svaret sagde stadig at det var lykkedes. Samme fejlklasse som klikket
|
|
3000
3856
|
// der svarede ok:true uden at siden reagerede. Nu laeses svaret, og der laeses
|
|
3001
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
|
+
|
|
3002
3872
|
const valg = await debuggerEval(tab.id, `
|
|
3003
3873
|
(function() {
|
|
3004
3874
|
const sel = document.querySelector(${JSON.stringify(params.selector)});
|
|
@@ -3011,20 +3881,44 @@ async function dispatch(port, method, params) {
|
|
|
3011
3881
|
return JSON.stringify({ found: false, error: 'Ingen mulighed matchede: ' + oensket,
|
|
3012
3882
|
available: Array.from(sel.options).map(o => o.text.trim()).slice(0, 25) });
|
|
3013
3883
|
}
|
|
3884
|
+
const foer = ${aftryk};
|
|
3014
3885
|
sel.value = opt.value;
|
|
3015
3886
|
sel.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3016
3887
|
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3017
|
-
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 });
|
|
3018
3889
|
})()
|
|
3019
3890
|
`);
|
|
3020
3891
|
let r; try { r = JSON.parse(valg); } catch { r = null; }
|
|
3021
3892
|
if (!r) return { ok: false, type: 'native_select', error: 'Kunne ikke laese resultatet af valget' };
|
|
3022
3893
|
if (!r.found) return { ok: false, type: 'native_select', error: r.error, available: r.available };
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
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
|
+
};
|
|
3026
3918
|
}
|
|
3027
|
-
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.' };
|
|
3028
3922
|
}
|
|
3029
3923
|
|
|
3030
3924
|
// Custom dropdown (Angular Material, React Select, etc.)
|
|
@@ -3052,12 +3946,15 @@ async function dispatch(port, method, params) {
|
|
|
3052
3946
|
// ordret "it never reports success without the field actually changing".
|
|
3053
3947
|
return {
|
|
3054
3948
|
...(valgKlik || {}),
|
|
3055
|
-
ok: valgKlik
|
|
3949
|
+
ok: klikLandede(valgKlik),
|
|
3056
3950
|
type: 'custom_dropdown',
|
|
3057
3951
|
selected: oensket,
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
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
|
+
: {})),
|
|
3061
3958
|
};
|
|
3062
3959
|
}
|
|
3063
3960
|
|
|
@@ -3113,6 +4010,8 @@ async function dispatch(port, method, params) {
|
|
|
3113
4010
|
if (tab.url.startsWith('chrome://')) throw new Error('Cannot interact with chrome:// pages');
|
|
3114
4011
|
const urlPattern = params.url_pattern || '';
|
|
3115
4012
|
const timeout = params.timeout || 15000;
|
|
4013
|
+
const netFrister = netvaerkFrister();
|
|
4014
|
+
const budgetSlut = Date.now() + netFrister.budgetMs;
|
|
3116
4015
|
|
|
3117
4016
|
await debuggerAttach(tab.id);
|
|
3118
4017
|
try {
|
|
@@ -3134,10 +4033,18 @@ async function dispatch(port, method, params) {
|
|
|
3134
4033
|
if (!urlPattern || url.includes(urlPattern)) {
|
|
3135
4034
|
chrome.debugger.onEvent.removeListener(listener);
|
|
3136
4035
|
clearTimeout(timer);
|
|
3137
|
-
// Try to get response body
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
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 => {
|
|
3141
4048
|
resolve({
|
|
3142
4049
|
ok: true,
|
|
3143
4050
|
url,
|
|
@@ -3201,8 +4108,101 @@ async function dispatch(port, method, params) {
|
|
|
3201
4108
|
}
|
|
3202
4109
|
|
|
3203
4110
|
case 'get_cookies': {
|
|
3204
|
-
|
|
3205
|
-
|
|
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 })) };
|
|
3206
4206
|
}
|
|
3207
4207
|
|
|
3208
4208
|
case 'get_local_storage': {
|
|
@@ -3220,12 +4220,64 @@ async function dispatch(port, method, params) {
|
|
|
3220
4220
|
}
|
|
3221
4221
|
|
|
3222
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);
|
|
3223
4261
|
const results = [];
|
|
3224
4262
|
const cookieList = Array.isArray(params.cookies) ? params.cookies : [params];
|
|
3225
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
|
+
}
|
|
3226
4277
|
try {
|
|
3227
4278
|
const cookie = await chrome.cookies.set({
|
|
3228
|
-
|
|
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 : '/'),
|
|
3229
4281
|
name: c.name,
|
|
3230
4282
|
value: c.value,
|
|
3231
4283
|
domain: c.domain,
|
|
@@ -3233,6 +4285,7 @@ async function dispatch(port, method, params) {
|
|
|
3233
4285
|
secure: c.secure !== false,
|
|
3234
4286
|
httpOnly: c.httpOnly || false,
|
|
3235
4287
|
sameSite: c.sameSite || 'lax',
|
|
4288
|
+
...(side.storeId ? { storeId: side.storeId } : {}),
|
|
3236
4289
|
});
|
|
3237
4290
|
results.push({ ok: true, name: c.name });
|
|
3238
4291
|
} catch (e) {
|
|
@@ -3317,7 +4370,7 @@ async function dispatch(port, method, params) {
|
|
|
3317
4370
|
chrome.notifications.create(notifId, {
|
|
3318
4371
|
type: 'basic',
|
|
3319
4372
|
iconUrl: 'icons/icon-128.png',
|
|
3320
|
-
title: `${session.label}
|
|
4373
|
+
title: `${session.label} - Action Required`,
|
|
3321
4374
|
message: params.message,
|
|
3322
4375
|
requireInteraction: true,
|
|
3323
4376
|
silent: false,
|
|
@@ -3378,7 +4431,7 @@ async function dispatch(port, method, params) {
|
|
|
3378
4431
|
|
|
3379
4432
|
const h = document.createElement('div');
|
|
3380
4433
|
h.style.cssText = 'font-size:14px;font-weight:600;color:#3b82f6;margin-bottom:4px';
|
|
3381
|
-
h.textContent = title || 'Agent360
|
|
4434
|
+
h.textContent = title || 'Agent360 - Action Required';
|
|
3382
4435
|
card.appendChild(h);
|
|
3383
4436
|
const badge = document.createElement('div');
|
|
3384
4437
|
badge.style.cssText = 'font-size:10px;color:#94a3b8;margin-bottom:12px';
|
|
@@ -3432,7 +4485,7 @@ async function dispatch(port, method, params) {
|
|
|
3432
4485
|
});
|
|
3433
4486
|
},
|
|
3434
4487
|
// MAALT 21/8: her stod `params.title` raat. Skemaet siger at title er VALGFRI
|
|
3435
|
-
// med standarden "Agent360
|
|
4488
|
+
// med standarden "Agent360 - Action Required", men udelades den, er vaerdien
|
|
3436
4489
|
// undefined — og chrome.scripting.executeScript afviser hele kaldet med
|
|
3437
4490
|
// "Error at property 'args': Error at index 1: Value is unserializable".
|
|
3438
4491
|
// Altsaa styrtede human-in-the-loop-vaerktoejet hver gang en agent fulgte sit
|
|
@@ -3443,7 +4496,7 @@ async function dispatch(port, method, params) {
|
|
|
3443
4496
|
// anvendes der hvor den er lovet.
|
|
3444
4497
|
args: [
|
|
3445
4498
|
String(params.message ?? ''),
|
|
3446
|
-
String(params.title ?? 'Agent360
|
|
4499
|
+
String(params.title ?? 'Agent360 - Action Required'),
|
|
3447
4500
|
Array.isArray(fields) ? fields : [],
|
|
3448
4501
|
Boolean(hasFields),
|
|
3449
4502
|
Number(timeout) || 120000,
|
|
@@ -3510,7 +4563,23 @@ async function dispatch(port, method, params) {
|
|
|
3510
4563
|
if (!lastCreatedTabId) return { error: 'No new tab detected' };
|
|
3511
4564
|
try {
|
|
3512
4565
|
const tab = await chrome.tabs.get(lastCreatedTabId);
|
|
3513
|
-
//
|
|
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
|
+
}
|
|
3514
4583
|
await addTabToSession(port, tab.id);
|
|
3515
4584
|
return { id: tab.id, url: tab.url, title: tab.title };
|
|
3516
4585
|
} catch {
|
|
@@ -3524,9 +4593,24 @@ async function dispatch(port, method, params) {
|
|
|
3524
4593
|
throw new Error(`Tab ${params.tab_id} does not belong to this session (${session.label})`);
|
|
3525
4594
|
}
|
|
3526
4595
|
const tab = await chrome.tabs.update(params.tab_id, { active: true });
|
|
4596
|
+
// Gør ogsaa VINDUET forrest. Uden det bliver fanen aktiv inde i sit vindue —
|
|
4597
|
+
// document.hasFocus() bliver sand — men document.visibilityState forbliver
|
|
4598
|
+
// 'hidden' fordi vinduet ligger bagved. Chrome struber timere i skjulte
|
|
4599
|
+
// faner, saa Angular-apps (Google Ads, GA4, Search Console) renderer aldrig
|
|
4600
|
+
// faerdigt: man laeser en halvt bygget side og drager forkerte konklusioner.
|
|
4601
|
+
// Kostede to opgaver og en forkert konklusion 31/8-2026.
|
|
4602
|
+
let vinduesFokus = null;
|
|
4603
|
+
try {
|
|
4604
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
4605
|
+
vinduesFokus = true;
|
|
4606
|
+
} catch (e) {
|
|
4607
|
+
// Vinduet kan vaere lukket eller paa et andet Space. Fanen er stadig
|
|
4608
|
+
// aktiv; vi siger bare aerligt at synligheden ikke kunne sikres.
|
|
4609
|
+
vinduesFokus = false;
|
|
4610
|
+
}
|
|
3527
4611
|
session.activeTabId = tab.id;
|
|
3528
4612
|
persistSessions();
|
|
3529
|
-
return { id: tab.id, url: tab.url, title: tab.title };
|
|
4613
|
+
return { id: tab.id, url: tab.url, title: tab.title, windowFocused: vinduesFokus };
|
|
3530
4614
|
}
|
|
3531
4615
|
|
|
3532
4616
|
case 'close_tab': {
|
|
@@ -3590,7 +4674,7 @@ async function dispatch(port, method, params) {
|
|
|
3590
4674
|
const { result: nodeResult } = await cdpSend(tab.id, 'Runtime.evaluate', {
|
|
3591
4675
|
expression: `(() => {
|
|
3592
4676
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
3593
|
-
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)} });
|
|
3594
4678
|
return JSON.stringify({ found: true, tag: el.tagName, type: el.type, accept: el.accept, multiple: el.multiple });
|
|
3595
4679
|
})()`,
|
|
3596
4680
|
returnByValue: true,
|
|
@@ -3864,6 +4948,8 @@ chrome.runtime.onStartup.addListener(() => ensureOffscreen().catch(console.error
|
|
|
3864
4948
|
// MAALT 22/8: uden tvangen slog en genindlaesning aldrig igennem til broen, og
|
|
3865
4949
|
// udvikling krævede en fuld genstart af Chrome hver gang.
|
|
3866
4950
|
chrome.runtime.onInstalled.addListener(async () => {
|
|
4951
|
+
// Foerst: aeldre udgaver gemte parametre (adgangskoder, cookie-vaerdier) i historikken.
|
|
4952
|
+
await rensHandlingslog();
|
|
3867
4953
|
try {
|
|
3868
4954
|
if (await chrome.offscreen.hasDocument()) await chrome.offscreen.closeDocument();
|
|
3869
4955
|
} catch (e) {
|
|
@@ -3898,4 +4984,33 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
3898
4984
|
if (alarm.name === 'ensure-offscreen') {
|
|
3899
4985
|
ensureOffscreen().catch(console.error);
|
|
3900
4986
|
}
|
|
4987
|
+
if (alarm.name.startsWith('frigiv-')) {
|
|
4988
|
+
const port = Number(alarm.name.slice('frigiv-'.length));
|
|
4989
|
+
// ── Hvorfor lageret laeses DIREKTE og ikke via restoreSessions() ─────────
|
|
4990
|
+
//
|
|
4991
|
+
// FUNDET AF REVIEW 7/9. Her stod `restoreSessions().then(...)`, og kommentaren
|
|
4992
|
+
// sagde at det var vaernet mod en genstartet service-worker. Den gjorde det
|
|
4993
|
+
// modsatte: `restoreSessions()` gendanner kun sessioner der har GYLDIGE FANER
|
|
4994
|
+
// (`if (validTabIds.size > 0)`) — og en tom session er praecis den her alarm
|
|
4995
|
+
// handler om. En MV3-worker suspenderes efter ~30 sekunder; fristen er paa fem
|
|
4996
|
+
// minutter, saa workeren er naesten altid frisk naar alarmen fyrer. Sessionen
|
|
4997
|
+
// blev derfor aldrig fundet, `if (!s) return` ramte, og porten blev holdt til
|
|
4998
|
+
// 4-timers-tomgangen — altsaa praecis den fejl frigivelsen skulle fjerne.
|
|
4999
|
+
//
|
|
5000
|
+
// restoreSessions() har god grund til ikke at genoplive doede sessioner (andre
|
|
5001
|
+
// kaldere vil ikke arve faner der ikke findes). Derfor rettes det HER.
|
|
5002
|
+
(async () => {
|
|
5003
|
+
const iHukommelsen = sessions.get(port);
|
|
5004
|
+
if (iHukommelsen) {
|
|
5005
|
+
if (iHukommelsen.tabIds.size > 0) return; // den arbejder igen — lad den vaere
|
|
5006
|
+
} else {
|
|
5007
|
+
const { sessions: gemte } = await chrome.storage.local.get({ sessions: {} });
|
|
5008
|
+
const gemt = gemte[String(port)];
|
|
5009
|
+
if (!gemt) return; // sessionen er reelt vaek
|
|
5010
|
+
if ((gemt.tabIds || []).length > 0) return; // den arbejder igen
|
|
5011
|
+
}
|
|
5012
|
+
frivilligtFrigivet.add(port);
|
|
5013
|
+
chrome.runtime.sendMessage({ type: 'terminate_mcp_session', port }).catch(() => {});
|
|
5014
|
+
})().catch(() => {});
|
|
5015
|
+
}
|
|
3901
5016
|
});
|