This commit is contained in:
2026-01-21 22:05:56 +08:00
parent 7a7b503b3c
commit 6600195a3f
2 changed files with 343 additions and 1 deletions

View File

@@ -51,7 +51,10 @@ from config import (
)
from utils import load_team_tracker, get_all_incomplete_accounts
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
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
)
from email_service import gptmail_service, unified_create_email
from logger import log
@@ -131,6 +134,7 @@ class ProvisionerBot:
("reload", self.cmd_reload),
("s2a_config", self.cmd_s2a_config),
("clean", self.cmd_clean),
("clean_errors", self.cmd_clean_errors),
("keys_usage", self.cmd_keys_usage),
]
for cmd, handler in handlers:
@@ -147,6 +151,10 @@ class ProvisionerBot:
self.callback_keys_usage,
pattern="^keys_usage:"
))
self.app.add_handler(CallbackQueryHandler(
self.callback_clean_errors,
pattern="^clean_errors:"
))
# 注册定时检查任务
if TELEGRAM_CHECK_INTERVAL > 0 and AUTH_PROVIDER == "s2a":
@@ -255,6 +263,7 @@ class ProvisionerBot:
/keys_usage - 查看 API 密钥用量
/stock - 查看账号库存
/s2a_config - 配置 S2A 参数
/clean_errors - 清理错误状态账号
<b>📤 导入账号:</b>
/import - 导入账号到 team.json
@@ -1414,6 +1423,167 @@ class ProvisionerBot:
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_import(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""上传账号到 team.json"""