x
This commit is contained in:
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal 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
38
config.yaml
Normal 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
14
core/__init__.py
Normal 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
89
core/config.py
Normal 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
86
core/database.py
Normal 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
23
core/exceptions.py
Normal 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
42
core/logger.py
Normal 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
61
import_emails.py
Normal 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
182
main.py
Normal 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
6
register/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .register_worker import FormBuilder, RegisterWorker
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'RegisterWorker',
|
||||||
|
'FormBuilder'
|
||||||
|
]
|
||||||
404
register/register_worker.py
Normal file
404
register/register_worker.py
Normal 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
27
requirements.txt
Normal 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
11
services/__init__.py
Normal 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
93
services/capsolver.py
Normal 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
245
services/email_manager.py
Normal 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
46
services/fetch_manager.py
Normal 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
80
services/fetch_service.py
Normal 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
38
services/proxy_pool.py
Normal 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
96
services/token_pool.py
Normal 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
32
services/uuid.py
Normal 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
117
services/yescaptcha.py
Normal 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
|
||||||
Reference in New Issue
Block a user