qoder-autopilot 0.2.1__py3-none-any.whl
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.
- qoder_autopilot/__init__.py +59 -0
- qoder_autopilot/__main__.py +5 -0
- qoder_autopilot/browser/__init__.py +5 -0
- qoder_autopilot/browser/camoufox.py +62 -0
- qoder_autopilot/browser/window_tiler.py +117 -0
- qoder_autopilot/captcha/__init__.py +14 -0
- qoder_autopilot/captcha/ai_vision.py +327 -0
- qoder_autopilot/captcha/manual.py +95 -0
- qoder_autopilot/captcha/opencv_detect.py +184 -0
- qoder_autopilot/captcha/slider.py +86 -0
- qoder_autopilot/captcha/solver.py +165 -0
- qoder_autopilot/cli.py +431 -0
- qoder_autopilot/config.py +300 -0
- qoder_autopilot/credentials.py +44 -0
- qoder_autopilot/deploy.py +338 -0
- qoder_autopilot/errors.py +132 -0
- qoder_autopilot/first_run.py +107 -0
- qoder_autopilot/identity.py +66 -0
- qoder_autopilot/logger.py +119 -0
- qoder_autopilot/ninerouter.py +105 -0
- qoder_autopilot/oauth.py +149 -0
- qoder_autopilot/otp.py +67 -0
- qoder_autopilot/register.py +383 -0
- qoder_autopilot/temp_mail.py +307 -0
- qoder_autopilot/user_config.py +171 -0
- qoder_autopilot/worker_template/package.json +21 -0
- qoder_autopilot/worker_template/schema.sql +24 -0
- qoder_autopilot/worker_template/scripts/setup.sh +186 -0
- qoder_autopilot/worker_template/src/config.js +6 -0
- qoder_autopilot/worker_template/src/handlers/api.js +226 -0
- qoder_autopilot/worker_template/src/handlers/email.js +50 -0
- qoder_autopilot/worker_template/src/index.js +43 -0
- qoder_autopilot/worker_template/src/utils.js +35 -0
- qoder_autopilot/worker_template/wrangler.toml.example +19 -0
- qoder_autopilot-0.2.1.dist-info/METADATA +299 -0
- qoder_autopilot-0.2.1.dist-info/RECORD +39 -0
- qoder_autopilot-0.2.1.dist-info/WHEEL +4 -0
- qoder_autopilot-0.2.1.dist-info/entry_points.txt +2 -0
- qoder_autopilot-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Qoder Autopilot â OpenCV Captcha Gap Detection
|
|
3
|
+
================================================
|
|
4
|
+
4-method computer vision approach to find the gap position in
|
|
5
|
+
Aliyun slide CAPTCHAs without requiring an AI API:
|
|
6
|
+
|
|
7
|
+
1. Column brightness drop analysis
|
|
8
|
+
2. Edge density analysis
|
|
9
|
+
3. Masked template matching (TM_CCOEFF_NORMED)
|
|
10
|
+
4. Masked SQDIFF matching (TM_SQDIFF_NORMED)
|
|
11
|
+
|
|
12
|
+
Results are combined via voting to find the most likely gap position.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import urllib.request
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from ..logger import log
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def detect_gap_position(page) -> float | None:
|
|
24
|
+
"""Use OpenCV to detect the puzzle gap position in the captcha image.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
page: Playwright/Camoufox page object with the captcha loaded.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The X offset scaled to track width, or None on failure.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
import cv2
|
|
34
|
+
|
|
35
|
+
# Get image URLs from the page
|
|
36
|
+
img_urls = await page.evaluate("""() => {
|
|
37
|
+
const bg = document.querySelector('#aliyunCaptcha-img');
|
|
38
|
+
const pz = document.querySelector('#aliyunCaptcha-puzzle');
|
|
39
|
+
return {
|
|
40
|
+
bgSrc: bg?.src || '',
|
|
41
|
+
pzSrc: pz?.src || '',
|
|
42
|
+
bgW: bg?.getBoundingClientRect()?.width || 300,
|
|
43
|
+
bgH: bg?.getBoundingClientRect()?.height || 200,
|
|
44
|
+
pzW: pz?.getBoundingClientRect()?.width || 52,
|
|
45
|
+
};
|
|
46
|
+
}""")
|
|
47
|
+
|
|
48
|
+
if not img_urls.get("bgSrc") or not img_urls.get("pzSrc"):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# Parse base64 data URIs
|
|
52
|
+
def parse_data_uri(uri: str) -> bytes:
|
|
53
|
+
if uri.startswith("data:"):
|
|
54
|
+
_, b64data = uri.split(",", 1)
|
|
55
|
+
return base64.b64decode(b64data)
|
|
56
|
+
else:
|
|
57
|
+
return urllib.request.urlopen(uri).read()
|
|
58
|
+
|
|
59
|
+
bg_data = parse_data_uri(img_urls["bgSrc"])
|
|
60
|
+
pz_data = parse_data_uri(img_urls["pzSrc"])
|
|
61
|
+
|
|
62
|
+
bg_img = cv2.imdecode(np.frombuffer(bg_data, np.uint8), cv2.IMREAD_COLOR)
|
|
63
|
+
pz_img = cv2.imdecode(np.frombuffer(pz_data, np.uint8), cv2.IMREAD_UNCHANGED)
|
|
64
|
+
|
|
65
|
+
if bg_img is None or pz_img is None:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Convert puzzle to grayscale + edge detection
|
|
69
|
+
if pz_img.shape[2] == 4:
|
|
70
|
+
pz_gray = pz_img[:, :, 3] # Use alpha channel
|
|
71
|
+
_, pz_mask = cv2.threshold(pz_gray, 127, 255, cv2.THRESH_BINARY)
|
|
72
|
+
pz_bgr = pz_img[:, :, :3]
|
|
73
|
+
else:
|
|
74
|
+
pz_gray = cv2.cvtColor(pz_img, cv2.COLOR_BGR2GRAY)
|
|
75
|
+
pz_mask = None
|
|
76
|
+
pz_bgr = pz_img
|
|
77
|
+
|
|
78
|
+
bg_gray = cv2.cvtColor(bg_img, cv2.COLOR_BGR2GRAY)
|
|
79
|
+
bg_h, bg_w = bg_gray.shape
|
|
80
|
+
pz_h, pz_w = pz_bgr.shape[:2]
|
|
81
|
+
|
|
82
|
+
# âââ Method 1: Column brightness drop analysis âââ
|
|
83
|
+
col_brightness = np.mean(bg_gray, axis=0)
|
|
84
|
+
kernel_size = max(3, pz_w // 4)
|
|
85
|
+
if kernel_size % 2 == 0:
|
|
86
|
+
kernel_size += 1
|
|
87
|
+
smoothed = cv2.GaussianBlur(col_brightness.reshape(1, -1), (kernel_size, 1), 0).flatten()
|
|
88
|
+
|
|
89
|
+
window = pz_w
|
|
90
|
+
min_brightness = float("inf")
|
|
91
|
+
min_col = bg_w // 2
|
|
92
|
+
|
|
93
|
+
start_col = int(bg_w * 0.15) # Skip first 15% (puzzle piece start area)
|
|
94
|
+
for col in range(start_col, bg_w - pz_w):
|
|
95
|
+
left = max(0, col - window // 2)
|
|
96
|
+
right = min(bg_w, col + window // 2)
|
|
97
|
+
region_brightness = np.mean(smoothed[left:right])
|
|
98
|
+
|
|
99
|
+
other_left = smoothed[:left] if left > 0 else np.array([])
|
|
100
|
+
other_right = smoothed[right:] if right < bg_w else np.array([])
|
|
101
|
+
other = np.concatenate([other_left, other_right])
|
|
102
|
+
if len(other) > 0:
|
|
103
|
+
other_brightness = np.mean(other)
|
|
104
|
+
drop = other_brightness - region_brightness
|
|
105
|
+
if drop > (np.mean(smoothed) - min_brightness):
|
|
106
|
+
min_brightness = region_brightness
|
|
107
|
+
min_col = col
|
|
108
|
+
|
|
109
|
+
gap_x_method1 = min_col
|
|
110
|
+
|
|
111
|
+
# âââ Method 2: Edge density analysis âââ
|
|
112
|
+
bg_edges = cv2.Canny(bg_gray, 50, 150)
|
|
113
|
+
col_edge_density = np.sum(bg_edges > 0, axis=0).astype(float)
|
|
114
|
+
edge_smoothed = cv2.GaussianBlur(
|
|
115
|
+
col_edge_density.reshape(1, -1), (kernel_size, 1), 0
|
|
116
|
+
).flatten()
|
|
117
|
+
|
|
118
|
+
max_edge = 0
|
|
119
|
+
max_edge_col = bg_w // 2
|
|
120
|
+
for col in range(start_col, bg_w - pz_w):
|
|
121
|
+
left = max(0, col - window // 2)
|
|
122
|
+
right = min(bg_w, col + window // 2)
|
|
123
|
+
edge_sum = np.sum(edge_smoothed[left:right])
|
|
124
|
+
if edge_sum > max_edge:
|
|
125
|
+
max_edge = edge_sum
|
|
126
|
+
max_edge_col = col
|
|
127
|
+
|
|
128
|
+
gap_x_method2 = max_edge_col
|
|
129
|
+
|
|
130
|
+
# âââ Method 3: Masked template matching âââ
|
|
131
|
+
pz_gray_tmpl = cv2.cvtColor(pz_bgr, cv2.COLOR_BGR2GRAY)
|
|
132
|
+
tm_mask = (
|
|
133
|
+
pz_mask if pz_mask is not None else np.ones(pz_bgr.shape[:2], dtype=np.uint8) * 255
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
result_masked = cv2.matchTemplate(bg_gray, pz_gray_tmpl, cv2.TM_CCOEFF_NORMED, mask=tm_mask)
|
|
137
|
+
_, max_val_masked, _, max_loc_masked = cv2.minMaxLoc(result_masked)
|
|
138
|
+
gap_x_method3 = max_loc_masked[0]
|
|
139
|
+
|
|
140
|
+
# âââ Method 4: SQDIFF masked âââ
|
|
141
|
+
result_sqdiff = cv2.matchTemplate(bg_gray, pz_gray_tmpl, cv2.TM_SQDIFF_NORMED, mask=tm_mask)
|
|
142
|
+
_, _, min_loc_sq, _ = cv2.minMaxLoc(result_sqdiff)
|
|
143
|
+
gap_x_method4 = min_loc_sq[0]
|
|
144
|
+
|
|
145
|
+
# âââ Combine via voting âââ
|
|
146
|
+
methods = {
|
|
147
|
+
"brightness": gap_x_method1,
|
|
148
|
+
"edge": gap_x_method2,
|
|
149
|
+
"maskedTM": gap_x_method3,
|
|
150
|
+
"sqdiffTM": gap_x_method4,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
best_votes = {}
|
|
154
|
+
for name, x in methods.items():
|
|
155
|
+
votes = sum(1 for other_x in methods.values() if abs(x - other_x) < 25)
|
|
156
|
+
best_votes[name] = votes
|
|
157
|
+
|
|
158
|
+
winner = max(best_votes.keys(), key=lambda k: k and best_votes[k])
|
|
159
|
+
gap_x = methods[winner]
|
|
160
|
+
|
|
161
|
+
agreeing = [x for x in methods.values() if abs(x - gap_x) < 25]
|
|
162
|
+
if len(agreeing) >= 2:
|
|
163
|
+
gap_x = int(np.mean(agreeing))
|
|
164
|
+
confidence = min(0.9, len(agreeing) * 0.25)
|
|
165
|
+
else:
|
|
166
|
+
gap_x = gap_x_method3
|
|
167
|
+
confidence = max_val_masked
|
|
168
|
+
|
|
169
|
+
method_str = f"winner={winner}(votes={best_votes[winner]}) | {methods}"
|
|
170
|
+
log(f" đ Gap: x={gap_x}px | {method_str}")
|
|
171
|
+
|
|
172
|
+
bg_img_width = bg_img.shape[1]
|
|
173
|
+
track_width = img_urls["bgW"]
|
|
174
|
+
scaled_x = (gap_x / bg_img_width) * track_width
|
|
175
|
+
|
|
176
|
+
log(
|
|
177
|
+
f" đ Gap: x={gap_x}px (img {bg_img_width}px) "
|
|
178
|
+
f"â track {scaled_x:.0f}/{track_width:.0f}px, conf={confidence:.2f}"
|
|
179
|
+
)
|
|
180
|
+
return scaled_x
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
log(f" â ī¸ Gap detection failed: {e}")
|
|
184
|
+
return None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Qoder Autopilot â Human-like Slider Movement
|
|
3
|
+
===============================================
|
|
4
|
+
Simulates realistic human mouse movement for sliding captcha puzzle pieces.
|
|
5
|
+
Includes variable speed, slight vertical wobble, and overshoot correction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
from ..logger import log
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def slide_puzzle(
|
|
15
|
+
page,
|
|
16
|
+
target_x: float,
|
|
17
|
+
track_width: float,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Slide the puzzle piece to the target position with human-like movement.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
page: Playwright/Camoufox page object.
|
|
23
|
+
target_x: Target X position on the track.
|
|
24
|
+
track_width: Total width of the sliding track.
|
|
25
|
+
"""
|
|
26
|
+
# Find the slider element
|
|
27
|
+
slider = await page.evaluate("""() => {
|
|
28
|
+
const sels = ['#aliyunCaptcha-sliding-slider',
|
|
29
|
+
'#aliyunCaptcha-sliding .aliyunCaptcha-sliding-btn',
|
|
30
|
+
'.nc_iconfont.btn_slide', '.slide-verify-slider',
|
|
31
|
+
'[class*="slider-move"]', '[id*="sliding-slider"]',
|
|
32
|
+
'[class*="slider"] [class*="btn"]', '[class*="drag"]',
|
|
33
|
+
'[class*="slide"] [class*="handle"]'];
|
|
34
|
+
for (const s of sels) {
|
|
35
|
+
const el = document.querySelector(s);
|
|
36
|
+
if (el) { const r = el.getBoundingClientRect();
|
|
37
|
+
return {x: r.x, y: r.y, w: r.width, h: r.height}; }
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}""")
|
|
41
|
+
|
|
42
|
+
if not slider:
|
|
43
|
+
log(" â ī¸ No slider found")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
sx = slider["x"] + slider["w"] / 2
|
|
47
|
+
sy = slider["y"] + slider["h"] / 2
|
|
48
|
+
|
|
49
|
+
log(f" đąī¸ Sliding {target_x:.0f}/{track_width:.0f}px")
|
|
50
|
+
|
|
51
|
+
# Move to slider start position
|
|
52
|
+
await page.mouse.move(sx, sy)
|
|
53
|
+
await asyncio.sleep(random.uniform(0.3, 0.5))
|
|
54
|
+
await page.mouse.down()
|
|
55
|
+
await asyncio.sleep(random.uniform(0.1, 0.2))
|
|
56
|
+
|
|
57
|
+
# âââ Main slide with variable speed âââ
|
|
58
|
+
cx = 0
|
|
59
|
+
steps = random.randint(25, 45)
|
|
60
|
+
|
|
61
|
+
for i in range(steps):
|
|
62
|
+
p = (i + 1) / steps
|
|
63
|
+
# Fast in the middle, slow at start and end
|
|
64
|
+
if p < 0.7:
|
|
65
|
+
speed = random.uniform(3, 8)
|
|
66
|
+
elif p < 0.9:
|
|
67
|
+
speed = random.uniform(1, 3)
|
|
68
|
+
else:
|
|
69
|
+
speed = random.uniform(0.3, 1)
|
|
70
|
+
dx = min(speed, target_x - cx)
|
|
71
|
+
cx += dx
|
|
72
|
+
# Add slight vertical wobble
|
|
73
|
+
await page.mouse.move(
|
|
74
|
+
sx + cx,
|
|
75
|
+
sy + random.uniform(-1.5, 1.5),
|
|
76
|
+
steps=1,
|
|
77
|
+
)
|
|
78
|
+
await asyncio.sleep(random.uniform(0.01, 0.04))
|
|
79
|
+
|
|
80
|
+
# âââ Overshoot + correct (mimics human behavior) âââ
|
|
81
|
+
await page.mouse.move(sx + cx + random.uniform(3, 10), sy, steps=2)
|
|
82
|
+
await asyncio.sleep(random.uniform(0.1, 0.3))
|
|
83
|
+
await page.mouse.move(sx + target_x, sy, steps=3)
|
|
84
|
+
await asyncio.sleep(random.uniform(0.1, 0.2))
|
|
85
|
+
await page.mouse.up()
|
|
86
|
+
await asyncio.sleep(2)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Qoder Autopilot â Captcha Solver Orchestrator
|
|
3
|
+
===============================================
|
|
4
|
+
Coordinates captcha detection, solving strategies, and retry logic.
|
|
5
|
+
|
|
6
|
+
Strategies (in priority order):
|
|
7
|
+
1. AI Vision (if API key configured) â most accurate
|
|
8
|
+
2. OpenCV (always available) â 4-method fallback
|
|
9
|
+
3. Manual (if requested) â user solves in browser
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
solver = CaptchaSolver(manual=False)
|
|
13
|
+
ok = await solver.solve(page)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import random
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
from .. import config
|
|
21
|
+
from ..logger import log, log_err, log_ok
|
|
22
|
+
from .ai_vision import gemini_detect_gap
|
|
23
|
+
from .manual import handle_captcha_manual
|
|
24
|
+
from .opencv_detect import detect_gap_position
|
|
25
|
+
from .slider import slide_puzzle
|
|
26
|
+
|
|
27
|
+
# CSS selectors for detecting captcha presence
|
|
28
|
+
_CAPTCHA_DETECT_SELECTORS = """
|
|
29
|
+
const sels = ['#aliyunCaptcha-sliding', '.aliyunCaptcha', '#nc_1_wrapper',
|
|
30
|
+
'.nc-container', 'iframe[src*="captcha"]', '.slide-verify',
|
|
31
|
+
'[class*="captcha"]', '[id*="captcha"]'];
|
|
32
|
+
for (const s of sels) {
|
|
33
|
+
const el = document.querySelector(s);
|
|
34
|
+
if (el && el.offsetParent !== null) return true;
|
|
35
|
+
}
|
|
36
|
+
return document.querySelectorAll(
|
|
37
|
+
'iframe[src*="captcha"], iframe[src*="aliyun"]'
|
|
38
|
+
).length > 0;
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_CAPTCHA_STILL_SELECTORS = """
|
|
42
|
+
const sels = ['#aliyunCaptcha-sliding-slider', '#aliyunCaptcha-window-float',
|
|
43
|
+
'#aliyunCaptcha-sliding', '.aliyunCaptcha', '#captcha-element',
|
|
44
|
+
'#nc_1_wrapper', '.nc-container'];
|
|
45
|
+
for (const s of sels) {
|
|
46
|
+
const el = document.querySelector(s);
|
|
47
|
+
if (el && el.offsetParent !== null) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CaptchaSolver:
|
|
54
|
+
"""Orchestrates captcha solving with multiple strategies and retries."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
manual: bool = False,
|
|
59
|
+
use_ai: bool = True,
|
|
60
|
+
max_attempts: int | None = None,
|
|
61
|
+
):
|
|
62
|
+
self.manual = manual
|
|
63
|
+
self.use_ai = use_ai and bool(config.AI_API_KEY)
|
|
64
|
+
self.max_attempts = max_attempts or config.MAX_CAPTCHA_ATTEMPTS
|
|
65
|
+
|
|
66
|
+
async def solve(self, page) -> bool:
|
|
67
|
+
"""Detect and solve the captcha on the current page.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
page: Playwright/Camoufox page object.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if captcha was solved (or not present), False on failure.
|
|
74
|
+
"""
|
|
75
|
+
if self.manual:
|
|
76
|
+
return await handle_captcha_manual(page)
|
|
77
|
+
|
|
78
|
+
await asyncio.sleep(2)
|
|
79
|
+
|
|
80
|
+
# Check if captcha is present
|
|
81
|
+
has_captcha = await page.evaluate(f"() => {{ {_CAPTCHA_DETECT_SELECTORS} }}")
|
|
82
|
+
if not has_captcha:
|
|
83
|
+
log(" âšī¸ No captcha detected")
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
log(" đ Captcha detected! Solving...")
|
|
87
|
+
config.SCREENSHOTS_DIR.mkdir(exist_ok=True)
|
|
88
|
+
await page.screenshot(path=str(config.SCREENSHOTS_DIR / f"captcha_{int(time.time())}.png"))
|
|
89
|
+
|
|
90
|
+
tried_positions: list[float] = []
|
|
91
|
+
|
|
92
|
+
for attempt in range(self.max_attempts):
|
|
93
|
+
# Wait for captcha image to load
|
|
94
|
+
img_ready = {"ready": False, "src": ""}
|
|
95
|
+
for _ in range(30): # up to 6 seconds
|
|
96
|
+
img_ready = await page.evaluate("""() => {
|
|
97
|
+
const bg = document.querySelector('#aliyunCaptcha-img');
|
|
98
|
+
if (!bg || !bg.src) return {ready: false, src: ''};
|
|
99
|
+
const isLoaded = bg.src.startsWith('data:')
|
|
100
|
+
? bg.complete
|
|
101
|
+
: (bg.complete && bg.naturalWidth > 0);
|
|
102
|
+
return {ready: isLoaded, src: bg.src.substring(0, 80)};
|
|
103
|
+
}""")
|
|
104
|
+
if img_ready.get("ready"):
|
|
105
|
+
break
|
|
106
|
+
await asyncio.sleep(0.2)
|
|
107
|
+
else:
|
|
108
|
+
log(f" â ī¸ Image not loaded after 6s, src='{img_ready.get('src', 'N/A')}'")
|
|
109
|
+
await asyncio.sleep(0.5)
|
|
110
|
+
|
|
111
|
+
# Get track width
|
|
112
|
+
track_w = await page.evaluate("""() => {
|
|
113
|
+
const sels = ['#aliyunCaptcha-sliding-body',
|
|
114
|
+
'.aliyunCaptcha-sliding-track',
|
|
115
|
+
'.nc-container .nc_scale', '.slide-verify-track',
|
|
116
|
+
'[class*="track"]'];
|
|
117
|
+
for (const s of sels) {
|
|
118
|
+
const el = document.querySelector(s);
|
|
119
|
+
if (el && el.getBoundingClientRect().width > 50)
|
|
120
|
+
return el.getBoundingClientRect().width;
|
|
121
|
+
}
|
|
122
|
+
return 300;
|
|
123
|
+
}""")
|
|
124
|
+
if track_w < 50:
|
|
125
|
+
track_w = 300
|
|
126
|
+
|
|
127
|
+
# âââ Detect gap position âââ
|
|
128
|
+
detected_x = None
|
|
129
|
+
if self.use_ai:
|
|
130
|
+
detected_x = await gemini_detect_gap(page)
|
|
131
|
+
if detected_x is None:
|
|
132
|
+
detected_x = await detect_gap_position(page)
|
|
133
|
+
|
|
134
|
+
base = detected_x if detected_x is not None else track_w * 0.6
|
|
135
|
+
|
|
136
|
+
# âââ Calculate target with retry offset strategy âââ
|
|
137
|
+
if attempt == 0:
|
|
138
|
+
offset = random.uniform(-2, 2)
|
|
139
|
+
elif attempt <= 2:
|
|
140
|
+
offset = random.uniform(-5, 5)
|
|
141
|
+
else:
|
|
142
|
+
offset = random.choice([-15, -10, -5, 5, 10, 15]) + random.uniform(-3, 3)
|
|
143
|
+
target = max(10, min(track_w - 10, base + offset))
|
|
144
|
+
tried_positions.append(target)
|
|
145
|
+
|
|
146
|
+
if detected_x is not None:
|
|
147
|
+
log(f" đ¯ Target: {target:.0f}/{track_w:.0f}px")
|
|
148
|
+
else:
|
|
149
|
+
log(f" đ¯ Fallback target: {target:.0f}/{track_w:.0f}px")
|
|
150
|
+
|
|
151
|
+
log(f" đąī¸ Attempt {attempt + 1}: sliding {target:.0f}/{track_w:.0f}px")
|
|
152
|
+
|
|
153
|
+
# âââ Execute the slide âââ
|
|
154
|
+
await slide_puzzle(page, target, track_w)
|
|
155
|
+
|
|
156
|
+
# âââ Check if captcha is gone âââ
|
|
157
|
+
still = await page.evaluate(f"() => {{ {_CAPTCHA_STILL_SELECTORS} }}")
|
|
158
|
+
if not still:
|
|
159
|
+
log_ok("Captcha solved!")
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
log(f" â ī¸ Captcha still present (attempt {attempt + 1}/{self.max_attempts})")
|
|
163
|
+
|
|
164
|
+
log_err(f"Failed to solve captcha after {self.max_attempts} attempts")
|
|
165
|
+
return False
|