captchakraken 2.8.0 → 3.0.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/AGENTS.md +112 -0
- package/NOTICE +36 -0
- package/dist/cli-invocation.d.ts +9 -40
- package/dist/cli-invocation.js +16 -49
- package/dist/errors.d.ts +7 -27
- package/dist/errors.js +6 -25
- package/dist/humanize.d.ts +56 -240
- package/dist/humanize.js +159 -415
- package/dist/index.d.ts +5 -129
- package/dist/index.js +4 -98
- package/dist/kinds.d.ts +135 -0
- package/dist/kinds.js +70 -0
- package/dist/limits.d.ts +0 -48
- package/dist/limits.js +2 -48
- package/dist/model-name.d.ts +4 -19
- package/dist/model-name.js +32 -40
- package/dist/playwright-types.d.ts +25 -95
- package/dist/playwright-types.js +0 -23
- package/dist/puppeteer-adapter.d.ts +4 -43
- package/dist/puppeteer-adapter.js +22 -58
- package/dist/python-command.d.ts +0 -41
- package/dist/python-command.js +0 -37
- package/dist/selectors.d.ts +54 -0
- package/dist/selectors.js +117 -0
- package/dist/slide-geometry.d.ts +2 -29
- package/dist/slide-geometry.js +18 -44
- package/dist/solver.d.ts +112 -539
- package/dist/solver.js +1088 -2950
- package/dist/timing.d.ts +7 -40
- package/dist/timing.js +4 -36
- package/dist/token-usage.d.ts +1 -7
- package/dist/token-usage.js +10 -95
- package/dist/types.d.ts +77 -483
- package/dist/watcher.d.ts +5 -78
- package/dist/watcher.js +0 -56
- package/package.json +12 -4
- package/python/AGENTS.md +122 -0
- package/python/NOTICE +36 -0
- package/python/README.md +1 -2
- package/python/examples/README.md +10 -0
- package/python/examples/_harness.py +4 -73
- package/python/examples/demoHcaptcha.py +0 -13
- package/python/examples/demoRecaptcha.py +0 -10
- package/python/examples/watch_playwright.py +1 -26
- package/python/examples/with_local_gguf.py +28 -0
- package/python/examples/with_playwright.py +0 -19
- package/python/pyproject.toml +68 -19
- package/python/src/captchakraken/__init__.py +37 -45
- package/python/src/captchakraken/action_types.py +14 -36
- package/python/src/captchakraken/cli.py +262 -879
- package/python/src/captchakraken/config.py +60 -146
- package/python/src/captchakraken/errors.py +17 -95
- package/python/src/captchakraken/humanize.py +174 -597
- package/python/src/captchakraken/image_processor.py +16 -235
- package/python/src/captchakraken/keyframes.py +69 -434
- package/python/src/captchakraken/kinds.py +152 -0
- package/python/src/captchakraken/models.json +79 -21
- package/python/src/captchakraken/overlay.py +65 -498
- package/python/src/captchakraken/page_solver.py +1123 -2564
- package/python/src/captchakraken/planner.py +70 -428
- package/python/src/captchakraken/prompts.py +45 -261
- package/python/src/captchakraken/selectors.py +184 -0
- package/python/src/captchakraken/server_manager.py +7 -58
- package/python/src/captchakraken/solver.py +128 -522
- package/python/src/captchakraken/timing.py +20 -74
- package/python/src/captchakraken/tool_calls/board_painted.py +47 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +20 -30
- package/python/src/captchakraken/tool_calls/find_grid.py +186 -1339
- package/python/src/captchakraken/tool_calls/track_piece.py +81 -57
- package/python/src/captchakraken/updater.py +7 -76
- package/python/src/captchakraken/watcher.py +12 -101
- package/scripts/copy-python.mjs +10 -10
- package/scripts/setup-python.js +1 -21
- package/dist/trajectory.d.ts +0 -58
- package/dist/trajectory.js +0 -266
- package/python/src/captchakraken/tool_calls/move_indicator.py +0 -431
- package/python/src/captchakraken/trajectory.py +0 -284
package/dist/humanize.js
CHANGED
|
@@ -1,90 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* How the driver
|
|
3
|
+
* How the driver moves: one pluggable object per input device. Mirrors python/humanize.py.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
* CaptchaKrakenFinetune pins that the mode names and the pause vocabulary have
|
|
8
|
-
* not drifted.
|
|
5
|
+
* The pointer position lives here, not in the solver, because a touch mode that dispatches no
|
|
6
|
+
* motion between taps still has to say where the next gesture starts.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - a caller driving a MOBILE page has no cursor. Dispatching mousemove at a
|
|
16
|
-
* touch-only widget is not weak humanisation, it is the wrong event type:
|
|
17
|
-
* the page's touch handlers never fire, and a vendor that scores pointer
|
|
18
|
-
* telemetry sees a desktop mouse on a phone.
|
|
19
|
-
* - a caller who has their OWN humanisation (a hardware pointer, a proxied
|
|
20
|
-
* device farm, a model of their own users) was composing two of them. That
|
|
21
|
-
* is not hypothetical — camoufox's `humanize` juggler re-humanises every
|
|
22
|
-
* `mouse.move()` it is handed, and running both measured 82.1s against
|
|
23
|
-
* 13.4s on one geetest_v4_slide solve, because each of the 60 trajectory
|
|
24
|
-
* points became its own humanised sub-trajectory.
|
|
25
|
-
* - a caller on their own infrastructure, against their own fixtures, is
|
|
26
|
-
* paying for texture nobody is measuring.
|
|
27
|
-
*
|
|
28
|
-
* So the vocabulary of gestures is an INTERFACE and the humanisation is an
|
|
29
|
-
* implementation of it. The solver says "tap here", "drag this there", "type
|
|
30
|
-
* that"; a `Humanizer` decides what events that is and how long it takes.
|
|
31
|
-
*
|
|
32
|
-
* Four modes, selected by `CaptchaKrakenConfig.humanization`:
|
|
33
|
-
*
|
|
34
|
-
* 'mouse' MouseHumanizer — the default, and byte-for-byte what the
|
|
35
|
-
* driver did before this module existed.
|
|
36
|
-
* 'mobile' MobileHumanizer — touch events, finger kinematics. Dispatches
|
|
37
|
-
* through a `TouchBackend`, so it drives either
|
|
38
|
-
* a Chromium-family page (CDP) or an
|
|
39
|
-
* Appium/WebdriverIO driver on a real device.
|
|
40
|
-
* 'none' NullHumanizer — the shortest legal path to the same DOM
|
|
41
|
-
* effect.
|
|
42
|
-
* custom anything satisfying `Humanizer`, passed as
|
|
43
|
-
* `CaptchaKrakenConfig.humanizer`.
|
|
44
|
-
*
|
|
45
|
-
* THE POINTER POSITION LIVES HERE, not in the solver. A humanizer that
|
|
46
|
-
* dispatches no motion at all (mobile, between taps) still has to answer "where
|
|
47
|
-
* is the pointer", because that is what the next gesture starts from and what
|
|
48
|
-
* `SolveResult.finalMousePosition` reports.
|
|
8
|
+
* Pluggable because camoufox's juggler re-humanises every mouse.move() it is handed, and running
|
|
9
|
+
* both measured 82s against 13s on one GeeTest slider; and a touch widget needs touch events, not
|
|
10
|
+
* mousemove. See TRIBAL_KNOWLEDGE.md. The 2026-09 cut removed our drag-overshoot redraw and swipe
|
|
11
|
+
* wobble in favour of Cursory's raw recordings; the one-pixel tap wobble stayed because a
|
|
12
|
+
* motionless tap is a synthetic one.
|
|
49
13
|
*/
|
|
50
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
15
|
exports.MODES = exports.NullHumanizer = exports.MobileHumanizer = exports.TouchscreenTouchBackend = exports.AppiumTouchBackend = exports.CdpTouchBackend = exports.MouseHumanizer = exports.BaseHumanizer = exports.PAUSE_KINDS = void 0;
|
|
16
|
+
exports.trajectory = trajectory;
|
|
52
17
|
exports.touchBackendFor = touchBackendFor;
|
|
53
18
|
exports.resolveHumanizer = resolveHumanizer;
|
|
54
|
-
const
|
|
19
|
+
const cursory_js_1 = require("cursory-js");
|
|
20
|
+
const kinds_js_1 = require("./kinds.js");
|
|
55
21
|
const delay = (ms) => new Promise((r) => setTimeout(r, Math.max(0, ms)));
|
|
56
22
|
const log = (m) => console.log(`[captchakraken] ${m}`);
|
|
57
23
|
const uniform = (lo, hi) => lo + Math.random() * (hi - lo);
|
|
58
|
-
|
|
59
|
-
* A move to where the pointer already is emits nothing.
|
|
60
|
-
*
|
|
61
|
-
* `moveAndClick` travels to the element and THEN clicks at the point it landed
|
|
62
|
-
* on, so without this every click would dispatch one redundant move at the
|
|
63
|
-
* coordinate the trajectory just finished on.
|
|
64
|
-
*/
|
|
24
|
+
// moveAndClick clicks at the landing point; without this every click would dispatch one redundant move.
|
|
65
25
|
const samePoint = (a, b) => Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
|
|
66
|
-
|
|
67
|
-
function gauss(mu, sigma) {
|
|
26
|
+
const gauss = (sigma) => {
|
|
68
27
|
let u = 0;
|
|
69
28
|
while (u === 0)
|
|
70
29
|
u = Math.random();
|
|
71
|
-
|
|
72
|
-
|
|
30
|
+
return sigma * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * Math.random());
|
|
31
|
+
};
|
|
32
|
+
const closedError = (e) => /Target closed|Session closed/.test(e instanceof Error ? e.message : String(e));
|
|
33
|
+
/** A recorded human movement morphed onto the endpoints; timings are cumulative ms. */
|
|
34
|
+
function trajectory(start, end, frequency) {
|
|
35
|
+
const { points, timings } = (0, cursory_js_1.generateTrajectory)(start, end, { frequency });
|
|
36
|
+
return [points.map((q) => [Number(q[0]), Number(q[1])]), timings.map(Number)];
|
|
73
37
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'between',
|
|
77
|
-
'grab',
|
|
78
|
-
'drop',
|
|
79
|
-
'probe',
|
|
80
|
-
'settle',
|
|
81
|
-
'key',
|
|
82
|
-
];
|
|
38
|
+
/** Every inter-gesture wait the driver takes, named, so each device supplies its own table. */
|
|
39
|
+
exports.PAUSE_KINDS = Object.values(kinds_js_1.PauseKind);
|
|
83
40
|
class BaseHumanizer {
|
|
84
41
|
constructor(start = [0, 0]) {
|
|
85
42
|
this.at = [Number(start[0]), Number(start[1])];
|
|
86
43
|
}
|
|
87
44
|
async reset(_page) { }
|
|
45
|
+
/** An unknown kind waits nothing, so adding a pause site cannot break a custom humanizer written against an older release. */
|
|
88
46
|
pauseMs(kind) {
|
|
89
47
|
const range = this.pauses[kind];
|
|
90
48
|
return range ? uniform(range[0], range[1]) : 0;
|
|
@@ -95,68 +53,86 @@ class BaseHumanizer {
|
|
|
95
53
|
async click(page, to) {
|
|
96
54
|
await this.move(page, to);
|
|
97
55
|
await this.press(page);
|
|
98
|
-
await this.pause(
|
|
56
|
+
await this.pause(kinds_js_1.PauseKind.TAP);
|
|
99
57
|
await this.release(page);
|
|
100
58
|
}
|
|
101
59
|
async drag(page, src, dst) {
|
|
102
60
|
await this.move(page, src);
|
|
103
61
|
await this.press(page);
|
|
104
|
-
await this.pause(
|
|
62
|
+
await this.pause(kinds_js_1.PauseKind.GRAB);
|
|
105
63
|
await this.move(page, dst);
|
|
106
|
-
await this.pause(
|
|
64
|
+
await this.pause(kinds_js_1.PauseKind.DROP);
|
|
107
65
|
await this.release(page);
|
|
108
66
|
}
|
|
109
67
|
}
|
|
110
68
|
exports.BaseHumanizer = BaseHumanizer;
|
|
111
|
-
// ───────────────────────────────────────────────────────────────────── mouse
|
|
112
|
-
/**
|
|
113
|
-
* A hand on a mouse. Bezier arcs, Fitts's-law durations, overshoot.
|
|
114
|
-
*
|
|
115
|
-
* Everything in here was `solver.performSmoothMove` / `tracePath` before this
|
|
116
|
-
* module existed, and is unchanged: the constants were measured and a refactor
|
|
117
|
-
* is not the place to revisit them.
|
|
118
|
-
*/
|
|
119
69
|
class MouseHumanizer extends BaseHumanizer {
|
|
120
70
|
constructor(start = [0, 0], frequency = 60) {
|
|
121
71
|
super(start);
|
|
122
|
-
this.name =
|
|
72
|
+
this.name = kinds_js_1.HumanizationMode.MOUSE;
|
|
123
73
|
this.hovers = true;
|
|
124
|
-
/** Inclusive ranges, in ms. See PauseKind. */
|
|
125
74
|
this.pauses = {
|
|
126
|
-
tap: [20, 50],
|
|
127
|
-
|
|
128
|
-
grab: [50, 100],
|
|
129
|
-
drop: [50, 100],
|
|
130
|
-
probe: [40, 80],
|
|
131
|
-
settle: [90, 210],
|
|
132
|
-
key: [45, 135],
|
|
75
|
+
tap: [20, 50], between: [80, 160], grab: [50, 100], drop: [50, 100],
|
|
76
|
+
probe: [40, 80], settle: [90, 210], key: [45, 135],
|
|
133
77
|
};
|
|
134
78
|
this.frequency = frequency;
|
|
135
79
|
}
|
|
80
|
+
async reset(_page) { this.viewportCache = undefined; }
|
|
81
|
+
/** The window to keep the cursor inside; null under camoufox, which reports no viewport and whose window we ask instead. */
|
|
82
|
+
async viewport(page) {
|
|
83
|
+
if (this.viewportCache !== undefined)
|
|
84
|
+
return this.viewportCache;
|
|
85
|
+
this.viewportCache = null;
|
|
86
|
+
const reads = [
|
|
87
|
+
() => page.viewportSize(),
|
|
88
|
+
() => page.evaluate ? page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight })) : null,
|
|
89
|
+
];
|
|
90
|
+
for (const read of reads) {
|
|
91
|
+
try {
|
|
92
|
+
const vp = await read();
|
|
93
|
+
if (vp && vp.width && vp.height) {
|
|
94
|
+
this.viewportCache = { width: vp.width, height: vp.height };
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch { /* try the next read */ }
|
|
99
|
+
}
|
|
100
|
+
return this.viewportCache;
|
|
101
|
+
}
|
|
136
102
|
async move(page, to) {
|
|
137
103
|
if (samePoint(this.at, to))
|
|
138
104
|
return;
|
|
139
|
-
const [points, timings] = (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
105
|
+
const [points, timings] = trajectory(this.at, to, this.frequency);
|
|
106
|
+
// Clamp only when the viewport is known: a coordinate pinned to the edge of a guessed one deadlocks camoufox's juggler (upstream #225).
|
|
107
|
+
const viewport = await this.viewport(page);
|
|
108
|
+
const startTime = Date.now();
|
|
109
|
+
for (let i = 0; i < points.length; i++) {
|
|
110
|
+
const cx = viewport ? Math.max(1, Math.min(points[i][0], viewport.width - 1)) : points[i][0];
|
|
111
|
+
const cy = viewport ? Math.max(1, Math.min(points[i][1], viewport.height - 1)) : points[i][1];
|
|
112
|
+
try {
|
|
113
|
+
await page.mouse.move(cx, cy);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
if (closedError(e)) {
|
|
117
|
+
log('could not move mouse; page or session closed');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
this.at = [cx, cy];
|
|
123
|
+
if (timings[i] !== undefined)
|
|
124
|
+
await delay(startTime + timings[i] - Date.now());
|
|
125
|
+
}
|
|
147
126
|
}
|
|
127
|
+
async press(page) { await page.mouse.down(); }
|
|
128
|
+
async release(page) { await page.mouse.up(); }
|
|
129
|
+
/** Per character with a humanised pause, not type(text, {delay}): a constant inter-key delay is itself a signal these vendors score. */
|
|
148
130
|
async typeText(page, _field, text) {
|
|
149
|
-
//
|
|
150
|
-
// typing would APPEND to it — submitting a string the model never read.
|
|
131
|
+
// Clear first: a retry round arrives with the previous attempt still in the box, and typing would append.
|
|
151
132
|
try {
|
|
152
133
|
await page.keyboard.press('Control+A');
|
|
153
134
|
}
|
|
154
|
-
catch {
|
|
155
|
-
/* not every field supports it; the type below still replaces on most */
|
|
156
|
-
}
|
|
157
|
-
// Per character rather than one `type(text, {delay})` call: a constant
|
|
158
|
-
// inter-key delay is itself a signal, and these are the vendors that score
|
|
159
|
-
// typing cadence.
|
|
135
|
+
catch { /* the type below still replaces on most */ }
|
|
160
136
|
for (const ch of text) {
|
|
161
137
|
try {
|
|
162
138
|
await page.keyboard.type(ch);
|
|
@@ -165,76 +141,26 @@ class MouseHumanizer extends BaseHumanizer {
|
|
|
165
141
|
log(`could not type into the captcha field: ${e}`);
|
|
166
142
|
return false;
|
|
167
143
|
}
|
|
168
|
-
await this.pause(
|
|
144
|
+
await this.pause(kinds_js_1.PauseKind.KEY);
|
|
169
145
|
}
|
|
170
146
|
return true;
|
|
171
147
|
}
|
|
172
|
-
async trace(page, points, timings) {
|
|
173
|
-
// KNOWN DIVERGENCE from the Python port, carried over unchanged rather than
|
|
174
|
-
// fixed inside a refactor. Python clamps only when the viewport is actually
|
|
175
|
-
// known, because camoufox reports `viewportSize` null and a coordinate
|
|
176
|
-
// pinned to the edge of a GUESSED viewport is what deadlocks its humanised-
|
|
177
|
-
// mouse juggler (upstream #225) — see MouseHumanizer._viewport there. This
|
|
178
|
-
// port still assumes 1920x1080. Worth fixing on its own, with its own test.
|
|
179
|
-
let viewport = { width: 1920, height: 1080 };
|
|
180
|
-
try {
|
|
181
|
-
const vp = page.viewportSize();
|
|
182
|
-
if (vp)
|
|
183
|
-
viewport = vp;
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
/* keep the default */
|
|
187
|
-
}
|
|
188
|
-
const startTime = Date.now();
|
|
189
|
-
for (let i = 0; i < points.length; i++) {
|
|
190
|
-
try {
|
|
191
|
-
const cx = Math.max(0, Math.min(points[i][0], viewport.width));
|
|
192
|
-
const cy = Math.max(0, Math.min(points[i][1], viewport.height));
|
|
193
|
-
await page.mouse.move(cx, cy);
|
|
194
|
-
this.at = [cx, cy];
|
|
195
|
-
if (timings[i] !== undefined) {
|
|
196
|
-
const wait = startTime + timings[i] - Date.now();
|
|
197
|
-
if (wait > 0)
|
|
198
|
-
await delay(wait);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
202
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
203
|
-
if (msg.includes('Target closed') || msg.includes('Session closed')) {
|
|
204
|
-
log('Warning: could not move mouse, page or session closed.');
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
// Any other per-sample failure is skipped rather than fatal — losing
|
|
208
|
-
// one mousemove must not lose the solve.
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
148
|
}
|
|
213
149
|
exports.MouseHumanizer = MouseHumanizer;
|
|
214
|
-
/**
|
|
215
|
-
* `Input.dispatchTouchEvent` over a CDP session on a Playwright page.
|
|
216
|
-
*
|
|
217
|
-
* Chromium-family only, and that is checked at construction rather than
|
|
218
|
-
* discovered per gesture: WebKit and Firefox (camoufox included) expose no touch
|
|
219
|
-
* dispatch through Playwright at all, so the alternatives there are to fail
|
|
220
|
-
* loudly or to quietly emit MOUSE events at a touch-only widget. The second is
|
|
221
|
-
* worse than not running — the page's touch handlers never fire, the solve fails
|
|
222
|
-
* for a reason nothing reports, and the report reads as a model that cannot
|
|
223
|
-
* solve mobile puzzles.
|
|
224
|
-
*
|
|
225
|
-
* The context must also have been created with `hasTouch: true`, or the page
|
|
226
|
-
* advertises no touch support to feature detection and a mobile widget renders
|
|
227
|
-
* its desktop branch.
|
|
228
|
-
*/
|
|
150
|
+
/** `Input.dispatchTouchEvent` over CDP; Chromium-family pages launched with `hasTouch: true`. */
|
|
229
151
|
class CdpTouchBackend {
|
|
230
152
|
constructor(session) {
|
|
231
|
-
this.name = 'cdp';
|
|
232
153
|
this.session = session;
|
|
154
|
+
this.name = 'cdp';
|
|
233
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Checked at construction, not per gesture. WebKit and Firefox expose no touch dispatch, and emitting
|
|
158
|
+
* mouse events instead is worse than not running: the page's touch handlers never fire and the report
|
|
159
|
+
* reads as a model that cannot solve mobile puzzles.
|
|
160
|
+
*/
|
|
234
161
|
static async open(page) {
|
|
235
162
|
try {
|
|
236
|
-
|
|
237
|
-
return new CdpTouchBackend(await context.newCDPSession(page));
|
|
163
|
+
return new CdpTouchBackend(await page.context().newCDPSession(page));
|
|
238
164
|
}
|
|
239
165
|
catch (e) {
|
|
240
166
|
throw new Error(`mobile humanisation needs touch dispatch, and this page offers none (${e}). ` +
|
|
@@ -243,24 +169,15 @@ class CdpTouchBackend {
|
|
|
243
169
|
}
|
|
244
170
|
}
|
|
245
171
|
async send(kind, points) {
|
|
172
|
+
// radius and force are what a real digitizer reports; a synthetic tap reports zero.
|
|
246
173
|
await this.session.send('Input.dispatchTouchEvent', {
|
|
247
174
|
type: kind,
|
|
248
175
|
touchPoints: points.map(([x, y]) => ({
|
|
249
|
-
|
|
250
|
-
// tap does not. A vendor reading `Touch.radiusX === 0` has a free bot
|
|
251
|
-
// signal otherwise.
|
|
252
|
-
x,
|
|
253
|
-
y,
|
|
254
|
-
radiusX: uniform(8, 14),
|
|
255
|
-
radiusY: uniform(8, 14),
|
|
256
|
-
force: uniform(0.35, 0.75),
|
|
257
|
-
id: 1,
|
|
176
|
+
x, y, radiusX: uniform(8, 14), radiusY: uniform(8, 14), force: uniform(0.35, 0.75), id: 1,
|
|
258
177
|
})),
|
|
259
178
|
});
|
|
260
179
|
}
|
|
261
|
-
async down(x, y) {
|
|
262
|
-
await this.send('touchStart', [[x, y]]);
|
|
263
|
-
}
|
|
180
|
+
async down(x, y) { await this.send('touchStart', [[x, y]]); }
|
|
264
181
|
async move(path) {
|
|
265
182
|
for (const [x, y, dt] of path) {
|
|
266
183
|
if (dt > 0)
|
|
@@ -268,61 +185,28 @@ class CdpTouchBackend {
|
|
|
268
185
|
await this.send('touchMove', [[x, y]]);
|
|
269
186
|
}
|
|
270
187
|
}
|
|
271
|
-
async up(_x, _y) {
|
|
272
|
-
// touchEnd carries no touchPoints: the point being lifted is identified by
|
|
273
|
-
// its absence, which is what the protocol says and what a real release
|
|
274
|
-
// looks like.
|
|
275
|
-
await this.send('touchEnd', []);
|
|
276
|
-
}
|
|
188
|
+
async up(_x, _y) { await this.send('touchEnd', []); }
|
|
277
189
|
}
|
|
278
190
|
exports.CdpTouchBackend = CdpTouchBackend;
|
|
279
191
|
/**
|
|
280
|
-
* W3C pointer actions
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* kinematics are reproduced BY THE DEVICE rather than by a JS loop round-tripping
|
|
284
|
-
* over the wire — over which a 90Hz path is not 90Hz at all.
|
|
285
|
-
*
|
|
286
|
-
* Press and release are separate `performActions()` calls on purpose. W3C input
|
|
287
|
-
* state is per SESSION, so a pointer put down in one chain stays down across
|
|
288
|
-
* later chains until it is lifted; that is what lets the puzzle-slider driver
|
|
289
|
-
* press, screenshot, steer, screenshot and only then release. (It is also why
|
|
290
|
-
* the spec has a "release actions" endpoint at all.)
|
|
291
|
-
*
|
|
292
|
-
* COORDINATES. Everything upstream of here is CSS pixels in the page's viewport,
|
|
293
|
-
* because that is what `boundingBox()` returns. A real device wants screen
|
|
294
|
-
* pixels, and the two differ by the device pixel ratio and by whatever chrome
|
|
295
|
-
* sits above the webview. Neither is guessable from here, so both are options:
|
|
296
|
-
*
|
|
297
|
-
* new AppiumTouchBackend(driver, { scale: 3, origin: [0, 132] })
|
|
298
|
-
*
|
|
299
|
-
* `scale` is usually `window.devicePixelRatio`; `origin` is the top-left of the
|
|
300
|
-
* webview in screen coordinates. Defaults are the identity transform, which is
|
|
301
|
-
* correct for browser mobile emulation and for any caller who has already mapped
|
|
302
|
-
* the coordinates themselves.
|
|
192
|
+
* W3C touch pointer actions for Appium and WebdriverIO, paced by the device from one chain per leg.
|
|
193
|
+
* Raw protocol payloads so this imports no client. Press and release are separate performs because W3C
|
|
194
|
+
* input state is per session: that is what lets the slider driver press, screenshot, steer, then release.
|
|
303
195
|
*/
|
|
304
196
|
class AppiumTouchBackend {
|
|
305
197
|
constructor(driver, transform = {}, page) {
|
|
198
|
+
this.driver = driver;
|
|
199
|
+
this.page = page;
|
|
306
200
|
this.name = 'appium';
|
|
307
201
|
this.checked = false;
|
|
308
|
-
this.driver = driver;
|
|
309
202
|
this.scaleGiven = transform.scale !== undefined;
|
|
310
203
|
this.scale = transform.scale ?? 1;
|
|
311
204
|
this.origin = transform.origin ?? [0, 0];
|
|
312
|
-
this.page = page;
|
|
313
205
|
}
|
|
314
206
|
/**
|
|
315
|
-
* Refuse an unset scale on a
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* valid W3C, the device performs it, and the finger lands somewhere else. The
|
|
319
|
-
* solve then fails looking exactly like a model that cannot read the puzzle.
|
|
320
|
-
*
|
|
321
|
-
* Read once, not per gesture — it is a round trip into the page and a solve
|
|
322
|
-
* makes hundreds of these. An unreadable ratio is absent evidence, not
|
|
323
|
-
* evidence of a mismatch, so it falls through to the identity. `origin`
|
|
324
|
-
* cannot be measured from here at all, so it is named in the refusal rather
|
|
325
|
-
* than guessed at.
|
|
207
|
+
* Refuse an unset scale on a device whose pixel ratio is not 1: the finger would land elsewhere. An
|
|
208
|
+
* explicit scale of 1 is the caller's word and is not re-checked; the ratio is read once, since a solve
|
|
209
|
+
* makes hundreds of these, and an unreadable ratio is taken as 1.
|
|
326
210
|
*/
|
|
327
211
|
async checkScale() {
|
|
328
212
|
this.checked = true;
|
|
@@ -333,51 +217,23 @@ class AppiumTouchBackend {
|
|
|
333
217
|
dpr = Number(await this.page.evaluate('() => window.devicePixelRatio'));
|
|
334
218
|
}
|
|
335
219
|
catch {
|
|
336
|
-
return;
|
|
220
|
+
return;
|
|
337
221
|
}
|
|
338
222
|
if (!Number.isFinite(dpr) || Math.abs(dpr - 1) < 1e-6)
|
|
339
223
|
return;
|
|
340
|
-
throw new Error(`the touch driver maps CSS pixels onto a device reporting devicePixelRatio ` +
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
`the chain is valid, the device performs it, and the solve fails looking ` +
|
|
344
|
-
`like a weak model.\n` +
|
|
345
|
-
` Fix: touchTransform: { scale: ${dpr}, origin: [x, y] }, where origin is ` +
|
|
346
|
-
`the webview's top-left in SCREEN coordinates. That half cannot be measured ` +
|
|
347
|
-
`from here.\n` +
|
|
348
|
-
` Or pass scale: 1 to assert the coordinates are already mapped.`);
|
|
224
|
+
throw new Error(`the touch driver maps CSS pixels onto a device reporting devicePixelRatio ${dpr}, and no scale ` +
|
|
225
|
+
`was given. Fix: touchTransform: { scale: ${dpr}, origin: [x, y] }, where origin is the webview's ` +
|
|
226
|
+
'top-left in SCREEN coordinates, or pass scale: 1 to assert the coordinates are already mapped.');
|
|
349
227
|
}
|
|
350
228
|
map(x, y) {
|
|
351
|
-
return [
|
|
352
|
-
Math.round(this.origin[0] + x * this.scale),
|
|
353
|
-
Math.round(this.origin[1] + y * this.scale),
|
|
354
|
-
];
|
|
229
|
+
return [Math.round(this.origin[0] + x * this.scale), Math.round(this.origin[1] + y * this.scale)];
|
|
355
230
|
}
|
|
356
|
-
/**
|
|
357
|
-
* One W3C action chain on a single touch pointer.
|
|
358
|
-
*
|
|
359
|
-
* Sent as the raw protocol payload rather than through a client's action
|
|
360
|
-
* builder, so this imports nothing and works against any client that speaks
|
|
361
|
-
* WebDriver — WebdriverIO (`performActions`), selenium-webdriver, or a thin
|
|
362
|
-
* HTTP wrapper of someone's own.
|
|
363
|
-
*/
|
|
364
231
|
async perform(actions) {
|
|
365
|
-
const chain = [
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
actions,
|
|
371
|
-
},
|
|
372
|
-
];
|
|
373
|
-
if (typeof this.driver.performActions === 'function') {
|
|
374
|
-
await this.driver.performActions(chain);
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
if (typeof this.driver.execute === 'function') {
|
|
378
|
-
await this.driver.execute('actions', { actions: chain });
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
232
|
+
const chain = [{ type: 'pointer', id: 'ck-finger', parameters: { pointerType: 'touch' }, actions }];
|
|
233
|
+
if (typeof this.driver.performActions === 'function')
|
|
234
|
+
return this.driver.performActions(chain);
|
|
235
|
+
if (typeof this.driver.execute === 'function')
|
|
236
|
+
return this.driver.execute('actions', { actions: chain });
|
|
381
237
|
throw new Error("the touch driver speaks neither performActions() nor execute('actions', …); " +
|
|
382
238
|
'pass a WebDriver-compatible driver or a custom TouchBackend.');
|
|
383
239
|
}
|
|
@@ -390,6 +246,7 @@ class AppiumTouchBackend {
|
|
|
390
246
|
{ type: 'pointerDown', button: 0 },
|
|
391
247
|
]);
|
|
392
248
|
}
|
|
249
|
+
/** The per-sample gap becomes the move's `duration`, so the device interpolates the leg itself. */
|
|
393
250
|
async move(path) {
|
|
394
251
|
if (!this.checked)
|
|
395
252
|
await this.checkScale();
|
|
@@ -397,10 +254,6 @@ class AppiumTouchBackend {
|
|
|
397
254
|
return;
|
|
398
255
|
await this.perform(path.map(([x, y, dt]) => {
|
|
399
256
|
const [mx, my] = this.map(x, y);
|
|
400
|
-
// The per-sample gap becomes the move's DURATION, not a pause before it:
|
|
401
|
-
// the device then interpolates over that window and reports intermediate
|
|
402
|
-
// samples of its own, which is what a finger sliding across a digitizer
|
|
403
|
-
// actually produces.
|
|
404
257
|
return { type: 'pointerMove', duration: Math.max(0, Math.round(dt)), origin: 'viewport', x: mx, y: my };
|
|
405
258
|
}));
|
|
406
259
|
}
|
|
@@ -411,26 +264,17 @@ class AppiumTouchBackend {
|
|
|
411
264
|
}
|
|
412
265
|
}
|
|
413
266
|
exports.AppiumTouchBackend = AppiumTouchBackend;
|
|
414
|
-
/**
|
|
415
|
-
* Playwright's `page.touchscreen.tap()` — taps only, no travel.
|
|
416
|
-
*
|
|
417
|
-
* The fallback for a browser with touch support but no CDP (WebKit). A tap still
|
|
418
|
-
* lands correctly; a DRAG cannot be expressed at all, so it throws rather than
|
|
419
|
-
* approximating one with a mouse, for the reason in `CdpTouchBackend`.
|
|
420
|
-
*/
|
|
267
|
+
/** Playwright's tap-only touchscreen, for browsers with touch but no CDP. A drag cannot be expressed. */
|
|
421
268
|
class TouchscreenTouchBackend {
|
|
422
269
|
constructor(page) {
|
|
270
|
+
this.page = page;
|
|
423
271
|
this.name = 'touchscreen';
|
|
424
272
|
this.pending = null;
|
|
425
|
-
this.page = page;
|
|
426
|
-
}
|
|
427
|
-
async down(x, y) {
|
|
428
|
-
this.pending = [x, y];
|
|
429
273
|
}
|
|
274
|
+
async down(x, y) { this.pending = [x, y]; }
|
|
430
275
|
async move(_path) {
|
|
431
|
-
throw new Error('this browser exposes taps but not touch travel, so a drag/slide puzzle ' +
|
|
432
|
-
'
|
|
433
|
-
'or an Appium driver.');
|
|
276
|
+
throw new Error('this browser exposes taps but not touch travel, so a drag/slide puzzle cannot be ' +
|
|
277
|
+
'driven on it. Use a Chromium-family browser (CDP touch dispatch) or an Appium driver.');
|
|
434
278
|
}
|
|
435
279
|
async up(x, y) {
|
|
436
280
|
const at = this.pending ?? [x, y];
|
|
@@ -439,19 +283,10 @@ class TouchscreenTouchBackend {
|
|
|
439
283
|
}
|
|
440
284
|
}
|
|
441
285
|
exports.TouchscreenTouchBackend = TouchscreenTouchBackend;
|
|
442
|
-
/**
|
|
443
|
-
* Pick a backend. An explicit `driver` always wins.
|
|
444
|
-
*
|
|
445
|
-
* `driver` is what a caller passes when the thing under automation is not the
|
|
446
|
-
* thing being touched — an Appium session driving a real handset while the page
|
|
447
|
-
* object is a webview bridge over it.
|
|
448
|
-
*/
|
|
449
286
|
async function touchBackendFor(page, driver, transform) {
|
|
450
287
|
if (driver) {
|
|
451
|
-
if (typeof driver.down === 'function' && typeof driver.up === 'function')
|
|
288
|
+
if (typeof driver.down === 'function' && typeof driver.up === 'function')
|
|
452
289
|
return driver;
|
|
453
|
-
}
|
|
454
|
-
// The page comes along so the backend can ask what it is mapping ONTO.
|
|
455
290
|
return new AppiumTouchBackend(driver, transform, page);
|
|
456
291
|
}
|
|
457
292
|
try {
|
|
@@ -465,43 +300,16 @@ async function touchBackendFor(page, driver, transform) {
|
|
|
465
300
|
throw e;
|
|
466
301
|
}
|
|
467
302
|
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* A finger on glass.
|
|
471
|
-
*
|
|
472
|
-
* What differs from `MouseHumanizer`, and none of it is cosmetic:
|
|
473
|
-
*
|
|
474
|
-
* - **A move that is not touching dispatches nothing.** There is no hover on a
|
|
475
|
-
* touchscreen. The position is still RECORDED, because the next gesture
|
|
476
|
-
* starts from it, but no event is emitted — emitting one would be the
|
|
477
|
-
* desktop tell this mode exists to remove.
|
|
478
|
-
* - **Taps carry a contact wobble.** A finger held on glass for 90ms does not
|
|
479
|
-
* report one unchanging coordinate; the centroid of the contact patch rolls
|
|
480
|
-
* a pixel or two under pressure. A tap with zero movement between touchstart
|
|
481
|
-
* and touchend is a synthetic tap.
|
|
482
|
-
* - **Touch kinematics**, via `generate_swipe` — see its docstring for why that
|
|
483
|
-
* is a different model rather than the mouse one retuned.
|
|
484
|
-
* - **Longer, more variable pauses.** Every measured touch interaction is
|
|
485
|
-
* slower than its mouse equivalent, and a phone's are more variable because
|
|
486
|
-
* the hand holding the device is also moving.
|
|
487
|
-
*/
|
|
303
|
+
/** A finger on glass: no hover, touch events only, slower and more variable pauses. */
|
|
488
304
|
class MobileHumanizer extends BaseHumanizer {
|
|
489
305
|
constructor(start = [0, 0], options = {}) {
|
|
490
306
|
super(start);
|
|
491
|
-
this.name =
|
|
307
|
+
this.name = kinds_js_1.HumanizationMode.MOBILE;
|
|
492
308
|
this.hovers = false;
|
|
309
|
+
// tap: measured human touch dwell clusters at 60-120ms. key: a soft keyboard is ~3x slower than a physical one.
|
|
493
310
|
this.pauses = {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
tap: [55, 130],
|
|
497
|
-
between: [140, 320],
|
|
498
|
-
grab: [90, 190],
|
|
499
|
-
drop: [80, 170],
|
|
500
|
-
probe: [70, 140],
|
|
501
|
-
settle: [140, 300],
|
|
502
|
-
// Soft-keyboard typing, which is ~3x slower than a physical keyboard and
|
|
503
|
-
// much more variable.
|
|
504
|
-
key: [110, 320],
|
|
311
|
+
tap: [55, 130], between: [140, 320], grab: [90, 190], drop: [80, 170],
|
|
312
|
+
probe: [70, 140], settle: [140, 300], key: [110, 320],
|
|
505
313
|
};
|
|
506
314
|
this.down = false;
|
|
507
315
|
this.backend = options.backend ?? null;
|
|
@@ -509,40 +317,27 @@ class MobileHumanizer extends BaseHumanizer {
|
|
|
509
317
|
this.transform = options.transform;
|
|
510
318
|
this.frequency = options.frequency ?? 90;
|
|
511
319
|
}
|
|
320
|
+
/** A solve that ended mid-gesture leaves a pointer down in the session's input state; lift it before the next one. */
|
|
512
321
|
async reset(page) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
await (await this.touch(page)).up(this.at[0], this.at[1]);
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
/* best effort; a stale pointer must not fail a solve */
|
|
522
|
-
}
|
|
523
|
-
this.down = false;
|
|
322
|
+
if (!this.down)
|
|
323
|
+
return;
|
|
324
|
+
try {
|
|
325
|
+
await (await this.touch(page)).up(this.at[0], this.at[1]);
|
|
524
326
|
}
|
|
327
|
+
catch { /* a stale pointer must not fail a solve */ }
|
|
328
|
+
this.down = false;
|
|
525
329
|
}
|
|
526
330
|
async touch(page) {
|
|
527
|
-
if (!this.backend)
|
|
331
|
+
if (!this.backend)
|
|
528
332
|
this.backend = await touchBackendFor(page, this.driver, this.transform);
|
|
529
|
-
}
|
|
530
333
|
return this.backend;
|
|
531
334
|
}
|
|
532
335
|
async move(page, to) {
|
|
533
336
|
const dest = [Number(to[0]), Number(to[1])];
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
this.
|
|
537
|
-
return;
|
|
337
|
+
if (this.down) {
|
|
338
|
+
const [points, timings] = trajectory(this.at, dest, this.frequency);
|
|
339
|
+
await (await this.touch(page)).move(points.map(([x, y], i) => [x, y, timings[i] - (i ? timings[i - 1] : 0)]));
|
|
538
340
|
}
|
|
539
|
-
const [points, timings] = (0, trajectory_js_1.generate_swipe)(this.at, dest, this.frequency);
|
|
540
|
-
const path = points.map(([x, y], i) => [
|
|
541
|
-
x,
|
|
542
|
-
y,
|
|
543
|
-
timings[i] - (i ? timings[i - 1] : 0),
|
|
544
|
-
]);
|
|
545
|
-
await (await this.touch(page)).move(path);
|
|
546
341
|
this.at = dest;
|
|
547
342
|
}
|
|
548
343
|
async press(page) {
|
|
@@ -553,30 +348,20 @@ class MobileHumanizer extends BaseHumanizer {
|
|
|
553
348
|
await (await this.touch(page)).up(this.at[0], this.at[1]);
|
|
554
349
|
this.down = false;
|
|
555
350
|
}
|
|
556
|
-
/**
|
|
351
|
+
/** A tap whose contact patch wobbles a pixel while held; a motionless tap is a synthetic one. */
|
|
557
352
|
async click(page, to) {
|
|
558
353
|
await this.move(page, to);
|
|
559
354
|
await this.press(page);
|
|
560
|
-
const held = this.pauseMs(
|
|
355
|
+
const held = this.pauseMs(kinds_js_1.PauseKind.TAP);
|
|
561
356
|
await delay(held / 2);
|
|
562
357
|
try {
|
|
563
|
-
await (await this.touch(page)).move([
|
|
564
|
-
[this.at[0] + gauss(0, 0.9), this.at[1] + gauss(0, 0.9), 0],
|
|
565
|
-
]);
|
|
566
|
-
}
|
|
567
|
-
catch {
|
|
568
|
-
/* a tap-only backend cannot wobble; the tap still lands */
|
|
358
|
+
await (await this.touch(page)).move([[this.at[0] + gauss(0.9), this.at[1] + gauss(0.9), 0]]);
|
|
569
359
|
}
|
|
360
|
+
catch { /* a tap-only backend cannot wobble; the tap still lands */ }
|
|
570
361
|
await delay(held / 2);
|
|
571
362
|
await this.release(page);
|
|
572
363
|
}
|
|
573
|
-
/**
|
|
574
|
-
* Into the field, at soft-keyboard pace.
|
|
575
|
-
*
|
|
576
|
-
* The mouse path clears with Control+A. There is no Control on a phone
|
|
577
|
-
* keyboard, so the box is cleared through the element instead — which also
|
|
578
|
-
* works on an Appium element, where `page.keyboard` does not exist.
|
|
579
|
-
*/
|
|
364
|
+
/** Clears through the element: there is no Control key on a phone and no `page.keyboard` on Appium. */
|
|
580
365
|
async typeText(page, field, text) {
|
|
581
366
|
const f = field;
|
|
582
367
|
for (const [name, arg] of [['clear', undefined], ['fill', '']]) {
|
|
@@ -586,45 +371,33 @@ class MobileHumanizer extends BaseHumanizer {
|
|
|
586
371
|
await f[name](arg);
|
|
587
372
|
break;
|
|
588
373
|
}
|
|
589
|
-
catch {
|
|
590
|
-
/* an uncleared box is recoverable; a crash is not */
|
|
591
|
-
}
|
|
374
|
+
catch { /* an uncleared box is recoverable */ }
|
|
592
375
|
}
|
|
593
376
|
for (const ch of text) {
|
|
594
377
|
try {
|
|
595
378
|
if (typeof f?.sendKeys === 'function')
|
|
596
|
-
await f.sendKeys(ch);
|
|
379
|
+
await f.sendKeys(ch);
|
|
597
380
|
else
|
|
598
|
-
await page.keyboard.type(ch);
|
|
381
|
+
await page.keyboard.type(ch);
|
|
599
382
|
}
|
|
600
383
|
catch (e) {
|
|
601
384
|
log(`could not type into the captcha field: ${e}`);
|
|
602
385
|
return false;
|
|
603
386
|
}
|
|
604
|
-
await this.pause(
|
|
387
|
+
await this.pause(kinds_js_1.PauseKind.KEY);
|
|
605
388
|
}
|
|
606
389
|
return true;
|
|
607
390
|
}
|
|
608
391
|
}
|
|
609
392
|
exports.MobileHumanizer = MobileHumanizer;
|
|
610
|
-
// ────────────────────────────────────────────────────────────────────── none
|
|
611
393
|
/**
|
|
612
|
-
* No humanisation:
|
|
613
|
-
*
|
|
614
|
-
* One mousemove per gesture instead of sixty, no dwell, no jitter, and text goes
|
|
615
|
-
* in with a single `fill()`. Roughly an order of magnitude faster on a slider
|
|
616
|
-
* puzzle, and it will be detected by anything that scores pointer telemetry —
|
|
617
|
-
* which is the whole trade, stated plainly. For fixtures, for self-hosted
|
|
618
|
-
* targets, and for callers whose stack humanises somewhere else.
|
|
619
|
-
*
|
|
620
|
-
* Still moves the mouse and still presses and releases: the events are what make
|
|
621
|
-
* the widget respond at all, and a click dispatched with no preceding move fails
|
|
622
|
-
* on the vendors that require a hover state first.
|
|
394
|
+
* No humanisation: one move per gesture and a single fill. Fast, and detectable. It still moves before
|
|
395
|
+
* it presses, because a click with no preceding move fails on the vendors that require a hover state first.
|
|
623
396
|
*/
|
|
624
397
|
class NullHumanizer extends BaseHumanizer {
|
|
625
398
|
constructor() {
|
|
626
399
|
super(...arguments);
|
|
627
|
-
this.name =
|
|
400
|
+
this.name = kinds_js_1.HumanizationMode.NONE;
|
|
628
401
|
this.hovers = false;
|
|
629
402
|
this.pauses = {};
|
|
630
403
|
}
|
|
@@ -636,32 +409,22 @@ class NullHumanizer extends BaseHumanizer {
|
|
|
636
409
|
await page.mouse.move(this.at[0], this.at[1]);
|
|
637
410
|
}
|
|
638
411
|
catch (e) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
log('Warning: could not move mouse, page or session closed.');
|
|
642
|
-
}
|
|
412
|
+
if (closedError(e))
|
|
413
|
+
log('could not move mouse; page or session closed');
|
|
643
414
|
}
|
|
644
415
|
}
|
|
645
|
-
async press(page) {
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
async release(page) {
|
|
649
|
-
await page.mouse.up();
|
|
650
|
-
}
|
|
416
|
+
async press(page) { await page.mouse.down(); }
|
|
417
|
+
async release(page) { await page.mouse.up(); }
|
|
651
418
|
async typeText(page, field, text) {
|
|
652
419
|
try {
|
|
653
|
-
await field.fill(text);
|
|
420
|
+
await field.fill(text);
|
|
654
421
|
return true;
|
|
655
422
|
}
|
|
656
|
-
catch {
|
|
657
|
-
/* fall through to the keyboard */
|
|
658
|
-
}
|
|
423
|
+
catch { /* fall through to the keyboard */ }
|
|
659
424
|
try {
|
|
660
425
|
await page.keyboard.press('Control+A');
|
|
661
426
|
}
|
|
662
|
-
catch {
|
|
663
|
-
/* optional */
|
|
664
|
-
}
|
|
427
|
+
catch { /* optional */ }
|
|
665
428
|
try {
|
|
666
429
|
await page.keyboard.type(text);
|
|
667
430
|
return true;
|
|
@@ -673,44 +436,25 @@ class NullHumanizer extends BaseHumanizer {
|
|
|
673
436
|
}
|
|
674
437
|
}
|
|
675
438
|
exports.NullHumanizer = NullHumanizer;
|
|
676
|
-
exports.MODES =
|
|
439
|
+
exports.MODES = Object.values(kinds_js_1.HumanizationMode);
|
|
677
440
|
/**
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
* 1. `config.humanizer` — a caller who handed us an OBJECT has already
|
|
683
|
-
* decided; there is nothing left to select.
|
|
684
|
-
* 2. `config.humanization` — an explicit mode set in code.
|
|
685
|
-
* 3. `CAPTCHA_HUMANIZATION` — for a caller who cannot edit the code.
|
|
686
|
-
* 4. 'mouse'.
|
|
687
|
-
*
|
|
688
|
-
* Note that the env var loses to code, which is the opposite of this package's
|
|
689
|
-
* model-identity settings. Deliberate: which mode is right is a property of the
|
|
690
|
-
* PAGE the caller is driving, and an env var flipping a desktop solve to touch
|
|
691
|
-
* dispatch would break every one of them silently. Pinning a model is a
|
|
692
|
-
* deployment decision; picking an input device is not.
|
|
441
|
+
* `config.humanizer`, else `config.humanization`, else CAPTCHA_HUMANIZATION, else mouse. The env var loses
|
|
442
|
+
* to code, the opposite of the model settings, on purpose: an env var flipping a desktop solve to touch
|
|
443
|
+
* dispatch would break every one of them silently.
|
|
693
444
|
*/
|
|
694
445
|
function resolveHumanizer(config = {}) {
|
|
695
446
|
if (config.humanizer)
|
|
696
447
|
return config.humanizer;
|
|
697
|
-
const raw = (config.humanization ?? process.env.CAPTCHA_HUMANIZATION ??
|
|
698
|
-
|
|
699
|
-
.trim()
|
|
700
|
-
.toLowerCase();
|
|
701
|
-
if (!exports.MODES.includes(raw)) {
|
|
448
|
+
const raw = (config.humanization ?? process.env.CAPTCHA_HUMANIZATION ?? kinds_js_1.HumanizationMode.MOUSE).toString().trim().toLowerCase();
|
|
449
|
+
if (!(0, kinds_js_1.isOneOf)(kinds_js_1.HumanizationMode, raw)) {
|
|
702
450
|
throw new Error(`unknown humanization mode '${raw}'; expected one of ${exports.MODES.join(', ')}, ` +
|
|
703
451
|
'or pass your own object as CaptchaKrakenConfig.humanizer');
|
|
704
452
|
}
|
|
705
453
|
const p = config.startingMousePosition;
|
|
706
454
|
const start = p ? [p.x, p.y] : [0, 0];
|
|
707
|
-
if (raw ===
|
|
708
|
-
return new MobileHumanizer(start, {
|
|
709
|
-
|
|
710
|
-
transform: config.touchTransform,
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
if (raw === 'none')
|
|
455
|
+
if (raw === kinds_js_1.HumanizationMode.MOBILE)
|
|
456
|
+
return new MobileHumanizer(start, { driver: config.touchDriver, transform: config.touchTransform });
|
|
457
|
+
if (raw === kinds_js_1.HumanizationMode.NONE)
|
|
714
458
|
return new NullHumanizer(start);
|
|
715
459
|
return new MouseHumanizer(start);
|
|
716
460
|
}
|