feat: Implement result deduplication by email and enhance summary reporting to detail only failed accounts.

This commit is contained in:
2026-01-18 06:59:35 +08:00
parent 49cb2430c8
commit 95fa705768
3 changed files with 135 additions and 15 deletions

1
run.py
View File

@@ -521,7 +521,6 @@ def process_accounts(accounts: list, team_name: str, team_index: int = 0,
) )
results.append(result) results.append(result)
_current_results.append(result)
# 更新进度: 账号完成 # 更新进度: 账号完成
is_success = result["status"] in ("success", "completed") is_success = result["status"] in ("success", "completed")

View File

@@ -5,6 +5,7 @@ import asyncio
import sys import sys
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import wraps from functools import wraps
from pathlib import Path
from typing import Optional from typing import Optional
from telegram import Update, Bot, BotCommand from telegram import Update, Bot, BotCommand
@@ -22,6 +23,8 @@ from config import (
TEAMS, TEAMS,
AUTH_PROVIDER, AUTH_PROVIDER,
TEAM_JSON_FILE, TEAM_JSON_FILE,
TEAM_TRACKER_FILE,
CSV_FILE,
TELEGRAM_CHECK_INTERVAL, TELEGRAM_CHECK_INTERVAL,
TELEGRAM_LOW_STOCK_THRESHOLD, TELEGRAM_LOW_STOCK_THRESHOLD,
CONFIG_FILE, CONFIG_FILE,
@@ -114,6 +117,7 @@ class ProvisionerBot:
("include_owners", self.cmd_include_owners), ("include_owners", self.cmd_include_owners),
("reload", self.cmd_reload), ("reload", self.cmd_reload),
("s2a_config", self.cmd_s2a_config), ("s2a_config", self.cmd_s2a_config),
("clean", self.cmd_clean),
] ]
for cmd, handler in handlers: for cmd, handler in handlers:
self.app.add_handler(CommandHandler(cmd, handler)) self.app.add_handler(CommandHandler(cmd, handler))
@@ -219,6 +223,7 @@ class ProvisionerBot:
/fingerprint - 开启/关闭随机指纹 /fingerprint - 开启/关闭随机指纹
/include_owners - 开启/关闭 Owner 入库 /include_owners - 开启/关闭 Owner 入库
/reload - 重载配置文件 (无需重启) /reload - 重载配置文件 (无需重启)
/clean - 清理 team.json 和 tracker 数据
<b>📊 S2A 专属:</b> <b>📊 S2A 专属:</b>
/dashboard - 查看 S2A 仪表盘 /dashboard - 查看 S2A 仪表盘
@@ -555,6 +560,113 @@ class ProvisionerBot:
except Exception as e: except Exception as e:
await update.message.reply_text(f"❌ 重载配置失败: {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 @admin_only
async def cmd_s2a_config(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_s2a_config(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""配置 S2A 服务参数""" """配置 S2A 服务参数"""

View File

@@ -203,6 +203,14 @@ def print_summary(results: list):
Args: Args:
results: [{"team": "...", "email": "...", "status": "...", "crs_id": "..."}] results: [{"team": "...", "email": "...", "status": "...", "crs_id": "..."}]
""" """
# 去重 (按 email 去重,保留最后一条记录)
seen = {}
for r in results:
email = r.get("email", "")
if email:
seen[email] = r
results = list(seen.values())
log.separator("=", 60) log.separator("=", 60)
log.header("执行摘要") log.header("执行摘要")
log.separator("=", 60) log.separator("=", 60)
@@ -212,30 +220,31 @@ def print_summary(results: list):
log.info(f"总计: {len(results)} 个账号") log.info(f"总计: {len(results)} 个账号")
log.success(f"成功: {success_count}") log.success(f"成功: {success_count}")
log.error(f"失败: {failed_count}") if failed_count > 0:
log.error(f"失败: {failed_count}")
# 按 Team 分组 # 按 Team 分组
teams = {} teams = {}
for r in results: for r in results:
team = r.get("team", "Unknown") team = r.get("team", "Unknown")
if team not in teams: if team not in teams:
teams[team] = {"success": 0, "failed": 0, "accounts": []} teams[team] = {"success": [], "failed": []}
if r.get("status") == "success": if r.get("status") == "success":
teams[team]["success"] += 1 teams[team]["success"].append(r)
else: else:
teams[team]["failed"] += 1 teams[team]["failed"].append(r)
teams[team]["accounts"].append(r) if teams:
log.info("按 Team 统计:")
for team_name, data in teams.items():
success_list = data["success"]
failed_list = data["failed"]
log.info(f" {team_name}: 成功 {len(success_list)}, 失败 {len(failed_list)}")
log.info("按 Team 统计:") # 只显示失败的账号详情
for team_name, data in teams.items(): for acc in failed_list:
log.info(f"{team_name}: 成功 {data['success']}, 失败 {data['failed']}", icon="team") log.error(f"{acc.get('email', 'Unknown')}")
for acc in data["accounts"]:
if acc.get("status") == "success":
log.success(f"{acc.get('email', 'Unknown')}")
else:
log.error(f"{acc.get('email', 'Unknown')}")
log.separator("=", 60) log.separator("=", 60)