h17-webpilot 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/LICENSE +191 -0
- package/README.md +177 -0
- package/SKILLS.md +299 -0
- package/bin/cli.js +857 -0
- package/client/cursor.js +32 -0
- package/client/element.js +119 -0
- package/client/index.js +13 -0
- package/client/keyboard.js +27 -0
- package/client/page.js +271 -0
- package/extension/content-script.js +1852 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +26 -0
- package/extension/service-worker.js +711 -0
- package/human-browser.config.example.js +82 -0
- package/index.js +264 -0
- package/lib/launcher.js +102 -0
- package/lib/server.js +348 -0
- package/package.json +62 -0
- package/protocol/PROTOCOL.md +451 -0
|
@@ -0,0 +1,1852 @@
|
|
|
1
|
+
// Element Handle Registry
|
|
2
|
+
|
|
3
|
+
const handles = new Map(); // handleId -> { ref: WeakRef<Element>, lastAccessed }
|
|
4
|
+
let handleCounter = 0;
|
|
5
|
+
const frameworkRuntime = {
|
|
6
|
+
handles: {
|
|
7
|
+
ttlMs: 15 * 60 * 1000,
|
|
8
|
+
cleanupIntervalMs: 60 * 1000,
|
|
9
|
+
},
|
|
10
|
+
debug: {
|
|
11
|
+
cursor: true,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
let handleCleanupTimer = null;
|
|
15
|
+
let lastFrameworkConfigJson = "";
|
|
16
|
+
|
|
17
|
+
function storeHandle(el) {
|
|
18
|
+
const id = `el_${++handleCounter}`;
|
|
19
|
+
handles.set(id, { ref: new WeakRef(el), lastAccessed: Date.now() });
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getHandle(id) {
|
|
24
|
+
const entry = handles.get(id);
|
|
25
|
+
if (!entry) throw new Error(`Handle ${id} not found`);
|
|
26
|
+
const el = entry.ref.deref();
|
|
27
|
+
if (!el) {
|
|
28
|
+
handles.delete(id);
|
|
29
|
+
throw new Error(`Handle ${id} was garbage collected`);
|
|
30
|
+
}
|
|
31
|
+
entry.lastAccessed = Date.now();
|
|
32
|
+
return el;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveElement(params) {
|
|
36
|
+
if (params.handleId) return getHandle(params.handleId);
|
|
37
|
+
if (params.selector) {
|
|
38
|
+
const el = document.querySelector(params.selector);
|
|
39
|
+
if (!el) throw new Error(`Element not found: ${params.selector}`);
|
|
40
|
+
return el;
|
|
41
|
+
}
|
|
42
|
+
throw new Error("No handleId or selector provided");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cleanupStaleHandles() {
|
|
46
|
+
const cutoff = Date.now() - frameworkRuntime.handles.ttlMs;
|
|
47
|
+
for (const [id, entry] of handles) {
|
|
48
|
+
if (!entry.ref.deref() || entry.lastAccessed < cutoff) {
|
|
49
|
+
handles.delete(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function restartHandleCleanupTimer() {
|
|
55
|
+
if (handleCleanupTimer) clearInterval(handleCleanupTimer);
|
|
56
|
+
handleCleanupTimer = setInterval(
|
|
57
|
+
cleanupStaleHandles,
|
|
58
|
+
frameworkRuntime.handles.cleanupIntervalMs,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Cursor State & Bezier Math
|
|
63
|
+
|
|
64
|
+
let cursorX = 0;
|
|
65
|
+
let cursorY = 0;
|
|
66
|
+
|
|
67
|
+
// Restore cursor position from service worker (survives page reloads)
|
|
68
|
+
chrome.runtime.sendMessage(
|
|
69
|
+
{ action: "cursor.getPosition", params: {} },
|
|
70
|
+
(response) => {
|
|
71
|
+
if (response && response.result) {
|
|
72
|
+
cursorX = response.result.x || 0;
|
|
73
|
+
cursorY = response.result.y || 0;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Save cursor position to service worker after moves
|
|
79
|
+
function saveCursorPosition() {
|
|
80
|
+
chrome.runtime.sendMessage(
|
|
81
|
+
{ action: "cursor.reportPosition", params: { x: cursorX, y: cursorY } },
|
|
82
|
+
() => {},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Debug mode — defaults are configurable from framework config.
|
|
87
|
+
let debugMode = frameworkRuntime.debug.cursor;
|
|
88
|
+
|
|
89
|
+
function applyFrameworkConfig(rawConfig) {
|
|
90
|
+
if (!rawConfig || typeof rawConfig !== "object") return;
|
|
91
|
+
|
|
92
|
+
const nextJson = JSON.stringify(rawConfig);
|
|
93
|
+
if (nextJson === lastFrameworkConfigJson) return;
|
|
94
|
+
lastFrameworkConfigJson = nextJson;
|
|
95
|
+
|
|
96
|
+
if (rawConfig.handles && typeof rawConfig.handles === "object") {
|
|
97
|
+
const ttlMs = Number(rawConfig.handles.ttlMs);
|
|
98
|
+
const cleanupIntervalMs = Number(rawConfig.handles.cleanupIntervalMs);
|
|
99
|
+
|
|
100
|
+
if (Number.isFinite(ttlMs) && ttlMs >= 1000) {
|
|
101
|
+
frameworkRuntime.handles.ttlMs = ttlMs;
|
|
102
|
+
}
|
|
103
|
+
if (Number.isFinite(cleanupIntervalMs) && cleanupIntervalMs >= 1000) {
|
|
104
|
+
frameworkRuntime.handles.cleanupIntervalMs = cleanupIntervalMs;
|
|
105
|
+
}
|
|
106
|
+
restartHandleCleanupTimer();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
rawConfig.debug &&
|
|
111
|
+
typeof rawConfig.debug === "object" &&
|
|
112
|
+
typeof rawConfig.debug.cursor === "boolean"
|
|
113
|
+
) {
|
|
114
|
+
debugMode = rawConfig.debug.cursor;
|
|
115
|
+
if (!debugMode) clearTrail();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
restartHandleCleanupTimer();
|
|
120
|
+
|
|
121
|
+
// Cubic bezier point at t (0-1)
|
|
122
|
+
function bezierPoint(t, p0, p1, p2, p3) {
|
|
123
|
+
const u = 1 - t;
|
|
124
|
+
return (
|
|
125
|
+
u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Ease-in-out curve: slow start, fast middle, slow end (like a real hand)
|
|
130
|
+
function easeInOut(t) {
|
|
131
|
+
if (t < 0.5) return 2 * t * t;
|
|
132
|
+
return -1 + (4 - 2 * t) * t;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Generate bezier path from (x0,y0) to (x1,y1) with human-like control points
|
|
136
|
+
function generateBezierPath(x0, y0, x1, y1, steps) {
|
|
137
|
+
const dist = Math.hypot(x1 - x0, y1 - y0);
|
|
138
|
+
if (dist < 2) return [{ x: x1, y: y1 }];
|
|
139
|
+
|
|
140
|
+
// Control points offset perpendicular to the line
|
|
141
|
+
// More spread = more arc curvature
|
|
142
|
+
const spread = Math.min(dist * 0.35, 120);
|
|
143
|
+
const angle = Math.atan2(y1 - y0, x1 - x0);
|
|
144
|
+
const perpAngle = angle + Math.PI / 2;
|
|
145
|
+
|
|
146
|
+
// Asymmetric control points — real hands don't make symmetric arcs
|
|
147
|
+
const bias1 = (Math.random() - 0.5) * 2; // -1 to 1
|
|
148
|
+
const bias2 = (Math.random() - 0.5) * 2;
|
|
149
|
+
const cp1x =
|
|
150
|
+
x0 +
|
|
151
|
+
(x1 - x0) * (0.2 + Math.random() * 0.15) +
|
|
152
|
+
Math.cos(perpAngle) * bias1 * spread;
|
|
153
|
+
const cp1y =
|
|
154
|
+
y0 +
|
|
155
|
+
(y1 - y0) * (0.2 + Math.random() * 0.15) +
|
|
156
|
+
Math.sin(perpAngle) * bias1 * spread;
|
|
157
|
+
const cp2x =
|
|
158
|
+
x0 +
|
|
159
|
+
(x1 - x0) * (0.65 + Math.random() * 0.15) +
|
|
160
|
+
Math.cos(perpAngle) * bias2 * spread * 0.6;
|
|
161
|
+
const cp2y =
|
|
162
|
+
y0 +
|
|
163
|
+
(y1 - y0) * (0.65 + Math.random() * 0.15) +
|
|
164
|
+
Math.sin(perpAngle) * bias2 * spread * 0.6;
|
|
165
|
+
|
|
166
|
+
const numSteps = steps || Math.max(15, Math.min(Math.floor(dist / 4), 100));
|
|
167
|
+
const points = [];
|
|
168
|
+
|
|
169
|
+
for (let i = 1; i <= numSteps; i++) {
|
|
170
|
+
// Apply ease-in-out to t — cursor accelerates then decelerates
|
|
171
|
+
const linearT = i / numSteps;
|
|
172
|
+
const t = easeInOut(linearT);
|
|
173
|
+
|
|
174
|
+
let px = bezierPoint(t, x0, cp1x, cp2x, x1);
|
|
175
|
+
let py = bezierPoint(t, y0, cp1y, cp2y, y1);
|
|
176
|
+
|
|
177
|
+
// Micro-jitter (hand tremor) — strongest in the middle of the movement
|
|
178
|
+
// Fades near start and end where hand is steadier
|
|
179
|
+
const jitterStrength =
|
|
180
|
+
Math.sin(linearT * Math.PI) * Math.min(dist * 0.003, 1.5);
|
|
181
|
+
px += (Math.random() - 0.5) * jitterStrength * 2;
|
|
182
|
+
py += (Math.random() - 0.5) * jitterStrength * 2;
|
|
183
|
+
|
|
184
|
+
points.push({ x: px, y: py });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Ensure final point is exactly on target
|
|
188
|
+
points[points.length - 1] = { x: x1, y: y1 };
|
|
189
|
+
|
|
190
|
+
return points;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Visual Cursor Dot + Debug Trail
|
|
194
|
+
|
|
195
|
+
let cursorDot = null;
|
|
196
|
+
let cursorFadeTimer = null;
|
|
197
|
+
let trailContainer = null;
|
|
198
|
+
|
|
199
|
+
function ensureCursorDot() {
|
|
200
|
+
if (cursorDot && document.body?.contains(cursorDot)) return cursorDot;
|
|
201
|
+
cursorDot = document.createElement("div");
|
|
202
|
+
cursorDot.id = "__bridge_cursor";
|
|
203
|
+
cursorDot.style.cssText = [
|
|
204
|
+
"position:fixed",
|
|
205
|
+
"z-index:2147483647",
|
|
206
|
+
"width:12px",
|
|
207
|
+
"height:12px",
|
|
208
|
+
"border-radius:50%",
|
|
209
|
+
"background:rgba(66,133,244,0.8)",
|
|
210
|
+
"box-shadow:0 0 8px rgba(66,133,244,0.5)",
|
|
211
|
+
"pointer-events:none",
|
|
212
|
+
"transform:translate(-50%,-50%)",
|
|
213
|
+
"transition:opacity 0.5s",
|
|
214
|
+
"opacity:0",
|
|
215
|
+
].join(";");
|
|
216
|
+
(document.body || document.documentElement).appendChild(cursorDot);
|
|
217
|
+
return cursorDot;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ensureTrailContainer() {
|
|
221
|
+
if (trailContainer && document.body?.contains(trailContainer))
|
|
222
|
+
return trailContainer;
|
|
223
|
+
trailContainer = document.createElement("div");
|
|
224
|
+
trailContainer.id = "__bridge_trail";
|
|
225
|
+
trailContainer.style.cssText =
|
|
226
|
+
"position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;";
|
|
227
|
+
(document.body || document.documentElement).appendChild(trailContainer);
|
|
228
|
+
return trailContainer;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function clearTrail() {
|
|
232
|
+
if (trailContainer) trailContainer.innerHTML = "";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function drawTrailDot(x, y, progress) {
|
|
236
|
+
const container = ensureTrailContainer();
|
|
237
|
+
const dot = document.createElement("div");
|
|
238
|
+
// Color shifts from green (start) → yellow (mid) → red (end)
|
|
239
|
+
const r = Math.floor(progress * 255);
|
|
240
|
+
const g = Math.floor((1 - progress) * 200);
|
|
241
|
+
const size = 3 + (1 - Math.abs(progress - 0.5) * 2) * 3; // larger in the middle
|
|
242
|
+
dot.style.cssText = [
|
|
243
|
+
"position:fixed",
|
|
244
|
+
"border-radius:50%",
|
|
245
|
+
"pointer-events:none",
|
|
246
|
+
`width:${size}px`,
|
|
247
|
+
`height:${size}px`,
|
|
248
|
+
`left:${x - size / 2}px`,
|
|
249
|
+
`top:${y - size / 2}px`,
|
|
250
|
+
`background:rgba(${r},${g},80,0.7)`,
|
|
251
|
+
"transition:opacity 2s",
|
|
252
|
+
].join(";");
|
|
253
|
+
container.appendChild(dot);
|
|
254
|
+
// Fade trail dots after 3s
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
dot.style.opacity = "0";
|
|
257
|
+
}, 3000);
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
dot.remove();
|
|
260
|
+
}, 5000);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function moveCursorDot(x, y) {
|
|
264
|
+
const dot = ensureCursorDot();
|
|
265
|
+
dot.style.left = x + "px";
|
|
266
|
+
dot.style.top = y + "px";
|
|
267
|
+
dot.style.opacity = "1";
|
|
268
|
+
clearTimeout(cursorFadeTimer);
|
|
269
|
+
cursorFadeTimer = setTimeout(() => {
|
|
270
|
+
dot.style.opacity = "0";
|
|
271
|
+
}, 2000);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Dispatch mouse move along path with variable timing (ease-in-out speed)
|
|
275
|
+
function dispatchMousePath(points) {
|
|
276
|
+
clearTrail();
|
|
277
|
+
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
let i = 0;
|
|
280
|
+
const total = points.length;
|
|
281
|
+
|
|
282
|
+
function step() {
|
|
283
|
+
if (i >= total) {
|
|
284
|
+
resolve();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const p = points[i];
|
|
289
|
+
const progress = i / total;
|
|
290
|
+
cursorX = p.x;
|
|
291
|
+
cursorY = p.y;
|
|
292
|
+
moveCursorDot(p.x, p.y);
|
|
293
|
+
drawTrailDot(p.x, p.y, progress);
|
|
294
|
+
|
|
295
|
+
const target = document.elementFromPoint(p.x, p.y) || document.body;
|
|
296
|
+
target.dispatchEvent(
|
|
297
|
+
new MouseEvent("mousemove", {
|
|
298
|
+
clientX: p.x,
|
|
299
|
+
clientY: p.y,
|
|
300
|
+
bubbles: true,
|
|
301
|
+
cancelable: true,
|
|
302
|
+
view: window,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
i++;
|
|
307
|
+
|
|
308
|
+
// Variable frame timing — occasionally skip a frame for micro-stutter
|
|
309
|
+
// Real hands don't move at perfectly uniform frame rate
|
|
310
|
+
if (Math.random() < 0.08 && i < total - 2) {
|
|
311
|
+
// Double-frame pause (~32ms instead of ~16ms) — simulates hand hesitation
|
|
312
|
+
requestAnimationFrame(() => requestAnimationFrame(step));
|
|
313
|
+
} else {
|
|
314
|
+
requestAnimationFrame(step);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
requestAnimationFrame(step);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Overshoot: move past target then correct back
|
|
322
|
+
function generateOvershootPath(x0, y0, x1, y1) {
|
|
323
|
+
const dist = Math.hypot(x1 - x0, y1 - y0);
|
|
324
|
+
if (dist < 100) return generateBezierPath(x0, y0, x1, y1);
|
|
325
|
+
|
|
326
|
+
// Overshoot proportional to distance, with randomization
|
|
327
|
+
const overshoot = Math.min(20, dist * 0.06) * (0.4 + Math.random() * 0.6);
|
|
328
|
+
const angle = Math.atan2(y1 - y0, x1 - x0);
|
|
329
|
+
// Slight perpendicular offset on the overshoot too
|
|
330
|
+
const perpOffset = (Math.random() - 0.5) * overshoot * 0.5;
|
|
331
|
+
const perpAngle = angle + Math.PI / 2;
|
|
332
|
+
const overX =
|
|
333
|
+
x1 + Math.cos(angle) * overshoot + Math.cos(perpAngle) * perpOffset;
|
|
334
|
+
const overY =
|
|
335
|
+
y1 + Math.sin(angle) * overshoot + Math.sin(perpAngle) * perpOffset;
|
|
336
|
+
|
|
337
|
+
const pathToOver = generateBezierPath(x0, y0, overX, overY);
|
|
338
|
+
const pathBack = generateBezierPath(overX, overY, x1, y1, 10);
|
|
339
|
+
|
|
340
|
+
return [...pathToOver, ...pathBack];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Action: dom.markElement / dom.unmarkElement (for service-worker eval)
|
|
344
|
+
|
|
345
|
+
function actionMarkElement(params) {
|
|
346
|
+
const el = getHandle(params.handleId);
|
|
347
|
+
el.setAttribute("data-bridge-eval", params.markerId);
|
|
348
|
+
return { marked: true };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function actionUnmarkElement(params) {
|
|
352
|
+
const el = document.querySelector(`[data-bridge-eval="${params.markerId}"]`);
|
|
353
|
+
if (el) el.removeAttribute("data-bridge-eval");
|
|
354
|
+
return { unmarked: true };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Register an element marked by evaluateHandle in MAIN world
|
|
358
|
+
function actionRegisterMarkedElement(params) {
|
|
359
|
+
const el = document.querySelector(`[data-bridge-handle="${params.marker}"]`);
|
|
360
|
+
if (!el) return null;
|
|
361
|
+
el.removeAttribute("data-bridge-handle");
|
|
362
|
+
return storeHandle(el);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// NOTE: dom.evaluate and dom.elementEvaluate are handled by the service worker
|
|
366
|
+
// using chrome.scripting.executeScript (MAIN world) to bypass MV3 CSP.
|
|
367
|
+
// They never reach the content script.
|
|
368
|
+
|
|
369
|
+
// Action: dom.querySelector / querySelectorAll / querySelectorWithin
|
|
370
|
+
|
|
371
|
+
function actionQuerySelector(params) {
|
|
372
|
+
const el = document.querySelector(params.selector);
|
|
373
|
+
return el ? storeHandle(el) : null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function actionQuerySelectorAll(params) {
|
|
377
|
+
const els = document.querySelectorAll(params.selector);
|
|
378
|
+
return Array.from(els).map(storeHandle);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function actionQuerySelectorWithin(params) {
|
|
382
|
+
const parent = getHandle(params.parentHandleId);
|
|
383
|
+
const el = parent.querySelector(params.selector);
|
|
384
|
+
return el ? storeHandle(el) : null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function actionQuerySelectorAllWithin(params) {
|
|
388
|
+
const parent = getHandle(params.parentHandleId);
|
|
389
|
+
return Array.from(parent.querySelectorAll(params.selector)).map(storeHandle);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Action: dom.queryAllInfo — single-call querySelectorAll + handles + element info
|
|
393
|
+
function actionQueryAllInfo(params) {
|
|
394
|
+
const els = document.querySelectorAll(params.selector);
|
|
395
|
+
return Array.from(els).map(el => {
|
|
396
|
+
const handleId = storeHandle(el);
|
|
397
|
+
return {
|
|
398
|
+
handleId,
|
|
399
|
+
tag: el.tagName.toLowerCase(),
|
|
400
|
+
id: el.id || null,
|
|
401
|
+
cls: [...el.classList].slice(0, 3).join(' ') || null,
|
|
402
|
+
text: (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60) || null,
|
|
403
|
+
label: el.getAttribute('aria-label') || el.getAttribute('name') || el.getAttribute('placeholder') || (el.labels && el.labels[0] ? el.labels[0].textContent.trim() : null),
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Action: dom.boundingBox
|
|
409
|
+
|
|
410
|
+
function actionBoundingBox(params) {
|
|
411
|
+
const el = resolveElement(params);
|
|
412
|
+
const rect = el.getBoundingClientRect();
|
|
413
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
414
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Action: dom.mouseMoveTo
|
|
418
|
+
|
|
419
|
+
async function actionMouseMoveTo(params) {
|
|
420
|
+
const el = resolveElement(params);
|
|
421
|
+
const rect = el.getBoundingClientRect();
|
|
422
|
+
// Target: random point within center 60% of element
|
|
423
|
+
const padX = rect.width * 0.2;
|
|
424
|
+
const padY = rect.height * 0.2;
|
|
425
|
+
const targetX = rect.x + padX + Math.random() * (rect.width - padX * 2);
|
|
426
|
+
const targetY = rect.y + padY + Math.random() * (rect.height - padY * 2);
|
|
427
|
+
|
|
428
|
+
let startX = cursorX;
|
|
429
|
+
let startY = cursorY;
|
|
430
|
+
|
|
431
|
+
// If cursor is already on or very near the target, drift away first so
|
|
432
|
+
// the movement path is always visible (avoids teleport-click appearance).
|
|
433
|
+
const dist = Math.hypot(targetX - startX, targetY - startY);
|
|
434
|
+
if (dist < 80) {
|
|
435
|
+
const driftAngle = Math.random() * Math.PI * 2;
|
|
436
|
+
const driftDist = 80 + Math.random() * 120;
|
|
437
|
+
const driftX = Math.max(0, Math.min(window.innerWidth, startX + Math.cos(driftAngle) * driftDist));
|
|
438
|
+
const driftY = Math.max(0, Math.min(window.innerHeight, startY + Math.sin(driftAngle) * driftDist));
|
|
439
|
+
await dispatchMousePath(generateBezierPath(startX, startY, driftX, driftY));
|
|
440
|
+
startX = cursorX;
|
|
441
|
+
startY = cursorY;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const dist2 = Math.hypot(targetX - startX, targetY - startY);
|
|
445
|
+
const points =
|
|
446
|
+
dist2 > 200
|
|
447
|
+
? generateOvershootPath(startX, startY, targetX, targetY)
|
|
448
|
+
: generateBezierPath(startX, startY, targetX, targetY);
|
|
449
|
+
|
|
450
|
+
await dispatchMousePath(points);
|
|
451
|
+
saveCursorPosition();
|
|
452
|
+
return { x: cursorX, y: cursorY };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Action: dom.click (with mousedown/mouseup/click dispatch)
|
|
456
|
+
|
|
457
|
+
function actionClick(params) {
|
|
458
|
+
const el = resolveElement(params);
|
|
459
|
+
const rect = el.getBoundingClientRect();
|
|
460
|
+
const x = cursorX || rect.x + rect.width / 2;
|
|
461
|
+
const y = cursorY || rect.y + rect.height / 2;
|
|
462
|
+
const clickCount = params.clickCount || 1;
|
|
463
|
+
|
|
464
|
+
for (let i = 1; i <= clickCount; i++) {
|
|
465
|
+
const opts = {
|
|
466
|
+
clientX: x,
|
|
467
|
+
clientY: y,
|
|
468
|
+
bubbles: true,
|
|
469
|
+
cancelable: true,
|
|
470
|
+
view: window,
|
|
471
|
+
button: 0,
|
|
472
|
+
detail: i,
|
|
473
|
+
};
|
|
474
|
+
// Dispatch on the element physically under the cursor.
|
|
475
|
+
// If nothing is at the cursor coordinates, abort — element is not truly visible.
|
|
476
|
+
const atPoint = document.elementFromPoint(x, y);
|
|
477
|
+
if (!atPoint) return;
|
|
478
|
+
atPoint.dispatchEvent(new MouseEvent("mousedown", opts));
|
|
479
|
+
if (i === 1) el.focus();
|
|
480
|
+
atPoint.dispatchEvent(new MouseEvent("mouseup", opts));
|
|
481
|
+
atPoint.dispatchEvent(new MouseEvent("click", opts));
|
|
482
|
+
|
|
483
|
+
// detail:2 = double-click (select word), detail:3 = triple-click (select all)
|
|
484
|
+
if (i === 2) atPoint.dispatchEvent(new MouseEvent("dblclick", opts));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Triple-click: select all text in input/textarea
|
|
488
|
+
if (
|
|
489
|
+
clickCount >= 3 &&
|
|
490
|
+
(el.tagName === "INPUT" || el.tagName === "TEXTAREA")
|
|
491
|
+
) {
|
|
492
|
+
el.setSelectionRange(0, el.value.length);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { clicked: true };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Action: dom.type
|
|
499
|
+
|
|
500
|
+
function actionType(params) {
|
|
501
|
+
const { text, handleId, selector } = params;
|
|
502
|
+
let target;
|
|
503
|
+
if (handleId) target = getHandle(handleId);
|
|
504
|
+
else if (selector) target = document.querySelector(selector);
|
|
505
|
+
else target = document.activeElement;
|
|
506
|
+
|
|
507
|
+
if (!target) throw new Error("No target for typing");
|
|
508
|
+
if (target !== document.activeElement) target.focus();
|
|
509
|
+
|
|
510
|
+
for (const char of text) {
|
|
511
|
+
const charCode = char.charCodeAt(0);
|
|
512
|
+
const shared = {
|
|
513
|
+
key: char,
|
|
514
|
+
code: `Key${char.toUpperCase()}`,
|
|
515
|
+
keyCode: charCode,
|
|
516
|
+
charCode,
|
|
517
|
+
which: charCode,
|
|
518
|
+
bubbles: true,
|
|
519
|
+
cancelable: true,
|
|
520
|
+
view: window,
|
|
521
|
+
};
|
|
522
|
+
target.dispatchEvent(new KeyboardEvent("keydown", shared));
|
|
523
|
+
target.dispatchEvent(new KeyboardEvent("keypress", shared));
|
|
524
|
+
|
|
525
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
526
|
+
const start = target.selectionStart || 0;
|
|
527
|
+
const end = target.selectionEnd || 0;
|
|
528
|
+
const newValue =
|
|
529
|
+
target.value.slice(0, start) + char + target.value.slice(end);
|
|
530
|
+
// Use native setter to trigger React's change detection
|
|
531
|
+
const proto =
|
|
532
|
+
target.tagName === "TEXTAREA"
|
|
533
|
+
? HTMLTextAreaElement.prototype
|
|
534
|
+
: HTMLInputElement.prototype;
|
|
535
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
536
|
+
if (nativeSetter) {
|
|
537
|
+
nativeSetter.call(target, newValue);
|
|
538
|
+
} else {
|
|
539
|
+
target.value = newValue;
|
|
540
|
+
}
|
|
541
|
+
target.selectionStart = target.selectionEnd = start + 1;
|
|
542
|
+
} else if (target.isContentEditable) {
|
|
543
|
+
document.execCommand("insertText", false, char);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
target.dispatchEvent(
|
|
547
|
+
new InputEvent("input", {
|
|
548
|
+
data: char,
|
|
549
|
+
inputType: "insertText",
|
|
550
|
+
bubbles: true,
|
|
551
|
+
cancelable: true,
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
target.dispatchEvent(new KeyboardEvent("keyup", shared));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return { typed: true };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Keyboard: key mapping, modifier tracking, functional keys
|
|
561
|
+
|
|
562
|
+
// Map Puppeteer key names → { key, code, keyCode }
|
|
563
|
+
const KEY_MAP = {
|
|
564
|
+
// Modifiers
|
|
565
|
+
Meta: { key: "Meta", code: "MetaLeft", keyCode: 91 },
|
|
566
|
+
Control: { key: "Control", code: "ControlLeft", keyCode: 17 },
|
|
567
|
+
Shift: { key: "Shift", code: "ShiftLeft", keyCode: 16 },
|
|
568
|
+
Alt: { key: "Alt", code: "AltLeft", keyCode: 18 },
|
|
569
|
+
// Action keys
|
|
570
|
+
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
571
|
+
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
572
|
+
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
573
|
+
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
574
|
+
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
575
|
+
Space: { key: " ", code: "Space", keyCode: 32 },
|
|
576
|
+
" ": { key: " ", code: "Space", keyCode: 32 },
|
|
577
|
+
// Arrow keys
|
|
578
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
579
|
+
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
580
|
+
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
581
|
+
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
582
|
+
// Navigation
|
|
583
|
+
Home: { key: "Home", code: "Home", keyCode: 36 },
|
|
584
|
+
End: { key: "End", code: "End", keyCode: 35 },
|
|
585
|
+
PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
586
|
+
PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 },
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Resolve Puppeteer key name (e.g. "KeyA", "Backspace", "Meta") to event properties
|
|
590
|
+
function resolveKey(rawKey) {
|
|
591
|
+
if (KEY_MAP[rawKey]) return KEY_MAP[rawKey];
|
|
592
|
+
|
|
593
|
+
// "KeyA" → key: "a", code: "KeyA"
|
|
594
|
+
const keyMatch = rawKey.match(/^Key([A-Z])$/);
|
|
595
|
+
if (keyMatch) {
|
|
596
|
+
const letter = keyMatch[1].toLowerCase();
|
|
597
|
+
return {
|
|
598
|
+
key: letter,
|
|
599
|
+
code: rawKey,
|
|
600
|
+
keyCode: letter.toUpperCase().charCodeAt(0),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// "Digit5" → key: "5", code: "Digit5"
|
|
605
|
+
const digitMatch = rawKey.match(/^Digit(\d)$/);
|
|
606
|
+
if (digitMatch) {
|
|
607
|
+
return {
|
|
608
|
+
key: digitMatch[1],
|
|
609
|
+
code: rawKey,
|
|
610
|
+
keyCode: digitMatch[1].charCodeAt(0),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Single character
|
|
615
|
+
if (rawKey.length === 1) {
|
|
616
|
+
const upper = rawKey.toUpperCase();
|
|
617
|
+
return { key: rawKey, code: `Key${upper}`, keyCode: upper.charCodeAt(0) };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Fallback
|
|
621
|
+
return { key: rawKey, code: rawKey, keyCode: 0 };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Track active modifiers for combo detection (Ctrl+A, etc.)
|
|
625
|
+
const activeModifiers = {
|
|
626
|
+
meta: false,
|
|
627
|
+
control: false,
|
|
628
|
+
shift: false,
|
|
629
|
+
alt: false,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
function isModifier(key) {
|
|
633
|
+
return (
|
|
634
|
+
key === "Meta" || key === "Control" || key === "Shift" || key === "Alt"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function buildEventProps(resolved) {
|
|
639
|
+
return {
|
|
640
|
+
key: resolved.key,
|
|
641
|
+
code: resolved.code,
|
|
642
|
+
keyCode: resolved.keyCode,
|
|
643
|
+
charCode: resolved.keyCode,
|
|
644
|
+
which: resolved.keyCode,
|
|
645
|
+
metaKey: activeModifiers.meta,
|
|
646
|
+
ctrlKey: activeModifiers.control,
|
|
647
|
+
shiftKey: activeModifiers.shift,
|
|
648
|
+
altKey: activeModifiers.alt,
|
|
649
|
+
bubbles: true,
|
|
650
|
+
cancelable: true,
|
|
651
|
+
view: window,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Handle functional side-effects of key presses (select-all, backspace, delete, enter)
|
|
656
|
+
function applyKeyEffect(target, resolved) {
|
|
657
|
+
const key = resolved.key;
|
|
658
|
+
const isInput = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
|
|
659
|
+
const isSelect = target.tagName === "SELECT";
|
|
660
|
+
const selectAll = activeModifiers.meta || activeModifiers.control;
|
|
661
|
+
|
|
662
|
+
if (selectAll && (key === "a" || key === "A")) {
|
|
663
|
+
// Ctrl+A / Cmd+A → select all
|
|
664
|
+
if (isInput) {
|
|
665
|
+
target.setSelectionRange(0, target.value.length);
|
|
666
|
+
} else if (target.isContentEditable) {
|
|
667
|
+
const range = document.createRange();
|
|
668
|
+
range.selectNodeContents(target);
|
|
669
|
+
const sel = window.getSelection();
|
|
670
|
+
sel.removeAllRanges();
|
|
671
|
+
sel.addRange(range);
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (isSelect && (key === "ArrowDown" || key === "ArrowUp")) {
|
|
677
|
+
const dir = key === "ArrowDown" ? 1 : -1;
|
|
678
|
+
const current = target.selectedIndex >= 0 ? target.selectedIndex : 0;
|
|
679
|
+
const next = Math.max(
|
|
680
|
+
0,
|
|
681
|
+
Math.min(target.options.length - 1, current + dir),
|
|
682
|
+
);
|
|
683
|
+
if (next !== target.selectedIndex) {
|
|
684
|
+
target.selectedIndex = next;
|
|
685
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
686
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (isSelect && key === "Enter") {
|
|
692
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (key === "Backspace" && isInput) {
|
|
697
|
+
const start = target.selectionStart ?? 0;
|
|
698
|
+
const end = target.selectionEnd ?? 0;
|
|
699
|
+
const val = target.value;
|
|
700
|
+
let newValue, newCursor;
|
|
701
|
+
if (start !== end) {
|
|
702
|
+
// Delete selection
|
|
703
|
+
newValue = val.slice(0, start) + val.slice(end);
|
|
704
|
+
newCursor = start;
|
|
705
|
+
} else if (start > 0) {
|
|
706
|
+
// Delete char before cursor
|
|
707
|
+
newValue = val.slice(0, start - 1) + val.slice(end);
|
|
708
|
+
newCursor = start - 1;
|
|
709
|
+
} else {
|
|
710
|
+
return; // nothing to delete
|
|
711
|
+
}
|
|
712
|
+
const proto =
|
|
713
|
+
target.tagName === "TEXTAREA"
|
|
714
|
+
? HTMLTextAreaElement.prototype
|
|
715
|
+
: HTMLInputElement.prototype;
|
|
716
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
717
|
+
if (nativeSetter) nativeSetter.call(target, newValue);
|
|
718
|
+
else target.value = newValue;
|
|
719
|
+
target.selectionStart = target.selectionEnd = newCursor;
|
|
720
|
+
target.dispatchEvent(
|
|
721
|
+
new InputEvent("input", {
|
|
722
|
+
inputType: "deleteContentBackward",
|
|
723
|
+
bubbles: true,
|
|
724
|
+
}),
|
|
725
|
+
);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (key === "Delete" && isInput) {
|
|
730
|
+
const start = target.selectionStart ?? 0;
|
|
731
|
+
const end = target.selectionEnd ?? 0;
|
|
732
|
+
const val = target.value;
|
|
733
|
+
let newValue;
|
|
734
|
+
if (start !== end) {
|
|
735
|
+
newValue = val.slice(0, start) + val.slice(end);
|
|
736
|
+
} else if (end < val.length) {
|
|
737
|
+
newValue = val.slice(0, start) + val.slice(end + 1);
|
|
738
|
+
} else {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const proto =
|
|
742
|
+
target.tagName === "TEXTAREA"
|
|
743
|
+
? HTMLTextAreaElement.prototype
|
|
744
|
+
: HTMLInputElement.prototype;
|
|
745
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
746
|
+
if (nativeSetter) nativeSetter.call(target, newValue);
|
|
747
|
+
else target.value = newValue;
|
|
748
|
+
target.selectionStart = target.selectionEnd = start;
|
|
749
|
+
target.dispatchEvent(
|
|
750
|
+
new InputEvent("input", {
|
|
751
|
+
inputType: "deleteContentForward",
|
|
752
|
+
bubbles: true,
|
|
753
|
+
}),
|
|
754
|
+
);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function actionKeyPress(params) {
|
|
760
|
+
const target = document.activeElement || document.body;
|
|
761
|
+
const resolved = resolveKey(params.key);
|
|
762
|
+
const props = buildEventProps(resolved);
|
|
763
|
+
target.dispatchEvent(new KeyboardEvent("keydown", props));
|
|
764
|
+
applyKeyEffect(target, resolved);
|
|
765
|
+
target.dispatchEvent(new KeyboardEvent("keyup", props));
|
|
766
|
+
return { pressed: true };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function actionKeyDown(params) {
|
|
770
|
+
const target = document.activeElement || document.body;
|
|
771
|
+
const resolved = resolveKey(params.key);
|
|
772
|
+
// Track modifier state
|
|
773
|
+
if (params.key === "Meta") activeModifiers.meta = true;
|
|
774
|
+
if (params.key === "Control") activeModifiers.control = true;
|
|
775
|
+
if (params.key === "Shift") activeModifiers.shift = true;
|
|
776
|
+
if (params.key === "Alt") activeModifiers.alt = true;
|
|
777
|
+
const props = buildEventProps(resolved);
|
|
778
|
+
target.dispatchEvent(new KeyboardEvent("keydown", props));
|
|
779
|
+
return { down: true };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function actionKeyUp(params) {
|
|
783
|
+
const target = document.activeElement || document.body;
|
|
784
|
+
const resolved = resolveKey(params.key);
|
|
785
|
+
const props = buildEventProps(resolved);
|
|
786
|
+
target.dispatchEvent(new KeyboardEvent("keyup", props));
|
|
787
|
+
// Clear modifier state
|
|
788
|
+
if (params.key === "Meta") activeModifiers.meta = false;
|
|
789
|
+
if (params.key === "Control") activeModifiers.control = false;
|
|
790
|
+
if (params.key === "Shift") activeModifiers.shift = false;
|
|
791
|
+
if (params.key === "Alt") activeModifiers.alt = false;
|
|
792
|
+
return { up: true };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Action: dom.scroll
|
|
796
|
+
|
|
797
|
+
function actionScroll(params) {
|
|
798
|
+
const {
|
|
799
|
+
selector,
|
|
800
|
+
amount = 400,
|
|
801
|
+
direction = "down",
|
|
802
|
+
behavior = "smooth",
|
|
803
|
+
} = params;
|
|
804
|
+
const top = direction === "down" ? amount : direction === "up" ? -amount : 0;
|
|
805
|
+
const left =
|
|
806
|
+
direction === "right" ? amount : direction === "left" ? -amount : 0;
|
|
807
|
+
|
|
808
|
+
// Support handleId, selector, or fallback to window
|
|
809
|
+
let el = null;
|
|
810
|
+
if (params.handleId) {
|
|
811
|
+
el = resolveElement(params);
|
|
812
|
+
} else if (selector) {
|
|
813
|
+
el = document.querySelector(selector);
|
|
814
|
+
}
|
|
815
|
+
const target = (el && el.scrollHeight > el.clientHeight + 10) ? el : window;
|
|
816
|
+
const before = target === window ? window.scrollY : target.scrollTop;
|
|
817
|
+
target.scrollBy({ top, left, behavior });
|
|
818
|
+
// Check actual scroll after a tick (smooth may not be instant)
|
|
819
|
+
const after = target === window ? window.scrollY : target.scrollTop;
|
|
820
|
+
return { scrolled: true, before, after, target: target === window ? "window" : "element" };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Action: dom.focus
|
|
824
|
+
|
|
825
|
+
function actionFocus(params) {
|
|
826
|
+
const el = resolveElement(params);
|
|
827
|
+
el.focus();
|
|
828
|
+
return { focused: true };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Action: dom.setValue
|
|
832
|
+
|
|
833
|
+
function actionSetValue(params) {
|
|
834
|
+
const el = resolveElement(params);
|
|
835
|
+
|
|
836
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
837
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
838
|
+
el instanceof HTMLInputElement
|
|
839
|
+
? window.HTMLInputElement.prototype
|
|
840
|
+
: window.HTMLTextAreaElement.prototype,
|
|
841
|
+
"value",
|
|
842
|
+
)?.set;
|
|
843
|
+
if (nativeSetter) nativeSetter.call(el, params.value);
|
|
844
|
+
else el.value = params.value;
|
|
845
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
846
|
+
// For select elements, assigning .value selects the matching option by value.
|
|
847
|
+
el.value = String(params.value);
|
|
848
|
+
if (el.value !== String(params.value)) {
|
|
849
|
+
const idx = Array.from(el.options).findIndex(
|
|
850
|
+
(opt) =>
|
|
851
|
+
opt.value === String(params.value) ||
|
|
852
|
+
opt.textContent?.trim() === String(params.value),
|
|
853
|
+
);
|
|
854
|
+
if (idx >= 0) el.selectedIndex = idx;
|
|
855
|
+
}
|
|
856
|
+
} else if (el.isContentEditable) {
|
|
857
|
+
el.textContent = String(params.value);
|
|
858
|
+
} else {
|
|
859
|
+
el.value = params.value;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
863
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
864
|
+
return { set: true };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Action: dom.getAttribute / dom.getProperty
|
|
868
|
+
|
|
869
|
+
function actionGetAttribute(params) {
|
|
870
|
+
const el = resolveElement(params);
|
|
871
|
+
return el.getAttribute(params.name);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function actionGetProperty(params) {
|
|
875
|
+
const el = resolveElement(params);
|
|
876
|
+
return el[params.name];
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Action: dom.waitForSelector (async)
|
|
880
|
+
|
|
881
|
+
function actionWaitForSelector(params, sendResponse) {
|
|
882
|
+
const { selector, timeout = 5000 } = params;
|
|
883
|
+
const start = Date.now();
|
|
884
|
+
|
|
885
|
+
// Check immediately
|
|
886
|
+
const existing = document.querySelector(selector);
|
|
887
|
+
if (existing) {
|
|
888
|
+
sendResponse({ result: storeHandle(existing) });
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Observe mutations
|
|
893
|
+
const observer = new MutationObserver(() => {
|
|
894
|
+
const el = document.querySelector(selector);
|
|
895
|
+
if (el) {
|
|
896
|
+
observer.disconnect();
|
|
897
|
+
clearTimeout(timer);
|
|
898
|
+
sendResponse({ result: storeHandle(el) });
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
observer.observe(document.documentElement, {
|
|
903
|
+
childList: true,
|
|
904
|
+
subtree: true,
|
|
905
|
+
attributes: true,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const timer = setTimeout(() => {
|
|
909
|
+
observer.disconnect();
|
|
910
|
+
sendResponse({ result: null });
|
|
911
|
+
}, timeout);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Avoid Check — custom element filtering for human.* commands
|
|
915
|
+
|
|
916
|
+
function checkAvoid(el, avoid) {
|
|
917
|
+
if (!avoid) return { avoided: false };
|
|
918
|
+
|
|
919
|
+
// Check CSS selectors (el.matches or el.closest)
|
|
920
|
+
for (const sel of avoid.selectors || []) {
|
|
921
|
+
try {
|
|
922
|
+
if (el.matches(sel) || el.closest(sel)) {
|
|
923
|
+
return { avoided: true, rule: `selector:${sel}` };
|
|
924
|
+
}
|
|
925
|
+
} catch {}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Check class names
|
|
929
|
+
for (const cls of avoid.classes || []) {
|
|
930
|
+
if (el.classList.contains(cls)) {
|
|
931
|
+
return { avoided: true, rule: `class:${cls}` };
|
|
932
|
+
}
|
|
933
|
+
// Also check ancestors
|
|
934
|
+
if (el.closest(`.${CSS.escape(cls)}`)) {
|
|
935
|
+
return { avoided: true, rule: `class:${cls}` };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Check IDs
|
|
940
|
+
for (const id of avoid.ids || []) {
|
|
941
|
+
if (el.id === id) {
|
|
942
|
+
return { avoided: true, rule: `id:${id}` };
|
|
943
|
+
}
|
|
944
|
+
if (el.closest(`#${CSS.escape(id)}`)) {
|
|
945
|
+
return { avoided: true, rule: `id:${id}` };
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Check attributes
|
|
950
|
+
for (const [attr, val] of Object.entries(avoid.attributes || {})) {
|
|
951
|
+
if (val === "*") {
|
|
952
|
+
if (el.hasAttribute(attr)) return { avoided: true, rule: `attr:${attr}` };
|
|
953
|
+
} else {
|
|
954
|
+
if (el.getAttribute(attr) === val)
|
|
955
|
+
return { avoided: true, rule: `attr:${attr}=${val}` };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return { avoided: false };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Honeypot / Ghost Element Detection (built-in, always runs)
|
|
963
|
+
|
|
964
|
+
function checkHoneypot(el) {
|
|
965
|
+
// SVG elements — not clickable targets
|
|
966
|
+
const isSvg =
|
|
967
|
+
el.tagName === "svg" || el.tagName === "SVG" || el.closest("svg");
|
|
968
|
+
if (isSvg) return { safe: false, reason: "svg-element" };
|
|
969
|
+
|
|
970
|
+
const hasDisplayContents =
|
|
971
|
+
el.getAttribute("data-display-contents") === "true" ||
|
|
972
|
+
getComputedStyle(el).display === "contents";
|
|
973
|
+
|
|
974
|
+
// Aria-hidden
|
|
975
|
+
if (el.getAttribute("aria-hidden") === "true")
|
|
976
|
+
return { safe: false, reason: "aria-hidden" };
|
|
977
|
+
|
|
978
|
+
// No offsetParent (hidden), unless display:contents
|
|
979
|
+
if (!hasDisplayContents && el.offsetParent === null)
|
|
980
|
+
return { safe: false, reason: "no-offsetParent" };
|
|
981
|
+
|
|
982
|
+
// Honeypot class patterns
|
|
983
|
+
const classStr = Array.from(el.classList).join(" ");
|
|
984
|
+
if (
|
|
985
|
+
/\b(ghost|sr-only|visually-hidden|trap|honey|offscreen|off-screen)\b/i.test(
|
|
986
|
+
classStr,
|
|
987
|
+
)
|
|
988
|
+
)
|
|
989
|
+
return { safe: false, reason: "honeypot-class", detail: classStr };
|
|
990
|
+
|
|
991
|
+
// CSS checks
|
|
992
|
+
const computed = getComputedStyle(el);
|
|
993
|
+
if (parseFloat(computed.opacity) === 0)
|
|
994
|
+
return { safe: false, reason: "opacity-zero" };
|
|
995
|
+
if (computed.visibility === "hidden")
|
|
996
|
+
return { safe: false, reason: "visibility-hidden" };
|
|
997
|
+
|
|
998
|
+
// Sub-pixel elements (tracking pixels, invisible traps)
|
|
999
|
+
const rect = el.getBoundingClientRect();
|
|
1000
|
+
if (rect.width < 5 || rect.height < 5)
|
|
1001
|
+
return {
|
|
1002
|
+
safe: false,
|
|
1003
|
+
reason: "sub-pixel",
|
|
1004
|
+
detail: `${rect.width.toFixed(1)}x${rect.height.toFixed(1)}`,
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
return { safe: true };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Shared helper: scroll element into comfortable view before interacting
|
|
1011
|
+
async function ensureElementVisible(el, params) {
|
|
1012
|
+
let rect = el.getBoundingClientRect();
|
|
1013
|
+
const vh = window.innerHeight;
|
|
1014
|
+
const vw = window.innerWidth;
|
|
1015
|
+
const fullyOffScreen =
|
|
1016
|
+
rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw;
|
|
1017
|
+
const partiallyVisible =
|
|
1018
|
+
!fullyOffScreen &&
|
|
1019
|
+
(rect.top < 0 ||
|
|
1020
|
+
rect.bottom > vh ||
|
|
1021
|
+
rect.top > vh * 0.85 ||
|
|
1022
|
+
rect.bottom < vh * 0.15);
|
|
1023
|
+
|
|
1024
|
+
if (fullyOffScreen || partiallyVisible) {
|
|
1025
|
+
// Smooth scroll into comfortable view first
|
|
1026
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1027
|
+
await new Promise((r) =>
|
|
1028
|
+
setTimeout(r, 400 + Math.floor(Math.random() * 300)),
|
|
1029
|
+
);
|
|
1030
|
+
rect = el.getBoundingClientRect();
|
|
1031
|
+
|
|
1032
|
+
// If still fully off-screen after scrollIntoView, use multi-step humanScroll
|
|
1033
|
+
if (rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw) {
|
|
1034
|
+
const maxSteps = 20;
|
|
1035
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
1036
|
+
rect = el.getBoundingClientRect();
|
|
1037
|
+
if (rect.top > vh * 0.15 && rect.bottom < vh * 0.85) break;
|
|
1038
|
+
|
|
1039
|
+
const direction = rect.top > vh ? "down" : "up";
|
|
1040
|
+
await actionHumanScroll({ ...params, direction });
|
|
1041
|
+
await new Promise((r) =>
|
|
1042
|
+
setTimeout(r, 600 + Math.floor(Math.random() * 800)),
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
rect = el.getBoundingClientRect();
|
|
1047
|
+
if (rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw) {
|
|
1048
|
+
return { visible: false, rect };
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return { visible: true, rect };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Action: human.click — safe click with honeypot detection + bezier movement
|
|
1055
|
+
|
|
1056
|
+
async function actionHumanClick(params) {
|
|
1057
|
+
const el = resolveElement(params);
|
|
1058
|
+
const config = params.config || {};
|
|
1059
|
+
const minDelay =
|
|
1060
|
+
config.thinkDelayMin !== undefined ? config.thinkDelayMin : 150;
|
|
1061
|
+
const maxDelay =
|
|
1062
|
+
config.thinkDelayMax !== undefined ? config.thinkDelayMax : 400;
|
|
1063
|
+
const maxShift = config.maxShiftPx !== undefined ? config.maxShiftPx : 50;
|
|
1064
|
+
|
|
1065
|
+
// Check custom avoid rules
|
|
1066
|
+
const avoidResult = checkAvoid(el, params.avoid);
|
|
1067
|
+
if (avoidResult.avoided)
|
|
1068
|
+
return { clicked: false, reason: "avoided", rule: avoidResult.rule };
|
|
1069
|
+
|
|
1070
|
+
// Built-in honeypot detection
|
|
1071
|
+
const honeypot = checkHoneypot(el);
|
|
1072
|
+
if (!honeypot.safe)
|
|
1073
|
+
return { clicked: false, reason: honeypot.reason, detail: honeypot.detail };
|
|
1074
|
+
|
|
1075
|
+
// Bounding box validation
|
|
1076
|
+
let rect = el.getBoundingClientRect();
|
|
1077
|
+
if (rect.width === 0 && rect.height === 0)
|
|
1078
|
+
return { clicked: false, reason: "no-bounding-box" };
|
|
1079
|
+
|
|
1080
|
+
// Scroll element into comfortable view
|
|
1081
|
+
const scrollResult = await ensureElementVisible(el, params);
|
|
1082
|
+
if (!scrollResult.visible)
|
|
1083
|
+
return {
|
|
1084
|
+
clicked: false,
|
|
1085
|
+
reason: "off-screen",
|
|
1086
|
+
detail: "could not scroll into view",
|
|
1087
|
+
};
|
|
1088
|
+
rect = scrollResult.rect;
|
|
1089
|
+
|
|
1090
|
+
// Bezier mouse move to element
|
|
1091
|
+
await actionMouseMoveTo(params);
|
|
1092
|
+
|
|
1093
|
+
// Human think-time delay
|
|
1094
|
+
const thinkTime =
|
|
1095
|
+
minDelay + Math.floor(Math.random() * (maxDelay - minDelay + 1));
|
|
1096
|
+
await new Promise((r) => setTimeout(r, thinkTime));
|
|
1097
|
+
|
|
1098
|
+
// Element shift detection — did it move during think time?
|
|
1099
|
+
const rectAfter = el.getBoundingClientRect();
|
|
1100
|
+
if (rectAfter.width === 0 && rectAfter.height === 0)
|
|
1101
|
+
return { clicked: false, reason: "element-disappeared" };
|
|
1102
|
+
|
|
1103
|
+
const shiftX = Math.abs(rectAfter.x - rect.x);
|
|
1104
|
+
const shiftY = Math.abs(rectAfter.y - rect.y);
|
|
1105
|
+
if (shiftX > maxShift || shiftY > maxShift)
|
|
1106
|
+
return {
|
|
1107
|
+
clicked: false,
|
|
1108
|
+
reason: "element-shifted",
|
|
1109
|
+
detail: `${shiftX.toFixed(0)}x${shiftY.toFixed(0)}px`,
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// Click dispatch (mousedown → mouseup → click)
|
|
1113
|
+
actionClick(params);
|
|
1114
|
+
return { clicked: true };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Action: human.type — per-character typing with human-like timing
|
|
1118
|
+
|
|
1119
|
+
async function actionHumanType(params) {
|
|
1120
|
+
const { text, handleId, selector } = params;
|
|
1121
|
+
const config = params.config || {};
|
|
1122
|
+
const baseMin = config.baseDelayMin !== undefined ? config.baseDelayMin : 80;
|
|
1123
|
+
const baseMax = config.baseDelayMax !== undefined ? config.baseDelayMax : 180;
|
|
1124
|
+
const variance = config.variance !== undefined ? config.variance : 25;
|
|
1125
|
+
const pauseChance =
|
|
1126
|
+
config.pauseChance !== undefined ? config.pauseChance : 0.12;
|
|
1127
|
+
const pauseMin = config.pauseMin !== undefined ? config.pauseMin : 150;
|
|
1128
|
+
const pauseMax = config.pauseMax !== undefined ? config.pauseMax : 400;
|
|
1129
|
+
|
|
1130
|
+
// Resolve target
|
|
1131
|
+
let target;
|
|
1132
|
+
if (handleId) target = getHandle(handleId);
|
|
1133
|
+
else if (selector) target = document.querySelector(selector);
|
|
1134
|
+
else target = document.activeElement;
|
|
1135
|
+
if (!target) throw new Error("No target for typing");
|
|
1136
|
+
|
|
1137
|
+
// Check avoid rules on target element
|
|
1138
|
+
const avoidResult = checkAvoid(target, params.avoid);
|
|
1139
|
+
if (avoidResult.avoided)
|
|
1140
|
+
return { typed: false, reason: "avoided", rule: avoidResult.rule };
|
|
1141
|
+
|
|
1142
|
+
if (target !== document.activeElement) target.focus();
|
|
1143
|
+
|
|
1144
|
+
// Tokenize: split text into regular chars and {SpecialKey} tokens
|
|
1145
|
+
const tokens = [];
|
|
1146
|
+
let pos = 0;
|
|
1147
|
+
while (pos < text.length) {
|
|
1148
|
+
if (text[pos] === "{") {
|
|
1149
|
+
const end = text.indexOf("}", pos);
|
|
1150
|
+
if (end > pos + 1) {
|
|
1151
|
+
tokens.push({ type: "key", value: text.slice(pos + 1, end) });
|
|
1152
|
+
pos = end + 1;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
tokens.push({ type: "char", value: text[pos] });
|
|
1157
|
+
pos++;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1161
|
+
const token = tokens[i];
|
|
1162
|
+
|
|
1163
|
+
if (token.type === "key") {
|
|
1164
|
+
// Special key — dispatch via resolveKey/buildEventProps like actionKeyPress
|
|
1165
|
+
const resolved = resolveKey(token.value);
|
|
1166
|
+
const props = buildEventProps(resolved);
|
|
1167
|
+
target.dispatchEvent(new KeyboardEvent("keydown", props));
|
|
1168
|
+
applyKeyEffect(target, resolved);
|
|
1169
|
+
target.dispatchEvent(new KeyboardEvent("keyup", props));
|
|
1170
|
+
} else {
|
|
1171
|
+
// Regular character
|
|
1172
|
+
const char = token.value;
|
|
1173
|
+
const charCode = char.charCodeAt(0);
|
|
1174
|
+
const shared = {
|
|
1175
|
+
key: char,
|
|
1176
|
+
code: `Key${char.toUpperCase()}`,
|
|
1177
|
+
keyCode: charCode,
|
|
1178
|
+
charCode,
|
|
1179
|
+
which: charCode,
|
|
1180
|
+
bubbles: true,
|
|
1181
|
+
cancelable: true,
|
|
1182
|
+
view: window,
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
target.dispatchEvent(new KeyboardEvent("keydown", shared));
|
|
1186
|
+
target.dispatchEvent(new KeyboardEvent("keypress", shared));
|
|
1187
|
+
|
|
1188
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
1189
|
+
const start = target.selectionStart || 0;
|
|
1190
|
+
const end = target.selectionEnd || 0;
|
|
1191
|
+
const newValue =
|
|
1192
|
+
target.value.slice(0, start) + char + target.value.slice(end);
|
|
1193
|
+
const proto =
|
|
1194
|
+
target.tagName === "TEXTAREA"
|
|
1195
|
+
? HTMLTextAreaElement.prototype
|
|
1196
|
+
: HTMLInputElement.prototype;
|
|
1197
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
1198
|
+
proto,
|
|
1199
|
+
"value",
|
|
1200
|
+
)?.set;
|
|
1201
|
+
if (nativeSetter) nativeSetter.call(target, newValue);
|
|
1202
|
+
else target.value = newValue;
|
|
1203
|
+
target.selectionStart = target.selectionEnd = start + 1;
|
|
1204
|
+
} else if (target.isContentEditable) {
|
|
1205
|
+
document.execCommand("insertText", false, char);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
target.dispatchEvent(
|
|
1209
|
+
new InputEvent("input", {
|
|
1210
|
+
data: char,
|
|
1211
|
+
inputType: "insertText",
|
|
1212
|
+
bubbles: true,
|
|
1213
|
+
cancelable: true,
|
|
1214
|
+
}),
|
|
1215
|
+
);
|
|
1216
|
+
target.dispatchEvent(new KeyboardEvent("keyup", shared));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Human delay between tokens
|
|
1220
|
+
const baseDelay =
|
|
1221
|
+
baseMin + Math.floor(Math.random() * (baseMax - baseMin + 1));
|
|
1222
|
+
const micro = Math.floor(Math.random() * (variance * 2 + 1)) - variance;
|
|
1223
|
+
const charDelay = Math.max(50, baseDelay + micro);
|
|
1224
|
+
await new Promise((r) => setTimeout(r, charDelay));
|
|
1225
|
+
|
|
1226
|
+
// Thinking pause (random chance, not on last token)
|
|
1227
|
+
if (Math.random() < pauseChance && i < tokens.length - 1) {
|
|
1228
|
+
const pause =
|
|
1229
|
+
pauseMin + Math.floor(Math.random() * (pauseMax - pauseMin + 1));
|
|
1230
|
+
await new Promise((r) => setTimeout(r, pause));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return { typed: true };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Action: human.scroll — natural scrolling with optional back-scroll
|
|
1238
|
+
|
|
1239
|
+
async function actionHumanScroll(params) {
|
|
1240
|
+
const config = params.config || {};
|
|
1241
|
+
const flickMin = config.flickMin !== undefined ? config.flickMin : 150;
|
|
1242
|
+
const flickMax = config.flickMax !== undefined ? config.flickMax : 350;
|
|
1243
|
+
const backScrollChance =
|
|
1244
|
+
config.backScrollChance !== undefined ? config.backScrollChance : 0.1;
|
|
1245
|
+
const backScrollMin =
|
|
1246
|
+
config.backScrollMin !== undefined ? config.backScrollMin : 15;
|
|
1247
|
+
const backScrollMax =
|
|
1248
|
+
config.backScrollMax !== undefined ? config.backScrollMax : 60;
|
|
1249
|
+
|
|
1250
|
+
const { selector, direction = "down", amount } = params;
|
|
1251
|
+
|
|
1252
|
+
// Determine total amount: use param if provided, else randomized default from config
|
|
1253
|
+
const defaultMin = config.amountMin !== undefined ? config.amountMin : 250;
|
|
1254
|
+
const defaultMax = config.amountMax !== undefined ? config.amountMax : 550;
|
|
1255
|
+
|
|
1256
|
+
const totalAmount =
|
|
1257
|
+
amount !== undefined
|
|
1258
|
+
? amount
|
|
1259
|
+
: defaultMin + Math.floor(Math.random() * (defaultMax - defaultMin + 1));
|
|
1260
|
+
let remaining = totalAmount;
|
|
1261
|
+
|
|
1262
|
+
let el = null;
|
|
1263
|
+
if (params.handleId) {
|
|
1264
|
+
try { el = resolveElement(params); } catch {}
|
|
1265
|
+
} else if (selector) {
|
|
1266
|
+
el = document.querySelector(selector);
|
|
1267
|
+
}
|
|
1268
|
+
const isTargetScrollable = el && el.scrollHeight > el.clientHeight + 10;
|
|
1269
|
+
const target = isTargetScrollable ? el : window;
|
|
1270
|
+
|
|
1271
|
+
while (remaining > 0) {
|
|
1272
|
+
// Determine this flick's amount
|
|
1273
|
+
const flickAmount = Math.min(
|
|
1274
|
+
remaining,
|
|
1275
|
+
flickMin + Math.floor(Math.random() * (flickMax - flickMin + 1)),
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
const top =
|
|
1279
|
+
direction === "down"
|
|
1280
|
+
? flickAmount
|
|
1281
|
+
: direction === "up"
|
|
1282
|
+
? -flickAmount
|
|
1283
|
+
: 0;
|
|
1284
|
+
const left =
|
|
1285
|
+
direction === "right"
|
|
1286
|
+
? flickAmount
|
|
1287
|
+
: direction === "left"
|
|
1288
|
+
? -flickAmount
|
|
1289
|
+
: 0;
|
|
1290
|
+
|
|
1291
|
+
target.scrollBy({ top, left, behavior: "smooth" });
|
|
1292
|
+
remaining -= flickAmount;
|
|
1293
|
+
|
|
1294
|
+
// Back-scroll for realism (per-flick chance)
|
|
1295
|
+
if (Math.random() < backScrollChance) {
|
|
1296
|
+
await new Promise((r) =>
|
|
1297
|
+
setTimeout(r, 200 + Math.floor(Math.random() * 100)),
|
|
1298
|
+
);
|
|
1299
|
+
const backAmount =
|
|
1300
|
+
backScrollMin +
|
|
1301
|
+
Math.floor(Math.random() * (backScrollMax - backScrollMin + 1));
|
|
1302
|
+
const backTop =
|
|
1303
|
+
direction === "down"
|
|
1304
|
+
? -backAmount
|
|
1305
|
+
: direction === "up"
|
|
1306
|
+
? backAmount
|
|
1307
|
+
: 0;
|
|
1308
|
+
const backLeft =
|
|
1309
|
+
direction === "right"
|
|
1310
|
+
? -backAmount
|
|
1311
|
+
: direction === "left"
|
|
1312
|
+
? backAmount
|
|
1313
|
+
: 0;
|
|
1314
|
+
target.scrollBy({ top: backTop, left: backLeft, behavior: "smooth" });
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Pause between flicks (unless it was the last one)
|
|
1318
|
+
if (remaining > 0) {
|
|
1319
|
+
const flickPause = 150 + Math.floor(Math.random() * 250);
|
|
1320
|
+
await new Promise((r) => setTimeout(r, flickPause));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Final settling pause for smooth scrolling to finish
|
|
1325
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1326
|
+
|
|
1327
|
+
return { scrolled: true, amount: totalAmount };
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Action: dom.batchQuery — perform multiple selector checks in one go
|
|
1331
|
+
function actionBatchQuery(params) {
|
|
1332
|
+
const { selectors = [] } = params;
|
|
1333
|
+
const results = {};
|
|
1334
|
+
for (const selector of selectors) {
|
|
1335
|
+
const el = document.querySelector(selector);
|
|
1336
|
+
results[selector] = !!el;
|
|
1337
|
+
}
|
|
1338
|
+
return results;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Action: human.clearInput — focus + select-all + delete with human timing
|
|
1342
|
+
|
|
1343
|
+
async function actionHumanClearInput(params) {
|
|
1344
|
+
// Use human.click to focus (gets honeypot + avoid checks)
|
|
1345
|
+
const clickResult = await actionHumanClick(params);
|
|
1346
|
+
if (!clickResult.clicked) return clickResult;
|
|
1347
|
+
|
|
1348
|
+
const el = resolveElement(params);
|
|
1349
|
+
|
|
1350
|
+
// Triple-click with human timing to select all
|
|
1351
|
+
await new Promise((r) => setTimeout(r, 40 + Math.floor(Math.random() * 50)));
|
|
1352
|
+
actionClick({ ...params, clickCount: 1 });
|
|
1353
|
+
await new Promise((r) => setTimeout(r, 50 + Math.floor(Math.random() * 60)));
|
|
1354
|
+
actionClick({ ...params, clickCount: 2 });
|
|
1355
|
+
await new Promise((r) => setTimeout(r, 45 + Math.floor(Math.random() * 55)));
|
|
1356
|
+
actionClick({ ...params, clickCount: 3 });
|
|
1357
|
+
|
|
1358
|
+
// Pause, then delete
|
|
1359
|
+
await new Promise((r) =>
|
|
1360
|
+
setTimeout(r, 120 + Math.floor(Math.random() * 120)),
|
|
1361
|
+
);
|
|
1362
|
+
actionKeyPress({ key: "Backspace" });
|
|
1363
|
+
await new Promise((r) =>
|
|
1364
|
+
setTimeout(r, 180 + Math.floor(Math.random() * 140)),
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
return { cleared: true };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Message Handler
|
|
1371
|
+
|
|
1372
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1373
|
+
const action = message?.action;
|
|
1374
|
+
const incomingParams = message?.params || {};
|
|
1375
|
+
applyFrameworkConfig(incomingParams.__frameworkConfig);
|
|
1376
|
+
const { __frameworkConfig, ...params } = incomingParams;
|
|
1377
|
+
|
|
1378
|
+
try {
|
|
1379
|
+
switch (action) {
|
|
1380
|
+
case "dom.markElement":
|
|
1381
|
+
sendResponse({ result: actionMarkElement(params) });
|
|
1382
|
+
return;
|
|
1383
|
+
case "dom.unmarkElement":
|
|
1384
|
+
sendResponse({ result: actionUnmarkElement(params) });
|
|
1385
|
+
return;
|
|
1386
|
+
case "dom.registerMarkedElement":
|
|
1387
|
+
sendResponse({ result: actionRegisterMarkedElement(params) });
|
|
1388
|
+
return;
|
|
1389
|
+
case "dom.querySelector":
|
|
1390
|
+
sendResponse({ result: actionQuerySelector(params) });
|
|
1391
|
+
return;
|
|
1392
|
+
case "dom.querySelectorAll":
|
|
1393
|
+
sendResponse({ result: actionQuerySelectorAll(params) });
|
|
1394
|
+
return;
|
|
1395
|
+
case "dom.querySelectorWithin":
|
|
1396
|
+
sendResponse({ result: actionQuerySelectorWithin(params) });
|
|
1397
|
+
return;
|
|
1398
|
+
case "dom.querySelectorAllWithin":
|
|
1399
|
+
sendResponse({ result: actionQuerySelectorAllWithin(params) });
|
|
1400
|
+
return;
|
|
1401
|
+
case "dom.queryAllInfo":
|
|
1402
|
+
sendResponse({ result: actionQueryAllInfo(params) });
|
|
1403
|
+
return;
|
|
1404
|
+
case "dom.batchQuery":
|
|
1405
|
+
sendResponse({ result: actionBatchQuery(params) });
|
|
1406
|
+
return;
|
|
1407
|
+
case "dom.boundingBox":
|
|
1408
|
+
sendResponse({ result: actionBoundingBox(params) });
|
|
1409
|
+
return;
|
|
1410
|
+
case "dom.mouseMoveTo":
|
|
1411
|
+
actionMouseMoveTo(params)
|
|
1412
|
+
.then((r) => sendResponse({ result: r }))
|
|
1413
|
+
.catch((e) => sendResponse({ error: e.message }));
|
|
1414
|
+
return true; // async
|
|
1415
|
+
case "dom.click":
|
|
1416
|
+
actionHumanClick(params)
|
|
1417
|
+
.then((r) => sendResponse({ result: r }))
|
|
1418
|
+
.catch((e) => sendResponse({ error: e.message }));
|
|
1419
|
+
return true; // async
|
|
1420
|
+
case "dom.type":
|
|
1421
|
+
sendResponse({ result: actionType(params) });
|
|
1422
|
+
return;
|
|
1423
|
+
case "dom.keyPress":
|
|
1424
|
+
sendResponse({ result: actionKeyPress(params) });
|
|
1425
|
+
return;
|
|
1426
|
+
case "dom.keyDown":
|
|
1427
|
+
sendResponse({ result: actionKeyDown(params) });
|
|
1428
|
+
return;
|
|
1429
|
+
case "dom.keyUp":
|
|
1430
|
+
sendResponse({ result: actionKeyUp(params) });
|
|
1431
|
+
return;
|
|
1432
|
+
case "dom.scroll":
|
|
1433
|
+
sendResponse({ result: actionScroll(params) });
|
|
1434
|
+
return;
|
|
1435
|
+
case "dom.focus":
|
|
1436
|
+
sendResponse({ result: actionFocus(params) });
|
|
1437
|
+
return;
|
|
1438
|
+
case "dom.setValue":
|
|
1439
|
+
sendResponse({ result: actionSetValue(params) });
|
|
1440
|
+
return;
|
|
1441
|
+
case "dom.getAttribute":
|
|
1442
|
+
sendResponse({ result: actionGetAttribute(params) });
|
|
1443
|
+
return;
|
|
1444
|
+
case "dom.getProperty":
|
|
1445
|
+
sendResponse({ result: actionGetProperty(params) });
|
|
1446
|
+
return;
|
|
1447
|
+
case "dom.getHTML":
|
|
1448
|
+
// CSP-safe HTML retrieval from ISOLATED world
|
|
1449
|
+
sendResponse({
|
|
1450
|
+
result: {
|
|
1451
|
+
html: document.documentElement?.outerHTML || "",
|
|
1452
|
+
title: document.title || "",
|
|
1453
|
+
url: location?.href || ""
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
return;
|
|
1457
|
+
case "dom.elementHTML": {
|
|
1458
|
+
// CSP-safe: get outerHTML/innerHTML of a specific handle
|
|
1459
|
+
const ehEl = resolveElement(params);
|
|
1460
|
+
sendResponse({
|
|
1461
|
+
result: {
|
|
1462
|
+
outer: ehEl.outerHTML.slice(0, params.limit || 5000),
|
|
1463
|
+
inner: ehEl.innerHTML.slice(0, params.limit || 5000),
|
|
1464
|
+
tag: ehEl.tagName.toLowerCase(),
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
case "dom.findScrollable": {
|
|
1470
|
+
// Find all scrollable containers on the page
|
|
1471
|
+
const scrollables = [];
|
|
1472
|
+
const all = document.querySelectorAll('*');
|
|
1473
|
+
for (const el of all) {
|
|
1474
|
+
if (el.scrollHeight > el.clientHeight + 20 && el !== document.documentElement && el !== document.body) {
|
|
1475
|
+
const style = getComputedStyle(el);
|
|
1476
|
+
const oy = style.overflowY;
|
|
1477
|
+
const ox = style.overflow;
|
|
1478
|
+
if (oy === 'visible' && ox === 'visible') continue; // skip non-scrollable
|
|
1479
|
+
const hid = storeHandle(el);
|
|
1480
|
+
scrollables.push({
|
|
1481
|
+
handleId: hid,
|
|
1482
|
+
tag: el.tagName.toLowerCase(),
|
|
1483
|
+
id: el.id || null,
|
|
1484
|
+
cls: [...el.classList].slice(0, 3).join(' ') || null,
|
|
1485
|
+
overflowY: oy,
|
|
1486
|
+
overflow: ox,
|
|
1487
|
+
scrollHeight: el.scrollHeight,
|
|
1488
|
+
clientHeight: el.clientHeight,
|
|
1489
|
+
children: el.children.length,
|
|
1490
|
+
text: (el.textContent || '').trim().slice(0, 80),
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
sendResponse({ result: scrollables });
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
case "dom.waitForSelector":
|
|
1499
|
+
actionWaitForSelector(params, sendResponse);
|
|
1500
|
+
return true; // async
|
|
1501
|
+
case "dom.evaluate":
|
|
1502
|
+
case "dom.evaluateViaScript": {
|
|
1503
|
+
// Inject inline <script> to run in MAIN world (works with 'unsafe-inline' CSP)
|
|
1504
|
+
const callId =
|
|
1505
|
+
"_hb_" + Date.now() + "_" + Math.floor(Math.random() * 1e6);
|
|
1506
|
+
const resultEl = document.createElement("div");
|
|
1507
|
+
resultEl.id = callId;
|
|
1508
|
+
resultEl.style.display = "none";
|
|
1509
|
+
document.documentElement.appendChild(resultEl);
|
|
1510
|
+
|
|
1511
|
+
const argsJson = JSON.stringify(params.args || []);
|
|
1512
|
+
let markerSetup = "";
|
|
1513
|
+
if (params.markerId) {
|
|
1514
|
+
markerSetup = `var __el = document.querySelector('[data-bridge-eval="${params.markerId}"]');
|
|
1515
|
+
if (__el) __el.removeAttribute('data-bridge-eval');`;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const script = document.createElement("script");
|
|
1519
|
+
script.textContent = `(function(){
|
|
1520
|
+
try {
|
|
1521
|
+
${markerSetup}
|
|
1522
|
+
var __fn = (${params.fn});
|
|
1523
|
+
var __args = ${argsJson};
|
|
1524
|
+
${params.markerId ? "if (__el) __args.unshift(__el);" : ""}
|
|
1525
|
+
var __r = __fn.apply(null, __args);
|
|
1526
|
+
if (__r && typeof __r.then === 'function') {
|
|
1527
|
+
__r.then(function(v) {
|
|
1528
|
+
document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:v}));
|
|
1529
|
+
}).catch(function(e) {
|
|
1530
|
+
document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
|
|
1531
|
+
});
|
|
1532
|
+
} else {
|
|
1533
|
+
document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:__r}));
|
|
1534
|
+
}
|
|
1535
|
+
} catch(e) {
|
|
1536
|
+
document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
|
|
1537
|
+
}
|
|
1538
|
+
})();`;
|
|
1539
|
+
document.documentElement.appendChild(script);
|
|
1540
|
+
script.remove();
|
|
1541
|
+
|
|
1542
|
+
// Check result — sync scripts already set data-result before we get here.
|
|
1543
|
+
// Async scripts: use MutationObserver for instant notification (no 50ms polling).
|
|
1544
|
+
const pollForResult = (resolve) => {
|
|
1545
|
+
const el = document.getElementById(callId);
|
|
1546
|
+
if (!el) {
|
|
1547
|
+
resolve({ error: "Result element removed" });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const harvest = () => {
|
|
1552
|
+
const resultAttr = el.getAttribute("data-result");
|
|
1553
|
+
if (resultAttr !== null) {
|
|
1554
|
+
el.remove();
|
|
1555
|
+
try {
|
|
1556
|
+
resolve({ result: JSON.parse(resultAttr).v });
|
|
1557
|
+
} catch {
|
|
1558
|
+
resolve({ result: resultAttr });
|
|
1559
|
+
}
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
const errorAttr = el.getAttribute("data-error");
|
|
1563
|
+
if (errorAttr !== null) {
|
|
1564
|
+
el.remove();
|
|
1565
|
+
resolve({ error: errorAttr });
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
return false;
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
// Instant path: synchronous scripts already wrote the result
|
|
1572
|
+
if (harvest()) return;
|
|
1573
|
+
|
|
1574
|
+
// Async path: observe attribute changes on the result element
|
|
1575
|
+
const observer = new MutationObserver(() => {
|
|
1576
|
+
if (harvest()) {
|
|
1577
|
+
observer.disconnect();
|
|
1578
|
+
clearTimeout(timer);
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
observer.observe(el, {
|
|
1582
|
+
attributes: true,
|
|
1583
|
+
attributeFilter: ["data-result", "data-error"],
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
const timer = setTimeout(() => {
|
|
1587
|
+
observer.disconnect();
|
|
1588
|
+
el.remove();
|
|
1589
|
+
resolve({ error: "Evaluate timed out" });
|
|
1590
|
+
}, 5000);
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
new Promise(pollForResult).then((res) => {
|
|
1594
|
+
if (res.error) sendResponse({ error: res.error });
|
|
1595
|
+
else sendResponse({ result: res.result });
|
|
1596
|
+
});
|
|
1597
|
+
return true; // async
|
|
1598
|
+
}
|
|
1599
|
+
case "dom.elementEvaluate": {
|
|
1600
|
+
// Route through dom.evaluateViaScript with markerId
|
|
1601
|
+
// First mark the element, then run evaluate
|
|
1602
|
+
const el = getHandle(params.handleId);
|
|
1603
|
+
const markerId =
|
|
1604
|
+
"_hbm_" + Date.now() + "_" + Math.floor(Math.random() * 1e6);
|
|
1605
|
+
el.setAttribute("data-bridge-eval", markerId);
|
|
1606
|
+
|
|
1607
|
+
const callId =
|
|
1608
|
+
"_hb_" + Date.now() + "_" + Math.floor(Math.random() * 1e6);
|
|
1609
|
+
const resultEl = document.createElement("div");
|
|
1610
|
+
resultEl.id = callId;
|
|
1611
|
+
resultEl.style.display = "none";
|
|
1612
|
+
document.documentElement.appendChild(resultEl);
|
|
1613
|
+
|
|
1614
|
+
const argsJson = JSON.stringify(params.args || []);
|
|
1615
|
+
const script = document.createElement("script");
|
|
1616
|
+
script.textContent = `(function(){
|
|
1617
|
+
try {
|
|
1618
|
+
var __el = document.querySelector('[data-bridge-eval="${markerId}"]');
|
|
1619
|
+
if (__el) __el.removeAttribute('data-bridge-eval');
|
|
1620
|
+
var __fn = (${params.fn});
|
|
1621
|
+
var __args = ${argsJson};
|
|
1622
|
+
var __r = __fn.apply(null, [__el].concat(__args));
|
|
1623
|
+
if (__r && typeof __r.then === 'function') {
|
|
1624
|
+
__r.then(function(v) {
|
|
1625
|
+
document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:v}));
|
|
1626
|
+
}).catch(function(e) {
|
|
1627
|
+
document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
|
|
1628
|
+
});
|
|
1629
|
+
} else {
|
|
1630
|
+
document.getElementById('${callId}').setAttribute('data-result', JSON.stringify({v:__r}));
|
|
1631
|
+
}
|
|
1632
|
+
} catch(e) {
|
|
1633
|
+
document.getElementById('${callId}').setAttribute('data-error', e.message || String(e));
|
|
1634
|
+
}
|
|
1635
|
+
})();`;
|
|
1636
|
+
document.documentElement.appendChild(script);
|
|
1637
|
+
script.remove();
|
|
1638
|
+
|
|
1639
|
+
const pollForResult2 = (resolve) => {
|
|
1640
|
+
const el = document.getElementById(callId);
|
|
1641
|
+
if (!el) {
|
|
1642
|
+
resolve({ error: "Result element removed" });
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const harvest = () => {
|
|
1647
|
+
const resultAttr = el.getAttribute("data-result");
|
|
1648
|
+
if (resultAttr !== null) {
|
|
1649
|
+
el.remove();
|
|
1650
|
+
try {
|
|
1651
|
+
resolve({ result: JSON.parse(resultAttr).v });
|
|
1652
|
+
} catch {
|
|
1653
|
+
resolve({ result: resultAttr });
|
|
1654
|
+
}
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
const errorAttr = el.getAttribute("data-error");
|
|
1658
|
+
if (errorAttr !== null) {
|
|
1659
|
+
el.remove();
|
|
1660
|
+
resolve({ error: errorAttr });
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
return false;
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
if (harvest()) return;
|
|
1667
|
+
|
|
1668
|
+
const observer = new MutationObserver(() => {
|
|
1669
|
+
if (harvest()) {
|
|
1670
|
+
observer.disconnect();
|
|
1671
|
+
clearTimeout(timer);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
observer.observe(el, {
|
|
1675
|
+
attributes: true,
|
|
1676
|
+
attributeFilter: ["data-result", "data-error"],
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
const timer = setTimeout(() => {
|
|
1680
|
+
observer.disconnect();
|
|
1681
|
+
el.remove();
|
|
1682
|
+
resolve({ error: "Evaluate timed out" });
|
|
1683
|
+
}, 5000);
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
new Promise(pollForResult2).then((res) => {
|
|
1687
|
+
if (res.error) sendResponse({ error: res.error });
|
|
1688
|
+
else sendResponse({ result: res.result });
|
|
1689
|
+
});
|
|
1690
|
+
return true; // async
|
|
1691
|
+
}
|
|
1692
|
+
case "dom.evaluateIsolated": {
|
|
1693
|
+
// CSP-safe evaluation in ISOLATED world (content script context)
|
|
1694
|
+
// Limited to DOM access, cannot access page JS globals
|
|
1695
|
+
try {
|
|
1696
|
+
const fn = new Function("return (" + params.fn + ")")();
|
|
1697
|
+
const result = fn.apply(null, params.args || []);
|
|
1698
|
+
sendResponse({ result });
|
|
1699
|
+
} catch (e) {
|
|
1700
|
+
sendResponse({ error: e.message });
|
|
1701
|
+
}
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
case "dom.discoverElements": {
|
|
1705
|
+
// CSP-safe element discovery — no eval needed
|
|
1706
|
+
const results = [];
|
|
1707
|
+
const seen = new Set();
|
|
1708
|
+
const vw = window.innerWidth;
|
|
1709
|
+
const vh = window.innerHeight;
|
|
1710
|
+
|
|
1711
|
+
// Helper: find a safe CSS class (no special chars like : [ ] ( ) )
|
|
1712
|
+
function safeClass(el) {
|
|
1713
|
+
for (const cls of el.classList) {
|
|
1714
|
+
if (/^[a-zA-Z_-][a-zA-Z0-9_-]*$/.test(cls)) return cls;
|
|
1715
|
+
}
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// Links
|
|
1720
|
+
for (const el of document.querySelectorAll("a[href]")) {
|
|
1721
|
+
const href = el.href || "";
|
|
1722
|
+
const text = (el.textContent || "").trim().slice(0, 80);
|
|
1723
|
+
if (!text || seen.has(href + text)) continue;
|
|
1724
|
+
seen.add(href + text);
|
|
1725
|
+
const rect = el.getBoundingClientRect();
|
|
1726
|
+
const cs = getComputedStyle(el);
|
|
1727
|
+
if (
|
|
1728
|
+
rect.width <= 0 ||
|
|
1729
|
+
rect.height <= 0 ||
|
|
1730
|
+
cs.display === "none" ||
|
|
1731
|
+
cs.visibility === "hidden"
|
|
1732
|
+
)
|
|
1733
|
+
continue;
|
|
1734
|
+
const sc = safeClass(el);
|
|
1735
|
+
const rawHref = el.getAttribute("href") || "";
|
|
1736
|
+
const selector = el.id
|
|
1737
|
+
? `a#${el.id}`
|
|
1738
|
+
: sc
|
|
1739
|
+
? `a.${sc}`
|
|
1740
|
+
: rawHref
|
|
1741
|
+
? `a[href="${rawHref.replace(/"/g, '\\"')}"]`
|
|
1742
|
+
: "a";
|
|
1743
|
+
results.push({
|
|
1744
|
+
type: "link",
|
|
1745
|
+
tag: "a",
|
|
1746
|
+
text,
|
|
1747
|
+
href,
|
|
1748
|
+
selector,
|
|
1749
|
+
handleId: storeHandle(el),
|
|
1750
|
+
rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Buttons
|
|
1755
|
+
for (const el of document.querySelectorAll('button, [role="button"]')) {
|
|
1756
|
+
const text = (el.textContent || "").trim().slice(0, 80);
|
|
1757
|
+
const rect = el.getBoundingClientRect();
|
|
1758
|
+
const cs = getComputedStyle(el);
|
|
1759
|
+
if (rect.width <= 0 || rect.height <= 0 || cs.display === "none")
|
|
1760
|
+
continue;
|
|
1761
|
+
const tag = el.tagName.toLowerCase();
|
|
1762
|
+
const sc = safeClass(el);
|
|
1763
|
+
const selector = el.id ? `#${el.id}` : sc ? `${tag}.${sc}` : tag;
|
|
1764
|
+
results.push({
|
|
1765
|
+
type: "button",
|
|
1766
|
+
tag,
|
|
1767
|
+
text,
|
|
1768
|
+
selector,
|
|
1769
|
+
handleId: storeHandle(el),
|
|
1770
|
+
rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Inputs / textareas
|
|
1775
|
+
for (const el of document.querySelectorAll(
|
|
1776
|
+
'input, textarea, [contenteditable="true"]',
|
|
1777
|
+
)) {
|
|
1778
|
+
const rect = el.getBoundingClientRect();
|
|
1779
|
+
const cs = getComputedStyle(el);
|
|
1780
|
+
if (rect.width <= 0 || rect.height <= 0 || cs.display === "none")
|
|
1781
|
+
continue;
|
|
1782
|
+
const inputType = el.type || el.tagName.toLowerCase();
|
|
1783
|
+
if (
|
|
1784
|
+
["hidden", "submit", "button", "image", "reset"].includes(inputType)
|
|
1785
|
+
)
|
|
1786
|
+
continue;
|
|
1787
|
+
const tag = el.tagName.toLowerCase();
|
|
1788
|
+
const selector = el.id
|
|
1789
|
+
? `#${el.id}`
|
|
1790
|
+
: el.name
|
|
1791
|
+
? `${tag}[name="${el.name}"]`
|
|
1792
|
+
: el.placeholder
|
|
1793
|
+
? `${tag}[placeholder="${el.placeholder}"]`
|
|
1794
|
+
: tag;
|
|
1795
|
+
results.push({
|
|
1796
|
+
type: "input",
|
|
1797
|
+
tag,
|
|
1798
|
+
inputType,
|
|
1799
|
+
name: el.name || "",
|
|
1800
|
+
placeholder: el.placeholder || "",
|
|
1801
|
+
selector,
|
|
1802
|
+
handleId: storeHandle(el),
|
|
1803
|
+
rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
sendResponse({
|
|
1808
|
+
result: {
|
|
1809
|
+
elements: results,
|
|
1810
|
+
cursor: { x: cursorX, y: cursorY },
|
|
1811
|
+
viewport: { width: vw, height: vh },
|
|
1812
|
+
scrollY: window.scrollY,
|
|
1813
|
+
},
|
|
1814
|
+
});
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
case "dom.setDebug":
|
|
1818
|
+
debugMode = !!params.enabled;
|
|
1819
|
+
frameworkRuntime.debug.cursor = debugMode;
|
|
1820
|
+
if (!debugMode) clearTrail();
|
|
1821
|
+
sendResponse({ result: { debug: debugMode } });
|
|
1822
|
+
return;
|
|
1823
|
+
|
|
1824
|
+
// Human commands — safe, human-like actions with timing + detection
|
|
1825
|
+
case "human.click":
|
|
1826
|
+
actionHumanClick(params)
|
|
1827
|
+
.then((r) => sendResponse({ result: r }))
|
|
1828
|
+
.catch((e) => sendResponse({ error: e.message }));
|
|
1829
|
+
return true; // async
|
|
1830
|
+
case "human.type":
|
|
1831
|
+
actionHumanType(params)
|
|
1832
|
+
.then((r) => sendResponse({ result: r }))
|
|
1833
|
+
.catch((e) => sendResponse({ error: e.message }));
|
|
1834
|
+
return true; // async
|
|
1835
|
+
case "human.scroll":
|
|
1836
|
+
actionHumanScroll(params)
|
|
1837
|
+
.then((r) => sendResponse({ result: r }))
|
|
1838
|
+
.catch((e) => sendResponse({ error: e.message }));
|
|
1839
|
+
return true; // async
|
|
1840
|
+
case "human.clearInput":
|
|
1841
|
+
actionHumanClearInput(params)
|
|
1842
|
+
.then((r) => sendResponse({ result: r }))
|
|
1843
|
+
.catch((e) => sendResponse({ error: e.message }));
|
|
1844
|
+
return true; // async
|
|
1845
|
+
|
|
1846
|
+
default:
|
|
1847
|
+
sendResponse({ error: `Unknown action: ${action}` });
|
|
1848
|
+
}
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
sendResponse({ error: err.message });
|
|
1851
|
+
}
|
|
1852
|
+
});
|