name: Update Clash Config on: schedule: - cron: '*/30 * * * *' workflow_dispatch: permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: pip install requests PyYAML - name: Run Updater run: | cat > updater.py << 'EOF' import requests import yaml import socket import concurrent.futures from urllib.parse import urlparse, parse_qs import base64 import json import re import sys from datetime import datetime, timezone SOURCE_URL = "https://system.mcufa.pro/sub/QA_mfZo9obfBEJohzfpRTFBxQ" OUTPUT_FILE = "clash.yaml" TIMEOUT = 3 MAX_WORKERS = 50 def parse_vmess(uri): try: b64_part = uri[8:].strip() padding = 4 - (len(b64_part) % 4) if padding != 4: b64_part += '=' * padding decoded = base64.b64decode(b64_part).decode('utf-8') vmess_json = json.loads(decoded) proxy = { "name": vmess_json.get("ps", "VMess"), "type": "vmess", "server": vmess_json.get("add", ""), "port": int(vmess_json.get("port", 0)), "uuid": vmess_json.get("id", ""), "alterId": int(vmess_json.get("aid", 0)), "cipher": vmess_json.get("scy", "auto"), "network": vmess_json.get("net", "tcp"), "udp": True } net = vmess_json.get("net", "tcp") if net == "ws": proxy["ws-opts"] = { "path": vmess_json.get("path", "/"), "headers": {"Host": vmess_json.get("host", vmess_json.get("add", ""))} } elif net == "grpc": proxy["grpc-opts"] = {"grpc-service-name": vmess_json.get("path", "")} if vmess_json.get("tls") == "tls": proxy["tls"] = True proxy["skip-cert-verify"] = True proxy["sni"] = vmess_json.get("sni", vmess_json.get("add", "")) return proxy except: return None def parse_trojan(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Trojan" proxy = { "name": name, "type": "trojan", "server": parsed.hostname, "port": parsed.port or 443, "password": parsed.username or "", "udp": True, "skip-cert-verify": True } if parsed.query: query = parse_qs(parsed.query) if "sni" in query: proxy["sni"] = query["sni"][0] if "type" in query: net_type = query["type"][0] if net_type == "ws": proxy["network"] = "ws" proxy["ws-opts"] = { "path": query.get("path", ["/"])[0], "headers": {"Host": query.get("host", [parsed.hostname])[0]} } elif net_type == "grpc": proxy["network"] = "grpc" proxy["grpc-opts"] = {"grpc-service-name": query.get("serviceName", [""])[0]} return proxy except: return None def parse_shadowsocks(uri): try: if "#" in uri: uri_part, name_part = uri.split("#", 1) name = requests.utils.unquote(name_part) else: uri_part = uri name = "Shadowsocks" uri_part = uri_part[5:] if '@' in uri_part: userinfo, server_port = uri_part.split('@', 1) try: padding = 4 - (len(userinfo) % 4) if padding != 4: userinfo += '=' * padding decoded = base64.b64decode(userinfo).decode('utf-8') cipher, password = decoded.split(':', 1) except: if ':' in userinfo: cipher, password = userinfo.split(':', 1) else: cipher, password = "aes-256-gcm", userinfo else: return None server, port = server_port.rsplit(':', 1) proxy = { "name": name, "type": "ss", "server": server, "port": int(port), "cipher": cipher, "password": password, "udp": True } return proxy except: return None def parse_vless(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "VLESS" proxy = { "name": name, "type": "vless", "server": parsed.hostname, "port": parsed.port or 443, "uuid": parsed.username or "", "udp": True, "skip-cert-verify": True, "encryption": "none" } if parsed.query: query = parse_qs(parsed.query) if "security" in query and query["security"][0] == "tls": proxy["tls"] = True if "sni" in query: proxy["sni"] = query["sni"][0] if "type" in query: net_type = query["type"][0] if net_type == "ws": proxy["network"] = "ws" proxy["ws-opts"] = { "path": query.get("path", ["/"])[0], "headers": {"Host": query.get("host", [parsed.hostname])[0]} } elif net_type == "grpc": proxy["network"] = "grpc" proxy["grpc-opts"] = {"grpc-service-name": query.get("serviceName", [""])[0]} return proxy except: return None def parse_socks5(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "SOCKS5" proxy = { "name": name, "type": "socks5", "server": parsed.hostname, "port": parsed.port or 1080, "udp": True } if parsed.username and parsed.password: proxy["username"] = parsed.username proxy["password"] = parsed.password return proxy except: return None def parse_http(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "HTTP" proxy = { "name": name, "type": "http", "server": parsed.hostname, "port": parsed.port or 8080, } if parsed.username and parsed.password: proxy["username"] = parsed.username proxy["password"] = parsed.password if parsed.scheme == "https": proxy["tls"] = True proxy["skip-cert-verify"] = True return proxy except: return None def parse_hysteria(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Hysteria" proxy = { "name": name, "type": "hysteria", "server": parsed.hostname, "port": parsed.port or 443, "up": 100, "down": 100, "skip-cert-verify": True } if parsed.query: query = parse_qs(parsed.query) if "auth" in query: proxy["auth-str"] = query["auth"][0] if "peer" in query: proxy["sni"] = query["peer"][0] if "upmbps" in query: proxy["up"] = int(query["upmbps"][0]) if "downmbps" in query: proxy["down"] = int(query["downmbps"][0]) return proxy except: return None def parse_hysteria2(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Hysteria2" password = parsed.username or "" proxy = { "name": name, "type": "hysteria2", "server": parsed.hostname, "port": parsed.port or 443, "password": password, "skip-cert-verify": True } if parsed.query: query = parse_qs(parsed.query) if "auth" in query and not password: proxy["password"] = query["auth"][0] if "sni" in query: proxy["sni"] = query["sni"][0] return proxy except: return None def parse_tuic(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Tuic" uuid = "" password = "" if parsed.username and ':' in parsed.username: uuid, password = parsed.username.split(':', 1) else: uuid = parsed.username or "" proxy = { "name": name, "type": "tuic", "server": parsed.hostname, "port": parsed.port or 443, "uuid": uuid, "password": password, "udp": True, "skip-cert-verify": True } if parsed.query: query = parse_qs(parsed.query) if "sni" in query: proxy["sni"] = query["sni"][0] return proxy except: return None def parse_wireguard(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "WireGuard" proxy = { "name": name, "type": "wireguard", "server": parsed.hostname, "port": parsed.port or 51820, "private-key": parsed.username or "", "udp": True } if parsed.query: query = parse_qs(parsed.query) if "publicKey" in query: proxy["public-key"] = query["publicKey"][0] if "address" in query: addresses = query["address"][0].split(',') proxy["ip"] = addresses[0] if addresses else None if "mtu" in query: proxy["mtu"] = int(query["mtu"][0]) return proxy except: return None def parse_shadowtls(uri): try: parsed = urlparse(uri) name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "ShadowTLS" proxy = { "name": name, "type": "shadowtls", "server": parsed.hostname, "port": parsed.port or 443, "version": 3, "skip-cert-verify": True } if parsed.query: query = parse_qs(parsed.query) if "password" in query: proxy["password"] = query["password"][0] if "sni" in query: proxy["sni"] = query["sni"][0] return proxy except: return None def parse_proxies_from_text(text): proxies = [] lines = text.strip().split('\n') parsers = { 'vmess://': parse_vmess, 'trojan://': parse_trojan, 'ss://': parse_shadowsocks, 'vless://': parse_vless, 'socks5://': parse_socks5, 'socks://': parse_socks5, 'http://': parse_http, 'https://': parse_http, 'hysteria://': parse_hysteria, 'hysteria2://': parse_hysteria2, 'hy2://': parse_hysteria2, 'tuic://': parse_tuic, 'wireguard://': parse_wireguard, 'wg://': parse_wireguard, 'shadowtls://': parse_shadowtls, } for line in lines: line = line.strip() if not line or line.startswith('#'): continue for prefix, parser in parsers.items(): if line.startswith(prefix): proxy = parser(line) if proxy: proxies.append(proxy) break return proxies def fix_duplicate_names(proxies): name_counts = {} fixed_proxies = [] for proxy in proxies: original_name = proxy["name"] name = original_name if name in name_counts: name_counts[name] += 1 new_name = f"{name} ({name_counts[name]})" while new_name in name_counts: name_counts[name] += 1 new_name = f"{name} ({name_counts[name]})" proxy["name"] = new_name name = new_name else: name_counts[name] = 1 fixed_proxies.append(proxy) return fixed_proxies def check_tcp_port(proxy): try: if proxy.get("type") == "wireguard": return proxy server = proxy.get("server") port = proxy.get("port") if not server or not port: return None sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(TIMEOUT) result = sock.connect_ex((server, port)) sock.close() if result == 0: return proxy else: return None except: return None def filter_alive_proxies(proxies): alive = [] print(f"🔄 Проверяем {len(proxies)} серверов...") with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = {executor.submit(check_tcp_port, p): p for p in proxies} for future in concurrent.futures.as_completed(futures): result = future.result() if result: alive.append(result) print(f"✅ Найдено живых: {len(alive)} из {len(proxies)}") return alive def generate_clash_config(proxies, total_parsed, alive_count): config = { "proxies": proxies, "proxy-groups": [ { "name": "🚀 Авто-выбор", "type": "url-test", "proxies": [p["name"] for p in proxies], "url": "http://www.gstatic.com/generate_204", "interval": 300 }, { "name": "🌍 Прокси", "type": "select", "proxies": ["🚀 Авто-выбор"] + [p["name"] for p in proxies] } ], "rules": [ "MATCH,🌍 Прокси" ] } return config def main(): print("🚀 Загрузка списка прокси...") try: response = requests.get(SOURCE_URL, timeout=30) response.raise_for_status() content = response.text except Exception as e: print(f"❌ Ошибка загрузки TXT: {e}") sys.exit(1) print("🔍 Парсинг всех форматов...") all_proxies = parse_proxies_from_text(content) total_parsed = len(all_proxies) print(f"📦 Распознано прокси: {total_parsed}") print("🩺 Проверка доступности...") alive_proxies = filter_alive_proxies(all_proxies) alive_count = len(alive_proxies) print("🔧 Исправление дубликатов имён...") fixed_proxies = fix_duplicate_names(alive_proxies) print("📝 Генерация YAML...") config = generate_clash_config(fixed_proxies, total_parsed, alive_count) # Сохраняем с мета-комментарием в начале файла now_utc = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC') header = f"# Обновлено: {now_utc}\n# Всего в источнике: {total_parsed}\n# Живых серверов: {alive_count}\n# Источник: 26.txt\n\n" with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: f.write(header) yaml.dump(config, f, allow_unicode=True, default_flow_style=False, sort_keys=False) print(f"✅ Готово! Сохранено в {OUTPUT_FILE}") if __name__ == "__main__": main() EOF python updater.py - name: Commit and Push changes run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git add clash.yaml # Коммитим только если файл реально изменился git diff --quiet && git diff --staged --quiet || (git commit -m "🔄 Auto-update config [skip ci]" && git push)