captchakraken 2.9.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +112 -0
- package/NOTICE +36 -0
- package/dist/cli-invocation.d.ts +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 +94 -712
- package/dist/solver.js +923 -3362
- 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 +75 -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 +1052 -3098
- package/python/src/captchakraken/planner.py +50 -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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PIECE_SELECTORS = exports.SLIDER_HANDLE_SELECTORS = exports.TEXT_INPUT_SELECTORS = exports.TEXT_INPUT_VENDOR_SELECTORS = exports.SUBMIT_SELECTORS = exports.submitByText = exports.ACCEPTED_SELECTORS = exports.RESPONSE_SELECTORS = exports.WIDGET_PROBES = exports.VENDORS = exports.SELECTORS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Every selector the driver knows, keyed by vendor. Mirrored in python/src/captchakraken/selectors.py; keep both in the
|
|
6
|
+
* same order, because detection takes the first visible match in table order.
|
|
7
|
+
*/
|
|
8
|
+
const kinds_1 = require("./kinds");
|
|
9
|
+
exports.SELECTORS = {
|
|
10
|
+
[kinds_1.Vendor.RECAPTCHA]: {
|
|
11
|
+
hosts: ['google.com/recaptcha', 'recaptcha.net'],
|
|
12
|
+
challenge: ['iframe[src*="recaptcha/api2/bframe"]'],
|
|
13
|
+
checkbox: ['iframe[src*="recaptcha/api2/anchor"]:not([src*="size=invisible"])'],
|
|
14
|
+
response: '[name="g-recaptcha-response"]',
|
|
15
|
+
checked: '.recaptcha-checkbox-checked',
|
|
16
|
+
fresh: '.rc-imageselect-instructions, #rc-imageselect',
|
|
17
|
+
submit: ['#recaptcha-verify-button'],
|
|
18
|
+
banners: [
|
|
19
|
+
['.rc-imageselect-error-select-more', kinds_1.RecaptchaBanner.SELECT_MORE],
|
|
20
|
+
['.rc-imageselect-error-dynamic-more', kinds_1.RecaptchaBanner.DYNAMIC_MORE],
|
|
21
|
+
['.rc-imageselect-incorrect-response', kinds_1.RecaptchaBanner.REJECTED],
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
// Keyed on the `hcaptcha` substring, not the apex host: challenges are served off newassets.hcaptcha.com.
|
|
25
|
+
[kinds_1.Vendor.HCAPTCHA]: {
|
|
26
|
+
hosts: ['hcaptcha.com'],
|
|
27
|
+
challenge: ['iframe[src*="hcaptcha"][src*="frame=challenge"]'],
|
|
28
|
+
checkbox: ['iframe[src*="hcaptcha"][src*="frame=checkbox"]'],
|
|
29
|
+
response: '[name="h-captcha-response"]',
|
|
30
|
+
checked: '#checkbox[aria-checked="true"]',
|
|
31
|
+
fresh: '.prompt-text',
|
|
32
|
+
images: ['.task-image .image', '.task .image', '.challenge-example img', '.image-wrapper img'],
|
|
33
|
+
submit: ['.button-submit'],
|
|
34
|
+
},
|
|
35
|
+
[kinds_1.Vendor.TURNSTILE]: {
|
|
36
|
+
hosts: ['challenges.cloudflare.com'],
|
|
37
|
+
checkbox: ['iframe[src*="challenges.cloudflare.com"]', '.cf-turnstile'],
|
|
38
|
+
response: '[name="cf-turnstile-response"]',
|
|
39
|
+
},
|
|
40
|
+
[kinds_1.Vendor.GEETEST]: {
|
|
41
|
+
hosts: ['geetest.com'],
|
|
42
|
+
widget: ['.geetest_box', '.geetest_panel_box', '.geetest_popup_window', '.geetest_widget'],
|
|
43
|
+
accepted: '.geetest_result_tips.geetest_success, .geetest_captcha.geetest_success, .geetest_captcha.geetest_lock_success',
|
|
44
|
+
submit: ['.geetest_submit'],
|
|
45
|
+
sliderHandle: ['.geetest_slider_button', '.geetest_btn', '.geetest_slider .geetest_arrow'],
|
|
46
|
+
piece: ['.geetest_slice'],
|
|
47
|
+
},
|
|
48
|
+
// Both in-page and iframe shapes stay (Tencent moved in-host on 2026-08-11; see TRIBAL_KNOWLEDGE.md), and the iframe id
|
|
49
|
+
// is prefix-anchored: `[id*=tcaptcha]` also matched MTCaptcha's iframe and hid that `.mtcap` matched nothing.
|
|
50
|
+
[kinds_1.Vendor.TENCENT]: {
|
|
51
|
+
hosts: ['captcha.gtimg.com', 'captcha.qcloud.com'],
|
|
52
|
+
widget: ['#tcaptcha_transform_dy', '#tCaptchaDyContent', '.tencent-captcha-dy__content', 'iframe#tcaptcha_iframe_dy', 'iframe[id^="tcaptcha"]', 'iframe[src*="captcha.gtimg.com"]', 'iframe[src*="captcha.qq.com"]'],
|
|
53
|
+
sliderHandle: ['.tencent-captcha-dy__slider-block', '#tcaptcha_drag_thumb', '.tc-slider-normal', '[id*=slideBlock]'],
|
|
54
|
+
piece: ['.tencent-captcha-dy__fg-item'],
|
|
55
|
+
},
|
|
56
|
+
[kinds_1.Vendor.YIDUN]: {
|
|
57
|
+
hosts: ['dun.163.com', 'cstaticdun.126.net', 'necaptcha.nosdn.127.net'],
|
|
58
|
+
widget: ['.yidun_panel', '.yidun'],
|
|
59
|
+
sliderHandle: ['.yidun_slider', '.yidun_jigsaw'],
|
|
60
|
+
piece: ['.yidun_jigsaw'],
|
|
61
|
+
},
|
|
62
|
+
[kinds_1.Vendor.YANDEX]: {
|
|
63
|
+
hosts: ['smartcaptcha.yandexcloud.net'],
|
|
64
|
+
widget: ['iframe[src*="smartcaptcha.yandexcloud.net/advanced"]', 'iframe[src*="smartcaptcha.yandexcloud.net"]', '.CheckboxCaptcha'],
|
|
65
|
+
textInput: ['.AdvancedCaptcha-Input input', 'input.Textinput-Control', 'input[name="rep"]'],
|
|
66
|
+
},
|
|
67
|
+
[kinds_1.Vendor.LEMIN]: {
|
|
68
|
+
hosts: ['leminnow.com'],
|
|
69
|
+
widget: ['#lemin-cropped-captcha', '.lemin-captcha-popup'],
|
|
70
|
+
sliderHandle: ['.lemin-slider-handle', '#lemin-cropped-captcha .slider'],
|
|
71
|
+
piece: ['.lemin-cropped-puzzle-piece', '#lemin-cropped-captcha canvas + canvas'],
|
|
72
|
+
},
|
|
73
|
+
[kinds_1.Vendor.PROSOPO]: {
|
|
74
|
+
hosts: ['prosopo.io'],
|
|
75
|
+
widget: ['.prosopo-modalInner', '.procaptcha-checkbox'],
|
|
76
|
+
},
|
|
77
|
+
[kinds_1.Vendor.MTCAPTCHA]: {
|
|
78
|
+
hosts: ['mtcaptcha.com'],
|
|
79
|
+
widget: ['iframe[src*="service.mtcaptcha.com"]', 'iframe[id^="mtcaptcha-iframe"]', '.mtcaptcha', '.mtcap'],
|
|
80
|
+
textInput: ['input.mtcap-inputtext', '.mtcap input[type=text]'],
|
|
81
|
+
},
|
|
82
|
+
[kinds_1.Vendor.BOTDETECT]: {
|
|
83
|
+
hosts: [],
|
|
84
|
+
widget: ['.BDC_CaptchaDiv'],
|
|
85
|
+
textInput: ['input[id*=captchaCode]', 'input#captchaCode', 'input[id*=validateCaptcha]', '.BDC_CaptchaDiv input[type=text]'],
|
|
86
|
+
},
|
|
87
|
+
[kinds_1.Vendor.UNKNOWN]: { hosts: [] },
|
|
88
|
+
};
|
|
89
|
+
exports.VENDORS = Object.entries(exports.SELECTORS);
|
|
90
|
+
const ofEveryVendor = (key) => exports.VENDORS.flatMap(([, s]) => s[key] ?? []);
|
|
91
|
+
/** Every host-page selector with what it names, in detection order: open challenges, then checkboxes, then inline widgets. */
|
|
92
|
+
exports.WIDGET_PROBES = [
|
|
93
|
+
[kinds_1.FrameRole.CHALLENGE, 'challenge'], [kinds_1.FrameRole.CHECKBOX, 'checkbox'], [kinds_1.FrameRole.UNKNOWN, 'widget'],
|
|
94
|
+
].flatMap(([role, key]) => exports.VENDORS.flatMap(([vendor, s]) => (s[key] ?? []).map((selector) => ({ vendor, role, selector }))));
|
|
95
|
+
exports.RESPONSE_SELECTORS = exports.VENDORS.flatMap(([, s]) => s.response ?? []);
|
|
96
|
+
exports.ACCEPTED_SELECTORS = exports.VENDORS.flatMap(([, s]) => s.accepted ?? []);
|
|
97
|
+
const lower = "translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')";
|
|
98
|
+
/** `.//` keeps the xpath relative so a host-page widget cannot reach the form's own submit. */
|
|
99
|
+
const submitByText = (text) => `xpath=.//button[contains(${lower}, '${text}')] | .//div[@role="button" and contains(${lower}, '${text}')]`;
|
|
100
|
+
exports.submitByText = submitByText;
|
|
101
|
+
/** A real Verify/Next/Submit/Skip first; the vendor-named controls are fallbacks. */
|
|
102
|
+
exports.SUBMIT_SELECTORS = [...['verify', 'next', 'submit', 'skip'].map(exports.submitByText), ...ofEveryVendor('submit')];
|
|
103
|
+
// Vendor-named first, generic last: the driver takes the first visible match, and a generic selector
|
|
104
|
+
// reached before the vendor's own is how a captcha's answer ends up in a login form's username box.
|
|
105
|
+
exports.TEXT_INPUT_VENDOR_SELECTORS = ofEveryVendor('textInput');
|
|
106
|
+
exports.TEXT_INPUT_SELECTORS = [...exports.TEXT_INPUT_VENDOR_SELECTORS,
|
|
107
|
+
'input[name*="captcha" i]', 'input[id*="captcha" i]', 'input[aria-label*="captcha" i]',
|
|
108
|
+
'input[placeholder*="code" i]', 'input[autocomplete="off"][type=text]',
|
|
109
|
+
'input[type=text]', 'input:not([type])', 'input[type=tel]', 'textarea',
|
|
110
|
+
];
|
|
111
|
+
// `[draggable=true]` is absent: HTML5 DnD fires dragstart, not pointermove.
|
|
112
|
+
exports.SLIDER_HANDLE_SELECTORS = [...ofEveryVendor('sliderHandle'),
|
|
113
|
+
'[role="slider"]', '[aria-valuenow]',
|
|
114
|
+
'[class*="slider"][class*="btn"]', '[class*="slider"][class*="button"]',
|
|
115
|
+
'[class*="slide"][class*="handle"]', '[class*="drag"][class*="thumb"]',
|
|
116
|
+
];
|
|
117
|
+
exports.PIECE_SELECTORS = [...ofEveryVendor('piece'), '[class*="puzzle"][class*="piece"]', '[class*="jigsaw"]'];
|
package/dist/slide-geometry.d.ts
CHANGED
|
@@ -1,33 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The algebra behind a puzzle-piece slider's closed loop.
|
|
3
|
-
*
|
|
4
|
-
* Split out of solver.ts because it is the one part of this feature that is
|
|
5
|
-
* pure arithmetic, duplicated across the two ports, and impossible to notice
|
|
6
|
-
* drifting: a wrong ratio here does not throw, it just releases the piece in the
|
|
7
|
-
* wrong place, and the puzzle fails the way an unsolved puzzle fails. Keeping it
|
|
8
|
-
* dependency-free means it can be unit-tested with `node --test` alongside
|
|
9
|
-
* limits.ts, against the same cases as
|
|
10
|
-
* `PageSolver._solve_slide_geometry` in the Python port.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Piece width and handle-to-piece travel ratio, from probe measurements.
|
|
14
|
-
*
|
|
15
|
-
* Each measurement is `[handle offset, width of what changed]`, where the width
|
|
16
|
-
* spans the piece's ORIGINAL left edge to its CURRENT right edge — so
|
|
17
|
-
* `width = pieceWidth + ratio x offset`. Two of them determine both unknowns.
|
|
18
|
-
*
|
|
19
|
-
* With only one usable measurement the system is underdetermined, so `ratio` is
|
|
20
|
-
* ASSUMED to be 1 — true of every vendor observed, and the assumption is stated
|
|
21
|
-
* here rather than buried as a default. A ratio solved from implausible
|
|
22
|
-
* measurements (a redraw, a piece that hit the wall between probes) is rejected
|
|
23
|
-
* the same way: better a 1:1 guess that overshoots and gets corrected than a
|
|
24
|
-
* ratio of 0.02 that sends the handle off the track and, under camoufox, into a
|
|
25
|
-
* mouse move that never returns.
|
|
26
|
-
*
|
|
27
|
-
* `pieceWidth: null` means "could not measure" — the caller falls back to
|
|
28
|
-
* steering by the handle's own travel.
|
|
29
|
-
*/
|
|
30
1
|
export declare function solveSlideGeometry(widths: Array<[number, number]>, widgetWidth: number): {
|
|
31
2
|
pieceWidth: number | null;
|
|
32
3
|
ratio: number;
|
|
33
4
|
};
|
|
5
|
+
export declare const MIN_PIECE_PX = 3;
|
|
6
|
+
export declare const MAX_PIECE_FRACTION = 0.6;
|
package/dist/slide-geometry.js
CHANGED
|
@@ -1,64 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* The algebra behind a puzzle-piece slider's closed loop.
|
|
4
|
-
*
|
|
5
|
-
* Split out of solver.ts because it is the one part of this feature that is
|
|
6
|
-
* pure arithmetic, duplicated across the two ports, and impossible to notice
|
|
7
|
-
* drifting: a wrong ratio here does not throw, it just releases the piece in the
|
|
8
|
-
* wrong place, and the puzzle fails the way an unsolved puzzle fails. Keeping it
|
|
9
|
-
* dependency-free means it can be unit-tested with `node --test` alongside
|
|
10
|
-
* limits.ts, against the same cases as
|
|
11
|
-
* `PageSolver._solve_slide_geometry` in the Python port.
|
|
12
|
-
*/
|
|
13
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_PIECE_FRACTION = exports.MIN_PIECE_PX = void 0;
|
|
14
4
|
exports.solveSlideGeometry = solveSlideGeometry;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* Each measurement is `[handle offset, width of what changed]`, where the width
|
|
19
|
-
* spans the piece's ORIGINAL left edge to its CURRENT right edge — so
|
|
20
|
-
* `width = pieceWidth + ratio x offset`. Two of them determine both unknowns.
|
|
21
|
-
*
|
|
22
|
-
* With only one usable measurement the system is underdetermined, so `ratio` is
|
|
23
|
-
* ASSUMED to be 1 — true of every vendor observed, and the assumption is stated
|
|
24
|
-
* here rather than buried as a default. A ratio solved from implausible
|
|
25
|
-
* measurements (a redraw, a piece that hit the wall between probes) is rejected
|
|
26
|
-
* the same way: better a 1:1 guess that overshoots and gets corrected than a
|
|
27
|
-
* ratio of 0.02 that sends the handle off the track and, under camoufox, into a
|
|
28
|
-
* mouse move that never returns.
|
|
29
|
-
*
|
|
30
|
-
* `pieceWidth: null` means "could not measure" — the caller falls back to
|
|
31
|
-
* steering by the handle's own travel.
|
|
32
|
-
*/
|
|
5
|
+
// Literals here match page_solver.py::_solve_slide_geometry; the ratio is assumed 1:1 unless two probes prove otherwise,
|
|
6
|
+
// which is true of every vendor observed.
|
|
33
7
|
function solveSlideGeometry(widths, widgetWidth) {
|
|
34
8
|
if (!widths.length)
|
|
35
9
|
return { pieceWidth: null, ratio: 1 };
|
|
36
10
|
let ratio = 1;
|
|
37
11
|
let pieceWidth = null;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
12
|
+
// The widest-apart pair, not the last two: 1px rounding between neighbouring probes once gave a ratio of 0.5.
|
|
13
|
+
const sorted = [...widths].sort((a, b) => a[0] - b[0]);
|
|
14
|
+
const [o1, w1] = sorted[0];
|
|
15
|
+
const [o2, w2] = sorted[sorted.length - 1];
|
|
16
|
+
if (o2 - o1 >= MIN_OFFSET_SPREAD_PX) {
|
|
17
|
+
const candidate = (w2 - w1) / (o2 - o1);
|
|
18
|
+
if (candidate >= MIN_RATIO && candidate <= MAX_RATIO) {
|
|
19
|
+
ratio = candidate;
|
|
20
|
+
pieceWidth = w1 - ratio * o1;
|
|
47
21
|
}
|
|
48
22
|
}
|
|
49
23
|
if (pieceWidth === null) {
|
|
50
24
|
const [o, w] = widths[widths.length - 1];
|
|
51
25
|
pieceWidth = w - ratio * o;
|
|
52
26
|
}
|
|
53
|
-
|
|
54
|
-
// measurement of something else — a wholesale redraw, a fade, a spinner.
|
|
55
|
-
if (pieceWidth < MIN_PIECE_PX || pieceWidth > widgetWidth * MAX_PIECE_FRACTION) {
|
|
27
|
+
if (pieceWidth < exports.MIN_PIECE_PX || pieceWidth > widgetWidth * exports.MAX_PIECE_FRACTION) {
|
|
56
28
|
return { pieceWidth: null, ratio };
|
|
57
29
|
}
|
|
58
30
|
return { pieceWidth, ratio };
|
|
59
31
|
}
|
|
60
|
-
//
|
|
32
|
+
// Box widths are ±1px, so the ratio error is ±2/spread: ±0.13 at 16px against ±0.5 at 4px, multiplied into every correction.
|
|
33
|
+
const MIN_OFFSET_SPREAD_PX = 16.0;
|
|
34
|
+
// A ratio of 0.02 from a redraw sends the handle off the track and, under camoufox, into a mouse move that never returns.
|
|
61
35
|
const MIN_RATIO = 0.2;
|
|
62
36
|
const MAX_RATIO = 3.0;
|
|
63
|
-
|
|
64
|
-
|
|
37
|
+
exports.MIN_PIECE_PX = 3.0;
|
|
38
|
+
exports.MAX_PIECE_FRACTION = 0.6;
|