diff --git a/.gitignore b/.gitignore index c497214..9ff4321 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ autogptplus_drission.py autogptplus_drission_oai.py accounts.json team-reg-go -CodexAuth \ No newline at end of file +CodexAuth +.agent/rules/use.md diff --git a/auto_gpt_team.py b/auto_gpt_team.py index 60dc1ff..1e5ae33 100644 --- a/auto_gpt_team.py +++ b/auto_gpt_team.py @@ -53,6 +53,14 @@ except ImportError: StripePaymentAPI = None api_payment_with_retry = None +# 导入代理池模块 +try: + import proxy_pool + PROXY_POOL_AVAILABLE = True +except ImportError: + proxy_pool = None + PROXY_POOL_AVAILABLE = False + # ================= 配置加载 ================= try: import tomllib @@ -2306,8 +2314,13 @@ def run_single_registration_api(progress_callback=None, step_callback=None, prox if not ibans: return {"success": False, "error": "没有可用的 IBAN,请先通过 /iban_add 导入"} - # 使用配置的代理或传入的代理 - use_proxy = proxy or API_PROXY or None + # 使用代理: 优先传入参数 > 代理池轮询 > 配置静态代理 + if proxy: + use_proxy = proxy + elif PROXY_POOL_AVAILABLE and proxy_pool.get_proxy_count() > 0: + use_proxy = proxy_pool.get_next_proxy() + else: + use_proxy = API_PROXY or None if use_proxy: log_status("代理", f"使用代理: {use_proxy}") @@ -2428,13 +2441,14 @@ def run_single_registration_api(progress_callback=None, step_callback=None, prox return {"success": False, "error": f"重试 {max_user_retries} 次后仍失败: {last_error}"} -def run_single_registration_auto(progress_callback=None, step_callback=None, mode: str = None) -> dict: +def run_single_registration_auto(progress_callback=None, step_callback=None, mode: str = None, proxy: str = None) -> dict: """自动选择模式执行注册 Args: progress_callback: 进度回调 step_callback: 步骤回调 mode: 强制指定模式 ("browser" / "api"),None 则使用配置 + proxy: 指定代理地址,None 则由代理池或配置决定 Returns: dict: 注册结果 @@ -2445,7 +2459,7 @@ def run_single_registration_auto(progress_callback=None, step_callback=None, mod if not API_MODE_AVAILABLE: log_status("警告", "协议模式不可用,回退到浏览器模式") return run_single_registration(progress_callback, step_callback) - return run_single_registration_api(progress_callback, step_callback) + return run_single_registration_api(progress_callback, step_callback, proxy=proxy) else: return run_single_registration(progress_callback, step_callback) diff --git a/proxy.txt b/proxy.txt new file mode 100644 index 0000000..e69de29 diff --git a/proxy_pool.py b/proxy_pool.py new file mode 100644 index 0000000..b4ce1d5 --- /dev/null +++ b/proxy_pool.py @@ -0,0 +1,312 @@ +""" +代理池管理模块 +- 从 proxy.txt 加载代理 +- 并发测试代理可用性 +- 线程安全的轮询分配 +""" + +import os +import time +import threading +import concurrent.futures +from pathlib import Path +from urllib.parse import urlparse + +# 尝试导入 curl_cffi (更好的指纹伪装) +try: + from curl_cffi import requests as curl_requests + CURL_AVAILABLE = True +except ImportError: + curl_requests = None + CURL_AVAILABLE = False + +import requests + +BASE_DIR = Path(__file__).parent +PROXY_FILE = BASE_DIR / "proxy.txt" + +# 测试目标 URL +TEST_URL = "https://api.openai.com/v1/models" +TEST_TIMEOUT = 10 # 秒 + + +def parse_proxy_url(proxy_url: str) -> dict | None: + """解析代理 URL,返回结构化信息 + + 支持格式: + http://host:port + http://username:password@host:port + socks5://host:port + socks5://username:password@host:port + + Returns: + dict: {"url": str, "scheme": str, "host": str, "port": int, "username": str, "password": str} + None: 格式无效 + """ + proxy_url = proxy_url.strip() + if not proxy_url: + return None + + # 确保有 scheme + if not proxy_url.startswith(("http://", "https://", "socks5://", "socks4://")): + proxy_url = "http://" + proxy_url + + try: + parsed = urlparse(proxy_url) + if not parsed.hostname or not parsed.port: + return None + + return { + "url": proxy_url, + "scheme": parsed.scheme, + "host": parsed.hostname, + "port": parsed.port, + "username": parsed.username or "", + "password": parsed.password or "", + } + except Exception: + return None + + +def load_proxies() -> list[str]: + """从 proxy.txt 加载代理列表 + + Returns: + list[str]: 代理 URL 列表 + """ + if not PROXY_FILE.exists(): + return [] + + proxies = [] + try: + with open(PROXY_FILE, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + # 跳过空行和注释 + if not line or line.startswith("#"): + continue + # 验证格式 + parsed = parse_proxy_url(line) + if parsed: + proxies.append(parsed["url"]) + except Exception: + pass + + return proxies + + +def save_proxies(proxies: list[str]): + """保存代理列表到 proxy.txt (保留文件头部注释) + + Args: + proxies: 代理 URL 列表 + """ + header_lines = [] + + # 读取原文件头部注释 + if PROXY_FILE.exists(): + try: + with open(PROXY_FILE, "r", encoding="utf-8") as f: + for line in f: + if line.strip().startswith("#") or not line.strip(): + header_lines.append(line.rstrip()) + else: + break + except Exception: + pass + + try: + with open(PROXY_FILE, "w", encoding="utf-8") as f: + # 写入头部注释 + if header_lines: + f.write("\n".join(header_lines) + "\n") + # 写入代理 + for proxy in proxies: + f.write(proxy + "\n") + except Exception as e: + print(f"[ProxyPool] 保存代理文件失败: {e}") + + +def test_single_proxy(proxy_url: str, timeout: int = TEST_TIMEOUT) -> bool: + """测试单个代理是否可用 + + Args: + proxy_url: 代理 URL + timeout: 超时秒数 + + Returns: + bool: 代理是否可用 + """ + proxies_dict = {"http": proxy_url, "https": proxy_url} + + try: + if CURL_AVAILABLE: + # 使用 curl_cffi (更好的指纹) + resp = curl_requests.head( + TEST_URL, + proxies=proxies_dict, + timeout=timeout, + verify=False, + impersonate="edge", + ) + else: + # 回退到 requests + resp = requests.head( + TEST_URL, + proxies=proxies_dict, + timeout=timeout, + verify=False, + ) + # 任何响应都算成功 (包括 401/403,说明代理本身是通的) + return True + except Exception: + return False + + +class ProxyPool: + """线程安全的代理池""" + + def __init__(self): + self._working_proxies: list[str] = [] + self._index = 0 + self._lock = threading.Lock() + self._last_test_time: float = 0 + self._last_test_results: dict = {} # {total, alive, removed} + + def reload(self) -> int: + """从文件重新加载代理 + + Returns: + int: 加载的代理数量 + """ + with self._lock: + self._working_proxies = load_proxies() + self._index = 0 + return len(self._working_proxies) + + def test_and_clean(self, concurrency: int = 20, timeout: int = TEST_TIMEOUT) -> dict: + """并发测试所有代理,移除不可用的 + + Args: + concurrency: 并发数 + timeout: 单个代理超时秒数 + + Returns: + dict: {"total": int, "alive": int, "removed": int, "duration": float} + """ + # 先从文件加载最新 + all_proxies = load_proxies() + if not all_proxies: + self._last_test_results = {"total": 0, "alive": 0, "removed": 0, "duration": 0} + return self._last_test_results + + total = len(all_proxies) + start_time = time.time() + + # 并发测试 + alive_proxies = [] + dead_proxies = [] + + with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor: + future_to_proxy = { + executor.submit(test_single_proxy, proxy, timeout): proxy + for proxy in all_proxies + } + + for future in concurrent.futures.as_completed(future_to_proxy): + proxy = future_to_proxy[future] + try: + is_alive = future.result() + if is_alive: + alive_proxies.append(proxy) + else: + dead_proxies.append(proxy) + except Exception: + dead_proxies.append(proxy) + + duration = time.time() - start_time + + # 更新工作代理池 + with self._lock: + self._working_proxies = alive_proxies + self._index = 0 + + # 保存存活的代理到文件 (移除死亡代理) + if dead_proxies: + save_proxies(alive_proxies) + + self._last_test_time = time.time() + self._last_test_results = { + "total": total, + "alive": len(alive_proxies), + "removed": len(dead_proxies), + "duration": round(duration, 1), + } + + return self._last_test_results + + def get_next_proxy(self) -> str | None: + """获取下一个代理 (轮询) + + Returns: + str: 代理 URL,池为空时返回 None + """ + with self._lock: + if not self._working_proxies: + return None + proxy = self._working_proxies[self._index % len(self._working_proxies)] + self._index += 1 + return proxy + + def get_proxy_count(self) -> int: + """获取当前可用代理数量""" + with self._lock: + return len(self._working_proxies) + + def get_status(self) -> dict: + """获取代理池状态 + + Returns: + dict: {"count": int, "last_test_time": float, "last_test_results": dict} + """ + with self._lock: + return { + "count": len(self._working_proxies), + "proxies": list(self._working_proxies), + "last_test_time": self._last_test_time, + "last_test_results": self._last_test_results, + } + + +# ============ 全局单例 ============ +_pool = ProxyPool() + + +def get_pool() -> ProxyPool: + """获取全局代理池实例""" + return _pool + + +def reload_proxies() -> int: + """重新加载代理""" + return _pool.reload() + + +def test_and_clean_proxies(concurrency: int = 20) -> dict: + """并发测试并清理代理""" + return _pool.test_and_clean(concurrency=concurrency) + + +def get_next_proxy() -> str | None: + """获取下一个代理 (轮询)""" + return _pool.get_next_proxy() + + +def get_proxy_count() -> int: + """获取可用代理数量""" + return _pool.get_proxy_count() + + +def get_proxy_status() -> dict: + """获取代理池状态""" + return _pool.get_status() diff --git a/telegram_bot.py b/telegram_bot.py index f14f4af..557944e 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -197,6 +197,10 @@ class ProvisionerBot: ("schedule", self.cmd_schedule), ("schedule_config", self.cmd_schedule_config), ("schedule_status", self.cmd_schedule_status), + # 代理池管理 + ("proxy_status", self.cmd_proxy_status), + ("proxy_test", self.cmd_proxy_test), + ("proxy_reload", self.cmd_proxy_reload), ] for cmd, handler in handlers: self.app.add_handler(CommandHandler(cmd, handler)) @@ -362,6 +366,10 @@ class ProvisionerBot: BotCommand("schedule", "定时调度器 开关"), BotCommand("schedule_config", "调度器参数配置"), BotCommand("schedule_status", "调度器运行状态"), + # 代理池 + BotCommand("proxy_status", "代理池状态"), + BotCommand("proxy_test", "测试并清理代理"), + BotCommand("proxy_reload", "重新加载代理"), ] try: await self.app.bot.set_my_commands(commands) @@ -451,6 +459,11 @@ class ProvisionerBot: /schedule_config - 配置调度器参数 /schedule_status - 查看调度器运行状态 +🌐 代理池管理: +/proxy_status - 查看代理池状态 +/proxy_test - 测试并清理不可用代理 +/proxy_reload - 从 proxy.txt 重新加载代理 + 💡 示例: /list - 查看所有待处理账号 /run 0 - 处理第一个 Team @@ -3949,6 +3962,45 @@ class ProvisionerBot: import json import threading + # ===== 代理池预检测 ===== + try: + import proxy_pool + pool_count = proxy_pool.get_proxy_count() + if pool_count == 0: + # 尝试加载 + pool_count = proxy_pool.reload_proxies() + + if pool_count > 0: + await self.app.bot.send_message( + chat_id, + f"🌐 代理池预检测\n\n" + f"正在测试 {pool_count} 个代理 (20 并发)...", + parse_mode="HTML" + ) + loop = asyncio.get_event_loop() + test_result = await loop.run_in_executor( + self.executor, + lambda: proxy_pool.test_and_clean_proxies(concurrency=20) + ) + await self.app.bot.send_message( + chat_id, + f"✅ 代理池就绪\n\n" + f"总计: {test_result['total']} | " + f"存活: {test_result['alive']} | " + f"移除: {test_result['removed']}\n" + f"耗时: {test_result['duration']}s", + parse_mode="HTML" + ) + if test_result['alive'] == 0: + await self.app.bot.send_message( + chat_id, + "⚠️ 所有代理都不可用,将使用直连或静态代理", + ) + except ImportError: + pass + except Exception as e: + log.warning(f"代理池预检测异常: {e}") + results = [] success_count = 0 fail_count = 0 @@ -6332,6 +6384,126 @@ class ProvisionerBot: except Exception as e: await update.message.reply_text(f"❌ 添加 IBAN 失败: {e}") + # ==================== 代理池管理 ==================== + + @admin_only + async def cmd_proxy_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """查看代理池状态""" + try: + import proxy_pool + + status = proxy_pool.get_proxy_status() + count = status["count"] + last_test = status["last_test_results"] + + lines = [f"🌐 代理池状态\n"] + lines.append(f"可用代理: {count} 个") + + if last_test: + lines.append(f"\n上次测试:") + lines.append(f" 总计: {last_test.get('total', 0)}") + lines.append(f" 存活: {last_test.get('alive', 0)}") + lines.append(f" 移除: {last_test.get('removed', 0)}") + lines.append(f" 耗时: {last_test.get('duration', 0)}s") + + if status["last_test_time"]: + from datetime import datetime + test_time = datetime.fromtimestamp(status["last_test_time"]) + lines.append(f" 时间: {test_time.strftime('%H:%M:%S')}") + + if count > 0: + # 显示前5个代理 (脱敏) + lines.append(f"\n代理列表 (前 5 个):") + for i, proxy in enumerate(status["proxies"][:5]): + # 脱敏: 隐藏密码部分 + display = proxy + if "@" in proxy: + parts = proxy.split("@") + scheme_auth = parts[0] + host_part = parts[1] + # 取 scheme 部分 + if "://" in scheme_auth: + scheme = scheme_auth.split("://")[0] + display = f"{scheme}://***@{host_part}" + else: + display = f"***@{host_part}" + lines.append(f" {i+1}. {display}") + if count > 5: + lines.append(f" ... 还有 {count - 5} 个") + else: + lines.append(f"\n💡 将代理添加到 proxy.txt 然后使用 /proxy_reload 加载") + + await update.message.reply_text("\n".join(lines), parse_mode="HTML") + + except ImportError: + await update.message.reply_text("❌ proxy_pool 模块未找到") + except Exception as e: + await update.message.reply_text(f"❌ 获取代理池状态失败: {e}") + + @admin_only + async def cmd_proxy_test(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """测试并清理代理""" + try: + import proxy_pool + + # 先加载最新 + pool_count = proxy_pool.reload_proxies() + if pool_count == 0: + await update.message.reply_text( + "📭 代理池为空\n\n" + "请将代理添加到 proxy.txt 后重试", + parse_mode="HTML" + ) + return + + msg = await update.message.reply_text( + f"🔄 正在测试代理\n\n" + f"代理数量: {pool_count}\n" + f"并发: 20\n" + f"请稍候...", + parse_mode="HTML" + ) + + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + self.executor, + lambda: proxy_pool.test_and_clean_proxies(concurrency=20) + ) + + await msg.edit_text( + f"✅ 代理测试完成\n\n" + f"总计: {result['total']}\n" + f"存活: {result['alive']} ✅\n" + f"移除: {result['removed']} ❌\n" + f"耗时: {result['duration']}s\n\n" + f"{'💡 不可用代理已从 proxy.txt 中移除' if result['removed'] > 0 else '🎉 所有代理均可用'}", + parse_mode="HTML" + ) + + except ImportError: + await update.message.reply_text("❌ proxy_pool 模块未找到") + except Exception as e: + await update.message.reply_text(f"❌ 代理测试失败: {e}") + + @admin_only + async def cmd_proxy_reload(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """从 proxy.txt 重新加载代理""" + try: + import proxy_pool + + count = proxy_pool.reload_proxies() + await update.message.reply_text( + f"✅ 代理池已重新加载\n\n" + f"已加载: {count} 个代理\n\n" + f"{'💡 使用 /proxy_test 测试代理可用性' if count > 0 else '📭 proxy.txt 为空或不存在'}", + parse_mode="HTML" + ) + + except ImportError: + await update.message.reply_text("❌ proxy_pool 模块未找到") + except Exception as e: + await update.message.reply_text(f"❌ 重新加载失败: {e}") + # ==================== 定时调度器 ==================== @admin_only @@ -6868,6 +7040,39 @@ class ProvisionerBot: import json import threading + # ===== 代理池预检测 ===== + try: + import proxy_pool + pool_count = proxy_pool.get_proxy_count() + if pool_count == 0: + pool_count = proxy_pool.reload_proxies() + + if pool_count > 0: + log.info(f"[Scheduler] 代理池预检测: 测试 {pool_count} 个代理...") + loop = asyncio.get_event_loop() + test_result = await loop.run_in_executor( + self.executor, + lambda: proxy_pool.test_and_clean_proxies(concurrency=20) + ) + log.info(f"[Scheduler] 代理池: 存活 {test_result['alive']}/{test_result['total']},移除 {test_result['removed']}") + + if chat_id: + try: + await self.app.bot.send_message( + chat_id, + f"🌐 代理池预检测\n\n" + f"存活: {test_result['alive']}/{test_result['total']} | " + f"移除: {test_result['removed']} | " + f"耗时: {test_result['duration']}s", + parse_mode="HTML" + ) + except Exception: + pass + except ImportError: + pass + except Exception as e: + log.warning(f"代理池预检测异常: {e}") + results = [] success_count = 0 fail_count = 0