Delete .gitea/workflows/update_clash.yaml
This commit is contained in:
@@ -1,550 +0,0 @@
|
|||||||
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://git.darkmode.duckdns.org/sirmrak/Goida26-Clash/src/branch/main/servers.json"
|
|
||||||
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)
|
|
||||||
Reference in New Issue
Block a user