""" 使用 Camoufox + Solver 完成注册 思路:从真实注册页开始,自动处理 Turnstile、邮箱验证码和密码设置 """ import os import re import threading import time import requests as std_requests from camoufox.sync_api import Camoufox from config import ( API_KEY_TIMEOUT, EMAIL_CODE_TIMEOUT, LOCAL_SOLVER_URL, REGISTER_HEADLESS, ) from mail_provider import get_email_code, get_verification_link TURNSTILE_SITEKEY = "0x4AAAAAAAQFNSW6xordsuIq" _HERE = os.path.dirname(os.path.abspath(__file__)) _SAVE_FILE = os.path.join(_HERE, "accounts.txt") _SAVE_LOCK = threading.Lock() def extract_signup_url(html): """从登录密码页里提取注册入口""" match = re.search(r'href="(/u/signup/identifier[^"]*)"', html) if not match: return None return f"https://auth.tavily.com{match.group(1)}" def has_unified_identifier_entry(page): """新版 Tavily 已改成登录/注册合一入口,当前页可直接作为注册起点。""" selectors = [ 'input[name="username"]', 'input[name="email"]', 'input[type="email"]', ] has_email_input = any(page.query_selector(selector) for selector in selectors) has_continue = any( page.query_selector(selector) for selector in ( 'button[type="submit"]', 'button:has-text("Continue")', 'button:has-text("Sign Up")', ) ) return has_email_input and has_continue def fill_first_input(page, selectors, value): """填充第一个存在的输入框""" for selector in selectors: if page.query_selector(selector): page.fill(selector, value) return selector return None def close_marketing_dialog(page): """关闭首页营销弹窗,避免遮挡 API Key 区域""" close_button = page.query_selector('button[aria-label="Close"]') if close_button: close_button.click() time.sleep(1) def extract_api_key(page): """从当前页面 HTML 里提取明文 API Key""" html = page.content() api_key_matches = re.findall(r'tvly-[a-zA-Z0-9_-]{20,}', html) api_keys = [k for k in api_key_matches if k != "tvly-YOUR_API_KEY"] if not api_keys: return None return max(api_keys, key=len) def wait_for_api_key(page, timeout=20): """等待首页 API Key 模块渲染出明文 key""" start_time = time.time() while time.time() - start_time < timeout: close_marketing_dialog(page) api_key = extract_api_key(page) if api_key: return api_key time.sleep(1) return None def save_account(email, password, api_key): """并发注册时串行写入 accounts.txt,避免落盘交叉。""" with _SAVE_LOCK: with open(_SAVE_FILE, 'a', encoding='utf-8') as f: f.write(f"{email},{password},{api_key}\n") def verify_api_key(api_key, timeout=30): """真实调用 Tavily API,验证新 key 可用。""" try: response = std_requests.post( "https://api.tavily.com/search", json={ "api_key": api_key, "query": "api key verification", "max_results": 1, }, timeout=timeout, ) except Exception as exc: print(f"❌ API Key 调用测试失败: {exc}") return False if response.status_code == 200: print("✅ API Key 调用测试通过") return True preview = response.text.strip().replace("\n", " ")[:160] print(f"❌ API Key 调用测试失败: HTTP {response.status_code}") if preview: print(f" 响应: {preview}") return False def submit_primary_action(page, input_selector=None): """优先提交默认 Continue 动作,避免误点 Resend / Go back""" button_selectors = [ 'button[data-action-button-primary="true"]', 'button[type="submit"][name="action"][value="default"]:not([aria-hidden="true"])', 'button[type="submit"]:not([aria-hidden="true"])', ] for selector in button_selectors: if page.query_selector(selector): try: page.click(selector, no_wait_after=True, timeout=3000) return True except Exception: continue if input_selector and page.query_selector(input_selector): try: page.press(input_selector, 'Enter') return True except Exception: return False return False def extract_page_feedback(page): """提取页面上的高价值提示,便于定位卡点。""" selectors = [ '[role="alert"]', '[data-error-visible="true"]', '.ulp-input-error-message', '.auth0-global-message', '.cf-turnstile-error', ] messages = [] for selector in selectors: for node in page.query_selector_all(selector): text = (node.inner_text() or "").strip() if text and text not in messages: messages.append(text) return " | ".join(messages) def print_feedback_hint(feedback): """针对常见失败原因补一条更直接的提示。""" if not feedback: return lowered = feedback.lower() if "suspicious activity detected" in lowered: print(" 提示: Tavily 当前会拦截公开 DuckMail 域名,建议改用私有 DuckMail 域名/API Key 或 Cloudflare。") elif "security challenge" in lowered: print(" 提示: 当前卡在密码页二次安全挑战,程序已尝试自动重提,但目标站仍可触发额外风控。") def wait_for_post_signup_target(page, timeout): """等待注册后跳转到 Tavily 或验证页。""" deadline = time.time() + (timeout / 1000) while time.time() < deadline: current_url = page.url.lower() if "app.tavily.com" in current_url or "/verify" in current_url or "/continue" in current_url: return True time.sleep(0.5) return False def normalize_feedback(feedback): """统一页面提示文案,方便做关键字判断。""" return (feedback or "").replace("’", "'").strip().lower() def get_turnstile_sitekey(page): """优先从当前页面提取 sitekey,拿不到再回退默认值。""" try: sitekey = page.evaluate( """ () => { const node = document.querySelector( '[data-captcha-sitekey], .cf-turnstile, [data-sitekey]' ); if (!node) { return ''; } return ( node.getAttribute('data-captcha-sitekey') || node.getAttribute('data-sitekey') || '' ); } """ ) except Exception: sitekey = "" if sitekey: return sitekey.strip() html = page.content() match = re.search( r'(?:data-captcha-sitekey|data-sitekey)=["\']([^"\']+)["\']', html, re.IGNORECASE, ) if match: return match.group(1).strip() return TURNSTILE_SITEKEY def collect_turnstile_state(page): """收集密码页 challenge 相关 DOM 状态,便于判断恢复策略。""" try: state = page.evaluate( """ () => { const passwordInput = document.querySelector('input[name="password"]'); const widget = document.querySelector( 'div[data-captcha-sitekey], .cf-turnstile, [data-sitekey]' ); const iframe = document.querySelector( 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' ); const captchaInput = document.querySelector( 'input[name="captcha"], input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"]' ); return { hasCaptchaDiv: !!widget, hasChallengeIframe: !!iframe, hasCaptchaInput: !!captchaInput, hasTurnstile: typeof window.turnstile !== 'undefined', hasPasswordInput: !!passwordInput, passwordValueLength: passwordInput ? passwordInput.value.length : 0, sitekey: widget ? (widget.getAttribute('data-captcha-sitekey') || widget.getAttribute('data-sitekey') || '') : '', }; } """ ) except Exception: state = {} return { "hasCaptchaDiv": bool(state.get("hasCaptchaDiv")), "hasChallengeIframe": bool(state.get("hasChallengeIframe")), "hasCaptchaInput": bool(state.get("hasCaptchaInput")), "hasTurnstile": bool(state.get("hasTurnstile")), "hasPasswordInput": bool(state.get("hasPasswordInput")), "passwordValueLength": int(state.get("passwordValueLength") or 0), "sitekey": (state.get("sitekey") or "").strip(), } def has_password_challenge_signal(feedback=None, state=None): """判断当前是否出现了密码页安全挑战。""" lowered = normalize_feedback(feedback) if any( keyword in lowered for keyword in ( "security challenge", "captcha", "turnstile", "cloudflare", "couldn't load the security challenge", ) ): return True state = state or {} return any( ( state.get("hasCaptchaDiv"), state.get("hasChallengeIframe"), state.get("hasCaptchaInput"), state.get("hasTurnstile"), ) ) def format_turnstile_state(state): """压缩输出 challenge 状态,便于终端观察。""" return ( f"captchaDiv={'Y' if state.get('hasCaptchaDiv') else 'N'}, " f"iframe={'Y' if state.get('hasChallengeIframe') else 'N'}, " f"input={'Y' if state.get('hasCaptchaInput') else 'N'}, " f"turnstile={'Y' if state.get('hasTurnstile') else 'N'}, " f"pwdLen={state.get('passwordValueLength', 0)}" ) def refill_password(page, password): """挑战失败后密码输入框经常会被清空,重填一次。""" selector = 'input[name="password"]' if not page.query_selector(selector): return False page.fill(selector, password) return True def refresh_password_page_if_needed(page, feedback, state): """如果挑战脚本没加载出来,刷新当前密码页拿一遍新的 DOM。""" lowered = normalize_feedback(feedback) if "couldn't load the security challenge" not in lowered: return False # 新版 Auth0 页面经常会先渲染占位 captcha div / hidden input, # 但真正的 Cloudflare iframe 还没挂起来;这种情况也需要刷新。 if state.get("hasChallengeIframe") or state.get("hasTurnstile"): return False print("🔄 检测到安全挑战加载失败,刷新密码页后重试...") try: page.reload(wait_until="networkidle", timeout=30000) page.wait_for_selector('input[name="password"]', timeout=15000) time.sleep(2) return True except Exception as exc: print(f"⚠️ 刷新密码页失败: {exc}") return False def wait_for_password_challenge_ready(page, timeout=8): """等待密码页的 Cloudflare challenge 尽量挂起。""" deadline = time.time() + timeout latest_state = {} while time.time() < deadline: latest_state = collect_turnstile_state(page) if latest_state.get("hasChallengeIframe") or latest_state.get("hasTurnstile"): return latest_state time.sleep(0.5) return latest_state def ensure_password_challenge_ready(page): """密码页刚出现时先给 challenge 一个渲染窗口,必要时主动刷新一次。""" state = wait_for_password_challenge_ready(page, timeout=6) if not state.get("hasPasswordInput"): return state if state.get("hasChallengeIframe") or state.get("hasTurnstile"): return state if state.get("hasCaptchaDiv") or state.get("hasCaptchaInput"): print("🔄 密码页 challenge 未完全渲染,先刷新一次页面...") try: page.reload(wait_until="networkidle", timeout=30000) page.wait_for_selector('input[name="password"]', timeout=15000) time.sleep(2) state = wait_for_password_challenge_ready(page, timeout=6) except Exception as exc: print(f"⚠️ 密码页预刷新失败: {exc}") return state def recover_password_challenge(page, password, max_attempts=3): """密码页未跳转时,尝试等待 challenge 渲染并恢复提交流程。""" print("⚠️ 密码页未完成跳转,开始处理安全挑战...") for attempt in range(1, max_attempts + 1): if wait_for_post_signup_target(page, timeout=5000): return True time.sleep(2) feedback = extract_page_feedback(page) state = collect_turnstile_state(page) print(f"🔁 密码页恢复尝试 {attempt}/{max_attempts}") print(f" DOM: {format_turnstile_state(state)}") if feedback: print(f" 提示: {feedback}") if wait_for_post_signup_target(page, timeout=2000): return True if refresh_password_page_if_needed(page, feedback, state): feedback = extract_page_feedback(page) state = collect_turnstile_state(page) print(f" 刷新后 DOM: {format_turnstile_state(state)}") if feedback: print(f" 刷新后提示: {feedback}") if wait_for_post_signup_target(page, timeout=2000): return True if has_password_challenge_signal(feedback, state): sitekey = state.get("sitekey") or get_turnstile_sitekey(page) print(f"🔐 尝试恢复 Turnstile challenge (sitekey={sitekey})") token = solve_turnstile(page.url, sitekey=sitekey) if token: if inject_turnstile_token(page, token): print("✅ 已注入密码页 challenge token") else: print("⚠️ Token 已拿到,但页面未确认注入成功,继续重提") else: print("⚠️ 密码页 challenge token 获取失败,继续执行普通重提") else: print("⏳ 未检测到显式 challenge DOM,执行延迟重提") if not refill_password(page, password): if wait_for_post_signup_target(page, timeout=5000): return True print("❌ 密码输入框丢失,无法继续恢复") return False time.sleep(1) submit_primary_action(page, 'input[name="password"]') time.sleep(4) return wait_for_post_signup_target(page, timeout=5000) def submit_password_with_recovery(page, password): """提交密码,并在随机 challenge 场景下自动恢复。""" initial_state = ensure_password_challenge_ready(page) if initial_state: print(f"🔍 密码页 challenge 状态: {format_turnstile_state(initial_state)}") if not refill_password(page, password): return False time.sleep(1) submit_primary_action(page, 'input[name="password"]') time.sleep(5) if wait_for_post_signup_target(page, timeout=15000): return True return recover_password_challenge(page, password) def solve_turnstile(url, sitekey=TURNSTILE_SITEKEY): """调用 Solver 获取 token""" try: r = std_requests.get( f"{LOCAL_SOLVER_URL}/turnstile", params={"url": url, "sitekey": sitekey or TURNSTILE_SITEKEY}, timeout=10 ) if r.status_code != 200: print(f"❌ Solver 请求失败: {r.status_code}") return None task_id = r.json().get("taskId") if not task_id: print("❌ 未获取到 Task ID") return None # 轮询结果 for i in range(60): time.sleep(2) res = std_requests.get( f"{LOCAL_SOLVER_URL}/result", params={"id": task_id}, timeout=10 ) if res.status_code == 200: data = res.json() status = data.get("status") if status == "ready": token = data.get("solution", {}).get("token") if token: return token elif status == "CAPTCHA_FAIL": return None return None except Exception as e: print(f"❌ Solver 异常: {e}") return None def inject_turnstile_token(page, token): """注入 Turnstile token 到页面""" safe_token = token.replace("\\", "\\\\").replace("'", "\\'") script = f""" (function() {{ const token = '{safe_token}'; const form = document.querySelector('form') || document.body; const names = ['captcha', 'cf-turnstile-response']; const ensureField = (name) => {{ let field = document.querySelector(`input[name="${{name}}"], textarea[name="${{name}}"]`); if (field) {{ return field; }} field = document.createElement(name.includes('response') ? 'textarea' : 'input'); if (field.tagName === 'INPUT') {{ field.type = 'hidden'; }} field.name = name; form.appendChild(field); return field; }}; names.forEach((name) => {{ const field = ensureField(name); field.value = token; field.dispatchEvent(new Event('input', {{ bubbles: true }})); field.dispatchEvent(new Event('change', {{ bubbles: true }})); }}); if (typeof window._turnstileTokenCallback === 'function') {{ window._turnstileTokenCallback(token); }} if (typeof window.turnstileCallback === 'function') {{ window.turnstileCallback(token); }} return true; }})(); """ return page.evaluate(script) def register_with_browser_solver(email, password): """使用浏览器 + Solver 注册""" print(f"🌐 使用浏览器模式注册: {email}") try: with Camoufox(headless=REGISTER_HEADLESS) as browser: page = browser.new_page() # 1. 访问 Tavily 统一登录/注册入口 page.goto("https://app.tavily.com/sign-in", wait_until="networkidle", timeout=30000) time.sleep(2) signup_url = extract_signup_url(page.content()) if signup_url: print("🧭 进入注册页...") page.goto(signup_url, wait_until="networkidle", timeout=30000) time.sleep(2) elif has_unified_identifier_entry(page): print("ℹ️ 检测到 Tavily 新版登录/注册合一入口,直接复用当前页面...") else: print(f"❌ 未找到注册入口: {page.url}") return None email_selector = fill_first_input(page, ['input[name="email"]', 'input[name="username"]'], email) if not email_selector: print("❌ 注册页未找到邮箱输入框") return None # 2. 注册页 Turnstile print("🔐 处理注册页 Turnstile...") token1 = solve_turnstile(page.url, sitekey=get_turnstile_sitekey(page)) if not token1: print("❌ Token 获取失败") return None print(f"✅ Token: {token1[:50]}...") if inject_turnstile_token(page, token1): print("✅ Token 已注入") else: print("⚠️ 未找到 captcha 输入框") # 3. 提交邮箱,进入验证码页 submit_primary_action(page, email_selector) time.sleep(6) try: page.wait_for_selector('input[name="code"], input[name="password"]', timeout=15000) except: print("⚠️ 首次提交后未跳转,重试点击 Continue...") submit_primary_action(page) time.sleep(3) try: page.wait_for_selector('input[name="code"], input[name="password"]', timeout=20000) except: feedback = extract_page_feedback(page) print(f"❌ 未进入验证码/密码页面: {page.url}") if feedback: print(f" 页面提示: {feedback}") print_feedback_hint(feedback) return None if page.query_selector('input[name="code"]'): print("✅ 到达邮箱验证码页") code = get_email_code(email, timeout=EMAIL_CODE_TIMEOUT) if not code: return None page.fill('input[name="code"]', code) submit_primary_action(page, 'input[name="code"]') time.sleep(3) # 4. 进入密码页 try: page.wait_for_selector('input[name="password"]', timeout=30000) print("✅ 到达注册密码页") except: print(f"❌ 未到达注册密码页: {page.url}") return None # 5. 设置密码 if not submit_password_with_recovery(page, password): feedback = extract_page_feedback(page) print(f"❌ 登录失败: {page.url}") if feedback: print(f" 页面提示: {feedback}") print_feedback_hint(feedback) return None print("✅ 登录成功") # 7. 检查是否还需要额外验证 time.sleep(3) if 'verify' in page.url.lower(): print("📧 需要邮件验证") verify_url = get_verification_link(email, timeout=60) if not verify_url: return None page.goto(verify_url, wait_until="networkidle", timeout=60000) page.wait_for_url("**/app.tavily.com/**", timeout=60000) time.sleep(3) # 8. 获取 API Key print("🔑 获取 API Key...") time.sleep(3) api_key = wait_for_api_key(page, timeout=API_KEY_TIMEOUT) if not api_key: print("⚠️ 未找到 API Key") return None print("🧪 验证 API Key 可用性...") if not verify_api_key(api_key): return None save_account(email, password, api_key) print(f"🎉 注册成功") print(f" 邮箱: {email}") print(f" 密码: {password}") print(f" Key : {api_key}") return api_key except Exception as e: print(f"❌ 注册失败: {e}") return None if __name__ == "__main__": from tavily_core import create_email email, password = create_email() register_with_browser_solver(email, password)