diff --git a/bot.py b/bot.py index e8c915f..c765a68 100644 --- a/bot.py +++ b/bot.py @@ -15,6 +15,13 @@ SUB_SECRET = os.environ.get('SUB_SECRET', 'changeme') SUB_HOST = os.environ.get('SUB_HOST', 'substore.mjjtop.com') 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(): """获取默认节点池的 secret,优先用 data.json 里的,没有就用环境变量""" data = load_data() @@ -190,7 +197,7 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): elif action == 'menu_list': if not data['subs']: 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'])] 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 = [] for i, s in enumerate(data['subs']): 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}')]) if not buttons: 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']: ok = await check_node(s['link'], s['type']) 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', []): 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']: ok = await check_node(s['link'], s['type']) 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) try: await msg.delete() except: pass @@ -355,7 +362,7 @@ async def cb_getsub(update: Update, context: ContextTypes.DEFAULT_TYPE): for i, s in enumerate(subs): gi = alive.index(s) 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')]) 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 --- async def check_node(link, proto): + """检测节点存活:TCP 直连,连不通返回 None(未知)""" try: 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.close(); await w.wait_closed() return True - except: return False + except: + return None # 连不通返回未知,不判定为死 + def parse_sp(link, proto): try: @@ -738,7 +748,7 @@ async def cb_subgroups(update: Update, context: ContextTypes.DEFAULT_TYPE): else: lines = [f"📁 *{sg['name']}* 节点列表\n"] 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']}") text = '\n'.join(lines) 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: ok = await check_node(s['link'], s['type']) 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) try: await msg.delete() except: pass @@ -790,7 +800,7 @@ async def cb_subgroups(update: Update, context: ContextTypes.DEFAULT_TYPE): parse_mode='Markdown', reply_markup=InlineKeyboardMarkup(buttons)) buttons = [] for i, s in enumerate(nodes): - st = '🟢' if s.get('alive', True) else '🔴' + st = _status_icon(s) buttons.append([InlineKeyboardButton( f"{st} [{s['type']}] {s['name']}", callback_data=f"sg_delnode_{gid}_{i}")]) 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)) 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(): app = Application.builder().token(TOKEN).build() @@ -960,7 +944,6 @@ def main(): app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_detect)) log.info('Sub Bot starting polling...') threading.Thread(target=start_http, daemon=True).start() - threading.Thread(target=run_cleanup, daemon=True).start() app.run_polling(drop_pending_updates=True) if __name__ == '__main__':