From effc1add377b0ad82fe1fb706760b7d41b9cd2b8 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Sat, 24 Jan 2026 07:52:10 +0800 Subject: [PATCH] feat(email_domains): Add domain validation and improve domain management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add validate_domain_format() function with comprehensive domain format validation * Validates domain structure (must contain dot, valid characters, proper TLD length) * Normalizes domain format (adds @ prefix, removes quotes/special chars) * Returns validation status with detailed error messages - Update add_email_domains() to use new validation function * Track invalid domains separately in return tuple * Return (added, skipped, invalid, total) instead of (added, skipped, total) * Improve error handling and domain normalization - Add get_file_domains_count() function to retrieve txt file domain count - Update clear_email_domains() to return count of cleared domains - Enhance telegram_bot.py command menu organization * Add s2a command handler and callback for S2A service management panel * Reorganize bot commands with category comments (基础信息, 任务控制, 配置管理, etc.) * Add missing commands: clean_errors, clean_teams, iban_list, iban_add, iban_clear, domain_list, domain_add, domain_del, domain_clear, team_fingerprint, team_register, s2a - Update domain_add command help text with format requirements - Improve code documentation and consistency across both files --- auto_gpt_team.py | 103 ++++- telegram_bot.py | 1080 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 1122 insertions(+), 61 deletions(-) diff --git a/auto_gpt_team.py b/auto_gpt_team.py index 8fe6c3c..68d5d22 100644 --- a/auto_gpt_team.py +++ b/auto_gpt_team.py @@ -315,40 +315,103 @@ def get_email_domains(): all_domains = file_domains | config_domains return sorted(all_domains) if all_domains else [] +def validate_domain_format(domain: str) -> tuple: + """验证域名格式是否正确 + + Args: + domain: 要验证的域名 (带或不带@前缀) + + Returns: + tuple: (是否有效, 标准化的域名或错误信息) + """ + domain = domain.strip().lower() + + # 移除开头的引号和尾部特殊字符 + domain = domain.strip('"\'') + + # 确保以 @ 开头 + if not domain.startswith("@"): + domain = "@" + domain + + # 移除尾部可能的引号或逗号 + domain = domain.rstrip('",\'') + + # 基本长度检查 (至少 @x.y) + if len(domain) < 4: + return False, "域名太短" + + # 提取 @ 后面的部分进行验证 + domain_part = domain[1:] # 去掉 @ + + # 检查是否包含至少一个点 + if "." not in domain_part: + return False, "域名缺少点号" + + # 检查点的位置 (不能在开头或结尾) + if domain_part.startswith(".") or domain_part.endswith("."): + return False, "点号位置不正确" + + # 检查不能有连续的点 + if ".." in domain_part: + return False, "不能有连续的点号" + + # 检查每个部分是否有效 + parts = domain_part.split(".") + for part in parts: + if not part: + return False, "域名部分为空" + # 检查是否只包含有效字符 (字母、数字、连字符) + if not all(c.isalnum() or c == "-" for c in part): + return False, f"域名包含无效字符" + # 不能以连字符开头或结尾 + if part.startswith("-") or part.endswith("-"): + return False, "域名部分不能以连字符开头或结尾" + + # 顶级域名至少2个字符 + if len(parts[-1]) < 2: + return False, "顶级域名太短" + + return True, domain + + def add_email_domains(new_domains: list) -> tuple: """添加域名到列表 - + Args: new_domains: 新的域名列表 - + Returns: - tuple: (添加数量, 跳过数量, 当前总数) + tuple: (添加数量, 跳过数量, 无效数量, 当前总数) """ # 获取当前所有域名(文件 + 配置) current = set(load_domains_from_file()) config_domains = set(EMAIL_DOMAINS) if EMAIL_DOMAINS else set() all_existing = current | config_domains - + added = 0 skipped = 0 - + invalid = 0 + for domain in new_domains: - domain = domain.strip().lower() - # 确保以 @ 开头 - if not domain.startswith("@"): - domain = "@" + domain - if not domain or len(domain) < 4: # 至少 @x.y + # 验证域名格式 + is_valid, result = validate_domain_format(domain) + + if not is_valid: + invalid += 1 continue + + domain = result # 使用标准化后的域名 + if domain in all_existing: skipped += 1 else: current.add(domain) all_existing.add(domain) added += 1 - + # 只保存通过 Bot 添加的域名到文件 save_domains_to_file(sorted(current)) - return added, skipped, len(all_existing) + return added, skipped, invalid, len(all_existing) def remove_email_domain(domain: str) -> bool: """删除指定域名 (只能删除通过 Bot 添加的域名) @@ -371,11 +434,21 @@ def remove_email_domain(domain: str) -> bool: return True return False -def clear_email_domains(): - """清空域名列表""" +def get_file_domains_count() -> int: + """获取txt文件中的域名数量 (不包含config配置的)""" + return len(load_domains_from_file()) + + +def clear_email_domains() -> int: + """清空域名列表 (只清空txt文件,保留config配置) + + Returns: + int: 被清空的域名数量 + """ + count = len(load_domains_from_file()) if DOMAIN_FILE.exists(): DOMAIN_FILE.unlink() - return True + return count # ================= 固定配置 ================= TARGET_URL = "https://chatgpt.com" diff --git a/telegram_bot.py b/telegram_bot.py index bb4f900..8bc3f70 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -149,6 +149,7 @@ class ProvisionerBot: ("clean_teams", self.cmd_clean_teams), ("keys_usage", self.cmd_keys_usage), ("autogptplus", self.cmd_autogptplus), + ("s2a", self.cmd_s2a_panel), ] for cmd, handler in handlers: self.app.add_handler(CommandHandler(cmd, handler)) @@ -180,6 +181,10 @@ class ProvisionerBot: self.callback_autogptplus, pattern="^autogptplus:" )) + self.app.add_handler(CallbackQueryHandler( + self.callback_s2a_panel, + pattern="^s2a:" + )) # 注册自定义数量输入处理器 (GPT Team 注册) self.app.add_handler(MessageHandler( @@ -231,6 +236,7 @@ class ProvisionerBot: async def _set_commands(self): """设置 Bot 命令菜单提示""" commands = [ + # 基础信息 BotCommand("help", "查看帮助信息"), BotCommand("list", "查看 team.json 账号列表"), BotCommand("status", "查看任务处理状态"), @@ -239,24 +245,46 @@ class ProvisionerBot: BotCommand("logs", "查看最近日志"), BotCommand("logs_live", "启用实时日志推送"), BotCommand("logs_stop", "停止实时日志推送"), + # 任务控制 BotCommand("run", "处理指定 Team"), BotCommand("run_all", "处理所有 Team"), BotCommand("resume", "继续处理未完成账号"), BotCommand("stop", "停止当前任务"), + # 配置管理 BotCommand("fingerprint", "开启/关闭随机指纹"), BotCommand("include_owners", "开启/关闭 Owner 入库"), BotCommand("reload", "重载配置文件"), - BotCommand("clean", "清理数据文件"), + # 清理管理 + BotCommand("clean", "清理已完成账号"), + BotCommand("clean_errors", "清理错误状态账号"), + BotCommand("clean_teams", "清理已完成 Team"), + # S2A BotCommand("dashboard", "查看 S2A 仪表盘"), BotCommand("keys_usage", "查看 API 密钥用量"), BotCommand("stock", "查看账号库存"), BotCommand("s2a_config", "配置 S2A 参数"), BotCommand("import", "导入账号到 team.json"), + # GPTMail BotCommand("gptmail_keys", "查看 GPTMail API Keys"), BotCommand("gptmail_add", "添加 GPTMail API Key"), BotCommand("gptmail_del", "删除 GPTMail API Key"), BotCommand("test_email", "测试邮箱创建"), + # 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 管理面板"), + # S2A + BotCommand("s2a", "S2A 服务管理面板"), ] try: await self.app.bot.set_my_commands(commands) @@ -2526,33 +2554,39 @@ class ProvisionerBot: "/domain_add @example.com\n" "/domain_add @a.com,@b.com\n\n" "支持空格或逗号分隔\n" - "@ 符号可省略,会自动添加", + "@ 符号可省略,会自动添加\n\n" + "格式要求:\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, total = add_email_domains(domains) - - await update.message.reply_text( - f"✅ 域名导入完成\n\n" - f"新增: {added}\n" - f"跳过 (重复): {skipped}\n" - f"当前总数: {total}", - parse_mode="HTML" - ) - + + added, skipped, invalid, total = add_email_domains(domains) + + # 构建响应消息 + lines = ["✅ 域名导入完成\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: @@ -2597,28 +2631,47 @@ class ProvisionerBot: @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_email_domains - count = len(get_email_domains()) + 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: - count = 0 - + file_count = 0 + config_count = 0 + + if file_count == 0: + await update.message.reply_text( + "📧 域名列表\n\n" + "txt文件中没有可清空的域名\n" + f"config配置中的域名: {config_count} 个 (不会被清空)", + parse_mode="HTML" + ) + return + await update.message.reply_text( f"⚠️ 确认清空域名列表?\n\n" - f"当前共有 {count} 个域名\n\n" + f"将清空txt文件中的域名: {file_count} 个\n" + f"config配置中的域名: {config_count} 个 (不会被清空)\n\n" f"确认请发送:\n" f"/domain_clear confirm", parse_mode="HTML" ) return - + try: - from auto_gpt_team import clear_email_domains - clear_email_domains() - await update.message.reply_text("✅ 域名列表已清空", parse_mode="HTML") + 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"✅ 域名列表已清空\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: @@ -3012,21 +3065,600 @@ class ProvisionerBot: self.current_task = None self.current_team = None + # ==================== S2A 管理面板 ==================== + + @admin_only + async def cmd_s2a_panel(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """S2A 服务管理面板""" + if AUTH_PROVIDER != "s2a": + await update.message.reply_text( + f"⚠️ S2A 面板仅在 S2A 模式下可用\n\n" + f"当前模式: {AUTH_PROVIDER}\n\n" + f"请在 config.toml 中设置:\n" + f"auth_provider = \"s2a\"", + parse_mode="HTML" + ) + return + + keyboard = self._get_s2a_main_keyboard() + await update.message.reply_text( + "📊 S2A 服务管理面板\n\n" + "Shared Account 服务配置管理\n\n" + "请选择功能:", + parse_mode="HTML", + reply_markup=keyboard + ) + + def _get_s2a_main_keyboard(self): + """获取 S2A 主菜单键盘""" + return InlineKeyboardMarkup([ + [ + InlineKeyboardButton("📊 仪表盘", callback_data="s2a:dashboard"), + InlineKeyboardButton("📦 库存查询", callback_data="s2a:stock"), + ], + [ + InlineKeyboardButton("🔑 密钥用量", callback_data="s2a:keys_usage"), + InlineKeyboardButton("⚙️ 服务配置", callback_data="s2a:config"), + ], + [ + InlineKeyboardButton("🧹 清理错误", callback_data="s2a:clean_errors"), + InlineKeyboardButton("🗑️ 清理Teams", callback_data="s2a:clean_teams"), + ], + [ + InlineKeyboardButton("📤 导入账号", callback_data="s2a:import"), + InlineKeyboardButton("🔄 测试连接", callback_data="s2a:test"), + ], + [ + InlineKeyboardButton("🚀 开始处理", callback_data="s2a:run"), + ], + ]) + + async def callback_s2a_panel(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理 S2A 面板回调""" + 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 == "dashboard": + await self._s2a_show_dashboard(query) + elif action == "stock": + await self._s2a_show_stock(query) + elif action == "keys_usage": + await self._s2a_show_keys_usage(query, sub_action) + elif action == "config": + await self._s2a_show_config(query) + elif action == "clean_errors": + await self._s2a_clean_errors(query, sub_action) + elif action == "clean_teams": + await self._s2a_clean_teams(query, sub_action) + elif action == "import": + await self._s2a_show_import(query) + elif action == "test": + await self._s2a_test_connection(query) + elif action == "run": + await self._s2a_show_run_options(query) + elif action == "run_team": + await self._s2a_run_team(query, context, sub_action) + elif action == "run_all": + await self._s2a_run_all(query, context) + elif action == "resume": + await self._s2a_resume(query, context) + elif action == "back": + await query.edit_message_text( + "📊 S2A 服务管理面板\n\n" + "Shared Account 服务配置管理\n\n" + "请选择功能:", + parse_mode="HTML", + reply_markup=self._get_s2a_main_keyboard() + ) + + async def _s2a_show_dashboard(self, query): + """显示 S2A 仪表盘""" + await query.edit_message_text("⏳ 正在获取仪表盘数据...") + + try: + stats = s2a_get_dashboard_stats() + if stats: + text = format_dashboard_stats(stats) + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(text, parse_mode="HTML", reply_markup=reply_markup) + else: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "❌ 获取仪表盘数据失败\n请检查 S2A 配置和 API 连接", + reply_markup=reply_markup + ) + except Exception as e: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(f"❌ 错误: {e}", reply_markup=reply_markup) + + async def _s2a_show_stock(self, query): + """显示库存信息""" + await query.edit_message_text("⏳ 正在获取库存数据...") + + try: + stats = s2a_get_dashboard_stats() + if not stats: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("❌ 获取库存信息失败", reply_markup=reply_markup) + return + + text = self._format_stock_message(stats) + keyboard = [ + [InlineKeyboardButton("🔄 刷新", callback_data="s2a:stock")], + [InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(text, parse_mode="HTML", reply_markup=reply_markup) + except Exception as e: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(f"❌ 错误: {e}", reply_markup=reply_markup) + + async def _s2a_show_keys_usage(self, query, period: str = ""): + """显示密钥用量""" + if not period: + # 显示时间选择菜单 + keyboard = [ + [ + InlineKeyboardButton("今日", callback_data="s2a:keys_usage:today"), + InlineKeyboardButton("本周", callback_data="s2a:keys_usage:week"), + ], + [ + InlineKeyboardButton("本月", callback_data="s2a:keys_usage:month"), + InlineKeyboardButton("全部", callback_data="s2a:keys_usage:all"), + ], + [InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "🔑 API 密钥用量查询\n\n选择时间范围:", + parse_mode="HTML", + reply_markup=reply_markup + ) + return + + await query.edit_message_text("⏳ 正在获取用量数据...") + + try: + keys_data = s2a_get_keys_with_usage(period) + if keys_data: + text = format_keys_usage(keys_data, period) + keyboard = [ + [InlineKeyboardButton("🔄 刷新", callback_data=f"s2a:keys_usage:{period}")], + [InlineKeyboardButton("◀️ 返回时间选择", callback_data="s2a:keys_usage")], + [InlineKeyboardButton("◀️ 返回主菜单", callback_data="s2a:back")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(text, parse_mode="HTML", reply_markup=reply_markup) + else: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:keys_usage")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("❌ 获取用量数据失败", reply_markup=reply_markup) + except Exception as e: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:keys_usage")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(f"❌ 错误: {e}", reply_markup=reply_markup) + + async def _s2a_show_config(self, query): + """显示 S2A 配置""" + # 脱敏显示 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 = [ + "⚙️ S2A 服务配置", + "", + f"API 地址: {S2A_API_BASE or '未配置'}", + f"CPA 地址: {CPA_API_BASE or '未配置'}", + f"CRS 地址: {CRS_API_BASE or '未配置'}", + f"Admin Key: {key_display}", + "", + f"并发数: {S2A_CONCURRENCY}", + f"优先级: {S2A_PRIORITY}", + f"分组名称: {groups_display}", + f"分组 ID: {group_ids_display}", + "", + "💡 使用命令修改配置:", + "/s2a_config concurrency 10", + "/s2a_config priority 50", + ] + + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + "\n".join(lines), + parse_mode="HTML", + reply_markup=reply_markup + ) + + async def _s2a_clean_errors(self, query, sub_action: str = ""): + """清理错误账号""" + if sub_action == "confirm": + await query.edit_message_text("⏳ 正在清理错误账号...") + + try: + # 获取错误账号 + error_accounts = s2a_get_error_accounts() + if not error_accounts: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "✅ 没有需要清理的错误账号", + reply_markup=reply_markup + ) + return + + # 批量删除 + deleted, failed = s2a_batch_delete_error_accounts(error_accounts) + + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"✅ 清理完成\n\n" + f"已删除: {deleted} 个\n" + f"失败: {failed} 个", + parse_mode="HTML", + reply_markup=reply_markup + ) + except Exception as e: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(f"❌ 清理失败: {e}", reply_markup=reply_markup) + return + + # 显示确认菜单 + try: + error_accounts = s2a_get_error_accounts() + count = len(error_accounts) if error_accounts else 0 + except: + count = 0 + + if count == 0: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "✅ 没有错误状态的账号需要清理", + reply_markup=reply_markup + ) + return + + keyboard = [ + [ + InlineKeyboardButton("✅ 确认清理", callback_data="s2a:clean_errors:confirm"), + InlineKeyboardButton("❌ 取消", callback_data="s2a:back"), + ], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + f"🧹 清理错误账号\n\n" + f"发现 {count} 个错误状态账号\n\n" + f"确认要清理这些账号吗?", + parse_mode="HTML", + reply_markup=reply_markup + ) + + async def _s2a_clean_teams(self, query, sub_action: str = ""): + """清理已完成 Teams""" + if sub_action == "confirm": + await query.edit_message_text("⏳ 正在清理已完成 Teams...") + + try: + completed_teams = get_completed_teams() + if not completed_teams: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "✅ 没有需要清理的已完成 Team", + reply_markup=reply_markup + ) + return + + # 批量删除 + removed_count = batch_remove_completed_teams() + + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"✅ 清理完成\n\n" + f"已清理: {removed_count} 个 Team", + parse_mode="HTML", + reply_markup=reply_markup + ) + except Exception as e: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(f"❌ 清理失败: {e}", reply_markup=reply_markup) + return + + # 显示确认菜单 + try: + completed_teams = get_completed_teams() + count = len(completed_teams) if completed_teams else 0 + except: + count = 0 + + if count == 0: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "✅ 没有已完成的 Team 需要清理", + reply_markup=reply_markup + ) + return + + keyboard = [ + [ + InlineKeyboardButton("✅ 确认清理", callback_data="s2a:clean_teams:confirm"), + InlineKeyboardButton("❌ 取消", callback_data="s2a:back"), + ], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + f"🗑️ 清理已完成 Teams\n\n" + f"发现 {count} 个已完成的 Team\n\n" + f"确认要从 tracker 中清理这些记录吗?", + parse_mode="HTML", + reply_markup=reply_markup + ) + + async def _s2a_show_import(self, query): + """显示导入说明""" + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + "📤 导入账号到 team.json\n\n" + "方式一: 使用命令\n" + "/import <json内容>\n\n" + "方式二: 发送 JSON 文件\n" + "直接发送 .json 文件即可自动导入\n\n" + "JSON 格式:\n" + "[{\"email\":\"...\",\"password\":\"...\"}]", + parse_mode="HTML", + reply_markup=reply_markup + ) + + async def _s2a_test_connection(self, query): + """测试 S2A API 连接""" + await query.edit_message_text("⏳ 正在测试 API 连接...") + + import requests + + results = [] + apis = [ + ("S2A", S2A_API_BASE), + ("CPA", CPA_API_BASE), + ("CRS", CRS_API_BASE), + ] + + for name, base_url in apis: + if not base_url: + results.append(f" {name}: ⚠️ 未配置") + continue + + try: + start = time.time() + resp = requests.get(f"{base_url}/health", timeout=5) + elapsed = (time.time() - start) * 1000 + + if resp.status_code == 200: + results.append(f" {name}: ✅ 正常 ({elapsed:.0f}ms)") + else: + results.append(f" {name}: ⚠️ 状态 {resp.status_code}") + except requests.exceptions.ConnectionError: + results.append(f" {name}: ❌ 连接失败") + except requests.exceptions.Timeout: + results.append(f" {name}: ❌ 超时") + except Exception as e: + results.append(f" {name}: ❌ {str(e)[:20]}") + + keyboard = [ + [InlineKeyboardButton("🔄 重新测试", callback_data="s2a:test")], + [InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + "🔄 API 连接测试\n\n" + + "\n".join(results), + parse_mode="HTML", + reply_markup=reply_markup + ) + + async def _s2a_show_run_options(self, query): + """显示运行选项""" + # 获取 Team 列表 + team_count = len(TEAMS) if TEAMS else 0 + + if team_count == 0: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "⚠️ 没有可处理的 Team\n\n" + "请先在 team.json 中添加账号", + parse_mode="HTML", + reply_markup=reply_markup + ) + return + + # 显示前几个 Team 的快捷按钮 + team_buttons = [] + for i, team in enumerate(TEAMS[:6]): + name = team.get("name", f"Team{i}") + team_buttons.append( + InlineKeyboardButton(f"{i}: {name[:8]}", callback_data=f"s2a:run_team:{i}") + ) + + # 每行2个按钮 + keyboard = [] + for i in range(0, len(team_buttons), 2): + row = team_buttons[i:i+2] + keyboard.append(row) + + keyboard.append([ + InlineKeyboardButton("🚀 处理全部", callback_data="s2a:run_all"), + InlineKeyboardButton("🔄 继续未完成", callback_data="s2a:resume"), + ]) + keyboard.append([InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + f"🚀 开始处理任务\n\n" + f"共有 {team_count} 个 Team\n\n" + f"选择要处理的 Team:", + parse_mode="HTML", + reply_markup=reply_markup + ) + + async def _s2a_run_team(self, query, context, team_idx_str: str): + """运行指定 Team""" + try: + team_idx = int(team_idx_str) + if team_idx < 0 or team_idx >= len(TEAMS): + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:run")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"❌ 无效的 Team 序号: {team_idx}", + reply_markup=reply_markup + ) + return + + # 检查是否有任务在运行 + if self.current_task and not self.current_task.done(): + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:run")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"⚠️ 当前有任务在运行: {self.current_team}\n\n" + f"请先使用 /stop 停止当前任务", + reply_markup=reply_markup + ) + return + + team = TEAMS[team_idx] + team_name = team.get("name", f"Team{team_idx}") + + await query.edit_message_text(f"🚀 开始处理 Team: {team_name}\n\n请查看日志了解进度") + + # 启动任务 + self.current_team = team_name + self.current_task = asyncio.create_task( + self._run_single_team_task(team_idx, query.message.chat_id) + ) + + except ValueError: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:run")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("❌ 无效的参数", reply_markup=reply_markup) + + async def _s2a_run_all(self, query, context): + """运行所有 Team""" + if self.current_task and not self.current_task.done(): + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:run")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"⚠️ 当前有任务在运行: {self.current_team}\n\n" + f"请先使用 /stop 停止当前任务", + reply_markup=reply_markup + ) + return + + if not TEAMS: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("⚠️ 没有可处理的 Team", reply_markup=reply_markup) + return + + await query.edit_message_text(f"🚀 开始处理所有 {len(TEAMS)} 个 Team\n\n请查看日志了解进度") + + self.current_team = "全部" + self.current_task = asyncio.create_task( + self._run_all_teams_task(query.message.chat_id) + ) + + async def _s2a_resume(self, query, context): + """继续处理未完成账号""" + if self.current_task and not self.current_task.done(): + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:run")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"⚠️ 当前有任务在运行: {self.current_team}\n\n" + f"请先使用 /stop 停止当前任务", + reply_markup=reply_markup + ) + return + + # 检查未完成账号 + incomplete = get_all_incomplete_accounts() + if not incomplete: + keyboard = [[InlineKeyboardButton("◀️ 返回", callback_data="s2a:back")]] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + "✅ 没有未完成的账号\n\n所有账号都已处理完成", + reply_markup=reply_markup + ) + return + + await query.edit_message_text(f"🔄 继续处理 {len(incomplete)} 个未完成账号\n\n请查看日志了解进度") + + self.current_team = "恢复" + self.current_task = asyncio.create_task( + self._run_resume_task(query.message.chat_id) + ) + + # ==================== AutoGPTPlus 管理面板 ==================== + @admin_only async def cmd_autogptplus(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """AutoGPTPlus 配置管理 - 交互式菜单""" keyboard = [ [ InlineKeyboardButton("📋 查看配置", callback_data="autogptplus:config"), - InlineKeyboardButton("� 设置 Token", callback_data="autogptplus:set_token"), + InlineKeyboardButton("🔑 设置 Token", callback_data="autogptplus:set_token"), ], [ - InlineKeyboardButton("� 测试邮件", callback_data="autogptplus:test_email"), + 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"), ], + [ + InlineKeyboardButton("🚀 开始注册", callback_data="autogptplus:register"), + ], ] reply_markup = InlineKeyboardMarkup(keyboard) - + await update.message.reply_text( "🤖 AutoGPTPlus 管理面板\n\n" "ChatGPT 订阅自动化配置管理\n\n" @@ -3035,21 +3667,46 @@ class ProvisionerBot: reply_markup=reply_markup ) + def _get_autogptplus_main_keyboard(self): + """获取 AutoGPTPlus 主菜单键盘""" + return InlineKeyboardMarkup([ + [ + 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"), + ], + [ + InlineKeyboardButton("🚀 开始注册", callback_data="autogptplus:register"), + ], + ]) + 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": @@ -3058,26 +3715,24 @@ class ProvisionerBot: 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) + elif action == "ibans": + await self._show_autogptplus_ibans(query, sub_action) + elif action == "fingerprint": + await self._toggle_autogptplus_fingerprint(query) + elif action == "stats": + await self._show_autogptplus_stats(query) + elif action == "register": + await self._start_autogptplus_register(query, context) elif action == "back": # 返回主菜单 - keyboard = [ - [ - InlineKeyboardButton("📋 查看配置", callback_data="autogptplus:config"), - InlineKeyboardButton("� 设置 Token", callback_data="autogptplus:set_token"), - ], - [ - InlineKeyboardButton("📧 测试邮件", callback_data="autogptplus:test_email"), - InlineKeyboardButton("🔄 测试 API", callback_data="autogptplus:test_api"), - ], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text( "🤖 AutoGPTPlus 管理面板\n\n" "ChatGPT 订阅自动化配置管理\n\n" "请选择功能:", parse_mode="HTML", - reply_markup=reply_markup + reply_markup=self._get_autogptplus_main_keyboard() ) async def _prompt_autogptplus_token(self, query, context: ContextTypes.DEFAULT_TYPE): @@ -3423,6 +4078,339 @@ class ProvisionerBot: reply_markup=reply_markup ) + async def _show_autogptplus_domains(self, query, sub_action: str = ""): + """显示/管理域名""" + 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"✅ 域名已清空\n\n" + f"已清空: {cleared} 个\n" + f"保留 (config): {config_count} 个", + parse_mode="HTML", + reply_markup=reply_markup + ) + return + + # 显示域名列表 + domains = get_email_domains() + file_count = get_file_domains_count() + config_count = len(EMAIL_DOMAINS) if EMAIL_DOMAINS else 0 + + lines = ["📧 邮箱域名管理\n"] + lines.append(f"总计: {len(domains)} 个") + lines.append(f" • txt文件: {file_count} 个") + lines.append(f" • config配置: {config_count} 个\n") + + if domains: + lines.append("域名列表:") + for i, domain in enumerate(domains[:15], 1): + lines.append(f" {i}. {domain}") + if len(domains) > 15: + lines.append(f" ... 还有 {len(domains) - 15} 个") + + keyboard = [ + [ + 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( + "❌ 模块未找到", + 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"❌ 操作失败\n\n{e}", + parse_mode="HTML", + reply_markup=reply_markup + ) + + async def _show_autogptplus_ibans(self, query, sub_action: str = ""): + """显示/管理 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( + "✅ IBAN 列表已清空", + parse_mode="HTML", + reply_markup=reply_markup + ) + return + + # 显示 IBAN 列表 + ibans = get_sepa_ibans() + file_ibans = load_ibans_from_file() + config_count = len(SEPA_IBANS) if SEPA_IBANS else 0 + + lines = ["💳 IBAN 管理\n"] + lines.append(f"总计: {len(ibans)} 个") + lines.append(f" • txt文件: {len(file_ibans)} 个") + lines.append(f" • config配置: {config_count} 个\n") + + if ibans: + lines.append("IBAN 列表:") + for i, iban in enumerate(ibans[:10], 1): + # 脱敏显示 + masked = f"{iban[:8]}...{iban[-4:]}" if len(iban) > 12 else iban + lines.append(f" {i}. {masked}") + if len(ibans) > 10: + lines.append(f" ... 还有 {len(ibans) - 10} 个") + + keyboard = [ + [ + 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( + "❌ 模块未找到", + 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"❌ 操作失败\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"🎭 随机指纹设置\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"❌ 设置失败\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 = ["📊 AutoGPTPlus 统计信息\n"] + + # 资源统计 + lines.append("📦 可用资源:") + lines.append(f" • 邮箱域名: {len(domains)} 个") + lines.append(f" • IBAN: {len(ibans)} 个") + lines.append(f" • 随机指纹: {'开启' if RANDOM_FINGERPRINT else '关闭'}") + lines.append("") + + # 账号统计 + lines.append("👥 已注册账号:") + lines.append(f" • 总计: {accounts_count} 个") + + # 配置状态 + lines.append("") + lines.append("⚙️ 配置状态:") + 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( + "❌ 模块未找到", + 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"❌ 获取统计失败\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( + "⚠️ 配置不完整\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( + "🚀 开始 ChatGPT Team 注册\n\n" + f"可用资源:\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( + "❌ 模块未找到", + 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"❌ 操作失败\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)"""