This commit is contained in:
2026-01-15 23:39:41 +08:00
parent 06f906abc7
commit b1953b5d95
2 changed files with 101 additions and 99 deletions

4
run.py
View File

@@ -87,7 +87,9 @@ def _signal_handler(signum, frame):
sys.exit(0) sys.exit(0)
# 注册信号处理器 # 注册信号处理器 (仅在主线程中)
import threading
if threading.current_thread() is threading.main_thread():
signal.signal(signal.SIGINT, _signal_handler) signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler) signal.signal(signal.SIGTERM, _signal_handler)
atexit.register(_save_state) atexit.register(_save_state)

View File

@@ -38,7 +38,7 @@ def admin_only(func):
async def wrapper(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def wrapper(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS: if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await update.message.reply_text("Unauthorized. Your ID is not in admin list.") await update.message.reply_text("⛔ 无权限,你的 ID 不在管理员列表中")
return return
return await func(self, update, context) return await func(self, update, context)
return wrapper return wrapper
@@ -108,7 +108,7 @@ class ProvisionerBot:
log.info(f"Admin Chat IDs: {TELEGRAM_ADMIN_CHAT_IDS}") log.info(f"Admin Chat IDs: {TELEGRAM_ADMIN_CHAT_IDS}")
# 发送启动通知 # 发送启动通知
await self.notifier.notify("<b>Bot Started</b>\nReady for commands. Send /help for usage.") await self.notifier.notify("<b>🤖 Bot 已启动</b>\n准备就绪,发送 /help 查看帮助")
# 运行 Bot # 运行 Bot
await self.app.initialize() await self.app.initialize()
@@ -131,29 +131,29 @@ class ProvisionerBot:
@admin_only @admin_only
async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""显示帮助信息""" """显示帮助信息"""
help_text = """<b>OpenAI Team Provisioner Bot</b> help_text = """<b>🤖 OpenAI Team 批量注册 Bot</b>
<b>Commands:</b> <b>📋 命令列表:</b>
/status - View all teams status /status - 查看所有 Team 状态
/team &lt;n&gt; - View team N details /team &lt;n&gt; - 查看第 n 个 Team 详情
/run &lt;n&gt; - Start processing team N /run &lt;n&gt; - 开始处理第 n 个 Team
/run_all - Start processing all teams /run_all - 开始处理所有 Team
/stop - Stop current task /stop - 停止当前任务
/logs [n] - View recent n logs (default 10) /logs [n] - 查看最近 n 条日志 (默认 10)
/dashboard - View S2A dashboard stats /dashboard - 查看 S2A 仪表盘
/stock - Check account stock /stock - 查看账号库存
/import - Upload accounts to team.json /import - 导入账号到 team.json
/help - Show this help /help - 显示此帮助
<b>Upload Accounts:</b> <b>📤 上传账号:</b>
Send a JSON file or use /import with JSON data: 直接发送 JSON 文件,或使用 /import JSON 数据:
<code>[{"account":"email","password":"pwd","token":"jwt"},...]</code> <code>[{"account":"邮箱","password":"密码","token":"jwt"},...]</code>
Then use /run to process them. 上传后使用 /run 开始处理
<b>Examples:</b> <b>💡 示例:</b>
<code>/run 0</code> - Process first team <code>/run 0</code> - 处理第一个 Team
<code>/team 1</code> - View second team status <code>/team 1</code> - 查看第二个 Team 状态
<code>/logs 20</code> - View last 20 logs""" <code>/logs 20</code> - 查看最近 20 条日志"""
await update.message.reply_text(help_text, parse_mode="HTML") await update.message.reply_text(help_text, parse_mode="HTML")
@admin_only @admin_only
@@ -163,25 +163,25 @@ Then use /run to process them.
teams_data = tracker.get("teams", {}) teams_data = tracker.get("teams", {})
if not teams_data: if not teams_data:
await update.message.reply_text("No data yet. Run tasks first.") await update.message.reply_text("📭 暂无数据,请先运行任务")
return return
lines = ["<b>Teams Status</b>\n"] lines = ["<b>📊 Team 状态总览</b>\n"]
for team_name, accounts in teams_data.items(): for team_name, accounts in teams_data.items():
total = len(accounts) total = len(accounts)
completed = sum(1 for a in accounts if a.get("status") == "completed") 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()) failed = sum(1 for a in accounts if "fail" in a.get("status", "").lower())
pending = total - completed - failed pending = total - completed - failed
status_icon = "OK" if completed == total else ("FAIL" if failed > 0 else "...") status_icon = "" if completed == total else ("" if failed > 0 else "")
lines.append( lines.append(
f"[{status_icon}] <b>{team_name}</b>: {completed}/{total} " f"{status_icon} <b>{team_name}</b>: {completed}/{total} "
f"(F:{failed} P:{pending})" f"(失败:{failed} 待处理:{pending})"
) )
# 当前任务状态 # 当前任务状态
if self.current_task and not self.current_task.done(): if self.current_task and not self.current_task.done():
lines.append(f"\n<b>Running:</b> {self.current_team or 'Unknown'}") lines.append(f"\n<b>🔄 运行中:</b> {self.current_team or '未知'}")
await update.message.reply_text("\n".join(lines), parse_mode="HTML") await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@@ -189,17 +189,17 @@ Then use /run to process them.
async def cmd_team(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_team(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看指定 Team 详情""" """查看指定 Team 详情"""
if not context.args: if not context.args:
await update.message.reply_text("Usage: /team <index>\nExample: /team 0") await update.message.reply_text("用法: /team <序号>\n示例: /team 0")
return return
try: try:
team_idx = int(context.args[0]) team_idx = int(context.args[0])
except ValueError: except ValueError:
await update.message.reply_text("Invalid team index. Must be a number.") await update.message.reply_text("❌ 无效的序号,必须是数字")
return return
if team_idx < 0 or team_idx >= len(TEAMS): if team_idx < 0 or team_idx >= len(TEAMS):
await update.message.reply_text(f"Team index out of range. Valid: 0-{len(TEAMS)-1}") await update.message.reply_text(f"❌ 序号超出范围,有效范围: 0-{len(TEAMS)-1}")
return return
team = TEAMS[team_idx] team = TEAMS[team_idx]
@@ -208,22 +208,22 @@ Then use /run to process them.
tracker = load_team_tracker() tracker = load_team_tracker()
accounts = tracker.get("teams", {}).get(team_name, []) accounts = tracker.get("teams", {}).get(team_name, [])
lines = [f"<b>Team {team_idx}: {team_name}</b>\n"] lines = [f"<b>📁 Team {team_idx}: {team_name}</b>\n"]
lines.append(f"Owner: {team.get('owner_email', 'N/A')}") lines.append(f"👤 Owner: {team.get('owner_email', '')}")
lines.append(f"Accounts: {len(accounts)}\n") lines.append(f"📊 账号数: {len(accounts)}\n")
if accounts: if accounts:
for acc in accounts: for acc in accounts:
email = acc.get("email", "") email = acc.get("email", "")
status = acc.get("status", "unknown") status = acc.get("status", "unknown")
role = acc.get("role", "member") role = acc.get("role", "member")
icon = {"completed": "OK", "authorized": "AUTH", "registered": "REG"}.get( icon = {"completed": "", "authorized": "🔐", "registered": "📝"}.get(
status, "FAIL" if "fail" in status.lower() else "..." status, "" if "fail" in status.lower() else ""
) )
role_tag = " [O]" if role == "owner" else "" role_tag = " [Owner]" if role == "owner" else ""
lines.append(f"[{icon}] {email}{role_tag}") lines.append(f"{icon} {email}{role_tag}")
else: else:
lines.append("No accounts processed yet.") lines.append("📭 暂无已处理的账号")
await update.message.reply_text("\n".join(lines), parse_mode="HTML") await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@@ -232,28 +232,28 @@ Then use /run to process them.
"""启动处理指定 Team""" """启动处理指定 Team"""
if self.current_task and not self.current_task.done(): if self.current_task and not self.current_task.done():
await update.message.reply_text( await update.message.reply_text(
f"Task already running: {self.current_team}\nUse /stop to cancel." f"⚠️ 任务正在运行: {self.current_team}\n使用 /stop 停止"
) )
return return
if not context.args: if not context.args:
await update.message.reply_text("Usage: /run <team_index>\nExample: /run 0") await update.message.reply_text("用法: /run <序号>\n示例: /run 0")
return return
try: try:
team_idx = int(context.args[0]) team_idx = int(context.args[0])
except ValueError: except ValueError:
await update.message.reply_text("Invalid team index. Must be a number.") await update.message.reply_text("❌ 无效的序号,必须是数字")
return return
if team_idx < 0 or team_idx >= len(TEAMS): if team_idx < 0 or team_idx >= len(TEAMS):
await update.message.reply_text(f"Team index out of range. Valid: 0-{len(TEAMS)-1}") await update.message.reply_text(f"❌ 序号超出范围,有效范围: 0-{len(TEAMS)-1}")
return return
team_name = TEAMS[team_idx].get("name", f"Team{team_idx}") team_name = TEAMS[team_idx].get("name", f"Team{team_idx}")
self.current_team = team_name self.current_team = team_name
await update.message.reply_text(f"Starting task for Team {team_idx}: {team_name}...") await update.message.reply_text(f"🚀 开始处理 Team {team_idx}: {team_name}...")
# 在后台线程执行任务 # 在后台线程执行任务
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@@ -271,12 +271,12 @@ Then use /run to process them.
"""启动处理所有 Team""" """启动处理所有 Team"""
if self.current_task and not self.current_task.done(): if self.current_task and not self.current_task.done():
await update.message.reply_text( await update.message.reply_text(
f"Task already running: {self.current_team}\nUse /stop to cancel." f"⚠️ 任务正在运行: {self.current_team}\n使用 /stop 停止"
) )
return return
self.current_team = "ALL" self.current_team = "全部"
await update.message.reply_text(f"Starting task for ALL teams ({len(TEAMS)} teams)...") await update.message.reply_text(f"🚀 开始处理所有 Team ({len(TEAMS)} )...")
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
self.current_task = loop.run_in_executor( self.current_task = loop.run_in_executor(
@@ -284,7 +284,7 @@ Then use /run to process them.
self._run_all_teams_task self._run_all_teams_task
) )
self.current_task = asyncio.ensure_future(self._wrap_task(self.current_task, "ALL")) self.current_task = asyncio.ensure_future(self._wrap_task(self.current_task, "全部"))
async def _wrap_task(self, task, team_name: str): async def _wrap_task(self, task, team_name: str):
"""包装任务以处理完成通知""" """包装任务以处理完成通知"""
@@ -294,7 +294,7 @@ Then use /run to process them.
failed = len(result or []) - success failed = len(result or []) - success
await self.notifier.notify_task_completed(team_name, success, failed) await self.notifier.notify_task_completed(team_name, success, failed)
except Exception as e: except Exception as e:
await self.notifier.notify_error(f"Task failed: {team_name}", str(e)) await self.notifier.notify_error(f"任务失败: {team_name}", str(e))
finally: finally:
self.current_team = None self.current_team = None
# 清理进度跟踪 # 清理进度跟踪
@@ -315,14 +315,14 @@ Then use /run to process them.
async def cmd_stop(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_stop(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""停止当前任务""" """停止当前任务"""
if not self.current_task or self.current_task.done(): if not self.current_task or self.current_task.done():
await update.message.reply_text("No task is running.") await update.message.reply_text("📭 当前没有运行中的任务")
return return
# 注意: 由于任务在线程池中运行,无法直接取消 # 注意: 由于任务在线程池中运行,无法直接取消
# 这里只能发送信号 # 这里只能发送信号
await update.message.reply_text( await update.message.reply_text(
f"Requesting stop for: {self.current_team}\n" f"🛑 正在停止: {self.current_team}\n"
"Note: Current account processing will complete before stopping." "注意: 当前账号处理完成后才会停止"
) )
# 设置全局停止标志 # 设置全局停止标志
@@ -346,7 +346,7 @@ Then use /run to process them.
from config import BASE_DIR from config import BASE_DIR
log_file = BASE_DIR / "logs" / "app.log" log_file = BASE_DIR / "logs" / "app.log"
if not log_file.exists(): if not log_file.exists():
await update.message.reply_text("No log file found.") await update.message.reply_text("📭 日志文件不存在")
return return
with open(log_file, "r", encoding="utf-8", errors="ignore") as f: with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
@@ -354,7 +354,7 @@ Then use /run to process them.
recent = lines[-n:] if len(lines) >= n else lines recent = lines[-n:] if len(lines) >= n else lines
if not recent: if not recent:
await update.message.reply_text("Log file is empty.") await update.message.reply_text("📭 日志文件为空")
return return
# 格式化日志 (移除 ANSI 颜色码) # 格式化日志 (移除 ANSI 颜色码)
@@ -369,19 +369,19 @@ Then use /run to process them.
await update.message.reply_text(f"<code>{log_text}</code>", parse_mode="HTML") await update.message.reply_text(f"<code>{log_text}</code>", parse_mode="HTML")
except Exception as e: except Exception as e:
await update.message.reply_text(f"Error reading logs: {e}") await update.message.reply_text(f"❌ 读取日志失败: {e}")
@admin_only @admin_only
async def cmd_dashboard(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_dashboard(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看 S2A 仪表盘统计""" """查看 S2A 仪表盘统计"""
if AUTH_PROVIDER != "s2a": if AUTH_PROVIDER != "s2a":
await update.message.reply_text( await update.message.reply_text(
f"Dashboard only available for S2A provider.\n" f"⚠️ 仪表盘仅支持 S2A 模式\n"
f"Current provider: {AUTH_PROVIDER}" f"当前模式: {AUTH_PROVIDER}"
) )
return return
await update.message.reply_text("Fetching dashboard stats...") await update.message.reply_text("⏳ 正在获取仪表盘数据...")
try: try:
stats = s2a_get_dashboard_stats() stats = s2a_get_dashboard_stats()
@@ -390,25 +390,25 @@ Then use /run to process them.
await update.message.reply_text(text, parse_mode="HTML") await update.message.reply_text(text, parse_mode="HTML")
else: else:
await update.message.reply_text( await update.message.reply_text(
"Failed to fetch dashboard stats.\n" "❌ 获取仪表盘数据失败\n"
"Check S2A configuration and API connection." "请检查 S2A 配置和 API 连接"
) )
except Exception as e: except Exception as e:
await update.message.reply_text(f"Error: {e}") await update.message.reply_text(f"❌ 错误: {e}")
@admin_only @admin_only
async def cmd_stock(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_stock(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看账号存货""" """查看账号存货"""
if AUTH_PROVIDER != "s2a": if AUTH_PROVIDER != "s2a":
await update.message.reply_text( await update.message.reply_text(
f"Stock check only available for S2A provider.\n" f"⚠️ 库存查询仅支持 S2A 模式\n"
f"Current provider: {AUTH_PROVIDER}" f"当前模式: {AUTH_PROVIDER}"
) )
return return
stats = s2a_get_dashboard_stats() stats = s2a_get_dashboard_stats()
if not stats: if not stats:
await update.message.reply_text("Failed to fetch stock info.") await update.message.reply_text("❌ 获取库存信息失败")
return return
text = self._format_stock_message(stats) text = self._format_stock_message(stats)
@@ -453,35 +453,35 @@ Then use /run to process them.
# 状态图标 # 状态图标
if normal <= TELEGRAM_LOW_STOCK_THRESHOLD: if normal <= TELEGRAM_LOW_STOCK_THRESHOLD:
status_icon = "LOW STOCK" status_icon = "⚠️ 库存不足"
status_line = f"<b>{status_icon}</b>" status_line = f"<b>{status_icon}</b>"
elif health_pct >= 80: elif health_pct >= 80:
status_icon = "OK" status_icon = "✅ 正常"
status_line = f"<b>{status_icon}</b>" status_line = f"<b>{status_icon}</b>"
elif health_pct >= 50: elif health_pct >= 50:
status_icon = "WARN" status_icon = "⚠️ 警告"
status_line = f"<b>{status_icon}</b>" status_line = f"<b>{status_icon}</b>"
else: else:
status_icon = "CRITICAL" status_icon = "🔴 严重"
status_line = f"<b>{status_icon}</b>" status_line = f"<b>{status_icon}</b>"
title = "LOW STOCK ALERT" if is_alert else "Account Stock" title = "🚨 库存不足警报" if is_alert else "📦 账号库存"
lines = [ lines = [
f"<b>{title}</b>", f"<b>{title}</b>",
"", "",
f"Status: {status_line}", f"状态: {status_line}",
f"Health: {health_pct:.1f}%", f"健康度: {health_pct:.1f}%",
"", "",
f"Normal: <b>{normal}</b>", f"正常: <b>{normal}</b>",
f"Error: {error}", f"异常: {error}",
f"RateLimit: {ratelimit}", f"限流: {ratelimit}",
f"Total: {total}", f"总计: {total}",
] ]
if is_alert: if is_alert:
lines.append("") lines.append("")
lines.append(f"Threshold: {TELEGRAM_LOW_STOCK_THRESHOLD}") lines.append(f"预警阈值: {TELEGRAM_LOW_STOCK_THRESHOLD}")
return "\n".join(lines) return "\n".join(lines)
@@ -491,13 +491,13 @@ Then use /run to process them.
# 获取命令后的 JSON 数据 # 获取命令后的 JSON 数据
if not context.args: if not context.args:
await update.message.reply_text( await update.message.reply_text(
"<b>Upload Accounts to team.json</b>\n\n" "<b>📤 导入账号到 team.json</b>\n\n"
"Usage:\n" "用法:\n"
"1. Send a JSON file directly\n" "1. 直接发送 JSON 文件\n"
"2. /import followed by JSON data\n\n" "2. /import 后跟 JSON 数据\n\n"
"JSON format:\n" "JSON 格式:\n"
"<code>[{\"account\":\"email\",\"password\":\"pwd\",\"token\":\"jwt\"},...]</code>\n\n" "<code>[{\"account\":\"邮箱\",\"password\":\"密码\",\"token\":\"jwt\"},...]</code>\n\n"
"After upload, use /run to start processing.", "导入后使用 /run 开始处理",
parse_mode="HTML" parse_mode="HTML"
) )
return return
@@ -512,14 +512,14 @@ Then use /run to process them.
# 检查是否是管理员 # 检查是否是管理员
user_id = update.effective_user.id user_id = update.effective_user.id
if user_id not in TELEGRAM_ADMIN_CHAT_IDS: if user_id not in TELEGRAM_ADMIN_CHAT_IDS:
await update.message.reply_text("Unauthorized.") await update.message.reply_text("⛔ 无权限")
return return
document = update.message.document document = update.message.document
if not document: if not document:
return return
await update.message.reply_text("Processing JSON file...") await update.message.reply_text("⏳ 正在处理 JSON 文件...")
try: try:
# 下载文件 # 下载文件
@@ -530,7 +530,7 @@ Then use /run to process them.
await self._process_import_json(update, json_text) await self._process_import_json(update, json_text)
except Exception as e: except Exception as e:
await update.message.reply_text(f"Error reading file: {e}") await update.message.reply_text(f"❌ 读取文件失败: {e}")
async def _process_import_json(self, update: Update, json_text: str): async def _process_import_json(self, update: Update, json_text: str):
"""处理导入的 JSON 数据,保存到 team.json""" """处理导入的 JSON 数据,保存到 team.json"""
@@ -540,7 +540,7 @@ Then use /run to process them.
try: try:
new_accounts = json.loads(json_text) new_accounts = json.loads(json_text)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
await update.message.reply_text(f"Invalid JSON format: {e}") await update.message.reply_text(f"❌ JSON 格式错误: {e}")
return return
if not isinstance(new_accounts, list): if not isinstance(new_accounts, list):
@@ -548,7 +548,7 @@ Then use /run to process them.
new_accounts = [new_accounts] new_accounts = [new_accounts]
if not new_accounts: if not new_accounts:
await update.message.reply_text("No accounts in JSON data") await update.message.reply_text("📭 JSON 数据中没有账号")
return return
# 验证格式 # 验证格式
@@ -569,7 +569,7 @@ Then use /run to process them.
}) })
if not valid_accounts: if not valid_accounts:
await update.message.reply_text("No valid accounts found (need account/email and token)") await update.message.reply_text("❌ 未找到有效账号 (需要 account/email token 字段)")
return return
# 读取现有 team.json # 读取现有 team.json
@@ -608,16 +608,16 @@ Then use /run to process them.
json.dump(existing_accounts, f, ensure_ascii=False, indent=2) json.dump(existing_accounts, f, ensure_ascii=False, indent=2)
await update.message.reply_text( await update.message.reply_text(
f"<b>Upload Complete</b>\n\n" f"<b>✅ 导入完成</b>\n\n"
f"Added: {added}\n" f"新增: {added}\n"
f"Skipped (duplicate): {skipped}\n" f"跳过 (重复): {skipped}\n"
f"Total in team.json: {len(existing_accounts)}\n\n" f"team.json 总数: {len(existing_accounts)}\n\n"
f"Use /run_all or /run &lt;n&gt; to start processing.", f"使用 /run_all /run &lt;n&gt; 开始处理",
parse_mode="HTML" parse_mode="HTML"
) )
except Exception as e: except Exception as e:
await update.message.reply_text(f"Error saving to team.json: {e}") await update.message.reply_text(f"❌ 保存到 team.json 失败: {e}")
async def main(): async def main():