@flrande/bak-extension 0.1.0
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/dist/background.global.js +623 -0
- package/dist/content.global.js +2042 -0
- package/dist/manifest.json +20 -0
- package/dist/popup.global.js +52 -0
- package/dist/popup.html +106 -0
- package/package.json +18 -0
- package/public/manifest.json +20 -0
- package/public/popup.html +106 -0
- package/scripts/copy-assets.mjs +16 -0
- package/src/background.ts +705 -0
- package/src/content.ts +2267 -0
- package/src/limitations.ts +38 -0
- package/src/popup.ts +65 -0
- package/src/privacy.ts +135 -0
- package/src/reconnect.ts +25 -0
- package/src/url-policy.ts +12 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,2042 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// src/privacy.ts
|
|
4
|
+
var MAX_SAFE_TEXT_LENGTH = 120;
|
|
5
|
+
var MAX_DEBUG_TEXT_LENGTH = 320;
|
|
6
|
+
var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
7
|
+
var LONG_DIGIT_PATTERN = /(?:\d[ -]?){13,19}/g;
|
|
8
|
+
var OTP_PATTERN = /^\d{4,8}$/;
|
|
9
|
+
var SECRET_QUERY_PARAM_PATTERN = /(token|secret|password|passwd|otp|code|session|auth)=/i;
|
|
10
|
+
var HIGH_ENTROPY_TOKEN_PATTERN = /^(?=.*\d)(?=.*[a-zA-Z])[A-Za-z0-9~!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`]{16,}$/;
|
|
11
|
+
var INPUT_TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
|
|
12
|
+
"text",
|
|
13
|
+
"search",
|
|
14
|
+
"email",
|
|
15
|
+
"password",
|
|
16
|
+
"tel",
|
|
17
|
+
"url",
|
|
18
|
+
"number",
|
|
19
|
+
"date",
|
|
20
|
+
"datetime-local",
|
|
21
|
+
"month",
|
|
22
|
+
"week",
|
|
23
|
+
"time"
|
|
24
|
+
]);
|
|
25
|
+
function normalize(raw) {
|
|
26
|
+
const cleaned = [...raw].map((char) => {
|
|
27
|
+
const code = char.charCodeAt(0);
|
|
28
|
+
return code < 32 || code === 127 ? " " : char;
|
|
29
|
+
}).join("");
|
|
30
|
+
return cleaned.replace(/\s+/g, " ").trim();
|
|
31
|
+
}
|
|
32
|
+
function clamp(text, options) {
|
|
33
|
+
const max = options.debugRichText ? MAX_DEBUG_TEXT_LENGTH : MAX_SAFE_TEXT_LENGTH;
|
|
34
|
+
return text.length <= max ? text : `${text.slice(0, max - 3)}...`;
|
|
35
|
+
}
|
|
36
|
+
function redactByPattern(text) {
|
|
37
|
+
let output = text;
|
|
38
|
+
if (SECRET_QUERY_PARAM_PATTERN.test(output)) {
|
|
39
|
+
return "[REDACTED:query-secret]";
|
|
40
|
+
}
|
|
41
|
+
output = output.replace(EMAIL_PATTERN, "[REDACTED:email]");
|
|
42
|
+
output = output.replace(LONG_DIGIT_PATTERN, "[REDACTED:number]");
|
|
43
|
+
if (OTP_PATTERN.test(output)) {
|
|
44
|
+
return "[REDACTED:otp]";
|
|
45
|
+
}
|
|
46
|
+
if (HIGH_ENTROPY_TOKEN_PATTERN.test(output) && !output.includes(" ")) {
|
|
47
|
+
return "[REDACTED:secret]";
|
|
48
|
+
}
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
function redactElementText(raw, options = {}) {
|
|
52
|
+
if (!raw) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
const normalized = normalize(raw);
|
|
56
|
+
if (!normalized) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
const redacted = redactByPattern(normalized);
|
|
60
|
+
return clamp(redacted, options);
|
|
61
|
+
}
|
|
62
|
+
function isTextEntryField(candidates) {
|
|
63
|
+
const tag = candidates.tag.toLowerCase();
|
|
64
|
+
const role = (candidates.role ?? "").toLowerCase();
|
|
65
|
+
const inputType = (candidates.inputType ?? "").toLowerCase();
|
|
66
|
+
if (role === "textbox") {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (tag === "textarea") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (tag === "input") {
|
|
73
|
+
return INPUT_TEXT_ENTRY_TYPES.has(inputType) || inputType === "";
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function inferSafeName(candidates, options = {}) {
|
|
78
|
+
const allowDebugText = Boolean(options.debugRichText);
|
|
79
|
+
const fromAria = redactElementText(candidates.ariaLabel, options);
|
|
80
|
+
const fromLabelledBy = redactElementText(candidates.labelledByText, options);
|
|
81
|
+
const fromLabel = redactElementText(candidates.labelText, options);
|
|
82
|
+
const fromPlaceholder = redactElementText(candidates.placeholder, options);
|
|
83
|
+
const fromNameAttr = redactElementText(candidates.nameAttr, options);
|
|
84
|
+
const fromText = redactElementText(candidates.text, options);
|
|
85
|
+
const ordered = [fromAria, fromLabelledBy, fromLabel, fromPlaceholder];
|
|
86
|
+
if (!isTextEntryField(candidates) || allowDebugText) {
|
|
87
|
+
ordered.push(fromText);
|
|
88
|
+
}
|
|
89
|
+
ordered.push(fromNameAttr);
|
|
90
|
+
for (const value of ordered) {
|
|
91
|
+
if (value) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return candidates.tag.toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/limitations.ts
|
|
99
|
+
var SHADOW_SELECTOR_PATTERN = /(>>>|::part|::slotted|\bshadowroot\b)/i;
|
|
100
|
+
var FRAME_SELECTOR_PATTERN = /(^|[\s>+~,(])(?:iframe|frame)(?=[$.#:[\s>+~,(]|$)/i;
|
|
101
|
+
function unsupportedLocator(locator) {
|
|
102
|
+
if (!locator?.css) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const css = locator.css;
|
|
106
|
+
if (SHADOW_SELECTOR_PATTERN.test(css)) {
|
|
107
|
+
return {
|
|
108
|
+
reason: "shadow-dom",
|
|
109
|
+
hint: "shadow-dom selectors are not supported in v1"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (FRAME_SELECTOR_PATTERN.test(css)) {
|
|
113
|
+
return {
|
|
114
|
+
reason: "iframe",
|
|
115
|
+
hint: "iframe selectors are not supported in v1"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function unsupportedLocatorHint(locator) {
|
|
121
|
+
return unsupportedLocator(locator)?.hint ?? null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/content.ts
|
|
125
|
+
var consoleEntries = [];
|
|
126
|
+
var networkEntries = [];
|
|
127
|
+
var elementCache = /* @__PURE__ */ new Map();
|
|
128
|
+
var contextState = {
|
|
129
|
+
framePath: [],
|
|
130
|
+
shadowPath: []
|
|
131
|
+
};
|
|
132
|
+
var networkSequence = 0;
|
|
133
|
+
var longTaskCount = 0;
|
|
134
|
+
var longTaskDurationMs = 0;
|
|
135
|
+
var performanceBaselineMs = 0;
|
|
136
|
+
function isHtmlElement(node) {
|
|
137
|
+
if (!node) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const view = node.ownerDocument.defaultView;
|
|
141
|
+
return Boolean(view && node instanceof view.HTMLElement);
|
|
142
|
+
}
|
|
143
|
+
function isInputElement(element) {
|
|
144
|
+
const view = element.ownerDocument.defaultView;
|
|
145
|
+
return Boolean(view && element instanceof view.HTMLInputElement);
|
|
146
|
+
}
|
|
147
|
+
function isTextAreaElement(element) {
|
|
148
|
+
const view = element.ownerDocument.defaultView;
|
|
149
|
+
return Boolean(view && element instanceof view.HTMLTextAreaElement);
|
|
150
|
+
}
|
|
151
|
+
function isFrameElement(element) {
|
|
152
|
+
const view = element.ownerDocument.defaultView;
|
|
153
|
+
if (!view) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const iframeMatch = element instanceof view.HTMLIFrameElement;
|
|
157
|
+
const frameCtor = view.HTMLFrameElement;
|
|
158
|
+
const frameMatch = typeof frameCtor === "function" ? element instanceof frameCtor : false;
|
|
159
|
+
return iframeMatch || frameMatch;
|
|
160
|
+
}
|
|
161
|
+
function pushConsole(level, message, source) {
|
|
162
|
+
consoleEntries.push({
|
|
163
|
+
level,
|
|
164
|
+
message,
|
|
165
|
+
source,
|
|
166
|
+
ts: Date.now()
|
|
167
|
+
});
|
|
168
|
+
if (consoleEntries.length > 1e3) {
|
|
169
|
+
consoleEntries.shift();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function patchConsoleCapture() {
|
|
173
|
+
const methods = [
|
|
174
|
+
{ method: "log", level: "log" },
|
|
175
|
+
{ method: "debug", level: "debug" },
|
|
176
|
+
{ method: "info", level: "info" },
|
|
177
|
+
{ method: "warn", level: "warn" },
|
|
178
|
+
{ method: "error", level: "error" }
|
|
179
|
+
];
|
|
180
|
+
for (const entry of methods) {
|
|
181
|
+
const original = console[entry.method];
|
|
182
|
+
console[entry.method] = (...args) => {
|
|
183
|
+
const message = args.map((item) => {
|
|
184
|
+
if (item instanceof Error) {
|
|
185
|
+
return item.message;
|
|
186
|
+
}
|
|
187
|
+
if (typeof item === "string") {
|
|
188
|
+
return item;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return JSON.stringify(item);
|
|
192
|
+
} catch {
|
|
193
|
+
return String(item);
|
|
194
|
+
}
|
|
195
|
+
}).join(" ");
|
|
196
|
+
pushConsole(entry.level, message, "isolated");
|
|
197
|
+
original.apply(console, args);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
window.addEventListener("bak:console", (event) => {
|
|
201
|
+
const detail = event.detail;
|
|
202
|
+
if (!detail || typeof detail !== "object") {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const level = detail.level === "debug" || detail.level === "info" || detail.level === "warn" || detail.level === "error" ? detail.level : "log";
|
|
206
|
+
const message = typeof detail.message === "string" ? detail.message : "";
|
|
207
|
+
if (!message) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
pushConsole(level, message, detail.source ?? "page");
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
const injector = document.createElement("script");
|
|
214
|
+
injector.textContent = `
|
|
215
|
+
(() => {
|
|
216
|
+
const g = window;
|
|
217
|
+
if (g.__bakPageConsolePatched) return;
|
|
218
|
+
g.__bakPageConsolePatched = true;
|
|
219
|
+
const emit = (level, message, source) =>
|
|
220
|
+
window.dispatchEvent(new CustomEvent('bak:console', { detail: { level, message, source, ts: Date.now() } }));
|
|
221
|
+
const serialize = (value) => {
|
|
222
|
+
if (value instanceof Error) return value.message;
|
|
223
|
+
if (typeof value === 'string') return value;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.stringify(value);
|
|
226
|
+
} catch {
|
|
227
|
+
return String(value);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
['log', 'debug', 'info', 'warn', 'error'].forEach((method) => {
|
|
231
|
+
const original = console[method];
|
|
232
|
+
console[method] = (...args) => {
|
|
233
|
+
emit(method, args.map(serialize).join(' '), 'page');
|
|
234
|
+
return original.apply(console, args);
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
window.addEventListener('error', (event) => {
|
|
238
|
+
emit('error', event.message || 'error event', event.filename || 'page');
|
|
239
|
+
});
|
|
240
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
241
|
+
const reason = event.reason instanceof Error ? event.reason.message : String(event.reason);
|
|
242
|
+
emit('error', 'unhandledrejection: ' + reason, 'page');
|
|
243
|
+
});
|
|
244
|
+
})();
|
|
245
|
+
`;
|
|
246
|
+
(document.documentElement ?? document.head ?? document.body).appendChild(injector);
|
|
247
|
+
injector.remove();
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
window.addEventListener("error", (event) => {
|
|
251
|
+
pushConsole("error", event.message, event.filename);
|
|
252
|
+
});
|
|
253
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
254
|
+
const reason = event.reason instanceof Error ? event.reason.message : String(event.reason);
|
|
255
|
+
pushConsole("error", `unhandledrejection: ${reason}`);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function pushNetwork(entry) {
|
|
259
|
+
networkEntries.push(entry);
|
|
260
|
+
if (networkEntries.length > 1e3) {
|
|
261
|
+
networkEntries.shift();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function patchNetworkCapture() {
|
|
265
|
+
window.addEventListener("bak:network", (event) => {
|
|
266
|
+
const detail = event.detail;
|
|
267
|
+
if (!detail || typeof detail !== "object") {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
pushNetwork({
|
|
271
|
+
id: typeof detail.id === "string" ? detail.id : `net_${Date.now()}_${networkSequence++}`,
|
|
272
|
+
kind: detail.kind === "xhr" ? "xhr" : "fetch",
|
|
273
|
+
method: typeof detail.method === "string" ? detail.method : "GET",
|
|
274
|
+
url: typeof detail.url === "string" ? detail.url : window.location.href,
|
|
275
|
+
status: typeof detail.status === "number" ? detail.status : 0,
|
|
276
|
+
ok: detail.ok === true,
|
|
277
|
+
ts: typeof detail.ts === "number" ? detail.ts : Date.now(),
|
|
278
|
+
durationMs: typeof detail.durationMs === "number" ? detail.durationMs : 0,
|
|
279
|
+
requestBytes: typeof detail.requestBytes === "number" ? detail.requestBytes : void 0,
|
|
280
|
+
responseBytes: typeof detail.responseBytes === "number" ? detail.responseBytes : void 0
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
const injector = document.createElement("script");
|
|
285
|
+
injector.textContent = `
|
|
286
|
+
(() => {
|
|
287
|
+
const g = window;
|
|
288
|
+
if (g.__bakPageNetworkPatched) return;
|
|
289
|
+
g.__bakPageNetworkPatched = true;
|
|
290
|
+
let seq = 0;
|
|
291
|
+
const emit = (entry) => window.dispatchEvent(new CustomEvent('bak:network', { detail: entry }));
|
|
292
|
+
const nativeFetch = window.fetch.bind(window);
|
|
293
|
+
window.fetch = async (input, init) => {
|
|
294
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
295
|
+
const method = (init && init.method ? init.method : 'GET').toUpperCase();
|
|
296
|
+
const started = performance.now();
|
|
297
|
+
const requestBytes =
|
|
298
|
+
init && typeof init.body === 'string'
|
|
299
|
+
? init.body.length
|
|
300
|
+
: init && init.body instanceof URLSearchParams
|
|
301
|
+
? init.body.toString().length
|
|
302
|
+
: undefined;
|
|
303
|
+
try {
|
|
304
|
+
const response = await nativeFetch(input, init);
|
|
305
|
+
emit({
|
|
306
|
+
id: 'net_' + Date.now() + '_' + seq++,
|
|
307
|
+
kind: 'fetch',
|
|
308
|
+
method,
|
|
309
|
+
url,
|
|
310
|
+
status: response.status,
|
|
311
|
+
ok: response.ok,
|
|
312
|
+
ts: Date.now(),
|
|
313
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
314
|
+
requestBytes,
|
|
315
|
+
responseBytes: Number(response.headers.get('content-length') || '0') || undefined
|
|
316
|
+
});
|
|
317
|
+
return response;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
emit({
|
|
320
|
+
id: 'net_' + Date.now() + '_' + seq++,
|
|
321
|
+
kind: 'fetch',
|
|
322
|
+
method,
|
|
323
|
+
url,
|
|
324
|
+
status: 0,
|
|
325
|
+
ok: false,
|
|
326
|
+
ts: Date.now(),
|
|
327
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
328
|
+
requestBytes
|
|
329
|
+
});
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const xhrOpen = XMLHttpRequest.prototype.open;
|
|
335
|
+
const xhrSend = XMLHttpRequest.prototype.send;
|
|
336
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
337
|
+
this.__bakMeta = {
|
|
338
|
+
method: String(method || 'GET').toUpperCase(),
|
|
339
|
+
url: typeof url === 'string' ? url : String(url),
|
|
340
|
+
started: performance.now()
|
|
341
|
+
};
|
|
342
|
+
return xhrOpen.call(this, method, url, ...rest);
|
|
343
|
+
};
|
|
344
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
345
|
+
if (typeof body === 'string') {
|
|
346
|
+
this.__bakMeta = { ...(this.__bakMeta || {}), requestBytes: body.length };
|
|
347
|
+
}
|
|
348
|
+
this.addEventListener('loadend', () => {
|
|
349
|
+
const meta = this.__bakMeta || {};
|
|
350
|
+
emit({
|
|
351
|
+
id: 'net_' + Date.now() + '_' + seq++,
|
|
352
|
+
kind: 'xhr',
|
|
353
|
+
method: meta.method || 'GET',
|
|
354
|
+
url: meta.url || window.location.href,
|
|
355
|
+
status: Number(this.status) || 0,
|
|
356
|
+
ok: Number(this.status) >= 200 && Number(this.status) < 400,
|
|
357
|
+
ts: Date.now(),
|
|
358
|
+
durationMs: Math.max(0, performance.now() - (meta.started || performance.now())),
|
|
359
|
+
requestBytes: typeof meta.requestBytes === 'number' ? meta.requestBytes : undefined
|
|
360
|
+
});
|
|
361
|
+
}, { once: true });
|
|
362
|
+
return xhrSend.call(this, body ?? null);
|
|
363
|
+
};
|
|
364
|
+
})();
|
|
365
|
+
`;
|
|
366
|
+
(document.documentElement ?? document.head ?? document.body).appendChild(injector);
|
|
367
|
+
injector.remove();
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
const originalFetch = window.fetch.bind(window);
|
|
371
|
+
window.fetch = (async (input, init) => {
|
|
372
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
373
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
374
|
+
const started = performance.now();
|
|
375
|
+
const requestBytes = typeof init?.body === "string" ? init.body.length : init?.body instanceof URLSearchParams ? init.body.toString().length : void 0;
|
|
376
|
+
try {
|
|
377
|
+
const response = await originalFetch(input, init);
|
|
378
|
+
const durationMs = Math.max(0, performance.now() - started);
|
|
379
|
+
pushNetwork({
|
|
380
|
+
id: `net_${Date.now()}_${networkSequence++}`,
|
|
381
|
+
kind: "fetch",
|
|
382
|
+
method,
|
|
383
|
+
url,
|
|
384
|
+
status: response.status,
|
|
385
|
+
ok: response.ok,
|
|
386
|
+
ts: Date.now(),
|
|
387
|
+
durationMs,
|
|
388
|
+
requestBytes,
|
|
389
|
+
responseBytes: Number(response.headers.get("content-length") ?? "0") || void 0
|
|
390
|
+
});
|
|
391
|
+
return response;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const durationMs = Math.max(0, performance.now() - started);
|
|
394
|
+
pushNetwork({
|
|
395
|
+
id: `net_${Date.now()}_${networkSequence++}`,
|
|
396
|
+
kind: "fetch",
|
|
397
|
+
method,
|
|
398
|
+
url,
|
|
399
|
+
status: 0,
|
|
400
|
+
ok: false,
|
|
401
|
+
ts: Date.now(),
|
|
402
|
+
durationMs,
|
|
403
|
+
requestBytes
|
|
404
|
+
});
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
const xhrOpen = XMLHttpRequest.prototype.open;
|
|
409
|
+
const xhrSend = XMLHttpRequest.prototype.send;
|
|
410
|
+
XMLHttpRequest.prototype.open = function open(method, url, ...rest) {
|
|
411
|
+
const self = this;
|
|
412
|
+
self.__bakMeta = {
|
|
413
|
+
method: method.toUpperCase(),
|
|
414
|
+
url: typeof url === "string" ? url : url.toString(),
|
|
415
|
+
started: performance.now()
|
|
416
|
+
};
|
|
417
|
+
return xhrOpen.call(this, method, url, ...rest);
|
|
418
|
+
};
|
|
419
|
+
XMLHttpRequest.prototype.send = function send(body) {
|
|
420
|
+
const self = this;
|
|
421
|
+
if (typeof body === "string") {
|
|
422
|
+
const current = self.__bakMeta ?? {
|
|
423
|
+
method: "GET",
|
|
424
|
+
url: window.location.href,
|
|
425
|
+
started: performance.now()
|
|
426
|
+
};
|
|
427
|
+
self.__bakMeta = {
|
|
428
|
+
...current,
|
|
429
|
+
requestBytes: body.length
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
self.addEventListener(
|
|
433
|
+
"loadend",
|
|
434
|
+
() => {
|
|
435
|
+
pushNetwork({
|
|
436
|
+
id: `net_${Date.now()}_${networkSequence++}`,
|
|
437
|
+
kind: "xhr",
|
|
438
|
+
method: self.__bakMeta?.method ?? "GET",
|
|
439
|
+
url: self.__bakMeta?.url ?? window.location.href,
|
|
440
|
+
status: self.status,
|
|
441
|
+
ok: self.status >= 200 && self.status < 400,
|
|
442
|
+
ts: Date.now(),
|
|
443
|
+
durationMs: Math.max(0, performance.now() - (self.__bakMeta?.started ?? performance.now())),
|
|
444
|
+
requestBytes: self.__bakMeta?.requestBytes
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
{ once: true }
|
|
448
|
+
);
|
|
449
|
+
return xhrSend.call(this, body ?? null);
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (typeof PerformanceObserver !== "undefined") {
|
|
453
|
+
try {
|
|
454
|
+
const observer = new PerformanceObserver((list) => {
|
|
455
|
+
for (const entry of list.getEntries()) {
|
|
456
|
+
longTaskCount += 1;
|
|
457
|
+
longTaskDurationMs += entry.duration;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
observer.observe({ entryTypes: ["longtask"] });
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
patchConsoleCapture();
|
|
465
|
+
patchNetworkCapture();
|
|
466
|
+
var unsafeKeywords = /(submit|delete|remove|send|upload|付款|支付|删除|提交|发送|上传)/i;
|
|
467
|
+
function fnv1a(input) {
|
|
468
|
+
let hash = 2166136261;
|
|
469
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
470
|
+
hash ^= input.charCodeAt(index);
|
|
471
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
472
|
+
}
|
|
473
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
474
|
+
}
|
|
475
|
+
function inferRole(element) {
|
|
476
|
+
if (element.getAttribute("role")) {
|
|
477
|
+
return element.getAttribute("role") ?? "generic";
|
|
478
|
+
}
|
|
479
|
+
switch (element.tagName.toLowerCase()) {
|
|
480
|
+
case "a":
|
|
481
|
+
return "link";
|
|
482
|
+
case "button":
|
|
483
|
+
return "button";
|
|
484
|
+
case "input": {
|
|
485
|
+
const type = element.type;
|
|
486
|
+
if (type === "checkbox") {
|
|
487
|
+
return "checkbox";
|
|
488
|
+
}
|
|
489
|
+
if (type === "radio") {
|
|
490
|
+
return "radio";
|
|
491
|
+
}
|
|
492
|
+
return "textbox";
|
|
493
|
+
}
|
|
494
|
+
case "select":
|
|
495
|
+
return "combobox";
|
|
496
|
+
case "textarea":
|
|
497
|
+
return "textbox";
|
|
498
|
+
default:
|
|
499
|
+
return "generic";
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function labelledByText(element) {
|
|
503
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
504
|
+
if (!labelledBy) {
|
|
505
|
+
return "";
|
|
506
|
+
}
|
|
507
|
+
return labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").filter(Boolean).join(" ");
|
|
508
|
+
}
|
|
509
|
+
function labelText(element) {
|
|
510
|
+
const ownerDocument = element.ownerDocument;
|
|
511
|
+
if (isInputElement(element) && element.id) {
|
|
512
|
+
const explicit = ownerDocument.querySelector(`label[for="${CSS.escape(element.id)}"]`);
|
|
513
|
+
if (explicit?.textContent) {
|
|
514
|
+
return explicit.textContent.trim();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const wrapper = element.closest("label");
|
|
518
|
+
return wrapper?.textContent?.trim() ?? "";
|
|
519
|
+
}
|
|
520
|
+
function inferName(element, options = {}) {
|
|
521
|
+
const inputType = isInputElement(element) ? element.type : null;
|
|
522
|
+
return inferSafeName(
|
|
523
|
+
{
|
|
524
|
+
tag: element.tagName,
|
|
525
|
+
role: inferRole(element),
|
|
526
|
+
inputType,
|
|
527
|
+
ariaLabel: element.getAttribute("aria-label"),
|
|
528
|
+
labelledByText: labelledByText(element),
|
|
529
|
+
labelText: labelText(element),
|
|
530
|
+
placeholder: isInputElement(element) || isTextAreaElement(element) ? element.placeholder : "",
|
|
531
|
+
text: element.innerText || element.textContent || "",
|
|
532
|
+
nameAttr: element.getAttribute("name")
|
|
533
|
+
},
|
|
534
|
+
options
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
function isElementVisible(element) {
|
|
538
|
+
const style = window.getComputedStyle(element);
|
|
539
|
+
if (style.display === "none" || style.visibility === "hidden" || Number.parseFloat(style.opacity || "1") <= 0) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const rect = element.getBoundingClientRect();
|
|
546
|
+
return rect.width > 1 && rect.height > 1;
|
|
547
|
+
}
|
|
548
|
+
function isInteractive(element) {
|
|
549
|
+
const tag = element.tagName.toLowerCase();
|
|
550
|
+
const role = element.getAttribute("role");
|
|
551
|
+
if (["button", "input", "select", "textarea", "a"].includes(tag)) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
if (role && ["button", "link", "textbox", "checkbox", "radio", "combobox"].includes(role)) {
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
if (element.hasAttribute("contenteditable")) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
const tabIndex = element.getAttribute("tabindex");
|
|
561
|
+
return tabIndex !== null && Number.parseInt(tabIndex, 10) >= 0;
|
|
562
|
+
}
|
|
563
|
+
function toCssSelector(element) {
|
|
564
|
+
if (element.id) {
|
|
565
|
+
return `#${CSS.escape(element.id)}`;
|
|
566
|
+
}
|
|
567
|
+
const name = element.getAttribute("name");
|
|
568
|
+
if (name) {
|
|
569
|
+
return `${element.tagName.toLowerCase()}[name="${CSS.escape(name)}"]`;
|
|
570
|
+
}
|
|
571
|
+
const classes = Array.from(element.classList).slice(0, 2);
|
|
572
|
+
if (classes.length > 0) {
|
|
573
|
+
return `${element.tagName.toLowerCase()}.${classes.map((item) => CSS.escape(item)).join(".")}`;
|
|
574
|
+
}
|
|
575
|
+
const parent = element.parentElement;
|
|
576
|
+
if (!parent) {
|
|
577
|
+
return element.tagName.toLowerCase();
|
|
578
|
+
}
|
|
579
|
+
const siblings = Array.from(parent.children).filter(
|
|
580
|
+
(child) => child.tagName.toLowerCase() === element.tagName.toLowerCase()
|
|
581
|
+
);
|
|
582
|
+
const index = siblings.indexOf(element) + 1;
|
|
583
|
+
return `${element.tagName.toLowerCase()}:nth-of-type(${index})`;
|
|
584
|
+
}
|
|
585
|
+
function buildEid(element) {
|
|
586
|
+
const rect = element.getBoundingClientRect();
|
|
587
|
+
const quantized = [
|
|
588
|
+
Math.round(rect.x / 10) * 10,
|
|
589
|
+
Math.round(rect.y / 10) * 10,
|
|
590
|
+
Math.round(rect.width / 10) * 10,
|
|
591
|
+
Math.round(rect.height / 10) * 10
|
|
592
|
+
].join(":");
|
|
593
|
+
const payload = [
|
|
594
|
+
window.location.hostname,
|
|
595
|
+
window.location.pathname,
|
|
596
|
+
inferRole(element),
|
|
597
|
+
inferName(element),
|
|
598
|
+
quantized
|
|
599
|
+
].join("|");
|
|
600
|
+
return `eid_${fnv1a(payload)}`;
|
|
601
|
+
}
|
|
602
|
+
function splitShadowSelector(selector) {
|
|
603
|
+
return selector.split(">>>").map((part) => part.trim()).filter(Boolean);
|
|
604
|
+
}
|
|
605
|
+
function querySelectorInTree(root, selector) {
|
|
606
|
+
try {
|
|
607
|
+
return root.querySelector(selector);
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function querySelectorAcrossOpenShadow(root, selector) {
|
|
613
|
+
const direct = querySelectorInTree(root, selector);
|
|
614
|
+
if (direct) {
|
|
615
|
+
return direct;
|
|
616
|
+
}
|
|
617
|
+
const stack = [root];
|
|
618
|
+
while (stack.length > 0) {
|
|
619
|
+
const current = stack.pop();
|
|
620
|
+
const hosts = Array.from(current.querySelectorAll("*")).filter((item) => item.shadowRoot);
|
|
621
|
+
for (const host of hosts) {
|
|
622
|
+
if (!host.shadowRoot) {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const found = querySelectorInTree(host.shadowRoot, selector);
|
|
626
|
+
if (found) {
|
|
627
|
+
return found;
|
|
628
|
+
}
|
|
629
|
+
stack.push(host.shadowRoot);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
function querySelectorAllAcrossOpenShadow(root, selector) {
|
|
635
|
+
const collected = [];
|
|
636
|
+
const seen = /* @__PURE__ */ new Set();
|
|
637
|
+
const push = (element) => {
|
|
638
|
+
if (seen.has(element)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
seen.add(element);
|
|
642
|
+
collected.push(element);
|
|
643
|
+
};
|
|
644
|
+
try {
|
|
645
|
+
for (const found of Array.from(root.querySelectorAll(selector))) {
|
|
646
|
+
push(found);
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
const stack = [root];
|
|
652
|
+
while (stack.length > 0) {
|
|
653
|
+
const current = stack.pop();
|
|
654
|
+
const hosts = Array.from(current.querySelectorAll("*")).filter((item) => item.shadowRoot);
|
|
655
|
+
for (const host of hosts) {
|
|
656
|
+
if (!host.shadowRoot) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
for (const found of Array.from(host.shadowRoot.querySelectorAll(selector))) {
|
|
661
|
+
push(found);
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
stack.push(host.shadowRoot);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return collected;
|
|
670
|
+
}
|
|
671
|
+
function resolveFrameDocument(framePath) {
|
|
672
|
+
let currentDocument = document;
|
|
673
|
+
for (const selector of framePath) {
|
|
674
|
+
const frame = currentDocument.querySelector(selector);
|
|
675
|
+
if (!frame || !isFrameElement(frame)) {
|
|
676
|
+
return { ok: false, error: { code: "E_NOT_FOUND", message: `frame not found: ${selector}` } };
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const childDocument = frame.contentDocument;
|
|
680
|
+
if (!childDocument) {
|
|
681
|
+
return { ok: false, error: { code: "E_NOT_READY", message: `frame document unavailable: ${selector}` } };
|
|
682
|
+
}
|
|
683
|
+
currentDocument = childDocument;
|
|
684
|
+
} catch {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
error: {
|
|
688
|
+
code: "E_PERMISSION",
|
|
689
|
+
message: `cross-origin frame is not accessible: ${selector}`
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return { ok: true, document: currentDocument };
|
|
695
|
+
}
|
|
696
|
+
function resolveShadowRoot(base, path) {
|
|
697
|
+
let root = base;
|
|
698
|
+
for (const selector of path) {
|
|
699
|
+
const host = querySelectorAcrossOpenShadow(root, selector);
|
|
700
|
+
if (!host) {
|
|
701
|
+
return { ok: false, error: { code: "E_NOT_FOUND", message: `shadow host not found: ${selector}` } };
|
|
702
|
+
}
|
|
703
|
+
if (!host.shadowRoot) {
|
|
704
|
+
return { ok: false, error: { code: "E_NOT_READY", message: `shadow root unavailable or closed: ${selector}` } };
|
|
705
|
+
}
|
|
706
|
+
root = host.shadowRoot;
|
|
707
|
+
}
|
|
708
|
+
return { ok: true, root };
|
|
709
|
+
}
|
|
710
|
+
function resolveRootForLocator(locator) {
|
|
711
|
+
const framePath = [...contextState.framePath, ...Array.isArray(locator?.framePath) ? locator.framePath : []];
|
|
712
|
+
const frameResult = resolveFrameDocument(framePath);
|
|
713
|
+
if (!frameResult.ok) {
|
|
714
|
+
return frameResult;
|
|
715
|
+
}
|
|
716
|
+
const shadowPath = [...contextState.shadowPath];
|
|
717
|
+
const shadowResult = resolveShadowRoot(frameResult.document, shadowPath);
|
|
718
|
+
if (!shadowResult.ok) {
|
|
719
|
+
return shadowResult;
|
|
720
|
+
}
|
|
721
|
+
return { ok: true, root: shadowResult.root };
|
|
722
|
+
}
|
|
723
|
+
function collectElements(options = {}, locator) {
|
|
724
|
+
const rootResult = resolveRootForLocator(locator);
|
|
725
|
+
if (!rootResult.ok) {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
const root = rootResult.root;
|
|
729
|
+
const nodes = Array.from(root.querySelectorAll("*"));
|
|
730
|
+
const results = [];
|
|
731
|
+
elementCache.clear();
|
|
732
|
+
for (const element of nodes) {
|
|
733
|
+
if (!isInteractive(element) || !isElementVisible(element)) {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const rect = element.getBoundingClientRect();
|
|
737
|
+
const name = inferName(element, options);
|
|
738
|
+
const role = inferRole(element);
|
|
739
|
+
const text = redactElementText(element.innerText || element.textContent || "", options);
|
|
740
|
+
const eid = buildEid(element);
|
|
741
|
+
const selectors = {
|
|
742
|
+
css: toCssSelector(element),
|
|
743
|
+
text: text || name ? (text || name).slice(0, 80) : null,
|
|
744
|
+
aria: role && name ? `${role}:${name.slice(0, 80)}` : null
|
|
745
|
+
};
|
|
746
|
+
const combined = `${name} ${text}`;
|
|
747
|
+
const risk = unsafeKeywords.test(combined) || isInputElement(element) && element.type === "file" ? "high" : "low";
|
|
748
|
+
const item = {
|
|
749
|
+
eid,
|
|
750
|
+
tag: element.tagName.toLowerCase(),
|
|
751
|
+
role,
|
|
752
|
+
name,
|
|
753
|
+
text,
|
|
754
|
+
bbox: {
|
|
755
|
+
x: rect.x,
|
|
756
|
+
y: rect.y,
|
|
757
|
+
width: rect.width,
|
|
758
|
+
height: rect.height
|
|
759
|
+
},
|
|
760
|
+
selectors,
|
|
761
|
+
risk
|
|
762
|
+
};
|
|
763
|
+
results.push(item);
|
|
764
|
+
elementCache.set(eid, element);
|
|
765
|
+
}
|
|
766
|
+
return results;
|
|
767
|
+
}
|
|
768
|
+
function getInteractiveElements(root, includeShadow = true) {
|
|
769
|
+
const results = Array.from(root.querySelectorAll("*")).filter(
|
|
770
|
+
(element) => isInteractive(element) && isElementVisible(element)
|
|
771
|
+
);
|
|
772
|
+
if (!includeShadow) {
|
|
773
|
+
return results;
|
|
774
|
+
}
|
|
775
|
+
const queue = [...Array.from(root.querySelectorAll("*")).filter((element) => element.shadowRoot)];
|
|
776
|
+
while (queue.length > 0) {
|
|
777
|
+
const host = queue.shift();
|
|
778
|
+
if (!host.shadowRoot) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
for (const element of Array.from(host.shadowRoot.querySelectorAll("*"))) {
|
|
782
|
+
if (element.shadowRoot) {
|
|
783
|
+
queue.push(element);
|
|
784
|
+
}
|
|
785
|
+
if (isInteractive(element) && isElementVisible(element)) {
|
|
786
|
+
results.push(element);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return results;
|
|
791
|
+
}
|
|
792
|
+
function resolveLocator(locator) {
|
|
793
|
+
if (!locator) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
if (locator.eid) {
|
|
797
|
+
const refreshed = collectElements({}, locator);
|
|
798
|
+
if (refreshed.length > 0) {
|
|
799
|
+
const fromCache = elementCache.get(locator.eid);
|
|
800
|
+
if (fromCache) {
|
|
801
|
+
return fromCache;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const rootResult = resolveRootForLocator(locator);
|
|
806
|
+
if (!rootResult.ok) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
const root = rootResult.root;
|
|
810
|
+
const interactive = getInteractiveElements(root, locator.shadow !== "none");
|
|
811
|
+
if (locator.role || locator.name) {
|
|
812
|
+
const role = locator.role?.toLowerCase();
|
|
813
|
+
const name = locator.name?.toLowerCase();
|
|
814
|
+
const matches = interactive.filter((element) => {
|
|
815
|
+
const roleMatch = role ? inferRole(element).toLowerCase() === role : true;
|
|
816
|
+
const nameMatch = name ? inferName(element).toLowerCase().includes(name) : true;
|
|
817
|
+
return roleMatch && nameMatch;
|
|
818
|
+
});
|
|
819
|
+
const found = indexedCandidate(matches, locator);
|
|
820
|
+
if (found) {
|
|
821
|
+
return found;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (locator.text) {
|
|
825
|
+
const needle = locator.text.toLowerCase();
|
|
826
|
+
const matches = interactive.filter((element) => {
|
|
827
|
+
const text = (element.innerText || element.textContent || "").toLowerCase();
|
|
828
|
+
return text.includes(needle);
|
|
829
|
+
});
|
|
830
|
+
const found = indexedCandidate(matches, locator);
|
|
831
|
+
if (found) {
|
|
832
|
+
return found;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (locator.css) {
|
|
836
|
+
const parts = splitShadowSelector(locator.css);
|
|
837
|
+
let currentRoot = root;
|
|
838
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
839
|
+
const selector = parts[index];
|
|
840
|
+
const found = locator.shadow === "none" ? querySelectorInTree(currentRoot, selector) : querySelectorAcrossOpenShadow(currentRoot, selector);
|
|
841
|
+
if (!found) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
if (index === parts.length - 1) {
|
|
845
|
+
let matches = [];
|
|
846
|
+
if (locator.shadow === "none") {
|
|
847
|
+
try {
|
|
848
|
+
matches = Array.from(currentRoot.querySelectorAll(selector));
|
|
849
|
+
} catch {
|
|
850
|
+
matches = [];
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
matches = querySelectorAllAcrossOpenShadow(currentRoot, selector);
|
|
854
|
+
}
|
|
855
|
+
const visibleMatches = matches.filter((item) => isElementVisible(item));
|
|
856
|
+
return indexedCandidate(visibleMatches, locator) ?? null;
|
|
857
|
+
}
|
|
858
|
+
if (!found.shadowRoot) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
currentRoot = found.shadowRoot;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (locator.name) {
|
|
865
|
+
const fallback = indexedCandidate(
|
|
866
|
+
interactive.filter((element) => inferName(element).toLowerCase().includes(locator.name.toLowerCase())),
|
|
867
|
+
locator
|
|
868
|
+
);
|
|
869
|
+
if (fallback) {
|
|
870
|
+
return fallback;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
function ensureOverlayRoot() {
|
|
876
|
+
let root = document.getElementById("bak-overlay-root");
|
|
877
|
+
if (!root) {
|
|
878
|
+
root = document.createElement("div");
|
|
879
|
+
root.id = "bak-overlay-root";
|
|
880
|
+
root.style.position = "fixed";
|
|
881
|
+
root.style.bottom = "12px";
|
|
882
|
+
root.style.right = "12px";
|
|
883
|
+
root.style.zIndex = "2147483647";
|
|
884
|
+
root.style.fontFamily = "ui-sans-serif, system-ui";
|
|
885
|
+
document.body.appendChild(root);
|
|
886
|
+
const badge = document.createElement("div");
|
|
887
|
+
badge.textContent = "BAK";
|
|
888
|
+
badge.style.background = "#0f172a";
|
|
889
|
+
badge.style.color = "#ffffff";
|
|
890
|
+
badge.style.padding = "6px 10px";
|
|
891
|
+
badge.style.borderRadius = "999px";
|
|
892
|
+
badge.style.fontSize = "12px";
|
|
893
|
+
badge.style.boxShadow = "0 6px 20px rgba(2, 6, 23, 0.35)";
|
|
894
|
+
root.appendChild(badge);
|
|
895
|
+
}
|
|
896
|
+
return root;
|
|
897
|
+
}
|
|
898
|
+
async function askConfirm(message) {
|
|
899
|
+
ensureOverlayRoot();
|
|
900
|
+
return new Promise((resolve) => {
|
|
901
|
+
const panel = document.createElement("div");
|
|
902
|
+
panel.style.position = "fixed";
|
|
903
|
+
panel.style.right = "12px";
|
|
904
|
+
panel.style.bottom = "52px";
|
|
905
|
+
panel.style.width = "320px";
|
|
906
|
+
panel.style.background = "#fff";
|
|
907
|
+
panel.style.border = "1px solid #e2e8f0";
|
|
908
|
+
panel.style.borderRadius = "8px";
|
|
909
|
+
panel.style.boxShadow = "0 12px 30px rgba(15, 23, 42, 0.22)";
|
|
910
|
+
panel.style.padding = "12px";
|
|
911
|
+
panel.style.zIndex = "2147483647";
|
|
912
|
+
const title = document.createElement("div");
|
|
913
|
+
title.textContent = "High-risk action requires confirmation";
|
|
914
|
+
title.style.fontSize = "13px";
|
|
915
|
+
title.style.fontWeight = "600";
|
|
916
|
+
const desc = document.createElement("div");
|
|
917
|
+
desc.textContent = message;
|
|
918
|
+
desc.style.marginTop = "8px";
|
|
919
|
+
desc.style.fontSize = "12px";
|
|
920
|
+
desc.style.color = "#334155";
|
|
921
|
+
const row = document.createElement("div");
|
|
922
|
+
row.style.marginTop = "12px";
|
|
923
|
+
row.style.display = "flex";
|
|
924
|
+
row.style.gap = "8px";
|
|
925
|
+
const approve = document.createElement("button");
|
|
926
|
+
approve.textContent = "Approve";
|
|
927
|
+
approve.style.border = "none";
|
|
928
|
+
approve.style.background = "#16a34a";
|
|
929
|
+
approve.style.color = "#fff";
|
|
930
|
+
approve.style.padding = "8px 10px";
|
|
931
|
+
approve.style.borderRadius = "6px";
|
|
932
|
+
approve.style.cursor = "pointer";
|
|
933
|
+
const reject = document.createElement("button");
|
|
934
|
+
reject.textContent = "Reject";
|
|
935
|
+
reject.style.border = "1px solid #cbd5e1";
|
|
936
|
+
reject.style.background = "#fff";
|
|
937
|
+
reject.style.color = "#0f172a";
|
|
938
|
+
reject.style.padding = "8px 10px";
|
|
939
|
+
reject.style.borderRadius = "6px";
|
|
940
|
+
reject.style.cursor = "pointer";
|
|
941
|
+
const done = (value) => {
|
|
942
|
+
panel.remove();
|
|
943
|
+
resolve(value);
|
|
944
|
+
};
|
|
945
|
+
approve.addEventListener("click", () => done(true));
|
|
946
|
+
reject.addEventListener("click", () => done(false));
|
|
947
|
+
row.appendChild(approve);
|
|
948
|
+
row.appendChild(reject);
|
|
949
|
+
panel.appendChild(title);
|
|
950
|
+
panel.appendChild(desc);
|
|
951
|
+
panel.appendChild(row);
|
|
952
|
+
document.body.appendChild(panel);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
function highlightCandidates(candidates) {
|
|
956
|
+
const disposers = [];
|
|
957
|
+
for (const candidate of candidates) {
|
|
958
|
+
const element = elementCache.get(candidate.eid) ?? resolveLocator({ eid: candidate.eid });
|
|
959
|
+
if (!element) {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const oldOutline = element.style.outline;
|
|
963
|
+
element.style.outline = "2px solid #f97316";
|
|
964
|
+
disposers.push(() => {
|
|
965
|
+
element.style.outline = oldOutline;
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
return disposers;
|
|
969
|
+
}
|
|
970
|
+
async function pickCandidate(candidates) {
|
|
971
|
+
collectElements();
|
|
972
|
+
return new Promise((resolve) => {
|
|
973
|
+
const disposers = highlightCandidates(candidates);
|
|
974
|
+
const panel = document.createElement("div");
|
|
975
|
+
panel.style.position = "fixed";
|
|
976
|
+
panel.style.right = "12px";
|
|
977
|
+
panel.style.bottom = "52px";
|
|
978
|
+
panel.style.width = "340px";
|
|
979
|
+
panel.style.maxHeight = "320px";
|
|
980
|
+
panel.style.overflow = "auto";
|
|
981
|
+
panel.style.background = "#fff";
|
|
982
|
+
panel.style.border = "1px solid #e2e8f0";
|
|
983
|
+
panel.style.borderRadius = "8px";
|
|
984
|
+
panel.style.boxShadow = "0 12px 30px rgba(15, 23, 42, 0.22)";
|
|
985
|
+
panel.style.padding = "12px";
|
|
986
|
+
panel.style.zIndex = "2147483647";
|
|
987
|
+
const title = document.createElement("div");
|
|
988
|
+
title.textContent = "Action failed, choose a target to heal skill";
|
|
989
|
+
title.style.fontWeight = "600";
|
|
990
|
+
title.style.fontSize = "13px";
|
|
991
|
+
panel.appendChild(title);
|
|
992
|
+
const cleanup = (value) => {
|
|
993
|
+
panel.remove();
|
|
994
|
+
for (const dispose of disposers) {
|
|
995
|
+
dispose();
|
|
996
|
+
}
|
|
997
|
+
resolve(value);
|
|
998
|
+
};
|
|
999
|
+
for (const candidate of candidates.slice(0, 3)) {
|
|
1000
|
+
const btn = document.createElement("button");
|
|
1001
|
+
btn.textContent = `${candidate.role ?? candidate.tag}: ${candidate.name || candidate.text || candidate.eid}`;
|
|
1002
|
+
btn.style.display = "block";
|
|
1003
|
+
btn.style.width = "100%";
|
|
1004
|
+
btn.style.marginTop = "8px";
|
|
1005
|
+
btn.style.textAlign = "left";
|
|
1006
|
+
btn.style.border = "1px solid #cbd5e1";
|
|
1007
|
+
btn.style.borderRadius = "6px";
|
|
1008
|
+
btn.style.padding = "8px";
|
|
1009
|
+
btn.style.background = "#f8fafc";
|
|
1010
|
+
btn.style.cursor = "pointer";
|
|
1011
|
+
btn.addEventListener("click", () => cleanup(candidate.eid));
|
|
1012
|
+
panel.appendChild(btn);
|
|
1013
|
+
}
|
|
1014
|
+
const cancel = document.createElement("button");
|
|
1015
|
+
cancel.textContent = "Cancel";
|
|
1016
|
+
cancel.style.marginTop = "8px";
|
|
1017
|
+
cancel.style.border = "none";
|
|
1018
|
+
cancel.style.background = "#e2e8f0";
|
|
1019
|
+
cancel.style.padding = "8px 10px";
|
|
1020
|
+
cancel.style.borderRadius = "6px";
|
|
1021
|
+
cancel.style.cursor = "pointer";
|
|
1022
|
+
cancel.addEventListener("click", () => cleanup(null));
|
|
1023
|
+
panel.appendChild(cancel);
|
|
1024
|
+
document.body.appendChild(panel);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function failAction(code, message, data) {
|
|
1028
|
+
return { ok: false, error: { code, message, data } };
|
|
1029
|
+
}
|
|
1030
|
+
function failAssessment(code, message, data) {
|
|
1031
|
+
return { ok: false, error: { code, message, data } };
|
|
1032
|
+
}
|
|
1033
|
+
function indexedCandidate(candidates, locator) {
|
|
1034
|
+
if (candidates.length === 0) {
|
|
1035
|
+
return void 0;
|
|
1036
|
+
}
|
|
1037
|
+
const index = typeof locator?.index === "number" ? Math.max(0, Math.floor(locator.index)) : 0;
|
|
1038
|
+
return candidates[index];
|
|
1039
|
+
}
|
|
1040
|
+
function notFoundForLocator(locator, message) {
|
|
1041
|
+
const hint = unsupportedLocatorHint(locator);
|
|
1042
|
+
return {
|
|
1043
|
+
code: "E_NOT_FOUND",
|
|
1044
|
+
message,
|
|
1045
|
+
data: hint ? { hint } : void 0
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function describeNode(node) {
|
|
1049
|
+
if (!isHtmlElement(node)) {
|
|
1050
|
+
return "unknown";
|
|
1051
|
+
}
|
|
1052
|
+
const id = node.id ? `#${node.id}` : "";
|
|
1053
|
+
const classes = node.classList.length > 0 ? `.${Array.from(node.classList).slice(0, 2).join(".")}` : "";
|
|
1054
|
+
return `${node.tagName.toLowerCase()}${id}${classes}`;
|
|
1055
|
+
}
|
|
1056
|
+
function centerPoint(rect) {
|
|
1057
|
+
const minX = Math.max(1, Math.floor(rect.left + 1));
|
|
1058
|
+
const minY = Math.max(1, Math.floor(rect.top + 1));
|
|
1059
|
+
const maxX = Math.max(minX, Math.floor(rect.right - 1));
|
|
1060
|
+
const maxY = Math.max(minY, Math.floor(rect.bottom - 1));
|
|
1061
|
+
const x = Math.min(Math.max(Math.floor(rect.left + rect.width / 2), minX), maxX);
|
|
1062
|
+
const y = Math.min(Math.max(Math.floor(rect.top + rect.height / 2), minY), maxY);
|
|
1063
|
+
return { x, y };
|
|
1064
|
+
}
|
|
1065
|
+
function assessActionTarget(target, action) {
|
|
1066
|
+
if (!isElementVisible(target)) {
|
|
1067
|
+
return failAssessment("E_NOT_FOUND", `${action} target is not visible`);
|
|
1068
|
+
}
|
|
1069
|
+
if (target.disabled || target.getAttribute("aria-disabled") === "true") {
|
|
1070
|
+
return failAssessment("E_PERMISSION", `${action} target is disabled`);
|
|
1071
|
+
}
|
|
1072
|
+
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
1073
|
+
const rect = target.getBoundingClientRect();
|
|
1074
|
+
if (rect.width <= 1 || rect.height <= 1) {
|
|
1075
|
+
return failAssessment("E_NOT_FOUND", `${action} target has invalid bounds`);
|
|
1076
|
+
}
|
|
1077
|
+
const ownerDocument = target.ownerDocument ?? document;
|
|
1078
|
+
const point = centerPoint(rect);
|
|
1079
|
+
const hit = ownerDocument.elementFromPoint(point.x, point.y);
|
|
1080
|
+
if (!hit) {
|
|
1081
|
+
return failAssessment("E_NOT_FOUND", `${action} target is outside viewport`);
|
|
1082
|
+
}
|
|
1083
|
+
let shadowHostBridge = false;
|
|
1084
|
+
const rootNode = target.getRootNode();
|
|
1085
|
+
if (rootNode instanceof ShadowRoot) {
|
|
1086
|
+
shadowHostBridge = hit === rootNode.host;
|
|
1087
|
+
}
|
|
1088
|
+
if (hit !== target && !target.contains(hit) && !shadowHostBridge) {
|
|
1089
|
+
return failAssessment("E_PERMISSION", `${action} target is obstructed by ${describeNode(hit)}`, {
|
|
1090
|
+
obstructedBy: describeNode(hit)
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
return { ok: true, point };
|
|
1094
|
+
}
|
|
1095
|
+
function dispatchPointer(target, type, point) {
|
|
1096
|
+
if (typeof PointerEvent === "undefined") {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
target.dispatchEvent(
|
|
1100
|
+
new PointerEvent(type, {
|
|
1101
|
+
bubbles: true,
|
|
1102
|
+
cancelable: true,
|
|
1103
|
+
composed: true,
|
|
1104
|
+
clientX: point.x,
|
|
1105
|
+
clientY: point.y,
|
|
1106
|
+
pointerId: 1,
|
|
1107
|
+
pointerType: "mouse",
|
|
1108
|
+
isPrimary: true,
|
|
1109
|
+
button: 0,
|
|
1110
|
+
buttons: type === "pointerup" ? 0 : 1
|
|
1111
|
+
})
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
function dispatchMouse(target, type, point) {
|
|
1115
|
+
target.dispatchEvent(
|
|
1116
|
+
new MouseEvent(type, {
|
|
1117
|
+
bubbles: true,
|
|
1118
|
+
cancelable: true,
|
|
1119
|
+
composed: true,
|
|
1120
|
+
clientX: point.x,
|
|
1121
|
+
clientY: point.y,
|
|
1122
|
+
button: 0,
|
|
1123
|
+
buttons: type === "mouseup" || type === "click" ? 0 : 1
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
function performClick(target, point) {
|
|
1128
|
+
target.focus({ preventScroll: true });
|
|
1129
|
+
dispatchPointer(target, "pointerdown", point);
|
|
1130
|
+
dispatchMouse(target, "mousedown", point);
|
|
1131
|
+
dispatchPointer(target, "pointerup", point);
|
|
1132
|
+
dispatchMouse(target, "mouseup", point);
|
|
1133
|
+
dispatchMouse(target, "click", point);
|
|
1134
|
+
}
|
|
1135
|
+
function dispatchInputEvents(target) {
|
|
1136
|
+
try {
|
|
1137
|
+
target.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
|
|
1138
|
+
} catch {
|
|
1139
|
+
target.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
1140
|
+
}
|
|
1141
|
+
target.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
1142
|
+
}
|
|
1143
|
+
function setNativeValue(target, nextValue) {
|
|
1144
|
+
const proto = isInputElement(target) ? target.ownerDocument.defaultView?.HTMLInputElement.prototype ?? HTMLInputElement.prototype : target.ownerDocument.defaultView?.HTMLTextAreaElement.prototype ?? HTMLTextAreaElement.prototype;
|
|
1145
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
|
|
1146
|
+
if (descriptor?.set) {
|
|
1147
|
+
descriptor.set.call(target, nextValue);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
target.value = nextValue;
|
|
1151
|
+
}
|
|
1152
|
+
function insertContentEditableText(target, text, clear) {
|
|
1153
|
+
target.focus({ preventScroll: true });
|
|
1154
|
+
if (clear) {
|
|
1155
|
+
target.textContent = "";
|
|
1156
|
+
}
|
|
1157
|
+
if (!text) {
|
|
1158
|
+
dispatchInputEvents(target);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const selection = window.getSelection();
|
|
1162
|
+
if (!selection) {
|
|
1163
|
+
target.append(document.createTextNode(text));
|
|
1164
|
+
dispatchInputEvents(target);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const range = document.createRange();
|
|
1168
|
+
range.selectNodeContents(target);
|
|
1169
|
+
range.collapse(false);
|
|
1170
|
+
selection.removeAllRanges();
|
|
1171
|
+
selection.addRange(range);
|
|
1172
|
+
const node = document.createTextNode(text);
|
|
1173
|
+
range.insertNode(node);
|
|
1174
|
+
range.setStartAfter(node);
|
|
1175
|
+
range.setEndAfter(node);
|
|
1176
|
+
selection.removeAllRanges();
|
|
1177
|
+
selection.addRange(range);
|
|
1178
|
+
dispatchInputEvents(target);
|
|
1179
|
+
}
|
|
1180
|
+
function isEditable(target) {
|
|
1181
|
+
return isInputElement(target) || isTextAreaElement(target);
|
|
1182
|
+
}
|
|
1183
|
+
function keyEvent(type, key, extra) {
|
|
1184
|
+
return new KeyboardEvent(type, {
|
|
1185
|
+
bubbles: true,
|
|
1186
|
+
cancelable: true,
|
|
1187
|
+
composed: true,
|
|
1188
|
+
key,
|
|
1189
|
+
code: key.length === 1 ? `Key${key.toUpperCase()}` : key,
|
|
1190
|
+
...extra
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function parseHotkey(keys) {
|
|
1194
|
+
const lowered = keys.map((item) => item.toLowerCase());
|
|
1195
|
+
const key = keys.find((item) => !["ctrl", "control", "alt", "shift", "meta", "cmd"].includes(item.toLowerCase())) ?? keys[0] ?? "";
|
|
1196
|
+
return {
|
|
1197
|
+
key,
|
|
1198
|
+
ctrlKey: lowered.includes("ctrl") || lowered.includes("control"),
|
|
1199
|
+
altKey: lowered.includes("alt"),
|
|
1200
|
+
shiftKey: lowered.includes("shift"),
|
|
1201
|
+
metaKey: lowered.includes("meta") || lowered.includes("cmd")
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function domSummary() {
|
|
1205
|
+
const allElements = Array.from(document.querySelectorAll("*"));
|
|
1206
|
+
const interactiveElements = Array.from(document.querySelectorAll("*")).filter((element) => isInteractive(element));
|
|
1207
|
+
const tags = /* @__PURE__ */ new Map();
|
|
1208
|
+
for (const element of allElements) {
|
|
1209
|
+
const tag = element.tagName.toLowerCase();
|
|
1210
|
+
tags.set(tag, (tags.get(tag) ?? 0) + 1);
|
|
1211
|
+
}
|
|
1212
|
+
const tagHistogram = [...tags.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([tag, count]) => ({ tag, count }));
|
|
1213
|
+
const shadowHosts = Array.from(document.querySelectorAll("*")).filter((element) => element.shadowRoot).length;
|
|
1214
|
+
return {
|
|
1215
|
+
url: window.location.href,
|
|
1216
|
+
title: document.title,
|
|
1217
|
+
totalElements: allElements.length,
|
|
1218
|
+
interactiveElements: interactiveElements.length,
|
|
1219
|
+
headings: document.querySelectorAll("h1,h2,h3,h4,h5,h6").length,
|
|
1220
|
+
links: document.querySelectorAll("a[href]").length,
|
|
1221
|
+
forms: document.querySelectorAll("form").length,
|
|
1222
|
+
iframes: document.querySelectorAll("iframe,frame").length,
|
|
1223
|
+
shadowHosts,
|
|
1224
|
+
tagHistogram
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function pageTextChunks(maxChunks = 24, chunkSize = 320) {
|
|
1228
|
+
const nodes = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,td,th,label,button,a,span,div"));
|
|
1229
|
+
const chunks = [];
|
|
1230
|
+
for (const node of nodes) {
|
|
1231
|
+
if (!isElementVisible(node)) {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
const text = (node.innerText || node.textContent || "").replace(/\s+/g, " ").trim();
|
|
1235
|
+
if (!text) {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
chunks.push({
|
|
1239
|
+
chunkId: `chunk_${chunks.length + 1}`,
|
|
1240
|
+
text: text.slice(0, chunkSize),
|
|
1241
|
+
sourceTag: node.tagName.toLowerCase()
|
|
1242
|
+
});
|
|
1243
|
+
if (chunks.length >= maxChunks) {
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return chunks;
|
|
1248
|
+
}
|
|
1249
|
+
function pageAccessibility(limit = 200) {
|
|
1250
|
+
const nodes = [];
|
|
1251
|
+
for (const element of getInteractiveElements(document, true).slice(0, limit)) {
|
|
1252
|
+
nodes.push({
|
|
1253
|
+
role: inferRole(element),
|
|
1254
|
+
name: inferName(element),
|
|
1255
|
+
tag: element.tagName.toLowerCase(),
|
|
1256
|
+
eid: buildEid(element)
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
return nodes;
|
|
1260
|
+
}
|
|
1261
|
+
function pageMetrics() {
|
|
1262
|
+
const navigation = performance.getEntriesByType("navigation")[0];
|
|
1263
|
+
const resources = performance.getEntriesByType("resource");
|
|
1264
|
+
return {
|
|
1265
|
+
navigation: {
|
|
1266
|
+
durationMs: navigation?.duration ?? 0,
|
|
1267
|
+
domContentLoadedMs: navigation ? navigation.domContentLoadedEventEnd - navigation.startTime : 0,
|
|
1268
|
+
loadEventMs: navigation ? navigation.loadEventEnd - navigation.startTime : 0
|
|
1269
|
+
},
|
|
1270
|
+
longTasks: {
|
|
1271
|
+
count: longTaskCount,
|
|
1272
|
+
totalDurationMs: Number(longTaskDurationMs.toFixed(2))
|
|
1273
|
+
},
|
|
1274
|
+
resources: {
|
|
1275
|
+
count: resources.length,
|
|
1276
|
+
transferSize: resources.reduce((sum, item) => sum + (item.transferSize ?? 0), 0),
|
|
1277
|
+
encodedBodySize: resources.reduce((sum, item) => sum + (item.encodedBodySize ?? 0), 0)
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
function performanceNetworkEntries() {
|
|
1282
|
+
const resources = performance.getEntriesByType("resource");
|
|
1283
|
+
const now = Date.now();
|
|
1284
|
+
const perfNow = performance.now();
|
|
1285
|
+
const entries = [];
|
|
1286
|
+
for (let index = 0; index < resources.length; index += 1) {
|
|
1287
|
+
const resource = resources[index];
|
|
1288
|
+
if (resource.startTime < performanceBaselineMs) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const responseEnd = resource.responseEnd > 0 ? resource.responseEnd : resource.startTime + resource.duration;
|
|
1292
|
+
const ts = now - Math.max(0, Math.round(perfNow - responseEnd));
|
|
1293
|
+
entries.push({
|
|
1294
|
+
id: `perf_${index}_${Math.round(resource.startTime)}_${fnv1a(resource.name).slice(0, 8)}`,
|
|
1295
|
+
kind: "resource",
|
|
1296
|
+
method: "GET",
|
|
1297
|
+
url: resource.name,
|
|
1298
|
+
status: 0,
|
|
1299
|
+
ok: true,
|
|
1300
|
+
ts,
|
|
1301
|
+
durationMs: Math.max(0, resource.duration),
|
|
1302
|
+
responseBytes: resource.transferSize > 0 ? resource.transferSize : void 0
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
return entries;
|
|
1306
|
+
}
|
|
1307
|
+
function networkSnapshotEntries() {
|
|
1308
|
+
const merged = [...networkEntries];
|
|
1309
|
+
const seen = new Set(merged.map((entry) => `${entry.kind}|${entry.url}|${Math.round(entry.ts / 10)}`));
|
|
1310
|
+
for (const entry of performanceNetworkEntries()) {
|
|
1311
|
+
const key = `${entry.kind}|${entry.url}|${Math.round(entry.ts / 10)}`;
|
|
1312
|
+
if (seen.has(key)) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
seen.add(key);
|
|
1316
|
+
merged.push(entry);
|
|
1317
|
+
}
|
|
1318
|
+
merged.sort((a, b) => a.ts - b.ts);
|
|
1319
|
+
return merged;
|
|
1320
|
+
}
|
|
1321
|
+
function filterNetworkEntries(params) {
|
|
1322
|
+
const urlIncludes = typeof params.urlIncludes === "string" ? params.urlIncludes : "";
|
|
1323
|
+
const method = typeof params.method === "string" ? params.method.toUpperCase() : "";
|
|
1324
|
+
const status = typeof params.status === "number" ? params.status : void 0;
|
|
1325
|
+
const sinceTs = typeof params.sinceTs === "number" ? params.sinceTs : void 0;
|
|
1326
|
+
const limit = typeof params.limit === "number" ? Math.max(1, Math.min(500, Math.floor(params.limit))) : 50;
|
|
1327
|
+
return networkSnapshotEntries().filter((entry) => {
|
|
1328
|
+
if (typeof sinceTs === "number" && entry.ts < sinceTs) {
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
if (urlIncludes && !entry.url.includes(urlIncludes)) {
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
if (method && entry.method.toUpperCase() !== method) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
if (typeof status === "number" && entry.status !== status) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
return true;
|
|
1341
|
+
}).slice(-limit).reverse();
|
|
1342
|
+
}
|
|
1343
|
+
async function waitForNetwork(params) {
|
|
1344
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? Math.max(1, params.timeoutMs) : 5e3;
|
|
1345
|
+
const sinceTs = typeof params.sinceTs === "number" ? params.sinceTs : Date.now() - timeoutMs;
|
|
1346
|
+
const start = Date.now();
|
|
1347
|
+
while (Date.now() - start < timeoutMs) {
|
|
1348
|
+
const matched = filterNetworkEntries({ ...params, sinceTs, limit: 1 })[0];
|
|
1349
|
+
if (matched) {
|
|
1350
|
+
return matched;
|
|
1351
|
+
}
|
|
1352
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
1353
|
+
}
|
|
1354
|
+
throw { code: "E_TIMEOUT", message: "network.waitFor timeout" };
|
|
1355
|
+
}
|
|
1356
|
+
function performDoubleClick(target, point) {
|
|
1357
|
+
performClick(target, point);
|
|
1358
|
+
performClick(target, point);
|
|
1359
|
+
dispatchMouse(target, "dblclick", point);
|
|
1360
|
+
}
|
|
1361
|
+
function performRightClick(target, point) {
|
|
1362
|
+
dispatchPointer(target, "pointerdown", point);
|
|
1363
|
+
target.dispatchEvent(
|
|
1364
|
+
new MouseEvent("mousedown", {
|
|
1365
|
+
bubbles: true,
|
|
1366
|
+
cancelable: true,
|
|
1367
|
+
composed: true,
|
|
1368
|
+
clientX: point.x,
|
|
1369
|
+
clientY: point.y,
|
|
1370
|
+
button: 2,
|
|
1371
|
+
buttons: 2
|
|
1372
|
+
})
|
|
1373
|
+
);
|
|
1374
|
+
dispatchPointer(target, "pointerup", point);
|
|
1375
|
+
target.dispatchEvent(
|
|
1376
|
+
new MouseEvent("mouseup", {
|
|
1377
|
+
bubbles: true,
|
|
1378
|
+
cancelable: true,
|
|
1379
|
+
composed: true,
|
|
1380
|
+
clientX: point.x,
|
|
1381
|
+
clientY: point.y,
|
|
1382
|
+
button: 2,
|
|
1383
|
+
buttons: 0
|
|
1384
|
+
})
|
|
1385
|
+
);
|
|
1386
|
+
target.dispatchEvent(
|
|
1387
|
+
new MouseEvent("contextmenu", {
|
|
1388
|
+
bubbles: true,
|
|
1389
|
+
cancelable: true,
|
|
1390
|
+
composed: true,
|
|
1391
|
+
clientX: point.x,
|
|
1392
|
+
clientY: point.y,
|
|
1393
|
+
button: 2
|
|
1394
|
+
})
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
async function handleAction(message) {
|
|
1398
|
+
try {
|
|
1399
|
+
ensureOverlayRoot();
|
|
1400
|
+
if (message.action === "scroll") {
|
|
1401
|
+
if (message.locator) {
|
|
1402
|
+
const target2 = resolveLocator(message.locator);
|
|
1403
|
+
if (!target2) {
|
|
1404
|
+
return failAction("E_NOT_FOUND", "scroll target not found");
|
|
1405
|
+
}
|
|
1406
|
+
target2.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
1407
|
+
return { ok: true };
|
|
1408
|
+
}
|
|
1409
|
+
window.scrollBy({ left: Number(message.dx ?? 0), top: Number(message.dy ?? 320), behavior: "smooth" });
|
|
1410
|
+
return { ok: true };
|
|
1411
|
+
}
|
|
1412
|
+
const target = resolveLocator(message.locator);
|
|
1413
|
+
if (!target) {
|
|
1414
|
+
return { ok: false, error: notFoundForLocator(message.locator, "Target not found") };
|
|
1415
|
+
}
|
|
1416
|
+
const name = inferName(target);
|
|
1417
|
+
const text = (target.innerText || target.textContent || "").trim();
|
|
1418
|
+
const riskText = `${name} ${text}`;
|
|
1419
|
+
const isHighRisk = message.requiresConfirm === true || unsafeKeywords.test(riskText) || isInputElement(target) && target.type === "file";
|
|
1420
|
+
if (isHighRisk) {
|
|
1421
|
+
const approved = await askConfirm(`Action: ${message.action} on "${name || text || target.tagName}"`);
|
|
1422
|
+
if (!approved) {
|
|
1423
|
+
return failAction("E_NEED_USER_CONFIRM", "User rejected high-risk action");
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (message.action === "focus") {
|
|
1427
|
+
target.focus({ preventScroll: true });
|
|
1428
|
+
return { ok: true };
|
|
1429
|
+
}
|
|
1430
|
+
if (message.action === "blur") {
|
|
1431
|
+
target.blur();
|
|
1432
|
+
return { ok: true };
|
|
1433
|
+
}
|
|
1434
|
+
if (message.action === "scrollIntoView") {
|
|
1435
|
+
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
1436
|
+
return { ok: true };
|
|
1437
|
+
}
|
|
1438
|
+
if (message.action === "dragDrop") {
|
|
1439
|
+
const from = message.from ? resolveLocator(message.from) : target;
|
|
1440
|
+
const to = message.to ? resolveLocator(message.to) : target;
|
|
1441
|
+
if (!from || !to) {
|
|
1442
|
+
return failAction("E_NOT_FOUND", "dragDrop endpoints not found");
|
|
1443
|
+
}
|
|
1444
|
+
const fromPoint = centerPoint(from.getBoundingClientRect());
|
|
1445
|
+
const toPoint = centerPoint(to.getBoundingClientRect());
|
|
1446
|
+
const transfer = new DataTransfer();
|
|
1447
|
+
from.dispatchEvent(new DragEvent("dragstart", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
|
1448
|
+
to.dispatchEvent(
|
|
1449
|
+
new DragEvent("dragenter", { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: toPoint.x, clientY: toPoint.y })
|
|
1450
|
+
);
|
|
1451
|
+
to.dispatchEvent(
|
|
1452
|
+
new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: toPoint.x, clientY: toPoint.y })
|
|
1453
|
+
);
|
|
1454
|
+
to.dispatchEvent(
|
|
1455
|
+
new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: toPoint.x, clientY: toPoint.y })
|
|
1456
|
+
);
|
|
1457
|
+
from.dispatchEvent(
|
|
1458
|
+
new DragEvent("dragend", { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: fromPoint.x, clientY: fromPoint.y })
|
|
1459
|
+
);
|
|
1460
|
+
return { ok: true };
|
|
1461
|
+
}
|
|
1462
|
+
const assessed = assessActionTarget(target, message.action);
|
|
1463
|
+
if (!assessed.ok) {
|
|
1464
|
+
return assessed;
|
|
1465
|
+
}
|
|
1466
|
+
if (message.action === "click") {
|
|
1467
|
+
performClick(target, assessed.point);
|
|
1468
|
+
return { ok: true };
|
|
1469
|
+
}
|
|
1470
|
+
if (message.action === "doubleClick") {
|
|
1471
|
+
performDoubleClick(target, assessed.point);
|
|
1472
|
+
return { ok: true };
|
|
1473
|
+
}
|
|
1474
|
+
if (message.action === "rightClick") {
|
|
1475
|
+
performRightClick(target, assessed.point);
|
|
1476
|
+
return { ok: true };
|
|
1477
|
+
}
|
|
1478
|
+
if (message.action === "hover") {
|
|
1479
|
+
dispatchPointer(target, "pointermove", assessed.point);
|
|
1480
|
+
dispatchMouse(target, "mousemove", assessed.point);
|
|
1481
|
+
target.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, clientX: assessed.point.x, clientY: assessed.point.y }));
|
|
1482
|
+
return { ok: true };
|
|
1483
|
+
}
|
|
1484
|
+
if (message.action === "type") {
|
|
1485
|
+
if (!(isEditable(target) || target.isContentEditable)) {
|
|
1486
|
+
return failAction("E_NOT_FOUND", "Type target is not editable");
|
|
1487
|
+
}
|
|
1488
|
+
if (isEditable(target)) {
|
|
1489
|
+
target.focus({ preventScroll: true });
|
|
1490
|
+
const appendText = message.text ?? "";
|
|
1491
|
+
const nextValue = message.clear ? appendText : `${target.value}${appendText}`;
|
|
1492
|
+
setNativeValue(target, nextValue);
|
|
1493
|
+
dispatchInputEvents(target);
|
|
1494
|
+
} else {
|
|
1495
|
+
insertContentEditableText(target, message.text ?? "", Boolean(message.clear));
|
|
1496
|
+
}
|
|
1497
|
+
return { ok: true };
|
|
1498
|
+
}
|
|
1499
|
+
if (message.action === "select") {
|
|
1500
|
+
if (!(target instanceof HTMLSelectElement)) {
|
|
1501
|
+
return failAction("E_NOT_FOUND", "select target is not a <select> element");
|
|
1502
|
+
}
|
|
1503
|
+
const values = message.values ?? [];
|
|
1504
|
+
if (values.length === 0) {
|
|
1505
|
+
return failAction("E_INVALID_PARAMS", "values is required");
|
|
1506
|
+
}
|
|
1507
|
+
for (const option of Array.from(target.options)) {
|
|
1508
|
+
option.selected = values.includes(option.value) || values.includes(option.text);
|
|
1509
|
+
}
|
|
1510
|
+
dispatchInputEvents(target);
|
|
1511
|
+
return { ok: true };
|
|
1512
|
+
}
|
|
1513
|
+
if (message.action === "check" || message.action === "uncheck") {
|
|
1514
|
+
if (!isInputElement(target) || target.type !== "checkbox" && target.type !== "radio") {
|
|
1515
|
+
return failAction("E_NOT_FOUND", `${message.action} target must be checkbox/radio`);
|
|
1516
|
+
}
|
|
1517
|
+
const desired = message.action === "check";
|
|
1518
|
+
if (target.checked !== desired) {
|
|
1519
|
+
target.checked = desired;
|
|
1520
|
+
dispatchInputEvents(target);
|
|
1521
|
+
}
|
|
1522
|
+
return { ok: true };
|
|
1523
|
+
}
|
|
1524
|
+
return failAction("E_NOT_FOUND", `Unsupported action ${message.action}`);
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
return failAction("E_INTERNAL", error instanceof Error ? error.message : String(error));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function waitConditionMet(message, root) {
|
|
1530
|
+
if (message.mode === "selector") {
|
|
1531
|
+
return Boolean(querySelectorAcrossOpenShadow(root, message.value));
|
|
1532
|
+
}
|
|
1533
|
+
if (message.mode === "text") {
|
|
1534
|
+
if (root instanceof Document) {
|
|
1535
|
+
const bodyText = root.body?.innerText ?? root.documentElement?.textContent ?? "";
|
|
1536
|
+
return bodyText.includes(message.value);
|
|
1537
|
+
}
|
|
1538
|
+
const text = root.textContent ?? "";
|
|
1539
|
+
return text.includes(message.value);
|
|
1540
|
+
}
|
|
1541
|
+
return window.location.href.includes(message.value);
|
|
1542
|
+
}
|
|
1543
|
+
async function waitFor(message) {
|
|
1544
|
+
const timeoutMs = message.timeoutMs ?? 5e3;
|
|
1545
|
+
const rootResult = resolveRootForLocator();
|
|
1546
|
+
if (!rootResult.ok) {
|
|
1547
|
+
return { ok: false, error: rootResult.error };
|
|
1548
|
+
}
|
|
1549
|
+
const root = rootResult.root;
|
|
1550
|
+
if (waitConditionMet(message, root)) {
|
|
1551
|
+
return { ok: true };
|
|
1552
|
+
}
|
|
1553
|
+
return new Promise((resolve) => {
|
|
1554
|
+
let done = false;
|
|
1555
|
+
const finish = (result) => {
|
|
1556
|
+
if (done) {
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
done = true;
|
|
1560
|
+
observer.disconnect();
|
|
1561
|
+
clearInterval(intervalId);
|
|
1562
|
+
clearTimeout(timerId);
|
|
1563
|
+
resolve(result);
|
|
1564
|
+
};
|
|
1565
|
+
const check = () => {
|
|
1566
|
+
const latestRoot = resolveRootForLocator();
|
|
1567
|
+
if (!latestRoot.ok) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
if (waitConditionMet(message, latestRoot.root)) {
|
|
1571
|
+
finish({ ok: true });
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
const observer = new MutationObserver(() => {
|
|
1575
|
+
check();
|
|
1576
|
+
});
|
|
1577
|
+
const observationRoot = root instanceof Document ? root.documentElement : root;
|
|
1578
|
+
if (observationRoot) {
|
|
1579
|
+
observer.observe(observationRoot, {
|
|
1580
|
+
childList: true,
|
|
1581
|
+
subtree: true,
|
|
1582
|
+
attributes: true,
|
|
1583
|
+
characterData: true
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
const intervalId = setInterval(() => {
|
|
1587
|
+
check();
|
|
1588
|
+
}, 120);
|
|
1589
|
+
const timerId = setTimeout(() => {
|
|
1590
|
+
finish(failAction("E_TIMEOUT", `wait timeout: ${message.mode}=${message.value}`));
|
|
1591
|
+
}, timeoutMs);
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
async function dispatchRpc(method, params = {}) {
|
|
1595
|
+
switch (method) {
|
|
1596
|
+
case "page.title":
|
|
1597
|
+
return { title: document.title };
|
|
1598
|
+
case "page.url":
|
|
1599
|
+
return { url: window.location.href };
|
|
1600
|
+
case "page.text":
|
|
1601
|
+
return {
|
|
1602
|
+
chunks: pageTextChunks(
|
|
1603
|
+
typeof params.maxChunks === "number" ? params.maxChunks : 24,
|
|
1604
|
+
typeof params.chunkSize === "number" ? params.chunkSize : 320
|
|
1605
|
+
)
|
|
1606
|
+
};
|
|
1607
|
+
case "page.dom":
|
|
1608
|
+
return { summary: domSummary() };
|
|
1609
|
+
case "page.accessibilityTree":
|
|
1610
|
+
return {
|
|
1611
|
+
nodes: pageAccessibility(typeof params.limit === "number" ? params.limit : 200)
|
|
1612
|
+
};
|
|
1613
|
+
case "page.scrollTo": {
|
|
1614
|
+
const x = typeof params.x === "number" ? params.x : window.scrollX;
|
|
1615
|
+
const y = typeof params.y === "number" ? params.y : window.scrollY;
|
|
1616
|
+
const behavior = params.behavior === "smooth" ? "smooth" : "auto";
|
|
1617
|
+
window.scrollTo({ left: x, top: y, behavior });
|
|
1618
|
+
return { ok: true, x: window.scrollX, y: window.scrollY };
|
|
1619
|
+
}
|
|
1620
|
+
case "page.metrics":
|
|
1621
|
+
return pageMetrics();
|
|
1622
|
+
case "page.viewport":
|
|
1623
|
+
return {
|
|
1624
|
+
width: window.innerWidth,
|
|
1625
|
+
height: window.innerHeight,
|
|
1626
|
+
devicePixelRatio: window.devicePixelRatio
|
|
1627
|
+
};
|
|
1628
|
+
case "element.hover":
|
|
1629
|
+
return handleAction({ type: "bak.performAction", action: "hover", locator: params.locator }).then((res) => {
|
|
1630
|
+
if (!res.ok) {
|
|
1631
|
+
throw res.error;
|
|
1632
|
+
}
|
|
1633
|
+
return { ok: true };
|
|
1634
|
+
});
|
|
1635
|
+
case "element.doubleClick":
|
|
1636
|
+
return handleAction({
|
|
1637
|
+
type: "bak.performAction",
|
|
1638
|
+
action: "doubleClick",
|
|
1639
|
+
locator: params.locator,
|
|
1640
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1641
|
+
}).then((res) => {
|
|
1642
|
+
if (!res.ok) {
|
|
1643
|
+
throw res.error;
|
|
1644
|
+
}
|
|
1645
|
+
return { ok: true };
|
|
1646
|
+
});
|
|
1647
|
+
case "element.rightClick":
|
|
1648
|
+
return handleAction({
|
|
1649
|
+
type: "bak.performAction",
|
|
1650
|
+
action: "rightClick",
|
|
1651
|
+
locator: params.locator,
|
|
1652
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1653
|
+
}).then((res) => {
|
|
1654
|
+
if (!res.ok) {
|
|
1655
|
+
throw res.error;
|
|
1656
|
+
}
|
|
1657
|
+
return { ok: true };
|
|
1658
|
+
});
|
|
1659
|
+
case "element.dragDrop":
|
|
1660
|
+
return handleAction({
|
|
1661
|
+
type: "bak.performAction",
|
|
1662
|
+
action: "dragDrop",
|
|
1663
|
+
locator: params.from,
|
|
1664
|
+
from: params.from,
|
|
1665
|
+
to: params.to,
|
|
1666
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1667
|
+
}).then((res) => {
|
|
1668
|
+
if (!res.ok) {
|
|
1669
|
+
throw res.error;
|
|
1670
|
+
}
|
|
1671
|
+
return { ok: true };
|
|
1672
|
+
});
|
|
1673
|
+
case "element.select":
|
|
1674
|
+
return handleAction({
|
|
1675
|
+
type: "bak.performAction",
|
|
1676
|
+
action: "select",
|
|
1677
|
+
locator: params.locator,
|
|
1678
|
+
values: Array.isArray(params.values) ? params.values : [],
|
|
1679
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1680
|
+
}).then((res) => {
|
|
1681
|
+
if (!res.ok) {
|
|
1682
|
+
throw res.error;
|
|
1683
|
+
}
|
|
1684
|
+
return { ok: true };
|
|
1685
|
+
});
|
|
1686
|
+
case "element.check":
|
|
1687
|
+
return handleAction({
|
|
1688
|
+
type: "bak.performAction",
|
|
1689
|
+
action: "check",
|
|
1690
|
+
locator: params.locator,
|
|
1691
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1692
|
+
}).then((res) => {
|
|
1693
|
+
if (!res.ok) {
|
|
1694
|
+
throw res.error;
|
|
1695
|
+
}
|
|
1696
|
+
return { ok: true };
|
|
1697
|
+
});
|
|
1698
|
+
case "element.uncheck":
|
|
1699
|
+
return handleAction({
|
|
1700
|
+
type: "bak.performAction",
|
|
1701
|
+
action: "uncheck",
|
|
1702
|
+
locator: params.locator,
|
|
1703
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1704
|
+
}).then((res) => {
|
|
1705
|
+
if (!res.ok) {
|
|
1706
|
+
throw res.error;
|
|
1707
|
+
}
|
|
1708
|
+
return { ok: true };
|
|
1709
|
+
});
|
|
1710
|
+
case "element.scrollIntoView":
|
|
1711
|
+
return handleAction({
|
|
1712
|
+
type: "bak.performAction",
|
|
1713
|
+
action: "scrollIntoView",
|
|
1714
|
+
locator: params.locator
|
|
1715
|
+
}).then((res) => {
|
|
1716
|
+
if (!res.ok) {
|
|
1717
|
+
throw res.error;
|
|
1718
|
+
}
|
|
1719
|
+
return { ok: true };
|
|
1720
|
+
});
|
|
1721
|
+
case "element.focus":
|
|
1722
|
+
return handleAction({
|
|
1723
|
+
type: "bak.performAction",
|
|
1724
|
+
action: "focus",
|
|
1725
|
+
locator: params.locator
|
|
1726
|
+
}).then((res) => {
|
|
1727
|
+
if (!res.ok) {
|
|
1728
|
+
throw res.error;
|
|
1729
|
+
}
|
|
1730
|
+
return { ok: true };
|
|
1731
|
+
});
|
|
1732
|
+
case "element.blur":
|
|
1733
|
+
return handleAction({
|
|
1734
|
+
type: "bak.performAction",
|
|
1735
|
+
action: "blur",
|
|
1736
|
+
locator: params.locator
|
|
1737
|
+
}).then((res) => {
|
|
1738
|
+
if (!res.ok) {
|
|
1739
|
+
throw res.error;
|
|
1740
|
+
}
|
|
1741
|
+
return { ok: true };
|
|
1742
|
+
});
|
|
1743
|
+
case "element.get": {
|
|
1744
|
+
const target = resolveLocator(params.locator);
|
|
1745
|
+
if (!target) {
|
|
1746
|
+
throw notFoundForLocator(params.locator, "element.get target not found");
|
|
1747
|
+
}
|
|
1748
|
+
const elements = collectElements({}, params.locator);
|
|
1749
|
+
const eid = buildEid(target);
|
|
1750
|
+
const element = elements.find((item) => item.eid === eid);
|
|
1751
|
+
if (!element) {
|
|
1752
|
+
throw { code: "E_NOT_FOUND", message: "element metadata not found" };
|
|
1753
|
+
}
|
|
1754
|
+
const attributes = {};
|
|
1755
|
+
for (const attr of Array.from(target.attributes)) {
|
|
1756
|
+
attributes[attr.name] = attr.value;
|
|
1757
|
+
}
|
|
1758
|
+
return {
|
|
1759
|
+
element,
|
|
1760
|
+
value: isEditable(target) ? target.value : target.isContentEditable ? target.textContent ?? "" : void 0,
|
|
1761
|
+
checked: isInputElement(target) ? target.checked : void 0,
|
|
1762
|
+
attributes
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
case "keyboard.press": {
|
|
1766
|
+
const key = String(params.key ?? "").trim();
|
|
1767
|
+
if (!key) {
|
|
1768
|
+
throw { code: "E_INVALID_PARAMS", message: "key is required" };
|
|
1769
|
+
}
|
|
1770
|
+
const target = document.activeElement ?? document.body;
|
|
1771
|
+
target.dispatchEvent(keyEvent("keydown", key));
|
|
1772
|
+
target.dispatchEvent(keyEvent("keyup", key));
|
|
1773
|
+
return { ok: true };
|
|
1774
|
+
}
|
|
1775
|
+
case "keyboard.type": {
|
|
1776
|
+
const text = String(params.text ?? "");
|
|
1777
|
+
const delayMs = typeof params.delayMs === "number" ? Math.max(0, params.delayMs) : 0;
|
|
1778
|
+
const target = document.activeElement ?? null;
|
|
1779
|
+
if (!target || !(isEditable(target) || target.isContentEditable)) {
|
|
1780
|
+
throw { code: "E_NOT_FOUND", message: "No editable active element for keyboard.type" };
|
|
1781
|
+
}
|
|
1782
|
+
for (const char of text) {
|
|
1783
|
+
target.dispatchEvent(keyEvent("keydown", char));
|
|
1784
|
+
if (isEditable(target)) {
|
|
1785
|
+
setNativeValue(target, `${target.value}${char}`);
|
|
1786
|
+
dispatchInputEvents(target);
|
|
1787
|
+
} else {
|
|
1788
|
+
insertContentEditableText(target, char, false);
|
|
1789
|
+
}
|
|
1790
|
+
target.dispatchEvent(keyEvent("keyup", char));
|
|
1791
|
+
if (delayMs > 0) {
|
|
1792
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return { ok: true };
|
|
1796
|
+
}
|
|
1797
|
+
case "keyboard.hotkey": {
|
|
1798
|
+
const keys = Array.isArray(params.keys) ? params.keys : [];
|
|
1799
|
+
if (keys.length === 0) {
|
|
1800
|
+
throw { code: "E_INVALID_PARAMS", message: "keys is required" };
|
|
1801
|
+
}
|
|
1802
|
+
const parsed = parseHotkey(keys);
|
|
1803
|
+
const target = document.activeElement ?? document.body;
|
|
1804
|
+
target.dispatchEvent(
|
|
1805
|
+
keyEvent("keydown", parsed.key, {
|
|
1806
|
+
ctrlKey: parsed.ctrlKey,
|
|
1807
|
+
altKey: parsed.altKey,
|
|
1808
|
+
shiftKey: parsed.shiftKey,
|
|
1809
|
+
metaKey: parsed.metaKey
|
|
1810
|
+
})
|
|
1811
|
+
);
|
|
1812
|
+
target.dispatchEvent(
|
|
1813
|
+
keyEvent("keyup", parsed.key, {
|
|
1814
|
+
ctrlKey: parsed.ctrlKey,
|
|
1815
|
+
altKey: parsed.altKey,
|
|
1816
|
+
shiftKey: parsed.shiftKey,
|
|
1817
|
+
metaKey: parsed.metaKey
|
|
1818
|
+
})
|
|
1819
|
+
);
|
|
1820
|
+
return { ok: true };
|
|
1821
|
+
}
|
|
1822
|
+
case "mouse.move": {
|
|
1823
|
+
const x = Number(params.x);
|
|
1824
|
+
const y = Number(params.y);
|
|
1825
|
+
const target = document.elementFromPoint(x, y);
|
|
1826
|
+
if (!target) {
|
|
1827
|
+
throw { code: "E_NOT_FOUND", message: "mouse.move target not found" };
|
|
1828
|
+
}
|
|
1829
|
+
target.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, clientX: x, clientY: y }));
|
|
1830
|
+
return { ok: true };
|
|
1831
|
+
}
|
|
1832
|
+
case "mouse.click": {
|
|
1833
|
+
const x = Number(params.x);
|
|
1834
|
+
const y = Number(params.y);
|
|
1835
|
+
const button = params.button === "right" ? 2 : params.button === "middle" ? 1 : 0;
|
|
1836
|
+
const target = document.elementFromPoint(x, y);
|
|
1837
|
+
if (!target) {
|
|
1838
|
+
throw { code: "E_NOT_FOUND", message: "mouse.click target not found" };
|
|
1839
|
+
}
|
|
1840
|
+
target.dispatchEvent(
|
|
1841
|
+
new MouseEvent("mousedown", { bubbles: true, cancelable: true, clientX: x, clientY: y, button })
|
|
1842
|
+
);
|
|
1843
|
+
target.dispatchEvent(
|
|
1844
|
+
new MouseEvent("mouseup", { bubbles: true, cancelable: true, clientX: x, clientY: y, button })
|
|
1845
|
+
);
|
|
1846
|
+
target.dispatchEvent(
|
|
1847
|
+
new MouseEvent(button === 2 ? "contextmenu" : "click", {
|
|
1848
|
+
bubbles: true,
|
|
1849
|
+
cancelable: true,
|
|
1850
|
+
clientX: x,
|
|
1851
|
+
clientY: y,
|
|
1852
|
+
button
|
|
1853
|
+
})
|
|
1854
|
+
);
|
|
1855
|
+
return { ok: true };
|
|
1856
|
+
}
|
|
1857
|
+
case "mouse.wheel": {
|
|
1858
|
+
const dx = typeof params.dx === "number" ? params.dx : 0;
|
|
1859
|
+
const dy = typeof params.dy === "number" ? params.dy : 120;
|
|
1860
|
+
window.dispatchEvent(new WheelEvent("wheel", { bubbles: true, deltaX: dx, deltaY: dy }));
|
|
1861
|
+
window.scrollBy({ left: dx, top: dy, behavior: "auto" });
|
|
1862
|
+
return { ok: true };
|
|
1863
|
+
}
|
|
1864
|
+
case "file.upload": {
|
|
1865
|
+
const target = resolveLocator(params.locator);
|
|
1866
|
+
if (!target || !isInputElement(target) || target.type !== "file") {
|
|
1867
|
+
throw notFoundForLocator(params.locator, "file.upload target must be <input type=file>");
|
|
1868
|
+
}
|
|
1869
|
+
if (params.requiresConfirm === true) {
|
|
1870
|
+
const targetName = inferName(target) || target.getAttribute("name") || target.id || "<input type=file>";
|
|
1871
|
+
const approved = await askConfirm(`Action: file.upload on "${targetName}"`);
|
|
1872
|
+
if (!approved) {
|
|
1873
|
+
throw { code: "E_NEED_USER_CONFIRM", message: "User rejected high-risk action" };
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
const files = Array.isArray(params.files) ? params.files : [];
|
|
1877
|
+
if (files.length === 0) {
|
|
1878
|
+
throw { code: "E_INVALID_PARAMS", message: "files is required" };
|
|
1879
|
+
}
|
|
1880
|
+
const transfer = new DataTransfer();
|
|
1881
|
+
for (const file of files) {
|
|
1882
|
+
const name = typeof file.name === "string" ? file.name : "upload.bin";
|
|
1883
|
+
const contentBase64 = typeof file.contentBase64 === "string" ? file.contentBase64 : "";
|
|
1884
|
+
const mimeType = typeof file.mimeType === "string" ? file.mimeType : "application/octet-stream";
|
|
1885
|
+
const bytes = Uint8Array.from(atob(contentBase64), (char) => char.charCodeAt(0));
|
|
1886
|
+
transfer.items.add(new File([bytes], name, { type: mimeType }));
|
|
1887
|
+
}
|
|
1888
|
+
target.files = transfer.files;
|
|
1889
|
+
dispatchInputEvents(target);
|
|
1890
|
+
return { ok: true, fileCount: transfer.files.length };
|
|
1891
|
+
}
|
|
1892
|
+
case "context.enterFrame": {
|
|
1893
|
+
if (params.reset === true) {
|
|
1894
|
+
contextState.framePath = [];
|
|
1895
|
+
}
|
|
1896
|
+
const framePath = Array.isArray(params.framePath) ? params.framePath.map(String) : [];
|
|
1897
|
+
if (framePath.length === 0 && params.locator && typeof params.locator.css === "string") {
|
|
1898
|
+
framePath.push(params.locator.css);
|
|
1899
|
+
}
|
|
1900
|
+
if (framePath.length === 0) {
|
|
1901
|
+
throw { code: "E_INVALID_PARAMS", message: "framePath or locator.css is required" };
|
|
1902
|
+
}
|
|
1903
|
+
const candidate = [...contextState.framePath, ...framePath];
|
|
1904
|
+
const check = resolveFrameDocument(candidate);
|
|
1905
|
+
if (!check.ok) {
|
|
1906
|
+
throw check.error;
|
|
1907
|
+
}
|
|
1908
|
+
contextState.framePath = candidate;
|
|
1909
|
+
return { ok: true, frameDepth: contextState.framePath.length, framePath: [...contextState.framePath] };
|
|
1910
|
+
}
|
|
1911
|
+
case "context.exitFrame": {
|
|
1912
|
+
if (params.reset === true) {
|
|
1913
|
+
contextState.framePath = [];
|
|
1914
|
+
} else {
|
|
1915
|
+
const levels = typeof params.levels === "number" ? Math.max(1, Math.floor(params.levels)) : 1;
|
|
1916
|
+
contextState.framePath = contextState.framePath.slice(0, Math.max(0, contextState.framePath.length - levels));
|
|
1917
|
+
}
|
|
1918
|
+
return { ok: true, frameDepth: contextState.framePath.length, framePath: [...contextState.framePath] };
|
|
1919
|
+
}
|
|
1920
|
+
case "context.enterShadow": {
|
|
1921
|
+
if (params.reset === true) {
|
|
1922
|
+
contextState.shadowPath = [];
|
|
1923
|
+
}
|
|
1924
|
+
const hostSelectors = Array.isArray(params.hostSelectors) ? params.hostSelectors.map(String) : [];
|
|
1925
|
+
if (hostSelectors.length === 0 && params.locator && typeof params.locator.css === "string") {
|
|
1926
|
+
hostSelectors.push(params.locator.css);
|
|
1927
|
+
}
|
|
1928
|
+
if (hostSelectors.length === 0) {
|
|
1929
|
+
throw { code: "E_INVALID_PARAMS", message: "hostSelectors or locator.css is required" };
|
|
1930
|
+
}
|
|
1931
|
+
const rootResult = resolveRootForLocator();
|
|
1932
|
+
if (!rootResult.ok) {
|
|
1933
|
+
throw rootResult.error;
|
|
1934
|
+
}
|
|
1935
|
+
const candidate = [...contextState.shadowPath, ...hostSelectors];
|
|
1936
|
+
const check = resolveShadowRoot(rootResult.root, candidate);
|
|
1937
|
+
if (!check.ok) {
|
|
1938
|
+
throw check.error;
|
|
1939
|
+
}
|
|
1940
|
+
contextState.shadowPath = candidate;
|
|
1941
|
+
return { ok: true, shadowDepth: contextState.shadowPath.length, shadowPath: [...contextState.shadowPath] };
|
|
1942
|
+
}
|
|
1943
|
+
case "context.exitShadow": {
|
|
1944
|
+
if (params.reset === true) {
|
|
1945
|
+
contextState.shadowPath = [];
|
|
1946
|
+
} else {
|
|
1947
|
+
const levels = typeof params.levels === "number" ? Math.max(1, Math.floor(params.levels)) : 1;
|
|
1948
|
+
contextState.shadowPath = contextState.shadowPath.slice(0, Math.max(0, contextState.shadowPath.length - levels));
|
|
1949
|
+
}
|
|
1950
|
+
return { ok: true, shadowDepth: contextState.shadowPath.length, shadowPath: [...contextState.shadowPath] };
|
|
1951
|
+
}
|
|
1952
|
+
case "context.reset":
|
|
1953
|
+
contextState.framePath = [];
|
|
1954
|
+
contextState.shadowPath = [];
|
|
1955
|
+
return { ok: true, frameDepth: 0, shadowDepth: 0 };
|
|
1956
|
+
case "network.list":
|
|
1957
|
+
return { entries: filterNetworkEntries(params) };
|
|
1958
|
+
case "network.get": {
|
|
1959
|
+
const id = String(params.id ?? "");
|
|
1960
|
+
const found = networkSnapshotEntries().find((entry) => entry.id === id);
|
|
1961
|
+
if (!found) {
|
|
1962
|
+
throw { code: "E_NOT_FOUND", message: `network entry not found: ${id}` };
|
|
1963
|
+
}
|
|
1964
|
+
return { entry: found };
|
|
1965
|
+
}
|
|
1966
|
+
case "network.waitFor":
|
|
1967
|
+
return { entry: await waitForNetwork(params) };
|
|
1968
|
+
case "network.clear":
|
|
1969
|
+
networkEntries.length = 0;
|
|
1970
|
+
performanceBaselineMs = performance.now();
|
|
1971
|
+
return { ok: true };
|
|
1972
|
+
case "debug.dumpState": {
|
|
1973
|
+
const consoleLimit = typeof params.consoleLimit === "number" ? Math.max(1, Math.floor(params.consoleLimit)) : 80;
|
|
1974
|
+
const networkLimit = typeof params.networkLimit === "number" ? Math.max(1, Math.floor(params.networkLimit)) : 80;
|
|
1975
|
+
const includeAccessibility = params.includeAccessibility === true;
|
|
1976
|
+
return {
|
|
1977
|
+
url: window.location.href,
|
|
1978
|
+
title: document.title,
|
|
1979
|
+
context: {
|
|
1980
|
+
framePath: [...contextState.framePath],
|
|
1981
|
+
shadowPath: [...contextState.shadowPath]
|
|
1982
|
+
},
|
|
1983
|
+
dom: domSummary(),
|
|
1984
|
+
text: pageTextChunks(12, 260),
|
|
1985
|
+
console: consoleEntries.slice(-consoleLimit),
|
|
1986
|
+
network: filterNetworkEntries({ limit: networkLimit }),
|
|
1987
|
+
accessibility: includeAccessibility ? pageAccessibility(200) : void 0
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
default:
|
|
1991
|
+
throw { code: "E_NOT_FOUND", message: `Unsupported content RPC method: ${method}` };
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1995
|
+
if (typeof message !== "object" || message === null || !("type" in message)) {
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
const typed = message;
|
|
1999
|
+
if (typed.type === "bak.collectElements") {
|
|
2000
|
+
const request = message;
|
|
2001
|
+
sendResponse({ elements: collectElements({ debugRichText: Boolean(request.debugRichText) }) });
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
2004
|
+
if (typed.type === "bak.getConsole") {
|
|
2005
|
+
const limit = Number(message.limit ?? 50);
|
|
2006
|
+
sendResponse({ entries: consoleEntries.slice(-limit) });
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
if (typed.type === "bak.performAction") {
|
|
2010
|
+
void handleAction(message).then(sendResponse);
|
|
2011
|
+
return true;
|
|
2012
|
+
}
|
|
2013
|
+
if (typed.type === "bak.waitFor") {
|
|
2014
|
+
void waitFor(message).then(sendResponse);
|
|
2015
|
+
return true;
|
|
2016
|
+
}
|
|
2017
|
+
if (typed.type === "bak.selectCandidate") {
|
|
2018
|
+
void pickCandidate(message.candidates).then((selectedEid) => {
|
|
2019
|
+
if (!selectedEid) {
|
|
2020
|
+
sendResponse({ ok: false, error: { code: "E_NEED_USER_CONFIRM", message: "No candidate selected" } });
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
sendResponse({ ok: true, selectedEid });
|
|
2024
|
+
});
|
|
2025
|
+
return true;
|
|
2026
|
+
}
|
|
2027
|
+
if (typed.type === "bak.rpc") {
|
|
2028
|
+
const request = message;
|
|
2029
|
+
void dispatchRpc(request.method, request.params ?? {}).then((result) => {
|
|
2030
|
+
const payload = { ok: true, result };
|
|
2031
|
+
sendResponse(payload);
|
|
2032
|
+
}).catch((error) => {
|
|
2033
|
+
const normalized = typeof error === "object" && error !== null && "code" in error ? error : { code: "E_INTERNAL", message: error instanceof Error ? error.message : String(error) };
|
|
2034
|
+
const payload = { ok: false, error: normalized };
|
|
2035
|
+
sendResponse(payload);
|
|
2036
|
+
});
|
|
2037
|
+
return true;
|
|
2038
|
+
}
|
|
2039
|
+
return false;
|
|
2040
|
+
});
|
|
2041
|
+
ensureOverlayRoot();
|
|
2042
|
+
})();
|