Published 3 months ago
TEXT
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
代理订阅自动更新脚本
用于从多个源获取配置并转换为sing-box格式
"""
import json
import requests
import yaml
import re
from typing import List, Dict, Any
from datetime import datetime
import logging
# ========== 配置区域 ==========
# 输出文件路径(修改为您的HTTP服务器目录)
OUTPUT_FILE = r"C:\Users\c\Videos\ip\subscription.txt"
# 日志文件路径(可选)
LOG_FILE = r"C:\Users\c\Videos\ip\update_log.txt"
# 请求超时时间(秒)
REQUEST_TIMEOUT = 30
# 配置链接列表
CONFIG_URLS = {
'clash': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/clash.meta2/1/config.yaml",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/clash.meta2/2/config.yaml",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/clash.meta2/3/config.yaml",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/clash.meta2/4/config.yaml",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/clash.meta2/5/config.yaml",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/clash.meta2/6/config.yaml",
],
'hysteria': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria/1/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria/2/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria/3/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria/4/config.json",
],
'hysteria2': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria2/1/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria2/2/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria2/3/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/hysteria2/4/config.json",
],
'xray': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/xray/1/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/xray/2/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/xray/3/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/xray/4/config.json",
],
'naiveproxy': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/naiveproxy/1/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/naiveproxy/2/config.json",
],
'singbox': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/singbox/1/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/singbox/2/config.json",
],
'shadowquic': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/shadowquic/1/client.yaml",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/shadowquic/2/client.yaml",
],
'mieru': [
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/mieru/1/config.json",
"https://www.gitlabip.xyz/Alvin9999/PAC/refs/heads/master/backup/img/1/2/ip/mieru/2/config.json",
],
}
# ========== 日志配置 ==========
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(LOG_FILE, encoding='utf-8'),
logging.StreamHandler()
]
)
# ========== 转换函数 ==========
def parse_server_port(server_str: str) -> tuple:
"""解析服务器地址和端口"""
if ':' in server_str:
parts = server_str.rsplit(':', 1)
server = parts[0]
port_str = parts[1]
# 处理端口范围,取第一个端口
if ',' in port_str:
port_str = port_str.split(',')[0]
if '-' in port_str:
port_str = port_str.split('-')[0]
return server, int(port_str)
return server_str, 0
def convert_hysteria_to_singbox(config: Dict, index: int, protocol: str = "hysteria") -> Dict:
"""转换Hysteria配置为sing-box格式"""
try:
server, port = parse_server_port(config.get('server', ''))
outbound = {
"type": protocol,
"tag": f"{protocol}-{index}",
"server": server,
"server_port": port,
"up_mbps": config.get('up_mbps', 11),
"down_mbps": config.get('down_mbps', 55),
"tls": {
"enabled": True,
"server_name": config.get('server_name', 'apple.com'),
"insecure": config.get('insecure', True),
"alpn": config.get('alpn', 'h3')
}
}
if protocol == "hysteria":
outbound["auth_str"] = config.get('auth_str', 'dongtaiwang.com')
return outbound
except Exception as e:
logging.error(f"转换Hysteria配置失败: {e}")
return None
def convert_hysteria2_to_singbox(config: Dict, index: int) -> Dict:
"""转换Hysteria2配置为sing-box格式"""
try:
server, port = parse_server_port(config.get('server', ''))
# 从bandwidth字段提取速率
bandwidth = config.get('bandwidth', {})
up_mbps = 11
down_mbps = 55
if isinstance(bandwidth, dict):
up_str = bandwidth.get('up', '11 mbps')
down_str = bandwidth.get('down', '55 mbps')
up_mbps = int(re.search(r'\d+', up_str).group()) if re.search(r'\d+', up_str) else 11
down_mbps = int(re.search(r'\d+', down_str).group()) if re.search(r'\d+', down_str) else 55
tls_config = config.get('tls', {})
outbound = {
"type": "hysteria2",
"tag": f"hysteria2-{index}",
"server": server,
"server_port": port,
"up_mbps": up_mbps,
"down_mbps": down_mbps,
"password": config.get('auth', 'dongtaiwang.com'),
"tls": {
"enabled": True,
"server_name": tls_config.get('sni', 'apple.com'),
"insecure": tls_config.get('insecure', True),
"alpn": "h3"
}
}
return outbound
except Exception as e:
logging.error(f"转换Hysteria2配置失败: {e}")
return None
def convert_xray_vless_to_singbox(config: Dict, index: int) -> Dict:
"""转换Xray VLESS配置为sing-box格式"""
try:
outbounds = config.get('outbounds', [])
proxy_outbound = None
for outbound in outbounds:
if outbound.get('protocol') == 'vless' and outbound.get('tag') == 'proxy':
proxy_outbound = outbound
break
if not proxy_outbound:
return None
vnext = proxy_outbound.get('settings', {}).get('vnext', [])
if not vnext:
return None
server_info = vnext[0]
users = server_info.get('users', [])
if not users:
return None
user = users[0]
stream_settings = proxy_outbound.get('streamSettings', {})
reality_settings = stream_settings.get('realitySettings', {})
outbound = {
"type": "vless",
"tag": f"xray-{index}",
"server": server_info.get('address'),
"server_port": server_info.get('port'),
"uuid": user.get('id'),
"packet_encoding": "xudp",
"tls": {
"enabled": True,
"server_name": reality_settings.get('serverName', 'itunes.apple.com'),
"utls": {
"enabled": True,
"fingerprint": reality_settings.get('fingerprint', 'chrome')
},
"reality": {
"enabled": True,
"public_key": reality_settings.get('publicKey', ''),
"short_id": reality_settings.get('shortId', '')
}
},
"multiplex": {
"enabled": True,
"protocol": "h2mux",
"max_connections": 1,
"min_streams": 4,
"padding": True,
"brutal": {
"enabled": True,
"up_mbps": 50,
"down_mbps": 100
}
}
}
return outbound
except Exception as e:
logging.error(f"转换Xray配置失败: {e}")
return None
def convert_singbox_vless_to_singbox(config: Dict, index: int) -> Dict:
"""转换sing-box VLESS配置为标准格式"""
try:
outbounds = config.get('outbounds', [])
if not outbounds:
return None
vless_outbound = outbounds[0]
# 清理tag,只保留关键信息
tag = f"singbox-{index}"
outbound = {
"type": vless_outbound.get('type', 'vless'),
"tag": tag,
"server": vless_outbound.get('server'),
"server_port": vless_outbound.get('server_port'),
"uuid": vless_outbound.get('uuid'),
"packet_encoding": vless_outbound.get('packet_encoding', 'xudp'),
"tls": vless_outbound.get('tls', {}),
"multiplex": vless_outbound.get('multiplex', {})
}
return outbound
except Exception as e:
logging.error(f"转换sing-box配置失败: {e}")
return None
def convert_clash_to_singbox(config: Dict, index: int) -> List[Dict]:
"""转换Clash配置为sing-box格式(可能有多个节点)"""
try:
outbounds = []
proxies = config.get('proxies', [])
for i, proxy in enumerate(proxies):
proxy_type = proxy.get('type', '').lower()
if proxy_type == 'hysteria':
outbound = {
"type": "hysteria",
"tag": f"clash-{index}-{i+1}",
"server": proxy.get('server'),
"server_port": proxy.get('port'),
"up_mbps": proxy.get('up', 11),
"down_mbps": proxy.get('down', 55),
"auth_str": proxy.get('auth_str', proxy.get('auth', 'dongtaiwang.com')),
"tls": {
"enabled": True,
"server_name": proxy.get('sni', 'apple.com'),
"insecure": proxy.get('skip-cert-verify', True),
"alpn": proxy.get('alpn', ['h3'])[0] if isinstance(proxy.get('alpn'), list) else proxy.get('alpn', 'h3')
}
}
outbounds.append(outbound)
elif proxy_type == 'hysteria2':
outbound = {
"type": "hysteria2",
"tag": f"clash-{index}-{i+1}",
"server": proxy.get('server'),
"server_port": proxy.get('port'),
"up_mbps": proxy.get('up', 11),
"down_mbps": proxy.get('down', 55),
"password": proxy.get('password', 'dongtaiwang.com'),
"tls": {
"enabled": True,
"server_name": proxy.get('sni', 'apple.com'),
"insecure": proxy.get('skip-cert-verify', True),
"alpn": "h3"
}
}
outbounds.append(outbound)
elif proxy_type in ['vless', 'vmess']:
outbound = {
"type": proxy_type,
"tag": f"clash-{index}-{i+1}",
"server": proxy.get('server'),
"server_port": proxy.get('port'),
}
if proxy_type == 'vless':
outbound["uuid"] = proxy.get('uuid')
outbound["packet_encoding"] = "xudp"
else:
outbound["uuid"] = proxy.get('uuid')
outbound["security"] = proxy.get('cipher', 'auto')
# 处理TLS配置
if proxy.get('tls') or proxy.get('reality-opts'):
tls_config = {
"enabled": True,
"server_name": proxy.get('servername', proxy.get('sni', 'apple.com')),
}
if proxy.get('reality-opts'):
reality = proxy.get('reality-opts', {})
tls_config["utls"] = {
"enabled": True,
"fingerprint": proxy.get('client-fingerprint', 'chrome')
}
tls_config["reality"] = {
"enabled": True,
"public_key": reality.get('public-key', ''),
"short_id": reality.get('short-id', '')
}
outbound["tls"] = tls_config
outbounds.append(outbound)
elif proxy_type == 'tuic':
outbound = {
"type": "tuic",
"tag": f"clash-{index}-{i+1}",
"server": proxy.get('server'),
"server_port": proxy.get('port'),
"uuid": proxy.get('uuid'),
"password": proxy.get('password', 'dongtaiwang.com'),
"congestion_control": proxy.get('congestion-controller', 'bbr'),
"udp_relay_mode": "native",
"zero_rtt_handshake": True,
"tls": {
"enabled": True,
"server_name": proxy.get('sni', 'apple.com'),
"insecure": proxy.get('skip-cert-verify', True),
"alpn": proxy.get('alpn', ['h3'])
}
}
outbounds.append(outbound)
return outbounds
except Exception as e:
logging.error(f"转换Clash配置失败: {e}")
return []
# ========== 获取配置函数 ==========
def fetch_config(url: str, config_type: str) -> Any:
"""从URL获取配置内容"""
try:
logging.info(f"正在获取配置: {url}")
response = requests.get(url, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
if config_type in ['clash', 'shadowquic']:
# YAML格式
return yaml.safe_load(response.text)
else:
# JSON格式
return json.loads(response.text)
except requests.exceptions.RequestException as e:
logging.error(f"获取配置失败 {url}: {e}")
return None
except (json.JSONDecodeError, yaml.YAMLError) as e:
logging.error(f"解析配置失败 {url}: {e}")
return None
# ========== 主处理函数 ==========
def generate_subscription():
"""生成订阅配置"""
all_outbounds = []
outbound_tags = []
# 处理Clash配置
for i, url in enumerate(CONFIG_URLS['clash'], 1):
config = fetch_config(url, 'clash')
if config:
outbounds = convert_clash_to_singbox(config, i)
for outbound in outbounds:
if outbound:
all_outbounds.append(outbound)
outbound_tags.append(outbound['tag'])
# 处理Hysteria配置
for i, url in enumerate(CONFIG_URLS['hysteria'], 1):
config = fetch_config(url, 'hysteria')
if config:
outbound = convert_hysteria_to_singbox(config, i, "hysteria")
if outbound:
all_outbounds.append(outbound)
outbound_tags.append(outbound['tag'])
# 处理Hysteria2配置
for i, url in enumerate(CONFIG_URLS['hysteria2'], 1):
config = fetch_config(url, 'hysteria2')
if config:
outbound = convert_hysteria2_to_singbox(config, i)
if outbound:
all_outbounds.append(outbound)
outbound_tags.append(outbound['tag'])
# 处理Xray配置
for i, url in enumerate(CONFIG_URLS['xray'], 1):
config = fetch_config(url, 'xray')
if config:
outbound = convert_xray_vless_to_singbox(config, i)
if outbound:
all_outbounds.append(outbound)
outbound_tags.append(outbound['tag'])
# 处理sing-box配置
for i, url in enumerate(CONFIG_URLS['singbox'], 1):
config = fetch_config(url, 'singbox')
if config:
outbound = convert_singbox_vless_to_singbox(config, i)
if outbound:
all_outbounds.append(outbound)
outbound_tags.append(outbound['tag'])
# 构建完整配置
final_config = {
"log": {
"level": "info",
"output": "box.log",
"timestamp": True
},
"dns": {
"servers": [
{
"tag": "dns-remote",
"address": "udp://101.101.101.101",
"address_resolver": "dns-direct"
},
{
"tag": "dns-trick-direct",
"address": "https://sky.rethinkdns.com/",
"detour": "direct-fragment"
},
{
"tag": "dns-direct",
"address": "223.5.5.5",
"address_resolver": "dns-local",
"detour": "direct"
},
{
"tag": "dns-local",
"address": "local",
"detour": "direct"
},
{
"tag": "dns-block",
"address": "rcode://success"
}
],
"rules": [
{
"domain": "cp.cloudflare.com",
"server": "dns-remote",
"rewrite_ttl": 3000
},
{
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"server": "dns-direct"
}
],
"final": "dns-remote",
"static_ips": {
"sky.rethinkdns.com": [
"104.17.148.22",
"104.17.147.22",
"104.18.1.48",
"104.18.0.48",
"2606:4700::6812:30",
"2606:4700::6812:130"
]
},
"independent_cache": True
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"mtu": 9000,
"inet4_address": "172.19.0.1/28",
"inet6_address": "fdfe:dcba:9876::1/126",
"auto_route": True,
"strict_route": True,
"endpoint_independent_nat": True,
"stack": "mixed",
"sniff": True,
"sniff_override_destination": True,
"domain_strategy": "prefer_ipv4"
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": 12334,
"sniff": True,
"sniff_override_destination": True,
"domain_strategy": "prefer_ipv4"
},
{
"type": "direct",
"tag": "dns-in",
"listen": "127.0.0.1",
"listen_port": 16450
}
],
"outbounds": [
{
"type": "selector",
"tag": "select",
"outbounds": ["auto"] + outbound_tags,
"default": "auto"
},
{
"type": "urltest",
"tag": "auto",
"outbounds": outbound_tags,
"url": "http://connectivitycheck.gstatic.com/generate_204",
"interval": "10m0s",
"idle_timeout": "1h40m0s"
}
] + all_outbounds + [
{
"type": "dns",
"tag": "dns-out"
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "direct",
"tag": "direct-fragment",
"tls_fragment": {
"enabled": True,
"size": "10-30",
"sleep": "2-8"
}
},
{
"type": "direct",
"tag": "bypass"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"rules": [
{
"rule_set": [
"geosite-ads",
"geosite-malware",
"geosite-phishing",
"geosite-cryptominers",
"geoip-malware",
"geoip-phishing"
],
"outbound": "block"
},
{
"rule_set": [
"geoip-cn",
"geosite-cn"
],
"outbound": "direct"
},
{
"inbound": "dns-in",
"outbound": "dns-out"
},
{
"port": 53,
"outbound": "dns-out"
},
{
"clash_mode": "Direct",
"outbound": "direct"
},
{
"clash_mode": "Global",
"outbound": "select"
},
{
"geoip": "private",
"outbound": "bypass"
}
],
"rule_set": [
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-cn.srs",
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-cn.srs",
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geosite-ads",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/block/geosite-category-ads-all.srs",
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geosite-malware",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/block/geosite-malware.srs",
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geosite-phishing",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/block/geosite-phishing.srs",
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geosite-cryptominers",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/block/geosite-cryptominers.srs",
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geoip-phishing",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/block/geoip-phishing.srs",
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geoip-malware",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/block/geoip-malware.srs",
"update_interval": "120h0m0s"
}
],
"final": "select",
"auto_detect_interface": True,
"override_android_vpn": True
},
"experimental": {
"cache_file": {
"enabled": True,
"path": "clash.db"
},
"clash_api": {
"external_controller": "127.0.0.1:16756",
"secret": "sc4CbRlbgwm24zg2"
}
}
}
return final_config
# ========== 主函数 ==========
def main():
"""主函数"""
try:
logging.info("=" * 60)
logging.info("开始更新订阅配置")
logging.info(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 生成配置
config = generate_subscription()
# 保存配置
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
logging.info(f"配置已保存到: {OUTPUT_FILE}")
logging.info(f"共获取 {len(config['outbounds']) - 5} 个节点") # 减去固定的5个outbound
logging.info("订阅更新完成!")
logging.info("=" * 60)
return True
except Exception as e:
logging.error(f"更新订阅失败: {e}")
import traceback
logging.error(traceback.format_exc())
return False
if __name__ == "__main__":
success = main()
exit(0 if success else 1)
Edited on Jul 03, 2026 By sytbbt .