#!/usr/bin/env python3 """TG 群管机器人 v2 — FastAPI + Tortoise ORM + python-telegram-bot""" import os import time import asyncio import logging from contextlib import asynccontextmanager # 代理(必须在其他 import 之前) PROXY_URL = os.getenv("PROXY_URL", "http://192.168.123.10:11055") TG_PROXY_URLS = [x.strip() for x in os.getenv( "TG_PROXY_URLS", ",".join([ PROXY_URL, "http://192.168.31.130:11055", "socks5://192.168.31.130:11088", "socks5://192.168.123.10:11088", ]), ).split(",") if x.strip()] os.environ["HTTPS_PROXY"] = PROXY_URL os.environ["HTTP_PROXY"] = PROXY_URL os.environ["NO_PROXY"] = os.getenv("NO_PROXY", "127.0.0.1,localhost") import uvicorn from fastapi import FastAPI from telegram import BotCommand, BotCommandScopeChat, Update from telegram.ext import Application, ContextTypes import config from db import init_db, close_db from handlers import register_all # ============ 日志 ============ log_fmt = "%(asctime)s [%(levelname)s] %(message)s" log_datefmt = "%Y-%m-%d %H:%M:%S" logging.basicConfig(level=logging.INFO, format=log_fmt, datefmt=log_datefmt) logger = logging.getLogger("spam_guard") # 阻止 httpx 刷屏 logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) # ============ Telegram Bot ============ tg_app: Application = None _last_polling_error_log = 0.0 _proxy_index = 0 def current_tg_proxy() -> str: return TG_PROXY_URLS[_proxy_index % len(TG_PROXY_URLS)] def rotate_tg_proxy() -> str: global _proxy_index _proxy_index = (_proxy_index + 1) % len(TG_PROXY_URLS) return current_tg_proxy() def polling_error_callback(exc): """压缩 Telegram polling 网络异常日志,避免代理偶发断连刷 traceback。""" global _last_polling_error_log now = time.time() # polling 会自动重试;同类网络波动 60 秒内只提示一次。 if now - _last_polling_error_log < 60: return _last_polling_error_log = now logger.warning(f"⚠️ TG polling 长轮询连接被代理中断,已自动重试: {type(exc).__name__}: {exc}") async def global_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE): """全局错误兜底,避免命令无响应且无日志。""" err = context.error logger.error(f"❌ Handler 异常: {type(err).__name__}: {err}") try: if isinstance(update, Update) and update.effective_chat: await context.bot.send_message(chat_id=update.effective_chat.id, text="❌ 指令处理失败,请稍后重试") except Exception as e: logger.warning(f"⚠️ 错误提示发送失败: {type(e).__name__}: {e}") async def start_tg_bot(): global tg_app from telegram.request import HTTPXRequest active_proxy = current_tg_proxy() logger.info(f"🌐 TG 使用代理: {active_proxy}") proxy_request = HTTPXRequest( proxy=active_proxy, connection_pool_size=64, connect_timeout=30.0, read_timeout=30.0, write_timeout=30.0, pool_timeout=30.0, ) get_updates_request = HTTPXRequest( proxy=active_proxy, connection_pool_size=8, connect_timeout=30.0, read_timeout=15.0, write_timeout=30.0, pool_timeout=30.0, ) tg_app = ( Application.builder() .token(config.TG_BOT_TOKEN) .request(proxy_request) .get_updates_request(get_updates_request) .build() ) register_all(tg_app) tg_app.add_error_handler(global_error_handler) await tg_app.initialize() await tg_app.start() await tg_app.updater.start_polling( poll_interval=0.5, timeout=3, drop_pending_updates=False, error_callback=polling_error_callback, ) # 菜单 commands = [ BotCommand("ask", "🤖 向知识库提问"), BotCommand("stats", "📊 今日统计"), BotCommand("checkin", "✅ 每日签到"), BotCommand("trivia", "🎮 知识答题"), BotCommand("lottery", "🎁 发起群内抽奖"), BotCommand("shop", "🛒 积分商城"), BotCommand("panel", "🎛️ 操作面板"), BotCommand("bind", "🔗 绑定账号"), BotCommand("me", "👤 查看授权信息"), BotCommand("dban", "🗑️ 删除并封禁"), BotCommand("dmute", "🔇 删除并禁言"), BotCommand("dwarn", "⚠️ 删除并警告"), BotCommand("help", "📖 帮助"), ] await tg_app.bot.delete_my_commands() await tg_app.bot.set_my_commands(commands, scope=BotCommandScopeChat(chat_id=config.TG_CHAT_ID)) logger.info(f"✅ TG Bot 已启动,菜单命令已设置 (群组: {config.TG_CHAT_ID})") async def stop_tg_bot(): global tg_app if tg_app: try: if tg_app.updater and tg_app.updater.running: await tg_app.updater.stop() if tg_app.running: await tg_app.stop() await tg_app.shutdown() logger.info("🛑 TG Bot 已停止") except Exception as e: logger.warning(f"⚠️ TG Bot 停止/清理异常: {type(e).__name__}: {e}") finally: tg_app = None # ============ FastAPI ============ from web.api import app as web_app async def restart_tg_bot_runtime(): """供 Web API 调用:只重启 TG Bot,不重启 Web。""" await stop_tg_bot() asyncio.create_task(start_tg_bot_safe()) async def start_tg_bot_safe(): """启动 TG Bot;初始化阶段网络失败时压缩日志并自动重试。""" delay = 10 while True: try: await start_tg_bot() return except asyncio.CancelledError: raise except Exception as e: next_proxy = rotate_tg_proxy() logger.error(f"❌ TG Bot 启动失败,将在 {delay}s 后用备用代理重试: {type(e).__name__}: {e}; next={next_proxy}") try: await stop_tg_bot() except Exception: pass await asyncio.sleep(delay) delay = min(120, delay * 2) @asynccontextmanager async def lifespan(app): # 初始化数据库 await init_db() # 启动 TG Bot import web.api as web_api web_api.set_restart_bot_callback(restart_tg_bot_runtime) task = asyncio.create_task(start_tg_bot_safe()) def log_task_result(t: asyncio.Task): if t.cancelled(): return exc = t.exception() if exc: logger.error("❌ TG Bot 任务异常结束", exc_info=exc) task.add_done_callback(log_task_result) logger.info("🚀 服务启动完成") try: yield finally: await stop_tg_bot() await close_db() logger.info("🛑 服务已关闭") web_app.router.lifespan_context = lifespan def run(): uvicorn.run(web_app, host="0.0.0.0", port=config.WEB_PORT, log_level="info", access_log=False) if __name__ == "__main__": run()