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