This commit is contained in:
huangzhenpc
2025-03-27 10:20:06 +08:00
parent 6bcfedeb04
commit cc2a3a34e3
21 changed files with 1791 additions and 0 deletions

61
.gitignore vendored Normal file
View File

@@ -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

38
config.yaml Normal file
View File

@@ -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

14
core/__init__.py Normal file
View File

@@ -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'
]

89
core/config.py Normal file
View File

@@ -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
)

86
core/database.py Normal file
View File

@@ -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()

23
core/exceptions.py Normal file
View File

@@ -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

42
core/logger.py Normal file
View File

@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
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="<blue>")
logger.level("INFO", color="<white>")
logger.level("SUCCESS", color="<green>")
logger.level("WARNING", color="<yellow>")
logger.level("ERROR", color="<red>")
return logger

61
import_emails.py Normal file
View File

@@ -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())

182
main.py Normal file
View File

@@ -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())

6
register/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from .register_worker import FormBuilder, RegisterWorker
__all__ = [
'RegisterWorker',
'FormBuilder'
]

404
register/register_worker.py Normal file
View File

@@ -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

27
requirements.txt Normal file
View File

@@ -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

11
services/__init__.py Normal file
View File

@@ -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'
]

93
services/capsolver.py Normal file
View File

@@ -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("验证码解决失败: 达到最大重试次数或超时")

245
services/email_manager.py Normal file
View File

@@ -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'<div[^>]*>(\d{6})</div>', 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

46
services/fetch_manager.py Normal file
View File

@@ -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'}

80
services/fetch_service.py Normal file
View File

@@ -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)}

38
services/proxy_pool.py Normal file
View File

@@ -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返回格式实现
...

96
services/token_pool.py Normal file
View File

@@ -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 []

32
services/uuid.py Normal file
View File

@@ -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)

117
services/yescaptcha.py Normal file
View File

@@ -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