sing-box-subscribe地址
sing-box-subscribe是根据配置模板生成 sing-box 使用的 config.json,主要用于将机场订阅节点添加到 config.json之中。
现在将它改成在docker容器中定时自动运行main.py来生成sing-box的config.json配置文件。
先将项目目录图片和如下内容丢给ai自动生成代码,然后再根据ai生成的代码再自行修改。
我现在的目录结构如图,帮我写一个在 docker 中运行 pyhon 命令的镜像,主要是运行 main.py,把 parsers、config_template 文件夹和 main.py 文件复制到容器里,在对应的映射卷中只有运行 main.py 时自动生成的 config.json 文件。main.py 在运行过程中会要求从终端输入一个数字,这个数字写进容器的环境变量中,main.py 从环境变量中读取。要能在容器的环境变量 CRON_TIME 中指定自动执行 main.py 的时间,如:CRON_TIME=25 20 * * *。所有依赖文件如 parsers 文件夹,config_template 文件夹、main.py、providers.json 全部打包到镜像。帮我做一个镜像。
项目文件清单(本地目录结构) 生成singbox-subscribe镜像的文件目录如下:
./项目根目录 ├── Dockerfile ├── entrypoint.sh # 容器启动入口脚本 ├── requirements.txt # Python依赖 ├── main.py # 运行的主程序 ├── providers.json # 内置默认配置打包进镜像 ├── docker-compose.yml # 安装镜像 ├── parsers/ # 完整解析器文件夹 │ ├── anytls.py │ ├── app.py │ ├── tool.py │ ├── clash2base64.py │ ├── http.py │ ├── https.py │ ├── hysteria.py │ ├── hysteria2.py │ ├── socks.py │ ├── ss.py │ ├── ssr.py │ ├── tool.py │ ├── trojan.py │ ├── tuic.py │ ├── vless.py │ ├── vmess.py │ ├── wg.py │ └── gh_proxy_helper.py └── config_template/ # 模板文件夹,内置到镜像 ├── sb-config-1.12.json ├── sb-config-1.14.json └── ...所有json模板
requirements.txt(Python 依赖) 根据 main.py 里 import 提取所有依赖:
requests paramiko scp chardet PyYAML ruamel.yaml Flask==2.3.2 cryptography flask flask-login flask-wtf
Dockerfile(完整镜像构建文件) 使用 python:slim 轻量基础镜像,内置 cron,打包全部代码、模板、providers.json,挂载/config为持久化卷(仅生成 config.json)
FROM python:3.11 -slimLABEL maintainer="singbox-subscribe" ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 ENV TZ=Asia/ShanghaiRUN apt-get update && apt-get install -y --no-install-recommends \ cron tzdata \ && rm -rf /var/lib/apt/lists/* && apt-get clean WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY main.py . COPY providers.json . COPY parsers ./parsers COPY config_template ./config_template COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh VOLUME ["/config" ] ENV TEMPLATE_INDEX=3 ENV CRON_TIME="59 10 * * *" ENTRYPOINT ["/entrypoint.sh" ]
docker-compose.yml cd到singbox-subscribe目录下,然后执行docker compose up -d
services: singbox-subscribe: image: singbox-subscribepro:latest container_name: singbox-subscribepro restart: on-failure environment: - CRON_TIME=59 10 * * * - RUN_ON_START=false - TEMPLATE_INDEX=3 - SUB_URL_1=http://192.168.50.1:8299/download/sub?target=sing-box - SUB_URL_2=https://tv.*****.top:0000/sub volumes: - /etc/localtime:/etc/localtime:ro - ./:/config logging: driver: "json-file" options: max-size: "10M" max-file: "3"
logging: 完整代码逐行解释
driver: “json-file” Docker 默认日志驱动,容器所有标准输出 / 错误(print、脚本打印、报错)会以 JSON 格式持久化存储在宿主机磁盘。
不配置时默认也是 json-file;
日志文件会保存在 Docker 数据目录(通常 /var/lib/docker/containers/容器ID/);
执行 docker logs 容器名 底层就是读取该 JSON 日志文件。
options.max-size: “10M”单日志文件最大上限 10 兆 容器持续运行、定时每日打印日志,日志文件会不断膨胀; 当日志文件达到 10MB 时,Docker 自动滚动分割日志,不再往这个文件追加内容。
options.max-file: “3”最多保留 3 份滚动日志文件 举个例子配合上面 max-size:10M: 初始日志:xxx-json.log(当前正在写入) 填满 10M → 重命名为 xxx-json.log.1,新建空日志继续输出 再次填满 10M → 生成 xxx-json.log.2 第三次填满 10M → 生成 xxx-json.log.3 第四次达到 10M 时:最旧的 .log.1 文件直接自动删除 ,只维持最多 3 个历史日志文件
整体作用总结
logging: driver: "json-file" options: max-size: "10M" max-file: "3"
限制容器日志磁盘占用:最多占用 3 × 10M = 30MB 存储空间 ,避免长期定时运行(每天 20:25 执行)日志无限堆积撑满服务器硬盘,是生产环境标准优化配置,推荐保留。
Docker 镜像标签(仓库名)强制要求全小写
镜像名称只能:小写字母、数字、横线 -、下划线 _、点 .;
不能包含任意大写英文字母;
镜像名分隔建议用短横线 -,可读性更好。
entrypoint.sh 启动程序入口
#!/bin/bash set -eln -sf /usr/share/zoneinfo/Asia/Shanghaiecho "Asia/Shanghai" > /etc/timezoneRUN_ON_START_RAW=" ${RUN\_ON\_START:-true} " RUN\_ON\_START=$ (echo " $RUN\_ON\_START\_RAW " | tr '[:upper:]' '[:lower:]' ) cat > /app/run\_task.sh <<'EOF' cd /app/usr/local/bin/python3 main.py >> /proc/1/fd/1 2>&1 EOF chmod +x /app/run\_task.shif [ "$ RUN_ON_START " = "true" ]; then echo "RUN_ON_START=true,容器启动立即执行转换脚本" /app/run_task.sh fi rm -f /etc/cron.d/app-tasktouch /etc/cron.d/app-taskif [ -n " ${CRON\_TIME} " ]; then echo "配置定时任务:$ {CRON_TIME} " CRON_FULL_CMD=" ${CRON\_TIME} root TEMPLATE\_INDEX=$ {TEMPLATE_INDEX} SUB_URL_1=' ${SUB\_URL\_1} ' SUB\_URL\_2='$ {SUB_URL_2} ' /app/run_task.sh" echo " $CRON\_FULL\_CMD " >> /etc/cron.d/app-task chmod 0644 /etc/cron.d/app-task crontab /etc/cron.d/app-task exec cron -f else echo "未设置CRON\_TIME,执行完成后容器退出" fi
以上代码的核心问题:cron 进程隔离环境变量 现象拆解
容器前台 exec /app/run\_task.sh 正常读取 SUB\_URL\_1/SUB\_URL\_2/TEMPLATE\_INDEX=3;
cron 定时执行 /app/run\_task.sh 时丢失所有自定义环境变量 ,程序读空,回退 default-url;
根本原理:Linux cron 运行在独立极简环境 ,不会继承容器启动时 -e 注入的环境变量,/app/run\_task.sh 内部 export $ VAR 拿不到父进程容器变量。
两种彻底解决方案(推荐方案 1) 方案 1:把所有环境变量写入 cron 任务行(一劳永逸,无需改 run_task.sh) 替换完整 entrypoint.sh,cron 命令行前置注入全部变量,cron 执行时自带,不受环境隔离限制:
#!/bin/bash set -eln -sf /usr/share/zoneinfo/Asia/Shanghaiecho "Asia/Shanghai" > /etc/timezoneRUN_ON_START_RAW=" ${RUN\_ON\_START:-true} " RUN\_ON\_START=$ (echo " $RUN\_ON\_START\_RAW " | tr '[:upper:]' '[:lower:]' ) cat > /app/run\_task.sh <<'EOF' cd /app/usr/local/bin/python3 main.py >> /proc/1/fd/1 2>&1 EOF chmod +x /app/run\_task.shif [ "$ RUN_ON_START " = "true" ]; then echo "RUN_ON_START=true,容器启动立即执行转换脚本" /app/run_task.sh fi rm -f /etc/cron.d/app-tasktouch /etc/cron.d/app-taskif [ -n " ${CRON\_TIME} " ]; then echo "配置定时任务:$ {CRON_TIME} " CRON_FULL_CMD=" ${CRON\_TIME} root TEMPLATE\_INDEX=$ {TEMPLATE_INDEX} SUB_URLS=' ${SUB\_URLS} ' SUB\_URL\_1='$ {SUB_URL_1} ' SUB_URL_2=' ${SUB\_URL\_2} ' SUB\_URL\_3='$ {SUB_URL_3} ' /app/run_task.sh" echo " $CRON\_FULL\_CMD " >> /etc/cron.d/app-task chmod 0644 /etc/cron.d/app-task crontab /etc/cron.d/app-task exec cron -f else echo "未设置CRON\_TIME,执行完成后容器退出" fi
关键改动说明 cron 任务不再只写 /app/run\_task.sh,而是:
13 10 * * * root TEMPLATE\_INDEX=3 SUB\_URL\_1="xxx" SUB\_URL\_2="xxx" /app/run\_task.sh
cron 执行这条命令时,临时定义环境变量 传递给脚本,不受 cron 隔离机制影响,和手动执行环境完全一致。
方案 2:全局持久环境文件(备选) 如果订阅链接含特殊引号导致方案 1 解析异常,可在 /etc/environment 写入变量,cron 默认读取该文件:
entrypoint 内增加:
cat > /etc/environment <<ENV TEMPLATE\_INDEX="$ {TEMPLATE_INDEX}" SUB_URLS=" ${SUB\_URLS}" SUB\_URL\_1="$ {SUB_URL_1}" SUB_URL_2=" ${SUB\_URL\_2}" SUB\_URL\_3="$ {SUB_URL_3}" ENV
run_task.sh 顶部加载全局环境:
#!/bin/bash while read line; do export " $line " ; done < /etc/environmentcd /app/usr/local/bin/python3 main.py >> /proc/1/fd/1 2>&1
部署生效步骤
替换本地 entrypoint.sh 为方案 1 完整代码
重新构建镜像
docker compose down docker rmi singbox-subscribepro:latest docker build -t singbox-subscribepro:latest . docker compose up -d
校验 cron 任务内容
docker exec -it singbox-subscribepro cat /etc/cron.d/app-task
输出应看到变量拼接在命令最前方,示例:
13 10 * * root TEMPLATE\_INDEX=3 SUB\_URLS='' SUB\_URL\_1='http://192.168.50.1:8299/download/sub?target=sing-box' SUB\_URL\_2='https://tv.****.top:2053/sub' SUB\_URL\_3='' /app/run\_task.sh
验证方式 手动模拟 cron 完整命令(复制 cat 输出的整行,去掉时间 + root 前缀执行):
docker exec -it singbox-subscribepro bash -c "TEMPLATE\_INDEX=3 SUB\_URL\_1='http://192.168.50.1:8299/download/sub?target=sing-box' SUB\_URL\_2='https://tv.*****.top:2053/sub' /app/run\_task.sh"
输出和你手动运行完全一致,不再出现未设置SUB\_URL、TEMPLATE\_INDEX无效报错。
补充原因总结 容器直接 exec:进程是容器主进程子进程,完整继承 -e 注入环境;
cron 定时任务:属于系统独立后台进程,不继承容器启动时的自定义环境变量,必须在 cron 命令行显式传入变量。
删除 main.py 中的 gh_proxy 代理 因为我是本地执行,不需要代理
一、确认要删除的 4 处 gh_proxy 代码,删除后程序完全正常运行 下面 4 段全部删掉,本地订阅逻辑不受任何影响,不会报错:
1. 顶部导入删除 原代码:
from parsers.gh\_proxy\_helper import set\_gh\_proxy
直接整行删掉。
2. argparse 参数删除 parser.add\_argument('--gh\_proxy\_index' , type =str , help ='github加速链接' )
整行删除。
3. 接收 gh_proxy_index 变量删除 gh\_proxy\_index = args.gh\_proxy\_index
整行删除。
4. github 加速逻辑整块删除 原完整块:
if hasattr (args, 'gh\_proxy\_index' ) and str (args.gh\_proxy\_index).isdigit(): gh\_proxy\_index = int (args.gh\_proxy\_index) print (gh\_proxy\_index) urls = [item["url" ] for item in config["route" ]["rule\_set" ]] new\_urls = set\_gh\_proxy(urls, gh\_proxy\_index) for item, new\_url in zip (config["route" ]["rule\_set" ], new\_urls): item["url" ] = new\_url
全部删掉。
二、删除后对应修改片段展示(对照修改,避免改错) 修改后头部导入区 import json, os, time, requests, sys, importlib, argparse, yaml, ruamelimport refrom datetime import datetimefrom urllib.parse import urlparsefrom collections import OrderedDictfrom parsers.app import TEMP\_DIRfrom parsers import toolfrom parsers.clash2base64 import clash2v2ray
修改后 argparse 区域 parser = argparse.ArgumentParser() parser.add\_argument('--temp\_json\_data' , type =parse\_json, help ='临时内容' ) parser.add\_argument('--template\_index' , type =int , help ='模板序号' ) args = parser.parse\_args() temp\_json\_data = args.temp\_json\_data
修改后主逻辑(删除加速块) nodes = process\_subscribes(providers["subscribes" ]) if providers.get('Only-nodes' ): combined\_contents = [] for sub\_tag, contents in nodes.items(): for content in contents: combined\_contents.append(content) final\_config = combined\_contents else : final\_config = combin\_to\_config(config, nodes)
三、为什么删除后程序 100% 正常?
gh_proxy 仅作用于github 规则集链接加速 ,你本地内网订阅(192.168、tv.weito)完全不依赖这段代码;
删除后不会调用set\_gh\_proxy函数,不会出现导入缺失报错;
--gh\_proxy\_index启动参数彻底移除,无多余参数干扰;
订阅拉取、节点解析、分流分组、定时、TEMPLATE_INDEX 全部原有逻辑保留;
不会影响providers.json、config 模板、/config 配置保存逻辑。
四、修改后验证步骤
保存修改后的 main.py
重建镜像
docker compose down docker rmi singbox-subscribepro:latest docker build -t singbox-subscribepro:latest . docker compose up -d
手动执行测试
docker exec -it singbox-subscribepro /app/run\_task.sh
输出正常加载 SUB_URL、zy_base 模板、正常生成 config.json,无任何报错即为修改成功。
五、额外清理(可选) parsers 文件夹里的gh\_proxy\_helper.py文件可以直接删除,不再使用,不删也不影响运行。
在sing-box-subscribe中添加节点存活测速 要在 main.py 主文件中实现节点延迟测试(HTTP/TCP 测速过滤),可以使用 Python 的并发线程池(concurrent.futures.ThreadPoolExecutor)结合 requests 请求测试 URL https://www.gstatic.com/generate_204 。
下面是针对 main.py 的具体修改方案。 在 main.py 中增加测速逻辑函数 test\_node\_latency 和并发过滤函数 filter\_nodes\_by\_latency,并在获取节点列表 process\_subscribes 之后调用过滤逻辑。
在 main.py 顶部导入并发线程池:from concurrent.futures import ThreadPoolExecutor, as\_completed
添加测速和并发过滤函数:在 main.py 中添加以下代码(建议放在 process\_subscribes(subscribes) 函数上方或下方):
TEST\_URL = "https://www.gstatic.com/generate\_204" TIMEOUT\_MS = 10000 MAX\_LATENCY = 2500 MAX\_WORKERS = 20 def test\_node\_latency (node ): server = node.get("server" ) port = node.get("server\_port" ) or node.get("port" ) if not server or not port: return node, -1 start\_time = time.time() try : proxy\_type = node.get("type" , "" ).lower() proxies = None if proxy\_type in ["http" , "socks5" ]: username = node.get("username" , "" ) password = node.get("password" , "" ) auth\_str = f"{username} :{password} @" if username and password else "" proxy\_url = f"{proxy\_type} ://{auth\_str} {server} :{port} " proxies = {"http" : proxy\_url, "https" : proxy\_url} resp = requests.get( TEST\_URL, proxies=proxies, timeout=TIMEOUT\_MS / 1000.0 , headers={"User-Agent" : "Mozilla/5.0" } ) latency = int ((time.time() - start\_time) * 1000 ) if resp.status\_code in [200 , 204 ]: return node, latency else : return node, -1 except Exception: return node, -1 def filter\_nodes\_by\_latency (nodes\_dict ): print (f"\\n开始对节点进行延迟测试 (并发数: {MAX\_WORKERS} , 超时时间: {TIMEOUT\_MS} ms, 阈值: {MAX\_LATENCY} ms)..." ) filtered\_nodes\_dict = {} total\_count = 0 valid\_count = 0 for tag, node\_list in nodes\_dict.items(): filtered\_list = [] total\_count += len (node\_list) with ThreadPoolExecutor(max\_workers=MAX\_WORKERS) as executor: future\_to\_node = { executor.submit(test\_node\_latency, node): node for node in node\_list } for future in as\_completed(future\_to\_node): node, latency = future.result() if 0 <= latency <= MAX\_LATENCY: node["latency" ] = latency filtered\_list.append(node) valid\_count += 1 print (f" [√] 节点 [{node.get('tag' )} ] 延迟: {latency} ms" ) else : reason = "超时/不可用" if latency < 0 else f"{latency} ms > {MAX\_LATENCY} ms" print (f" [X] 节点 [{node.get('tag' )} ] 过滤 ({reason} )" ) filtered\_nodes\_dict[tag] = filtered\_list print (f"测速完成!共测试 {total\_count} 个节点,保留 {valid\_count} 个可用节点。\\n" ) return filtered\_nodes\_dict
修改 main.py 的主流程入口 (\_\_main\_\_)
在 main.py 的主逻辑中,找到 nodes = process\_subscribes(providers["subscribes"]),在其下方加入 filter\_nodes\_by\_latency 调用:
nodes = process\_subscribes(providers["subscribes" ]) nodes = filter\_nodes\_by\_latency(nodes) if providers.get('Only-nodes' ): combined\_contents = [] for sub\_tag, contents in nodes.items(): for content in contents: combined\_contents.append(content) final\_config = combined\_contents else : final\_config = combin\_to\_config(config, nodes) save\_config(providers["save\_config\_path" ], final\_config)
其它地方都不用修改
测试的结果与sing-box测试的结果不同,这是非常正常的现象!之前的 Python 脚本测速并不是真实的“节点代理测速” ,而是直连测试 。
为什么测试结果会完全不同?
Python 脚本的直连测试 :此前用 requests.get() 或 socket 请求 [https://www.gstatic.com/generate\_204](https://www.gstatic.com/generate\_204) 时,使用的是运行 Python 脚本那台服务器/本机自身的网络 去直连节点的 IP,或者根本没有通过节点的加密协议(如 VLESS、Hysteria2)进行握手与转发。这测出来的只是你的机器到节点服务器的 IP 延迟,无法判断加密代理管道是否真正通畅。
sing-box 客户端的代理测速 :sing-box 客户端(图中界面)会将数据包真正输入到节点的出站内核中,完成协议握手(如 vless/hy2 复杂的 TLS/QUIC 握手),通过代理通道访问目标 URL 成功后才算有效并返回延迟。如果节点协议参数有误、证书失效或节点被墙,sing-box 就会直接报红色警告(超时/连接失败)。
在sing-box-subscribe中调用sing-box内核进行节点存活测速 在容器内直接运行sing-box二进制文件(非跨主机,容器独立运行),在 Dockerfile 构建时将 sing-box 的 Linux 二进制文件直接复制/安装到容器内部。 这种方式下,Python 脚本可以直接在容器本地通过 subprocess.run(["sing-box", "check", ...]) 执行节点语法校验或启动临时轻量实例测速,方案就是在容器内部启动一个专用的轻量级临时 sing-box 进程 。
构建临时配置 :Python 将所有待测节点注入到一个无额外分流代理/直连代理链 的专用临时配置文件中(只有节点本身,出站策略直连目标)。
后台启动临时测速内核 :Python 在容器后台以该临时配置启动一个独立 sing-box 实例(开在临时端口,如 9090)。
调用 RESTful API 测速 :Python 通过 API ([http://127.0.0.1:9090/clash/delay](http://127.0.0.1:9090/clash/delay)) 批量对各个节点触发延迟测试。
清理与过滤 :测试完成后终止临时进程、删除临时配置文件,筛选出延迟落在 0~3500ms 范围内的节点。
修改 Dockerfile(安装 sing-box 二进制) 将sing-box-1.13.11-linux-amd64.tar.gz下载放到Dockerfile同级目录下。修改Dockerfile文件内容:
FROM python:3.11 -slimLABEL maintainer="singbox-nodes" ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 ENV TZ=Asia/ShanghaiRUN apt-get update && apt-get install -y --no-install-recommends \\ cron tzdata \\ curl ca-certificates tar \\ && rm -rf /var/lib/apt/lists/* && apt-get clean COPY sing-box-1.13.11-linux-amd64.tar.gz /tmp/ RUN tar -xzf /tmp/sing-box-1.13.11-linux-amd64.tar.gz -C /tmp \\ && mv /tmp/sing-box-*/sing-box /usr/local/bin/ \\ && rm -rf /tmp/sing-box* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY main.py . COPY providers.json . COPY parsers ./parsers COPY config\_template ./config\_template COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh VOLUME ["/config" ] ENV TEMPLATE\_INDEX=1 ENV CRON\_TIME="" ENTRYPOINT ["/entrypoint.sh" ]
修改 main.py(增加内核测速逻辑) 在 main.py 中引入 subprocess 和 处理URL 网址的解析、拼接、编码、解码的 urllib.parse,在 main.py 顶部导入:
import subprocessimport urllib.parse
添加内置临时内核测速函数:在 main.py 中添加以下代码(建议放在 process\_subscribes(subscribes) 函数上方或下方):
TEST\_URL = "https://www.gstatic.com/generate\_204" TIMEOUT\_MS = int (os.getenv("TIMEOUT\_MS" , 30000 )) MAX\_LATENCY = int (os.getenv("MAX\_LATENCY" , 3500 )) API\_PORT = 19090 def filter\_nodes\_by\_singbox (nodes\_dict ): all\_nodes = [] for tag, n\_list in nodes\_dict.items(): all\_nodes.extend(n\_list) if not all\_nodes: return nodes\_dict clean\_nodes = [node for node in all\_nodes if isinstance (node, dict ) and node.get("type" ) and node.get("tag" )] if not clean\_nodes: print ("[测速内核] 没有有效的节点参与测速!" ) return nodes\_dict temp\_config = { "log" : { "level" : "warn" }, "dns" : { "servers" : [ { "tag" : "local-dns" , "type" : "udp" , "server" : "223.5.5.5" , "server\_port" : 53 } ] }, "inbounds" : [], "outbounds" : clean\_nodes + [ {"type" : "direct" , "tag" : "direct" } ], "route" : { "rules" : [ { "protocol" : "dns" , "outbound" : "direct" } ] }, "experimental" : { "clash\_api" : { "external\_controller" : f"127.0.0.1:{API\_PORT} " } } } temp\_config\_path = "/tmp/speedtest\_config.json" with open (temp\_config\_path, "w" , encoding="utf-8" ) as f: json.dump(temp\_config, f, indent=2 , ensure\_ascii=False ) env = os.environ.copy() env["ENABLE\_DEPRECATED\_LEGACY\_DNS\_SERVERS" ] = "true" env["ENABLE\_DEPRECATED\_OUTBOUND\_DNS\_RULE\_ITEM" ] = "true" check\_res = subprocess.run(["sing-box" , "check" , "-c" , temp\_config\_path], capture\_output=True , text=True , env=env) if check\_res.returncode != 0 : print (f"\\n[测速内核错误] 临时配置语法不通过:\\n{check\_res.stderr} " ) print ("[警告] 跳过测速程序,保留全部节点以防止生成空文件。" ) if os.path.exists(temp\_config\_path): os.remove(temp\_config\_path) return nodes\_dict print (f"\\n[测速内核] 启动临时 sing-box 进程 (API 端口: {API\_PORT} )..." ) process = subprocess.Popen( ["sing-box" , "run" , "-c" , temp\_config\_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env ) api\_ready = False for \_ in range (10 ): time.sleep(0.5 ) try : resp = requests.get(f"http://127.0.0.1:{API\_PORT} /version" , timeout=1 ) if resp.status\_code == 200 : api\_ready = True break except Exception: pass if not api\_ready: print ("[测速内核错误] API 服务未能成功监听,终止测速。" ) process.terminate() process.wait() if os.path.exists(temp\_config\_path): os.remove(temp\_config\_path) return nodes\_dict valid\_node\_tags = set () try : print (f"[测速内核] 开始批量测试 {len (clean\_nodes)} 个节点 (目标: {TEST\_URL} , 超时: {TIMEOUT\_MS} ms)..." ) for node in clean\_nodes: tag = node.get("tag" ) encoded\_tag = urllib.parse.quote(tag, safe="" ) try : api\_req\_url = f"http://127.0.0.1:{API\_PORT} /proxies/{encoded\_tag} /delay?timeout={TIMEOUT\_MS} &url={TEST\_URL} " resp = requests.get( api\_req\_url, timeout=(TIMEOUT\_MS / 1000.0 ) + 2 ) if resp.status\_code == 200 : delay = resp.json().get("delay" , -1 ) if 0 < delay <= MAX\_LATENCY: valid\_node\_tags.add(tag) print (f" [√] 节点 [{tag} ] 真实延迟: {delay} ms" ) else : print ( f" [X] 节点 [{tag} ] 延迟超标 ({delay} ms > {MAX\_LATENCY} ms)" ) else : print (f" [X] 节点 [{tag} ] 测试失败/超时" ) except Exception as e: print (f" [X] 节点 [{tag} ] 测速异常: {e} " ) finally : process.terminate() process.wait() if os.path.exists(temp\_config\_path): os.remove(temp\_config\_path) print ("[测速内核] 临时进程已停止,环境已清理。\\n" ) if not valid\_node\_tags: print ("[警告] 测速后可用节点数为 0,为防止主配置报错,保留原始节点。" ) return nodes\_dict filtered\_nodes\_dict = {} for group\_tag, n\_list in nodes\_dict.items(): filtered\_list = [node for node in n\_list if node.get("tag" ) in valid\_node\_tags] filtered\_nodes\_dict[group\_tag] = filtered\_list return filtered\_nodes\_dict
在 main.py 主程序中调用,在 main.py 的主入口逻辑中:
nodes = process\_subscribes(providers["subscribes" ]) nodes = filter\_nodes\_by\_singbox(nodes) if providers.get('Only-nodes' ): combined\_contents = [] for sub\_tag, contents in nodes.items(): for content in contents: combined\_contents.append(content) final\_config = combined\_contents else : final\_config = combin\_to\_config(config, nodes) save\_config(providers["save\_config\_path" ], final\_config)
方案优势
测速 100% 准确 :节点直接建立底层隧道并发起连通测试,测试结果与客户端一致(能测出 VLESS / HY2 等各种协议的真正可用性)。
绝对无前置代理干扰 :临时配置文件只有纯节点列表(outbounds)和 clash\_api,不会出现代理嵌套导致测速失真的情况。
隔离性好 :全程在容器内自给自足,不占用宿主机资源,也不会破坏 Docker 外的环境。
修改entrypoint.sh代码 因为在docker-compose.yml中将TIMEOUT\_MS、MAX\_LATENCY、SUB\_URL\_3写入了容器的环境变量(SUB_URL_3是在providers.json文件中增加了一个订阅链接)。docker-compose.yml内容如下:
services: singbox-subscribe: image: singbox-nodes:latest container\_name: singbox-nodes restart: on-failure environment: - CRON\_TIME=05 19 * * * - RUN\_ON\_START=false - TEMPLATE\_INDEX=3 - TIMEOUT\_MS=30000 - MAX\_LATENCY=2500 - SUB\_URL\_1=http://192.168.50.1:8299/download/sub?target=sing-box - SUB\_URL\_2=https://***.***.top/*****/sub/raw?app=xray#%F0%9F%92%A6%20BPB%20Raw - SUB\_URL\_3=https://***.***.top:6666/sub volumes: - /etc/localtime:/etc/localtime:ro - ./:/config logging: driver: "json-file" options: max-size: "10M" max-file: "3"
增加的这些内容中SUB\_URL\_2参数里 中包含 & 或 # 等特殊字符 日志中显示你的 SUB\_URL\_2 包含了 #(例如 ...#%F0%9F%92%A6%20BPB%20Raw)。 在 Linux Shell 和 Cron 配置中,# 会被直接当作注释符 !Cron 读取到 # 时,后面拼接的所有内容(包括 /app/run\_task.sh)都被当作注释干掉了,相当于这行任务根本没有包含要执行的脚本 。所以直接CRON\_FULL\_CMD="$ {CRON_TIME} root TEMPLATE_INDEX= ${TEMPLATE\_INDEX} TIMEOUT\_MS=$ {TIMEOUT_MS} MAX_LATENCY= ${MAX\_LATENCY} SUB\_URL\_1='$ {SUB_URL_1}' SUB_URL_2=' ${SUB\_URL\_2}' SUB\_URL\_3='$ {SUB_URL_3}' /app/run_task.sh"这样子拼接时运行会报错,main.py运行时自动退出了。
最稳妥的修复方案 :不要把环境变量硬塞进 cron 的表达式单行里 。直接在 entrypoint.sh 中把容器的环境变量全量导出到 /etc/environment,run_task.sh 执行时会自动引入它们,完美避开 # 注释和引号转义问题。
修改你的 entrypoint.sh 如下:
set -e ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime echo "Asia/Shanghai" > /etc/timezone RUN_ON_START_RAW=" ${RUN\_ON\_START:-true}" RUN\_ON\_START=$ (echo " $RUN\_ON\_START\_RAW" | tr '[:upper:]' '[:lower:]' ) printenv | grep -v "no\_proxy" > /etc/environment cat > /app/run\_task.sh <<'EOF' cd /app export $ (cat /etc/environment | xargs) /usr/local/bin/python3 main.py >> /proc/1 /fd/1 2 >&1 EOF chmod +x /app/run_task.sh if [ " $RUN\_ON\_START" = "true" ]; then echo "RUN\_ON\_START=true,容器启动立即执行转换脚本" /app/run\_task.sh fi rm -f /etc/cron.d/app-task touch /etc/cron.d/app-task if [ -n "$ {CRON_TIME}" ]; then echo "配置定时任务: ${CRON\_TIME}" echo "$ {CRON_TIME} root /app/run_task.sh" >> /etc/cron.d/app-task echo "" >> /etc/cron.d/app-task chmod 0644 /etc/cron.d/app-task crontab /etc/cron.d/app-task exec cron -f else echo "未设置 CRON_TIME,执行完成后容器退出" fi
消除了 # 截断 :SUB_URL_2 里的 # 保存在 /etc/environment 文件中,不会再触发 Cron 配置文件的语法注释机制。
无限扩展 :以后无论你再增加多少个变量(如 TIMEOUT_MS, MAX_LATENCY, SUB_URL_99),都不用再去改 CRON_FULL_CMD 拼接逻辑,Python 脚本都能 100% 正常读取到。
修改好的测速main.py文件
第一次修改的main.py文件 :只修改了在容器中运行,把config_template和解析器拷贝到容器,在容器安装运行时再将config_template从容器中拷贝出来到挂载的目录中。