Files
codexTool/telegram_bot.py

7250 lines
282 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ==================== Telegram Bot 主程序 ====================
# 通过 Telegram 远程控制 OpenAI Team 批量注册任务
import asyncio
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from functools import wraps
from pathlib import Path
from typing import Optional
from telegram import Update, Bot, BotCommand, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
CallbackQueryHandler,
filters,
ContextTypes,
)
from datetime import datetime, timedelta, timezone, time as dt_time
# 北京时间 UTC+8
BEIJING_TZ = timezone(timedelta(hours=8))
from config import (
TELEGRAM_BOT_TOKEN,
TELEGRAM_ADMIN_CHAT_IDS,
TEAMS,
AUTH_PROVIDER,
TEAM_JSON_FILE,
TEAM_TRACKER_FILE,
CSV_FILE,
TELEGRAM_CHECK_INTERVAL,
TELEGRAM_LOW_STOCK_THRESHOLD,
CONFIG_FILE,
EMAIL_PROVIDER,
ACCOUNTS_PER_TEAM,
PROXY_ENABLED,
PROXIES,
S2A_API_BASE,
CPA_API_BASE,
CRS_API_BASE,
get_gptmail_keys,
add_gptmail_key,
remove_gptmail_key,
GPTMAIL_API_KEYS,
INCLUDE_TEAM_OWNERS,
reload_config,
S2A_CONCURRENCY,
S2A_PRIORITY,
S2A_GROUP_NAMES,
S2A_GROUP_IDS,
S2A_ADMIN_KEY,
S2A_API_MODE,
BROWSER_RANDOM_FINGERPRINT,
batch_remove_teams_by_names,
SCHEDULER_ENABLED,
SCHEDULER_START_HOUR,
SCHEDULER_END_HOUR,
SCHEDULER_BATCH_SIZE,
SCHEDULER_COOLDOWN_MINUTES,
SCHEDULER_OUTPUT_TYPE,
SCHEDULER_MAX_CONSECUTIVE_FAILURES,
)
from utils import load_team_tracker, get_all_incomplete_accounts, save_team_tracker, get_completed_teams, batch_remove_completed_teams
from bot_notifier import BotNotifier, set_notifier, progress_finish
from s2a_service import (
s2a_get_dashboard_stats, format_dashboard_stats, s2a_get_keys_with_usage, format_keys_usage,
s2a_get_error_accounts, s2a_delete_account, s2a_batch_delete_error_accounts,
s2a_api_authorize_single, s2a_batch_api_authorize
)
from email_service import gptmail_service, unified_create_email
from logger import log
def admin_only(func):
"""装饰器: 仅允许管理员执行命令"""
@wraps(func)
async def wrapper(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
# 兼容 message 和 callback_query
message = update.message or (update.callback_query.message if update.callback_query else None)
if message:
await message.reply_text("⛔ 无权限,你的 ID 不在管理员列表中")
return
return await func(self, update, context)
return wrapper
def get_message(update: Update):
"""获取可用于回复的 message 对象,兼容普通消息和 callback_query"""
return update.message or (update.callback_query.message if update.callback_query else None)
class ProvisionerBot:
"""OpenAI Team Provisioner Telegram Bot"""
def __init__(self):
self.executor = ThreadPoolExecutor(max_workers=1)
self.current_task: Optional[asyncio.Task] = None
self.current_team: Optional[str] = None
self.app: Optional[Application] = None
self.notifier: Optional[BotNotifier] = None
self._shutdown_event = asyncio.Event()
# JSON 导入批量进度跟踪
self._import_progress_message = None # 进度消息对象
self._import_progress_lock = asyncio.Lock() # 并发锁
self._import_batch_stats = { # 批量统计
"total_files": 0,
"processed_files": 0,
"total_added": 0,
"total_skipped": 0,
"current_file": "",
"errors": []
}
self._import_batch_timeout_task = None # 超时任务
# ==================== 调度器状态 ====================
self._scheduler_active = False # 调度器是否正在运行
self._scheduler_task: Optional[asyncio.Task] = None # 调度器 asyncio.Task
self._scheduler_stop_event = asyncio.Event() # 用于通知调度器停止
self._scheduler_round = 0 # 当前轮次
self._scheduler_stats = { # 累计统计
"total_rounds": 0,
"total_registered": 0,
"total_ingested": 0,
"total_failed": 0,
"consecutive_failures": 0,
"start_time": None,
}
self._scheduler_suspended_date = None # /stop 后挂起当天调度 (date 对象)
async def start(self):
"""启动 Bot"""
if not TELEGRAM_BOT_TOKEN:
log.error("Telegram Bot Token not configured")
return
# 创建 Application
self.app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
# 初始化通知器
self.notifier = BotNotifier(self.app.bot, TELEGRAM_ADMIN_CHAT_IDS)
set_notifier(self.notifier)
# 注册命令处理器
handlers = [
("start", self.cmd_help),
("help", self.cmd_help),
("status", self.cmd_status),
("team", self.cmd_team),
("list", self.cmd_list),
("config", self.cmd_config),
("fingerprint", self.cmd_fingerprint),
("concurrent", self.cmd_concurrent),
("run", self.cmd_run),
("run_all", self.cmd_run_all),
("resume", self.cmd_resume),
("stop", self.cmd_stop),
("logs", self.cmd_logs),
("logs_live", self.cmd_logs_live),
("logs_stop", self.cmd_logs_stop),
("dashboard", self.cmd_dashboard),
("import", self.cmd_import),
("verify", self.cmd_verify),
("verify_all", self.cmd_verify_all),
("stock", self.cmd_stock),
("gptmail_keys", self.cmd_gptmail_keys),
("gptmail_add", self.cmd_gptmail_add),
("gptmail_del", self.cmd_gptmail_del),
("cloudmail", self.cmd_cloudmail),
("cloudmail_token", self.cmd_cloudmail_token),
("cloudmail_api", self.cmd_cloudmail_api),
("cloudmail_domains", self.cmd_cloudmail_domains),
("iban_list", self.cmd_iban_list),
("iban_add", self.cmd_iban_add),
("iban_clear", self.cmd_iban_clear),
("domain_list", self.cmd_domain_list),
("domain_add", self.cmd_domain_add),
("domain_del", self.cmd_domain_del),
("domain_clear", self.cmd_domain_clear),
("team_fingerprint", self.cmd_team_fingerprint),
("team_register", self.cmd_team_register),
("test_email", self.cmd_test_email),
("include_owners", self.cmd_include_owners),
("reload", self.cmd_reload),
("s2a_config", self.cmd_s2a_config),
("api_auth", self.cmd_api_auth),
("clean", self.cmd_clean),
("clean_errors", self.cmd_clean_errors),
("clean_teams", self.cmd_clean_teams),
("keys_usage", self.cmd_keys_usage),
("autogptplus", self.cmd_autogptplus),
("update_token", self.cmd_update_token),
("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))
# 注册文件上传处理器 (JSON 文件)
self.app.add_handler(MessageHandler(
filters.Document.MimeType("application/json"),
self.handle_json_file
))
# 注册回调查询处理器 (InlineKeyboard)
self.app.add_handler(CallbackQueryHandler(
self.callback_keys_usage,
pattern="^keys_usage:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_clean_errors,
pattern="^clean_errors:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_clean_teams,
pattern="^clean_teams:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_team_register,
pattern="^team_reg:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_autogptplus,
pattern="^autogptplus:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_run_all,
pattern="^run_all:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_run,
pattern="^run:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_cloudmail,
pattern="^cloudmail:"
))
# 注册自定义数量输入处理器 (GPT Team 注册)
self.app.add_handler(MessageHandler(
filters.TEXT & ~filters.COMMAND,
self.handle_team_custom_count
))
# 注册定时检查任务
if TELEGRAM_CHECK_INTERVAL > 0 and AUTH_PROVIDER == "s2a":
self.app.job_queue.run_repeating(
self.scheduled_stock_check,
interval=TELEGRAM_CHECK_INTERVAL,
first=60, # 启动后1分钟执行第一次
name="stock_check"
)
log.info(f"Stock check scheduled every {TELEGRAM_CHECK_INTERVAL}s")
# 注册定时调度器 (每天 start_hour 触发)
if SCHEDULER_ENABLED:
trigger_time = dt_time(hour=SCHEDULER_START_HOUR, minute=0, second=0, tzinfo=BEIJING_TZ)
self.app.job_queue.run_daily(
self._scheduler_daily_trigger,
time=trigger_time,
name="scheduler_daily"
)
log.info(f"Scheduler registered: daily at {SCHEDULER_START_HOUR:02d}:00 - {SCHEDULER_END_HOUR:02d}:00 (Beijing Time)")
# 启动通知器
await self.notifier.start()
log.success("Telegram Bot started")
log.info(f"Admin Chat IDs: {TELEGRAM_ADMIN_CHAT_IDS}")
# 发送启动通知
await self.notifier.notify("<b>🤖 Bot 已启动</b>\n准备就绪,发送 /help 查看帮助")
# 运行 Bot
await self.app.initialize()
await self.app.start()
# 设置命令菜单提示
await self._set_commands()
await self.app.updater.start_polling(drop_pending_updates=True)
# 等待关闭信号
await self._shutdown_event.wait()
# 清理
await self.app.updater.stop()
await self.app.stop()
await self.app.shutdown()
await self.notifier.stop()
def request_shutdown(self):
"""请求关闭 Bot"""
self._shutdown_event.set()
async def _set_commands(self):
"""设置 Bot 命令菜单提示"""
commands = [
# 基础信息
BotCommand("help", "查看帮助信息"),
BotCommand("list", "查看 team.json 账号列表"),
BotCommand("status", "查看任务处理状态"),
BotCommand("team", "查看指定 Team 详情"),
BotCommand("config", "查看系统配置"),
BotCommand("logs", "查看最近日志"),
BotCommand("logs_live", "启用实时日志推送"),
BotCommand("logs_stop", "停止实时日志推送"),
# 任务控制
BotCommand("run", "选择数量和邮箱开始处理"),
BotCommand("run_all", "处理所有 Team"),
BotCommand("resume", "继续处理未完成账号"),
BotCommand("stop", "停止当前任务"),
# 配置管理
BotCommand("fingerprint", "开启/关闭随机指纹"),
BotCommand("concurrent", "开启/关闭并发处理"),
BotCommand("include_owners", "开启/关闭 Owner 入库"),
BotCommand("reload", "重载配置文件"),
# 清理管理
BotCommand("clean", "清理已完成账号"),
BotCommand("clean_errors", "清理错误状态账号"),
BotCommand("clean_teams", "清理已完成 Team"),
# S2A
BotCommand("dashboard", "查看 S2A 仪表盘"),
BotCommand("keys_usage", "查看 API 密钥用量"),
BotCommand("stock", "查看账号库存"),
BotCommand("s2a_config", "配置 S2A 参数"),
BotCommand("api_auth", "API 模式授权账号"),
BotCommand("import", "导入账号到 team.json"),
BotCommand("verify", "验证未验证的账号"),
BotCommand("verify_all", "强制重新验证所有账号"),
# GPTMail
BotCommand("gptmail_keys", "查看 GPTMail API Keys"),
BotCommand("gptmail_add", "添加 GPTMail API Key"),
BotCommand("gptmail_del", "删除 GPTMail API Key"),
BotCommand("test_email", "测试邮箱创建"),
# Cloud Mail
BotCommand("cloudmail", "Cloud Mail 管理面板"),
BotCommand("cloudmail_token", "设置 Cloud Mail Token"),
BotCommand("cloudmail_api", "设置 Cloud Mail API 地址"),
BotCommand("cloudmail_domains", "Cloud Mail 域名管理"),
# IBAN 管理
BotCommand("iban_list", "查看 IBAN 列表"),
BotCommand("iban_add", "添加 IBAN"),
BotCommand("iban_clear", "清空 IBAN 列表"),
# 域名管理
BotCommand("domain_list", "查看邮箱域名列表"),
BotCommand("domain_add", "添加邮箱域名"),
BotCommand("domain_del", "删除指定域名"),
BotCommand("domain_clear", "清空域名列表"),
# GPT Team
BotCommand("team_fingerprint", "GPT Team 随机指纹"),
BotCommand("team_register", "GPT Team 自动注册"),
# AutoGPTPlus
BotCommand("autogptplus", "AutoGPTPlus 管理面板"),
BotCommand("update_token", "更新邮件 API Token"),
# 定时调度器
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)
log.info("Bot 命令菜单已设置")
except Exception as e:
log.warning(f"设置命令菜单失败: {e}")
@admin_only
async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""显示帮助信息"""
help_text = """<b>🤖 OpenAI Team 批量注册 Bot</b>
<b>📋 查看信息:</b>
/list - 查看 team.json 账号列表
/status - 查看任务处理状态
/team &lt;n&gt; - 查看第 n 个 Team 处理详情
/config - 查看系统配置
/logs [n] - 查看最近 n 条日志
/logs_live - 启用实时日志推送
/logs_stop - 停止实时日志推送
<b>🚀 任务控制:</b>
/run - 选择数量和邮箱服务开始处理
/run_all - 开始处理所有 Team
/resume - 继续处理未完成账号
/stop - 停止当前任务
<b>⚙️ 配置管理:</b>
/fingerprint - 开启/关闭随机指纹
/concurrent - 开启/关闭并发处理
/concurrent &lt;n&gt; - 设置并发数 (1-10)
/include_owners - 开启/关闭 Owner 入库
/reload - 重载配置文件 (无需重启)
/clean - 清理 team.json 和 tracker 数据
<b>📊 S2A 专属:</b>
/dashboard - 查看 S2A 仪表盘
/keys_usage - 查看 API 密钥用量
/stock - 查看账号库存
/s2a_config - 配置 S2A 参数
/api_auth - API 模式授权 (无需浏览器)
/clean_errors - 清理错误状态账号
<b>🧹 清理管理:</b>
/clean - 清理已完成账号 (team.json)
/clean_teams - 清理已完成 Team (tracker)
<b>📤 导入账号:</b>
/import - 导入账号到 team.json
/verify - 验证未验证的账号
/verify_all - 强制重新验证所有账号
或直接发送 JSON 文件
<b>📧 GPTMail 管理:</b>
/gptmail_keys - 查看所有 API Keys
/gptmail_add &lt;key&gt; - 添加 API Key
/gptmail_del &lt;key&gt; - 删除 API Key
/test_email - 测试邮箱创建
<b>☁️ Cloud Mail 管理:</b>
/cloudmail - Cloud Mail 管理面板
/cloudmail_token &lt;token&gt; - 设置 API Token
/cloudmail_api &lt;url&gt; - 设置 API 地址
/cloudmail_domains - 域名管理 (查看/添加/删除)
<b>💳 IBAN 管理 (GPT Team):</b>
/iban_list - 查看 IBAN 列表
/iban_add &lt;ibans&gt; - 添加 IBAN (每行一个或逗号分隔)
/iban_clear - 清空 IBAN 列表
<b>📧 域名管理 (GPT Team):</b>
/domain_list - 查看邮箱域名列表
/domain_add &lt;domains&gt; - 添加域名 (每行一个或逗号分隔)
/domain_del &lt;domain&gt; - 删除指定域名
/domain_clear - 清空域名列表
<b>🤖 GPT Team:</b>
/team_fingerprint - 开启/关闭随机指纹
/team_register - 开始自动订阅注册
<b>🔧 AutoGPTPlus:</b>
/autogptplus - ChatGPT 订阅自动化管理面板
/update_token &lt;token&gt; - 更新邮件 API Token
<b>⏰ 定时调度器:</b>
/schedule on|off - 开启/关闭定时调度器
/schedule_config - 配置调度器参数
/schedule_status - 查看调度器运行状态
<b>🌐 代理池管理:</b>
/proxy_status - 查看代理池状态
/proxy_test - 测试并清理不可用代理
/proxy_reload - 从 proxy.txt 重新加载代理
<b>💡 示例:</b>
<code>/list</code> - 查看所有待处理账号
<code>/run 0</code> - 处理第一个 Team
<code>/concurrent 4</code> - 开启 4 并发处理
<code>/gptmail_add my-api-key</code> - 添加 Key
<code>/iban_add DE123...,DE456...</code> - 添加 IBAN
<code>/schedule on</code> - 启动调度器"""
await update.message.reply_text(help_text, parse_mode="HTML")
@admin_only
async def cmd_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看所有 Team 状态"""
tracker = load_team_tracker()
teams_data = tracker.get("teams", {})
if not teams_data:
await update.message.reply_text("📭 暂无数据,请先运行任务")
return
lines = ["<b>📊 Team 状态总览</b>\n"]
for team_name, accounts in teams_data.items():
total = len(accounts)
completed = sum(1 for a in accounts if a.get("status") == "completed")
failed = sum(1 for a in accounts if "fail" in a.get("status", "").lower())
pending = total - completed - failed
status_icon = "" if completed == total else ("" if failed > 0 else "")
lines.append(
f"{status_icon} <b>{team_name}</b>: {completed}/{total} "
f"(失败:{failed} 待处理:{pending})"
)
# 当前任务状态
if self.current_task and not self.current_task.done():
lines.append(f"\n<b>🔄 运行中:</b> {self.current_team or '未知'}")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_team(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看指定 Team 详情"""
if not context.args:
await update.message.reply_text("用法: /team <序号>\n示例: /team 0")
return
try:
team_idx = int(context.args[0])
except ValueError:
await update.message.reply_text("❌ 无效的序号,必须是数字")
return
if team_idx < 0 or team_idx >= len(TEAMS):
await update.message.reply_text(f"❌ 序号超出范围,有效范围: 0-{len(TEAMS)-1}")
return
team = TEAMS[team_idx]
team_name = team.get("name", f"Team{team_idx}")
tracker = load_team_tracker()
accounts = tracker.get("teams", {}).get(team_name, [])
lines = [f"<b>📁 Team {team_idx}: {team_name}</b>\n"]
lines.append(f"👤 Owner: {team.get('owner_email', '')}")
lines.append(f"📊 账号数: {len(accounts)}\n")
if accounts:
for acc in accounts:
email = acc.get("email", "")
status = acc.get("status", "unknown")
role = acc.get("role", "member")
icon = {"completed": "", "authorized": "🔐", "registered": "📝"}.get(
status, "" if "fail" in status.lower() else ""
)
role_tag = " [Owner]" if role == "owner" else ""
lines.append(f"{icon} {email}{role_tag}")
else:
lines.append("📭 暂无已处理的账号")
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
# 加载 tracker 以获取成员账号完成状态
tracker = load_team_tracker()
tracker_teams = tracker.get("teams", {})
lines = [f"<b>📋 team.json 账号列表 (共 {len(TEAMS)} 个)</b>\n"]
total_completed = 0
total_accounts = 0
for i, team in enumerate(TEAMS):
email = team.get("owner_email", "")
team_name = team.get("name", "")
has_token = "🔑" if team.get("auth_token") else "🔒"
needs_login = " [需登录]" if team.get("needs_login") else ""
# 从 tracker 获取该 Team 的成员完成情况
team_accounts = tracker_teams.get(team_name, [])
completed_count = sum(1 for acc in team_accounts if acc.get("status") == "completed")
account_count = len(team_accounts)
total_completed += completed_count
total_accounts += account_count
# 显示完成进度
if account_count > 0:
progress = f" [{completed_count}/{account_count}]"
else:
progress = ""
lines.append(f"{i}. {has_token} {email}{progress}{needs_login}")
# 统计
with_token = sum(1 for t in TEAMS if t.get("auth_token"))
lines.append(f"\n<b>📊 统计:</b>")
lines.append(f"有 Token: {with_token}/{len(TEAMS)}")
lines.append(f"已完成: {total_completed}/{total_accounts}")
# 消息太长时分段发送
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):
"""查看当前系统配置"""
from config import CONCURRENT_ENABLED, CONCURRENT_WORKERS
# 授权服务地址
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 = "未启用"
# Owner 入库状态
include_owners_status = "✅ 已开启" if INCLUDE_TEAM_OWNERS else "❌ 未开启"
# 随机指纹状态
fingerprint_status = "✅ 已开启" if BROWSER_RANDOM_FINGERPRINT else "❌ 未开启"
# 并发处理状态
if CONCURRENT_ENABLED:
concurrent_status = f"✅ 已开启 ({CONCURRENT_WORKERS} 并发)"
else:
concurrent_status = "❌ 未开启"
lines = [
"<b>⚙️ 系统配置</b>",
"",
"<b>📧 邮箱服务</b>",
f" 提供商: {EMAIL_PROVIDER}",
"",
"<b>🔐 授权服务</b>",
f" 模式: {AUTH_PROVIDER.upper()}",
f" 地址: {auth_url}",
f" Owner 入库: {include_owners_status}",
"",
"<b>🌐 浏览器</b>",
f" 随机指纹: {fingerprint_status}",
f" 并发处理: {concurrent_status}",
"",
"<b>👥 账号设置</b>",
f" 每 Team 账号数: {ACCOUNTS_PER_TEAM}",
f" team.json 账号: {len(TEAMS)}",
"",
"<b>🔗 代理</b>",
f" 状态: {proxy_info}",
"",
"<b>💡 提示:</b>",
"/fingerprint - 切换随机指纹",
"/concurrent - 切换并发处理",
"/include_owners - 切换 Owner 入库",
"/s2a_config - 配置 S2A 参数",
]
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_fingerprint(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("random_fingerprint", True)
new_value = not current
# 更新配置
if "browser" not in config:
config["browser"] = {}
config["browser"]["random_fingerprint"] = 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"开启后每次启动浏览器将随机使用不同的:\n"
f"• User-Agent (Chrome 133-144)\n"
f"• WebGL 显卡指纹\n"
f"• 屏幕分辨率\n\n"
f"💡 使用 /reload 立即生效",
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_include_owners(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""切换 Owner 入库开关"""
import tomli_w
try:
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 获取当前状态 (顶层配置)
current = config.get("include_team_owners", False)
new_value = not current
# 更新配置 (写到顶层)
config["include_team_owners"] = new_value
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
status = "✅ 已开启" if new_value else "❌ 已关闭"
desc = "运行任务时会将 Team Owner 账号也入库到授权服务" if new_value else "运行任务时不会入库 Team Owner 账号"
await update.message.reply_text(
f"<b>👤 Owner 入库开关</b>\n\n"
f"状态: {status}\n"
f"{desc}\n\n"
f"💡 使用 /reload 立即生效",
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_concurrent(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""切换并发处理开关或设置并发数"""
import tomli_w
from config import CONCURRENT_ENABLED, CONCURRENT_WORKERS
try:
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 确保 concurrent section 存在
if "concurrent" not in config:
config["concurrent"] = {"enabled": False, "workers": 4}
# 解析参数
args = context.args if context.args else []
if not args:
# 无参数: 切换开关
current = config["concurrent"].get("enabled", False)
new_value = not current
config["concurrent"]["enabled"] = new_value
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
workers = config["concurrent"].get("workers", 4)
status = "✅ 已开启" if new_value else "❌ 已关闭"
await update.message.reply_text(
f"<b>⚡ 并发处理</b>\n\n"
f"状态: {status}\n"
f"并发数: {workers}\n\n"
f"<b>说明:</b>\n"
f"• 注册流程并发执行\n"
f"• 授权回调串行执行 (避免端口冲突)\n\n"
f"<b>💡 设置并发数:</b>\n"
f"<code>/concurrent 4</code> - 设置为 4 并发\n\n"
f"使用 /reload 立即生效",
parse_mode="HTML"
)
else:
# 有参数: 设置并发数
try:
workers = int(args[0])
if workers < 1 or workers > 10:
await update.message.reply_text("❌ 并发数范围: 1-10")
return
config["concurrent"]["workers"] = workers
config["concurrent"]["enabled"] = True # 设置并发数时自动开启
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
await update.message.reply_text(
f"<b>⚡ 并发处理</b>\n\n"
f"状态: ✅ 已开启\n"
f"并发数: {workers}\n\n"
f"<b>说明:</b>\n"
f"• 注册流程并发执行\n"
f"• 授权回调串行执行 (避免端口冲突)\n\n"
f"💡 使用 /reload 立即生效",
parse_mode="HTML"
)
except ValueError:
await update.message.reply_text(
"❌ 无效的并发数\n\n"
"用法:\n"
"/concurrent - 切换开关\n"
"/concurrent 4 - 设置为 4 并发"
)
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_reload(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""重载配置文件"""
# 检查是否有任务正在运行
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 有任务正在运行: {self.current_team}\n"
"请等待任务完成或使用 /stop 停止后再重载配置"
)
return
await update.message.reply_text("⏳ 正在重载配置...")
try:
# 调用重载函数
result = reload_config()
if result["success"]:
# 重新导入更新后的配置变量
from config import (
EMAIL_PROVIDER as new_email_provider,
AUTH_PROVIDER as new_auth_provider,
INCLUDE_TEAM_OWNERS as new_include_owners,
ACCOUNTS_PER_TEAM as new_accounts_per_team,
BROWSER_RANDOM_FINGERPRINT as new_random_fingerprint,
)
lines = [
"<b>✅ 配置重载成功</b>",
"",
f"📁 team.json: {result['teams_count']} 个账号",
f"📄 config.toml: {'已更新' if result['config_updated'] else '未变化'}",
"",
"<b>当前配置:</b>",
f" 邮箱服务: {new_email_provider}",
f" 授权服务: {new_auth_provider}",
f" Owner 入库: {'' if new_include_owners else ''}",
f" 随机指纹: {'' if new_random_fingerprint else ''}",
f" 每 Team 账号: {new_accounts_per_team}",
]
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
else:
await update.message.reply_text(
f"<b>❌ 配置重载失败</b>\n\n{result['message']}",
parse_mode="HTML"
)
except Exception as e:
await update.message.reply_text(f"❌ 重载配置失败: {e}")
@admin_only
async def cmd_clean(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""清理 team.json 和 team_tracker.json 数据"""
# 检查是否有任务正在运行
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 有任务正在运行: {self.current_team}\n"
"请等待任务完成或使用 /stop 停止后再清理"
)
return
# 解析参数
args = [a.lower() for a in context.args] if context.args else []
include_csv = "all" in args
confirmed = "confirm" in args
# 需要确认参数
if not confirmed:
# 统计当前数据
team_count = len(TEAMS)
tracker = load_team_tracker()
tracker_accounts = sum(len(accs) for accs in tracker.get("teams", {}).values())
# 统计 CSV 行数
csv_count = 0
csv_path = Path(CSV_FILE)
if csv_path.exists():
try:
with open(csv_path, "r", encoding="utf-8") as f:
csv_count = max(0, sum(1 for _ in f) - 1) # 减去表头
except:
pass
msg = (
f"<b>⚠️ 确认清理数据?</b>\n\n"
f"将清空以下文件:\n"
f"• team.json ({team_count} 个 Team)\n"
f"• team_tracker.json ({tracker_accounts} 个账号记录)\n"
)
if include_csv:
msg += f"• accounts.csv ({csv_count} 条记录)\n"
msg += (
f"\n<b>此操作不可恢复!</b>\n\n"
f"确认清理请发送:\n"
f"<code>/clean confirm</code>\n\n"
f"如需同时清理 CSV:\n"
f"<code>/clean all confirm</code>"
)
await update.message.reply_text(msg, parse_mode="HTML")
return
# 执行清理
cleaned = []
errors = []
# 清理 team.json
try:
if TEAM_JSON_FILE.exists():
with open(TEAM_JSON_FILE, "w", encoding="utf-8") as f:
f.write("[]")
cleaned.append("team.json")
except Exception as e:
errors.append(f"team.json: {e}")
# 清理 team_tracker.json
try:
tracker_file = Path(TEAM_TRACKER_FILE)
if tracker_file.exists():
with open(tracker_file, "w", encoding="utf-8") as f:
f.write('{"teams": {}}')
cleaned.append("team_tracker.json")
except Exception as e:
errors.append(f"team_tracker.json: {e}")
# 清理 accounts.csv (可选)
if include_csv:
try:
csv_path = Path(CSV_FILE)
if csv_path.exists():
with open(csv_path, "w", encoding="utf-8") as f:
f.write("email,password,team,status,crs_id,timestamp\n")
cleaned.append("accounts.csv")
except Exception as e:
errors.append(f"accounts.csv: {e}")
# 重载配置以清空内存中的 TEAMS
reload_config()
# 返回结果
if errors:
await update.message.reply_text(
f"<b>⚠️ 部分清理失败</b>\n\n"
f"已清理: {', '.join(cleaned) or ''}\n"
f"失败: {'; '.join(errors)}",
parse_mode="HTML"
)
else:
await update.message.reply_text(
f"<b>✅ 清理完成</b>\n\n"
f"已清空: {', '.join(cleaned)}\n\n"
f"现在可以导入新的 team.json 了",
parse_mode="HTML"
)
@admin_only
async def cmd_s2a_config(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""配置 S2A 服务参数"""
import tomli_w
# 无参数时显示当前配置
if not context.args:
# 脱敏显示 API Key
key_display = "未配置"
if S2A_ADMIN_KEY:
if len(S2A_ADMIN_KEY) > 10:
key_display = f"{S2A_ADMIN_KEY[:4]}...{S2A_ADMIN_KEY[-4:]}"
else:
key_display = S2A_ADMIN_KEY[:4] + "..."
groups_display = ", ".join(S2A_GROUP_NAMES) if S2A_GROUP_NAMES else "默认分组"
group_ids_display = ", ".join(str(x) for x in S2A_GROUP_IDS) if S2A_GROUP_IDS else ""
lines = [
"<b>📊 S2A 服务配置</b>",
"",
f"<b>API 地址:</b> {S2A_API_BASE or '未配置'}",
f"<b>Admin Key:</b> <code>{key_display}</code>",
f"<b>并发数:</b> {S2A_CONCURRENCY}",
f"<b>优先级:</b> {S2A_PRIORITY}",
f"<b>分组名称:</b> {groups_display}",
f"<b>分组 ID:</b> {group_ids_display}",
"",
"<b>💡 修改配置:</b>",
"<code>/s2a_config concurrency 10</code>",
"<code>/s2a_config priority 50</code>",
"<code>/s2a_config groups 分组1,分组2</code>",
"<code>/s2a_config api_base https://...</code>",
"<code>/s2a_config admin_key sk-xxx</code>",
]
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
return
# 解析参数
param = context.args[0].lower()
value = " ".join(context.args[1:]) if len(context.args) > 1 else None
if not value:
await update.message.reply_text(
f"❌ 缺少值\n用法: /s2a_config {param} <值>"
)
return
try:
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 确保 s2a section 存在
if "s2a" not in config:
config["s2a"] = {}
# 根据参数类型处理
updated_key = None
updated_value = None
if param == "concurrency":
try:
new_val = int(value)
if new_val < 1 or new_val > 100:
await update.message.reply_text("❌ 并发数范围: 1-100")
return
config["s2a"]["concurrency"] = new_val
updated_key = "并发数"
updated_value = str(new_val)
except ValueError:
await update.message.reply_text("❌ 并发数必须是数字")
return
elif param == "priority":
try:
new_val = int(value)
if new_val < 0 or new_val > 100:
await update.message.reply_text("❌ 优先级范围: 0-100")
return
config["s2a"]["priority"] = new_val
updated_key = "优先级"
updated_value = str(new_val)
except ValueError:
await update.message.reply_text("❌ 优先级必须是数字")
return
elif param in ("groups", "group_names"):
# 支持逗号分隔的分组名称
groups = [g.strip() for g in value.split(",") if g.strip()]
config["s2a"]["group_names"] = groups
updated_key = "分组名称"
updated_value = ", ".join(groups) if groups else "默认分组"
elif param == "group_ids":
# 支持逗号分隔的分组 ID
ids = [i.strip() for i in value.split(",") if i.strip()]
config["s2a"]["group_ids"] = ids
updated_key = "分组 ID"
updated_value = ", ".join(ids) if ids else ""
elif param == "api_base":
config["s2a"]["api_base"] = value
updated_key = "API 地址"
updated_value = value
elif param == "admin_key":
config["s2a"]["admin_key"] = value
updated_key = "Admin Key"
# 脱敏显示
if len(value) > 10:
updated_value = f"{value[:4]}...{value[-4:]}"
else:
updated_value = value[:4] + "..."
else:
await update.message.reply_text(
f"❌ 未知参数: {param}\n\n"
"可用参数:\n"
"• concurrency - 并发数\n"
"• priority - 优先级\n"
"• groups - 分组名称\n"
"• group_ids - 分组 ID\n"
"• api_base - API 地址\n"
"• admin_key - Admin Key"
)
return
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
await update.message.reply_text(
f"<b>✅ S2A 配置已更新</b>\n\n"
f"{updated_key}: <code>{updated_value}</code>\n\n"
f"💡 使用 /reload 立即生效",
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_api_auth(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""使用纯 API 模式授权账号到 S2A (无需浏览器)
用法:
/api_auth email password - 授权单个账号
/api_auth - 显示帮助信息
"""
if AUTH_PROVIDER != "s2a":
await update.message.reply_text(
"❌ 此命令仅在 S2A 模式下可用\n"
"当前授权服务: " + AUTH_PROVIDER
)
return
# 无参数时显示帮助
if not context.args:
api_mode_status = "✅ 已启用" if S2A_API_MODE else "❌ 未启用"
lines = [
"<b>🔐 S2A API 授权</b>",
"",
f"<b>API 模式:</b> {api_mode_status}",
"",
"<b>用法:</b>",
"<code>/api_auth email password</code>",
"",
"<b>示例:</b>",
"<code>/api_auth test@example.com MyPassword123</code>",
"",
"<b>说明:</b>",
"• 使用纯 API 方式完成 OAuth 授权",
"• 无需浏览器,更快更稳定",
"• 需要安装 curl_cffi: <code>pip install curl_cffi</code>",
"",
"<b>💡 提示:</b>",
"• 在 config.toml 中设置 <code>s2a.api_mode = true</code>",
"• 可让所有 S2A 授权自动使用 API 模式",
]
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
return
# 解析参数
if len(context.args) < 2:
await update.message.reply_text(
"❌ 参数不足\n"
"用法: /api_auth <email> <password>"
)
return
email = context.args[0]
password = " ".join(context.args[1:]) # 密码可能包含空格
# 发送处理中消息
msg = await update.message.reply_text(
f"🔄 正在授权: <code>{email}</code>\n"
"请稍候...",
parse_mode="HTML"
)
try:
# 执行 API 授权
success, message = s2a_api_authorize_single(email, password)
if success:
await msg.edit_text(
f"✅ <b>授权成功</b>\n\n"
f"📧 邮箱: <code>{email}</code>\n"
f"📝 {message}",
parse_mode="HTML"
)
else:
await msg.edit_text(
f"❌ <b>授权失败</b>\n\n"
f"📧 邮箱: <code>{email}</code>\n"
f"📝 {message}",
parse_mode="HTML"
)
except Exception as e:
await msg.edit_text(
f"❌ <b>授权异常</b>\n\n"
f"📧 邮箱: <code>{email}</code>\n"
f"📝 错误: {str(e)}",
parse_mode="HTML"
)
@admin_only
async def cmd_run(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""启动处理指定数量的 Team - 交互式选择"""
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 任务正在运行: {self.current_team}\n使用 /stop 停止"
)
return
if not TEAMS:
await update.message.reply_text("📭 team.json 中没有账号")
return
total_teams = len(TEAMS)
# 构建选择按钮
keyboard = [
[
InlineKeyboardButton(f"📦 全部 ({total_teams})", callback_data=f"run:count:{total_teams}"),
],
[
InlineKeyboardButton("✏️ 自定义数量", callback_data="run:custom"),
],
[
InlineKeyboardButton("❌ 取消", callback_data="run:cancel"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"<b>🚀 启动处理 Team</b>\n\n"
f"{total_teams} 个 Team 可处理\n\n"
f"请选择要处理的数量:",
parse_mode="HTML",
reply_markup=reply_markup
)
@admin_only
async def cmd_run_all(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""启动处理所有 Team - 先选择邮箱服务"""
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 任务正在运行: {self.current_team}\n使用 /stop 停止"
)
return
# 显示邮箱服务选择界面
keyboard = [
[
InlineKeyboardButton("📧 GPTMail", callback_data="run_all:select_email:gptmail"),
InlineKeyboardButton("☁️ Cloud Mail", callback_data="run_all:select_email:cloudmail"),
],
[
InlineKeyboardButton("❌ 取消", callback_data="run_all:cancel"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"<b>🚀 启动处理所有 Team</b>\n\n"
f"{len(TEAMS)} 个 Team 待处理\n\n"
f"请选择邮箱服务:",
parse_mode="HTML",
reply_markup=reply_markup
)
@admin_only
async def cmd_resume(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""继续处理未完成的账号"""
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 任务正在运行: {self.current_team}\n使用 /stop 停止"
)
return
# 检查是否有未完成的账号
tracker = load_team_tracker()
all_incomplete = get_all_incomplete_accounts(tracker)
if not all_incomplete:
await update.message.reply_text("✅ 没有待处理的账号,所有任务已完成")
return
# 统计未完成账号
total_incomplete = sum(len(accs) for accs in all_incomplete.values())
teams_count = len(all_incomplete)
# 构建消息
lines = [
f"<b>⏳ 发现 {total_incomplete} 个未完成账号</b>",
f"涉及 {teams_count} 个 Team:",
""
]
for team_name, accounts in all_incomplete.items():
lines.append(f" • <b>{team_name}</b>: {len(accounts)}")
lines.append("")
lines.append("🚀 开始继续处理...")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
# 启动任务 (run_all_teams 会自动处理未完成的账号)
self.current_team = "继续处理"
# 重置停止标志,确保新任务可以正常运行
try:
import run
run._shutdown_requested = False
except Exception:
pass
loop = asyncio.get_event_loop()
self.current_task = loop.run_in_executor(
self.executor,
self._run_all_teams_task
)
self.current_task = asyncio.ensure_future(self._wrap_task(self.current_task, "继续处理"))
async def _wrap_task(self, task, team_name: str):
"""包装任务以处理完成通知"""
try:
result = await task
# 收集成功和失败的账号
success_accounts = [r.get("email") for r in (result or []) if r.get("status") == "success"]
failed_accounts = [r.get("email") for r in (result or []) if r.get("status") != "success"]
log.info(f"任务完成: {team_name}, 成功: {len(success_accounts)}, 失败: {len(failed_accounts)}")
await self.notifier.notify_task_completed(team_name, success_accounts, failed_accounts)
# run_all / resume 完成后自动清理 team.json
if team_name in ("全部", "继续处理"):
await self._auto_clean_after_run_all()
except Exception as e:
log.error(f"任务异常: {team_name}, 错误: {e}")
await self.notifier.notify_error(f"任务失败: {team_name}", str(e))
finally:
self.current_team = None
# 清理进度跟踪
progress_finish()
# 注意: 不在这里重置 _shutdown_requested
# 让标志保持 True直到下次任务启动时再重置
# 这样可以确保线程池中的任务能够正确检测到停止信号
async def _auto_clean_after_run_all(self):
"""run_all 完成后自动清理 team.json 和 team_tracker.json"""
from config import reload_config as _reload
try:
cleaned = []
# 清理 team.json
if TEAM_JSON_FILE.exists():
with open(TEAM_JSON_FILE, "w", encoding="utf-8") as f:
f.write("[]")
cleaned.append("team.json")
# 清理 team_tracker.json
tracker_file = Path(TEAM_TRACKER_FILE)
if tracker_file.exists():
with open(tracker_file, "w", encoding="utf-8") as f:
f.write('{"teams": {}}')
cleaned.append("team_tracker.json")
# 重载配置 (保护调度器内存状态reload 会从 toml 重置)
import config as cfg
scheduler_was_enabled = cfg.SCHEDULER_ENABLED
_reload()
if self._scheduler_active or scheduler_was_enabled:
cfg.SCHEDULER_ENABLED = scheduler_was_enabled
if cleaned:
log.info(f"自动清理完成: {', '.join(cleaned)}")
for chat_id in TELEGRAM_ADMIN_CHAT_IDS:
try:
await self.app.bot.send_message(
chat_id,
f"🧹 <b>自动清理完成</b>\n\n"
f"已清空: {', '.join(cleaned)}\n\n"
f"现在可以导入新的 team.json 了",
parse_mode="HTML"
)
except Exception:
pass
except Exception as e:
log.error(f"自动清理失败: {e}")
def _run_team_task(self, team_idx: int):
"""执行单个 Team 任务 (在线程池中运行)"""
# 延迟导入避免循环依赖
from run import run_single_team
from team_service import preload_all_account_ids
from utils import load_team_tracker, save_team_tracker, add_team_owners_to_tracker
from config import DEFAULT_PASSWORD
# 预加载 account_id
preload_all_account_ids()
_tracker = load_team_tracker()
add_team_owners_to_tracker(_tracker, DEFAULT_PASSWORD)
save_team_tracker(_tracker)
return run_single_team(team_idx)
def _run_all_teams_task(self):
"""执行所有 Team 任务 (在线程池中运行)"""
from run import run_all_teams
from team_service import preload_all_account_ids
from utils import load_team_tracker, save_team_tracker, add_team_owners_to_tracker
from config import DEFAULT_PASSWORD
# 预加载 account_id
preload_all_account_ids()
_tracker = load_team_tracker()
add_team_owners_to_tracker(_tracker, DEFAULT_PASSWORD)
save_team_tracker(_tracker)
return run_all_teams()
def _run_teams_by_count_task(self, count: int):
"""执行指定数量的 Team 任务 (在线程池中运行)"""
from run import run_teams_by_count
from team_service import preload_all_account_ids
from utils import load_team_tracker, save_team_tracker, add_team_owners_to_tracker
from config import DEFAULT_PASSWORD
# 预加载 account_id
preload_all_account_ids()
_tracker = load_team_tracker()
add_team_owners_to_tracker(_tracker, DEFAULT_PASSWORD)
save_team_tracker(_tracker)
return run_teams_by_count(count)
@admin_only
async def cmd_stop(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""强制停止当前任务"""
# 如果调度器正在运行
scheduler_was_active = self._scheduler_active
if self._scheduler_active:
self._scheduler_stop_event.set()
self._scheduler_suspended_date = datetime.now().date() # 挂起今天的调度
if (not self.current_task or self.current_task.done()) and not scheduler_was_active:
await update.message.reply_text("📭 当前没有运行中的任务")
return
task_name = self.current_team or "未知任务"
await update.message.reply_text(f"🛑 正在强制停止: {task_name}...")
try:
# 1. 设置全局停止标志
try:
import run
run._shutdown_requested = True
# 获取当前运行结果
current_results = run._current_results.copy() if run._current_results else []
except Exception:
current_results = []
# 2. 取消 asyncio 任务
if self.current_task and not self.current_task.done():
self.current_task.cancel()
# 3. 强制关闭浏览器进程
try:
from browser_automation import cleanup_chrome_processes
cleanup_chrome_processes()
except Exception as e:
log.warning(f"清理浏览器进程失败: {e}")
# 4. 重置状态
self.current_team = None
# 注意:不在这里重置 _shutdown_requested让任务完成后在 _wrap_task 中重置
# 这样可以确保线程池中的任务有足够时间检测到停止信号
# 清理进度跟踪
progress_finish()
# 6. 生成停止报告
report_lines = [
f"<b>🛑 任务已停止</b>",
f"任务: {task_name}",
"",
]
# 本次运行结果
if current_results:
success_count = sum(1 for r in current_results if r.get("status") == "success")
failed_count = len(current_results) - success_count
report_lines.append(f"<b>📊 本次运行结果:</b>")
report_lines.append(f" 成功: {success_count}")
report_lines.append(f" 失败: {failed_count}")
report_lines.append("")
# 获取未完成账号信息
tracker = load_team_tracker()
all_incomplete = get_all_incomplete_accounts(tracker)
if all_incomplete:
total_incomplete = sum(len(accs) for accs in all_incomplete.values())
report_lines.append(f"<b>⏳ 待继续处理: {total_incomplete} 个账号</b>")
# 显示每个 Team 的未完成账号 (最多显示 3 个 Team)
shown_teams = 0
for team_name, accounts in all_incomplete.items():
if shown_teams >= 3:
remaining = len(all_incomplete) - 3
report_lines.append(f" ... 还有 {remaining} 个 Team")
break
report_lines.append(f" <b>{team_name}</b>: {len(accounts)}")
# 显示第一个待处理账号
if accounts:
first_acc = accounts[0]
report_lines.append(f" 下一个: <code>{first_acc['email']}</code>")
shown_teams += 1
report_lines.append("")
report_lines.append("💡 使用 /resume 继续处理")
else:
report_lines.append("✅ 没有待处理的账号")
await update.message.reply_text("\n".join(report_lines), parse_mode="HTML")
# 如果调度器被停止,额外通知
if scheduler_was_active:
await update.message.reply_text(
"<b>⏰ 调度器已停止</b>\n\n"
"调度器已禁用,不会继续运行\n"
"明天 08:00 将自动重新启动\n\n"
"如需手动恢复: <code>/schedule on</code>",
parse_mode="HTML"
)
except Exception as e:
await update.message.reply_text(f"❌ 停止任务时出错: {e}")
@admin_only
async def cmd_logs(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看最近日志"""
try:
n = int(context.args[0]) if context.args else 10
except ValueError:
n = 10
n = min(n, 50) # 限制最大条数
try:
from config import BASE_DIR
log_file = BASE_DIR / "logs" / "app.log"
if not log_file.exists():
await update.message.reply_text("📭 日志文件不存在")
return
with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
recent = lines[-n:] if len(lines) >= n else lines
if not recent:
await update.message.reply_text("📭 日志文件为空")
return
# 格式化日志 (移除 ANSI 颜色码)
import re
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
clean_lines = [ansi_escape.sub('', line.strip()) for line in recent]
log_text = "\n".join(clean_lines)
if len(log_text) > 4000:
log_text = log_text[-4000:]
await update.message.reply_text(f"<code>{log_text}</code>", parse_mode="HTML")
except Exception as e:
await update.message.reply_text(f"❌ 读取日志失败: {e}")
@admin_only
async def cmd_logs_live(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""启用实时日志推送"""
from logger import log
if log.is_telegram_logging_enabled():
await update.message.reply_text("📡 实时日志已经在运行中")
return
# 启用 Telegram 日志推送
def log_callback(message: str, level: str):
"""日志回调函数"""
if self.notifier:
self.notifier.queue_message(message, level)
log.enable_telegram_logging(log_callback)
await update.message.reply_text(
"✅ 实时日志已启用\n"
"所有日志将实时推送到此聊天\n"
"使用 /logs_stop 停止推送"
)
@admin_only
async def cmd_logs_stop(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""停止实时日志推送"""
from logger import log
if not log.is_telegram_logging_enabled():
await update.message.reply_text("📭 实时日志未启用")
return
log.disable_telegram_logging()
await update.message.reply_text("✅ 实时日志已停止")
@admin_only
async def cmd_dashboard(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看 S2A 仪表盘统计"""
if AUTH_PROVIDER != "s2a":
await update.message.reply_text(
f"⚠️ 仪表盘仅支持 S2A 模式\n"
f"当前模式: {AUTH_PROVIDER}"
)
return
await update.message.reply_text("⏳ 正在获取仪表盘数据...")
try:
stats = s2a_get_dashboard_stats()
if stats:
text = format_dashboard_stats(stats)
await update.message.reply_text(text, parse_mode="HTML")
else:
await update.message.reply_text(
"❌ 获取仪表盘数据失败\n"
"请检查 S2A 配置和 API 连接"
)
except Exception as e:
await update.message.reply_text(f"❌ 错误: {e}")
@admin_only
async def cmd_keys_usage(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看 API 密钥用量 - 显示时间选择菜单"""
if AUTH_PROVIDER != "s2a":
await update.message.reply_text(
f"⚠️ 密钥用量查询仅支持 S2A 模式\n"
f"当前模式: {AUTH_PROVIDER}"
)
return
# 创建时间选择按钮
keyboard = [
[
InlineKeyboardButton("📍 今天", callback_data="keys_usage:today"),
InlineKeyboardButton("◀ 昨天", callback_data="keys_usage:yesterday"),
],
[
InlineKeyboardButton("◀ 近 7 天", callback_data="keys_usage:7d"),
InlineKeyboardButton("◀ 近 14 天", callback_data="keys_usage:14d"),
],
[
InlineKeyboardButton("◀ 近 30 天", callback_data="keys_usage:30d"),
InlineKeyboardButton("📅 本月", callback_data="keys_usage:this_month"),
],
[
InlineKeyboardButton("📅 上月", callback_data="keys_usage:last_month"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"<b>🔑 API 密钥用量查询</b>\n\n请选择时间范围:",
reply_markup=reply_markup,
parse_mode="HTML"
)
async def callback_keys_usage(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理密钥用量查询的回调"""
query = update.callback_query
await query.answer()
# 验证权限
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.edit_message_text("⛔ 无权限")
return
from datetime import datetime, timedelta
# 解析回调数据
data = query.data.replace("keys_usage:", "")
today = datetime.now()
if data == "today":
start_date = today.strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
period_text = "今天"
elif data == "yesterday":
yesterday = today - timedelta(days=1)
start_date = yesterday.strftime("%Y-%m-%d")
end_date = yesterday.strftime("%Y-%m-%d")
period_text = "昨天"
elif data == "7d":
start_date = (today - timedelta(days=6)).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
period_text = "近 7 天"
elif data == "14d":
start_date = (today - timedelta(days=13)).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
period_text = "近 14 天"
elif data == "30d":
start_date = (today - timedelta(days=29)).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
period_text = "近 30 天"
elif data == "this_month":
start_date = today.replace(day=1).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
period_text = "本月"
elif data == "last_month":
first_of_this_month = today.replace(day=1)
last_month_end = first_of_this_month - timedelta(days=1)
last_month_start = last_month_end.replace(day=1)
start_date = last_month_start.strftime("%Y-%m-%d")
end_date = last_month_end.strftime("%Y-%m-%d")
period_text = "上月"
else:
await query.edit_message_text("❌ 未知的时间选项")
return
await query.edit_message_text(f"⏳ 正在获取密钥用量 ({period_text})...")
try:
keys = s2a_get_keys_with_usage(start_date, end_date)
if keys is not None:
text = format_keys_usage(keys, period_text)
# 消息太长时分段发送
if len(text) > 4000:
# 先更新原消息
await query.edit_message_text(text[:4000], parse_mode="HTML")
# 剩余部分新消息发送
remaining = text[4000:]
while remaining:
chunk = remaining[:4000]
remaining = remaining[4000:]
await context.bot.send_message(
chat_id=update.effective_chat.id,
text=chunk,
parse_mode="HTML"
)
else:
await query.edit_message_text(text, parse_mode="HTML")
else:
await query.edit_message_text(
"❌ 获取密钥用量失败\n"
"请检查 S2A 配置和 API 连接"
)
except Exception as e:
await query.edit_message_text(f"❌ 错误: {e}")
@admin_only
async def cmd_stock(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看账号存货"""
if AUTH_PROVIDER != "s2a":
await update.message.reply_text(
f"⚠️ 库存查询仅支持 S2A 模式\n"
f"当前模式: {AUTH_PROVIDER}"
)
return
stats = s2a_get_dashboard_stats()
if not stats:
await update.message.reply_text("❌ 获取库存信息失败")
return
text = self._format_stock_message(stats)
await update.message.reply_text(text, parse_mode="HTML")
async def scheduled_stock_check(self, context: ContextTypes.DEFAULT_TYPE):
"""定时检查账号存货"""
try:
stats = s2a_get_dashboard_stats()
if not stats:
return
normal = stats.get("normal_accounts", 0)
total = stats.get("total_accounts", 0)
# 只在低库存时发送通知
if normal <= TELEGRAM_LOW_STOCK_THRESHOLD:
text = self._format_stock_message(stats, is_alert=True)
for chat_id in TELEGRAM_ADMIN_CHAT_IDS:
try:
await context.bot.send_message(
chat_id=chat_id,
text=text,
parse_mode="HTML"
)
except Exception:
pass
except Exception as e:
log.warning(f"Stock check failed: {e}")
def _format_stock_message(self, stats: dict, is_alert: bool = False) -> str:
"""格式化存货消息"""
total = stats.get("total_accounts", 0)
normal = stats.get("normal_accounts", 0)
error = stats.get("error_accounts", 0)
ratelimit = stats.get("ratelimit_accounts", 0)
overload = stats.get("overload_accounts", 0)
# 计算健康度
health_pct = (normal / total * 100) if total > 0 else 0
# 状态图标
if normal <= TELEGRAM_LOW_STOCK_THRESHOLD:
status_icon = "⚠️ 库存不足"
status_line = f"<b>{status_icon}</b>"
elif health_pct >= 80:
status_icon = "✅ 正常"
status_line = f"<b>{status_icon}</b>"
elif health_pct >= 50:
status_icon = "⚠️ 警告"
status_line = f"<b>{status_icon}</b>"
else:
status_icon = "🔴 严重"
status_line = f"<b>{status_icon}</b>"
title = "🚨 库存不足警报" if is_alert else "📦 账号库存"
lines = [
f"<b>{title}</b>",
"",
f"状态: {status_line}",
f"健康度: {health_pct:.1f}%",
"",
f"正常: <b>{normal}</b>",
f"异常: {error}",
f"限流: {ratelimit}",
f"总计: {total}",
]
if is_alert:
lines.append("")
lines.append(f"预警阈值: {TELEGRAM_LOW_STOCK_THRESHOLD}")
return "\n".join(lines)
@admin_only
async def cmd_clean_errors(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""清理错误状态的账号"""
if AUTH_PROVIDER != "s2a":
await update.message.reply_text(
f"⚠️ 清理错误账号仅支持 S2A 模式\n"
f"当前模式: {AUTH_PROVIDER}"
)
return
# 获取错误账号
error_accounts, total = s2a_get_error_accounts()
if total == 0:
await update.message.reply_text("✅ 没有错误状态的账号需要清理")
return
# 存储账号数据到 context.bot_data 供分页使用
context.bot_data["clean_errors_accounts"] = error_accounts
context.bot_data["clean_errors_total"] = total
# 显示第一页
text, keyboard = self._build_clean_errors_page(error_accounts, total, page=0)
await update.message.reply_text(text, reply_markup=keyboard, parse_mode="HTML")
def _build_clean_errors_page(self, accounts: list, total: int, page: int = 0, page_size: int = 10):
"""构建错误账号预览页面"""
total_pages = (total + page_size - 1) // page_size
start_idx = page * page_size
end_idx = min(start_idx + page_size, total)
page_accounts = accounts[start_idx:end_idx]
# 按错误类型分组统计(全部账号)
error_types = {}
for acc in accounts:
error_msg = acc.get("error_message", "Unknown")
error_key = error_msg[:50] if error_msg else "Unknown"
error_types[error_key] = error_types.get(error_key, 0) + 1
lines = [
"<b>🗑️ 清理错误账号 (预览)</b>",
"",
f"共发现 <b>{total}</b> 个错误状态账号",
"",
"<b>错误类型统计:</b>",
]
# 显示前5种错误类型
sorted_errors = sorted(error_types.items(), key=lambda x: x[1], reverse=True)[:5]
for error_msg, count in sorted_errors:
lines.append(f"{count}x: {error_msg}...")
if len(error_types) > 5:
lines.append(f"• ... 还有 {len(error_types) - 5} 种其他错误")
lines.extend([
"",
f"<b>账号列表 (第 {page + 1}/{total_pages} 页):</b>",
])
# 显示当前页的账号
for i, acc in enumerate(page_accounts, start=start_idx + 1):
name = acc.get("name", "Unknown")[:25]
error_msg = acc.get("error_message", "")[:30]
lines.append(f"{i}. {name} - <code>{error_msg}</code>")
lines.extend([
"",
"⚠️ <b>此操作不可撤销!</b>",
])
text = "\n".join(lines)
# 构建分页按钮
nav_buttons = []
if page > 0:
nav_buttons.append(InlineKeyboardButton("⬅️ 上一页", callback_data=f"clean_errors:page:{page - 1}"))
if page < total_pages - 1:
nav_buttons.append(InlineKeyboardButton("下一页 ➡️", callback_data=f"clean_errors:page:{page + 1}"))
keyboard = [
nav_buttons,
[InlineKeyboardButton(f"🗑️ 确认删除全部 ({total})", callback_data="clean_errors:confirm")],
[InlineKeyboardButton("❌ 取消", callback_data="clean_errors:cancel")],
]
# 过滤空行
keyboard = [row for row in keyboard if row]
return text, InlineKeyboardMarkup(keyboard)
async def callback_clean_errors(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理清理错误账号的回调"""
query = update.callback_query
await query.answer()
# 验证权限
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.edit_message_text("⛔ 无权限")
return
# 解析回调数据
data = query.data.replace("clean_errors:", "")
if data.startswith("page:"):
# 分页浏览
page = int(data.replace("page:", ""))
accounts = context.bot_data.get("clean_errors_accounts", [])
total = context.bot_data.get("clean_errors_total", 0)
if not accounts:
await query.edit_message_text("❌ 数据已过期,请重新使用 /clean_errors")
return
text, keyboard = self._build_clean_errors_page(accounts, total, page)
await query.edit_message_text(text, reply_markup=keyboard, parse_mode="HTML")
elif data == "cancel":
# 取消操作
context.bot_data.pop("clean_errors_accounts", None)
context.bot_data.pop("clean_errors_total", None)
await query.edit_message_text("✅ 已取消清理操作")
elif data == "confirm":
# 执行删除
total = context.bot_data.get("clean_errors_total", 0)
await query.edit_message_text(
f"<b>🗑️ 正在删除 {total} 个错误账号...</b>\n\n"
"进度: 0%",
parse_mode="HTML"
)
# 同步执行删除
results = s2a_batch_delete_error_accounts()
# 清理缓存数据
context.bot_data.pop("clean_errors_accounts", None)
context.bot_data.pop("clean_errors_total", None)
# 显示结果
lines = [
"<b>✅ 清理完成</b>",
"",
f"成功删除: <b>{results['success']}</b>",
f"删除失败: {results['failed']}",
f"总计: {results['total']}",
]
# 如果有失败的,显示部分失败详情
failed_details = [d for d in results.get("details", []) if d.get("status") == "failed"]
if failed_details:
lines.append("")
lines.append("<b>失败详情:</b>")
for detail in failed_details[:5]:
lines.append(f"{detail.get('name', '')}: {detail.get('message', '')}")
if len(failed_details) > 5:
lines.append(f"• ... 还有 {len(failed_details) - 5} 个失败")
await query.edit_message_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_clean_teams(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""清理处理成功的 Team"""
# 获取已完成的 teams
tracker = load_team_tracker()
completed_teams = get_completed_teams(tracker)
if not completed_teams:
await update.message.reply_text("✅ 没有处理成功的 Team 需要清理")
return
# 存储数据供分页使用
context.bot_data["clean_teams_data"] = completed_teams
context.bot_data["clean_teams_total"] = len(completed_teams)
# 显示第一页
text, keyboard = self._build_clean_teams_page(completed_teams, page=0)
await update.message.reply_text(text, reply_markup=keyboard, parse_mode="HTML")
def _build_clean_teams_page(self, teams: list, page: int = 0, page_size: int = 10):
"""构建已完成 Team 预览页面"""
total = len(teams)
total_pages = (total + page_size - 1) // page_size
start_idx = page * page_size
end_idx = min(start_idx + page_size, total)
page_teams = teams[start_idx:end_idx]
# 统计总账号数
total_accounts = sum(t["total"] for t in teams)
lines = [
"<b>🧹 清理已完成 Team (预览)</b>",
"",
f"共发现 <b>{total}</b> 个已完成的 Team",
f"涉及 <b>{total_accounts}</b> 个账号记录",
"",
f"<b>Team 列表 (第 {page + 1}/{total_pages} 页):</b>",
]
# 显示当前页的 Team
for i, team in enumerate(page_teams, start=start_idx + 1):
name = team["name"][:25]
count = team["total"]
lines.append(f"{i}. ✅ {name} ({count} 个账号)")
lines.extend([
"",
"⚠️ <b>此操作将同时清理:</b>",
"• team_tracker.json (账号处理记录)",
"• team.json (Team 配置)",
])
text = "\n".join(lines)
# 构建分页按钮
nav_buttons = []
if page > 0:
nav_buttons.append(InlineKeyboardButton("⬅️ 上一页", callback_data=f"clean_teams:page:{page - 1}"))
if page < total_pages - 1:
nav_buttons.append(InlineKeyboardButton("下一页 ➡️", callback_data=f"clean_teams:page:{page + 1}"))
keyboard = [
nav_buttons,
[InlineKeyboardButton(f"🧹 确认清理全部 ({total})", callback_data="clean_teams:confirm")],
[InlineKeyboardButton("❌ 取消", callback_data="clean_teams:cancel")],
]
# 过滤空行
keyboard = [row for row in keyboard if row]
return text, InlineKeyboardMarkup(keyboard)
async def callback_clean_teams(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理清理已完成 Team 的回调"""
query = update.callback_query
await query.answer()
# 验证权限
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.edit_message_text("⛔ 无权限")
return
# 解析回调数据
data = query.data.replace("clean_teams:", "")
if data.startswith("page:"):
# 分页浏览
page = int(data.replace("page:", ""))
teams = context.bot_data.get("clean_teams_data", [])
if not teams:
await query.edit_message_text("❌ 数据已过期,请重新使用 /clean_teams")
return
text, keyboard = self._build_clean_teams_page(teams, page)
await query.edit_message_text(text, reply_markup=keyboard, parse_mode="HTML")
elif data == "cancel":
# 取消操作
context.bot_data.pop("clean_teams_data", None)
context.bot_data.pop("clean_teams_total", None)
await query.edit_message_text("✅ 已取消清理操作")
elif data == "confirm":
# 执行清理
teams_data = context.bot_data.get("clean_teams_data", [])
total = context.bot_data.get("clean_teams_total", 0)
await query.edit_message_text(
f"<b>🧹 正在清理 {total} 个已完成 Team...</b>",
parse_mode="HTML"
)
# 获取要删除的 team 名称列表
team_names = [t["name"] for t in teams_data]
# 1. 清理 tracker
tracker = load_team_tracker()
tracker_results = batch_remove_completed_teams(tracker)
save_team_tracker(tracker)
# 2. 清理 team.json
json_results = batch_remove_teams_by_names(team_names)
# 清理缓存数据
context.bot_data.pop("clean_teams_data", None)
context.bot_data.pop("clean_teams_total", None)
# 统计清理的账号数
total_accounts = sum(d.get("accounts", 0) for d in tracker_results.get("details", []) if d.get("status") == "success")
# 显示结果
lines = [
"<b>✅ 清理完成</b>",
"",
f"清理 Team: <b>{tracker_results['success']}</b>",
f"清理账号记录: <b>{total_accounts}</b>",
"",
"<b>📁 文件清理:</b>",
f"• tracker: {tracker_results['success']} 个 Team",
f"• team.json: {json_results['success']} 个 Team",
]
if tracker_results['failed'] > 0 or json_results['failed'] > 0:
lines.append("")
lines.append(f"<b>失败:</b> tracker={tracker_results['failed']}, json={json_results['failed']}")
await query.edit_message_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_import(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""上传账号到 team.json"""
# 获取命令后的 JSON 数据
if not context.args:
await update.message.reply_text(
"<b>📤 导入账号到 team.json</b>\n\n"
"用法:\n"
"1. 直接发送 JSON 文件\n"
"2. /import 后跟 JSON 数据\n\n"
"JSON 格式:\n"
"<code>[{\"account\":\"邮箱\",\"password\":\"密码\",\"token\":\"jwt\"},...]</code>\n\n"
"导入后使用 /run 开始处理",
parse_mode="HTML"
)
return
# 尝试解析 JSON
json_text = " ".join(context.args)
await self._process_import_json(update, json_text)
def _reset_import_batch_stats(self):
"""重置批量导入统计"""
self._import_batch_stats = {
"total_files": 0,
"processed_files": 0,
"total_added": 0,
"total_skipped": 0,
"current_file": "",
"errors": [],
"team_json_total": 0
}
def _get_import_progress_text(self, is_processing: bool = True) -> str:
"""生成导入进度消息文本"""
stats = self._import_batch_stats
if is_processing:
lines = [
"<b>⏳ 正在处理 JSON 文件...</b>",
"",
f"📁 文件: {stats['processed_files']}/{stats['total_files']}",
]
if stats['current_file']:
lines.append(f"📄 当前: <code>{stats['current_file']}</code>")
lines.extend([
"",
f"新增: {stats['total_added']}",
f"跳过 (重复): {stats['total_skipped']}",
])
else:
# 完成状态
lines = [
"<b>✅ 导入完成</b>",
"",
f"📁 处理文件: {stats['processed_files']}",
f"📄 已更新 team.json",
f"新增: {stats['total_added']}",
f"跳过 (重复): {stats['total_skipped']}",
f"team.json 总数: {stats['team_json_total']}",
]
if stats['errors']:
lines.append("")
lines.append(f"⚠️ 错误 ({len(stats['errors'])} 个):")
for err in stats['errors'][:3]: # 最多显示3个错误
lines.append(f"{err}")
if len(stats['errors']) > 3:
lines.append(f" ... 还有 {len(stats['errors']) - 3} 个错误")
lines.extend([
"",
"✅ 配置已自动刷新",
"使用 /run_all 或 /run &lt;n&gt; 开始处理"
])
return "\n".join(lines)
async def _update_import_progress(self, chat_id: int, is_final: bool = False):
"""更新导入进度消息"""
text = self._get_import_progress_text(is_processing=not is_final)
try:
if self._import_progress_message:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=self._import_progress_message.message_id,
text=text,
parse_mode="HTML"
)
except Exception:
pass # 忽略编辑失败
async def _finalize_import_batch(self, chat_id: int):
"""完成批量导入,发送最终结果,并自动验证 account_id"""
async with self._import_progress_lock:
if self._import_progress_message is None:
return
# 取消超时任务 (job_queue job)
if self._import_batch_timeout_task:
try:
self._import_batch_timeout_task.schedule_removal()
except Exception:
pass
self._import_batch_timeout_task = None
# 更新最终进度
await self._update_import_progress(chat_id, is_final=True)
# 保存统计数据用于后续验证
added_count = self._import_batch_stats.get("total_added", 0)
# 重置状态
self._import_progress_message = None
self._reset_import_batch_stats()
# 如果有新增账号,自动验证 account_id 并移除无效账号
if added_count > 0:
await self._validate_and_cleanup_accounts(chat_id)
async def _validate_and_cleanup_accounts(self, chat_id: int, force_all: bool = False):
"""验证新导入账号的 account_id移除无效账号
Args:
chat_id: Telegram 聊天 ID用于发送进度消息
force_all: 是否强制重新验证所有有 token 的账号
"""
import json
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
mode_text = "重新验证" if force_all else "验证"
# 发送开始验证的消息
progress_msg = await self.app.bot.send_message(
chat_id=chat_id,
text=f"<b>🔍 正在{mode_text}账号...</b>\n\n⏳ 获取 account_id 中...",
parse_mode="HTML"
)
try:
# 读取当前 team.json
team_json_path = Path(TEAM_JSON_FILE)
if not team_json_path.exists():
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=progress_msg.message_id,
text="❌ team.json 不存在",
parse_mode="HTML"
)
return
with open(team_json_path, "r", encoding="utf-8") as f:
accounts = json.load(f)
if not isinstance(accounts, list):
accounts = [accounts]
# 筛选需要验证的账号
accounts_to_verify = []
for i, acc in enumerate(accounts):
token = acc.get("token", "")
account_id = acc.get("account_id", "")
if force_all:
# 强制模式:验证所有有 token 的账号
if token:
accounts_to_verify.append((i, acc))
else:
# 普通模式:只验证有 token 但没有 account_id 的账号
if token and not account_id:
accounts_to_verify.append((i, acc))
if not accounts_to_verify:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=progress_msg.message_id,
text="<b>✅ 验证完成</b>\n\n所有账号已有 account_id无需验证",
parse_mode="HTML"
)
return
total = len(accounts_to_verify)
# 生成进度条的辅助函数
def make_progress_bar(current: int, total: int, width: int = 16) -> str:
"""生成进度条字符串"""
if total == 0:
return "[" + "" * width + "]"
filled = int(width * current / total)
empty = width - filled
percent = int(100 * current / total)
return f"[{'' * filled}{'' * empty}] {current}/{total} ({percent}%)"
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=progress_msg.message_id,
text=f"<b>🔍 正在{mode_text}账号...</b>\n\n"
f"<code>{make_progress_bar(0, total)}</code>\n\n"
f"⏳ 验证 {total} 个账号的 account_id (20 并发)...",
parse_mode="HTML"
)
# 并行获取 account_id
valid_indices = set() # 验证成功的账号索引
failed_accounts = [] # 验证失败的账号信息
def verify_account(idx_acc_tuple):
"""验证单个账号"""
idx, acc = idx_acc_tuple
email = acc.get("account") or acc.get("email", "")
token = acc.get("token", "")
# 构造临时 team 配置用于获取 account_id
temp_team = {
"name": email.split("@")[0] if "@" in email else f"Team{idx}",
"auth_token": token,
"account_id": ""
}
from team_service import fetch_account_id
account_id = fetch_account_id(temp_team, silent=True)
return idx, email, account_id
# 使用线程池并行验证 (20 并发)
max_workers = min(20, total)
completed_count = 0
last_update_time = 0
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(verify_account, item): item for item in accounts_to_verify}
for future in as_completed(futures):
idx, email, account_id = future.result()
completed_count += 1
if account_id:
# 验证成功,更新 account_id
accounts[idx]["account_id"] = account_id
valid_indices.add(idx)
else:
# 验证失败
failed_accounts.append({"idx": idx, "email": email})
# 每处理 10 个或间隔 1 秒更新一次进度
import time
current_time = time.time()
if completed_count % 10 == 0 or completed_count == total or current_time - last_update_time > 1:
last_update_time = current_time
try:
progress_bar = make_progress_bar(completed_count, total)
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=progress_msg.message_id,
text=f"<b>🔍 正在{mode_text}账号...</b>\n\n"
f"<code>{progress_bar}</code>\n\n"
f"✅ 成功: {len(valid_indices)}\n"
f"❌ 失败: {len(failed_accounts)}",
parse_mode="HTML"
)
except Exception:
pass
# 移除验证失败的账号
if failed_accounts:
failed_indices = {item["idx"] for item in failed_accounts}
accounts = [acc for i, acc in enumerate(accounts) if i not in failed_indices]
# 保存更新后的 team.json
with open(team_json_path, "w", encoding="utf-8") as f:
json.dump(accounts, f, ensure_ascii=False, indent=2)
# 重载配置
reload_config()
# 发送最终结果
lines = [
"<b>✅ 账号验证完成</b>",
"",
f"<code>{make_progress_bar(total, total)}</code>",
"",
f"📊 验证结果:",
f" • 验证总数: {total}",
f" • 成功: {len(valid_indices)}",
f" • 失败并移除: {len(failed_accounts)}",
f" • team.json 剩余: {len(accounts)}",
]
if failed_accounts:
lines.append("")
lines.append("❌ 已移除的无效账号:")
for item in failed_accounts[:5]: # 最多显示 5 个
lines.append(f"{item['email']}")
if len(failed_accounts) > 5:
lines.append(f" ... 还有 {len(failed_accounts) - 5}")
lines.extend([
"",
"💡 使用 /run_all 或 /run &lt;n&gt; 开始处理"
])
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=progress_msg.message_id,
text="\n".join(lines),
parse_mode="HTML"
)
except Exception as e:
log.error(f"验证账号失败: {e}")
try:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=progress_msg.message_id,
text=f"<b>❌ 验证失败</b>\n\n{str(e)}",
parse_mode="HTML"
)
except Exception:
pass
@admin_only
async def cmd_verify(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""手动验证 team.json 中的账号,获取 account_id 并移除无效账号
用法:
/verify - 验证未验证的账号
/verify_all - 强制重新验证所有账号
"""
chat_id = update.effective_chat.id
# 检查是否有任务正在运行
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 有任务正在运行: {self.current_team}\n"
"请等待任务完成或使用 /stop 停止后再验证"
)
return
# 解析参数
force_all = context.args and context.args[0].lower() == "all"
# 检查 team.json 是否存在
from pathlib import Path
team_json_path = Path(TEAM_JSON_FILE)
if not team_json_path.exists():
await update.message.reply_text("❌ team.json 不存在,请先导入账号")
return
# 统计需要验证的账号
import json
try:
with open(team_json_path, "r", encoding="utf-8") as f:
accounts = json.load(f)
if not isinstance(accounts, list):
accounts = [accounts]
except Exception as e:
await update.message.reply_text(f"❌ 读取 team.json 失败: {e}")
return
total_accounts = len(accounts)
has_token = sum(1 for acc in accounts if acc.get("token"))
already_verified = sum(1 for acc in accounts if acc.get("account_id"))
if force_all:
# 强制重新验证所有有 token 的账号
need_verify = has_token
else:
# 只验证未验证的账号
need_verify = sum(1 for acc in accounts if acc.get("token") and not acc.get("account_id"))
if need_verify == 0:
await update.message.reply_text(
f"<b>✅ 无需验证</b>\n\n"
f"team.json 共 {total_accounts} 个账号\n"
f"有 Token: {has_token}\n"
f"已验证: {already_verified}\n"
f"待验证: 0\n\n"
f"💡 使用 /verify_all 强制重新验证所有账号",
parse_mode="HTML"
)
return
mode_text = "强制重新验证" if force_all else "验证"
await update.message.reply_text(
f"<b>🔍 开始{mode_text}账号</b>\n\n"
f"team.json 共 {total_accounts} 个账号\n"
f"有 Token: {has_token}\n"
f"已验证: {already_verified}\n"
f"待验证: {need_verify}\n\n"
f"⏳ 正在验证...",
parse_mode="HTML"
)
# 执行验证
await self._validate_and_cleanup_accounts(chat_id, force_all=force_all)
@admin_only
async def cmd_verify_all(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""强制重新验证所有账号"""
chat_id = update.effective_chat.id
# 检查是否有任务正在运行
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 有任务正在运行: {self.current_team}\n"
"请等待任务完成或使用 /stop 停止后再验证"
)
return
# 检查 team.json 是否存在
from pathlib import Path
team_json_path = Path(TEAM_JSON_FILE)
if not team_json_path.exists():
await update.message.reply_text("❌ team.json 不存在,请先导入账号")
return
# 统计需要验证的账号
import json
try:
with open(team_json_path, "r", encoding="utf-8") as f:
accounts = json.load(f)
if not isinstance(accounts, list):
accounts = [accounts]
except Exception as e:
await update.message.reply_text(f"❌ 读取 team.json 失败: {e}")
return
total_accounts = len(accounts)
has_token = sum(1 for acc in accounts if acc.get("token"))
already_verified = sum(1 for acc in accounts if acc.get("account_id"))
if has_token == 0:
await update.message.reply_text(
f"<b>❌ 无法验证</b>\n\n"
f"team.json 共 {total_accounts} 个账号\n"
f"有 Token: 0\n\n"
f"没有可验证的账号",
parse_mode="HTML"
)
return
await update.message.reply_text(
f"<b>🔍 开始强制重新验证所有账号</b>\n\n"
f"team.json 共 {total_accounts} 个账号\n"
f"有 Token: {has_token}\n"
f"已验证: {already_verified}\n"
f"待验证: {has_token}\n\n"
f"⏳ 正在验证...",
parse_mode="HTML"
)
# 执行验证 (强制模式)
await self._validate_and_cleanup_accounts(chat_id, force_all=True)
async def _import_batch_timeout_callback(self, context: ContextTypes.DEFAULT_TYPE):
"""批量导入超时回调 - 由 job_queue 调用"""
chat_id = context.job.data.get("chat_id")
if chat_id:
await self._finalize_import_batch(chat_id)
def _schedule_import_finalize(self, context: ContextTypes.DEFAULT_TYPE, chat_id: int, delay: float = 1.5):
"""调度批量导入完成任务"""
# 取消之前的超时任务
if self._import_batch_timeout_task:
self._import_batch_timeout_task.schedule_removal()
self._import_batch_timeout_task = None
# 使用 job_queue 调度新的超时任务
self._import_batch_timeout_task = context.job_queue.run_once(
self._import_batch_timeout_callback,
when=delay,
data={"chat_id": chat_id},
name="import_batch_finalize"
)
@admin_only
async def handle_json_file(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理上传的 JSON 文件 - 支持批量导入进度更新"""
# 检查是否是管理员
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await update.message.reply_text("⛔ 无权限")
return
document = update.message.document
if not document:
return
chat_id = update.effective_chat.id
file_name = document.file_name or "unknown.json"
async with self._import_progress_lock:
# 取消之前的超时任务(如果有)
if self._import_batch_timeout_task:
self._import_batch_timeout_task.schedule_removal()
self._import_batch_timeout_task = None
# 更新统计
self._import_batch_stats["total_files"] += 1
self._import_batch_stats["current_file"] = file_name
# 如果是新批次,发送初始进度消息
if self._import_progress_message is None:
self._reset_import_batch_stats()
self._import_batch_stats["total_files"] = 1
self._import_batch_stats["current_file"] = file_name
self._import_progress_message = await update.message.reply_text(
self._get_import_progress_text(is_processing=True),
parse_mode="HTML"
)
else:
# 更新进度消息
await self._update_import_progress(chat_id)
try:
# 下载文件
file = await document.get_file()
file_bytes = await file.download_as_bytearray()
json_text = file_bytes.decode("utf-8")
# 处理导入并获取结果
result = await self._process_import_json_batch(json_text)
async with self._import_progress_lock:
self._import_batch_stats["processed_files"] += 1
self._import_batch_stats["total_added"] += result.get("added", 0)
self._import_batch_stats["total_skipped"] += result.get("skipped", 0)
self._import_batch_stats["team_json_total"] = result.get("total", 0)
self._import_batch_stats["current_file"] = ""
if result.get("error"):
self._import_batch_stats["errors"].append(f"{file_name}: {result['error']}")
# 更新进度
await self._update_import_progress(chat_id)
# 检查是否所有文件都已处理,如果是则调度完成任务
stats = self._import_batch_stats
if stats["processed_files"] >= stats["total_files"]:
# 所有文件处理完成,短延迟后完成批次(防止更多文件到来)
self._schedule_import_finalize(context, chat_id, delay=1.0)
else:
# 还有文件在处理,设置较长的超时
self._schedule_import_finalize(context, chat_id, delay=3.0)
except Exception as e:
async with self._import_progress_lock:
self._import_batch_stats["processed_files"] += 1
self._import_batch_stats["errors"].append(f"{file_name}: {str(e)}")
self._import_batch_stats["current_file"] = ""
await self._update_import_progress(chat_id)
# 调度完成任务
stats = self._import_batch_stats
if stats["processed_files"] >= stats["total_files"]:
self._schedule_import_finalize(context, chat_id, delay=1.0)
else:
self._schedule_import_finalize(context, chat_id, delay=3.0)
async def _process_import_json_batch(self, json_text: str) -> dict:
"""处理导入的 JSON 数据,保存到 team.json (批量版本,返回结果)
Returns:
dict: {"added": int, "skipped": int, "total": int, "error": str|None}
"""
import json
from pathlib import Path
result = {"added": 0, "skipped": 0, "total": 0, "error": None}
try:
new_accounts = json.loads(json_text)
except json.JSONDecodeError as e:
result["error"] = f"JSON 格式错误: {e}"
return result
if not isinstance(new_accounts, list):
new_accounts = [new_accounts]
if not new_accounts:
result["error"] = "JSON 数据中没有账号"
return result
# 验证格式
valid_accounts = []
for acc in new_accounts:
if not isinstance(acc, dict):
continue
email = acc.get("account") or acc.get("email", "")
token = acc.get("token", "")
password = acc.get("password", "")
if email and token:
valid_accounts.append({
"account": email,
"password": password,
"token": token
})
if not valid_accounts:
result["error"] = "未找到有效账号"
return result
# 读取现有 team.json
team_json_path = Path(TEAM_JSON_FILE)
existing_accounts = []
if team_json_path.exists():
try:
with open(team_json_path, "r", encoding="utf-8") as f:
existing_accounts = json.load(f)
if not isinstance(existing_accounts, list):
existing_accounts = [existing_accounts]
except Exception:
existing_accounts = []
# 检查重复
existing_emails = set()
for acc in existing_accounts:
email = acc.get("account") or acc.get("user", {}).get("email", "")
if email:
existing_emails.add(email.lower())
added = 0
skipped = 0
for acc in valid_accounts:
email = acc.get("account", "").lower()
if email in existing_emails:
skipped += 1
else:
existing_accounts.append(acc)
existing_emails.add(email)
added += 1
# 保存到 team.json
try:
team_json_path.parent.mkdir(parents=True, exist_ok=True)
with open(team_json_path, "w", encoding="utf-8") as f:
json.dump(existing_accounts, f, ensure_ascii=False, indent=2)
# 重载配置
reload_config()
result["added"] = added
result["skipped"] = skipped
result["total"] = len(existing_accounts)
except Exception as e:
result["error"] = f"保存失败: {e}"
return result
async def _process_import_json(self, update: Update, json_text: str):
"""处理导入的 JSON 数据,保存到 team.json"""
import json
from pathlib import Path
try:
new_accounts = json.loads(json_text)
except json.JSONDecodeError as e:
await update.message.reply_text(f"❌ JSON 格式错误: {e}")
return
if not isinstance(new_accounts, list):
# 如果是单个对象,转成列表
new_accounts = [new_accounts]
if not new_accounts:
await update.message.reply_text("📭 JSON 数据中没有账号")
return
# 验证格式
valid_accounts = []
for acc in new_accounts:
if not isinstance(acc, dict):
continue
# 支持 account 或 email 字段
email = acc.get("account") or acc.get("email", "")
token = acc.get("token", "")
password = acc.get("password", "")
if email and token:
valid_accounts.append({
"account": email,
"password": password,
"token": token
})
if not valid_accounts:
await update.message.reply_text("❌ 未找到有效账号 (需要 account/email 和 token 字段)")
return
# 读取现有 team.json (不存在则自动创建)
team_json_path = Path(TEAM_JSON_FILE)
existing_accounts = []
is_new_file = not team_json_path.exists()
if not is_new_file:
try:
with open(team_json_path, "r", encoding="utf-8") as f:
existing_accounts = json.load(f)
if not isinstance(existing_accounts, list):
existing_accounts = [existing_accounts]
except Exception:
existing_accounts = []
# 检查重复
existing_emails = set()
for acc in existing_accounts:
email = acc.get("account") or acc.get("user", {}).get("email", "")
if email:
existing_emails.add(email.lower())
added = 0
skipped = 0
for acc in valid_accounts:
email = acc.get("account", "").lower()
if email in existing_emails:
skipped += 1
else:
existing_accounts.append(acc)
existing_emails.add(email)
added += 1
# 保存到 team.json (自动创建文件)
try:
# 确保父目录存在
team_json_path.parent.mkdir(parents=True, exist_ok=True)
with open(team_json_path, "w", encoding="utf-8") as f:
json.dump(existing_accounts, f, ensure_ascii=False, indent=2)
file_status = "📄 已创建 team.json" if is_new_file else "📄 已更新 team.json"
# 自动重载配置,同步内存状态,避免 save_team_json() 覆盖新导入的数据
reload_result = reload_config()
await update.message.reply_text(
f"<b>✅ 导入完成</b>\n\n"
f"{file_status}\n"
f"新增: {added}\n"
f"跳过 (重复): {skipped}\n"
f"team.json 总数: {len(existing_accounts)}\n\n"
f"✅ 配置已自动刷新\n"
f"⏳ 正在验证账号 account_id...",
parse_mode="HTML"
)
# 如果有新增账号,自动验证 account_id 并移除无效账号
if added > 0:
chat_id = update.effective_chat.id
await self._validate_and_cleanup_accounts(chat_id)
except Exception as e:
await update.message.reply_text(f"❌ 保存到 team.json 失败: {e}")
# ==================== GPTMail Key 管理命令 ====================
@admin_only
async def cmd_gptmail_keys(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看所有 GPTMail API Keys"""
keys = get_gptmail_keys()
config_keys = set(GPTMAIL_API_KEYS)
if not keys:
await update.message.reply_text(
"<b>📧 GPTMail API Keys</b>\n\n"
"📭 暂无配置 Key\n\n"
"使用 /gptmail_add &lt;key&gt; 添加",
parse_mode="HTML"
)
return
lines = [f"<b>📧 GPTMail API Keys (共 {len(keys)} 个)</b>\n"]
for i, key in enumerate(keys):
# 脱敏显示
if len(key) > 10:
masked = f"{key[:4]}...{key[-4:]}"
else:
masked = key[:4] + "..." if len(key) > 4 else key
# 标记来源
source = "📁 配置" if key in config_keys else "🔧 动态"
lines.append(f"{i+1}. <code>{masked}</code> {source}")
lines.append(f"\n<b>💡 管理:</b>")
lines.append(f"/gptmail_add &lt;key&gt; - 添加 Key")
lines.append(f"/gptmail_del &lt;key&gt; - 删除动态 Key")
lines.append(f"/test_email - 测试邮箱创建")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_gptmail_add(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""添加 GPTMail API Key (支持批量导入)"""
message = get_message(update)
if not message:
return
if not context.args:
await message.reply_text(
"<b>📧 添加 GPTMail API Key</b>\n\n"
"<b>单个添加:</b>\n"
"<code>/gptmail_add gpt-xxx</code>\n\n"
"<b>批量添加 (空格分隔):</b>\n"
"<code>/gptmail_add key1 key2 key3</code>\n\n"
"<b>批量添加 (换行分隔):</b>\n"
"<code>/gptmail_add key1\nkey2\nkey3</code>",
parse_mode="HTML"
)
return
# 合并所有参数,支持空格和换行分隔
raw_input = " ".join(context.args)
# 按空格和换行分割
keys = []
for part in raw_input.replace("\n", " ").split():
key = part.strip()
if key:
keys.append(key)
if not keys:
await message.reply_text("❌ Key 不能为空")
return
# 获取现有 keys
existing_keys = set(get_gptmail_keys())
# 统计结果
added = []
skipped = []
invalid = []
await message.reply_text(f"⏳ 正在验证 {len(keys)} 个 Key...")
for key in keys:
# 检查是否已存在
if key in existing_keys:
skipped.append(key)
continue
# 测试 Key 是否有效 (使用线程池避免阻塞,带超时)
try:
success, msg = await asyncio.wait_for(
asyncio.to_thread(gptmail_service.test_api_key, key),
timeout=15
)
except asyncio.TimeoutError:
invalid.append(key)
continue
if not success:
invalid.append(key)
continue
# 添加 Key
if add_gptmail_key(key):
added.append(key)
existing_keys.add(key)
# 生成结果报告
lines = ["<b>📧 GPTMail Key 导入结果</b>\n"]
if added:
lines.append(f"<b>✅ 成功添加:</b> {len(added)}")
for k in added[:5]: # 最多显示5个
masked = f"{k[:4]}...{k[-4:]}" if len(k) > 10 else k
lines.append(f" • <code>{masked}</code>")
if len(added) > 5:
lines.append(f" • ... 等 {len(added)}")
if skipped:
lines.append(f"\n<b>⏭️ 已跳过 (已存在):</b> {len(skipped)}")
if invalid:
lines.append(f"\n<b>❌ 无效 Key:</b> {len(invalid)}")
for k in invalid[:3]: # 最多显示3个
masked = f"{k[:4]}...{k[-4:]}" if len(k) > 10 else k
lines.append(f" • <code>{masked}</code>")
lines.append(f"\n<b>当前 Key 总数:</b> {len(get_gptmail_keys())}")
await message.reply_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_gptmail_del(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""删除 GPTMail API Key"""
if not context.args:
await update.message.reply_text(
"<b>📧 删除 GPTMail API Key</b>\n\n"
"用法: /gptmail_del &lt;key&gt;\n\n"
"注意: 只能删除动态添加的 Key配置文件中的 Key 请直接修改 config.toml",
parse_mode="HTML"
)
return
key = context.args[0].strip()
# 检查是否是配置文件中的 Key
if key in GPTMAIL_API_KEYS:
await update.message.reply_text(
"⚠️ 该 Key 在配置文件中,无法通过 Bot 删除\n"
"请直接修改 config.toml"
)
return
# 删除 Key
if remove_gptmail_key(key):
await update.message.reply_text(
f"<b>✅ Key 已删除</b>\n\n"
f"当前 Key 总数: {len(get_gptmail_keys())}",
parse_mode="HTML"
)
else:
await update.message.reply_text("❌ Key 不存在或删除失败")
# ==================== Cloud Mail 管理命令 ====================
@admin_only
async def cmd_cloudmail(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Cloud Mail 管理面板"""
from config import EMAIL_API_BASE, EMAIL_API_AUTH, EMAIL_DOMAINS, EMAIL_PROVIDER
# 获取当前配置
api_base = EMAIL_API_BASE or "未配置"
auth_display = "未配置"
if EMAIL_API_AUTH:
if len(EMAIL_API_AUTH) > 10:
auth_display = f"{EMAIL_API_AUTH[:8]}...{EMAIL_API_AUTH[-4:]}"
else:
auth_display = EMAIL_API_AUTH[:4] + "..."
domains_count = len(EMAIL_DOMAINS) if EMAIL_DOMAINS else 0
domains_display = ", ".join(EMAIL_DOMAINS[:3]) if EMAIL_DOMAINS else "未配置"
if domains_count > 3:
domains_display += f" (+{domains_count - 3})"
is_active = EMAIL_PROVIDER == "cloudmail"
keyboard = [
[
InlineKeyboardButton("🔑 设置 Token", callback_data="cloudmail:set_token"),
InlineKeyboardButton("🌐 设置 API 地址", callback_data="cloudmail:set_api"),
],
[
InlineKeyboardButton("📧 域名管理", callback_data="cloudmail:domains"),
InlineKeyboardButton("🔄 测试连接", callback_data="cloudmail:test"),
],
[
InlineKeyboardButton(
f"{'' if is_active else ''} 设为当前邮箱服务",
callback_data="cloudmail:activate"
),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"<b>☁️ Cloud Mail 管理</b>\n\n"
f"<b>当前配置:</b>\n"
f" API 地址: <code>{api_base}</code>\n"
f" Token: <code>{auth_display}</code>\n"
f" 域名数量: {domains_count}\n"
f" 域名: {domains_display}\n\n"
f"<b>状态:</b> {'✅ 当前使用中' if is_active else '⬜ 未激活'}\n\n"
f"选择操作:",
parse_mode="HTML",
reply_markup=reply_markup
)
@admin_only
async def cmd_cloudmail_token(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""设置 Cloud Mail API Token"""
if not context.args:
await update.message.reply_text(
"<b>🔑 设置 Cloud Mail Token</b>\n\n"
"用法: <code>/cloudmail_token &lt;token&gt;</code>\n\n"
"此命令会更新 config.toml 中的:\n"
"• [email] api_auth",
parse_mode="HTML"
)
return
new_token = context.args[0].strip()
try:
import tomli_w
import tomllib
from config import CONFIG_FILE
# 读取配置
with open(CONFIG_FILE, "rb") as f:
config = tomllib.load(f)
# 确保 [email] section 存在
if "email" not in config:
config["email"] = {}
# 更新 api_auth
config["email"]["api_auth"] = new_token
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
from config import reload_config
reload_config()
token_display = f"{new_token[:8]}...{new_token[-4:]}" if len(new_token) > 12 else new_token
await update.message.reply_text(
f"<b>✅ Cloud Mail Token 已更新</b>\n\n"
f"新 Token: <code>{token_display}</code>",
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_cloudmail_api(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""设置 Cloud Mail API 地址"""
if not context.args:
await update.message.reply_text(
"<b>🌐 设置 Cloud Mail API 地址</b>\n\n"
"用法: <code>/cloudmail_api &lt;url&gt;</code>\n\n"
"示例: <code>/cloudmail_api https://mail.example.com/api/public</code>",
parse_mode="HTML"
)
return
new_api = context.args[0].strip()
try:
import tomli_w
import tomllib
from config import CONFIG_FILE
# 读取配置
with open(CONFIG_FILE, "rb") as f:
config = tomllib.load(f)
# 确保 [email] section 存在
if "email" not in config:
config["email"] = {}
# 更新 api_base
config["email"]["api_base"] = new_api
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
from config import reload_config
reload_config()
await update.message.reply_text(
f"<b>✅ Cloud Mail API 地址已更新</b>\n\n"
f"新地址: <code>{new_api}</code>",
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_cloudmail_domains(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""管理 Cloud Mail 域名"""
from config import EMAIL_DOMAINS
if not context.args:
# 显示当前域名列表
if not EMAIL_DOMAINS:
await update.message.reply_text(
"<b>📧 Cloud Mail 域名</b>\n\n"
"📭 暂无配置域名\n\n"
"使用 <code>/cloudmail_domains add domain.com</code> 添加",
parse_mode="HTML"
)
return
lines = [f"<b>📧 Cloud Mail 域名 (共 {len(EMAIL_DOMAINS)} 个)</b>\n"]
for i, domain in enumerate(EMAIL_DOMAINS):
lines.append(f"{i+1}. <code>{domain}</code>")
lines.append(f"\n<b>💡 管理:</b>")
lines.append(f"/cloudmail_domains add &lt;domain&gt; - 添加域名")
lines.append(f"/cloudmail_domains del &lt;domain&gt; - 删除域名")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
return
action = context.args[0].lower()
if action == "add" and len(context.args) > 1:
domain = context.args[1].strip()
await self._cloudmail_add_domain(update, domain)
elif action == "del" and len(context.args) > 1:
domain = context.args[1].strip()
await self._cloudmail_del_domain(update, domain)
else:
await update.message.reply_text(
"<b>📧 Cloud Mail 域名管理</b>\n\n"
"用法:\n"
"• <code>/cloudmail_domains</code> - 查看域名列表\n"
"• <code>/cloudmail_domains add domain.com</code> - 添加域名\n"
"• <code>/cloudmail_domains del domain.com</code> - 删除域名",
parse_mode="HTML"
)
async def _cloudmail_add_domain(self, update: Update, domain: str):
"""添加 Cloud Mail 域名"""
message = get_message(update)
try:
import tomli_w
from config import CONFIG_FILE, EMAIL_DOMAINS
import tomllib
if domain in EMAIL_DOMAINS:
await message.reply_text(f"⚠️ 域名 {domain} 已存在")
return
# 读取配置
with open(CONFIG_FILE, "rb") as f:
config = tomllib.load(f)
# 确保 [email] section 存在
if "email" not in config:
config["email"] = {}
# 更新 domains 列表
current_domains = config["email"].get("domains", [])
if domain not in current_domains:
current_domains.append(domain)
config["email"]["domains"] = current_domains
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
from config import reload_config
reload_config()
await message.reply_text(f"✅ 已添加域名: {domain}")
except ImportError:
await message.reply_text("❌ 缺少 tomli_w 依赖\n请运行: uv add tomli_w")
except Exception as e:
await message.reply_text(f"❌ 添加失败: {e}")
async def _cloudmail_del_domain(self, update: Update, domain: str):
"""删除 Cloud Mail 域名"""
message = get_message(update)
try:
import tomli_w
from config import CONFIG_FILE, EMAIL_DOMAINS
import tomllib
if domain not in EMAIL_DOMAINS:
await message.reply_text(f"⚠️ 域名 {domain} 不存在")
return
# 读取配置
with open(CONFIG_FILE, "rb") as f:
config = tomllib.load(f)
# 更新 domains 列表
if "email" in config and "domains" in config["email"]:
config["email"]["domains"] = [d for d in config["email"]["domains"] if d != domain]
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
from config import reload_config
reload_config()
await message.reply_text(f"✅ 已删除域名: {domain}")
except ImportError:
await message.reply_text("❌ 缺少 tomli_w 依赖\n请运行: uv add tomli_w")
except Exception as e:
await message.reply_text(f"❌ 删除失败: {e}")
@admin_only
async def cmd_test_email(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""测试邮箱创建功能"""
if EMAIL_PROVIDER != "gptmail":
await update.message.reply_text(
f"⚠️ 当前邮箱提供商: {EMAIL_PROVIDER}\n"
f"测试功能仅支持 GPTMail 模式"
)
return
await update.message.reply_text("⏳ 正在测试邮箱创建...")
try:
# 测试创建邮箱
email, password = unified_create_email()
if email:
await update.message.reply_text(
f"<b>✅ 邮箱创建成功</b>\n\n"
f"邮箱: <code>{email}</code>\n"
f"密码: <code>{password}</code>\n\n"
f"当前 Key 数量: {len(get_gptmail_keys())}",
parse_mode="HTML"
)
else:
await update.message.reply_text(
"<b>❌ 邮箱创建失败</b>\n\n"
"请检查 GPTMail API Key 配置",
parse_mode="HTML"
)
except Exception as e:
await update.message.reply_text(f"❌ 测试失败: {e}")
@admin_only
async def cmd_iban_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看 IBAN 列表"""
try:
from auto_gpt_team import get_sepa_ibans
ibans = get_sepa_ibans()
if not ibans:
await update.message.reply_text(
"<b>💳 SEPA IBAN 列表</b>\n\n"
"📭 暂无 IBAN\n\n"
"使用 /iban_add 添加 IBAN",
parse_mode="HTML"
)
return
# 显示 IBAN 列表
lines = [f"<b>💳 SEPA IBAN 列表 ({len(ibans)} 个)</b>\n"]
for i, iban in enumerate(ibans[:50], 1): # 最多显示 50 个
lines.append(f"{i}. <code>{iban}</code>")
if len(ibans) > 50:
lines.append(f"\n... 还有 {len(ibans) - 50} 个未显示")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 获取 IBAN 列表失败: {e}")
@admin_only
async def cmd_iban_add(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""添加 IBAN"""
if not context.args:
await update.message.reply_text(
"<b>💳 添加 IBAN</b>\n\n"
"用法:\n"
"<code>/iban_add DE123... DE456...</code>\n"
"<code>/iban_add DE123...,DE456...</code>\n\n"
"支持空格或逗号分隔,每行一个也可以",
parse_mode="HTML"
)
return
try:
from auto_gpt_team import add_sepa_ibans
# 解析输入 (支持空格、逗号、换行分隔)
raw_input = " ".join(context.args)
# 替换逗号和换行为空格,然后按空格分割
ibans = [s.strip() for s in raw_input.replace(",", " ").replace("\n", " ").split() if s.strip()]
if not ibans:
await update.message.reply_text("❌ 未提供有效的 IBAN")
return
added, skipped, total = add_sepa_ibans(ibans)
await update.message.reply_text(
f"<b>✅ IBAN 导入完成</b>\n\n"
f"新增: {added}\n"
f"跳过 (重复): {skipped}\n"
f"当前总数: {total}",
parse_mode="HTML"
)
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 添加 IBAN 失败: {e}")
@admin_only
async def cmd_iban_clear(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""清空 IBAN 列表"""
# 需要确认
if not context.args or context.args[0].lower() != "confirm":
try:
from auto_gpt_team import get_sepa_ibans
count = len(get_sepa_ibans())
except:
count = 0
await update.message.reply_text(
f"<b>⚠️ 确认清空 IBAN 列表?</b>\n\n"
f"当前共有 {count} 个 IBAN\n\n"
f"确认请发送:\n"
f"<code>/iban_clear confirm</code>",
parse_mode="HTML"
)
return
try:
from auto_gpt_team import clear_sepa_ibans
clear_sepa_ibans()
await update.message.reply_text("<b>✅ IBAN 列表已清空</b>", parse_mode="HTML")
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 清空 IBAN 失败: {e}")
@admin_only
async def cmd_domain_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看邮箱域名列表"""
try:
from auto_gpt_team import get_email_domains
domains = get_email_domains()
if not domains:
await update.message.reply_text(
"<b>📧 邮箱域名列表</b>\n\n"
"📭 暂无域名\n\n"
"使用 /domain_add 添加域名",
parse_mode="HTML"
)
return
# 显示域名列表
lines = [f"<b>📧 邮箱域名列表 ({len(domains)} 个)</b>\n"]
for i, domain in enumerate(domains[:50], 1): # 最多显示 50 个
lines.append(f"{i}. <code>{domain}</code>")
if len(domains) > 50:
lines.append(f"\n... 还有 {len(domains) - 50} 个未显示")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 获取域名列表失败: {e}")
@admin_only
async def cmd_domain_add(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""添加邮箱域名"""
if not context.args:
await update.message.reply_text(
"<b>📧 添加邮箱域名</b>\n\n"
"用法:\n"
"<code>/domain_add @example.com</code>\n"
"<code>/domain_add @a.com,@b.com</code>\n\n"
"支持空格或逗号分隔\n"
"@ 符号可省略,会自动添加\n\n"
"<b>格式要求:</b>\n"
"• 域名需包含至少一个点号\n"
"• 只能包含字母、数字、连字符\n"
"• 顶级域名至少2个字符",
parse_mode="HTML"
)
return
try:
from auto_gpt_team import add_email_domains
# 解析输入 (支持空格、逗号、换行分隔)
raw_input = " ".join(context.args)
# 替换逗号和换行为空格,然后按空格分割
domains = [s.strip() for s in raw_input.replace(",", " ").replace("\n", " ").split() if s.strip()]
if not domains:
await update.message.reply_text("❌ 未提供有效的域名")
return
added, skipped, invalid, total = add_email_domains(domains)
# 构建响应消息
lines = ["<b>✅ 域名导入完成</b>\n"]
lines.append(f"新增: {added}")
lines.append(f"跳过 (重复): {skipped}")
if invalid > 0:
lines.append(f"无效 (格式错误): {invalid}")
lines.append(f"当前总数: {total}")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 添加域名失败: {e}")
@admin_only
async def cmd_domain_del(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""删除指定域名"""
if not context.args:
await update.message.reply_text(
"<b>📧 删除域名</b>\n\n"
"用法:\n"
"<code>/domain_del @example.com</code>\n\n"
"@ 符号可省略",
parse_mode="HTML"
)
return
try:
from auto_gpt_team import remove_email_domain, get_email_domains
domain = context.args[0].strip()
if remove_email_domain(domain):
total = len(get_email_domains())
await update.message.reply_text(
f"<b>✅ 域名已删除</b>\n\n"
f"已删除: <code>{domain}</code>\n"
f"剩余: {total}",
parse_mode="HTML"
)
else:
await update.message.reply_text(
f"❌ 域名不存在: <code>{domain}</code>",
parse_mode="HTML"
)
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 删除域名失败: {e}")
@admin_only
async def cmd_domain_clear(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""清空域名列表 (只清空txt文件中的域名保留config配置)"""
# 需要确认
if not context.args or context.args[0].lower() != "confirm":
try:
from auto_gpt_team import get_file_domains_count, EMAIL_DOMAINS
file_count = get_file_domains_count()
config_count = len(EMAIL_DOMAINS) if EMAIL_DOMAINS else 0
except:
file_count = 0
config_count = 0
if file_count == 0:
await update.message.reply_text(
"<b>📧 域名列表</b>\n\n"
"txt文件中没有可清空的域名\n"
f"config配置中的域名: {config_count} 个 (不会被清空)",
parse_mode="HTML"
)
return
await update.message.reply_text(
f"<b>⚠️ 确认清空域名列表?</b>\n\n"
f"将清空txt文件中的域名: {file_count}\n"
f"config配置中的域名: {config_count} 个 (不会被清空)\n\n"
f"确认请发送:\n"
f"<code>/domain_clear confirm</code>",
parse_mode="HTML"
)
return
try:
from auto_gpt_team import clear_email_domains, EMAIL_DOMAINS
cleared_count = clear_email_domains()
config_count = len(EMAIL_DOMAINS) if EMAIL_DOMAINS else 0
await update.message.reply_text(
f"<b>✅ 域名列表已清空</b>\n\n"
f"已清空: {cleared_count} 个域名\n"
f"保留 (config配置): {config_count}",
parse_mode="HTML"
)
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 清空域名失败: {e}")
@admin_only
async def cmd_team_fingerprint(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""切换 GPT Team 随机指纹"""
import tomli_w
try:
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 确保 GPT Team section 存在
if "GPT Team" not in config:
config["GPT Team"] = {}
# 获取当前状态
current = config.get("GPT Team", {}).get("random_fingerprint", True)
new_value = not current
# 更新配置
config["GPT Team"]["random_fingerprint"] = 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>🎭 GPT Team 随机指纹</b>\n\n"
f"状态: {status}\n\n"
f"开启后每次运行将随机使用不同的:\n"
f"• User-Agent (Chrome 139-144)\n"
f"• WebGL 显卡指纹 (NVIDIA/AMD/Intel)\n"
f"• 屏幕分辨率 (1080p/1440p/4K)\n\n"
f"💡 下次运行 auto_gpt_team.py 时生效",
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}")
async def _test_mail_api_connection(self, mail_api_base: str, mail_api_token: str, domain: str) -> tuple[bool, str]:
"""测试邮件 API 连接
Args:
mail_api_base: 邮件 API 地址
mail_api_token: 邮件 API Token
domain: 测试用的邮箱域名
Returns:
tuple: (success, message)
"""
import requests
import random
import string
try:
# 生成测试邮箱
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
test_email = f"test-{random_str}@{domain.lstrip('@')}"
# 测试 API 连接
url = f"{mail_api_base}/api/public/emailList"
headers = {
"Authorization": mail_api_token,
"Content-Type": "application/json"
}
payload = {
"toEmail": test_email,
"timeSort": "desc",
"size": 1
}
response = requests.post(url, headers=headers, json=payload, timeout=10)
data = response.json()
if data.get("code") == 200:
return True, "邮件 API 连接正常"
else:
error_msg = data.get("message", "未知错误")
return False, f"API 响应异常: {error_msg}"
except requests.exceptions.Timeout:
return False, "连接超时,无法连接到邮件 API 服务器"
except requests.exceptions.ConnectionError:
return False, "连接失败,请检查 mail_api_base 配置"
except Exception as e:
return False, f"测试失败: {e}"
@admin_only
async def cmd_team_register(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""开始 GPT Team 自动订阅注册"""
# 检查是否有任务正在运行
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 有任务正在运行: {self.current_team}\n"
"请等待任务完成或使用 /stop 停止后再开始注册"
)
return
# 检查配置
try:
from auto_gpt_team import MAIL_API_TOKEN, MAIL_API_BASE, get_email_domains, get_sepa_ibans
if not MAIL_API_TOKEN or not MAIL_API_BASE:
await update.message.reply_text(
"<b>❌ 配置错误</b>\n\n"
"请在 config.toml 中配置 [GPT Team] 段:\n"
"• mail_api_token\n"
"• mail_api_base",
parse_mode="HTML"
)
return
domains = get_email_domains()
if not domains:
await update.message.reply_text(
"<b>❌ 没有可用的邮箱域名</b>\n\n"
"请先使用 /domain_add 导入域名",
parse_mode="HTML"
)
return
ibans = get_sepa_ibans()
if not ibans:
await update.message.reply_text(
"<b>❌ 没有可用的 IBAN</b>\n\n"
"请先使用 /iban_add 导入 IBAN",
parse_mode="HTML"
)
return
# 测试邮件 API 连接
await update.message.reply_text("⏳ 正在检测邮件 API 连接...")
import random
test_domain = random.choice(domains)
success, message = await self._test_mail_api_connection(MAIL_API_BASE, MAIL_API_TOKEN, test_domain)
if not success:
await update.message.reply_text(
f"<b>❌ 邮件 API 连接失败</b>\n\n"
f"错误: {message}\n\n"
f"请检查配置后重试:\n"
f"• mail_api_base: {MAIL_API_BASE}\n"
f"• mail_api_token: {'已配置' if MAIL_API_TOKEN else '未配置'}",
parse_mode="HTML"
)
return
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
return
# 显示数量选择
keyboard = [
[
InlineKeyboardButton("1 个", callback_data="team_reg:count:1"),
InlineKeyboardButton("3 个", callback_data="team_reg:count:3"),
InlineKeyboardButton("5 个", callback_data="team_reg:count:5"),
],
[
InlineKeyboardButton("10 个", callback_data="team_reg:count:10"),
InlineKeyboardButton("20 个", callback_data="team_reg:count:20"),
InlineKeyboardButton("自定义", callback_data="team_reg:count:custom"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"<b>🚀 GPT Team 自动订阅</b>\n\n"
f"📧 邮箱域名: {len(domains)}\n"
f"💳 可用 IBAN: {len(ibans)}\n\n"
"请选择注册数量:",
parse_mode="HTML",
reply_markup=reply_markup
)
async def callback_team_register(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 GPT Team 注册回调"""
query = update.callback_query
# 权限检查
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.answer("⛔ 无权限", show_alert=True)
return
await query.answer()
data = query.data.split(":")
action = data[1] if len(data) > 1 else ""
value = data[2] if len(data) > 2 else ""
# 处理简化格式: team_reg:1, team_reg:3, team_reg:5, team_reg:custom
if action.isdigit():
# team_reg:1 -> 直接是数量
count = int(action)
# 显示输出方式选择
keyboard = [
[
InlineKeyboardButton("📄 JSON 文件", callback_data=f"team_reg:output:json:{count}"),
],
[
InlineKeyboardButton("📥 添加到 team.json", callback_data=f"team_reg:output:team:{count}"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>⚙️ 配置完成</b>\n\n"
f"注册数量: {count}\n\n"
f"请选择输出方式:",
parse_mode="HTML",
reply_markup=reply_markup
)
return
if action == "custom":
# team_reg:custom -> 自定义数量
await query.edit_message_text(
"<b>📝 自定义数量</b>\n\n"
"请发送数量 (1-50):\n"
"直接回复一个数字即可\n\n"
"例如: <code>20</code>",
parse_mode="HTML"
)
# 设置等待输入状态
context.user_data["team_waiting_count"] = True
return
if action == "count":
if value == "custom":
await query.edit_message_text(
"<b>📝 自定义数量</b>\n\n"
"请发送数量 (1-50):\n"
"直接回复一个数字即可\n\n"
"例如: <code>20</code>",
parse_mode="HTML"
)
# 设置等待输入状态
context.user_data["team_waiting_count"] = True
return
count = int(value)
# 显示输出方式选择
keyboard = [
[
InlineKeyboardButton("📄 JSON 文件", callback_data=f"team_reg:output:json:{count}"),
],
[
InlineKeyboardButton("📥 添加到 team.json", callback_data=f"team_reg:output:team:{count}"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>⚙️ 配置完成</b>\n\n"
f"注册数量: {count}\n\n"
f"请选择输出方式:",
parse_mode="HTML",
reply_markup=reply_markup
)
elif action == "output":
output_type = value # json 或 team
count = int(data[3]) if len(data) > 3 else 1
await query.edit_message_text(
f"<b>⚙️ 配置完成</b>\n\n"
f"注册数量: {count}\n"
f"输出方式: {'📄 JSON 文件' if output_type == 'json' else '📥 team.json'}\n\n"
f"即将开始完整注册流程...",
parse_mode="HTML"
)
# 开始注册任务
self.current_team = f"GPT Team 注册 ({count}个)"
# 重置停止标志,确保新任务可以正常运行
try:
import run
run._shutdown_requested = False
except Exception:
pass
self.current_task = asyncio.create_task(
self._run_team_registration(query.message.chat_id, count, output_type)
)
async def _run_team_registration(self, chat_id: int, count: int, output_type: str):
"""执行 GPT Team 注册任务 (支持并发)"""
from auto_gpt_team import run_single_registration_auto, cleanup_chrome_processes, get_register_mode
from config import CONCURRENT_ENABLED, CONCURRENT_WORKERS
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"🌐 <b>代理池预检测</b>\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"✅ <b>代理池就绪</b>\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
results_lock = threading.Lock()
# 获取当前注册模式和并发数
current_mode = get_register_mode()
mode_display = "🌐 协议模式" if current_mode == "api" else "🖥️ 浏览器模式"
# 确定并发数
workers = CONCURRENT_WORKERS if CONCURRENT_ENABLED else 1
workers = min(workers, count) # 不超过总数
# 当前步骤 (用于显示)
current_steps = {} # worker_id -> step
step_lock = threading.Lock()
completed_count = [0] # 使用列表以便在闭包中修改
def make_step_callback(worker_id):
"""创建步骤回调"""
def callback(step: str):
with step_lock:
current_steps[worker_id] = step
return callback
# 发送开始消息
progress_msg = await self.app.bot.send_message(
chat_id,
f"<b>🚀 开始注册</b>\n\n"
f"模式: {mode_display}\n"
f"并发: {workers}\n"
f"进度: 0/{count}\n"
f"{'' * 20}",
parse_mode="HTML"
)
# 进度更新任务
async def update_progress_loop():
"""定期更新进度消息"""
last_text = ""
while True:
await asyncio.sleep(1.5)
try:
with results_lock:
s_count = success_count
f_count = fail_count
with step_lock:
steps_copy = dict(current_steps)
total_done = s_count + f_count
progress = int(total_done / count * 20) if count > 0 else 0
progress_bar = '' * progress + '' * (20 - progress)
text = (
f"<b>🚀 注册中...</b>\n\n"
f"模式: {mode_display}\n"
f"并发: {workers}\n"
f"进度: {total_done}/{count}\n"
f"{progress_bar}\n\n"
f"✅ 成功: {s_count}\n"
f"❌ 失败: {f_count}\n"
)
# 显示各 worker 状态
if steps_copy:
text += "\n<b>工作状态:</b>\n"
for wid, step in sorted(steps_copy.items()):
if step:
text += f" #{wid+1}: {step[:30]}\n"
if text != last_text:
last_text = text
try:
await progress_msg.edit_text(text, parse_mode="HTML")
except:
pass
except asyncio.CancelledError:
break
except:
pass
# 启动进度更新任务
progress_task = asyncio.create_task(update_progress_loop())
# 任务队列
task_queue = list(range(count))
queue_lock = threading.Lock()
def worker_task(worker_id: int):
"""单个 worker 的任务"""
nonlocal success_count, fail_count
step_callback = make_step_callback(worker_id)
while True:
# 检查停止请求
try:
import run
if run._shutdown_requested:
step_callback("已停止")
break
except:
pass
# 获取任务
with queue_lock:
if not task_queue:
break
task_idx = task_queue.pop(0)
step_callback(f"{task_idx + 1} 个...")
try:
result = run_single_registration_auto(
progress_callback=None,
step_callback=step_callback
)
with results_lock:
if result.get("stopped"):
step_callback("已停止")
break
elif result.get("success"):
success_count += 1
results.append({
"account": result["account"],
"password": result["password"],
"token": result["token"],
"account_id": result.get("account_id", "")
})
else:
fail_count += 1
log.warning(f"Worker {worker_id}: 注册失败: {result.get('error', '未知错误')}")
except Exception as e:
with results_lock:
fail_count += 1
log.error(f"Worker {worker_id}: 注册异常: {e}")
# 清理浏览器进程
cleanup_chrome_processes()
# 清理 worker 状态
with step_lock:
if worker_id in current_steps:
del current_steps[worker_id]
# 使用线程池并发执行 (通过 run_in_executor 避免阻塞 event loop)
import concurrent.futures
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
async_futures = [loop.run_in_executor(executor, worker_task, i) for i in range(workers)]
results_done = await asyncio.gather(*async_futures, return_exceptions=True)
for r in results_done:
if isinstance(r, Exception):
log.error(f"Worker 异常: {r}")
# 检查是否被停止
stopped = False
try:
import run
stopped = run._shutdown_requested
except:
pass
# 停止进度更新任务
progress_task.cancel()
try:
await progress_task
except asyncio.CancelledError:
pass
# 完成进度
progress_bar = '' * 20
completed = success_count + fail_count
if stopped:
status_text = f"<b>🛑 注册已停止</b> {completed}/{count}"
else:
status_text = f"<b>🎉 注册完成!</b> {success_count}/{count}"
await progress_msg.edit_text(
f"{status_text}\n"
f"{progress_bar}\n\n"
f"✅ 成功: {success_count}\n"
f"❌ 失败: {fail_count}",
parse_mode="HTML"
)
# 处理结果
if results:
if output_type == "json":
# 生成 JSON 文件
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"team_accounts_{timestamp}.json"
filepath = Path(filename)
with open(filepath, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
# 发送文件
await self.app.bot.send_document(
chat_id,
document=open(filepath, "rb"),
filename=filename,
caption=f"📄 注册结果 ({success_count} 个账号)"
)
# 删除临时文件
filepath.unlink()
elif output_type == "team":
# 添加到 team.json
try:
team_file = TEAM_JSON_FILE
existing = []
if team_file.exists():
with open(team_file, "r", encoding="utf-8") as f:
existing = json.load(f)
existing.extend(results)
with open(team_file, "w", encoding="utf-8") as f:
json.dump(existing, f, ensure_ascii=False, indent=2)
# 重载配置
reload_config()
await self.app.bot.send_message(
chat_id,
f"<b>✅ 已添加到 team.json</b>\n\n"
f"新增: {success_count} 个账号\n"
f"当前总数: {len(existing)}",
parse_mode="HTML"
)
except Exception as e:
await self.app.bot.send_message(
chat_id,
f"❌ 保存到 team.json 失败: {e}"
)
self.current_task = None
self.current_team = None
@admin_only
async def cmd_update_token(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""更新 config.toml 中的邮件 API Token"""
if not context.args:
await update.message.reply_text(
"<b>📝 更新邮件 API Token</b>\n\n"
"用法: <code>/update_token &lt;new_token&gt;</code>\n\n"
"此命令会更新 config.toml 中的:\n"
"• [autogptplus] mail_api_token",
parse_mode="HTML"
)
return
new_token = context.args[0].strip()
if not new_token:
await update.message.reply_text("❌ Token 不能为空")
return
try:
import tomllib
import tomli_w
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
config = tomllib.load(f)
# 确保 autogptplus section 存在
if "autogptplus" not in config:
config["autogptplus"] = {}
# 获取旧 token (用于显示)
old_token = config["autogptplus"].get("mail_api_token", "")
old_display = f"{old_token[:8]}...{old_token[-4:]}" if len(old_token) > 12 else old_token or "(空)"
# 更新 token
config["autogptplus"]["mail_api_token"] = new_token
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
# 重载配置
reload_config()
new_display = f"{new_token[:8]}...{new_token[-4:]}" if len(new_token) > 12 else new_token
# 发送更新成功消息
progress_msg = await update.message.reply_text(
f"<b>✅ Token 更新成功</b>\n\n"
f"旧 Token: <code>{old_display}</code>\n"
f"新 Token: <code>{new_display}</code>\n\n"
f"⏳ 正在检测邮件 API 连接...",
parse_mode="HTML"
)
# 测试 API 连接
await self._test_mail_api_connection_with_edit(update.effective_chat.id, progress_msg.message_id, new_token, config)
except ImportError:
await update.message.reply_text(
"❌ 缺少 tomli_w 依赖\n"
"请运行: uv add tomli_w"
)
except Exception as e:
await update.message.reply_text(f"❌ 更新 Token 失败: {e}")
async def _test_mail_api_connection_with_edit(self, chat_id: int, message_id: int, token: str, config: dict):
"""测试邮件 API 连接 (带消息编辑)"""
import requests
mail_api_base = config.get("autogptplus", {}).get("mail_api_base", "")
if not mail_api_base:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text="<b>❌ 邮件 API 连接失败</b>\n\n"
"错误: mail_api_base 未配置\n\n"
"请检查配置后重试:\n"
"• mail_api_base: 未配置\n"
"• mail_api_token: 已配置",
parse_mode="HTML"
)
return
try:
# 测试 API 连接
url = f"{mail_api_base}/api/public/emailList"
headers = {
"Authorization": token,
"Content-Type": "application/json"
}
payload = {
"toEmail": "test@test.com",
"timeSort": "desc",
"size": 1
}
start_time = time.time()
response = requests.post(url, headers=headers, json=payload, timeout=10)
elapsed = time.time() - start_time
data = response.json()
token_display = f"{token[:8]}...{token[-4:]}" if len(token) > 12 else token
if response.status_code == 200 and data.get("code") == 200:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=f"<b>✅ Token 更新成功</b>\n\n"
f"新 Token: <code>{token_display}</code>\n\n"
f"<b>✅ 邮件 API 连接成功</b>\n"
f"• 服务器: {mail_api_base}\n"
f"• 响应时间: {elapsed*1000:.0f}ms\n"
f"• 状态: 正常",
parse_mode="HTML"
)
else:
error_msg = data.get("message", "未知错误")
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=f"<b>✅ Token 更新成功</b>\n\n"
f"新 Token: <code>{token_display}</code>\n\n"
f"<b>❌ 邮件 API 连接失败</b>\n"
f"错误: API 响应异常: {error_msg}\n\n"
f"请检查配置后重试:\n"
f"• mail_api_base: {mail_api_base}\n"
f"• mail_api_token: 已配置",
parse_mode="HTML"
)
except requests.exceptions.ConnectionError:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=f"<b>✅ Token 更新成功</b>\n\n"
f"<b>❌ 邮件 API 连接失败</b>\n"
f"错误: 无法连接到服务器\n\n"
f"请检查配置后重试:\n"
f"• mail_api_base: {mail_api_base}\n"
f"• mail_api_token: 已配置",
parse_mode="HTML"
)
except requests.exceptions.Timeout:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=f"<b>✅ Token 更新成功</b>\n\n"
f"<b>❌ 邮件 API 连接失败</b>\n"
f"错误: 连接超时\n\n"
f"请检查配置后重试:\n"
f"• mail_api_base: {mail_api_base}\n"
f"• mail_api_token: 已配置",
parse_mode="HTML"
)
except Exception as e:
await self.app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=f"<b>✅ Token 更新成功</b>\n\n"
f"<b>❌ 邮件 API 连接失败</b>\n"
f"错误: {str(e)}\n\n"
f"请检查配置后重试:\n"
f"• mail_api_base: {mail_api_base}\n"
f"• mail_api_token: 已配置",
parse_mode="HTML"
)
@admin_only
async def cmd_autogptplus(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""AutoGPTPlus 配置管理 - 交互式菜单"""
await update.message.reply_text(
"<b>🤖 AutoGPTPlus 管理面板</b>\n\n"
"ChatGPT 订阅自动化配置管理\n\n"
"请选择功能:",
parse_mode="HTML",
reply_markup=self._get_autogptplus_main_keyboard()
)
def _get_autogptplus_main_keyboard(self):
"""获取 AutoGPTPlus 主菜单键盘"""
# 检查协议模式是否可用
try:
from auto_gpt_team import is_api_mode_supported, get_register_mode
api_supported = is_api_mode_supported()
current_mode = get_register_mode()
except ImportError:
api_supported = False
current_mode = "browser"
# 获取当前并发数
try:
from config import CONCURRENT_WORKERS, CONCURRENT_ENABLED
current_workers = CONCURRENT_WORKERS if CONCURRENT_ENABLED else 1
except ImportError:
current_workers = 1
keyboard = [
[
InlineKeyboardButton("📋 查看配置", callback_data="autogptplus:config"),
InlineKeyboardButton("🔑 设置 Token", callback_data="autogptplus:set_token"),
],
[
InlineKeyboardButton("📧 域名管理", callback_data="autogptplus:domains"),
InlineKeyboardButton("💳 IBAN 管理", callback_data="autogptplus:ibans"),
],
[
InlineKeyboardButton("🎭 随机指纹", callback_data="autogptplus:fingerprint"),
InlineKeyboardButton("📊 统计信息", callback_data="autogptplus:stats"),
],
[
InlineKeyboardButton("📧 测试邮件", callback_data="autogptplus:test_email"),
InlineKeyboardButton("🔄 测试 API", callback_data="autogptplus:test_api"),
],
]
# 添加注册模式选择按钮
mode_icon = "🌐" if current_mode == "api" else "🖥️"
mode_text = "协议模式" if current_mode == "api" else "浏览器模式"
keyboard.append([
InlineKeyboardButton(f"⚙️ 注册模式: {mode_icon} {mode_text}", callback_data="autogptplus:select_mode"),
])
# 添加并发设置按钮
keyboard.append([
InlineKeyboardButton(f"⚡ 并发数: {current_workers}", callback_data="autogptplus:set_concurrent"),
])
keyboard.append([
InlineKeyboardButton("🚀 开始注册", callback_data="autogptplus:register"),
])
return InlineKeyboardMarkup(keyboard)
async def callback_run(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 run 命令的回调 - 选择数量和邮箱服务"""
query = update.callback_query
# 权限检查
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.answer("⛔ 无权限", show_alert=True)
return
await query.answer()
data = query.data.split(":")
action = data[1] if len(data) > 1 else ""
value = data[2] if len(data) > 2 else ""
if action == "cancel":
await query.edit_message_text("❌ 已取消")
return
if action == "custom":
# 提示用户输入自定义数量
context.user_data["waiting_run_count"] = True
context.user_data["run_message_id"] = query.message.message_id
await query.edit_message_text(
"<b>✏️ 自定义数量</b>\n\n"
f"请输入要处理的 Team 数量 (1-{len(TEAMS)}):",
parse_mode="HTML"
)
return
if action == "count":
# 选择了数量,显示邮箱服务选择
count = int(value)
context.user_data["run_team_count"] = count
keyboard = [
[
InlineKeyboardButton("📧 GPTMail", callback_data=f"run:email:gptmail"),
InlineKeyboardButton("☁️ Cloud Mail", callback_data=f"run:email:cloudmail"),
],
[
InlineKeyboardButton("⬅️ 返回", callback_data="run:back"),
InlineKeyboardButton("❌ 取消", callback_data="run:cancel"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>🚀 启动处理 Team</b>\n\n"
f"已选择: <b>{count}</b> 个 Team\n\n"
f"请选择邮箱服务:",
parse_mode="HTML",
reply_markup=reply_markup
)
return
if action == "back":
# 返回数量选择
total_teams = len(TEAMS)
keyboard = [
[
InlineKeyboardButton(f"📦 全部 ({total_teams})", callback_data=f"run:count:{total_teams}"),
],
[
InlineKeyboardButton("✏️ 自定义数量", callback_data="run:custom"),
],
[
InlineKeyboardButton("❌ 取消", callback_data="run:cancel"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>🚀 启动处理 Team</b>\n\n"
f"{total_teams} 个 Team 可处理\n\n"
f"请选择要处理的数量:",
parse_mode="HTML",
reply_markup=reply_markup
)
return
if action == "email":
# 选择了邮箱服务,开始测试连接并执行任务
email_provider = value
count = context.user_data.get("run_team_count", 1)
await query.edit_message_text(
f"<b>⏳ 正在测试邮箱服务连接...</b>\n\n"
f"邮箱服务: {'GPTMail' if email_provider == 'gptmail' else 'Cloud Mail'}",
parse_mode="HTML"
)
# 测试邮箱连接
success, message = await self._test_email_provider_connection(email_provider)
if not success:
keyboard = [
[
InlineKeyboardButton("🔄 重试", callback_data=f"run:email:{email_provider}"),
InlineKeyboardButton("❌ 取消", callback_data="run:cancel"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 邮箱服务连接失败</b>\n\n"
f"邮箱服务: {'GPTMail' if email_provider == 'gptmail' else 'Cloud Mail'}\n"
f"错误: {message}\n\n"
f"请检查配置后重试",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 连接成功,更新配置并开始任务
await self._update_email_provider(email_provider)
await query.edit_message_text(
f"<b>✅ 邮箱服务连接成功</b>\n\n"
f"邮箱服务: {'GPTMail' if email_provider == 'gptmail' else 'Cloud Mail'}\n"
f"响应: {message}\n\n"
f"🚀 开始处理 {count} 个 Team...",
parse_mode="HTML"
)
# 启动任务
if count == 1:
self.current_team = TEAMS[0].get("name", "Team0")
else:
self.current_team = f"{count}"
# 重置停止标志
try:
import run
run._shutdown_requested = False
except Exception:
pass
loop = asyncio.get_event_loop()
self.current_task = loop.run_in_executor(
self.executor,
self._run_teams_by_count_task,
count
)
self.current_task = asyncio.ensure_future(self._wrap_task(self.current_task, self.current_team))
async def callback_run_all(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 run_all 邮箱选择回调"""
query = update.callback_query
# 权限检查
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.answer("⛔ 无权限", show_alert=True)
return
await query.answer()
data = query.data.split(":")
action = data[1] if len(data) > 1 else ""
value = data[2] if len(data) > 2 else ""
if action == "cancel":
await query.edit_message_text("❌ 已取消")
return
if action == "select_email":
# 选择了邮箱服务,开始测试连接
email_provider = value # gptmail 或 cloudmail
await query.edit_message_text(
f"<b>⏳ 正在测试邮箱服务连接...</b>\n\n"
f"邮箱服务: {'GPTMail' if email_provider == 'gptmail' else 'Cloud Mail'}",
parse_mode="HTML"
)
# 测试邮箱连接
success, message = await self._test_email_provider_connection(email_provider)
if not success:
keyboard = [
[
InlineKeyboardButton("🔄 重试", callback_data=f"run_all:select_email:{email_provider}"),
InlineKeyboardButton("❌ 取消", callback_data="run_all:cancel"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 邮箱服务连接失败</b>\n\n"
f"邮箱服务: {'GPTMail' if email_provider == 'gptmail' else 'Cloud Mail'}\n"
f"错误: {message}\n\n"
f"请检查配置后重试",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 连接成功,更新配置并开始任务
await self._update_email_provider(email_provider)
await query.edit_message_text(
f"<b>✅ 邮箱服务连接成功</b>\n\n"
f"邮箱服务: {'GPTMail' if email_provider == 'gptmail' else 'Cloud Mail'}\n"
f"响应: {message}\n\n"
f"🚀 开始处理所有 Team (共 {len(TEAMS)} 个)...",
parse_mode="HTML"
)
# 启动任务
self.current_team = "全部"
# 重置停止标志
try:
import run
run._shutdown_requested = False
except Exception:
pass
loop = asyncio.get_event_loop()
self.current_task = loop.run_in_executor(
self.executor,
self._run_all_teams_task
)
self.current_task = asyncio.ensure_future(self._wrap_task(self.current_task, "全部"))
async def _test_email_provider_connection(self, provider: str) -> tuple:
"""测试邮箱服务连接
Args:
provider: 邮箱服务类型 (gptmail / cloudmail)
Returns:
tuple: (success, message)
"""
import requests
try:
if provider == "gptmail":
# 测试 GPTMail
from config import GPTMAIL_API_BASE, get_gptmail_keys
keys = get_gptmail_keys()
if not keys:
return False, "没有配置 GPTMail API Key"
api_key = keys[0]
# 使用正确的 API 接口: /api/generate-email (GET 请求)
url = f"{GPTMAIL_API_BASE}/api/generate-email"
headers = {
"X-API-Key": api_key, # 正确的请求头
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get("success"):
email = data.get("data", {}).get("email", "")
return True, f"生成测试邮箱: {email}"
else:
return False, data.get("error", "Unknown error")
else:
return False, f"HTTP {response.status_code}"
elif provider == "cloudmail":
# 测试 Cloud Mail
from config import EMAIL_API_BASE, EMAIL_API_AUTH, EMAIL_DOMAINS
if not EMAIL_API_BASE:
return False, "未配置 email.api_base"
if not EMAIL_API_AUTH:
return False, "未配置 email.api_auth"
if not EMAIL_DOMAINS:
return False, "未配置 email.domains"
# 自动补全 /api/public 路径
api_base = EMAIL_API_BASE.rstrip("/")
if not api_base.endswith("/api/public"):
api_base = f"{api_base}/api/public"
url = f"{api_base}/emailList"
headers = {
"Authorization": EMAIL_API_AUTH,
"Content-Type": "application/json"
}
payload = {
"toEmail": f"test@{EMAIL_DOMAINS[0]}",
"timeSort": "desc",
"size": 1
}
response = requests.post(url, headers=headers, json=payload, timeout=10)
# 检查响应状态码
if response.status_code != 200:
return False, f"HTTP {response.status_code}: {response.text[:100]}"
# 检查响应内容
if not response.text or not response.text.strip():
return False, "API 返回空响应"
try:
data = response.json()
except Exception:
return False, f"API 返回非 JSON 格式: {response.text[:100]}"
if data.get("code") == 200:
return True, "API 连接正常"
else:
return False, data.get("message", f"错误码: {data.get('code')}")
return False, "未知的邮箱服务类型"
except requests.exceptions.Timeout:
return False, "连接超时"
except requests.exceptions.ConnectionError:
return False, "无法连接服务器"
except Exception as e:
return False, str(e)
async def _update_email_provider(self, provider: str):
"""更新邮箱服务配置
Args:
provider: 邮箱服务类型 (gptmail / cloudmail)
"""
try:
import tomli_w
from config import CONFIG_FILE
# 读取当前配置
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
content = f.read()
# 更新 email_provider
import re
if re.search(r'^email_provider\s*=', content, re.MULTILINE):
content = re.sub(
r'^(email_provider\s*=\s*).*$',
f'\\g<1>"{provider}"',
content,
flags=re.MULTILINE
)
else:
# 在文件开头添加
content = f'email_provider = "{provider}"\n' + content
# 写回配置文件
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
f.write(content)
# 重载配置
from config import reload_config
reload_config()
except Exception as e:
log.error(f"更新邮箱配置失败: {e}")
async def callback_cloudmail(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 Cloud Mail 回调"""
query = update.callback_query
# 权限检查
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.answer("⛔ 无权限", show_alert=True)
return
await query.answer()
data = query.data.split(":")
action = data[1] if len(data) > 1 else ""
if action == "set_token":
await query.edit_message_text(
"<b>🔑 设置 Cloud Mail Token</b>\n\n"
"请使用命令设置:\n"
"<code>/cloudmail_token &lt;your_token&gt;</code>",
parse_mode="HTML"
)
elif action == "set_api":
await query.edit_message_text(
"<b>🌐 设置 Cloud Mail API 地址</b>\n\n"
"请使用命令设置:\n"
"<code>/cloudmail_api &lt;url&gt;</code>\n\n"
"示例:\n"
"<code>/cloudmail_api https://mail.example.com/api/public</code>",
parse_mode="HTML"
)
elif action == "domains":
await query.edit_message_text(
"<b>📧 Cloud Mail 域名管理</b>\n\n"
"请使用命令管理:\n"
"• <code>/cloudmail_domains</code> - 查看域名列表\n"
"• <code>/cloudmail_domains add domain.com</code> - 添加域名\n"
"• <code>/cloudmail_domains del domain.com</code> - 删除域名",
parse_mode="HTML"
)
elif action == "test":
await query.edit_message_text("⏳ 正在测试 Cloud Mail 连接...")
success, message = await self._test_email_provider_connection("cloudmail")
if success:
await query.edit_message_text(
f"<b>✅ Cloud Mail 连接成功</b>\n\n"
f"状态: {message}",
parse_mode="HTML"
)
else:
await query.edit_message_text(
f"<b>❌ Cloud Mail 连接失败</b>\n\n"
f"错误: {message}",
parse_mode="HTML"
)
elif action == "activate":
await self._update_email_provider("cloudmail")
await query.answer("✅ 已切换到 Cloud Mail", show_alert=True)
# 刷新面板
from config import EMAIL_API_BASE, EMAIL_API_AUTH, EMAIL_DOMAINS
api_base = EMAIL_API_BASE or "未配置"
auth_display = "未配置"
if EMAIL_API_AUTH:
if len(EMAIL_API_AUTH) > 10:
auth_display = f"{EMAIL_API_AUTH[:8]}...{EMAIL_API_AUTH[-4:]}"
else:
auth_display = EMAIL_API_AUTH[:4] + "..."
domains_count = len(EMAIL_DOMAINS) if EMAIL_DOMAINS else 0
domains_display = ", ".join(EMAIL_DOMAINS[:3]) if EMAIL_DOMAINS else "未配置"
if domains_count > 3:
domains_display += f" (+{domains_count - 3})"
keyboard = [
[
InlineKeyboardButton("🔑 设置 Token", callback_data="cloudmail:set_token"),
InlineKeyboardButton("🌐 设置 API 地址", callback_data="cloudmail:set_api"),
],
[
InlineKeyboardButton("📧 域名管理", callback_data="cloudmail:domains"),
InlineKeyboardButton("🔄 测试连接", callback_data="cloudmail:test"),
],
[
InlineKeyboardButton("✅ 当前邮箱服务", callback_data="cloudmail:activate"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>☁️ Cloud Mail 管理</b>\n\n"
f"<b>当前配置:</b>\n"
f" API 地址: <code>{api_base}</code>\n"
f" Token: <code>{auth_display}</code>\n"
f" 域名数量: {domains_count}\n"
f" 域名: {domains_display}\n\n"
f"<b>状态:</b> ✅ 当前使用中\n\n"
f"选择操作:",
parse_mode="HTML",
reply_markup=reply_markup
)
async def callback_autogptplus(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 AutoGPTPlus 回调"""
query = update.callback_query
# 权限检查
user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await query.answer("⛔ 无权限", show_alert=True)
return
await query.answer()
data = query.data.split(":")
action = data[1] if len(data) > 1 else ""
sub_action = data[2] if len(data) > 2 else ""
if action == "config":
await self._show_autogptplus_config(query)
elif action == "set_token":
await self._prompt_autogptplus_token(query, context)
elif action == "test_email":
await self._test_autogptplus_email(query)
elif action == "test_api":
await self._test_autogptplus_api(query)
elif action == "domains":
await self._show_autogptplus_domains(query, sub_action, context)
elif action == "ibans":
await self._show_autogptplus_ibans(query, sub_action, context)
elif action == "fingerprint":
await self._toggle_autogptplus_fingerprint(query)
elif action == "stats":
await self._show_autogptplus_stats(query)
elif action == "select_mode":
await self._show_autogptplus_mode_selection(query)
elif action == "set_mode":
await self._set_autogptplus_mode(query, sub_action)
elif action == "register":
await self._start_autogptplus_register(query, context)
elif action == "set_concurrent":
await self._show_autogptplus_concurrent_selection(query)
elif action == "concurrent":
await self._set_autogptplus_concurrent(query, sub_action)
elif action == "back":
# 返回主菜单
await query.edit_message_text(
"<b>🤖 AutoGPTPlus 管理面板</b>\n\n"
"ChatGPT 订阅自动化配置管理\n\n"
"请选择功能:",
parse_mode="HTML",
reply_markup=self._get_autogptplus_main_keyboard()
)
async def _prompt_autogptplus_token(self, query, context: ContextTypes.DEFAULT_TYPE):
"""提示用户输入 Token"""
try:
from auto_gpt_team import MAIL_API_TOKEN
# 脱敏显示当前 Token
current_display = "未配置"
if MAIL_API_TOKEN:
if len(MAIL_API_TOKEN) > 10:
current_display = f"{MAIL_API_TOKEN[:8]}...{MAIL_API_TOKEN[-4:]}"
else:
current_display = MAIL_API_TOKEN[:4] + "..."
keyboard = [[InlineKeyboardButton("❌ 取消", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>🔑 设置 Cloud Mail API Token</b>\n\n"
f"当前 Token: <code>{current_display}</code>\n\n"
"请直接发送新的 Token:\n"
"(发送后将自动保存到 config.toml)",
parse_mode="HTML",
reply_markup=reply_markup
)
# 设置等待输入状态
context.user_data["autogptplus_waiting_token"] = True
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>\n\n"
"auto_gpt_team 模块未安装或导入失败",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _show_autogptplus_config(self, query):
"""显示 AutoGPTPlus 配置"""
try:
from auto_gpt_team import (
MAIL_API_TOKEN, MAIL_API_BASE, EMAIL_DOMAINS,
SEPA_IBANS, RANDOM_FINGERPRINT, get_email_domains, get_sepa_ibans,
REGISTER_MODE, API_PROXY, is_api_mode_supported
)
# 脱敏显示 Token
token_display = "未配置"
if MAIL_API_TOKEN:
if len(MAIL_API_TOKEN) > 10:
token_display = f"{MAIL_API_TOKEN[:8]}...{MAIL_API_TOKEN[-4:]}"
else:
token_display = MAIL_API_TOKEN[:4] + "..."
# 获取域名列表
domains = get_email_domains()
domains_display = ", ".join(domains[:3]) if domains else "未配置"
if len(domains) > 3:
domains_display += f" (+{len(domains) - 3})"
# 获取 IBAN 列表
ibans = get_sepa_ibans()
# 随机指纹状态
fingerprint_status = "✅ 已开启" if RANDOM_FINGERPRINT else "❌ 已关闭"
# 注册模式
api_supported = is_api_mode_supported()
if REGISTER_MODE == "api":
mode_display = "🌐 协议模式 (API)"
else:
mode_display = "🖥️ 浏览器模式"
if not api_supported:
mode_display += " (协议模式不可用)"
# 代理配置
proxy_display = API_PROXY if API_PROXY else "未配置"
lines = [
"<b>📋 AutoGPTPlus 配置</b>",
"",
"<b>🔑 Cloud Mail API</b>",
f" Token: <code>{token_display}</code>",
f" 地址: {MAIL_API_BASE or '未配置'}",
"",
"<b>📧 邮箱域名</b>",
f" 数量: {len(domains)}",
f" 域名: {domains_display}",
"",
"<b>💳 SEPA IBAN</b>",
f" 数量: {len(ibans)}",
"",
"<b>⚙️ 注册设置</b>",
f" 模式: {mode_display}",
f" 随机指纹: {fingerprint_status}",
f" API 代理: {proxy_display}",
]
# 配置状态检查
config_ok = bool(MAIL_API_TOKEN and MAIL_API_BASE and domains)
if config_ok:
lines.append("\n<b>✅ 配置完整,可以使用</b>")
else:
lines.append("\n<b>⚠️ 配置不完整:</b>")
if not MAIL_API_TOKEN:
lines.append(" • 缺少 mail_api_token")
if not MAIL_API_BASE:
lines.append(" • 缺少 mail_api_base")
if not domains:
lines.append(" • 缺少 email_domains")
keyboard = [
[
InlineKeyboardButton("🔑 设置 Token", callback_data="autogptplus:set_token"),
InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back"),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"\n".join(lines),
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>\n\n"
"auto_gpt_team 模块未安装或导入失败",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"❌ 获取配置失败: {e}",
reply_markup=reply_markup
)
async def _show_autogptplus_concurrent_selection(self, query):
"""显示并发数选择界面"""
try:
from config import CONCURRENT_WORKERS, CONCURRENT_ENABLED
current_workers = CONCURRENT_WORKERS if CONCURRENT_ENABLED else 1
except ImportError:
current_workers = 1
keyboard = [
[
InlineKeyboardButton(
f"{'' if current_workers == 1 else ''}1 并发",
callback_data="autogptplus:concurrent:1"
),
InlineKeyboardButton(
f"{'' if current_workers == 3 else ''}3 并发",
callback_data="autogptplus:concurrent:3"
),
InlineKeyboardButton(
f"{'' if current_workers == 5 else ''}5 并发",
callback_data="autogptplus:concurrent:5"
),
],
[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>⚡ 设置并发数</b>\n\n"
f"当前并发数: <b>{current_workers}</b>\n\n"
"选择注册时的并发数量:\n"
"• 1 并发 - 稳定,适合测试\n"
"• 3 并发 - 平衡速度和稳定性\n"
"• 5 并发 - 最快,需要较好的网络",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _set_autogptplus_concurrent(self, query, workers_str: str):
"""设置并发数"""
try:
workers = int(workers_str)
if workers not in [1, 3, 5]:
await query.answer("❌ 无效的并发数", show_alert=True)
return
except ValueError:
await query.answer("❌ 无效的并发数", show_alert=True)
return
# 更新 config.toml
try:
from config import CONFIG_FILE
# 读取当前配置文件
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
content = f.read()
# 检查是否存在 [concurrent] section
if "[concurrent]" not in content:
# 添加 [concurrent] section
content += f"\n\n[concurrent]\nenabled = true\nworkers = {workers}\n"
else:
# 更新 workers 值
import re
# 更新 enabled
if re.search(r"^\s*enabled\s*=", content, re.MULTILINE):
content = re.sub(
r"^(\s*enabled\s*=\s*).*$",
f"\\g<1>true",
content,
flags=re.MULTILINE
)
else:
# 在 [concurrent] 后添加 enabled
content = content.replace("[concurrent]", "[concurrent]\nenabled = true")
# 更新 workers
if re.search(r"^\s*workers\s*=", content, re.MULTILINE):
content = re.sub(
r"^(\s*workers\s*=\s*).*$",
f"\\g<1>{workers}",
content,
flags=re.MULTILINE
)
else:
# 在 [concurrent] section 中添加 workers
content = re.sub(
r"(\[concurrent\][^\[]*)",
f"\\g<1>workers = {workers}\n",
content
)
# 写回配置文件
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
f.write(content)
# 重载配置
from config import reload_config
reload_config()
await query.answer(f"✅ 并发数已设置为 {workers}", show_alert=True)
# 返回主菜单
await query.edit_message_text(
"<b>🤖 AutoGPTPlus 管理面板</b>\n\n"
"ChatGPT 订阅自动化配置管理\n\n"
"请选择功能:",
parse_mode="HTML",
reply_markup=self._get_autogptplus_main_keyboard()
)
except Exception as e:
await query.answer(f"❌ 设置失败: {e}", show_alert=True)
"""测试 AutoGPTPlus 邮件创建"""
await query.edit_message_text("⏳ 正在测试邮件创建...")
try:
from auto_gpt_team import (
MAIL_API_TOKEN, MAIL_API_BASE, get_email_domains
)
import requests
import random
import string
# 检查配置
if not MAIL_API_TOKEN or not MAIL_API_BASE:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 配置不完整</b>\n\n"
"请先在 config.toml 中配置:\n"
"• mail_api_token\n"
"• mail_api_base",
parse_mode="HTML",
reply_markup=reply_markup
)
return
domains = get_email_domains()
if not domains:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 邮箱域名未配置</b>\n\n"
"请先在 config.toml 中配置 email_domains\n"
"或使用 /domain_add 添加域名",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 生成测试邮箱
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
domain = random.choice(domains)
test_email = f"test-{random_str}@{domain.lstrip('@')}"
# 测试创建邮箱 (通过查询邮件列表来验证 API 连接)
url = f"{MAIL_API_BASE}/api/public/emailList"
headers = {
"Authorization": MAIL_API_TOKEN,
"Content-Type": "application/json"
}
payload = {
"toEmail": test_email,
"timeSort": "desc",
"size": 1
}
response = requests.post(url, headers=headers, json=payload, timeout=10)
data = response.json()
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
if data.get("code") == 200:
await query.edit_message_text(
"<b>✅ 邮件 API 测试成功</b>\n\n"
f"测试邮箱: <code>{test_email}</code>\n"
f"API 响应: 正常\n\n"
f"邮件系统已就绪,可以接收验证码",
parse_mode="HTML",
reply_markup=reply_markup
)
else:
error_msg = data.get("message", "未知错误")
await query.edit_message_text(
f"<b>⚠️ API 响应异常</b>\n\n"
f"状态码: {data.get('code')}\n"
f"错误: {error_msg}",
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>\n\n"
"auto_gpt_team 模块未安装或导入失败",
parse_mode="HTML",
reply_markup=reply_markup
)
except requests.exceptions.Timeout:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 连接超时</b>\n\n"
"无法连接到邮件 API 服务器\n"
"请检查 mail_api_base 配置",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 测试失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _test_autogptplus_api(self, query):
"""测试 AutoGPTPlus API 连接"""
await query.edit_message_text("⏳ 正在测试 API 连接...")
try:
from auto_gpt_team import MAIL_API_TOKEN, MAIL_API_BASE
import requests
# 检查配置
if not MAIL_API_TOKEN or not MAIL_API_BASE:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 配置不完整</b>\n\n"
"请先在 config.toml 中配置:\n"
"• mail_api_token\n"
"• mail_api_base",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 测试 API 连接
url = f"{MAIL_API_BASE}/api/public/emailList"
headers = {
"Authorization": MAIL_API_TOKEN,
"Content-Type": "application/json"
}
payload = {
"toEmail": "test@test.com",
"timeSort": "desc",
"size": 1
}
start_time = time.time()
response = requests.post(url, headers=headers, json=payload, timeout=10)
elapsed = time.time() - start_time
data = response.json()
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
if response.status_code == 200 and data.get("code") == 200:
await query.edit_message_text(
"<b>✅ API 连接测试成功</b>\n\n"
f"<b>服务器:</b> {MAIL_API_BASE}\n"
f"<b>响应时间:</b> {elapsed*1000:.0f}ms\n"
f"<b>状态:</b> 正常\n\n"
"Cloud Mail API 服务运行正常",
parse_mode="HTML",
reply_markup=reply_markup
)
else:
error_msg = data.get("message", "未知错误")
await query.edit_message_text(
f"<b>⚠️ API 响应异常</b>\n\n"
f"<b>HTTP 状态:</b> {response.status_code}\n"
f"<b>API 状态:</b> {data.get('code')}\n"
f"<b>错误:</b> {error_msg}\n"
f"<b>响应时间:</b> {elapsed*1000:.0f}ms",
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>\n\n"
"auto_gpt_team 模块未安装或导入失败",
parse_mode="HTML",
reply_markup=reply_markup
)
except requests.exceptions.ConnectionError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 连接失败</b>\n\n"
"无法连接到邮件 API 服务器\n"
"请检查:\n"
"• mail_api_base 地址是否正确\n"
"• 服务器是否在线\n"
"• 网络连接是否正常",
parse_mode="HTML",
reply_markup=reply_markup
)
except requests.exceptions.Timeout:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 连接超时</b>\n\n"
"API 服务器响应超时 (>10s)\n"
"请检查服务器状态",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 测试失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _show_autogptplus_domains(self, query, sub_action: str = "", context=None):
"""显示/管理域名"""
try:
from auto_gpt_team import (
get_email_domains, get_file_domains_count, EMAIL_DOMAINS,
clear_email_domains
)
if sub_action == "clear":
# 清空域名
cleared = clear_email_domains()
config_count = len(EMAIL_DOMAINS) if EMAIL_DOMAINS else 0
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:domains")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>✅ 域名已清空</b>\n\n"
f"已清空: {cleared}\n"
f"保留 (config): {config_count}",
parse_mode="HTML",
reply_markup=reply_markup
)
return
if sub_action == "add":
# 提示添加域名
keyboard = [[InlineKeyboardButton("❌ 取消", callback_data="autogptplus:domains")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b> 添加邮箱域名</b>\n\n"
"请直接发送要添加的域名:\n\n"
"<b>支持格式:</b>\n"
"• 单个: <code>@example.com</code>\n"
"• 多个: <code>@a.com @b.com</code>\n"
"• 逗号分隔: <code>@a.com,@b.com</code>\n\n"
"<i>@ 符号可省略,会自动添加</i>",
parse_mode="HTML",
reply_markup=reply_markup
)
if context:
context.user_data["autogptplus_waiting_domain"] = True
return
# 显示域名列表
domains = get_email_domains()
file_count = get_file_domains_count()
config_count = len(EMAIL_DOMAINS) if EMAIL_DOMAINS else 0
lines = ["<b>📧 邮箱域名管理</b>\n"]
lines.append(f"<b>总计:</b> {len(domains)}")
lines.append(f" • txt文件: {file_count}")
lines.append(f" • config配置: {config_count}\n")
if domains:
lines.append("<b>域名列表:</b>")
for i, domain in enumerate(domains[:15], 1):
lines.append(f" {i}. <code>{domain}</code>")
if len(domains) > 15:
lines.append(f" ... 还有 {len(domains) - 15}")
keyboard = [
[
InlineKeyboardButton(" 添加域名", callback_data="autogptplus:domains:add"),
InlineKeyboardButton("🗑️ 清空txt域名", callback_data="autogptplus:domains:clear"),
],
[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"\n".join(lines),
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 操作失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _show_autogptplus_ibans(self, query, sub_action: str = "", context=None):
"""显示/管理 IBAN"""
try:
from auto_gpt_team import get_sepa_ibans, load_ibans_from_file, SEPA_IBANS, clear_sepa_ibans
if sub_action == "clear":
# 清空 IBAN
clear_sepa_ibans()
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:ibans")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>✅ IBAN 列表已清空</b>",
parse_mode="HTML",
reply_markup=reply_markup
)
return
if sub_action == "add":
# 提示添加 IBAN
keyboard = [[InlineKeyboardButton("❌ 取消", callback_data="autogptplus:ibans")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b> 添加 IBAN</b>\n\n"
"请直接发送要添加的 IBAN:\n\n"
"<b>支持格式:</b>\n"
"• 单个: <code>DE89370400440532013000</code>\n"
"• 多个: 每行一个或逗号分隔\n\n"
"<i>只支持德国 IBAN (DE开头)</i>",
parse_mode="HTML",
reply_markup=reply_markup
)
if context:
context.user_data["autogptplus_waiting_iban"] = True
return
# 显示 IBAN 列表
ibans = get_sepa_ibans()
file_ibans = load_ibans_from_file()
config_count = len(SEPA_IBANS) if SEPA_IBANS else 0
lines = ["<b>💳 IBAN 管理</b>\n"]
lines.append(f"<b>总计:</b> {len(ibans)}")
lines.append(f" • txt文件: {len(file_ibans)}")
lines.append(f" • config配置: {config_count}\n")
if ibans:
lines.append("<b>IBAN 列表:</b>")
for i, iban in enumerate(ibans[:10], 1):
# 脱敏显示
masked = f"{iban[:8]}...{iban[-4:]}" if len(iban) > 12 else iban
lines.append(f" {i}. <code>{masked}</code>")
if len(ibans) > 10:
lines.append(f" ... 还有 {len(ibans) - 10}")
keyboard = [
[
InlineKeyboardButton(" 添加IBAN", callback_data="autogptplus:ibans:add"),
InlineKeyboardButton("🗑️ 清空IBAN", callback_data="autogptplus:ibans:clear"),
],
[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"\n".join(lines),
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 操作失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _toggle_autogptplus_fingerprint(self, query):
"""切换随机指纹"""
import tomli_w
try:
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 确保 autogptplus section 存在
if "autogptplus" not in config:
config["autogptplus"] = {}
# 获取当前状态并切换
current = config.get("autogptplus", {}).get("random_fingerprint", True)
new_value = not current
# 更新配置
config["autogptplus"]["random_fingerprint"] = new_value
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
# 重新加载模块
import importlib
import auto_gpt_team
importlib.reload(auto_gpt_team)
status = "✅ 已开启" if new_value else "❌ 已关闭"
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>🎭 随机指纹设置</b>\n\n"
f"状态: {status}\n\n"
f"{'每次注册将使用随机浏览器指纹' if new_value else '将使用固定浏览器指纹'}",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 设置失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _show_autogptplus_mode_selection(self, query):
"""显示注册模式选择界面"""
try:
from auto_gpt_team import is_api_mode_supported, get_register_mode
api_supported = is_api_mode_supported()
current_mode = get_register_mode()
lines = [
"<b>⚙️ 选择注册模式</b>\n",
"请选择 ChatGPT Team 注册使用的方式:\n",
]
# 浏览器模式说明
browser_check = "" if current_mode == "browser" else ""
lines.append(f"<b>{browser_check} 🖥️ 浏览器模式</b>")
lines.append("全程使用 DrissionPage 浏览器自动化")
lines.append("• 兼容性好,无需额外依赖")
lines.append("• 速度较慢,资源占用较高")
lines.append("")
# 协议模式说明
api_check = "" if current_mode == "api" else ""
api_status = "" if api_supported else " (不可用)"
lines.append(f"<b>{api_check} 🌐 协议模式{api_status}</b>")
lines.append("使用 API 快速注册,仅支付环节用浏览器")
lines.append("• 速度快,资源占用少")
lines.append("• 需要 curl_cffi 依赖")
if not api_supported:
lines.append("• <i>请安装: pip install curl_cffi</i>")
# 构建按钮
keyboard = []
# 浏览器模式按钮
browser_icon = "" if current_mode == "browser" else "🖥️"
keyboard.append([
InlineKeyboardButton(
f"{browser_icon} 浏览器模式",
callback_data="autogptplus:set_mode:browser"
)
])
# 协议模式按钮
if api_supported:
api_icon = "" if current_mode == "api" else "🌐"
keyboard.append([
InlineKeyboardButton(
f"{api_icon} 协议模式 (推荐)",
callback_data="autogptplus:set_mode:api"
)
])
else:
keyboard.append([
InlineKeyboardButton(
"🌐 协议模式 (需安装依赖)",
callback_data="autogptplus:set_mode:api_unavailable"
)
])
keyboard.append([
InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")
])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"\n".join(lines),
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 模块导入失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 获取配置失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _set_autogptplus_mode(self, query, mode: str):
"""设置注册模式"""
import tomli_w
try:
from auto_gpt_team import is_api_mode_supported, get_register_mode, set_register_mode
# 处理协议模式不可用的情况
if mode == "api_unavailable":
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:select_mode")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 协议模式不可用</b>\n\n"
"需要安装 curl_cffi 依赖:\n"
"<code>pip install curl_cffi</code>\n\n"
"安装后重启程序即可使用协议模式",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 检查是否已经是当前模式
current_mode = get_register_mode()
if mode == current_mode:
await query.answer(f"当前已是{'协议' if mode == 'api' else '浏览器'}模式", show_alert=False)
return
# 检查协议模式是否可用
if mode == "api" and not is_api_mode_supported():
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:select_mode")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 协议模式不可用</b>\n\n"
"需要安装 curl_cffi 依赖:\n"
"<code>pip install curl_cffi</code>\n\n"
"安装后重启程序即可使用协议模式",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 确保 autogptplus section 存在
if "autogptplus" not in config:
config["autogptplus"] = {}
# 更新配置
config["autogptplus"]["register_mode"] = mode
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
# 更新运行时配置
set_register_mode(mode)
# 重新加载模块
import importlib
import auto_gpt_team
importlib.reload(auto_gpt_team)
# 显示成功消息
if mode == "api":
mode_name = "🌐 协议模式"
mode_desc = (
"使用 API 快速完成注册流程,仅支付环节使用浏览器\n\n"
"<b>特点:</b>\n"
"• 注册速度更快\n"
"• 资源占用更少\n"
"• 更稳定可靠"
)
else:
mode_name = "🖥️ 浏览器模式"
mode_desc = (
"全程使用 DrissionPage 浏览器自动化\n\n"
"<b>特点:</b>\n"
"• 兼容性更好\n"
"• 无需额外依赖\n"
"• 可视化调试方便"
)
keyboard = [[InlineKeyboardButton("◀️ 返回主菜单", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>✅ 注册模式已设置</b>\n\n"
f"当前模式: {mode_name}\n\n"
f"{mode_desc}",
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 模块导入失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 设置失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _show_autogptplus_stats(self, query):
"""显示统计信息"""
try:
from auto_gpt_team import get_email_domains, get_sepa_ibans, RANDOM_FINGERPRINT
import json
from pathlib import Path
# 读取账号文件统计
accounts_file = Path("accounts.json")
accounts_count = 0
if accounts_file.exists():
try:
with open(accounts_file, "r", encoding="utf-8") as f:
accounts = json.load(f)
accounts_count = len(accounts)
except:
pass
domains = get_email_domains()
ibans = get_sepa_ibans()
lines = ["<b>📊 AutoGPTPlus 统计信息</b>\n"]
# 资源统计
lines.append("<b>📦 可用资源:</b>")
lines.append(f" • 邮箱域名: {len(domains)}")
lines.append(f" • IBAN: {len(ibans)}")
lines.append(f" • 随机指纹: {'开启' if RANDOM_FINGERPRINT else '关闭'}")
lines.append("")
# 账号统计
lines.append("<b>👥 已注册账号:</b>")
lines.append(f" • 总计: {accounts_count}")
# 配置状态
lines.append("")
lines.append("<b>⚙️ 配置状态:</b>")
if len(domains) > 0 and len(ibans) > 0:
lines.append(" ✅ 已就绪,可以开始注册")
else:
if len(domains) == 0:
lines.append(" ⚠️ 缺少邮箱域名")
if len(ibans) == 0:
lines.append(" ⚠️ 缺少 IBAN")
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"\n".join(lines),
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 获取统计失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _start_autogptplus_register(self, query, context):
"""快速开始注册 (跳转到 team_register)"""
try:
from auto_gpt_team import get_email_domains, get_sepa_ibans, MAIL_API_TOKEN, MAIL_API_BASE
# 检查配置
domains = get_email_domains()
ibans = get_sepa_ibans()
missing = []
if not MAIL_API_TOKEN:
missing.append("mail_api_token")
if not MAIL_API_BASE:
missing.append("mail_api_base")
if not domains:
missing.append("邮箱域名")
if not ibans:
missing.append("IBAN")
if missing:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>⚠️ 配置不完整</b>\n\n"
"缺少以下配置:\n" +
"\n".join(f"{m}" for m in missing) +
"\n\n请先完成配置后再开始注册",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 显示注册选项
keyboard = [
[
InlineKeyboardButton("1⃣ 注册1个", callback_data="team_reg:1"),
InlineKeyboardButton("3⃣ 注册3个", callback_data="team_reg:3"),
],
[
InlineKeyboardButton("5⃣ 注册5个", callback_data="team_reg:5"),
InlineKeyboardButton("🔢 自定义", callback_data="team_reg:custom"),
],
[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>🚀 开始 ChatGPT Team 注册</b>\n\n"
f"<b>可用资源:</b>\n"
f" • 邮箱域名: {len(domains)}\n"
f" • IBAN: {len(ibans)}\n\n"
"选择注册数量:",
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
"<b>❌ 模块未找到</b>",
parse_mode="HTML",
reply_markup=reply_markup
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"<b>❌ 操作失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
@admin_only
async def handle_team_custom_count(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理文本输入 (GPT Team 自定义数量 / AutoGPTPlus Token / 域名 / IBAN / run 自定义数量)"""
# 处理 AutoGPTPlus Token 输入
if context.user_data.get("autogptplus_waiting_token"):
context.user_data["autogptplus_waiting_token"] = False
await self._handle_autogptplus_token_input(update, context)
return
# 处理域名添加输入
if context.user_data.get("autogptplus_waiting_domain"):
context.user_data["autogptplus_waiting_domain"] = False
await self._handle_autogptplus_domain_input(update, context)
return
# 处理 IBAN 添加输入
if context.user_data.get("autogptplus_waiting_iban"):
context.user_data["autogptplus_waiting_iban"] = False
await self._handle_autogptplus_iban_input(update, context)
return
# 处理 /run 自定义数量输入
if context.user_data.get("waiting_run_count"):
context.user_data["waiting_run_count"] = False
await self._handle_run_custom_count_input(update, context)
return
# 处理 GPT Team 自定义数量输入
if not context.user_data.get("team_waiting_count"):
return # 不在等待状态,忽略消息
# 清除等待状态
context.user_data["team_waiting_count"] = False
text = update.message.text.strip()
# 验证输入
try:
count = int(text)
if count < 1 or count > 50:
await update.message.reply_text(
"❌ 数量必须在 1-50 之间\n\n"
"请重新使用 /team_register 开始"
)
return
except ValueError:
await update.message.reply_text(
"❌ 请输入有效的数字\n\n"
"请重新使用 /team_register 开始"
)
return
# 显示输出方式选择
keyboard = [
[
InlineKeyboardButton("📄 JSON 文件", callback_data=f"team_reg:output:json:{count}"),
],
[
InlineKeyboardButton("📥 添加到 team.json", callback_data=f"team_reg:output:team:{count}"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"<b>⚙️ 配置完成</b>\n\n"
f"注册数量: {count}\n\n"
f"请选择输出方式:",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _handle_run_custom_count_input(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 /run 自定义数量输入"""
text = update.message.text.strip()
total_teams = len(TEAMS)
try:
count = int(text)
if count < 1 or count > total_teams:
await update.message.reply_text(
f"❌ 数量必须在 1-{total_teams} 之间\n\n"
"请重新使用 /run 开始"
)
return
except ValueError:
await update.message.reply_text(
"❌ 请输入有效的数字\n\n"
"请重新使用 /run 开始"
)
return
# 保存数量并显示邮箱选择
context.user_data["run_team_count"] = count
keyboard = [
[
InlineKeyboardButton("📧 GPTMail", callback_data=f"run:email:gptmail"),
InlineKeyboardButton("☁️ Cloud Mail", callback_data=f"run:email:cloudmail"),
],
[
InlineKeyboardButton("❌ 取消", callback_data="run:cancel"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"<b>🚀 启动处理 Team</b>\n\n"
f"已选择: <b>{count}</b> 个 Team\n\n"
f"请选择邮箱服务:",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _handle_autogptplus_token_input(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 AutoGPTPlus Token 输入 - 保存后立即测试"""
import tomli_w
import requests
token = update.message.text.strip()
if not token:
await update.message.reply_text("❌ Token 不能为空")
return
# 脱敏显示
if len(token) > 10:
token_display = f"{token[:8]}...{token[-4:]}"
else:
token_display = token[:4] + "..."
# 先发送保存中的消息
status_msg = await update.message.reply_text(
f"⏳ 正在保存并验证 Token...\n\n"
f"Token: <code>{token_display}</code>",
parse_mode="HTML"
)
try:
# 读取当前配置获取 API 地址
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
mail_api_base = config.get("autogptplus", {}).get("mail_api_base", "")
if not mail_api_base:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await status_msg.edit_text(
"<b>❌ 无法验证 Token</b>\n\n"
"mail_api_base 未配置\n"
"请先在 config.toml 中配置 API 地址",
parse_mode="HTML",
reply_markup=reply_markup
)
return
# 测试 Token 是否有效
url = f"{mail_api_base}/api/public/emailList"
headers = {
"Authorization": token,
"Content-Type": "application/json"
}
payload = {
"toEmail": "test@test.com",
"timeSort": "desc",
"size": 1
}
start_time = time.time()
response = requests.post(url, headers=headers, json=payload, timeout=10)
elapsed = time.time() - start_time
data = response.json()
# 检查 Token 是否有效
if response.status_code == 200 and data.get("code") == 200:
# Token 有效,保存到配置文件
if "autogptplus" not in config:
config["autogptplus"] = {}
config["autogptplus"]["mail_api_token"] = token
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
keyboard = [
[
InlineKeyboardButton("📋 查看配置", callback_data="autogptplus:config"),
InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await status_msg.edit_text(
f"<b>✅ Token 验证成功并已保存</b>\n\n"
f"Token: <code>{token_display}</code>\n"
f"响应时间: {elapsed*1000:.0f}ms\n\n"
f"💡 使用 /reload 重载配置使其生效",
parse_mode="HTML",
reply_markup=reply_markup
)
else:
# Token 无效
error_msg = data.get("message", "未知错误")
keyboard = [
[
InlineKeyboardButton("🔑 重新设置", callback_data="autogptplus:set_token"),
InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back"),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await status_msg.edit_text(
f"<b>❌ Token 验证失败</b>\n\n"
f"Token: <code>{token_display}</code>\n"
f"错误: {error_msg}\n\n"
f"Token 未保存,请检查后重试",
parse_mode="HTML",
reply_markup=reply_markup
)
except requests.exceptions.ConnectionError:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await status_msg.edit_text(
"<b>❌ 连接失败</b>\n\n"
"无法连接到 API 服务器\n"
"请检查 mail_api_base 配置",
parse_mode="HTML",
reply_markup=reply_markup
)
except requests.exceptions.Timeout:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await status_msg.edit_text(
"<b>❌ 连接超时</b>\n\n"
"API 服务器响应超时",
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
await status_msg.edit_text(
"❌ 缺少 tomli_w 依赖\n"
"请运行: uv add tomli_w"
)
except Exception as e:
keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="autogptplus:back")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await status_msg.edit_text(
f"<b>❌ 验证失败</b>\n\n{e}",
parse_mode="HTML",
reply_markup=reply_markup
)
async def _handle_autogptplus_domain_input(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理域名添加输入"""
try:
from auto_gpt_team import add_email_domains
text = update.message.text.strip()
if not text:
await update.message.reply_text("❌ 域名不能为空")
return
# 解析输入 (支持空格、逗号、换行分隔)
domains = [s.strip() for s in text.replace(",", " ").replace("\n", " ").split() if s.strip()]
if not domains:
await update.message.reply_text("❌ 未提供有效的域名")
return
added, skipped, invalid, total = add_email_domains(domains)
# 构建响应消息
lines = ["<b>✅ 域名导入完成</b>\n"]
lines.append(f"新增: {added}")
lines.append(f"跳过 (重复): {skipped}")
if invalid > 0:
lines.append(f"无效 (格式错误): {invalid}")
lines.append(f"当前总数: {total}")
keyboard = [[InlineKeyboardButton("📧 域名管理", callback_data="autogptplus:domains")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"\n".join(lines),
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
except Exception as e:
await update.message.reply_text(f"❌ 添加域名失败: {e}")
async def _handle_autogptplus_iban_input(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 IBAN 添加输入"""
try:
from auto_gpt_team import add_sepa_ibans
text = update.message.text.strip()
if not text:
await update.message.reply_text("❌ IBAN 不能为空")
return
# 解析输入 (支持空格、逗号、换行分隔)
ibans = [s.strip() for s in text.replace(",", " ").replace("\n", " ").split() if s.strip()]
if not ibans:
await update.message.reply_text("❌ 未提供有效的 IBAN")
return
added, skipped, total = add_sepa_ibans(ibans)
# 构建响应消息
lines = ["<b>✅ IBAN 导入完成</b>\n"]
lines.append(f"新增: {added}")
lines.append(f"跳过 (重复/无效): {skipped}")
lines.append(f"当前总数: {total}")
keyboard = [[InlineKeyboardButton("💳 IBAN 管理", callback_data="autogptplus:ibans")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"\n".join(lines),
parse_mode="HTML",
reply_markup=reply_markup
)
except ImportError:
await update.message.reply_text("❌ auto_gpt_team 模块未找到")
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"<b>🌐 代理池状态</b>\n"]
lines.append(f"可用代理: {count}")
if last_test:
lines.append(f"\n<b>上次测试:</b>")
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<b>代理列表</b> (前 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}. <code>{display}</code>")
if count > 5:
lines.append(f" ... 还有 {count - 5}")
else:
lines.append(f"\n💡 将代理添加到 <code>proxy.txt</code> 然后使用 /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(
"📭 <b>代理池为空</b>\n\n"
"请将代理添加到 <code>proxy.txt</code> 后重试",
parse_mode="HTML"
)
return
msg = await update.message.reply_text(
f"🔄 <b>正在测试代理</b>\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"✅ <b>代理测试完成</b>\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"✅ <b>代理池已重新加载</b>\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
async def cmd_schedule(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""开启/关闭定时调度器"""
import config as cfg
args = context.args
if not args:
# 显示当前状态
status = "✅ 运行中" if self._scheduler_active else ("⏸️ 已启用 (等待触发)" if cfg.SCHEDULER_ENABLED else "❌ 已关闭")
text = (
f"<b>⏰ 定时调度器</b>\n\n"
f"状态: {status}\n"
f"时间窗口: {cfg.SCHEDULER_START_HOUR:02d}:00 - {cfg.SCHEDULER_END_HOUR:02d}:00\n"
f"每轮注册: {cfg.SCHEDULER_BATCH_SIZE}\n"
f"冷却时间: {cfg.SCHEDULER_COOLDOWN_MINUTES} 分钟\n"
f"连续失败上限: {cfg.SCHEDULER_MAX_CONSECUTIVE_FAILURES}\n\n"
f"用法:\n"
f" <code>/schedule on</code> - 立即开启\n"
f" <code>/schedule off</code> - 关闭\n"
f" <code>/schedule_config</code> - 配置参数\n"
f" <code>/schedule_status</code> - 查看运行状态"
)
await update.message.reply_text(text, parse_mode="HTML")
return
action = args[0].lower()
if action == "on":
if self._scheduler_active:
await update.message.reply_text("⚠️ 调度器已经在运行中")
return
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 有任务正在运行: {self.current_team}\n"
"请先等待任务完成或使用 /stop 停止"
)
return
# 更新 config 并持久化
cfg.SCHEDULER_ENABLED = True
self._persist_scheduler_enabled(True)
# 检查是否在时间窗口内
now = datetime.now()
if cfg.SCHEDULER_START_HOUR <= now.hour < cfg.SCHEDULER_END_HOUR:
# 在时间窗口内,立即启动
self._start_scheduler()
await update.message.reply_text(
f"<b>✅ 调度器已启动</b>\n\n"
f"当前在时间窗口内,立即开始\n"
f"窗口: {cfg.SCHEDULER_START_HOUR:02d}:00 - {cfg.SCHEDULER_END_HOUR:02d}:00\n"
f"每轮: 注册 {cfg.SCHEDULER_BATCH_SIZE} 个 → run_all → 冷却 {cfg.SCHEDULER_COOLDOWN_MINUTES} 分钟",
parse_mode="HTML"
)
else:
# 不在时间窗口内,注册 daily job
self._register_scheduler_daily_job()
await update.message.reply_text(
f"<b>✅ 调度器已启用</b>\n\n"
f"当前不在时间窗口内\n"
f"将在每天 {cfg.SCHEDULER_START_HOUR:02d}:00 自动启动\n"
f"窗口: {cfg.SCHEDULER_START_HOUR:02d}:00 - {cfg.SCHEDULER_END_HOUR:02d}:00",
parse_mode="HTML"
)
elif action == "off":
cfg.SCHEDULER_ENABLED = False
self._persist_scheduler_enabled(False)
if self._scheduler_active:
self._scheduler_stop_event.set()
await update.message.reply_text(
"<b>🛑 调度器正在停止</b>\n\n"
"当前轮次完成后将停止调度器\n"
"如需立即停止任务,请使用 /stop",
parse_mode="HTML"
)
else:
# 移除 daily job
self._remove_scheduler_daily_job()
await update.message.reply_text(
"<b>❌ 调度器已关闭</b>",
parse_mode="HTML"
)
else:
await update.message.reply_text(
"用法: <code>/schedule on</code> 或 <code>/schedule off</code>",
parse_mode="HTML"
)
@admin_only
async def cmd_schedule_config(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""配置调度器参数"""
import config as cfg
import tomli_w
args = context.args
if not args:
text = (
f"<b>⚙️ 调度器配置</b>\n\n"
f"<code>start_hour</code> = {cfg.SCHEDULER_START_HOUR} (开始时间)\n"
f"<code>end_hour</code> = {cfg.SCHEDULER_END_HOUR} (结束时间)\n"
f"<code>batch_size</code> = {cfg.SCHEDULER_BATCH_SIZE} (每轮注册数)\n"
f"<code>cooldown</code> = {cfg.SCHEDULER_COOLDOWN_MINUTES} (冷却分钟)\n"
f"<code>max_failures</code> = {cfg.SCHEDULER_MAX_CONSECUTIVE_FAILURES} (连续失败上限)\n\n"
f"修改示例:\n"
f" <code>/schedule_config batch_size 30</code>\n"
f" <code>/schedule_config start_hour 9</code>\n"
f" <code>/schedule_config cooldown 10</code>"
)
await update.message.reply_text(text, parse_mode="HTML")
return
if len(args) < 2:
await update.message.reply_text("❌ 用法: <code>/schedule_config 参数名 值</code>", parse_mode="HTML")
return
key = args[0].lower()
try:
value = int(args[1])
except ValueError:
await update.message.reply_text("❌ 值必须为整数")
return
# 参数映射
config_map = {
"start_hour": ("start_hour", 0, 23),
"end_hour": ("end_hour", 0, 23),
"batch_size": ("batch_size", 1, 200),
"cooldown": ("cooldown_minutes", 1, 60),
"max_failures": ("max_consecutive_failures", 1, 20),
}
if key not in config_map:
valid_keys = ", ".join(config_map.keys())
await update.message.reply_text(f"❌ 无效参数。可选: {valid_keys}")
return
toml_key, min_val, max_val = config_map[key]
if not (min_val <= value <= max_val):
await update.message.reply_text(f"❌ 值必须在 {min_val}-{max_val} 之间")
return
# 更新内存中的配置
attr_name = f"SCHEDULER_{toml_key.upper()}"
old_value = getattr(cfg, attr_name)
setattr(cfg, attr_name, value)
# 持久化到 config.toml
try:
import tomllib
with open(CONFIG_FILE, "rb") as f:
config = tomllib.load(f)
if "scheduler" not in config:
config["scheduler"] = {}
config["scheduler"][toml_key] = value
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
await update.message.reply_text(
f"<b>✅ 配置已更新</b>\n\n"
f"<code>{key}</code>: {old_value}{value}\n\n"
f"💡 如调度器正在运行,下轮生效",
parse_mode="HTML"
)
except ImportError:
await update.message.reply_text(
f"⚠️ 内存配置已更新 ({key}: {old_value}{value})\n"
"但无法持久化: 缺少 tomli_w 依赖"
)
except Exception as e:
await update.message.reply_text(
f"⚠️ 内存配置已更新 ({key}: {old_value}{value})\n"
f"但持久化失败: {e}"
)
@admin_only
async def cmd_schedule_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看调度器运行状态"""
import config as cfg
if not self._scheduler_active:
status = "⏸️ 已启用 (等待触发)" if cfg.SCHEDULER_ENABLED else "❌ 未启用"
await update.message.reply_text(
f"<b>📊 调度器状态</b>\n\n"
f"状态: {status}\n\n"
f"使用 <code>/schedule on</code> 启动",
parse_mode="HTML"
)
return
stats = self._scheduler_stats
elapsed = ""
if stats["start_time"]:
delta = datetime.now() - stats["start_time"]
hours, remainder = divmod(int(delta.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
elapsed = f"{hours}h{minutes}m{seconds}s"
text = (
f"<b>📊 调度器运行状态</b>\n\n"
f"🟢 运行中 | 第 {self._scheduler_round}\n"
f"⏱️ 已运行: {elapsed}\n"
f"⏰ 窗口: {cfg.SCHEDULER_START_HOUR:02d}:00 - {cfg.SCHEDULER_END_HOUR:02d}:00\n\n"
f"<b>累计统计:</b>\n"
f" 📝 注册: {stats['total_registered']}\n"
f" 📥 入库: {stats['total_ingested']}\n"
f" ❌ 失败轮次: {stats['total_failed']}\n"
f" 🔄 总轮次: {stats['total_rounds']}\n"
f" ⚠️ 连续失败: {stats['consecutive_failures']}/{cfg.SCHEDULER_MAX_CONSECUTIVE_FAILURES}"
)
if self.current_team:
text += f"\n\n🏃 当前任务: {self.current_team}"
await update.message.reply_text(text, parse_mode="HTML")
def _persist_scheduler_enabled(self, enabled: bool):
"""持久化 scheduler.enabled 到 config.toml"""
try:
import tomllib, tomli_w
with open(CONFIG_FILE, "rb") as f:
config = tomllib.load(f)
if "scheduler" not in config:
config["scheduler"] = {}
config["scheduler"]["enabled"] = enabled
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
except Exception as e:
log.warning(f"持久化 scheduler.enabled 失败: {e}")
def _register_scheduler_daily_job(self):
"""注册每日定时触发 Job"""
import config as cfg
# 先移除旧的 job
self._remove_scheduler_daily_job()
if self.app and self.app.job_queue:
trigger_time = dt_time(hour=cfg.SCHEDULER_START_HOUR, minute=0, second=0, tzinfo=BEIJING_TZ)
self.app.job_queue.run_daily(
self._scheduler_daily_trigger,
time=trigger_time,
name="scheduler_daily"
)
log.info(f"Scheduler daily job registered at {cfg.SCHEDULER_START_HOUR:02d}:00 (Beijing Time)")
def _remove_scheduler_daily_job(self):
"""移除每日定时触发 Job"""
if self.app and self.app.job_queue:
jobs = self.app.job_queue.get_jobs_by_name("scheduler_daily")
for job in jobs:
job.schedule_removal()
async def _scheduler_daily_trigger(self, context: ContextTypes.DEFAULT_TYPE):
"""每日定时触发回调 - 由 job_queue 调用"""
import config as cfg
if not cfg.SCHEDULER_ENABLED:
return
# 检查是否被 /stop 挂起了今天的调度
today = datetime.now().date()
if self._scheduler_suspended_date == today:
log.info(f"Scheduler suspended for today ({today}), skipping")
return
if self._scheduler_active:
log.info("Scheduler already active, skipping daily trigger")
return
if self.current_task and not self.current_task.done():
log.warning("Task already running, skipping scheduler trigger")
# 通知管理员
for chat_id in TELEGRAM_ADMIN_CHAT_IDS:
try:
await context.bot.send_message(
chat_id,
f"⚠️ <b>调度器触发跳过</b>\n\n"
f"当前有任务在运行: {self.current_team}\n"
f"请任务完成后手动使用 /schedule on 启动",
parse_mode="HTML"
)
except Exception:
pass
return
# 启动调度器
self._start_scheduler()
# 通知管理员
for chat_id in TELEGRAM_ADMIN_CHAT_IDS:
try:
await context.bot.send_message(
chat_id,
f"<b>⏰ 定时调度器已启动</b>\n\n"
f"时间窗口: {cfg.SCHEDULER_START_HOUR:02d}:00 - {cfg.SCHEDULER_END_HOUR:02d}:00\n"
f"每轮: 注册 {cfg.SCHEDULER_BATCH_SIZE} 个 → run_all → 冷却 {cfg.SCHEDULER_COOLDOWN_MINUTES} 分钟",
parse_mode="HTML"
)
except Exception:
pass
def _start_scheduler(self):
"""启动调度器循环"""
self._scheduler_active = True
self._scheduler_stop_event.clear()
self._scheduler_round = 0
self._scheduler_stats = {
"total_rounds": 0,
"total_registered": 0,
"total_ingested": 0,
"total_failed": 0,
"consecutive_failures": 0,
"start_time": datetime.now(),
}
self._scheduler_task = asyncio.create_task(self._scheduler_loop())
async def _scheduler_loop(self):
"""调度器主循环: 注册 → run_all → 冷却 → 重复"""
import config as cfg
log.section("定时调度器启动")
chat_id = TELEGRAM_ADMIN_CHAT_IDS[0] if TELEGRAM_ADMIN_CHAT_IDS else None
try:
while not self._scheduler_stop_event.is_set():
# ===== 时间窗口检查 =====
now = datetime.now()
if not (cfg.SCHEDULER_START_HOUR <= now.hour < cfg.SCHEDULER_END_HOUR):
log.info(f"超出时间窗口 ({cfg.SCHEDULER_START_HOUR:02d}:00 - {cfg.SCHEDULER_END_HOUR:02d}:00),调度结束")
break
self._scheduler_round += 1
round_num = self._scheduler_round
log.section(f"调度器 - 第 {round_num}")
# ===== Phase 1: Provisioning (注册 GPT Team 账号) =====
log.info(f"[Phase 1] 注册 {cfg.SCHEDULER_BATCH_SIZE} 个 GPT Team 账号...")
if chat_id:
try:
await self.app.bot.send_message(
chat_id,
f"<b>🔄 第 {round_num} 轮开始</b>\n\n"
f"📝 Phase 1: 注册 {cfg.SCHEDULER_BATCH_SIZE} 个账号...\n"
f"{now.strftime('%H:%M:%S')}",
parse_mode="HTML"
)
except Exception:
pass
# 重置停止标志
try:
import run
run._shutdown_requested = False
except Exception:
pass
self.current_team = f"调度器-第{round_num}轮-注册"
# 执行注册
reg_success = 0
reg_fail = 0
try:
reg_success, reg_fail = await self._scheduler_run_registration(
chat_id, cfg.SCHEDULER_BATCH_SIZE, cfg.SCHEDULER_OUTPUT_TYPE
)
except Exception as e:
log.error(f"调度器注册阶段异常: {e}")
reg_fail = cfg.SCHEDULER_BATCH_SIZE
self._scheduler_stats["total_registered"] += reg_success
# 检查中断
if self._scheduler_stop_event.is_set():
break
# 注册全部失败检查
if reg_success == 0:
self._scheduler_stats["consecutive_failures"] += 1
self._scheduler_stats["total_failed"] += 1
log.warning(f"{round_num} 轮注册全部失败 (连续失败: {self._scheduler_stats['consecutive_failures']})")
if self._scheduler_stats["consecutive_failures"] >= cfg.SCHEDULER_MAX_CONSECUTIVE_FAILURES:
log.error(f"连续失败 {self._scheduler_stats['consecutive_failures']} 轮,调度器自动暂停")
if chat_id:
try:
await self.app.bot.send_message(
chat_id,
f"<b>🚨 调度器自动暂停</b>\n\n"
f"连续 {self._scheduler_stats['consecutive_failures']} 轮注册全部失败\n"
f"请检查配置后使用 /schedule on 重新启动\n\n"
f"可能原因:\n"
f"• IBAN 已用完\n"
f"• 邮件 API 异常\n"
f"• 域名全部拉黑",
parse_mode="HTML"
)
except Exception:
pass
break
continue # 跳过 run_all直接下一轮
else:
self._scheduler_stats["consecutive_failures"] = 0
# ===== Phase 1.5: Verify (验证 account_id) =====
log.info(f"[Phase 1.5] 验证注册账号 account_id...")
if chat_id:
try:
await self.app.bot.send_message(
chat_id,
f"<b>🔍 第 {round_num} 轮 - Phase 1.5</b>\n\n"
f"📝 注册完成: ✅ {reg_success} / ❌ {reg_fail}\n"
f"🔍 正在验证 account_id...",
parse_mode="HTML"
)
except Exception:
pass
self.current_team = f"调度器-第{round_num}轮-验证"
try:
await self._validate_and_cleanup_accounts(chat_id, force_all=True)
log.success("verify_all 完成")
except Exception as e:
log.error(f"调度器验证阶段异常: {e}")
# 检查中断
if self._scheduler_stop_event.is_set():
break
# ===== Phase 2: Ingestion (run_all) =====
log.info(f"[Phase 2] 执行 run_all 入库处理...")
if chat_id:
try:
await self.app.bot.send_message(
chat_id,
f"<b>🚀 第 {round_num} 轮 - Phase 2</b>\n\n"
f"📝 注册: ✅ {reg_success} / ❌ {reg_fail}\n"
f"🔍 验证完成\n"
f"📥 开始 run_all 入库处理...",
parse_mode="HTML"
)
except Exception:
pass
self.current_team = f"调度器-第{round_num}轮-run_all"
ingested = 0
try:
ingested = await self._scheduler_run_all()
except Exception as e:
log.error(f"调度器 run_all 阶段异常: {e}")
self._scheduler_stats["total_ingested"] += ingested
self._scheduler_stats["total_rounds"] += 1
# 检查中断
if self._scheduler_stop_event.is_set():
break
# ===== Graceful Boundary: 任务完成后检查时间 =====
now = datetime.now()
if not (cfg.SCHEDULER_START_HOUR <= now.hour < cfg.SCHEDULER_END_HOUR):
log.info(f"run_all 完成后已超出时间窗口 ({now.strftime('%H:%M:%S')}),调度结束")
if chat_id:
try:
await self.app.bot.send_message(
chat_id,
f"<b>⏰ 第 {round_num} 轮完成后已超出时间窗口</b>\n\n"
f"当前: {now.strftime('%H:%M:%S')}\n"
f"窗口: {cfg.SCHEDULER_START_HOUR:02d}:00 - {cfg.SCHEDULER_END_HOUR:02d}:00\n"
f"本日调度结束",
parse_mode="HTML"
)
except Exception:
pass
break
# ===== Phase 3: Cooldown =====
cooldown = cfg.SCHEDULER_COOLDOWN_MINUTES * 60
log.info(f"[Phase 3] 冷却 {cfg.SCHEDULER_COOLDOWN_MINUTES} 分钟...")
if chat_id:
try:
next_time = now + timedelta(seconds=cooldown)
await self.app.bot.send_message(
chat_id,
f"<b>⏳ 第 {round_num} 轮完成</b>\n\n"
f"✅ 注册: {reg_success} | 📥 入库: {ingested}\n"
f"冷却 {cfg.SCHEDULER_COOLDOWN_MINUTES} 分钟...\n"
f"下轮预计: {next_time.strftime('%H:%M:%S')}",
parse_mode="HTML"
)
except Exception:
pass
self.current_team = f"调度器-冷却中"
# 可中断的等待
try:
await asyncio.wait_for(
self._scheduler_stop_event.wait(),
timeout=cooldown
)
# 如果 wait 返回,说明收到了停止信号
break
except asyncio.TimeoutError:
# 正常超时,继续下一轮
pass
except Exception as e:
log.error(f"调度器异常退出: {e}")
finally:
self._scheduler_active = False
self.current_task = None
self.current_team = None
log.info("调度器已停止")
# 发送日报
await self._send_daily_report()
async def _scheduler_run_registration(self, chat_id: int, count: int, output_type: str) -> tuple:
"""调度器: 执行注册阶段,返回 (success_count, fail_count)"""
from auto_gpt_team import run_single_registration_auto, cleanup_chrome_processes, get_register_mode
from config import CONCURRENT_ENABLED, CONCURRENT_WORKERS
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"🌐 <b>代理池预检测</b>\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
results_lock = threading.Lock()
current_mode = get_register_mode()
workers = CONCURRENT_WORKERS if CONCURRENT_ENABLED else 1
workers = min(workers, count)
task_queue = list(range(count))
queue_lock = threading.Lock()
def worker_task(worker_id: int):
nonlocal success_count, fail_count
while True:
try:
import run
if run._shutdown_requested:
break
except Exception:
pass
with queue_lock:
if not task_queue:
break
task_idx = task_queue.pop(0)
try:
result = run_single_registration_auto(
progress_callback=None,
step_callback=None
)
with results_lock:
if result.get("stopped"):
break
elif result.get("success"):
success_count += 1
results.append({
"account": result["account"],
"password": result["password"],
"token": result["token"],
"account_id": result.get("account_id", "")
})
else:
fail_count += 1
except Exception as e:
with results_lock:
fail_count += 1
log.error(f"Scheduler Worker {worker_id}: 注册异常: {e}")
cleanup_chrome_processes()
# 使用线程池并发执行
import concurrent.futures
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
async_futures = [loop.run_in_executor(executor, worker_task, i) for i in range(workers)]
await asyncio.gather(*async_futures, return_exceptions=True)
# 保存结果到 team.json
if results and output_type == "team":
try:
team_file = TEAM_JSON_FILE
existing = []
if team_file.exists():
with open(team_file, "r", encoding="utf-8") as f:
existing = json.load(f)
existing.extend(results)
with open(team_file, "w", encoding="utf-8") as f:
json.dump(existing, f, ensure_ascii=False, indent=2)
import config as cfg_inner
scheduler_was_enabled = cfg_inner.SCHEDULER_ENABLED
reload_config()
if self._scheduler_active or scheduler_was_enabled:
cfg_inner.SCHEDULER_ENABLED = scheduler_was_enabled
log.success(f"调度器: {success_count} 个账号已写入 team.json (总计 {len(existing)})")
except Exception as e:
log.error(f"调度器: 保存 team.json 失败: {e}")
return (success_count, fail_count)
async def _scheduler_run_all(self) -> int:
"""调度器: 执行 run_all返回成功入库数"""
from run import run_all_teams
from team_service import preload_all_account_ids
from utils import load_team_tracker, save_team_tracker, add_team_owners_to_tracker
from config import DEFAULT_PASSWORD
loop = asyncio.get_event_loop()
def _task():
preload_all_account_ids()
_tracker = load_team_tracker()
add_team_owners_to_tracker(_tracker, DEFAULT_PASSWORD)
save_team_tracker(_tracker)
return run_all_teams()
results = await loop.run_in_executor(self.executor, _task)
# 自动清理 team.json 和 team_tracker.json
await self._auto_clean_after_run_all()
# 统计成功入库数
ingested = sum(1 for r in (results or []) if r.get("status") in ("success", "completed"))
return ingested
async def _send_daily_report(self):
"""发送调度器日报"""
stats = self._scheduler_stats
if stats["total_rounds"] == 0 and stats["total_registered"] == 0:
return # 没有任何数据,不发送
elapsed = ""
if stats["start_time"]:
delta = datetime.now() - stats["start_time"]
hours, remainder = divmod(int(delta.total_seconds()), 3600)
minutes, _ = divmod(remainder, 60)
elapsed = f"{hours}h{minutes}m"
text = (
f"<b>📊 调度器日报</b>\n\n"
f"⏱️ 运行时长: {elapsed}\n"
f"🔄 总轮次: {stats['total_rounds']}\n\n"
f"<b>累计结果:</b>\n"
f" 📝 注册成功: {stats['total_registered']}\n"
f" 📥 入库成功: {stats['total_ingested']}\n"
f" ❌ 失败轮次: {stats['total_failed']}\n"
)
if stats["consecutive_failures"] >= 1:
text += f"\n⚠️ 最终连续失败: {stats['consecutive_failures']}"
for chat_id in TELEGRAM_ADMIN_CHAT_IDS:
try:
await self.app.bot.send_message(
chat_id,
text,
parse_mode="HTML"
)
except Exception:
pass
async def main():
"""主函数"""
if not TELEGRAM_BOT_TOKEN:
print("Telegram Bot Token 未配置,请在 config.toml 中设置 telegram.bot_token")
sys.exit(1)
if not TELEGRAM_ADMIN_CHAT_IDS:
print("管理员 Chat ID 未配置,请在 config.toml 中设置 telegram.admin_chat_ids")
sys.exit(1)
bot = ProvisionerBot()
# 处理 Ctrl+C
import signal
def signal_handler(sig, frame):
log.info("正在关闭...")
bot.request_shutdown()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
await bot.start()
if __name__ == "__main__":
asyncio.run(main())