This commit is contained in:
2026-01-15 23:53:33 +08:00
parent b1953b5d95
commit 51d03038e0
5 changed files with 232 additions and 51 deletions

View File

@@ -6,6 +6,7 @@ import time
import random import random
import subprocess import subprocess
import os import os
import platform
from contextlib import contextmanager from contextlib import contextmanager
from DrissionPage import ChromiumPage, ChromiumOptions from DrissionPage import ChromiumPage, ChromiumOptions
@@ -156,22 +157,27 @@ def log_url_change(page, old_url: str, action: str = None):
def cleanup_chrome_processes(): def cleanup_chrome_processes():
"""清理残留的 Chrome 进程 (Windows)""" """清理残留的 Chrome 进程 (跨平台支持)"""
try: try:
# 查找并终止残留的 chrome 进程 (仅限无头或调试模式的) if platform.system() == "Windows":
# Windows: 使用 tasklist 和 taskkill
result = subprocess.run( result = subprocess.run(
['tasklist', '/FI', 'IMAGENAME eq chrome.exe', '/FO', 'CSV'], ['tasklist', '/FI', 'IMAGENAME eq chrome.exe', '/FO', 'CSV'],
capture_output=True, text=True, timeout=5 capture_output=True, text=True, timeout=5
) )
if 'chrome.exe' in result.stdout: if 'chrome.exe' in result.stdout:
# 只清理可能是自动化残留的进程,不影响用户正常使用的浏览器
# 通过检查命令行参数来判断
subprocess.run( subprocess.run(
['taskkill', '/F', '/IM', 'chromedriver.exe'], ['taskkill', '/F', '/IM', 'chromedriver.exe'],
capture_output=True, timeout=5 capture_output=True, timeout=5
) )
log.step("已清理 chromedriver 残留进程") log.step("已清理 chromedriver 残留进程")
else:
# Linux/Mac: 使用 pkill
subprocess.run(
['pkill', '-f', 'chromedriver'],
capture_output=True, timeout=5
)
except Exception: except Exception:
pass # 静默处理,不影响主流程 pass # 静默处理,不影响主流程
@@ -188,6 +194,7 @@ def init_browser(max_retries: int = BROWSER_MAX_RETRIES) -> ChromiumPage:
log.info("初始化浏览器...", icon="browser") log.info("初始化浏览器...", icon="browser")
last_error = None last_error = None
is_linux = platform.system() == "Linux"
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
@@ -204,7 +211,30 @@ def init_browser(max_retries: int = BROWSER_MAX_RETRIES) -> ChromiumPage:
co.set_argument('--disable-gpu') # 减少资源占用 co.set_argument('--disable-gpu') # 减少资源占用
co.set_argument('--disable-dev-shm-usage') # 避免共享内存问题 co.set_argument('--disable-dev-shm-usage') # 避免共享内存问题
co.set_argument('--no-sandbox') # 服务器环境需要 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: if BROWSER_HEADLESS:

View File

@@ -11,4 +11,5 @@ dependencies = [
"rich>=14.2.0", "rich>=14.2.0",
"setuptools>=80.9.0", "setuptools>=80.9.0",
"tomli>=2.3.0", "tomli>=2.3.0",
"tomli-w>=1.2.0",
] ]

View File

@@ -512,7 +512,7 @@ def format_dashboard_stats(stats: Dict[str, Any]) -> str:
str: 格式化后的文本 str: 格式化后的文本
""" """
if not stats: if not stats:
return "No data available" return "暂无数据"
def fmt_num(n): 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) avg_duration = stats.get("average_duration_ms", 0)
lines = [ lines = [
"<b>S2A Dashboard</b>", "<b>📊 S2A 仪表盘</b>",
"", "",
"<b>Accounts</b>", "<b>📦 账号状态</b>",
f" Total: {total_accounts} | Normal: {normal_accounts}", f" 总计: {total_accounts} | 正常: {normal_accounts}",
f" Error: {error_accounts} | RateLimit: {ratelimit_accounts}", f" 异常: {error_accounts} | 限流: {ratelimit_accounts}",
"", "",
"<b>Today</b>", "<b>📅 今日统计</b>",
f" Requests: {fmt_num(today_requests)}", f" 请求数: {fmt_num(today_requests)}",
f" Tokens: {fmt_tokens(today_tokens)}", f" Token: {fmt_tokens(today_tokens)}",
f" Input: {fmt_tokens(today_input)} | Output: {fmt_tokens(today_output)}", f" 输入: {fmt_tokens(today_input)} | 输出: {fmt_tokens(today_output)}",
f" Cache: {fmt_tokens(today_cache_read)}", f" 缓存: {fmt_tokens(today_cache_read)}",
f" Cost: ${fmt_num(today_cost)}", f" 费用: ${fmt_num(today_cost)}",
"", "",
"<b>Total</b>", "<b>📈 累计统计</b>",
f" Requests: {fmt_num(total_requests)}", f" 请求数: {fmt_num(total_requests)}",
f" Tokens: {fmt_tokens(total_tokens)}", f" Token: {fmt_tokens(total_tokens)}",
f" Cost: ${fmt_num(total_cost)}", f" 费用: ${fmt_num(total_cost)}",
"", "",
"<b>Realtime</b>", "<b>⚡ 实时状态</b>",
f" RPM: {rpm} | TPM: {fmt_num(tpm)}", f" RPM: {rpm} | TPM: {fmt_num(tpm)}",
f" Active Users: {active_users}", f" 活跃用户: {active_users}",
f" Avg Duration: {avg_duration:.0f}ms", f" 平均延迟: {avg_duration:.0f}ms",
] ]
return "\n".join(lines) return "\n".join(lines)

View File

@@ -25,6 +25,15 @@ from config import (
TEAM_JSON_FILE, TEAM_JSON_FILE,
TELEGRAM_CHECK_INTERVAL, TELEGRAM_CHECK_INTERVAL,
TELEGRAM_LOW_STOCK_THRESHOLD, 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 utils import load_team_tracker
from bot_notifier import BotNotifier, set_notifier, progress_finish from bot_notifier import BotNotifier, set_notifier, progress_finish
@@ -74,6 +83,9 @@ class ProvisionerBot:
("help", self.cmd_help), ("help", self.cmd_help),
("status", self.cmd_status), ("status", self.cmd_status),
("team", self.cmd_team), ("team", self.cmd_team),
("list", self.cmd_list),
("config", self.cmd_config),
("headless", self.cmd_headless),
("run", self.cmd_run), ("run", self.cmd_run),
("run_all", self.cmd_run_all), ("run_all", self.cmd_run_all),
("stop", self.cmd_stop), ("stop", self.cmd_stop),
@@ -133,27 +145,33 @@ class ProvisionerBot:
"""显示帮助信息""" """显示帮助信息"""
help_text = """<b>🤖 OpenAI Team 批量注册 Bot</b> help_text = """<b>🤖 OpenAI Team 批量注册 Bot</b>
<b>📋 命令列表:</b> <b>📋 查看信息:</b>
/status - 查看所有 Team 状态 /list - 查看 team.json 账号列表
/team &lt;n&gt; - 查看第 n 个 Team 详情 /status - 查看任务处理状态
/team &lt;n&gt; - 查看第 n 个 Team 处理详情
/config - 查看系统配置
/logs [n] - 查看最近 n 条日志
<b>🚀 任务控制:</b>
/run &lt;n&gt; - 开始处理第 n 个 Team /run &lt;n&gt; - 开始处理第 n 个 Team
/run_all - 开始处理所有 Team /run_all - 开始处理所有 Team
/stop - 停止当前任务 /stop - 停止当前任务
/logs [n] - 查看最近 n 条日志 (默认 10)
<b>⚙️ 配置管理:</b>
/headless - 开启/关闭无头模式
<b>📊 S2A 专属:</b>
/dashboard - 查看 S2A 仪表盘 /dashboard - 查看 S2A 仪表盘
/stock - 查看账号库存 /stock - 查看账号库存
/import - 导入账号到 team.json
/help - 显示此帮助
<b>📤 上传账号:</b> <b>📤 导入账号:</b>
直接发送 JSON 文件,或使用 /import 加 JSON 数据: /import - 导入账号到 team.json
<code>[{"account":"邮箱","password":"密码","token":"jwt"},...]</code> 或直接发送 JSON 文件
上传后使用 /run 开始处理
<b>💡 示例:</b> <b>💡 示例:</b>
<code>/list</code> - 查看所有待处理账号
<code>/run 0</code> - 处理第一个 Team <code>/run 0</code> - 处理第一个 Team
<code>/team 1</code> - 查看第二个 Team 状态 <code>/config</code> - 查看当前配置"""
<code>/logs 20</code> - 查看最近 20 条日志"""
await update.message.reply_text(help_text, parse_mode="HTML") await update.message.reply_text(help_text, parse_mode="HTML")
@admin_only @admin_only
@@ -227,6 +245,127 @@ class ProvisionerBot:
await update.message.reply_text("\n".join(lines), parse_mode="HTML") 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"<b>📋 team.json 账号列表 (共 {len(TEAMS)} 个)</b>\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<b>📊 统计:</b>")
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 = [
"<b>⚙️ 系统配置</b>",
"",
"<b>📧 邮箱服务</b>",
f" 提供商: {EMAIL_PROVIDER}",
"",
"<b>🔐 授权服务</b>",
f" 模式: {AUTH_PROVIDER.upper()}",
f" 地址: {auth_url}",
"",
"<b>🌐 浏览器</b>",
f" 无头模式: {headless_status}",
"",
"<b>👥 账号设置</b>",
f" 每 Team 账号数: {ACCOUNTS_PER_TEAM}",
f" team.json 账号: {len(TEAMS)}",
"",
"<b>🔗 代理</b>",
f" 状态: {proxy_info}",
"",
"<b>💡 提示:</b>",
"使用 /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"<b>🌐 无头模式</b>\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 @admin_only
async def cmd_run(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_run(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""启动处理指定 Team""" """启动处理指定 Team"""

11
uv.lock generated
View File

@@ -331,6 +331,7 @@ dependencies = [
{ name = "rich" }, { name = "rich" },
{ name = "setuptools" }, { name = "setuptools" },
{ name = "tomli" }, { name = "tomli" },
{ name = "tomli-w" },
] ]
[package.metadata] [package.metadata]
@@ -341,6 +342,7 @@ requires-dist = [
{ name = "rich", specifier = ">=14.2.0" }, { name = "rich", specifier = ">=14.2.0" },
{ name = "setuptools", specifier = ">=80.9.0" }, { name = "setuptools", specifier = ">=80.9.0" },
{ name = "tomli", specifier = ">=2.3.0" }, { name = "tomli", specifier = ">=2.3.0" },
{ name = "tomli-w", specifier = ">=1.2.0" },
] ]
[[package]] [[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" }, { 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"