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
package/dist/model-name.js
CHANGED
|
@@ -4,41 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getBundledCliRoot = getBundledCliRoot;
|
|
7
|
+
exports.isHostedEndpoint = isHostedEndpoint;
|
|
7
8
|
exports.resolveLoraName = resolveLoraName;
|
|
8
|
-
/**
|
|
9
|
-
* Which served LoRA name this client asks for — the SAME answer the Python
|
|
10
|
-
* client's `config.lora_name()` gives, and for the same reasons.
|
|
11
|
-
*
|
|
12
|
-
* This has to be shared, not re-decided per port. The name is not just routing:
|
|
13
|
-
* the Python CLI feeds it to `prompts.resolve()`, which maps it through
|
|
14
|
-
* models.json to a PROMPT GENERATION. A port that picks a different name sends
|
|
15
|
-
* a different generation of prompts to the same weights, and nothing errors for
|
|
16
|
-
* any family both generations have a prompt for — it just answers worse. That
|
|
17
|
-
* is the failure models.json exists to prevent, and the JS port shipped it:
|
|
18
|
-
* a hardcoded `'captcha'` (CaptchaKraken_v1.1, generation 1) against the
|
|
19
|
-
* generation-2 adapter `latest` has named since v1.2.
|
|
20
|
-
*
|
|
21
|
-
* Precedence mirrors config.py exactly:
|
|
22
|
-
* 1. CAPTCHA_LORA_NAME — an explicit pin always wins, and pinning is opt-in.
|
|
23
|
-
* 2. models.json's `latest` entry, so the default model and the default
|
|
24
|
-
* prompt generation move forward together and cannot get out of step.
|
|
25
|
-
* 3. pinned_model.json, so a hand-edited or older manifest still resolves.
|
|
26
|
-
*/
|
|
27
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
28
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
29
|
-
/**
|
|
30
|
-
* Where the bundled Python engine lives — the copy of `models.json` this client
|
|
31
|
-
* actually answers from.
|
|
32
|
-
*
|
|
33
|
-
* Lives HERE rather than in solver.ts because the registry lookup below is its
|
|
34
|
-
* only remaining caller-visible use, and a second copy of this path logic is
|
|
35
|
-
* how the two ports drifted apart in the first place. solver.ts imports it.
|
|
36
|
-
*
|
|
37
|
-
* When installed from npm this file is in `<pkgRoot>/dist` (compiled) or
|
|
38
|
-
* `<pkgRoot>/src` (dev), and published packages bundle the engine at
|
|
39
|
-
* `<pkgRoot>/python` (copied in by scripts/copy-python.mjs at build time). In
|
|
40
|
-
* the source development checkout it instead lives at the sibling `../python`.
|
|
41
|
-
*/
|
|
42
11
|
function getBundledCliRoot() {
|
|
43
12
|
const bundled = node_path_1.default.resolve(__dirname, '..', 'python');
|
|
44
13
|
if (node_fs_1.default.existsSync(bundled))
|
|
@@ -48,23 +17,46 @@ function getBundledCliRoot() {
|
|
|
48
17
|
function readJson(cliRoot, name) {
|
|
49
18
|
return JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cliRoot, 'src', 'captchakraken', name), 'utf-8'));
|
|
50
19
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
20
|
+
// An exact host list, not "is it remote": a self-hoster's vLLM across the network is remote too, and the hosted-only model 404s there.
|
|
21
|
+
const HOSTED_HOSTS = ['api.captchakraken.com'];
|
|
22
|
+
function isHostedEndpoint(baseUrl, env = process.env) {
|
|
23
|
+
if (!baseUrl)
|
|
24
|
+
return false;
|
|
25
|
+
const extra = (env.CAPTCHA_HOSTED_HOSTS ?? '')
|
|
26
|
+
.split(',').map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
27
|
+
try {
|
|
28
|
+
return [...HOSTED_HOSTS, ...extra].includes(new URL(baseUrl).hostname.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Hosted: the routing alias, not an arm's `lora_name`, because a routed mixture is several names and only the alias routes. A missing or broken registry falls through to the pin, never throws. */
|
|
35
|
+
function resolveLoraName({ cliRoot = getBundledCliRoot(), env = process.env, baseUrl } = {}) {
|
|
57
36
|
if (env.CAPTCHA_LORA_NAME)
|
|
58
37
|
return env.CAPTCHA_LORA_NAME;
|
|
59
38
|
try {
|
|
60
39
|
const reg = readJson(cliRoot, 'models.json');
|
|
40
|
+
if (isHostedEndpoint(baseUrl, env)) {
|
|
41
|
+
const repo = reg?.hosted_default;
|
|
42
|
+
if (typeof repo === 'string' && repo) {
|
|
43
|
+
const aliases = Object.entries(reg?.served_aliases ?? {})
|
|
44
|
+
.filter(([a, target]) => target === repo && !a.startsWith('_'))
|
|
45
|
+
.map(([a]) => a);
|
|
46
|
+
const routed = aliases.find((a) => {
|
|
47
|
+
const id = (reg?.served_aliases ?? {})[a];
|
|
48
|
+
return Object.keys(reg?.models?.[id]?.experts ?? {}).length > 0;
|
|
49
|
+
});
|
|
50
|
+
const picked = routed ?? aliases[0] ?? reg?.models?.[repo]?.lora_name;
|
|
51
|
+
if (typeof picked === 'string' && picked)
|
|
52
|
+
return picked;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
61
55
|
const name = reg?.models?.[reg?.latest]?.lora_name;
|
|
62
56
|
if (typeof name === 'string' && name)
|
|
63
57
|
return name;
|
|
64
58
|
}
|
|
65
59
|
catch {
|
|
66
|
-
// A missing or broken registry falls through to the pin, never throws —
|
|
67
|
-
// same contract as config.py's `_registry_default`.
|
|
68
60
|
}
|
|
69
61
|
return readJson(cliRoot, 'pinned_model.json').lora_name;
|
|
70
62
|
}
|
|
@@ -1,95 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal, implementation-neutral structural types for the slice of the
|
|
3
|
-
* Playwright API the solver actually uses.
|
|
4
|
-
*
|
|
5
|
-
* **Why these exist.** CaptchaKraken never launches a browser — you bring your
|
|
6
|
-
* own Playwright-compatible launcher and hand `solve()` a `Page`. To stay truly
|
|
7
|
-
* browser-agnostic the package depends on NO concrete browser implementation
|
|
8
|
-
* (not `playwright`, not `patchright`, not `camoufox-js`). Typing the public API
|
|
9
|
-
* against any one of those packages' `Page` would (a) force that package into
|
|
10
|
-
* consumers' trees and (b) break across version skew — e.g. `patchright` pins
|
|
11
|
-
* its own forked, older core whose `Page` is missing the newest `playwright-core`
|
|
12
|
-
* methods, so a `patchright` page won't structurally match a `playwright-core`
|
|
13
|
-
* page and vice-versa.
|
|
14
|
-
*
|
|
15
|
-
* Instead we declare a duck-typed surface covering only what the solver calls.
|
|
16
|
-
* Every real Playwright `Page` / `Frame` / `ElementHandle` — from vanilla
|
|
17
|
-
* `playwright`, `patchright`, `camoufox-js`, or anything else — structurally
|
|
18
|
-
* satisfies these, regardless of which Playwright version it was built against.
|
|
19
|
-
*
|
|
20
|
-
* Keep these in sync with `solver.ts`: if the solver starts calling a new
|
|
21
|
-
* Playwright method, add it here. The set is intentionally small and stable —
|
|
22
|
-
* these are long-standing Playwright primitives, not bleeding-edge additions.
|
|
23
|
-
*/
|
|
24
|
-
/** `{ x, y, width, height }` in CSS pixels — the shape `boundingBox()` returns. */
|
|
25
1
|
export interface BoundingBoxRect {
|
|
26
2
|
x: number;
|
|
27
3
|
y: number;
|
|
28
4
|
width: number;
|
|
29
5
|
height: number;
|
|
30
6
|
}
|
|
31
|
-
/** `{ width, height }` — the shape `viewportSize()` returns. */
|
|
32
7
|
export interface ViewportSize {
|
|
33
8
|
width: number;
|
|
34
9
|
height: number;
|
|
35
10
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Structural subset of Playwright's `ElementHandle`. Returned by `Page.$` /
|
|
38
|
-
* `Frame.$` / `Page.waitForSelector` and accepted by the solver as the captcha
|
|
39
|
-
* element / verify button / iframe handle.
|
|
40
|
-
*/
|
|
41
11
|
export interface PlaywrightElementHandle {
|
|
42
|
-
/** Screenshot just this element to a PNG file (the only option the solver passes). */
|
|
43
12
|
screenshot(options?: {
|
|
44
13
|
path?: string;
|
|
45
14
|
timeout?: number;
|
|
46
15
|
animations?: 'disabled' | 'allow';
|
|
47
16
|
}): Promise<Buffer>;
|
|
48
|
-
/** The content document of an `<iframe>` element handle, or null if not a frame. */
|
|
49
17
|
contentFrame(): Promise<PlaywrightFrame | null>;
|
|
50
|
-
/** Element box in page CSS pixels, or null if not rendered. */
|
|
51
18
|
boundingBox(): Promise<BoundingBoxRect | null>;
|
|
52
|
-
/** Scroll the element into view if it isn't already. */
|
|
53
19
|
scrollIntoViewIfNeeded(options?: {
|
|
54
20
|
timeout?: number;
|
|
55
21
|
}): Promise<void>;
|
|
56
|
-
/** Read an attribute, or null if absent. */
|
|
57
22
|
getAttribute(name: string): Promise<string | null>;
|
|
58
|
-
/** Whether the element is visible. */
|
|
59
23
|
isVisible(): Promise<boolean>;
|
|
60
|
-
/** The element's text content, or null. */
|
|
61
24
|
textContent(): Promise<string | null>;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* that render into the host page rather than an iframe (GeeTest, Yidun,
|
|
65
|
-
* BotDetect, …) have no Frame to scope against, so the widget element is the
|
|
66
|
-
* boundary that keeps a generic selector off the rest of the document.
|
|
67
|
-
*/
|
|
68
|
-
$(selector: string): Promise<PlaywrightElementHandle | null>;
|
|
25
|
+
inputValue(): Promise<string>;
|
|
26
|
+
evaluate<R>(pageFunction: (element: Element) => R): Promise<R>;
|
|
69
27
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
28
|
+
/** The subset of Playwright's Locator the driver needs. `filter({ visible })` is Playwright 1.51+. */
|
|
29
|
+
export interface PlaywrightLocator {
|
|
30
|
+
locator(selector: string): PlaywrightLocator;
|
|
31
|
+
filter(options: {
|
|
32
|
+
visible?: boolean;
|
|
33
|
+
}): PlaywrightLocator;
|
|
34
|
+
all(): Promise<PlaywrightLocator[]>;
|
|
35
|
+
count(): Promise<number>;
|
|
36
|
+
elementHandle(options?: {
|
|
37
|
+
timeout?: number;
|
|
38
|
+
}): Promise<PlaywrightElementHandle | null>;
|
|
39
|
+
}
|
|
40
|
+
/** Anything selectors can be run inside: a page, a frame, or the locator of an inline widget. */
|
|
41
|
+
export interface PlaywrightScope {
|
|
42
|
+
locator(selector: string): PlaywrightLocator;
|
|
43
|
+
}
|
|
44
|
+
export interface PlaywrightFrame extends PlaywrightScope {
|
|
77
45
|
waitForSelector(selector: string, options?: {
|
|
78
46
|
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
|
79
47
|
timeout?: number;
|
|
80
48
|
}): Promise<PlaywrightElementHandle | null>;
|
|
81
|
-
/** Poll a predicate evaluated in the page until it returns truthy. */
|
|
82
49
|
waitForFunction(pageFunction: Function | string, arg?: any, options?: {
|
|
83
50
|
timeout?: number;
|
|
84
51
|
polling?: number | 'raf';
|
|
85
52
|
}): Promise<unknown>;
|
|
86
53
|
}
|
|
87
54
|
/**
|
|
88
|
-
* Structural
|
|
89
|
-
*
|
|
55
|
+
* Structural on purpose: the package depends on no browser library, and the version skew between
|
|
56
|
+
* playwright, patchright and camoufox-js makes a nominal import the wrong one for someone.
|
|
90
57
|
*/
|
|
91
|
-
export interface PlaywrightPage {
|
|
92
|
-
/** Low-level mouse control used to drive human-like trajectories and clicks. */
|
|
58
|
+
export interface PlaywrightPage extends PlaywrightScope {
|
|
93
59
|
mouse: {
|
|
94
60
|
move(x: number, y: number, options?: {
|
|
95
61
|
steps?: number;
|
|
@@ -103,11 +69,7 @@ export interface PlaywrightPage {
|
|
|
103
69
|
clickCount?: number;
|
|
104
70
|
}): Promise<void>;
|
|
105
71
|
};
|
|
106
|
-
/**
|
|
107
|
-
* Key input. Used to type a distorted-text captcha's answer character by
|
|
108
|
-
* character — `fill()` would set the value with no keystrokes at all, and
|
|
109
|
-
* these are the vendors that score typing cadence.
|
|
110
|
-
*/
|
|
72
|
+
/** `type` is called per character by the humanizer; a constant `delay` would itself be a signal. */
|
|
111
73
|
keyboard: {
|
|
112
74
|
type(text: string, options?: {
|
|
113
75
|
delay?: number;
|
|
@@ -116,52 +78,20 @@ export interface PlaywrightPage {
|
|
|
116
78
|
delay?: number;
|
|
117
79
|
}): Promise<void>;
|
|
118
80
|
};
|
|
119
|
-
/** Sleep `timeout` ms (Playwright's own timer). */
|
|
120
81
|
waitForTimeout(timeout: number): Promise<void>;
|
|
121
|
-
/** Wait for a selector to reach the given state; resolves to the handle (or null). */
|
|
122
82
|
waitForSelector(selector: string, options?: {
|
|
123
83
|
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
|
124
84
|
timeout?: number;
|
|
125
85
|
}): Promise<PlaywrightElementHandle | null>;
|
|
126
|
-
/** Current viewport size, or null if not set. */
|
|
127
86
|
viewportSize(): ViewportSize | null;
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
* `humanization: 'mobile'` needs it, and then only to open a CDP session for
|
|
131
|
-
* `Input.dispatchTouchEvent` — the mouse and none modes never touch it. A
|
|
132
|
-
* launcher that does not expose it simply cannot serve mobile mode, and says
|
|
133
|
-
* so at construction rather than mid-solve.
|
|
134
|
-
*/
|
|
87
|
+
/** Optional: the mouse humanizer asks the window for its size when `viewportSize()` is null (camoufox). */
|
|
88
|
+
evaluate?<R>(pageFunction: () => R): Promise<R>;
|
|
135
89
|
context?(): {
|
|
136
90
|
newCDPSession(page: PlaywrightPage): Promise<any>;
|
|
137
91
|
};
|
|
138
|
-
/**
|
|
139
|
-
* Playwright's touchscreen, present when the context was created with
|
|
140
|
-
* `hasTouch: true`. OPTIONAL, and tap-only: it is the fallback for a browser
|
|
141
|
-
* with touch support but no CDP, and cannot express a drag.
|
|
142
|
-
*/
|
|
143
92
|
touchscreen?: {
|
|
144
93
|
tap(x: number, y: number): Promise<void>;
|
|
145
94
|
};
|
|
146
|
-
/** First matching element handle, or null. */
|
|
147
|
-
$(selector: string): Promise<PlaywrightElementHandle | null>;
|
|
148
|
-
/** All matching element handles. */
|
|
149
|
-
$$(selector: string): Promise<PlaywrightElementHandle[]>;
|
|
150
|
-
/**
|
|
151
|
-
* Whether the page has been closed. OPTIONAL because it is the one member
|
|
152
|
-
* here the solver itself never needs — the auto-solve watcher uses it to end
|
|
153
|
-
* its loop when the caller closes the browser out from under it, and falls
|
|
154
|
-
* back to treating the page as open when a launcher does not expose it.
|
|
155
|
-
* Playwright and Puppeteer both do.
|
|
156
|
-
*/
|
|
157
95
|
isClosed?(): boolean;
|
|
158
|
-
/** Run `pageFunction` against the first matching element, in the page context. */
|
|
159
|
-
$eval<R>(selector: string, pageFunction: (element: Element) => R, arg?: any): Promise<R>;
|
|
160
96
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Public alias. This is the type the solver's `solve(page)` accepts — kept under
|
|
163
|
-
* a friendly name so consumers can annotate against it. It is the
|
|
164
|
-
* implementation-neutral Playwright `Page`; pass a page from whichever
|
|
165
|
-
* Playwright-compatible launcher you prefer.
|
|
166
|
-
*/
|
|
167
97
|
export type Page = PlaywrightPage;
|
package/dist/playwright-types.js
CHANGED
|
@@ -1,25 +1,2 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Minimal, implementation-neutral structural types for the slice of the
|
|
4
|
-
* Playwright API the solver actually uses.
|
|
5
|
-
*
|
|
6
|
-
* **Why these exist.** CaptchaKraken never launches a browser — you bring your
|
|
7
|
-
* own Playwright-compatible launcher and hand `solve()` a `Page`. To stay truly
|
|
8
|
-
* browser-agnostic the package depends on NO concrete browser implementation
|
|
9
|
-
* (not `playwright`, not `patchright`, not `camoufox-js`). Typing the public API
|
|
10
|
-
* against any one of those packages' `Page` would (a) force that package into
|
|
11
|
-
* consumers' trees and (b) break across version skew — e.g. `patchright` pins
|
|
12
|
-
* its own forked, older core whose `Page` is missing the newest `playwright-core`
|
|
13
|
-
* methods, so a `patchright` page won't structurally match a `playwright-core`
|
|
14
|
-
* page and vice-versa.
|
|
15
|
-
*
|
|
16
|
-
* Instead we declare a duck-typed surface covering only what the solver calls.
|
|
17
|
-
* Every real Playwright `Page` / `Frame` / `ElementHandle` — from vanilla
|
|
18
|
-
* `playwright`, `patchright`, `camoufox-js`, or anything else — structurally
|
|
19
|
-
* satisfies these, regardless of which Playwright version it was built against.
|
|
20
|
-
*
|
|
21
|
-
* Keep these in sync with `solver.ts`: if the solver starts calling a new
|
|
22
|
-
* Playwright method, add it here. The set is intentionally small and stable —
|
|
23
|
-
* these are long-standing Playwright primitives, not bleeding-edge additions.
|
|
24
|
-
*/
|
|
25
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -1,38 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Puppeteer → CaptchaKraken adapter.
|
|
3
|
-
*
|
|
4
|
-
* The solver speaks the Playwright API surface (see playwright-types.ts). Puppeteer
|
|
5
|
-
* is ~95% the same shape but differs in a handful of method names/options. Rather
|
|
6
|
-
* than couple the solver to either library, this thin adapter wraps a Puppeteer
|
|
7
|
-
* `Page` (and, lazily, the `Frame`/`ElementHandle` objects it hands back) so it
|
|
8
|
-
* satisfies the structural `PlaywrightPage` interface the solver consumes.
|
|
9
|
-
*
|
|
10
|
-
* The deltas it bridges (verified against Puppeteer 24.x):
|
|
11
|
-
* - `page.viewportSize()` → `page.viewport()` (same `{width,height}` shape)
|
|
12
|
-
* - `page.waitForTimeout(ms)` → removed in modern Puppeteer; use a timer
|
|
13
|
-
* - `*.waitForSelector(sel, {state}) → Puppeteer uses `{visible}/{hidden}`
|
|
14
|
-
* - `handle.getAttribute(name)` → `handle.evaluate((el,n)=>el.getAttribute(n), name)`
|
|
15
|
-
* - `handle.textContent()` → `handle.evaluate(el=>el.textContent)`
|
|
16
|
-
* - `handle.scrollIntoViewIfNeeded()`→ `handle.scrollIntoView()`
|
|
17
|
-
* - `page.isClosed()` → same, but must be forwarded explicitly
|
|
18
|
-
* Everything else (`$`, `$$`, `$eval`, `waitForFunction`, `mouse.*`,
|
|
19
|
-
* `screenshot({path})`, `boundingBox`, `contentFrame`, `isVisible`) is
|
|
20
|
-
* call-compatible and passed straight through.
|
|
21
|
-
*
|
|
22
|
-
* Usage:
|
|
23
|
-
* ```typescript
|
|
24
|
-
* import puppeteer from 'puppeteer';
|
|
25
|
-
* import { CaptchaKrakenSolver, fromPuppeteer } from 'captchakraken';
|
|
26
|
-
*
|
|
27
|
-
* const browser = await puppeteer.launch({ headless: false });
|
|
28
|
-
* const page = await browser.newPage();
|
|
29
|
-
* await page.goto('https://www.google.com/recaptcha/api2/demo');
|
|
30
|
-
*
|
|
31
|
-
* const solver = new CaptchaKrakenSolver();
|
|
32
|
-
* await solver.solve(fromPuppeteer(page));
|
|
33
|
-
* await browser.close();
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
1
|
import { PlaywrightPage, BoundingBoxRect, ViewportSize } from './playwright-types';
|
|
37
2
|
interface PuppeteerElementHandle {
|
|
38
3
|
screenshot(options?: {
|
|
@@ -43,10 +8,10 @@ interface PuppeteerElementHandle {
|
|
|
43
8
|
scrollIntoView(): Promise<void>;
|
|
44
9
|
isVisible(): Promise<boolean>;
|
|
45
10
|
evaluate(pageFunction: (el: any, ...args: any[]) => any, ...args: any[]): Promise<any>;
|
|
46
|
-
|
|
11
|
+
$$(selector: string): Promise<PuppeteerElementHandle[]>;
|
|
47
12
|
}
|
|
48
13
|
interface PuppeteerFrame {
|
|
49
|
-
|
|
14
|
+
$$(selector: string): Promise<PuppeteerElementHandle[]>;
|
|
50
15
|
waitForSelector(selector: string, options?: any): Promise<PuppeteerElementHandle | null>;
|
|
51
16
|
waitForFunction(pageFunction: Function | string, options?: any, ...args: any[]): Promise<unknown>;
|
|
52
17
|
}
|
|
@@ -68,14 +33,10 @@ interface PuppeteerPage {
|
|
|
68
33
|
};
|
|
69
34
|
waitForSelector(selector: string, options?: any): Promise<PuppeteerElementHandle | null>;
|
|
70
35
|
viewport(): ViewportSize | null;
|
|
71
|
-
|
|
36
|
+
evaluate<R>(pageFunction: () => R): Promise<R>;
|
|
72
37
|
$$(selector: string): Promise<PuppeteerElementHandle[]>;
|
|
73
|
-
$eval(selector: string, pageFunction: (element: Element) => any, ...args: any[]): Promise<any>;
|
|
74
38
|
isClosed(): boolean;
|
|
39
|
+
evaluate(pageFunction: () => any): Promise<any>;
|
|
75
40
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Wrap a Puppeteer `Page` so it satisfies the solver's structural Playwright
|
|
78
|
-
* `Page`. Pass the result to `solver.solve(...)`.
|
|
79
|
-
*/
|
|
80
41
|
export declare function fromPuppeteer(page: PuppeteerPage): PlaywrightPage;
|
|
81
42
|
export {};
|
|
@@ -1,42 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Puppeteer → CaptchaKraken adapter.
|
|
4
|
-
*
|
|
5
|
-
* The solver speaks the Playwright API surface (see playwright-types.ts). Puppeteer
|
|
6
|
-
* is ~95% the same shape but differs in a handful of method names/options. Rather
|
|
7
|
-
* than couple the solver to either library, this thin adapter wraps a Puppeteer
|
|
8
|
-
* `Page` (and, lazily, the `Frame`/`ElementHandle` objects it hands back) so it
|
|
9
|
-
* satisfies the structural `PlaywrightPage` interface the solver consumes.
|
|
10
|
-
*
|
|
11
|
-
* The deltas it bridges (verified against Puppeteer 24.x):
|
|
12
|
-
* - `page.viewportSize()` → `page.viewport()` (same `{width,height}` shape)
|
|
13
|
-
* - `page.waitForTimeout(ms)` → removed in modern Puppeteer; use a timer
|
|
14
|
-
* - `*.waitForSelector(sel, {state}) → Puppeteer uses `{visible}/{hidden}`
|
|
15
|
-
* - `handle.getAttribute(name)` → `handle.evaluate((el,n)=>el.getAttribute(n), name)`
|
|
16
|
-
* - `handle.textContent()` → `handle.evaluate(el=>el.textContent)`
|
|
17
|
-
* - `handle.scrollIntoViewIfNeeded()`→ `handle.scrollIntoView()`
|
|
18
|
-
* - `page.isClosed()` → same, but must be forwarded explicitly
|
|
19
|
-
* Everything else (`$`, `$$`, `$eval`, `waitForFunction`, `mouse.*`,
|
|
20
|
-
* `screenshot({path})`, `boundingBox`, `contentFrame`, `isVisible`) is
|
|
21
|
-
* call-compatible and passed straight through.
|
|
22
|
-
*
|
|
23
|
-
* Usage:
|
|
24
|
-
* ```typescript
|
|
25
|
-
* import puppeteer from 'puppeteer';
|
|
26
|
-
* import { CaptchaKrakenSolver, fromPuppeteer } from 'captchakraken';
|
|
27
|
-
*
|
|
28
|
-
* const browser = await puppeteer.launch({ headless: false });
|
|
29
|
-
* const page = await browser.newPage();
|
|
30
|
-
* await page.goto('https://www.google.com/recaptcha/api2/demo');
|
|
31
|
-
*
|
|
32
|
-
* const solver = new CaptchaKrakenSolver();
|
|
33
|
-
* await solver.solve(fromPuppeteer(page));
|
|
34
|
-
* await browser.close();
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
3
|
exports.fromPuppeteer = fromPuppeteer;
|
|
39
|
-
/** Translate Playwright `{state}` selector options to Puppeteer `{visible}/{hidden}`. */
|
|
40
4
|
function toPuppeteerSelectorOptions(options) {
|
|
41
5
|
if (!options)
|
|
42
6
|
return undefined;
|
|
@@ -48,10 +12,22 @@ function toPuppeteerSelectorOptions(options) {
|
|
|
48
12
|
out.visible = true;
|
|
49
13
|
else if (state === 'hidden')
|
|
50
14
|
out.hidden = true;
|
|
51
|
-
// 'attached'/'detached' have no direct Puppeteer flag — default wait (attached)
|
|
52
|
-
// is the closest match, so we pass no visibility flag.
|
|
53
15
|
return out;
|
|
54
16
|
}
|
|
17
|
+
/** Puppeteer's own Locator has no `all()`, `count()` or visibility filter, so a Playwright-shaped one is built over `$$`. */
|
|
18
|
+
function locatorOver(resolve) {
|
|
19
|
+
return {
|
|
20
|
+
locator: (selector) => locatorOver(async () => (await Promise.all((await resolve()).map((h) => h.$$(selector)))).flat()),
|
|
21
|
+
filter: ({ visible }) => locatorOver(async () => {
|
|
22
|
+
const found = await resolve();
|
|
23
|
+
const shown = await Promise.all(found.map((h) => (visible === undefined ? true : h.isVisible().then((v) => v === visible))));
|
|
24
|
+
return found.filter((_, i) => shown[i]);
|
|
25
|
+
}),
|
|
26
|
+
all: async () => (await resolve()).map((h) => locatorOver(async () => [h])),
|
|
27
|
+
count: async () => (await resolve()).length,
|
|
28
|
+
elementHandle: async () => wrapHandle((await resolve())[0] ?? null),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
55
31
|
function wrapHandle(h) {
|
|
56
32
|
if (!h)
|
|
57
33
|
return null;
|
|
@@ -63,24 +39,19 @@ function wrapHandle(h) {
|
|
|
63
39
|
getAttribute: (name) => h.evaluate((el, n) => el.getAttribute(n), name),
|
|
64
40
|
isVisible: () => h.isVisible(),
|
|
65
41
|
textContent: () => h.evaluate((el) => el.textContent),
|
|
66
|
-
|
|
42
|
+
inputValue: () => h.evaluate((el) => (typeof el.value === 'string' ? el.value : '')),
|
|
43
|
+
evaluate: (pageFunction) => h.evaluate(pageFunction),
|
|
67
44
|
};
|
|
68
45
|
}
|
|
69
46
|
function wrapFrame(f) {
|
|
70
47
|
if (!f)
|
|
71
48
|
return null;
|
|
72
49
|
return {
|
|
73
|
-
|
|
50
|
+
locator: (selector) => locatorOver(() => f.$$(selector)),
|
|
74
51
|
waitForSelector: async (selector, options) => wrapHandle(await f.waitForSelector(selector, toPuppeteerSelectorOptions(options))),
|
|
75
|
-
waitForFunction: (pageFunction, arg, options) =>
|
|
76
|
-
// Playwright: (fn, arg, {timeout,polling}); Puppeteer: (fn, {timeout,polling}, ...args).
|
|
77
|
-
f.waitForFunction(pageFunction, options, arg),
|
|
52
|
+
waitForFunction: (pageFunction, arg, options) => f.waitForFunction(pageFunction, options, arg),
|
|
78
53
|
};
|
|
79
54
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Wrap a Puppeteer `Page` so it satisfies the solver's structural Playwright
|
|
82
|
-
* `Page`. Pass the result to `solver.solve(...)`.
|
|
83
|
-
*/
|
|
84
55
|
function fromPuppeteer(page) {
|
|
85
56
|
return {
|
|
86
57
|
mouse: {
|
|
@@ -90,9 +61,7 @@ function fromPuppeteer(page) {
|
|
|
90
61
|
},
|
|
91
62
|
keyboard: {
|
|
92
63
|
type: (text, options) => page.keyboard.type(text, options),
|
|
93
|
-
// Puppeteer has no
|
|
94
|
-
// released. Playwright accepts the combo string directly, which is why
|
|
95
|
-
// the solver speaks Playwright and this adapter translates.
|
|
64
|
+
// Puppeteer has no 'Control+A' combo syntax; the solver speaks Playwright and this side translates.
|
|
96
65
|
press: async (key) => {
|
|
97
66
|
const parts = key.split('+');
|
|
98
67
|
const target = parts.pop();
|
|
@@ -106,14 +75,9 @@ function fromPuppeteer(page) {
|
|
|
106
75
|
waitForTimeout: (timeout) => new Promise((resolve) => setTimeout(resolve, timeout)),
|
|
107
76
|
waitForSelector: async (selector, options) => wrapHandle(await page.waitForSelector(selector, toPuppeteerSelectorOptions(options))),
|
|
108
77
|
viewportSize: () => page.viewport(),
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Same name and shape on both libraries, but it still has to be forwarded:
|
|
113
|
-
// the wrapper is a fresh object literal, so anything not listed here is
|
|
114
|
-
// simply absent. The auto-solve watcher reads it to end its loop when the
|
|
115
|
-
// caller closes the browser, and an undefined `isClosed` reads as "still
|
|
116
|
-
// open" — a watcher that polls a dead page forever.
|
|
78
|
+
evaluate: (pageFunction) => page.evaluate(pageFunction),
|
|
79
|
+
locator: (selector) => locatorOver(() => page.$$(selector)),
|
|
80
|
+
// Forwarded explicitly: without it the watcher polls a dead page forever.
|
|
117
81
|
isClosed: () => page.isClosed(),
|
|
118
82
|
};
|
|
119
83
|
}
|
package/dist/python-command.d.ts
CHANGED
|
@@ -1,50 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which interpreter runs the bundled CaptchaKraken CLI.
|
|
3
|
-
*
|
|
4
|
-
* This used to be `pythonCommand = 'python'`, inline in solver.ts. On
|
|
5
|
-
* Debian/Ubuntu — the most common Linux deployment target, and what CI runs —
|
|
6
|
-
* there is no `python` on PATH at all, only `python3`. So every JS solve died
|
|
7
|
-
* with `/bin/sh: 1: python: not found` before it ever reached the model, unless
|
|
8
|
-
* a `.venv` happened to exist next to the package. The type doc claimed the
|
|
9
|
-
* default was "'python' or 'python3'"; the code never tried `python3`.
|
|
10
|
-
*
|
|
11
|
-
* That is a straight violation of the rule that the Python and JS clients
|
|
12
|
-
* behave identically: the Python client obviously ran, so the divergence looked
|
|
13
|
-
* like a model or endpoint problem rather than a missing binary.
|
|
14
|
-
*
|
|
15
|
-
* Kept in its own module, with the probe injected, so the resolution order is
|
|
16
|
-
* testable without spawning anything.
|
|
17
|
-
*/
|
|
18
|
-
/** Env var that overrides everything below it. */
|
|
19
1
|
export declare const PYTHON_ENV_VAR = "CAPTCHA_KRAKEN_PYTHON";
|
|
20
|
-
/**
|
|
21
|
-
* Candidates tried in order when nothing is configured.
|
|
22
|
-
*
|
|
23
|
-
* `python3` FIRST. On Debian-family systems `python` is absent; where both
|
|
24
|
-
* exist they are the same interpreter in practice, and PEP 394 makes `python3`
|
|
25
|
-
* the portable spelling. `python` stays as the fallback for Windows and for
|
|
26
|
-
* venvs that only expose the unsuffixed name.
|
|
27
|
-
*/
|
|
28
2
|
export declare const PYTHON_CANDIDATES: readonly ["python3", "python"];
|
|
29
3
|
export interface ResolveOptions {
|
|
30
|
-
/** `config.pythonCommand`, if the caller set one. Wins outright. */
|
|
31
4
|
configured?: string;
|
|
32
|
-
/** The venv interpreter, when one was found next to the CLI. Wins over probing. */
|
|
33
5
|
venvPython?: string | null;
|
|
34
|
-
/** Process env, injected for testability. */
|
|
35
6
|
env?: NodeJS.ProcessEnv;
|
|
36
|
-
/** Returns true if the command is runnable. Injected so tests never spawn. */
|
|
37
7
|
exists?: (command: string) => boolean;
|
|
38
8
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Resolve the interpreter, most specific first:
|
|
41
|
-
*
|
|
42
|
-
* 1. an explicitly configured `pythonCommand`
|
|
43
|
-
* 2. `CAPTCHAKRAKEN_PYTHON`
|
|
44
|
-
* 3. the venv interpreter beside the bundled CLI
|
|
45
|
-
* 4. the first of `python3`, `python` that actually exists
|
|
46
|
-
*
|
|
47
|
-
* Falls back to `python3` rather than `python` when nothing can be probed: on
|
|
48
|
-
* the platform where the two differ, `python3` is the one that is present.
|
|
49
|
-
*/
|
|
50
9
|
export declare function resolvePythonCommand(options?: ResolveOptions): string;
|
package/dist/python-command.js
CHANGED
|
@@ -1,46 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Which interpreter runs the bundled CaptchaKraken CLI.
|
|
4
|
-
*
|
|
5
|
-
* This used to be `pythonCommand = 'python'`, inline in solver.ts. On
|
|
6
|
-
* Debian/Ubuntu — the most common Linux deployment target, and what CI runs —
|
|
7
|
-
* there is no `python` on PATH at all, only `python3`. So every JS solve died
|
|
8
|
-
* with `/bin/sh: 1: python: not found` before it ever reached the model, unless
|
|
9
|
-
* a `.venv` happened to exist next to the package. The type doc claimed the
|
|
10
|
-
* default was "'python' or 'python3'"; the code never tried `python3`.
|
|
11
|
-
*
|
|
12
|
-
* That is a straight violation of the rule that the Python and JS clients
|
|
13
|
-
* behave identically: the Python client obviously ran, so the divergence looked
|
|
14
|
-
* like a model or endpoint problem rather than a missing binary.
|
|
15
|
-
*
|
|
16
|
-
* Kept in its own module, with the probe injected, so the resolution order is
|
|
17
|
-
* testable without spawning anything.
|
|
18
|
-
*/
|
|
19
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
3
|
exports.PYTHON_CANDIDATES = exports.PYTHON_ENV_VAR = void 0;
|
|
21
4
|
exports.resolvePythonCommand = resolvePythonCommand;
|
|
22
|
-
/** Env var that overrides everything below it. */
|
|
23
5
|
exports.PYTHON_ENV_VAR = 'CAPTCHA_KRAKEN_PYTHON';
|
|
24
|
-
/**
|
|
25
|
-
* Candidates tried in order when nothing is configured.
|
|
26
|
-
*
|
|
27
|
-
* `python3` FIRST. On Debian-family systems `python` is absent; where both
|
|
28
|
-
* exist they are the same interpreter in practice, and PEP 394 makes `python3`
|
|
29
|
-
* the portable spelling. `python` stays as the fallback for Windows and for
|
|
30
|
-
* venvs that only expose the unsuffixed name.
|
|
31
|
-
*/
|
|
32
6
|
exports.PYTHON_CANDIDATES = ['python3', 'python'];
|
|
33
|
-
/**
|
|
34
|
-
* Resolve the interpreter, most specific first:
|
|
35
|
-
*
|
|
36
|
-
* 1. an explicitly configured `pythonCommand`
|
|
37
|
-
* 2. `CAPTCHAKRAKEN_PYTHON`
|
|
38
|
-
* 3. the venv interpreter beside the bundled CLI
|
|
39
|
-
* 4. the first of `python3`, `python` that actually exists
|
|
40
|
-
*
|
|
41
|
-
* Falls back to `python3` rather than `python` when nothing can be probed: on
|
|
42
|
-
* the platform where the two differ, `python3` is the one that is present.
|
|
43
|
-
*/
|
|
44
7
|
function resolvePythonCommand(options = {}) {
|
|
45
8
|
const { configured, venvPython, env = process.env, exists } = options;
|
|
46
9
|
if (configured)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every selector the driver knows, keyed by vendor. Mirrored in python/src/captchakraken/selectors.py; keep both in the
|
|
3
|
+
* same order, because detection takes the first visible match in table order.
|
|
4
|
+
*/
|
|
5
|
+
import { FrameRole, RecaptchaBanner, Vendor } from './kinds';
|
|
6
|
+
export interface VendorSelectors {
|
|
7
|
+
/** URL substrings of the vendor's code on the wire. A tripwire, not a detector: the host stays on the wire when a vendor renames its markup (see TRIBAL_KNOWLEDGE.md). BotDetect is self-hosted, so it has none. */
|
|
8
|
+
readonly hosts: readonly string[];
|
|
9
|
+
/** On the host page: the open challenge. Detected first. */
|
|
10
|
+
readonly challenge?: readonly string[];
|
|
11
|
+
/** On the host page: the checkbox. Detected while `response` is empty and `checked` is not showing. */
|
|
12
|
+
readonly checkbox?: readonly string[];
|
|
13
|
+
/** On the host page: inline widget shapes, detected last. Iframe selectors go first; a class that lives inside the frame document (.CheckboxCaptcha, .mtcap) only ever matches an inline embed. */
|
|
14
|
+
readonly widget?: readonly string[];
|
|
15
|
+
/** On the host page: the field the vendor fills on success. Read first and unconditionally, because hCaptcha's overlay hides the anchor. */
|
|
16
|
+
readonly response?: string;
|
|
17
|
+
/** Inside the checkbox frame: the box shows accepted. Demo pages do not always fill `response`. */
|
|
18
|
+
readonly checked?: string;
|
|
19
|
+
/** On the host page: the vendor painted its success state. Visibility is part of the test (see TRIBAL_KNOWLEDGE.md). */
|
|
20
|
+
readonly accepted?: string;
|
|
21
|
+
/** Inside the challenge frame: a new round has painted, with text. */
|
|
22
|
+
readonly fresh?: string;
|
|
23
|
+
/** Inside the challenge frame: the pictures a board is made of; the driver waits for each to have painted. */
|
|
24
|
+
readonly images?: readonly string[];
|
|
25
|
+
/** Inside the widget: the vendor's own submit control, tried after the generic text probes. */
|
|
26
|
+
readonly submit?: readonly string[];
|
|
27
|
+
/** Inside the challenge frame: verdict banners and what each means. */
|
|
28
|
+
readonly banners?: ReadonlyArray<readonly [string, RecaptchaBanner]>;
|
|
29
|
+
/** Inside the widget, and its enclosing fieldset/form: the answer box of a typed captcha. */
|
|
30
|
+
readonly textInput?: readonly string[];
|
|
31
|
+
/** Inside the widget: the knob a slide must start on; a drag from the piece moves nothing. */
|
|
32
|
+
readonly sliderHandle?: readonly string[];
|
|
33
|
+
/** Inside the widget: the piece, measured to steer the slide and dragged directly when there is no track. */
|
|
34
|
+
readonly piece?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
export declare const SELECTORS: Readonly<Record<Vendor, VendorSelectors>>;
|
|
37
|
+
export declare const VENDORS: ReadonlyArray<readonly [Vendor, VendorSelectors]>;
|
|
38
|
+
export interface WidgetProbe {
|
|
39
|
+
readonly vendor: Vendor;
|
|
40
|
+
readonly role: FrameRole;
|
|
41
|
+
readonly selector: string;
|
|
42
|
+
}
|
|
43
|
+
/** Every host-page selector with what it names, in detection order: open challenges, then checkboxes, then inline widgets. */
|
|
44
|
+
export declare const WIDGET_PROBES: ReadonlyArray<WidgetProbe>;
|
|
45
|
+
export declare const RESPONSE_SELECTORS: readonly string[];
|
|
46
|
+
export declare const ACCEPTED_SELECTORS: readonly string[];
|
|
47
|
+
/** `.//` keeps the xpath relative so a host-page widget cannot reach the form's own submit. */
|
|
48
|
+
export declare const submitByText: (text: string) => string;
|
|
49
|
+
/** A real Verify/Next/Submit/Skip first; the vendor-named controls are fallbacks. */
|
|
50
|
+
export declare const SUBMIT_SELECTORS: readonly string[];
|
|
51
|
+
export declare const TEXT_INPUT_VENDOR_SELECTORS: readonly string[];
|
|
52
|
+
export declare const TEXT_INPUT_SELECTORS: readonly string[];
|
|
53
|
+
export declare const SLIDER_HANDLE_SELECTORS: readonly string[];
|
|
54
|
+
export declare const PIECE_SELECTORS: readonly string[];
|