可用
This commit is contained in:
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Python忽略项
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.idea/
|
||||
# Node.js忽略项
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.npm
|
||||
|
||||
.yarn-integrity
|
||||
.env
|
||||
.next
|
||||
.nuxt
|
||||
.cache
|
||||
dist/
|
||||
coverage/
|
||||
|
||||
# IDE相关
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
cursor.db
|
||||
logs/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.log
|
||||
email.txt
|
||||
60
add_extracted_field.py
Normal file
60
add_extracted_field.py
Normal 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
39
config.yaml
Normal 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
14
core/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .config import Config
|
||||
from .exceptions import (CursorRegisterException, EmailError, ProxyFetchError,
|
||||
RegisterError, TokenGenerationError)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
__all__ = [
|
||||
'Config',
|
||||
'CursorRegisterException',
|
||||
'TokenGenerationError',
|
||||
'ProxyFetchError',
|
||||
'RegisterError',
|
||||
'EmailError'
|
||||
]
|
||||
89
core/config.py
Normal file
89
core/config.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlobalConfig:
|
||||
max_concurrency: int
|
||||
timeout: int
|
||||
retry_times: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class DatabaseConfig:
|
||||
path: str
|
||||
pool_size: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProxyConfig:
|
||||
api_url: str
|
||||
batch_size: int
|
||||
check_interval: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegisterConfig:
|
||||
delay_range: Tuple[int, int]
|
||||
batch_size: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmailConfig:
|
||||
file_path: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class CapsolverConfig:
|
||||
api_key: str
|
||||
website_url: str
|
||||
website_key: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class YesCaptchaConfig:
|
||||
client_key: str
|
||||
website_url: str
|
||||
website_key: str
|
||||
use_cn_server: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaptchaConfig:
|
||||
provider: str
|
||||
capsolver: CapsolverConfig
|
||||
yescaptcha: YesCaptchaConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
global_config: GlobalConfig
|
||||
database_config: DatabaseConfig
|
||||
proxy_config: ProxyConfig
|
||||
register_config: RegisterConfig
|
||||
email_config: EmailConfig
|
||||
captcha_config: CaptchaConfig
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: str = "config.yaml"):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 创建 captcha 配置对象
|
||||
captcha_data = data['captcha']
|
||||
captcha_config = CaptchaConfig(
|
||||
provider=captcha_data['provider'],
|
||||
capsolver=CapsolverConfig(**captcha_data['capsolver']),
|
||||
yescaptcha=YesCaptchaConfig(**captcha_data['yescaptcha'])
|
||||
)
|
||||
|
||||
return cls(
|
||||
global_config=GlobalConfig(**data['global']),
|
||||
database_config=DatabaseConfig(**data['database']),
|
||||
proxy_config=ProxyConfig(**data['proxy']),
|
||||
register_config=RegisterConfig(**data['register']),
|
||||
email_config=EmailConfig(**data['email']),
|
||||
captcha_config=captcha_config
|
||||
)
|
||||
86
core/database.py
Normal file
86
core/database.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import aiosqlite
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
def __init__(self, config: Config):
|
||||
self.db_path = config.database_config.path
|
||||
self._pool_size = config.database_config.pool_size
|
||||
self._pool: List[aiosqlite.Connection] = []
|
||||
self._pool_lock = asyncio.Lock()
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化数据库连接池"""
|
||||
logger.info("初始化数据库连接池")
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
in_use BOOLEAN DEFAULT 0,
|
||||
cursor_password TEXT,
|
||||
cursor_cookie TEXT,
|
||||
sold BOOLEAN DEFAULT 0,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
await db.commit()
|
||||
|
||||
# 初始化连接池
|
||||
for i in range(self._pool_size):
|
||||
conn = await aiosqlite.connect(self.db_path)
|
||||
self._pool.append(conn)
|
||||
logger.info(f"数据库连接池初始化完成,大小: {self._pool_size}")
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理数据库连接"""
|
||||
for conn in self._pool:
|
||||
await conn.close()
|
||||
self._pool.clear()
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_connection(self):
|
||||
"""获取数据库连接"""
|
||||
async with self._pool_lock:
|
||||
if not self._pool:
|
||||
conn = await aiosqlite.connect(self.db_path)
|
||||
else:
|
||||
conn = self._pool.pop()
|
||||
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
if len(self._pool) < self._pool_size:
|
||||
self._pool.append(conn)
|
||||
else:
|
||||
await conn.close()
|
||||
|
||||
async def execute(self, query: str, params: tuple = ()) -> Any:
|
||||
"""执行SQL语句"""
|
||||
async with self.get_connection() as conn:
|
||||
cursor = await conn.execute(query, params)
|
||||
await conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[tuple]:
|
||||
"""查询单条记录"""
|
||||
async with self.get_connection() as conn:
|
||||
cursor = await conn.execute(query, params)
|
||||
return await cursor.fetchone()
|
||||
|
||||
async def fetch_all(self, query: str, params: tuple = ()) -> List[tuple]:
|
||||
"""查询多条记录"""
|
||||
async with self.get_connection() as conn:
|
||||
cursor = await conn.execute(query, params)
|
||||
return await cursor.fetchall()
|
||||
23
core/exceptions.py
Normal file
23
core/exceptions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
class CursorRegisterException(Exception):
|
||||
"""基础异常类"""
|
||||
pass
|
||||
|
||||
|
||||
class TokenGenerationError(CursorRegisterException):
|
||||
"""Token生成失败"""
|
||||
pass
|
||||
|
||||
|
||||
class ProxyFetchError(CursorRegisterException):
|
||||
"""代理获取失败"""
|
||||
pass
|
||||
|
||||
|
||||
class RegisterError(CursorRegisterException):
|
||||
"""注册失败"""
|
||||
pass
|
||||
|
||||
|
||||
class EmailError(CursorRegisterException):
|
||||
"""邮件处理错误"""
|
||||
pass
|
||||
42
core/logger.py
Normal file
42
core/logger.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
|
||||
|
||||
def setup_logger(config: Config):
|
||||
"""配置日志系统"""
|
||||
# 移除默认的处理器
|
||||
logger.remove()
|
||||
|
||||
# 添加控制台处理器,改为 DEBUG 级别
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
level="DEBUG" # 修改为 DEBUG
|
||||
)
|
||||
|
||||
# 创建日志目录
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 文件处理器保持 DEBUG 级别
|
||||
logger.add(
|
||||
"logs/cursor_{time:YYYY-MM-DD}.log",
|
||||
rotation="00:00", # 每天轮换
|
||||
retention="7 days", # 保留7天
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
level="DEBUG",
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
# 设置一些常用的日志格式
|
||||
logger.level("DEBUG", color="<blue>")
|
||||
logger.level("INFO", color="<white>")
|
||||
logger.level("SUCCESS", color="<green>")
|
||||
logger.level("WARNING", color="<yellow>")
|
||||
logger.level("ERROR", color="<red>")
|
||||
|
||||
return logger
|
||||
BIN
cursor.db-journal
Normal file
BIN
cursor.db-journal
Normal file
Binary file not shown.
227
delete_db.py
Normal file
227
delete_db.py
Normal 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
61
import_emails.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import asyncio
|
||||
|
||||
import aiosqlite
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
|
||||
|
||||
async def import_emails(config: Config, file_path: str):
|
||||
"""导入邮箱账号到数据库"""
|
||||
DEFAULT_CLIENT_ID = "9e5f94bc-e8a4-4e73-b8be-63364c29d753"
|
||||
|
||||
async with aiosqlite.connect(config.database_config.path) as db:
|
||||
# 创建表
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
in_use BOOLEAN DEFAULT 0,
|
||||
cursor_password TEXT,
|
||||
cursor_cookie TEXT,
|
||||
cursor_token TEXT,
|
||||
sold BOOLEAN DEFAULT 0,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 读取文件并导入数据
|
||||
count = 0
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
if line.strip():
|
||||
try:
|
||||
email, password, client_id, refresh_token = line.strip().split('----')
|
||||
await db.execute('''
|
||||
INSERT INTO email_accounts (
|
||||
email, password, client_id, refresh_token, status
|
||||
) VALUES (?, ?, ?, ?, 'pending')
|
||||
''', (email, password, client_id, refresh_token))
|
||||
count += 1
|
||||
except aiosqlite.IntegrityError:
|
||||
logger.warning(f"重复的邮箱: {email}")
|
||||
except ValueError:
|
||||
logger.error(f"无效的数据行: {line.strip()}")
|
||||
|
||||
await db.commit()
|
||||
logger.success(f"成功导入 {count} 个邮箱账号")
|
||||
|
||||
|
||||
async def main():
|
||||
config = Config.from_yaml()
|
||||
await import_emails(config, "email.txt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
170
main.py
Normal file
170
main.py
Normal 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
561
menu.py
Normal 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
224
read_db.py
Normal 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
6
register/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .register_worker import FormBuilder, RegisterWorker
|
||||
|
||||
__all__ = [
|
||||
'RegisterWorker',
|
||||
'FormBuilder'
|
||||
]
|
||||
404
register/register_worker.py
Normal file
404
register/register_worker.py
Normal file
@@ -0,0 +1,404 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
from typing import Optional, Tuple
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.exceptions import RegisterError
|
||||
from services.email_manager import EmailAccount, EmailManager
|
||||
from services.fetch_manager import FetchManager
|
||||
from services.uuid import ULID
|
||||
|
||||
|
||||
def extract_jwt(cookie_string: str) -> str:
|
||||
"""从cookie字符串中提取JWT token"""
|
||||
try:
|
||||
return cookie_string.split(';')[0].split('=')[1].split('%3A%3A')[1]
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 提取JWT失败: {str(e)}")
|
||||
return ""
|
||||
|
||||
class FormBuilder:
|
||||
@staticmethod
|
||||
def _generate_password() -> str:
|
||||
"""生成随机密码
|
||||
规则: 12-16位,包含大小写字母、数字和特殊字符
|
||||
"""
|
||||
length = random.randint(12, 16)
|
||||
lowercase = string.ascii_lowercase
|
||||
uppercase = string.ascii_uppercase
|
||||
digits = string.digits
|
||||
special = "!@#$%^&*"
|
||||
|
||||
# 确保每种字符至少有一个
|
||||
password = [
|
||||
random.choice(lowercase),
|
||||
random.choice(uppercase),
|
||||
random.choice(digits),
|
||||
random.choice(special)
|
||||
]
|
||||
|
||||
# 填充剩余长度
|
||||
all_chars = lowercase + uppercase + digits + special
|
||||
password.extend(random.choice(all_chars) for _ in range(length - 4))
|
||||
|
||||
# 打乱顺序
|
||||
random.shuffle(password)
|
||||
return ''.join(password)
|
||||
|
||||
@staticmethod
|
||||
def _generate_name() -> tuple[str, str]:
|
||||
"""生成随机的名字和姓氏
|
||||
Returns:
|
||||
tuple: (first_name, last_name)
|
||||
"""
|
||||
first_names = ["Alex", "Sam", "Chris", "Jordan", "Taylor", "Morgan", "Casey", "Drew", "Pat", "Quinn"]
|
||||
last_names = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Moore", "Taylor", "Anderson", "Thomas", "Jackson"]
|
||||
|
||||
return (
|
||||
random.choice(first_names),
|
||||
random.choice(last_names)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_register_form(boundary: str, email: str, token: str) -> tuple[str, str]:
|
||||
"""构建注册表单数据,返回(form_data, password)"""
|
||||
password = FormBuilder._generate_password()
|
||||
|
||||
fields = {
|
||||
"1_state": "{\"returnTo\":\"/settings\"}",
|
||||
"1_redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
"1_bot_detection_token": token,
|
||||
"1_first_name": "wa",
|
||||
"1_last_name": "niu",
|
||||
"1_email": email,
|
||||
"1_password": password,
|
||||
"1_intent": "sign-up",
|
||||
"0": "[\"$K1\"]"
|
||||
}
|
||||
|
||||
form_data = []
|
||||
for key, value in fields.items():
|
||||
form_data.append(f'--{boundary}')
|
||||
form_data.append(f'Content-Disposition: form-data; name="{key}"')
|
||||
form_data.append('')
|
||||
form_data.append(value)
|
||||
|
||||
form_data.append(f'--{boundary}--')
|
||||
return '\r\n'.join(form_data), password
|
||||
|
||||
@staticmethod
|
||||
def build_verify_form(boundary: str, email: str, token: str, code: str, pending_token: str) -> str:
|
||||
"""构建验证表单数据"""
|
||||
fields = {
|
||||
"1_pending_authentication_token": pending_token,
|
||||
"1_email": email,
|
||||
"1_state": "{\"returnTo\":\"/settings\"}",
|
||||
"1_redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
"1_bot_detection_token": token,
|
||||
"1_code": code,
|
||||
"0": "[\"$K1\"]"
|
||||
}
|
||||
|
||||
form_data = []
|
||||
for key, value in fields.items():
|
||||
form_data.append(f'--{boundary}')
|
||||
form_data.append(f'Content-Disposition: form-data; name="{key}"')
|
||||
form_data.append('')
|
||||
form_data.append(value)
|
||||
|
||||
form_data.append(f'--{boundary}--')
|
||||
return '\r\n'.join(form_data)
|
||||
|
||||
|
||||
class RegisterWorker:
|
||||
def __init__(self, config: Config, fetch_manager: FetchManager, email_manager: EmailManager):
|
||||
self.config = config
|
||||
self.fetch_manager = fetch_manager
|
||||
self.email_manager = email_manager
|
||||
self.form_builder = FormBuilder()
|
||||
self.uuid = ULID()
|
||||
|
||||
async def random_delay(self):
|
||||
delay = random.uniform(*self.config.register_config.delay_range)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
@staticmethod
|
||||
async def _extract_auth_token(response_text: str, email_account: EmailAccount, email_manager: EmailManager) -> str | None:
|
||||
"""从响应文本中提取pending_authentication_token"""
|
||||
res = response_text.split('\n')
|
||||
logger.debug(f"开始提取 auth_token,响应行数: {len(res)}")
|
||||
|
||||
# 检查邮箱是否可用
|
||||
for line in res:
|
||||
if '"code":"email_not_available"' in line:
|
||||
logger.error("不受支持的邮箱")
|
||||
await email_manager.update_account_status(email_account.id, 'unavailable')
|
||||
raise RegisterError("Email is not available")
|
||||
|
||||
try:
|
||||
for i, r in enumerate(res):
|
||||
if r.startswith('0:'):
|
||||
logger.debug(f"在第 {i+1} 行找到匹配")
|
||||
data = json.loads(r.split('0:')[1])
|
||||
auth_data = data[1][0][0][1]["children"][1]["children"][1]["children"][1]["children"][0]
|
||||
params_str = auth_data.split('?')[1]
|
||||
params_dict = json.loads(params_str)
|
||||
token = params_dict['pending_authentication_token']
|
||||
logger.debug(f"方法2提取成功: {token[:10]}...")
|
||||
return token
|
||||
except Exception as e:
|
||||
logger.error(f"提取token失败: {str(e)}")
|
||||
logger.debug("响应内容预览:", response_text[:200])
|
||||
|
||||
return None
|
||||
|
||||
async def register(self, proxy: str, token_pair: Tuple[str, str], email_account: EmailAccount):
|
||||
"""完整的注册流程"""
|
||||
token1, token2 = token_pair
|
||||
session_id = self.uuid.generate()
|
||||
try:
|
||||
logger.info(f"开始注册账号: {email_account.email}")
|
||||
|
||||
# 第一次注册请求
|
||||
try:
|
||||
email, pending_token, cursor_password = await self._first_register(
|
||||
proxy,
|
||||
token1,
|
||||
email_account.email,
|
||||
email_account,
|
||||
session_id=session_id
|
||||
)
|
||||
except RegisterError as e:
|
||||
if "Email is not available" in str(e):
|
||||
logger.warning(f"邮箱 {email_account.email} 不受支持,跳过处理")
|
||||
return None
|
||||
raise e
|
||||
|
||||
# 获取验证码的同时,可以开始准备下一步的操作
|
||||
verification_code_task = self._get_verification_code_with_retry(
|
||||
email_account.email,
|
||||
email_account.refresh_token,
|
||||
email_account.client_id,
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
# 等待验证码
|
||||
verification_code = await verification_code_task
|
||||
if not verification_code:
|
||||
logger.error(f"账号 {email_account.email} 获取验证码失败")
|
||||
await self.email_manager.update_account_status(email_account.id, 'failed')
|
||||
raise RegisterError("Failed to get verification code")
|
||||
|
||||
logger.debug(f"邮箱 {email_account.email} 获取到验证码: {verification_code}")
|
||||
|
||||
await self.random_delay()
|
||||
|
||||
# 验证码验证
|
||||
redirect_url = await self._verify_code(
|
||||
proxy=proxy,
|
||||
token=token2,
|
||||
code=verification_code,
|
||||
pending_token=pending_token,
|
||||
email=email,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
if not redirect_url:
|
||||
raise RegisterError("No redirect URL found")
|
||||
|
||||
await self.random_delay()
|
||||
|
||||
# callback请求
|
||||
cookies = await self._callback(proxy, redirect_url)
|
||||
if not cookies:
|
||||
raise RegisterError("Failed to get cookies")
|
||||
|
||||
logger.success(f"账号 {email_account.email} 注册成功")
|
||||
return {
|
||||
'account_id': email_account.id,
|
||||
'cursor_password': cursor_password,
|
||||
'cursor_cookie': cookies,
|
||||
'cursor_jwt': extract_jwt(cookies)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"账号 {email_account.email} 注册失败: {str(e)}")
|
||||
if not str(e).startswith("Email is not available"):
|
||||
await self.email_manager.update_account_status(email_account.id, 'failed')
|
||||
raise RegisterError(f"Registration failed: {str(e)}")
|
||||
|
||||
async def _first_register(
|
||||
self,
|
||||
proxy: str,
|
||||
token: str,
|
||||
email: str,
|
||||
email_account: EmailAccount,
|
||||
session_id: str
|
||||
) -> tuple[str, str, str]:
|
||||
"""第一次注册请求"""
|
||||
logger.debug(f"开始第一次注册请求 - 邮箱: {email}, 代理: {proxy}")
|
||||
|
||||
first_name, last_name = self.form_builder._generate_name()
|
||||
|
||||
# 在headers中定义boundary
|
||||
boundary = "----WebKitFormBoundary2rKlvTagBEhneWi3"
|
||||
headers = {
|
||||
"accept": "text/x-component",
|
||||
"next-action": "770926d8148e29539286d20e1c1548d2aff6c0b9",
|
||||
"content-type": f"multipart/form-data; boundary={boundary}",
|
||||
"origin": "https://authenticator.cursor.sh",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin"
|
||||
}
|
||||
|
||||
params = {
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
"email": email,
|
||||
"state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D",
|
||||
"redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
}
|
||||
|
||||
# 构建form数据
|
||||
form_data, cursor_password = self.form_builder.build_register_form(boundary, email, token)
|
||||
|
||||
response = await self.fetch_manager.request(
|
||||
"POST",
|
||||
"https://authenticator.cursor.sh/sign-up/password",
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=form_data,
|
||||
proxy=proxy
|
||||
)
|
||||
|
||||
if 'error' in response:
|
||||
raise RegisterError(f"First register request failed: {response['error']}")
|
||||
|
||||
text = response['body'].decode()
|
||||
pending_token = await self._extract_auth_token(text, email_account, self.email_manager)
|
||||
if not pending_token:
|
||||
raise RegisterError("Failed to extract auth token")
|
||||
|
||||
logger.debug(f"第一次请求完成 - pending_token: {pending_token[:10]}...")
|
||||
return email, pending_token, cursor_password
|
||||
|
||||
async def _verify_code(
|
||||
self,
|
||||
proxy: str,
|
||||
token: str,
|
||||
code: str,
|
||||
pending_token: str,
|
||||
email: str,
|
||||
session_id: str
|
||||
) -> str:
|
||||
"""验证码验证请求"""
|
||||
logger.debug(f"开始验证码验证 - 邮箱: {email}, 验证码: {code}")
|
||||
|
||||
boundary = "----WebKitFormBoundaryqEBf0rEYwwb9aUoF"
|
||||
headers = {
|
||||
"accept": "text/x-component",
|
||||
"content-type": f"multipart/form-data; boundary={boundary}",
|
||||
"next-action": "e75011da58d295bef5aa55740d0758a006468655",
|
||||
"origin": "https://authenticator.cursor.sh",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
params = {
|
||||
"email": email,
|
||||
"pending_authentication_token": pending_token,
|
||||
"state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D",
|
||||
"redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
"authorization_session_id": session_id
|
||||
}
|
||||
|
||||
form_data = self.form_builder.build_verify_form(
|
||||
boundary=boundary,
|
||||
email=email,
|
||||
token=token,
|
||||
code=code,
|
||||
pending_token=pending_token,
|
||||
)
|
||||
|
||||
response = await self.fetch_manager.request(
|
||||
"POST",
|
||||
"https://authenticator.cursor.sh/email-verification",
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=form_data,
|
||||
proxy=proxy
|
||||
)
|
||||
|
||||
redirect_url = response.get('headers', {}).get('x-action-redirect')
|
||||
if not redirect_url:
|
||||
raise RegisterError("未找到重定向URL,响应头: %s" % json.dumps(response.get('headers')))
|
||||
|
||||
return redirect_url
|
||||
|
||||
async def _callback(self, proxy: str, redirect_url: str) -> str:
|
||||
"""Callback请求"""
|
||||
logger.debug(f"开始callback请求 - URL: {redirect_url[:50]}...")
|
||||
|
||||
parsed = urlparse(redirect_url)
|
||||
code = parse_qs(parsed.query)['code'][0]
|
||||
logger.debug(f"从URL提取的code: {code[:10]}...")
|
||||
|
||||
headers = {
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
"accept-language": "zh-CN,zh;q=0.9",
|
||||
"sec-fetch-dest": "document",
|
||||
"sec-fetch-mode": "navigate",
|
||||
"sec-fetch-site": "cross-site",
|
||||
"upgrade-insecure-requests": "1",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
callback_url = "https://www.cursor.com/api/auth/callback"
|
||||
params = {
|
||||
"code": code,
|
||||
"state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D"
|
||||
}
|
||||
|
||||
response = await self.fetch_manager.request(
|
||||
"GET",
|
||||
callback_url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
proxy=proxy,
|
||||
allow_redirects=False
|
||||
)
|
||||
|
||||
if 'error' in response:
|
||||
raise RegisterError(f"Callback request failed: {response['error']}")
|
||||
|
||||
cookies = response['headers'].get('set-cookie')
|
||||
if cookies:
|
||||
logger.debug("成功获取到cookies")
|
||||
else:
|
||||
logger.error("未获取到cookies")
|
||||
return cookies
|
||||
|
||||
async def _get_verification_code_with_retry(self, email: str, refresh_token: str, client_id: str, max_retries: int = 3) -> Optional[str]:
|
||||
"""带重试的验证码获取"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
code = await self.email_manager.get_verification_code(
|
||||
email, refresh_token, client_id
|
||||
)
|
||||
if code:
|
||||
return code
|
||||
await asyncio.sleep(2) # 短暂延迟后重试
|
||||
except Exception as e:
|
||||
logger.warning(f"第 {attempt + 1} 次获取验证码失败: {str(e)}")
|
||||
if attempt == max_retries - 1: # 最后一次尝试
|
||||
return None
|
||||
await asyncio.sleep(2) # 失败后等待更长时间
|
||||
return None
|
||||
27
requirements.txt
Normal file
27
requirements.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
# HTTP related
|
||||
aiohttp
|
||||
requests
|
||||
curl_cffi
|
||||
|
||||
# Email processing
|
||||
aioimaplib
|
||||
|
||||
# Type hints and data structures
|
||||
dataclasses
|
||||
typing
|
||||
|
||||
# Config file processing
|
||||
pyyaml
|
||||
|
||||
# Async support
|
||||
asyncio
|
||||
|
||||
|
||||
# Utils
|
||||
python-dateutil
|
||||
|
||||
# Database
|
||||
aiosqlite
|
||||
|
||||
# Logging
|
||||
loguru==0.7.2
|
||||
136
reset_extracted.py
Normal file
136
reset_extracted.py
Normal 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
106
reset_retrying.py
Normal 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
200
retry_failed.py
Normal 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
66
reupload_all.py
Normal 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
11
services/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .email_manager import EmailManager
|
||||
from .fetch_manager import FetchManager
|
||||
from .proxy_pool import ProxyPool
|
||||
from .token_pool import TokenPool
|
||||
|
||||
__all__ = [
|
||||
'FetchManager',
|
||||
'ProxyPool',
|
||||
'TokenPool',
|
||||
'EmailManager'
|
||||
]
|
||||
93
services/capsolver.py
Normal file
93
services/capsolver.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
from typing import Optional
|
||||
import time
|
||||
|
||||
class Capsolver:
|
||||
def __init__(self, api_key: str, website_url: str, website_key: str):
|
||||
self.api_key = api_key
|
||||
self.website_url = website_url
|
||||
self.website_key = website_key
|
||||
self.base_url = "https://api.capsolver.com"
|
||||
|
||||
async def create_task(self) -> Optional[str]:
|
||||
"""创建验证码任务"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"clientKey": self.api_key,
|
||||
"task": {
|
||||
"type": "AntiTurnstileTaskProxyLess",
|
||||
"websiteURL": self.website_url,
|
||||
"websiteKey": self.website_key,
|
||||
}
|
||||
}
|
||||
|
||||
async with session.post(f"{self.base_url}/createTask", json=payload) as resp:
|
||||
result = await resp.json()
|
||||
if result.get("errorId") > 0:
|
||||
logger.error(f"创建任务失败: {result.get('errorDescription')}")
|
||||
return None
|
||||
return result.get("taskId")
|
||||
|
||||
async def get_task_result(self, task_id: str) -> Optional[dict]:
|
||||
"""获取任务结果"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"clientKey": self.api_key,
|
||||
"taskId": task_id
|
||||
}
|
||||
|
||||
async with session.post(f"{self.base_url}/getTaskResult", json=payload) as resp:
|
||||
result = await resp.json()
|
||||
if result.get("errorId") > 0:
|
||||
logger.error(f"获取结果失败: {result.get('errorDescription')}")
|
||||
return None
|
||||
|
||||
if result.get("status") == "ready":
|
||||
return result.get("solution", {})
|
||||
return None
|
||||
|
||||
async def solve_turnstile(self) -> Optional[str]:
|
||||
"""
|
||||
解决 Turnstile 验证码
|
||||
"""
|
||||
task_id = await self.create_task()
|
||||
if not task_id:
|
||||
raise Exception("创建验证码任务失败")
|
||||
|
||||
# 增加重试次数限制和超时时间控制
|
||||
max_retries = 5 # 减少最大重试次数
|
||||
retry_delay = 2 # 设置重试间隔为2秒
|
||||
timeout = 15 # 设置总超时时间为15秒
|
||||
|
||||
start_time = time.time()
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
logger.debug(f"第 {attempt} 次尝试获取验证码结果")
|
||||
result = await self.get_task_result(task_id)
|
||||
|
||||
if result and "token" in result:
|
||||
token = result["token"]
|
||||
logger.success(f"成功获取验证码 token: {token[:40]}...")
|
||||
return token
|
||||
|
||||
# 检查是否超时
|
||||
if time.time() - start_time > timeout:
|
||||
logger.error("验证码请求总时间超过15秒")
|
||||
break
|
||||
|
||||
await asyncio.sleep(retry_delay)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取验证码结果失败: {str(e)}")
|
||||
if attempt == max_retries:
|
||||
raise
|
||||
|
||||
if time.time() - start_time > timeout:
|
||||
logger.error("验证码请求总时间超过15秒")
|
||||
break
|
||||
|
||||
await asyncio.sleep(retry_delay)
|
||||
|
||||
raise Exception("验证码解决失败: 达到最大重试次数或超时")
|
||||
245
services/email_manager.py
Normal file
245
services/email_manager.py
Normal file
@@ -0,0 +1,245 @@
|
||||
import asyncio
|
||||
import email
|
||||
from dataclasses import dataclass
|
||||
from email.header import decode_header, make_header
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
from core.exceptions import EmailError
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmailAccount:
|
||||
id: int
|
||||
email: str
|
||||
password: str # 这里实际上是 refresh_token
|
||||
client_id: str
|
||||
refresh_token: str
|
||||
in_use: bool = False
|
||||
cursor_password: Optional[str] = None
|
||||
cursor_cookie: Optional[str] = None
|
||||
sold: bool = False
|
||||
status: str = 'pending' # 新增状态字段: pending, unavailable, success
|
||||
|
||||
|
||||
class EmailManager:
|
||||
def __init__(self, config: Config, db_manager: DatabaseManager):
|
||||
self.config = config
|
||||
self.db = db_manager
|
||||
self.verification_subjects = [
|
||||
"Verify your email address",
|
||||
"Complete code challenge",
|
||||
]
|
||||
|
||||
async def batch_get_accounts(self, num: int) -> List[EmailAccount]:
|
||||
"""批量获取未使用的邮箱账号"""
|
||||
logger.info(f"尝试获取 {num} 个未使用的邮箱账号")
|
||||
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET in_use = 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id IN (
|
||||
SELECT id FROM email_accounts
|
||||
WHERE in_use = 0 AND sold = 0 AND status = 'pending'
|
||||
LIMIT ?
|
||||
)
|
||||
RETURNING id, email, password, client_id, refresh_token
|
||||
'''
|
||||
|
||||
results = await self.db.fetch_all(query, (num,))
|
||||
logger.debug(f"实际获取到 {len(results)} 个账号")
|
||||
return [
|
||||
EmailAccount(
|
||||
id=row[0],
|
||||
email=row[1],
|
||||
password=row[2],
|
||||
client_id=row[3],
|
||||
refresh_token=row[4],
|
||||
in_use=True
|
||||
)
|
||||
for row in results
|
||||
]
|
||||
|
||||
async def update_account_status(self, account_id: int, status: str):
|
||||
"""更新账号状态"""
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET
|
||||
status = ?,
|
||||
in_use = 0,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
'''
|
||||
await self.db.execute(query, (status, account_id))
|
||||
|
||||
async def update_account(self, account_id: int, cursor_password: str, cursor_cookie: str, cursor_token: str):
|
||||
"""更新账号信息"""
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET
|
||||
cursor_password = ?,
|
||||
cursor_cookie = ?,
|
||||
cursor_token = ?,
|
||||
in_use = 0,
|
||||
sold = 1,
|
||||
status = 'success',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
'''
|
||||
await self.db.execute(query, (cursor_password, cursor_cookie, cursor_token, account_id))
|
||||
|
||||
async def release_account(self, account_id: int):
|
||||
"""释放账号"""
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET in_use = 0, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
'''
|
||||
await self.db.execute(query, (account_id,))
|
||||
|
||||
async def _get_access_token(self, client_id: str, refresh_token: str) -> str:
|
||||
"""获取微软 access token"""
|
||||
logger.debug(f"开始获取 access token - client_id: {client_id}")
|
||||
|
||||
url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
|
||||
data = {
|
||||
'client_id': client_id,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refresh_token,
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, data=data) as response:
|
||||
result = await response.json()
|
||||
|
||||
if 'error' in result:
|
||||
error = result.get('error')
|
||||
logger.error(f"获取 access token 失败: {error}")
|
||||
raise EmailError(f"Failed to get access token: {error}")
|
||||
|
||||
access_token = result['access_token']
|
||||
logger.debug("成功获取 access token")
|
||||
return access_token
|
||||
|
||||
async def get_verification_code(self, email: str, refresh_token: str, client_id: str) -> str:
|
||||
"""获取验证码"""
|
||||
logger.info(f"开始获取邮箱验证码 - {email}")
|
||||
try:
|
||||
# 1. 获取 access token
|
||||
access_token = await self._get_access_token(client_id, refresh_token)
|
||||
logger.debug(f"[{email}] 获取 access token 成功")
|
||||
|
||||
# 2. 构建认证字符串
|
||||
auth_string = f"user={email}\1auth=Bearer {access_token}\1\1"
|
||||
logger.debug(f"[{email}] 认证字符串构建完成")
|
||||
|
||||
# 3. 连接邮箱
|
||||
import imaplib
|
||||
mail = imaplib.IMAP4_SSL('outlook.live.com')
|
||||
mail.authenticate('XOAUTH2', lambda x: auth_string)
|
||||
mail.select('inbox')
|
||||
logger.debug(f"[{email}] 邮箱连接成功")
|
||||
|
||||
# 4. 等待并获取验证码邮件
|
||||
for i in range(15):
|
||||
logger.debug(f"[{email}] 第 {i + 1} 次尝试获取验证码")
|
||||
|
||||
# 搜索来自 no-reply@cursor.sh 的最新邮件
|
||||
result, data = mail.search(None, '(FROM "no-reply@cursor.sh")')
|
||||
if result != "OK" or not data[0]:
|
||||
logger.debug(f"[{email}] 未找到来自 cursor 的邮件,等待1秒后重试")
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
mail_ids = data[0].split()
|
||||
if not mail_ids:
|
||||
logger.debug(f"[{email}] 邮件ID列表为空,等待1秒后重试")
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
# 获取最新的3封邮件
|
||||
last_mail_ids = sorted(mail_ids, reverse=True)[:3]
|
||||
|
||||
for mail_id in last_mail_ids:
|
||||
result, msg_data = mail.fetch(mail_id, "(RFC822)")
|
||||
if result != 'OK':
|
||||
logger.warning(f"[{email}] 获取邮件内容失败: {result}")
|
||||
continue
|
||||
|
||||
# 确保 msg_data 不为空且格式正确
|
||||
if not msg_data or not msg_data[0] or len(msg_data[0]) < 2:
|
||||
logger.warning(f"[{email}] 邮件数据格式不正确")
|
||||
continue
|
||||
|
||||
# 正确导入 email 模块
|
||||
from email import message_from_bytes
|
||||
email_message = message_from_bytes(msg_data[0][1])
|
||||
|
||||
# 检查发件人
|
||||
from_addr = str(make_header(decode_header(email_message['From'])))
|
||||
if 'no-reply@cursor.sh' not in from_addr:
|
||||
logger.debug(f"[{email}] 跳过非 Cursor 邮件,发件人: {from_addr}")
|
||||
continue
|
||||
|
||||
# 检查主题
|
||||
subject = str(make_header(decode_header(email_message['SUBJECT'])))
|
||||
if not any(verify_subject in subject for verify_subject in self.verification_subjects):
|
||||
logger.debug(f"[{email}] 跳过非验证码邮件,主题: {subject}")
|
||||
continue
|
||||
|
||||
code = self._extract_code_from_email(email_message)
|
||||
if code:
|
||||
logger.debug(f"[{email}] 成功获取验证码: {code}")
|
||||
mail.close()
|
||||
mail.logout()
|
||||
return code
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
logger.error(f"[{email}] 验证码邮件未收到")
|
||||
raise EmailError("Verification code not received")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{email}] 获取验证码失败: {str(e)}")
|
||||
raise EmailError(f"Failed to get verification code: {str(e)}")
|
||||
|
||||
def _extract_code_from_email(self, email_message) -> Optional[str]:
|
||||
"""从邮件内容中提取验证码"""
|
||||
try:
|
||||
# 获取邮件内容
|
||||
if email_message.is_multipart():
|
||||
for part in email_message.walk():
|
||||
if part.get_content_type() == "text/html":
|
||||
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||||
break
|
||||
else:
|
||||
body = email_message.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||||
|
||||
# 提取6位数字验证码
|
||||
import re
|
||||
|
||||
# 在HTML中查找包含6位数字的div
|
||||
match = re.search(r'<div[^>]*>(\d{6})</div>', body)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
logger.debug(f"从HTML中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
# 备用方案:搜索任何6位数字
|
||||
match = re.search(r'\b\d{6}\b', body)
|
||||
if match:
|
||||
code = match.group(0)
|
||||
logger.debug(f"从文本中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
logger.warning(f"[{email}] 未能从邮件中提取到验证码")
|
||||
logger.debug(f"[{email}] 邮件内容预览: " + body[:200])
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{email}] 提取验证码失败: {str(e)}")
|
||||
return None
|
||||
46
services/fetch_manager.py
Normal file
46
services/fetch_manager.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import asyncio
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
|
||||
from .fetch_service import FetchService
|
||||
|
||||
|
||||
class FetchManager:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.fetch_service = FetchService()
|
||||
self.semaphore = asyncio.Semaphore(config.global_config.max_concurrency)
|
||||
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
proxy: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
使用信号量控制并发的请求方法
|
||||
"""
|
||||
async with self.semaphore:
|
||||
for _ in range(self.config.global_config.retry_times):
|
||||
try:
|
||||
response = await self.fetch_service.request(
|
||||
method=method,
|
||||
url=url,
|
||||
proxy=proxy,
|
||||
timeout=self.config.global_config.timeout,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if 'error' not in response:
|
||||
return response
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"请求超时,正在重试: {url}")
|
||||
continue
|
||||
|
||||
logger.error(f"达到最大重试次数: {url}")
|
||||
return {'error': 'Max retries exceeded'}
|
||||
80
services/fetch_service.py
Normal file
80
services/fetch_service.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from curl_cffi.requests import AsyncSession
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class FetchService:
|
||||
def __init__(self):
|
||||
self.default_headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd"
|
||||
}
|
||||
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
headers: Optional[Dict] = None,
|
||||
params: Optional[Dict] = None,
|
||||
data: Optional[Union[Dict, str]] = None,
|
||||
json: Optional[Dict] = None,
|
||||
cookies: Optional[Dict] = None,
|
||||
proxy: Optional[str] = None,
|
||||
impersonate: str = "chrome124",
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
通用请求方法
|
||||
|
||||
Args:
|
||||
method: 请求方法 (GET, POST 等)
|
||||
url: 请求URL
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
data: 表单数据
|
||||
json: JSON数据
|
||||
cookies: Cookie
|
||||
proxy: 代理地址
|
||||
impersonate: 浏览器仿真类型
|
||||
**kwargs: 其他curl_cffi支持的参数
|
||||
|
||||
Returns:
|
||||
Dict 包含响应信息
|
||||
"""
|
||||
# 合并默认headers
|
||||
request_headers = self.default_headers.copy()
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
|
||||
try:
|
||||
async with AsyncSession(impersonate=impersonate) as session:
|
||||
response = await session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=request_headers,
|
||||
params=params,
|
||||
data=data,
|
||||
json=json,
|
||||
cookies=cookies,
|
||||
proxies={'http': proxy, 'https': proxy} if proxy else None,
|
||||
verify=False,
|
||||
quote=False,
|
||||
stream=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return {
|
||||
'status': response.status_code,
|
||||
'headers': dict(response.headers),
|
||||
'cookies': dict(response.cookies),
|
||||
'body': await response.acontent(),
|
||||
'raw_response': response
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"请求失败: {str(e)}")
|
||||
return {'error': str(e)}
|
||||
38
services/proxy_pool.py
Normal file
38
services/proxy_pool.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import List
|
||||
|
||||
from core.config import Config
|
||||
from core.exceptions import ProxyFetchError
|
||||
|
||||
from .fetch_manager import FetchManager
|
||||
|
||||
|
||||
class ProxyPool:
|
||||
def __init__(self, config: Config, fetch_manager: FetchManager):
|
||||
self.config = config
|
||||
self.fetch_manager = fetch_manager
|
||||
|
||||
async def batch_get(self, num: int) -> List[str]:
|
||||
"""获取num个代理"""
|
||||
# 临时代理
|
||||
return ['http://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
96
services/token_pool.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import asyncio
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.exceptions import TokenGenerationError
|
||||
from services.yescaptcha import TurnstileConfig, YesCaptcha
|
||||
from services.capsolver import Capsolver
|
||||
|
||||
|
||||
class TokenPool:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
|
||||
if config.captcha_config.provider == "capsolver":
|
||||
self.solver = Capsolver(
|
||||
api_key=config.captcha_config.capsolver.api_key,
|
||||
website_url=config.captcha_config.capsolver.website_url,
|
||||
website_key=config.captcha_config.capsolver.website_key
|
||||
)
|
||||
else:
|
||||
self.turnstile_config = TurnstileConfig(
|
||||
client_key=config.captcha_config.yescaptcha.client_key,
|
||||
website_url=config.captcha_config.yescaptcha.website_url,
|
||||
website_key=config.captcha_config.yescaptcha.website_key,
|
||||
use_cn_server=config.captcha_config.yescaptcha.use_cn_server
|
||||
)
|
||||
self.solver = YesCaptcha(self.turnstile_config)
|
||||
|
||||
async def _get_token(self) -> str:
|
||||
"""获取单个token"""
|
||||
try:
|
||||
if isinstance(self.solver, Capsolver):
|
||||
# Capsolver 是异步的,直接调用
|
||||
token = await self.solver.solve_turnstile()
|
||||
else:
|
||||
# YesCaptcha 是同步的,需要转换
|
||||
token = await asyncio.to_thread(self.solver.solve_turnstile)
|
||||
|
||||
if not token:
|
||||
raise TokenGenerationError("Failed to get token")
|
||||
return token
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取 token 失败: {str(e)}")
|
||||
raise TokenGenerationError(f"Failed to get token: {str(e)}")
|
||||
|
||||
async def get_token_pair(self) -> Tuple[str, str]:
|
||||
"""获取一对token"""
|
||||
token1 = await self._get_token()
|
||||
token2 = await self._get_token()
|
||||
return token1, token2
|
||||
|
||||
async def batch_generate(self, num: int) -> List[Tuple[str, str]]:
|
||||
"""批量生成token对
|
||||
|
||||
Args:
|
||||
num: 需要的token对数量
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str]]: token对列表,每个元素是(token1, token2)
|
||||
"""
|
||||
logger.info(f"开始批量生成 {num} 对 token")
|
||||
|
||||
# 创建所有token获取任务
|
||||
tasks = []
|
||||
for _ in range(num * 2): # 每对需要两个token
|
||||
tasks.append(self._get_token())
|
||||
|
||||
# 并发执行所有任务
|
||||
try:
|
||||
tokens = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 过滤出成功的token(仅保留字符串类型)
|
||||
valid_tokens = [
|
||||
token for token in tokens
|
||||
if isinstance(token, str) and token.startswith('0.')
|
||||
]
|
||||
|
||||
# 将token分组为对
|
||||
token_pairs = []
|
||||
for i in range(0, num * 2, 2):
|
||||
try:
|
||||
pair = (valid_tokens[i], valid_tokens[i+1])
|
||||
token_pairs.append(pair)
|
||||
except IndexError:
|
||||
logger.error(f"生成token对时索引越界,i={i}, tokens数量={len(valid_tokens)}")
|
||||
break
|
||||
|
||||
logger.success(f"成功生成 {len(token_pairs)} 对 token")
|
||||
return token_pairs
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量生成 token 失败: {str(e)}")
|
||||
return []
|
||||
32
services/uuid.py
Normal file
32
services/uuid.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
class ULID:
|
||||
def __init__(self):
|
||||
# 定义字符集,使用Crockford's Base32字符集
|
||||
self.encoding = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
|
||||
def generate(self) -> str:
|
||||
# 获取当前时间戳(毫秒)
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
# 生成随机数部分
|
||||
randomness = random.getrandbits(80) # 80位随机数
|
||||
|
||||
# 转换时间戳为base32字符串(10个字符)
|
||||
time_chars = []
|
||||
for _ in range(10):
|
||||
timestamp, mod = divmod(timestamp, 32)
|
||||
time_chars.append(self.encoding[mod])
|
||||
time_chars.reverse()
|
||||
|
||||
# 转换随机数为base32字符串(16个字符)
|
||||
random_chars = []
|
||||
for _ in range(16):
|
||||
randomness, mod = divmod(randomness, 32)
|
||||
random_chars.append(self.encoding[mod])
|
||||
random_chars.reverse()
|
||||
|
||||
# 组合最终结果
|
||||
return ''.join(time_chars + random_chars)
|
||||
117
services/yescaptcha.py
Normal file
117
services/yescaptcha.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnstileConfig:
|
||||
client_key: str
|
||||
website_url: str
|
||||
website_key: str
|
||||
use_cn_server: bool = True
|
||||
|
||||
|
||||
class YesCaptcha:
|
||||
API_URL_GLOBAL = "https://api.yescaptcha.com"
|
||||
API_URL_CN = "https://cn.yescaptcha.com"
|
||||
|
||||
def __init__(self, config: TurnstileConfig):
|
||||
self.config = config
|
||||
self.base_url = self.API_URL_CN if config.use_cn_server else self.API_URL_GLOBAL
|
||||
logger.debug(f"YesCaptcha 初始化 - 使用{'国内' if config.use_cn_server else '国际'}服务器")
|
||||
|
||||
def create_task(self, task_type: str = "TurnstileTaskProxyless") -> Dict:
|
||||
"""
|
||||
Create a new Turnstile solving task
|
||||
|
||||
Args:
|
||||
task_type: Either "TurnstileTaskProxyless" (25 points) or "TurnstileTaskProxylessM1" (30 points)
|
||||
|
||||
Returns:
|
||||
Dict containing task ID if successful
|
||||
"""
|
||||
url = f"{self.base_url}/createTask"
|
||||
logger.debug(f"创建验证任务 - 类型: {task_type}")
|
||||
|
||||
payload = {
|
||||
"clientKey": self.config.client_key,
|
||||
"task": {
|
||||
"type": task_type,
|
||||
"websiteURL": self.config.website_url,
|
||||
"websiteKey": self.config.website_key
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errorId", 1) != 0:
|
||||
logger.error(f"创建任务失败: {result.get('errorDescription')}")
|
||||
else:
|
||||
logger.debug(f"创建任务成功 - TaskID: {result.get('taskId')}")
|
||||
|
||||
return result
|
||||
|
||||
def get_task_result(self, task_id: str) -> Dict:
|
||||
"""
|
||||
Get the result of a task
|
||||
|
||||
Args:
|
||||
task_id: Task ID from create_task
|
||||
|
||||
Returns:
|
||||
Dict containing task result if successful
|
||||
"""
|
||||
url = f"{self.base_url}/getTaskResult"
|
||||
logger.debug(f"获取任务结果 - TaskID: {task_id}")
|
||||
|
||||
payload = {
|
||||
"clientKey": self.config.client_key,
|
||||
"taskId": task_id
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errorId", 1) != 0:
|
||||
logger.error(f"获取结果失败: {result.get('errorDescription')}")
|
||||
elif result.get("status") == "ready":
|
||||
logger.debug("成功获取到结果")
|
||||
|
||||
return result
|
||||
|
||||
def solve_turnstile(self, max_attempts: int = 60) -> Optional[str]:
|
||||
"""
|
||||
Complete turnstile solving process
|
||||
|
||||
Args:
|
||||
max_attempts: Maximum number of attempts to get result
|
||||
|
||||
Returns:
|
||||
Token string if successful, None otherwise
|
||||
"""
|
||||
# 创建任务
|
||||
create_result = self.create_task()
|
||||
if create_result.get("errorId", 1) != 0:
|
||||
return None
|
||||
|
||||
task_id = create_result.get("taskId")
|
||||
if not task_id:
|
||||
return None
|
||||
|
||||
# 轮询获取结果
|
||||
for _ in range(max_attempts):
|
||||
result = self.get_task_result(task_id)
|
||||
|
||||
if result.get("status") == "ready":
|
||||
return result.get("solution", {}).get("token")
|
||||
|
||||
if result.get("errorId", 1) != 0:
|
||||
return None
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
return None
|
||||
71
test_api.py
Normal file
71
test_api.py
Normal 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
448
upload_accounts.py
Normal 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())
|
||||
Reference in New Issue
Block a user