captchakraken 2.9.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +112 -0
- package/NOTICE +36 -0
- package/dist/cli-invocation.d.ts +7 -36
- package/dist/cli-invocation.js +8 -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 +131 -708
- package/dist/solver.js +1319 -3382
- 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 -493
- package/dist/watcher.d.ts +5 -78
- package/dist/watcher.js +0 -56
- package/package.json +11 -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 -883
- 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 +46 -484
- package/python/src/captchakraken/kinds.py +152 -0
- package/python/src/captchakraken/models.json +36 -4
- package/python/src/captchakraken/overlay.py +65 -498
- package/python/src/captchakraken/page_solver.py +1282 -3101
- package/python/src/captchakraken/planner.py +65 -487
- 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/solver.d.ts
CHANGED
|
@@ -1,834 +1,257 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Browser page driver. Finds the widget, photographs it, asks the bundled Python engine what to do,
|
|
3
|
+
* and performs the answer. Mirrors python/src/captchakraken/page_solver.py.
|
|
4
|
+
*/
|
|
5
|
+
import { PlaywrightPage as Page, PlaywrightElementHandle as ElementHandle, PlaywrightLocator as Locator } from './playwright-types';
|
|
2
6
|
import { CaptchaWatcher, WatchOptions } from './watcher';
|
|
3
7
|
import { PhaseBudget } from './timing';
|
|
4
8
|
import { CaptchaKrakenConfig, SolveResult } from './types';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
*/
|
|
9
|
+
import { FrameRole, Vendor } from './kinds';
|
|
10
|
+
/** What detection found: the handle to photograph, the locator to search inside, and which table entry named it. */
|
|
11
|
+
export interface Widget {
|
|
12
|
+
el: ElementHandle;
|
|
13
|
+
at: Locator;
|
|
14
|
+
vendor: Vendor;
|
|
15
|
+
role: FrameRole;
|
|
16
|
+
}
|
|
17
|
+
/** An allow-list of what needs nothing: the other direction raises instead of clicking at the origin. */
|
|
25
18
|
export declare function answerNeedsElementBox(actions: ReadonlyArray<{
|
|
26
19
|
action?: string;
|
|
27
20
|
}>): 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
|
-
*/
|
|
21
|
+
/** The widget moved on under us (hCaptcha swapped rounds, GeeTest closed on accept): re-detect, do not fail. */
|
|
45
22
|
export declare function isStaleHandleError(message: string): boolean;
|
|
46
23
|
export declare const SOLVE_DEFAULTS: {
|
|
47
24
|
readonly maxSolveLoops: 6;
|
|
48
25
|
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
26
|
readonly keyframeWaitTimeoutMs: 9000;
|
|
60
27
|
};
|
|
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
|
-
*/
|
|
28
|
+
/** A hang detector for a burst whose screenshot never returns, sized off the ceiling. */
|
|
75
29
|
export declare function burstHangDeadlineMs(cfg: {
|
|
76
30
|
videoBurstMaxMs?: number;
|
|
77
31
|
}): number;
|
|
78
32
|
export declare class CaptchaKrakenSolver {
|
|
79
33
|
private config;
|
|
80
|
-
/** Extra ms this solve has been granted for a recording; see recordKeyframeBurst. */
|
|
81
34
|
private videoBudgetMs;
|
|
82
35
|
private videoBudgetGranted;
|
|
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
36
|
private human;
|
|
89
|
-
private imageCounter;
|
|
90
|
-
private sessionDebugDir;
|
|
91
37
|
private stepIndex;
|
|
92
38
|
private solveStartMs;
|
|
93
|
-
private gridDebugDir;
|
|
94
|
-
private gridDebugSeq;
|
|
95
39
|
private cvWorker;
|
|
96
40
|
private cvWorkerReady;
|
|
97
41
|
private cvWorkerSeq;
|
|
98
42
|
private cvWorkerPending;
|
|
99
43
|
private cvWorkerBuf;
|
|
44
|
+
/** Answers keyed by screenshot hash; a hit means the answer already ran and changed nothing. */
|
|
100
45
|
private solutionCache;
|
|
101
46
|
private repeatedAnswerSeen;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
*/
|
|
47
|
+
private retriedUnusableAnswer;
|
|
48
|
+
private knownAnimated;
|
|
49
|
+
private animatedProbeDone;
|
|
50
|
+
/** The one recording for the animated board on screen, and its answer until the widget refuses it. */
|
|
120
51
|
private animatedPlan;
|
|
121
52
|
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
53
|
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
54
|
private solveDeadlineAt;
|
|
148
55
|
private lastAnswerSig;
|
|
149
56
|
private noProgressRounds;
|
|
150
|
-
/** How far up planner.RESAMPLE_TEMPERATURES this BOARD has escalated. */
|
|
151
57
|
private resampleLevel;
|
|
152
|
-
|
|
58
|
+
/** The recording `classifyByRecording` started and left running for the branch below to finish or drop. */
|
|
59
|
+
private pendingBurst;
|
|
60
|
+
/**
|
|
61
|
+
* The camera on the animated board in front of us, running across rounds until that board is gone.
|
|
62
|
+
*
|
|
63
|
+
* Distinct from `pendingBurst`, which is the speculative film one round starts and the same round spends.
|
|
64
|
+
* This one outlives a rejected answer on purpose: it is the only thing that can give the next ask a
|
|
65
|
+
* different question to answer, since the answer itself is deterministic at temperature 0.
|
|
66
|
+
*/
|
|
67
|
+
private animatedFilm;
|
|
68
|
+
private lastBurstFps;
|
|
69
|
+
private actedOnBoard;
|
|
153
70
|
private lastSubmitFrameHash;
|
|
154
71
|
private solveSessionId;
|
|
155
|
-
/** Interpreter + CLI root, resolved once. See resolveCli. */
|
|
156
72
|
private cliCache;
|
|
157
|
-
/** The LoRA name models.json calls `latest`, read once. See askModel. */
|
|
158
73
|
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
74
|
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
75
|
private ph;
|
|
171
76
|
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
77
|
private get lastMousePosition();
|
|
177
78
|
private set lastMousePosition(value);
|
|
79
|
+
/** `disabled` freezes CSS animation, which once sliced GeeTest svg to a static clip; callers filming motion pass `allow`. */
|
|
80
|
+
private shot;
|
|
178
81
|
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
|
-
*/
|
|
82
|
+
/** Tell the hosted API whether the widget accepted, through the CLI, detached, never awaited. */
|
|
195
83
|
private reportOutcome;
|
|
196
|
-
/**
|
|
197
|
-
* Install an auto-solver: probe `page` on a timer and solve any captcha that
|
|
198
|
-
* becomes visible, until `stop()`.
|
|
199
|
-
*
|
|
200
|
-
* Returns immediately — the watching happens in the background, so your
|
|
201
|
-
* automation carries on and captchas are handled underneath it.
|
|
202
|
-
*
|
|
203
|
-
* Nothing is injected into the page on any platform; see watcher.ts for why,
|
|
204
|
-
* and for the note on Camoufox, where the probe's DOM reads land in its
|
|
205
|
-
* isolated world by default.
|
|
206
|
-
*
|
|
207
|
-
* ```typescript
|
|
208
|
-
* const solver = new CaptchaKrakenSolver();
|
|
209
|
-
* const watcher = solver.watch(page);
|
|
210
|
-
* await page.goto('https://example.com/protected'); // solved as it appears
|
|
211
|
-
* await watcher.stop();
|
|
212
|
-
* ```
|
|
213
|
-
*/
|
|
84
|
+
/** Solve captchas on `page` as they appear, until `stop()`. Injects nothing into the page. */
|
|
214
85
|
watch(page: Page, options?: WatchOptions): CaptchaWatcher;
|
|
215
86
|
private solveImpl;
|
|
216
|
-
/**
|
|
217
|
-
* Fire the optional onStep observer with a fresh screenshot of the captcha
|
|
218
|
-
* element. No-op (beyond a cheap early return) when no callback is set, so it
|
|
219
|
-
* stays off the critical path in normal runs. The emitted PNG is owned by the
|
|
220
|
-
* callback — we never delete it. Best-effort: a screenshot or callback error
|
|
221
|
-
* never fails the solve.
|
|
222
|
-
*/
|
|
87
|
+
/** Fire the optional onStep observer with a fresh screenshot; best-effort, never fails the solve. */
|
|
223
88
|
private emitStep;
|
|
224
89
|
private solveSingle;
|
|
225
90
|
private getVerifyButton;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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.
|
|
249
|
-
*/
|
|
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
|
-
*/
|
|
91
|
+
/** Any element at `selector` carrying a non-blank value; response fields are hidden, so no visibility filter. */
|
|
92
|
+
private hasValue;
|
|
93
|
+
/** A visible match carrying text; an empty banner or prompt is a placeholder, not a signal. */
|
|
94
|
+
private visibleWithText;
|
|
95
|
+
private checkedIn;
|
|
257
96
|
private bannerIsFatalAfterRetry;
|
|
258
|
-
|
|
259
|
-
private
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* True the moment the vendor reports the whole captcha solved — the anchor
|
|
263
|
-
* checkbox flipped to checked, or the response token got populated. This is a
|
|
264
|
-
* definitive "done" signal that a lingering, animating-closed challenge frame
|
|
265
|
-
* is not: after the final submit, hCaptcha keeps the challenge iframe VISIBLE
|
|
266
|
-
* for a couple of seconds while it verifies, so treating that frame as a fresh
|
|
267
|
-
* puzzle (the old behavior) burned ~18s re-running the pipeline on it.
|
|
268
|
-
*/
|
|
97
|
+
/** Which verdict banner the open challenge shows; `dynamic-more` is the dynamic board's normal flow, not an error. */
|
|
98
|
+
private bannerKind;
|
|
99
|
+
/** The vendor's own done signal: a response token, a painted success state, or a checked box. */
|
|
269
100
|
private isCaptchaSolved;
|
|
270
|
-
/**
|
|
271
|
-
* True when an image challenge is open AND has actually rendered its prompt —
|
|
272
|
-
* i.e. a fresh round we should solve, as opposed to a frame animating closed
|
|
273
|
-
* (whose prompt has already gone). Used to tell "next round" from "solved,
|
|
274
|
-
* closing" after a submit without waiting out a fixed timeout.
|
|
275
|
-
*/
|
|
101
|
+
/** A next round has painted, as opposed to the answered frame animating closed. */
|
|
276
102
|
private isChallengeFreshlyRendered;
|
|
277
103
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* hCaptcha renders each grid tile as a `.task-image .image` div whose
|
|
282
|
-
* `background-image` is set once the asset loads; the prompt sits in
|
|
283
|
-
* `.prompt-text`. Image-select (click/drag) challenges use a single
|
|
284
|
-
* `.challenge-example` / `canvas` surface instead. We wait for either family
|
|
285
|
-
* to be present AND for the background-image URLs to be populated (not the
|
|
286
|
-
* empty `url("")` placeholder hCaptcha ships before the asset arrives).
|
|
287
|
-
*
|
|
288
|
-
* Best-effort: a timeout or a missing content frame just falls through to the
|
|
289
|
-
* screenshot rather than throwing — the existing fail-fast path still covers a
|
|
290
|
-
* genuinely unsupported puzzle.
|
|
291
|
-
*/
|
|
292
|
-
private waitForHcaptchaChallengeImages;
|
|
293
|
-
/**
|
|
294
|
-
* Stage-1 detection: is an *interactive* captcha widget present in the DOM at
|
|
295
|
-
* all — even if its iframe hasn't finished rendering yet?
|
|
296
|
-
*
|
|
297
|
-
* This is deliberately broader than detectCaptcha (which only returns a
|
|
298
|
-
* VISIBLE, not-yet-solved element). We use it to distinguish two cases that
|
|
299
|
-
* detectCaptcha() === null cannot tell apart:
|
|
300
|
-
*
|
|
301
|
-
* - A reCAPTCHA-v2 / hCaptcha widget IS in the DOM but is still loading
|
|
302
|
-
* (iframe present, glyph not painted) → we should WAIT for it.
|
|
303
|
-
* - There is no interactive widget — reCAPTCHA v3 (score-based, invisible)
|
|
304
|
-
* or an hCaptcha that only triggers on a user action → we must FAIL FAST.
|
|
305
|
-
*
|
|
306
|
-
* reCAPTCHA v3 injects only `iframe[src*="recaptcha/api2/anchor"]` with
|
|
307
|
-
* `size=invisible` in the src, and never an `api2/bframe` challenge frame, so
|
|
308
|
-
* we exclude the invisible variant here.
|
|
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.
|
|
104
|
+
* Best-effort: hold until the board's pictures have painted. Nothing to wait for is ready, and so is no prompt: waiting
|
|
105
|
+
* on the prompt as a selector rejects when absent and paid the whole timeout per board.
|
|
327
106
|
*/
|
|
107
|
+
private waitForBoardImages;
|
|
108
|
+
/** Which vendors' code the page loaded, from resource timing and linked URLs. A tripwire, not a detector. */
|
|
109
|
+
vendorsOnTheWire(page: Page): Promise<Vendor[]>;
|
|
328
110
|
private noWidgetMessage;
|
|
329
|
-
hasInteractiveWidgetInDom(page: Page): Promise<boolean>;
|
|
330
|
-
detectCaptcha(page: Page): Promise<ElementHandle | null>;
|
|
331
|
-
/**
|
|
332
|
-
* Initialize a fresh dump directory for one reCAPTCHA 3x3 dynamic-driver
|
|
333
|
-
* session. Frames + a state.jsonl log land here so the click/fade/wait timing
|
|
334
|
-
* can be replayed offline. Gated on CAPTCHA_DEBUG=1 — the per-frame dumps and
|
|
335
|
-
* extra state queries add latency, so they stay off in normal runs. Set
|
|
336
|
-
* CAPTCHA_DEBUG=1 to capture them when diagnosing timing. Best-effort.
|
|
337
|
-
*/
|
|
338
|
-
private initGridDebug;
|
|
339
111
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* copies that frame into the dump dir under a sequenced, labeled name so the
|
|
343
|
-
* record can be matched to the exact pixels the detector saw. Best-effort.
|
|
344
|
-
*/
|
|
345
|
-
private gridDebug;
|
|
346
|
-
private saveImageForDebug;
|
|
347
|
-
private archiveLatestDebugRun;
|
|
348
|
-
/**
|
|
349
|
-
* Resolve the bundled CaptchaKraken CLI root and the python interpreter to
|
|
350
|
-
* run it with. Prefers the packaged venv python (postinstall bootstrap),
|
|
351
|
-
* falling back to the configured/`python` command. Throws if the CLI folder
|
|
352
|
-
* is missing — callers that must not throw (e.g. runCliTool) wrap this.
|
|
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.
|
|
112
|
+
* Is a widget in the DOM at all, rendered or not? Invisible reCAPTCHA is excluded by its selector. The inline vendors
|
|
113
|
+
* count: without them this port failed fast in under a second on every GeeTest/Yidun page and disagreed with Python in Tier 3.
|
|
360
114
|
*/
|
|
115
|
+
hasInteractiveWidgetInDom(page: Page): Promise<boolean>;
|
|
116
|
+
/** Open challenges first, then unsolved checkboxes, then the inline vendors; every probe is queried at once. */
|
|
117
|
+
detectCaptcha(page: Page): Promise<Widget | null>;
|
|
118
|
+
/** A checkbox the vendor has already accepted is not a captcha to solve. */
|
|
119
|
+
private alreadyAccepted;
|
|
361
120
|
private loraName;
|
|
121
|
+
/** The `--model` argv, or nothing: without VLLM_BASE_URL the CLI can see a credentials file this process cannot. */
|
|
122
|
+
private modelName;
|
|
362
123
|
private resolveCli;
|
|
363
|
-
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
|
|
124
|
+
/** What is LEFT of the solve, as a bound on one ask.
|
|
125
|
+
*
|
|
126
|
+
* The CLI is a child process and the solve deadline is only read between steps, so nothing stops an
|
|
127
|
+
* ask already in flight — measured against the hosted endpoint, one hung for ~124s inside a 45s
|
|
128
|
+
* budget and the attempt ran 143s. Never below the floor: a keyframe ask carries six images, and a
|
|
129
|
+
* timeout shorter than the work turns a busy endpoint into a guaranteed failure. */
|
|
130
|
+
private askTimeoutMs;
|
|
131
|
+
/** One-shot CLI tool call; `{}` on any failure so polling callers keep going. */
|
|
370
132
|
private runCliTool;
|
|
371
|
-
/**
|
|
372
|
-
* Lazily start the persistent CV worker (`python -m captchakraken.cli serve`) and resolve
|
|
373
|
-
* once it has imported cv2/numpy and emitted its `{"ready":true}` handshake.
|
|
374
|
-
* Returns false if it can't be started (caller then falls back to one-shot
|
|
375
|
-
* subprocesses). Idempotent: subsequent calls await the same readiness promise.
|
|
376
|
-
*/
|
|
133
|
+
/** Start the persistent CV worker once; false when it cannot start, so callers fall back to one-shot spawns. */
|
|
377
134
|
private ensureCvWorker;
|
|
378
|
-
/** Send one request to the CV worker and await its JSON result. Throws on any
|
|
379
|
-
* worker failure so callers can fall back to the one-shot path. */
|
|
380
135
|
private cvWorkerRequest;
|
|
381
|
-
/** Kill the worker and clear state. Safe to call repeatedly. */
|
|
382
136
|
private teardownCvWorker;
|
|
383
|
-
/**
|
|
384
|
-
* Run a CV tool through the persistent worker when available, falling back to a
|
|
385
|
-
* one-shot `runCliTool` subprocess otherwise. `cmd`/`payload` map to the
|
|
386
|
-
* worker's protocol; `fallbackArgs` is the equivalent one-shot argv. Worker
|
|
387
|
-
* results are wrapped to match the one-shot JSON shape:
|
|
388
|
-
* - grid-cell-states[-fixed]: worker returns the states object directly, or
|
|
389
|
-
* {grid:null}; the one-shot returns the same shape, so just pass through.
|
|
390
|
-
* - find-grid: worker returns the array (or null) as `result`.
|
|
391
|
-
* Best-effort: never throws.
|
|
392
|
-
*/
|
|
137
|
+
/** A CV tool through the worker when it is up, else a one-shot subprocess. Never throws. */
|
|
393
138
|
private runCvTool;
|
|
139
|
+
/** A still board is not a loaded board: hold until the panel's centre carries structure. Returns how long it waited. */
|
|
140
|
+
private waitForBoardPainted;
|
|
394
141
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* in over ~1s; capturing mid-fade feeds the LoRA a blank/partial grid.
|
|
398
|
-
*
|
|
399
|
-
* We poll: screenshot the challenge element, keep the last two frames, and ask
|
|
400
|
-
* the CLI's batched `grid-cell-states` (one subprocess per poll) which cells
|
|
401
|
-
* are empty/changing/loaded. We return as soon as every cell is loaded, or on
|
|
402
|
-
* timeout. Best-effort, mirroring `waitForHcaptchaChallengeImages`: never
|
|
403
|
-
* throws, and falls through on timeout so a stuck/odd grid still proceeds to
|
|
404
|
-
* the normal screenshot path. Temp frames are always cleaned up.
|
|
142
|
+
* Screenshot `el` every `intervalMs` until `judge(lastTwoFrames)` answers, or the time is up.
|
|
143
|
+
* A screenshot that fails ends the poll: the widget is gone or going.
|
|
405
144
|
*/
|
|
145
|
+
private pollFrames;
|
|
406
146
|
private waitForGridCellsLoaded;
|
|
407
|
-
/**
|
|
408
|
-
* Read a PNG's pixel dimensions from its IHDR chunk (bytes 16-23, big-endian).
|
|
409
|
-
* Avoids pulling in an image-size dependency. Returns null if the file isn't a
|
|
410
|
-
* readable PNG.
|
|
411
|
-
*/
|
|
412
|
-
private readPngDimensions;
|
|
413
|
-
/**
|
|
414
|
-
* Detect the reCAPTCHA grid once for a puzzle session: screenshot the element,
|
|
415
|
-
* run `find-grid`, and read the screenshot's pixel dimensions. Grid boxes are
|
|
416
|
-
* pixel coords in SCREENSHOT space (not page CSS space). Returns null if no
|
|
417
|
-
* grid is detected. The geometry is stable across the in-place dynamic refresh
|
|
418
|
-
* (only tile images change), so callers cache the result for the session.
|
|
419
|
-
*/
|
|
420
147
|
private getGridBoxes;
|
|
421
|
-
/**
|
|
422
|
-
* Map a model-returned normalized bbox (fractions of the element/screenshot)
|
|
423
|
-
* to a 1-indexed grid cell. Uses the bbox center, converts to screenshot
|
|
424
|
-
* pixels, and returns the cell whose pixel box contains it. Cell numbering is
|
|
425
|
-
* row-major (matches the CLI's find_grid output). Returns null if the center
|
|
426
|
-
* falls outside every cell (e.g. in a gutter) — callers click the raw bbox
|
|
427
|
-
* anyway and skip per-tile tracking.
|
|
428
|
-
*/
|
|
429
148
|
private bboxToCell;
|
|
430
|
-
/**
|
|
431
|
-
* Center of a 1-indexed grid cell in PAGE pixel space, for mouse moves.
|
|
432
|
-
* Converts the cached screenshot-pixel box to page coords via the session's
|
|
433
|
-
* scaleX/scaleY (screenshot px -> page px) and element origin.
|
|
434
|
-
*/
|
|
435
149
|
private cellCenterPage;
|
|
436
|
-
/** Smooth-move the mouse over one cell's center with intra-cell jitter. */
|
|
437
150
|
private hoverCell;
|
|
438
|
-
/**
|
|
439
|
-
* Query per-cell grid state using the SESSION'S CACHED grid boxes via the
|
|
440
|
-
* `grid-cell-states-fixed` CLI command. This is critical: the dynamic refresh
|
|
441
|
-
* blanks tiles to near-white, which makes find_grid fail on that frame, so the
|
|
442
|
-
* self-detecting `grid-cell-states` would return {grid:null} mid-fade and a
|
|
443
|
-
* naive caller would misread that as "nothing loading / solved". Passing the
|
|
444
|
-
* cached boxes keeps empty/changing/selected correct even while tiles are
|
|
445
|
-
* blank. Returns null only on a genuine CLI failure. Best-effort.
|
|
446
|
-
*/
|
|
151
|
+
/** Per-cell state with the session's cached boxes: a blank mid-refresh frame has no lattice to detect. */
|
|
447
152
|
private gridCellStates;
|
|
448
|
-
/** Order a loading set so `priority` cells (just-clicked) come first. */
|
|
449
153
|
private orderByPriority;
|
|
154
|
+
private hoverLoop;
|
|
450
155
|
/**
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* Two answers because reCAPTCHA gives a click one of exactly two replies, and
|
|
454
|
-
* they are the two kinds of board:
|
|
455
|
-
*
|
|
456
|
-
* - `chipped`: the small blue chip landed in the tiles' top-left corners —
|
|
457
|
-
* the photos were KEPT. Nothing is on its way in, the selection is the
|
|
458
|
-
* answer, and the caller should press Verify.
|
|
459
|
-
* - `loading`: the photos are blanking or dissolving under a large centred
|
|
460
|
-
* check — those tiles are being SWAPPED, and what lands may match too, so
|
|
461
|
-
* the board has to be read again.
|
|
462
|
-
*
|
|
463
|
-
* A widget that swaps one clicked cell swaps them all, so the two never share
|
|
464
|
-
* a board and one look at the tiles we just clicked settles it. The chip is
|
|
465
|
-
* what the CV layer reports as `selected` (top-left corner only, behind a
|
|
466
|
-
* circularity and a centroid test a centred check fails), and we already read
|
|
467
|
-
* it on every poll. `chipped` needs EVERY watched tile: a partial reading is a
|
|
468
|
-
* misread, and calling a swapping board finished submits half an answer and
|
|
469
|
-
* burns the attempt, where calling a chipped board unfinished costs one
|
|
470
|
-
* inference.
|
|
471
|
-
*
|
|
472
|
-
* The blank/fade transition lags the click by a beat, so a single snapshot
|
|
473
|
-
* right after clicking misses it (the tile still shows its old image — not yet
|
|
474
|
-
* white, not yet changing). We poll consecutive frames and mark a cell loading
|
|
475
|
-
* if it is `empty` (≥97% near-white) OR `changing` (>2% pixels differ). HOVERS
|
|
476
|
-
* a clicked tile each poll so the mouse keeps moving (no unnatural pauses).
|
|
477
|
-
* Returns as soon as either verdict is in, or `{loading: [], chipped: false}`
|
|
478
|
-
* if the whole window passes with nothing happening (→ solved). Logs every
|
|
479
|
-
* poll + frame.
|
|
156
|
+
* `chipped`: the photos were kept, press Verify. `loading`: those tiles are being swapped, read the board again.
|
|
157
|
+
* Chip is tested first: a chip zooms the photo, which reads as `changing` on the very frame that shows it.
|
|
480
158
|
*/
|
|
481
159
|
private watchClickedTiles;
|
|
482
|
-
/**
|
|
483
|
-
* After loading is detected, wait until at least one of the given blank/fading
|
|
484
|
-
* cells reaches the `loaded` state, HOVERING those cells (in order) the whole
|
|
485
|
-
* time so the mouse never sits still. Returns true once a tile loads, false on
|
|
486
|
-
* timeout (caller proceeds anyway). Uses the session's cached grid boxes so it
|
|
487
|
-
* works while tiles are blank. Logs every poll + frame.
|
|
488
|
-
*/
|
|
489
160
|
private waitForAnyClickedTileLoaded;
|
|
490
|
-
/**
|
|
491
|
-
* Multi-round driver for reCAPTCHA 3x3 dynamic puzzles ("click all X" where
|
|
492
|
-
* tiles refresh in place). One invocation = one puzzle session.
|
|
493
|
-
*
|
|
494
|
-
* The CLI is authoritative about WHAT to do — it runs the blue-badge detector,
|
|
495
|
-
* filters out already-selected and still-loading tiles, and returns one of:
|
|
496
|
-
* - `click`: click these tiles (already filtered to fresh, ready tiles)
|
|
497
|
-
* - `wait` : nothing to click yet, tiles are still loading — do NOT submit
|
|
498
|
-
* - `done` : nothing matching remains — submit (click Verify)
|
|
499
|
-
*
|
|
500
|
-
* This driver owns the HUMAN-LIKE WAITING the CLI can't: after a click round,
|
|
501
|
-
* and on a `wait`, it hovers the just-clicked / currently blank+fading tiles
|
|
502
|
-
* (in click order) and waits for at least one to finish reloading before
|
|
503
|
-
* re-screenshotting and re-solving — so we don't burn a solver call on a grid
|
|
504
|
-
* that's still mid-fade.
|
|
505
|
-
*
|
|
506
|
-
* Only a board that SWAPS a clicked tile out is worth those extra rounds. One
|
|
507
|
-
* that ticks the tile and keeps the photo has been fully answered by the round
|
|
508
|
-
* that clicked it — same as the 4x4 — so `watchClickedTiles` reports the chip
|
|
509
|
-
* and this submits there and then. Rounds 2..N exist for the fading board and
|
|
510
|
-
* nothing else. It submits on `done`, and on a board that ticked our clicks.
|
|
511
|
-
*
|
|
512
|
-
* Returns the same shape as solveSingle so the outer solve loop — including the
|
|
513
|
-
* under-selection retry and post-solve detectCaptcha — wraps it unchanged.
|
|
514
|
-
*/
|
|
161
|
+
/** Click, watch the clicked tiles, re-solve while they swap, submit on `done` or a chipped board. */
|
|
515
162
|
private solveRecaptchaGrid;
|
|
516
163
|
/**
|
|
517
|
-
*
|
|
164
|
+
* Start filming now and decide later what it was for: the classifier, the speculative film, or the
|
|
165
|
+
* recording an animated answer is sliced from. Every window is wall-clock. A cycle is a screen that
|
|
166
|
+
* comes back; a board that shows nothing new for a floor window has settled. There is deliberately no
|
|
167
|
+
* "enough screens, stop" exit: measured on number_with_highest_value_video it failed every seed either way.
|
|
518
168
|
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
169
|
+
* `continuous` keeps the camera rolling past the first answer, for the whole solve. A one-shot burst can
|
|
170
|
+
* only ever show the model the screens that happened to fall inside its window — measured, a 4s burst
|
|
171
|
+
* against a 5.3s cycle showed two screens of three, and when the answer lived on the third there was no
|
|
172
|
+
* second chance, because the reused answer is identical by construction. Filming on means every later
|
|
173
|
+
* round slices a STRICTLY LONGER film: more repetitions for `_detect_cycle` to confirm against, and, on a
|
|
174
|
+
* board that never repeats, six evenly-spread picks across the whole solve instead of across four seconds.
|
|
524
175
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* keyframes the wait gate re-reads on every poll live inside it.
|
|
529
|
-
*/
|
|
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.
|
|
176
|
+
* Longer, but never of TWO boards. A vendor that deals a fresh puzzle on a miss has replaced everything
|
|
177
|
+
* filmed so far, and keyframes cut across both states describe neither. `resume` cuts the film where our
|
|
178
|
+
* answer landed; a screen from before the cut coming back is what un-cuts it. See `snapshot`.
|
|
551
179
|
*/
|
|
552
180
|
private startKeyframeBurst;
|
|
553
|
-
/**
|
|
554
|
-
* Slice a recorded burst into keyframes and solve them in ONE model request.
|
|
555
|
-
*
|
|
556
|
-
* Deliberately not routed through `solveFrameFreshnessGuarded`. That guard
|
|
557
|
-
* re-solves when the frame changes during inference, and an animated challenge
|
|
558
|
-
* changes by definition — every attempt would be judged stale and the whole
|
|
559
|
-
* re-solve budget would burn without ever acting. The `frame` in the answer is
|
|
560
|
-
* the real guard: it names the state to act in, and `waitForKeyframe` enforces it.
|
|
561
|
-
*
|
|
562
|
-
* Also not deduped by screenshot hash: there is no single screenshot, and two
|
|
563
|
-
* recordings of the same widget are never byte-identical anyway.
|
|
564
|
-
*/
|
|
181
|
+
/** Slice a burst into keyframes and solve them in one model request. The frame number is the guard. */
|
|
565
182
|
private getAnimatedSolution;
|
|
566
|
-
/**
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
*
|
|
574
|
-
* Only the neighbourhood of the action point is compared, with the same box and
|
|
575
|
-
* metric the training label's frame was chosen with. Local rather than
|
|
576
|
-
* whole-frame because everything ELSE in these puzzles is also moving: a
|
|
577
|
-
* whole-frame match would need every unrelated sprite to align too, and would
|
|
578
|
-
* essentially never open.
|
|
579
|
-
*
|
|
580
|
-
* Never throws. Returns whether the state was reached; on timeout the caller
|
|
581
|
-
* clicks anyway (see `keyframeWaitTimeoutMs`).
|
|
582
|
-
*
|
|
583
|
-
* NOT ATTEMPTED on an `even` clip. The slicer picks that mode precisely when
|
|
584
|
-
* the clip never revisits a picture it has already shown — a rotation, a
|
|
585
|
-
* one-way fade, a sprite crossing — so there is no state to come back to and
|
|
586
|
-
* this can only run out its full 6s, PER CLICK, before clicking the
|
|
587
|
-
* coordinates it already had. It is also the normal case, not a corner: all
|
|
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
|
|
590
|
-
* 28.8s solve, closest region diff 0.0721 against a 0.05 tolerance, then the
|
|
591
|
-
* same click, then solved. Kept for `cycle`/`static`, where the state does
|
|
592
|
-
* come back and waiting is the difference between the sprite and background.
|
|
593
|
-
*/
|
|
183
|
+
/** The session id and the credential travel in the environment, never in argv. */
|
|
184
|
+
private solveEnvironment;
|
|
185
|
+
/** Exit 2 is an unsupported puzzle, exit 3 the hosted API's own refusal; both are relayed, not reworded. */
|
|
186
|
+
private cliError;
|
|
187
|
+
/** Does the chosen keyframe's answer area appear in a sibling keyframe of the same clip? */
|
|
188
|
+
private answerRegionRecurs;
|
|
189
|
+
/** Hold until the widget looks like the keyframe around the action point, or the wait is spent. */
|
|
594
190
|
private waitForKeyframe;
|
|
595
|
-
/**
|
|
596
|
-
* Per-solve state that must not leak into the next challenge on the page.
|
|
597
|
-
*
|
|
598
|
-
* A repeat is a fact about ONE challenge. Carrying it forward would make the
|
|
599
|
-
* captcha after a cycling one record a burst it does not need.
|
|
600
|
-
*/
|
|
601
191
|
private resetSolveState;
|
|
602
|
-
/**
|
|
603
|
-
* A stable identity for "the answer this round is about to execute".
|
|
604
|
-
*
|
|
605
|
-
* Keyed on the retry mode too: the missed-tiles retry deliberately re-asks
|
|
606
|
-
* about the same board and its answer legitimately overlaps the previous one,
|
|
607
|
-
* so counting that as a repeat would abandon the one path built to recover
|
|
608
|
-
* from an under-selection.
|
|
609
|
-
*
|
|
610
|
-
* Coordinates are ROUNDED rather than compared exactly — the same tile chosen
|
|
611
|
-
* twice can differ in the last float digit after the normalise/clamp
|
|
612
|
-
* round-trip, and a repeat that reads as "different" is a repeat that costs a
|
|
613
|
-
* round. Returns null when the answer cannot be summarised, which counts as
|
|
614
|
-
* "not a repeat": an unreadable answer must never be why a solve is
|
|
615
|
-
* abandoned. Mirrors page_solver.py `_answer_signature`.
|
|
616
|
-
*/
|
|
192
|
+
/** Keyed on the retry mode too: the missed-tiles answer legitimately overlaps the previous one. */
|
|
617
193
|
private static answerSignature;
|
|
618
|
-
/**
|
|
194
|
+
/** The recording still stands; dropping only the refused answer makes the retry an inference, not a burst. */
|
|
195
|
+
private invalidateAnimatedAnswer;
|
|
196
|
+
/** True when this answer already ran and changed nothing: resample, and let the recording path have a go. */
|
|
619
197
|
private noteAnswer;
|
|
620
|
-
/**
|
|
621
|
-
* The answer for this picture, asking the model only if we have not already.
|
|
622
|
-
*
|
|
623
|
-
* The cache saving is real and is kept: a byte-identical picture costs no
|
|
624
|
-
* second inference. What changed is what a hit MEANS. It used to mean
|
|
625
|
-
* "nothing has changed, so this answer still stands"; it actually means the
|
|
626
|
-
* answer already ran and moved nothing, because every answer this returns is
|
|
627
|
-
* executed. So the hit is served (it is free) and recorded, and the round
|
|
628
|
-
* after it stops guessing at a still frame — see `shouldRetryAsAnimated`.
|
|
629
|
-
*/
|
|
198
|
+
/** A cache hit costs no inference, and means the answer already ran: the board cycles. */
|
|
630
199
|
private answerFor;
|
|
631
200
|
/**
|
|
632
|
-
*
|
|
633
|
-
* a still?
|
|
201
|
+
* Drop the ANSWER, keep the camera.
|
|
634
202
|
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
203
|
+
* The two are deliberately separable. An answer the widget refused is spent — reusing it just presses the
|
|
204
|
+
* same wrong thing until the no-progress fence trips, three rounds in, with half the budget unspent. The
|
|
205
|
+
* film is the opposite: every second it keeps running makes the next slice a better question. So a
|
|
206
|
+
* refusal discards this and leaves `animatedFilm` alone; only a board that is GONE stops the camera.
|
|
639
207
|
*/
|
|
640
|
-
/** Forget the recorded answer, and delete the frames it was holding. */
|
|
641
208
|
private discardAnimatedPlan;
|
|
642
|
-
/**
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
*/
|
|
209
|
+
/** The board is gone: its film can never describe the next one, so end it and free the frames. */
|
|
210
|
+
private stopAnimatedFilm;
|
|
211
|
+
/** Static or animated, decided by filming once. The film stays running for the branch that uses it. */
|
|
212
|
+
private classifyByRecording;
|
|
213
|
+
private releasePendingBurst;
|
|
653
214
|
private shouldSpeculate;
|
|
215
|
+
/** One second look per solve: a repeated answer arms it, and the recording it takes spends it. */
|
|
654
216
|
private shouldRetryAsAnimated;
|
|
655
217
|
private getSolution;
|
|
656
|
-
/** One inference
|
|
218
|
+
/** One inference through the CLI. */
|
|
657
219
|
private askModel;
|
|
658
|
-
/**
|
|
659
|
-
* Query the model for `initialShot`, then guard against the captcha frame
|
|
660
|
-
* having changed DURING inference. reCAPTCHA/hCaptcha fade fresh tiles in over
|
|
661
|
-
* ~1s; if new imagery painted while the model was generating, its answer
|
|
662
|
-
* targets a stale ("undeveloped") frame and its tile picks / bboxes no longer
|
|
663
|
-
* line up with what's on screen. After the model returns we re-screenshot the
|
|
664
|
-
* element and diff it against the frame we sent (reusing the `check-movement`
|
|
665
|
-
* primitive); if it moved beyond the threshold we discard the answer and
|
|
666
|
-
* re-solve on the fresh frame, up to `maxStaleFrameReSolves` times, then act on
|
|
667
|
-
* the latest answer rather than spin.
|
|
668
|
-
*
|
|
669
|
-
* `runQuery` performs the actual model call for a given screenshot path (the
|
|
670
|
-
* caller wraps it in idle-wander where appropriate); it is re-invoked with the
|
|
671
|
-
* fresh path on each re-solve. Token usage from every query — including
|
|
672
|
-
* discarded ones — is accumulated, since those tokens were really spent. The
|
|
673
|
-
* caller owns `initialShot`; every fresh frame captured here is created and
|
|
674
|
-
* cleaned up here. Best-effort: a screenshot/diff failure falls through to the
|
|
675
|
-
* answer already in hand.
|
|
676
|
-
*/
|
|
220
|
+
/** Re-solve when the frame changed during inference; twice means the board cycles. */
|
|
677
221
|
private solveFrameFreshnessGuarded;
|
|
678
|
-
/**
|
|
679
|
-
* Screenshot the captcha element to a throwaway temp frame and diff it against
|
|
680
|
-
* `priorPath` (the frame we sent the model). Returns true when the frame
|
|
681
|
-
* changed beyond `threshold` — i.e. tiles faded in / refreshed since. Reuses
|
|
682
|
-
* the persistent CV worker's `check-movement` (falling back to a one-shot
|
|
683
|
-
* subprocess), the same frame-diff the settle detectors use. Cleans up its own
|
|
684
|
-
* temp frame. Best-effort: any screenshot/diff failure returns false so the
|
|
685
|
-
* caller acts on the answer it already has rather than spinning.
|
|
686
|
-
*/
|
|
687
222
|
private captchaFrameChangedSince;
|
|
688
|
-
/** Record a challenge-state transition. No-op for behaviour; logs the change
|
|
689
|
-
* (via gridDebug, so only when CAPTCHA_DEBUG=1) for offline diagnosis. */
|
|
690
|
-
private setState;
|
|
691
|
-
/**
|
|
692
|
-
* Screenshot the challenge element to a throwaway file and return its sha1
|
|
693
|
-
* content hash (or null on failure). Used to tell whether the frame has
|
|
694
|
-
* changed (e.g. the post-submit transition to the next round). Cleans up.
|
|
695
|
-
*/
|
|
696
223
|
private elementFrameHash;
|
|
697
|
-
/**
|
|
698
|
-
* Monitor the challenge element until its pixels stop changing (settled), or
|
|
699
|
-
* until it's clear it never will (animated / video). Polls screenshots and
|
|
700
|
-
* diffs consecutive frames with `check-movement` (the persistent CV worker).
|
|
701
|
-
* Returns:
|
|
702
|
-
* 'settled' — `settleFrames` consecutive frame-pairs showed no movement;
|
|
703
|
-
* safe to screenshot for the model.
|
|
704
|
-
* 'animated' — it kept moving right up to `animatedChallengeAfterMs` without
|
|
705
|
-
* ever settling → very likely a video/animated puzzle.
|
|
706
|
-
* 'timeout' — neither happened within `settleTimeoutMs` (proceed best-effort).
|
|
707
|
-
*
|
|
708
|
-
* Note this is a *pixel* settle; the caller pairs it with the DOM-level
|
|
709
|
-
* `waitForHcaptchaChallengeImages` so a static loading frame (spinner on grey,
|
|
710
|
-
* below the pixel threshold) isn't mistaken for painted tiles. Cleans up.
|
|
711
|
-
*/
|
|
224
|
+
/** Poll until the pixels stop changing. `motionStreak` exits early: rotating_obj_video changes every 133-171ms and once spent 4.5s proving it. */
|
|
712
225
|
private waitForElementSettled;
|
|
713
|
-
/**
|
|
714
|
-
* After a submit we EXPECT the challenge frame to change (advance to the next
|
|
715
|
-
* round, or close because it was accepted). Poll until the element's content
|
|
716
|
-
* hash differs from `sinceHash` — the transition beginning — so we never
|
|
717
|
-
* screenshot/solve the pre-transition frame again. Returns true once it
|
|
718
|
-
* changed, false if it never changed within `postSubmitChangeTimeoutMs` (e.g.
|
|
719
|
-
* it was already the final state). Best-effort.
|
|
720
|
-
*/
|
|
721
226
|
private waitForChangeSince;
|
|
722
|
-
/**
|
|
723
|
-
* Run `fn` (typically the model query) while idly drifting the cursor over
|
|
724
|
-
* the captcha, so the mouse behaves like a human weighing the options instead
|
|
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.
|
|
729
|
-
*/
|
|
227
|
+
/** Drift the cursor over the widget while `fn` (the model call) runs; cancelled the instant it resolves. */
|
|
730
228
|
private withIdleWander;
|
|
731
229
|
move(page: Page, selectorOrElement: string | ElementHandle, options?: {
|
|
732
230
|
paddingPercentage?: number;
|
|
733
231
|
}): Promise<void>;
|
|
734
232
|
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
|
-
*/
|
|
740
233
|
private performSmoothMove;
|
|
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
|
-
*/
|
|
234
|
+
/** Element-relative click point: a random spot inside the box, inset 10% off its border. */
|
|
750
235
|
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
|
-
*/
|
|
236
|
+
/** Park on the target first, so only a mouse-down separates the right screen from the click. */
|
|
766
237
|
private clickWhenFrameMatches;
|
|
767
238
|
private executeClick;
|
|
768
239
|
private executeDrag;
|
|
769
240
|
/**
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
* `scope` is the challenge frame, or — for the vendors that render into the
|
|
773
|
-
* host page rather than an iframe — the widget element itself. Never the
|
|
774
|
-
* page: the generic tail of both selector tables would otherwise happily
|
|
775
|
-
* match a login form's text box or a carousel's drag handle somewhere else on
|
|
776
|
-
* the document, and the answer would go there.
|
|
241
|
+
* The slider piece's box: the first visible match small enough to be a piece. Runs during detection, so it must
|
|
242
|
+
* not set `actedOnBoard`: marking there disabled `shouldSpeculate` on every slide solve.
|
|
777
243
|
*/
|
|
244
|
+
private measurePieceBox;
|
|
778
245
|
private findControl;
|
|
779
246
|
/**
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
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.
|
|
247
|
+
* The text box inside the widget, else a vendor-named one in its enclosing fieldset/form, never the page: the generic
|
|
248
|
+
* tail outside the widget is how a captcha's answer lands in a login form's username box. The widening exists for
|
|
249
|
+
* BotDetect, whose 280x50 `.BDC_CaptchaDiv` holds only the image while `#captchaCode` sits in a sibling div.
|
|
801
250
|
*/
|
|
802
251
|
private answerBox;
|
|
803
|
-
/** Put the model's reading of a distorted-text captcha into its box. */
|
|
804
252
|
private executeType;
|
|
805
|
-
/** `captchakraken track-piece` — box of what moved, handle masked out. */
|
|
806
253
|
private trackPiece;
|
|
807
|
-
/**
|
|
808
|
-
* Drive a puzzle-piece slider until the PIECE reaches the model's slot.
|
|
809
|
-
*
|
|
810
|
-
* The model is asked for one thing here — the centre of the gap — because it
|
|
811
|
-
* is the only thing the picture can tell it. What it cannot know is how far
|
|
812
|
-
* the handle must travel to put the piece there: the handle is elsewhere on
|
|
813
|
-
* the widget, and the ratio between the two is a vendor implementation detail
|
|
814
|
-
* that several of them deliberately vary.
|
|
815
|
-
*
|
|
816
|
-
* So this is closed-loop, not a calculation. Press the handle, nudge it twice
|
|
817
|
-
* by known amounts, and watch the screen: union(before, after) spans the
|
|
818
|
-
* piece's ORIGINAL left edge to its CURRENT right edge, so its width is
|
|
819
|
-
* pieceWidth + ratio x nudge. Two nudges, two widths, two unknowns — solve for
|
|
820
|
-
* both, then steer the remaining distance and re-measure. The mouse is not
|
|
821
|
-
* released until the piece is home, because on every one of these puzzles
|
|
822
|
-
* releasing IS the submit; there is no Verify button to reconsider at.
|
|
823
|
-
*
|
|
824
|
-
* Returns false if there is nothing here to drag, leaving the caller's normal
|
|
825
|
-
* no-op handling to deal with it.
|
|
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
254
|
private shotScale;
|
|
255
|
+
/** Aim the handle at the slot once, then look and correct until the piece is home. Release is the submit. */
|
|
833
256
|
private executeSlide;
|
|
834
257
|
}
|