fix: 检测改三态(🟢通/🟡未知/🔴不通),去掉自动检测删除
This commit is contained in:
57
bot.py
57
bot.py
@@ -15,6 +15,13 @@ SUB_SECRET = os.environ.get('SUB_SECRET', 'changeme')
|
|||||||
SUB_HOST = os.environ.get('SUB_HOST', 'substore.mjjtop.com')
|
SUB_HOST = os.environ.get('SUB_HOST', 'substore.mjjtop.com')
|
||||||
WAITING_ADD = set() # user_ids waiting to add sub
|
WAITING_ADD = set() # user_ids waiting to add sub
|
||||||
|
|
||||||
|
def _status_icon(s):
|
||||||
|
"""节点状态图标:True=🟢 False=🔴 None=🟡"""
|
||||||
|
v = s.get('alive')
|
||||||
|
if v is True: return '🟢'
|
||||||
|
if v is False: return '🔴'
|
||||||
|
return '🟡'
|
||||||
|
|
||||||
def get_default_secret():
|
def get_default_secret():
|
||||||
"""获取默认节点池的 secret,优先用 data.json 里的,没有就用环境变量"""
|
"""获取默认节点池的 secret,优先用 data.json 里的,没有就用环境变量"""
|
||||||
data = load_data()
|
data = load_data()
|
||||||
@@ -190,7 +197,7 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
elif action == 'menu_list':
|
elif action == 'menu_list':
|
||||||
if not data['subs']:
|
if not data['subs']:
|
||||||
return await send_del(q.message, '📭 暂无订阅')
|
return await send_del(q.message, '📭 暂无订阅')
|
||||||
lines = [f"`{i+1}` {'🟢' if s.get('alive',True) else '🔴'} [{s['type']}] {s['name']}"
|
lines = [f"`{i+1}` {_status_icon(s)} [{s['type']}] {s['name']}"
|
||||||
for i, s in enumerate(data['subs'])]
|
for i, s in enumerate(data['subs'])]
|
||||||
await send_del(q.message, '📋 *订阅列表*\n\n' + '\n'.join(lines), parse_mode='Markdown')
|
await send_del(q.message, '📋 *订阅列表*\n\n' + '\n'.join(lines), parse_mode='Markdown')
|
||||||
|
|
||||||
@@ -216,7 +223,7 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
buttons = []
|
buttons = []
|
||||||
for i, s in enumerate(data['subs']):
|
for i, s in enumerate(data['subs']):
|
||||||
if uid != ADMIN_ID and uid != s.get('added_by'): continue
|
if uid != ADMIN_ID and uid != s.get('added_by'): continue
|
||||||
st = '🟢' if s.get('alive', True) else '🔴'
|
st = _status_icon(s)
|
||||||
buttons.append([InlineKeyboardButton(f"{st} [{s['type']}] {s['name']}", callback_data=f'del_{i}')])
|
buttons.append([InlineKeyboardButton(f"{st} [{s['type']}] {s['name']}", callback_data=f'del_{i}')])
|
||||||
if not buttons:
|
if not buttons:
|
||||||
return await send_del(q.message, '没有可删除的订阅')
|
return await send_del(q.message, '没有可删除的订阅')
|
||||||
@@ -236,7 +243,7 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
for s in data['subs']:
|
for s in data['subs']:
|
||||||
ok = await check_node(s['link'], s['type'])
|
ok = await check_node(s['link'], s['type'])
|
||||||
s['alive'] = ok
|
s['alive'] = ok
|
||||||
results.append(f"{'🟢' if ok else '🔴'} [{s['type']}] {s['name']}")
|
results.append(f"{_status_icon(s)} [{s['type']}] {s['name']}")
|
||||||
# 检测所有订阅分组
|
# 检测所有订阅分组
|
||||||
for sg in data.get('sub_groups', []):
|
for sg in data.get('sub_groups', []):
|
||||||
if not sg.get('nodes'): continue
|
if not sg.get('nodes'): continue
|
||||||
@@ -244,7 +251,7 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
for s in sg['nodes']:
|
for s in sg['nodes']:
|
||||||
ok = await check_node(s['link'], s['type'])
|
ok = await check_node(s['link'], s['type'])
|
||||||
s['alive'] = ok
|
s['alive'] = ok
|
||||||
results.append(f"{'🟢' if ok else '🔴'} [{s['type']}] {s['name']}")
|
results.append(f"{_status_icon(s)} [{s['type']}] {s['name']}")
|
||||||
save_data(data)
|
save_data(data)
|
||||||
try: await msg.delete()
|
try: await msg.delete()
|
||||||
except: pass
|
except: pass
|
||||||
@@ -355,7 +362,7 @@ async def cb_getsub(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
for i, s in enumerate(subs):
|
for i, s in enumerate(subs):
|
||||||
gi = alive.index(s)
|
gi = alive.index(s)
|
||||||
buttons.append([InlineKeyboardButton(
|
buttons.append([InlineKeyboardButton(
|
||||||
f"{'🟢' if s.get('alive',True) else '🔴'} {s['name']}", callback_data=f'pick_{gi}')])
|
f"{_status_icon(s)} {s['name']}", callback_data=f'pick_{gi}')])
|
||||||
buttons.append([InlineKeyboardButton(f"📦 全部 ({len(subs)})", callback_data=f'send_{proto}_raw')])
|
buttons.append([InlineKeyboardButton(f"📦 全部 ({len(subs)})", callback_data=f'send_{proto}_raw')])
|
||||||
return await send_del(q.message, '选择节点:', reply_markup=InlineKeyboardMarkup(buttons))
|
return await send_del(q.message, '选择节点:', reply_markup=InlineKeyboardMarkup(buttons))
|
||||||
|
|
||||||
@@ -545,13 +552,16 @@ async def auto_detect(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
# --- node health check ---
|
# --- node health check ---
|
||||||
async def check_node(link, proto):
|
async def check_node(link, proto):
|
||||||
|
"""检测节点存活:TCP 直连,连不通返回 None(未知)"""
|
||||||
try:
|
try:
|
||||||
server, port = parse_sp(link, proto)
|
server, port = parse_sp(link, proto)
|
||||||
if not server: return False
|
if not server: return None
|
||||||
_, w = await asyncio.wait_for(asyncio.open_connection(server, int(port)), timeout=5)
|
_, w = await asyncio.wait_for(asyncio.open_connection(server, int(port)), timeout=5)
|
||||||
w.close(); await w.wait_closed()
|
w.close(); await w.wait_closed()
|
||||||
return True
|
return True
|
||||||
except: return False
|
except:
|
||||||
|
return None # 连不通返回未知,不判定为死
|
||||||
|
|
||||||
|
|
||||||
def parse_sp(link, proto):
|
def parse_sp(link, proto):
|
||||||
try:
|
try:
|
||||||
@@ -738,7 +748,7 @@ async def cb_subgroups(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
else:
|
else:
|
||||||
lines = [f"📁 *{sg['name']}* 节点列表\n"]
|
lines = [f"📁 *{sg['name']}* 节点列表\n"]
|
||||||
for i, s in enumerate(nodes):
|
for i, s in enumerate(nodes):
|
||||||
st = '🟢' if s.get('alive', True) else '🔴'
|
st = _status_icon(s)
|
||||||
lines.append(f"`{i+1}` {st} [{s['type']}] {s['name']}")
|
lines.append(f"`{i+1}` {st} [{s['type']}] {s['name']}")
|
||||||
text = '\n'.join(lines)
|
text = '\n'.join(lines)
|
||||||
buttons = [[InlineKeyboardButton("◀️ 返回", callback_data=f"sg_detail_{gid}")]]
|
buttons = [[InlineKeyboardButton("◀️ 返回", callback_data=f"sg_detail_{gid}")]]
|
||||||
@@ -771,7 +781,7 @@ async def cb_subgroups(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
for s in nodes:
|
for s in nodes:
|
||||||
ok = await check_node(s['link'], s['type'])
|
ok = await check_node(s['link'], s['type'])
|
||||||
s['alive'] = ok
|
s['alive'] = ok
|
||||||
results.append(f"{'🟢' if ok else '🔴'} [{s['type']}] {s['name']}")
|
results.append(f"{_status_icon(s)} [{s['type']}] {s['name']}")
|
||||||
save_data(data)
|
save_data(data)
|
||||||
try: await msg.delete()
|
try: await msg.delete()
|
||||||
except: pass
|
except: pass
|
||||||
@@ -790,7 +800,7 @@ async def cb_subgroups(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
parse_mode='Markdown', reply_markup=InlineKeyboardMarkup(buttons))
|
parse_mode='Markdown', reply_markup=InlineKeyboardMarkup(buttons))
|
||||||
buttons = []
|
buttons = []
|
||||||
for i, s in enumerate(nodes):
|
for i, s in enumerate(nodes):
|
||||||
st = '🟢' if s.get('alive', True) else '🔴'
|
st = _status_icon(s)
|
||||||
buttons.append([InlineKeyboardButton(
|
buttons.append([InlineKeyboardButton(
|
||||||
f"{st} [{s['type']}] {s['name']}", callback_data=f"sg_delnode_{gid}_{i}")])
|
f"{st} [{s['type']}] {s['name']}", callback_data=f"sg_delnode_{gid}_{i}")])
|
||||||
buttons.append([InlineKeyboardButton("🗑 删除所有不可用", callback_data=f"sg_deldown_{gid}")])
|
buttons.append([InlineKeyboardButton("🗑 删除所有不可用", callback_data=f"sg_deldown_{gid}")])
|
||||||
@@ -919,32 +929,6 @@ async def cb_subgroups(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
parse_mode='Markdown', reply_markup=InlineKeyboardMarkup(buttons))
|
parse_mode='Markdown', reply_markup=InlineKeyboardMarkup(buttons))
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
async def auto_cleanup(bot_token):
|
|
||||||
"""Every 6h: check all nodes, update alive status only (no auto-delete)"""
|
|
||||||
await asyncio.sleep(60)
|
|
||||||
while True:
|
|
||||||
data = load_data()
|
|
||||||
changed = False
|
|
||||||
# 默认分组:只更新状态
|
|
||||||
for s in data['subs']:
|
|
||||||
ok = await check_node(s['link'], s['type'])
|
|
||||||
if s.get('alive', True) != ok:
|
|
||||||
s['alive'] = ok
|
|
||||||
changed = True
|
|
||||||
# 订阅分组:只更新状态
|
|
||||||
for sg in data.get('sub_groups', []):
|
|
||||||
for s in sg.get('nodes', []):
|
|
||||||
ok = await check_node(s['link'], s['type'])
|
|
||||||
if s.get('alive', True) != ok:
|
|
||||||
s['alive'] = ok
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
save_data(data)
|
|
||||||
log.info('Auto check: updated node status')
|
|
||||||
await asyncio.sleep(21600)
|
|
||||||
|
|
||||||
def run_cleanup():
|
|
||||||
asyncio.run(auto_cleanup(TOKEN))
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = Application.builder().token(TOKEN).build()
|
app = Application.builder().token(TOKEN).build()
|
||||||
@@ -960,7 +944,6 @@ def main():
|
|||||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_detect))
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_detect))
|
||||||
log.info('Sub Bot starting polling...')
|
log.info('Sub Bot starting polling...')
|
||||||
threading.Thread(target=start_http, daemon=True).start()
|
threading.Thread(target=start_http, daemon=True).start()
|
||||||
threading.Thread(target=run_cleanup, daemon=True).start()
|
|
||||||
app.run_polling(drop_pending_updates=True)
|
app.run_polling(drop_pending_updates=True)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user