@leejungkiin/awkit 1.4.0 → 1.4.3
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/bin/awk.js +458 -7
- package/bin/claude-generators.js +122 -0
- package/core/AGENTS.md +16 -0
- package/core/CLAUDE.md +155 -0
- package/core/GEMINI.md +44 -9
- package/package.json +1 -1
- package/skills/ai-sprite-maker/SKILL.md +81 -0
- package/skills/ai-sprite-maker/scripts/animate_sprite.py +102 -0
- package/skills/ai-sprite-maker/scripts/process_sprites.py +140 -0
- package/skills/code-review/SKILL.md +21 -33
- package/skills/lucylab-tts/SKILL.md +64 -0
- package/skills/lucylab-tts/resources/voices_library.json +908 -0
- package/skills/lucylab-tts/scripts/.env +1 -0
- package/skills/lucylab-tts/scripts/lucylab_tts.py +506 -0
- package/skills/orchestrator/SKILL.md +5 -0
- package/skills/short-maker/SKILL.md +150 -0
- package/skills/short-maker/_backup/storyboard.html +106 -0
- package/skills/short-maker/_backup/video_mixer.py +296 -0
- package/skills/short-maker/outputs/fitbite-promo/background.jpg +0 -0
- package/skills/short-maker/outputs/fitbite-promo/final/promo-final.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/script.md +19 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-01.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-02.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-03.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-04.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-01.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-02.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-03.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-04.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard.html +133 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard.json +38 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/merged_chroma.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/merged_crossfaded.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_00.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_01.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_02.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_03.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/manifest.json +31 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-01.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-02.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-03.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-04.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts_script.txt +11 -0
- package/skills/short-maker/scripts/google-flow-cli/.project-identity +41 -0
- package/skills/short-maker/scripts/google-flow-cli/.trae/rules/project_rules.md +52 -0
- package/skills/short-maker/scripts/google-flow-cli/CODEBASE.md +67 -0
- package/skills/short-maker/scripts/google-flow-cli/GoogleFlowCli.code-workspace +29 -0
- package/skills/short-maker/scripts/google-flow-cli/README.md +168 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/PROJECT.md +12 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/REQUIREMENTS.md +22 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/ROADMAP.md +16 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/TECH-SPEC.md +13 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/__init__.py +3 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/__init__.py +19 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/client.py +1921 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/models.py +64 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/rpc_ids.py +98 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/__init__.py +15 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/browser_auth.py +692 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/humanizer.py +417 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/proxy_ext.py +120 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/recaptcha.py +482 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/__init__.py +5 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/client.py +414 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/cli/__init__.py +1 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/cli/main.py +1075 -0
- package/skills/short-maker/scripts/google-flow-cli/pyproject.toml +36 -0
- package/skills/short-maker/scripts/google-flow-cli/script.txt +22 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/__init__.py +0 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/test_batchexecute.py +113 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/test_client.py +190 -0
- package/skills/short-maker/templates/aida_script.md +40 -0
- package/skills/short-maker/templates/mimic_analyzer.md +29 -0
- package/skills/single-flow-task-execution/SKILL.md +9 -6
- package/skills/skill-creator/SKILL.md +44 -0
- package/skills/spm-build-analysis/SKILL.md +92 -0
- package/skills/spm-build-analysis/references/build-optimization-sources.md +155 -0
- package/skills/spm-build-analysis/references/recommendation-format.md +85 -0
- package/skills/spm-build-analysis/references/spm-analysis-checks.md +105 -0
- package/skills/spm-build-analysis/scripts/check_spm_pins.py +118 -0
- package/skills/symphony-enforcer/SKILL.md +51 -83
- package/skills/symphony-orchestrator/SKILL.md +1 -1
- package/skills/trello-sync/SKILL.md +27 -28
- package/skills/verification-gate/SKILL.md +13 -2
- package/skills/xcode-build-benchmark/SKILL.md +88 -0
- package/skills/xcode-build-benchmark/references/benchmark-artifacts.md +94 -0
- package/skills/xcode-build-benchmark/references/benchmarking-workflow.md +67 -0
- package/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json +230 -0
- package/skills/xcode-build-benchmark/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-fixer/SKILL.md +218 -0
- package/skills/xcode-build-fixer/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-build-fixer/references/fix-patterns.md +290 -0
- package/skills/xcode-build-fixer/references/recommendation-format.md +85 -0
- package/skills/xcode-build-fixer/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-orchestrator/SKILL.md +156 -0
- package/skills/xcode-build-orchestrator/references/benchmark-artifacts.md +94 -0
- package/skills/xcode-build-orchestrator/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-build-orchestrator/references/orchestration-report-template.md +143 -0
- package/skills/xcode-build-orchestrator/references/recommendation-format.md +85 -0
- package/skills/xcode-build-orchestrator/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py +273 -0
- package/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py +533 -0
- package/skills/xcode-compilation-analyzer/SKILL.md +89 -0
- package/skills/xcode-compilation-analyzer/references/build-optimization-sources.md +155 -0
- package/skills/xcode-compilation-analyzer/references/code-compilation-checks.md +106 -0
- package/skills/xcode-compilation-analyzer/references/recommendation-format.md +85 -0
- package/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py +273 -0
- package/skills/xcode-project-analyzer/SKILL.md +76 -0
- package/skills/xcode-project-analyzer/references/build-optimization-sources.md +155 -0
- package/skills/xcode-project-analyzer/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-project-analyzer/references/project-audit-checks.md +101 -0
- package/skills/xcode-project-analyzer/references/recommendation-format.md +85 -0
- package/templates/project-identity/android.json +0 -10
- package/templates/project-identity/backend-nestjs.json +0 -10
- package/templates/project-identity/expo.json +0 -10
- package/templates/project-identity/ios.json +0 -10
- package/templates/project-identity/web-nextjs.json +0 -10
- package/workflows/_uncategorized/ship-to-code.md +85 -0
- package/workflows/context/codebase-sync.md +10 -87
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""
|
|
2
|
+
reCAPTCHA Enterprise token provider for Google Flow.
|
|
3
|
+
|
|
4
|
+
Flow requires a reCAPTCHA Enterprise v3 token with every generation request.
|
|
5
|
+
reCAPTCHA Enterprise v3 uses deep behavioral scoring — it tracks mouse movements,
|
|
6
|
+
browsing history, interaction patterns, and more. No automated/headless browser
|
|
7
|
+
can pass this scoring, regardless of how well it's disguised.
|
|
8
|
+
|
|
9
|
+
This module connects to the Chrome browser that was kept alive after `gflow auth`.
|
|
10
|
+
Since that browser has genuine user interaction (the user logged in manually),
|
|
11
|
+
reCAPTCHA Enterprise gives it a good score and accepts the tokens.
|
|
12
|
+
|
|
13
|
+
The connection is via Chrome DevTools Protocol (CDP) over WebSocket to the
|
|
14
|
+
--remote-debugging-port that was set during `gflow auth`.
|
|
15
|
+
|
|
16
|
+
Site key (from Flow network traffic):
|
|
17
|
+
6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
import urllib.request
|
|
27
|
+
import urllib.error
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("gflow.recaptcha")
|
|
30
|
+
|
|
31
|
+
# reCAPTCHA Enterprise site key for labs.google
|
|
32
|
+
RECAPTCHA_SITE_KEY = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
|
33
|
+
|
|
34
|
+
# Flow URL to load for reCAPTCHA context
|
|
35
|
+
FLOW_URL = "https://labs.google/fx/tools/flow"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RecaptchaProvider:
|
|
39
|
+
"""
|
|
40
|
+
Provides fresh reCAPTCHA Enterprise tokens by connecting to the
|
|
41
|
+
Chrome browser that was kept alive from `gflow auth`.
|
|
42
|
+
|
|
43
|
+
This avoids launching any new browser — it reuses the authenticated
|
|
44
|
+
session where the user already interacted, which is the only way
|
|
45
|
+
to get valid reCAPTCHA Enterprise v3 tokens.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, cookies: str = "", debug: bool = False):
|
|
49
|
+
self.debug = debug
|
|
50
|
+
self._cookies = cookies
|
|
51
|
+
self._ws = None
|
|
52
|
+
self._msg_id: int = 0
|
|
53
|
+
self._ready = False
|
|
54
|
+
# Threading lock prevents concurrent reconnection storms
|
|
55
|
+
# (inspired by notebooklm-py's asyncio.Lock pattern)
|
|
56
|
+
self._lock = threading.Lock()
|
|
57
|
+
self._connecting = False
|
|
58
|
+
|
|
59
|
+
def get_token(self, action: str = "IMAGE_GENERATION") -> str:
|
|
60
|
+
"""
|
|
61
|
+
Get a fresh reCAPTCHA Enterprise token.
|
|
62
|
+
|
|
63
|
+
Thread-safe: concurrent callers share a single connection/reconnection
|
|
64
|
+
cycle via a lock, preventing reCAPTCHA refresh storms (inspired by
|
|
65
|
+
notebooklm-py's concurrency-safe auth refresh pattern).
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
action: The reCAPTCHA action string. Flow uses:
|
|
69
|
+
- "IMAGE_GENERATION" for image generation
|
|
70
|
+
- "VIDEO_GENERATION" for video generation
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A reCAPTCHA token string
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
RecaptchaError if token cannot be obtained
|
|
77
|
+
"""
|
|
78
|
+
if not self._ready:
|
|
79
|
+
with self._lock:
|
|
80
|
+
# Double-check after acquiring lock (another thread may have connected)
|
|
81
|
+
if not self._ready:
|
|
82
|
+
self._connect()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
return self._execute_recaptcha(action)
|
|
86
|
+
except RecaptchaError:
|
|
87
|
+
# Token execution failed — try reconnecting once before giving up
|
|
88
|
+
# (the CDP WebSocket may have dropped)
|
|
89
|
+
with self._lock:
|
|
90
|
+
logger.info("reCAPTCHA token failed, attempting reconnect...")
|
|
91
|
+
self._ready = False
|
|
92
|
+
self._close_ws()
|
|
93
|
+
self._connect()
|
|
94
|
+
return self._execute_recaptcha(action)
|
|
95
|
+
|
|
96
|
+
def _connect(self) -> None:
|
|
97
|
+
"""Connect to the existing auth Chrome browser via CDP, launching one if needed."""
|
|
98
|
+
from gflow.auth.browser_auth import get_saved_cdp_port
|
|
99
|
+
|
|
100
|
+
port = get_saved_cdp_port()
|
|
101
|
+
ws_url = None
|
|
102
|
+
|
|
103
|
+
if port:
|
|
104
|
+
if self.debug:
|
|
105
|
+
logger.info("Checking saved CDP port %d...", port)
|
|
106
|
+
ws_url = self._find_flow_tab(port) or self._find_any_tab(port)
|
|
107
|
+
if not ws_url:
|
|
108
|
+
if self.debug:
|
|
109
|
+
logger.info("CDP port %d is dead or has no tabs. Falling back to auto-launch...", port)
|
|
110
|
+
port = None
|
|
111
|
+
|
|
112
|
+
if not port:
|
|
113
|
+
logger.info("No valid Chrome session found — auto-launching auth browser...")
|
|
114
|
+
port = self._auto_launch_chrome()
|
|
115
|
+
if not port:
|
|
116
|
+
raise RecaptchaError(
|
|
117
|
+
"No Chrome browser session found and auto-launch failed.\n"
|
|
118
|
+
"Run 'gflow auth' first — it opens a Chrome window that stays\n"
|
|
119
|
+
"open for reCAPTCHA. Don't close it until you're done generating."
|
|
120
|
+
)
|
|
121
|
+
ws_url = self._find_flow_tab(port) or self._find_any_tab(port)
|
|
122
|
+
if not ws_url:
|
|
123
|
+
raise RecaptchaError(
|
|
124
|
+
"Chrome is running but has no usable tabs.\n"
|
|
125
|
+
"Run 'gflow auth' again to set up a fresh session."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if self.debug:
|
|
129
|
+
logger.info("Connecting to auth Chrome on port %d...", port)
|
|
130
|
+
|
|
131
|
+
self._connect_ws(ws_url)
|
|
132
|
+
# Ensure we navigate to the flow page if we grabbed a generic tab
|
|
133
|
+
self._cdp_send("Page.enable")
|
|
134
|
+
self._cdp_navigate(FLOW_URL)
|
|
135
|
+
|
|
136
|
+
# Wait for reCAPTCHA to be available
|
|
137
|
+
self._wait_for_recaptcha()
|
|
138
|
+
|
|
139
|
+
# Warm up reCAPTCHA to build trust score
|
|
140
|
+
self._warm_up()
|
|
141
|
+
|
|
142
|
+
self._ready = True
|
|
143
|
+
|
|
144
|
+
if self.debug:
|
|
145
|
+
logger.info("Connected to auth Chrome, reCAPTCHA ready")
|
|
146
|
+
|
|
147
|
+
def _auto_launch_chrome(self) -> int | None:
|
|
148
|
+
"""Auto-launch Chrome with CDP for reCAPTCHA, reusing saved cookies."""
|
|
149
|
+
import platform
|
|
150
|
+
import subprocess
|
|
151
|
+
from gflow.auth.browser_auth import (
|
|
152
|
+
_get_chrome_path, _find_free_port, save_cdp_port,
|
|
153
|
+
_wait_for_cdp_page, ENV_DIR, kill_auth_browser
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# First, forcefully kill any leftover Chrome process that might be
|
|
158
|
+
# holding the profile lock but not responding.
|
|
159
|
+
kill_auth_browser()
|
|
160
|
+
except Exception as e:
|
|
161
|
+
if self.debug:
|
|
162
|
+
logger.warning("Failed to kill auth browser before launch: %s", e)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
chrome_path = _get_chrome_path()
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
cdp_port = _find_free_port()
|
|
170
|
+
profile_dir = str(ENV_DIR / "chrome-profile")
|
|
171
|
+
|
|
172
|
+
args = [
|
|
173
|
+
chrome_path,
|
|
174
|
+
f"--remote-debugging-port={cdp_port}",
|
|
175
|
+
"--remote-allow-origins=*",
|
|
176
|
+
f"--user-data-dir={profile_dir}",
|
|
177
|
+
"--no-first-run",
|
|
178
|
+
"--no-default-browser-check",
|
|
179
|
+
# Run visible (not headless or offscreen — reCAPTCHA detects
|
|
180
|
+
# hidden/offscreen windows and tanks the trust score)
|
|
181
|
+
"--window-size=800,600",
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# Route Chrome through residential proxy if configured
|
|
185
|
+
try:
|
|
186
|
+
from gflow.auth.proxy_ext import get_chrome_proxy_args
|
|
187
|
+
proxy_args = get_chrome_proxy_args()
|
|
188
|
+
if proxy_args:
|
|
189
|
+
args.extend(proxy_args)
|
|
190
|
+
logger.info("Chrome using residential proxy")
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
args.append(FLOW_URL)
|
|
195
|
+
|
|
196
|
+
if self.debug:
|
|
197
|
+
logger.info("Auto-launching Chrome on CDP port %d", cdp_port)
|
|
198
|
+
|
|
199
|
+
creation_flags = 0
|
|
200
|
+
if platform.system() == "Windows":
|
|
201
|
+
creation_flags = (
|
|
202
|
+
subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
subprocess.Popen(
|
|
207
|
+
args,
|
|
208
|
+
stdout=subprocess.DEVNULL,
|
|
209
|
+
stderr=subprocess.DEVNULL,
|
|
210
|
+
creationflags=creation_flags if platform.system() == "Windows" else 0,
|
|
211
|
+
start_new_session=(platform.system() != "Windows"),
|
|
212
|
+
)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.warning("Failed to launch Chrome: %s", e)
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# Wait for CDP to become available
|
|
218
|
+
try:
|
|
219
|
+
_wait_for_cdp_page(cdp_port, timeout=30)
|
|
220
|
+
except Exception:
|
|
221
|
+
logger.warning("Chrome launched but CDP not available")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
save_cdp_port(cdp_port)
|
|
225
|
+
|
|
226
|
+
# Give the page time to fully load reCAPTCHA scripts
|
|
227
|
+
import time as _time
|
|
228
|
+
_time.sleep(5)
|
|
229
|
+
|
|
230
|
+
return cdp_port
|
|
231
|
+
|
|
232
|
+
def _find_flow_tab(self, port: int) -> str | None:
|
|
233
|
+
"""Find a tab that's already on labs.google/fx."""
|
|
234
|
+
try:
|
|
235
|
+
url = f"http://127.0.0.1:{port}/json/list"
|
|
236
|
+
resp = urllib.request.urlopen(url, timeout=5)
|
|
237
|
+
targets = json.loads(resp.read().decode())
|
|
238
|
+
|
|
239
|
+
for target in targets:
|
|
240
|
+
if target.get("type") == "page":
|
|
241
|
+
page_url = target.get("url", "")
|
|
242
|
+
if "labs.google/fx" in page_url:
|
|
243
|
+
return target.get("webSocketDebuggerUrl", "")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
if self.debug:
|
|
246
|
+
logger.warning("Failed to list tabs: %s", e)
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
def _find_any_tab(self, port: int) -> str | None:
|
|
250
|
+
"""Find any page tab we can use, or create one if none exist (macOS closed-window case)."""
|
|
251
|
+
try:
|
|
252
|
+
url = f"http://127.0.0.1:{port}/json/list"
|
|
253
|
+
resp = urllib.request.urlopen(url, timeout=5)
|
|
254
|
+
targets = json.loads(resp.read().decode())
|
|
255
|
+
|
|
256
|
+
for target in targets:
|
|
257
|
+
if target.get("type") == "page" and target.get("webSocketDebuggerUrl"):
|
|
258
|
+
return target.get("webSocketDebuggerUrl", "")
|
|
259
|
+
|
|
260
|
+
# Chrome is running but has no windows/tabs (common on macOS). Create one.
|
|
261
|
+
new_url = f"http://127.0.0.1:{port}/json/new"
|
|
262
|
+
resp = urllib.request.urlopen(new_url, timeout=5)
|
|
263
|
+
target = json.loads(resp.read().decode())
|
|
264
|
+
if target.get("type") == "page" and target.get("webSocketDebuggerUrl"):
|
|
265
|
+
return target.get("webSocketDebuggerUrl", "")
|
|
266
|
+
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
def _connect_ws(self, ws_url: str) -> None:
|
|
272
|
+
"""Connect to a CDP WebSocket endpoint."""
|
|
273
|
+
import websocket
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
self._ws = websocket.create_connection(ws_url, timeout=30)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
raise RecaptchaError(
|
|
279
|
+
f"Failed to connect to Chrome: {e}\n"
|
|
280
|
+
"The auth browser may have been closed. Run 'gflow auth' again."
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def _cdp_send(self, method: str, params: dict | None = None) -> dict:
|
|
284
|
+
"""Send a CDP command and wait for the result."""
|
|
285
|
+
self._msg_id += 1
|
|
286
|
+
msg = {"id": self._msg_id, "method": method}
|
|
287
|
+
if params:
|
|
288
|
+
msg["params"] = params
|
|
289
|
+
|
|
290
|
+
self._ws.send(json.dumps(msg))
|
|
291
|
+
|
|
292
|
+
deadline = time.time() + 30
|
|
293
|
+
while time.time() < deadline:
|
|
294
|
+
try:
|
|
295
|
+
self._ws.settimeout(5)
|
|
296
|
+
raw = self._ws.recv()
|
|
297
|
+
data = json.loads(raw)
|
|
298
|
+
if data.get("id") == self._msg_id:
|
|
299
|
+
if "error" in data:
|
|
300
|
+
raise RecaptchaError(f"CDP error: {data['error']}")
|
|
301
|
+
return data.get("result", {})
|
|
302
|
+
except RecaptchaError:
|
|
303
|
+
raise
|
|
304
|
+
except Exception as e:
|
|
305
|
+
if "timed out" in str(e).lower() or "timeout" in str(e).lower():
|
|
306
|
+
continue
|
|
307
|
+
raise
|
|
308
|
+
|
|
309
|
+
raise RecaptchaError("CDP command timed out")
|
|
310
|
+
|
|
311
|
+
def _cdp_navigate(self, url: str) -> None:
|
|
312
|
+
"""Navigate to a URL and wait for it to load."""
|
|
313
|
+
self._cdp_send("Page.navigate", {"url": url})
|
|
314
|
+
|
|
315
|
+
deadline = time.time() + 30
|
|
316
|
+
while time.time() < deadline:
|
|
317
|
+
try:
|
|
318
|
+
self._ws.settimeout(2)
|
|
319
|
+
raw = self._ws.recv()
|
|
320
|
+
data = json.loads(raw)
|
|
321
|
+
method = data.get("method", "")
|
|
322
|
+
if method in ("Page.loadEventFired", "Page.frameStoppedLoading"):
|
|
323
|
+
return
|
|
324
|
+
except Exception:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
if self.debug:
|
|
328
|
+
logger.info("Navigation wait timed out, continuing...")
|
|
329
|
+
|
|
330
|
+
def _cdp_evaluate(self, expression: str):
|
|
331
|
+
"""Evaluate JS expression in the page and return the result."""
|
|
332
|
+
result = self._cdp_send("Runtime.evaluate", {
|
|
333
|
+
"expression": expression,
|
|
334
|
+
"awaitPromise": True,
|
|
335
|
+
"returnByValue": True,
|
|
336
|
+
})
|
|
337
|
+
inner = result.get("result", {})
|
|
338
|
+
if inner.get("subtype") == "error":
|
|
339
|
+
raise RecaptchaError(f"JS error: {inner.get('description', 'unknown')}")
|
|
340
|
+
return inner.get("value")
|
|
341
|
+
|
|
342
|
+
def _wait_for_recaptcha(self, timeout: int = 30) -> None:
|
|
343
|
+
"""Wait for reCAPTCHA Enterprise to load on the page."""
|
|
344
|
+
deadline = time.time() + timeout
|
|
345
|
+
|
|
346
|
+
while time.time() < deadline:
|
|
347
|
+
try:
|
|
348
|
+
ready = self._cdp_evaluate(
|
|
349
|
+
"typeof grecaptcha !== 'undefined' && "
|
|
350
|
+
"typeof grecaptcha.enterprise !== 'undefined' && "
|
|
351
|
+
"typeof grecaptcha.enterprise.execute === 'function'"
|
|
352
|
+
)
|
|
353
|
+
if ready:
|
|
354
|
+
if self.debug:
|
|
355
|
+
logger.info("reCAPTCHA Enterprise loaded")
|
|
356
|
+
return
|
|
357
|
+
except RecaptchaError:
|
|
358
|
+
pass
|
|
359
|
+
time.sleep(1)
|
|
360
|
+
|
|
361
|
+
raise RecaptchaError(
|
|
362
|
+
"reCAPTCHA Enterprise did not load within 30s.\n"
|
|
363
|
+
"The auth browser may have navigated away from Flow.\n"
|
|
364
|
+
"Run 'gflow auth' again."
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def _warm_up(self) -> None:
|
|
368
|
+
"""Warm up reCAPTCHA with realistic human-like browser interaction.
|
|
369
|
+
|
|
370
|
+
Uses CDP Input.dispatchMouseEvent / dispatchMouseWheel to send
|
|
371
|
+
real browser-level input events (Bezier-curved mouse movement,
|
|
372
|
+
gradual scrolling, idle fidgeting). These register identically
|
|
373
|
+
to hardware input in reCAPTCHA's behavioral scoring model.
|
|
374
|
+
|
|
375
|
+
Then pre-executes reCAPTCHA tokens to further build trust.
|
|
376
|
+
"""
|
|
377
|
+
logger.info("Warming up reCAPTCHA (building trust score)...")
|
|
378
|
+
|
|
379
|
+
# ── Phase 1: Human-like interaction via CDP Input.dispatch* ──
|
|
380
|
+
try:
|
|
381
|
+
from gflow.auth.humanizer import CDPHumanizer
|
|
382
|
+
|
|
383
|
+
humanizer = CDPHumanizer(
|
|
384
|
+
cdp_send=lambda method, params: self._cdp_send(method, params),
|
|
385
|
+
)
|
|
386
|
+
# Run full warm-up sequence: idle movement, scrolling, clicking,
|
|
387
|
+
# mouse exploration — ~12 seconds of convincing human behavior
|
|
388
|
+
humanizer.full_warmup(duration=12.0)
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.warning("Humanizer warm-up failed (%s), falling back to basic warm-up", e)
|
|
391
|
+
# Fallback: basic JS-dispatched events (less effective but better than nothing)
|
|
392
|
+
try:
|
|
393
|
+
self._cdp_evaluate("""
|
|
394
|
+
(function() {
|
|
395
|
+
window.scrollTo(0, 200);
|
|
396
|
+
window.scrollTo(0, 0);
|
|
397
|
+
document.dispatchEvent(new MouseEvent('mousemove', {clientX: 100, clientY: 200}));
|
|
398
|
+
document.dispatchEvent(new MouseEvent('mousemove', {clientX: 300, clientY: 400}));
|
|
399
|
+
document.dispatchEvent(new MouseEvent('mousemove', {clientX: 500, clientY: 300}));
|
|
400
|
+
window.dispatchEvent(new Event('focus'));
|
|
401
|
+
return true;
|
|
402
|
+
})()
|
|
403
|
+
""")
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
time.sleep(2)
|
|
407
|
+
|
|
408
|
+
# ── Phase 2: Pre-execute reCAPTCHA tokens to build trust ──
|
|
409
|
+
# Adaptive backoff between warm-up tokens (inspired by notebooklm-py's
|
|
410
|
+
# rate-limit-aware retry pattern). Each successful token slightly
|
|
411
|
+
# reduces the delay; failures increase it.
|
|
412
|
+
warmup_delay = 1.5
|
|
413
|
+
for i in range(3):
|
|
414
|
+
try:
|
|
415
|
+
token = self._cdp_evaluate(
|
|
416
|
+
f"grecaptcha.enterprise.execute('{RECAPTCHA_SITE_KEY}', {{action: 'WARM_UP'}})"
|
|
417
|
+
)
|
|
418
|
+
if token and isinstance(token, str) and len(token) > 100:
|
|
419
|
+
if self.debug:
|
|
420
|
+
logger.info(" Warm-up %d/%d OK (%d chars)", i + 1, 3, len(token))
|
|
421
|
+
# Success — can reduce delay slightly for next iteration
|
|
422
|
+
warmup_delay = max(1.0, warmup_delay * 0.8)
|
|
423
|
+
else:
|
|
424
|
+
warmup_delay = min(5.0, warmup_delay * 1.5)
|
|
425
|
+
time.sleep(warmup_delay)
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.warning(" Warm-up %d/%d failed: %s", i + 1, 3, e)
|
|
428
|
+
warmup_delay = min(5.0, warmup_delay * 2.0)
|
|
429
|
+
time.sleep(warmup_delay)
|
|
430
|
+
|
|
431
|
+
logger.info("reCAPTCHA warm-up complete")
|
|
432
|
+
|
|
433
|
+
def _execute_recaptcha(self, action: str = "IMAGE_GENERATION") -> str:
|
|
434
|
+
"""Execute reCAPTCHA and get a token."""
|
|
435
|
+
try:
|
|
436
|
+
token = self._cdp_evaluate(
|
|
437
|
+
f"grecaptcha.enterprise.execute('{RECAPTCHA_SITE_KEY}', {{action: '{action}'}})"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if not token or not isinstance(token, str):
|
|
441
|
+
raise RecaptchaError(f"reCAPTCHA returned invalid token: {token}")
|
|
442
|
+
|
|
443
|
+
if len(token) < 100:
|
|
444
|
+
raise RecaptchaError(f"reCAPTCHA token too short ({len(token)} chars)")
|
|
445
|
+
|
|
446
|
+
if self.debug:
|
|
447
|
+
logger.info("Got reCAPTCHA token: %s... (%d chars)", token[:30], len(token))
|
|
448
|
+
|
|
449
|
+
return token
|
|
450
|
+
|
|
451
|
+
except RecaptchaError:
|
|
452
|
+
raise
|
|
453
|
+
except Exception as e:
|
|
454
|
+
raise RecaptchaError(f"Failed to execute reCAPTCHA: {e}")
|
|
455
|
+
|
|
456
|
+
def _close_ws(self) -> None:
|
|
457
|
+
"""Close the WebSocket connection only (internal helper)."""
|
|
458
|
+
if self._ws:
|
|
459
|
+
try:
|
|
460
|
+
self._ws.close()
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
self._ws = None
|
|
464
|
+
|
|
465
|
+
def close(self) -> None:
|
|
466
|
+
"""Close the WebSocket connection (does NOT close Chrome)."""
|
|
467
|
+
self._close_ws()
|
|
468
|
+
self._ready = False
|
|
469
|
+
|
|
470
|
+
def __del__(self):
|
|
471
|
+
self.close()
|
|
472
|
+
|
|
473
|
+
def __enter__(self):
|
|
474
|
+
return self
|
|
475
|
+
|
|
476
|
+
def __exit__(self, *args):
|
|
477
|
+
self.close()
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class RecaptchaError(Exception):
|
|
481
|
+
"""Raised when reCAPTCHA token cannot be obtained."""
|
|
482
|
+
pass
|