From 51d03038e035c3605ddef3cfd2a18bd822b2d8dd Mon Sep 17 00:00:00 2001 From: kyx236 Date: Thu, 15 Jan 2026 23:53:33 +0800 Subject: [PATCH] 33 --- browser_automation.py | 72 +++++++++++++------ pyproject.toml | 1 + s2a_service.py | 36 +++++----- telegram_bot.py | 163 ++++++++++++++++++++++++++++++++++++++---- uv.lock | 11 +++ 5 files changed, 232 insertions(+), 51 deletions(-) diff --git a/browser_automation.py b/browser_automation.py index c01fb6e..38c71dc 100644 --- a/browser_automation.py +++ b/browser_automation.py @@ -6,6 +6,7 @@ import time import random import subprocess import os +import platform from contextlib import contextmanager from DrissionPage import ChromiumPage, ChromiumOptions @@ -156,22 +157,27 @@ def log_url_change(page, old_url: str, action: str = None): def cleanup_chrome_processes(): - """清理残留的 Chrome 进程 (Windows)""" + """清理残留的 Chrome 进程 (跨平台支持)""" try: - # 查找并终止残留的 chrome 进程 (仅限无头或调试模式的) - result = subprocess.run( - ['tasklist', '/FI', 'IMAGENAME eq chrome.exe', '/FO', 'CSV'], - capture_output=True, text=True, timeout=5 - ) - - if 'chrome.exe' in result.stdout: - # 只清理可能是自动化残留的进程,不影响用户正常使用的浏览器 - # 通过检查命令行参数来判断 + if platform.system() == "Windows": + # Windows: 使用 tasklist 和 taskkill + result = subprocess.run( + ['tasklist', '/FI', 'IMAGENAME eq chrome.exe', '/FO', 'CSV'], + capture_output=True, text=True, timeout=5 + ) + + if 'chrome.exe' in result.stdout: + subprocess.run( + ['taskkill', '/F', '/IM', 'chromedriver.exe'], + capture_output=True, timeout=5 + ) + log.step("已清理 chromedriver 残留进程") + else: + # Linux/Mac: 使用 pkill subprocess.run( - ['taskkill', '/F', '/IM', 'chromedriver.exe'], + ['pkill', '-f', 'chromedriver'], capture_output=True, timeout=5 ) - log.step("已清理 chromedriver 残留进程") except Exception: pass # 静默处理,不影响主流程 @@ -186,9 +192,10 @@ def init_browser(max_retries: int = BROWSER_MAX_RETRIES) -> ChromiumPage: ChromiumPage: 浏览器实例 """ log.info("初始化浏览器...", icon="browser") - + last_error = None - + is_linux = platform.system() == "Linux" + for attempt in range(max_retries): try: # 首次尝试或重试前清理残留进程 @@ -196,7 +203,7 @@ def init_browser(max_retries: int = BROWSER_MAX_RETRIES) -> ChromiumPage: log.warning(f"浏览器启动重试 ({attempt + 1}/{max_retries})...") cleanup_chrome_processes() time.sleep(BROWSER_RETRY_DELAY) - + co = ChromiumOptions() co.set_argument('--no-first-run') co.set_argument('--disable-infobars') @@ -204,8 +211,31 @@ def init_browser(max_retries: int = BROWSER_MAX_RETRIES) -> ChromiumPage: co.set_argument('--disable-gpu') # 减少资源占用 co.set_argument('--disable-dev-shm-usage') # 避免共享内存问题 co.set_argument('--no-sandbox') # 服务器环境需要 - co.auto_port() # 自动分配端口,确保每次都是新实例 - + + # Linux 服务器特殊配置 + if is_linux: + co.set_argument('--disable-software-rasterizer') + co.set_argument('--disable-extensions') + co.set_argument('--disable-setuid-sandbox') + co.set_argument('--single-process') # 某些 Linux 环境需要 + co.set_argument('--remote-debugging-port=0') # 让系统自动分配端口 + + # 尝试查找 Chrome/Chromium 路径 + chrome_paths = [ + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + '/usr/bin/chromium-browser', + '/usr/bin/chromium', + '/snap/bin/chromium', + ] + for chrome_path in chrome_paths: + if os.path.exists(chrome_path): + co.set_browser_path(chrome_path) + log.step(f"使用浏览器: {chrome_path}") + break + else: + co.auto_port() # Windows 使用自动分配端口 + # 无头模式 (服务器运行) if BROWSER_HEADLESS: co.set_argument('--headless=new') @@ -213,10 +243,10 @@ def init_browser(max_retries: int = BROWSER_MAX_RETRIES) -> ChromiumPage: log.step("启动 Chrome (无头模式)...") else: log.step("启动 Chrome (无痕模式)...") - + # 设置超时 co.set_timeouts(base=PAGE_LOAD_TIMEOUT, page_load=PAGE_LOAD_TIMEOUT * 2) - + page = ChromiumPage(co) log.success("浏览器启动成功") return page @@ -224,10 +254,10 @@ def init_browser(max_retries: int = BROWSER_MAX_RETRIES) -> ChromiumPage: except Exception as e: last_error = e log.warning(f"浏览器启动失败 (尝试 {attempt + 1}/{max_retries}): {e}") - + # 清理可能的残留 cleanup_chrome_processes() - + # 所有重试都失败 log.error(f"浏览器启动失败,已重试 {max_retries} 次: {last_error}") raise last_error diff --git a/pyproject.toml b/pyproject.toml index 591f09d..52e5c0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,4 +11,5 @@ dependencies = [ "rich>=14.2.0", "setuptools>=80.9.0", "tomli>=2.3.0", + "tomli-w>=1.2.0", ] diff --git a/s2a_service.py b/s2a_service.py index 621c223..6bf08ac 100644 --- a/s2a_service.py +++ b/s2a_service.py @@ -512,7 +512,7 @@ def format_dashboard_stats(stats: Dict[str, Any]) -> str: str: 格式化后的文本 """ if not stats: - return "No data available" + return "暂无数据" def fmt_num(n): """格式化数字 (添加千分位)""" @@ -557,28 +557,28 @@ def format_dashboard_stats(stats: Dict[str, Any]) -> str: avg_duration = stats.get("average_duration_ms", 0) lines = [ - "S2A Dashboard", + "📊 S2A 仪表盘", "", - "Accounts", - f" Total: {total_accounts} | Normal: {normal_accounts}", - f" Error: {error_accounts} | RateLimit: {ratelimit_accounts}", + "📦 账号状态", + f" 总计: {total_accounts} | 正常: {normal_accounts}", + f" 异常: {error_accounts} | 限流: {ratelimit_accounts}", "", - "Today", - f" Requests: {fmt_num(today_requests)}", - f" Tokens: {fmt_tokens(today_tokens)}", - f" Input: {fmt_tokens(today_input)} | Output: {fmt_tokens(today_output)}", - f" Cache: {fmt_tokens(today_cache_read)}", - f" Cost: ${fmt_num(today_cost)}", + "📅 今日统计", + f" 请求数: {fmt_num(today_requests)}", + f" Token: {fmt_tokens(today_tokens)}", + f" 输入: {fmt_tokens(today_input)} | 输出: {fmt_tokens(today_output)}", + f" 缓存: {fmt_tokens(today_cache_read)}", + f" 费用: ${fmt_num(today_cost)}", "", - "Total", - f" Requests: {fmt_num(total_requests)}", - f" Tokens: {fmt_tokens(total_tokens)}", - f" Cost: ${fmt_num(total_cost)}", + "📈 累计统计", + f" 请求数: {fmt_num(total_requests)}", + f" Token: {fmt_tokens(total_tokens)}", + f" 费用: ${fmt_num(total_cost)}", "", - "Realtime", + "⚡ 实时状态", f" RPM: {rpm} | TPM: {fmt_num(tpm)}", - f" Active Users: {active_users}", - f" Avg Duration: {avg_duration:.0f}ms", + f" 活跃用户: {active_users}", + f" 平均延迟: {avg_duration:.0f}ms", ] return "\n".join(lines) diff --git a/telegram_bot.py b/telegram_bot.py index a367667..f03fb94 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -25,6 +25,15 @@ from config import ( TEAM_JSON_FILE, TELEGRAM_CHECK_INTERVAL, TELEGRAM_LOW_STOCK_THRESHOLD, + CONFIG_FILE, + EMAIL_PROVIDER, + BROWSER_HEADLESS, + ACCOUNTS_PER_TEAM, + PROXY_ENABLED, + PROXIES, + S2A_API_BASE, + CPA_API_BASE, + CRS_API_BASE, ) from utils import load_team_tracker from bot_notifier import BotNotifier, set_notifier, progress_finish @@ -74,6 +83,9 @@ class ProvisionerBot: ("help", self.cmd_help), ("status", self.cmd_status), ("team", self.cmd_team), + ("list", self.cmd_list), + ("config", self.cmd_config), + ("headless", self.cmd_headless), ("run", self.cmd_run), ("run_all", self.cmd_run_all), ("stop", self.cmd_stop), @@ -133,27 +145,33 @@ class ProvisionerBot: """显示帮助信息""" help_text = """🤖 OpenAI Team 批量注册 Bot -📋 命令列表: -/status - 查看所有 Team 状态 -/team <n> - 查看第 n 个 Team 详情 +📋 查看信息: +/list - 查看 team.json 账号列表 +/status - 查看任务处理状态 +/team <n> - 查看第 n 个 Team 处理详情 +/config - 查看系统配置 +/logs [n] - 查看最近 n 条日志 + +🚀 任务控制: /run <n> - 开始处理第 n 个 Team /run_all - 开始处理所有 Team /stop - 停止当前任务 -/logs [n] - 查看最近 n 条日志 (默认 10) + +⚙️ 配置管理: +/headless - 开启/关闭无头模式 + +📊 S2A 专属: /dashboard - 查看 S2A 仪表盘 /stock - 查看账号库存 -/import - 导入账号到 team.json -/help - 显示此帮助 -📤 上传账号: -直接发送 JSON 文件,或使用 /import 加 JSON 数据: -[{"account":"邮箱","password":"密码","token":"jwt"},...] -上传后使用 /run 开始处理 +📤 导入账号: +/import - 导入账号到 team.json +或直接发送 JSON 文件 💡 示例: +/list - 查看所有待处理账号 /run 0 - 处理第一个 Team -/team 1 - 查看第二个 Team 状态 -/logs 20 - 查看最近 20 条日志""" +/config - 查看当前配置""" await update.message.reply_text(help_text, parse_mode="HTML") @admin_only @@ -227,6 +245,127 @@ class ProvisionerBot: await update.message.reply_text("\n".join(lines), parse_mode="HTML") + @admin_only + async def cmd_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """查看 team.json 中的账号列表""" + if not TEAMS: + await update.message.reply_text("📭 team.json 中没有账号") + return + + lines = [f"📋 team.json 账号列表 (共 {len(TEAMS)} 个)\n"] + + for i, team in enumerate(TEAMS): + email = team.get("owner_email", "") + has_token = "🔑" if team.get("auth_token") else "🔒" + authorized = "✅" if team.get("authorized") else "" + needs_login = " [需登录]" if team.get("needs_login") else "" + + lines.append(f"{i}. {has_token} {email}{authorized}{needs_login}") + + # 统计 + with_token = sum(1 for t in TEAMS if t.get("auth_token")) + authorized = sum(1 for t in TEAMS if t.get("authorized")) + + lines.append(f"\n📊 统计:") + lines.append(f"有 Token: {with_token}/{len(TEAMS)}") + lines.append(f"已授权: {authorized}/{len(TEAMS)}") + + # 消息太长时分段发送 + text = "\n".join(lines) + if len(text) > 4000: + # 分段 + for i in range(0, len(lines), 30): + chunk = "\n".join(lines[i:i+30]) + await update.message.reply_text(chunk, parse_mode="HTML") + else: + await update.message.reply_text(text, parse_mode="HTML") + + @admin_only + async def cmd_config(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """查看当前系统配置""" + # 授权服务地址 + if AUTH_PROVIDER == "s2a": + auth_url = S2A_API_BASE or "未配置" + elif AUTH_PROVIDER == "cpa": + auth_url = CPA_API_BASE or "未配置" + else: + auth_url = CRS_API_BASE or "未配置" + + # 代理信息 + if PROXY_ENABLED and PROXIES: + proxy_info = f"已启用 ({len(PROXIES)} 个)" + else: + proxy_info = "未启用" + + # 无头模式状态 + headless_status = "✅ 已开启" if BROWSER_HEADLESS else "❌ 未开启" + + lines = [ + "⚙️ 系统配置", + "", + "📧 邮箱服务", + f" 提供商: {EMAIL_PROVIDER}", + "", + "🔐 授权服务", + f" 模式: {AUTH_PROVIDER.upper()}", + f" 地址: {auth_url}", + "", + "🌐 浏览器", + f" 无头模式: {headless_status}", + "", + "👥 账号设置", + f" 每 Team 账号数: {ACCOUNTS_PER_TEAM}", + f" team.json 账号: {len(TEAMS)}", + "", + "🔗 代理", + f" 状态: {proxy_info}", + "", + "💡 提示:", + "使用 /headless 开启/关闭无头模式", + ] + + await update.message.reply_text("\n".join(lines), parse_mode="HTML") + + @admin_only + async def cmd_headless(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """切换无头模式""" + import tomli_w + + try: + # 读取当前配置 + with open(CONFIG_FILE, "rb") as f: + import tomllib + config = tomllib.load(f) + + # 获取当前状态 + current = config.get("browser", {}).get("headless", False) + new_value = not current + + # 更新配置 + if "browser" not in config: + config["browser"] = {} + config["browser"]["headless"] = new_value + + # 写回文件 + with open(CONFIG_FILE, "wb") as f: + tomli_w.dump(config, f) + + status = "✅ 已开启" if new_value else "❌ 已关闭" + await update.message.reply_text( + f"🌐 无头模式\n\n" + f"状态: {status}\n\n" + f"⚠️ 需要重启 Bot 生效", + parse_mode="HTML" + ) + + except ImportError: + await update.message.reply_text( + "❌ 缺少 tomli_w 依赖\n" + "请运行: uv add tomli_w" + ) + except Exception as e: + await update.message.reply_text(f"❌ 修改配置失败: {e}") + @admin_only async def cmd_run(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """启动处理指定 Team""" diff --git a/uv.lock b/uv.lock index ecd8db0..6fd647e 100644 --- a/uv.lock +++ b/uv.lock @@ -331,6 +331,7 @@ dependencies = [ { name = "rich" }, { name = "setuptools" }, { name = "tomli" }, + { name = "tomli-w" }, ] [package.metadata] @@ -341,6 +342,7 @@ requires-dist = [ { name = "rich", specifier = ">=14.2.0" }, { name = "setuptools", specifier = ">=80.9.0" }, { name = "tomli", specifier = ">=2.3.0" }, + { name = "tomli-w", specifier = ">=1.2.0" }, ] [[package]] @@ -507,6 +509,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"