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