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 .