captchakraken 2.3.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/errors.d.ts +45 -0
- package/dist/errors.js +74 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +52 -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 +1297 -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 +224 -2
- package/python/src/captchakraken/config.py +119 -17
- package/python/src/captchakraken/errors.py +195 -0
- 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 +328 -73
- 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
package/dist/solver.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { PlaywrightPage as Page, PlaywrightElementHandle as ElementHandle } from './playwright-types';
|
|
2
|
+
import { CaptchaWatcher, WatchOptions } from './watcher';
|
|
2
3
|
import { CaptchaKrakenConfig, SolveResult } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* The two numbers that bound a solve, together, because they only make sense
|
|
6
|
+
* together: `maxSolveLoops` is the count that FITS inside the timeout at the
|
|
7
|
+
* ~4-7s a round costs, and the timeout is the backstop for when it does not.
|
|
8
|
+
* Named rather than inlined as `??` defaults so the relationship between them
|
|
9
|
+
* can be pinned by a test — see no-progress.test.ts. The Python port keeps the
|
|
10
|
+
* same pair on `PageSolverConfig`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SOLVE_DEFAULTS: {
|
|
13
|
+
readonly maxSolveLoops: 6;
|
|
14
|
+
readonly overallSolveTimeoutMs: 45000;
|
|
15
|
+
};
|
|
3
16
|
export declare class CaptchaKrakenSolver {
|
|
4
17
|
private config;
|
|
5
18
|
private lastMousePosition;
|
|
@@ -15,11 +28,34 @@ export declare class CaptchaKrakenSolver {
|
|
|
15
28
|
private cvWorkerPending;
|
|
16
29
|
private cvWorkerBuf;
|
|
17
30
|
private solutionCache;
|
|
31
|
+
private repeatedAnswerSeen;
|
|
32
|
+
private keyframeMode;
|
|
33
|
+
private lastAnswerSig;
|
|
34
|
+
private noProgressRounds;
|
|
18
35
|
private state;
|
|
19
36
|
private lastSubmitFrameHash;
|
|
20
37
|
private solveSessionId;
|
|
21
38
|
constructor(config?: CaptchaKrakenConfig);
|
|
22
39
|
solve(page: Page): Promise<SolveResult | void>;
|
|
40
|
+
/**
|
|
41
|
+
* Install an auto-solver: probe `page` on a timer and solve any captcha that
|
|
42
|
+
* becomes visible, until `stop()`.
|
|
43
|
+
*
|
|
44
|
+
* Returns immediately — the watching happens in the background, so your
|
|
45
|
+
* automation carries on and captchas are handled underneath it.
|
|
46
|
+
*
|
|
47
|
+
* Nothing is injected into the page on any platform; see watcher.ts for why,
|
|
48
|
+
* and for the note on Camoufox, where the probe's DOM reads land in its
|
|
49
|
+
* isolated world by default.
|
|
50
|
+
*
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const solver = new CaptchaKrakenSolver();
|
|
53
|
+
* const watcher = solver.watch(page);
|
|
54
|
+
* await page.goto('https://example.com/protected'); // solved as it appears
|
|
55
|
+
* await watcher.stop();
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
watch(page: Page, options?: WatchOptions): CaptchaWatcher;
|
|
23
59
|
private solveImpl;
|
|
24
60
|
/**
|
|
25
61
|
* Fire the optional onStep observer with a fresh screenshot of the captcha
|
|
@@ -208,17 +244,37 @@ export declare class CaptchaKrakenSolver {
|
|
|
208
244
|
/** Order a loading set so `priority` cells (just-clicked) come first. */
|
|
209
245
|
private orderByPriority;
|
|
210
246
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
247
|
+
* Watch the just-clicked tiles until the widget says what it did with them.
|
|
248
|
+
*
|
|
249
|
+
* Two answers because reCAPTCHA gives a click one of exactly two replies, and
|
|
250
|
+
* they are the two kinds of board:
|
|
251
|
+
*
|
|
252
|
+
* - `chipped`: the small blue chip landed in the tiles' top-left corners —
|
|
253
|
+
* the photos were KEPT. Nothing is on its way in, the selection is the
|
|
254
|
+
* answer, and the caller should press Verify.
|
|
255
|
+
* - `loading`: the photos are blanking or dissolving under a large centred
|
|
256
|
+
* check — those tiles are being SWAPPED, and what lands may match too, so
|
|
257
|
+
* the board has to be read again.
|
|
258
|
+
*
|
|
259
|
+
* A widget that swaps one clicked cell swaps them all, so the two never share
|
|
260
|
+
* a board and one look at the tiles we just clicked settles it. The chip is
|
|
261
|
+
* what the CV layer reports as `selected` (top-left corner only, behind a
|
|
262
|
+
* circularity and a centroid test a centred check fails), and we already read
|
|
263
|
+
* it on every poll. `chipped` needs EVERY watched tile: a partial reading is a
|
|
264
|
+
* misread, and calling a swapping board finished submits half an answer and
|
|
265
|
+
* burns the attempt, where calling a chipped board unfinished costs one
|
|
266
|
+
* inference.
|
|
267
|
+
*
|
|
268
|
+
* The blank/fade transition lags the click by a beat, so a single snapshot
|
|
269
|
+
* right after clicking misses it (the tile still shows its old image — not yet
|
|
270
|
+
* white, not yet changing). We poll consecutive frames and mark a cell loading
|
|
271
|
+
* if it is `empty` (≥97% near-white) OR `changing` (>2% pixels differ). HOVERS
|
|
272
|
+
* a clicked tile each poll so the mouse keeps moving (no unnatural pauses).
|
|
273
|
+
* Returns as soon as either verdict is in, or `{loading: [], chipped: false}`
|
|
274
|
+
* if the whole window passes with nothing happening (→ solved). Logs every
|
|
275
|
+
* poll + frame.
|
|
220
276
|
*/
|
|
221
|
-
private
|
|
277
|
+
private watchClickedTiles;
|
|
222
278
|
/**
|
|
223
279
|
* After loading is detected, wait until at least one of the given blank/fading
|
|
224
280
|
* cells reaches the `loaded` state, HOVERING those cells (in order) the whole
|
|
@@ -241,13 +297,124 @@ export declare class CaptchaKrakenSolver {
|
|
|
241
297
|
* and on a `wait`, it hovers the just-clicked / currently blank+fading tiles
|
|
242
298
|
* (in click order) and waits for at least one to finish reloading before
|
|
243
299
|
* re-screenshotting and re-solving — so we don't burn a solver call on a grid
|
|
244
|
-
* that's still mid-fade.
|
|
300
|
+
* that's still mid-fade.
|
|
301
|
+
*
|
|
302
|
+
* Only a board that SWAPS a clicked tile out is worth those extra rounds. One
|
|
303
|
+
* that ticks the tile and keeps the photo has been fully answered by the round
|
|
304
|
+
* that clicked it — same as the 4x4 — so `watchClickedTiles` reports the chip
|
|
305
|
+
* and this submits there and then. Rounds 2..N exist for the fading board and
|
|
306
|
+
* nothing else. It submits on `done`, and on a board that ticked our clicks.
|
|
245
307
|
*
|
|
246
308
|
* Returns the same shape as solveSingle so the outer solve loop — including the
|
|
247
309
|
* under-selection retry and post-solve detectCaptcha — wraps it unchanged.
|
|
248
310
|
*/
|
|
249
311
|
private solveRecaptchaGrid;
|
|
312
|
+
/**
|
|
313
|
+
* Record the animated challenge and return the directory holding the burst.
|
|
314
|
+
*
|
|
315
|
+
* The driver owns the browser, so it does the recording; the CLI does the
|
|
316
|
+
* slicing (`solve-animated`). One zero-padded PNG per frame, because the slicer
|
|
317
|
+
* sorts by name and reads the clip's temporal structure — `frame_9.png` sorting
|
|
318
|
+
* after `frame_10.png` would shuffle the burst and turn a detectable cycle into
|
|
319
|
+
* noise.
|
|
320
|
+
*
|
|
321
|
+
* Geometry comes from config and defaults to the collector's (4s @ 10fps), so a
|
|
322
|
+
* challenge recorded here is the same shape of artifact the model trained on.
|
|
323
|
+
* The caller must remove the directory when the actions are done with — the
|
|
324
|
+
* keyframes the wait gate re-reads on every poll live inside it.
|
|
325
|
+
*/
|
|
326
|
+
private recordKeyframeBurst;
|
|
327
|
+
/**
|
|
328
|
+
* Slice a recorded burst into keyframes and solve them in ONE model request.
|
|
329
|
+
*
|
|
330
|
+
* Deliberately not routed through `solveFrameFreshnessGuarded`. That guard
|
|
331
|
+
* re-solves when the frame changes during inference, and an animated challenge
|
|
332
|
+
* changes by definition — every attempt would be judged stale and the whole
|
|
333
|
+
* re-solve budget would burn without ever acting. The `frame` in the answer is
|
|
334
|
+
* the real guard: it names the state to act in, and `waitForKeyframe` enforces it.
|
|
335
|
+
*
|
|
336
|
+
* Also not deduped by screenshot hash: there is no single screenshot, and two
|
|
337
|
+
* recordings of the same widget are never byte-identical anyway.
|
|
338
|
+
*/
|
|
339
|
+
private getAnimatedSolution;
|
|
340
|
+
/**
|
|
341
|
+
* Hold until the widget looks like `keyframePath` around the 0–1 point (cx, cy).
|
|
342
|
+
*
|
|
343
|
+
* This is the reason an animated answer names a frame. The model picked the
|
|
344
|
+
* moment its target was visible, and the coordinates are only correct at that
|
|
345
|
+
* moment; clicking as soon as the answer arrives lands on whatever the sprite
|
|
346
|
+
* happens to be doing, which for a cross-fade is usually background.
|
|
347
|
+
*
|
|
348
|
+
* Only the neighbourhood of the action point is compared, with the same box and
|
|
349
|
+
* metric the training label's frame was chosen with. Local rather than
|
|
350
|
+
* whole-frame because everything ELSE in these puzzles is also moving: a
|
|
351
|
+
* whole-frame match would need every unrelated sprite to align too, and would
|
|
352
|
+
* essentially never open.
|
|
353
|
+
*
|
|
354
|
+
* Never throws. Returns whether the state was reached; on timeout the caller
|
|
355
|
+
* clicks anyway (see `keyframeWaitTimeoutMs`).
|
|
356
|
+
*
|
|
357
|
+
* NOT ATTEMPTED on an `even` clip. The slicer picks that mode precisely when
|
|
358
|
+
* the clip never revisits a picture it has already shown — a rotation, a
|
|
359
|
+
* one-way fade, a sprite crossing — so there is no state to come back to and
|
|
360
|
+
* this can only run out its full 6s, PER CLICK, before clicking the
|
|
361
|
+
* coordinates it already had. It is also the normal case, not a corner: all
|
|
362
|
+
* 116 real clips under cleanSamples/test/raw are `even` and `cycle` has never
|
|
363
|
+
* fired on real footage. Measured on hcaptcha_rotating_obj_video: 6.0s of a
|
|
364
|
+
* 28.8s solve, closest region diff 0.0721 against a 0.05 tolerance, then the
|
|
365
|
+
* same click, then solved. Kept for `cycle`/`static`, where the state does
|
|
366
|
+
* come back and waiting is the difference between the sprite and background.
|
|
367
|
+
*/
|
|
368
|
+
private waitForKeyframe;
|
|
369
|
+
/**
|
|
370
|
+
* Per-solve state that must not leak into the next challenge on the page.
|
|
371
|
+
*
|
|
372
|
+
* A repeat is a fact about ONE challenge. Carrying it forward would make the
|
|
373
|
+
* captcha after a cycling one record a burst it does not need.
|
|
374
|
+
*/
|
|
375
|
+
private resetSolveState;
|
|
376
|
+
/**
|
|
377
|
+
* A stable identity for "the answer this round is about to execute".
|
|
378
|
+
*
|
|
379
|
+
* Keyed on the retry mode too: the missed-tiles retry deliberately re-asks
|
|
380
|
+
* about the same board and its answer legitimately overlaps the previous one,
|
|
381
|
+
* so counting that as a repeat would abandon the one path built to recover
|
|
382
|
+
* from an under-selection.
|
|
383
|
+
*
|
|
384
|
+
* Coordinates are ROUNDED rather than compared exactly — the same tile chosen
|
|
385
|
+
* twice can differ in the last float digit after the normalise/clamp
|
|
386
|
+
* round-trip, and a repeat that reads as "different" is a repeat that costs a
|
|
387
|
+
* round. Returns null when the answer cannot be summarised, which counts as
|
|
388
|
+
* "not a repeat": an unreadable answer must never be why a solve is
|
|
389
|
+
* abandoned. Mirrors page_solver.py `_answer_signature`.
|
|
390
|
+
*/
|
|
391
|
+
private static answerSignature;
|
|
392
|
+
/** Count consecutive identical answers; see `maxNoProgressRounds`. */
|
|
393
|
+
private noteAnswer;
|
|
394
|
+
/**
|
|
395
|
+
* The answer for this picture, asking the model only if we have not already.
|
|
396
|
+
*
|
|
397
|
+
* The cache saving is real and is kept: a byte-identical picture costs no
|
|
398
|
+
* second inference. What changed is what a hit MEANS. It used to mean
|
|
399
|
+
* "nothing has changed, so this answer still stands"; it actually means the
|
|
400
|
+
* answer already ran and moved nothing, because every answer this returns is
|
|
401
|
+
* executed. So the hit is served (it is free) and recorded, and the round
|
|
402
|
+
* after it stops guessing at a still frame — see `shouldRetryAsAnimated`.
|
|
403
|
+
*/
|
|
404
|
+
private answerFor;
|
|
405
|
+
/**
|
|
406
|
+
* Should this round be recorded and solved from keyframes instead of read as
|
|
407
|
+
* a still?
|
|
408
|
+
*
|
|
409
|
+
* reCAPTCHA is excluded deliberately. Its dynamic 3x3 REPLACES tiles in place
|
|
410
|
+
* and has its own multi-round driver with its own fade gates; its grids are
|
|
411
|
+
* never animated, so escalating there would swap a path that works for one
|
|
412
|
+
* that cannot read a grid.
|
|
413
|
+
*/
|
|
414
|
+
private shouldRetryAsAnimated;
|
|
250
415
|
private getSolution;
|
|
416
|
+
/** One inference: build the CLI invocation, run it, parse what comes back. */
|
|
417
|
+
private askModel;
|
|
251
418
|
/**
|
|
252
419
|
* Query the model for `initialShot`, then guard against the captcha frame
|
|
253
420
|
* having changed DURING inference. reCAPTCHA/hCaptcha fade fresh tiles in over
|
|
@@ -315,7 +482,7 @@ export declare class CaptchaKrakenSolver {
|
|
|
315
482
|
/**
|
|
316
483
|
* Run `fn` (typically the model query) while idly drifting the cursor over
|
|
317
484
|
* the captcha, so the mouse behaves like a human weighing the options instead
|
|
318
|
-
* of freezing during inference. Uses the same
|
|
485
|
+
* of freezing during inference. Uses the same generate_trajectory paths as real
|
|
319
486
|
* clicks; cancelled the instant `fn` resolves. Best-effort — any wander error
|
|
320
487
|
* is swallowed and never fails the solve. Disable via config.idleMouseWander.
|
|
321
488
|
*/
|
|
@@ -328,4 +495,39 @@ export declare class CaptchaKrakenSolver {
|
|
|
328
495
|
private tracePath;
|
|
329
496
|
private executeClick;
|
|
330
497
|
private executeDrag;
|
|
498
|
+
/**
|
|
499
|
+
* First VISIBLE match for `selectors`, tried in order.
|
|
500
|
+
*
|
|
501
|
+
* `scope` is the challenge frame, or — for the vendors that render into the
|
|
502
|
+
* host page rather than an iframe — the widget element itself. Never the
|
|
503
|
+
* page: the generic tail of both selector tables would otherwise happily
|
|
504
|
+
* match a login form's text box or a carousel's drag handle somewhere else on
|
|
505
|
+
* the document, and the answer would go there.
|
|
506
|
+
*/
|
|
507
|
+
private findControl;
|
|
508
|
+
/** Put the model's reading of a distorted-text captcha into its box. */
|
|
509
|
+
private executeType;
|
|
510
|
+
/** `captchakraken track-piece` — box of what moved, handle masked out. */
|
|
511
|
+
private trackPiece;
|
|
512
|
+
/**
|
|
513
|
+
* Drive a puzzle-piece slider until the PIECE reaches the model's slot.
|
|
514
|
+
*
|
|
515
|
+
* The model is asked for one thing here — the centre of the gap — because it
|
|
516
|
+
* is the only thing the picture can tell it. What it cannot know is how far
|
|
517
|
+
* the handle must travel to put the piece there: the handle is elsewhere on
|
|
518
|
+
* the widget, and the ratio between the two is a vendor implementation detail
|
|
519
|
+
* that several of them deliberately vary.
|
|
520
|
+
*
|
|
521
|
+
* So this is closed-loop, not a calculation. Press the handle, nudge it twice
|
|
522
|
+
* by known amounts, and watch the screen: union(before, after) spans the
|
|
523
|
+
* piece's ORIGINAL left edge to its CURRENT right edge, so its width is
|
|
524
|
+
* pieceWidth + ratio x nudge. Two nudges, two widths, two unknowns — solve for
|
|
525
|
+
* both, then steer the remaining distance and re-measure. The mouse is not
|
|
526
|
+
* released until the piece is home, because on every one of these puzzles
|
|
527
|
+
* releasing IS the submit; there is no Verify button to reconsider at.
|
|
528
|
+
*
|
|
529
|
+
* Returns false if there is nothing here to drag, leaving the caller's normal
|
|
530
|
+
* no-op handling to deal with it.
|
|
531
|
+
*/
|
|
532
|
+
private executeSlide;
|
|
331
533
|
}
|