feat: add telegram proxy fallbacks and mobile dialog polish
This commit is contained in:
@@ -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"))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 Bot?Web 管理页不会重启。', '重启 TG Bot', {
|
this.$confirm('确认只重启 Telegram Bot?Web 管理页不会重启。', '重启 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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user