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

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

60
add_extracted_field.py Normal file
View File

@@ -0,0 +1,60 @@
import asyncio
import sys
from loguru import logger
from core.config import Config
from core.database import DatabaseManager
from core.logger import setup_logger
async def add_extracted_field():
# 初始化
config = Config.from_yaml()
logger = setup_logger(config)
db_manager = DatabaseManager(config)
try:
await db_manager.initialize()
# 检查字段是否存在
async with db_manager.get_connection() as conn:
# SQLite中查询表结构的方法
cursor = await conn.execute("PRAGMA table_info(email_accounts)")
columns = await cursor.fetchall()
# 检查是否已存在extracted字段
extracted_exists = any(column[1] == 'extracted' for column in columns)
if extracted_exists:
logger.info("extracted字段已存在无需添加")
return
# 添加extracted字段
logger.info("正在添加extracted字段...")
await conn.execute("""
ALTER TABLE email_accounts
ADD COLUMN extracted BOOLEAN DEFAULT 0
""")
await conn.commit()
logger.success("成功添加extracted字段")
# 初始化字段值
logger.info("正在初始化extracted字段值...")
await conn.execute("""
UPDATE email_accounts
SET extracted = 0
""")
await conn.commit()
logger.success("初始化完成所有账号的extracted字段已设置为0")
except Exception as e:
logger.error(f"添加字段时出错: {str(e)}")
finally:
await db_manager.cleanup()
if __name__ == "__main__":
asyncio.run(add_extracted_field())

39
config.yaml Normal file
View File

@@ -0,0 +1,39 @@
# 全局配置
global:
max_concurrency: 20
timeout: 30
retry_times: 3
# 数据库配置
database:
path: "cursor.db"
pool_size: 10
# 代理配置
proxy:
api_url: "https://api.proxyscrape.com/v2/?request=displayproxies&protocol=http&timeout=10000&country=all&ssl=all&anonymity=all&format=text"
batch_size: 100
check_interval: 300
# 注册配置
register:
delay_range: [1, 2]
batch_size: 15
#这里是注册的并发数量
# 邮件配置
email:
file_path: "email.txt"
captcha:
provider: "capsolver" # 可选值: "capsolver" 或 "yescaptcha"
capsolver:
api_key: "CAP-E0A11882290AC7ADE2F799286B8E2DA497D7CD0510BFA477F3900507809F8AA3"
#api_key: "CAP-D87AD373EB0849F6F7BC511D772389EC0707D4BC74451F7B83425F820A70C2C0"
website_url: "https://authenticator.cursor.sh"
website_key: "0x4AAAAAAAMNIvC45A4Wjjln"
yescaptcha:
client_key: ""
website_url: "https://authenticator.cursor.sh"
website_key: "0x4AAAAAAAMNIvC45A4Wjjln"
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

BIN
cursor.db-journal Normal file

Binary file not shown.

227
delete_db.py Normal file
View File

@@ -0,0 +1,227 @@
import asyncio
import sys
from datetime import datetime, timedelta
import argparse
from loguru import logger
from core.config import Config
from core.database import DatabaseManager
from core.logger import setup_logger
class DatabaseCleaner:
def __init__(self):
self.config = Config.from_yaml()
self.logger = setup_logger(self.config)
self.db_manager = DatabaseManager(self.config)
async def initialize(self):
"""初始化数据库连接"""
await self.db_manager.initialize()
async def cleanup(self):
"""清理资源"""
await self.db_manager.cleanup()
async def get_status_stats(self) -> dict:
"""获取所有状态的统计信息"""
try:
async with self.db_manager.get_connection() as conn:
cursor = await conn.execute(
"""
SELECT status, COUNT(*) as count
FROM email_accounts
GROUP BY status
ORDER BY count DESC
"""
)
rows = await cursor.fetchall()
stats = {}
total = 0
for row in rows:
status, count = row
stats[status] = count
total += count
stats['total'] = total
return stats
except Exception as e:
self.logger.error(f"获取状态统计时出错: {e}")
return {}
async def delete_all(self, is_dry_run: bool = True) -> int:
"""删除所有记录"""
try:
async with self.db_manager.get_connection() as conn:
if is_dry_run:
# 在预览模式下,只统计数量
cursor = await conn.execute("SELECT COUNT(*) FROM email_accounts")
count = (await cursor.fetchone())[0]
self.logger.info(f"预览模式: 将删除 {count} 条记录")
return count
else:
# 实际删除
await conn.execute("DELETE FROM email_accounts")
await conn.commit()
self.logger.success("已删除所有记录")
return 0
except Exception as e:
self.logger.error(f"删除所有记录时出错: {e}")
return -1
async def delete_by_conditions(self, status: str = None, before_date: str = None,
email_domain: str = None, is_dry_run: bool = True) -> int:
"""根据条件删除记录"""
try:
conditions = []
params = []
if status:
conditions.append("status = ?")
params.append(status)
if before_date:
conditions.append("created_at < ?")
params.append(before_date)
if email_domain:
conditions.append("email LIKE ?")
params.append(f"%@{email_domain}")
if not conditions:
self.logger.error("未设置任何删除条件")
return -1
where_clause = " AND ".join(conditions)
async with self.db_manager.get_connection() as conn:
if is_dry_run:
# 在预览模式下,只统计数量
cursor = await conn.execute(
f"SELECT COUNT(*) FROM email_accounts WHERE {where_clause}",
params
)
count = (await cursor.fetchone())[0]
self.logger.info(f"预览模式: 将删除 {count} 条记录")
return count
else:
# 实际删除
await conn.execute(
f"DELETE FROM email_accounts WHERE {where_clause}",
params
)
await conn.commit()
self.logger.success("已删除符合条件的记录")
return 0
except Exception as e:
self.logger.error(f"删除记录时出错: {e}")
return -1
async def main():
# 解析命令行参数
parser = argparse.ArgumentParser(description="删除数据库中的记录")
parser.add_argument("--status", help="账号状态 (success/failed/pending/unavailable/retrying)")
parser.add_argument("--before", help="删除指定日期之前的记录 (YYYY-MM-DD)")
parser.add_argument("--domain", help="删除指定邮箱域名的记录")
parser.add_argument("--dry-run", action="store_true", help="预览模式,不实际删除")
parser.add_argument("--confirm", action="store_true", help="确认删除")
parser.add_argument("--all", action="store_true", help="删除所有记录")
parser.add_argument("--interactive", action="store_true", help="交互式删除")
args = parser.parse_args()
# 初始化
cleaner = DatabaseCleaner()
await cleaner.initialize()
try:
if args.interactive:
# 获取状态统计
stats = await cleaner.get_status_stats()
if not stats:
print("获取状态统计失败")
return
print("\n当前数据库状态统计:")
print("-" * 40)
for status, count in stats.items():
if status != 'total':
print(f"{status:15} : {count:5} 条记录")
print("-" * 40)
print(f"总计: {stats['total']} 条记录")
# 交互式选择要删除的状态
print("\n请选择要删除的状态 (输入状态名称,多个状态用逗号分隔):")
status_input = input().strip()
if not status_input:
print("未选择任何状态")
return
# 解析输入的状态
statuses = [s.strip() for s in status_input.split(',')]
# 验证状态是否有效
valid_statuses = [s for s in statuses if s in stats]
if not valid_statuses:
print("没有有效的状态")
return
# 显示将要删除的记录数量
total_to_delete = sum(stats[s] for s in valid_statuses)
print(f"\n将要删除 {total_to_delete} 条记录")
print("确定要删除吗?(y/n)", end=" ")
if input().strip().lower() != 'y':
print("操作已取消")
return
# 执行删除
for status in valid_statuses:
await cleaner.delete_by_conditions(status=status, is_dry_run=False)
return
# 检查是否设置了任何条件
if not any([args.status, args.before, args.domain, args.all]):
print("错误: 必须设置至少一个删除条件")
return
# 检查是否确认删除
if not args.dry_run and not args.confirm:
print("错误: 实际删除操作需要添加 --confirm 参数")
return
if args.all:
# 删除所有记录
await cleaner.delete_all(args.dry_run)
else:
# 按条件删除
await cleaner.delete_by_conditions(
status=args.status,
before_date=args.before,
email_domain=args.domain,
is_dry_run=args.dry_run
)
finally:
await cleaner.cleanup()
if __name__ == "__main__":
if len(sys.argv) == 1:
print("\n删除数据库账号工具使用说明:")
print(" 交互式删除python delete_db.py --interactive")
print(" 删除指定状态的账号python delete_db.py --status unavailable")
print(" 删除指定日期前的账号python delete_db.py --before 2025-03-20")
print(" 删除指定域名的账号python delete_db.py --domain outlook.com")
print(" 组合条件python delete_db.py --status unavailable --domain outlook.com")
print(" 预览模式不实际删除python delete_db.py --status unavailable --dry-run")
print(" 自动确认删除不提示python delete_db.py --status unavailable --confirm")
print(" 删除所有记录python delete_db.py --all --confirm")
print("\n注意:必须指定至少一个过滤条件或使用交互式模式\n")
asyncio.run(main())

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

170
main.py Normal file
View File

@@ -0,0 +1,170 @@
import asyncio
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)
# 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 result in results:
if isinstance(result, Exception):
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())

561
menu.py Normal file
View File

@@ -0,0 +1,561 @@
import asyncio
import sys
import os
from datetime import datetime
from typing import Dict, Any, List, Callable, Awaitable
from loguru import logger
# 导入功能模块
import upload_accounts
import reset_extracted
import reupload_all
try:
import retry_failed
has_retry_failed = True
except ImportError:
has_retry_failed = False
try:
import reset_retrying
has_reset_retrying = True
except ImportError:
has_reset_retrying = False
try:
import delete_db
has_delete_db = True
except ImportError:
has_delete_db = False
from core.config import Config
from core.logger import setup_logger
from core.database import DatabaseManager
class MenuSystem:
def __init__(self):
self.config = Config.from_yaml()
self.logger = setup_logger(self.config)
self.db_manager = DatabaseManager(self.config)
# 定义菜单项
self.menu_items = [
{
"key": "1",
"title": "重新上传所有账号",
"description": "重置所有账号的提取状态并重新上传到API服务器推荐",
"handler": self.reupload_all_menu
},
{
"key": "2",
"title": "上传账号到API",
"description": "将注册成功的账号上传到API服务器",
"handler": self.upload_accounts_menu
},
{
"key": "3",
"title": "重置账号提取状态",
"description": "将账号的extracted字段重置为0允许重新上传",
"handler": self.reset_extracted_menu
}
]
# 添加可选功能
if has_retry_failed:
self.menu_items.append({
"key": "4",
"title": "重试失败账号",
"description": "重新尝试注册失败的账号",
"handler": self.retry_failed_menu
})
if has_reset_retrying:
self.menu_items.append({
"key": "5",
"title": "重置正在重试的账号",
"description": "将状态为retrying的账号重置为failed",
"handler": self.reset_retrying_menu
})
if has_delete_db:
self.menu_items.append({
"key": "6",
"title": "删除数据库记录",
"description": "根据条件删除数据库中的记录",
"handler": self.delete_db_menu
})
# 添加退出选项
self.menu_items.append({
"key": "q",
"title": "退出程序",
"description": "退出菜单系统",
"handler": self.exit_menu
})
async def initialize(self):
"""初始化数据库连接"""
await self.db_manager.initialize()
async def cleanup(self):
"""清理资源"""
await self.db_manager.cleanup()
async def show_menu(self):
"""显示主菜单"""
os.system('cls' if os.name == 'nt' else 'clear')
print("\n===== Cursor 账号管理系统 =====\n")
# 显示菜单项
for item in self.menu_items:
print(f"{item['key']}. {item['title']} - {item['description']}")
print("\n请选择功能 (输入对应数字或q退出):", end=" ")
choice = input().strip().lower()
# 查找选择的菜单项
selected_item = next((item for item in self.menu_items if item["key"] == choice), None)
if selected_item:
await selected_item["handler"]()
else:
print("\n无效的选择,请重试!")
input("按Enter键继续...")
await self.show_menu()
async def upload_accounts_menu(self):
"""上传账号菜单"""
os.system('cls' if os.name == 'nt' else 'clear')
print("\n===== 上传账号到API =====\n")
# 统计账号状态
resetter = reset_extracted.ExtractedResetter()
await resetter.initialize()
stats = await resetter.count_success_accounts()
await resetter.cleanup()
print(f"当前状态: 总成功账号: {stats['total']}, 已提取: {stats['extracted']}, 未提取: {stats['not_extracted']}")
if stats['not_extracted'] == 0:
print("\n没有待上传的账号。是否要重置所有账号的提取状态?(y/n)", end=" ")
reset_choice = input().strip().lower()
if reset_choice == 'y':
await self.reset_all_extracted()
print("\n已重置所有账号的提取状态,现在可以上传。")
else:
print("\n未重置状态,无法上传账号。")
input("\n按Enter键返回主菜单...")
return
print("\n选项:")
print("1. 预览待上传账号 (dry-run)")
print("2. 上传所有账号")
print("3. 上传指定批次数量")
print("4. 修改批次大小 (默认: 100)")
print("5. 禁用代理")
print("b. 返回主菜单")
choice = input("\n请选择操作: ").strip().lower()
args = []
use_proxy = True
batch_size = 100
max_batches = 0
if choice == "1":
args = ["--dry-run"]
elif choice == "2":
args = [] # 使用默认设置上传所有
elif choice == "3":
try:
batches = int(input("请输入要处理的批次数量: ").strip())
if batches > 0:
args = [f"--batches", f"{batches}"]
max_batches = batches
else:
print("批次数量必须大于0")
input("按Enter键继续...")
await self.upload_accounts_menu()
return
except ValueError:
print("无效的输入,必须是整数")
input("按Enter键继续...")
await self.upload_accounts_menu()
return
elif choice == "4":
try:
size = int(input("请输入批次大小: ").strip())
if size > 0:
batch_size = size
args = [f"--batch-size", f"{size}"]
else:
print("批次大小必须大于0")
input("按Enter键继续...")
await self.upload_accounts_menu()
return
except ValueError:
print("无效的输入,必须是整数")
input("按Enter键继续...")
await self.upload_accounts_menu()
return
elif choice == "5":
use_proxy = False
args = ["--no-proxy"]
elif choice == "b":
await self.show_menu()
return
else:
print("无效的选择,请重试!")
input("按Enter键继续...")
await self.upload_accounts_menu()
return
# 处理输入的批次大小和批次数量组合
if choice == "3" and choice == "4":
args = [f"--batches", f"{max_batches}", f"--batch-size", f"{batch_size}"]
# 添加代理设置
if not use_proxy and "--no-proxy" not in args:
args.append("--no-proxy")
print(f"\n准备执行命令: python upload_accounts.py {' '.join(args)}")
print("按Enter键开始执行或按Ctrl+C取消...")
input()
# 存储原始sys.argv并替换
original_argv = sys.argv.copy()
sys.argv = [sys.argv[0]] + args
try:
# 执行上传账号功能
await upload_accounts.main()
except Exception as e:
self.logger.error(f"执行上传账号出错: {e}")
finally:
# 恢复sys.argv
sys.argv = original_argv
input("\n操作完成按Enter键返回主菜单...")
await self.show_menu()
async def reset_extracted_menu(self):
"""重置提取状态菜单"""
os.system('cls' if os.name == 'nt' else 'clear')
print("\n===== 重置账号提取状态 =====\n")
# 统计账号状态
resetter = reset_extracted.ExtractedResetter()
await resetter.initialize()
stats = await resetter.count_success_accounts()
print(f"当前状态: 总成功账号: {stats['total']}, 已提取: {stats['extracted']}, 未提取: {stats['not_extracted']}")
print("\n选项:")
print("1. 预览账号状态 (dry-run)")
print("2. 重置所有账号的提取状态")
print("3. 重置指定模式的账号提取状态")
print("b. 返回主菜单")
choice = input("\n请选择操作: ").strip().lower()
args = []
if choice == "1":
args = ["--dry-run"]
elif choice == "2":
print("\n确定要重置所有账号的提取状态吗?这将允许已提取的账号再次上传。(y/n)", end=" ")
confirm = input().strip().lower()
if confirm != 'y':
print("操作已取消")
input("按Enter键继续...")
await self.reset_extracted_menu()
return
elif choice == "3":
pattern = input("请输入邮箱匹配模式 (例如: outlook.com): ").strip()
if pattern:
args = ["--pattern", pattern]
else:
print("模式不能为空")
input("按Enter键继续...")
await self.reset_extracted_menu()
return
elif choice == "b":
await self.show_menu()
return
else:
print("无效的选择,请重试!")
input("按Enter键继续...")
await self.reset_extracted_menu()
return
# 执行重置
print(f"\n准备执行命令: python reset_extracted.py {' '.join(args)}")
print("按Enter键开始执行或按Ctrl+C取消...")
input()
# 存储原始sys.argv并替换
original_argv = sys.argv.copy()
sys.argv = [sys.argv[0]] + args
try:
# 执行重置操作
await reset_extracted.main()
except Exception as e:
self.logger.error(f"执行重置提取状态出错: {e}")
finally:
# 恢复sys.argv
sys.argv = original_argv
await resetter.cleanup()
input("\n操作完成按Enter键返回主菜单...")
await self.show_menu()
async def reupload_all_menu(self):
"""重新上传所有账号菜单"""
os.system('cls' if os.name == 'nt' else 'clear')
print("\n===== 重新上传所有账号 =====\n")
print("此操作将:")
print("1. 重置所有成功账号的提取状态")
print("2. 重新上传所有账号到API服务器")
print("\n警告: 此操作可能需要较长时间,请确保网络连接稳定。")
print("是否继续? (y/n)", end=" ")
choice = input().strip().lower()
if choice != 'y':
print("\n操作已取消")
input("\n按Enter键返回主菜单...")
await self.show_menu()
return
try:
# 执行重新上传
await reupload_all.main()
except Exception as e:
self.logger.error(f"重新上传过程中出错: {e}")
finally:
input("\n操作完成按Enter键返回主菜单...")
await self.show_menu()
async def retry_failed_menu(self):
"""重试失败账号菜单"""
if not has_retry_failed:
print("\n功能不可用: retry_failed.py 未找到")
input("按Enter键返回主菜单...")
await self.show_menu()
return
os.system('cls' if os.name == 'nt' else 'clear')
print("\n===== 重试失败账号 =====\n")
print("此功能将重新尝试注册失败的账号。")
# 这里可以添加更多的统计信息显示
print("\n选项:")
print("1. 执行账号重试")
print("b. 返回主菜单")
choice = input("\n请选择操作: ").strip().lower()
if choice == "1":
# 执行重试失败账号功能
print("\n准备重试失败账号...")
print("按Enter键开始执行或按Ctrl+C取消...")
input()
# 存储原始sys.argv
original_argv = sys.argv.copy()
sys.argv = [sys.argv[0]]
try:
# 执行重试功能
await retry_failed.main()
except Exception as e:
self.logger.error(f"执行重试失败账号出错: {e}")
finally:
# 恢复sys.argv
sys.argv = original_argv
elif choice == "b":
await self.show_menu()
return
else:
print("无效的选择,请重试!")
input("按Enter键继续...")
await self.retry_failed_menu()
return
input("\n操作完成按Enter键返回主菜单...")
await self.show_menu()
async def reset_retrying_menu(self):
"""重置正在重试的账号菜单"""
if not has_reset_retrying:
print("\n功能不可用: reset_retrying.py 未找到")
input("按Enter键返回主菜单...")
await self.show_menu()
return
os.system('cls' if os.name == 'nt' else 'clear')
print("\n===== 重置正在重试的账号 =====\n")
print("此功能将状态为retrying的账号重置为failed。")
# 这里可以添加更多的统计信息显示
print("\n选项:")
print("1. 执行重置retrying状态")
print("b. 返回主菜单")
choice = input("\n请选择操作: ").strip().lower()
if choice == "1":
# 执行重置retrying状态功能
print("\n准备重置正在重试的账号...")
print("按Enter键开始执行或按Ctrl+C取消...")
input()
# 存储原始sys.argv
original_argv = sys.argv.copy()
sys.argv = [sys.argv[0]]
try:
# 执行重置功能
await reset_retrying.main()
except Exception as e:
self.logger.error(f"执行重置retrying状态出错: {e}")
finally:
# 恢复sys.argv
sys.argv = original_argv
elif choice == "b":
await self.show_menu()
return
else:
print("无效的选择,请重试!")
input("按Enter键继续...")
await self.reset_retrying_menu()
return
input("\n操作完成按Enter键返回主菜单...")
await self.show_menu()
async def delete_db_menu(self):
"""删除数据库记录菜单"""
if not has_delete_db:
print("\n功能不可用: delete_db.py 未找到")
input("按Enter键返回主菜单...")
await self.show_menu()
return
os.system('cls' if os.name == 'nt' else 'clear')
print("\n===== 删除数据库记录 =====\n")
print("此功能将根据条件删除数据库中的记录。")
print("警告: 删除操作不可恢复,请谨慎操作!")
print("\n选项:")
print("1. 交互式删除(按状态)")
print("2. 删除所有记录")
print("b. 返回主菜单")
choice = input("\n请选择操作: ").strip().lower()
if choice == "b":
await self.show_menu()
return
elif choice == "2":
print("\n警告: 此操作将删除数据库中的所有记录!")
print("确定要继续吗?(y/n)", end=" ")
confirm = input().strip().lower()
if confirm != 'y':
print("操作已取消")
input("按Enter键继续...")
await self.delete_db_menu()
return
# 执行全部删除
args = ["--all", "--confirm"]
print(f"\n准备执行命令: python delete_db.py {' '.join(args)}")
print("按Enter键开始执行或按Ctrl+C取消...")
input()
# 存储原始sys.argv
original_argv = sys.argv.copy()
sys.argv = [sys.argv[0]] + args
try:
# 执行删除功能
await delete_db.main()
except Exception as e:
self.logger.error(f"执行删除数据库记录出错: {e}")
finally:
# 恢复sys.argv
sys.argv = original_argv
input("\n操作完成按Enter键返回主菜单...")
await self.show_menu()
return
elif choice == "1":
# 执行交互式删除
args = ["--interactive"]
print(f"\n准备执行命令: python delete_db.py {' '.join(args)}")
print("按Enter键开始执行或按Ctrl+C取消...")
input()
# 存储原始sys.argv
original_argv = sys.argv.copy()
sys.argv = [sys.argv[0]] + args
try:
# 执行删除功能
await delete_db.main()
except Exception as e:
self.logger.error(f"执行删除数据库记录出错: {e}")
finally:
# 恢复sys.argv
sys.argv = original_argv
input("\n操作完成按Enter键返回主菜单...")
await self.show_menu()
return
else:
print("无效的选择,请重试!")
input("按Enter键继续...")
await self.delete_db_menu()
return
async def reset_all_extracted(self):
"""重置所有账号的提取状态"""
resetter = reset_extracted.ExtractedResetter()
await resetter.initialize()
try:
await resetter.reset_extracted()
finally:
await resetter.cleanup()
async def exit_menu(self):
"""退出菜单"""
print("\n感谢使用,再见!")
sys.exit(0)
async def main():
menu = MenuSystem()
try:
await menu.initialize()
await menu.show_menu()
except KeyboardInterrupt:
print("\n程序被用户中断")
except Exception as e:
logger.error(f"程序执行出错: {e}")
finally:
await menu.cleanup()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
except Exception as e:
print(f"程序崩溃: {e}")

224
read_db.py Normal file
View File

@@ -0,0 +1,224 @@
import sqlite3
import sys
from datetime import datetime
def format_timestamp(timestamp):
"""格式化时间戳为可读格式"""
if not timestamp:
return ""
try:
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
return dt.strftime('%Y-%m-%d')
except:
return timestamp
def read_accounts(limit=10, offset=0):
"""读取数据库中的账号信息"""
try:
# 连接到数据库
conn = sqlite3.connect('cursor.db')
# 设置行工厂,让查询结果以字典形式返回
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 获取总账号数
cursor.execute("SELECT COUNT(*) FROM email_accounts")
total_count = cursor.fetchone()[0]
# 按各种状态统计账号数量
cursor.execute("SELECT COUNT(*) FROM email_accounts WHERE status = 'success'")
success_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM email_accounts WHERE in_use = 1")
in_use_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM email_accounts WHERE sold = 1")
sold_count = cursor.fetchone()[0]
# 获取账号详情
cursor.execute(f"""
SELECT id, email, password, cursor_password, status, in_use, sold, created_at, updated_at
FROM email_accounts
ORDER BY id DESC
LIMIT {limit} OFFSET {offset}
""")
accounts = cursor.fetchall()
# 打印统计信息
print("-" * 80)
print(f"总账号数: {total_count} | 注册成功: {success_count} | 使用中: {in_use_count} | 已售出: {sold_count}")
success_rate = (success_count / total_count * 100) if total_count > 0 else 0
print(f"注册成功率: {success_rate:.2f}%")
print("-" * 80)
# 打印表头
print(f"{'ID':<5} {'邮箱':<25} {'密码':<15} {'Cursor密码':<15} {'状态':<8} {'使用中':<4} {'已售':<4} {'创建时间':<10}")
print("-" * 80)
# 打印账号信息
for account in accounts:
in_use = "" if account['in_use'] else ""
sold = "" if account['sold'] else ""
created_at = format_timestamp(account['created_at'])
# 截断过长的字段
email = account['email'][:23] + '..' if len(account['email']) > 25 else account['email']
password = account['password'][:13] + '..' if len(account['password']) > 15 else account['password']
cursor_pwd = (account['cursor_password'] or '')[:13] + '..' if account['cursor_password'] and len(account['cursor_password']) > 15 else (account['cursor_password'] or '')
print(f"{account['id']:<5} {email:<25} {password:<15} "
f"{cursor_pwd:<15} {account['status']:<8} "
f"{in_use:<4} {sold:<4} {created_at:<10}")
print("-" * 80)
print(f"显示 {len(accounts)} 条记录,总共 {total_count} 条记录")
except sqlite3.Error as e:
print(f"数据库错误: {e}")
except Exception as e:
print(f"发生错误: {e}")
finally:
if conn:
conn.close()
def analyze_domains():
"""分析邮箱域名分布"""
try:
conn = sqlite3.connect('cursor.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 提取域名并计数
cursor.execute("""
SELECT
substr(email, instr(email, '@') + 1) as domain,
COUNT(*) as count,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count
FROM email_accounts
GROUP BY domain
ORDER BY count DESC
""")
domains = cursor.fetchall()
print("\n----- 邮箱域名分布 -----")
print(f"{'域名':<20} {'账号数':<10} {'成功数':<10} {'成功率':<10}")
print("-" * 50)
for domain in domains:
success_rate = (domain['success_count'] / domain['count'] * 100) if domain['count'] > 0 else 0
print(f"{domain['domain']:<20} {domain['count']:<10} {domain['success_count']:<10} {success_rate:.2f}%")
except sqlite3.Error as e:
print(f"数据库错误: {e}")
finally:
if conn:
conn.close()
def analyze_time():
"""分析注册时间分布"""
try:
conn = sqlite3.connect('cursor.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 按日期分组统计
cursor.execute("""
SELECT
substr(created_at, 1, 10) as date,
COUNT(*) as total,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success
FROM email_accounts
GROUP BY date
ORDER BY date DESC
LIMIT 30
""")
dates = cursor.fetchall()
print("\n----- 注册时间分布 (最近30天) -----")
print(f"{'日期':<15} {'注册数':<10} {'成功数':<10} {'成功率':<10}")
print("-" * 50)
for date in dates:
success_rate = (date['success'] / date['total'] * 100) if date['total'] > 0 else 0
print(f"{date['date']:<15} {date['total']:<10} {date['success']:<10} {success_rate:.2f}%")
except sqlite3.Error as e:
print(f"数据库错误: {e}")
finally:
if conn:
conn.close()
def analyze_status():
"""分析注册状态分布"""
try:
conn = sqlite3.connect('cursor.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 按状态分组统计
cursor.execute("""
SELECT
status,
COUNT(*) as count,
(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM email_accounts)) as percentage
FROM email_accounts
GROUP BY status
ORDER BY count DESC
""")
statuses = cursor.fetchall()
print("\n----- 注册状态分布 -----")
print(f"{'状态':<20} {'数量':<10} {'百分比':<10}")
print("-" * 40)
for status in statuses:
print(f"{status['status']:<20} {status['count']:<10} {status['percentage']:.2f}%")
except sqlite3.Error as e:
print(f"数据库错误: {e}")
finally:
if conn:
conn.close()
if __name__ == "__main__":
# 解析命令行参数
limit = 20 # 默认显示20条
offset = 0 # 默认从第一条开始
if len(sys.argv) > 1:
if sys.argv[1] == 'domains':
analyze_domains()
sys.exit(0)
elif sys.argv[1] == 'time':
analyze_time()
sys.exit(0)
elif sys.argv[1] == 'status':
analyze_status()
sys.exit(0)
elif sys.argv[1] == 'all':
read_accounts(10, 0)
analyze_domains()
analyze_time()
analyze_status()
sys.exit(0)
try:
limit = int(sys.argv[1])
except ValueError:
print("参数错误: limit必须是整数")
sys.exit(1)
if len(sys.argv) > 2:
try:
offset = int(sys.argv[2])
except ValueError:
print("参数错误: offset必须是整数")
sys.exit(1)
read_accounts(limit, offset)
print("\n使用方法:")
print("python read_db.py [limit] [offset] - 显示账号列表")
print("python read_db.py domains - 显示邮箱域名分布")
print("python read_db.py time - 显示注册时间分布")
print("python read_db.py status - 显示注册状态分布")
print("python read_db.py all - 显示所有统计信息")

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

136
reset_extracted.py Normal file
View File

@@ -0,0 +1,136 @@
import asyncio
import sys
from typing import List, Dict, Any
import aiohttp
from loguru import logger
from core.config import Config
from core.database import DatabaseManager
from core.logger import setup_logger
class ExtractedResetter:
def __init__(self):
self.config = Config.from_yaml()
self.logger = setup_logger(self.config)
self.db_manager = DatabaseManager(self.config)
async def initialize(self):
"""初始化数据库"""
await self.db_manager.initialize()
async def cleanup(self):
"""清理资源"""
await self.db_manager.cleanup()
async def reset_extracted(self, include_pattern: str = None) -> int:
"""将账号的extracted字段重置为0"""
try:
query = """
UPDATE email_accounts
SET
extracted = 0,
updated_at = CURRENT_TIMESTAMP
WHERE status = 'success' AND sold = 1
"""
params = []
# 添加条件过滤
if include_pattern:
query += " AND email LIKE ?"
params.append(f"%{include_pattern}%")
async with self.db_manager.get_connection() as conn:
cursor = await conn.execute(query, tuple(params))
updated_count = cursor.rowcount
await conn.commit()
self.logger.success(f"成功重置 {updated_count} 个账号的extracted状态")
return updated_count
except Exception as e:
self.logger.error(f"重置extracted状态时出错: {e}")
return 0
async def count_success_accounts(self) -> Dict[str, int]:
"""统计成功账号的提取状态"""
try:
async with self.db_manager.get_connection() as conn:
# 统计已提取的账号
cursor1 = await conn.execute(
"SELECT COUNT(*) FROM email_accounts WHERE status = 'success' AND sold = 1 AND extracted = 1"
)
extracted_count = (await cursor1.fetchone())[0]
# 统计未提取的账号
cursor2 = await conn.execute(
"SELECT COUNT(*) FROM email_accounts WHERE status = 'success' AND sold = 1 AND extracted = 0"
)
not_extracted_count = (await cursor2.fetchone())[0]
# 统计总成功账号
cursor3 = await conn.execute(
"SELECT COUNT(*) FROM email_accounts WHERE status = 'success' AND sold = 1"
)
total_count = (await cursor3.fetchone())[0]
return {
"extracted": extracted_count,
"not_extracted": not_extracted_count,
"total": total_count
}
except Exception as e:
self.logger.error(f"统计账号时出错: {e}")
return {
"extracted": 0,
"not_extracted": 0,
"total": 0
}
async def main():
# 解析命令行参数
import argparse
parser = argparse.ArgumentParser(description="重置账号的已提取状态")
parser.add_argument("--pattern", type=str, help="只重置匹配此模式的邮箱")
parser.add_argument("--dry-run", action="store_true", help="仅统计,不重置")
args = parser.parse_args()
# 初始化
resetter = ExtractedResetter()
await resetter.initialize()
try:
# 统计当前状态
stats = await resetter.count_success_accounts()
logger.info(f"当前状态: 总成功账号: {stats['total']}, 已提取: {stats['extracted']}, 未提取: {stats['not_extracted']}")
# 预览模式
if args.dry_run:
logger.info("预览模式,不执行重置操作")
return
# 执行重置
if args.pattern:
logger.info(f"将重置邮箱包含 '{args.pattern}' 的账号的extracted状态")
else:
logger.info("将重置所有成功账号的extracted状态")
updated = await resetter.reset_extracted(args.pattern)
# 统计重置后的状态
new_stats = await resetter.count_success_accounts()
logger.info(f"重置后状态: 总成功账号: {new_stats['total']}, 已提取: {new_stats['extracted']}, 未提取: {new_stats['not_extracted']}")
except Exception as e:
logger.error(f"程序执行出错: {str(e)}")
finally:
await resetter.cleanup()
if __name__ == "__main__":
asyncio.run(main())

106
reset_retrying.py Normal file
View File

@@ -0,0 +1,106 @@
import asyncio
import sys
from datetime import datetime, timedelta
from loguru import logger
from core.config import Config
from core.database import DatabaseManager
from core.logger import setup_logger
class RetryingAccountsReset:
def __init__(self):
self.config = Config.from_yaml()
self.logger = setup_logger(self.config)
self.db_manager = DatabaseManager(self.config)
async def initialize(self):
"""初始化数据库"""
await self.db_manager.initialize()
async def cleanup(self):
"""清理资源"""
await self.db_manager.cleanup()
async def reset_stuck_accounts(self, timeout_minutes=30):
"""重置卡在retrying状态的账号"""
try:
# 计算超时时间
timeout_time = datetime.now() - timedelta(minutes=timeout_minutes)
timeout_str = timeout_time.strftime('%Y-%m-%d %H:%M:%S')
self.logger.info(f"开始重置超时的retrying账号 (超过{timeout_minutes}分钟)")
async with self.db_manager.get_connection() as conn:
# 查询当前处于retrying状态的账号数量
cursor = await conn.execute(
"SELECT COUNT(*) FROM email_accounts WHERE status = 'retrying'"
)
count = (await cursor.fetchone())[0]
if count == 0:
self.logger.info("没有找到处于retrying状态的账号")
return 0
# 更新超时的retrying账号为failed状态
cursor = await conn.execute(
"""
UPDATE email_accounts
SET
status = 'failed',
updated_at = CURRENT_TIMESTAMP
WHERE
status = 'retrying'
AND updated_at < ?
RETURNING id, email
""",
(timeout_str,)
)
# 获取被重置的账号信息
reset_accounts = await cursor.fetchall()
await conn.commit()
reset_count = len(reset_accounts)
# 打印重置的账号列表
if reset_count > 0:
self.logger.info(f"已重置 {reset_count} 个账号状态从retrying到failed:")
for account in reset_accounts:
self.logger.debug(f"ID: {account[0]}, 邮箱: {account[1]}")
else:
self.logger.info(f"没有找到超过{timeout_minutes}分钟的retrying账号")
return reset_count
except Exception as e:
self.logger.error(f"重置retrying账号失败: {str(e)}")
return 0
async def main():
# 解析命令行参数
timeout_minutes = 30 # 默认超时时间30分钟
if len(sys.argv) > 1:
try:
timeout_minutes = int(sys.argv[1])
except ValueError:
print(f"参数错误: 超时时间必须是整数,将使用默认值 {timeout_minutes}分钟")
# 初始化
reset = RetryingAccountsReset()
await reset.initialize()
try:
# 执行重置
await reset.reset_stuck_accounts(timeout_minutes)
except Exception as e:
logger.error(f"程序执行出错: {str(e)}")
finally:
await reset.cleanup()
if __name__ == "__main__":
asyncio.run(main())

200
retry_failed.py Normal file
View File

@@ -0,0 +1,200 @@
import asyncio
import sys
from typing import List
from loguru import logger
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 EmailAccount, EmailManager
from services.fetch_manager import FetchManager
from services.proxy_pool import ProxyPool
from services.token_pool import TokenPool
class FailedAccountsRetry:
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 get_failed_accounts(self, limit: int = 0) -> List[EmailAccount]:
"""获取注册失败的账号"""
async with self.db_manager.get_connection() as conn:
# 将limit=0视为无限制
limit_clause = f"LIMIT {limit}" if limit > 0 else ""
# 查询所有失败的账号
cursor = await conn.execute(
f"""
SELECT id, email, password, client_id, refresh_token, status
FROM email_accounts
WHERE status = 'failed'
ORDER BY id DESC
{limit_clause}
"""
)
rows = await cursor.fetchall()
accounts = []
for row in rows:
account = EmailAccount(
id=row[0],
email=row[1],
password=row[2],
client_id=row[3],
refresh_token=row[4],
status=row[5]
)
accounts.append(account)
return accounts
async def update_account_status(self, account_id: int, status: str):
"""更新账号状态"""
try:
async with self.db_manager.get_connection() as conn:
await conn.execute(
"""
UPDATE email_accounts
SET status = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""",
(status, account_id)
)
await conn.commit()
self.logger.debug(f"账号 ID {account_id} 状态已更新为: {status}")
except Exception as e:
self.logger.error(f"更新账号状态失败: {e}")
async def retry_account(self, account: EmailAccount) -> bool:
"""重试一个失败的账号"""
self.logger.info(f"开始重试账号: {account.email}")
try:
# 先将状态改为处理中
await self.update_account_status(account.id, 'retrying')
# 获取代理
proxy = (await self.proxy_pool.batch_get(1))[0]
# 获取token对
token_pair = (await self.token_pool.batch_generate(1))[0]
# 执行注册
result = await self.register_worker.register(proxy, token_pair, account)
# 处理结果
if result is None:
self.logger.warning(f"账号 {account.email} 被跳过")
await self.update_account_status(account.id, 'unavailable')
return False
# 详细记录结果内容,用于调试
self.logger.debug(f"注册结果: {result}")
# 检查必要字段是否存在
required_fields = ['account_id', 'cursor_password', 'cursor_cookie']
missing_fields = [field for field in required_fields if field not in result]
if missing_fields:
self.logger.error(f"注册结果缺少必要字段: {missing_fields}")
await self.update_account_status(account.id, 'failed')
return False
# 检查值是否有效
if not result['cursor_password'] or not result['cursor_cookie']:
self.logger.error(f"注册结果包含空值: password={bool(result['cursor_password'])}, cookie={bool(result['cursor_cookie'])}")
await self.update_account_status(account.id, 'failed')
return False
# 更新数据库
try:
await self.email_manager.update_account(
result['account_id'],
result['cursor_password'],
result['cursor_cookie'],
result.get('cursor_jwt', '') # 添加cursor_jwt参数如果不存在则使用空字符串
)
self.logger.success(f"账号数据更新成功: {account.email}")
except Exception as update_error:
self.logger.error(f"更新账号数据失败: {update_error}")
await self.update_account_status(account.id, 'failed')
return False
self.logger.success(f"账号 {account.email} 重试成功")
return True
except Exception as e:
self.logger.error(f"账号 {account.email} 重试失败: {str(e)}")
await self.update_account_status(account.id, 'failed')
return False
async def retry_batch(self, batch_size: int):
"""批量重试失败的账号"""
# 获取所有失败的账号
failed_accounts = await self.get_failed_accounts(batch_size)
if not failed_accounts:
self.logger.info("没有找到失败的账号")
return
self.logger.info(f"找到 {len(failed_accounts)} 个失败的账号,开始重试")
# 并发重试
tasks = [self.retry_account(account) for account in failed_accounts]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 统计结果
success_count = sum(1 for r in results if r is True)
error_count = sum(1 for r in results if isinstance(r, Exception))
failed_count = len(failed_accounts) - success_count - error_count
self.logger.info(f"重试完成: 成功 {success_count}, 失败 {failed_count}, 错误 {error_count}")
async def main():
# 解析命令行参数
batch_size = 10 # 默认批次大小
if len(sys.argv) > 1:
try:
batch_size = int(sys.argv[1])
except ValueError:
print(f"参数错误: 批次大小必须是整数,将使用默认值 {batch_size}")
# 初始化
retry = FailedAccountsRetry()
await retry.initialize()
try:
# 执行重试
await retry.retry_batch(batch_size)
except Exception as e:
logger.error(f"程序执行出错: {str(e)}")
finally:
await retry.cleanup()
if __name__ == "__main__":
asyncio.run(main())

66
reupload_all.py Normal file
View File

@@ -0,0 +1,66 @@
import asyncio
from loguru import logger
from core.config import Config
from core.database import DatabaseManager
from core.logger import setup_logger
from upload_accounts import AccountUploader
class ReuploadAll:
def __init__(self):
self.config = Config.from_yaml()
self.logger = setup_logger(self.config)
self.db_manager = DatabaseManager(self.config)
self.uploader = AccountUploader()
async def initialize(self):
"""初始化数据库和上传器"""
await self.db_manager.initialize()
await self.uploader.initialize()
async def cleanup(self):
"""清理资源"""
await self.db_manager.cleanup()
await self.uploader.cleanup()
async def reset_all_extracted(self):
"""重置所有账号的提取状态"""
try:
async with self.db_manager.get_connection() as conn:
await conn.execute(
"""
UPDATE email_accounts
SET extracted = 0, updated_at = CURRENT_TIMESTAMP
WHERE status = 'success' AND sold = 1
"""
)
await conn.commit()
self.logger.success("已重置所有账号的提取状态")
except Exception as e:
self.logger.error(f"重置提取状态时出错: {e}")
async def process(self):
"""处理重新上传所有账号"""
try:
# 重置所有账号的提取状态
await self.reset_all_extracted()
# 使用上传器处理所有账号
processed = await self.uploader.process_accounts()
self.logger.success(f"重新上传完成,共处理 {processed} 个账号")
except Exception as e:
self.logger.error(f"重新上传过程中出错: {e}")
finally:
await self.cleanup()
async def main():
reuploader = ReuploadAll()
await reuploader.initialize()
await reuploader.process()
if __name__ == "__main__":
asyncio.run(main())

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

71
test_api.py Normal file
View File

@@ -0,0 +1,71 @@
import requests
import json
# API URL
api_url = "https://cursorapi.nosqli.com/admin/api.AutoCursor/commonadd"
# 测试数据 - 尝试不同字段格式
test_data1 = [
{
"email": "test1@example.com",
"email_password": "test_password",
"cursor_email": "test1@example.com",
"cursor_password": "test_cursor_password",
"cookie": "test_cookie",
"token": "test_token"
}
]
test_data2 = [
{
"email": "test2@example.com",
"password": "test_password", # 使用password而不是email_password
"cursor_email": "test2@example.com",
"cursor_password": "test_cursor_password",
"cookie": "test_cookie",
"token": "test_token"
}
]
test_data3 = [
{
"email": "test3@example.com",
"email_password": "test_password",
"password": "test_password", # 同时提供password和email_password
"cursor_email": "test3@example.com",
"cursor_password": "test_cursor_password",
"cookie": "test_cookie",
"token": "test_token"
}
]
# 请求头
headers = {
"Content-Type": "application/json"
}
# 测试函数
def test_api(data, description):
print(f"\n测试 {description}:")
print(f"发送数据: {json.dumps(data, ensure_ascii=False)}")
try:
response = requests.post(api_url, headers=headers, json=data)
print(f"状态码: {response.status_code}")
if response.status_code == 200:
try:
result = response.json()
print(f"响应内容: {json.dumps(result, ensure_ascii=False, indent=2)}")
except json.JSONDecodeError:
print(f"非JSON响应: {response.text[:200]}...")
else:
print(f"请求失败: {response.text[:200]}...")
except Exception as e:
print(f"请求异常: {str(e)}")
# 执行测试
if __name__ == "__main__":
test_api(test_data1, "使用email_password字段")
test_api(test_data2, "使用password字段")
test_api(test_data3, "同时提供password和email_password字段")

448
upload_accounts.py Normal file
View File

@@ -0,0 +1,448 @@
import asyncio
import sys
import json
from typing import List, Dict, Any
import aiohttp
from loguru import logger
from core.config import Config
from core.database import DatabaseManager
from core.logger import setup_logger
class AccountUploader:
def __init__(self):
self.config = Config.from_yaml()
self.logger = setup_logger(self.config)
self.db_manager = DatabaseManager(self.config)
self.api_url = "https://cursorapi.nosqli.com/admin/api.AutoCursor/commonadd"
self.batch_size = 100
# 添加代理设置
self.proxy = "http://1ddbeae0f7a67106fd58:f72e512b10893a1d@gw.dataimpulse.com:823"
self.use_proxy = True # 默认使用代理
async def initialize(self):
"""初始化数据库"""
await self.db_manager.initialize()
async def cleanup(self):
"""清理资源"""
await self.db_manager.cleanup()
async def get_success_accounts(self, limit: int = 100, last_id: int = 0) -> List[Dict[str, Any]]:
"""获取状态为success且未提取的账号"""
async with self.db_manager.get_connection() as conn:
cursor = await conn.execute(
"""
SELECT
id, email, password, cursor_password, cursor_cookie, cursor_token
FROM email_accounts
WHERE status = 'success' AND sold = 1 AND extracted = 0
AND id > ?
ORDER BY id ASC
LIMIT ?
""",
(last_id, limit)
)
rows = await cursor.fetchall()
accounts = []
for row in rows:
account = {
"id": row[0],
"email": row[1],
"password": row[2],
"cursor_password": row[3],
"cursor_cookie": row[4],
"cursor_token": row[5]
}
accounts.append(account)
return accounts
async def count_success_accounts(self) -> int:
"""统计未提取的成功账号数量"""
async with self.db_manager.get_connection() as conn:
cursor = await conn.execute(
"SELECT COUNT(*) FROM email_accounts WHERE status = 'success' AND sold = 1 AND extracted = 0"
)
count = (await cursor.fetchone())[0]
return count
async def mark_as_extracted(self, account_ids: List[int]) -> bool:
"""将账号标记为已提取"""
if not account_ids:
return True
try:
placeholders = ", ".join("?" for _ in account_ids)
async with self.db_manager.get_connection() as conn:
await conn.execute(
f"""
UPDATE email_accounts
SET
extracted = 1,
updated_at = CURRENT_TIMESTAMP
WHERE id IN ({placeholders})
""",
tuple(account_ids)
)
await conn.commit()
return True
except Exception as e:
self.logger.error(f"标记账号为已提取时出错: {e}")
return False
async def upload_accounts(self, accounts: List[Dict[str, Any]]) -> bool:
"""上传账号到API"""
if not accounts:
return True
try:
# 准备上传数据
upload_data = []
for account in accounts:
# 根据API需要的格式构建账号项
upload_item = {
"email": account["email"],
"password": account["password"], # 同时提供password字段
"email_password": account["password"], # 同时提供email_password字段
"cursor_email": account["email"],
"cursor_password": account["cursor_password"],
"cookie": account["cursor_cookie"] or "", # 确保不为None
"token": account.get("cursor_token", "")
}
# 确保所有必须字段都有值
for key, value in upload_item.items():
if value is None:
upload_item[key] = "" # 将None替换为空字符串
upload_data.append(upload_item)
# 打印上传数据的结构(去掉长字符串的详细内容)
debug_data = []
for item in upload_data[:2]: # 只打印前2个账号作为示例
debug_item = item.copy()
if "cookie" in debug_item and debug_item["cookie"]:
debug_item["cookie"] = debug_item["cookie"][:20] + "..." if len(debug_item["cookie"]) > 20 else debug_item["cookie"]
if "token" in debug_item and debug_item["token"]:
debug_item["token"] = debug_item["token"][:20] + "..." if len(debug_item["token"]) > 20 else debug_item["token"]
debug_data.append(debug_item)
self.logger.debug(f"准备上传数据示例: {json.dumps(debug_data, ensure_ascii=False)}")
self.logger.debug(f"API URL: {self.api_url}")
# 发送请求
# 使用代理创建ClientSession
connector = aiohttp.TCPConnector(ssl=False) # 禁用SSL验证以防代理问题
async with aiohttp.ClientSession(connector=connector) as session:
# 添加超时设置
timeout = aiohttp.ClientTimeout(total=60) # 增加超时时间
# 准备代理配置
proxy = self.proxy if self.use_proxy else None
if self.use_proxy:
self.logger.debug(f"通过代理发送请求: {self.proxy.split('@')[1]}")
else:
self.logger.debug("不使用代理直接连接API")
# 根据API错误信息确保发送的是账号数组
self.logger.debug(f"发送账号数组,共 {len(upload_data)} 条记录")
try:
# 直接发送数组格式,使用代理
async with session.post(self.api_url, json=upload_data, timeout=timeout, proxy=proxy) as response:
response_text = await response.text()
self.logger.debug(f"API响应状态码: {response.status}")
self.logger.debug(f"API响应内容: {response_text}")
if response.status != 200:
self.logger.error(f"API响应错误 - 状态码: {response.status}")
return True # 即使HTTP错误也继续处理下一批
# 解析响应
try:
result = json.loads(response_text)
except json.JSONDecodeError:
self.logger.error(f"响应不是有效的JSON: {response_text}")
return True # JSON解析错误也继续处理下一批
# 判断上传是否成功 - 修改判断逻辑
# API返回code为0表示成功
if result.get("code") == 0:
# 检查data中的成功计数
success_count = result.get("data", {}).get("success", 0)
failed_count = result.get("data", {}).get("failed", 0)
self.logger.info(f"API返回结果: 成功: {success_count}, 失败: {failed_count}")
if success_count > 0:
self.logger.success(f"成功上传 {success_count} 个账号")
return True
else:
# 检查详细错误信息
details = result.get("data", {}).get("details", [])
if details:
for detail in details[:3]: # 只显示前三个错误
self.logger.error(f"账号 {detail.get('email')} 上传失败: {detail.get('message')}")
# 输出更详细的错误信息,帮助诊断
self.logger.debug(f"错误账号详情: {json.dumps(detail, ensure_ascii=False)}")
self.logger.error(f"账号上传全部失败: {result.get('msg', '未知错误')}")
return True # 即使全部失败也继续处理下一批
else:
self.logger.error(f"上传失败: {result.get('msg', '未知错误')}")
# 检查是否有详细错误信息
if 'data' in result:
self.logger.error(f"错误详情: {json.dumps(result['data'], ensure_ascii=False)}")
return True # API返回错误也继续处理下一批
except aiohttp.ClientError as e:
self.logger.error(f"HTTP请求错误: {str(e)}")
return True # 网络错误也继续处理下一批
except Exception as e:
self.logger.error(f"上传账号时出错: {str(e)}")
import traceback
self.logger.error(f"错误堆栈: {traceback.format_exc()}")
return True # 其他错误也继续处理下一批
async def process_accounts(self, max_batches: int = 0) -> int:
"""处理账号上传max_batches=0表示处理所有批次"""
total_count = await self.count_success_accounts()
if total_count == 0:
self.logger.info("没有找到待上传的账号")
return 0
self.logger.info(f"找到 {total_count} 个待上传账号")
processed_count = 0
batch_count = 0
failed_batches = 0 # 记录失败的批次数
last_id = 0 # 记录最后处理的ID
while True:
# 检查是否达到最大批次
if max_batches > 0 and batch_count >= max_batches:
self.logger.info(f"已达到指定的最大批次数 {max_batches}")
break
# 获取一批账号
accounts = await self.get_success_accounts(self.batch_size, last_id)
if not accounts:
self.logger.info("没有更多账号可上传")
break
batch_count += 1
current_batch_size = len(accounts)
self.logger.info(f"处理第 {batch_count} 批 - {current_batch_size} 个账号")
# 上传账号
account_ids = [account["id"] for account in accounts]
upload_success = await self.upload_accounts(accounts)
if upload_success:
# 标记为已提取
mark_success = await self.mark_as_extracted(account_ids)
if mark_success:
processed_count += current_batch_size
self.logger.success(f"{batch_count} 批处理完成,累计处理 {processed_count}/{total_count}")
failed_batches = 0 # 重置失败计数
# 更新最后处理的ID
last_id = max(account_ids)
else:
self.logger.error(f"{batch_count} 批标记已提取失败")
failed_batches += 1
else:
self.logger.error(f"{batch_count} 批上传失败")
failed_batches += 1
# 如果连续失败次数过多,才中断处理
if failed_batches >= 3:
self.logger.error(f"连续 {failed_batches} 批处理失败,停止处理")
break
# 简单的延迟,避免请求过快
await asyncio.sleep(1)
return processed_count
async def test_api_connection(self) -> bool:
"""测试API连接是否正常"""
self.logger.info(f"正在测试API连接: {self.api_url}")
self.logger.info(f"使用代理: {'使用代理' if self.use_proxy else '不使用代理'}")
try:
# 准备一个简单的测试数据
test_data = [
{
"email": "test@example.com",
"password": "test_password",
"email_password": "test_password", # 同时提供两个字段
"cursor_email": "test@example.com",
"cursor_password": "test_cursor_password",
"cookie": "test_cookie",
"token": "test_token"
}
]
# 使用代理创建ClientSession
connector = aiohttp.TCPConnector(ssl=False) # 禁用SSL验证以防代理问题
async with aiohttp.ClientSession(connector=connector) as session:
# 准备代理配置
proxy = self.proxy if self.use_proxy else None
# 尝试HEAD请求检查服务器是否可达
try:
self.logger.debug("尝试HEAD请求...")
async with session.head(self.api_url, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
self.logger.debug(f"HEAD响应状态码: {response.status}")
if response.status >= 400:
self.logger.error(f"API服务器响应错误: {response.status}")
return False
except Exception as e:
self.logger.warning(f"HEAD请求失败: {str(e)}尝试GET请求...")
# 尝试GET请求
try:
base_url = self.api_url.split('/admin')[0]
ping_url = f"{base_url}/ping"
self.logger.debug(f"尝试GET请求检查服务健康: {ping_url}")
async with session.get(ping_url, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
self.logger.debug(f"GET响应状态码: {response.status}")
if response.status == 200:
response_text = await response.text()
self.logger.debug(f"GET响应内容: {response_text}")
return True
except Exception as e:
self.logger.warning(f"GET请求失败: {str(e)}")
# 尝试OPTIONS请求获取允许的HTTP方法
try:
self.logger.debug("尝试OPTIONS请求...")
async with session.options(self.api_url, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
self.logger.debug(f"OPTIONS响应状态码: {response.status}")
if response.status == 200:
allowed_methods = response.headers.get('Allow', '')
self.logger.debug(f"允许的HTTP方法: {allowed_methods}")
return 'POST' in allowed_methods
except Exception as e:
self.logger.warning(f"OPTIONS请求失败: {str(e)}")
# 最后尝试一个小请求
self.logger.debug("尝试轻量级POST请求...")
try:
# 不同格式的请求体
formats = [
{"test": "connection"},
[{"test": "connection"}],
"test"
]
for i, payload in enumerate(formats):
try:
async with session.post(self.api_url, json=payload, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
self.logger.debug(f"POST格式{i+1}响应状态码: {response.status}")
response_text = await response.text()
self.logger.debug(f"POST格式{i+1}响应内容: {response_text[:200]}...")
# 即使返回错误也是说明服务器可达
if response.status != 0:
return True
except Exception as e:
self.logger.debug(f"POST格式{i+1}失败: {str(e)}")
return False
except Exception as e:
self.logger.error(f"测试POST请求失败: {str(e)}")
return False
except Exception as e:
self.logger.error(f"API连接测试失败: {str(e)}")
return False
def set_proxy_usage(self, use_proxy: bool):
"""设置是否使用代理"""
self.use_proxy = use_proxy
if use_proxy:
self.logger.info(f"已启用HTTP代理: {self.proxy.split('@')[1]}")
else:
self.logger.info("已禁用HTTP代理将直接连接")
async def main():
# 解析命令行参数
import argparse
parser = argparse.ArgumentParser(description="上传成功账号到API并标记为已提取")
parser.add_argument("--batches", type=int, default=0, help="处理的最大批次数0表示处理所有批次")
parser.add_argument("--batch-size", type=int, default=100, help="每批处理的账号数量")
parser.add_argument("--dry-run", action="store_true", help="预览模式,不实际上传和更新")
parser.add_argument("--test-api", action="store_true", help="测试API连接")
parser.add_argument("--no-proxy", action="store_true", help="不使用代理直接连接")
args = parser.parse_args()
# 初始化
uploader = AccountUploader()
await uploader.initialize()
if args.batch_size > 0:
uploader.batch_size = args.batch_size
# 设置是否使用代理
uploader.set_proxy_usage(not args.no_proxy)
try:
# 测试API连接
if args.test_api:
logger.info("开始测试API连接...")
is_connected = await uploader.test_api_connection()
if is_connected:
logger.success("API连接测试成功")
else:
logger.error("API连接测试失败")
return
# 预览模式
if args.dry_run:
total_count = await uploader.count_success_accounts()
if total_count == 0:
logger.info("没有找到待上传的账号")
return
logger.info(f"预览模式:找到 {total_count} 个待上传账号")
# 获取示例账号
accounts = await uploader.get_success_accounts(min(5, total_count))
logger.info(f"示例账号 ({len(accounts)}/{total_count}):")
for account in accounts:
logger.info(f"ID: {account['id']}, 邮箱: {account['email']}, Cursor密码: {account['cursor_password']}")
batch_count = (total_count + uploader.batch_size - 1) // uploader.batch_size
logger.info(f"预计分 {batch_count} 批上传,每批 {uploader.batch_size} 个账号")
return
# 实际处理
processed = await uploader.process_accounts(args.batches)
logger.success(f"处理完成,共上传 {processed} 个账号")
except Exception as e:
logger.error(f"程序执行出错: {str(e)}")
finally:
await uploader.cleanup()
if __name__ == "__main__":
asyncio.run(main())