7045 lines
274 KiB
Python
7045 lines
274 KiB
Python
# ==================== 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),
|
||
]
|
||
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", "调度器运行状态"),
|
||
]
|
||
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 <n> - 查看第 n 个 Team 处理详情
|
||
/config - 查看系统配置
|
||
/logs [n] - 查看最近 n 条日志
|
||
/logs_live - 启用实时日志推送
|
||
/logs_stop - 停止实时日志推送
|
||
|
||
<b>🚀 任务控制:</b>
|
||
/run - 选择数量和邮箱服务开始处理
|
||
/run_all - 开始处理所有 Team
|
||
/resume - 继续处理未完成账号
|
||
/stop - 停止当前任务
|
||
|
||
<b>⚙️ 配置管理:</b>
|
||
/fingerprint - 开启/关闭随机指纹
|
||
/concurrent - 开启/关闭并发处理
|
||
/concurrent <n> - 设置并发数 (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 <key> - 添加 API Key
|
||
/gptmail_del <key> - 删除 API Key
|
||
/test_email - 测试邮箱创建
|
||
|
||
<b>☁️ Cloud Mail 管理:</b>
|
||
/cloudmail - Cloud Mail 管理面板
|
||
/cloudmail_token <token> - 设置 API Token
|
||
/cloudmail_api <url> - 设置 API 地址
|
||
/cloudmail_domains - 域名管理 (查看/添加/删除)
|
||
|
||
<b>💳 IBAN 管理 (GPT Team):</b>
|
||
/iban_list - 查看 IBAN 列表
|
||
/iban_add <ibans> - 添加 IBAN (每行一个或逗号分隔)
|
||
/iban_clear - 清空 IBAN 列表
|
||
|
||
<b>📧 域名管理 (GPT Team):</b>
|
||
/domain_list - 查看邮箱域名列表
|
||
/domain_add <domains> - 添加域名 (每行一个或逗号分隔)
|
||
/domain_del <domain> - 删除指定域名
|
||
/domain_clear - 清空域名列表
|
||
|
||
<b>🤖 GPT Team:</b>
|
||
/team_fingerprint - 开启/关闭随机指纹
|
||
/team_register - 开始自动订阅注册
|
||
|
||
<b>🔧 AutoGPTPlus:</b>
|
||
/autogptplus - ChatGPT 订阅自动化管理面板
|
||
/update_token <token> - 更新邮件 API Token
|
||
|
||
<b>⏰ 定时调度器:</b>
|
||
/schedule on|off - 开启/关闭定时调度器
|
||
/schedule_config - 配置调度器参数
|
||
/schedule_status - 查看调度器运行状态
|
||
|
||
<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 <n> 开始处理"
|
||
])
|
||
|
||
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 <n> 开始处理"
|
||
])
|
||
|
||
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 <key> 添加",
|
||
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 <key> - 添加 Key")
|
||
lines.append(f"/gptmail_del <key> - 删除动态 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 <key>\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 <token></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 <url></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 <domain> - 添加域名")
|
||
lines.append(f"/cloudmail_domains del <domain> - 删除域名")
|
||
|
||
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
|
||
|
||
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 <new_token></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 <your_token></code>",
|
||
parse_mode="HTML"
|
||
)
|
||
elif action == "set_api":
|
||
await query.edit_message_text(
|
||
"<b>🌐 设置 Cloud Mail API 地址</b>\n\n"
|
||
"请使用命令设置:\n"
|
||
"<code>/cloudmail_api <url></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_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
|
||
|
||
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())
|