This commit is contained in:
hkyc
2025-03-31 09:55:54 +08:00
parent 4ef08d7775
commit 4759813f6e
32 changed files with 3879 additions and 0 deletions

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://1ddbeae0f7a67106fd58:f72e512b10893a1d@gw.dataimpulse.com:823'] * 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