feat: add telegram proxy fallbacks and mobile dialog polish

This commit is contained in:
ngfchl
2026-07-07 10:51:29 +08:00
parent 32187eb9a6
commit 41b0bdf0d7
5 changed files with 62 additions and 15 deletions
+1
View File
@@ -17,6 +17,7 @@ OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3:8b")
# Proxy # Proxy
PROXY_URL = os.getenv("PROXY_URL", "http://192.168.123.10:11055") PROXY_URL = os.getenv("PROXY_URL", "http://192.168.123.10:11055")
TG_PROXY_URL = os.getenv("TG_PROXY_URL", PROXY_URL)
# Web # Web
WEB_PORT = int(os.getenv("WEB_PORT", "8766")) WEB_PORT = int(os.getenv("WEB_PORT", "8766"))
+26 -3
View File
@@ -8,6 +8,15 @@ from contextlib import asynccontextmanager
# 代理(必须在其他 import 之前) # 代理(必须在其他 import 之前)
PROXY_URL = os.getenv("PROXY_URL", "http://192.168.123.10:11055") 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["HTTPS_PROXY"] = PROXY_URL
os.environ["HTTP_PROXY"] = PROXY_URL os.environ["HTTP_PROXY"] = PROXY_URL
os.environ["NO_PROXY"] = os.getenv("NO_PROXY", "127.0.0.1,localhost") 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 tg_app: Application = None
_last_polling_error_log = 0.0 _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): def polling_error_callback(exc):
@@ -66,8 +86,10 @@ async def start_tg_bot():
from telegram.request import HTTPXRequest from telegram.request import HTTPXRequest
active_proxy = current_tg_proxy()
logger.info(f"🌐 TG 使用代理: {active_proxy}")
proxy_request = HTTPXRequest( proxy_request = HTTPXRequest(
proxy=PROXY_URL, proxy=active_proxy,
connection_pool_size=64, connection_pool_size=64,
connect_timeout=30.0, connect_timeout=30.0,
read_timeout=30.0, read_timeout=30.0,
@@ -75,7 +97,7 @@ async def start_tg_bot():
pool_timeout=30.0, pool_timeout=30.0,
) )
get_updates_request = HTTPXRequest( get_updates_request = HTTPXRequest(
proxy=PROXY_URL, proxy=active_proxy,
connection_pool_size=8, connection_pool_size=8,
connect_timeout=30.0, connect_timeout=30.0,
read_timeout=15.0, read_timeout=15.0,
@@ -160,7 +182,8 @@ async def start_tg_bot_safe():
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
except Exception as e: 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: try:
await stop_tg_bot() await stop_tg_bot()
except Exception: except Exception:
+2
View File
@@ -6,3 +6,5 @@ tortoise-orm>=0.24.0
aerich>=0.8.0 aerich>=0.8.0
asyncpg>=0.30.0 asyncpg>=0.30.0
sse-starlette>=3.4.5 sse-starlette>=3.4.5
socksio>=1.0.0
+3 -3
View File
@@ -199,7 +199,7 @@ async def api_send(request: Request):
if len(text) > 3500: if len(text) > 3500:
return JSONResponse({"ok": False, "error": "消息太长"}, status_code=400) 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) msg = await bot.send_message(chat_id=config.TG_CHAT_ID, text=text)
rec = await Message.create( rec = await Message.create(
msg_id=msg.message_id, 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: if not rec.chat_id or not rec.msg_id:
return JSONResponse({"ok": False, "error": "缺少 TG chat_id/msg_id,无法删除 TG 消息"}, status_code=400) 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_deleted = True
tg_error = None tg_error = None
try: try:
@@ -322,7 +322,7 @@ async def api_unban(user_id: int):
from telegram import Bot from telegram import Bot
from telegram.request import HTTPXRequest 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 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 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"}) await Action.create(action="UNBAN", user_id=user_id, details={"method": "web"})
+30 -9
View File
@@ -122,6 +122,11 @@
.el-card { border: none; border-radius: 12px; } .el-card { border: none; border-radius: 12px; }
.el-button--mini, .el-button--small { border-radius: 8px; } .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; } .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); } } @media (max-width: 900px) { .spam-tooltip { max-width: calc(100vw - 32px); } }
@@ -151,8 +156,10 @@
.panel-actions { gap: 6px; } .panel-actions { gap: 6px; }
.scroll-area { height: 40vh; min-height: 280px; flex: none; } .scroll-area { height: 40vh; min-height: 280px; flex: none; }
.chat-item { padding: 8px; } .chat-item { padding: 8px; }
.chat-item .meta { flex-direction: column; gap: 4px; } .chat-item .meta { flex-direction: column; gap: 5px; }
.chat-item .meta-right { justify-content: flex-start; width: 100%; } .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 .user { max-width: 100%; white-space: normal; word-break: break-all; }
.chat-item .time { white-space: normal; } .chat-item .time { white-space: normal; }
.log-window { height: 34vh; min-height: 250px; flex: none; font-size: 10px; } .log-window { height: 34vh; min-height: 250px; flex: none; font-size: 10px; }
@@ -174,6 +181,9 @@
.log-msg { white-space: normal; } .log-msg { white-space: normal; }
.log-line { align-items: start; } .log-line { align-items: start; }
.fixed-send-box { width: calc(100vw - 12px); } .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; }
} }
</style> </style>
</head> </head>
@@ -217,10 +227,12 @@
<div class="meta"> <div class="meta">
<span class="user">@{{ item.username || item.first_name || 'unknown' }} <small>({{ item.user_id }})</small></span> <span class="user">@{{ item.username || item.first_name || 'unknown' }} <small>({{ item.user_id }})</small></span>
<span class="meta-right"> <span class="meta-right">
<span class="time">#{{ item.msg_id || '-' }} · {{ formatTime(item.time) }}</span> <span class="time"><span class="chat-id-pill">#{{ item.msg_id || '-' }}</span> · {{ formatTime(item.time) }}</span>
<el-tag v-if="item.deleted" size="mini" type="info">手动删除</el-tag> <span class="chat-actions">
<el-button v-else type="text" size="mini" icon="el-icon-delete" :loading="item._deleting" @click="deleteChatMessage(item, false)">删除</el-button> <el-tag v-if="item.deleted" size="mini" type="info">手动删除</el-tag>
<el-button v-if="!item.deleted && item.user_id" type="text" size="mini" icon="el-icon-remove-outline" :loading="item._deleting" @click="deleteChatMessage(item, true)"></el-button> <el-button v-else type="text" size="mini" icon="el-icon-delete" :loading="item._deleting" @click="deleteChatMessage(item, false)"></el-button>
<el-button v-if="!item.deleted && item.user_id" type="text" size="mini" icon="el-icon-remove-outline" :loading="item._deleting" @click="deleteChatMessage(item, true)">删封</el-button>
</span>
</span> </span>
</div> </div>
<div class="text">{{ item.text }}</div> <div class="text">{{ item.text }}</div>
@@ -383,7 +395,10 @@ new Vue({
this.$confirm('确认只重启 Telegram BotWeb 管理页不会重启。', '重启 TG Bot', { this.$confirm('确认只重启 Telegram BotWeb 管理页不会重启。', '重启 TG Bot', {
confirmButtonText: '确认重启', confirmButtonText: '确认重启',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning',
customClass: 'confirm-dialog',
dangerouslyUseHTMLString: false,
distinguishCancelAndClose: true
}).then(async () => { }).then(async () => {
this.restartingBot = true this.restartingBot = true
try { try {
@@ -405,7 +420,10 @@ new Vue({
this.$confirm('确认重启 TG Spam Guard 服务?重启期间 Web 和 Bot 会短暂不可用。', '重启服务', { this.$confirm('确认重启 TG Spam Guard 服务?重启期间 Web 和 Bot 会短暂不可用。', '重启服务', {
confirmButtonText: '确认重启', confirmButtonText: '确认重启',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning',
customClass: 'confirm-dialog',
dangerouslyUseHTMLString: false,
distinguishCancelAndClose: true
}).then(async () => { }).then(async () => {
this.restarting = true this.restarting = true
try { try {
@@ -455,7 +473,10 @@ new Vue({
this.$confirm(banUser ? '先删除 Telegram 群内消息,再封禁该用户,并标记数据库。继续吗?' : '先删除 Telegram 群内消息,再把数据库记录标记为手动删除。继续吗?', banUser ? '删除并封禁' : '删除聊天记录', { this.$confirm(banUser ? '先删除 Telegram 群内消息,再封禁该用户,并标记数据库。继续吗?' : '先删除 Telegram 群内消息,再把数据库记录标记为手动删除。继续吗?', banUser ? '删除并封禁' : '删除聊天记录', {
confirmButtonText: banUser ? '删封' : '删除', confirmButtonText: banUser ? '删封' : '删除',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning',
customClass: 'confirm-dialog',
dangerouslyUseHTMLString: false,
distinguishCancelAndClose: true
}).then(async () => { }).then(async () => {
this.$set(item, '_deleting', true) this.$set(item, '_deleting', true)
try { try {