captchakraken 3.2.0 → 4.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.
Files changed (60) hide show
  1. package/AGENTS.md +5 -9
  2. package/LICENSE +165 -145
  3. package/NOTICE +4 -4
  4. package/README.md +10 -11
  5. package/dist/cli-invocation.d.ts +6 -3
  6. package/dist/cli-invocation.js +26 -10
  7. package/dist/humanize.d.ts +37 -1
  8. package/dist/humanize.js +143 -9
  9. package/dist/index.d.ts +13 -12
  10. package/dist/index.js +25 -33
  11. package/dist/kinds.d.ts +15 -2
  12. package/dist/kinds.js +12 -4
  13. package/dist/playwright-types.d.ts +3 -0
  14. package/dist/selectors.d.ts +96 -11
  15. package/dist/selectors.js +226 -112
  16. package/dist/solver.d.ts +93 -13
  17. package/dist/solver.js +579 -208
  18. package/dist/types.d.ts +53 -8
  19. package/dist/verdicts.d.ts +3 -0
  20. package/dist/verdicts.js +6 -0
  21. package/package.json +4 -4
  22. package/python/AGENTS.md +7 -11
  23. package/python/LICENSE +165 -145
  24. package/python/NOTICE +4 -4
  25. package/python/README.md +10 -9
  26. package/python/pyproject.toml +27 -13
  27. package/python/src/captchakraken/__init__.py +43 -46
  28. package/python/src/captchakraken/action_types.py +32 -1
  29. package/python/src/captchakraken/captchakraken.config.json +416 -0
  30. package/python/src/captchakraken/cli.py +66 -30
  31. package/python/src/captchakraken/clipvideo.py +258 -0
  32. package/python/src/captchakraken/humanize.py +141 -10
  33. package/python/src/captchakraken/image_processor.py +3 -7
  34. package/python/src/captchakraken/keyframes.py +96 -14
  35. package/python/src/captchakraken/kinds.py +14 -2
  36. package/python/src/captchakraken/models.json +28 -3
  37. package/python/src/captchakraken/page_solver.py +621 -259
  38. package/python/src/captchakraken/planner.py +298 -47
  39. package/python/src/captchakraken/pointer/__init__.py +20 -0
  40. package/python/src/captchakraken/pointer/detector.py +333 -0
  41. package/python/src/captchakraken/pointer/elements.py +273 -0
  42. package/python/src/captchakraken/pointer/heads.py +163 -0
  43. package/python/src/captchakraken/pointer/layout.py +319 -0
  44. package/python/src/captchakraken/pointer/marks.py +184 -0
  45. package/python/src/captchakraken/pointer/server.py +146 -0
  46. package/python/src/captchakraken/pointer/service.py +553 -0
  47. package/python/src/captchakraken/pointer/vllm_plugin.py +109 -0
  48. package/python/src/captchakraken/prompts.py +111 -34
  49. package/python/src/captchakraken/selectors.py +224 -131
  50. package/python/src/captchakraken/server_manager.py +18 -0
  51. package/python/src/captchakraken/solver.py +169 -88
  52. package/python/src/captchakraken/tool_calls/board_painted.py +11 -6
  53. package/python/src/captchakraken/tool_calls/fill_tracker.py +152 -0
  54. package/python/src/captchakraken/tool_calls/find_checkbox.py +14 -59
  55. package/python/src/captchakraken/tool_calls/find_grid.py +273 -1191
  56. package/python/src/captchakraken/tool_calls/slide_tracker.py +1102 -0
  57. package/python/src/captchakraken/verdicts.py +16 -0
  58. package/dist/slide-geometry.d.ts +0 -6
  59. package/dist/slide-geometry.js +0 -38
  60. package/python/src/captchakraken/tool_calls/track_piece.py +0 -120
package/dist/selectors.js CHANGED
@@ -1,121 +1,235 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  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;
6
+ exports.PIECE_SELECTORS = exports.REFRESH_SELECTORS = exports.SLIDER_HANDLE_SELECTORS = exports.TEXT_INPUT_SELECTORS = exports.TEXT_INPUT_VENDOR_SELECTORS = exports.SUBMIT_SELECTORS = exports.ACCEPTED_SELECTORS = exports.RESPONSE_SELECTORS = exports.ENCLAVE_SELECTORS = exports.WIDGET_PROBES = exports.VENDORS = exports.DEFAULT_TABLE = exports.SelectorTable = exports.submitByText = exports.SELECTORS = exports.DEFAULT_CONFIG = exports.DEFAULT_CONFIG_PATH = exports.DETECTOR_MODELS = exports.SelectorConfigError = void 0;
7
+ exports.loadConfig = loadConfig;
4
8
  /**
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.
9
+ * Every selector the driver knows, keyed by vendor, read from `captchakraken.config.json` at the repository root. The
10
+ * Python port reads the same file. Detection takes the first visible match in table order, so the file's order is the
11
+ * detection order.
7
12
  */
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_path_1 = __importDefault(require("node:path"));
8
15
  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
- box: ['#recaptcha-anchor'],
17
- fresh: '.rc-imageselect-instructions, #rc-imageselect',
18
- submit: ['#recaptcha-verify-button'],
19
- banners: [
20
- ['.rc-imageselect-error-select-more', kinds_1.RecaptchaBanner.SELECT_MORE],
21
- ['.rc-imageselect-error-dynamic-more', kinds_1.RecaptchaBanner.DYNAMIC_MORE],
22
- ['.rc-imageselect-incorrect-response', kinds_1.RecaptchaBanner.REJECTED],
23
- ],
24
- blocked: ['.rc-doscaptcha-header-text'],
25
- passive: ['iframe[src*="size=invisible"]'],
26
- },
27
- // Keyed on the `hcaptcha` substring, not the apex host: challenges are served off newassets.hcaptcha.com.
28
- [kinds_1.Vendor.HCAPTCHA]: {
29
- hosts: ['hcaptcha.com'],
30
- challenge: ['iframe[src*="hcaptcha"][src*="frame=challenge"]'],
31
- checkbox: ['iframe[src*="hcaptcha"][src*="frame=checkbox"]'],
32
- response: '[name="h-captcha-response"]',
33
- checked: '#checkbox[aria-checked="true"]',
34
- box: ['#checkbox'],
35
- fresh: '.prompt-text',
36
- images: ['.task-image .image', '.task .image', '.challenge-example img', '.image-wrapper img'],
37
- submit: ['.button-submit'],
38
- },
39
- [kinds_1.Vendor.TURNSTILE]: {
40
- hosts: ['challenges.cloudflare.com'],
41
- checkbox: ['iframe[src*="challenges.cloudflare.com"]', '.cf-turnstile'],
42
- response: '[name="cf-turnstile-response"]',
43
- },
44
- [kinds_1.Vendor.GEETEST]: {
45
- hosts: ['geetest.com'],
46
- widget: ['.geetest_box', '.geetest_panel_box', '.geetest_popup_window', '.geetest_widget'],
47
- accepted: '.geetest_result_tips.geetest_success, .geetest_captcha.geetest_success, .geetest_captcha.geetest_lock_success',
48
- submit: ['.geetest_submit'],
49
- sliderHandle: ['.geetest_slider_button', '.geetest_btn', '.geetest_slider .geetest_arrow'],
50
- piece: ['.geetest_slice'],
51
- },
52
- // Both in-page and iframe shapes stay (Tencent moved in-host on 2026-08-11; see TRIBAL_KNOWLEDGE.md), and the iframe id
53
- // is prefix-anchored: `[id*=tcaptcha]` also matched MTCaptcha's iframe and hid that `.mtcap` matched nothing.
54
- [kinds_1.Vendor.TENCENT]: {
55
- hosts: ['captcha.gtimg.com', 'captcha.qcloud.com'],
56
- 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"]'],
57
- sliderHandle: ['.tencent-captcha-dy__slider-block', '#tcaptcha_drag_thumb', '.tc-slider-normal', '[id*=slideBlock]'],
58
- piece: ['.tencent-captcha-dy__fg-item'],
59
- },
60
- [kinds_1.Vendor.YIDUN]: {
61
- hosts: ['dun.163.com', 'cstaticdun.126.net', 'necaptcha.nosdn.127.net'],
62
- widget: ['.yidun_panel', '.yidun'],
63
- sliderHandle: ['.yidun_slider', '.yidun_jigsaw'],
64
- piece: ['.yidun_jigsaw'],
65
- },
66
- [kinds_1.Vendor.YANDEX]: {
67
- hosts: ['smartcaptcha.yandexcloud.net'],
68
- widget: ['iframe[src*="smartcaptcha.yandexcloud.net/advanced"]', 'iframe[src*="smartcaptcha.yandexcloud.net"]', '.CheckboxCaptcha'],
69
- textInput: ['.AdvancedCaptcha-Input input', 'input.Textinput-Control', 'input[name="rep"]'],
70
- },
71
- [kinds_1.Vendor.LEMIN]: {
72
- hosts: ['leminnow.com'],
73
- widget: ['#lemin-cropped-captcha', '.lemin-captcha-popup'],
74
- sliderHandle: ['.lemin-slider-handle', '#lemin-cropped-captcha .slider'],
75
- piece: ['.lemin-cropped-puzzle-piece', '#lemin-cropped-captcha canvas + canvas'],
76
- },
77
- [kinds_1.Vendor.PROSOPO]: {
78
- hosts: ['prosopo.io'],
79
- widget: ['.prosopo-modalInner', '.procaptcha-checkbox'],
80
- },
81
- [kinds_1.Vendor.MTCAPTCHA]: {
82
- hosts: ['mtcaptcha.com'],
83
- widget: ['iframe[src*="service.mtcaptcha.com"]', 'iframe[id^="mtcaptcha-iframe"]', '.mtcaptcha', '.mtcap'],
84
- textInput: ['input.mtcap-inputtext', '.mtcap input[type=text]'],
85
- },
86
- [kinds_1.Vendor.BOTDETECT]: {
87
- hosts: [],
88
- widget: ['.BDC_CaptchaDiv'],
89
- textInput: ['input[id*=captchaCode]', 'input#captchaCode', 'input[id*=validateCaptcha]', '.BDC_CaptchaDiv input[type=text]'],
90
- },
91
- [kinds_1.Vendor.UNKNOWN]: { hosts: [] },
92
- };
93
- exports.VENDORS = Object.entries(exports.SELECTORS);
94
- const ofEveryVendor = (key) => exports.VENDORS.flatMap(([, s]) => s[key] ?? []);
95
- /** Every host-page selector with what it names, in detection order: open challenges, then checkboxes, then inline widgets. */
96
- exports.WIDGET_PROBES = [
97
- [kinds_1.FrameRole.CHALLENGE, 'challenge'], [kinds_1.FrameRole.CHECKBOX, 'checkbox'], [kinds_1.FrameRole.UNKNOWN, 'widget'],
98
- ].flatMap(([role, key]) => exports.VENDORS.flatMap(([vendor, s]) => (s[key] ?? []).map((selector) => ({ vendor, role, selector }))));
99
- exports.RESPONSE_SELECTORS = exports.VENDORS.flatMap(([, s]) => s.response ?? []);
100
- exports.ACCEPTED_SELECTORS = exports.VENDORS.flatMap(([, s]) => s.accepted ?? []);
16
+ const model_name_1 = require("./model-name");
17
+ /** A selector config the solver cannot use; the message names the file and the key. */
18
+ class SelectorConfigError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'SelectorConfigError';
22
+ }
23
+ }
24
+ exports.SelectorConfigError = SelectorConfigError;
25
+ /**
26
+ * The element detector's weights: `lite` keeps a board near 100 ms on one vCPU, `standard` is the larger network, and
27
+ * `auto` takes `lite` on a machine of two CPUs or fewer, else times one board and takes `standard` when it fits.
28
+ * `CAPTCHA_KRAKEN_DETECTOR_MODEL` in the environment overrides the config.
29
+ */
30
+ exports.DETECTOR_MODELS = ['auto', 'standard', 'lite'];
31
+ const GENERIC = ['submit_text', 'text_input', 'slider_handle', 'piece'];
32
+ // The file is spelled as the Python port's fields; these three are camelCase here.
33
+ const CAMEL = { text_input: 'textInput', slider_handle: 'sliderHandle', hold_completes: 'holdCompletes' };
34
+ const FIELDS = ['hosts', 'challenge', 'checkbox', 'enclave', 'widget', 'response', 'checked', 'box', 'accepted', 'fresh',
35
+ 'images', 'submit', 'banners', 'blocked', 'passive', 'text_input', 'slider_handle', 'piece', 'refresh', 'hold_completes'];
36
+ const ONE = new Set(['response', 'checked', 'accepted', 'fresh']);
37
+ const BANNERS = new Set(Object.values(kinds_1.RecaptchaBanner));
38
+ const VENDOR_VALUES = new Set(Object.values(kinds_1.Vendor));
39
+ const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
40
+ function strings(where, value) {
41
+ if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {
42
+ throw new SelectorConfigError(`${where}: must be a list of strings`);
43
+ }
44
+ return value;
45
+ }
46
+ function entry(where, raw, hostsRequired = true) {
47
+ if (!isObject(raw))
48
+ throw new SelectorConfigError(`${where}: must be an object`);
49
+ const out = {};
50
+ for (const [key, value] of Object.entries(raw)) {
51
+ const at = `${where}.${key}`;
52
+ if (key === 'note' || key.startsWith('_'))
53
+ continue;
54
+ if (!FIELDS.includes(key))
55
+ throw new SelectorConfigError(`${at}: not a selector field (one of ${FIELDS.join(', ')})`);
56
+ const name = CAMEL[key] ?? key;
57
+ if (ONE.has(key)) {
58
+ if (value !== null && typeof value !== 'string')
59
+ throw new SelectorConfigError(`${at}: must be one selector string`);
60
+ if (value !== null)
61
+ out[name] = value;
62
+ }
63
+ else if (key === 'hold_completes') {
64
+ if (typeof value !== 'boolean')
65
+ throw new SelectorConfigError(`${at}: must be true or false`);
66
+ if (value)
67
+ out[name] = value;
68
+ }
69
+ else if (key === 'banners') {
70
+ if (!Array.isArray(value))
71
+ throw new SelectorConfigError(`${at}: must be a list of [selector, verdict] pairs`);
72
+ out[name] = value.map((pair, i) => {
73
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== 'string') {
74
+ throw new SelectorConfigError(`${at}[${i}]: must be a [selector, verdict] pair`);
75
+ }
76
+ if (!BANNERS.has(pair[1])) {
77
+ throw new SelectorConfigError(`${at}[${i}]: verdict ${JSON.stringify(pair[1])} is not one of ${JSON.stringify([...BANNERS])}`);
78
+ }
79
+ return [pair[0], pair[1]];
80
+ });
81
+ }
82
+ else {
83
+ const list = strings(at, value);
84
+ if (list.length || key === 'hosts')
85
+ out[name] = list;
86
+ }
87
+ }
88
+ if (!('hosts' in out)) {
89
+ if (hostsRequired)
90
+ throw new SelectorConfigError(`${where}.hosts: missing (an empty list when the vendor has no host of its own)`);
91
+ out.hosts = [];
92
+ }
93
+ return out;
94
+ }
95
+ function parse(name, doc, base) {
96
+ if (!isObject(doc))
97
+ throw new SelectorConfigError(`${name}: the top level must be an object`);
98
+ for (const key of Object.keys(doc)) {
99
+ if (!key.startsWith('_') && !['detector', 'custom', 'vendors', 'generic'].includes(key)) {
100
+ throw new SelectorConfigError(`${name}: ${key}: not a config key (detector, custom, vendors, generic)`);
101
+ }
102
+ }
103
+ let detector = base?.detector ?? 'auto';
104
+ if ('detector' in doc) {
105
+ if (!exports.DETECTOR_MODELS.includes(doc.detector)) {
106
+ throw new SelectorConfigError(`${name}: detector: ${JSON.stringify(doc.detector)} is not one of ${exports.DETECTOR_MODELS.join(', ')}`);
107
+ }
108
+ detector = doc.detector;
109
+ }
110
+ let custom = base?.custom ?? null;
111
+ if ('custom' in doc)
112
+ custom = doc.custom === null ? null : entry(`${name}: custom`, doc.custom, false);
113
+ const rawVendors = doc.vendors ?? {};
114
+ if (!isObject(rawVendors))
115
+ throw new SelectorConfigError(`${name}: vendors: must be an object keyed by vendor`);
116
+ const vendors = new Map(base?.vendors ?? []);
117
+ for (const [key, raw] of Object.entries(rawVendors)) {
118
+ if (key.startsWith('_'))
119
+ continue;
120
+ if (!VENDOR_VALUES.has(key)) {
121
+ throw new SelectorConfigError(`${name}: vendors.${key}: not a vendor this solver knows; a captcha of your own goes under \`custom\``);
122
+ }
123
+ vendors.set(key, entry(`${name}: vendors.${key}`, raw));
124
+ }
125
+ if (!base) {
126
+ const missing = [...VENDOR_VALUES].filter((v) => !vendors.has(v)).sort();
127
+ if (missing.length)
128
+ throw new SelectorConfigError(`${name}: vendors: every vendor must have an entry; missing ${JSON.stringify(missing)}`);
129
+ }
130
+ const rawGeneric = doc.generic ?? {};
131
+ if (!isObject(rawGeneric))
132
+ throw new SelectorConfigError(`${name}: generic: must be an object`);
133
+ const generic = { ...(base?.generic ?? {}) };
134
+ for (const [key, value] of Object.entries(rawGeneric)) {
135
+ if (key.startsWith('_'))
136
+ continue;
137
+ if (!GENERIC.includes(key)) {
138
+ throw new SelectorConfigError(`${name}: generic.${key}: not a generic list (one of ${GENERIC.join(', ')})`);
139
+ }
140
+ generic[key] = strings(`${name}: generic.${key}`, value);
141
+ }
142
+ const absent = GENERIC.filter((k) => !generic[k]);
143
+ if (absent.length)
144
+ throw new SelectorConfigError(`${name}: generic: missing ${JSON.stringify(absent)}`);
145
+ return { custom, vendors: [...vendors], generic: generic, detector };
146
+ }
147
+ function read(file) {
148
+ let text;
149
+ try {
150
+ text = node_fs_1.default.readFileSync(file, 'utf-8');
151
+ }
152
+ catch (e) {
153
+ throw new SelectorConfigError(`${file}: cannot be read (${e.message})`);
154
+ }
155
+ try {
156
+ return JSON.parse(text);
157
+ }
158
+ catch (e) {
159
+ throw new SelectorConfigError(`${file}: cannot be read as JSON (${e.message})`);
160
+ }
161
+ }
162
+ /**
163
+ * The default config: the repository's own `captchakraken.config.json` when this package runs from a checkout, so an
164
+ * edit there is what the solver reads, else the copy the bundled Python engine ships (an installed package has no
165
+ * repository root). The Python port resolves the same file.
166
+ */
167
+ exports.DEFAULT_CONFIG_PATH = (() => {
168
+ const checkout = node_path_1.default.resolve(__dirname, '..', '..', 'captchakraken.config.json');
169
+ return node_fs_1.default.existsSync(checkout)
170
+ ? checkout
171
+ : node_path_1.default.join((0, model_name_1.getBundledCliRoot)(), 'src', 'captchakraken', 'captchakraken.config.json');
172
+ })();
173
+ /**
174
+ * The shipped default, or `source` (a path or a parsed object) laid over it: a key it gives replaces the default's
175
+ * `custom`, one vendor's whole entry, or one generic list. Throws SelectorConfigError naming the bad key.
176
+ */
177
+ function loadConfig(source) {
178
+ const base = parse(exports.DEFAULT_CONFIG_PATH, read(exports.DEFAULT_CONFIG_PATH), null);
179
+ if (source === undefined || (typeof source === 'string' && node_path_1.default.resolve(source) === exports.DEFAULT_CONFIG_PATH))
180
+ return base;
181
+ return typeof source === 'string' ? parse(source, read(source), base) : parse('selectors', source, base);
182
+ }
183
+ exports.DEFAULT_CONFIG = loadConfig();
184
+ /** Every vendor's entry, in detection order, as the default config gives it. */
185
+ exports.SELECTORS = Object.fromEntries(exports.DEFAULT_CONFIG.vendors);
101
186
  const lower = "translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')";
102
187
  /** `.//` keeps the xpath relative so a host-page widget cannot reach the form's own submit. */
103
188
  const submitByText = (text) => `xpath=.//button[contains(${lower}, '${text}')] | .//div[@role="button" and contains(${lower}, '${text}')]`;
104
189
  exports.submitByText = submitByText;
105
- /** A real Verify/Next/Submit/Skip first; the vendor-named controls are fallbacks. */
106
- exports.SUBMIT_SELECTORS = [...['verify', 'next', 'submit', 'skip'].map(exports.submitByText), ...ofEveryVendor('submit')];
107
- // Vendor-named first, generic last: the driver takes the first visible match, and a generic selector
108
- // reached before the vendor's own is how a captcha's answer ends up in a login form's username box.
109
- exports.TEXT_INPUT_VENDOR_SELECTORS = ofEveryVendor('textInput');
110
- exports.TEXT_INPUT_SELECTORS = [...exports.TEXT_INPUT_VENDOR_SELECTORS,
111
- 'input[name*="captcha" i]', 'input[id*="captcha" i]', 'input[aria-label*="captcha" i]',
112
- 'input[placeholder*="code" i]', 'input[autocomplete="off"][type=text]',
113
- 'input[type=text]', 'input:not([type])', 'input[type=tel]', 'textarea',
114
- ];
115
- // `[draggable=true]` is absent: HTML5 DnD fires dragstart, not pointermove.
116
- exports.SLIDER_HANDLE_SELECTORS = [...ofEveryVendor('sliderHandle'),
117
- '[role="slider"]', '[aria-valuenow]',
118
- '[class*="slider"][class*="btn"]', '[class*="slider"][class*="button"]',
119
- '[class*="slide"][class*="handle"]', '[class*="drag"][class*="thumb"]',
120
- ];
121
- exports.PIECE_SELECTORS = [...ofEveryVendor('piece'), '[class*="puzzle"][class*="piece"]', '[class*="jigsaw"]'];
190
+ const DETECTED_BY = [[kinds_1.FrameRole.CHALLENGE, 'challenge'], [kinds_1.FrameRole.CHECKBOX, 'checkbox'], [kinds_1.FrameRole.UNKNOWN, 'widget']];
191
+ /**
192
+ * The table one solver detects and drives with: a selector config's vendors, plus its `custom` entry when it has one.
193
+ * `config` is a path, a parsed object or a loaded SelectorConfig, laid over the shipped default (see `loadConfig`);
194
+ * `custom` (the older `customSelectors`) replaces the config's own. A custom entry is reported as `Vendor.UNKNOWN` and
195
+ * searched before the built-in vendors, because the caller wrote it for the page in front of them.
196
+ */
197
+ class SelectorTable {
198
+ constructor(custom, config) {
199
+ const loaded = isLoaded(config) ? config : loadConfig(config);
200
+ const own = custom ?? loaded.custom ?? undefined;
201
+ if (own && !DETECTED_BY.some(([, key]) => own[key]?.length)) {
202
+ throw new SelectorConfigError('custom selectors name nothing to detect: give the entry a `widget`, `challenge` or `checkbox`');
203
+ }
204
+ this.config = loaded;
205
+ this.vendors = own
206
+ ? [[kinds_1.Vendor.UNKNOWN, { hosts: [], ...own }], ...loaded.vendors.filter(([v]) => v !== kinds_1.Vendor.UNKNOWN)]
207
+ : loaded.vendors;
208
+ this.selectors = Object.fromEntries(this.vendors);
209
+ const every = (key) => this.vendors.flatMap(([, s]) => s[key] ?? []);
210
+ this.widgetProbes = DETECTED_BY.flatMap(([role, key]) => this.vendors.flatMap(([vendor, s]) => (s[key] ?? []).map((selector) => ({ vendor, role, selector }))));
211
+ this.enclave = every('enclave');
212
+ this.response = this.vendors.flatMap(([, s]) => s.response ?? []);
213
+ this.accepted = this.vendors.flatMap(([, s]) => s.accepted ?? []);
214
+ this.submit = [...loaded.generic.submit_text.map(exports.submitByText), ...every('submit')];
215
+ this.textInputVendor = every('textInput');
216
+ this.textInput = [...this.textInputVendor, ...loaded.generic.text_input];
217
+ this.sliderHandle = [...every('sliderHandle'), ...loaded.generic.slider_handle];
218
+ this.refresh = every('refresh');
219
+ this.piece = [...every('piece'), ...loaded.generic.piece];
220
+ }
221
+ }
222
+ exports.SelectorTable = SelectorTable;
223
+ const isLoaded = (c) => isObject(c) && Array.isArray(c.vendors) && isObject(c.generic) && 'custom' in c;
224
+ exports.DEFAULT_TABLE = new SelectorTable(undefined, exports.DEFAULT_CONFIG);
225
+ exports.VENDORS = exports.DEFAULT_TABLE.vendors;
226
+ exports.WIDGET_PROBES = exports.DEFAULT_TABLE.widgetProbes;
227
+ exports.ENCLAVE_SELECTORS = exports.DEFAULT_TABLE.enclave;
228
+ exports.RESPONSE_SELECTORS = exports.DEFAULT_TABLE.response;
229
+ exports.ACCEPTED_SELECTORS = exports.DEFAULT_TABLE.accepted;
230
+ exports.SUBMIT_SELECTORS = exports.DEFAULT_TABLE.submit;
231
+ exports.TEXT_INPUT_VENDOR_SELECTORS = exports.DEFAULT_TABLE.textInputVendor;
232
+ exports.TEXT_INPUT_SELECTORS = exports.DEFAULT_TABLE.textInput;
233
+ exports.SLIDER_HANDLE_SELECTORS = exports.DEFAULT_TABLE.sliderHandle;
234
+ exports.REFRESH_SELECTORS = exports.DEFAULT_TABLE.refresh;
235
+ exports.PIECE_SELECTORS = exports.DEFAULT_TABLE.piece;
package/dist/solver.d.ts CHANGED
@@ -14,6 +14,15 @@ export interface Widget {
14
14
  vendor: Vendor;
15
15
  role: FrameRole;
16
16
  }
17
+ /**
18
+ * A typed captcha's image and its text box, photographed and pointed into as one element.
19
+ *
20
+ * BotDetect's widget element is a 280x50 strip holding only the image; its text box is a sibling. Every training
21
+ * board and every real capture of a typed captcha shows the box under the image, and on the bare strip the pointer
22
+ * read the image as a click puzzle or doubled a glyph. The answer's points are taken in this union's frame, so
23
+ * `boundingBox` is the union everywhere the round reads it; everything else is the element's own.
24
+ */
25
+ export declare function withAnswerBox(el: ElementHandle, box: ElementHandle): ElementHandle;
17
26
  /** An allow-list of what needs nothing: the other direction raises instead of clicking at the origin. */
18
27
  export declare function answerNeedsElementBox(actions: ReadonlyArray<{
19
28
  action?: string;
@@ -23,6 +32,11 @@ export declare function isStaleHandleError(message: string): boolean;
23
32
  /** A closed target is not a stale handle: retried as one, a dead page was re-detected until the loops ran out. */
24
33
  export declare function isClosedTargetError(message: string): boolean;
25
34
  export declare const INPUT_SETTLE_MS = 400;
35
+ export declare const IDLE_DRIFT_EVERY_MS: readonly [number, number];
36
+ export declare const IDLE_DRIFT_PX = 3;
37
+ export declare const GRID_LOAD_CAP_FACTOR = 3;
38
+ export declare const HOLD_SETTLE_MS = 900;
39
+ export declare const HOLD_MAX_MS = 12000;
26
40
  export declare const SOLVE_DEFAULTS: {
27
41
  readonly maxSolveLoops: 6;
28
42
  readonly overallSolveTimeoutMs: 45000;
@@ -38,6 +52,7 @@ export declare class CaptchaKrakenSolver {
38
52
  private videoBudgetMs;
39
53
  private videoBudgetGranted;
40
54
  private human;
55
+ private sel;
41
56
  private stepIndex;
42
57
  private solveStartMs;
43
58
  private cvWorker;
@@ -47,6 +62,15 @@ export declare class CaptchaKrakenSolver {
47
62
  private cvWorkerBuf;
48
63
  /** Answers keyed by screenshot hash; a hit means the answer already ran and changed nothing. */
49
64
  private solutionCache;
65
+ /** The other complete plans the pointer returned for this board, best first; a refresh is last. */
66
+ private fallbackPlans;
67
+ /** The screenshot those plans answered. A different board makes them the wrong answer, not an alternative. */
68
+ private fallbackBoardHash;
69
+ /** True only once THIS board refused the plan at the front. Nothing is pressed speculatively. */
70
+ private fallbackArmed;
71
+ private exchangesAtAnswer;
72
+ /** Earlier screens of the same paged puzzle (FunCaptcha), oldest first; the CLI sends them as `--history`. */
73
+ private pointerHistory;
50
74
  private repeatedAnswerSeen;
51
75
  private knownAnimated;
52
76
  private animatedProbeDone;
@@ -56,8 +80,9 @@ export declare class CaptchaKrakenSolver {
56
80
  private keyframeSteadyScreens;
57
81
  private solveDeadlineAt;
58
82
  private lastAnswerSig;
83
+ private triedAnswers;
84
+ private refreshedBoards;
59
85
  private noProgressRounds;
60
- private resampleLevel;
61
86
  /** The recording `classifyByRecording` started and left running for the branch below to finish or drop. */
62
87
  private pendingBurst;
63
88
  /**
@@ -70,6 +95,7 @@ export declare class CaptchaKrakenSolver {
70
95
  private animatedFilm;
71
96
  private lastBurstFps;
72
97
  private actedOnBoard;
98
+ private submittedOnBoard;
73
99
  private lastSubmitFrameHash;
74
100
  private solveSessionId;
75
101
  /** Which vendor and site this solve is for, sent to the hosted API; the site is a hostname only. */
@@ -134,6 +160,9 @@ export declare class CaptchaKrakenSolver {
134
160
  /** The vendor showing its refusal screen: rate-limited or flagged, with no board to answer. */
135
161
  private isBlocked;
136
162
  /** The env the engine reads its routing headers from: session, and the vendor and site once they are known. */
163
+ /** This solver's endpoint and key: two solvers in one process may talk to different servers. */
164
+ private get baseUrl();
165
+ private get apiKey();
137
166
  private routingEnv;
138
167
  /** Fire the optional onStep observer with a fresh screenshot; best-effort, never fails the solve. */
139
168
  private emitStep;
@@ -150,8 +179,13 @@ export declare class CaptchaKrakenSolver {
150
179
  * a closed shadow root, and a frame can refuse access; both are found on screen instead.
151
180
  */
152
181
  private checkboxBox;
153
- /** One capture and OpenCV: a point inside the tick box, in page coordinates. No model is asked. */
182
+ /**
183
+ * A point inside the tick box, in page coordinates, or null when the widget passed without drawing one. Turnstile
184
+ * shows "Verifying…" before its box, so an empty capture is looked at again until `detectionTimeoutMs`.
185
+ */
154
186
  private findCheckboxOnScreen;
187
+ /** One capture and the element detector, no model: a point inside the tick box, or null when none is drawn. */
188
+ private checkboxOnScreen;
155
189
  /**
156
190
  * A clicked checkbox passes or opens a challenge, and both show in the DOM.
157
191
  * Without this wait the next round found the same checkbox still up and clicked it again.
@@ -163,7 +197,6 @@ export declare class CaptchaKrakenSolver {
163
197
  /** A visible match carrying text; an empty banner or prompt is a placeholder, not a signal. */
164
198
  private visibleWithText;
165
199
  private checkedIn;
166
- private bannerIsFatalAfterRetry;
167
200
  /** Which verdict banner the open challenge shows; `dynamic-more` is the dynamic board's normal flow, not an error. */
168
201
  private bannerKind;
169
202
  /** The vendor's own done signal: a response token, a painted success state, or a checked box. */
@@ -177,7 +210,7 @@ export declare class CaptchaKrakenSolver {
177
210
  private waitForBoardImages;
178
211
  /** Which vendors' code the page loaded, from resource timing and linked URLs. A tripwire, not a detector. */
179
212
  vendorsOnTheWire(page: Page): Promise<Vendor[]>;
180
- /** Vendors showing a frame no selector names. Only asked once detection has found nothing, and a passive frame (an invisible badge) is not a widget, so what is left is markup the table no longer matches. */
213
+ /** Vendors showing a frame no selector names. Only asked once detection has found nothing, and a passive frame (an invisible badge, an enclave) is not a widget, so what is left is markup the table no longer matches. */
181
214
  private unmatchedVendorFrames;
182
215
  private noWidgetMessage;
183
216
  /**
@@ -190,7 +223,7 @@ export declare class CaptchaKrakenSolver {
190
223
  /** A checkbox the vendor has already accepted is not a captcha to solve. */
191
224
  private alreadyAccepted;
192
225
  private loraName;
193
- /** The `--model` argv, or nothing: without VLLM_BASE_URL the CLI can see a credentials file this process cannot. */
226
+ /** The `--model` argv, or nothing: without a base URL the CLI can see a credentials file this process cannot. */
194
227
  private modelName;
195
228
  private resolveCli;
196
229
  /** What is LEFT of the solve, as a bound on one ask.
@@ -215,10 +248,18 @@ export declare class CaptchaKrakenSolver {
215
248
  * A screenshot that fails ends the poll: the widget is gone or going.
216
249
  */
217
250
  private pollFrames;
251
+ /**
252
+ * Hold the next ask until every tile has painted: a model asked about a half-loaded board answers it.
253
+ *
254
+ * Past the timeout a blank tile that has stopped changing is taken as painted (a pale photo, or a tile whose
255
+ * image never came), and a board with no grid to read is let through. A tile still painting is waited on until
256
+ * GRID_LOAD_CAP_FACTOR times that, or the solve's deadline.
257
+ */
218
258
  private waitForGridCellsLoaded;
219
259
  private getGridBoxes;
220
260
  private bboxToCell;
221
261
  private cellCenterPage;
262
+ /** Rest the hand on a tile: reach it once, then drift a few pixels inside it. */
222
263
  private hoverCell;
223
264
  /** Per-cell state with the session's cached boxes: a blank mid-refresh frame has no lattice to detect. */
224
265
  private gridCellStates;
@@ -260,13 +301,27 @@ export declare class CaptchaKrakenSolver {
260
301
  private answerRegionRecurs;
261
302
  /** Hold until the widget looks like the keyframe around the action point, or the wait is spent. */
262
303
  private waitForKeyframe;
304
+ private dropPointerHistory;
305
+ /** A click board with no Verify submits itself at a count, so a plan of any other count leaves its tiles ticked
306
+ * and never submits, and the next plan's clicks untick them. */
307
+ private keepPlansOfThisManyClicks;
263
308
  private resetSolveState;
264
- /** Keyed on the retry mode too: the missed-tiles answer legitimately overlaps the previous one. */
309
+ /**
310
+ * Keyed on the BOARD and the retry mode, not the answer alone. A paged puzzle's next arrow sits at the
311
+ * same point on every page, so a signature over the actions alone would call a correct page turn a
312
+ * repeated answer and stop pressing it.
313
+ */
265
314
  private static answerSignature;
266
315
  /** The recording still stands; dropping only the refused answer makes the retry an inference, not a burst. */
267
316
  private invalidateAnimatedAnswer;
268
- /** True when this answer already ran and changed nothing: resample, and let the recording path have a go. */
317
+ /** True when this answer already ran and changed nothing: re-ask, and let the recording path have a go. */
269
318
  private noteAnswer;
319
+ /** Whether the round after a repeat can differ. At temperature 0 the same question gets the same answer, so it
320
+ * takes a different question (a retry mode, a film) or one of the model's other plans. */
321
+ private anotherMoveLeft;
322
+ private boardStayedSilent;
323
+ /** The board's identity, always from a plain photograph: a filmed frame of an unchanged board differs in bytes. */
324
+ private boardHash;
270
325
  /** A cache hit costs no inference, and means the answer already ran: the board cycles. */
271
326
  private answerFor;
272
327
  /**
@@ -305,15 +360,34 @@ export declare class CaptchaKrakenSolver {
305
360
  private performSmoothMove;
306
361
  /** Element-relative click point: a random spot inside the box, inset 10% off its border. */
307
362
  private clickPointFor;
363
+ /**
364
+ * The press point of a hold: the model's own point, the middle of its box. A hold can be on a bar narrower than that
365
+ * box, and a random spot in it lands off the bar, or close enough to its edge that the hand's drift lets go: the
366
+ * widget then never fills, and the same answer fails the same way.
367
+ */
368
+ private holdPointFor;
308
369
  /** Park on the target first, so only a mouse-down separates the right screen from the click. */
309
370
  private clickWhenFrameMatches;
310
371
  private executeClick;
311
- private executeDrag;
312
372
  /**
313
- * The slider piece's box: the first visible match small enough to be a piece. Runs during detection, so it must
314
- * not set `actedOnBoard`: marking there disabled `shouldSpeculate` on every slide solve.
373
+ * Press-and-hold until the widget completes: true when it did, false when the cap ran out. Complete is the vendor's
374
+ * done signal, the widget closing, or its picture having changed (the fill) and then sat still for HOLD_SETTLE_MS.
375
+ * When the answer names the fill's line, the fill reaching its end is complete too: see `fillReached`. Except on a
376
+ * widget that `completesItself` (HUMAN's button): it verifies only once it has shown its done state, and letting go
377
+ * AT full resets it, so only its own signals end the hold.
378
+ */
379
+ private executeHold;
380
+ /**
381
+ * One look at a held widget's fill (`fill_tracker.py` behind the CV worker): true once it reaches the answer's `to`,
382
+ * after waiting out the tracker's `release_in` when the fill arrives between looks.
383
+ * The first look only shoots the reference, after the press, so the button's own pressed look is not read as fill;
384
+ * `looks` carries what each later look saw. A look that cannot be shot or read is not a release: the widget's own
385
+ * signals still end the hold.
315
386
  */
316
- private measurePieceBox;
387
+ private fillReached;
388
+ /** Deal a new puzzle: the vendor's own control, else the model's point on one. False when there is neither. */
389
+ private executeRefresh;
390
+ private executeDrag;
317
391
  private findControl;
318
392
  /**
319
393
  * The text box inside the widget, else a vendor-named one in its enclosing fieldset/form, never the page: the generic
@@ -322,8 +396,14 @@ export declare class CaptchaKrakenSolver {
322
396
  */
323
397
  private answerBox;
324
398
  private executeType;
325
- private trackPiece;
399
+ /** One look of the slide loop (`slide_tracker.py` behind the CV worker); null when the part cannot be followed. */
400
+ private slideStep;
326
401
  private shotScale;
327
- /** Aim the handle at the slot once, then look and correct until the piece is home. Release is the submit. */
402
+ /**
403
+ * Press the handle, nudge it to see what it moves, then steer that part's point onto its target. Release is the submit.
404
+ * A generation-3 answer names all three points, so nothing is read from the page but pixels. A generation-2 answer
405
+ * names only the target: its handle comes from the selector table and the part it moves is whatever moved most.
406
+ * That path goes when the last generation-2 model does.
407
+ */
328
408
  private executeSlide;
329
409
  }