captchakraken 2.6.1 → 2.9.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/README.md +90 -7
- package/dist/cli-invocation.d.ts +16 -16
- package/dist/cli-invocation.js +33 -21
- package/dist/errors.d.ts +1 -1
- package/dist/humanize.d.ts +377 -0
- package/dist/humanize.js +716 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +44 -1
- package/dist/model-name.d.ts +1 -1
- package/dist/model-name.js +1 -1
- package/dist/playwright-types.d.ts +18 -0
- package/dist/solver.d.ts +314 -16
- package/dist/solver.js +1208 -278
- package/dist/timing.d.ts +48 -0
- package/dist/timing.js +103 -0
- package/dist/trajectory.d.ts +28 -1
- package/dist/trajectory.js +105 -1
- package/dist/types.d.ts +113 -2
- package/package.json +2 -1
- package/python/README.md +43 -0
- package/python/pyproject.toml +1 -1
- package/python/src/captchakraken/__init__.py +36 -1
- package/python/src/captchakraken/cli.py +66 -4
- package/python/src/captchakraken/config.py +28 -2
- package/python/src/captchakraken/errors.py +24 -0
- package/python/src/captchakraken/humanize.py +929 -0
- package/python/src/captchakraken/keyframes.py +184 -12
- package/python/src/captchakraken/models.json +180 -18
- package/python/src/captchakraken/page_solver.py +1132 -321
- package/python/src/captchakraken/pinned_model.json +3 -2
- package/python/src/captchakraken/planner.py +204 -16
- package/python/src/captchakraken/prompts.py +186 -4
- package/python/src/captchakraken/server_manager.py +9 -4
- package/python/src/captchakraken/solver.py +98 -58
- package/python/src/captchakraken/timing.py +8 -3
- package/python/src/captchakraken/tool_calls/find_grid.py +357 -20
- package/python/src/captchakraken/trajectory.py +117 -0
- package/python/src/captchakraken/updater.py +62 -1
package/dist/index.d.ts
CHANGED
|
@@ -104,3 +104,36 @@ export type { Page, PlaywrightPage, PlaywrightFrame, PlaywrightElementHandle } f
|
|
|
104
104
|
*/
|
|
105
105
|
export { watchPage } from './watcher';
|
|
106
106
|
export type { CaptchaWatcher, WatchOptions, WatchableSolver } from './watcher';
|
|
107
|
+
/**
|
|
108
|
+
* Humanisation: how the driver moves. `humanization: 'mouse' | 'mobile' |
|
|
109
|
+
* 'none'` picks a built-in; `humanizer` takes one of your own.
|
|
110
|
+
*
|
|
111
|
+
* `MobileHumanizer` emits real touch events with finger kinematics, through a
|
|
112
|
+
* `TouchBackend` — CDP against a Chromium-family page, or W3C pointer actions
|
|
113
|
+
* against an Appium / WebdriverIO driver on a real handset.
|
|
114
|
+
*
|
|
115
|
+
* @example Mobile emulation in the browser
|
|
116
|
+
* ```typescript
|
|
117
|
+
* import { chromium, devices } from 'playwright';
|
|
118
|
+
* import { CaptchaKrakenSolver } from 'captchakraken';
|
|
119
|
+
*
|
|
120
|
+
* const browser = await chromium.launch();
|
|
121
|
+
* const context = await browser.newContext({ ...devices['Pixel 7'], hasTouch: true });
|
|
122
|
+
* const page = await context.newPage();
|
|
123
|
+
* await page.goto('https://example.com/signup');
|
|
124
|
+
*
|
|
125
|
+
* await new CaptchaKrakenSolver({ humanization: 'mobile' }).solve(page);
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @example A real device over Appium
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const solver = new CaptchaKrakenSolver({
|
|
131
|
+
* humanization: 'mobile',
|
|
132
|
+
* touchDriver: driver, // the WebdriverIO browser
|
|
133
|
+
* touchTransform: { scale: 3, origin: [0, 132] }, // CSS px -> screen px
|
|
134
|
+
* });
|
|
135
|
+
* await solver.solve(page);
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export { MouseHumanizer, MobileHumanizer, NullHumanizer, BaseHumanizer, resolveHumanizer, touchBackendFor, CdpTouchBackend, AppiumTouchBackend, TouchscreenTouchBackend, PAUSE_KINDS, MODES as HUMANIZATION_MODES, } from './humanize';
|
|
139
|
+
export type { Humanizer, HumanizationMode, HumanizerOptions, TouchBackend, TouchSample, TouchTransform, PauseKind, } from './humanize';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.watchPage = exports.fromPuppeteer = exports.CaptchaKrakenAPIError = exports.resolveLoraName = exports.CaptchaKrakenSolver = void 0;
|
|
17
|
+
exports.HUMANIZATION_MODES = exports.PAUSE_KINDS = exports.TouchscreenTouchBackend = exports.AppiumTouchBackend = exports.CdpTouchBackend = exports.touchBackendFor = exports.resolveHumanizer = exports.BaseHumanizer = exports.NullHumanizer = exports.MobileHumanizer = exports.MouseHumanizer = exports.watchPage = exports.fromPuppeteer = exports.CaptchaKrakenAPIError = exports.resolveLoraName = exports.CaptchaKrakenSolver = void 0;
|
|
18
18
|
__exportStar(require("./types"), exports);
|
|
19
19
|
var solver_1 = require("./solver");
|
|
20
20
|
Object.defineProperty(exports, "CaptchaKrakenSolver", { enumerable: true, get: function () { return solver_1.CaptchaKrakenSolver; } });
|
|
@@ -92,3 +92,46 @@ Object.defineProperty(exports, "fromPuppeteer", { enumerable: true, get: functio
|
|
|
92
92
|
*/
|
|
93
93
|
var watcher_1 = require("./watcher");
|
|
94
94
|
Object.defineProperty(exports, "watchPage", { enumerable: true, get: function () { return watcher_1.watchPage; } });
|
|
95
|
+
/**
|
|
96
|
+
* Humanisation: how the driver moves. `humanization: 'mouse' | 'mobile' |
|
|
97
|
+
* 'none'` picks a built-in; `humanizer` takes one of your own.
|
|
98
|
+
*
|
|
99
|
+
* `MobileHumanizer` emits real touch events with finger kinematics, through a
|
|
100
|
+
* `TouchBackend` — CDP against a Chromium-family page, or W3C pointer actions
|
|
101
|
+
* against an Appium / WebdriverIO driver on a real handset.
|
|
102
|
+
*
|
|
103
|
+
* @example Mobile emulation in the browser
|
|
104
|
+
* ```typescript
|
|
105
|
+
* import { chromium, devices } from 'playwright';
|
|
106
|
+
* import { CaptchaKrakenSolver } from 'captchakraken';
|
|
107
|
+
*
|
|
108
|
+
* const browser = await chromium.launch();
|
|
109
|
+
* const context = await browser.newContext({ ...devices['Pixel 7'], hasTouch: true });
|
|
110
|
+
* const page = await context.newPage();
|
|
111
|
+
* await page.goto('https://example.com/signup');
|
|
112
|
+
*
|
|
113
|
+
* await new CaptchaKrakenSolver({ humanization: 'mobile' }).solve(page);
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @example A real device over Appium
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const solver = new CaptchaKrakenSolver({
|
|
119
|
+
* humanization: 'mobile',
|
|
120
|
+
* touchDriver: driver, // the WebdriverIO browser
|
|
121
|
+
* touchTransform: { scale: 3, origin: [0, 132] }, // CSS px -> screen px
|
|
122
|
+
* });
|
|
123
|
+
* await solver.solve(page);
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
var humanize_1 = require("./humanize");
|
|
127
|
+
Object.defineProperty(exports, "MouseHumanizer", { enumerable: true, get: function () { return humanize_1.MouseHumanizer; } });
|
|
128
|
+
Object.defineProperty(exports, "MobileHumanizer", { enumerable: true, get: function () { return humanize_1.MobileHumanizer; } });
|
|
129
|
+
Object.defineProperty(exports, "NullHumanizer", { enumerable: true, get: function () { return humanize_1.NullHumanizer; } });
|
|
130
|
+
Object.defineProperty(exports, "BaseHumanizer", { enumerable: true, get: function () { return humanize_1.BaseHumanizer; } });
|
|
131
|
+
Object.defineProperty(exports, "resolveHumanizer", { enumerable: true, get: function () { return humanize_1.resolveHumanizer; } });
|
|
132
|
+
Object.defineProperty(exports, "touchBackendFor", { enumerable: true, get: function () { return humanize_1.touchBackendFor; } });
|
|
133
|
+
Object.defineProperty(exports, "CdpTouchBackend", { enumerable: true, get: function () { return humanize_1.CdpTouchBackend; } });
|
|
134
|
+
Object.defineProperty(exports, "AppiumTouchBackend", { enumerable: true, get: function () { return humanize_1.AppiumTouchBackend; } });
|
|
135
|
+
Object.defineProperty(exports, "TouchscreenTouchBackend", { enumerable: true, get: function () { return humanize_1.TouchscreenTouchBackend; } });
|
|
136
|
+
Object.defineProperty(exports, "PAUSE_KINDS", { enumerable: true, get: function () { return humanize_1.PAUSE_KINDS; } });
|
|
137
|
+
Object.defineProperty(exports, "HUMANIZATION_MODES", { enumerable: true, get: function () { return humanize_1.MODES; } });
|
package/dist/model-name.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* When installed from npm this file is in `<pkgRoot>/dist` (compiled) or
|
|
10
10
|
* `<pkgRoot>/src` (dev), and published packages bundle the engine at
|
|
11
11
|
* `<pkgRoot>/python` (copied in by scripts/copy-python.mjs at build time). In
|
|
12
|
-
* the source
|
|
12
|
+
* the source development checkout it instead lives at the sibling `../python`.
|
|
13
13
|
*/
|
|
14
14
|
export declare function getBundledCliRoot(): string;
|
|
15
15
|
/**
|
package/dist/model-name.js
CHANGED
|
@@ -37,7 +37,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
37
37
|
* When installed from npm this file is in `<pkgRoot>/dist` (compiled) or
|
|
38
38
|
* `<pkgRoot>/src` (dev), and published packages bundle the engine at
|
|
39
39
|
* `<pkgRoot>/python` (copied in by scripts/copy-python.mjs at build time). In
|
|
40
|
-
* the source
|
|
40
|
+
* the source development checkout it instead lives at the sibling `../python`.
|
|
41
41
|
*/
|
|
42
42
|
function getBundledCliRoot() {
|
|
43
43
|
const bundled = node_path_1.default.resolve(__dirname, '..', 'python');
|
|
@@ -125,6 +125,24 @@ export interface PlaywrightPage {
|
|
|
125
125
|
}): Promise<PlaywrightElementHandle | null>;
|
|
126
126
|
/** Current viewport size, or null if not set. */
|
|
127
127
|
viewportSize(): ViewportSize | null;
|
|
128
|
+
/**
|
|
129
|
+
* The browser context this page belongs to. OPTIONAL because only
|
|
130
|
+
* `humanization: 'mobile'` needs it, and then only to open a CDP session for
|
|
131
|
+
* `Input.dispatchTouchEvent` — the mouse and none modes never touch it. A
|
|
132
|
+
* launcher that does not expose it simply cannot serve mobile mode, and says
|
|
133
|
+
* so at construction rather than mid-solve.
|
|
134
|
+
*/
|
|
135
|
+
context?(): {
|
|
136
|
+
newCDPSession(page: PlaywrightPage): Promise<any>;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Playwright's touchscreen, present when the context was created with
|
|
140
|
+
* `hasTouch: true`. OPTIONAL, and tap-only: it is the fallback for a browser
|
|
141
|
+
* with touch support but no CDP, and cannot express a drag.
|
|
142
|
+
*/
|
|
143
|
+
touchscreen?: {
|
|
144
|
+
tap(x: number, y: number): Promise<void>;
|
|
145
|
+
};
|
|
128
146
|
/** First matching element handle, or null. */
|
|
129
147
|
$(selector: string): Promise<PlaywrightElementHandle | null>;
|
|
130
148
|
/** All matching element handles. */
|
package/dist/solver.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PlaywrightPage as Page, PlaywrightElementHandle as ElementHandle } from './playwright-types';
|
|
2
2
|
import { CaptchaWatcher, WatchOptions } from './watcher';
|
|
3
|
+
import { PhaseBudget } from './timing';
|
|
3
4
|
import { CaptchaKrakenConfig, SolveResult } from './types';
|
|
4
5
|
/**
|
|
5
6
|
* The two numbers that bound a solve, together, because they only make sense
|
|
@@ -9,16 +10,82 @@ import { CaptchaKrakenConfig, SolveResult } from './types';
|
|
|
9
10
|
* can be pinned by a test — see no-progress.test.ts. The Python port keeps the
|
|
10
11
|
* same pair on `PageSolverConfig`.
|
|
11
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Does this answer need the widget's box to be performed?
|
|
15
|
+
*
|
|
16
|
+
* Only a coordinate-bearing action does. `done` means "nothing left to click"
|
|
17
|
+
* and is the answer a vendor that closes on success will be showing no widget
|
|
18
|
+
* for, so requiring a box for it fails the solve at the moment it succeeded.
|
|
19
|
+
*
|
|
20
|
+
* Written as an ALLOW-LIST of what needs nothing rather than a list of what
|
|
21
|
+
* does: a new coordinate action added later must default to needing the box,
|
|
22
|
+
* because that is the direction where being wrong throws instead of clicking
|
|
23
|
+
* nowhere.
|
|
24
|
+
*/
|
|
25
|
+
export declare function answerNeedsElementBox(actions: ReadonlyArray<{
|
|
26
|
+
action?: string;
|
|
27
|
+
}>): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Is this the challenge TRANSITIONING under us, rather than a dead puzzle?
|
|
30
|
+
*
|
|
31
|
+
* hCaptcha swaps in the next round while we hold the old iframe, so a
|
|
32
|
+
* screenshot on it fails "not visible" / "Timeout" / "not attached". GeeTest
|
|
33
|
+
* does the mirror image: it CLOSES the panel the moment it accepts an answer,
|
|
34
|
+
* so the widget we are holding loses its layout box between the screenshot and
|
|
35
|
+
* the click. Both are the widget moving on, and the answer to both is to
|
|
36
|
+
* re-detect on the next round rather than to fail the solve.
|
|
37
|
+
*
|
|
38
|
+
* The caller gates this on having ALREADY INTERACTED, which is what keeps it
|
|
39
|
+
* honest: a first-frame failure is a genuine problem and still surfaces.
|
|
40
|
+
*
|
|
41
|
+
* Measured 2026-09-07 on gt4.geetest.com — every live GeeTest attempt ended
|
|
42
|
+
* "Could not get bounding box of captcha element" on a panel the vendor had
|
|
43
|
+
* just accepted, and each was recorded as the model being wrong.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isStaleHandleError(message: string): boolean;
|
|
12
46
|
export declare const SOLVE_DEFAULTS: {
|
|
13
47
|
readonly maxSolveLoops: 6;
|
|
14
48
|
readonly overallSolveTimeoutMs: 45000;
|
|
49
|
+
/**
|
|
50
|
+
* How long to hold a click waiting for the screen the model answered about.
|
|
51
|
+
*
|
|
52
|
+
* 9000, not the 6000 it was. A GeeTest svg board dwells p50 1.5s / p75 2.0s /
|
|
53
|
+
* max 2.7s per screen, so a 3-screen cycle runs 4.5s median and 8.1s worst
|
|
54
|
+
* case — a 6s budget could not catch the worst case however well aimed, and
|
|
55
|
+
* gave up one screen short. Named here rather than left as a literal in two
|
|
56
|
+
* places because `video_budget_ms` is derived from it on both ports and the
|
|
57
|
+
* arithmetic has to agree.
|
|
58
|
+
*/
|
|
59
|
+
readonly keyframeWaitTimeoutMs: 9000;
|
|
15
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* How long a keyframe burst may run before it is called HUNG.
|
|
63
|
+
*
|
|
64
|
+
* A hang detector, not a budget: the solve timeout is what bounds real time.
|
|
65
|
+
* This only has to notice a screenshot that never returns, so it is
|
|
66
|
+
* deliberately slack.
|
|
67
|
+
*
|
|
68
|
+
* Sized off the CEILING, because that is what sizes the loop — the burst plans
|
|
69
|
+
* `videoBurstMaxMs` worth of frames, not `videoBurstDurationMs` worth. Must
|
|
70
|
+
* match `burst_hang_deadline_ms` in the python port, which had this guard
|
|
71
|
+
* first and had it sized off the floor: a 120-frame burst got 17s, i.e. 141ms
|
|
72
|
+
* per frame against a 100ms interval, and a burst that simply ran to its
|
|
73
|
+
* ceiling killed the attempt.
|
|
74
|
+
*/
|
|
75
|
+
export declare function burstHangDeadlineMs(cfg: {
|
|
76
|
+
videoBurstMaxMs?: number;
|
|
77
|
+
}): number;
|
|
16
78
|
export declare class CaptchaKrakenSolver {
|
|
17
79
|
private config;
|
|
18
80
|
/** Extra ms this solve has been granted for a recording; see recordKeyframeBurst. */
|
|
19
81
|
private videoBudgetMs;
|
|
20
82
|
private videoBudgetGranted;
|
|
21
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Every gesture goes through here, and it owns the pointer position. See
|
|
85
|
+
* humanize.ts — the solver names gestures, this decides what events they are
|
|
86
|
+
* and how long they take.
|
|
87
|
+
*/
|
|
88
|
+
private human;
|
|
22
89
|
private imageCounter;
|
|
23
90
|
private sessionDebugDir;
|
|
24
91
|
private stepIndex;
|
|
@@ -32,14 +99,100 @@ export declare class CaptchaKrakenSolver {
|
|
|
32
99
|
private cvWorkerBuf;
|
|
33
100
|
private solutionCache;
|
|
34
101
|
private repeatedAnswerSeen;
|
|
102
|
+
/**
|
|
103
|
+
* The ONE recording and the ONE answer for the animated challenge on screen.
|
|
104
|
+
*
|
|
105
|
+
* A cycling board needs a single inference and no more. The burst shows the
|
|
106
|
+
* model every screen; the answer names the cell AND the screen it is on
|
|
107
|
+
* (`frame` / `await_keyframe`); the gate then holds the click until that
|
|
108
|
+
* screen is back up. Nothing about that changes between rounds — the board
|
|
109
|
+
* keeps cycling through the same pictures with the same target — so asking
|
|
110
|
+
* again buys nothing and costs a burst plus a multi-image inference.
|
|
111
|
+
*
|
|
112
|
+
* It was asking again every round. Loops 3, 4 and 5 each re-recorded and
|
|
113
|
+
* re-inferred, which is what exhausted a 66s budget on a puzzle whose whole
|
|
114
|
+
* task is one click.
|
|
115
|
+
*
|
|
116
|
+
* Dropped when the gate reports it never saw the chosen screen: that means
|
|
117
|
+
* the widget is no longer the board this plan was made for, which is exactly
|
|
118
|
+
* when a fresh recording is warranted.
|
|
119
|
+
*/
|
|
120
|
+
private animatedPlan;
|
|
35
121
|
private keyframeMode;
|
|
122
|
+
/**
|
|
123
|
+
* How many distinct steady screens the current clip sits on; 0 if it is not
|
|
124
|
+
* that kind of clip. THE GATE KEYS ON THIS, NOT ON `keyframeMode`.
|
|
125
|
+
*
|
|
126
|
+
* `even` was being read as "no state recurs, do not wait". It does not mean
|
|
127
|
+
* that — it means the slicer could not PROVE recurrence, which for a board
|
|
128
|
+
* whose loop is longer than a 4s burst it never can. 32 of 60 real clips
|
|
129
|
+
* sliced `even` while sitting on 2-3 steady screens, so the gate was off on
|
|
130
|
+
* every real animated captcha and the driver clicked whichever screen was up.
|
|
131
|
+
*/
|
|
132
|
+
private keyframeSteadyScreens;
|
|
133
|
+
/**
|
|
134
|
+
* When the current solve must be over, in `Date.now()` terms. 0 outside one.
|
|
135
|
+
*
|
|
136
|
+
* The budget used to be checked ONLY at the top of each solve loop, so any
|
|
137
|
+
* single round could overrun it without limit — and an animated round is the
|
|
138
|
+
* long one: a 4s burst of 40 element screenshots, a multi-image inference,
|
|
139
|
+
* then a gated click per target. Measured on GeeTest svg, 2026-09-07: rounds
|
|
140
|
+
* that re-recorded ran past a 66s budget to the harness's own 180s ceiling
|
|
141
|
+
* and were reported "hung, not slow", which is a message about the model for
|
|
142
|
+
* a driver that simply never looked at the clock.
|
|
143
|
+
*
|
|
144
|
+
* The Python port has always checked mid-wait (`_check_deadline`), so this
|
|
145
|
+
* was also a cross-port divergence on exactly the puzzles Tier 3 times.
|
|
146
|
+
*/
|
|
147
|
+
private solveDeadlineAt;
|
|
36
148
|
private lastAnswerSig;
|
|
37
149
|
private noProgressRounds;
|
|
150
|
+
/** How far up planner.RESAMPLE_TEMPERATURES this BOARD has escalated. */
|
|
151
|
+
private resampleLevel;
|
|
38
152
|
private state;
|
|
39
153
|
private lastSubmitFrameHash;
|
|
40
154
|
private solveSessionId;
|
|
155
|
+
/** Interpreter + CLI root, resolved once. See resolveCli. */
|
|
156
|
+
private cliCache;
|
|
157
|
+
/** The LoRA name models.json calls `latest`, read once. See askModel. */
|
|
158
|
+
private loraNameCache;
|
|
159
|
+
/**
|
|
160
|
+
* Phase accounting for the CURRENT or MOST RECENT solve; see timing.ts.
|
|
161
|
+
* Null before the first solve, and readable after a failed one.
|
|
162
|
+
*/
|
|
163
|
+
budget: PhaseBudget | null;
|
|
164
|
+
/**
|
|
165
|
+
* Attribute the enclosed wall-clock to `name` for this solve's budget.
|
|
166
|
+
*
|
|
167
|
+
* A no-op outside a solve, so every helper stays callable from tests and from
|
|
168
|
+
* the watcher without a budget having been opened.
|
|
169
|
+
*/
|
|
170
|
+
private ph;
|
|
41
171
|
constructor(config?: CaptchaKrakenConfig);
|
|
172
|
+
/**
|
|
173
|
+
* Where the pointer is. Owned by the humanizer, because a mode that
|
|
174
|
+
* dispatches no motion (mobile, between taps) still has to answer this.
|
|
175
|
+
*/
|
|
176
|
+
private get lastMousePosition();
|
|
177
|
+
private set lastMousePosition(value);
|
|
42
178
|
solve(page: Page): Promise<SolveResult | void>;
|
|
179
|
+
/**
|
|
180
|
+
* Tell the hosted API whether the widget accepted — the one fact the server
|
|
181
|
+
* cannot see for itself, since a wrong answer reaches it as a 200 with
|
|
182
|
+
* well-formed JSON in it.
|
|
183
|
+
*
|
|
184
|
+
* THROUGH THE PYTHON CLI, not a `fetch` here. The endpoint, the credential,
|
|
185
|
+
* the extra-header handling and the "one 404 and stop asking" rule all live
|
|
186
|
+
* in `planner.report_outcome`; a second copy in TypeScript would agree with
|
|
187
|
+
* it until one of them was edited. That is the drift `model-name.ts` exists
|
|
188
|
+
* to prevent, on the same seam, and this port already crosses that subprocess
|
|
189
|
+
* boundary for every model call.
|
|
190
|
+
*
|
|
191
|
+
* NOT AWAITED, and detached. The solve is over and its result is decided, so
|
|
192
|
+
* nothing here may add latency to what the caller gets back or keep the
|
|
193
|
+
* process alive past it. `unref()` is what makes the second true.
|
|
194
|
+
*/
|
|
195
|
+
private reportOutcome;
|
|
43
196
|
/**
|
|
44
197
|
* Install an auto-solver: probe `page` on a timer and solve any captcha that
|
|
45
198
|
* becomes visible, until `stop()`.
|
|
@@ -72,16 +225,37 @@ export declare class CaptchaKrakenSolver {
|
|
|
72
225
|
private getVerifyButton;
|
|
73
226
|
private hasNonEmptyFieldValue;
|
|
74
227
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
228
|
+
* WHICH of reCAPTCHA's three bframe banners is showing, if any.
|
|
229
|
+
*
|
|
230
|
+
* They share a corner and a look and they mean different things:
|
|
231
|
+
*
|
|
232
|
+
* `rejected` "Please try again." The answer was wrong; a fresh board
|
|
233
|
+
* follows, so the useful response is to solve that one.
|
|
234
|
+
* `select-more` "Please select all matching images." Under-selected, and
|
|
235
|
+
* the tiles do NOT refresh — so a driver that re-submits
|
|
236
|
+
* the same answer loops until the session times out. This
|
|
237
|
+
* is the one the missed-tiles retry and the abort exist for.
|
|
238
|
+
* `dynamic-more` "Please also check the new images." NOT AN ERROR. It is
|
|
239
|
+
* the dynamic 3x3's normal flow: cleared tiles fade out,
|
|
240
|
+
* replacements fade in, and the widget says so — on
|
|
241
|
+
* essentially every round of that variant.
|
|
242
|
+
*
|
|
243
|
+
* This used to return a boolean over all three, which made the third one
|
|
244
|
+
* indistinguishable from the first two and armed the one-retry abort with the
|
|
245
|
+
* sentence that means "you are doing fine". Every dynamic board therefore
|
|
246
|
+
* died at round two while the vendor was still dealing. Measured 2026-09-06:
|
|
247
|
+
* three of three `recaptcha_3x3_fade` attempts stopped at exactly boards=2,
|
|
248
|
+
* against `recaptcha_4x4` passing at 2, 3 and 5 boards on the same run.
|
|
83
249
|
*/
|
|
84
|
-
|
|
250
|
+
/**
|
|
251
|
+
* May a repeat of this banner end the solve?
|
|
252
|
+
*
|
|
253
|
+
* Only for the banners where repeating means STUCK. `dynamic-more` repeating
|
|
254
|
+
* means the board is still being cleared, which is progress, and treating it
|
|
255
|
+
* as fatal is the bug this pair of methods was split to fix.
|
|
256
|
+
*/
|
|
257
|
+
private bannerIsFatalAfterRetry;
|
|
258
|
+
private recaptchaBannerKind;
|
|
85
259
|
private isRecaptchaAnchorChecked;
|
|
86
260
|
private isHcaptchaAnchorChecked;
|
|
87
261
|
/**
|
|
@@ -133,6 +307,25 @@ export declare class CaptchaKrakenSolver {
|
|
|
133
307
|
* `size=invisible` in the src, and never an `api2/bframe` challenge frame, so
|
|
134
308
|
* we exclude the invisible variant here.
|
|
135
309
|
*/
|
|
310
|
+
/**
|
|
311
|
+
* Which vendors' code this page actually loaded.
|
|
312
|
+
*
|
|
313
|
+
* Read out of the page's own resource timing plus the `src`/`href` of
|
|
314
|
+
* everything it linked, so it works whenever it is called — a request
|
|
315
|
+
* listener would have to have been attached before navigation, and by the
|
|
316
|
+
* time a solve fails that ship has sailed.
|
|
317
|
+
*
|
|
318
|
+
* Best-effort by construction: the resource-timing buffer is finite and a
|
|
319
|
+
* page may have cleared it. An empty answer is "nothing seen", never "no
|
|
320
|
+
* captcha here". Mirrors `vendors_on_the_wire` in page_solver.py.
|
|
321
|
+
*/
|
|
322
|
+
vendorsOnTheWire(page: Page): Promise<string[]>;
|
|
323
|
+
/**
|
|
324
|
+
* Why nothing was found — the two cases told apart. The old text guessed
|
|
325
|
+
* "reCAPTCHA v3 / invisible" every time, including the times a vendor had
|
|
326
|
+
* simply moved its DOM. See VENDOR_URL_MARKERS.
|
|
327
|
+
*/
|
|
328
|
+
private noWidgetMessage;
|
|
136
329
|
hasInteractiveWidgetInDom(page: Page): Promise<boolean>;
|
|
137
330
|
detectCaptcha(page: Page): Promise<ElementHandle | null>;
|
|
138
331
|
/**
|
|
@@ -158,6 +351,14 @@ export declare class CaptchaKrakenSolver {
|
|
|
158
351
|
* falling back to the configured/`python` command. Throws if the CLI folder
|
|
159
352
|
* is missing — callers that must not throw (e.g. runCliTool) wrap this.
|
|
160
353
|
*/
|
|
354
|
+
/**
|
|
355
|
+
* The adapter name models.json calls `latest`, resolved once per solver.
|
|
356
|
+
*
|
|
357
|
+
* `resolveLoraName` reads models.json (and pinned_model.json) off disk
|
|
358
|
+
* SYNCHRONOUSLY, and it was doing that on every inference for a file that
|
|
359
|
+
* cannot change inside one process.
|
|
360
|
+
*/
|
|
361
|
+
private loraName;
|
|
161
362
|
private resolveCli;
|
|
162
363
|
/**
|
|
163
364
|
* Run an OpenCV tool subcommand of the CLI (e.g. `grid-cell-states a.png
|
|
@@ -327,6 +528,28 @@ export declare class CaptchaKrakenSolver {
|
|
|
327
528
|
* keyframes the wait gate re-reads on every poll live inside it.
|
|
328
529
|
*/
|
|
329
530
|
private recordKeyframeBurst;
|
|
531
|
+
/**
|
|
532
|
+
* Start recording NOW, and let the caller decide later what it was for.
|
|
533
|
+
*
|
|
534
|
+
* This is what makes the speculative path possible. The burst is the only
|
|
535
|
+
* thing that can tell a cycling board from a still one, and it is also the
|
|
536
|
+
* recording that answers it — so it is started at the same moment the still
|
|
537
|
+
* screenshot goes to the model, and the two run together.
|
|
538
|
+
*
|
|
539
|
+
* - the board never moves -> `moved()` stays false, the still answer is
|
|
540
|
+
* used, and the frames are thrown away. The recording cost nothing but
|
|
541
|
+
* CPU, because it happened inside an inference we were already waiting
|
|
542
|
+
* for.
|
|
543
|
+
* - the board moves -> the still answer is DISCARDED unread, the
|
|
544
|
+
* burst runs on to the end of the cycle, and the multi-image answer is
|
|
545
|
+
* the one acted on. Two inference CALLS, but only about one inference of
|
|
546
|
+
* wall-clock, because the first ran during the recording.
|
|
547
|
+
*
|
|
548
|
+
* The old shape paid for that knowledge in whole ROUNDS: answer a still, act
|
|
549
|
+
* on it, notice nothing changed, answer another still, and only then record.
|
|
550
|
+
* ~15s of a 40s solve to learn something the first four frames already knew.
|
|
551
|
+
*/
|
|
552
|
+
private startKeyframeBurst;
|
|
330
553
|
/**
|
|
331
554
|
* Slice a recorded burst into keyframes and solve them in ONE model request.
|
|
332
555
|
*
|
|
@@ -362,8 +585,8 @@ export declare class CaptchaKrakenSolver {
|
|
|
362
585
|
* one-way fade, a sprite crossing — so there is no state to come back to and
|
|
363
586
|
* this can only run out its full 6s, PER CLICK, before clicking the
|
|
364
587
|
* coordinates it already had. It is also the normal case, not a corner: all
|
|
365
|
-
* 116 real clips under
|
|
366
|
-
* fired on real footage. Measured on
|
|
588
|
+
* 116 real clips under the held-out sample corpus are `even` and `cycle` has never
|
|
589
|
+
* fired on real footage. Measured on an hCaptcha rotating-object animation: 6.0s of a
|
|
367
590
|
* 28.8s solve, closest region diff 0.0721 against a 0.05 tolerance, then the
|
|
368
591
|
* same click, then solved. Kept for `cycle`/`static`, where the state does
|
|
369
592
|
* come back and waiting is the difference between the sprite and background.
|
|
@@ -414,6 +637,20 @@ export declare class CaptchaKrakenSolver {
|
|
|
414
637
|
* never animated, so escalating there would swap a path that works for one
|
|
415
638
|
* that cannot read a grid.
|
|
416
639
|
*/
|
|
640
|
+
/** Forget the recorded answer, and delete the frames it was holding. */
|
|
641
|
+
private discardAnimatedPlan;
|
|
642
|
+
/**
|
|
643
|
+
* Should this round record while it asks?
|
|
644
|
+
*
|
|
645
|
+
* reCAPTCHA is excluded for the same reason it is excluded from the animated
|
|
646
|
+
* path at all: its dynamic 3x3 REPLACES tiles in place, has its own
|
|
647
|
+
* multi-round driver with its own fade gates, and its grids are never
|
|
648
|
+
* animated — so a burst there would film a fade and call it a cycle.
|
|
649
|
+
*
|
|
650
|
+
* Distorted-text rounds are excluded because the answer is a string, not a
|
|
651
|
+
* place, and nothing about a recording helps read one.
|
|
652
|
+
*/
|
|
653
|
+
private shouldSpeculate;
|
|
417
654
|
private shouldRetryAsAnimated;
|
|
418
655
|
private getSolution;
|
|
419
656
|
/** One inference: build the CLI invocation, run it, parse what comes back. */
|
|
@@ -485,17 +722,48 @@ export declare class CaptchaKrakenSolver {
|
|
|
485
722
|
/**
|
|
486
723
|
* Run `fn` (typically the model query) while idly drifting the cursor over
|
|
487
724
|
* the captcha, so the mouse behaves like a human weighing the options instead
|
|
488
|
-
* of freezing during inference. Uses the same
|
|
489
|
-
*
|
|
490
|
-
* is swallowed and never fails the solve.
|
|
725
|
+
* of freezing during inference. Uses the same humanizer as real clicks, so a
|
|
726
|
+
* mode with no cursor skips it entirely; cancelled the instant `fn` resolves.
|
|
727
|
+
* Best-effort — any wander error is swallowed and never fails the solve.
|
|
728
|
+
* Disable via config.idleMouseWander.
|
|
491
729
|
*/
|
|
492
730
|
private withIdleWander;
|
|
493
731
|
move(page: Page, selectorOrElement: string | ElementHandle, options?: {
|
|
494
732
|
paddingPercentage?: number;
|
|
495
733
|
}): Promise<void>;
|
|
496
734
|
moveAndClick(page: Page, element: ElementHandle): Promise<void>;
|
|
735
|
+
/**
|
|
736
|
+
* Travel to a point. What that MEANS is the humanizer's business — on a
|
|
737
|
+
* touchscreen with no finger down it is a bookkeeping update and emits
|
|
738
|
+
* nothing at all.
|
|
739
|
+
*/
|
|
497
740
|
private performSmoothMove;
|
|
498
|
-
|
|
741
|
+
/**
|
|
742
|
+
* Where in the element this click lands, in element-relative pixels.
|
|
743
|
+
*
|
|
744
|
+
* Split out of `executeClick` because on an animated board the point has to
|
|
745
|
+
* be chosen ONCE and then used three times — to park the pointer, to ask
|
|
746
|
+
* `match-region` about the right neighbourhood, and to press. Choosing it
|
|
747
|
+
* inside the click (it is randomised within the target) meant the gate
|
|
748
|
+
* watched the bbox centre while the press landed somewhere else.
|
|
749
|
+
*/
|
|
750
|
+
private clickPointFor;
|
|
751
|
+
/**
|
|
752
|
+
* Click one target on an ANIMATED board, at the moment its screen is up.
|
|
753
|
+
*
|
|
754
|
+
* The order is the point of it. The pointer is parked on the target FIRST,
|
|
755
|
+
* then the gate opens, then the press happens in place — so the only thing
|
|
756
|
+
* between "the right screen is showing" and the click is a mouse-down.
|
|
757
|
+
*
|
|
758
|
+
* It used to wait and then call `executeClick`, which begins with a humanised
|
|
759
|
+
* move: 274ms p10 / 398ms p50 / 647ms max across a 340x384 widget, against a
|
|
760
|
+
* 1500ms median dwell. A quarter to a third of the screen's life spent
|
|
761
|
+
* travelling after we had already confirmed it was the right one — so the
|
|
762
|
+
* gate could do its job and the click still land on the next screen.
|
|
763
|
+
* `move()` short-circuits when it is already at the target, so parking early
|
|
764
|
+
* costs nothing and removes all of it.
|
|
765
|
+
*/
|
|
766
|
+
private clickWhenFrameMatches;
|
|
499
767
|
private executeClick;
|
|
500
768
|
private executeDrag;
|
|
501
769
|
/**
|
|
@@ -508,6 +776,30 @@ export declare class CaptchaKrakenSolver {
|
|
|
508
776
|
* the document, and the answer would go there.
|
|
509
777
|
*/
|
|
510
778
|
private findControl;
|
|
779
|
+
/**
|
|
780
|
+
* Where a distorted-text captcha's answer goes.
|
|
781
|
+
*
|
|
782
|
+
* Inside the widget first. If the widget holds no text box at all, widen ONCE
|
|
783
|
+
* to its enclosing <fieldset>/<form> and look again for a VENDOR-NAMED box
|
|
784
|
+
* only.
|
|
785
|
+
*
|
|
786
|
+
* BotDetect is why, and nothing else needs it. BotDetect is a self-hosted
|
|
787
|
+
* LIBRARY, so the host application owns the layout: measured on captcha.com
|
|
788
|
+
* 2026-08-24, `.BDC_CaptchaDiv` — the element detectCaptcha returns — is
|
|
789
|
+
* 280x50 and holds the image alone, while `#captchaCode` sits in a sibling
|
|
790
|
+
* `<div class="validationDiv">` under the enclosing <fieldset>. Scoped to the
|
|
791
|
+
* widget the solver found no box, textMode stayed false, and it read the code
|
|
792
|
+
* correctly and never typed it. A clean solve thrown away, and
|
|
793
|
+
* indistinguishable in any report from a model that cannot read warped text.
|
|
794
|
+
*
|
|
795
|
+
* The widened pass is restricted to TEXT_INPUT_VENDOR_SELECTORS on purpose.
|
|
796
|
+
* The generic tail is what makes the in-widget lookup work for every other
|
|
797
|
+
* vendor, and OUTSIDE the widget it is exactly how a captcha's answer ends up
|
|
798
|
+
* in a login form's username box. One level, named selectors, never the page.
|
|
799
|
+
*
|
|
800
|
+
* Mirrors `_answer_box` in page_solver.py — CLAUDE.md 1c.
|
|
801
|
+
*/
|
|
802
|
+
private answerBox;
|
|
511
803
|
/** Put the model's reading of a distorted-text captcha into its box. */
|
|
512
804
|
private executeType;
|
|
513
805
|
/** `captchakraken track-piece` — box of what moved, handle masked out. */
|
|
@@ -532,5 +824,11 @@ export declare class CaptchaKrakenSolver {
|
|
|
532
824
|
* Returns false if there is nothing here to drag, leaving the caller's normal
|
|
533
825
|
* no-op handling to deal with it.
|
|
534
826
|
*/
|
|
827
|
+
/**
|
|
828
|
+
* Device pixels per CSS pixel, read off the shot we are about to measure.
|
|
829
|
+
* 1 when the image cannot be read — an unreadable shot is already the
|
|
830
|
+
* trackPiece-returns-null path, and guessing a ratio would steer by it.
|
|
831
|
+
*/
|
|
832
|
+
private shotScale;
|
|
535
833
|
private executeSlide;
|
|
536
834
|
}
|