captchakraken 2.3.0 → 2.6.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/LICENSE +47 -7
- package/README.md +24 -4
- package/dist/cli-invocation.d.ts +46 -0
- package/dist/cli-invocation.js +68 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +74 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +52 -1
- package/dist/limits.d.ts +51 -0
- package/dist/limits.js +54 -0
- package/dist/model-name.d.ts +23 -0
- package/dist/model-name.js +70 -0
- package/dist/playwright-types.d.ts +28 -0
- package/dist/puppeteer-adapter.d.ts +11 -0
- package/dist/puppeteer-adapter.js +23 -0
- package/dist/python-command.d.ts +50 -0
- package/dist/python-command.js +60 -0
- package/dist/slide-geometry.d.ts +33 -0
- package/dist/slide-geometry.js +64 -0
- package/dist/solver.d.ts +214 -12
- package/dist/solver.js +1297 -165
- package/dist/trajectory.d.ts +31 -0
- package/dist/trajectory.js +162 -0
- package/dist/types.d.ts +128 -11
- package/dist/watcher.d.ts +99 -0
- package/dist/watcher.js +129 -0
- package/package.json +2 -5
- package/python/LICENSE +47 -7
- package/python/README.md +25 -8
- package/python/examples/README.md +36 -0
- package/python/examples/_harness.py +182 -94
- package/python/examples/demoHcaptcha.py +2 -1
- package/python/examples/demoRecaptcha.py +2 -1
- package/python/examples/watch_playwright.py +56 -0
- package/python/examples/with_playwright.py +43 -0
- package/python/pyproject.toml +4 -3
- package/python/src/captchakraken/__init__.py +5 -3
- package/python/src/captchakraken/action_types.py +24 -3
- package/python/src/captchakraken/cli.py +224 -2
- package/python/src/captchakraken/config.py +119 -17
- package/python/src/captchakraken/errors.py +195 -0
- package/python/src/captchakraken/keyframes.py +690 -0
- package/python/src/captchakraken/models.json +120 -0
- package/python/src/captchakraken/page_solver.py +1465 -102
- package/python/src/captchakraken/pinned_model.json +5 -7
- package/python/src/captchakraken/planner.py +328 -73
- package/python/src/captchakraken/prompts.py +485 -0
- package/python/src/captchakraken/solver.py +182 -11
- package/python/src/captchakraken/timing.py +70 -0
- package/python/src/captchakraken/tool_calls/find_grid.py +367 -52
- package/python/src/captchakraken/tool_calls/track_piece.py +96 -0
- package/python/src/captchakraken/trajectory.py +2 -7
- package/python/src/captchakraken/watcher.py +193 -0
- package/python/src/captchakraken/planner_types.py +0 -74
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""What the hosted API's refusals mean, in words a user can act on.
|
|
2
|
+
|
|
3
|
+
The solver talks to an OpenAI-compatible endpoint that may be a local vLLM the
|
|
4
|
+
user started themselves or the hosted CaptchaKraken gateway. Those two fail very
|
|
5
|
+
differently, and until this module existed both were reported the same way:
|
|
6
|
+
`vLLM 402 Payment Required at https://api.captchakraken.com/v1/chat/completions`.
|
|
7
|
+
A camoufox user who has never heard of vLLM, run vLLM, or intended to run vLLM
|
|
8
|
+
would hit that on the day their credits ran out.
|
|
9
|
+
|
|
10
|
+
Two rules, both inherited from the gateway's own `src/errors.ts`:
|
|
11
|
+
|
|
12
|
+
1. BRANCH ON `error.code`, NEVER ON PROSE. The messages get reworded; the codes
|
|
13
|
+
are the contract. A client that pattern-matches "Out of credits" breaks the
|
|
14
|
+
day someone improves the wording.
|
|
15
|
+
|
|
16
|
+
2. AN UNRECOGNISED CODE MUST STILL PRODUCE A USEFUL MESSAGE. There are ten
|
|
17
|
+
codes today and there will be more. Enumerating them exhaustively here means
|
|
18
|
+
the eleventh is reported worse than the ten — so the fallback carries the
|
|
19
|
+
server's own `message` and `resolution_url` through, and only the phrasing
|
|
20
|
+
is lost.
|
|
21
|
+
|
|
22
|
+
The self-hosted path is deliberately untouched: a local vLLM does not send this
|
|
23
|
+
envelope, so `from_response` returns a message shaped like the old one and the
|
|
24
|
+
401/403 hint about the bearer token survives. Nobody self-hosting sees a word
|
|
25
|
+
about credits.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from typing import Any, Dict, Optional
|
|
29
|
+
|
|
30
|
+
# Where a user with no `resolution_url` in hand should be sent. Only used as a
|
|
31
|
+
# fallback — the server almost always supplies a deep link that is better than
|
|
32
|
+
# this, and when it does we prefer it.
|
|
33
|
+
_DASHBOARD = "https://captchakraken.com/dashboard"
|
|
34
|
+
_SUPPORT = "https://captchakraken.com/support"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CaptchaKrakenAPIError(RuntimeError):
|
|
38
|
+
"""A refusal from the hosted API, already translated.
|
|
39
|
+
|
|
40
|
+
Carries the machine-readable parts alongside the message so a caller that
|
|
41
|
+
wants to react programmatically (retry on `rate_limited`, stop on
|
|
42
|
+
`insufficient_credits`) can do so without re-parsing anything. `str(e)` is
|
|
43
|
+
the human sentence and is what reaches a console.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
message: str,
|
|
49
|
+
*,
|
|
50
|
+
status: Optional[int] = None,
|
|
51
|
+
code: Optional[str] = None,
|
|
52
|
+
resolution_url: Optional[str] = None,
|
|
53
|
+
retry_after_seconds: Optional[float] = None,
|
|
54
|
+
):
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
self.status = status
|
|
57
|
+
self.code = code
|
|
58
|
+
self.resolution_url = resolution_url
|
|
59
|
+
self.retry_after_seconds = retry_after_seconds
|
|
60
|
+
|
|
61
|
+
def to_payload(self) -> Dict[str, Any]:
|
|
62
|
+
"""The shape the CLI puts on stderr for the JS driver to pick up.
|
|
63
|
+
|
|
64
|
+
The JS half shells out to this CLI and can only see stdout/stderr, so
|
|
65
|
+
the structured fields would otherwise be flattened to a string at the
|
|
66
|
+
process boundary and have to be re-parsed out of prose — exactly the
|
|
67
|
+
thing rule 1 forbids.
|
|
68
|
+
"""
|
|
69
|
+
return {
|
|
70
|
+
"error": str(self),
|
|
71
|
+
"ck_error": {
|
|
72
|
+
"status": self.status,
|
|
73
|
+
"code": self.code,
|
|
74
|
+
"resolution_url": self.resolution_url,
|
|
75
|
+
"retry_after_seconds": self.retry_after_seconds,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _retry_after(headers: Any) -> Optional[float]:
|
|
81
|
+
"""Seconds from the `Retry-After` header, if it is the numeric form.
|
|
82
|
+
|
|
83
|
+
HTTP also permits an HTTP-date here. The gateway only ever sends deltas, and
|
|
84
|
+
guessing wrong about a date would produce a confidently incorrect "wait 3
|
|
85
|
+
seconds", so anything unparseable is dropped rather than approximated.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
raw = headers.get("Retry-After")
|
|
89
|
+
except Exception: # noqa: BLE001 — a mapping-ish object is all we assume
|
|
90
|
+
return None
|
|
91
|
+
if raw is None:
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
return float(str(raw).strip())
|
|
95
|
+
except ValueError:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _sentence(code: str, message: str, url: Optional[str], retry: Optional[float]) -> str:
|
|
100
|
+
"""The user-facing line for a known code.
|
|
101
|
+
|
|
102
|
+
Every branch names CaptchaKraken. That is the entire point of this module:
|
|
103
|
+
the person reading it needs to know which product is refusing them before
|
|
104
|
+
anything else in the sentence can help.
|
|
105
|
+
"""
|
|
106
|
+
if code == "insufficient_credits":
|
|
107
|
+
return (
|
|
108
|
+
"CaptchaKraken: your account is out of credits, so this solve was refused. "
|
|
109
|
+
f"Top up at {url or _DASHBOARD} and retry."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if code == "solve_abandoned":
|
|
113
|
+
return (
|
|
114
|
+
"CaptchaKraken: this captcha attempt was served too many times without "
|
|
115
|
+
"settling and has been abandoned. That usually means the IP reputation or "
|
|
116
|
+
"the browser fingerprint is being rejected — not that the answers were "
|
|
117
|
+
"wrong. Start a fresh attempt with a new x-ck-session (the driver mints "
|
|
118
|
+
"one per solve() call, so a new solve is enough)."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if code == "rate_limited":
|
|
122
|
+
wait = f" Retry in about {retry:g}s." if retry else " Back off and retry."
|
|
123
|
+
return f"CaptchaKraken: too many requests.{wait}"
|
|
124
|
+
|
|
125
|
+
if code == "account_suspended":
|
|
126
|
+
return (
|
|
127
|
+
"CaptchaKraken: this account is suspended, so solving is disabled. "
|
|
128
|
+
f"Contact support at {url or _SUPPORT}."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if code == "request_too_large":
|
|
132
|
+
return (
|
|
133
|
+
"CaptchaKraken: the screenshot sent for this solve exceeded the request "
|
|
134
|
+
f"size limit. ({message}) Capture the captcha element rather than the "
|
|
135
|
+
"whole page if you are not already."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if code in ("missing_api_key", "invalid_api_key"):
|
|
139
|
+
return (
|
|
140
|
+
"CaptchaKraken: the API key was missing or not accepted. Set "
|
|
141
|
+
"CAPTCHA_KRAKEN_API_KEY, or run the CaptchaKraken MCP server's "
|
|
142
|
+
"`create_api_key` tool to write one to ~/.captchakraken/credentials. "
|
|
143
|
+
f"Manage keys at {url or _DASHBOARD}."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if code == "upstream_unavailable":
|
|
147
|
+
return (
|
|
148
|
+
"CaptchaKraken: the solver fleet is temporarily unreachable. This is on "
|
|
149
|
+
"our side, not yours — retry shortly."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# `unrecognized_prompt`, `invalid_request`, and anything added after this was
|
|
153
|
+
# written. The server's own message is the best thing available, so it is
|
|
154
|
+
# carried through verbatim rather than replaced with a guess.
|
|
155
|
+
tail = f" See {url}." if url else ""
|
|
156
|
+
return f"CaptchaKraken: {message}{tail}"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def from_response(resp: Any, url: str) -> Exception:
|
|
160
|
+
"""Build the exception for a non-OK response, hosted or self-hosted.
|
|
161
|
+
|
|
162
|
+
Returns rather than raises so the caller's `raise` keeps the traceback
|
|
163
|
+
anchored at the request site, where it is useful.
|
|
164
|
+
"""
|
|
165
|
+
body_text = (getattr(resp, "text", "") or "")[:300]
|
|
166
|
+
status = getattr(resp, "status_code", None)
|
|
167
|
+
|
|
168
|
+
error: Dict[str, Any] = {}
|
|
169
|
+
try:
|
|
170
|
+
parsed = resp.json()
|
|
171
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("error"), dict):
|
|
172
|
+
error = parsed["error"]
|
|
173
|
+
except Exception: # noqa: BLE001 — a non-JSON body is the self-hosted case
|
|
174
|
+
error = {}
|
|
175
|
+
|
|
176
|
+
code = error.get("code")
|
|
177
|
+
if isinstance(code, str) and code:
|
|
178
|
+
retry = _retry_after(getattr(resp, "headers", {}) or {})
|
|
179
|
+
resolution = error.get("resolution_url") or None
|
|
180
|
+
message = str(error.get("message") or "").strip() or "the request was refused"
|
|
181
|
+
return CaptchaKrakenAPIError(
|
|
182
|
+
_sentence(code, message, resolution, retry),
|
|
183
|
+
status=status,
|
|
184
|
+
code=code,
|
|
185
|
+
resolution_url=resolution,
|
|
186
|
+
retry_after_seconds=retry,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# No gateway envelope: a local vLLM, a proxy in between, or an HTML error
|
|
190
|
+
# page. Keep the pre-existing message so self-hosted debugging is unchanged.
|
|
191
|
+
hint = ""
|
|
192
|
+
if status in (401, 403):
|
|
193
|
+
hint = " — check CAPTCHA_KRAKEN_API_KEY is set and forwarded to the CLI"
|
|
194
|
+
reason = getattr(resp, "reason", "") or ""
|
|
195
|
+
return RuntimeError(f"vLLM {status} {reason} at {url}{hint}. Body: {body_text}")
|