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"