diff --git a/config.py b/config.py index dde39ef..b60b332 100644 --- a/config.py +++ b/config.py @@ -214,6 +214,91 @@ def save_team_json(): return False +def remove_team_by_name(team_name: str) -> bool: + """从 team.json 中删除指定的 Team + + Args: + team_name: Team 名称 + + Returns: + bool: 是否成功删除 + """ + global _raw_teams, TEAMS + + # 查找要删除的 team 索引 + team_idx = None + for i, team in enumerate(TEAMS): + if team.get("name") == team_name: + team_idx = i + break + + if team_idx is None: + return False + + # 从 TEAMS 和 _raw_teams 中删除 + TEAMS.pop(team_idx) + _raw_teams.pop(team_idx) + + # 保存到文件 + try: + with open(TEAM_JSON_FILE, "w", encoding="utf-8") as f: + json.dump(_raw_teams, f, ensure_ascii=False, indent=2) + _log_config("INFO", "team.json", f"已删除 Team: {team_name}") + return True + except Exception as e: + _log_config("ERROR", "team.json", "保存失败", str(e)) + return False + + +def batch_remove_teams_by_names(team_names: list) -> dict: + """批量从 team.json 中删除指定的 Teams + + Args: + team_names: Team 名称列表 + + Returns: + dict: {"success": 成功数, "failed": 失败数, "total": 总数} + """ + global _raw_teams, TEAMS + + results = {"success": 0, "failed": 0, "total": len(team_names)} + + # 收集要保留的 teams + names_to_remove = set(team_names) + new_teams = [] + new_raw_teams = [] + removed_count = 0 + + for i, team in enumerate(TEAMS): + if team.get("name") in names_to_remove: + removed_count += 1 + else: + new_teams.append(team) + new_raw_teams.append(_raw_teams[i]) + + if removed_count == 0: + return results + + # 保存到文件 + try: + with open(TEAM_JSON_FILE, "w", encoding="utf-8") as f: + json.dump(new_raw_teams, f, ensure_ascii=False, indent=2) + + # 更新内存中的数据 + TEAMS.clear() + TEAMS.extend(new_teams) + _raw_teams.clear() + _raw_teams.extend(new_raw_teams) + + results["success"] = removed_count + _log_config("INFO", "team.json", f"已删除 {removed_count} 个 Team") + except Exception as e: + results["failed"] = len(team_names) + _log_config("ERROR", "team.json", "批量删除失败", str(e)) + + return results + + def reload_config() -> dict: """重新加载配置文件 (config.toml 和 team.json) diff --git a/telegram_bot.py b/telegram_bot.py index 3c1e3c8..a4578a0 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -48,8 +48,9 @@ from config import ( S2A_GROUP_IDS, S2A_ADMIN_KEY, BROWSER_RANDOM_FINGERPRINT, + batch_remove_teams_by_names, ) -from utils import load_team_tracker, get_all_incomplete_accounts +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, @@ -135,6 +136,7 @@ class ProvisionerBot: ("s2a_config", self.cmd_s2a_config), ("clean", self.cmd_clean), ("clean_errors", self.cmd_clean_errors), + ("clean_teams", self.cmd_clean_teams), ("keys_usage", self.cmd_keys_usage), ] for cmd, handler in handlers: @@ -155,6 +157,10 @@ class ProvisionerBot: self.callback_clean_errors, pattern="^clean_errors:" )) + self.app.add_handler(CallbackQueryHandler( + self.callback_clean_teams, + pattern="^clean_teams:" + )) # 注册定时检查任务 if TELEGRAM_CHECK_INTERVAL > 0 and AUTH_PROVIDER == "s2a": @@ -265,6 +271,10 @@ class ProvisionerBot: /s2a_config - 配置 S2A 参数 /clean_errors - 清理错误状态账号 +🧹 清理管理: +/clean - 清理已完成账号 (team.json) +/clean_teams - 清理已完成 Team (tracker) + 📤 导入账号: /import - 导入账号到 team.json 或直接发送 JSON 文件 @@ -1584,6 +1594,155 @@ class ProvisionerBot: 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 = [ + "🧹 清理已完成 Team (预览)", + "", + f"共发现 {total} 个已完成的 Team", + f"涉及 {total_accounts} 个账号记录", + "", + f"Team 列表 (第 {page + 1}/{total_pages} 页):", + ] + + # 显示当前页的 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([ + "", + "⚠️ 此操作将同时清理:", + "• 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"🧹 正在清理 {total} 个已完成 Team...", + 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 = [ + "✅ 清理完成", + "", + f"清理 Team: {tracker_results['success']}", + f"清理账号记录: {total_accounts}", + "", + "📁 文件清理:", + 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"失败: 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""" diff --git a/utils.py b/utils.py index f014207..d45f5d3 100644 --- a/utils.py +++ b/utils.py @@ -370,3 +370,93 @@ def add_team_owners_to_tracker(tracker: dict, password: str) -> int: log.info(f"已添加 {added_count} 个 Team Owner 到 tracker", icon="sync") return added_count + + +def get_completed_teams(tracker: dict) -> list: + """获取所有处理成功的 Team 列表 + + 处理成功的定义:所有账号状态都是 completed,且没有失败的账号 + + Args: + tracker: team_tracker 数据 + + Returns: + list: [{"name": "team_name", "total": 4, "completed": 4, "failed": 0, "pending": 0}] + """ + completed_teams = [] + teams_data = tracker.get("teams", {}) + + for team_name, accounts in teams_data.items(): + total = len(accounts) + if total == 0: + continue + + 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 + + # 全部完成且没有失败的才算成功 + if completed == total and failed == 0: + completed_teams.append({ + "name": team_name, + "total": total, + "completed": completed, + "failed": failed, + "pending": pending + }) + + return completed_teams + + +def remove_team_from_tracker(tracker: dict, team_name: str) -> bool: + """从 tracker 中删除整个 Team + + Args: + tracker: team_tracker 数据 + team_name: Team 名称 + + Returns: + bool: 是否成功删除 + """ + if team_name in tracker.get("teams", {}): + del tracker["teams"][team_name] + return True + return False + + +def batch_remove_completed_teams(tracker: dict) -> dict: + """批量删除所有处理成功的 Team + + Args: + tracker: team_tracker 数据 + + Returns: + dict: {"success": 删除成功数, "failed": 删除失败数, "total": 总数, "details": [...]} + """ + completed_teams = get_completed_teams(tracker) + + results = { + "success": 0, + "failed": 0, + "total": len(completed_teams), + "details": [] + } + + for team_info in completed_teams: + team_name = team_info["name"] + if remove_team_from_tracker(tracker, team_name): + results["success"] += 1 + results["details"].append({ + "name": team_name, + "status": "success", + "accounts": team_info["total"] + }) + else: + results["failed"] += 1 + results["details"].append({ + "name": team_name, + "status": "failed", + "message": "删除失败" + }) + + return results