@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,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Human-like browser interaction via Chrome DevTools Protocol (CDP).
|
|
3
|
+
|
|
4
|
+
Adapted from a Playwright-based humanizer to work over raw CDP WebSocket.
|
|
5
|
+
Provides Bezier-curved mouse movement, gradual scrolling with reading pace,
|
|
6
|
+
and realistic timing — all via Input.dispatchMouseEvent / Input.dispatchKeyEvent.
|
|
7
|
+
|
|
8
|
+
reCAPTCHA Enterprise v3 tracks real DOM-level input events, not just JS
|
|
9
|
+
dispatched events. CDP Input.dispatch* fires at the browser engine level,
|
|
10
|
+
identical to actual hardware input, so reCAPTCHA scores them highly.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import random
|
|
17
|
+
import time
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Tuple, List, Optional, Callable
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("gflow.humanizer")
|
|
22
|
+
|
|
23
|
+
# ── Types ──
|
|
24
|
+
Point = Tuple[float, float]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
28
|
+
# Timing — session state machine + calibrated delay distributions
|
|
29
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
class HumanTiming:
|
|
32
|
+
"""Delay distributions calibrated against real human interaction data."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, speed_multiplier: float = 1.0):
|
|
35
|
+
self.speed_multiplier = speed_multiplier
|
|
36
|
+
|
|
37
|
+
def _scale(self, ms: float) -> float:
|
|
38
|
+
return max(10, ms * self.speed_multiplier)
|
|
39
|
+
|
|
40
|
+
def _gaussian(self, mean: float, stddev: float, min_val: float = 0) -> float:
|
|
41
|
+
return max(min_val, random.gauss(mean, stddev))
|
|
42
|
+
|
|
43
|
+
# ── Specific delay types (return seconds) ──
|
|
44
|
+
|
|
45
|
+
def pre_click_delay(self) -> float:
|
|
46
|
+
return self._scale(self._gaussian(120, 40, 50)) / 1000
|
|
47
|
+
|
|
48
|
+
def click_hold_duration(self) -> float:
|
|
49
|
+
return self._scale(self._gaussian(80, 20, 40)) / 1000
|
|
50
|
+
|
|
51
|
+
def post_click_delay(self) -> float:
|
|
52
|
+
return self._scale(self._gaussian(200, 80, 80)) / 1000
|
|
53
|
+
|
|
54
|
+
def between_actions_delay(self) -> float:
|
|
55
|
+
return self._scale(self._gaussian(400, 150, 150)) / 1000
|
|
56
|
+
|
|
57
|
+
def scroll_tick_delay(self) -> float:
|
|
58
|
+
return self._scale(self._gaussian(55, 15, 20)) / 1000
|
|
59
|
+
|
|
60
|
+
def scroll_reading_pause(self) -> float:
|
|
61
|
+
return self._scale(self._gaussian(1200, 400, 300)) / 1000
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
65
|
+
# Bezier math — same algorithms as the Playwright humanizer
|
|
66
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
def _bezier_point(t: float, points: List[Point]) -> Point:
|
|
69
|
+
"""De Casteljau evaluation of an arbitrary-degree Bezier curve."""
|
|
70
|
+
pts = list(points)
|
|
71
|
+
n = len(pts)
|
|
72
|
+
for r in range(1, n):
|
|
73
|
+
for i in range(n - r):
|
|
74
|
+
pts[i] = (
|
|
75
|
+
(1 - t) * pts[i][0] + t * pts[i + 1][0],
|
|
76
|
+
(1 - t) * pts[i][1] + t * pts[i + 1][1],
|
|
77
|
+
)
|
|
78
|
+
return pts[0]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _distance(a: Point, b: Point) -> float:
|
|
82
|
+
return math.hypot(b[0] - a[0], b[1] - a[1])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _fitts_time(distance: float, target_width: float = 50) -> float:
|
|
86
|
+
"""Fitts's Law: movement time in ms."""
|
|
87
|
+
a, b = 50, 150
|
|
88
|
+
w = max(target_width, 1)
|
|
89
|
+
return a + b * math.log2(distance / w + 1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _generate_control_points(start: Point, end: Point, num_points: int = 2) -> List[Point]:
|
|
93
|
+
dx = end[0] - start[0]
|
|
94
|
+
dy = end[1] - start[1]
|
|
95
|
+
dist = math.hypot(dx, dy)
|
|
96
|
+
angle = math.atan2(dy, dx)
|
|
97
|
+
spread = dist * random.uniform(0.15, 0.4)
|
|
98
|
+
|
|
99
|
+
controls = []
|
|
100
|
+
for i in range(num_points):
|
|
101
|
+
t = (i + 1) / (num_points + 1) + random.uniform(-0.1, 0.1)
|
|
102
|
+
t = max(0.1, min(0.9, t))
|
|
103
|
+
offset = random.gauss(0, spread * 0.4)
|
|
104
|
+
cx = start[0] + dx * t + math.cos(angle + math.pi / 2) * offset
|
|
105
|
+
cy = start[1] + dy * t + math.sin(angle + math.pi / 2) * offset
|
|
106
|
+
controls.append((cx, cy))
|
|
107
|
+
return controls
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _generate_path(start: Point, end: Point, steps: int = 50) -> List[Point]:
|
|
111
|
+
num_controls = random.choice([2, 2, 3])
|
|
112
|
+
controls = _generate_control_points(start, end, num_controls)
|
|
113
|
+
all_points = [start] + controls + [end]
|
|
114
|
+
return [_bezier_point(i / steps, all_points) for i in range(steps + 1)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _add_jitter(path: List[Point], intensity: float = 1.2) -> List[Point]:
|
|
118
|
+
jittered = []
|
|
119
|
+
for i, (x, y) in enumerate(path):
|
|
120
|
+
edge_factor = 0.3 if (i < 5 or i > len(path) - 5) else 1.0
|
|
121
|
+
jx = x + random.gauss(0, intensity * edge_factor)
|
|
122
|
+
jy = y + random.gauss(0, intensity * edge_factor)
|
|
123
|
+
jittered.append((jx, jy))
|
|
124
|
+
jittered[0] = path[0]
|
|
125
|
+
jittered[-1] = path[-1]
|
|
126
|
+
return jittered
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _generate_step_delays(num_steps: int, total_time_ms: float) -> List[float]:
|
|
130
|
+
"""Sine-based easing: slow at start/end, fast in middle."""
|
|
131
|
+
raw = [math.sin(math.pi * i / num_steps) + 0.3 for i in range(num_steps)]
|
|
132
|
+
total_raw = sum(raw)
|
|
133
|
+
delays = [(r / total_raw) * total_time_ms for r in raw]
|
|
134
|
+
return [max(3, d + random.gauss(0, d * 0.15)) for d in delays]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
138
|
+
# CDP Humanizer — ties math to actual CDP Input.dispatch* commands
|
|
139
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
class CDPHumanizer:
|
|
142
|
+
"""
|
|
143
|
+
Human-like mouse/scroll controller that sends real input events
|
|
144
|
+
through Chrome DevTools Protocol.
|
|
145
|
+
|
|
146
|
+
Usage:
|
|
147
|
+
humanizer = CDPHumanizer(cdp_send_fn)
|
|
148
|
+
humanizer.move_mouse(500, 300)
|
|
149
|
+
humanizer.click(500, 300)
|
|
150
|
+
humanizer.scroll_down(400)
|
|
151
|
+
humanizer.full_warmup() # Run a complete warm-up sequence
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, cdp_send: Callable, timing: Optional[HumanTiming] = None):
|
|
155
|
+
"""
|
|
156
|
+
Args:
|
|
157
|
+
cdp_send: Function that sends CDP commands. Signature:
|
|
158
|
+
cdp_send(method: str, params: dict) -> dict
|
|
159
|
+
timing: HumanTiming instance (created with defaults if None)
|
|
160
|
+
"""
|
|
161
|
+
self._cdp = cdp_send
|
|
162
|
+
self.timing = timing or HumanTiming()
|
|
163
|
+
# Start mouse at a random realistic position
|
|
164
|
+
self.mouse_x = random.uniform(300, 700)
|
|
165
|
+
self.mouse_y = random.uniform(200, 400)
|
|
166
|
+
|
|
167
|
+
# ── Low-level CDP dispatchers ──
|
|
168
|
+
|
|
169
|
+
def _dispatch_mouse(self, event_type: str, x: float, y: float,
|
|
170
|
+
button: str = "none", click_count: int = 0) -> None:
|
|
171
|
+
"""Send Input.dispatchMouseEvent via CDP."""
|
|
172
|
+
self._cdp("Input.dispatchMouseEvent", {
|
|
173
|
+
"type": event_type,
|
|
174
|
+
"x": round(x, 2),
|
|
175
|
+
"y": round(y, 2),
|
|
176
|
+
"button": button,
|
|
177
|
+
"clickCount": click_count,
|
|
178
|
+
"timestamp": time.time(),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
def _dispatch_mouse_wheel(self, x: float, y: float,
|
|
182
|
+
delta_x: float = 0, delta_y: float = 0) -> None:
|
|
183
|
+
"""Send a mouse wheel event via CDP."""
|
|
184
|
+
self._cdp("Input.dispatchMouseEvent", {
|
|
185
|
+
"type": "mouseWheel",
|
|
186
|
+
"x": round(x, 2),
|
|
187
|
+
"y": round(y, 2),
|
|
188
|
+
"deltaX": round(delta_x),
|
|
189
|
+
"deltaY": round(delta_y),
|
|
190
|
+
"timestamp": time.time(),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
# ── Mouse movement ──
|
|
194
|
+
|
|
195
|
+
def move_mouse(self, target_x: float, target_y: float,
|
|
196
|
+
target_width: float = 50) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Move mouse to (target_x, target_y) along a Bezier curve
|
|
199
|
+
with Fitts's Law timing and micro-jitter.
|
|
200
|
+
"""
|
|
201
|
+
start = (self.mouse_x, self.mouse_y)
|
|
202
|
+
end = (target_x, target_y)
|
|
203
|
+
dist = _distance(start, end)
|
|
204
|
+
|
|
205
|
+
if dist < 2:
|
|
206
|
+
return # Already there
|
|
207
|
+
|
|
208
|
+
# Calculate movement time via Fitts's Law
|
|
209
|
+
total_time_ms = _fitts_time(dist, target_width) * self.timing.speed_multiplier
|
|
210
|
+
|
|
211
|
+
# More steps for longer distances
|
|
212
|
+
steps = max(15, min(80, int(dist / 8)))
|
|
213
|
+
|
|
214
|
+
# Generate curved path with jitter
|
|
215
|
+
path = _generate_path(start, end, steps)
|
|
216
|
+
path = _add_jitter(path, intensity=random.uniform(0.8, 1.5))
|
|
217
|
+
|
|
218
|
+
# Generate per-step timing (slow-fast-slow)
|
|
219
|
+
delays = _generate_step_delays(len(path) - 1, total_time_ms)
|
|
220
|
+
|
|
221
|
+
# Execute movement
|
|
222
|
+
for i in range(1, len(path)):
|
|
223
|
+
px, py = path[i]
|
|
224
|
+
self._dispatch_mouse("mouseMoved", px, py)
|
|
225
|
+
time.sleep(delays[i - 1] / 1000)
|
|
226
|
+
|
|
227
|
+
self.mouse_x = target_x
|
|
228
|
+
self.mouse_y = target_y
|
|
229
|
+
|
|
230
|
+
def click(self, x: float, y: float, target_width: float = 50) -> None:
|
|
231
|
+
"""Move to position and click with human timing."""
|
|
232
|
+
self.move_mouse(x, y, target_width)
|
|
233
|
+
|
|
234
|
+
# Overshoot and correct (~20% chance)
|
|
235
|
+
if random.random() < 0.20:
|
|
236
|
+
overshoot_dist = random.uniform(5, 15)
|
|
237
|
+
angle = random.uniform(0, 2 * math.pi)
|
|
238
|
+
ox = x + math.cos(angle) * overshoot_dist
|
|
239
|
+
oy = y + math.sin(angle) * overshoot_dist
|
|
240
|
+
|
|
241
|
+
self._dispatch_mouse("mouseMoved", ox, oy)
|
|
242
|
+
time.sleep(random.uniform(0.05, 0.12))
|
|
243
|
+
|
|
244
|
+
# Correct back
|
|
245
|
+
correction_path = _generate_path((ox, oy), (x, y), random.randint(5, 10))
|
|
246
|
+
for px, py in correction_path[1:]:
|
|
247
|
+
self._dispatch_mouse("mouseMoved", px, py)
|
|
248
|
+
time.sleep(random.uniform(0.005, 0.015))
|
|
249
|
+
|
|
250
|
+
# Pre-click pause
|
|
251
|
+
time.sleep(self.timing.pre_click_delay())
|
|
252
|
+
|
|
253
|
+
# Mouse down
|
|
254
|
+
self._dispatch_mouse("mousePressed", x, y, button="left", click_count=1)
|
|
255
|
+
time.sleep(self.timing.click_hold_duration())
|
|
256
|
+
|
|
257
|
+
# Mouse up
|
|
258
|
+
self._dispatch_mouse("mouseReleased", x, y, button="left", click_count=1)
|
|
259
|
+
|
|
260
|
+
# Post-click wait
|
|
261
|
+
time.sleep(self.timing.post_click_delay())
|
|
262
|
+
|
|
263
|
+
self.mouse_x = x
|
|
264
|
+
self.mouse_y = y
|
|
265
|
+
|
|
266
|
+
# ── Scrolling ──
|
|
267
|
+
|
|
268
|
+
def scroll_down(self, pixels: int) -> None:
|
|
269
|
+
"""Scroll down gradually in bursts, like a real human."""
|
|
270
|
+
scrolled = 0
|
|
271
|
+
target = pixels + random.randint(-30, 30)
|
|
272
|
+
mx = self.mouse_x
|
|
273
|
+
my = self.mouse_y
|
|
274
|
+
|
|
275
|
+
while scrolled < target:
|
|
276
|
+
tick_amount = random.randint(50, 150)
|
|
277
|
+
tick_amount = min(tick_amount, target - scrolled + random.randint(0, 30))
|
|
278
|
+
|
|
279
|
+
burst_ticks = random.randint(2, 5)
|
|
280
|
+
for _ in range(burst_ticks):
|
|
281
|
+
if scrolled >= target:
|
|
282
|
+
break
|
|
283
|
+
self._dispatch_mouse_wheel(mx, my, delta_y=tick_amount)
|
|
284
|
+
scrolled += tick_amount
|
|
285
|
+
time.sleep(self.timing.scroll_tick_delay())
|
|
286
|
+
|
|
287
|
+
# Reading pause ~30% of the time
|
|
288
|
+
if random.random() < 0.30:
|
|
289
|
+
time.sleep(self.timing.scroll_reading_pause())
|
|
290
|
+
else:
|
|
291
|
+
time.sleep(random.uniform(0.1, 0.4) * self.timing.speed_multiplier)
|
|
292
|
+
|
|
293
|
+
# Overshoot and correct (~25% chance)
|
|
294
|
+
if random.random() < 0.25:
|
|
295
|
+
overshoot = random.randint(50, 150)
|
|
296
|
+
self._dispatch_mouse_wheel(mx, my, delta_y=overshoot)
|
|
297
|
+
time.sleep(random.uniform(0.2, 0.5))
|
|
298
|
+
# Scroll back
|
|
299
|
+
ticks = random.randint(2, 3)
|
|
300
|
+
per_tick = overshoot // ticks
|
|
301
|
+
for _ in range(ticks):
|
|
302
|
+
self._dispatch_mouse_wheel(mx, my, delta_y=-per_tick)
|
|
303
|
+
time.sleep(self.timing.scroll_tick_delay())
|
|
304
|
+
|
|
305
|
+
def scroll_up(self, pixels: int) -> None:
|
|
306
|
+
"""Scroll up gradually."""
|
|
307
|
+
scrolled = 0
|
|
308
|
+
target = pixels + random.randint(-20, 20)
|
|
309
|
+
mx = self.mouse_x
|
|
310
|
+
my = self.mouse_y
|
|
311
|
+
|
|
312
|
+
while scrolled < target:
|
|
313
|
+
tick_amount = random.randint(50, 120)
|
|
314
|
+
tick_amount = min(tick_amount, target - scrolled + random.randint(0, 20))
|
|
315
|
+
|
|
316
|
+
burst_ticks = random.randint(2, 4)
|
|
317
|
+
for _ in range(burst_ticks):
|
|
318
|
+
if scrolled >= target:
|
|
319
|
+
break
|
|
320
|
+
self._dispatch_mouse_wheel(mx, my, delta_y=-tick_amount)
|
|
321
|
+
scrolled += tick_amount
|
|
322
|
+
time.sleep(self.timing.scroll_tick_delay())
|
|
323
|
+
|
|
324
|
+
if random.random() < 0.25:
|
|
325
|
+
time.sleep(self.timing.scroll_reading_pause())
|
|
326
|
+
else:
|
|
327
|
+
time.sleep(random.uniform(0.1, 0.3) * self.timing.speed_multiplier)
|
|
328
|
+
|
|
329
|
+
# ── Idle fidgeting ──
|
|
330
|
+
|
|
331
|
+
def idle_movement(self, duration: float = 3.0) -> None:
|
|
332
|
+
"""Small random mouse movements simulating fidgeting while reading."""
|
|
333
|
+
end_time = time.time() + duration
|
|
334
|
+
while time.time() < end_time:
|
|
335
|
+
dx = random.gauss(0, 40)
|
|
336
|
+
dy = random.gauss(0, 30)
|
|
337
|
+
target_x = max(50, min(1400, self.mouse_x + dx))
|
|
338
|
+
target_y = max(50, min(800, self.mouse_y + dy))
|
|
339
|
+
|
|
340
|
+
# Small movement (fewer steps for idle)
|
|
341
|
+
path = _generate_path(
|
|
342
|
+
(self.mouse_x, self.mouse_y),
|
|
343
|
+
(target_x, target_y),
|
|
344
|
+
random.randint(5, 12),
|
|
345
|
+
)
|
|
346
|
+
for px, py in path[1:]:
|
|
347
|
+
self._dispatch_mouse("mouseMoved", px, py)
|
|
348
|
+
time.sleep(random.uniform(0.01, 0.03))
|
|
349
|
+
|
|
350
|
+
self.mouse_x = target_x
|
|
351
|
+
self.mouse_y = target_y
|
|
352
|
+
time.sleep(random.uniform(0.8, 2.5))
|
|
353
|
+
|
|
354
|
+
# ── Full warm-up sequence ──
|
|
355
|
+
|
|
356
|
+
def full_warmup(self, duration: float = 12.0) -> None:
|
|
357
|
+
"""
|
|
358
|
+
Run a complete warm-up that convincingly mimics a real user
|
|
359
|
+
browsing the Flow page for several seconds.
|
|
360
|
+
|
|
361
|
+
Sequence:
|
|
362
|
+
1. Idle mouse movements (user landed on page, looking around)
|
|
363
|
+
2. Scroll down to explore content
|
|
364
|
+
3. Move mouse to various page elements
|
|
365
|
+
4. Click on something benign
|
|
366
|
+
5. Scroll back up
|
|
367
|
+
6. More idle movement
|
|
368
|
+
|
|
369
|
+
This builds enough behavioral signal for reCAPTCHA to assign
|
|
370
|
+
a good trust score.
|
|
371
|
+
"""
|
|
372
|
+
logger.info("Running human-like warm-up (%ds)...", int(duration))
|
|
373
|
+
start = time.time()
|
|
374
|
+
|
|
375
|
+
# Phase 1: Initial idle — user just loaded the page, eyes scanning
|
|
376
|
+
self.idle_movement(duration=min(2.5, duration * 0.2))
|
|
377
|
+
|
|
378
|
+
if time.time() - start > duration:
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
# Phase 2: Scroll down to explore
|
|
382
|
+
self.scroll_down(random.randint(200, 500))
|
|
383
|
+
time.sleep(random.uniform(0.5, 1.5))
|
|
384
|
+
|
|
385
|
+
if time.time() - start > duration:
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
# Phase 3: Move mouse to a few random positions (reading content)
|
|
389
|
+
for _ in range(random.randint(3, 5)):
|
|
390
|
+
target_x = random.uniform(200, 900)
|
|
391
|
+
target_y = random.uniform(150, 600)
|
|
392
|
+
self.move_mouse(target_x, target_y)
|
|
393
|
+
time.sleep(random.uniform(0.3, 1.0))
|
|
394
|
+
if time.time() - start > duration:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# Phase 4: Click somewhere benign (body area, not a link)
|
|
398
|
+
click_x = random.uniform(400, 800)
|
|
399
|
+
click_y = random.uniform(300, 500)
|
|
400
|
+
self.click(click_x, click_y)
|
|
401
|
+
|
|
402
|
+
if time.time() - start > duration:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
# Phase 5: Scroll back up
|
|
406
|
+
self.scroll_up(random.randint(100, 300))
|
|
407
|
+
time.sleep(random.uniform(0.3, 0.8))
|
|
408
|
+
|
|
409
|
+
if time.time() - start > duration:
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Phase 6: More idle movement
|
|
413
|
+
remaining = max(0.5, duration - (time.time() - start))
|
|
414
|
+
self.idle_movement(duration=min(remaining, 3.0))
|
|
415
|
+
|
|
416
|
+
elapsed = time.time() - start
|
|
417
|
+
logger.info("Human warm-up complete (%.1fs)", elapsed)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate a tiny Chrome extension that handles proxy authentication.
|
|
3
|
+
|
|
4
|
+
Chrome's --proxy-server flag doesn't support user:pass authentication.
|
|
5
|
+
This module creates a minimal Manifest V3 extension that:
|
|
6
|
+
1. Sets Chrome to use the configured proxy via chrome.proxy API
|
|
7
|
+
2. Handles the 407 auth challenge automatically via onAuthRequired
|
|
8
|
+
|
|
9
|
+
The extension is written to ~/.gflow/proxy-ext/ and loaded via
|
|
10
|
+
--load-extension when Chrome launches.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("gflow.auth.proxy")
|
|
20
|
+
|
|
21
|
+
EXT_DIR = Path.home() / ".gflow" / "proxy-ext"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_proxy_extension(host: str, port: int, username: str, password: str,
|
|
25
|
+
scheme: str = "http") -> str:
|
|
26
|
+
"""Create a Chrome proxy auth extension and return its path.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
host: Proxy hostname
|
|
30
|
+
port: Proxy port
|
|
31
|
+
username: Proxy username
|
|
32
|
+
password: Proxy password
|
|
33
|
+
scheme: Proxy scheme (http or socks5)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Path to the extension directory (for --load-extension)
|
|
37
|
+
"""
|
|
38
|
+
EXT_DIR.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
# Manifest V3
|
|
41
|
+
manifest = {
|
|
42
|
+
"version": "1.0.0",
|
|
43
|
+
"manifest_version": 3,
|
|
44
|
+
"name": "Proxy Auth",
|
|
45
|
+
"permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
|
|
46
|
+
"host_permissions": ["<all_urls>"],
|
|
47
|
+
"background": {
|
|
48
|
+
"service_worker": "background.js"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Background service worker
|
|
53
|
+
background_js = f"""
|
|
54
|
+
// Set proxy configuration
|
|
55
|
+
chrome.proxy.settings.set({{
|
|
56
|
+
value: {{
|
|
57
|
+
mode: "fixed_servers",
|
|
58
|
+
rules: {{
|
|
59
|
+
singleProxy: {{
|
|
60
|
+
scheme: "{scheme}",
|
|
61
|
+
host: "{host}",
|
|
62
|
+
port: {port}
|
|
63
|
+
}},
|
|
64
|
+
bypassList: ["localhost", "127.0.0.1"]
|
|
65
|
+
}}
|
|
66
|
+
}},
|
|
67
|
+
scope: "regular"
|
|
68
|
+
}});
|
|
69
|
+
|
|
70
|
+
// Handle proxy authentication
|
|
71
|
+
chrome.webRequest.onAuthRequired.addListener(
|
|
72
|
+
function(details) {{
|
|
73
|
+
return {{
|
|
74
|
+
authCredentials: {{
|
|
75
|
+
username: "{username}",
|
|
76
|
+
password: "{password}"
|
|
77
|
+
}}
|
|
78
|
+
}};
|
|
79
|
+
}},
|
|
80
|
+
{{ urls: ["<all_urls>"] }},
|
|
81
|
+
["blocking"]
|
|
82
|
+
);
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
(EXT_DIR / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
86
|
+
(EXT_DIR / "background.js").write_text(background_js)
|
|
87
|
+
|
|
88
|
+
logger.info("Proxy extension created at %s (-> %s:%d)", EXT_DIR, host, port)
|
|
89
|
+
return str(EXT_DIR)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_chrome_proxy_args() -> list[str]:
|
|
93
|
+
"""Return Chrome CLI args to route all traffic through the residential proxy.
|
|
94
|
+
|
|
95
|
+
Returns empty list if no proxy is configured.
|
|
96
|
+
Uses the first proxy from ~/.gflow/proxies.txt (sticky session).
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
from gflow.api.client import get_active_proxy, parse_proxy_url
|
|
100
|
+
except ImportError:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
proxy_url = get_active_proxy()
|
|
104
|
+
if not proxy_url:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
info = parse_proxy_url(proxy_url)
|
|
108
|
+
if not info["host"]:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
# Create proxy auth extension
|
|
112
|
+
ext_path = create_proxy_extension(
|
|
113
|
+
host=info["host"],
|
|
114
|
+
port=info["port"],
|
|
115
|
+
username=info["username"],
|
|
116
|
+
password=info["password"],
|
|
117
|
+
scheme=info["scheme"],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return [f"--load-extension={ext_path}"]
|