@biggora/claude-plugins 1.1.1 → 1.2.2
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/.claude/settings.local.json +3 -1
- package/README.md +24 -17
- package/package.json +1 -1
- package/registry/registry.json +319 -244
- package/specs/coding.md +24 -0
- package/specs/pod.md +2 -0
- package/src/skills/captcha/README.md +221 -0
- package/src/skills/captcha/SKILL.md +355 -0
- package/src/skills/captcha/references/captcha-types.md +254 -0
- package/src/skills/captcha/references/services.md +172 -0
- package/src/skills/captcha/references/stealth.md +238 -0
- package/src/skills/captcha/scripts/solve_captcha.py +323 -0
- package/src/skills/captcha/scripts/solve_image_grid.py +350 -0
- package/src/skills/codex-cli/SKILL.md +21 -11
- package/src/skills/gemini-cli/SKILL.md +27 -13
- package/src/skills/gemini-cli/references/commands.md +21 -14
- package/src/skills/gemini-cli/references/configuration.md +23 -18
- package/src/skills/gemini-cli/references/headless-and-scripting.md +7 -17
- package/src/skills/gemini-cli/references/mcp-and-extensions.md +12 -6
- package/src/skills/google-merchant-api/SKILL.md +581 -0
- package/src/skills/google-merchant-api/references/accounts.md +247 -0
- package/src/skills/google-merchant-api/references/content-api-legacy.md +216 -0
- package/src/skills/google-merchant-api/references/datasources.md +233 -0
- package/src/skills/google-merchant-api/references/inventories.md +201 -0
- package/src/skills/google-merchant-api/references/migration.md +267 -0
- package/src/skills/google-merchant-api/references/products.md +316 -0
- package/src/skills/google-merchant-api/references/promotions.md +201 -0
- package/src/skills/google-merchant-api/references/reports.md +240 -0
- package/src/skills/lv-aggregators-api/SKILL.md +113 -0
- package/src/skills/lv-aggregators-api/references/integration-guide.md +368 -0
- package/src/skills/lv-aggregators-api/references/kurpirkt.md +103 -0
- package/src/skills/lv-aggregators-api/references/salidzini.md +122 -0
- package/src/skills/notebook-lm/SKILL.md +1 -1
- package/src/skills/screen-recording/SKILL.md +243 -213
- package/src/skills/screen-recording/references/design-patterns.md +4 -2
- package/src/skills/screen-recording/references/ffmpeg-recording.md +473 -0
- package/src/skills/screen-recording/references/{approach1-programmatic.md → programmatic-generation.md} +45 -22
- package/src/skills/screen-recording/references/python-fallback.md +222 -0
- package/src/skills/tailwindcss-best-practices/SKILL.md +180 -0
- package/src/skills/tailwindcss-best-practices/references/best-practices-utility-patterns.md +87 -0
- package/src/skills/tailwindcss-best-practices/references/core-installation.md +109 -0
- package/src/skills/tailwindcss-best-practices/references/core-preflight.md +200 -0
- package/src/skills/tailwindcss-best-practices/references/core-responsive.md +163 -0
- package/src/skills/tailwindcss-best-practices/references/core-source-detection.md +114 -0
- package/src/skills/tailwindcss-best-practices/references/core-theme.md +108 -0
- package/src/skills/tailwindcss-best-practices/references/core-utility-classes.md +59 -0
- package/src/skills/tailwindcss-best-practices/references/core-variants.md +204 -0
- package/src/skills/tailwindcss-best-practices/references/effects-form-controls.md +76 -0
- package/src/skills/tailwindcss-best-practices/references/effects-mask.md +91 -0
- package/src/skills/tailwindcss-best-practices/references/effects-scroll-snap.md +59 -0
- package/src/skills/tailwindcss-best-practices/references/effects-text-shadow.md +78 -0
- package/src/skills/tailwindcss-best-practices/references/effects-transition-animation.md +80 -0
- package/src/skills/tailwindcss-best-practices/references/effects-visibility-interactivity.md +82 -0
- package/src/skills/tailwindcss-best-practices/references/features-content-detection.md +175 -0
- package/src/skills/tailwindcss-best-practices/references/features-custom-styles.md +203 -0
- package/src/skills/tailwindcss-best-practices/references/features-dark-mode.md +137 -0
- package/src/skills/tailwindcss-best-practices/references/features-functions-directives.md +241 -0
- package/src/skills/tailwindcss-best-practices/references/features-upgrade.md +160 -0
- package/src/skills/tailwindcss-best-practices/references/layout-aspect-ratio.md +39 -0
- package/src/skills/tailwindcss-best-practices/references/layout-columns.md +80 -0
- package/src/skills/tailwindcss-best-practices/references/layout-display.md +110 -0
- package/src/skills/tailwindcss-best-practices/references/layout-flexbox.md +112 -0
- package/src/skills/tailwindcss-best-practices/references/layout-grid.md +87 -0
- package/src/skills/tailwindcss-best-practices/references/layout-height.md +97 -0
- package/src/skills/tailwindcss-best-practices/references/layout-inset.md +103 -0
- package/src/skills/tailwindcss-best-practices/references/layout-logical-properties.md +92 -0
- package/src/skills/tailwindcss-best-practices/references/layout-margin.md +126 -0
- package/src/skills/tailwindcss-best-practices/references/layout-min-max-sizing.md +63 -0
- package/src/skills/tailwindcss-best-practices/references/layout-object-fit-position.md +64 -0
- package/src/skills/tailwindcss-best-practices/references/layout-overflow.md +57 -0
- package/src/skills/tailwindcss-best-practices/references/layout-padding.md +77 -0
- package/src/skills/tailwindcss-best-practices/references/layout-position.md +85 -0
- package/src/skills/tailwindcss-best-practices/references/layout-tables.md +67 -0
- package/src/skills/tailwindcss-best-practices/references/layout-width.md +102 -0
- package/src/skills/tailwindcss-best-practices/references/transform-base.md +68 -0
- package/src/skills/tailwindcss-best-practices/references/transform-rotate.md +70 -0
- package/src/skills/tailwindcss-best-practices/references/transform-scale.md +83 -0
- package/src/skills/tailwindcss-best-practices/references/transform-skew.md +62 -0
- package/src/skills/tailwindcss-best-practices/references/transform-translate.md +77 -0
- package/src/skills/tailwindcss-best-practices/references/typography-font-text.md +142 -0
- package/src/skills/tailwindcss-best-practices/references/typography-list-style.md +65 -0
- package/src/skills/tailwindcss-best-practices/references/typography-text-align.md +60 -0
- package/src/skills/tailwindcss-best-practices/references/visual-background.md +76 -0
- package/src/skills/tailwindcss-best-practices/references/visual-border.md +108 -0
- package/src/skills/tailwindcss-best-practices/references/visual-effects.md +111 -0
- package/src/skills/tailwindcss-best-practices/references/visual-svg.md +82 -0
- package/src/skills/test-mobile-app/SKILL.md +11 -6
- package/src/skills/test-mobile-app/scripts/analyze_apk.py +15 -4
- package/src/skills/test-mobile-app/scripts/check_environment.py +5 -5
- package/src/skills/test-mobile-app/scripts/run_tests.py +1 -1
- package/src/skills/test-web-ui/SKILL.md +264 -84
- package/src/skills/test-web-ui/scripts/discover.py +25 -12
- package/src/skills/test-web-ui/scripts/run_tests.py +3 -2
- package/src/skills/tm-search/SKILL.md +242 -106
- package/src/skills/tm-search/references/scraping-fallback.md +60 -95
- package/src/skills/tm-search/scripts/tm_search.py +453 -375
- package/src/skills/vite-best-practices/SKILL.md +115 -0
- package/src/skills/vite-best-practices/references/build-and-ssr.md +255 -0
- package/src/skills/vite-best-practices/references/core-config.md +231 -0
- package/src/skills/vite-best-practices/references/core-features.md +222 -0
- package/src/skills/vite-best-practices/references/core-plugin-api.md +294 -0
- package/src/skills/vite-best-practices/references/environment-api.md +108 -0
- package/src/skills/vite-best-practices/references/rolldown-migration.md +242 -0
- package/src/skills/screen-recording/references/approach2-xvfb.md +0 -232
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CAPTCHA solver — submits to 2captcha, CapMonster, or Anti-Captcha and returns a token.
|
|
4
|
+
|
|
5
|
+
No external dependencies required (uses Python stdlib only).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python solve_captcha.py --type recaptcha-v2 --sitekey KEY --pageurl URL
|
|
9
|
+
python solve_captcha.py --type recaptcha-v3 --sitekey KEY --pageurl URL --action verify
|
|
10
|
+
python solve_captcha.py --type hcaptcha --sitekey KEY --pageurl URL
|
|
11
|
+
python solve_captcha.py --type turnstile --sitekey KEY --pageurl URL
|
|
12
|
+
python solve_captcha.py --type image --image path/to/captcha.png
|
|
13
|
+
|
|
14
|
+
Environment variables:
|
|
15
|
+
CAPTCHA_API_KEY — API key for the solving service
|
|
16
|
+
CAPTCHA_SERVICE — "2captcha" (default) | "capmonster" | "anticaptcha"
|
|
17
|
+
|
|
18
|
+
Output (JSON to stdout):
|
|
19
|
+
{"success": true, "token": "03AGdBq25...", "type": "recaptcha-v2"}
|
|
20
|
+
{"success": false, "error": "ERROR_ZERO_BALANCE"}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import base64
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.parse
|
|
31
|
+
import urllib.request
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Base URLs for each service — all support the same 2captcha-compatible API
|
|
35
|
+
SERVICE_URLS = {
|
|
36
|
+
"2captcha": "https://2captcha.com",
|
|
37
|
+
"capmonster": "https://api.capmonster.cloud",
|
|
38
|
+
"anticaptcha": "https://api.anti-captcha.com",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# How long to wait before first poll (solvers need processing time)
|
|
42
|
+
INITIAL_WAIT = 15 # seconds
|
|
43
|
+
|
|
44
|
+
# Interval between poll attempts
|
|
45
|
+
POLL_INTERVAL = 5 # seconds
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# HTTP helpers (stdlib only)
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def _post(url, data):
|
|
53
|
+
"""HTTP POST with form-encoded body. Returns response text."""
|
|
54
|
+
payload = urllib.parse.urlencode(data).encode()
|
|
55
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
56
|
+
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
57
|
+
try:
|
|
58
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
59
|
+
return resp.read().decode()
|
|
60
|
+
except urllib.error.HTTPError as e:
|
|
61
|
+
raise RuntimeError(f"HTTP {e.code}: {e.read().decode()}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get(url):
|
|
65
|
+
"""HTTP GET. Returns response text."""
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(url, timeout=30) as resp:
|
|
68
|
+
return resp.read().decode()
|
|
69
|
+
except urllib.error.HTTPError as e:
|
|
70
|
+
raise RuntimeError(f"HTTP {e.code}: {e.read().decode()}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Task submission
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def _submit(api_key, service, data):
|
|
78
|
+
"""Submit a task. Returns the task ID string."""
|
|
79
|
+
base = SERVICE_URLS.get(service, SERVICE_URLS["2captcha"])
|
|
80
|
+
data = {**data, "key": api_key, "json": "1"}
|
|
81
|
+
resp = _post(f"{base}/in.php", data)
|
|
82
|
+
result = json.loads(resp)
|
|
83
|
+
if result.get("status") != 1:
|
|
84
|
+
raise RuntimeError(result.get("request", str(result)))
|
|
85
|
+
return str(result["request"])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def submit_recaptcha_v2(api_key, sitekey, pageurl, service="2captcha", invisible=False):
|
|
89
|
+
data = {
|
|
90
|
+
"method": "userrecaptcha",
|
|
91
|
+
"googlekey": sitekey,
|
|
92
|
+
"pageurl": pageurl,
|
|
93
|
+
}
|
|
94
|
+
if invisible:
|
|
95
|
+
data["invisible"] = "1"
|
|
96
|
+
return _submit(api_key, service, data)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def submit_recaptcha_v3(api_key, sitekey, pageurl, action="verify",
|
|
100
|
+
min_score=0.5, service="2captcha"):
|
|
101
|
+
return _submit(api_key, service, {
|
|
102
|
+
"method": "userrecaptcha",
|
|
103
|
+
"version": "v3",
|
|
104
|
+
"googlekey": sitekey,
|
|
105
|
+
"pageurl": pageurl,
|
|
106
|
+
"action": action,
|
|
107
|
+
"min_score": str(min_score),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def submit_hcaptcha(api_key, sitekey, pageurl, service="2captcha"):
|
|
112
|
+
return _submit(api_key, service, {
|
|
113
|
+
"method": "hcaptcha",
|
|
114
|
+
"sitekey": sitekey,
|
|
115
|
+
"pageurl": pageurl,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def submit_turnstile(api_key, sitekey, pageurl, service="2captcha"):
|
|
120
|
+
return _submit(api_key, service, {
|
|
121
|
+
"method": "turnstile",
|
|
122
|
+
"sitekey": sitekey,
|
|
123
|
+
"pageurl": pageurl,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def submit_image(api_key, image_source, service="2captcha"):
|
|
128
|
+
"""image_source: file path or raw base64 string."""
|
|
129
|
+
if os.path.isfile(image_source):
|
|
130
|
+
with open(image_source, "rb") as f:
|
|
131
|
+
img_b64 = base64.b64encode(f.read()).decode()
|
|
132
|
+
else:
|
|
133
|
+
img_b64 = image_source # assume already base64
|
|
134
|
+
return _submit(api_key, service, {
|
|
135
|
+
"method": "base64",
|
|
136
|
+
"body": img_b64,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Polling
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def poll(api_key, task_id, service="2captcha", timeout=120):
|
|
145
|
+
"""Poll until solved. Returns the token string."""
|
|
146
|
+
base = SERVICE_URLS.get(service, SERVICE_URLS["2captcha"])
|
|
147
|
+
url = (
|
|
148
|
+
f"{base}/res.php"
|
|
149
|
+
f"?key={urllib.parse.quote(api_key)}"
|
|
150
|
+
f"&action=get"
|
|
151
|
+
f"&id={urllib.parse.quote(task_id)}"
|
|
152
|
+
f"&json=1"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
print(f"[captcha] Waiting {INITIAL_WAIT}s for solver...", file=sys.stderr)
|
|
156
|
+
time.sleep(INITIAL_WAIT)
|
|
157
|
+
|
|
158
|
+
deadline = time.time() + timeout
|
|
159
|
+
while time.time() < deadline:
|
|
160
|
+
resp = _get(url)
|
|
161
|
+
result = json.loads(resp)
|
|
162
|
+
|
|
163
|
+
if result.get("status") == 1:
|
|
164
|
+
return result["request"]
|
|
165
|
+
|
|
166
|
+
if result.get("request") == "CAPCHA_NOT_READY":
|
|
167
|
+
remaining = int(deadline - time.time())
|
|
168
|
+
print(f"[captcha] Not ready yet, polling again in {POLL_INTERVAL}s "
|
|
169
|
+
f"({remaining}s remaining)...", file=sys.stderr)
|
|
170
|
+
time.sleep(POLL_INTERVAL)
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Any other response is an error
|
|
174
|
+
raise RuntimeError(result.get("request", str(result)))
|
|
175
|
+
|
|
176
|
+
raise TimeoutError(f"CAPTCHA not solved within {timeout}s. "
|
|
177
|
+
"Try increasing --timeout or check service status.")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# High-level solve()
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def solve(captcha_type, api_key, service="2captcha", **kwargs):
|
|
185
|
+
"""
|
|
186
|
+
Solve a CAPTCHA and return the token string.
|
|
187
|
+
|
|
188
|
+
captcha_type: recaptcha-v2 | recaptcha-v3 | hcaptcha | turnstile | image
|
|
189
|
+
Required kwargs by type:
|
|
190
|
+
recaptcha-v2/v3, hcaptcha, turnstile: sitekey, pageurl
|
|
191
|
+
image: image (file path or base64)
|
|
192
|
+
Optional:
|
|
193
|
+
invisible (bool) — for recaptcha-v2
|
|
194
|
+
action (str) — for recaptcha-v3, default "verify"
|
|
195
|
+
min_score (float) — for recaptcha-v3, default 0.5
|
|
196
|
+
timeout (int) — max seconds to wait, default 120
|
|
197
|
+
"""
|
|
198
|
+
dispatch = {
|
|
199
|
+
"recaptcha-v2": lambda: submit_recaptcha_v2(
|
|
200
|
+
api_key, kwargs["sitekey"], kwargs["pageurl"],
|
|
201
|
+
service=service, invisible=kwargs.get("invisible", False)
|
|
202
|
+
),
|
|
203
|
+
"recaptcha2": lambda: submit_recaptcha_v2(
|
|
204
|
+
api_key, kwargs["sitekey"], kwargs["pageurl"],
|
|
205
|
+
service=service, invisible=kwargs.get("invisible", False)
|
|
206
|
+
),
|
|
207
|
+
"recaptcha-v3": lambda: submit_recaptcha_v3(
|
|
208
|
+
api_key, kwargs["sitekey"], kwargs["pageurl"],
|
|
209
|
+
action=kwargs.get("action", "verify"),
|
|
210
|
+
min_score=kwargs.get("min_score", 0.5),
|
|
211
|
+
service=service
|
|
212
|
+
),
|
|
213
|
+
"recaptcha3": lambda: submit_recaptcha_v3(
|
|
214
|
+
api_key, kwargs["sitekey"], kwargs["pageurl"],
|
|
215
|
+
action=kwargs.get("action", "verify"),
|
|
216
|
+
min_score=kwargs.get("min_score", 0.5),
|
|
217
|
+
service=service
|
|
218
|
+
),
|
|
219
|
+
"hcaptcha": lambda: submit_hcaptcha(
|
|
220
|
+
api_key, kwargs["sitekey"], kwargs["pageurl"], service=service
|
|
221
|
+
),
|
|
222
|
+
"turnstile": lambda: submit_turnstile(
|
|
223
|
+
api_key, kwargs["sitekey"], kwargs["pageurl"], service=service
|
|
224
|
+
),
|
|
225
|
+
"image": lambda: submit_image(api_key, kwargs["image"], service=service),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
submit_fn = dispatch.get(captcha_type)
|
|
229
|
+
if submit_fn is None:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Unknown CAPTCHA type: {captcha_type!r}. "
|
|
232
|
+
f"Choose from: {', '.join(sorted(set(dispatch.keys())))}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
task_id = submit_fn()
|
|
236
|
+
print(f"[captcha] Task submitted (id={task_id})", file=sys.stderr)
|
|
237
|
+
return poll(api_key, task_id, service=service, timeout=kwargs.get("timeout", 120))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# CLI
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def main():
|
|
245
|
+
parser = argparse.ArgumentParser(
|
|
246
|
+
description="Solve a CAPTCHA via a third-party service and print the token as JSON.",
|
|
247
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
248
|
+
epilog=__doc__,
|
|
249
|
+
)
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
"--type", required=True,
|
|
252
|
+
choices=["recaptcha-v2", "recaptcha-v3", "hcaptcha", "turnstile", "image"],
|
|
253
|
+
help="CAPTCHA type",
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument("--sitekey", help="CAPTCHA sitekey (recaptcha/hcaptcha/turnstile)")
|
|
256
|
+
parser.add_argument("--pageurl", help="Full URL of the page with the CAPTCHA")
|
|
257
|
+
parser.add_argument("--image", help="Image file path or base64 string (for --type image)")
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
"--api-key",
|
|
260
|
+
default=os.environ.get("CAPTCHA_API_KEY"),
|
|
261
|
+
help="Solving service API key (default: $CAPTCHA_API_KEY)",
|
|
262
|
+
)
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"--service",
|
|
265
|
+
default=os.environ.get("CAPTCHA_SERVICE", "2captcha"),
|
|
266
|
+
choices=list(SERVICE_URLS.keys()),
|
|
267
|
+
help="Solving service to use (default: 2captcha)",
|
|
268
|
+
)
|
|
269
|
+
parser.add_argument("--action", default="verify", help="reCAPTCHA v3 action name")
|
|
270
|
+
parser.add_argument("--min-score", type=float, default=0.5,
|
|
271
|
+
help="reCAPTCHA v3 minimum acceptable score (0.1–0.9)")
|
|
272
|
+
parser.add_argument("--invisible", action="store_true",
|
|
273
|
+
help="Use invisible reCAPTCHA v2 mode")
|
|
274
|
+
parser.add_argument("--timeout", type=int, default=120,
|
|
275
|
+
help="Max seconds to wait for a solution (default: 120)")
|
|
276
|
+
|
|
277
|
+
args = parser.parse_args()
|
|
278
|
+
|
|
279
|
+
if not args.api_key:
|
|
280
|
+
out = {"success": False, "error":
|
|
281
|
+
"No API key provided. Use --api-key or set CAPTCHA_API_KEY env var."}
|
|
282
|
+
print(json.dumps(out))
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
|
|
285
|
+
# Validate required args per type
|
|
286
|
+
needs_sitekey = args.type in ("recaptcha-v2", "recaptcha-v3", "hcaptcha", "turnstile")
|
|
287
|
+
if needs_sitekey and not args.sitekey:
|
|
288
|
+
print(json.dumps({"success": False,
|
|
289
|
+
"error": f"--sitekey is required for --type {args.type}"}))
|
|
290
|
+
sys.exit(1)
|
|
291
|
+
if needs_sitekey and not args.pageurl:
|
|
292
|
+
print(json.dumps({"success": False,
|
|
293
|
+
"error": f"--pageurl is required for --type {args.type}"}))
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
if args.type == "image" and not args.image:
|
|
296
|
+
print(json.dumps({"success": False,
|
|
297
|
+
"error": "--image is required for --type image"}))
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
token = solve(
|
|
302
|
+
args.type,
|
|
303
|
+
args.api_key,
|
|
304
|
+
service=args.service,
|
|
305
|
+
sitekey=args.sitekey,
|
|
306
|
+
pageurl=args.pageurl,
|
|
307
|
+
image=args.image,
|
|
308
|
+
action=args.action,
|
|
309
|
+
min_score=args.min_score,
|
|
310
|
+
invisible=args.invisible,
|
|
311
|
+
timeout=args.timeout,
|
|
312
|
+
)
|
|
313
|
+
print(json.dumps({"success": True, "token": token, "type": args.type}))
|
|
314
|
+
except TimeoutError as e:
|
|
315
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
main()
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Solve reCAPTCHA v2 image grid challenges using vision AI.
|
|
4
|
+
|
|
5
|
+
After clicking the checkbox, if an image grid appears ("select all traffic
|
|
6
|
+
lights"), this script screenshots the grid, sends it to Claude or GPT-4V,
|
|
7
|
+
gets back which cells to click, and handles multiple rounds automatically.
|
|
8
|
+
|
|
9
|
+
Usage in your Playwright script:
|
|
10
|
+
from solve_image_grid import solve_image_grid
|
|
11
|
+
|
|
12
|
+
# Click the checkbox first
|
|
13
|
+
page.frame_locator('iframe[title="reCAPTCHA"]') \\
|
|
14
|
+
.locator('.recaptcha-checkbox-border').click()
|
|
15
|
+
|
|
16
|
+
# Solve the grid (if it appears)
|
|
17
|
+
solved = solve_image_grid(page)
|
|
18
|
+
|
|
19
|
+
Standalone test (opens the Google demo):
|
|
20
|
+
python solve_image_grid.py
|
|
21
|
+
|
|
22
|
+
Requirements:
|
|
23
|
+
pip install playwright anthropic # Claude (recommended)
|
|
24
|
+
# or: pip install playwright openai # GPT-4V fallback
|
|
25
|
+
|
|
26
|
+
Environment:
|
|
27
|
+
ANTHROPIC_API_KEY — Claude API key (preferred, better spatial reasoning)
|
|
28
|
+
OPENAI_API_KEY — OpenAI API key (fallback)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import base64
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
import time
|
|
37
|
+
import random
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Timing constants
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
CELL_CLICK_DELAY = (0.25, 0.65) # random pause between cell clicks (seconds)
|
|
44
|
+
VERIFY_WAIT_SEC = 2.5 # pause after clicking Verify before checking result
|
|
45
|
+
ROUND_DELAY = (1.5, 3.0) # pause between rounds
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Selectors
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
CHECKBOX_IFRAME = 'iframe[title="reCAPTCHA"]'
|
|
52
|
+
|
|
53
|
+
# The challenge iframe may have slightly different titles across reCAPTCHA versions
|
|
54
|
+
CHALLENGE_IFRAME_SELECTORS = [
|
|
55
|
+
'iframe[title*="recaptcha challenge"]',
|
|
56
|
+
'iframe[src*="recaptcha/api2/bframe"]',
|
|
57
|
+
'iframe[src*="recaptcha/enterprise/bframe"]',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Selectors *inside* the challenge iframe
|
|
61
|
+
TASK_TEXT_SEL = '.rc-imageselect-desc-no-canonical, .rc-imageselect-desc'
|
|
62
|
+
GRID_SEL = '.rc-imageselect-table-33, .rc-imageselect-table-44, table.rc-imageselect-table'
|
|
63
|
+
CELL_SEL = 'td.rc-imageselect-tile, .rc-imageselect-tile'
|
|
64
|
+
VERIFY_BTN = '#recaptcha-verify-button'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Vision AI: ask which cells match the task
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def _prompt(task_text: str, grid_size: int) -> str:
|
|
72
|
+
n = grid_size * grid_size
|
|
73
|
+
return (
|
|
74
|
+
f'This is a reCAPTCHA image grid ({grid_size}×{grid_size}, {n} cells total).\n'
|
|
75
|
+
f'Task: "{task_text}"\n\n'
|
|
76
|
+
f'Number the cells 1–{n} left-to-right, top-to-bottom (row 1 first).\n'
|
|
77
|
+
f'Return ONLY a JSON array of the cell numbers that match the task.\n'
|
|
78
|
+
f'Examples: [2,5,8] or [] (empty if none match)\n'
|
|
79
|
+
f'No explanation — just the array.'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _call_claude(image_b64: str, task_text: str, grid_size: int) -> list:
|
|
84
|
+
import anthropic
|
|
85
|
+
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
|
86
|
+
msg = client.messages.create(
|
|
87
|
+
model="claude-opus-4-6",
|
|
88
|
+
max_tokens=64,
|
|
89
|
+
messages=[{
|
|
90
|
+
"role": "user",
|
|
91
|
+
"content": [
|
|
92
|
+
{
|
|
93
|
+
"type": "image",
|
|
94
|
+
"source": {
|
|
95
|
+
"type": "base64",
|
|
96
|
+
"media_type": "image/png",
|
|
97
|
+
"data": image_b64,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{"type": "text", "text": _prompt(task_text, grid_size)},
|
|
101
|
+
],
|
|
102
|
+
}],
|
|
103
|
+
)
|
|
104
|
+
return _parse_response(msg.content[0].text, grid_size)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _call_openai(image_b64: str, task_text: str, grid_size: int) -> list:
|
|
108
|
+
from openai import OpenAI
|
|
109
|
+
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
|
110
|
+
resp = client.chat.completions.create(
|
|
111
|
+
model="gpt-4o",
|
|
112
|
+
max_tokens=64,
|
|
113
|
+
messages=[{
|
|
114
|
+
"role": "user",
|
|
115
|
+
"content": [
|
|
116
|
+
{
|
|
117
|
+
"type": "image_url",
|
|
118
|
+
"image_url": {"url": f"data:image/png;base64,{image_b64}"},
|
|
119
|
+
},
|
|
120
|
+
{"type": "text", "text": _prompt(task_text, grid_size)},
|
|
121
|
+
],
|
|
122
|
+
}],
|
|
123
|
+
)
|
|
124
|
+
return _parse_response(resp.choices[0].message.content, grid_size)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_response(raw: str, grid_size: int) -> list:
|
|
128
|
+
"""Extract a valid list of cell ints from the AI's raw response."""
|
|
129
|
+
match = re.search(r'\[.*?\]', raw.strip(), re.DOTALL)
|
|
130
|
+
if not match:
|
|
131
|
+
return []
|
|
132
|
+
try:
|
|
133
|
+
cells = json.loads(match.group())
|
|
134
|
+
n = grid_size * grid_size
|
|
135
|
+
return [c for c in cells if isinstance(c, int) and 1 <= c <= n]
|
|
136
|
+
except (json.JSONDecodeError, TypeError):
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def ask_vision_ai(image_b64: str, task_text: str, grid_size: int) -> list:
|
|
141
|
+
"""
|
|
142
|
+
Ask the available vision AI which cells match the task.
|
|
143
|
+
Tries Claude first, falls back to GPT-4V.
|
|
144
|
+
Returns a list of 1-indexed cell numbers.
|
|
145
|
+
"""
|
|
146
|
+
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
147
|
+
try:
|
|
148
|
+
cells = _call_claude(image_b64, task_text, grid_size)
|
|
149
|
+
print(f"[grid] Claude selected: {cells}", file=sys.stderr)
|
|
150
|
+
return cells
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f"[grid] Claude error: {e} — trying OpenAI...", file=sys.stderr)
|
|
153
|
+
|
|
154
|
+
if os.environ.get("OPENAI_API_KEY"):
|
|
155
|
+
cells = _call_openai(image_b64, task_text, grid_size)
|
|
156
|
+
print(f"[grid] GPT-4V selected: {cells}", file=sys.stderr)
|
|
157
|
+
return cells
|
|
158
|
+
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
"No vision AI key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Playwright helpers
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def _find_challenge_frame(page):
|
|
169
|
+
"""Return the challenge FrameLocator if visible, else None."""
|
|
170
|
+
for sel in CHALLENGE_IFRAME_SELECTORS:
|
|
171
|
+
try:
|
|
172
|
+
frame = page.frame_locator(sel)
|
|
173
|
+
if frame.locator(TASK_TEXT_SEL).is_visible(timeout=1500):
|
|
174
|
+
return frame
|
|
175
|
+
except Exception:
|
|
176
|
+
continue
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _challenge_visible(page) -> bool:
|
|
181
|
+
for sel in CHALLENGE_IFRAME_SELECTORS:
|
|
182
|
+
try:
|
|
183
|
+
if page.locator(sel).is_visible(timeout=400):
|
|
184
|
+
return True
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _get_task(frame) -> str:
|
|
191
|
+
try:
|
|
192
|
+
return frame.locator(TASK_TEXT_SEL).first.inner_text(timeout=3000).strip()
|
|
193
|
+
except Exception:
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _get_grid_size(frame) -> int:
|
|
198
|
+
try:
|
|
199
|
+
if frame.locator('.rc-imageselect-table-44').count() > 0:
|
|
200
|
+
return 4
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
return 3
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _screenshot_grid(frame) -> str:
|
|
207
|
+
"""Screenshot just the image grid and return as base64 PNG."""
|
|
208
|
+
try:
|
|
209
|
+
png = frame.locator(GRID_SEL).first.screenshot()
|
|
210
|
+
except Exception:
|
|
211
|
+
png = frame.locator("body").screenshot()
|
|
212
|
+
return base64.b64encode(png).decode()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _click_cells(frame, cell_numbers: list, grid_size: int) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Click the specified cells (1-indexed, left-to-right top-to-bottom).
|
|
218
|
+
|
|
219
|
+
Uses element-based clicking via frame_locator rather than raw pixel
|
|
220
|
+
coordinates — more reliable across different viewport sizes and DPIs.
|
|
221
|
+
"""
|
|
222
|
+
if not cell_numbers:
|
|
223
|
+
return
|
|
224
|
+
cells = frame.locator(CELL_SEL).all()
|
|
225
|
+
for num in cell_numbers:
|
|
226
|
+
idx = num - 1
|
|
227
|
+
if idx < len(cells):
|
|
228
|
+
try:
|
|
229
|
+
cells[idx].click()
|
|
230
|
+
time.sleep(random.uniform(*CELL_CLICK_DELAY))
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"[grid] Cell {num} click failed: {e}", file=sys.stderr)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Main public function
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def solve_image_grid(page, max_rounds: int = 6) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Solve a reCAPTCHA v2 image grid challenge on the given Playwright page.
|
|
242
|
+
|
|
243
|
+
Call this after clicking the reCAPTCHA checkbox. If a grid challenge
|
|
244
|
+
appears, the function loops through rounds (new grids keep appearing
|
|
245
|
+
until Google is satisfied) and clicks the correct cells each time.
|
|
246
|
+
|
|
247
|
+
If the checkbox passed without triggering a grid (common on trusted IPs
|
|
248
|
+
and during testing), the function returns True immediately.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
page: Playwright Page object
|
|
252
|
+
max_rounds: Give up after this many rounds (default: 6)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True — challenge closed (solved or no challenge appeared)
|
|
256
|
+
False — failed to solve within max_rounds or API error
|
|
257
|
+
"""
|
|
258
|
+
# Wait briefly for the challenge to appear
|
|
259
|
+
print("[grid] Checking for image challenge...", file=sys.stderr)
|
|
260
|
+
for _ in range(8):
|
|
261
|
+
if _find_challenge_frame(page):
|
|
262
|
+
break
|
|
263
|
+
time.sleep(0.7)
|
|
264
|
+
else:
|
|
265
|
+
print("[grid] No grid challenge — checkbox solved directly.", file=sys.stderr)
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
for round_num in range(1, max_rounds + 1):
|
|
269
|
+
frame = _find_challenge_frame(page)
|
|
270
|
+
if not frame:
|
|
271
|
+
print("[grid] Challenge closed — done.", file=sys.stderr)
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
task_text = _get_task(frame)
|
|
275
|
+
grid_size = _get_grid_size(frame)
|
|
276
|
+
print(f"[grid] Round {round_num}: '{task_text}' ({grid_size}×{grid_size})",
|
|
277
|
+
file=sys.stderr)
|
|
278
|
+
|
|
279
|
+
image_b64 = _screenshot_grid(frame)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
cells = ask_vision_ai(image_b64, task_text, grid_size)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
print(f"[grid] Vision AI failed: {e}", file=sys.stderr)
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
_click_cells(frame, cells, grid_size)
|
|
288
|
+
|
|
289
|
+
# Small pause before clicking Verify (mimics human review time)
|
|
290
|
+
time.sleep(random.uniform(0.6, 1.4))
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
frame.locator(VERIFY_BTN).click()
|
|
294
|
+
except Exception as e:
|
|
295
|
+
print(f"[grid] Verify click failed: {e}", file=sys.stderr)
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
time.sleep(VERIFY_WAIT_SEC + random.uniform(0, 1))
|
|
299
|
+
|
|
300
|
+
if not _challenge_visible(page):
|
|
301
|
+
print("[grid] Challenge dismissed — solved.", file=sys.stderr)
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
print("[grid] New round appeared.", file=sys.stderr)
|
|
305
|
+
|
|
306
|
+
print(f"[grid] Gave up after {max_rounds} rounds.", file=sys.stderr)
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
# Standalone test
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
from playwright.sync_api import sync_playwright
|
|
316
|
+
|
|
317
|
+
if not (os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("OPENAI_API_KEY")):
|
|
318
|
+
print("ERROR: set ANTHROPIC_API_KEY or OPENAI_API_KEY", file=sys.stderr)
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
with sync_playwright() as p:
|
|
322
|
+
browser = p.chromium.launch(headless=False)
|
|
323
|
+
context = browser.new_context(
|
|
324
|
+
viewport={"width": 1280, "height": 800},
|
|
325
|
+
user_agent=(
|
|
326
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
327
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
328
|
+
"Chrome/124.0.0.0 Safari/537.36"
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
context.add_init_script(
|
|
332
|
+
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
|
333
|
+
)
|
|
334
|
+
page = context.new_page()
|
|
335
|
+
page.goto("https://www.google.com/recaptcha/api2/demo", wait_until="networkidle")
|
|
336
|
+
|
|
337
|
+
print("[main] Clicking checkbox...")
|
|
338
|
+
page.frame_locator(CHECKBOX_IFRAME) \
|
|
339
|
+
.locator(".recaptcha-checkbox-border").click()
|
|
340
|
+
|
|
341
|
+
ok = solve_image_grid(page)
|
|
342
|
+
print(f"[main] Grid result: {'SOLVED' if ok else 'FAILED'}")
|
|
343
|
+
|
|
344
|
+
if ok:
|
|
345
|
+
page.click("#recaptcha-demo-submit")
|
|
346
|
+
page.wait_for_timeout(3000)
|
|
347
|
+
|
|
348
|
+
page.screenshot(path="grid_result.png")
|
|
349
|
+
print("[main] Screenshot saved to grid_result.png")
|
|
350
|
+
browser.close()
|