diff --git a/run.py b/run.py index 5f72581..1c9c696 100644 --- a/run.py +++ b/run.py @@ -87,10 +87,12 @@ def _signal_handler(signum, frame): sys.exit(0) -# 注册信号处理器 -signal.signal(signal.SIGINT, _signal_handler) -signal.signal(signal.SIGTERM, _signal_handler) -atexit.register(_save_state) +# 注册信号处理器 (仅在主线程中) +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) def process_single_team(team: dict) -> tuple[list, list]: diff --git a/telegram_bot.py b/telegram_bot.py index 13685aa..a367667 100644 --- a/telegram_bot.py +++ b/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("Bot Started\nReady for commands. Send /help for usage.") + await self.notifier.notify("🤖 Bot 已启动\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 = """OpenAI Team Provisioner Bot + help_text = """🤖 OpenAI Team 批量注册 Bot -Commands: -/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 +📋 命令列表: +/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 - 显示此帮助 -Upload Accounts: -Send a JSON file or use /import with JSON data: -[{"account":"email","password":"pwd","token":"jwt"},...] -Then use /run to process them. +📤 上传账号: +直接发送 JSON 文件,或使用 /import 加 JSON 数据: +[{"account":"邮箱","password":"密码","token":"jwt"},...] +上传后使用 /run 开始处理 -Examples: -/run 0 - Process first team -/team 1 - View second team status -/logs 20 - View last 20 logs""" +💡 示例: +/run 0 - 处理第一个 Team +/team 1 - 查看第二个 Team 状态 +/logs 20 - 查看最近 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 = ["Teams Status\n"] + lines = ["📊 Team 状态总览\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}] {team_name}: {completed}/{total} " - f"(F:{failed} P:{pending})" + f"{status_icon} {team_name}: {completed}/{total} " + f"(失败:{failed} 待处理:{pending})" ) # 当前任务状态 if self.current_task and not self.current_task.done(): - lines.append(f"\nRunning: {self.current_team or 'Unknown'}") + lines.append(f"\n🔄 运行中: {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 \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"Team {team_idx}: {team_name}\n"] - lines.append(f"Owner: {team.get('owner_email', 'N/A')}") - lines.append(f"Accounts: {len(accounts)}\n") + lines = [f"📁 Team {team_idx}: {team_name}\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 \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"{log_text}", 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"{status_icon}" elif health_pct >= 80: - status_icon = "OK" + status_icon = "✅ 正常" status_line = f"{status_icon}" elif health_pct >= 50: - status_icon = "WARN" + status_icon = "⚠️ 警告" status_line = f"{status_icon}" else: - status_icon = "CRITICAL" + status_icon = "🔴 严重" status_line = f"{status_icon}" - title = "LOW STOCK ALERT" if is_alert else "Account Stock" + title = "🚨 库存不足警报" if is_alert else "📦 账号库存" lines = [ f"{title}", "", - f"Status: {status_line}", - f"Health: {health_pct:.1f}%", + f"状态: {status_line}", + f"健康度: {health_pct:.1f}%", "", - f"Normal: {normal}", - f"Error: {error}", - f"RateLimit: {ratelimit}", - f"Total: {total}", + f"正常: {normal}", + 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( - "Upload Accounts to team.json\n\n" - "Usage:\n" - "1. Send a JSON file directly\n" - "2. /import followed by JSON data\n\n" - "JSON format:\n" - "[{\"account\":\"email\",\"password\":\"pwd\",\"token\":\"jwt\"},...]\n\n" - "After upload, use /run to start processing.", + "📤 导入账号到 team.json\n\n" + "用法:\n" + "1. 直接发送 JSON 文件\n" + "2. /import 后跟 JSON 数据\n\n" + "JSON 格式:\n" + "[{\"account\":\"邮箱\",\"password\":\"密码\",\"token\":\"jwt\"},...]\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"Upload Complete\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"✅ 导入完成\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():