diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..01e39bf
--- /dev/null
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/add_extracted_field.py b/add_extracted_field.py
new file mode 100644
index 0000000..2dba09d
--- /dev/null
+++ b/add_extracted_field.py
@@ -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())
\ No newline at end of file
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..2a7c959
--- /dev/null
+++ b/config.yaml
@@ -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
diff --git a/core/__init__.py b/core/__init__.py
new file mode 100644
index 0000000..b56e535
--- /dev/null
+++ b/core/__init__.py
@@ -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'
+]
diff --git a/core/config.py b/core/config.py
new file mode 100644
index 0000000..4c6434d
--- /dev/null
+++ b/core/config.py
@@ -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
+ )
diff --git a/core/database.py b/core/database.py
new file mode 100644
index 0000000..20a2125
--- /dev/null
+++ b/core/database.py
@@ -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()
\ No newline at end of file
diff --git a/core/exceptions.py b/core/exceptions.py
new file mode 100644
index 0000000..af98c5c
--- /dev/null
+++ b/core/exceptions.py
@@ -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
diff --git a/core/logger.py b/core/logger.py
new file mode 100644
index 0000000..36a2bb8
--- /dev/null
+++ b/core/logger.py
@@ -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="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
+ 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="")
+ logger.level("INFO", color="")
+ logger.level("SUCCESS", color="")
+ logger.level("WARNING", color="")
+ logger.level("ERROR", color="")
+
+ return logger
diff --git a/cursor.db-journal b/cursor.db-journal
new file mode 100644
index 0000000..0a7a01b
Binary files /dev/null and b/cursor.db-journal differ
diff --git a/delete_db.py b/delete_db.py
new file mode 100644
index 0000000..15923ac
--- /dev/null
+++ b/delete_db.py
@@ -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())
\ No newline at end of file
diff --git a/import_emails.py b/import_emails.py
new file mode 100644
index 0000000..88b2894
--- /dev/null
+++ b/import_emails.py
@@ -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())
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..8a83ced
--- /dev/null
+++ b/main.py
@@ -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())
diff --git a/menu.py b/menu.py
new file mode 100644
index 0000000..f80422f
--- /dev/null
+++ b/menu.py
@@ -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}")
\ No newline at end of file
diff --git a/read_db.py b/read_db.py
new file mode 100644
index 0000000..3e780d6
--- /dev/null
+++ b/read_db.py
@@ -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 - 显示所有统计信息")
\ No newline at end of file
diff --git a/register/__init__.py b/register/__init__.py
new file mode 100644
index 0000000..4485b90
--- /dev/null
+++ b/register/__init__.py
@@ -0,0 +1,6 @@
+from .register_worker import FormBuilder, RegisterWorker
+
+__all__ = [
+ 'RegisterWorker',
+ 'FormBuilder'
+]
diff --git a/register/register_worker.py b/register/register_worker.py
new file mode 100644
index 0000000..93a3365
--- /dev/null
+++ b/register/register_worker.py
@@ -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
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..48dc6cc
--- /dev/null
+++ b/requirements.txt
@@ -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
diff --git a/reset_extracted.py b/reset_extracted.py
new file mode 100644
index 0000000..755e685
--- /dev/null
+++ b/reset_extracted.py
@@ -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())
\ No newline at end of file
diff --git a/reset_retrying.py b/reset_retrying.py
new file mode 100644
index 0000000..7c5ec06
--- /dev/null
+++ b/reset_retrying.py
@@ -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())
\ No newline at end of file
diff --git a/retry_failed.py b/retry_failed.py
new file mode 100644
index 0000000..0731f19
--- /dev/null
+++ b/retry_failed.py
@@ -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())
\ No newline at end of file
diff --git a/reupload_all.py b/reupload_all.py
new file mode 100644
index 0000000..ee95e8d
--- /dev/null
+++ b/reupload_all.py
@@ -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())
\ No newline at end of file
diff --git a/services/__init__.py b/services/__init__.py
new file mode 100644
index 0000000..7e0bd1d
--- /dev/null
+++ b/services/__init__.py
@@ -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'
+]
diff --git a/services/capsolver.py b/services/capsolver.py
new file mode 100644
index 0000000..35ca147
--- /dev/null
+++ b/services/capsolver.py
@@ -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("验证码解决失败: 达到最大重试次数或超时")
\ No newline at end of file
diff --git a/services/email_manager.py b/services/email_manager.py
new file mode 100644
index 0000000..5705c47
--- /dev/null
+++ b/services/email_manager.py
@@ -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']*>(\d{6})
', 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
\ No newline at end of file
diff --git a/services/fetch_manager.py b/services/fetch_manager.py
new file mode 100644
index 0000000..6dc7061
--- /dev/null
+++ b/services/fetch_manager.py
@@ -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'}
\ No newline at end of file
diff --git a/services/fetch_service.py b/services/fetch_service.py
new file mode 100644
index 0000000..fd78c37
--- /dev/null
+++ b/services/fetch_service.py
@@ -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)}
diff --git a/services/proxy_pool.py b/services/proxy_pool.py
new file mode 100644
index 0000000..612bae1
--- /dev/null
+++ b/services/proxy_pool.py
@@ -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返回格式实现
+ ...
diff --git a/services/token_pool.py b/services/token_pool.py
new file mode 100644
index 0000000..381deb2
--- /dev/null
+++ b/services/token_pool.py
@@ -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 []
diff --git a/services/uuid.py b/services/uuid.py
new file mode 100644
index 0000000..b7b6d7b
--- /dev/null
+++ b/services/uuid.py
@@ -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)
diff --git a/services/yescaptcha.py b/services/yescaptcha.py
new file mode 100644
index 0000000..d2d6fba
--- /dev/null
+++ b/services/yescaptcha.py
@@ -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
diff --git a/test_api.py b/test_api.py
new file mode 100644
index 0000000..481f0cb
--- /dev/null
+++ b/test_api.py
@@ -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字段")
\ No newline at end of file
diff --git a/upload_accounts.py b/upload_accounts.py
new file mode 100644
index 0000000..74708db
--- /dev/null
+++ b/upload_accounts.py
@@ -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())