Spaces:
Sleeping
Sleeping
| import urllib.parse | |
| import random | |
| import time | |
| import requests as std_requests | |
| from datetime import datetime | |
| import yaml | |
| import os | |
| import re | |
| import threading | |
| from concurrent.futures import ThreadPoolExecutor | |
| CLASH_API_URL = "" | |
| LOCAL_PROXY_URL = "" | |
| ENABLE_NODE_SWITCH = False | |
| POOL_MODE = False | |
| FASTEST_MODE = False | |
| PROXY_GROUP_NAME = "节点选择" | |
| CLASH_SECRET = "" | |
| NODE_BLACKLIST = [] | |
| _IS_IN_DOCKER = os.path.exists('/.dockerenv') | |
| _global_switch_lock = threading.Lock() | |
| _last_switch_time = 0 | |
| CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| BASE_DIR = os.path.dirname(CURRENT_DIR) | |
| def format_docker_url(url: str) -> str: | |
| """智能检测:如果在 Docker 中运行,自动把 127.0.0.1 转为宿主机魔法地址""" | |
| if not url or not isinstance(url, str): | |
| return url | |
| if _IS_IN_DOCKER: | |
| if "127.0.0.1" in url: | |
| return url.replace("127.0.0.1", "host.docker.internal") | |
| if "localhost" in url: | |
| return url.replace("localhost", "host.docker.internal") | |
| return url | |
| def reload_proxy_config(): | |
| global CLASH_API_URL, LOCAL_PROXY_URL, ENABLE_NODE_SWITCH, POOL_MODE, \ | |
| FASTEST_MODE, PROXY_GROUP_NAME, CLASH_SECRET, NODE_BLACKLIST | |
| config_dir = os.path.join(BASE_DIR, "data") | |
| config_path = os.path.join(config_dir, "config.yaml") | |
| if not os.path.exists(config_path): | |
| print(f"[{ts()}] [WARNING] 配置文件 {config_path} 不存在,使用默认代理设置。") | |
| conf_data = {} | |
| else: | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| conf_data = yaml.safe_load(f) or {} | |
| clash_conf = conf_data.get("clash_proxy_pool", {}) | |
| ENABLE_NODE_SWITCH = clash_conf.get("enable", False) | |
| POOL_MODE = clash_conf.get("pool_mode", False) | |
| FASTEST_MODE = clash_conf.get("fastest_mode", False) | |
| CLASH_API_URL = format_docker_url(clash_conf.get("api_url", "http://127.0.0.1:9090")) | |
| LOCAL_PROXY_URL = format_docker_url(clash_conf.get("test_proxy_url", "http://127.0.0.1:7890")) | |
| PROXY_GROUP_NAME = clash_conf.get("group_name", "节点选择") | |
| CLASH_SECRET = clash_conf.get("secret", "") | |
| NODE_BLACKLIST = clash_conf.get("blacklist", ["港", "HK", "台", "TW", "中国", "CN"]) | |
| print(f"[{ts()}] [系统] 代理管理模块配置已同步更新。") | |
| def ts() -> str: | |
| """获取当前时间戳字符串,用于日志""" | |
| return datetime.now().strftime("%H:%M:%S") | |
| def clean_for_log(text: str) -> str: | |
| """用于日志输出:过滤掉字符串中的国旗、飞机、火箭等 Emoji 符号""" | |
| emoji_pattern = re.compile( | |
| r'[\U0001F1E6-\U0001F1FF]' | |
| r'|[\U0001F300-\U0001F6FF]' | |
| r'|[\U0001F900-\U0001F9FF]' | |
| r'|[\U00002600-\U000027BF]' | |
| r'|[\uFE0F]' | |
| ) | |
| return emoji_pattern.sub('', text).strip() | |
| def get_display_name(proxy_url: str) -> str: | |
| """统一日志脱敏:将 URL 转换为 [X号机] 或隐藏域名""" | |
| if not proxy_url: | |
| return "全局单机" | |
| try: | |
| parsed = urllib.parse.urlparse(proxy_url) | |
| if parsed.port and 41000 < parsed.port <= 41050: | |
| return f"{parsed.port - 41000}号机" | |
| return f"端口:{parsed.port}" | |
| except: | |
| return "未知通道" | |
| def get_api_url_for_proxy(proxy_url: str) -> str: | |
| """根据开关决定是独立容器 API,还是使用固定 API""" | |
| if not POOL_MODE or not proxy_url: | |
| return CLASH_API_URL | |
| try: | |
| parsed = urllib.parse.urlparse(proxy_url) | |
| port = parsed.port | |
| if port and 41000 < port <= 41050: | |
| api_port = port + 1000 | |
| return format_docker_url(f"http://{parsed.hostname}:{api_port}") | |
| except Exception: | |
| pass | |
| return CLASH_API_URL | |
| def test_proxy_liveness(proxy_url=None): | |
| """测试当前代理是否可用 (脱敏)""" | |
| raw_url = proxy_url if proxy_url else LOCAL_PROXY_URL | |
| target_proxy = format_docker_url(raw_url) | |
| proxies = {"http": target_proxy, "https": target_proxy} | |
| display_name = get_display_name(proxy_url if proxy_url else LOCAL_PROXY_URL) | |
| try: | |
| res = std_requests.get("https://cloudflare.com/cdn-cgi/trace", proxies=proxies, timeout=5) | |
| if res.status_code == 200: | |
| loc = "UNKNOWN" | |
| for line in res.text.split('\n'): | |
| if line.startswith("loc="): | |
| loc = line.split("=")[1].strip() | |
| blocked_regions = ["CN", "HK"] | |
| if loc in blocked_regions: | |
| print(f"[{ts()}] [代理测活] {display_name} 地区受限 ({loc}),弃用!") | |
| return False | |
| print(f"[{ts()}] [代理测活] {display_name} 成功!地区 ({loc}),延迟: {res.elapsed.total_seconds():.2f}s") | |
| return True | |
| return False | |
| except Exception: | |
| print(f"[{ts()}] [代理测活] {display_name} 链路中断或超时。") | |
| return False | |
| def smart_switch_node(proxy_url=None): | |
| global _last_switch_time | |
| if not ENABLE_NODE_SWITCH: | |
| return True | |
| # 如果是独立代理池模式,互相不影响,不需要锁 | |
| if POOL_MODE and proxy_url: | |
| return _do_smart_switch(proxy_url) | |
| with _global_switch_lock: | |
| if time.time() - _last_switch_time < 10: | |
| print(f"[{ts()}] [代理池] 其他线程刚完成切换,跳过本次请求...") | |
| return True | |
| success = _do_smart_switch(proxy_url) | |
| if success: | |
| _last_switch_time = time.time() | |
| return success | |
| def _do_smart_switch(proxy_url=None): | |
| """智能切换节点并测活的核心逻辑 (脱敏)""" | |
| if not ENABLE_NODE_SWITCH: | |
| return True | |
| current_api_url = get_api_url_for_proxy(proxy_url) | |
| headers = {"Authorization": f"Bearer {CLASH_SECRET}"} if CLASH_SECRET else {} | |
| display_name = get_display_name(proxy_url) | |
| api_display = get_display_name(current_api_url).replace("号机", "号API") | |
| try: | |
| resp = std_requests.get(f"{current_api_url}/proxies", headers=headers, timeout=5) | |
| if resp.status_code != 200: | |
| print(f"[{ts()}] [ERROR] 无法连接 Clash API ({api_display}),请检查容器状态。") | |
| return False | |
| proxies_data = resp.json().get('proxies', {}) | |
| actual_group_name = None | |
| for key in proxies_data.keys(): | |
| if PROXY_GROUP_NAME in key and isinstance(proxies_data[key], dict) and 'all' in proxies_data[key]: | |
| actual_group_name = key | |
| break | |
| if not actual_group_name: | |
| print(f"[{ts()}] [ERROR] {display_name} 找不到策略组关键词 '{PROXY_GROUP_NAME}'") | |
| return False | |
| safe_group_name = urllib.parse.quote(actual_group_name) | |
| all_nodes = proxies_data[actual_group_name].get('all', []) | |
| valid_nodes = [ | |
| n for n in all_nodes | |
| if not any(kw.upper() in n.upper() for kw in NODE_BLACKLIST) | |
| ] | |
| if not valid_nodes: | |
| print(f"[{ts()}] [ERROR] {display_name} 过滤后无可用节点,请检查黑名单。") | |
| return False | |
| if FASTEST_MODE: | |
| print(f"\n[{ts()}] [代理池] {display_name} 开启优选模式,并发测速 {len(valid_nodes)} 个节点...") | |
| session = std_requests.Session() | |
| def trigger_delay(n): | |
| enc_n = urllib.parse.quote(n, safe="") | |
| try: | |
| session.get( | |
| f"{current_api_url}/proxies/{enc_n}/delay?timeout=2000&url=http://www.gstatic.com/generate_204", | |
| headers=headers, timeout=2.5 | |
| ) | |
| except: | |
| pass | |
| thread_count = min(10, len(valid_nodes)) | |
| with ThreadPoolExecutor(max_workers=thread_count) as executor: | |
| executor.map(trigger_delay, valid_nodes) | |
| session.close() | |
| time.sleep(1.5) | |
| try: | |
| resp2 = std_requests.get(f"{current_api_url}/proxies", headers=headers, timeout=5) | |
| if resp2.status_code == 200: | |
| p_data = resp2.json().get('proxies', {}) | |
| best_node = None | |
| min_delay = float('inf') | |
| for n in valid_nodes: | |
| history = p_data.get(n, {}).get("history", []) | |
| if history: | |
| delay = history[-1].get("delay", 0) | |
| if 0 < delay < min_delay: | |
| min_delay = delay | |
| best_node = n | |
| if best_node: | |
| print(f"[{ts()}] [代理池] {display_name} 测速完成,最快节点: [{clean_for_log(best_node)}] ({min_delay}ms)") | |
| switch_resp = std_requests.put( | |
| f"{current_api_url}/proxies/{safe_group_name}", | |
| headers=headers, json={"name": best_node}, timeout=5 | |
| ) | |
| if switch_resp.status_code == 204: | |
| time.sleep(1) | |
| if test_proxy_liveness(proxy_url): | |
| return True | |
| print(f"[{ts()}] [代理池] {display_name} 最快节点测活失败,回退到随机抽卡模式...") | |
| else: | |
| print(f"[{ts()}] [代理池] {display_name} 所有节点均超时,回退到随机抽卡模式...") | |
| except Exception as e: | |
| print(f"[{ts()}] [代理池] {display_name} 优选模式异常: {e},回退到随机抽卡模式...") | |
| max_retries = 10 | |
| for i in range(1, max_retries + 1): | |
| selected_node = random.choice(valid_nodes) | |
| print(f"\n[{ts()}] [代理池] {display_name} 尝试切换节点: [{clean_for_log(selected_node)}] ({i}/{max_retries})") | |
| switch_resp = std_requests.put( | |
| f"{current_api_url}/proxies/{safe_group_name}", | |
| headers=headers, json={"name": selected_node}, timeout=5 | |
| ) | |
| if switch_resp.status_code == 204: | |
| time.sleep(1.5) | |
| if test_proxy_liveness(proxy_url): | |
| return True | |
| print(f"[{ts()}] [代理池] {display_name} 测活失败,重新抽卡...") | |
| else: | |
| print(f"[{ts()}] [代理池] {display_name} 指令下发失败 (HTTP {switch_resp.status_code})。") | |
| print(f"\n[{ts()}] [代理池] {display_name} 连续 10 次抽卡均不可用!") | |
| return False | |
| except Exception as e: | |
| print(f"[{ts()}] [ERROR] {display_name} 切换节点异常: {e}") | |
| return False | |
| reload_proxy_config() |