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.
Files changed (39) hide show
  1. qoder_autopilot/__init__.py +59 -0
  2. qoder_autopilot/__main__.py +5 -0
  3. qoder_autopilot/browser/__init__.py +5 -0
  4. qoder_autopilot/browser/camoufox.py +62 -0
  5. qoder_autopilot/browser/window_tiler.py +117 -0
  6. qoder_autopilot/captcha/__init__.py +14 -0
  7. qoder_autopilot/captcha/ai_vision.py +327 -0
  8. qoder_autopilot/captcha/manual.py +95 -0
  9. qoder_autopilot/captcha/opencv_detect.py +184 -0
  10. qoder_autopilot/captcha/slider.py +86 -0
  11. qoder_autopilot/captcha/solver.py +165 -0
  12. qoder_autopilot/cli.py +431 -0
  13. qoder_autopilot/config.py +300 -0
  14. qoder_autopilot/credentials.py +44 -0
  15. qoder_autopilot/deploy.py +338 -0
  16. qoder_autopilot/errors.py +132 -0
  17. qoder_autopilot/first_run.py +107 -0
  18. qoder_autopilot/identity.py +66 -0
  19. qoder_autopilot/logger.py +119 -0
  20. qoder_autopilot/ninerouter.py +105 -0
  21. qoder_autopilot/oauth.py +149 -0
  22. qoder_autopilot/otp.py +67 -0
  23. qoder_autopilot/register.py +383 -0
  24. qoder_autopilot/temp_mail.py +307 -0
  25. qoder_autopilot/user_config.py +171 -0
  26. qoder_autopilot/worker_template/package.json +21 -0
  27. qoder_autopilot/worker_template/schema.sql +24 -0
  28. qoder_autopilot/worker_template/scripts/setup.sh +186 -0
  29. qoder_autopilot/worker_template/src/config.js +6 -0
  30. qoder_autopilot/worker_template/src/handlers/api.js +226 -0
  31. qoder_autopilot/worker_template/src/handlers/email.js +50 -0
  32. qoder_autopilot/worker_template/src/index.js +43 -0
  33. qoder_autopilot/worker_template/src/utils.js +35 -0
  34. qoder_autopilot/worker_template/wrangler.toml.example +19 -0
  35. qoder_autopilot-0.2.1.dist-info/METADATA +299 -0
  36. qoder_autopilot-0.2.1.dist-info/RECORD +39 -0
  37. qoder_autopilot-0.2.1.dist-info/WHEEL +4 -0
  38. qoder_autopilot-0.2.1.dist-info/entry_points.txt +2 -0
  39. 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