captchakraken 2.4.0 → 2.6.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 +47 -7
- package/README.md +24 -4
- package/dist/cli-invocation.d.ts +46 -0
- package/dist/cli-invocation.js +68 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +29 -1
- package/dist/limits.d.ts +51 -0
- package/dist/limits.js +54 -0
- package/dist/model-name.d.ts +23 -0
- package/dist/model-name.js +70 -0
- package/dist/playwright-types.d.ts +28 -0
- package/dist/puppeteer-adapter.d.ts +11 -0
- package/dist/puppeteer-adapter.js +23 -0
- package/dist/python-command.d.ts +50 -0
- package/dist/python-command.js +60 -0
- package/dist/slide-geometry.d.ts +33 -0
- package/dist/slide-geometry.js +64 -0
- package/dist/solver.d.ts +214 -12
- package/dist/solver.js +1285 -165
- package/dist/trajectory.d.ts +31 -0
- package/dist/trajectory.js +162 -0
- package/dist/types.d.ts +128 -11
- package/dist/watcher.d.ts +99 -0
- package/dist/watcher.js +129 -0
- package/package.json +2 -5
- package/python/LICENSE +47 -7
- package/python/README.md +25 -8
- package/python/examples/README.md +36 -0
- package/python/examples/_harness.py +182 -94
- package/python/examples/demoHcaptcha.py +2 -1
- package/python/examples/demoRecaptcha.py +2 -1
- package/python/examples/watch_playwright.py +56 -0
- package/python/examples/with_playwright.py +43 -0
- package/python/pyproject.toml +4 -3
- package/python/src/captchakraken/__init__.py +5 -3
- package/python/src/captchakraken/action_types.py +24 -3
- package/python/src/captchakraken/cli.py +212 -2
- package/python/src/captchakraken/config.py +39 -5
- package/python/src/captchakraken/keyframes.py +690 -0
- package/python/src/captchakraken/models.json +120 -0
- package/python/src/captchakraken/page_solver.py +1465 -102
- package/python/src/captchakraken/pinned_model.json +5 -7
- package/python/src/captchakraken/planner.py +253 -59
- package/python/src/captchakraken/prompts.py +485 -0
- package/python/src/captchakraken/solver.py +182 -11
- package/python/src/captchakraken/timing.py +70 -0
- package/python/src/captchakraken/tool_calls/find_grid.py +367 -52
- package/python/src/captchakraken/tool_calls/track_piece.py +96 -0
- package/python/src/captchakraken/trajectory.py +2 -7
- package/python/src/captchakraken/watcher.py +193 -0
- package/python/src/captchakraken/planner_types.py +0 -74
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-like mouse trajectories for the page driver.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript side of `python/src/captchakraken/trajectory.py`; the two are
|
|
5
|
+
* one implementation in two languages, pinned by `test_trajectory_parity.py`.
|
|
6
|
+
*
|
|
7
|
+
* What makes the output human rather than a lerp:
|
|
8
|
+
* - a Bezier arc, not a straight line (hand motion bows)
|
|
9
|
+
* - Fitts's-law duration: distance and precision set the time, not a constant
|
|
10
|
+
* - an ease-in-out velocity profile, so the cursor accelerates then brakes
|
|
11
|
+
* - sub-pixel jitter that scales with speed (fast motion is sloppier)
|
|
12
|
+
* - a small overshoot-and-correct on longer moves, which is the single most
|
|
13
|
+
* recognisable human tell and the one a linear interpolation never produces
|
|
14
|
+
*
|
|
15
|
+
* Camoufox's own `humanize` is a DIFFERENT, browser-level mechanism that
|
|
16
|
+
* re-humanises every `mouse.move()` it is handed. Running both composes them —
|
|
17
|
+
* measured 82.1s vs 13.4s on one geetest_v4_slide solve, because each of the 60
|
|
18
|
+
* points below became its own humanised sub-trajectory. Drive with humanize off.
|
|
19
|
+
*/
|
|
20
|
+
export type Point = [number, number];
|
|
21
|
+
/**
|
|
22
|
+
* Returns `[points, timings]`. `timings` is CUMULATIVE milliseconds from the
|
|
23
|
+
* gesture start — not per-step deltas — because that is what `tracePath` sleeps
|
|
24
|
+
* against, and both drivers must pace identically.
|
|
25
|
+
*
|
|
26
|
+
* `frequency` is samples per second (the driver passes 60).
|
|
27
|
+
* `frequencyRandomizer` jitters each frame's timestamp so the sample interval
|
|
28
|
+
* is not a metronome; a perfectly periodic mousemove stream is trivially
|
|
29
|
+
* detectable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function generate_trajectory(target_start: readonly number[], target_end: readonly number[], frequency?: number, frequencyRandomizer?: number): [Point[], number[]];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Human-like mouse trajectories for the page driver.
|
|
4
|
+
*
|
|
5
|
+
* The TypeScript side of `python/src/captchakraken/trajectory.py`; the two are
|
|
6
|
+
* one implementation in two languages, pinned by `test_trajectory_parity.py`.
|
|
7
|
+
*
|
|
8
|
+
* What makes the output human rather than a lerp:
|
|
9
|
+
* - a Bezier arc, not a straight line (hand motion bows)
|
|
10
|
+
* - Fitts's-law duration: distance and precision set the time, not a constant
|
|
11
|
+
* - an ease-in-out velocity profile, so the cursor accelerates then brakes
|
|
12
|
+
* - sub-pixel jitter that scales with speed (fast motion is sloppier)
|
|
13
|
+
* - a small overshoot-and-correct on longer moves, which is the single most
|
|
14
|
+
* recognisable human tell and the one a linear interpolation never produces
|
|
15
|
+
*
|
|
16
|
+
* Camoufox's own `humanize` is a DIFFERENT, browser-level mechanism that
|
|
17
|
+
* re-humanises every `mouse.move()` it is handed. Running both composes them —
|
|
18
|
+
* measured 82.1s vs 13.4s on one geetest_v4_slide solve, because each of the 60
|
|
19
|
+
* points below became its own humanised sub-trajectory. Drive with humanize off.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.generate_trajectory = generate_trajectory;
|
|
23
|
+
// Fitts's law: MT = a + b * log2(distance / width + 1). The constants are the
|
|
24
|
+
// usual empirical range for a mouse; `WIDTH` stands in for target precision.
|
|
25
|
+
const FITTS_A_MS = 90.0;
|
|
26
|
+
const FITTS_B_MS = 105.0;
|
|
27
|
+
const WIDTH_PX = 28.0;
|
|
28
|
+
// Below this, a move is a nudge inside one element: no overshoot, no arc.
|
|
29
|
+
const SHORT_MOVE_PX = 24.0;
|
|
30
|
+
// Overshooting a 30px hop looks like a twitch, not a human.
|
|
31
|
+
const OVERSHOOT_MIN_DISTANCE_PX = 220.0;
|
|
32
|
+
/** Cubic ease. Real pointer motion is roughly bell-shaped in velocity. */
|
|
33
|
+
function easeInOut(t) {
|
|
34
|
+
if (t < 0.5)
|
|
35
|
+
return 4.0 * t * t * t;
|
|
36
|
+
return 1.0 - Math.pow(-2.0 * t + 2.0, 3) / 2.0;
|
|
37
|
+
}
|
|
38
|
+
function cubicBezier(p0, p1, p2, p3, t) {
|
|
39
|
+
const u = 1.0 - t;
|
|
40
|
+
const x = u * u * u * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t * t * t * p3[0];
|
|
41
|
+
const y = u * u * u * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t * t * t * p3[1];
|
|
42
|
+
return [x, y];
|
|
43
|
+
}
|
|
44
|
+
function uniform(lo, hi) {
|
|
45
|
+
return lo + Math.random() * (hi - lo);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Box-Muller. `Math.random()` is uniform and the jitter model wants a normal
|
|
49
|
+
* deviate; a uniform one has hard edges that show up as a boxy distribution of
|
|
50
|
+
* off-path error, which is itself a fingerprint.
|
|
51
|
+
*/
|
|
52
|
+
function gauss(mu, sigma) {
|
|
53
|
+
let u = 0;
|
|
54
|
+
while (u === 0)
|
|
55
|
+
u = Math.random(); // log(0) is -Infinity
|
|
56
|
+
const v = Math.random();
|
|
57
|
+
return mu + sigma * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Two control points offset PERPENDICULAR to the travel direction, so the path
|
|
61
|
+
* bows to one side. The sign is random per gesture: always bowing the same way
|
|
62
|
+
* would itself be a fingerprint.
|
|
63
|
+
*/
|
|
64
|
+
function controlPoints(start, end, distance) {
|
|
65
|
+
const dx = end[0] - start[0];
|
|
66
|
+
const dy = end[1] - start[1];
|
|
67
|
+
if (distance === 0)
|
|
68
|
+
return [start, end];
|
|
69
|
+
// Unit normal to the direction of travel.
|
|
70
|
+
const nx = -dy / distance;
|
|
71
|
+
const ny = dx / distance;
|
|
72
|
+
// Bow proportional to distance, but capped — long drags don't arc absurdly.
|
|
73
|
+
const bow = Math.min(distance * uniform(0.06, 0.16), 140.0) * (Math.random() < 0.5 ? -1.0 : 1.0);
|
|
74
|
+
// Control points at roughly 1/3 and 2/3 along, each pushed off-axis.
|
|
75
|
+
const c1 = [
|
|
76
|
+
start[0] + dx * uniform(0.2, 0.4) + nx * bow,
|
|
77
|
+
start[1] + dy * uniform(0.2, 0.4) + ny * bow,
|
|
78
|
+
];
|
|
79
|
+
const c2 = [
|
|
80
|
+
start[0] + dx * uniform(0.6, 0.8) + nx * bow * uniform(0.4, 0.9),
|
|
81
|
+
start[1] + dy * uniform(0.6, 0.8) + ny * bow * uniform(0.4, 0.9),
|
|
82
|
+
];
|
|
83
|
+
return [c1, c2];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns `[points, timings]`. `timings` is CUMULATIVE milliseconds from the
|
|
87
|
+
* gesture start — not per-step deltas — because that is what `tracePath` sleeps
|
|
88
|
+
* against, and both drivers must pace identically.
|
|
89
|
+
*
|
|
90
|
+
* `frequency` is samples per second (the driver passes 60).
|
|
91
|
+
* `frequencyRandomizer` jitters each frame's timestamp so the sample interval
|
|
92
|
+
* is not a metronome; a perfectly periodic mousemove stream is trivially
|
|
93
|
+
* detectable.
|
|
94
|
+
*/
|
|
95
|
+
function generate_trajectory(target_start, target_end, frequency = 60, frequencyRandomizer = 0.12) {
|
|
96
|
+
const start = [Number(target_start[0]), Number(target_start[1])];
|
|
97
|
+
const end = [Number(target_end[0]), Number(target_end[1])];
|
|
98
|
+
const dx = end[0] - start[0];
|
|
99
|
+
const dy = end[1] - start[1];
|
|
100
|
+
const distance = Math.hypot(dx, dy);
|
|
101
|
+
if (distance < 1e-6)
|
|
102
|
+
return [[end], [0.0]];
|
|
103
|
+
let durationMs = FITTS_A_MS + FITTS_B_MS * Math.log2(distance / WIDTH_PX + 1.0);
|
|
104
|
+
durationMs *= uniform(0.85, 1.2);
|
|
105
|
+
const steps = Math.max(2, Math.round((durationMs / 1000.0) * Math.max(1, frequency)));
|
|
106
|
+
// A short hop is a straight ease with jitter — bowing a 15px move looks worse
|
|
107
|
+
// than not bowing it.
|
|
108
|
+
let c1;
|
|
109
|
+
let c2;
|
|
110
|
+
if (distance < SHORT_MOVE_PX) {
|
|
111
|
+
c1 = start;
|
|
112
|
+
c2 = end;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
[c1, c2] = controlPoints(start, end, distance);
|
|
116
|
+
}
|
|
117
|
+
// Overshoot: on a long move the hand arrives past the target and corrects.
|
|
118
|
+
// Modelled as a longer primary gesture to a point beyond the target, then a
|
|
119
|
+
// short settle back — the settle is appended as extra samples below.
|
|
120
|
+
const overshoot = distance >= OVERSHOOT_MIN_DISTANCE_PX && Math.random() < 0.55;
|
|
121
|
+
let aim = end;
|
|
122
|
+
if (overshoot) {
|
|
123
|
+
const over = uniform(0.01, 0.035) * distance;
|
|
124
|
+
aim = [end[0] + (dx / distance) * over, end[1] + (dy / distance) * over];
|
|
125
|
+
}
|
|
126
|
+
const points = [];
|
|
127
|
+
const timings = [];
|
|
128
|
+
let elapsed = 0.0;
|
|
129
|
+
const perStep = durationMs / steps;
|
|
130
|
+
for (let i = 0; i <= steps; i++) {
|
|
131
|
+
const t = easeInOut(i / steps);
|
|
132
|
+
let [px, py] = cubicBezier(start, c1, c2, aim, t);
|
|
133
|
+
// Jitter scales with instantaneous speed: the faster the cursor is moving,
|
|
134
|
+
// the less precisely a human tracks the intended path.
|
|
135
|
+
const speed = Math.abs(easeInOut(Math.min(1.0, (i + 1) / steps)) - easeInOut(i / steps));
|
|
136
|
+
const jitter = Math.min(1.6, 0.25 + speed * steps * 0.5);
|
|
137
|
+
px += gauss(0.0, jitter * 0.5);
|
|
138
|
+
py += gauss(0.0, jitter * 0.5);
|
|
139
|
+
points.push([px, py]);
|
|
140
|
+
timings.push(elapsed);
|
|
141
|
+
elapsed += perStep * uniform(1.0 - frequencyRandomizer, 1.0 + frequencyRandomizer);
|
|
142
|
+
}
|
|
143
|
+
if (overshoot) {
|
|
144
|
+
// Correction: a couple of small samples back onto the true target, at the
|
|
145
|
+
// slower pace of a deliberate fine adjustment.
|
|
146
|
+
const settleSteps = Math.floor(uniform(2, 5)); // 2..4 inclusive
|
|
147
|
+
const origin = points[points.length - 1];
|
|
148
|
+
for (let i = 1; i <= settleSteps; i++) {
|
|
149
|
+
const t = i / settleSteps;
|
|
150
|
+
points.push([
|
|
151
|
+
origin[0] + (end[0] - origin[0]) * t + gauss(0.0, 0.35),
|
|
152
|
+
origin[1] + (end[1] - origin[1]) * t + gauss(0.0, 0.35),
|
|
153
|
+
]);
|
|
154
|
+
timings.push(elapsed);
|
|
155
|
+
elapsed += uniform(14.0, 30.0);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Land exactly on the requested pixel. Everything above is texture; the final
|
|
159
|
+
// sample must be the point the caller asked for or clicks drift off-target.
|
|
160
|
+
points[points.length - 1] = end;
|
|
161
|
+
return [points, timings];
|
|
162
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,12 +12,13 @@ export interface SolveStepEvent {
|
|
|
12
12
|
* Coarse stage kind:
|
|
13
13
|
* - `initial` : screenshot taken before any action (baseline)
|
|
14
14
|
* - `click` : after a click (or batch of clicks) was executed
|
|
15
|
-
* - `drag` : after a drag was executed
|
|
15
|
+
* - `drag` : after a drag was executed (a slider counts as one)
|
|
16
|
+
* - `type` : after a distorted-text answer was typed into its box
|
|
16
17
|
* - `wait` : after a CLI-requested wait elapsed
|
|
17
18
|
* - `submit` : after the Verify/Next/Submit button was clicked
|
|
18
19
|
* - `round` : a dynamic reCAPTCHA 3x3 round boundary (pre-solve snapshot)
|
|
19
20
|
*/
|
|
20
|
-
stage: 'initial' | 'click' | 'drag' | 'wait' | 'submit' | 'round';
|
|
21
|
+
stage: 'initial' | 'click' | 'drag' | 'type' | 'wait' | 'submit' | 'round';
|
|
21
22
|
/** Short human label, e.g. "round-2:clicked 3 tile(s)". */
|
|
22
23
|
label: string;
|
|
23
24
|
/**
|
|
@@ -68,8 +69,13 @@ export interface CaptchaKrakenConfig {
|
|
|
68
69
|
*/
|
|
69
70
|
pythonCommand?: string;
|
|
70
71
|
/**
|
|
71
|
-
* vLLM LoRA name to invoke
|
|
72
|
-
*
|
|
72
|
+
* vLLM LoRA name to invoke.
|
|
73
|
+
*
|
|
74
|
+
* Defaults to whatever `models.json` calls `latest` (via CAPTCHA_LORA_NAME if
|
|
75
|
+
* set) — NOT a literal. The name selects the PROMPT GENERATION as well as the
|
|
76
|
+
* weights, so a constant here would pin the prompts to one generation while
|
|
77
|
+
* `latest` moved on. Override only if you've registered a different module
|
|
78
|
+
* with the vLLM server; see model-name.ts.
|
|
73
79
|
*/
|
|
74
80
|
model?: string;
|
|
75
81
|
/**
|
|
@@ -88,9 +94,30 @@ export interface CaptchaKrakenConfig {
|
|
|
88
94
|
* Automatically re-check for newly opened / next-step captchas after each solve
|
|
89
95
|
* attempt (e.g., clicking a checkbox opens an image challenge).
|
|
90
96
|
*
|
|
91
|
-
*
|
|
97
|
+
* The loop count that FITS inside overallSolveTimeoutMs, rather than one that
|
|
98
|
+
* needs policing by the clock: a round costs ~4-7s, so six rounds is the 45s
|
|
99
|
+
* budget. A backstop, not the normal way a hopeless solve ends — that is
|
|
100
|
+
* maxNoProgressRounds. Reaching this number is a bug report.
|
|
101
|
+
*
|
|
102
|
+
* Default: 6
|
|
92
103
|
*/
|
|
93
104
|
maxSolveLoops?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Consecutive rounds producing the SAME answer before the solve is abandoned.
|
|
107
|
+
*
|
|
108
|
+
* At temperature 0 the model is a function of the picture, so an identical
|
|
109
|
+
* answer means an identical picture — and every answer this driver produces is
|
|
110
|
+
* EXECUTED, so the previous one already ran and moved nothing. Repeating it
|
|
111
|
+
* cannot do better; it just spends a round.
|
|
112
|
+
*
|
|
113
|
+
* 2 rather than 1 because the first repeat also escalates to a RECORDING (see
|
|
114
|
+
* shouldRetryAsAnimated), which is the one recovery worth trying: a cycling
|
|
115
|
+
* board reads as a still and answers the same way every round. One more round
|
|
116
|
+
* buys that chance.
|
|
117
|
+
*
|
|
118
|
+
* Default: 2
|
|
119
|
+
*/
|
|
120
|
+
maxNoProgressRounds?: number;
|
|
94
121
|
/**
|
|
95
122
|
* Delay (ms) after executing actions before re-detecting captchas.
|
|
96
123
|
* Useful to allow challenge frames / new images to appear.
|
|
@@ -101,7 +128,7 @@ export interface CaptchaKrakenConfig {
|
|
|
101
128
|
/**
|
|
102
129
|
* Overall time limit (ms) for the entire solve loop.
|
|
103
130
|
*
|
|
104
|
-
* Default:
|
|
131
|
+
* Default: 45000
|
|
105
132
|
*/
|
|
106
133
|
overallSolveTimeoutMs?: number;
|
|
107
134
|
/**
|
|
@@ -184,9 +211,26 @@ export interface CaptchaKrakenConfig {
|
|
|
184
211
|
* soon as the solved signal appears avoids re-entering the solve pipeline on a
|
|
185
212
|
* challenge frame that is merely animating closed.
|
|
186
213
|
*
|
|
187
|
-
*
|
|
214
|
+
* That is the window's whole job, and it is why a WRONG answer spends all of
|
|
215
|
+
* it: the vendors emit nothing on a wrong answer, they just re-deal. So
|
|
216
|
+
* sizing it means asking how late a real SUCCESS can arrive — measured over
|
|
217
|
+
* 34 successful rounds across 20 puzzle types on the fixture server: p50
|
|
218
|
+
* 360ms, max 528ms. 1000ms is that worst case plus headroom for a vendor
|
|
219
|
+
* round-trip nobody has measured yet. Down from 2500, which was spent in full
|
|
220
|
+
* on every unsuccessful round.
|
|
221
|
+
*
|
|
222
|
+
* Default: 1000
|
|
188
223
|
*/
|
|
189
224
|
postSolveOutcomeTimeoutMs?: number;
|
|
225
|
+
/**
|
|
226
|
+
* Poll interval (ms) inside that window.
|
|
227
|
+
*
|
|
228
|
+
* The success signals are polled, so detection latency is a MULTIPLE of this.
|
|
229
|
+
* Each poll is a couple of cheap DOM reads.
|
|
230
|
+
*
|
|
231
|
+
* Default: 75
|
|
232
|
+
*/
|
|
233
|
+
postSolveOutcomePollMs?: number;
|
|
190
234
|
/**
|
|
191
235
|
* Guard against the captcha frame changing WHILE the model is generating.
|
|
192
236
|
* reCAPTCHA/hCaptcha fade fresh tiles in over ~1s; if new imagery paints
|
|
@@ -225,6 +269,12 @@ export interface CaptchaKrakenConfig {
|
|
|
225
269
|
* Default: 0.01
|
|
226
270
|
*/
|
|
227
271
|
settleDiffThreshold?: number;
|
|
272
|
+
/**
|
|
273
|
+
* How long to wait for hCaptcha's task images to paint before screenshotting
|
|
274
|
+
* anyway. Best-effort by design — the screenshot happens either way — so it
|
|
275
|
+
* is bounded by what a loading tile plausibly costs. Default: 3000
|
|
276
|
+
*/
|
|
277
|
+
hcaptchaImagesTimeoutMs?: number;
|
|
228
278
|
/** Poll interval (ms) for the settle monitor. Default: 220 */
|
|
229
279
|
settlePollMs?: number;
|
|
230
280
|
/**
|
|
@@ -245,6 +295,41 @@ export interface CaptchaKrakenConfig {
|
|
|
245
295
|
* Default: 4500
|
|
246
296
|
*/
|
|
247
297
|
animatedChallengeAfterMs?: number;
|
|
298
|
+
/**
|
|
299
|
+
* Record and solve animated challenges. When false, a challenge that never
|
|
300
|
+
* settles fails with `.animated = true` as it did before — for callers who
|
|
301
|
+
* would rather fail fast than spend the recording time.
|
|
302
|
+
*
|
|
303
|
+
* Default: true
|
|
304
|
+
*/
|
|
305
|
+
videoSolveEnabled?: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* Length (ms) and frame rate of the burst recorded from an animated challenge.
|
|
308
|
+
*
|
|
309
|
+
* Do not tune these casually. They are deliberately identical to the geometry
|
|
310
|
+
* the training corpus was collected at (4000ms @ 10fps), and the keyframe
|
|
311
|
+
* slicer reads the clip's temporal structure — a different length or rate
|
|
312
|
+
* changes where a cycle's period lands across the frames, so the live set gets
|
|
313
|
+
* sliced differently from the trained set and the model's frame number stops
|
|
314
|
+
* meaning what the driver thinks it means.
|
|
315
|
+
*
|
|
316
|
+
* Defaults: 4000 / 10
|
|
317
|
+
*/
|
|
318
|
+
videoBurstDurationMs?: number;
|
|
319
|
+
videoBurstFps?: number;
|
|
320
|
+
/**
|
|
321
|
+
* How long (ms) to wait for the widget to return to the keyframe the model
|
|
322
|
+
* chose, before clicking anyway; and the poll interval while waiting.
|
|
323
|
+
*
|
|
324
|
+
* Bounded because the alternative is worse: these puzzles cycle, so the state
|
|
325
|
+
* does come back — but if the recording caught a one-off transition it never
|
|
326
|
+
* will, and a click on the model's coordinates is a better use of the
|
|
327
|
+
* remaining budget than a timeout.
|
|
328
|
+
*
|
|
329
|
+
* Defaults: 6000 / 120
|
|
330
|
+
*/
|
|
331
|
+
keyframeWaitTimeoutMs?: number;
|
|
332
|
+
keyframeWaitPollMs?: number;
|
|
248
333
|
/**
|
|
249
334
|
* After clicking Submit/Verify, the solver EXPECTS the frame to change (advance
|
|
250
335
|
* to the next round, or close because it was accepted). This is how long (ms)
|
|
@@ -297,7 +382,27 @@ export interface BoundingBox {
|
|
|
297
382
|
2: number;
|
|
298
383
|
3: number;
|
|
299
384
|
}
|
|
300
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Fields present ONLY on actions from an animated challenge.
|
|
387
|
+
*
|
|
388
|
+
* Why an action carries a picture: on an animated puzzle the target is visible
|
|
389
|
+
* only part of the time, so the coordinates are correct only while the widget
|
|
390
|
+
* looks the way it did in that keyframe. The driver holds the mouse until the
|
|
391
|
+
* live neighbourhood around the click point matches the same neighbourhood of
|
|
392
|
+
* `await_keyframe`, then clicks. Without the wait, a click on a fading sprite
|
|
393
|
+
* lands on background.
|
|
394
|
+
*
|
|
395
|
+
* Both are set together or not at all — a number with no image cannot be waited
|
|
396
|
+
* on, and an image with no number cannot be reported. Absent on every still
|
|
397
|
+
* puzzle, where there is no moment to wait for.
|
|
398
|
+
*/
|
|
399
|
+
export interface AnimatedActionFields {
|
|
400
|
+
/** Absolute path to the keyframe the model chose to act on. */
|
|
401
|
+
await_keyframe?: string | null;
|
|
402
|
+
/** Its 1-based number in the keyframe set that was sent. */
|
|
403
|
+
frame?: number | null;
|
|
404
|
+
}
|
|
405
|
+
export interface ClickAction extends AnimatedActionFields {
|
|
301
406
|
action: 'click';
|
|
302
407
|
/**
|
|
303
408
|
* One or more normalized [x1, y1, x2, y2] bboxes (0–1 fractions of the
|
|
@@ -310,11 +415,23 @@ export interface ClickAction {
|
|
|
310
415
|
target_bounding_box?: [number, number, number, number] | null;
|
|
311
416
|
target_coordinates?: [number, number] | null;
|
|
312
417
|
}
|
|
313
|
-
export interface DragAction {
|
|
418
|
+
export interface DragAction extends AnimatedActionFields {
|
|
314
419
|
action: 'drag';
|
|
315
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Null on a PUZZLE-PIECE SLIDER. Not a missing field — the shape of the
|
|
422
|
+
* answer: what you grab (a handle, elsewhere on the widget) is not what has
|
|
423
|
+
* to arrive (the piece), and how far one moves the other is a vendor detail
|
|
424
|
+
* no picture reveals. The driver reads a null source as "find the slider and
|
|
425
|
+
* close the loop on the piece" (see executeSlide).
|
|
426
|
+
*/
|
|
427
|
+
source_bounding_box: [number, number, number, number] | null;
|
|
316
428
|
target_bounding_box: [number, number, number, number];
|
|
317
429
|
}
|
|
430
|
+
/** Distorted-text captchas: the answer is a string, not a place. */
|
|
431
|
+
export interface TypeAction {
|
|
432
|
+
action: 'type';
|
|
433
|
+
text: string;
|
|
434
|
+
}
|
|
318
435
|
export interface DoneAction {
|
|
319
436
|
action: 'done';
|
|
320
437
|
}
|
|
@@ -322,7 +439,7 @@ export interface WaitAction {
|
|
|
322
439
|
action: 'wait';
|
|
323
440
|
duration_ms: number;
|
|
324
441
|
}
|
|
325
|
-
export type CaptchaAction = ClickAction | WaitAction | DragAction | DoneAction;
|
|
442
|
+
export type CaptchaAction = ClickAction | WaitAction | DragAction | TypeAction | DoneAction;
|
|
326
443
|
export type SolverResult = CaptchaAction | CaptchaAction[];
|
|
327
444
|
export interface TokenUsage {
|
|
328
445
|
input_tokens: number;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-solve watcher — install once, and captchas are solved as they appear.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS A POLL AND NOT AN INJECTED OBSERVER
|
|
5
|
+
* The obvious implementation is a `MutationObserver` inside the page that calls
|
|
6
|
+
* out through `exposeBinding`. That is faster to react, and it is also the one
|
|
7
|
+
* design that cannot be made stealthy on every platform: `exposeBinding` puts a
|
|
8
|
+
* function on `window` and an observer is script the page can enumerate. On
|
|
9
|
+
* Camoufox both are sandboxed and invisible; on vanilla Playwright or Puppeteer
|
|
10
|
+
* they are not, and a captcha vendor is exactly the party that looks.
|
|
11
|
+
*
|
|
12
|
+
* So the watcher injects NOTHING. It drives the same `detectCaptcha()` the
|
|
13
|
+
* solver already uses, from the driver side, on a timer. Consequences worth
|
|
14
|
+
* knowing:
|
|
15
|
+
*
|
|
16
|
+
* - Nothing is added to the page on ANY platform, so there is no new
|
|
17
|
+
* detection surface to reason about per launcher.
|
|
18
|
+
* - On Camoufox the probe's DOM reads run in Camoufox's isolated Juggler
|
|
19
|
+
* world for free — that is its default for all Playwright evaluation
|
|
20
|
+
* (`main_world_eval` / an `mw:` prefix is the opt-OUT). The watcher never
|
|
21
|
+
* opts out, so it is isolated there by construction, not by special-casing.
|
|
22
|
+
* - Reaction time is bounded by `intervalMs`, not by the mutation itself.
|
|
23
|
+
* A captcha that appears just after a tick waits up to one interval.
|
|
24
|
+
*
|
|
25
|
+
*
|
|
26
|
+
* ONE WATCHER COVERS THE WHOLE PAGE, FOR ITS WHOLE LIFE
|
|
27
|
+
* A `Page` outlives navigation, and so does the watcher holding it: install it
|
|
28
|
+
* once and it keeps probing across every `goto`, staying quiet while there is
|
|
29
|
+
* nothing to solve and re-arming after each solve. That is deliberately the
|
|
30
|
+
* whole API — the case that motivates a browser-wide installer is almost always
|
|
31
|
+
* a challenge appearing on request 40 of a run, on the SAME page object the run
|
|
32
|
+
* started with, and that is already covered by one line. Pinned in
|
|
33
|
+
* browser-compat.test.ts.
|
|
34
|
+
* DETECTION IS THE SOLVER'S, NOT A COPY
|
|
35
|
+
* The trigger is `solver.detectCaptcha(page)`, the same method `solve()` calls,
|
|
36
|
+
* so "what counts as a captcha" has exactly one definition. An earlier sketch
|
|
37
|
+
* kept a union of the vendor selectors here for a cheaper single-roundtrip
|
|
38
|
+
* probe; it drifted from VENDOR_WIDGET_LOCATORS the first time a vendor was
|
|
39
|
+
* added, which is the failure this indirection exists to prevent.
|
|
40
|
+
*
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const solver = new CaptchaKrakenSolver();
|
|
43
|
+
* const watcher = solver.watch(page, { onSolved: r => console.log(r.isSolved) });
|
|
44
|
+
* // ... your automation runs; captchas are solved underneath it ...
|
|
45
|
+
* await watcher.stop();
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
import { PlaywrightPage as Page } from './playwright-types';
|
|
49
|
+
import { SolveResult } from './types';
|
|
50
|
+
/**
|
|
51
|
+
* The slice of the solver the watcher drives.
|
|
52
|
+
*
|
|
53
|
+
* Structural rather than an import of `CaptchaKrakenSolver` so this module has
|
|
54
|
+
* no runtime dependency on solver.ts (which imports this one for the `watch()`
|
|
55
|
+
* method — a real cycle under CommonJS), and so tests can drive it with a fake.
|
|
56
|
+
*/
|
|
57
|
+
export interface WatchableSolver {
|
|
58
|
+
detectCaptcha(page: Page): Promise<unknown | null>;
|
|
59
|
+
solve(page: Page): Promise<SolveResult | void>;
|
|
60
|
+
}
|
|
61
|
+
export interface WatchOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Milliseconds between detection probes. Default 1000.
|
|
64
|
+
*
|
|
65
|
+
* This is the latency floor for noticing a captcha, and each tick costs one
|
|
66
|
+
* `detectCaptcha()` — a handful of selector queries. Below ~250ms you are
|
|
67
|
+
* paying real CPU for latency the solve itself (seconds) makes irrelevant.
|
|
68
|
+
*/
|
|
69
|
+
intervalMs?: number;
|
|
70
|
+
/** Stop automatically after this many successful solves. Default: unlimited. */
|
|
71
|
+
maxSolves?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Wait this long after a solve THROWS before probing again. Default 5000.
|
|
74
|
+
*
|
|
75
|
+
* Without it a permanently unsupported challenge — an invisible reCAPTCHA v3,
|
|
76
|
+
* say — turns the watcher into a hot loop that re-attempts and re-bills every
|
|
77
|
+
* `intervalMs` forever.
|
|
78
|
+
*/
|
|
79
|
+
errorBackoffMs?: number;
|
|
80
|
+
/** Called after each solve that returns a result. Exceptions here are swallowed. */
|
|
81
|
+
onSolved?: (result: SolveResult) => void | Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Called when detection or solving throws. Exceptions here are swallowed.
|
|
84
|
+
*
|
|
85
|
+
* Errors are reported, never fatal: `NoCaptchaFoundError` fires routinely
|
|
86
|
+
* when a widget disappears between the probe and the solve, and a watcher
|
|
87
|
+
* that died on it would be useless.
|
|
88
|
+
*/
|
|
89
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
export interface CaptchaWatcher {
|
|
92
|
+
/** Stop probing and resolve once any in-flight solve has finished. */
|
|
93
|
+
stop(): Promise<void>;
|
|
94
|
+
/** False once stopped, the page closed, or `maxSolves` was reached. */
|
|
95
|
+
readonly running: boolean;
|
|
96
|
+
/** Successful solves so far. */
|
|
97
|
+
readonly solves: number;
|
|
98
|
+
}
|
|
99
|
+
export declare function watchPage(solver: WatchableSolver, page: Page, options?: WatchOptions): CaptchaWatcher;
|
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-solve watcher — install once, and captchas are solved as they appear.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A POLL AND NOT AN INJECTED OBSERVER
|
|
6
|
+
* The obvious implementation is a `MutationObserver` inside the page that calls
|
|
7
|
+
* out through `exposeBinding`. That is faster to react, and it is also the one
|
|
8
|
+
* design that cannot be made stealthy on every platform: `exposeBinding` puts a
|
|
9
|
+
* function on `window` and an observer is script the page can enumerate. On
|
|
10
|
+
* Camoufox both are sandboxed and invisible; on vanilla Playwright or Puppeteer
|
|
11
|
+
* they are not, and a captcha vendor is exactly the party that looks.
|
|
12
|
+
*
|
|
13
|
+
* So the watcher injects NOTHING. It drives the same `detectCaptcha()` the
|
|
14
|
+
* solver already uses, from the driver side, on a timer. Consequences worth
|
|
15
|
+
* knowing:
|
|
16
|
+
*
|
|
17
|
+
* - Nothing is added to the page on ANY platform, so there is no new
|
|
18
|
+
* detection surface to reason about per launcher.
|
|
19
|
+
* - On Camoufox the probe's DOM reads run in Camoufox's isolated Juggler
|
|
20
|
+
* world for free — that is its default for all Playwright evaluation
|
|
21
|
+
* (`main_world_eval` / an `mw:` prefix is the opt-OUT). The watcher never
|
|
22
|
+
* opts out, so it is isolated there by construction, not by special-casing.
|
|
23
|
+
* - Reaction time is bounded by `intervalMs`, not by the mutation itself.
|
|
24
|
+
* A captcha that appears just after a tick waits up to one interval.
|
|
25
|
+
*
|
|
26
|
+
*
|
|
27
|
+
* ONE WATCHER COVERS THE WHOLE PAGE, FOR ITS WHOLE LIFE
|
|
28
|
+
* A `Page` outlives navigation, and so does the watcher holding it: install it
|
|
29
|
+
* once and it keeps probing across every `goto`, staying quiet while there is
|
|
30
|
+
* nothing to solve and re-arming after each solve. That is deliberately the
|
|
31
|
+
* whole API — the case that motivates a browser-wide installer is almost always
|
|
32
|
+
* a challenge appearing on request 40 of a run, on the SAME page object the run
|
|
33
|
+
* started with, and that is already covered by one line. Pinned in
|
|
34
|
+
* browser-compat.test.ts.
|
|
35
|
+
* DETECTION IS THE SOLVER'S, NOT A COPY
|
|
36
|
+
* The trigger is `solver.detectCaptcha(page)`, the same method `solve()` calls,
|
|
37
|
+
* so "what counts as a captcha" has exactly one definition. An earlier sketch
|
|
38
|
+
* kept a union of the vendor selectors here for a cheaper single-roundtrip
|
|
39
|
+
* probe; it drifted from VENDOR_WIDGET_LOCATORS the first time a vendor was
|
|
40
|
+
* added, which is the failure this indirection exists to prevent.
|
|
41
|
+
*
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const solver = new CaptchaKrakenSolver();
|
|
44
|
+
* const watcher = solver.watch(page, { onSolved: r => console.log(r.isSolved) });
|
|
45
|
+
* // ... your automation runs; captchas are solved underneath it ...
|
|
46
|
+
* await watcher.stop();
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.watchPage = watchPage;
|
|
51
|
+
/** A page that has closed, where the launcher exposes that (Playwright and Puppeteer both do). */
|
|
52
|
+
function isPageClosed(page) {
|
|
53
|
+
return typeof page.isClosed === 'function' ? page.isClosed() : false;
|
|
54
|
+
}
|
|
55
|
+
/** Report to a user callback without letting its failure reach the loop. */
|
|
56
|
+
async function report(fn, value) {
|
|
57
|
+
if (!fn)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
await fn(value);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A throwing callback is the caller's bug and must not stop the watcher.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function watchPage(solver, page, options = {}) {
|
|
67
|
+
const intervalMs = options.intervalMs ?? 1000;
|
|
68
|
+
const errorBackoffMs = options.errorBackoffMs ?? 5000;
|
|
69
|
+
const maxSolves = options.maxSolves ?? Infinity;
|
|
70
|
+
let running = true;
|
|
71
|
+
let solves = 0;
|
|
72
|
+
// Resolved by the loop on its way out, so stop() can await a solve in flight.
|
|
73
|
+
// Definite assignment: the Promise executor runs synchronously, so `finished`
|
|
74
|
+
// is set before any code below can reach it — TS cannot prove that itself.
|
|
75
|
+
let finished;
|
|
76
|
+
const done = new Promise((resolve) => { finished = resolve; });
|
|
77
|
+
// Held so stop() can cut a pending sleep short instead of waiting it out.
|
|
78
|
+
let wake = null;
|
|
79
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
80
|
+
const timer = setTimeout(() => { wake = null; resolve(); }, ms);
|
|
81
|
+
wake = () => { clearTimeout(timer); wake = null; resolve(); };
|
|
82
|
+
});
|
|
83
|
+
(async () => {
|
|
84
|
+
try {
|
|
85
|
+
while (running) {
|
|
86
|
+
await sleep(intervalMs);
|
|
87
|
+
if (!running)
|
|
88
|
+
break;
|
|
89
|
+
if (isPageClosed(page))
|
|
90
|
+
break;
|
|
91
|
+
try {
|
|
92
|
+
if (!(await solver.detectCaptcha(page)))
|
|
93
|
+
continue;
|
|
94
|
+
// Re-check: detectCaptcha can take a moment, and stop() during it
|
|
95
|
+
// should not be followed by a fresh multi-second solve.
|
|
96
|
+
if (!running)
|
|
97
|
+
break;
|
|
98
|
+
const result = await solver.solve(page);
|
|
99
|
+
if (result) {
|
|
100
|
+
solves += 1;
|
|
101
|
+
await report(options.onSolved, result);
|
|
102
|
+
}
|
|
103
|
+
if (solves >= maxSolves)
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (isPageClosed(page))
|
|
108
|
+
break;
|
|
109
|
+
await report(options.onError, error);
|
|
110
|
+
await sleep(errorBackoffMs);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
running = false;
|
|
116
|
+
finished();
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
return {
|
|
120
|
+
stop() {
|
|
121
|
+
running = false;
|
|
122
|
+
if (wake)
|
|
123
|
+
wake();
|
|
124
|
+
return done;
|
|
125
|
+
},
|
|
126
|
+
get running() { return running; },
|
|
127
|
+
get solves() { return solves; },
|
|
128
|
+
};
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captchakraken",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Browser-agnostic Playwright/Puppeteer captcha solver. Detects the captcha, reads the grid with a fine-tuned Qwen3.5-9B vision model on vLLM, and clicks through to a token.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"prebuild": "node scripts/copy-python.mjs",
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
17
|
-
"test": "tsc
|
|
17
|
+
"test": "tsc -p tsconfig.test.json && node --test .test-build/*.test.js",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"postinstall": "node scripts/setup-python.js"
|
|
20
20
|
},
|
|
@@ -43,8 +43,5 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^24.10.2",
|
|
45
45
|
"typescript": "^5.9.3"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"cursory-ts": "^1.0.0"
|
|
49
46
|
}
|
|
50
47
|
}
|