From 41b0bdf0d7df1da4098f94bd9ec0cde3db45ecd5 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Tue, 7 Jul 2026 10:51:29 +0800 Subject: [PATCH] feat: add telegram proxy fallbacks and mobile dialog polish --- config.py | 1 + main.py | 29 ++++++++++++++++++++++++++--- requirements.txt | 2 ++ web/api.py | 6 +++--- web/static/index.html | 39 ++++++++++++++++++++++++++++++--------- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/config.py b/config.py index 9390f3f..0119451 100644 --- a/config.py +++ b/config.py @@ -17,6 +17,7 @@ OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3:8b") # Proxy PROXY_URL = os.getenv("PROXY_URL", "http://192.168.123.10:11055") +TG_PROXY_URL = os.getenv("TG_PROXY_URL", PROXY_URL) # Web WEB_PORT = int(os.getenv("WEB_PORT", "8766")) diff --git a/main.py b/main.py index a0f2b2e..a90718a 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,15 @@ 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") @@ -37,6 +46,17 @@ logging.getLogger("httpcore").setLevel(logging.WARNING) 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): @@ -66,8 +86,10 @@ async def start_tg_bot(): from telegram.request import HTTPXRequest + active_proxy = current_tg_proxy() + logger.info(f"🌐 TG 使用代理: {active_proxy}") proxy_request = HTTPXRequest( - proxy=PROXY_URL, + proxy=active_proxy, connection_pool_size=64, connect_timeout=30.0, read_timeout=30.0, @@ -75,7 +97,7 @@ async def start_tg_bot(): pool_timeout=30.0, ) get_updates_request = HTTPXRequest( - proxy=PROXY_URL, + proxy=active_proxy, connection_pool_size=8, connect_timeout=30.0, read_timeout=15.0, @@ -160,7 +182,8 @@ async def start_tg_bot_safe(): except asyncio.CancelledError: raise except Exception as e: - logger.error(f"❌ TG Bot 启动失败,将在 {delay}s 后重试: {type(e).__name__}: {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: diff --git a/requirements.txt b/requirements.txt index 94174df..34afa94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,5 @@ tortoise-orm>=0.24.0 aerich>=0.8.0 asyncpg>=0.30.0 sse-starlette>=3.4.5 + +socksio>=1.0.0 diff --git a/web/api.py b/web/api.py index c456e6b..f5c47a2 100644 --- a/web/api.py +++ b/web/api.py @@ -199,7 +199,7 @@ async def api_send(request: Request): if len(text) > 3500: return JSONResponse({"ok": False, "error": "消息太长"}, status_code=400) - bot = Bot(token=config.TG_BOT_TOKEN, request=HTTPXRequest(proxy=config.PROXY_URL)) + bot = Bot(token=config.TG_BOT_TOKEN, request=HTTPXRequest(proxy=getattr(config, "TG_PROXY_URL", config.PROXY_URL))) msg = await bot.send_message(chat_id=config.TG_CHAT_ID, text=text) rec = await Message.create( msg_id=msg.message_id, @@ -243,7 +243,7 @@ async def api_delete_message(message_id: int, request: Request): if not rec.chat_id or not rec.msg_id: return JSONResponse({"ok": False, "error": "缺少 TG chat_id/msg_id,无法删除 TG 消息"}, status_code=400) - bot = Bot(token=config.TG_BOT_TOKEN, request=HTTPXRequest(proxy=config.PROXY_URL, connect_timeout=20.0, read_timeout=20.0)) + bot = Bot(token=config.TG_BOT_TOKEN, request=HTTPXRequest(proxy=getattr(config, "TG_PROXY_URL", config.PROXY_URL), connect_timeout=20.0, read_timeout=20.0)) tg_deleted = True tg_error = None try: @@ -322,7 +322,7 @@ async def api_unban(user_id: int): from telegram import Bot from telegram.request import HTTPXRequest - bot = Bot(token=config.TG_BOT_TOKEN, request=HTTPXRequest(proxy=config.PROXY_URL)) + bot = Bot(token=config.TG_BOT_TOKEN, request=HTTPXRequest(proxy=getattr(config, "TG_PROXY_URL", config.PROXY_URL))) await bot.unban_chat_member(chat_id=config.TG_CHAT_ID, user_id=user_id) await Ban.filter(user_id=user_id).update(auto_unban=False, unban_at=now_tz().replace(tzinfo=None)) await Action.create(action="UNBAN", user_id=user_id, details={"method": "web"}) diff --git a/web/static/index.html b/web/static/index.html index bc05b1f..98b4ce5 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -122,6 +122,11 @@ .el-card { border: none; border-radius: 12px; } .el-button--mini, .el-button--small { border-radius: 8px; } + .confirm-dialog { border-radius: 14px; max-width: calc(100vw - 32px); } + .confirm-dialog .el-message-box__content { word-break: break-word; } + .chat-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } + .chat-actions .el-button--text { padding: 0; } + .chat-id-pill { color: #94a3b8; font-size: 11px; } .spam-tooltip { max-width: min(720px, calc(100vw - 48px)); word-break: break-word; line-height: 1.45; } @media (max-width: 900px) { .spam-tooltip { max-width: calc(100vw - 32px); } } @@ -151,8 +156,10 @@ .panel-actions { gap: 6px; } .scroll-area { height: 40vh; min-height: 280px; flex: none; } .chat-item { padding: 8px; } - .chat-item .meta { flex-direction: column; gap: 4px; } - .chat-item .meta-right { justify-content: flex-start; width: 100%; } + .chat-item .meta { flex-direction: column; gap: 5px; } + .chat-item .meta-right { justify-content: space-between; align-items: flex-start; width: 100%; gap: 6px; } + .chat-actions { justify-content: flex-end; } + .chat-item .text { margin-top: 2px; padding: 6px 8px; background: #f8fafc; border-radius: 8px; } .chat-item .user { max-width: 100%; white-space: normal; word-break: break-all; } .chat-item .time { white-space: normal; } .log-window { height: 34vh; min-height: 250px; flex: none; font-size: 10px; } @@ -174,6 +181,9 @@ .log-msg { white-space: normal; } .log-line { align-items: start; } .fixed-send-box { width: calc(100vw - 12px); } + .confirm-dialog { width: calc(100vw - 28px) !important; } + .confirm-dialog .el-message-box__btns { display: flex; gap: 8px; } + .confirm-dialog .el-message-box__btns button { flex: 1; margin-left: 0; } } @@ -217,10 +227,12 @@
@{{ item.username || item.first_name || 'unknown' }} ({{ item.user_id }}) - #{{ item.msg_id || '-' }} · {{ formatTime(item.time) }} - 手动删除 - 删除 - 删封 + #{{ item.msg_id || '-' }} · {{ formatTime(item.time) }} + + 手动删除 + 删除 + 删封 +
{{ item.text }}
@@ -383,7 +395,10 @@ new Vue({ this.$confirm('确认只重启 Telegram Bot?Web 管理页不会重启。', '重启 TG Bot', { confirmButtonText: '确认重启', cancelButtonText: '取消', - type: 'warning' + type: 'warning', + customClass: 'confirm-dialog', + dangerouslyUseHTMLString: false, + distinguishCancelAndClose: true }).then(async () => { this.restartingBot = true try { @@ -405,7 +420,10 @@ new Vue({ this.$confirm('确认重启 TG Spam Guard 服务?重启期间 Web 和 Bot 会短暂不可用。', '重启服务', { confirmButtonText: '确认重启', cancelButtonText: '取消', - type: 'warning' + type: 'warning', + customClass: 'confirm-dialog', + dangerouslyUseHTMLString: false, + distinguishCancelAndClose: true }).then(async () => { this.restarting = true try { @@ -455,7 +473,10 @@ new Vue({ this.$confirm(banUser ? '先删除 Telegram 群内消息,再封禁该用户,并标记数据库。继续吗?' : '先删除 Telegram 群内消息,再把数据库记录标记为手动删除。继续吗?', banUser ? '删除并封禁' : '删除聊天记录', { confirmButtonText: banUser ? '删封' : '删除', cancelButtonText: '取消', - type: 'warning' + type: 'warning', + customClass: 'confirm-dialog', + dangerouslyUseHTMLString: false, + distinguishCancelAndClose: true }).then(async () => { this.$set(item, '_deleting', true) try {