captchakraken 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +43 -0
- package/dist/playwright-types.d.ts +119 -0
- package/dist/playwright-types.js +25 -0
- package/dist/puppeteer-adapter.d.ts +70 -0
- package/dist/puppeteer-adapter.js +96 -0
- package/dist/solver.d.ts +264 -0
- package/dist/solver.js +1922 -0
- package/dist/token-usage.d.ts +15 -0
- package/dist/token-usage.js +102 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.js +2 -0
- package/package.json +49 -0
- package/python/Dockerfile +41 -0
- package/python/README.md +71 -0
- package/python/examples/README.md +68 -0
- package/python/examples/_harness.py +158 -0
- package/python/examples/demoHcaptcha.py +20 -0
- package/python/examples/demoRecaptcha.py +17 -0
- package/python/pyproject.toml +79 -0
- package/python/src/captchakraken/__init__.py +61 -0
- package/python/src/captchakraken/action_types.py +56 -0
- package/python/src/captchakraken/cli.py +656 -0
- package/python/src/captchakraken/config.py +78 -0
- package/python/src/captchakraken/image_processor.py +244 -0
- package/python/src/captchakraken/overlay.py +520 -0
- package/python/src/captchakraken/planner.py +408 -0
- package/python/src/captchakraken/planner_types.py +74 -0
- package/python/src/captchakraken/server_manager.py +290 -0
- package/python/src/captchakraken/solver.py +434 -0
- package/python/src/captchakraken/timing.py +42 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +72 -0
- package/python/src/captchakraken/tool_calls/find_grid.py +1762 -0
- package/python/src/captchakraken/tool_calls/move_indicator.py +431 -0
- package/scripts/copy-python.mjs +29 -0
- package/scripts/setup-python.js +104 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared runner for the CaptchaKraken Python demos.
|
|
3
|
+
|
|
4
|
+
The Python port is the *engine* (OpenCV detection + the vLLM planner). These demos
|
|
5
|
+
launch a real stealth browser (camoufox, using the binary from your fork —
|
|
6
|
+
JWriter20/camoufox releases; see README.md), navigate to a captcha demo site,
|
|
7
|
+
open the challenge, screenshot it, and run the engine on that frame. They report
|
|
8
|
+
token-generation speed, total time, and whether the engine produced a valid
|
|
9
|
+
solution — plus a best-effort reason when it didn't.
|
|
10
|
+
|
|
11
|
+
Note: full click-replay + multi-round verification in a live page is what the
|
|
12
|
+
TypeScript port (`captchakraken`) does end-to-end. Here we validate the
|
|
13
|
+
engine + model + server on a real challenge frame.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from camoufox.sync_api import Camoufox
|
|
22
|
+
|
|
23
|
+
from captchakraken import CaptchaSolver
|
|
24
|
+
from captchakraken.action_types import ClickAction
|
|
25
|
+
from captchakraken.solver import UnsupportedCaptchaError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class DemoSpec:
|
|
30
|
+
name: str
|
|
31
|
+
url: str
|
|
32
|
+
vendor: str # "recaptcha" | "hcaptcha"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _launch_kwargs() -> dict:
|
|
36
|
+
kw = dict(headless=os.getenv("HEADLESS", "1") != "0", humanize=True, geoip=False)
|
|
37
|
+
# Point camoufox at YOUR fork's binary. If unset, camoufox uses its default
|
|
38
|
+
# cached binary (`python -m camoufox fetch`).
|
|
39
|
+
binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
|
|
40
|
+
if binary:
|
|
41
|
+
kw["executable_path"] = binary
|
|
42
|
+
return kw
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _capture_challenge(page, spec: DemoSpec) -> str:
|
|
46
|
+
"""Reach the challenge iframe and screenshot it to a temp PNG. Returns the path.
|
|
47
|
+
|
|
48
|
+
Raises RuntimeError with a human message if the challenge never appears.
|
|
49
|
+
"""
|
|
50
|
+
out = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
|
|
51
|
+
|
|
52
|
+
if spec.vendor == "recaptcha":
|
|
53
|
+
anchor = page.wait_for_selector('iframe[src*="/recaptcha/api2/anchor"]', timeout=30_000)
|
|
54
|
+
frame = anchor.content_frame()
|
|
55
|
+
frame.click("#recaptcha-anchor", timeout=15_000)
|
|
56
|
+
challenge = page.wait_for_selector('iframe[src*="/recaptcha/api2/bframe"]', timeout=30_000)
|
|
57
|
+
# Let the tiles paint.
|
|
58
|
+
page.wait_for_timeout(2500)
|
|
59
|
+
challenge.screenshot(path=out)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
if spec.vendor == "hcaptcha":
|
|
63
|
+
box = page.wait_for_selector('iframe[src*="hcaptcha.com"][src*="checkbox"], iframe[title*="checkbox"]',
|
|
64
|
+
timeout=30_000)
|
|
65
|
+
box.content_frame().click("#checkbox, div[role=checkbox]", timeout=15_000)
|
|
66
|
+
challenge = page.wait_for_selector('iframe[title*="hCaptcha challenge"], iframe[src*="hcaptcha.com"][src*="frame=challenge"]',
|
|
67
|
+
timeout=30_000)
|
|
68
|
+
page.wait_for_timeout(2500)
|
|
69
|
+
challenge.screenshot(path=out)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
raise RuntimeError(f"unknown vendor {spec.vendor!r}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _tokens_per_sec(solver: CaptchaSolver, elapsed_s: float) -> tuple[int, int, float]:
|
|
76
|
+
"""(input_tokens, output_tokens, tokens/sec) from the planner's usage log."""
|
|
77
|
+
inp = out = 0
|
|
78
|
+
for u in getattr(solver.planner, "token_usage", []) or []:
|
|
79
|
+
inp += int(u.get("prompt_tokens", 0) or 0)
|
|
80
|
+
out += int(u.get("completion_tokens", 0) or 0)
|
|
81
|
+
tps = out / elapsed_s if out and elapsed_s > 0 else 0.0
|
|
82
|
+
return inp, out, tps
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _explain(vendor: str, err: Exception) -> str:
|
|
86
|
+
msg = str(err).lower()
|
|
87
|
+
if isinstance(err, UnsupportedCaptchaError) or "cannot solve" in msg or "unsupported" in msg:
|
|
88
|
+
if vendor == "hcaptcha":
|
|
89
|
+
return ("hCaptcha served a non-grid challenge (drag / video / choose-the-card). "
|
|
90
|
+
"The engine handles image grids + checkboxes only — re-run to try for a grid.")
|
|
91
|
+
return "Not a supported grid/checkbox challenge — re-run to try again."
|
|
92
|
+
if "vllm" in msg or "connection" in msg or "refused" in msg or "max retries" in msg:
|
|
93
|
+
return ("Could not reach the vLLM server — is it up and is VLLM_BASE_URL correct? "
|
|
94
|
+
"(A local server auto-starts only if captchakraken[serve] is installed.)")
|
|
95
|
+
if "timeout" in msg or "wait_for_selector" in msg:
|
|
96
|
+
return "The challenge iframe never appeared (slow network, blocked widget, or the checkbox auto-passed)."
|
|
97
|
+
return str(err) or "Unknown failure."
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_demo(spec: DemoSpec) -> None:
|
|
101
|
+
t0 = time.time()
|
|
102
|
+
ok = False
|
|
103
|
+
reason = None
|
|
104
|
+
inp = out = 0
|
|
105
|
+
tps = 0.0
|
|
106
|
+
solve_s = 0.0
|
|
107
|
+
plan_desc = "-"
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
with Camoufox(**_launch_kwargs()) as browser:
|
|
111
|
+
page = browser.new_page()
|
|
112
|
+
page.goto(spec.url, wait_until="domcontentloaded", timeout=60_000)
|
|
113
|
+
shot = _capture_challenge(page, spec)
|
|
114
|
+
|
|
115
|
+
solver = CaptchaSolver()
|
|
116
|
+
s0 = time.time()
|
|
117
|
+
try:
|
|
118
|
+
actions = solver.solve(shot, puzzle_source=spec.vendor)
|
|
119
|
+
finally:
|
|
120
|
+
solve_s = time.time() - s0
|
|
121
|
+
inp, out, tps = _tokens_per_sec(solver, solve_s)
|
|
122
|
+
|
|
123
|
+
acts = actions if isinstance(actions, list) else [actions]
|
|
124
|
+
clicks = [a for a in acts if isinstance(a, ClickAction) and (a.target_bounding_boxes or [])]
|
|
125
|
+
n_tiles = sum(len(a.target_bounding_boxes or []) for a in clicks)
|
|
126
|
+
if n_tiles > 0:
|
|
127
|
+
ok = True
|
|
128
|
+
plan_desc = f"click plan: {n_tiles} tile(s)/target(s)"
|
|
129
|
+
else:
|
|
130
|
+
plan_desc = f"actions: {[type(a).__name__ for a in acts]}"
|
|
131
|
+
reason = ("The model returned no tiles to click — either none matched the prompt, "
|
|
132
|
+
"or the challenge frame wasn't a clean grid. Re-run to try again.")
|
|
133
|
+
except Exception as err: # noqa: BLE001 — demo: report, don't traceback
|
|
134
|
+
reason = _explain(spec.vendor, err)
|
|
135
|
+
|
|
136
|
+
_report(spec, ok=ok, total_s=time.time() - t0, solve_s=solve_s,
|
|
137
|
+
inp=inp, out=out, tps=tps, plan=plan_desc, reason=reason)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _fmt(s: float) -> str:
|
|
141
|
+
return f"{s:.1f}s" if s >= 1 else f"{int(s * 1000)}ms"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _report(spec, *, ok, total_s, solve_s, inp, out, tps, plan, reason):
|
|
145
|
+
line = "─" * 52
|
|
146
|
+
print(f"\n{line}")
|
|
147
|
+
print(f" CaptchaKraken demo (engine) — {spec.name}")
|
|
148
|
+
print(f" {spec.url}")
|
|
149
|
+
print(line)
|
|
150
|
+
print(f" result : {'✓ engine produced a solution' if ok else '✗ no solution'}")
|
|
151
|
+
print(f" {plan}")
|
|
152
|
+
print(f" total time : {_fmt(total_s)} (solve: {_fmt(solve_s)})")
|
|
153
|
+
print(f" tokens : {inp} in / {out} out")
|
|
154
|
+
print(f" gen speed : {f'~{tps:.1f} tok/s' if tps > 0 else 'n/a'}")
|
|
155
|
+
if reason:
|
|
156
|
+
print(f" reason : {reason}")
|
|
157
|
+
print(f"{line}\n")
|
|
158
|
+
raise SystemExit(0 if ok else 1)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine demo: run CaptchaKraken on the standard hCaptcha demo page.
|
|
3
|
+
|
|
4
|
+
pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
|
|
5
|
+
pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
|
|
6
|
+
source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
|
|
7
|
+
python examples/demoHcaptcha.py
|
|
8
|
+
|
|
9
|
+
Note: hCaptcha randomly serves non-grid puzzles (drag / video / choose-the-card),
|
|
10
|
+
which the engine does not handle yet — the report says so; re-run for a grid.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from _harness import DemoSpec, run_demo
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
run_demo(DemoSpec(
|
|
17
|
+
name="hCaptcha",
|
|
18
|
+
url="https://accounts.hcaptcha.com/demo",
|
|
19
|
+
vendor="hcaptcha",
|
|
20
|
+
))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine demo: run CaptchaKraken on Google's standard reCAPTCHA v2 demo page.
|
|
3
|
+
|
|
4
|
+
pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
|
|
5
|
+
pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
|
|
6
|
+
source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
|
|
7
|
+
python examples/demoRecaptcha.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from _harness import DemoSpec, run_demo
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
run_demo(DemoSpec(
|
|
14
|
+
name="reCAPTCHA v2",
|
|
15
|
+
url="https://www.google.com/recaptcha/api2/demo",
|
|
16
|
+
vendor="recaptcha",
|
|
17
|
+
))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "captchakraken"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Self-hosted captcha solver: OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA served on vLLM."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "GPL-3.0-or-later" }
|
|
12
|
+
authors = [{ name = "Jake Writer" }]
|
|
13
|
+
keywords = ["captcha", "recaptcha", "hcaptcha", "vllm", "qwen", "computer-vision", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Core = the lightweight CLIENT: OpenCV grid detection + the HTTP planner that
|
|
21
|
+
# talks to a vLLM server (local or remote). This is all you need to SOLVE
|
|
22
|
+
# captchas against an existing endpoint. The heavy serving stack (vllm/torch)
|
|
23
|
+
# is deliberately NOT here — see the `serve` extra.
|
|
24
|
+
dependencies = [
|
|
25
|
+
"pydantic>=2.0.0",
|
|
26
|
+
"pillow>=10.0.0",
|
|
27
|
+
"numpy>=1.24.0",
|
|
28
|
+
"opencv-python-headless>=4.10.0",
|
|
29
|
+
"requests>=2.31.0",
|
|
30
|
+
"python-dotenv>=1.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# Self-hosting: install this to RUN a local vLLM server (the setup script does
|
|
35
|
+
# `pip install "captchakraken[serve]"`). Not needed to call a remote server.
|
|
36
|
+
serve = [
|
|
37
|
+
"vllm>=0.6.3",
|
|
38
|
+
"torch>=2.0.0",
|
|
39
|
+
"transformers>=4.40.0",
|
|
40
|
+
"accelerate>=0.27.0",
|
|
41
|
+
"huggingface_hub>=0.23.0",
|
|
42
|
+
]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=7.0.0",
|
|
45
|
+
"mypy>=1.0.0",
|
|
46
|
+
"ruff>=0.1.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
captchakraken = "captchakraken.cli:main"
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/JWriter20/CaptchaKraken"
|
|
54
|
+
Issues = "https://github.com/JWriter20/CaptchaKraken/issues"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/captchakraken"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
pythonpath = ["src"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.10"
|
|
65
|
+
warn_return_any = false
|
|
66
|
+
warn_unused_configs = true
|
|
67
|
+
disallow_untyped_defs = false
|
|
68
|
+
check_untyped_defs = false
|
|
69
|
+
ignore_missing_imports = true
|
|
70
|
+
no_implicit_optional = false
|
|
71
|
+
exclude = ["build/", "dist/"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 120
|
|
75
|
+
target-version = "py310"
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "F", "W", "I"]
|
|
79
|
+
ignore = ["E501"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CaptchaKraken — OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA
|
|
3
|
+
served on vLLM.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from captchakraken import CaptchaSolver
|
|
7
|
+
solver = CaptchaSolver() # auto-starts / connects to a local vLLM server
|
|
8
|
+
actions = solver.solve("captcha.png")
|
|
9
|
+
|
|
10
|
+
Model/endpoint defaults live in `captchakraken.config` and are fully
|
|
11
|
+
env-overridable (VLLM_BASE_URL, CAPTCHA_LORA_ADAPTER, …); the solver itself is
|
|
12
|
+
model-agnostic. The legacy v1 stack (SAM3 grounding, multi-provider planner,
|
|
13
|
+
detect/segment/drag-refine) lives on the `v1-old-architecture` branch.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
try: # pragma: no cover
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
|
|
21
|
+
project_root = Path(__file__).resolve().parent.parent
|
|
22
|
+
load_dotenv(project_root / ".env")
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
from .action_types import (
|
|
27
|
+
CaptchaAction,
|
|
28
|
+
ClickAction,
|
|
29
|
+
DragAction,
|
|
30
|
+
TypeAction,
|
|
31
|
+
WaitAction,
|
|
32
|
+
)
|
|
33
|
+
from .image_processor import ImageProcessor
|
|
34
|
+
from .overlay import add_overlays_to_image
|
|
35
|
+
|
|
36
|
+
# The planner (requests) and solver (torch/vllm/transformers) pull in the heavy
|
|
37
|
+
# serving stack. Keep them optional so leaf modules — e.g. tool_calls.find_grid,
|
|
38
|
+
# which needs only cv2 + numpy + pillow — can be imported in a minimal env (CI's
|
|
39
|
+
# hermetic grid-detection test) without the full GPU dependency set installed.
|
|
40
|
+
try: # pragma: no cover - exercised only when the serving stack is installed
|
|
41
|
+
from .planner import ActionPlanner
|
|
42
|
+
from .solver import CaptchaSolver, solve_captcha
|
|
43
|
+
except ModuleNotFoundError:
|
|
44
|
+
ActionPlanner = None # type: ignore[assignment,misc]
|
|
45
|
+
CaptchaSolver = None # type: ignore[assignment,misc]
|
|
46
|
+
solve_captcha = None # type: ignore[assignment]
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"CaptchaSolver",
|
|
50
|
+
"solve_captcha",
|
|
51
|
+
"ActionPlanner",
|
|
52
|
+
"ImageProcessor",
|
|
53
|
+
"CaptchaAction",
|
|
54
|
+
"ClickAction",
|
|
55
|
+
"DragAction",
|
|
56
|
+
"TypeAction",
|
|
57
|
+
"WaitAction",
|
|
58
|
+
"add_overlays_to_image",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
__version__ = "2.0.0"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import List, Literal, Optional, Union, Tuple
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, RootModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BoundingBox(RootModel):
|
|
7
|
+
"""
|
|
8
|
+
Strongly typed bounding box: [x1, y1, x2, y2] in percentages (0.0 to 1.0).
|
|
9
|
+
Acts like a list for convenience but ensures exactly 4 float elements.
|
|
10
|
+
"""
|
|
11
|
+
root: Tuple[float, float, float, float]
|
|
12
|
+
|
|
13
|
+
def __iter__(self):
|
|
14
|
+
return iter(self.root)
|
|
15
|
+
|
|
16
|
+
def __getitem__(self, item):
|
|
17
|
+
return self.root[item]
|
|
18
|
+
|
|
19
|
+
def __len__(self):
|
|
20
|
+
return len(self.root)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Action(BaseModel):
|
|
24
|
+
action: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClickAction(Action):
|
|
28
|
+
action: Literal["click"]
|
|
29
|
+
target_bounding_boxes: List[BoundingBox] # List of [x1, y1, x2, y2] in percentages
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DragAction(Action):
|
|
33
|
+
action: Literal["drag"]
|
|
34
|
+
source_bounding_box: BoundingBox = None # [x1, y1, x2, y2] in percentages
|
|
35
|
+
target_bounding_box: BoundingBox = None # [x1, y1, x2, y2] in percentages
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TypeAction(Action):
|
|
39
|
+
"""Type text into an input."""
|
|
40
|
+
action: Literal["type"]
|
|
41
|
+
text: str
|
|
42
|
+
target_bounding_box: BoundingBox = None # [x1, y1, x2, y2] in percentages
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WaitAction(Action):
|
|
46
|
+
"""Wait for a specified duration."""
|
|
47
|
+
action: Literal["wait"]
|
|
48
|
+
duration_ms: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DoneAction(Action):
|
|
52
|
+
"""Signal that the captcha is solved or no further actions are needed."""
|
|
53
|
+
action: Literal["done"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
CaptchaAction = Union[ClickAction, DragAction, TypeAction, WaitAction, DoneAction]
|