diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01e39bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Python忽略项 +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +.pytest_cache/ +.coverage +htmlcov/ +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.idea/ +# Node.js忽略项 +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm + +.yarn-integrity +.env +.next +.nuxt +.cache +dist/ +coverage/ + +# IDE相关 +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store +Thumbs.db + +cursor.db +logs/ +__pycache__/ +*.pyc +*.log +email.txt \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..bbba9f3 --- /dev/null +++ b/config.yaml @@ -0,0 +1,38 @@ +# 全局配置 +global: + max_concurrency: 20 + timeout: 30 + retry_times: 3 + +# 数据库配置 +database: + path: "cursor.db" + pool_size: 10 + +# 代理配置 +proxy: + api_url: "https://api.proxy.com/getProxy" + batch_size: 100 + check_interval: 300 + +# 注册配置 +register: + delay_range: [1, 2] + batch_size: 1 +#这里是注册的并发数量 + +# 邮件配置 +email: + file_path: "email.txt" + +captcha: + provider: "capsolver" # 可选值: "capsolver" 或 "yescaptcha" + capsolver: + api_key: "CAP-E0A11882290AC7ADE2F799286B8E2DA497D7CD0510BFA477F3900507809F8AA3" + website_url: "https://authenticator.cursor.sh" + website_key: "0x4AAAAAAAMNIvC45A4Wjjln" + yescaptcha: + client_key: "a5ef0062c1d2674900e78722c5670e3a3484bc8c64273" + website_url: "https://authenticator.cursor.sh" + website_key: "a5ef0062c1d2674900e78722c5670e3a3484bc8c64273" + use_cn_server: false diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..b56e535 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,14 @@ +from .config import Config +from .exceptions import (CursorRegisterException, EmailError, ProxyFetchError, + RegisterError, TokenGenerationError) + +__version__ = "1.0.0" + +__all__ = [ + 'Config', + 'CursorRegisterException', + 'TokenGenerationError', + 'ProxyFetchError', + 'RegisterError', + 'EmailError' +] diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..4c6434d --- /dev/null +++ b/core/config.py @@ -0,0 +1,89 @@ +from dataclasses import dataclass +from typing import Tuple + +import yaml + + +@dataclass +class GlobalConfig: + max_concurrency: int + timeout: int + retry_times: int + + +@dataclass +class DatabaseConfig: + path: str + pool_size: int + + +@dataclass +class ProxyConfig: + api_url: str + batch_size: int + check_interval: int + + +@dataclass +class RegisterConfig: + delay_range: Tuple[int, int] + batch_size: int + + +@dataclass +class EmailConfig: + file_path: str + + +@dataclass +class CapsolverConfig: + api_key: str + website_url: str + website_key: str + + +@dataclass +class YesCaptchaConfig: + client_key: str + website_url: str + website_key: str + use_cn_server: bool + + +@dataclass +class CaptchaConfig: + provider: str + capsolver: CapsolverConfig + yescaptcha: YesCaptchaConfig + + +@dataclass +class Config: + global_config: GlobalConfig + database_config: DatabaseConfig + proxy_config: ProxyConfig + register_config: RegisterConfig + email_config: EmailConfig + captcha_config: CaptchaConfig + + @classmethod + def from_yaml(cls, path: str = "config.yaml"): + with open(path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) + + # 创建 captcha 配置对象 + captcha_data = data['captcha'] + captcha_config = CaptchaConfig( + provider=captcha_data['provider'], + capsolver=CapsolverConfig(**captcha_data['capsolver']), + yescaptcha=YesCaptchaConfig(**captcha_data['yescaptcha']) + ) + + return cls( + global_config=GlobalConfig(**data['global']), + database_config=DatabaseConfig(**data['database']), + proxy_config=ProxyConfig(**data['proxy']), + register_config=RegisterConfig(**data['register']), + email_config=EmailConfig(**data['email']), + captcha_config=captcha_config + ) diff --git a/core/database.py b/core/database.py new file mode 100644 index 0000000..20a2125 --- /dev/null +++ b/core/database.py @@ -0,0 +1,86 @@ +import asyncio +from contextlib import asynccontextmanager +from typing import Any, List, Optional + +import aiosqlite +from loguru import logger + +from core.config import Config + + +class DatabaseManager: + def __init__(self, config: Config): + self.db_path = config.database_config.path + self._pool_size = config.database_config.pool_size + self._pool: List[aiosqlite.Connection] = [] + self._pool_lock = asyncio.Lock() + + async def initialize(self): + """初始化数据库连接池""" + logger.info("初始化数据库连接池") + async with aiosqlite.connect(self.db_path) as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS email_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + client_id TEXT NOT NULL, + refresh_token TEXT NOT NULL, + in_use BOOLEAN DEFAULT 0, + cursor_password TEXT, + cursor_cookie TEXT, + sold BOOLEAN DEFAULT 0, + status TEXT DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + await db.commit() + + # 初始化连接池 + for i in range(self._pool_size): + conn = await aiosqlite.connect(self.db_path) + self._pool.append(conn) + logger.info(f"数据库连接池初始化完成,大小: {self._pool_size}") + + async def cleanup(self): + """清理数据库连接""" + for conn in self._pool: + await conn.close() + self._pool.clear() + + @asynccontextmanager + async def get_connection(self): + """获取数据库连接""" + async with self._pool_lock: + if not self._pool: + conn = await aiosqlite.connect(self.db_path) + else: + conn = self._pool.pop() + + try: + yield conn + finally: + if len(self._pool) < self._pool_size: + self._pool.append(conn) + else: + await conn.close() + + async def execute(self, query: str, params: tuple = ()) -> Any: + """执行SQL语句""" + async with self.get_connection() as conn: + cursor = await conn.execute(query, params) + await conn.commit() + return cursor.lastrowid + + async def fetch_one(self, query: str, params: tuple = ()) -> Optional[tuple]: + """查询单条记录""" + async with self.get_connection() as conn: + cursor = await conn.execute(query, params) + return await cursor.fetchone() + + async def fetch_all(self, query: str, params: tuple = ()) -> List[tuple]: + """查询多条记录""" + async with self.get_connection() as conn: + cursor = await conn.execute(query, params) + return await cursor.fetchall() \ No newline at end of file diff --git a/core/exceptions.py b/core/exceptions.py new file mode 100644 index 0000000..af98c5c --- /dev/null +++ b/core/exceptions.py @@ -0,0 +1,23 @@ +class CursorRegisterException(Exception): + """基础异常类""" + pass + + +class TokenGenerationError(CursorRegisterException): + """Token生成失败""" + pass + + +class ProxyFetchError(CursorRegisterException): + """代理获取失败""" + pass + + +class RegisterError(CursorRegisterException): + """注册失败""" + pass + + +class EmailError(CursorRegisterException): + """邮件处理错误""" + pass diff --git a/core/logger.py b/core/logger.py new file mode 100644 index 0000000..36a2bb8 --- /dev/null +++ b/core/logger.py @@ -0,0 +1,42 @@ +import sys +from pathlib import Path + +from loguru import logger + +from core.config import Config + + +def setup_logger(config: Config): + """配置日志系统""" + # 移除默认的处理器 + logger.remove() + + # 添加控制台处理器,改为 DEBUG 级别 + logger.add( + sys.stdout, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + level="DEBUG" # 修改为 DEBUG + ) + + # 创建日志目录 + log_dir = Path("logs") + log_dir.mkdir(exist_ok=True) + + # 文件处理器保持 DEBUG 级别 + logger.add( + "logs/cursor_{time:YYYY-MM-DD}.log", + rotation="00:00", # 每天轮换 + retention="7 days", # 保留7天 + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + level="DEBUG", + encoding="utf-8" + ) + + # 设置一些常用的日志格式 + logger.level("DEBUG", color="") + logger.level("INFO", color="") + logger.level("SUCCESS", color="") + logger.level("WARNING", color="") + logger.level("ERROR", color="") + + return logger diff --git a/import_emails.py b/import_emails.py new file mode 100644 index 0000000..88b2894 --- /dev/null +++ b/import_emails.py @@ -0,0 +1,61 @@ +import asyncio + +import aiosqlite +from loguru import logger + +from core.config import Config + + +async def import_emails(config: Config, file_path: str): + """导入邮箱账号到数据库""" + DEFAULT_CLIENT_ID = "9e5f94bc-e8a4-4e73-b8be-63364c29d753" + + async with aiosqlite.connect(config.database_config.path) as db: + # 创建表 + await db.execute(''' + CREATE TABLE IF NOT EXISTS email_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + client_id TEXT NOT NULL, + refresh_token TEXT NOT NULL, + in_use BOOLEAN DEFAULT 0, + cursor_password TEXT, + cursor_cookie TEXT, + cursor_token TEXT, + sold BOOLEAN DEFAULT 0, + status TEXT DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 读取文件并导入数据 + count = 0 + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + if line.strip(): + try: + email, password, client_id, refresh_token = line.strip().split('----') + await db.execute(''' + INSERT INTO email_accounts ( + email, password, client_id, refresh_token, status + ) VALUES (?, ?, ?, ?, 'pending') + ''', (email, password, client_id, refresh_token)) + count += 1 + except aiosqlite.IntegrityError: + logger.warning(f"重复的邮箱: {email}") + except ValueError: + logger.error(f"无效的数据行: {line.strip()}") + + await db.commit() + logger.success(f"成功导入 {count} 个邮箱账号") + + +async def main(): + config = Config.from_yaml() + await import_emails(config, "email.txt") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..61aafe4 --- /dev/null +++ b/main.py @@ -0,0 +1,182 @@ +import asyncio +import sys + +# Windows平台特殊处理,强制使用SelectorEventLoop +if sys.platform.startswith('win'): + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +from typing import Dict, List + +from core.config import Config +from core.database import DatabaseManager +from core.logger import setup_logger +from register.register_worker import RegisterWorker +from services.email_manager import EmailManager +from services.fetch_manager import FetchManager +from services.proxy_pool import ProxyPool +from services.token_pool import TokenPool + + +class CursorRegister: + def __init__(self): + self.config = Config.from_yaml() + self.logger = setup_logger(self.config) + self.db_manager = DatabaseManager(self.config) + self.fetch_manager = FetchManager(self.config) + self.proxy_pool = ProxyPool(self.config, self.fetch_manager) + self.token_pool = TokenPool(self.config) + self.email_manager = EmailManager(self.config, self.db_manager) + self.register_worker = RegisterWorker( + self.config, + self.fetch_manager, + self.email_manager + ) + + async def initialize(self): + """初始化数据库""" + await self.db_manager.initialize() + + async def cleanup(self): + """清理资源""" + await self.db_manager.cleanup() + + async def batch_register(self, num: int): + """批量注册""" + try: + self.logger.info(f"开始批量注册 {num} 个账号") + + # 1. 先获取token对 + token_pairs = await self.token_pool.batch_generate(num) + if not token_pairs: + self.logger.error("获取token失败,终止注册") + return [] + + actual_num = len(token_pairs) # 根据实际获取到的token对数量调整注册数量 + if actual_num < num: + self.logger.warning(f"只获取到 {actual_num} 对token,将减少注册数量") + num = actual_num + + # 2. 获取邮箱账号 + email_accounts = await self.email_manager.batch_get_accounts(num) + if len(email_accounts) < num: + self.logger.warning(f"可用邮箱账号不足,仅获取到 {len(email_accounts)} 个") + num = len(email_accounts) + + # 3. 获取代理 + proxies = await self.proxy_pool.batch_get(num) + + # 在关键位置添加详细日志 + self.logger.debug(f"代理列表: {proxies}") + self.logger.debug(f"邮箱账号: {[a.email for a in email_accounts]}") + self.logger.debug(f"尝试使用的token对数量: {len(token_pairs)}") + + # 4. 创建注册任务 + tasks = [] + for account, proxy, token_pair in zip(email_accounts, proxies, token_pairs): + task = self.register_worker.register(proxy, token_pair, account) + tasks.append(task) + + # 5. 并发执行 + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 6. 处理结果 + successful = [] + failed = [] + skipped = 0 + + for i, result in enumerate(results): + if isinstance(result, Exception): + self.logger.error(f"注册任务 {i+1} 失败: {str(result)}") + failed.append(str(result)) + elif result is None: # 跳过的账号 + skipped += 1 + else: + try: + # 更新数据库 + await self.email_manager.update_account( + result['account_id'], + result['cursor_password'], + result['cursor_cookie'], + result['cursor_jwt'] + ) + self.logger.debug(f"更新数据库成功 - 账号ID: {result['account_id']}") + successful.append(result) + except Exception as e: + self.logger.error(f"更新数据库失败 - 账号ID: {result['account_id']}, 错误: {str(e)}") + failed.append(str(e)) + + self.logger.info(f"注册完成: 成功 {len(successful)}, 失败 {len(failed)}, 跳过 {skipped}") + return successful + + except Exception as e: + self.logger.error(f"批量注册失败: {str(e)}") + return [] + + async def _process_results(self, results: List[Dict]): + """处理注册结果""" + successful = [] + failed = [] + + for result in results: + if isinstance(result, Exception): + failed.append(str(result)) + else: + # 更新数据库 + await self.email_manager.update_account( + result['account_id'], + result['cursor_password'], + result['cursor_cookie'] + ) + successful.append(result) + + print(f"Successfully registered: {len(successful)}") + print(f"Failed registrations: {len(failed)}") + + return successful + + +async def main(): + register = CursorRegister() + await register.initialize() + + try: + batch_size = register.config.register_config.batch_size + total_registered = 0 + + while True: + # 检查是否还有可用的邮箱账号 + available_accounts = await register.email_manager.batch_get_accounts(1) + if not available_accounts: + register.logger.info("没有更多可用的邮箱账号,注册完成") + break + + # 释放检查用的账号 + await register.email_manager.update_account_status(available_accounts[0].id, 'pending') + + # 执行批量注册 + register.logger.info(f"开始新一轮批量注册,批次大小: {batch_size}") + results = await register.batch_register(batch_size) + + # 统计结果 + successful = len(results) + total_registered += successful + + register.logger.info(f"当前总进度: 已注册 {total_registered} 个账号") + + # 如果本批次注册失败率过高,暂停一段时间 + if successful < batch_size * 0.5: # 成功率低于50% + register.logger.warning("本批次成功率过低,暂停60秒后继续") + await asyncio.sleep(60) + else: + # 正常等待一个较短的时间再继续下一批 + await asyncio.sleep(5) + + except Exception as e: + register.logger.error(f"程序执行出错: {str(e)}") + finally: + register.logger.info(f"程序结束,总共成功注册 {total_registered} 个账号") + await register.cleanup() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/register/__init__.py b/register/__init__.py new file mode 100644 index 0000000..4485b90 --- /dev/null +++ b/register/__init__.py @@ -0,0 +1,6 @@ +from .register_worker import FormBuilder, RegisterWorker + +__all__ = [ + 'RegisterWorker', + 'FormBuilder' +] diff --git a/register/register_worker.py b/register/register_worker.py new file mode 100644 index 0000000..93a3365 --- /dev/null +++ b/register/register_worker.py @@ -0,0 +1,404 @@ +import asyncio +import json +import random +import string +from typing import Optional, Tuple +from urllib.parse import parse_qs, urlparse + +from loguru import logger + +from core.config import Config +from core.exceptions import RegisterError +from services.email_manager import EmailAccount, EmailManager +from services.fetch_manager import FetchManager +from services.uuid import ULID + + +def extract_jwt(cookie_string: str) -> str: + """从cookie字符串中提取JWT token""" + try: + return cookie_string.split(';')[0].split('=')[1].split('%3A%3A')[1] + except Exception as e: + logger.error(f"[错误] 提取JWT失败: {str(e)}") + return "" + +class FormBuilder: + @staticmethod + def _generate_password() -> str: + """生成随机密码 + 规则: 12-16位,包含大小写字母、数字和特殊字符 + """ + length = random.randint(12, 16) + lowercase = string.ascii_lowercase + uppercase = string.ascii_uppercase + digits = string.digits + special = "!@#$%^&*" + + # 确保每种字符至少有一个 + password = [ + random.choice(lowercase), + random.choice(uppercase), + random.choice(digits), + random.choice(special) + ] + + # 填充剩余长度 + all_chars = lowercase + uppercase + digits + special + password.extend(random.choice(all_chars) for _ in range(length - 4)) + + # 打乱顺序 + random.shuffle(password) + return ''.join(password) + + @staticmethod + def _generate_name() -> tuple[str, str]: + """生成随机的名字和姓氏 + Returns: + tuple: (first_name, last_name) + """ + first_names = ["Alex", "Sam", "Chris", "Jordan", "Taylor", "Morgan", "Casey", "Drew", "Pat", "Quinn"] + last_names = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Moore", "Taylor", "Anderson", "Thomas", "Jackson"] + + return ( + random.choice(first_names), + random.choice(last_names) + ) + + @staticmethod + def build_register_form(boundary: str, email: str, token: str) -> tuple[str, str]: + """构建注册表单数据,返回(form_data, password)""" + password = FormBuilder._generate_password() + + fields = { + "1_state": "{\"returnTo\":\"/settings\"}", + "1_redirect_uri": "https://cursor.com/api/auth/callback", + "1_bot_detection_token": token, + "1_first_name": "wa", + "1_last_name": "niu", + "1_email": email, + "1_password": password, + "1_intent": "sign-up", + "0": "[\"$K1\"]" + } + + form_data = [] + for key, value in fields.items(): + form_data.append(f'--{boundary}') + form_data.append(f'Content-Disposition: form-data; name="{key}"') + form_data.append('') + form_data.append(value) + + form_data.append(f'--{boundary}--') + return '\r\n'.join(form_data), password + + @staticmethod + def build_verify_form(boundary: str, email: str, token: str, code: str, pending_token: str) -> str: + """构建验证表单数据""" + fields = { + "1_pending_authentication_token": pending_token, + "1_email": email, + "1_state": "{\"returnTo\":\"/settings\"}", + "1_redirect_uri": "https://cursor.com/api/auth/callback", + "1_bot_detection_token": token, + "1_code": code, + "0": "[\"$K1\"]" + } + + form_data = [] + for key, value in fields.items(): + form_data.append(f'--{boundary}') + form_data.append(f'Content-Disposition: form-data; name="{key}"') + form_data.append('') + form_data.append(value) + + form_data.append(f'--{boundary}--') + return '\r\n'.join(form_data) + + +class RegisterWorker: + def __init__(self, config: Config, fetch_manager: FetchManager, email_manager: EmailManager): + self.config = config + self.fetch_manager = fetch_manager + self.email_manager = email_manager + self.form_builder = FormBuilder() + self.uuid = ULID() + + async def random_delay(self): + delay = random.uniform(*self.config.register_config.delay_range) + await asyncio.sleep(delay) + + @staticmethod + async def _extract_auth_token(response_text: str, email_account: EmailAccount, email_manager: EmailManager) -> str | None: + """从响应文本中提取pending_authentication_token""" + res = response_text.split('\n') + logger.debug(f"开始提取 auth_token,响应行数: {len(res)}") + + # 检查邮箱是否可用 + for line in res: + if '"code":"email_not_available"' in line: + logger.error("不受支持的邮箱") + await email_manager.update_account_status(email_account.id, 'unavailable') + raise RegisterError("Email is not available") + + try: + for i, r in enumerate(res): + if r.startswith('0:'): + logger.debug(f"在第 {i+1} 行找到匹配") + data = json.loads(r.split('0:')[1]) + auth_data = data[1][0][0][1]["children"][1]["children"][1]["children"][1]["children"][0] + params_str = auth_data.split('?')[1] + params_dict = json.loads(params_str) + token = params_dict['pending_authentication_token'] + logger.debug(f"方法2提取成功: {token[:10]}...") + return token + except Exception as e: + logger.error(f"提取token失败: {str(e)}") + logger.debug("响应内容预览:", response_text[:200]) + + return None + + async def register(self, proxy: str, token_pair: Tuple[str, str], email_account: EmailAccount): + """完整的注册流程""" + token1, token2 = token_pair + session_id = self.uuid.generate() + try: + logger.info(f"开始注册账号: {email_account.email}") + + # 第一次注册请求 + try: + email, pending_token, cursor_password = await self._first_register( + proxy, + token1, + email_account.email, + email_account, + session_id=session_id + ) + except RegisterError as e: + if "Email is not available" in str(e): + logger.warning(f"邮箱 {email_account.email} 不受支持,跳过处理") + return None + raise e + + # 获取验证码的同时,可以开始准备下一步的操作 + verification_code_task = self._get_verification_code_with_retry( + email_account.email, + email_account.refresh_token, + email_account.client_id, + max_retries=3 + ) + + # 等待验证码 + verification_code = await verification_code_task + if not verification_code: + logger.error(f"账号 {email_account.email} 获取验证码失败") + await self.email_manager.update_account_status(email_account.id, 'failed') + raise RegisterError("Failed to get verification code") + + logger.debug(f"邮箱 {email_account.email} 获取到验证码: {verification_code}") + + await self.random_delay() + + # 验证码验证 + redirect_url = await self._verify_code( + proxy=proxy, + token=token2, + code=verification_code, + pending_token=pending_token, + email=email, + session_id=session_id + ) + + if not redirect_url: + raise RegisterError("No redirect URL found") + + await self.random_delay() + + # callback请求 + cookies = await self._callback(proxy, redirect_url) + if not cookies: + raise RegisterError("Failed to get cookies") + + logger.success(f"账号 {email_account.email} 注册成功") + return { + 'account_id': email_account.id, + 'cursor_password': cursor_password, + 'cursor_cookie': cookies, + 'cursor_jwt': extract_jwt(cookies) + } + + except Exception as e: + logger.error(f"账号 {email_account.email} 注册失败: {str(e)}") + if not str(e).startswith("Email is not available"): + await self.email_manager.update_account_status(email_account.id, 'failed') + raise RegisterError(f"Registration failed: {str(e)}") + + async def _first_register( + self, + proxy: str, + token: str, + email: str, + email_account: EmailAccount, + session_id: str + ) -> tuple[str, str, str]: + """第一次注册请求""" + logger.debug(f"开始第一次注册请求 - 邮箱: {email}, 代理: {proxy}") + + first_name, last_name = self.form_builder._generate_name() + + # 在headers中定义boundary + boundary = "----WebKitFormBoundary2rKlvTagBEhneWi3" + headers = { + "accept": "text/x-component", + "next-action": "770926d8148e29539286d20e1c1548d2aff6c0b9", + "content-type": f"multipart/form-data; boundary={boundary}", + "origin": "https://authenticator.cursor.sh", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin" + } + + params = { + "first_name": first_name, + "last_name": last_name, + "email": email, + "state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D", + "redirect_uri": "https://cursor.com/api/auth/callback", + } + + # 构建form数据 + form_data, cursor_password = self.form_builder.build_register_form(boundary, email, token) + + response = await self.fetch_manager.request( + "POST", + "https://authenticator.cursor.sh/sign-up/password", + headers=headers, + params=params, + data=form_data, + proxy=proxy + ) + + if 'error' in response: + raise RegisterError(f"First register request failed: {response['error']}") + + text = response['body'].decode() + pending_token = await self._extract_auth_token(text, email_account, self.email_manager) + if not pending_token: + raise RegisterError("Failed to extract auth token") + + logger.debug(f"第一次请求完成 - pending_token: {pending_token[:10]}...") + return email, pending_token, cursor_password + + async def _verify_code( + self, + proxy: str, + token: str, + code: str, + pending_token: str, + email: str, + session_id: str + ) -> str: + """验证码验证请求""" + logger.debug(f"开始验证码验证 - 邮箱: {email}, 验证码: {code}") + + boundary = "----WebKitFormBoundaryqEBf0rEYwwb9aUoF" + headers = { + "accept": "text/x-component", + "content-type": f"multipart/form-data; boundary={boundary}", + "next-action": "e75011da58d295bef5aa55740d0758a006468655", + "origin": "https://authenticator.cursor.sh", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" + } + + params = { + "email": email, + "pending_authentication_token": pending_token, + "state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D", + "redirect_uri": "https://cursor.com/api/auth/callback", + "authorization_session_id": session_id + } + + form_data = self.form_builder.build_verify_form( + boundary=boundary, + email=email, + token=token, + code=code, + pending_token=pending_token, + ) + + response = await self.fetch_manager.request( + "POST", + "https://authenticator.cursor.sh/email-verification", + headers=headers, + params=params, + data=form_data, + proxy=proxy + ) + + redirect_url = response.get('headers', {}).get('x-action-redirect') + if not redirect_url: + raise RegisterError("未找到重定向URL,响应头: %s" % json.dumps(response.get('headers'))) + + return redirect_url + + async def _callback(self, proxy: str, redirect_url: str) -> str: + """Callback请求""" + logger.debug(f"开始callback请求 - URL: {redirect_url[:50]}...") + + parsed = urlparse(redirect_url) + code = parse_qs(parsed.query)['code'][0] + logger.debug(f"从URL提取的code: {code[:10]}...") + + headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-language": "zh-CN,zh;q=0.9", + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "cross-site", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" + } + + callback_url = "https://www.cursor.com/api/auth/callback" + params = { + "code": code, + "state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D" + } + + response = await self.fetch_manager.request( + "GET", + callback_url, + headers=headers, + params=params, + proxy=proxy, + allow_redirects=False + ) + + if 'error' in response: + raise RegisterError(f"Callback request failed: {response['error']}") + + cookies = response['headers'].get('set-cookie') + if cookies: + logger.debug("成功获取到cookies") + else: + logger.error("未获取到cookies") + return cookies + + async def _get_verification_code_with_retry(self, email: str, refresh_token: str, client_id: str, max_retries: int = 3) -> Optional[str]: + """带重试的验证码获取""" + for attempt in range(max_retries): + try: + code = await self.email_manager.get_verification_code( + email, refresh_token, client_id + ) + if code: + return code + await asyncio.sleep(2) # 短暂延迟后重试 + except Exception as e: + logger.warning(f"第 {attempt + 1} 次获取验证码失败: {str(e)}") + if attempt == max_retries - 1: # 最后一次尝试 + return None + await asyncio.sleep(2) # 失败后等待更长时间 + return None diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..48dc6cc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +# HTTP related +aiohttp +requests +curl_cffi + +# Email processing +aioimaplib + +# Type hints and data structures +dataclasses +typing + +# Config file processing +pyyaml + +# Async support +asyncio + + +# Utils +python-dateutil + +# Database +aiosqlite + +# Logging +loguru==0.7.2 diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..7e0bd1d --- /dev/null +++ b/services/__init__.py @@ -0,0 +1,11 @@ +from .email_manager import EmailManager +from .fetch_manager import FetchManager +from .proxy_pool import ProxyPool +from .token_pool import TokenPool + +__all__ = [ + 'FetchManager', + 'ProxyPool', + 'TokenPool', + 'EmailManager' +] diff --git a/services/capsolver.py b/services/capsolver.py new file mode 100644 index 0000000..35ca147 --- /dev/null +++ b/services/capsolver.py @@ -0,0 +1,93 @@ +import asyncio +import aiohttp +from loguru import logger +from typing import Optional +import time + +class Capsolver: + def __init__(self, api_key: str, website_url: str, website_key: str): + self.api_key = api_key + self.website_url = website_url + self.website_key = website_key + self.base_url = "https://api.capsolver.com" + + async def create_task(self) -> Optional[str]: + """创建验证码任务""" + async with aiohttp.ClientSession() as session: + payload = { + "clientKey": self.api_key, + "task": { + "type": "AntiTurnstileTaskProxyLess", + "websiteURL": self.website_url, + "websiteKey": self.website_key, + } + } + + async with session.post(f"{self.base_url}/createTask", json=payload) as resp: + result = await resp.json() + if result.get("errorId") > 0: + logger.error(f"创建任务失败: {result.get('errorDescription')}") + return None + return result.get("taskId") + + async def get_task_result(self, task_id: str) -> Optional[dict]: + """获取任务结果""" + async with aiohttp.ClientSession() as session: + payload = { + "clientKey": self.api_key, + "taskId": task_id + } + + async with session.post(f"{self.base_url}/getTaskResult", json=payload) as resp: + result = await resp.json() + if result.get("errorId") > 0: + logger.error(f"获取结果失败: {result.get('errorDescription')}") + return None + + if result.get("status") == "ready": + return result.get("solution", {}) + return None + + async def solve_turnstile(self) -> Optional[str]: + """ + 解决 Turnstile 验证码 + """ + task_id = await self.create_task() + if not task_id: + raise Exception("创建验证码任务失败") + + # 增加重试次数限制和超时时间控制 + max_retries = 5 # 减少最大重试次数 + retry_delay = 2 # 设置重试间隔为2秒 + timeout = 15 # 设置总超时时间为15秒 + + start_time = time.time() + for attempt in range(1, max_retries + 1): + try: + logger.debug(f"第 {attempt} 次尝试获取验证码结果") + result = await self.get_task_result(task_id) + + if result and "token" in result: + token = result["token"] + logger.success(f"成功获取验证码 token: {token[:40]}...") + return token + + # 检查是否超时 + if time.time() - start_time > timeout: + logger.error("验证码请求总时间超过15秒") + break + + await asyncio.sleep(retry_delay) + + except Exception as e: + logger.error(f"获取验证码结果失败: {str(e)}") + if attempt == max_retries: + raise + + if time.time() - start_time > timeout: + logger.error("验证码请求总时间超过15秒") + break + + await asyncio.sleep(retry_delay) + + raise Exception("验证码解决失败: 达到最大重试次数或超时") \ No newline at end of file diff --git a/services/email_manager.py b/services/email_manager.py new file mode 100644 index 0000000..5705c47 --- /dev/null +++ b/services/email_manager.py @@ -0,0 +1,245 @@ +import asyncio +import email +from dataclasses import dataclass +from email.header import decode_header, make_header +from typing import Dict, List, Optional + +import aiohttp +from loguru import logger + +from core.config import Config +from core.database import DatabaseManager +from core.exceptions import EmailError + + +@dataclass +class EmailAccount: + id: int + email: str + password: str # 这里实际上是 refresh_token + client_id: str + refresh_token: str + in_use: bool = False + cursor_password: Optional[str] = None + cursor_cookie: Optional[str] = None + sold: bool = False + status: str = 'pending' # 新增状态字段: pending, unavailable, success + + +class EmailManager: + def __init__(self, config: Config, db_manager: DatabaseManager): + self.config = config + self.db = db_manager + self.verification_subjects = [ + "Verify your email address", + "Complete code challenge", + ] + + async def batch_get_accounts(self, num: int) -> List[EmailAccount]: + """批量获取未使用的邮箱账号""" + logger.info(f"尝试获取 {num} 个未使用的邮箱账号") + + query = ''' + UPDATE email_accounts + SET in_use = 1, updated_at = CURRENT_TIMESTAMP + WHERE id IN ( + SELECT id FROM email_accounts + WHERE in_use = 0 AND sold = 0 AND status = 'pending' + LIMIT ? + ) + RETURNING id, email, password, client_id, refresh_token + ''' + + results = await self.db.fetch_all(query, (num,)) + logger.debug(f"实际获取到 {len(results)} 个账号") + return [ + EmailAccount( + id=row[0], + email=row[1], + password=row[2], + client_id=row[3], + refresh_token=row[4], + in_use=True + ) + for row in results + ] + + async def update_account_status(self, account_id: int, status: str): + """更新账号状态""" + query = ''' + UPDATE email_accounts + SET + status = ?, + in_use = 0, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''' + await self.db.execute(query, (status, account_id)) + + async def update_account(self, account_id: int, cursor_password: str, cursor_cookie: str, cursor_token: str): + """更新账号信息""" + query = ''' + UPDATE email_accounts + SET + cursor_password = ?, + cursor_cookie = ?, + cursor_token = ?, + in_use = 0, + sold = 1, + status = 'success', + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''' + await self.db.execute(query, (cursor_password, cursor_cookie, cursor_token, account_id)) + + async def release_account(self, account_id: int): + """释放账号""" + query = ''' + UPDATE email_accounts + SET in_use = 0, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''' + await self.db.execute(query, (account_id,)) + + async def _get_access_token(self, client_id: str, refresh_token: str) -> str: + """获取微软 access token""" + logger.debug(f"开始获取 access token - client_id: {client_id}") + + url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + data = { + 'client_id': client_id, + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token, + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, data=data) as response: + result = await response.json() + + if 'error' in result: + error = result.get('error') + logger.error(f"获取 access token 失败: {error}") + raise EmailError(f"Failed to get access token: {error}") + + access_token = result['access_token'] + logger.debug("成功获取 access token") + return access_token + + async def get_verification_code(self, email: str, refresh_token: str, client_id: str) -> str: + """获取验证码""" + logger.info(f"开始获取邮箱验证码 - {email}") + try: + # 1. 获取 access token + access_token = await self._get_access_token(client_id, refresh_token) + logger.debug(f"[{email}] 获取 access token 成功") + + # 2. 构建认证字符串 + auth_string = f"user={email}\1auth=Bearer {access_token}\1\1" + logger.debug(f"[{email}] 认证字符串构建完成") + + # 3. 连接邮箱 + import imaplib + mail = imaplib.IMAP4_SSL('outlook.live.com') + mail.authenticate('XOAUTH2', lambda x: auth_string) + mail.select('inbox') + logger.debug(f"[{email}] 邮箱连接成功") + + # 4. 等待并获取验证码邮件 + for i in range(15): + logger.debug(f"[{email}] 第 {i + 1} 次尝试获取验证码") + + # 搜索来自 no-reply@cursor.sh 的最新邮件 + result, data = mail.search(None, '(FROM "no-reply@cursor.sh")') + if result != "OK" or not data[0]: + logger.debug(f"[{email}] 未找到来自 cursor 的邮件,等待1秒后重试") + await asyncio.sleep(1) + continue + + mail_ids = data[0].split() + if not mail_ids: + logger.debug(f"[{email}] 邮件ID列表为空,等待1秒后重试") + await asyncio.sleep(1) + continue + + # 获取最新的3封邮件 + last_mail_ids = sorted(mail_ids, reverse=True)[:3] + + for mail_id in last_mail_ids: + result, msg_data = mail.fetch(mail_id, "(RFC822)") + if result != 'OK': + logger.warning(f"[{email}] 获取邮件内容失败: {result}") + continue + + # 确保 msg_data 不为空且格式正确 + if not msg_data or not msg_data[0] or len(msg_data[0]) < 2: + logger.warning(f"[{email}] 邮件数据格式不正确") + continue + + # 正确导入 email 模块 + from email import message_from_bytes + email_message = message_from_bytes(msg_data[0][1]) + + # 检查发件人 + from_addr = str(make_header(decode_header(email_message['From']))) + if 'no-reply@cursor.sh' not in from_addr: + logger.debug(f"[{email}] 跳过非 Cursor 邮件,发件人: {from_addr}") + continue + + # 检查主题 + subject = str(make_header(decode_header(email_message['SUBJECT']))) + if not any(verify_subject in subject for verify_subject in self.verification_subjects): + logger.debug(f"[{email}] 跳过非验证码邮件,主题: {subject}") + continue + + code = self._extract_code_from_email(email_message) + if code: + logger.debug(f"[{email}] 成功获取验证码: {code}") + mail.close() + mail.logout() + return code + + await asyncio.sleep(1) + + logger.error(f"[{email}] 验证码邮件未收到") + raise EmailError("Verification code not received") + + except Exception as e: + logger.error(f"[{email}] 获取验证码失败: {str(e)}") + raise EmailError(f"Failed to get verification code: {str(e)}") + + def _extract_code_from_email(self, email_message) -> Optional[str]: + """从邮件内容中提取验证码""" + try: + # 获取邮件内容 + if email_message.is_multipart(): + for part in email_message.walk(): + if part.get_content_type() == "text/html": + body = part.get_payload(decode=True).decode('utf-8', errors='ignore') + break + else: + body = email_message.get_payload(decode=True).decode('utf-8', errors='ignore') + + # 提取6位数字验证码 + import re + + # 在HTML中查找包含6位数字的div + match = re.search(r']*>(\d{6})', body) + if match: + code = match.group(1) + logger.debug(f"从HTML中提取到验证码: {code}") + return code + + # 备用方案:搜索任何6位数字 + match = re.search(r'\b\d{6}\b', body) + if match: + code = match.group(0) + logger.debug(f"从文本中提取到验证码: {code}") + return code + + logger.warning(f"[{email}] 未能从邮件中提取到验证码") + logger.debug(f"[{email}] 邮件内容预览: " + body[:200]) + return None + + except Exception as e: + logger.error(f"[{email}] 提取验证码失败: {str(e)}") + return None \ No newline at end of file diff --git a/services/fetch_manager.py b/services/fetch_manager.py new file mode 100644 index 0000000..6dc7061 --- /dev/null +++ b/services/fetch_manager.py @@ -0,0 +1,46 @@ +import asyncio +from typing import Any, Dict, Optional + +from loguru import logger + +from core.config import Config + +from .fetch_service import FetchService + + +class FetchManager: + def __init__(self, config: Config): + self.config = config + self.fetch_service = FetchService() + self.semaphore = asyncio.Semaphore(config.global_config.max_concurrency) + + async def request( + self, + method: str, + url: str, + proxy: Optional[str] = None, + **kwargs + ) -> Dict[str, Any]: + """ + 使用信号量控制并发的请求方法 + """ + async with self.semaphore: + for _ in range(self.config.global_config.retry_times): + try: + response = await self.fetch_service.request( + method=method, + url=url, + proxy=proxy, + timeout=self.config.global_config.timeout, + **kwargs + ) + + if 'error' not in response: + return response + + except asyncio.TimeoutError: + logger.warning(f"请求超时,正在重试: {url}") + continue + + logger.error(f"达到最大重试次数: {url}") + return {'error': 'Max retries exceeded'} \ No newline at end of file diff --git a/services/fetch_service.py b/services/fetch_service.py new file mode 100644 index 0000000..fd78c37 --- /dev/null +++ b/services/fetch_service.py @@ -0,0 +1,80 @@ +from typing import Any, Dict, Optional, Union + +from curl_cffi.requests import AsyncSession +from loguru import logger + + +class FetchService: + def __init__(self): + self.default_headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "Accept": "*/*", + "Accept-Language": "zh-CN,zh;q=0.9", + "Accept-Encoding": "gzip, deflate, br, zstd" + } + + async def request( + self, + method: str, + url: str, + *, + headers: Optional[Dict] = None, + params: Optional[Dict] = None, + data: Optional[Union[Dict, str]] = None, + json: Optional[Dict] = None, + cookies: Optional[Dict] = None, + proxy: Optional[str] = None, + impersonate: str = "chrome124", + **kwargs + ) -> Dict[str, Any]: + """ + 通用请求方法 + + Args: + method: 请求方法 (GET, POST 等) + url: 请求URL + headers: 请求头 + params: URL参数 + data: 表单数据 + json: JSON数据 + cookies: Cookie + proxy: 代理地址 + impersonate: 浏览器仿真类型 + **kwargs: 其他curl_cffi支持的参数 + + Returns: + Dict 包含响应信息 + """ + # 合并默认headers + request_headers = self.default_headers.copy() + if headers: + request_headers.update(headers) + + try: + async with AsyncSession(impersonate=impersonate) as session: + response = await session.request( + method=method, + url=url, + headers=request_headers, + params=params, + data=data, + json=json, + cookies=cookies, + proxies={'http': proxy, 'https': proxy} if proxy else None, + verify=False, + quote=False, + stream=True, + **kwargs + ) + + return { + 'status': response.status_code, + 'headers': dict(response.headers), + 'cookies': dict(response.cookies), + 'body': await response.acontent(), + 'raw_response': response + } + + except Exception as e: + logger.error(f"请求失败: {str(e)}") + return {'error': str(e)} diff --git a/services/proxy_pool.py b/services/proxy_pool.py new file mode 100644 index 0000000..7b58ec9 --- /dev/null +++ b/services/proxy_pool.py @@ -0,0 +1,38 @@ +from typing import List + +from core.config import Config +from core.exceptions import ProxyFetchError + +from .fetch_manager import FetchManager + + +class ProxyPool: + def __init__(self, config: Config, fetch_manager: FetchManager): + self.config = config + self.fetch_manager = fetch_manager + + async def batch_get(self, num: int) -> List[str]: + """获取num个代理""" + # 临时代理 + return ['http://127.0.0.1:3057'] * num + + try: + response = await self.fetch_manager.request( + 'GET', + self.config.proxy_config.api_url.format(num=num) + ) + + if 'error' in response: + raise ProxyFetchError(response['error']) + + # 这里需要根据实际的代理API返回格式进行解析 + proxies = self._parse_proxies(response['body']) + return proxies[:num] + + except Exception as e: + raise ProxyFetchError(f"Failed to fetch proxies: {str(e)}") + + def _parse_proxies(self, response_body: str) -> List[str]: + """解析代理API返回的数据""" + # 需要根据实际API返回格式实现 + ... diff --git a/services/token_pool.py b/services/token_pool.py new file mode 100644 index 0000000..381deb2 --- /dev/null +++ b/services/token_pool.py @@ -0,0 +1,96 @@ +import asyncio +from typing import Any, List, Tuple + +from loguru import logger + +from core.config import Config +from core.exceptions import TokenGenerationError +from services.yescaptcha import TurnstileConfig, YesCaptcha +from services.capsolver import Capsolver + + +class TokenPool: + def __init__(self, config: Config): + self.config = config + + if config.captcha_config.provider == "capsolver": + self.solver = Capsolver( + api_key=config.captcha_config.capsolver.api_key, + website_url=config.captcha_config.capsolver.website_url, + website_key=config.captcha_config.capsolver.website_key + ) + else: + self.turnstile_config = TurnstileConfig( + client_key=config.captcha_config.yescaptcha.client_key, + website_url=config.captcha_config.yescaptcha.website_url, + website_key=config.captcha_config.yescaptcha.website_key, + use_cn_server=config.captcha_config.yescaptcha.use_cn_server + ) + self.solver = YesCaptcha(self.turnstile_config) + + async def _get_token(self) -> str: + """获取单个token""" + try: + if isinstance(self.solver, Capsolver): + # Capsolver 是异步的,直接调用 + token = await self.solver.solve_turnstile() + else: + # YesCaptcha 是同步的,需要转换 + token = await asyncio.to_thread(self.solver.solve_turnstile) + + if not token: + raise TokenGenerationError("Failed to get token") + return token + + except Exception as e: + logger.error(f"获取 token 失败: {str(e)}") + raise TokenGenerationError(f"Failed to get token: {str(e)}") + + async def get_token_pair(self) -> Tuple[str, str]: + """获取一对token""" + token1 = await self._get_token() + token2 = await self._get_token() + return token1, token2 + + async def batch_generate(self, num: int) -> List[Tuple[str, str]]: + """批量生成token对 + + Args: + num: 需要的token对数量 + + Returns: + List[Tuple[str, str]]: token对列表,每个元素是(token1, token2) + """ + logger.info(f"开始批量生成 {num} 对 token") + + # 创建所有token获取任务 + tasks = [] + for _ in range(num * 2): # 每对需要两个token + tasks.append(self._get_token()) + + # 并发执行所有任务 + try: + tokens = await asyncio.gather(*tasks, return_exceptions=True) + + # 过滤出成功的token(仅保留字符串类型) + valid_tokens = [ + token for token in tokens + if isinstance(token, str) and token.startswith('0.') + ] + + # 将token分组为对 + token_pairs = [] + for i in range(0, num * 2, 2): + try: + pair = (valid_tokens[i], valid_tokens[i+1]) + token_pairs.append(pair) + except IndexError: + logger.error(f"生成token对时索引越界,i={i}, tokens数量={len(valid_tokens)}") + break + + logger.success(f"成功生成 {len(token_pairs)} 对 token") + return token_pairs + + except Exception as e: + logger.error(f"批量生成 token 失败: {str(e)}") + return [] diff --git a/services/uuid.py b/services/uuid.py new file mode 100644 index 0000000..b7b6d7b --- /dev/null +++ b/services/uuid.py @@ -0,0 +1,32 @@ +import random +import time + + +class ULID: + def __init__(self): + # 定义字符集,使用Crockford's Base32字符集 + self.encoding = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + + def generate(self) -> str: + # 获取当前时间戳(毫秒) + timestamp = int(time.time() * 1000) + + # 生成随机数部分 + randomness = random.getrandbits(80) # 80位随机数 + + # 转换时间戳为base32字符串(10个字符) + time_chars = [] + for _ in range(10): + timestamp, mod = divmod(timestamp, 32) + time_chars.append(self.encoding[mod]) + time_chars.reverse() + + # 转换随机数为base32字符串(16个字符) + random_chars = [] + for _ in range(16): + randomness, mod = divmod(randomness, 32) + random_chars.append(self.encoding[mod]) + random_chars.reverse() + + # 组合最终结果 + return ''.join(time_chars + random_chars) diff --git a/services/yescaptcha.py b/services/yescaptcha.py new file mode 100644 index 0000000..d2d6fba --- /dev/null +++ b/services/yescaptcha.py @@ -0,0 +1,117 @@ +import time +from dataclasses import dataclass +from typing import Dict, Optional + +import requests +from loguru import logger + + +@dataclass +class TurnstileConfig: + client_key: str + website_url: str + website_key: str + use_cn_server: bool = True + + +class YesCaptcha: + API_URL_GLOBAL = "https://api.yescaptcha.com" + API_URL_CN = "https://cn.yescaptcha.com" + + def __init__(self, config: TurnstileConfig): + self.config = config + self.base_url = self.API_URL_CN if config.use_cn_server else self.API_URL_GLOBAL + logger.debug(f"YesCaptcha 初始化 - 使用{'国内' if config.use_cn_server else '国际'}服务器") + + def create_task(self, task_type: str = "TurnstileTaskProxyless") -> Dict: + """ + Create a new Turnstile solving task + + Args: + task_type: Either "TurnstileTaskProxyless" (25 points) or "TurnstileTaskProxylessM1" (30 points) + + Returns: + Dict containing task ID if successful + """ + url = f"{self.base_url}/createTask" + logger.debug(f"创建验证任务 - 类型: {task_type}") + + payload = { + "clientKey": self.config.client_key, + "task": { + "type": task_type, + "websiteURL": self.config.website_url, + "websiteKey": self.config.website_key + } + } + + response = requests.post(url, json=payload) + result = response.json() + + if result.get("errorId", 1) != 0: + logger.error(f"创建任务失败: {result.get('errorDescription')}") + else: + logger.debug(f"创建任务成功 - TaskID: {result.get('taskId')}") + + return result + + def get_task_result(self, task_id: str) -> Dict: + """ + Get the result of a task + + Args: + task_id: Task ID from create_task + + Returns: + Dict containing task result if successful + """ + url = f"{self.base_url}/getTaskResult" + logger.debug(f"获取任务结果 - TaskID: {task_id}") + + payload = { + "clientKey": self.config.client_key, + "taskId": task_id + } + + response = requests.post(url, json=payload) + result = response.json() + + if result.get("errorId", 1) != 0: + logger.error(f"获取结果失败: {result.get('errorDescription')}") + elif result.get("status") == "ready": + logger.debug("成功获取到结果") + + return result + + def solve_turnstile(self, max_attempts: int = 60) -> Optional[str]: + """ + Complete turnstile solving process + + Args: + max_attempts: Maximum number of attempts to get result + + Returns: + Token string if successful, None otherwise + """ + # 创建任务 + create_result = self.create_task() + if create_result.get("errorId", 1) != 0: + return None + + task_id = create_result.get("taskId") + if not task_id: + return None + + # 轮询获取结果 + for _ in range(max_attempts): + result = self.get_task_result(task_id) + + if result.get("status") == "ready": + return result.get("solution", {}).get("token") + + if result.get("errorId", 1) != 0: + return None + + time.sleep(1) + + return None