This commit is contained in:
2026-01-24 07:54:40 +08:00
parent effc1add37
commit 970340fbd4

View File

@@ -149,7 +149,6 @@ 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))
@@ -181,10 +180,6 @@ 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(
@@ -283,8 +278,6 @@ class ProvisionerBot:
BotCommand("team_register", "GPT Team 自动注册"),
# AutoGPTPlus
BotCommand("autogptplus", "AutoGPTPlus 管理面板"),
# S2A
BotCommand("s2a", "S2A 服务管理面板"),
]
try:
await self.app.bot.set_my_commands(commands)
@@ -3065,574 +3058,6 @@ 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"<b>⚠️ S2A 面板仅在 S2A 模式下可用</b>\n\n"
f"当前模式: {AUTH_PROVIDER}\n\n"
f"请在 config.toml 中设置:\n"
f"<code>auth_provider = \"s2a\"</code>",
parse_mode="HTML"
)
return
keyboard = self._get_s2a_main_keyboard()
await update.message.reply_text(
"<b>📊 S2A 服务管理面板</b>\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(
"<b>📊 S2A 服务管理面板</b>\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(
"<b>🔑 API 密钥用量查询</b>\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 = [
"<b>⚙️ S2A 服务配置</b>",
"",
f"<b>API 地址:</b> {S2A_API_BASE or '未配置'}",
f"<b>CPA 地址:</b> {CPA_API_BASE or '未配置'}",
f"<b>CRS 地址:</b> {CRS_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>",
]
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"<b>✅ 清理完成</b>\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"<b>🧹 清理错误账号</b>\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"<b>✅ 清理完成</b>\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"<b>🗑️ 清理已完成 Teams</b>\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(
"<b>📤 导入账号到 team.json</b>\n\n"
"<b>方式一:</b> 使用命令\n"
"<code>/import &lt;json内容&gt;</code>\n\n"
"<b>方式二:</b> 发送 JSON 文件\n"
"直接发送 .json 文件即可自动导入\n\n"
"<b>JSON 格式:</b>\n"
"<code>[{\"email\":\"...\",\"password\":\"...\"}]</code>",
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(
"<b>🔄 API 连接测试</b>\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(
"<b>⚠️ 没有可处理的 Team</b>\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"<b>🚀 开始处理任务</b>\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 配置管理 - 交互式菜单"""