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